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
1import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2import { Hono } from "hono";
3import { createTestContext, type TestContext } from "../../lib/__tests__/test-context.js";
4import type { Variables } from "../../types.js";
5import { posts, users, modActions } from "@atbb/db";
6import { TID } from "@atproto/common-web";
7import { SpaceAtbbPost as Post } from "@atbb/lexicon";
8
9// Mock auth and permission middleware at the module level
10let mockPutRecord: ReturnType<typeof vi.fn>;
11let mockUser: any;
12
13vi.mock("../../middleware/auth.js", () => ({
14 requireAuth: vi.fn(() => async (c: any, next: any) => {
15 c.set("user", mockUser);
16 await next();
17 }),
18}));
19
20vi.mock("../../middleware/permissions.js", async (importOriginal) => {
21 const actual = await importOriginal<typeof import("../../middleware/permissions.js")>();
22 return {
23 ...actual, // Keep requireNotBanned real so ban enforcement tests work
24 requirePermission: vi.fn(() => async (c: any, next: any) => {
25 await next();
26 }),
27 };
28});
29
30// Import after mocking
31const { createPostsRoutes } = await import("../posts.js");
32
33describe("POST /api/posts", () => {
34 let ctx: TestContext;
35 let app: Hono<{ Variables: Variables }>;
36 let topicId: string;
37 let replyId: string;
38
39 beforeEach(async () => {
40 ctx = await createTestContext();
41
42 // Insert test user
43 await ctx.db.insert(users).values({
44 did: "did:plc:test-user",
45 handle: "testuser.test",
46 indexedAt: new Date(),
47 });
48
49 // Insert topic (root post) and get its ID
50 const [topicPost] = await ctx.db
51 .insert(posts)
52 .values({
53 did: "did:plc:test-user",
54 rkey: "3lbk7topic",
55 cid: "bafytopic",
56 text: "Topic post",
57 forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`,
58 createdAt: new Date(),
59 indexedAt: new Date(),
60 })
61 .returning({ id: posts.id });
62
63 // Store topic ID for tests
64 topicId = topicPost.id.toString();
65
66 // Insert reply and get its ID
67 const [replyPost] = await ctx.db
68 .insert(posts)
69 .values({
70 did: "did:plc:test-user",
71 rkey: "3lbk8reply",
72 cid: "bafyreply",
73 text: "Reply post",
74 forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`,
75 rootPostId: topicPost.id,
76 parentPostId: topicPost.id,
77 rootUri: "at://did:plc:test-user/space.atbb.post/3lbk7topic",
78 parentUri: "at://did:plc:test-user/space.atbb.post/3lbk7topic",
79 createdAt: new Date(),
80 indexedAt: new Date(),
81 })
82 .returning({ id: posts.id });
83
84 // Store reply ID for tests
85 replyId = replyPost.id.toString();
86
87 // Mock putRecord to track calls
88 mockPutRecord = vi.fn(async () => ({
89 data: {
90 uri: "at://did:plc:test-user/space.atbb.post/3lbk9test",
91 cid: "bafytest",
92 },
93 }));
94
95 // Set up mock user for auth middleware
96 mockUser = {
97 did: "did:plc:test-user",
98 handle: "testuser.test",
99 pdsUrl: "https://test.pds",
100 agent: {
101 com: {
102 atproto: {
103 repo: {
104 putRecord: mockPutRecord,
105 },
106 },
107 },
108 },
109 };
110
111 app = new Hono<{ Variables: Variables }>();
112 app.route("/api/posts", createPostsRoutes(ctx));
113 });
114
115 afterEach(async () => {
116 await ctx.cleanup();
117 });
118
119 it("creates reply to topic", async () => {
120 const res = await app.request("/api/posts", {
121 method: "POST",
122 headers: { "Content-Type": "application/json" },
123 body: JSON.stringify({
124 text: "My reply",
125 rootPostId: topicId,
126 parentPostId: topicId,
127 }),
128 });
129
130 expect(res.status).toBe(201);
131 const data = await res.json();
132 expect(data.uri).toBeTruthy();
133 expect(data.cid).toBeTruthy();
134 expect(data.rkey).toBeTruthy();
135
136 // Verify the reply ref written to PDS passes Post.isReplyRef() — the actual
137 // contract the indexer uses. A string literal check on $type would pass even
138 // with a typo that still breaks the indexer's runtime guard.
139 const putCall = mockPutRecord.mock.calls[0][0];
140 expect(Post.isReplyRef(putCall.record.reply)).toBe(true);
141 expect(putCall.record.reply.root.uri).toMatch(/^at:\/\/did:plc:.*\/space\.atbb\.post\//);
142 expect(putCall.record.reply.parent.uri).toMatch(/^at:\/\/did:plc:.*\/space\.atbb\.post\//);
143 });
144
145 it("creates reply to reply", async () => {
146 const res = await app.request("/api/posts", {
147 method: "POST",
148 headers: { "Content-Type": "application/json" },
149 body: JSON.stringify({
150 text: "Nested reply",
151 rootPostId: topicId,
152 parentPostId: replyId, // Reply to the reply
153 }),
154 });
155
156 expect(res.status).toBe(201);
157
158 // Same isReplyRef contract check as the direct-reply case — nested replies
159 // use the same construction path and must also include $type.
160 const putCall = mockPutRecord.mock.calls[0][0];
161 expect(Post.isReplyRef(putCall.record.reply)).toBe(true);
162 });
163
164 it("returns 400 for invalid parent ID format", async () => {
165 const res = await app.request("/api/posts", {
166 method: "POST",
167 headers: { "Content-Type": "application/json" },
168 body: JSON.stringify({
169 text: "Test",
170 rootPostId: "not-a-number",
171 parentPostId: "1",
172 }),
173 });
174
175 expect(res.status).toBe(400);
176 const data = await res.json();
177 expect(data.error).toContain("Invalid");
178 });
179
180 it("returns 404 when root post does not exist", async () => {
181 const res = await app.request("/api/posts", {
182 method: "POST",
183 headers: { "Content-Type": "application/json" },
184 body: JSON.stringify({
185 text: "Test",
186 rootPostId: "999",
187 parentPostId: "999",
188 }),
189 });
190
191 expect(res.status).toBe(404);
192 const data = await res.json();
193 expect(data.error).toContain("not found");
194 });
195
196 it("returns 404 when parent post does not exist", async () => {
197 const res = await app.request("/api/posts", {
198 method: "POST",
199 headers: { "Content-Type": "application/json" },
200 body: JSON.stringify({
201 text: "Test",
202 rootPostId: topicId,
203 parentPostId: "999",
204 }),
205 });
206
207 expect(res.status).toBe(404);
208 const data = await res.json();
209 expect(data.error).toContain("not found");
210 });
211
212 it("returns 400 when parent belongs to different thread", async () => {
213 // Insert a different topic and get its ID
214 const [otherTopic] = await ctx.db
215 .insert(posts)
216 .values({
217 did: "did:plc:test-user",
218 rkey: "3lbkaother",
219 cid: "bafyother",
220 text: "Other topic",
221 forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`,
222 createdAt: new Date(),
223 indexedAt: new Date(),
224 })
225 .returning({ id: posts.id });
226
227 const res = await app.request("/api/posts", {
228 method: "POST",
229 headers: { "Content-Type": "application/json" },
230 body: JSON.stringify({
231 text: "Test",
232 rootPostId: topicId,
233 parentPostId: otherTopic.id.toString(), // Different thread
234 }),
235 });
236
237 expect(res.status).toBe(400);
238 const data = await res.json();
239 expect(data.error).toContain("thread");
240 });
241
242 // Critical Issue #1: Test type guard for validatePostText
243 it("returns 400 when text is missing", async () => {
244 const res = await app.request("/api/posts", {
245 method: "POST",
246 headers: { "Content-Type": "application/json" },
247 body: JSON.stringify({
248 rootPostId: topicId,
249 parentPostId: topicId,
250 }), // No text field
251 });
252
253 expect(res.status).toBe(400);
254 const data = await res.json();
255 expect(data.error).toContain("Text is required");
256 });
257
258 it("returns 400 for non-string text (array)", async () => {
259 const res = await app.request("/api/posts", {
260 method: "POST",
261 headers: { "Content-Type": "application/json" },
262 body: JSON.stringify({
263 text: ["not", "a", "string"],
264 rootPostId: topicId,
265 parentPostId: topicId,
266 }),
267 });
268
269 expect(res.status).toBe(400);
270 const data = await res.json();
271 expect(data.error).toContain("must be a string");
272 });
273
274 // Critical Issue #2: Test malformed JSON handling
275 it("returns 400 for malformed JSON", async () => {
276 const res = await app.request("/api/posts", {
277 method: "POST",
278 headers: { "Content-Type": "application/json" },
279 body: '{"text": "incomplete',
280 });
281
282 expect(res.status).toBe(400);
283 const data = await res.json();
284 expect(data.error).toContain("Invalid JSON");
285 });
286
287 // Critical test coverage: PDS network errors (503)
288 it("returns 503 when PDS connection fails (network error)", async () => {
289 mockPutRecord.mockRejectedValueOnce(new Error("Network request failed"));
290
291 const res = await app.request("/api/posts", {
292 method: "POST",
293 headers: { "Content-Type": "application/json" },
294 body: JSON.stringify({
295 text: "Test reply",
296 rootPostId: topicId,
297 parentPostId: topicId,
298 }),
299 });
300
301 expect(res.status).toBe(503);
302 const data = await res.json();
303 expect(data.error).toContain("Unable to reach external service");
304 });
305
306 it("returns 503 when DNS resolution fails (ENOTFOUND)", async () => {
307 mockPutRecord.mockRejectedValueOnce(new Error("getaddrinfo ENOTFOUND"));
308
309 const res = await app.request("/api/posts", {
310 method: "POST",
311 headers: { "Content-Type": "application/json" },
312 body: JSON.stringify({
313 text: "Test reply",
314 rootPostId: topicId,
315 parentPostId: topicId,
316 }),
317 });
318
319 expect(res.status).toBe(503);
320 const data = await res.json();
321 expect(data.error).toContain("Unable to reach external service");
322 });
323
324 it("returns 503 when request times out", async () => {
325 mockPutRecord.mockRejectedValueOnce(new Error("timeout of 5000ms exceeded"));
326
327 const res = await app.request("/api/posts", {
328 method: "POST",
329 headers: { "Content-Type": "application/json" },
330 body: JSON.stringify({
331 text: "Test reply",
332 rootPostId: topicId,
333 parentPostId: topicId,
334 }),
335 });
336
337 expect(res.status).toBe(503);
338 const data = await res.json();
339 expect(data.error).toContain("Unable to reach external service");
340 });
341
342 // Critical test coverage: PDS server errors (500)
343 it("returns 500 when PDS returns server error", async () => {
344 mockPutRecord.mockRejectedValueOnce(new Error("PDS internal error"));
345
346 const res = await app.request("/api/posts", {
347 method: "POST",
348 headers: { "Content-Type": "application/json" },
349 body: JSON.stringify({
350 text: "Test reply",
351 rootPostId: topicId,
352 parentPostId: topicId,
353 }),
354 });
355
356 expect(res.status).toBe(500);
357 const data = await res.json();
358 expect(data.error).toContain("Failed to create post");
359 });
360
361 it("returns 500 for unexpected errors", async () => {
362 mockPutRecord.mockRejectedValueOnce(new Error("Unexpected error occurred"));
363
364 const res = await app.request("/api/posts", {
365 method: "POST",
366 headers: { "Content-Type": "application/json" },
367 body: JSON.stringify({
368 text: "Test reply",
369 rootPostId: topicId,
370 parentPostId: topicId,
371 }),
372 });
373
374 expect(res.status).toBe(500);
375 const data = await res.json();
376 expect(data.error).toContain("Failed to create post");
377 });
378
379 it("returns 503 when database is unavailable during post creation", async () => {
380 const helpers = await import("../helpers.js");
381 const getPostsByIdsSpy = vi.spyOn(helpers, "getPostsByIds");
382 getPostsByIdsSpy.mockRejectedValueOnce(new Error("Database connection lost"));
383
384 const res = await app.request("/api/posts", {
385 method: "POST",
386 headers: { "Content-Type": "application/json" },
387 body: JSON.stringify({
388 text: "Test reply",
389 rootPostId: topicId,
390 parentPostId: topicId,
391 }),
392 });
393
394 expect(res.status).toBe(503);
395 const data = await res.json();
396 expect(data.error).toBe("Database temporarily unavailable. Please try again later.");
397
398 getPostsByIdsSpy.mockRestore();
399 });
400});
401
402describe("POST /api/posts - ban enforcement", () => {
403 let ctx: TestContext;
404 let app: Hono<{ Variables: Variables }>;
405 let topicId: string;
406
407 beforeEach(async () => {
408 ctx = await createTestContext();
409
410 // Insert test user (use onConflictDoNothing in case tests share users)
411 await ctx.db.insert(users).values({
412 did: "did:plc:ban-test-user",
413 handle: "bantestuser.test",
414 indexedAt: new Date(),
415 }).onConflictDoNothing();
416
417 // Insert topic (root post) with unique rkey
418 const banTopicRkey = TID.nextStr();
419 const [topicPost] = await ctx.db
420 .insert(posts)
421 .values({
422 did: "did:plc:ban-test-user",
423 rkey: banTopicRkey,
424 cid: `bafy${banTopicRkey}`,
425 text: "Topic for ban tests",
426 forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`,
427 createdAt: new Date(),
428 indexedAt: new Date(),
429 })
430 .returning({ id: posts.id });
431
432 topicId = topicPost.id.toString();
433
434 // Set up mock user
435 mockUser = {
436 did: "did:plc:ban-test-user",
437 handle: "bantestuser.test",
438 pdsUrl: "https://test.pds",
439 agent: {
440 com: {
441 atproto: {
442 repo: {
443 putRecord: vi.fn(async () => ({
444 data: {
445 uri: "at://did:plc:ban-test-user/space.atbb.post/3lbkbanreply",
446 cid: "bafybanreply",
447 },
448 })),
449 },
450 },
451 },
452 },
453 };
454
455 app = new Hono<{ Variables: Variables }>();
456 app.route("/api/posts", createPostsRoutes(ctx));
457 });
458
459 afterEach(async () => {
460 await ctx.cleanup();
461 });
462
463 it("allows non-banned user to create reply", async () => {
464 const res = await app.request("/api/posts", {
465 method: "POST",
466 headers: { "Content-Type": "application/json" },
467 body: JSON.stringify({
468 text: "Reply from non-banned user",
469 rootPostId: topicId,
470 parentPostId: topicId,
471 }),
472 });
473
474 expect(res.status).toBe(201);
475 const data = await res.json();
476 expect(data.uri).toBeDefined();
477 });
478
479 it("blocks banned user from creating reply", async () => {
480 // Ban the user
481 const banRkey = TID.nextStr();
482 await ctx.db.insert(modActions).values({
483 did: ctx.config.forumDid,
484 rkey: banRkey,
485 cid: `bafy${banRkey}`,
486 action: "space.atbb.modAction.ban",
487 subjectDid: mockUser.did,
488 createdBy: "did:plc:admin",
489 createdAt: new Date(),
490 indexedAt: new Date(),
491 });
492
493 const res = await app.request("/api/posts", {
494 method: "POST",
495 headers: { "Content-Type": "application/json" },
496 body: JSON.stringify({
497 text: "Reply from banned user",
498 rootPostId: topicId,
499 parentPostId: topicId,
500 }),
501 });
502
503 expect(res.status).toBe(403);
504 const data = await res.json();
505 expect(data.error).toBe("You are banned from this forum");
506 });
507
508 it("returns 503 when ban check fails with database error", async () => {
509 const consoleErrorSpy = vi.spyOn(ctx.logger, "error").mockImplementation(() => {});
510
511 const helpers = await import("../helpers.js");
512 const getActiveBansSpy = vi.spyOn(helpers, "getActiveBans");
513 getActiveBansSpy.mockRejectedValueOnce(new Error("Database connection lost"));
514
515 const res = await app.request("/api/posts", {
516 method: "POST",
517 headers: { "Content-Type": "application/json" },
518 body: JSON.stringify({
519 text: "Reply attempt during DB error",
520 rootPostId: topicId,
521 parentPostId: topicId,
522 }),
523 });
524
525 expect(res.status).toBe(503);
526 const data = await res.json();
527 expect(data.error).toBe("Database temporarily unavailable. Please try again later.");
528
529 expect(consoleErrorSpy).toHaveBeenCalledWith(
530 "Unable to verify ban status",
531 expect.objectContaining({
532 operation: "POST /api/posts - ban check",
533 userId: mockUser.did,
534 error: "Database connection lost",
535 })
536 );
537
538 consoleErrorSpy.mockRestore();
539 getActiveBansSpy.mockRestore();
540 });
541
542 it("returns 500 when ban check fails with unexpected error (fail closed)", async () => {
543 const consoleErrorSpy = vi.spyOn(ctx.logger, "error").mockImplementation(() => {});
544
545 const helpers = await import("../helpers.js");
546 const getActiveBansSpy = vi.spyOn(helpers, "getActiveBans");
547 getActiveBansSpy.mockRejectedValueOnce(new Error("Unexpected internal error"));
548
549 const res = await app.request("/api/posts", {
550 method: "POST",
551 headers: { "Content-Type": "application/json" },
552 body: JSON.stringify({
553 text: "Reply attempt during unexpected error",
554 rootPostId: topicId,
555 parentPostId: topicId,
556 }),
557 });
558
559 expect(res.status).toBe(500);
560 const data = await res.json();
561 expect(data.error).toBe("Unable to verify ban status. Please contact support if this persists.");
562
563 expect(consoleErrorSpy).toHaveBeenCalledWith(
564 "Unable to verify ban status",
565 expect.objectContaining({
566 operation: "POST /api/posts - ban check",
567 userId: mockUser.did,
568 error: "Unexpected internal error",
569 })
570 );
571
572 consoleErrorSpy.mockRestore();
573 getActiveBansSpy.mockRestore();
574 });
575
576 it("re-throws programming errors from ban check", async () => {
577 const consoleErrorSpy = vi.spyOn(ctx.logger, "error").mockImplementation(() => {});
578
579 const helpers = await import("../helpers.js");
580 const getActiveBansSpy = vi.spyOn(helpers, "getActiveBans");
581 getActiveBansSpy.mockImplementationOnce(() => {
582 throw new TypeError("Cannot read property 'has' of undefined");
583 });
584
585 // Hono catches re-thrown errors via its internal error handler
586 const res = await app.request("/api/posts", {
587 method: "POST",
588 headers: { "Content-Type": "application/json" },
589 body: JSON.stringify({
590 text: "Reply with programming error",
591 rootPostId: topicId,
592 parentPostId: topicId,
593 }),
594 });
595
596 // Hono's default error handler returns 500 for uncaught throws
597 expect(res.status).toBe(500);
598
599 // Verify CRITICAL error was logged (proves the re-throw path was executed)
600 expect(consoleErrorSpy).toHaveBeenCalledWith(
601 "CRITICAL: Programming error in POST /api/posts - ban check",
602 expect.objectContaining({
603 operation: "POST /api/posts - ban check",
604 userId: mockUser.did,
605 error: "Cannot read property 'has' of undefined",
606 stack: expect.any(String),
607 })
608 );
609
610 // Verify the normal error path was NOT taken
611 expect(consoleErrorSpy).not.toHaveBeenCalledWith(
612 "Unable to verify ban status",
613 expect.any(Object)
614 );
615
616 consoleErrorSpy.mockRestore();
617 getActiveBansSpy.mockRestore();
618 });
619});
620
621describe("POST /api/posts - lock enforcement", () => {
622 let ctx: TestContext;
623 let app: Hono<{ Variables: Variables }>;
624 let topicId: string;
625 let topicRkey: string;
626
627 beforeEach(async () => {
628 ctx = await createTestContext();
629
630 // Insert test user (use onConflictDoNothing in case tests share users)
631 await ctx.db.insert(users).values({
632 did: "did:plc:lock-test-user",
633 handle: "locktestuser.test",
634 indexedAt: new Date(),
635 }).onConflictDoNothing();
636
637 // Insert topic (root post) with unique rkey
638 topicRkey = TID.nextStr();
639 const [topicPost] = await ctx.db
640 .insert(posts)
641 .values({
642 did: "did:plc:lock-test-user",
643 rkey: topicRkey,
644 cid: `bafy${topicRkey}`,
645 text: "Topic for lock tests",
646 forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`,
647 createdAt: new Date(),
648 indexedAt: new Date(),
649 })
650 .returning({ id: posts.id });
651
652 topicId = topicPost.id.toString();
653
654 // Set up mock user
655 mockUser = {
656 did: "did:plc:lock-test-user",
657 handle: "locktestuser.test",
658 pdsUrl: "https://test.pds",
659 agent: {
660 com: {
661 atproto: {
662 repo: {
663 putRecord: vi.fn(async () => ({
664 data: {
665 uri: "at://did:plc:lock-test-user/space.atbb.post/3lbklockreply",
666 cid: "bafylockreply",
667 },
668 })),
669 },
670 },
671 },
672 },
673 };
674
675 app = new Hono<{ Variables: Variables }>();
676 app.route("/api/posts", createPostsRoutes(ctx));
677 });
678
679 afterEach(async () => {
680 await ctx.cleanup();
681 });
682
683 it("allows reply when topic is unlocked", async () => {
684 const res = await app.request("/api/posts", {
685 method: "POST",
686 headers: { "Content-Type": "application/json" },
687 body: JSON.stringify({
688 text: "Reply to unlocked topic",
689 rootPostId: topicId,
690 parentPostId: topicId,
691 }),
692 });
693
694 expect(res.status).toBe(201);
695 const data = await res.json();
696 expect(data.uri).toBeDefined();
697 });
698
699 it("blocks reply when topic is locked", async () => {
700 // Lock the topic
701 const lockRkey = TID.nextStr();
702 const topicUri = `at://did:plc:lock-test-user/space.atbb.post/${topicRkey}`;
703
704 await ctx.db.insert(modActions).values({
705 did: ctx.config.forumDid,
706 rkey: lockRkey,
707 cid: `bafy${lockRkey}`,
708 action: "space.atbb.modAction.lock",
709 subjectPostUri: topicUri,
710 createdBy: "did:plc:admin",
711 createdAt: new Date(),
712 indexedAt: new Date(),
713 });
714
715 const res = await app.request("/api/posts", {
716 method: "POST",
717 headers: { "Content-Type": "application/json" },
718 body: JSON.stringify({
719 text: "Reply to locked topic",
720 rootPostId: topicId,
721 parentPostId: topicId,
722 }),
723 });
724
725 expect(res.status).toBe(403);
726 const data = await res.json();
727 expect(data.error).toContain("locked");
728 });
729
730 it("allows reply when topic was locked then unlocked", async () => {
731 const topicUri = `at://did:plc:lock-test-user/space.atbb.post/${topicRkey}`;
732
733 // Lock the topic first
734 const lockRkey = TID.nextStr();
735 await ctx.db.insert(modActions).values({
736 did: ctx.config.forumDid,
737 rkey: lockRkey,
738 cid: `bafy${lockRkey}`,
739 action: "space.atbb.modAction.lock",
740 subjectPostUri: topicUri,
741 createdBy: "did:plc:admin",
742 createdAt: new Date("2024-01-01T00:00:00Z"),
743 indexedAt: new Date("2024-01-01T00:00:00Z"),
744 });
745
746 // Then unlock the topic (more recent action)
747 const unlockRkey = TID.nextStr();
748 await ctx.db.insert(modActions).values({
749 did: ctx.config.forumDid,
750 rkey: unlockRkey,
751 cid: `bafy${unlockRkey}`,
752 action: "space.atbb.modAction.unlock",
753 subjectPostUri: topicUri,
754 createdBy: "did:plc:admin",
755 createdAt: new Date("2024-01-02T00:00:00Z"),
756 indexedAt: new Date("2024-01-02T00:00:00Z"),
757 });
758
759 const res = await app.request("/api/posts", {
760 method: "POST",
761 headers: { "Content-Type": "application/json" },
762 body: JSON.stringify({
763 text: "Reply to re-opened topic",
764 rootPostId: topicId,
765 parentPostId: topicId,
766 }),
767 });
768
769 expect(res.status).toBe(201);
770 const data = await res.json();
771 expect(data.uri).toBeDefined();
772 });
773});