From 053ff0568d0d4cd2c016b36efa72097388004d6c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 2 Jun 2025 18:08:21 +0300 Subject: [PATCH 01/10] test(etapi): port app-info --- _regroup/test-etapi/app-info.http | 7 ------- apps/server/spec/etapi/app-info.spec.ts | 20 ++++++++++++++++++++ apps/server/spec/etapi/utils.ts | 15 +++++++++++++++ apps/server/spec/setup.ts | 2 ++ apps/server/vite.config.mts | 2 +- 5 files changed, 38 insertions(+), 8 deletions(-) delete mode 100644 _regroup/test-etapi/app-info.http create mode 100644 apps/server/spec/etapi/app-info.spec.ts create mode 100644 apps/server/spec/etapi/utils.ts diff --git a/_regroup/test-etapi/app-info.http b/_regroup/test-etapi/app-info.http deleted file mode 100644 index a851005c2..000000000 --- a/_regroup/test-etapi/app-info.http +++ /dev/null @@ -1,7 +0,0 @@ -GET {{triliumHost}}/etapi/app-info -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.clipperProtocolVersion === "1.0"); -%} diff --git a/apps/server/spec/etapi/app-info.spec.ts b/apps/server/spec/etapi/app-info.spec.ts new file mode 100644 index 000000000..03a5a389b --- /dev/null +++ b/apps/server/spec/etapi/app-info.spec.ts @@ -0,0 +1,20 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import buildApp from "../../src/app.js"; +import supertest from "supertest"; + +let app: Application; +let token: string; + +describe("etapi/app-info", () => { + beforeAll(async () => { + app = await buildApp(); + }); + + it("retrieves correct app info", async () => { + const response = await supertest(app) + .get("/etapi/app-info") + .expect(200); + expect(response.body.clipperProtocolVersion).toBe("1.0"); + }); +}); diff --git a/apps/server/spec/etapi/utils.ts b/apps/server/spec/etapi/utils.ts new file mode 100644 index 000000000..40895648f --- /dev/null +++ b/apps/server/spec/etapi/utils.ts @@ -0,0 +1,15 @@ +import type { Application } from "express"; +import supertest from "supertest"; +import { expect } from "vitest"; + +export async function login(app: Application) { + // Obtain auth token. + const response = await supertest(app) + .post("/etapi/auth/login") + .send({ + "password": "demo1234" + }) + .expect(201); + const token = response.body.authToken; + expect(token).toBeTruthy(); +} diff --git a/apps/server/spec/setup.ts b/apps/server/spec/setup.ts index b2b6a2a71..74e7ff746 100644 --- a/apps/server/spec/setup.ts +++ b/apps/server/spec/setup.ts @@ -7,6 +7,8 @@ import dayjs from "dayjs"; process.env.TRILIUM_DATA_DIR = join(__dirname, "db"); process.env.TRILIUM_RESOURCE_DIR = join(__dirname, "../src"); process.env.TRILIUM_INTEGRATION_TEST = "memory"; +process.env.TRILIUM_ENV = "dev"; +process.env.TRILIUM_PUBLIC_SERVER = "http://localhost:4200"; beforeAll(async () => { // Initialize the translations manually to avoid any side effects. diff --git a/apps/server/vite.config.mts b/apps/server/vite.config.mts index eae95a616..0f290b08e 100644 --- a/apps/server/vite.config.mts +++ b/apps/server/vite.config.mts @@ -10,7 +10,7 @@ export default defineConfig(() => ({ globals: true, setupFiles: ["./spec/setup.ts"], environment: "node", - include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + include: ['{src,spec}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], reporters: ['default'], coverage: { reportsDirectory: './test-output/vitest/coverage', From b88af5e4b3beaf71588cd9692db81557a58b6297 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 2 Jun 2025 19:02:01 +0300 Subject: [PATCH 02/10] test(etapi): port api-metrics --- _regroup/test-etapi/_login.http | 12 ------ _regroup/test-etapi/api-metrics.http | 43 ------------------- apps/server/spec/etapi/api-metrics.spec.ts | 48 ++++++++++++++++++++++ 3 files changed, 48 insertions(+), 55 deletions(-) delete mode 100644 _regroup/test-etapi/_login.http delete mode 100644 _regroup/test-etapi/api-metrics.http create mode 100644 apps/server/spec/etapi/api-metrics.spec.ts diff --git a/_regroup/test-etapi/_login.http b/_regroup/test-etapi/_login.http deleted file mode 100644 index 9976e7cd4..000000000 --- a/_regroup/test-etapi/_login.http +++ /dev/null @@ -1,12 +0,0 @@ -POST {{triliumHost}}/etapi/auth/login -Content-Type: application/json - -{ - "password": "1234" -} - -> {% - client.assert(response.status === 201); - - client.global.set("authToken", response.body.authToken); -%} diff --git a/_regroup/test-etapi/api-metrics.http b/_regroup/test-etapi/api-metrics.http deleted file mode 100644 index 78aee7217..000000000 --- a/_regroup/test-etapi/api-metrics.http +++ /dev/null @@ -1,43 +0,0 @@ -### Test regular API metrics endpoint (requires session authentication) - -### Get metrics from regular API (default Prometheus format) -GET {{triliumHost}}/api/metrics - -> {% -client.test("API metrics endpoint returns Prometheus format by default", function() { - client.assert(response.status === 200, "Response status is not 200"); - client.assert(response.headers["content-type"].includes("text/plain"), "Content-Type should be text/plain"); - client.assert(response.body.includes("trilium_info"), "Should contain trilium_info metric"); - client.assert(response.body.includes("trilium_notes_total"), "Should contain trilium_notes_total metric"); - client.assert(response.body.includes("# HELP"), "Should contain HELP comments"); - client.assert(response.body.includes("# TYPE"), "Should contain TYPE comments"); -}); -%} - -### Get metrics in JSON format -GET {{triliumHost}}/api/metrics?format=json - -> {% -client.test("API metrics endpoint returns JSON when requested", function() { - client.assert(response.status === 200, "Response status is not 200"); - client.assert(response.headers["content-type"].includes("application/json"), "Content-Type should be application/json"); - client.assert(response.body.version, "Version info not present"); - client.assert(response.body.database, "Database info not present"); - client.assert(response.body.timestamp, "Timestamp not present"); - client.assert(typeof response.body.database.totalNotes === 'number', "Total notes should be a number"); - client.assert(typeof response.body.database.activeNotes === 'number', "Active notes should be a number"); - client.assert(response.body.noteTypes, "Note types breakdown not present"); - client.assert(response.body.attachmentTypes, "Attachment types breakdown not present"); - client.assert(response.body.statistics, "Statistics not present"); -}); -%} - -### Test invalid format parameter -GET {{triliumHost}}/api/metrics?format=xml - -> {% -client.test("Invalid format parameter returns error", function() { - client.assert(response.status === 500, "Response status should be 500"); - client.assert(response.body.message.includes("prometheus"), "Error message should mention supported formats"); -}); -%} \ No newline at end of file diff --git a/apps/server/spec/etapi/api-metrics.spec.ts b/apps/server/spec/etapi/api-metrics.spec.ts new file mode 100644 index 000000000..a9c98df87 --- /dev/null +++ b/apps/server/spec/etapi/api-metrics.spec.ts @@ -0,0 +1,48 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import buildApp from "../../src/app.js"; +import supertest from "supertest"; + +let app: Application; +let token: string; + +// TODO: This is an API test, not ETAPI. + +describe("api/metrics", () => { + beforeAll(async () => { + app = await buildApp(); + }); + + it("returns Prometheus format by default", async () => { + const response = await supertest(app) + .get("/api/metrics") + .expect(200); + expect(response.headers["content-type"]).toContain("text/plain"); + expect(response.text).toContain("trilium_info"); + expect(response.text).toContain("trilium_notes_total"); + expect(response.text).toContain("# HELP"); + expect(response.text).toContain("# TYPE"); + }); + + it("returns JSON when requested", async() => { + const response = await supertest(app) + .get("/api/metrics?format=json") + .expect(200); + expect(response.headers["content-type"]).toContain("application/json"); + expect(response.body.version).toBeTruthy(); + expect(response.body.database).toBeTruthy(); + expect(response.body.timestamp).toBeTruthy(); + expect(response.body.database.totalNotes).toBeTypeOf("number"); + expect(response.body.database.activeNotes).toBeTypeOf("number"); + expect(response.body.noteTypes).toBeTruthy(); + expect(response.body.attachmentTypes).toBeTruthy(); + expect(response.body.statistics).toBeTruthy(); + }); + + it("returns error on invalid format", async() => { + const response = await supertest(app) + .get("/api/metrics?format=xml") + .expect(500); + expect(response.body.message).toContain("prometheus"); + }); +}); From f9f3f1983f1920603fa85acb2a7deb2290537d07 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 2 Jun 2025 19:16:48 +0300 Subject: [PATCH 03/10] test(etapi): port basic-auth --- _regroup/test-etapi/basic-auth.http | 21 ------------ apps/server/spec/etapi/basic-auth.spec.ts | 41 +++++++++++++++++++++++ apps/server/spec/etapi/utils.ts | 1 + 3 files changed, 42 insertions(+), 21 deletions(-) delete mode 100644 _regroup/test-etapi/basic-auth.http create mode 100644 apps/server/spec/etapi/basic-auth.spec.ts diff --git a/_regroup/test-etapi/basic-auth.http b/_regroup/test-etapi/basic-auth.http deleted file mode 100644 index cf79c357e..000000000 --- a/_regroup/test-etapi/basic-auth.http +++ /dev/null @@ -1,21 +0,0 @@ -GET {{triliumHost}}/etapi/app-info -Authorization: Basic etapi {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.clipperProtocolVersion === "1.0"); -%} - -### - -GET {{triliumHost}}/etapi/app-info -Authorization: Basic etapi wrong - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/app-info -Authorization: Basic wrong {{authToken}} - -> {% client.assert(response.status === 401); %} diff --git a/apps/server/spec/etapi/basic-auth.spec.ts b/apps/server/spec/etapi/basic-auth.spec.ts new file mode 100644 index 000000000..b3fbc837d --- /dev/null +++ b/apps/server/spec/etapi/basic-auth.spec.ts @@ -0,0 +1,41 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; +const URL = "/etapi/notes/root"; + +describe("basic-auth", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + }); + + it("auth token works", async () => { + const response = await supertest(app) + .get(URL) + .auth(USER, token, { "type": "basic"}) + .expect(200); + }); + + it("rejects wrong password", async () => { + const response = await supertest(app) + .get(URL) + .auth(USER, "wrong", { "type": "basic"}) + .expect(401); + }); + + it("rejects wrong user", async () => { + const response = await supertest(app) + .get(URL) + .auth("wrong", token, { "type": "basic"}) + .expect(401); + }); +}); diff --git a/apps/server/spec/etapi/utils.ts b/apps/server/spec/etapi/utils.ts index 40895648f..f9657eeab 100644 --- a/apps/server/spec/etapi/utils.ts +++ b/apps/server/spec/etapi/utils.ts @@ -12,4 +12,5 @@ export async function login(app: Application) { .expect(201); const token = response.body.authToken; expect(token).toBeTruthy(); + return token; } From 9e6d78b62506e6501c75dbe7f92094f77553831d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 2 Jun 2025 19:26:36 +0300 Subject: [PATCH 04/10] test(etapi): port no-token --- _regroup/test-etapi/no-token.http | 109 ------------------------ apps/server/spec/etapi/no-token.spec.ts | 54 ++++++++++++ 2 files changed, 54 insertions(+), 109 deletions(-) delete mode 100644 _regroup/test-etapi/no-token.http create mode 100644 apps/server/spec/etapi/no-token.spec.ts diff --git a/_regroup/test-etapi/no-token.http b/_regroup/test-etapi/no-token.http deleted file mode 100644 index d8198ed2b..000000000 --- a/_regroup/test-etapi/no-token.http +++ /dev/null @@ -1,109 +0,0 @@ -GET {{triliumHost}}/etapi/notes?search=aaa - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/notes/root - -> {% client.assert(response.status === 401); %} - -### - -PATCH {{triliumHost}}/etapi/notes/root -Authorization: fakeauth - -> {% client.assert(response.status === 401); %} - -### - -DELETE {{triliumHost}}/etapi/notes/root -Authorization: fakeauth - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/branches/root -Authorization: fakeauth - -> {% client.assert(response.status === 401); %} - -### - -PATCH {{triliumHost}}/etapi/branches/root - -> {% client.assert(response.status === 401); %} - -### - -DELETE {{triliumHost}}/etapi/branches/root - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/attributes/000 - -> {% client.assert(response.status === 401); %} - -### - -PATCH {{triliumHost}}/etapi/attributes/000 - -> {% client.assert(response.status === 401); %} - -### - -DELETE {{triliumHost}}/etapi/attributes/000 - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/inbox/2022-02-22 - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/calendar/days/2022-02-22 -Authorization: fakeauth - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/calendar/weeks/2022-02-22 - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/calendar/months/2022-02 - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/calendar/years/2022 - -> {% client.assert(response.status === 401); %} - -### - -POST {{triliumHost}}/etapi/create-note - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/app-info - -> {% client.assert(response.status === 401); %} - -### Fake URL will get a 404 even without token - -GET {{triliumHost}}/etapi/zzzzzz - -> {% client.assert(response.status === 404); %} diff --git a/apps/server/spec/etapi/no-token.spec.ts b/apps/server/spec/etapi/no-token.spec.ts new file mode 100644 index 000000000..d4a7a2f9f --- /dev/null +++ b/apps/server/spec/etapi/no-token.spec.ts @@ -0,0 +1,54 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; +import type TestAgent from "supertest/lib/agent.js"; + +let app: Application; + +const USER = "etapi"; + +const routes = [ + "GET /etapi/notes?search=aaa", + "GET /etapi/notes/root", + "PATCH /etapi/notes/root", + "DELETE /etapi/notes/root", + "GET /etapi/branches/root", + "PATCH /etapi/branches/root", + "DELETE /etapi/branches/root", + "GET /etapi/attributes/000", + "PATCH /etapi/attributes/000", + "DELETE /etapi/attributes/000", + "GET /etapi/inbox/2022-02-22", + "GET /etapi/calendar/days/2022-02-22", + "GET /etapi/calendar/weeks/2022-02-22", + "GET /etapi/calendar/months/2022-02", + "GET /etapi/calendar/years/2022", + "POST /etapi/create-note", + "GET /etapi/app-info", +] + +describe("no-token", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + }); + + for (const route of routes) { + const [ method, url ] = route.split(" ", 2); + + it(`rejects access to ${method} ${url}`, () => { + (supertest(app)[method.toLowerCase()](url) as TestAgent) + .auth(USER, "fakeauth", { "type": "basic"}) + .expect(401) + }); + } + + it("responds with 404 even without token", () => { + supertest(app) + .get("/etapi/zzzzzz") + .expect(404); + }); +}); From 6121fb0ad644b5eb493edfadb1f0468a37aa2112 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 2 Jun 2025 19:30:06 +0300 Subject: [PATCH 05/10] test(etapi): port create-backup --- _regroup/test-etapi/create-backup.http | 4 --- apps/server/spec/etapi/create-backup.spec.ts | 26 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) delete mode 100644 _regroup/test-etapi/create-backup.http create mode 100644 apps/server/spec/etapi/create-backup.spec.ts diff --git a/_regroup/test-etapi/create-backup.http b/_regroup/test-etapi/create-backup.http deleted file mode 100644 index 59ffbebc4..000000000 --- a/_regroup/test-etapi/create-backup.http +++ /dev/null @@ -1,4 +0,0 @@ -PUT {{triliumHost}}/etapi/backup/etapi_test -Authorization: {{authToken}} - -> {% client.assert(response.status === 201); %} diff --git a/apps/server/spec/etapi/create-backup.spec.ts b/apps/server/spec/etapi/create-backup.spec.ts new file mode 100644 index 000000000..efab80218 --- /dev/null +++ b/apps/server/spec/etapi/create-backup.spec.ts @@ -0,0 +1,26 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; + +describe("etapi/backup", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + }); + + it("backup works", async () => { + const response = await supertest(app) + .put("/etapi/backup/etapi_test") + .auth(USER, token, { "type": "basic"}) + .expect(201); + }); +}); From 887a7f900c89c997f312448bf21eb4ca7b607290 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 2 Jun 2025 20:36:15 +0300 Subject: [PATCH 06/10] test(etapi): port create-entities --- _regroup/test-etapi/create-entities.http | 158 ---------------- .../server/spec/etapi/create-entities.spec.ts | 178 ++++++++++++++++++ 2 files changed, 178 insertions(+), 158 deletions(-) delete mode 100644 _regroup/test-etapi/create-entities.http create mode 100644 apps/server/spec/etapi/create-entities.spec.ts diff --git a/_regroup/test-etapi/create-entities.http b/_regroup/test-etapi/create-entities.http deleted file mode 100644 index 98dae28b1..000000000 --- a/_regroup/test-etapi/create-entities.http +++ /dev/null @@ -1,158 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "forcedId{{$randomInt}}", - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!", - "dateCreated": "2023-08-21 23:38:51.123+0200", - "utcDateCreated": "2023-08-21 23:38:51.123Z" -} - -> {% - client.assert(response.status === 201); - client.assert(response.body.note.noteId.startsWith("forcedId")); - client.assert(response.body.note.title == "Hello"); - client.assert(response.body.note.dateCreated == "2023-08-21 23:38:51.123+0200"); - client.assert(response.body.note.utcDateCreated == "2023-08-21 23:38:51.123Z"); - client.assert(response.body.branch.parentNoteId == "root"); - - client.log(`Created note ` + response.body.note.noteId + ` and branch ` + response.body.branch.branchId); - - client.global.set("createdNoteId", response.body.note.noteId); - client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### Clone to another location - -POST {{triliumHost}}/etapi/branches -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "parentNoteId": "_hidden" -} - -> {% - client.assert(response.status === 201); - client.assert(response.body.parentNoteId == "_hidden"); - - client.global.set("clonedBranchId", response.body.branchId); - - client.log(`Created cloned branch ` + response.body.branchId); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.noteId == client.global.get("createdNoteId")); - client.assert(response.body.title == "Hello"); - // order is not defined and may fail in the future - client.assert(response.body.parentBranchIds[0] == client.global.get("clonedBranchId")) - client.assert(response.body.parentBranchIds[1] == client.global.get("createdBranchId")); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body == "Hi there!"); -%} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.branchId == client.global.get("createdBranchId")); - client.assert(response.body.parentNoteId == "root"); -%} - -### - -GET {{triliumHost}}/etapi/branches/{{clonedBranchId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.branchId == client.global.get("clonedBranchId")); - client.assert(response.body.parentNoteId == "_hidden"); -%} - -### - -POST {{triliumHost}}/etapi/attributes -Content-Type: application/json -Authorization: {{authToken}} - -{ - "attributeId": "forcedAttributeId{{$randomInt}}", - "noteId": "{{createdNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% - client.assert(response.status === 201); - client.assert(response.body.attributeId.startsWith("forcedAttributeId")); - - client.global.set("createdAttributeId", response.body.attributeId); -%} - -### - -GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.attributeId == client.global.get("createdAttributeId")); -%} - -### - -POST {{triliumHost}}/etapi/attachments -Content-Type: application/json -Authorization: {{authToken}} - -{ - "ownerId": "{{createdNoteId}}", - "role": "file", - "mime": "plain/text", - "title": "my attachment", - "content": "my text" -} - -> {% - client.assert(response.status === 201); - - client.global.set("createdAttachmentId", response.body.attachmentId); -%} - -### - -GET {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.attachmentId == client.global.get("createdAttachmentId")); - client.assert(response.body.role == "file"); - client.assert(response.body.mime == "plain/text"); - client.assert(response.body.title == "my attachment"); -%} diff --git a/apps/server/spec/etapi/create-entities.spec.ts b/apps/server/spec/etapi/create-entities.spec.ts new file mode 100644 index 000000000..25dab1d45 --- /dev/null +++ b/apps/server/spec/etapi/create-entities.spec.ts @@ -0,0 +1,178 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; +import { randomInt } from "crypto"; + +let app: Application; +let token: string; +let createdNoteId: string; +let createdBranchId: string; +let clonedBranchId: string; +let createdAttributeId: string; +let createdAttachmentId: string; + +const USER = "etapi"; + +describe("etapi/create-entities", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + + ({ createdNoteId, createdBranchId } = await createNote()); + clonedBranchId = await createClone(); + createdAttributeId = await createAttribute(); + createdAttachmentId = await createAttachment(); + }); + + it("returns note info", async () => { + const response = await supertest(app) + .get(`/etapi/notes/${createdNoteId}`) + .auth(USER, token, { "type": "basic"}) + .send({ + noteId: createdNoteId, + parentNoteId: "_hidden" + }) + .expect(200); + expect(response.body).toMatchObject({ + noteId: createdNoteId, + title: "Hello" + }); + expect(new Set(response.body.parentBranchIds)) + .toStrictEqual(new Set([ clonedBranchId, createdBranchId ])); + }); + + it("obtains note content", async () => { + await supertest(app) + .get(`/etapi/notes/${createdNoteId}/content`) + .auth(USER, token, { "type": "basic"}) + .expect(200) + .expect("Hi there!"); + }); + + it("obtains created branch information", async () => { + const response = await supertest(app) + .get(`/etapi/branches/${createdBranchId}`) + .auth(USER, token, { "type": "basic"}) + .expect(200); + expect(response.body).toMatchObject({ + branchId: createdBranchId, + parentNoteId: "root" + }); + }); + + it("obtains cloned branch information", async () => { + const response = await supertest(app) + .get(`/etapi/branches/${clonedBranchId}`) + .auth(USER, token, { "type": "basic"}) + .expect(200); + expect(response.body).toMatchObject({ + branchId: clonedBranchId, + parentNoteId: "_hidden" + }); + }); + + it("obtains attribute information", async () => { + const response = await supertest(app) + .get(`/etapi/attributes/${createdAttributeId}`) + .auth(USER, token, { "type": "basic"}) + .expect(200); + expect(response.body.attributeId).toStrictEqual(createdAttributeId); + }); + + it("obtains attachment information", async () => { + const response = await supertest(app) + .get(`/etapi/attachments/${createdAttachmentId}`) + .auth(USER, token, { "type": "basic"}) + .expect(200); + expect(response.body.attachmentId).toStrictEqual(createdAttachmentId); + expect(response.body).toMatchObject({ + role: "file", + mime: "plain/text", + title: "my attachment" + }); + }); +}); + +async function createNote() { + const noteId = `forcedId${randomInt(1000)}`; + const response = await supertest(app) + .post("/etapi/create-note") + .auth(USER, token, { "type": "basic"}) + .send({ + "noteId": noteId, + "parentNoteId": "root", + "title": "Hello", + "type": "text", + "content": "Hi there!", + "dateCreated": "2023-08-21 23:38:51.123+0200", + "utcDateCreated": "2023-08-21 23:38:51.123Z" + }) + .expect(201); + expect(response.body.note.noteId).toStrictEqual(noteId); + expect(response.body).toMatchObject({ + note: { + noteId, + title: "Hello", + dateCreated: "2023-08-21 23:38:51.123+0200", + utcDateCreated: "2023-08-21 23:38:51.123Z" + }, + branch: { + parentNoteId: "root" + } + }); + + return { + createdNoteId: response.body.note.noteId, + createdBranchId: response.body.branch.branchId + }; +} + +async function createClone() { + const response = await supertest(app) + .post("/etapi/branches") + .auth(USER, token, { "type": "basic"}) + .send({ + noteId: createdNoteId, + parentNoteId: "_hidden" + }) + .expect(201); + expect(response.body.parentNoteId).toStrictEqual("_hidden"); + return response.body.branchId; +} + +async function createAttribute() { + const attributeId = `forcedId${randomInt(1000)}`; + const response = await supertest(app) + .post("/etapi/attributes") + .auth(USER, token, { "type": "basic"}) + .send({ + "attributeId": attributeId, + "noteId": createdNoteId, + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": true + }) + .expect(201); + expect(response.body.attributeId).toStrictEqual(attributeId); + return response.body.attributeId; +} + +async function createAttachment() { + const response = await supertest(app) + .post("/etapi/attachments") + .auth(USER, token, { "type": "basic"}) + .send({ + "ownerId": createdNoteId, + "role": "file", + "mime": "plain/text", + "title": "my attachment", + "content": "my text" + }) + .expect(201); + return response.body.attachmentId; +} From 4e81be8c76d091f6773ea4a31cd2efb504c0c77d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 2 Jun 2025 20:59:25 +0300 Subject: [PATCH 07/10] test(etapi): port other --- _regroup/test-etapi/other.http | 4 ---- apps/server/spec/etapi/other.spec.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) delete mode 100644 _regroup/test-etapi/other.http create mode 100644 apps/server/spec/etapi/other.spec.ts diff --git a/_regroup/test-etapi/other.http b/_regroup/test-etapi/other.http deleted file mode 100644 index c3f92fc94..000000000 --- a/_regroup/test-etapi/other.http +++ /dev/null @@ -1,4 +0,0 @@ -POST {{triliumHost}}/etapi/refresh-note-ordering/root -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} \ No newline at end of file diff --git a/apps/server/spec/etapi/other.spec.ts b/apps/server/spec/etapi/other.spec.ts new file mode 100644 index 000000000..733ab12a6 --- /dev/null +++ b/apps/server/spec/etapi/other.spec.ts @@ -0,0 +1,26 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; + +describe("etapi/refresh-note-ordering/root", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + }); + + it("refreshes note ordering", async () => { + await supertest(app) + .post("/etapi/refresh-note-ordering/root") + .auth(USER, token, { "type": "basic"}) + .expect(200); + }); +}); From 95641a3b6db7980dc2922953b7c0d3ba093d993c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 2 Jun 2025 21:16:57 +0300 Subject: [PATCH 08/10] test(etapi): port search --- _regroup/test-etapi/search.http | 39 -------------------------- apps/server/spec/etapi/search.spec.ts | 40 +++++++++++++++++++++++++++ apps/server/spec/etapi/utils.ts | 17 ++++++++++++ 3 files changed, 57 insertions(+), 39 deletions(-) delete mode 100644 _regroup/test-etapi/search.http create mode 100644 apps/server/spec/etapi/search.spec.ts diff --git a/_regroup/test-etapi/search.http b/_regroup/test-etapi/search.http deleted file mode 100644 index 4655f22e0..000000000 --- a/_regroup/test-etapi/search.http +++ /dev/null @@ -1,39 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "title", - "type": "text", - "content": "{{$uuid}}" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content -Authorization: {{authToken}} - -> {% client.global.set("content", response.body); %} - -### - -GET {{triliumHost}}/etapi/notes?search={{content}}&debug=true -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.results.length === 1); -%} - -### Same but with fast search which doesn't look in the content so 0 notes should be found - -GET {{triliumHost}}/etapi/notes?search={{content}}&fastSearch=true -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.results.length === 0); -%} diff --git a/apps/server/spec/etapi/search.spec.ts b/apps/server/spec/etapi/search.spec.ts new file mode 100644 index 000000000..bfd14e740 --- /dev/null +++ b/apps/server/spec/etapi/search.spec.ts @@ -0,0 +1,40 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { createNote, login } from "./utils.js"; +import config from "../../src/services/config.js"; +import { randomUUID } from "crypto"; + +let app: Application; +let token: string; + +const USER = "etapi"; +let content: string; + +describe("etapi/search", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + + content = randomUUID(); + await createNote(app, token, content); + }); + + it("finds by content", async () => { + const response = await supertest(app) + .get(`/etapi/notes?search=${content}&debug=true`) + .auth(USER, token, { "type": "basic"}) + .expect(200); + expect(response.body.results).toHaveLength(1); + }); + + it("does not find by content when fast search is on", async () => { + const response = await supertest(app) + .get(`/etapi/notes?search=${content}&debug=true&fastSearch=true`) + .auth(USER, token, { "type": "basic"}) + .expect(200); + expect(response.body.results).toHaveLength(0); + }); +}); diff --git a/apps/server/spec/etapi/utils.ts b/apps/server/spec/etapi/utils.ts index f9657eeab..509698370 100644 --- a/apps/server/spec/etapi/utils.ts +++ b/apps/server/spec/etapi/utils.ts @@ -14,3 +14,20 @@ export async function login(app: Application) { expect(token).toBeTruthy(); return token; } + +export async function createNote(app: Application, token: string, content?: string) { + const response = await supertest(app) + .post("/etapi/create-note") + .auth("etapi", token, { "type": "basic"}) + .send({ + "parentNoteId": "root", + "title": "Hello", + "type": "text", + "content": content ?? "Hi there!", + }) + .expect(201); + + const noteId = response.body.note.noteId; + expect(noteId).toStrictEqual(noteId); + return noteId; +} From 26fcc4fb243a7e7e2f872e889f75ca95e2375fd3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 May 2025 21:26:19 +0300 Subject: [PATCH 09/10] feat(flake): support darwin --- flake.nix | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/flake.nix b/flake.nix index a2dd0197b..4ffa16168 100644 --- a/flake.nix +++ b/flake.nix @@ -27,23 +27,35 @@ pnpm stdenv wrapGAppsHook3 + xcodebuild + darwin ; desktop = stdenv.mkDerivation (finalAttrs: { pname = "triliumnext-desktop"; version = packageJSON.version; src = lib.cleanSource ./.; - nativeBuildInputs = [ - pnpm.configHook - nodejs - nodejs.python - copyDesktopItems - makeBinaryWrapper - wrapGAppsHook3 - ]; + nativeBuildInputs = + [ + pnpm.configHook + nodejs + nodejs.python + copyDesktopItems + makeBinaryWrapper + wrapGAppsHook3 + ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ + xcodebuild + darwin.cctools + ]; dontWrapGApps = true; + preBuild = lib.optionalString stdenv.hostPlatform.isLinux '' + patchelf --set-interpreter $(cat $NIX_CC/nix-support/dynamic-linker) \ + node_modules/.pnpm/sass-embedded-linux-x64@*/node_modules/sass-embedded-linux-x64/dart-sass/src/dart + ''; + buildPhase = '' runHook preBuild @@ -51,8 +63,6 @@ export NX_TUI=false export NX_DAEMON=false - patchelf --set-interpreter $(cat $NIX_CC/nix-support/dynamic-linker) \ - node_modules/.pnpm/sass-embedded-linux-x64@*/node_modules/sass-embedded-linux-x64/dart-sass/src/dart pnpm nx run desktop:build --outputStyle stream --verbose # Rebuild dependencies From 4ed30e0624454f38b7f20e409ae7283d3c21d2b2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 2 Jun 2025 21:43:40 +0300 Subject: [PATCH 10/10] test(etapi): port basic-auth --- _regroup/test-etapi/logout.http | 34 ----------------------- apps/server/spec/etapi/basic-auth.spec.ts | 13 +++++++++ 2 files changed, 13 insertions(+), 34 deletions(-) delete mode 100644 _regroup/test-etapi/logout.http diff --git a/_regroup/test-etapi/logout.http b/_regroup/test-etapi/logout.http deleted file mode 100644 index 9bd7355e0..000000000 --- a/_regroup/test-etapi/logout.http +++ /dev/null @@ -1,34 +0,0 @@ -POST {{triliumHost}}/etapi/auth/login -Content-Type: application/json - -{ - "password": "1234" -} - -> {% - client.assert(response.status === 201); - - client.global.set("testAuthToken", response.body.authToken); -%} - -### - -GET {{triliumHost}}/etapi/notes/root -Authorization: {{testAuthToken}} - -> {% client.assert(response.status === 200); %} - -### - -POST {{triliumHost}}/etapi/auth/logout -Authorization: {{testAuthToken}} -Content-Type: application/json - -> {% client.assert(response.status === 204); %} - -### - -GET {{triliumHost}}/etapi/notes/root -Authorization: {{testAuthToken}} - -> {% client.assert(response.status === 401); %} diff --git a/apps/server/spec/etapi/basic-auth.spec.ts b/apps/server/spec/etapi/basic-auth.spec.ts index b3fbc837d..6518c7a12 100644 --- a/apps/server/spec/etapi/basic-auth.spec.ts +++ b/apps/server/spec/etapi/basic-auth.spec.ts @@ -38,4 +38,17 @@ describe("basic-auth", () => { .auth("wrong", token, { "type": "basic"}) .expect(401); }); + + it("logs out", async () => { + await supertest(app) + .post("/etapi/auth/logout") + .auth(USER, token, { "type": "basic"}) + .expect(204); + + // Ensure we can't access it anymore + await supertest(app) + .get("/etapi/notes/root") + .auth(USER, token, { "type": "basic"}) + .expect(401); + }); });