WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

test(web): add failing tests for category proxy routes (ATB-47)

Malpercio f768317e 6cc0056b

+310
+310
apps/web/src/routes/__tests__/admin.test.tsx
··· 996 996 expect(res.headers.get("location")).toBe("/login"); 997 997 }); 998 998 }); 999 + 1000 + describe("createAdminRoutes — POST /admin/structure/categories", () => { 1001 + beforeEach(() => { 1002 + vi.stubGlobal("fetch", mockFetch); 1003 + vi.stubEnv("APPVIEW_URL", "http://localhost:3000"); 1004 + vi.resetModules(); 1005 + }); 1006 + 1007 + afterEach(() => { 1008 + vi.unstubAllGlobals(); 1009 + vi.unstubAllEnvs(); 1010 + mockFetch.mockReset(); 1011 + }); 1012 + 1013 + function mockResponse(body: unknown, ok = true, status = 200) { 1014 + return { ok, status, statusText: ok ? "OK" : "Error", json: () => Promise.resolve(body) }; 1015 + } 1016 + 1017 + function setupSession(permissions: string[]) { 1018 + mockFetch.mockResolvedValueOnce( 1019 + mockResponse({ authenticated: true, did: "did:plc:admin", handle: "admin.bsky.social" }) 1020 + ); 1021 + mockFetch.mockResolvedValueOnce(mockResponse({ permissions })); 1022 + } 1023 + 1024 + async function loadAdminRoutes() { 1025 + const { createAdminRoutes } = await import("../admin.js"); 1026 + return createAdminRoutes("http://localhost:3000"); 1027 + } 1028 + 1029 + function postForm(body: Record<string, string>) { 1030 + const params = new URLSearchParams(body); 1031 + return { 1032 + method: "POST", 1033 + headers: { 1034 + cookie: "atbb_session=token", 1035 + "content-type": "application/x-www-form-urlencoded", 1036 + }, 1037 + body: params.toString(), 1038 + }; 1039 + } 1040 + 1041 + it("redirects to /login when unauthenticated", async () => { 1042 + mockFetch.mockResolvedValueOnce(mockResponse({ authenticated: false })); 1043 + const routes = await loadAdminRoutes(); 1044 + const res = await routes.request("/admin/structure/categories", postForm({ name: "General" })); 1045 + expect(res.status).toBe(302); 1046 + expect(res.headers.get("location")).toBe("/login"); 1047 + }); 1048 + 1049 + it("returns 403 without manageCategories permission", async () => { 1050 + setupSession(["space.atbb.permission.manageMembers"]); 1051 + const routes = await loadAdminRoutes(); 1052 + const res = await routes.request("/admin/structure/categories", postForm({ name: "General" })); 1053 + expect(res.status).toBe(403); 1054 + }); 1055 + 1056 + it("redirects to /admin/structure on success", async () => { 1057 + setupSession(["space.atbb.permission.manageCategories"]); 1058 + mockFetch.mockResolvedValueOnce( 1059 + mockResponse({ uri: "at://did:plc:forum/space.atbb.forum.category/abc", cid: "bafyrei..." }, true, 201) 1060 + ); 1061 + 1062 + const routes = await loadAdminRoutes(); 1063 + const res = await routes.request( 1064 + "/admin/structure/categories", 1065 + postForm({ name: "General", description: "Talk about anything", sortOrder: "1" }) 1066 + ); 1067 + 1068 + expect(res.status).toBe(302); 1069 + expect(res.headers.get("location")).toBe("/admin/structure"); 1070 + }); 1071 + 1072 + it("redirects with ?error= when name is missing", async () => { 1073 + setupSession(["space.atbb.permission.manageCategories"]); 1074 + 1075 + const routes = await loadAdminRoutes(); 1076 + const res = await routes.request( 1077 + "/admin/structure/categories", 1078 + postForm({ name: "" }) 1079 + ); 1080 + 1081 + expect(res.status).toBe(302); 1082 + const location = res.headers.get("location") ?? ""; 1083 + expect(location).toContain("/admin/structure"); 1084 + expect(location).toContain("error="); 1085 + }); 1086 + 1087 + it("redirects with ?error= on AppView error", async () => { 1088 + setupSession(["space.atbb.permission.manageCategories"]); 1089 + mockFetch.mockResolvedValueOnce( 1090 + mockResponse({ error: "Unexpected error" }, false, 500) 1091 + ); 1092 + 1093 + const routes = await loadAdminRoutes(); 1094 + const res = await routes.request( 1095 + "/admin/structure/categories", 1096 + postForm({ name: "General" }) 1097 + ); 1098 + 1099 + expect(res.status).toBe(302); 1100 + const location = res.headers.get("location") ?? ""; 1101 + expect(location).toContain("/admin/structure"); 1102 + expect(location).toContain("error="); 1103 + }); 1104 + 1105 + it("redirects with ?error= on network error", async () => { 1106 + setupSession(["space.atbb.permission.manageCategories"]); 1107 + mockFetch.mockRejectedValueOnce(new Error("fetch failed")); 1108 + 1109 + const routes = await loadAdminRoutes(); 1110 + const res = await routes.request( 1111 + "/admin/structure/categories", 1112 + postForm({ name: "General" }) 1113 + ); 1114 + 1115 + expect(res.status).toBe(302); 1116 + const location = res.headers.get("location") ?? ""; 1117 + expect(location).toContain("/admin/structure"); 1118 + expect(location).toContain("error="); 1119 + }); 1120 + }); 1121 + 1122 + describe("createAdminRoutes — POST /admin/structure/categories/:id/edit", () => { 1123 + beforeEach(() => { 1124 + vi.stubGlobal("fetch", mockFetch); 1125 + vi.stubEnv("APPVIEW_URL", "http://localhost:3000"); 1126 + vi.resetModules(); 1127 + }); 1128 + 1129 + afterEach(() => { 1130 + vi.unstubAllGlobals(); 1131 + vi.unstubAllEnvs(); 1132 + mockFetch.mockReset(); 1133 + }); 1134 + 1135 + function mockResponse(body: unknown, ok = true, status = 200) { 1136 + return { ok, status, statusText: ok ? "OK" : "Error", json: () => Promise.resolve(body) }; 1137 + } 1138 + 1139 + function setupSession(permissions: string[]) { 1140 + mockFetch.mockResolvedValueOnce( 1141 + mockResponse({ authenticated: true, did: "did:plc:admin", handle: "admin.bsky.social" }) 1142 + ); 1143 + mockFetch.mockResolvedValueOnce(mockResponse({ permissions })); 1144 + } 1145 + 1146 + async function loadAdminRoutes() { 1147 + const { createAdminRoutes } = await import("../admin.js"); 1148 + return createAdminRoutes("http://localhost:3000"); 1149 + } 1150 + 1151 + function postForm(body: Record<string, string>) { 1152 + const params = new URLSearchParams(body); 1153 + return { 1154 + method: "POST", 1155 + headers: { 1156 + cookie: "atbb_session=token", 1157 + "content-type": "application/x-www-form-urlencoded", 1158 + }, 1159 + body: params.toString(), 1160 + }; 1161 + } 1162 + 1163 + it("redirects to /login when unauthenticated", async () => { 1164 + mockFetch.mockResolvedValueOnce(mockResponse({ authenticated: false })); 1165 + const routes = await loadAdminRoutes(); 1166 + const res = await routes.request("/admin/structure/categories/5/edit", postForm({ name: "Updated" })); 1167 + expect(res.status).toBe(302); 1168 + expect(res.headers.get("location")).toBe("/login"); 1169 + }); 1170 + 1171 + it("returns 403 without manageCategories", async () => { 1172 + setupSession(["space.atbb.permission.manageMembers"]); 1173 + const routes = await loadAdminRoutes(); 1174 + const res = await routes.request("/admin/structure/categories/5/edit", postForm({ name: "Updated" })); 1175 + expect(res.status).toBe(403); 1176 + }); 1177 + 1178 + it("redirects to /admin/structure on success", async () => { 1179 + setupSession(["space.atbb.permission.manageCategories"]); 1180 + mockFetch.mockResolvedValueOnce( 1181 + mockResponse({ uri: "at://...", cid: "bafyrei..." }, true, 200) 1182 + ); 1183 + 1184 + const routes = await loadAdminRoutes(); 1185 + const res = await routes.request( 1186 + "/admin/structure/categories/5/edit", 1187 + postForm({ name: "Updated Name", description: "", sortOrder: "2" }) 1188 + ); 1189 + 1190 + expect(res.status).toBe(302); 1191 + expect(res.headers.get("location")).toBe("/admin/structure"); 1192 + }); 1193 + 1194 + it("redirects with ?error= when name is missing", async () => { 1195 + setupSession(["space.atbb.permission.manageCategories"]); 1196 + const routes = await loadAdminRoutes(); 1197 + const res = await routes.request("/admin/structure/categories/5/edit", postForm({ name: "" })); 1198 + expect(res.status).toBe(302); 1199 + const location = res.headers.get("location") ?? ""; 1200 + expect(location).toContain("error="); 1201 + }); 1202 + 1203 + it("redirects with ?error= on AppView error", async () => { 1204 + setupSession(["space.atbb.permission.manageCategories"]); 1205 + mockFetch.mockResolvedValueOnce(mockResponse({ error: "Not found" }, false, 404)); 1206 + const routes = await loadAdminRoutes(); 1207 + const res = await routes.request("/admin/structure/categories/5/edit", postForm({ name: "Updated" })); 1208 + expect(res.status).toBe(302); 1209 + const location = res.headers.get("location") ?? ""; 1210 + expect(location).toContain("error="); 1211 + }); 1212 + }); 1213 + 1214 + describe("createAdminRoutes — POST /admin/structure/categories/:id/delete", () => { 1215 + beforeEach(() => { 1216 + vi.stubGlobal("fetch", mockFetch); 1217 + vi.stubEnv("APPVIEW_URL", "http://localhost:3000"); 1218 + vi.resetModules(); 1219 + }); 1220 + 1221 + afterEach(() => { 1222 + vi.unstubAllGlobals(); 1223 + vi.unstubAllEnvs(); 1224 + mockFetch.mockReset(); 1225 + }); 1226 + 1227 + function mockResponse(body: unknown, ok = true, status = 200) { 1228 + return { ok, status, statusText: ok ? "OK" : "Error", json: () => Promise.resolve(body) }; 1229 + } 1230 + 1231 + function setupSession(permissions: string[]) { 1232 + mockFetch.mockResolvedValueOnce( 1233 + mockResponse({ authenticated: true, did: "did:plc:admin", handle: "admin.bsky.social" }) 1234 + ); 1235 + mockFetch.mockResolvedValueOnce(mockResponse({ permissions })); 1236 + } 1237 + 1238 + async function loadAdminRoutes() { 1239 + const { createAdminRoutes } = await import("../admin.js"); 1240 + return createAdminRoutes("http://localhost:3000"); 1241 + } 1242 + 1243 + function postForm(body: Record<string, string> = {}) { 1244 + const params = new URLSearchParams(body); 1245 + return { 1246 + method: "POST", 1247 + headers: { 1248 + cookie: "atbb_session=token", 1249 + "content-type": "application/x-www-form-urlencoded", 1250 + }, 1251 + body: params.toString(), 1252 + }; 1253 + } 1254 + 1255 + it("redirects to /login when unauthenticated", async () => { 1256 + mockFetch.mockResolvedValueOnce(mockResponse({ authenticated: false })); 1257 + const routes = await loadAdminRoutes(); 1258 + const res = await routes.request("/admin/structure/categories/5/delete", postForm()); 1259 + expect(res.status).toBe(302); 1260 + expect(res.headers.get("location")).toBe("/login"); 1261 + }); 1262 + 1263 + it("returns 403 without manageCategories", async () => { 1264 + setupSession(["space.atbb.permission.manageMembers"]); 1265 + const routes = await loadAdminRoutes(); 1266 + const res = await routes.request("/admin/structure/categories/5/delete", postForm()); 1267 + expect(res.status).toBe(403); 1268 + }); 1269 + 1270 + it("redirects to /admin/structure on success", async () => { 1271 + setupSession(["space.atbb.permission.manageCategories"]); 1272 + mockFetch.mockResolvedValueOnce(mockResponse({}, true, 200)); 1273 + 1274 + const routes = await loadAdminRoutes(); 1275 + const res = await routes.request("/admin/structure/categories/5/delete", postForm()); 1276 + 1277 + expect(res.status).toBe(302); 1278 + expect(res.headers.get("location")).toBe("/admin/structure"); 1279 + }); 1280 + 1281 + it("redirects with ?error= on AppView error (e.g. 409 has boards)", async () => { 1282 + setupSession(["space.atbb.permission.manageCategories"]); 1283 + mockFetch.mockResolvedValueOnce( 1284 + mockResponse({ error: "Cannot delete category with boards. Remove all boards first." }, false, 409) 1285 + ); 1286 + 1287 + const routes = await loadAdminRoutes(); 1288 + const res = await routes.request("/admin/structure/categories/5/delete", postForm()); 1289 + 1290 + expect(res.status).toBe(302); 1291 + const location = res.headers.get("location") ?? ""; 1292 + expect(location).toContain("/admin/structure"); 1293 + expect(location).toContain("error="); 1294 + expect(decodeURIComponent(location)).toContain("Cannot delete category with boards"); 1295 + }); 1296 + 1297 + it("redirects with ?error= on network error", async () => { 1298 + setupSession(["space.atbb.permission.manageCategories"]); 1299 + mockFetch.mockRejectedValueOnce(new Error("fetch failed")); 1300 + 1301 + const routes = await loadAdminRoutes(); 1302 + const res = await routes.request("/admin/structure/categories/5/delete", postForm()); 1303 + 1304 + expect(res.status).toBe(302); 1305 + const location = res.headers.get("location") ?? ""; 1306 + expect(location).toContain("error="); 1307 + }); 1308 + });