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

Configure Feed

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

at master 173 lines 5.7 kB view raw
1import { describe, it, expect, beforeEach, afterEach } from "vitest"; 2import { 3 encryptToken, 4 decryptToken, 5 generateEncryptionKey, 6 isEncryptionConfigured, 7} from "./encryption.utils"; 8import { ApiError } from "../errors"; 9 10// A valid 64-char hex key (32 bytes) for testing 11const TEST_KEY = 12 "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"; 13 14describe("Encryption Utils", () => { 15 let originalKey: string | undefined; 16 17 beforeEach(() => { 18 originalKey = process.env.TOKEN_ENCRYPTION_KEY; 19 }); 20 21 afterEach(() => { 22 if (originalKey !== undefined) { 23 process.env.TOKEN_ENCRYPTION_KEY = originalKey; 24 } else { 25 delete process.env.TOKEN_ENCRYPTION_KEY; 26 } 27 }); 28 29 describe("generateEncryptionKey", () => { 30 it("generates a 64-character hex string", () => { 31 const key = generateEncryptionKey(); 32 expect(key).toHaveLength(64); 33 expect(key).toMatch(/^[0-9a-f]{64}$/); 34 }); 35 36 it("generates unique keys each time", () => { 37 const key1 = generateEncryptionKey(); 38 const key2 = generateEncryptionKey(); 39 expect(key1).not.toBe(key2); 40 }); 41 }); 42 43 describe("encryptToken / decryptToken", () => { 44 beforeEach(() => { 45 process.env.TOKEN_ENCRYPTION_KEY = TEST_KEY; 46 }); 47 48 it("encrypts and decrypts a string", () => { 49 const data = "sensitive-data"; 50 const encrypted = encryptToken(data); 51 const decrypted = decryptToken<string>(encrypted); 52 expect(decrypted).toBe(data); 53 }); 54 55 it("encrypts and decrypts an object", () => { 56 const data = { did: "did:plc:test123", handle: "test.bsky.social" }; 57 const encrypted = encryptToken(data); 58 const decrypted = decryptToken<typeof data>(encrypted); 59 expect(decrypted).toEqual(data); 60 }); 61 62 it("produces different ciphertext each time (random IV)", () => { 63 const data = "same-data"; 64 const encrypted1 = encryptToken(data); 65 const encrypted2 = encryptToken(data); 66 expect(encrypted1).not.toBe(encrypted2); 67 }); 68 69 it("encrypted output is valid JSON with iv, data, tag fields", () => { 70 const encrypted = encryptToken("test"); 71 const payload = JSON.parse(encrypted); 72 expect(payload).toHaveProperty("iv"); 73 expect(payload).toHaveProperty("data"); 74 expect(payload).toHaveProperty("tag"); 75 expect(typeof payload.iv).toBe("string"); 76 expect(typeof payload.data).toBe("string"); 77 expect(typeof payload.tag).toBe("string"); 78 }); 79 80 it("throws ApiError when decrypting tampered data", () => { 81 const encrypted = encryptToken("test"); 82 const payload = JSON.parse(encrypted); 83 // Tamper with encrypted data 84 payload.data = "0000" + payload.data.slice(4); 85 const tampered = JSON.stringify(payload); 86 87 expect(() => decryptToken(tampered)).toThrow(ApiError); 88 }); 89 90 it("throws ApiError when decrypting invalid JSON", () => { 91 expect(() => decryptToken("not-json")).toThrow(ApiError); 92 }); 93 94 it("throws ApiError when decrypting with missing fields", () => { 95 expect(() => decryptToken(JSON.stringify({ iv: "aa" }))).toThrow( 96 ApiError, 97 ); 98 }); 99 100 it("handles null and undefined values", () => { 101 const encryptedNull = encryptToken(null); 102 expect(decryptToken(encryptedNull)).toBeNull(); 103 }); 104 105 it("handles arrays", () => { 106 const data = [1, 2, 3]; 107 const encrypted = encryptToken(data); 108 expect(decryptToken<number[]>(encrypted)).toEqual(data); 109 }); 110 111 it("handles empty string", () => { 112 const encrypted = encryptToken(""); 113 expect(decryptToken<string>(encrypted)).toBe(""); 114 }); 115 }); 116 117 describe("encryptToken / decryptToken - key errors", () => { 118 it("throws ApiError when TOKEN_ENCRYPTION_KEY is not set", () => { 119 delete process.env.TOKEN_ENCRYPTION_KEY; 120 expect(() => encryptToken("data")).toThrow(ApiError); 121 try { 122 encryptToken("data"); 123 expect.fail("Should have thrown"); 124 } catch (error) { 125 const apiError = error as ApiError; 126 expect(apiError.statusCode).toBe(500); 127 expect(apiError.details).toContain("Encryption key not configured"); 128 } 129 }); 130 131 it("throws ApiError when TOKEN_ENCRYPTION_KEY has wrong length", () => { 132 process.env.TOKEN_ENCRYPTION_KEY = "tooshort"; 133 expect(() => encryptToken("data")).toThrow(ApiError); 134 try { 135 encryptToken("data"); 136 expect.fail("Should have thrown"); 137 } catch (error) { 138 const apiError = error as ApiError; 139 expect(apiError.statusCode).toBe(500); 140 expect(apiError.details).toContain("Invalid encryption key"); 141 } 142 }); 143 }); 144 145 describe("isEncryptionConfigured", () => { 146 it("returns true when key is set", () => { 147 process.env.TOKEN_ENCRYPTION_KEY = TEST_KEY; 148 expect(isEncryptionConfigured()).toBe(true); 149 }); 150 151 it("returns false when key is not set (non-production)", () => { 152 delete process.env.TOKEN_ENCRYPTION_KEY; 153 const originalNodeEnv = process.env.NODE_ENV; 154 process.env.NODE_ENV = "test"; 155 expect(isEncryptionConfigured()).toBe(false); 156 process.env.NODE_ENV = originalNodeEnv; 157 }); 158 159 it("throws ApiError when key is not set in production", () => { 160 delete process.env.TOKEN_ENCRYPTION_KEY; 161 const originalNodeEnv = process.env.NODE_ENV; 162 process.env.NODE_ENV = "production"; 163 try { 164 expect(() => isEncryptionConfigured()).toThrow(ApiError); 165 expect(() => isEncryptionConfigured()).toThrow( 166 "Encryption key not configured in production", 167 ); 168 } finally { 169 process.env.NODE_ENV = originalNodeEnv; 170 } 171 }); 172 }); 173});