ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
16
fork

Configure Feed

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

test(api): add session service unit tests

byarielm.fyi bd941622 9bf02b0f

verified
+199
+199
packages/api/src/services/SessionService.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 + import { SessionService } from "./SessionService"; 3 + import { AuthenticationError } from "../errors"; 4 + 5 + // Mock the infrastructure dependencies 6 + vi.mock("../infrastructure/oauth/OAuthClientFactory", () => ({ 7 + createOAuthClient: vi.fn(), 8 + })); 9 + 10 + vi.mock("../infrastructure/oauth", () => ({ 11 + userSessionStore: { 12 + get: vi.fn(), 13 + set: vi.fn(), 14 + del: vi.fn(), 15 + }, 16 + sessionStore: { 17 + get: vi.fn(), 18 + set: vi.fn(), 19 + del: vi.fn(), 20 + }, 21 + })); 22 + 23 + // Import mocked modules after vi.mock declarations 24 + import { createOAuthClient } from "../infrastructure/oauth/OAuthClientFactory"; 25 + import { userSessionStore, sessionStore } from "../infrastructure/oauth"; 26 + import type { Context } from "hono"; 27 + 28 + const mockUserSessionStore = vi.mocked(userSessionStore); 29 + const mockCreateOAuthClient = vi.mocked(createOAuthClient); 30 + const mockSessionStore = vi.mocked(sessionStore); 31 + 32 + /** Creates a minimal mock Hono Context */ 33 + function createMockContext( 34 + headers: Record<string, string> = {}, 35 + ): Context { 36 + return { 37 + req: { 38 + header: (name: string) => headers[name.toLowerCase()], 39 + }, 40 + } as Context; 41 + } 42 + 43 + describe("SessionService", () => { 44 + beforeEach(() => { 45 + vi.clearAllMocks(); 46 + }); 47 + 48 + describe("verifySession", () => { 49 + it("returns true for existing session", async () => { 50 + mockUserSessionStore.get.mockResolvedValue({ did: "did:plc:test" }); 51 + 52 + const result = await SessionService.verifySession("valid-session-id"); 53 + 54 + expect(result).toBe(true); 55 + expect(mockUserSessionStore.get).toHaveBeenCalledWith("valid-session-id"); 56 + }); 57 + 58 + it("returns false for non-existent session", async () => { 59 + mockUserSessionStore.get.mockResolvedValue(undefined); 60 + 61 + const result = await SessionService.verifySession("invalid-session"); 62 + 63 + expect(result).toBe(false); 64 + }); 65 + }); 66 + 67 + describe("getDIDForSession", () => { 68 + it("returns DID for existing session", async () => { 69 + mockUserSessionStore.get.mockResolvedValue({ did: "did:plc:user123" }); 70 + 71 + const result = await SessionService.getDIDForSession("session-id"); 72 + 73 + expect(result).toBe("did:plc:user123"); 74 + }); 75 + 76 + it("returns null for non-existent session", async () => { 77 + mockUserSessionStore.get.mockResolvedValue(undefined); 78 + 79 + const result = await SessionService.getDIDForSession("bad-session"); 80 + 81 + expect(result).toBeNull(); 82 + }); 83 + }); 84 + 85 + describe("getAgentForSession", () => { 86 + const mockContext = createMockContext({ host: "localhost:8888" }); 87 + 88 + it("throws AuthenticationError for invalid session", async () => { 89 + mockUserSessionStore.get.mockResolvedValue(undefined); 90 + 91 + await expect( 92 + SessionService.getAgentForSession("bad-session", mockContext), 93 + ).rejects.toThrow(AuthenticationError); 94 + }); 95 + 96 + it("creates OAuth client and restores agent for valid session", async () => { 97 + mockUserSessionStore.get.mockResolvedValue({ did: "did:plc:test" }); 98 + 99 + const mockOAuthSession = { did: "did:plc:test" }; 100 + const mockClient = { 101 + restore: vi.fn().mockResolvedValue(mockOAuthSession), 102 + }; 103 + mockCreateOAuthClient.mockResolvedValue(mockClient as never); 104 + mockSessionStore.get.mockResolvedValue({} as never); 105 + 106 + const result = await SessionService.getAgentForSession( 107 + "valid-session", 108 + mockContext, 109 + ); 110 + 111 + expect(result.did).toBe("did:plc:test"); 112 + expect(result.agent).toBeDefined(); 113 + expect(result.client).toBe(mockClient); 114 + expect(mockClient.restore).toHaveBeenCalledWith("did:plc:test"); 115 + }); 116 + 117 + it("uses cached OAuth client within 5-minute window", async () => { 118 + mockUserSessionStore.get.mockResolvedValue({ did: "did:plc:test" }); 119 + 120 + const mockOAuthSession = { did: "did:plc:test" }; 121 + const mockClient = { 122 + restore: vi.fn().mockResolvedValue(mockOAuthSession), 123 + }; 124 + mockCreateOAuthClient.mockResolvedValue(mockClient as never); 125 + mockSessionStore.get.mockResolvedValue({} as never); 126 + 127 + // First call - creates client 128 + await SessionService.getAgentForSession("cached-session", mockContext); 129 + // Second call - should use cached 130 + await SessionService.getAgentForSession("cached-session", mockContext); 131 + 132 + // createOAuthClient called only once (cached for second call) 133 + expect(mockCreateOAuthClient).toHaveBeenCalledTimes(1); 134 + }); 135 + 136 + it("throws AuthenticationError when OAuth session restore fails", async () => { 137 + mockUserSessionStore.get.mockResolvedValue({ did: "did:plc:test" }); 138 + 139 + const mockClient = { 140 + restore: vi.fn().mockRejectedValue(new Error("Token expired")), 141 + }; 142 + mockCreateOAuthClient.mockResolvedValue(mockClient as never); 143 + 144 + await expect( 145 + SessionService.getAgentForSession("failing-session", mockContext), 146 + ).rejects.toThrow(AuthenticationError); 147 + }); 148 + }); 149 + 150 + describe("deleteSession", () => { 151 + const mockContext = createMockContext({ host: "localhost:8888" }); 152 + 153 + it("does nothing for non-existent session", async () => { 154 + mockUserSessionStore.get.mockResolvedValue(undefined); 155 + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); 156 + 157 + await SessionService.deleteSession("non-existent", mockContext); 158 + 159 + expect(mockUserSessionStore.del).not.toHaveBeenCalled(); 160 + logSpy.mockRestore(); 161 + }); 162 + 163 + it("revokes OAuth session and deletes user session", async () => { 164 + mockUserSessionStore.get.mockResolvedValue({ did: "did:plc:test" }); 165 + 166 + const mockClient = { 167 + revoke: vi.fn().mockResolvedValue(undefined), 168 + }; 169 + mockCreateOAuthClient.mockResolvedValue(mockClient as never); 170 + 171 + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); 172 + 173 + await SessionService.deleteSession("session-to-delete", mockContext); 174 + 175 + expect(mockClient.revoke).toHaveBeenCalledWith("did:plc:test"); 176 + expect(mockUserSessionStore.del).toHaveBeenCalledWith( 177 + "session-to-delete", 178 + ); 179 + logSpy.mockRestore(); 180 + }); 181 + 182 + it("continues deletion even if OAuth revocation fails", async () => { 183 + mockUserSessionStore.get.mockResolvedValue({ did: "did:plc:test" }); 184 + 185 + const mockClient = { 186 + revoke: vi.fn().mockRejectedValue(new Error("Revocation failed")), 187 + }; 188 + mockCreateOAuthClient.mockResolvedValue(mockClient as never); 189 + 190 + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); 191 + 192 + await SessionService.deleteSession("session-id", mockContext); 193 + 194 + // Should still delete the user session even if revocation fails 195 + expect(mockUserSessionStore.del).toHaveBeenCalledWith("session-id"); 196 + logSpy.mockRestore(); 197 + }); 198 + }); 199 + });