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

byarielm.fyi cde8671b 1db09e9c

verified
+173
+173
packages/api/src/utils/encryption.utils.test.ts
··· 1 + import { describe, it, expect, beforeEach, afterEach } from "vitest"; 2 + import { 3 + encryptToken, 4 + decryptToken, 5 + generateEncryptionKey, 6 + isEncryptionConfigured, 7 + } from "./encryption.utils"; 8 + import { ApiError } from "../errors"; 9 + 10 + // A valid 64-char hex key (32 bytes) for testing 11 + const TEST_KEY = 12 + "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"; 13 + 14 + describe("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 + });