mirror of
https://github.com/zadam/trilium.git
synced 2025-11-08 06:15:48 +01:00
373 lines
14 KiB
TypeScript
373 lines
14 KiB
TypeScript
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);
|
|
}, 30000); // Increase timeout to 30 seconds for app initialization
|
|
|
|
describe("Basic Search", () => {
|
|
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);
|
|
});
|
|
|
|
it("returns proper response structure", async () => {
|
|
const response = await supertest(app)
|
|
.get(`/etapi/notes?search=${content}`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
expect(response.body).toHaveProperty("results");
|
|
expect(Array.isArray(response.body.results)).toBe(true);
|
|
|
|
if (response.body.results.length > 0) {
|
|
const note = response.body.results[0];
|
|
expect(note).toHaveProperty("noteId");
|
|
expect(note).toHaveProperty("title");
|
|
expect(note).toHaveProperty("type");
|
|
}
|
|
});
|
|
|
|
it("returns debug info when requested", async () => {
|
|
const response = await supertest(app)
|
|
.get(`/etapi/notes?search=${content}&debug=true`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
expect(response.body).toHaveProperty("debugInfo");
|
|
expect(response.body.debugInfo).toBeTruthy();
|
|
});
|
|
|
|
it("returns 400 for missing search parameter", async () => {
|
|
await supertest(app)
|
|
.get("/etapi/notes")
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(400);
|
|
});
|
|
|
|
it("returns 400 for empty search parameter", async () => {
|
|
await supertest(app)
|
|
.get("/etapi/notes?search=")
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(400);
|
|
});
|
|
});
|
|
|
|
describe("Search Parameters", () => {
|
|
let testNoteId: string;
|
|
|
|
beforeAll(async () => {
|
|
// Create a test note with unique content
|
|
const uniqueContent = `test-${randomUUID()}`;
|
|
testNoteId = await createNote(app, token, uniqueContent);
|
|
}, 10000);
|
|
|
|
it("respects fastSearch parameter", async () => {
|
|
// Fast search should not find by content
|
|
const fastResponse = await supertest(app)
|
|
.get(`/etapi/notes?search=${content}&fastSearch=true`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
expect(fastResponse.body.results).toHaveLength(0);
|
|
|
|
// Regular search should find by content
|
|
const regularResponse = await supertest(app)
|
|
.get(`/etapi/notes?search=${content}&fastSearch=false`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
expect(regularResponse.body.results.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("respects includeArchivedNotes parameter", async () => {
|
|
// Default should include archived notes
|
|
const withArchivedResponse = await supertest(app)
|
|
.get(`/etapi/notes?search=*&includeArchivedNotes=true`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
const withoutArchivedResponse = await supertest(app)
|
|
.get(`/etapi/notes?search=*&includeArchivedNotes=false`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
// Note: Actual behavior depends on whether there are archived notes
|
|
expect(withArchivedResponse.body.results).toBeDefined();
|
|
expect(withoutArchivedResponse.body.results).toBeDefined();
|
|
});
|
|
|
|
it("respects limit parameter", async () => {
|
|
const limit = 5;
|
|
const response = await supertest(app)
|
|
.get(`/etapi/notes?search=*&limit=${limit}`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
expect(response.body.results.length).toBeLessThanOrEqual(limit);
|
|
});
|
|
|
|
it("handles fuzzyAttributeSearch parameter", async () => {
|
|
const response = await supertest(app)
|
|
.get(`/etapi/notes?search=*&fuzzyAttributeSearch=true`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
expect(response.body.results).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("Search Queries", () => {
|
|
let titleNoteId: string;
|
|
let labelNoteId: string;
|
|
|
|
beforeAll(async () => {
|
|
// Create test notes with specific attributes
|
|
const uniqueTitle = `SearchTest-${randomUUID()}`;
|
|
|
|
// Create note with specific title
|
|
const titleResponse = await supertest(app)
|
|
.post("/etapi/create-note")
|
|
.auth(USER, token, { "type": "basic"})
|
|
.send({
|
|
"parentNoteId": "root",
|
|
"title": uniqueTitle,
|
|
"type": "text",
|
|
"content": "Title test content"
|
|
})
|
|
.expect(201);
|
|
titleNoteId = titleResponse.body.note.noteId;
|
|
|
|
// Create note with label
|
|
const labelResponse = await supertest(app)
|
|
.post("/etapi/create-note")
|
|
.auth(USER, token, { "type": "basic"})
|
|
.send({
|
|
"parentNoteId": "root",
|
|
"title": "Label Test",
|
|
"type": "text",
|
|
"content": "Label test content"
|
|
})
|
|
.expect(201);
|
|
labelNoteId = labelResponse.body.note.noteId;
|
|
|
|
// Add label to note
|
|
await supertest(app)
|
|
.post("/etapi/attributes")
|
|
.auth(USER, token, { "type": "basic"})
|
|
.send({
|
|
"noteId": labelNoteId,
|
|
"type": "label",
|
|
"name": "testlabel",
|
|
"value": "testvalue"
|
|
})
|
|
.expect(201);
|
|
}, 15000); // 15 second timeout for setup
|
|
|
|
it("searches by title", async () => {
|
|
// Get the title we created
|
|
const noteResponse = await supertest(app)
|
|
.get(`/etapi/notes/${titleNoteId}`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
const title = noteResponse.body.title;
|
|
|
|
const searchResponse = await supertest(app)
|
|
.get(`/etapi/notes?search=${encodeURIComponent(title)}`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
expect(searchResponse.body.results.length).toBeGreaterThan(0);
|
|
const foundNote = searchResponse.body.results.find((n: any) => n.noteId === titleNoteId);
|
|
expect(foundNote).toBeTruthy();
|
|
});
|
|
|
|
it("searches by label", async () => {
|
|
const searchResponse = await supertest(app)
|
|
.get(`/etapi/notes?search=${encodeURIComponent("#testlabel")}`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
expect(searchResponse.body.results.length).toBeGreaterThan(0);
|
|
const foundNote = searchResponse.body.results.find((n: any) => n.noteId === labelNoteId);
|
|
expect(foundNote).toBeTruthy();
|
|
});
|
|
|
|
it("searches by label with value", async () => {
|
|
const searchResponse = await supertest(app)
|
|
.get(`/etapi/notes?search=${encodeURIComponent("#testlabel=testvalue")}`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
expect(searchResponse.body.results.length).toBeGreaterThan(0);
|
|
const foundNote = searchResponse.body.results.find((n: any) => n.noteId === labelNoteId);
|
|
expect(foundNote).toBeTruthy();
|
|
});
|
|
|
|
it("handles complex queries with AND operator", async () => {
|
|
const searchResponse = await supertest(app)
|
|
.get(`/etapi/notes?search=${encodeURIComponent("#testlabel AND note.type=text")}`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
expect(searchResponse.body.results).toBeDefined();
|
|
});
|
|
|
|
it("handles queries with OR operator", async () => {
|
|
const searchResponse = await supertest(app)
|
|
.get(`/etapi/notes?search=${encodeURIComponent("#testlabel OR #nonexistent")}`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
expect(searchResponse.body.results.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("handles queries with NOT operator", async () => {
|
|
const searchResponse = await supertest(app)
|
|
.get(`/etapi/notes?search=${encodeURIComponent("#testlabel NOT #nonexistent")}`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
expect(searchResponse.body.results.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("handles wildcard searches", async () => {
|
|
const searchResponse = await supertest(app)
|
|
.get(`/etapi/notes?search=note.type%3Dtext&limit=10`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
expect(searchResponse.body.results).toBeDefined();
|
|
// Should return results if any text notes exist
|
|
expect(Array.isArray(searchResponse.body.results)).toBe(true);
|
|
});
|
|
|
|
it("handles empty results gracefully", async () => {
|
|
const nonexistentQuery = `nonexistent-${randomUUID()}`;
|
|
const searchResponse = await supertest(app)
|
|
.get(`/etapi/notes?search=${encodeURIComponent(nonexistentQuery)}`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
expect(searchResponse.body.results).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("Error Handling", () => {
|
|
it("handles invalid query syntax gracefully", async () => {
|
|
const response = await supertest(app)
|
|
.get(`/etapi/notes?search=${encodeURIComponent("(((")}`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
// Should return empty results or handle error gracefully
|
|
expect(response.body.results).toBeDefined();
|
|
});
|
|
|
|
it("requires authentication", async () => {
|
|
await supertest(app)
|
|
.get(`/etapi/notes?search=test`)
|
|
.expect(401);
|
|
});
|
|
|
|
it("rejects invalid authentication", async () => {
|
|
await supertest(app)
|
|
.get(`/etapi/notes?search=test`)
|
|
.auth(USER, "invalid-token", { "type": "basic"})
|
|
.expect(401);
|
|
});
|
|
});
|
|
|
|
describe("Performance", () => {
|
|
it("handles large result sets", async () => {
|
|
const startTime = Date.now();
|
|
|
|
const response = await supertest(app)
|
|
.get(`/etapi/notes?search=*&limit=100`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
const endTime = Date.now();
|
|
const duration = endTime - startTime;
|
|
|
|
expect(response.body.results).toBeDefined();
|
|
// Search should complete in reasonable time (5 seconds)
|
|
expect(duration).toBeLessThan(5000);
|
|
});
|
|
|
|
it("handles queries efficiently", async () => {
|
|
const startTime = Date.now();
|
|
|
|
await supertest(app)
|
|
.get(`/etapi/notes?search=${encodeURIComponent("#*")}`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
const endTime = Date.now();
|
|
const duration = endTime - startTime;
|
|
|
|
// Attribute search should be fast
|
|
expect(duration).toBeLessThan(3000);
|
|
});
|
|
});
|
|
|
|
describe("Special Characters", () => {
|
|
it("handles special characters in search", async () => {
|
|
const specialChars = "test@#$%";
|
|
const response = await supertest(app)
|
|
.get(`/etapi/notes?search=${encodeURIComponent(specialChars)}`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
expect(response.body.results).toBeDefined();
|
|
});
|
|
|
|
it("handles unicode characters", async () => {
|
|
const unicode = "测试";
|
|
const response = await supertest(app)
|
|
.get(`/etapi/notes?search=${encodeURIComponent(unicode)}`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
expect(response.body.results).toBeDefined();
|
|
});
|
|
|
|
it("handles quotes in search", async () => {
|
|
const quoted = '"test phrase"';
|
|
const response = await supertest(app)
|
|
.get(`/etapi/notes?search=${encodeURIComponent(quoted)}`)
|
|
.auth(USER, token, { "type": "basic"})
|
|
.expect(200);
|
|
|
|
expect(response.body.results).toBeDefined();
|
|
});
|
|
});
|
|
});
|