@jant/core 0.3.24 → 0.3.25

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.
Files changed (206) hide show
  1. package/dist/app.js +50 -25
  2. package/dist/db/schema.js +1 -1
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +3 -9
  7. package/dist/lib/constants.js +1 -0
  8. package/dist/lib/nav-reorder.js +1 -1
  9. package/dist/lib/navigation.js +26 -1
  10. package/dist/lib/pagination.js +44 -0
  11. package/dist/lib/render.js +7 -11
  12. package/dist/lib/schemas.js +3 -3
  13. package/dist/lib/theme.js +4 -4
  14. package/dist/lib/timeline.js +24 -48
  15. package/dist/lib/view.js +2 -2
  16. package/dist/routes/api/collections.js +124 -0
  17. package/dist/routes/api/nav-items.js +104 -0
  18. package/dist/routes/api/pages.js +91 -0
  19. package/dist/routes/api/posts.js +2 -2
  20. package/dist/routes/api/search.js +2 -2
  21. package/dist/routes/api/settings.js +68 -0
  22. package/dist/routes/compose.js +48 -0
  23. package/dist/routes/dash/collections.js +2 -2
  24. package/dist/routes/dash/index.js +1 -1
  25. package/dist/routes/dash/media.js +2 -2
  26. package/dist/routes/dash/pages.js +411 -62
  27. package/dist/routes/dash/posts.js +3 -5
  28. package/dist/routes/dash/redirects.js +2 -2
  29. package/dist/routes/dash/settings.js +79 -5
  30. package/dist/routes/feed/rss.js +2 -2
  31. package/dist/routes/feed/sitemap.js +1 -1
  32. package/dist/routes/pages/archive.js +3 -6
  33. package/dist/routes/pages/collection.js +3 -6
  34. package/dist/routes/pages/collections.js +28 -0
  35. package/dist/routes/pages/featured.js +32 -0
  36. package/dist/routes/pages/home.js +9 -50
  37. package/dist/routes/pages/page.js +29 -32
  38. package/dist/routes/pages/post.js +3 -6
  39. package/dist/routes/pages/search.js +3 -6
  40. package/dist/services/page.js +5 -1
  41. package/dist/services/post.js +40 -6
  42. package/dist/services/search.js +1 -1
  43. package/dist/ui/compose/ComposeDialog.js +452 -0
  44. package/dist/ui/compose/ComposePrompt.js +55 -0
  45. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  46. package/dist/{theme/components → ui/dash}/PostForm.js +0 -27
  47. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  48. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  49. package/dist/{theme/components → ui/dash}/index.js +3 -6
  50. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  51. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  52. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  53. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  54. package/dist/ui/feed/TimelineFeed.js +41 -0
  55. package/dist/ui/feed/TimelineItem.js +27 -0
  56. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  57. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  58. package/dist/ui/layouts/SiteLayout.js +141 -0
  59. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  60. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  61. package/dist/ui/pages/CollectionsPage.js +76 -0
  62. package/dist/ui/pages/FeaturedPage.js +24 -0
  63. package/dist/ui/pages/HomePage.js +24 -0
  64. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  65. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  66. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  67. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  68. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  69. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  70. package/dist/ui/shared/index.js +5 -0
  71. package/package.json +1 -9
  72. package/src/__tests__/helpers/db.ts +3 -0
  73. package/src/app.tsx +57 -27
  74. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  75. package/src/db/migrations/meta/_journal.json +7 -0
  76. package/src/db/schema.ts +1 -1
  77. package/src/i18n/locales/en.po +332 -181
  78. package/src/i18n/locales/en.ts +1 -1
  79. package/src/i18n/locales/zh-Hans.po +332 -181
  80. package/src/i18n/locales/zh-Hans.ts +1 -1
  81. package/src/i18n/locales/zh-Hant.po +332 -181
  82. package/src/i18n/locales/zh-Hant.ts +1 -1
  83. package/src/index.ts +7 -36
  84. package/src/lib/__tests__/schemas.test.ts +60 -19
  85. package/src/lib/__tests__/timeline.test.ts +45 -81
  86. package/src/lib/__tests__/view.test.ts +13 -7
  87. package/src/lib/constants.ts +1 -0
  88. package/src/lib/nav-reorder.ts +1 -1
  89. package/src/lib/navigation.ts +40 -2
  90. package/src/lib/pagination.ts +50 -0
  91. package/src/lib/render.tsx +7 -14
  92. package/src/lib/schemas.ts +8 -6
  93. package/src/lib/theme.ts +5 -5
  94. package/src/lib/timeline.ts +28 -57
  95. package/src/lib/view.ts +2 -2
  96. package/src/preset.css +2 -1
  97. package/src/routes/__tests__/compose.test.ts +199 -0
  98. package/src/routes/api/__tests__/collections.test.ts +249 -0
  99. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  100. package/src/routes/api/__tests__/pages.test.ts +218 -0
  101. package/src/routes/api/__tests__/settings.test.ts +132 -0
  102. package/src/routes/api/collections.ts +143 -0
  103. package/src/routes/api/nav-items.ts +115 -0
  104. package/src/routes/api/pages.ts +101 -0
  105. package/src/routes/api/posts.ts +2 -2
  106. package/src/routes/api/search.ts +2 -2
  107. package/src/routes/api/settings.ts +91 -0
  108. package/src/routes/compose.ts +63 -0
  109. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  110. package/src/routes/dash/collections.tsx +2 -2
  111. package/src/routes/dash/index.tsx +1 -1
  112. package/src/routes/dash/media.tsx +2 -2
  113. package/src/routes/dash/pages.tsx +443 -70
  114. package/src/routes/dash/posts.tsx +3 -7
  115. package/src/routes/dash/redirects.tsx +2 -2
  116. package/src/routes/dash/settings.tsx +83 -5
  117. package/src/routes/feed/rss.ts +2 -2
  118. package/src/routes/feed/sitemap.ts +1 -1
  119. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  120. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  121. package/src/routes/pages/archive.tsx +2 -6
  122. package/src/routes/pages/collection.tsx +2 -6
  123. package/src/routes/pages/collections.tsx +36 -0
  124. package/src/routes/pages/featured.tsx +38 -0
  125. package/src/routes/pages/home.tsx +9 -55
  126. package/src/routes/pages/page.tsx +28 -30
  127. package/src/routes/pages/post.tsx +2 -5
  128. package/src/routes/pages/search.tsx +2 -6
  129. package/src/services/__tests__/page.test.ts +106 -0
  130. package/src/services/__tests__/post.test.ts +114 -15
  131. package/src/services/page.ts +13 -1
  132. package/src/services/post.ts +57 -7
  133. package/src/services/search.ts +2 -2
  134. package/src/styles/tokens.css +47 -0
  135. package/src/styles/ui.css +491 -0
  136. package/src/types.ts +29 -159
  137. package/src/ui/compose/ComposeDialog.tsx +395 -0
  138. package/src/ui/compose/ComposePrompt.tsx +55 -0
  139. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  140. package/src/{theme/components → ui/dash}/PostForm.tsx +0 -25
  141. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  142. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  143. package/src/ui/dash/index.ts +10 -0
  144. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  145. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  146. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  147. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  148. package/src/ui/feed/TimelineFeed.tsx +49 -0
  149. package/src/ui/feed/TimelineItem.tsx +45 -0
  150. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  151. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  152. package/src/ui/layouts/SiteLayout.tsx +150 -0
  153. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  154. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  155. package/src/ui/pages/CollectionsPage.tsx +73 -0
  156. package/src/ui/pages/FeaturedPage.tsx +31 -0
  157. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  158. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  159. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  160. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  161. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  162. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  163. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  164. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  165. package/src/ui/shared/index.ts +12 -0
  166. package/bin/jant.js +0 -185
  167. package/dist/lib/theme-components.js +0 -46
  168. package/dist/routes/dash/navigation.js +0 -289
  169. package/dist/theme/index.js +0 -18
  170. package/dist/theme/layouts/index.js +0 -2
  171. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  172. package/dist/themes/threads/index.js +0 -81
  173. package/dist/themes/threads/pages/HomePage.js +0 -25
  174. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  175. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  176. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  177. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  178. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  179. package/src/lib/__tests__/theme-components.test.ts +0 -105
  180. package/src/lib/theme-components.ts +0 -65
  181. package/src/routes/dash/navigation.tsx +0 -317
  182. package/src/theme/components/index.ts +0 -23
  183. package/src/theme/index.ts +0 -22
  184. package/src/theme/layouts/index.ts +0 -7
  185. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  186. package/src/themes/threads/index.ts +0 -100
  187. package/src/themes/threads/style.css +0 -336
  188. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  189. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  190. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  191. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  192. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  193. /package/dist/{theme → ui}/color-themes.js +0 -0
  194. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  195. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  196. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  197. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  198. /package/dist/{theme/components → ui/dash}/PageForm.js +0 -0
  199. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  200. /package/src/{theme → ui}/color-themes.ts +0 -0
  201. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  202. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  203. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  204. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  205. /package/src/{theme/components → ui/dash}/PageForm.tsx +0 -0
  206. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -0,0 +1,222 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+ import { navItemsApiRoutes } from "../nav-items.js";
4
+
5
+ describe("Nav Items API Routes", () => {
6
+ describe("GET /api/nav-items", () => {
7
+ it("returns empty list when no nav items exist", async () => {
8
+ const { app } = createTestApp();
9
+ app.route("/api/nav-items", navItemsApiRoutes);
10
+
11
+ const res = await app.request("/api/nav-items");
12
+ expect(res.status).toBe(200);
13
+
14
+ const body = await res.json();
15
+ expect(body.navItems).toEqual([]);
16
+ });
17
+
18
+ it("returns nav items ordered by position", async () => {
19
+ const { app, services } = createTestApp();
20
+ app.route("/api/nav-items", navItemsApiRoutes);
21
+
22
+ await services.navItems.create({
23
+ type: "link",
24
+ label: "Home",
25
+ url: "/",
26
+ });
27
+ await services.navItems.create({
28
+ type: "link",
29
+ label: "Blog",
30
+ url: "/blog",
31
+ });
32
+
33
+ const res = await app.request("/api/nav-items");
34
+ const body = await res.json();
35
+
36
+ expect(body.navItems).toHaveLength(2);
37
+ expect(body.navItems[0].label).toBe("Home");
38
+ expect(body.navItems[1].label).toBe("Blog");
39
+ });
40
+ });
41
+
42
+ describe("POST /api/nav-items", () => {
43
+ it("returns 401 when not authenticated", async () => {
44
+ const { app } = createTestApp({ authenticated: false });
45
+ app.route("/api/nav-items", navItemsApiRoutes);
46
+
47
+ const res = await app.request("/api/nav-items", {
48
+ method: "POST",
49
+ headers: { "Content-Type": "application/json" },
50
+ body: JSON.stringify({
51
+ type: "link",
52
+ label: "Home",
53
+ url: "/",
54
+ }),
55
+ });
56
+
57
+ expect(res.status).toBe(401);
58
+ });
59
+
60
+ it("creates a nav item when authenticated", async () => {
61
+ const { app } = createTestApp({ authenticated: true });
62
+ app.route("/api/nav-items", navItemsApiRoutes);
63
+
64
+ const res = await app.request("/api/nav-items", {
65
+ method: "POST",
66
+ headers: { "Content-Type": "application/json" },
67
+ body: JSON.stringify({
68
+ type: "link",
69
+ label: "GitHub",
70
+ url: "https://github.com",
71
+ }),
72
+ });
73
+
74
+ expect(res.status).toBe(201);
75
+ const body = await res.json();
76
+ expect(body.label).toBe("GitHub");
77
+ expect(body.url).toBe("https://github.com");
78
+ expect(body.type).toBe("link");
79
+ });
80
+
81
+ it("returns 400 for missing required fields", async () => {
82
+ const { app } = createTestApp({ authenticated: true });
83
+ app.route("/api/nav-items", navItemsApiRoutes);
84
+
85
+ const res = await app.request("/api/nav-items", {
86
+ method: "POST",
87
+ headers: { "Content-Type": "application/json" },
88
+ body: JSON.stringify({ type: "link" }),
89
+ });
90
+
91
+ expect(res.status).toBe(400);
92
+ });
93
+ });
94
+
95
+ describe("PUT /api/nav-items/reorder", () => {
96
+ it("returns 401 when not authenticated", async () => {
97
+ const { app } = createTestApp({ authenticated: false });
98
+ app.route("/api/nav-items", navItemsApiRoutes);
99
+
100
+ const res = await app.request("/api/nav-items/reorder", {
101
+ method: "PUT",
102
+ headers: { "Content-Type": "application/json" },
103
+ body: JSON.stringify({ ids: [1, 2] }),
104
+ });
105
+
106
+ expect(res.status).toBe(401);
107
+ });
108
+
109
+ it("reorders nav items when authenticated", async () => {
110
+ const { app, services } = createTestApp({ authenticated: true });
111
+ app.route("/api/nav-items", navItemsApiRoutes);
112
+
113
+ const item1 = await services.navItems.create({
114
+ type: "link",
115
+ label: "First",
116
+ url: "/first",
117
+ });
118
+ const item2 = await services.navItems.create({
119
+ type: "link",
120
+ label: "Second",
121
+ url: "/second",
122
+ });
123
+
124
+ // Reverse order
125
+ const res = await app.request("/api/nav-items/reorder", {
126
+ method: "PUT",
127
+ headers: { "Content-Type": "application/json" },
128
+ body: JSON.stringify({ ids: [item2.id, item1.id] }),
129
+ });
130
+
131
+ expect(res.status).toBe(200);
132
+ const body = await res.json();
133
+ expect(body.navItems[0].label).toBe("Second");
134
+ expect(body.navItems[1].label).toBe("First");
135
+ });
136
+ });
137
+
138
+ describe("PUT /api/nav-items/:id", () => {
139
+ it("updates a nav item when authenticated", async () => {
140
+ const { app, services } = createTestApp({ authenticated: true });
141
+ app.route("/api/nav-items", navItemsApiRoutes);
142
+
143
+ const item = await services.navItems.create({
144
+ type: "link",
145
+ label: "Old Label",
146
+ url: "/old",
147
+ });
148
+
149
+ const res = await app.request(`/api/nav-items/${item.id}`, {
150
+ method: "PUT",
151
+ headers: { "Content-Type": "application/json" },
152
+ body: JSON.stringify({ label: "New Label" }),
153
+ });
154
+
155
+ expect(res.status).toBe(200);
156
+ const body = await res.json();
157
+ expect(body.label).toBe("New Label");
158
+ });
159
+
160
+ it("returns 404 for non-existent item", async () => {
161
+ const { app } = createTestApp({ authenticated: true });
162
+ app.route("/api/nav-items", navItemsApiRoutes);
163
+
164
+ const res = await app.request("/api/nav-items/9999", {
165
+ method: "PUT",
166
+ headers: { "Content-Type": "application/json" },
167
+ body: JSON.stringify({ label: "test" }),
168
+ });
169
+
170
+ expect(res.status).toBe(404);
171
+ });
172
+ });
173
+
174
+ describe("DELETE /api/nav-items/:id", () => {
175
+ it("returns 401 when not authenticated", async () => {
176
+ const { app, services } = createTestApp({ authenticated: false });
177
+ app.route("/api/nav-items", navItemsApiRoutes);
178
+
179
+ const item = await services.navItems.create({
180
+ type: "link",
181
+ label: "Delete Me",
182
+ url: "/delete",
183
+ });
184
+
185
+ const res = await app.request(`/api/nav-items/${item.id}`, {
186
+ method: "DELETE",
187
+ });
188
+
189
+ expect(res.status).toBe(401);
190
+ });
191
+
192
+ it("deletes a nav item when authenticated", async () => {
193
+ const { app, services } = createTestApp({ authenticated: true });
194
+ app.route("/api/nav-items", navItemsApiRoutes);
195
+
196
+ const item = await services.navItems.create({
197
+ type: "link",
198
+ label: "Delete Me",
199
+ url: "/delete",
200
+ });
201
+
202
+ const res = await app.request(`/api/nav-items/${item.id}`, {
203
+ method: "DELETE",
204
+ });
205
+
206
+ expect(res.status).toBe(200);
207
+ const body = await res.json();
208
+ expect(body.success).toBe(true);
209
+ });
210
+
211
+ it("returns 404 for non-existent item", async () => {
212
+ const { app } = createTestApp({ authenticated: true });
213
+ app.route("/api/nav-items", navItemsApiRoutes);
214
+
215
+ const res = await app.request("/api/nav-items/9999", {
216
+ method: "DELETE",
217
+ });
218
+
219
+ expect(res.status).toBe(404);
220
+ });
221
+ });
222
+ });
@@ -0,0 +1,218 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+ import { pagesApiRoutes } from "../pages.js";
4
+
5
+ describe("Pages API Routes", () => {
6
+ describe("GET /api/pages", () => {
7
+ it("returns empty list when no pages exist", async () => {
8
+ const { app } = createTestApp();
9
+ app.route("/api/pages", pagesApiRoutes);
10
+
11
+ const res = await app.request("/api/pages");
12
+ expect(res.status).toBe(200);
13
+
14
+ const body = await res.json();
15
+ expect(body.pages).toEqual([]);
16
+ });
17
+
18
+ it("returns pages list", async () => {
19
+ const { app, services } = createTestApp();
20
+ app.route("/api/pages", pagesApiRoutes);
21
+
22
+ await services.pages.create({ slug: "about", title: "About" });
23
+ await services.pages.create({ slug: "contact", title: "Contact" });
24
+
25
+ const res = await app.request("/api/pages");
26
+ const body = await res.json();
27
+
28
+ expect(body.pages).toHaveLength(2);
29
+ });
30
+ });
31
+
32
+ describe("GET /api/pages/:id", () => {
33
+ it("returns a page by id", async () => {
34
+ const { app, services } = createTestApp();
35
+ app.route("/api/pages", pagesApiRoutes);
36
+
37
+ const page = await services.pages.create({
38
+ slug: "about",
39
+ title: "About Us",
40
+ });
41
+
42
+ const res = await app.request(`/api/pages/${page.id}`);
43
+ expect(res.status).toBe(200);
44
+
45
+ const body = await res.json();
46
+ expect(body.title).toBe("About Us");
47
+ expect(body.slug).toBe("about");
48
+ });
49
+
50
+ it("returns 400 for invalid id", async () => {
51
+ const { app } = createTestApp();
52
+ app.route("/api/pages", pagesApiRoutes);
53
+
54
+ const res = await app.request("/api/pages/abc");
55
+ expect(res.status).toBe(400);
56
+ });
57
+
58
+ it("returns 404 for non-existent page", async () => {
59
+ const { app } = createTestApp();
60
+ app.route("/api/pages", pagesApiRoutes);
61
+
62
+ const res = await app.request("/api/pages/9999");
63
+ expect(res.status).toBe(404);
64
+ });
65
+ });
66
+
67
+ describe("POST /api/pages", () => {
68
+ it("returns 401 when not authenticated", async () => {
69
+ const { app } = createTestApp({ authenticated: false });
70
+ app.route("/api/pages", pagesApiRoutes);
71
+
72
+ const res = await app.request("/api/pages", {
73
+ method: "POST",
74
+ headers: { "Content-Type": "application/json" },
75
+ body: JSON.stringify({ slug: "about", title: "About" }),
76
+ });
77
+
78
+ expect(res.status).toBe(401);
79
+ });
80
+
81
+ it("creates a page when authenticated", async () => {
82
+ const { app } = createTestApp({ authenticated: true });
83
+ app.route("/api/pages", pagesApiRoutes);
84
+
85
+ const res = await app.request("/api/pages", {
86
+ method: "POST",
87
+ headers: { "Content-Type": "application/json" },
88
+ body: JSON.stringify({
89
+ slug: "about",
90
+ title: "About Us",
91
+ body: "We are Jant.",
92
+ status: "published",
93
+ }),
94
+ });
95
+
96
+ expect(res.status).toBe(201);
97
+ const body = await res.json();
98
+ expect(body.slug).toBe("about");
99
+ expect(body.title).toBe("About Us");
100
+ });
101
+
102
+ it("returns 400 for missing slug", async () => {
103
+ const { app } = createTestApp({ authenticated: true });
104
+ app.route("/api/pages", pagesApiRoutes);
105
+
106
+ const res = await app.request("/api/pages", {
107
+ method: "POST",
108
+ headers: { "Content-Type": "application/json" },
109
+ body: JSON.stringify({ title: "No Slug" }),
110
+ });
111
+
112
+ expect(res.status).toBe(400);
113
+ });
114
+ });
115
+
116
+ describe("PUT /api/pages/:id", () => {
117
+ it("returns 401 when not authenticated", async () => {
118
+ const { app, services } = createTestApp({ authenticated: false });
119
+ app.route("/api/pages", pagesApiRoutes);
120
+
121
+ const page = await services.pages.create({
122
+ slug: "about",
123
+ title: "About",
124
+ });
125
+
126
+ const res = await app.request(`/api/pages/${page.id}`, {
127
+ method: "PUT",
128
+ headers: { "Content-Type": "application/json" },
129
+ body: JSON.stringify({ title: "Updated" }),
130
+ });
131
+
132
+ expect(res.status).toBe(401);
133
+ });
134
+
135
+ it("updates a page when authenticated", async () => {
136
+ const { app, services } = createTestApp({ authenticated: true });
137
+ app.route("/api/pages", pagesApiRoutes);
138
+
139
+ const page = await services.pages.create({
140
+ slug: "about",
141
+ title: "About",
142
+ });
143
+
144
+ const res = await app.request(`/api/pages/${page.id}`, {
145
+ method: "PUT",
146
+ headers: { "Content-Type": "application/json" },
147
+ body: JSON.stringify({ title: "Updated About" }),
148
+ });
149
+
150
+ expect(res.status).toBe(200);
151
+ const body = await res.json();
152
+ expect(body.title).toBe("Updated About");
153
+ });
154
+
155
+ it("returns 404 for non-existent page", async () => {
156
+ const { app } = createTestApp({ authenticated: true });
157
+ app.route("/api/pages", pagesApiRoutes);
158
+
159
+ const res = await app.request("/api/pages/9999", {
160
+ method: "PUT",
161
+ headers: { "Content-Type": "application/json" },
162
+ body: JSON.stringify({ title: "test" }),
163
+ });
164
+
165
+ expect(res.status).toBe(404);
166
+ });
167
+ });
168
+
169
+ describe("DELETE /api/pages/:id", () => {
170
+ it("returns 401 when not authenticated", async () => {
171
+ const { app, services } = createTestApp({ authenticated: false });
172
+ app.route("/api/pages", pagesApiRoutes);
173
+
174
+ const page = await services.pages.create({
175
+ slug: "about",
176
+ title: "About",
177
+ });
178
+
179
+ const res = await app.request(`/api/pages/${page.id}`, {
180
+ method: "DELETE",
181
+ });
182
+
183
+ expect(res.status).toBe(401);
184
+ });
185
+
186
+ it("deletes a page when authenticated", async () => {
187
+ const { app, services } = createTestApp({ authenticated: true });
188
+ app.route("/api/pages", pagesApiRoutes);
189
+
190
+ const page = await services.pages.create({
191
+ slug: "about",
192
+ title: "About",
193
+ });
194
+
195
+ const res = await app.request(`/api/pages/${page.id}`, {
196
+ method: "DELETE",
197
+ });
198
+
199
+ expect(res.status).toBe(200);
200
+ const body = await res.json();
201
+ expect(body.success).toBe(true);
202
+
203
+ const found = await services.pages.getById(page.id);
204
+ expect(found).toBeNull();
205
+ });
206
+
207
+ it("returns 404 for non-existent page", async () => {
208
+ const { app } = createTestApp({ authenticated: true });
209
+ app.route("/api/pages", pagesApiRoutes);
210
+
211
+ const res = await app.request("/api/pages/9999", {
212
+ method: "DELETE",
213
+ });
214
+
215
+ expect(res.status).toBe(404);
216
+ });
217
+ });
218
+ });
@@ -0,0 +1,132 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+ import { settingsApiRoutes } from "../settings.js";
4
+
5
+ describe("Settings API Routes", () => {
6
+ describe("GET /api/settings", () => {
7
+ it("returns 401 when not authenticated", async () => {
8
+ const { app } = createTestApp({ authenticated: false });
9
+ app.route("/api/settings", settingsApiRoutes);
10
+
11
+ const res = await app.request("/api/settings");
12
+ expect(res.status).toBe(401);
13
+ });
14
+
15
+ it("returns default settings when none are stored", async () => {
16
+ const { app } = createTestApp({ authenticated: true });
17
+ app.route("/api/settings", settingsApiRoutes);
18
+
19
+ const res = await app.request("/api/settings");
20
+ expect(res.status).toBe(200);
21
+
22
+ const body = await res.json();
23
+ expect(body.settings).toBeDefined();
24
+ expect(body.settings.SITE_NAME).toBe("Jant");
25
+ expect(body.settings.SITE_DESCRIPTION).toBe(
26
+ "A microblog powered by Jant",
27
+ );
28
+ expect(body.settings.SITE_LANGUAGE).toBe("en");
29
+ });
30
+
31
+ it("returns stored settings overriding defaults", async () => {
32
+ const { app, services } = createTestApp({ authenticated: true });
33
+ app.route("/api/settings", settingsApiRoutes);
34
+
35
+ await services.settings.set("SITE_NAME" as never, "My Blog");
36
+
37
+ const res = await app.request("/api/settings");
38
+ const body = await res.json();
39
+
40
+ expect(body.settings.SITE_NAME).toBe("My Blog");
41
+ });
42
+
43
+ it("does not include env-only settings", async () => {
44
+ const { app } = createTestApp({ authenticated: true });
45
+ app.route("/api/settings", settingsApiRoutes);
46
+
47
+ const res = await app.request("/api/settings");
48
+ const body = await res.json();
49
+
50
+ // Env-only keys should not be in the response
51
+ expect(body.settings.AUTH_SECRET).toBeUndefined();
52
+ expect(body.settings.SITE_URL).toBeUndefined();
53
+ });
54
+ });
55
+
56
+ describe("PUT /api/settings", () => {
57
+ it("returns 401 when not authenticated", async () => {
58
+ const { app } = createTestApp({ authenticated: false });
59
+ app.route("/api/settings", settingsApiRoutes);
60
+
61
+ const res = await app.request("/api/settings", {
62
+ method: "PUT",
63
+ headers: { "Content-Type": "application/json" },
64
+ body: JSON.stringify({ SITE_NAME: "New Name" }),
65
+ });
66
+
67
+ expect(res.status).toBe(401);
68
+ });
69
+
70
+ it("updates editable settings", async () => {
71
+ const { app } = createTestApp({ authenticated: true });
72
+ app.route("/api/settings", settingsApiRoutes);
73
+
74
+ const res = await app.request("/api/settings", {
75
+ method: "PUT",
76
+ headers: { "Content-Type": "application/json" },
77
+ body: JSON.stringify({ SITE_NAME: "Updated Blog" }),
78
+ });
79
+
80
+ expect(res.status).toBe(200);
81
+ const body = await res.json();
82
+ expect(body.settings.SITE_NAME).toBe("Updated Blog");
83
+ });
84
+
85
+ it("rejects env-only keys", async () => {
86
+ const { app } = createTestApp({ authenticated: true });
87
+ app.route("/api/settings", settingsApiRoutes);
88
+
89
+ const res = await app.request("/api/settings", {
90
+ method: "PUT",
91
+ headers: { "Content-Type": "application/json" },
92
+ body: JSON.stringify({ AUTH_SECRET: "should-not-work" }),
93
+ });
94
+
95
+ expect(res.status).toBe(400);
96
+ const body = await res.json();
97
+ expect(body.rejectedKeys).toContain("AUTH_SECRET");
98
+ });
99
+
100
+ it("partially applies when mixing editable and env-only keys", async () => {
101
+ const { app } = createTestApp({ authenticated: true });
102
+ app.route("/api/settings", settingsApiRoutes);
103
+
104
+ const res = await app.request("/api/settings", {
105
+ method: "PUT",
106
+ headers: { "Content-Type": "application/json" },
107
+ body: JSON.stringify({
108
+ SITE_NAME: "Mixed Update",
109
+ AUTH_SECRET: "ignored",
110
+ }),
111
+ });
112
+
113
+ expect(res.status).toBe(200);
114
+ const body = await res.json();
115
+ expect(body.settings.SITE_NAME).toBe("Mixed Update");
116
+ expect(body.rejectedKeys).toContain("AUTH_SECRET");
117
+ });
118
+
119
+ it("returns 400 for invalid body", async () => {
120
+ const { app } = createTestApp({ authenticated: true });
121
+ app.route("/api/settings", settingsApiRoutes);
122
+
123
+ const res = await app.request("/api/settings", {
124
+ method: "PUT",
125
+ headers: { "Content-Type": "application/json" },
126
+ body: JSON.stringify("not an object"),
127
+ });
128
+
129
+ expect(res.status).toBe(400);
130
+ });
131
+ });
132
+ });