tangled
alpha
login
or
join now
graham.systems
/
cistern
3
fork
atom
Encrypted, ephemeral, private memos on atproto
3
fork
atom
overview
issues
pulls
pipelines
test(producer): add suite
graham.systems
4 months ago
683dd271
95448e96
verified
This commit was signed with the committer's
known signature
.
graham.systems
SSH Key Fingerprint:
SHA256:Fvaam8TgCBeBlr/Fo7eA6VGAIAWmzjwUqUTw5o6anWA=
+347
-1
3 changed files
expand all
collapse all
unified
split
deno.lock
packages
producer
deno.jsonc
mod.test.ts
+1
deno.lock
···
240
240
},
241
241
"packages/producer": {
242
242
"dependencies": [
243
243
+
"jsr:@std/expect@^1.0.17",
243
244
"npm:@atcute/atproto@^3.1.9",
244
245
"npm:@atcute/client@^4.0.5",
245
246
"npm:@atcute/lexicons@^1.2.2",
+2
-1
packages/producer/deno.jsonc
···
7
7
"@atcute/atproto": "npm:@atcute/atproto@^3.1.9",
8
8
"@atcute/client": "npm:@atcute/client@^4.0.5",
9
9
"@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2",
10
10
-
"@atcute/tid": "npm:@atcute/tid@^1.0.3"
10
10
+
"@atcute/tid": "npm:@atcute/tid@^1.0.3",
11
11
+
"@std/expect": "jsr:@std/expect@^1.0.17"
11
12
}
12
13
}
+344
packages/producer/mod.test.ts
···
1
1
+
import { expect } from "@std/expect";
2
2
+
import { Producer } from "./mod.ts";
3
3
+
import { generateKeys } from "@cistern/crypto";
4
4
+
import type { ProducerParams, PublicKeyOption } from "./types.ts";
5
5
+
import type { Client, CredentialManager } from "@atcute/client";
6
6
+
import type { Did, Handle, ResourceUri } from "@atcute/lexicons";
7
7
+
8
8
+
// Helper to create a mock Producer instance
9
9
+
function createMockProducer(
10
10
+
overrides?: Partial<ProducerParams>,
11
11
+
): Producer {
12
12
+
const mockParams: ProducerParams = {
13
13
+
miniDoc: {
14
14
+
did: "did:plc:test123" as Did,
15
15
+
handle: "test.bsky.social" as Handle,
16
16
+
pds: "https://test.pds.example",
17
17
+
signing_key: "test-key",
18
18
+
},
19
19
+
manager: {} as CredentialManager,
20
20
+
rpc: createMockRpcClient(),
21
21
+
options: {
22
22
+
handle: "test.bsky.social" as Handle,
23
23
+
appPassword: "test-password",
24
24
+
},
25
25
+
...overrides,
26
26
+
};
27
27
+
28
28
+
return new Producer(mockParams);
29
29
+
}
30
30
+
31
31
+
// Helper to create a mock RPC client
32
32
+
function createMockRpcClient(): Client {
33
33
+
return {
34
34
+
get: () => {
35
35
+
throw new Error("Mock RPC get not implemented");
36
36
+
},
37
37
+
post: () => {
38
38
+
throw new Error("Mock RPC post not implemented");
39
39
+
},
40
40
+
} as unknown as Client;
41
41
+
}
42
42
+
43
43
+
Deno.test({
44
44
+
name: "Producer constructor initializes with provided params",
45
45
+
fn() {
46
46
+
const producer = createMockProducer();
47
47
+
48
48
+
expect(producer.did).toEqual("did:plc:test123");
49
49
+
expect(producer.publicKey).toBeUndefined();
50
50
+
expect(producer.rpc).toBeDefined();
51
51
+
expect(producer.manager).toBeDefined();
52
52
+
},
53
53
+
});
54
54
+
55
55
+
Deno.test({
56
56
+
name: "Producer constructor initializes with existing public key",
57
57
+
fn() {
58
58
+
const mockPublicKey: PublicKeyOption = {
59
59
+
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
60
60
+
name: "Test Key",
61
61
+
content: new Uint8Array(32).toBase64(),
62
62
+
};
63
63
+
64
64
+
const producer = createMockProducer({
65
65
+
publicKey: mockPublicKey,
66
66
+
});
67
67
+
68
68
+
expect(producer.publicKey).toBeDefined();
69
69
+
expect(producer.publicKey?.uri).toEqual(mockPublicKey.uri);
70
70
+
expect(producer.publicKey?.name).toEqual("Test Key");
71
71
+
},
72
72
+
});
73
73
+
74
74
+
Deno.test({
75
75
+
name: "createItem successfully creates and uploads an encrypted item",
76
76
+
async fn() {
77
77
+
const keys = generateKeys();
78
78
+
let capturedRecord: unknown;
79
79
+
let capturedCollection: string | undefined;
80
80
+
81
81
+
const mockRpc = {
82
82
+
post: (endpoint: string, params: { input: unknown }) => {
83
83
+
if (endpoint === "com.atproto.repo.createRecord") {
84
84
+
const input = params.input as {
85
85
+
collection: string;
86
86
+
record: unknown;
87
87
+
};
88
88
+
capturedCollection = input.collection;
89
89
+
capturedRecord = input.record;
90
90
+
91
91
+
return Promise.resolve({
92
92
+
ok: true,
93
93
+
data: {
94
94
+
uri: "at://did:plc:test/app.cistern.lexicon.item/item123" as ResourceUri,
95
95
+
},
96
96
+
});
97
97
+
}
98
98
+
return Promise.resolve({ ok: false, status: 500, data: {} });
99
99
+
},
100
100
+
} as unknown as Client;
101
101
+
102
102
+
const producer = createMockProducer({
103
103
+
rpc: mockRpc,
104
104
+
publicKey: {
105
105
+
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
106
106
+
name: "Test Key",
107
107
+
content: keys.publicKey.toBase64(),
108
108
+
},
109
109
+
});
110
110
+
111
111
+
const uri = await producer.createItem("Test message");
112
112
+
113
113
+
expect(uri).toEqual("at://did:plc:test/app.cistern.lexicon.item/item123");
114
114
+
expect(capturedCollection).toEqual("app.cistern.lexicon.item");
115
115
+
expect(capturedRecord).toMatchObject({
116
116
+
$type: "app.cistern.lexicon.item",
117
117
+
algorithm: "x_wing-xchacha20_poly1305-sha3_512",
118
118
+
});
119
119
+
},
120
120
+
});
121
121
+
122
122
+
Deno.test({
123
123
+
name: "createItem throws when no public key is set",
124
124
+
async fn() {
125
125
+
const producer = createMockProducer();
126
126
+
127
127
+
await expect(producer.createItem("Test message")).rejects.toThrow(
128
128
+
"no public key set; select a public key before creating an item",
129
129
+
);
130
130
+
},
131
131
+
});
132
132
+
133
133
+
Deno.test({
134
134
+
name: "createItem throws when upload fails",
135
135
+
async fn() {
136
136
+
const keys = generateKeys();
137
137
+
const mockRpc = {
138
138
+
post: () =>
139
139
+
Promise.resolve({
140
140
+
ok: false,
141
141
+
status: 500,
142
142
+
data: { error: "Internal Server Error" },
143
143
+
}),
144
144
+
} as unknown as Client;
145
145
+
146
146
+
const producer = createMockProducer({
147
147
+
rpc: mockRpc,
148
148
+
publicKey: {
149
149
+
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
150
150
+
name: "Test Key",
151
151
+
content: keys.publicKey.toBase64(),
152
152
+
},
153
153
+
});
154
154
+
155
155
+
await expect(producer.createItem("Test message")).rejects.toThrow(
156
156
+
"failed to create new item",
157
157
+
);
158
158
+
},
159
159
+
});
160
160
+
161
161
+
Deno.test({
162
162
+
name: "listPublicKeys yields public keys from PDS",
163
163
+
async fn() {
164
164
+
const mockRpc = {
165
165
+
get: (endpoint: string) => {
166
166
+
if (endpoint === "com.atproto.repo.listRecords") {
167
167
+
return Promise.resolve({
168
168
+
ok: true,
169
169
+
data: {
170
170
+
records: [
171
171
+
{
172
172
+
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1",
173
173
+
value: {
174
174
+
$type: "app.cistern.lexicon.pubkey",
175
175
+
name: "Key 1",
176
176
+
algorithm: "x_wing",
177
177
+
content: new Uint8Array(32).toBase64(),
178
178
+
createdAt: new Date().toISOString(),
179
179
+
},
180
180
+
},
181
181
+
{
182
182
+
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key2",
183
183
+
value: {
184
184
+
$type: "app.cistern.lexicon.pubkey",
185
185
+
name: "Key 2",
186
186
+
algorithm: "x_wing",
187
187
+
content: new Uint8Array(32).toBase64(),
188
188
+
createdAt: new Date().toISOString(),
189
189
+
},
190
190
+
},
191
191
+
],
192
192
+
cursor: undefined,
193
193
+
},
194
194
+
});
195
195
+
}
196
196
+
return Promise.resolve({ ok: false, status: 500, data: {} });
197
197
+
},
198
198
+
} as unknown as Client;
199
199
+
200
200
+
const producer = createMockProducer({ rpc: mockRpc });
201
201
+
202
202
+
const keys = [];
203
203
+
for await (const key of producer.listPublicKeys()) {
204
204
+
keys.push(key);
205
205
+
}
206
206
+
207
207
+
expect(keys).toHaveLength(2);
208
208
+
expect(keys[0].name).toEqual("Key 1");
209
209
+
expect(keys[1].name).toEqual("Key 2");
210
210
+
},
211
211
+
});
212
212
+
213
213
+
Deno.test({
214
214
+
name: "listPublicKeys handles pagination",
215
215
+
async fn() {
216
216
+
let callCount = 0;
217
217
+
const mockRpc = {
218
218
+
get: (endpoint: string, params?: { params?: { cursor?: string } }) => {
219
219
+
if (endpoint === "com.atproto.repo.listRecords") {
220
220
+
callCount++;
221
221
+
222
222
+
if (callCount === 1) {
223
223
+
return Promise.resolve({
224
224
+
ok: true,
225
225
+
data: {
226
226
+
records: [
227
227
+
{
228
228
+
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1",
229
229
+
value: {
230
230
+
$type: "app.cistern.lexicon.pubkey",
231
231
+
name: "Key 1",
232
232
+
algorithm: "x_wing",
233
233
+
content: new Uint8Array(32).toBase64(),
234
234
+
createdAt: new Date().toISOString(),
235
235
+
},
236
236
+
},
237
237
+
],
238
238
+
cursor: "next-page",
239
239
+
},
240
240
+
});
241
241
+
} else {
242
242
+
return Promise.resolve({
243
243
+
ok: true,
244
244
+
data: {
245
245
+
records: [
246
246
+
{
247
247
+
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key2",
248
248
+
value: {
249
249
+
$type: "app.cistern.lexicon.pubkey",
250
250
+
name: "Key 2",
251
251
+
algorithm: "x_wing",
252
252
+
content: new Uint8Array(32).toBase64(),
253
253
+
createdAt: new Date().toISOString(),
254
254
+
},
255
255
+
},
256
256
+
],
257
257
+
cursor: undefined,
258
258
+
},
259
259
+
});
260
260
+
}
261
261
+
}
262
262
+
return Promise.resolve({ ok: false, status: 500, data: {} });
263
263
+
},
264
264
+
} as unknown as Client;
265
265
+
266
266
+
const producer = createMockProducer({ rpc: mockRpc });
267
267
+
268
268
+
const keys = [];
269
269
+
for await (const key of producer.listPublicKeys()) {
270
270
+
keys.push(key);
271
271
+
}
272
272
+
273
273
+
expect(keys).toHaveLength(2);
274
274
+
expect(keys[0].name).toEqual("Key 1");
275
275
+
expect(keys[1].name).toEqual("Key 2");
276
276
+
expect(callCount).toEqual(2);
277
277
+
},
278
278
+
});
279
279
+
280
280
+
Deno.test({
281
281
+
name: "listPublicKeys throws when request fails",
282
282
+
async fn() {
283
283
+
const mockRpc = {
284
284
+
get: () =>
285
285
+
Promise.resolve({
286
286
+
ok: false,
287
287
+
status: 401,
288
288
+
data: { error: "Unauthorized" },
289
289
+
}),
290
290
+
} as unknown as Client;
291
291
+
292
292
+
const producer = createMockProducer({ rpc: mockRpc });
293
293
+
294
294
+
const iterator = producer.listPublicKeys();
295
295
+
await expect(iterator.next()).rejects.toThrow("failed to list public keys");
296
296
+
},
297
297
+
});
298
298
+
299
299
+
Deno.test({
300
300
+
name: "selectPublicKey sets the active public key",
301
301
+
fn() {
302
302
+
const producer = createMockProducer();
303
303
+
304
304
+
const mockPublicKey: PublicKeyOption = {
305
305
+
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/key1" as ResourceUri,
306
306
+
name: "Selected Key",
307
307
+
content: new Uint8Array(32).toBase64(),
308
308
+
};
309
309
+
310
310
+
expect(producer.publicKey).toBeUndefined();
311
311
+
312
312
+
producer.selectPublicKey(mockPublicKey);
313
313
+
314
314
+
expect(producer.publicKey).toBeDefined();
315
315
+
expect(producer.publicKey?.uri).toEqual(mockPublicKey.uri);
316
316
+
expect(producer.publicKey?.name).toEqual("Selected Key");
317
317
+
},
318
318
+
});
319
319
+
320
320
+
Deno.test({
321
321
+
name: "selectPublicKey can change the active key",
322
322
+
fn() {
323
323
+
const producer = createMockProducer({
324
324
+
publicKey: {
325
325
+
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/old" as ResourceUri,
326
326
+
name: "Old Key",
327
327
+
content: new Uint8Array(32).toBase64(),
328
328
+
},
329
329
+
});
330
330
+
331
331
+
expect(producer.publicKey?.name).toEqual("Old Key");
332
332
+
333
333
+
const newKey: PublicKeyOption = {
334
334
+
uri: "at://did:plc:test/app.cistern.lexicon.pubkey/new" as ResourceUri,
335
335
+
name: "New Key",
336
336
+
content: new Uint8Array(32).toBase64(),
337
337
+
};
338
338
+
339
339
+
producer.selectPublicKey(newKey);
340
340
+
341
341
+
expect(producer.publicKey?.name).toEqual("New Key");
342
342
+
expect(producer.publicKey?.uri).toEqual(newKey.uri);
343
343
+
},
344
344
+
});