-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathprovider-surface.test.js
More file actions
158 lines (139 loc) · 6.25 KB
/
Copy pathprovider-surface.test.js
File metadata and controls
158 lines (139 loc) · 6.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/**
* Tests for the NIP-07 provider surface (nostr-provider.js).
*
* The provider must advertise ONLY what it implements, so that page-side
* feature-detection is truthful:
* - exposes getPublicKey, signEvent, nip44.{encrypt,decrypt}
* - does NOT expose nip04 (deprecated, unauthenticated; not shipped)
* - does NOT expose getRelays (Podkey holds no relay list)
*
* nostr-provider.js is a page-context IIFE that assigns window.nostr, so the
* test mounts a minimal window (EventTarget-backed) and imports the module to
* run it, then inspects the installed surface and its request wiring.
*/
import { describe, it, before } from 'node:test';
import assert from 'node:assert/strict';
let nostr;
const events = new EventTarget();
before(async () => {
global.window = {
addEventListener: (...a) => events.addEventListener(...a),
removeEventListener: (...a) => events.removeEventListener(...a),
dispatchEvent: (e) => events.dispatchEvent(e)
};
// Running the IIFE installs window.nostr.
await import('../src/nostr-provider.js');
nostr = global.window.nostr;
});
describe('provider surface (honest feature-detection)', () => {
it('exposes exactly getPublicKey, signEvent and nip44', () => {
assert.deepEqual(Object.keys(nostr).sort(), ['getPublicKey', 'nip44', 'signEvent']);
});
it('exposes nip44.encrypt and nip44.decrypt as functions', () => {
assert.equal(typeof nostr.nip44.encrypt, 'function');
assert.equal(typeof nostr.nip44.decrypt, 'function');
});
it('does NOT expose nip04 (deprecated scheme is not shipped)', () => {
assert.equal('nip04' in nostr, false);
assert.equal(nostr.nip04, undefined);
});
it('does NOT expose getRelays (no relay list is held)', () => {
assert.equal('getRelays' in nostr, false);
assert.equal(nostr.getRelays, undefined);
});
it('getPublicKey and signEvent are async functions', () => {
assert.equal(typeof nostr.getPublicKey, 'function');
assert.equal(typeof nostr.signEvent, 'function');
});
});
describe('signEvent input validation (before any signing)', () => {
// These reject synchronously inside the provider, never reaching the wire,
// so no response listener is needed.
it('rejects a non-object event', async () => {
await assert.rejects(() => nostr.signEvent(null), /Event must be an object/);
await assert.rejects(() => nostr.signEvent('nope'), /Event must be an object/);
});
it('rejects a missing/typed kind', async () => {
await assert.rejects(() => nostr.signEvent({ created_at: 1, tags: [], content: '' }), /kind must be a number/);
});
it('rejects a missing created_at', async () => {
await assert.rejects(() => nostr.signEvent({ kind: 1, tags: [], content: '' }), /created_at must be a number/);
});
it('rejects non-array tags', async () => {
await assert.rejects(() => nostr.signEvent({ kind: 1, created_at: 1, tags: 'x', content: '' }), /tags must be an array/);
});
it('rejects non-string content', async () => {
await assert.rejects(() => nostr.signEvent({ kind: 1, created_at: 1, tags: [], content: 5 }), /content must be a string/);
});
});
describe('provider request wiring (podkey-request CustomEvent)', () => {
/**
* Capture the next podkey-request the provider emits, reply to it with the
* matching id, and return the request detail for assertions.
*/
function captureRequest (reply) {
return new Promise((resolve) => {
const handler = (e) => {
global.window.removeEventListener('podkey-request', handler);
const detail = e.detail;
// Respond on the next tick so the provider's listener is registered.
queueMicrotask(() => {
global.window.dispatchEvent(new CustomEvent('podkey-response', {
detail: { id: detail.id, result: reply(detail) }
}));
});
resolve(detail);
};
global.window.addEventListener('podkey-request', handler);
});
}
it('getPublicKey dispatches a GET_PUBLIC_KEY request and resolves the response', async () => {
const reqP = captureRequest(() => 'deadbeef'.repeat(8));
const resP = nostr.getPublicKey();
const req = await reqP;
assert.equal(req.type, 'GET_PUBLIC_KEY');
assert.ok(req.id, 'request must carry a correlation id');
assert.equal(await resP, 'deadbeef'.repeat(8));
});
it('signEvent forwards the validated event under SIGN_EVENT', async () => {
const event = { kind: 1, created_at: 1700000000, tags: [['t', 'x']], content: 'gm' };
const reqP = captureRequest((d) => ({ ...d.event, id: 'a'.repeat(64), pubkey: 'b'.repeat(64), sig: 'c'.repeat(128) }));
const resP = nostr.signEvent(event);
const req = await reqP;
assert.equal(req.type, 'SIGN_EVENT');
assert.deepEqual(req.event, event);
const signed = await resP;
assert.equal(signed.sig.length, 128);
});
it('nip44.encrypt dispatches NIP44_ENCRYPT with pubkey + plaintext (field is "pubkey")', async () => {
const reqP = captureRequest(() => 'BASE64PAYLOAD');
const resP = nostr.nip44.encrypt('f'.repeat(64), 'secret');
const req = await reqP;
assert.equal(req.type, 'NIP44_ENCRYPT');
assert.equal(req.pubkey, 'f'.repeat(64));
assert.equal(req.plaintext, 'secret');
assert.equal('peer' in req, false, 'the reconciled field name is "pubkey", not "peer"');
assert.equal(await resP, 'BASE64PAYLOAD');
});
it('nip44.decrypt dispatches NIP44_DECRYPT with pubkey + ciphertext', async () => {
const reqP = captureRequest(() => 'plaintext-out');
const resP = nostr.nip44.decrypt('f'.repeat(64), 'BASE64PAYLOAD');
const req = await reqP;
assert.equal(req.type, 'NIP44_DECRYPT');
assert.equal(req.pubkey, 'f'.repeat(64));
assert.equal(req.ciphertext, 'BASE64PAYLOAD');
assert.equal(await resP, 'plaintext-out');
});
it('propagates a background error back to the caller as a rejection', async () => {
const handler = (e) => {
global.window.removeEventListener('podkey-request', handler);
queueMicrotask(() => {
global.window.dispatchEvent(new CustomEvent('podkey-response', {
detail: { id: e.detail.id, error: 'User denied permission' }
}));
});
};
global.window.addEventListener('podkey-request', handler);
await assert.rejects(() => nostr.getPublicKey(), /User denied permission/);
});
});