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