mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			v0.98.1
			...
			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