mirror of
https://github.com/zadam/trilium.git
synced 2025-10-26 15:56:29 +01:00
Compare commits
3 Commits
kev/share-
...
fix/resolv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72dbf7f31d | ||
|
|
755254d037 | ||
|
|
7963f03e71 |
148
apps/server/src/routes/api_docs.spec.ts
Normal file
148
apps/server/src/routes/api_docs.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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/",
|
||||
|
||||
139
apps/server/src/services/api_reference_help.spec.ts
Normal file
139
apps/server/src/services/api_reference_help.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user