@jant/core 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/routes/api/posts.js +1 -1
- package/package.json +10 -4
- package/src/__tests__/helpers/app.ts +97 -0
- package/src/__tests__/helpers/db.ts +85 -0
- package/src/lib/__tests__/constants.test.ts +44 -0
- package/src/lib/__tests__/markdown.test.ts +133 -0
- package/src/lib/__tests__/schemas.test.ts +220 -0
- package/src/lib/__tests__/sqid.test.ts +65 -0
- package/src/lib/__tests__/sse.test.ts +86 -0
- package/src/lib/__tests__/time.test.ts +112 -0
- package/src/lib/__tests__/url.test.ts +138 -0
- package/src/middleware/__tests__/auth.test.ts +139 -0
- package/src/routes/api/__tests__/posts.test.ts +306 -0
- package/src/routes/api/__tests__/search.test.ts +77 -0
- package/src/routes/api/posts.ts +3 -1
- package/src/services/__tests__/collection.test.ts +226 -0
- package/src/services/__tests__/media.test.ts +134 -0
- package/src/services/__tests__/post.test.ts +636 -0
- package/src/services/__tests__/redirect.test.ts +110 -0
- package/src/services/__tests__/search.test.ts +143 -0
- package/src/services/__tests__/settings.test.ts +110 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
3
|
+
import { createRedirectService } from "../redirect.js";
|
|
4
|
+
import type { Database } from "../../db/index.js";
|
|
5
|
+
|
|
6
|
+
describe("RedirectService", () => {
|
|
7
|
+
let db: Database;
|
|
8
|
+
let redirectService: ReturnType<typeof createRedirectService>;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
const testDb = createTestDatabase();
|
|
12
|
+
db = testDb.db as unknown as Database;
|
|
13
|
+
redirectService = createRedirectService(db);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("create", () => {
|
|
17
|
+
it("creates a 301 redirect by default", async () => {
|
|
18
|
+
const redirect = await redirectService.create("/old-path", "/new-path");
|
|
19
|
+
|
|
20
|
+
expect(redirect.fromPath).toBe("old-path"); // normalizePath removes leading slash
|
|
21
|
+
expect(redirect.toPath).toBe("/new-path");
|
|
22
|
+
expect(redirect.type).toBe(301);
|
|
23
|
+
expect(redirect.id).toBe(1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("creates a 302 redirect", async () => {
|
|
27
|
+
const redirect = await redirectService.create(
|
|
28
|
+
"/temp",
|
|
29
|
+
"/destination",
|
|
30
|
+
302,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(redirect.type).toBe(302);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("normalizes from path", async () => {
|
|
37
|
+
const redirect = await redirectService.create(
|
|
38
|
+
" /OLD-PATH/ ",
|
|
39
|
+
"/new-path",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(redirect.fromPath).toBe("old-path");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("replaces existing redirect for same from path", async () => {
|
|
46
|
+
await redirectService.create("/old", "/first");
|
|
47
|
+
const second = await redirectService.create("/old", "/second");
|
|
48
|
+
|
|
49
|
+
expect(second.toPath).toBe("/second");
|
|
50
|
+
|
|
51
|
+
const list = await redirectService.list();
|
|
52
|
+
expect(list).toHaveLength(1);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("getByPath", () => {
|
|
57
|
+
it("finds redirect by from path", async () => {
|
|
58
|
+
await redirectService.create("/old-page", "/new-page");
|
|
59
|
+
|
|
60
|
+
const found = await redirectService.getByPath("/old-page");
|
|
61
|
+
expect(found).not.toBeNull();
|
|
62
|
+
expect(found?.toPath).toBe("/new-page");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("normalizes the lookup path", async () => {
|
|
66
|
+
await redirectService.create("/old-page", "/new-page");
|
|
67
|
+
|
|
68
|
+
const found = await redirectService.getByPath(" /OLD-PAGE/ ");
|
|
69
|
+
expect(found).not.toBeNull();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("returns null for non-existent path", async () => {
|
|
73
|
+
const found = await redirectService.getByPath("/nonexistent");
|
|
74
|
+
expect(found).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("delete", () => {
|
|
79
|
+
it("deletes a redirect by ID", async () => {
|
|
80
|
+
const redirect = await redirectService.create("/old", "/new");
|
|
81
|
+
const result = await redirectService.delete(redirect.id);
|
|
82
|
+
|
|
83
|
+
expect(result).toBe(true);
|
|
84
|
+
|
|
85
|
+
const found = await redirectService.getByPath("/old");
|
|
86
|
+
expect(found).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns false for non-existent ID", async () => {
|
|
90
|
+
const result = await redirectService.delete(9999);
|
|
91
|
+
expect(result).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("list", () => {
|
|
96
|
+
it("returns empty array when no redirects exist", async () => {
|
|
97
|
+
const redirects = await redirectService.list();
|
|
98
|
+
expect(redirects).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns all redirects", async () => {
|
|
102
|
+
await redirectService.create("/a", "/b");
|
|
103
|
+
await redirectService.create("/c", "/d");
|
|
104
|
+
await redirectService.create("/e", "/f");
|
|
105
|
+
|
|
106
|
+
const redirects = await redirectService.list();
|
|
107
|
+
expect(redirects).toHaveLength(3);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
3
|
+
import { createSearchService } from "../search.js";
|
|
4
|
+
import { createPostService } from "../post.js";
|
|
5
|
+
import type { Database } from "../../db/index.js";
|
|
6
|
+
import type BetterSqlite3 from "better-sqlite3";
|
|
7
|
+
|
|
8
|
+
describe("SearchService", () => {
|
|
9
|
+
let db: Database;
|
|
10
|
+
let sqlite: BetterSqlite3.Database;
|
|
11
|
+
let postService: ReturnType<typeof createPostService>;
|
|
12
|
+
|
|
13
|
+
// Create a mock D1Database interface wrapping better-sqlite3
|
|
14
|
+
function createMockD1(sqliteDb: BetterSqlite3.Database) {
|
|
15
|
+
return {
|
|
16
|
+
prepare(query: string) {
|
|
17
|
+
return {
|
|
18
|
+
bind(...params: unknown[]) {
|
|
19
|
+
return {
|
|
20
|
+
async all<T>() {
|
|
21
|
+
const stmt = sqliteDb.prepare(query);
|
|
22
|
+
const rows = stmt.all(...(params as never[])) as T[];
|
|
23
|
+
return { results: rows };
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
} as unknown as D1Database;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
const testDb = createTestDatabase({ fts: true });
|
|
34
|
+
db = testDb.db as unknown as Database;
|
|
35
|
+
sqlite = testDb.sqlite;
|
|
36
|
+
postService = createPostService(db);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns empty results for empty query", async () => {
|
|
40
|
+
const d1 = createMockD1(sqlite);
|
|
41
|
+
const searchService = createSearchService(d1);
|
|
42
|
+
|
|
43
|
+
const results = await searchService.search("");
|
|
44
|
+
expect(results).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("returns empty results for whitespace-only query", async () => {
|
|
48
|
+
const d1 = createMockD1(sqlite);
|
|
49
|
+
const searchService = createSearchService(d1);
|
|
50
|
+
|
|
51
|
+
const results = await searchService.search(" ");
|
|
52
|
+
expect(results).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("finds posts by content", async () => {
|
|
56
|
+
await postService.create({
|
|
57
|
+
type: "note",
|
|
58
|
+
content: "Hello world from jant",
|
|
59
|
+
visibility: "featured",
|
|
60
|
+
});
|
|
61
|
+
await postService.create({
|
|
62
|
+
type: "note",
|
|
63
|
+
content: "Another post entirely",
|
|
64
|
+
visibility: "featured",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const d1 = createMockD1(sqlite);
|
|
68
|
+
const searchService = createSearchService(d1);
|
|
69
|
+
|
|
70
|
+
const results = await searchService.search("jant");
|
|
71
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
72
|
+
expect(results[0]?.post.content).toContain("jant");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("finds posts by title", async () => {
|
|
76
|
+
await postService.create({
|
|
77
|
+
type: "article",
|
|
78
|
+
title: "Introduction to TypeScript",
|
|
79
|
+
content: "Some article body",
|
|
80
|
+
visibility: "quiet",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const d1 = createMockD1(sqlite);
|
|
84
|
+
const searchService = createSearchService(d1);
|
|
85
|
+
|
|
86
|
+
const results = await searchService.search("TypeScript");
|
|
87
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
88
|
+
expect(results[0]?.post.title).toContain("TypeScript");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("respects visibility filter", async () => {
|
|
92
|
+
await postService.create({
|
|
93
|
+
type: "note",
|
|
94
|
+
content: "visible post about testing",
|
|
95
|
+
visibility: "featured",
|
|
96
|
+
});
|
|
97
|
+
await postService.create({
|
|
98
|
+
type: "note",
|
|
99
|
+
content: "draft post about testing",
|
|
100
|
+
visibility: "draft",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const d1 = createMockD1(sqlite);
|
|
104
|
+
const searchService = createSearchService(d1);
|
|
105
|
+
|
|
106
|
+
const results = await searchService.search("testing", {
|
|
107
|
+
visibility: ["featured"],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(results.every((r) => r.post.visibility === "featured")).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("excludes deleted posts", async () => {
|
|
114
|
+
const post = await postService.create({
|
|
115
|
+
type: "note",
|
|
116
|
+
content: "deleted post with unique search term xyzzy",
|
|
117
|
+
visibility: "featured",
|
|
118
|
+
});
|
|
119
|
+
await postService.delete(post.id);
|
|
120
|
+
|
|
121
|
+
const d1 = createMockD1(sqlite);
|
|
122
|
+
const searchService = createSearchService(d1);
|
|
123
|
+
|
|
124
|
+
const results = await searchService.search("xyzzy");
|
|
125
|
+
expect(results).toHaveLength(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("supports limit and offset", async () => {
|
|
129
|
+
for (let i = 0; i < 5; i++) {
|
|
130
|
+
await postService.create({
|
|
131
|
+
type: "note",
|
|
132
|
+
content: `searchable post number ${i}`,
|
|
133
|
+
visibility: "featured",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const d1 = createMockD1(sqlite);
|
|
138
|
+
const searchService = createSearchService(d1);
|
|
139
|
+
|
|
140
|
+
const limited = await searchService.search("searchable", { limit: 2 });
|
|
141
|
+
expect(limited.length).toBeLessThanOrEqual(2);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
3
|
+
import { createSettingsService } from "../settings.js";
|
|
4
|
+
import type { Database } from "../../db/index.js";
|
|
5
|
+
|
|
6
|
+
describe("SettingsService", () => {
|
|
7
|
+
let db: Database;
|
|
8
|
+
let settingsService: ReturnType<typeof createSettingsService>;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
const testDb = createTestDatabase();
|
|
12
|
+
db = testDb.db as unknown as Database;
|
|
13
|
+
settingsService = createSettingsService(db);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("get", () => {
|
|
17
|
+
it("returns null for non-existent key", async () => {
|
|
18
|
+
const result = await settingsService.get("SITE_NAME");
|
|
19
|
+
expect(result).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns value after set", async () => {
|
|
23
|
+
await settingsService.set("SITE_NAME", "My Blog");
|
|
24
|
+
const result = await settingsService.get("SITE_NAME");
|
|
25
|
+
expect(result).toBe("My Blog");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("set", () => {
|
|
30
|
+
it("creates a new setting", async () => {
|
|
31
|
+
await settingsService.set("SITE_NAME", "Test Site");
|
|
32
|
+
const result = await settingsService.get("SITE_NAME");
|
|
33
|
+
expect(result).toBe("Test Site");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("updates existing setting (upsert)", async () => {
|
|
37
|
+
await settingsService.set("SITE_NAME", "Original");
|
|
38
|
+
await settingsService.set("SITE_NAME", "Updated");
|
|
39
|
+
const result = await settingsService.get("SITE_NAME");
|
|
40
|
+
expect(result).toBe("Updated");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("getAll", () => {
|
|
45
|
+
it("returns empty object when no settings exist", async () => {
|
|
46
|
+
const result = await settingsService.getAll();
|
|
47
|
+
expect(result).toEqual({});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns all settings as key-value pairs", async () => {
|
|
51
|
+
await settingsService.set("SITE_NAME", "My Blog");
|
|
52
|
+
await settingsService.set("SITE_DESCRIPTION", "A cool blog");
|
|
53
|
+
|
|
54
|
+
const result = await settingsService.getAll();
|
|
55
|
+
expect(result).toEqual({
|
|
56
|
+
SITE_NAME: "My Blog",
|
|
57
|
+
SITE_DESCRIPTION: "A cool blog",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("setMany", () => {
|
|
63
|
+
it("sets multiple values at once", async () => {
|
|
64
|
+
await settingsService.setMany({
|
|
65
|
+
SITE_NAME: "My Blog",
|
|
66
|
+
SITE_DESCRIPTION: "Description",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(await settingsService.get("SITE_NAME")).toBe("My Blog");
|
|
70
|
+
expect(await settingsService.get("SITE_DESCRIPTION")).toBe("Description");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("skips undefined values", async () => {
|
|
74
|
+
await settingsService.set("SITE_NAME", "Original");
|
|
75
|
+
await settingsService.setMany({
|
|
76
|
+
SITE_NAME: undefined,
|
|
77
|
+
SITE_DESCRIPTION: "New",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(await settingsService.get("SITE_NAME")).toBe("Original");
|
|
81
|
+
expect(await settingsService.get("SITE_DESCRIPTION")).toBe("New");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("remove", () => {
|
|
86
|
+
it("removes a setting", async () => {
|
|
87
|
+
await settingsService.set("SITE_NAME", "Test");
|
|
88
|
+
await settingsService.remove("SITE_NAME");
|
|
89
|
+
const result = await settingsService.get("SITE_NAME");
|
|
90
|
+
expect(result).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("does not throw when removing non-existent key", async () => {
|
|
94
|
+
await expect(settingsService.remove("SITE_NAME")).resolves.not.toThrow();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("onboarding", () => {
|
|
99
|
+
it("returns false when onboarding is not complete", async () => {
|
|
100
|
+
const result = await settingsService.isOnboardingComplete();
|
|
101
|
+
expect(result).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns true after completing onboarding", async () => {
|
|
105
|
+
await settingsService.completeOnboarding();
|
|
106
|
+
const result = await settingsService.isOnboardingComplete();
|
|
107
|
+
expect(result).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|