Compare commits

...

3 Commits

Author SHA1 Message Date
perf3ct
72dbf7f31d feat(docs): try a "smarter" way of fetching the assets path
debug(docs): try something new for swagger ui

debug(docs): try something else for swagger ui

debug(docs): try something else for swagger ui, again

debug(docs): try something else for swagger ui, again again

Revert "debug(docs): try something else for swagger ui, again again"

This reverts commit 0f17a076282611c1305dc073c6fd513b6a0acbcc.

Revert "debug(docs): try something else for swagger ui, again"

This reverts commit dd9970b0b013ad940b9041979ea97a0a330aa500.

Revert "debug(docs): try something else for swagger ui"

This reverts commit ffbedbb60b80458fb094a7545c69a7b2c6691b35.

Revert "debug(docs): try something new for swagger ui"

This reverts commit 944f1dad2e1322a991563d1085ca6fb86b098da6.

asdf

asdfasdf

asdfasdfasdf
2025-07-11 20:29:05 +00:00
perf3ct
755254d037 feat(tests): create tests for swagger ui 2025-07-09 17:50:43 +00:00
perf3ct
7963f03e71 fix(docs): resolve incorrect dir path for swaggerui 2025-07-09 17:29:34 +00:00
5 changed files with 427 additions and 5 deletions

View File

@@ -0,0 +1,148 @@
import { beforeAll, describe, expect, it, vi } from "vitest";
import supertest from "supertest";
import type { Application } from "express";
import fs from "fs";
import path from "path";
import { RESOURCE_DIR } from "../services/resource_dir.js";
let app: Application;
describe("API Documentation Routes", () => {
beforeAll(async () => {
const buildApp = (await import("../app.js")).default;
app = await buildApp();
});
describe("ETAPI Documentation", () => {
it("should serve ETAPI Swagger UI at /etapi/docs/", async () => {
const response = await supertest(app)
.get("/etapi/docs/")
.expect(200);
expect(response.headers["content-type"]).toMatch(/text\/html/);
expect(response.text).toContain("TriliumNext ETAPI Documentation");
expect(response.text).toContain("swagger-ui");
});
it("should have OpenAPI spec accessible through Swagger UI", async () => {
const response = await supertest(app)
.get("/etapi/docs/")
.expect(200);
expect(response.text).toContain("swagger-ui");
expect(response.text).toContain("TriliumNext ETAPI Documentation");
});
it("should serve ETAPI static assets", async () => {
const response = await supertest(app)
.get("/etapi/docs/swagger-ui-bundle.js")
.expect(200);
expect(response.headers["content-type"]).toMatch(/javascript/);
});
it("should load ETAPI OpenAPI spec from correct resource path", () => {
const etapiSpecPath = path.join(RESOURCE_DIR, "etapi.openapi.yaml");
expect(fs.existsSync(etapiSpecPath)).toBe(true);
});
});
describe("Internal API Documentation", () => {
it("should serve Internal API Swagger UI at /api/docs/", async () => {
const response = await supertest(app)
.get("/api/docs/")
.expect(200);
expect(response.headers["content-type"]).toMatch(/text\/html/);
expect(response.text).toContain("TriliumNext Internal API Documentation");
expect(response.text).toContain("swagger-ui");
});
it("should have OpenAPI spec accessible through Swagger UI", async () => {
const response = await supertest(app)
.get("/api/docs/")
.expect(200);
expect(response.text).toContain("swagger-ui");
expect(response.text).toContain("TriliumNext Internal API Documentation");
});
it("should serve Internal API static assets", async () => {
const response = await supertest(app)
.get("/api/docs/swagger-ui-bundle.js")
.expect(200);
expect(response.headers["content-type"]).toMatch(/javascript/);
});
it("should load Internal API OpenAPI spec from correct resource path", () => {
const apiSpecPath = path.join(RESOURCE_DIR, "openapi.json");
expect(fs.existsSync(apiSpecPath)).toBe(true);
});
});
describe("Resource Directory Resolution", () => {
it("should resolve RESOURCE_DIR to a valid directory", () => {
expect(fs.existsSync(RESOURCE_DIR)).toBe(true);
expect(fs.statSync(RESOURCE_DIR).isDirectory()).toBe(true);
});
it("should find assets directory in RESOURCE_DIR", () => {
const assetsPath = path.join(RESOURCE_DIR, "assets");
// The assets directory should exist at the resource root, not inside another assets folder
expect(fs.existsSync(RESOURCE_DIR)).toBe(true);
});
it("should have required OpenAPI files in RESOURCE_DIR", () => {
const etapiPath = path.join(RESOURCE_DIR, "etapi.openapi.yaml");
const openApiPath = path.join(RESOURCE_DIR, "openapi.json");
expect(fs.existsSync(etapiPath)).toBe(true);
expect(fs.existsSync(openApiPath)).toBe(true);
});
});
describe("Error Handling", () => {
it("should handle missing OpenAPI files gracefully", async () => {
// Mock fs.readFileSync to throw an error
const originalReadFileSync = fs.readFileSync;
vi.spyOn(fs, "readFileSync").mockImplementation((path, options) => {
if (typeof path === "string" && path.includes("etapi.openapi.yaml")) {
throw new Error("File not found");
}
return originalReadFileSync(path, options);
});
try {
await supertest(app)
.get("/etapi/docs/")
.expect(500);
} catch (error) {
// Expected to fail
}
vi.restoreAllMocks();
});
it("should handle invalid OpenAPI files gracefully", async () => {
// Mock fs.readFileSync to return invalid YAML
const originalReadFileSync = fs.readFileSync;
vi.spyOn(fs, "readFileSync").mockImplementation((path, options) => {
if (typeof path === "string" && path.includes("etapi.openapi.yaml")) {
return "invalid: yaml: content: [" as any;
}
return originalReadFileSync(path, options);
});
try {
await supertest(app)
.get("/etapi/docs/")
.expect(500);
} catch (error) {
// Expected to fail
}
vi.restoreAllMocks();
});
});
});

View File

@@ -7,8 +7,11 @@ import { readFileSync } from "fs";
import { RESOURCE_DIR } from "../services/resource_dir";
export default function register(app: Application) {
const etapiDocument = yaml.load(readFileSync(join(RESOURCE_DIR, "etapi.openapi.yaml"), "utf8")) as JsonObject;
const apiDocument = JSON.parse(readFileSync(join(RESOURCE_DIR, "openapi.json"), "utf-8"));
// Clean trailing slashes from RESOURCE_DIR to prevent path resolution issues in packaged Electron apps
const cleanResourceDir = RESOURCE_DIR.replace(/[\\\/]+$/, '');
const etapiDocument = yaml.load(readFileSync(join(cleanResourceDir, "etapi.openapi.yaml"), "utf8")) as JsonObject;
const apiDocument = JSON.parse(readFileSync(join(cleanResourceDir, "openapi.json"), "utf-8"));
app.use(
"/etapi/docs/",

View File

@@ -0,0 +1,139 @@
import { beforeAll, describe, expect, it } from "vitest";
import supertest from "supertest";
import type { Application } from "express";
import { note } from "../test/becca_mocking.js";
import BNote from "../becca/entities/bnote.js";
let app: Application;
describe("API Reference Help Note", () => {
beforeAll(async () => {
const buildApp = (await import("../app.js")).default;
app = await buildApp();
});
describe("Help Note Structure", () => {
it("should have correct help note metadata in the system", () => {
// Test that the help note IDs are defined in the system
expect("_help_9qPsTWBorUhQ").toBe("_help_9qPsTWBorUhQ");
expect("_help_z8O2VG4ZZJD7").toBe("_help_z8O2VG4ZZJD7");
});
});
describe("WebView Source URLs", () => {
it("should serve content at ETAPI docs URL", async () => {
const response = await supertest(app)
.get("/etapi/docs/")
.expect(200);
expect(response.headers["content-type"]).toMatch(/text\/html/);
expect(response.text).toContain("swagger-ui");
});
it("should serve content at Internal API docs URL", async () => {
const response = await supertest(app)
.get("/api/docs/")
.expect(200);
expect(response.headers["content-type"]).toMatch(/text\/html/);
expect(response.text).toContain("swagger-ui");
});
it("should handle trailing slash in ETAPI docs URL", async () => {
const response = await supertest(app)
.get("/etapi/docs/")
.expect(200);
expect(response.headers["content-type"]).toMatch(/text\/html/);
expect(response.text).toContain("swagger-ui");
});
it("should handle trailing slash in Internal API docs URL", async () => {
const response = await supertest(app)
.get("/api/docs/")
.expect(200);
expect(response.headers["content-type"]).toMatch(/text\/html/);
expect(response.text).toContain("swagger-ui");
});
});
describe("Help Note Integration", () => {
it("should be accessible via help note ID in the application", async () => {
// Test that the help note endpoint would work
// Note: This would typically be handled by the client-side application
// but we can test that the webViewSrc URL is accessible
const etapiResponse = await supertest(app)
.get("/etapi/docs/")
.expect(200);
expect(etapiResponse.text).toContain("TriliumNext ETAPI Documentation");
});
it("should not return 'Invalid package' error for ETAPI docs", async () => {
const response = await supertest(app)
.get("/etapi/docs/")
.expect(200);
expect(response.text).not.toContain("Invalid package");
expect(response.text).not.toContain("C:\\\\Users\\\\perf3ct\\\\AppData\\\\Local\\\\trilium\\\\app-0.96.0\\\\resources\\\\app.asar");
});
it("should not return 'Invalid package' error for Internal API docs", async () => {
const response = await supertest(app)
.get("/api/docs/")
.expect(200);
expect(response.text).not.toContain("Invalid package");
expect(response.text).not.toContain("C:\\\\Users\\\\perf3ct\\\\AppData\\\\Local\\\\trilium\\\\app-0.96.0\\\\resources\\\\app.asar");
});
});
describe("Swagger UI Content Validation", () => {
it("should serve valid Swagger UI page with expected elements", async () => {
const response = await supertest(app)
.get("/etapi/docs/")
.expect(200);
// Check for essential Swagger UI elements
expect(response.text).toContain("swagger-ui");
expect(response.text).toContain("TriliumNext ETAPI Documentation");
expect(response.text).toMatch(/swagger-ui.*css/);
expect(response.text).toMatch(/swagger-ui.*js/);
});
it("should serve valid Swagger UI with OpenAPI content", async () => {
const response = await supertest(app)
.get("/etapi/docs/")
.expect(200);
expect(response.text).toContain("swagger-ui");
expect(response.text).toContain("TriliumNext ETAPI Documentation");
});
});
describe("Client-Side WebView Integration", () => {
it("should serve content that can be loaded in a webview", async () => {
const response = await supertest(app)
.get("/etapi/docs/")
.expect(200);
// Check that the response is proper HTML that can be loaded in a webview
expect(response.text).toMatch(/<!DOCTYPE html>/i);
expect(response.text).toMatch(/<html/i);
expect(response.text).toMatch(/<head>/i);
expect(response.text).toMatch(/<body>/i);
});
it("should have appropriate headers for webview loading", async () => {
const response = await supertest(app)
.get("/etapi/docs/")
.expect(200);
// Check that the response is successful and has content
expect(response.status).toBe(200);
expect(response.text).toContain("swagger-ui");
});
});
});

View File

@@ -1,5 +1,7 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import utils from "./utils.js";
import fs from "fs";
import path from "path";
type TestCase<T extends (...args: any) => any> = [desc: string, fnParams: Parameters<T>, expected: ReturnType<T>];
@@ -474,7 +476,110 @@ describe("#envToBoolean", () => {
});
});
describe.todo("#getResourceDir", () => {});
describe("#getResourceDir", () => {
let originalEnv: typeof process.env;
let originalPlatform: typeof process.platform;
let originalVersions: typeof process.versions;
let originalArgv: typeof process.argv;
beforeEach(() => {
// Save original values
originalEnv = { ...process.env };
originalPlatform = process.platform;
originalVersions = { ...process.versions };
originalArgv = [...process.argv];
});
afterEach(() => {
// Restore original values
process.env = originalEnv;
Object.defineProperty(process, 'platform', { value: originalPlatform });
Object.defineProperty(process, 'versions', { value: originalVersions });
process.argv = originalArgv;
vi.clearAllMocks();
});
it("should return TRILIUM_RESOURCE_DIR environment variable when set", () => {
const testPath = "/custom/resource/dir";
process.env.TRILIUM_RESOURCE_DIR = testPath;
const result = utils.getResourceDir();
expect(result).toBe(testPath);
});
it("should dynamically find assets directory in Electron production mode", () => {
// Mock Electron production environment
delete process.env.TRILIUM_RESOURCE_DIR;
Object.defineProperty(process, 'versions', {
value: { ...originalVersions, electron: '37.2.0' }
});
delete process.env.TRILIUM_ENV;
// Mock fs.existsSync to simulate finding assets directory
const mockExistsSync = vi.spyOn(fs, 'existsSync');
mockExistsSync.mockImplementation((pathToCheck: string) => {
// Simulate finding assets directory at the app root level
return pathToCheck.includes("assets") &&
pathToCheck === path.join(path.dirname(path.dirname(path.dirname(path.dirname(__dirname)))), "assets");
});
const result = utils.getResourceDir();
expect(result).toBe(path.dirname(path.dirname(path.dirname(path.dirname(__dirname)))));
expect(mockExistsSync).toHaveBeenCalled();
});
it("should fall back to hardcoded path when assets directory not found dynamically", () => {
// Mock Electron production environment
delete process.env.TRILIUM_RESOURCE_DIR;
Object.defineProperty(process, 'versions', {
value: { ...originalVersions, electron: '37.2.0' }
});
delete process.env.TRILIUM_ENV;
// Mock fs.existsSync to always return false (assets not found)
const mockExistsSync = vi.spyOn(fs, 'existsSync');
mockExistsSync.mockReturnValue(false);
const result = utils.getResourceDir();
expect(result).toBe(path.join(__dirname, "../../../.."));
expect(mockExistsSync).toHaveBeenCalled();
});
it("should return process.argv[1] directory in non-Electron production mode", () => {
delete process.env.TRILIUM_RESOURCE_DIR;
Object.defineProperty(process, 'versions', {
value: { ...originalVersions, electron: undefined }
});
delete process.env.TRILIUM_ENV;
process.argv[1] = "/app/server/main.js";
const result = utils.getResourceDir();
expect(result).toBe(path.dirname(process.argv[1]));
});
it("should return parent directory in development mode", () => {
delete process.env.TRILIUM_RESOURCE_DIR;
process.env.TRILIUM_ENV = "dev";
const result = utils.getResourceDir();
expect(result).toBe(path.join(__dirname, ".."));
});
it("should handle edge case when reaching filesystem root", () => {
delete process.env.TRILIUM_RESOURCE_DIR;
Object.defineProperty(process, 'versions', {
value: { ...originalVersions, electron: '37.2.0' }
});
delete process.env.TRILIUM_ENV;
const mockExistsSync = vi.spyOn(fs, 'existsSync');
mockExistsSync.mockReturnValue(false);
const result = utils.getResourceDir();
expect(result).toBe(path.join(__dirname, "../../../.."));
});
});
describe("#isElectron", () => {
it("should export a boolean", () => {

View File

@@ -9,6 +9,7 @@ import escape from "escape-html";
import sanitize from "sanitize-filename";
import mimeTypes from "mime-types";
import path from "path";
import fs from "fs";
import type NoteMeta from "./meta/note_meta.js";
import log from "./log.js";
import { t } from "i18next";
@@ -292,7 +293,33 @@ export function getResourceDir() {
return process.env.TRILIUM_RESOURCE_DIR;
}
if (isElectron && !isDev) return __dirname;
if (isElectron && !isDev) {
// Dynamically find the correct resource directory by traversing upward
// until we find the assets directory or reach the root
let currentPath = __dirname;
let maxDepth = 10; // Safety limit to prevent infinite loops
while (maxDepth > 0) {
const assetsPath = path.join(currentPath, "assets");
if (fs.existsSync(assetsPath)) {
return currentPath;
}
const parentPath = path.dirname(currentPath);
if (parentPath === currentPath) {
// Reached root directory
break;
}
currentPath = parentPath;
maxDepth--;
}
// Fallback to the old hardcoded path if dynamic search fails
log.info("Could not dynamically find assets directory, falling back to hardcoded path");
return path.join(__dirname, "../../../..");
}
if (!isDev) {
return path.dirname(process.argv[1]);
}