@flowerforce/flowerbase-client 0.1.0
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/CHANGELOG.md +0 -0
- package/LICENSE +3 -0
- package/README.md +209 -0
- package/dist/app.d.ts +85 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +461 -0
- package/dist/bson.d.ts +8 -0
- package/dist/bson.d.ts.map +1 -0
- package/dist/bson.js +10 -0
- package/dist/credentials.d.ts +8 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +30 -0
- package/dist/functions.d.ts +6 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +47 -0
- package/dist/http.d.ts +35 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +170 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/mongo.d.ts +4 -0
- package/dist/mongo.d.ts.map +1 -0
- package/dist/mongo.js +106 -0
- package/dist/session.d.ts +18 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +105 -0
- package/dist/session.native.d.ts +14 -0
- package/dist/session.native.d.ts.map +1 -0
- package/dist/session.native.js +76 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/user.d.ts +37 -0
- package/dist/user.d.ts.map +1 -0
- package/dist/user.js +125 -0
- package/dist/watch.d.ts +3 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +139 -0
- package/jest.config.ts +13 -0
- package/package.json +41 -0
- package/project.json +11 -0
- package/rollup.config.js +17 -0
- package/src/__tests__/auth.test.ts +213 -0
- package/src/__tests__/compat.test.ts +22 -0
- package/src/__tests__/functions.test.ts +312 -0
- package/src/__tests__/mongo.test.ts +83 -0
- package/src/__tests__/session.test.ts +597 -0
- package/src/__tests__/watch.test.ts +336 -0
- package/src/app.ts +562 -0
- package/src/bson.ts +6 -0
- package/src/credentials.ts +31 -0
- package/src/functions.ts +56 -0
- package/src/http.ts +221 -0
- package/src/index.ts +15 -0
- package/src/mongo.ts +112 -0
- package/src/session.native.ts +89 -0
- package/src/session.ts +114 -0
- package/src/types.ts +114 -0
- package/src/user.ts +150 -0
- package/src/watch.ts +156 -0
- package/tsconfig.json +34 -0
- package/tsconfig.spec.json +13 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { App } from '../app'
|
|
2
|
+
import { Credentials } from '../credentials'
|
|
3
|
+
import { MongoDBRealmError } from '../http'
|
|
4
|
+
|
|
5
|
+
const encodeBase64Url = (value: string) =>
|
|
6
|
+
Buffer.from(value, 'utf8').toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
|
|
7
|
+
|
|
8
|
+
const buildJwt = (payload: Record<string, unknown>) => `header.${encodeBase64Url(JSON.stringify(payload))}.signature`
|
|
9
|
+
|
|
10
|
+
describe('flowerbase-client session', () => {
|
|
11
|
+
const originalFetch = global.fetch
|
|
12
|
+
const originalLocalStorage = (globalThis as typeof globalThis & { localStorage?: unknown }).localStorage
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
global.fetch = originalFetch
|
|
16
|
+
if (typeof originalLocalStorage === 'undefined') {
|
|
17
|
+
Reflect.deleteProperty(globalThis, 'localStorage')
|
|
18
|
+
} else {
|
|
19
|
+
Object.defineProperty(globalThis, 'localStorage', {
|
|
20
|
+
configurable: true,
|
|
21
|
+
value: originalLocalStorage
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('refreshes access token', async () => {
|
|
27
|
+
global.fetch = jest
|
|
28
|
+
.fn()
|
|
29
|
+
.mockResolvedValueOnce({
|
|
30
|
+
ok: true,
|
|
31
|
+
text: async () => JSON.stringify({
|
|
32
|
+
access_token: 'access',
|
|
33
|
+
refresh_token: 'refresh',
|
|
34
|
+
user_id: 'user-1'
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
.mockResolvedValueOnce({
|
|
38
|
+
ok: true,
|
|
39
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
40
|
+
})
|
|
41
|
+
.mockResolvedValueOnce({
|
|
42
|
+
ok: true,
|
|
43
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
44
|
+
})
|
|
45
|
+
.mockResolvedValueOnce({
|
|
46
|
+
ok: true,
|
|
47
|
+
text: async () => JSON.stringify({ access_token: 'access-3' })
|
|
48
|
+
})
|
|
49
|
+
.mockResolvedValue({
|
|
50
|
+
ok: true,
|
|
51
|
+
text: async () => JSON.stringify({ access_token: 'access-fallback' })
|
|
52
|
+
}) as unknown as typeof fetch
|
|
53
|
+
|
|
54
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
55
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
56
|
+
|
|
57
|
+
const token = await app.currentUser!.refreshAccessToken()
|
|
58
|
+
expect(token).toBe('access-2')
|
|
59
|
+
expect(global.fetch).toHaveBeenLastCalledWith(
|
|
60
|
+
'http://localhost:3000/api/client/v2.0/auth/session',
|
|
61
|
+
expect.objectContaining({
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: expect.objectContaining({ Authorization: 'Bearer refresh' })
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('clears session when refresh fails', async () => {
|
|
69
|
+
global.fetch = jest
|
|
70
|
+
.fn()
|
|
71
|
+
.mockResolvedValueOnce({
|
|
72
|
+
ok: true,
|
|
73
|
+
text: async () => JSON.stringify({
|
|
74
|
+
access_token: 'access',
|
|
75
|
+
refresh_token: 'refresh',
|
|
76
|
+
user_id: 'user-1'
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
.mockResolvedValueOnce({
|
|
80
|
+
ok: true,
|
|
81
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
82
|
+
})
|
|
83
|
+
.mockResolvedValueOnce({
|
|
84
|
+
ok: false,
|
|
85
|
+
status: 401,
|
|
86
|
+
text: async () => JSON.stringify({ message: 'Invalid refresh token provided' })
|
|
87
|
+
}) as unknown as typeof fetch
|
|
88
|
+
|
|
89
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
90
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
91
|
+
|
|
92
|
+
await expect(app.currentUser!.refreshAccessToken()).rejects.toThrow('Invalid refresh token provided')
|
|
93
|
+
expect(app.currentUser).toBeNull()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('revokes session on logout', async () => {
|
|
97
|
+
global.fetch = jest
|
|
98
|
+
.fn()
|
|
99
|
+
.mockResolvedValueOnce({
|
|
100
|
+
ok: true,
|
|
101
|
+
text: async () => JSON.stringify({
|
|
102
|
+
access_token: 'access',
|
|
103
|
+
refresh_token: 'refresh',
|
|
104
|
+
user_id: 'user-1'
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
.mockResolvedValueOnce({
|
|
108
|
+
ok: true,
|
|
109
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
110
|
+
})
|
|
111
|
+
.mockResolvedValueOnce({
|
|
112
|
+
ok: true,
|
|
113
|
+
text: async () => JSON.stringify({ status: 'ok' })
|
|
114
|
+
}) as unknown as typeof fetch
|
|
115
|
+
|
|
116
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
117
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
118
|
+
await app.currentUser!.logOut()
|
|
119
|
+
|
|
120
|
+
expect(global.fetch).toHaveBeenLastCalledWith(
|
|
121
|
+
'http://localhost:3000/api/client/v2.0/auth/session',
|
|
122
|
+
expect.objectContaining({ method: 'DELETE' })
|
|
123
|
+
)
|
|
124
|
+
expect(app.currentUser).toBeNull()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('retries function call after access token 401', async () => {
|
|
128
|
+
global.fetch = jest
|
|
129
|
+
.fn()
|
|
130
|
+
.mockResolvedValueOnce({
|
|
131
|
+
ok: true,
|
|
132
|
+
text: async () =>
|
|
133
|
+
JSON.stringify({
|
|
134
|
+
access_token: 'access',
|
|
135
|
+
refresh_token: 'refresh',
|
|
136
|
+
user_id: 'user-1'
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
.mockResolvedValueOnce({
|
|
140
|
+
ok: true,
|
|
141
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
142
|
+
})
|
|
143
|
+
.mockResolvedValueOnce({
|
|
144
|
+
ok: false,
|
|
145
|
+
status: 401,
|
|
146
|
+
text: async () => JSON.stringify({ message: 'token expired' })
|
|
147
|
+
})
|
|
148
|
+
.mockResolvedValueOnce({
|
|
149
|
+
ok: true,
|
|
150
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
151
|
+
})
|
|
152
|
+
.mockResolvedValueOnce({
|
|
153
|
+
ok: true,
|
|
154
|
+
text: async () => JSON.stringify({ result: 42 })
|
|
155
|
+
}) as unknown as typeof fetch
|
|
156
|
+
|
|
157
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
158
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
159
|
+
|
|
160
|
+
const result = await app.currentUser!.functions.sum(40, 2)
|
|
161
|
+
expect(result).toEqual({ result: 42 })
|
|
162
|
+
expect((global.fetch as jest.Mock).mock.calls[3][0]).toBe('http://localhost:3000/api/client/v2.0/auth/session')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('tracks users in allUsers and supports switch/remove', async () => {
|
|
166
|
+
global.fetch = jest
|
|
167
|
+
.fn()
|
|
168
|
+
.mockResolvedValueOnce({
|
|
169
|
+
ok: true,
|
|
170
|
+
text: async () => JSON.stringify({
|
|
171
|
+
access_token: 'access',
|
|
172
|
+
refresh_token: 'refresh',
|
|
173
|
+
user_id: 'user-1'
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
.mockResolvedValueOnce({
|
|
177
|
+
ok: true,
|
|
178
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
179
|
+
})
|
|
180
|
+
.mockResolvedValueOnce({
|
|
181
|
+
ok: true,
|
|
182
|
+
text: async () => JSON.stringify({ status: 'ok' })
|
|
183
|
+
}) as unknown as typeof fetch
|
|
184
|
+
|
|
185
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
186
|
+
const user = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
187
|
+
|
|
188
|
+
expect(Object.keys(app.allUsers)).toContain('user-1')
|
|
189
|
+
app.switchUser(user)
|
|
190
|
+
|
|
191
|
+
await app.removeUser(user)
|
|
192
|
+
expect(app.currentUser).toBeNull()
|
|
193
|
+
expect(Object.keys(app.allUsers)).not.toContain('user-1')
|
|
194
|
+
expect(user.state).toBe('removed')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('exposes providerType, customData and identities', async () => {
|
|
198
|
+
const accessToken = buildJwt({ user_data: { plan: 'pro' } })
|
|
199
|
+
global.fetch = jest
|
|
200
|
+
.fn()
|
|
201
|
+
.mockResolvedValueOnce({
|
|
202
|
+
ok: true,
|
|
203
|
+
text: async () =>
|
|
204
|
+
JSON.stringify({
|
|
205
|
+
access_token: accessToken,
|
|
206
|
+
refresh_token: 'refresh',
|
|
207
|
+
user_id: 'user-1'
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
.mockResolvedValueOnce({
|
|
211
|
+
ok: true,
|
|
212
|
+
text: async () => JSON.stringify({ access_token: accessToken })
|
|
213
|
+
})
|
|
214
|
+
.mockResolvedValueOnce({
|
|
215
|
+
ok: true,
|
|
216
|
+
text: async () =>
|
|
217
|
+
JSON.stringify({
|
|
218
|
+
data: { email: 'john@doe.com' },
|
|
219
|
+
identities: [{ id: 'identity-1', provider_type: 'local-userpass' }],
|
|
220
|
+
custom_data: { plan: 'pro' }
|
|
221
|
+
})
|
|
222
|
+
}) as unknown as typeof fetch
|
|
223
|
+
|
|
224
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
225
|
+
const user = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
226
|
+
await user.refreshCustomData()
|
|
227
|
+
|
|
228
|
+
expect(user.providerType).toBe('local-userpass')
|
|
229
|
+
expect(user.customData).toEqual({ plan: 'pro' })
|
|
230
|
+
expect(user.identities).toEqual([{ id: 'identity-1', provider_type: 'local-userpass' }])
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('throws MongoDBRealmError with status metadata', async () => {
|
|
234
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
235
|
+
ok: false,
|
|
236
|
+
status: 401,
|
|
237
|
+
statusText: 'Unauthorized',
|
|
238
|
+
text: async () => JSON.stringify({ error: 'Unauthorized', error_code: 'InvalidSession' })
|
|
239
|
+
}) as unknown as typeof fetch
|
|
240
|
+
|
|
241
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
242
|
+
await expect(app.logIn(Credentials.anonymous())).rejects.toBeInstanceOf(MongoDBRealmError)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('orders allUsers as active first then logged-out and persists order', async () => {
|
|
246
|
+
const storage = new Map<string, string>()
|
|
247
|
+
Object.defineProperty(globalThis, 'localStorage', {
|
|
248
|
+
configurable: true,
|
|
249
|
+
value: {
|
|
250
|
+
getItem: (key: string) => storage.get(key) ?? null,
|
|
251
|
+
setItem: (key: string, value: string) => {
|
|
252
|
+
storage.set(key, value)
|
|
253
|
+
},
|
|
254
|
+
removeItem: (key: string) => {
|
|
255
|
+
storage.delete(key)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
global.fetch = jest
|
|
261
|
+
.fn()
|
|
262
|
+
.mockResolvedValueOnce({
|
|
263
|
+
ok: true,
|
|
264
|
+
text: async () => JSON.stringify({
|
|
265
|
+
access_token: 'access-1',
|
|
266
|
+
refresh_token: 'refresh-1',
|
|
267
|
+
user_id: 'user-1'
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
.mockResolvedValueOnce({
|
|
271
|
+
ok: true,
|
|
272
|
+
text: async () => JSON.stringify({ access_token: 'access-1b' })
|
|
273
|
+
})
|
|
274
|
+
.mockResolvedValueOnce({
|
|
275
|
+
ok: true,
|
|
276
|
+
text: async () => JSON.stringify({ status: 'ok' })
|
|
277
|
+
})
|
|
278
|
+
.mockResolvedValueOnce({
|
|
279
|
+
ok: true,
|
|
280
|
+
text: async () => JSON.stringify({
|
|
281
|
+
access_token: 'access-2',
|
|
282
|
+
refresh_token: 'refresh-2',
|
|
283
|
+
user_id: 'user-2'
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
.mockResolvedValueOnce({
|
|
287
|
+
ok: true,
|
|
288
|
+
text: async () => JSON.stringify({ access_token: 'access-2b' })
|
|
289
|
+
}) as unknown as typeof fetch
|
|
290
|
+
|
|
291
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
292
|
+
const user1 = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
293
|
+
await user1.logOut()
|
|
294
|
+
const user2 = await app.logIn(Credentials.anonymous())
|
|
295
|
+
|
|
296
|
+
expect(app.currentUser?.id).toBe('user-2')
|
|
297
|
+
expect(Object.keys(app.allUsers)).toEqual(['user-2', 'user-1'])
|
|
298
|
+
expect(app.allUsers['user-1']?.state).toBe('logged-out')
|
|
299
|
+
expect(user2.state).toBe('active')
|
|
300
|
+
|
|
301
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
302
|
+
ok: true,
|
|
303
|
+
text: async () => JSON.stringify({ access_token: 'access-2c' })
|
|
304
|
+
}) as unknown as typeof fetch
|
|
305
|
+
|
|
306
|
+
const appReloaded = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
307
|
+
await appReloaded.getProfile().catch(() => undefined)
|
|
308
|
+
|
|
309
|
+
expect(Object.keys(appReloaded.allUsers)).toEqual(['user-2', 'user-1'])
|
|
310
|
+
expect(appReloaded.currentUser?.id).toBe('user-2')
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('switchUser changes active session used by app calls', async () => {
|
|
314
|
+
global.fetch = jest
|
|
315
|
+
.fn()
|
|
316
|
+
.mockResolvedValueOnce({
|
|
317
|
+
ok: true,
|
|
318
|
+
text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
|
|
319
|
+
})
|
|
320
|
+
.mockResolvedValueOnce({
|
|
321
|
+
ok: true,
|
|
322
|
+
text: async () => JSON.stringify({ access_token: 'access-1' })
|
|
323
|
+
})
|
|
324
|
+
.mockResolvedValueOnce({
|
|
325
|
+
ok: true,
|
|
326
|
+
text: async () => JSON.stringify({ access_token: 'login-a2', refresh_token: 'refresh-2', user_id: 'user-2' })
|
|
327
|
+
})
|
|
328
|
+
.mockResolvedValueOnce({
|
|
329
|
+
ok: true,
|
|
330
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
331
|
+
})
|
|
332
|
+
.mockResolvedValue({
|
|
333
|
+
ok: true,
|
|
334
|
+
text: async () => JSON.stringify({ ok: true })
|
|
335
|
+
}) as unknown as typeof fetch
|
|
336
|
+
|
|
337
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
338
|
+
const user1 = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
339
|
+
await app.logIn(Credentials.anonymous())
|
|
340
|
+
|
|
341
|
+
await app.callFunction('firstCall', [])
|
|
342
|
+
let request = (global.fetch as jest.Mock).mock.calls[4][1]
|
|
343
|
+
expect(request.headers.Authorization).toBe('Bearer access-2')
|
|
344
|
+
|
|
345
|
+
app.switchUser(user1)
|
|
346
|
+
await app.callFunction('secondCall', [])
|
|
347
|
+
request = (global.fetch as jest.Mock).mock.calls[5][1]
|
|
348
|
+
expect(request.headers.Authorization).toBe('Bearer access-1')
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('notifies app listeners on login, switch and logout', async () => {
|
|
352
|
+
global.fetch = jest
|
|
353
|
+
.fn()
|
|
354
|
+
.mockResolvedValueOnce({
|
|
355
|
+
ok: true,
|
|
356
|
+
text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
|
|
357
|
+
})
|
|
358
|
+
.mockResolvedValueOnce({
|
|
359
|
+
ok: true,
|
|
360
|
+
text: async () => JSON.stringify({ access_token: 'access-1' })
|
|
361
|
+
})
|
|
362
|
+
.mockResolvedValueOnce({
|
|
363
|
+
ok: true,
|
|
364
|
+
text: async () => JSON.stringify({ access_token: 'login-a2', refresh_token: 'refresh-2', user_id: 'user-2' })
|
|
365
|
+
})
|
|
366
|
+
.mockResolvedValueOnce({
|
|
367
|
+
ok: true,
|
|
368
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
369
|
+
})
|
|
370
|
+
.mockResolvedValueOnce({
|
|
371
|
+
ok: true,
|
|
372
|
+
text: async () => JSON.stringify({ status: 'ok' })
|
|
373
|
+
}) as unknown as typeof fetch
|
|
374
|
+
|
|
375
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
376
|
+
const appListener = jest.fn()
|
|
377
|
+
app.addListener(appListener)
|
|
378
|
+
|
|
379
|
+
const user1 = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
380
|
+
await app.logIn(Credentials.anonymous())
|
|
381
|
+
app.switchUser(user1)
|
|
382
|
+
await app.logoutUser()
|
|
383
|
+
|
|
384
|
+
expect(appListener).toHaveBeenCalledTimes(4)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('notifies user listeners on token refresh and custom data refresh', async () => {
|
|
388
|
+
global.fetch = jest
|
|
389
|
+
.fn()
|
|
390
|
+
.mockResolvedValueOnce({
|
|
391
|
+
ok: true,
|
|
392
|
+
text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
|
|
393
|
+
})
|
|
394
|
+
.mockResolvedValueOnce({
|
|
395
|
+
ok: true,
|
|
396
|
+
text: async () => JSON.stringify({ access_token: 'access-1' })
|
|
397
|
+
})
|
|
398
|
+
.mockResolvedValueOnce({
|
|
399
|
+
ok: true,
|
|
400
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
401
|
+
})
|
|
402
|
+
.mockResolvedValueOnce({
|
|
403
|
+
ok: true,
|
|
404
|
+
text: async () => JSON.stringify({ data: {}, custom_data: { x: 1 }, identities: [] })
|
|
405
|
+
}) as unknown as typeof fetch
|
|
406
|
+
|
|
407
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
408
|
+
const user = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
409
|
+
const listener = jest.fn()
|
|
410
|
+
user.addListener(listener)
|
|
411
|
+
|
|
412
|
+
await user.refreshAccessToken()
|
|
413
|
+
await user.refreshCustomData()
|
|
414
|
+
|
|
415
|
+
expect(listener).toHaveBeenCalledTimes(2)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('supports removing app and user listeners', async () => {
|
|
419
|
+
global.fetch = jest
|
|
420
|
+
.fn()
|
|
421
|
+
.mockResolvedValueOnce({
|
|
422
|
+
ok: true,
|
|
423
|
+
text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
|
|
424
|
+
})
|
|
425
|
+
.mockResolvedValueOnce({
|
|
426
|
+
ok: true,
|
|
427
|
+
text: async () => JSON.stringify({ access_token: 'access-1' })
|
|
428
|
+
})
|
|
429
|
+
.mockResolvedValueOnce({
|
|
430
|
+
ok: true,
|
|
431
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
432
|
+
})
|
|
433
|
+
.mockResolvedValueOnce({
|
|
434
|
+
ok: true,
|
|
435
|
+
text: async () => JSON.stringify({ access_token: 'access-3' })
|
|
436
|
+
})
|
|
437
|
+
.mockResolvedValue({
|
|
438
|
+
ok: true,
|
|
439
|
+
text: async () => JSON.stringify({ access_token: 'access-fallback' })
|
|
440
|
+
}) as unknown as typeof fetch
|
|
441
|
+
|
|
442
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
443
|
+
const appListener = jest.fn()
|
|
444
|
+
app.addListener(appListener)
|
|
445
|
+
app.removeListener(appListener)
|
|
446
|
+
|
|
447
|
+
const user = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
448
|
+
expect(appListener).not.toHaveBeenCalled()
|
|
449
|
+
|
|
450
|
+
const userListener = jest.fn()
|
|
451
|
+
user.addListener(userListener)
|
|
452
|
+
user.removeAllListeners()
|
|
453
|
+
await user.refreshAccessToken()
|
|
454
|
+
|
|
455
|
+
expect(userListener).not.toHaveBeenCalled()
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('emits app listener notifications in deterministic user order', async () => {
|
|
459
|
+
global.fetch = jest
|
|
460
|
+
.fn()
|
|
461
|
+
.mockResolvedValueOnce({
|
|
462
|
+
ok: true,
|
|
463
|
+
text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
|
|
464
|
+
})
|
|
465
|
+
.mockResolvedValueOnce({
|
|
466
|
+
ok: true,
|
|
467
|
+
text: async () => JSON.stringify({ access_token: 'access-1' })
|
|
468
|
+
})
|
|
469
|
+
.mockResolvedValueOnce({
|
|
470
|
+
ok: true,
|
|
471
|
+
text: async () => JSON.stringify({ access_token: 'login-a2', refresh_token: 'refresh-2', user_id: 'user-2' })
|
|
472
|
+
})
|
|
473
|
+
.mockResolvedValueOnce({
|
|
474
|
+
ok: true,
|
|
475
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
476
|
+
})
|
|
477
|
+
.mockResolvedValueOnce({
|
|
478
|
+
ok: true,
|
|
479
|
+
text: async () => JSON.stringify({ status: 'ok' })
|
|
480
|
+
}) as unknown as typeof fetch
|
|
481
|
+
|
|
482
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
483
|
+
const events: string[] = []
|
|
484
|
+
app.addListener(() => {
|
|
485
|
+
events.push(app.currentUser?.id ?? 'null')
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
const user1 = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
489
|
+
await app.logIn(Credentials.anonymous())
|
|
490
|
+
app.switchUser(user1)
|
|
491
|
+
await app.logoutUser()
|
|
492
|
+
|
|
493
|
+
expect(events).toEqual(['user-1', 'user-2', 'user-1', 'user-2'])
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it('notifies only the target user listener for user-scoped operations', async () => {
|
|
497
|
+
global.fetch = jest
|
|
498
|
+
.fn()
|
|
499
|
+
.mockResolvedValueOnce({
|
|
500
|
+
ok: true,
|
|
501
|
+
text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
|
|
502
|
+
})
|
|
503
|
+
.mockResolvedValueOnce({
|
|
504
|
+
ok: true,
|
|
505
|
+
text: async () => JSON.stringify({ access_token: 'access-1' })
|
|
506
|
+
})
|
|
507
|
+
.mockResolvedValueOnce({
|
|
508
|
+
ok: true,
|
|
509
|
+
text: async () => JSON.stringify({ access_token: 'login-a2', refresh_token: 'refresh-2', user_id: 'user-2' })
|
|
510
|
+
})
|
|
511
|
+
.mockResolvedValueOnce({
|
|
512
|
+
ok: true,
|
|
513
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
514
|
+
})
|
|
515
|
+
.mockResolvedValueOnce({
|
|
516
|
+
ok: true,
|
|
517
|
+
text: async () => JSON.stringify({ access_token: 'access-1b' })
|
|
518
|
+
}) as unknown as typeof fetch
|
|
519
|
+
|
|
520
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
521
|
+
const user1 = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
522
|
+
await app.logIn(Credentials.anonymous())
|
|
523
|
+
|
|
524
|
+
const user1Listener = jest.fn()
|
|
525
|
+
const user2Listener = jest.fn()
|
|
526
|
+
user1.addListener(user1Listener)
|
|
527
|
+
app.currentUser!.addListener(user2Listener)
|
|
528
|
+
|
|
529
|
+
await user1.refreshAccessToken()
|
|
530
|
+
|
|
531
|
+
expect(user1Listener).toHaveBeenCalledTimes(1)
|
|
532
|
+
expect(user2Listener).toHaveBeenCalledTimes(0)
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
it('continues dispatch when a listener throws', async () => {
|
|
536
|
+
global.fetch = jest
|
|
537
|
+
.fn()
|
|
538
|
+
.mockResolvedValueOnce({
|
|
539
|
+
ok: true,
|
|
540
|
+
text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
|
|
541
|
+
})
|
|
542
|
+
.mockResolvedValueOnce({
|
|
543
|
+
ok: true,
|
|
544
|
+
text: async () => JSON.stringify({ access_token: 'access-1' })
|
|
545
|
+
}) as unknown as typeof fetch
|
|
546
|
+
|
|
547
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
548
|
+
const badListener = jest.fn(() => {
|
|
549
|
+
throw new Error('listener failure')
|
|
550
|
+
})
|
|
551
|
+
const goodListener = jest.fn()
|
|
552
|
+
app.addListener(badListener)
|
|
553
|
+
app.addListener(goodListener)
|
|
554
|
+
|
|
555
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
556
|
+
|
|
557
|
+
expect(badListener).toHaveBeenCalledTimes(1)
|
|
558
|
+
expect(goodListener).toHaveBeenCalledTimes(1)
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
it('supports listener removal during dispatch', async () => {
|
|
562
|
+
global.fetch = jest
|
|
563
|
+
.fn()
|
|
564
|
+
.mockImplementation(async () => ({
|
|
565
|
+
ok: true,
|
|
566
|
+
text: async () => JSON.stringify({ access_token: 'access-fallback' })
|
|
567
|
+
}))
|
|
568
|
+
.mockResolvedValueOnce({
|
|
569
|
+
ok: true,
|
|
570
|
+
text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
|
|
571
|
+
})
|
|
572
|
+
.mockResolvedValueOnce({
|
|
573
|
+
ok: true,
|
|
574
|
+
text: async () => JSON.stringify({ access_token: 'access-1' })
|
|
575
|
+
})
|
|
576
|
+
.mockResolvedValueOnce({
|
|
577
|
+
ok: true,
|
|
578
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
579
|
+
}) as unknown as typeof fetch
|
|
580
|
+
|
|
581
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
582
|
+
const user = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
583
|
+
|
|
584
|
+
const onceListener = jest.fn(() => {
|
|
585
|
+
user.removeListener(onceListener)
|
|
586
|
+
})
|
|
587
|
+
const stableListener = jest.fn()
|
|
588
|
+
user.addListener(onceListener)
|
|
589
|
+
user.addListener(stableListener)
|
|
590
|
+
|
|
591
|
+
await user.refreshAccessToken()
|
|
592
|
+
await user.refreshAccessToken()
|
|
593
|
+
|
|
594
|
+
expect(onceListener).toHaveBeenCalledTimes(1)
|
|
595
|
+
expect(stableListener).toHaveBeenCalledTimes(2)
|
|
596
|
+
})
|
|
597
|
+
})
|