Encrypted, ephemeral, private memos on atproto
at main 7.5 kB view raw
1import { expect } from "@std/expect"; 2import { createConsumer } from "@cistern/consumer"; 3import { createProducer } from "@cistern/producer"; 4import type { Handle } from "@atcute/lexicons"; 5 6/** 7 * End-to-end integration test for Cistern 8 * 9 * This test requires the following environment variables: 10 * - CISTERN_HANDLE: Your Bluesky handle (e.g., "user.bsky.social") 11 * - CISTERN_APP_PASSWORD: Your app password 12 * 13 * To run this test: 14 * ```bash 15 * CISTERN_HANDLE="your.handle" CISTERN_APP_PASSWORD="your-app-password" deno test --allow-env --allow-net e2e.test.ts 16 * ``` 17 */ 18 19const SKIP_E2E = !Deno.env.get("CISTERN_HANDLE") || 20 !Deno.env.get("CISTERN_APP_PASSWORD"); 21 22Deno.test({ 23 name: "E2E: Full encryption workflow", 24 ignore: SKIP_E2E, 25 async fn(t) { 26 const handle = Deno.env.get("CISTERN_HANDLE") as Handle; 27 const appPassword = Deno.env.get("CISTERN_APP_PASSWORD")!; 28 29 let consumer: Awaited<ReturnType<typeof createConsumer>>; 30 let producer: Awaited<ReturnType<typeof createProducer>>; 31 let keypair: Awaited<ReturnType<typeof consumer.generateKeyPair>>; 32 let memoUri: string; 33 let testMessage: string; 34 35 await t.step("Create consumer", async () => { 36 consumer = await createConsumer({ 37 handle, 38 appPassword, 39 }); 40 41 expect(consumer.did).toBeDefined(); 42 expect(consumer.rpc).toBeDefined(); 43 }); 44 45 await t.step("Generate keypair", async () => { 46 keypair = await consumer.generateKeyPair(); 47 48 expect(keypair.privateKey).toBeInstanceOf(Uint8Array); 49 expect(keypair.publicKey).toBeDefined(); 50 expect(keypair.publicKey).toContain("app.cistern.pubkey"); 51 }); 52 53 try { 54 await t.step("Create producer with public key", async () => { 55 const publicKeyRkey = keypair.publicKey.split("/").pop()!; 56 producer = await createProducer({ 57 handle, 58 appPassword, 59 publicKey: publicKeyRkey, 60 }); 61 62 expect(producer.publicKey).toBeDefined(); 63 expect(producer.publicKey?.uri).toEqual(keypair.publicKey); 64 }); 65 66 await t.step("Create encrypted memo", async () => { 67 testMessage = `E2E Test - ${new Date().toISOString()}`; 68 memoUri = await producer.createMemo(testMessage); 69 70 expect(memoUri).toBeDefined(); 71 expect(memoUri).toContain("app.cistern.memo"); 72 }); 73 74 await t.step("List and decrypt memos", async () => { 75 const memos = []; 76 for await (const memo of consumer.listMemos()) { 77 memos.push(memo); 78 } 79 80 expect(memos.length).toBeGreaterThan(0); 81 82 const ourMemo = memos.find((memo) => memo.text === testMessage); 83 expect(ourMemo).toBeDefined(); 84 expect(ourMemo!.text).toEqual(testMessage); 85 }); 86 87 await t.step("Delete memo", async () => { 88 const memoRkey = memoUri.split("/").pop()!; 89 await consumer.deleteMemo(memoRkey); 90 91 // Verify deletion 92 const memosAfterDelete = []; 93 for await (const memo of consumer.listMemos()) { 94 memosAfterDelete.push(memo); 95 } 96 97 const deletedMemo = memosAfterDelete.find( 98 (memo) => memo.text === testMessage, 99 ); 100 expect(deletedMemo).toBeUndefined(); 101 }); 102 103 await t.step("List public keys", async () => { 104 const keys = []; 105 for await (const key of producer.listPublicKeys()) { 106 keys.push(key); 107 } 108 109 expect(keys.length).toBeGreaterThan(0); 110 111 const ourKey = keys.find((key) => key.uri === keypair.publicKey); 112 expect(ourKey).toBeDefined(); 113 expect(ourKey!.uri).toEqual(keypair.publicKey); 114 }); 115 } finally { 116 await t.step("Cleanup: Delete test keypair", async () => { 117 const publicKeyRkey = keypair.publicKey.split("/").pop()!; 118 119 const res = await consumer.rpc.post("com.atproto.repo.deleteRecord", { 120 input: { 121 collection: "app.cistern.pubkey", 122 repo: consumer.did, 123 rkey: publicKeyRkey, 124 }, 125 }); 126 127 expect(res.ok).toBe(true); 128 }); 129 } 130 }, 131}); 132 133Deno.test({ 134 name: "E2E: Multiple memos with same keypair", 135 ignore: SKIP_E2E, 136 async fn(t) { 137 const handle = Deno.env.get("CISTERN_HANDLE") as Handle; 138 const appPassword = Deno.env.get("CISTERN_APP_PASSWORD")!; 139 140 let consumer: Awaited<ReturnType<typeof createConsumer>>; 141 let producer: Awaited<ReturnType<typeof createProducer>>; 142 let keypair: Awaited<ReturnType<typeof consumer.generateKeyPair>>; 143 let messages: string[]; 144 let memoUris: string[]; 145 146 await t.step("Create consumer and generate keypair", async () => { 147 consumer = await createConsumer({ 148 handle, 149 appPassword, 150 }); 151 152 keypair = await consumer.generateKeyPair(); 153 154 expect(keypair.privateKey).toBeInstanceOf(Uint8Array); 155 expect(keypair.publicKey).toBeDefined(); 156 }); 157 158 try { 159 await t.step("Create producer", async () => { 160 const publicKeyRkey = keypair.publicKey.split("/").pop()!; 161 producer = await createProducer({ 162 handle, 163 appPassword, 164 publicKey: publicKeyRkey, 165 }); 166 167 expect(producer.publicKey?.uri).toEqual(keypair.publicKey); 168 }); 169 170 await t.step("Create multiple encrypted memos", async () => { 171 messages = [ 172 `E2E Memo 1 - ${new Date().toISOString()}`, 173 `E2E Memo 2 - ${new Date().toISOString()}`, 174 `E2E Memo 3 - ${new Date().toISOString()}`, 175 ]; 176 177 memoUris = []; 178 for (const message of messages) { 179 const uri = await producer.createMemo(message); 180 memoUris.push(uri); 181 } 182 183 expect(memoUris).toHaveLength(3); 184 }); 185 186 await t.step("Decrypt all memos", async () => { 187 const memos = []; 188 for await (const memo of consumer.listMemos()) { 189 memos.push(memo); 190 } 191 192 expect(memos.length).toBeGreaterThanOrEqual(3); 193 194 // Verify all test messages are present 195 for (const message of messages) { 196 const memo = memos.find((m) => m.text === message); 197 expect(memo).toBeDefined(); 198 expect(memo!.text).toEqual(message); 199 } 200 }); 201 202 await t.step("Cleanup: Delete test memos", async () => { 203 for (const uri of memoUris) { 204 const rkey = uri.split("/").pop()!; 205 await consumer.deleteMemo(rkey); 206 } 207 208 // Verify all memos deleted 209 const remainingMemos = []; 210 for await (const memo of consumer.listMemos()) { 211 remainingMemos.push(memo); 212 } 213 214 for (const message of messages) { 215 const memo = remainingMemos.find((m) => m.text === message); 216 expect(memo).toBeUndefined(); 217 } 218 }); 219 } finally { 220 await t.step("Cleanup: Delete test keypair", async () => { 221 const publicKeyRkey = keypair.publicKey.split("/").pop()!; 222 223 const res = await consumer.rpc.post("com.atproto.repo.deleteRecord", { 224 input: { 225 collection: "app.cistern.pubkey", 226 repo: consumer.did, 227 rkey: publicKeyRkey, 228 }, 229 }); 230 231 expect(res.ok).toBe(true); 232 }); 233 } 234 }, 235}); 236 237if (SKIP_E2E) { 238 console.log(` 239⚠️ E2E tests skipped - missing environment variables 240 241To run E2E tests, set the following environment variables: 242 CISTERN_HANDLE="your.bsky.social" 243 CISTERN_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx" 244 245Then run: 246 deno test --allow-env --allow-net e2e.test.ts 247`); 248}