Encrypted, ephemeral, private memos on atproto

test: e2e case

graham.systems 29199ed4 6d2a3932

verified
Changed files
+257 -1
+5 -1
deno.jsonc
··· 1 1 { 2 2 "workspace": [ 3 3 "packages/*" 4 - ] 4 + ], 5 + "imports": { 6 + "@std/expect": "jsr:@std/expect@^1.0.17", 7 + "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2" 8 + } 5 9 }
+4
deno.lock
··· 211 211 } 212 212 }, 213 213 "workspace": { 214 + "dependencies": [ 215 + "jsr:@std/expect@^1.0.17", 216 + "npm:@atcute/lexicons@^1.2.2" 217 + ], 214 218 "members": { 215 219 "packages/consumer": { 216 220 "dependencies": [
+248
e2e.test.ts
··· 1 + import { expect } from "@std/expect"; 2 + import { createConsumer } from "@cistern/consumer"; 3 + import { createProducer } from "@cistern/producer"; 4 + import 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 + 19 + const SKIP_E2E = !Deno.env.get("CISTERN_HANDLE") || 20 + !Deno.env.get("CISTERN_APP_PASSWORD"); 21 + 22 + Deno.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 itemUri: 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.lexicon.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 item", async () => { 67 + testMessage = `E2E Test - ${new Date().toISOString()}`; 68 + itemUri = await producer.createItem(testMessage); 69 + 70 + expect(itemUri).toBeDefined(); 71 + expect(itemUri).toContain("app.cistern.lexicon.item"); 72 + }); 73 + 74 + await t.step("List and decrypt items", async () => { 75 + const items = []; 76 + for await (const item of consumer.listItems()) { 77 + items.push(item); 78 + } 79 + 80 + expect(items.length).toBeGreaterThan(0); 81 + 82 + const ourItem = items.find((item) => item.text === testMessage); 83 + expect(ourItem).toBeDefined(); 84 + expect(ourItem!.text).toEqual(testMessage); 85 + }); 86 + 87 + await t.step("Delete item", async () => { 88 + const itemRkey = itemUri.split("/").pop()!; 89 + await consumer.deleteItem(itemRkey); 90 + 91 + // Verify deletion 92 + const itemsAfterDelete = []; 93 + for await (const item of consumer.listItems()) { 94 + itemsAfterDelete.push(item); 95 + } 96 + 97 + const deletedItem = itemsAfterDelete.find( 98 + (item) => item.text === testMessage, 99 + ); 100 + expect(deletedItem).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.lexicon.pubkey", 122 + repo: consumer.did, 123 + rkey: publicKeyRkey, 124 + }, 125 + }); 126 + 127 + expect(res.ok).toBe(true); 128 + }); 129 + } 130 + }, 131 + }); 132 + 133 + Deno.test({ 134 + name: "E2E: Multiple items 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 itemUris: 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 items", async () => { 171 + messages = [ 172 + `E2E Item 1 - ${new Date().toISOString()}`, 173 + `E2E Item 2 - ${new Date().toISOString()}`, 174 + `E2E Item 3 - ${new Date().toISOString()}`, 175 + ]; 176 + 177 + itemUris = []; 178 + for (const message of messages) { 179 + const uri = await producer.createItem(message); 180 + itemUris.push(uri); 181 + } 182 + 183 + expect(itemUris).toHaveLength(3); 184 + }); 185 + 186 + await t.step("Decrypt all items", async () => { 187 + const items = []; 188 + for await (const item of consumer.listItems()) { 189 + items.push(item); 190 + } 191 + 192 + expect(items.length).toBeGreaterThanOrEqual(3); 193 + 194 + // Verify all test messages are present 195 + for (const message of messages) { 196 + const item = items.find((i) => i.text === message); 197 + expect(item).toBeDefined(); 198 + expect(item!.text).toEqual(message); 199 + } 200 + }); 201 + 202 + await t.step("Cleanup: Delete test items", async () => { 203 + for (const uri of itemUris) { 204 + const rkey = uri.split("/").pop()!; 205 + await consumer.deleteItem(rkey); 206 + } 207 + 208 + // Verify all items deleted 209 + const remainingItems = []; 210 + for await (const item of consumer.listItems()) { 211 + remainingItems.push(item); 212 + } 213 + 214 + for (const message of messages) { 215 + const item = remainingItems.find((i) => i.text === message); 216 + expect(item).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.lexicon.pubkey", 226 + repo: consumer.did, 227 + rkey: publicKeyRkey, 228 + }, 229 + }); 230 + 231 + expect(res.ok).toBe(true); 232 + }); 233 + } 234 + }, 235 + }); 236 + 237 + if (SKIP_E2E) { 238 + console.log(` 239 + ⚠️ E2E tests skipped - missing environment variables 240 + 241 + To run E2E tests, set the following environment variables: 242 + CISTERN_HANDLE="your.bsky.social" 243 + CISTERN_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx" 244 + 245 + Then run: 246 + deno test --allow-env --allow-net e2e.test.ts 247 + `); 248 + }