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 security util unit tests

byarielm.fyi c930099b 19166dd5

verified
+183
+183
packages/api/src/utils/session-security.test.ts
··· 1 + import { describe, it, expect, vi } from "vitest"; 2 + import type { Context } from "hono"; 3 + import { 4 + SessionSecurityService, 5 + createSecureSessionData, 6 + } from "./session-security"; 7 + import type { SessionFingerprint } from "./session-security"; 8 + 9 + /** 10 + * Creates a minimal mock Hono Context with configurable request headers. 11 + */ 12 + function createMockContext( 13 + headers: Record<string, string> = {}, 14 + ): Context { 15 + return { 16 + req: { 17 + header: (name: string) => headers[name.toLowerCase()], 18 + }, 19 + } as Context; 20 + } 21 + 22 + describe("SessionSecurityService", () => { 23 + describe("generateFingerprint", () => { 24 + it("extracts user-agent from request headers", () => { 25 + const c = createMockContext({ 26 + "user-agent": "Mozilla/5.0 TestBrowser", 27 + }); 28 + 29 + const fingerprint = SessionSecurityService.generateFingerprint(c); 30 + 31 + expect(fingerprint.userAgent).toBe("Mozilla/5.0 TestBrowser"); 32 + }); 33 + 34 + it("uses 'unknown' when user-agent is missing", () => { 35 + const c = createMockContext({}); 36 + 37 + const fingerprint = SessionSecurityService.generateFingerprint(c); 38 + 39 + expect(fingerprint.userAgent).toBe("unknown"); 40 + }); 41 + 42 + it("extracts IP from x-forwarded-for (first entry)", () => { 43 + const c = createMockContext({ 44 + "x-forwarded-for": "192.168.1.1, 10.0.0.1", 45 + }); 46 + 47 + const fingerprint = SessionSecurityService.generateFingerprint(c); 48 + 49 + expect(fingerprint.ipAddress).toBe("192.168.1.1"); 50 + }); 51 + 52 + it("falls back to client-ip when x-forwarded-for is missing", () => { 53 + const c = createMockContext({ 54 + "client-ip": "10.0.0.5", 55 + }); 56 + 57 + const fingerprint = SessionSecurityService.generateFingerprint(c); 58 + 59 + expect(fingerprint.ipAddress).toBe("10.0.0.5"); 60 + }); 61 + 62 + it("uses 'unknown' when no IP headers are present", () => { 63 + const c = createMockContext({}); 64 + 65 + const fingerprint = SessionSecurityService.generateFingerprint(c); 66 + 67 + expect(fingerprint.ipAddress).toBe("unknown"); 68 + }); 69 + 70 + it("includes createdAt timestamp", () => { 71 + const before = Date.now(); 72 + const c = createMockContext({ "user-agent": "test" }); 73 + 74 + const fingerprint = SessionSecurityService.generateFingerprint(c); 75 + 76 + expect(fingerprint.createdAt).toBeGreaterThanOrEqual(before); 77 + expect(fingerprint.createdAt).toBeLessThanOrEqual(Date.now()); 78 + }); 79 + 80 + it("trims whitespace from x-forwarded-for IP", () => { 81 + const c = createMockContext({ 82 + "x-forwarded-for": " 192.168.1.1 , 10.0.0.1", 83 + }); 84 + 85 + const fingerprint = SessionSecurityService.generateFingerprint(c); 86 + 87 + expect(fingerprint.ipAddress).toBe("192.168.1.1"); 88 + }); 89 + }); 90 + 91 + describe("verifyFingerprint", () => { 92 + const baseFingerprint: SessionFingerprint = { 93 + userAgent: "Mozilla/5.0 TestBrowser", 94 + ipAddress: "192.168.1.1", 95 + createdAt: Date.now(), 96 + }; 97 + 98 + it("returns true when fingerprints match exactly", () => { 99 + const current: SessionFingerprint = { ...baseFingerprint }; 100 + expect( 101 + SessionSecurityService.verifyFingerprint(baseFingerprint, current), 102 + ).toBe(true); 103 + }); 104 + 105 + it("returns false when user-agent changes", () => { 106 + const current: SessionFingerprint = { 107 + ...baseFingerprint, 108 + userAgent: "DifferentBrowser/1.0", 109 + }; 110 + 111 + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); 112 + const result = SessionSecurityService.verifyFingerprint( 113 + baseFingerprint, 114 + current, 115 + ); 116 + 117 + expect(result).toBe(false); 118 + expect(warnSpy).toHaveBeenCalledWith( 119 + expect.stringContaining("User-Agent changed"), 120 + ); 121 + warnSpy.mockRestore(); 122 + }); 123 + 124 + it("returns true when IP changes (logs but doesnt fail)", () => { 125 + const current: SessionFingerprint = { 126 + ...baseFingerprint, 127 + ipAddress: "10.0.0.99", 128 + }; 129 + 130 + const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {}); 131 + const result = SessionSecurityService.verifyFingerprint( 132 + baseFingerprint, 133 + current, 134 + ); 135 + 136 + expect(result).toBe(true); 137 + expect(infoSpy).toHaveBeenCalledWith( 138 + expect.stringContaining("Session IP changed"), 139 + ); 140 + infoSpy.mockRestore(); 141 + }); 142 + 143 + it("returns true when createdAt differs (not compared)", () => { 144 + const current: SessionFingerprint = { 145 + ...baseFingerprint, 146 + createdAt: baseFingerprint.createdAt + 100000, 147 + }; 148 + 149 + expect( 150 + SessionSecurityService.verifyFingerprint(baseFingerprint, current), 151 + ).toBe(true); 152 + }); 153 + }); 154 + }); 155 + 156 + describe("createSecureSessionData", () => { 157 + it("returns object with did and fingerprint", () => { 158 + const c = createMockContext({ 159 + "user-agent": "TestAgent/1.0", 160 + "x-forwarded-for": "192.168.1.1", 161 + }); 162 + 163 + const result = createSecureSessionData(c, "did:plc:test123"); 164 + 165 + expect(result.did).toBe("did:plc:test123"); 166 + expect(result.fingerprint).toBeDefined(); 167 + expect(result.fingerprint.userAgent).toBe("TestAgent/1.0"); 168 + expect(result.fingerprint.ipAddress).toBe("192.168.1.1"); 169 + expect(typeof result.fingerprint.createdAt).toBe("number"); 170 + }); 171 + 172 + it("generates fingerprint from the provided context", () => { 173 + const c = createMockContext({ 174 + "user-agent": "Custom/2.0", 175 + "client-ip": "10.10.10.10", 176 + }); 177 + 178 + const result = createSecureSessionData(c, "did:web:example.com"); 179 + 180 + expect(result.did).toBe("did:web:example.com"); 181 + expect(result.fingerprint.ipAddress).toBe("10.10.10.10"); 182 + }); 183 + });