+5
-1
deno.jsonc
+5
-1
deno.jsonc
+4
deno.lock
+4
deno.lock
+248
e2e.test.ts
+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
+
}