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.

fix(appview): address review feedback on ATB-37 bootstrap upgrade

- Pass ctx.logger to parseAtUri so malformed roleUris emit structured
warnings at parse time
- Add explicit error log when parseAtUri returns null (data integrity
signal — a stored roleUri that can't be parsed is unexpected)
- Upgrade warn → error in DB failure catch: a failed role lookup during
bootstrap upgrade has the same consequence as the role-not-found path
(firehose will null out roleUri), so both warrant error level
- Add DB failure test for the role lookup catch block
- Add malformed roleUri test (parseAtUri returns null path)
- Add result.created assertions to tests 1 and 2
- Add DB state assertions to all three new bootstrap upgrade tests

Malpercio 681248eb 4d0b11c3

+158 -5
+149 -2
apps/appview/src/lib/__tests__/membership.test.ts
··· 528 528 }, 529 529 } as any; 530 530 531 - await createMembershipForUser(ctx, mockAgent, testDid); 531 + const result = await createMembershipForUser(ctx, mockAgent, testDid); 532 + 533 + expect(result.created).toBe(true); 532 534 533 535 // The PDS record must include the role strongRef so the firehose 534 536 // preserves the roleUri when it re-indexes the upgrade event. ··· 544 546 }), 545 547 }) 546 548 ); 549 + 550 + // DB row must reflect the upgrade: real rkey/cid, roleUri preserved 551 + const [updated] = await ctx.db 552 + .select() 553 + .from(memberships) 554 + .where(and(eq(memberships.did, testDid), eq(memberships.forumUri, forumUri))) 555 + .limit(1); 556 + expect(updated.cid).toBe("bafyupgradedabc"); 557 + expect(updated.rkey).not.toBe("bootstrap"); 558 + expect(updated.roleUri).toBe(ownerRoleUri); 547 559 }); 548 560 549 561 it("omits role from PDS record when upgrading bootstrap membership without a roleUri", async () => { ··· 581 593 }, 582 594 } as any; 583 595 584 - await createMembershipForUser(ctx, mockAgent, testDid); 596 + const result = await createMembershipForUser(ctx, mockAgent, testDid); 585 597 598 + expect(result.created).toBe(true); 586 599 const callArg = mockAgent.com.atproto.repo.putRecord.mock.calls[0][0]; 587 600 expect(callArg.record.role).toBeUndefined(); 601 + 602 + // DB row must reflect the upgrade: real rkey/cid, roleUri stays null 603 + const [updated] = await ctx.db 604 + .select() 605 + .from(memberships) 606 + .where(and(eq(memberships.did, testDid), eq(memberships.forumUri, forumUri))) 607 + .limit(1); 608 + expect(updated.cid).toBe("bafynoroledef"); 609 + expect(updated.rkey).not.toBe("bootstrap"); 610 + expect(updated.roleUri).toBeNull(); 588 611 }); 589 612 590 613 it("upgrades bootstrap membership without role when roleUri references a role not in DB", async () => { ··· 630 653 631 654 const callArg = mockAgent.com.atproto.repo.putRecord.mock.calls[0][0]; 632 655 expect(callArg.record.role).toBeUndefined(); 656 + 657 + // DB row must reflect the upgrade: real rkey/cid, dangling roleUri preserved 658 + const [updated] = await ctx.db 659 + .select() 660 + .from(memberships) 661 + .where(and(eq(memberships.did, testDid), eq(memberships.forumUri, forumUri))) 662 + .limit(1); 663 + expect(updated.cid).toBe("bafymissingghi"); 664 + expect(updated.rkey).not.toBe("bootstrap"); 665 + expect(updated.roleUri).toBe(danglingRoleUri); 666 + }); 667 + 668 + it("logs error and continues upgrade when role DB lookup fails during bootstrap upgrade", async () => { 669 + const testDid = `did:plc:test-bootstrap-dberr-${Date.now()}`; 670 + const forumUri = `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`; 671 + const ownerRoleUri = `at://${ctx.config.forumDid}/space.atbb.forum.role/ownerrole`; 672 + 673 + await ctx.db.insert(users).values({ 674 + did: testDid, 675 + handle: "bootstrap.dberr", 676 + indexedAt: new Date(), 677 + }); 678 + 679 + await ctx.db.insert(memberships).values({ 680 + did: testDid, 681 + rkey: "bootstrap", 682 + cid: "bootstrap", 683 + forumUri, 684 + roleUri: ownerRoleUri, 685 + role: "Owner", 686 + createdAt: new Date(), 687 + indexedAt: new Date(), 688 + }); 689 + 690 + const origSelect = ctx.db.select.bind(ctx.db); 691 + vi.spyOn(ctx.db, "select") 692 + .mockImplementationOnce(() => origSelect() as any) // forums lookup 693 + .mockImplementationOnce(() => origSelect() as any) // memberships check (bootstrap found) 694 + .mockReturnValueOnce({ // roles query in upgradeBootstrapMembership 695 + from: vi.fn().mockReturnValue({ 696 + where: vi.fn().mockReturnValue({ 697 + limit: vi.fn().mockRejectedValue(new Error("DB connection lost")), 698 + }), 699 + }), 700 + } as any); 701 + 702 + const errorSpy = vi.spyOn(ctx.logger, "error"); 703 + 704 + const mockAgent = { 705 + com: { 706 + atproto: { 707 + repo: { 708 + putRecord: vi.fn().mockResolvedValue({ 709 + data: { 710 + uri: `at://${testDid}/space.atbb.membership/tidjkl`, 711 + cid: "bafydberrjkl", 712 + }, 713 + }), 714 + }, 715 + }, 716 + }, 717 + } as any; 718 + 719 + const result = await createMembershipForUser(ctx, mockAgent, testDid); 720 + 721 + expect(result.created).toBe(true); 722 + const callArg = mockAgent.com.atproto.repo.putRecord.mock.calls[0][0]; 723 + expect(callArg.record.role).toBeUndefined(); 724 + expect(errorSpy).toHaveBeenCalledWith( 725 + expect.stringContaining("Role lookup failed during bootstrap upgrade"), 726 + expect.objectContaining({ operation: "upgradeBootstrapMembership" }) 727 + ); 728 + 729 + vi.restoreAllMocks(); 730 + }); 731 + 732 + it("logs error and omits role when bootstrap membership has a malformed roleUri", async () => { 733 + const testDid = `did:plc:test-bootstrap-malformed-${Date.now()}`; 734 + const forumUri = `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`; 735 + // Syntactically invalid AT URI — parseAtUri will return null 736 + const malformedRoleUri = "not-a-valid-at-uri"; 737 + 738 + await ctx.db.insert(users).values({ 739 + did: testDid, 740 + handle: "bootstrap.malformed", 741 + indexedAt: new Date(), 742 + }); 743 + 744 + await ctx.db.insert(memberships).values({ 745 + did: testDid, 746 + rkey: "bootstrap", 747 + cid: "bootstrap", 748 + forumUri, 749 + roleUri: malformedRoleUri, 750 + createdAt: new Date(), 751 + indexedAt: new Date(), 752 + }); 753 + 754 + const errorSpy = vi.spyOn(ctx.logger, "error"); 755 + 756 + const mockAgent = { 757 + com: { 758 + atproto: { 759 + repo: { 760 + putRecord: vi.fn().mockResolvedValue({ 761 + data: { 762 + uri: `at://${testDid}/space.atbb.membership/tidmno`, 763 + cid: "bafymalformedmno", 764 + }, 765 + }), 766 + }, 767 + }, 768 + }, 769 + } as any; 770 + 771 + const result = await createMembershipForUser(ctx, mockAgent, testDid); 772 + 773 + expect(result.created).toBe(true); 774 + const callArg = mockAgent.com.atproto.repo.putRecord.mock.calls[0][0]; 775 + expect(callArg.record.role).toBeUndefined(); 776 + expect(errorSpy).toHaveBeenCalledWith( 777 + expect.stringContaining("roleUri failed to parse"), 778 + expect.objectContaining({ operation: "upgradeBootstrapMembership", roleUri: malformedRoleUri }) 779 + ); 633 780 }); 634 781 });
+9 -3
apps/appview/src/lib/membership.ts
··· 130 130 // (record.role?.role.uri ?? null), stripping the member's role. 131 131 let roleRef: { uri: string; cid: string } | null = null; 132 132 if (roleUri) { 133 - const parsed = parseAtUri(roleUri); 134 - if (parsed) { 133 + const parsed = parseAtUri(roleUri, ctx.logger); 134 + if (!parsed) { 135 + ctx.logger.error("roleUri failed to parse — role omitted from PDS record", { 136 + operation: "upgradeBootstrapMembership", 137 + did, 138 + roleUri, 139 + }); 140 + } else { 135 141 try { 136 142 const [role] = await ctx.db 137 143 .select({ cid: roles.cid }) ··· 145 151 if (error instanceof TypeError || error instanceof ReferenceError || error instanceof SyntaxError) { 146 152 throw error; 147 153 } 148 - ctx.logger.warn("Role lookup failed during bootstrap upgrade — omitting role from PDS record", { 154 + ctx.logger.error("Role lookup failed during bootstrap upgrade — omitting role from PDS record", { 149 155 operation: "upgradeBootstrapMembership", 150 156 did, 151 157 roleUri,