pyric-admin 0.1.0-alpha.11 → 0.1.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md.orig +2 -0
- package/package.json +7 -2
- package/src/app/index.ts +235 -0
- package/src/app/lifecycle.ts +17 -0
- package/src/auth/auth.test.ts +322 -0
- package/src/auth/index.ts +905 -0
- package/src/database/database.test.ts +196 -0
- package/src/database/index.ts +1187 -0
- package/src/firestore/index.ts +100 -0
- package/src/messaging/index.ts +319 -0
- package/src/storage/index.ts +615 -0
- package/src/storage/storage.test.ts +236 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3 + Phase 4b tests for `pyric-admin/database`.
|
|
3
|
+
*
|
|
4
|
+
* Two paths exercised:
|
|
5
|
+
*
|
|
6
|
+
* 1. Sandbox app (`ADMIN_APP_TARGET === 'sandbox'`) → in-memory RTDB
|
|
7
|
+
* backend defined in this package. Tests cover the load-bearing
|
|
8
|
+
* data-plane subset: set/get roundtrip, update merges, push key
|
|
9
|
+
* uniqueness, remove deletes, child returns a sub-ref, reset
|
|
10
|
+
* clears state.
|
|
11
|
+
*
|
|
12
|
+
* 2. Prod app (`ADMIN_APP_TARGET === 'prod'`) → dispatch to
|
|
13
|
+
* `firebase-admin/database`. The module is mocked so the test
|
|
14
|
+
* stays hermetic — no real GCP credentials, no network. Asserting
|
|
15
|
+
* the returned identity (and call shape) proves dispatch happened.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect } from 'bun:test';
|
|
19
|
+
|
|
20
|
+
import { initializeSandbox } from 'pyric/sandbox';
|
|
21
|
+
import type { Sandbox } from 'pyric/sandbox';
|
|
22
|
+
|
|
23
|
+
import { ADMIN_APP_TARGET, type PyricAdminApp } from '../app/index.js';
|
|
24
|
+
|
|
25
|
+
import { getDatabase } from './index.js';
|
|
26
|
+
|
|
27
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/** Build a sandbox-flavored `PyricAdminApp` over a real `Sandbox`. We
|
|
30
|
+
* bypass `initializeApp` so tests don't need to construct credentials
|
|
31
|
+
* for the prod branch — `initializeApp` is exercised by the app tests. */
|
|
32
|
+
function sandboxApp(sandbox: Sandbox): PyricAdminApp {
|
|
33
|
+
return {
|
|
34
|
+
[ADMIN_APP_TARGET]: 'sandbox',
|
|
35
|
+
sandbox,
|
|
36
|
+
name: 'database-test',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Sandbox-backend tests ─────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
describe('pyric-admin/database — sandbox backend (Phase 4b)', () => {
|
|
43
|
+
it('returns a Database handle for a sandbox app', () => {
|
|
44
|
+
const sandbox = initializeSandbox();
|
|
45
|
+
const db = getDatabase(sandboxApp(sandbox));
|
|
46
|
+
expect(db).toBeDefined();
|
|
47
|
+
expect(typeof db.ref).toBe('function');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('set + get roundtrips the written value', async () => {
|
|
51
|
+
const sandbox = initializeSandbox();
|
|
52
|
+
const db = getDatabase(sandboxApp(sandbox));
|
|
53
|
+
const ref = db.ref('/greetings/hello');
|
|
54
|
+
await ref.set({ text: 'hi' });
|
|
55
|
+
const snap = await ref.get();
|
|
56
|
+
expect(snap.exists()).toBe(true);
|
|
57
|
+
expect(snap.val()).toEqual({ text: 'hi' });
|
|
58
|
+
expect(snap.key).toBe('hello');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('get on an absent path returns a snapshot where exists() is false', async () => {
|
|
62
|
+
const sandbox = initializeSandbox();
|
|
63
|
+
const db = getDatabase(sandboxApp(sandbox));
|
|
64
|
+
const snap = await db.ref('/nope').get();
|
|
65
|
+
expect(snap.exists()).toBe(false);
|
|
66
|
+
expect(snap.val()).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('update merges children (does not replace the whole object)', async () => {
|
|
70
|
+
const sandbox = initializeSandbox();
|
|
71
|
+
const db = getDatabase(sandboxApp(sandbox));
|
|
72
|
+
const ref = db.ref('/user');
|
|
73
|
+
await ref.set({ name: 'Ada', age: 36 });
|
|
74
|
+
await ref.update({ age: 37, city: 'London' });
|
|
75
|
+
const snap = await ref.get();
|
|
76
|
+
expect(snap.val()).toEqual({ name: 'Ada', age: 37, city: 'London' });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('update with a null value removes the corresponding child', async () => {
|
|
80
|
+
const sandbox = initializeSandbox();
|
|
81
|
+
const db = getDatabase(sandboxApp(sandbox));
|
|
82
|
+
const ref = db.ref('/user');
|
|
83
|
+
await ref.set({ name: 'Ada', age: 36 });
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
+
await ref.update({ age: null } as any);
|
|
86
|
+
const snap = await ref.get();
|
|
87
|
+
expect(snap.val()).toEqual({ name: 'Ada' });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('push generates a 20-char, lex-sortable key and writes the value', async () => {
|
|
91
|
+
const sandbox = initializeSandbox();
|
|
92
|
+
const db = getDatabase(sandboxApp(sandbox));
|
|
93
|
+
const listRef = db.ref('/posts');
|
|
94
|
+
|
|
95
|
+
const r1 = listRef.push({ title: 'first' });
|
|
96
|
+
const r2 = listRef.push({ title: 'second' });
|
|
97
|
+
|
|
98
|
+
expect(r1.key).toBeTypeOf('string');
|
|
99
|
+
expect(r1.key!.length).toBe(20);
|
|
100
|
+
expect(r2.key).toBeTypeOf('string');
|
|
101
|
+
expect(r2.key!.length).toBe(20);
|
|
102
|
+
// Keys must be distinct.
|
|
103
|
+
expect(r1.key).not.toBe(r2.key);
|
|
104
|
+
// And lex-sortable in mint order — that's the whole point of the
|
|
105
|
+
// push-id format.
|
|
106
|
+
expect(r1.key! < r2.key!).toBe(true);
|
|
107
|
+
|
|
108
|
+
// Values landed at the minted paths.
|
|
109
|
+
const snap1 = await r1.get();
|
|
110
|
+
expect(snap1.val()).toEqual({ title: 'first' });
|
|
111
|
+
const snap2 = await r2.get();
|
|
112
|
+
expect(snap2.val()).toEqual({ title: 'second' });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('push without a value mints a key but does not write', async () => {
|
|
116
|
+
const sandbox = initializeSandbox();
|
|
117
|
+
const db = getDatabase(sandboxApp(sandbox));
|
|
118
|
+
const listRef = db.ref('/items');
|
|
119
|
+
const r = listRef.push();
|
|
120
|
+
expect(r.key).toBeTypeOf('string');
|
|
121
|
+
// Path is empty — get() should report no value.
|
|
122
|
+
const snap = await r.get();
|
|
123
|
+
expect(snap.exists()).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('remove deletes the subtree', async () => {
|
|
127
|
+
const sandbox = initializeSandbox();
|
|
128
|
+
const db = getDatabase(sandboxApp(sandbox));
|
|
129
|
+
const ref = db.ref('/temp');
|
|
130
|
+
await ref.set({ a: 1, b: 2 });
|
|
131
|
+
await ref.remove();
|
|
132
|
+
const snap = await ref.get();
|
|
133
|
+
expect(snap.exists()).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('child returns a sub-ref at the joined path', async () => {
|
|
137
|
+
const sandbox = initializeSandbox();
|
|
138
|
+
const db = getDatabase(sandboxApp(sandbox));
|
|
139
|
+
const userRef = db.ref('/users/alice');
|
|
140
|
+
const nameRef = userRef.child('profile/name');
|
|
141
|
+
expect(nameRef.key).toBe('name');
|
|
142
|
+
await nameRef.set('Alice');
|
|
143
|
+
const snap = await db.ref('/users/alice/profile/name').get();
|
|
144
|
+
expect(snap.val()).toBe('Alice');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('sandbox.reset() clears the database state', async () => {
|
|
148
|
+
const sandbox = initializeSandbox();
|
|
149
|
+
const db = getDatabase(sandboxApp(sandbox));
|
|
150
|
+
await db.ref('/k').set('v');
|
|
151
|
+
expect((await db.ref('/k').get()).val()).toBe('v');
|
|
152
|
+
|
|
153
|
+
sandbox.reset();
|
|
154
|
+
|
|
155
|
+
const snap = await db.ref('/k').get();
|
|
156
|
+
expect(snap.exists()).toBe(false);
|
|
157
|
+
expect(snap.val()).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('successive getDatabase calls on the same sandbox share data', async () => {
|
|
161
|
+
const sandbox = initializeSandbox();
|
|
162
|
+
const app = sandboxApp(sandbox);
|
|
163
|
+
const db1 = getDatabase(app);
|
|
164
|
+
const db2 = getDatabase(app);
|
|
165
|
+
await db1.ref('/shared').set(42);
|
|
166
|
+
const snap = await db2.ref('/shared').get();
|
|
167
|
+
expect(snap.val()).toBe(42);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('snapshot forEach iterates children', async () => {
|
|
171
|
+
const sandbox = initializeSandbox();
|
|
172
|
+
const db = getDatabase(sandboxApp(sandbox));
|
|
173
|
+
await db.ref('/letters').set({ a: 1, b: 2, c: 3 });
|
|
174
|
+
const snap = await db.ref('/letters').get();
|
|
175
|
+
const seen: Array<{ key: string | null; val: unknown }> = [];
|
|
176
|
+
snap.forEach((child) => {
|
|
177
|
+
seen.push({ key: child.key, val: child.val() });
|
|
178
|
+
});
|
|
179
|
+
expect(seen).toHaveLength(3);
|
|
180
|
+
expect(seen).toContainEqual({ key: 'a', val: 1 });
|
|
181
|
+
expect(seen).toContainEqual({ key: 'b', val: 2 });
|
|
182
|
+
expect(seen).toContainEqual({ key: 'c', val: 3 });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('listeners (on/off) and transactions throw "not implemented"', async () => {
|
|
186
|
+
const sandbox = initializeSandbox();
|
|
187
|
+
const db = getDatabase(sandboxApp(sandbox));
|
|
188
|
+
const ref = db.ref('/x');
|
|
189
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
190
|
+
expect(() => (ref as any).on('value', () => {})).toThrow(/not implemented/);
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
192
|
+
expect(() => (ref as any).transaction(() => 1)).toThrow(/not implemented/);
|
|
193
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
194
|
+
expect(() => (ref as any).onDisconnect()).toThrow(/not implemented/);
|
|
195
|
+
});
|
|
196
|
+
});
|