@flowerforce/flowerbase-client 0.1.1-beta.2
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 +198 -0
- package/dist/app.d.ts +40 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +186 -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 +7 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +24 -0
- package/dist/functions.d.ts +3 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +30 -0
- package/dist/http.d.ts +15 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +74 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/mongo.d.ts +4 -0
- package/dist/mongo.d.ts.map +1 -0
- package/dist/mongo.js +61 -0
- package/dist/session.d.ts +12 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +53 -0
- package/dist/session.native.d.ts +14 -0
- package/dist/session.native.d.ts.map +1 -0
- package/dist/session.native.js +81 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/user.d.ts +17 -0
- package/dist/user.d.ts.map +1 -0
- package/dist/user.js +30 -0
- package/dist/watch.d.ts +3 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +138 -0
- package/jest.config.ts +13 -0
- package/package.json +30 -0
- package/project.json +11 -0
- package/rollup.config.js +17 -0
- package/src/__tests__/auth.test.ts +164 -0
- package/src/__tests__/compat.test.ts +12 -0
- package/src/__tests__/functions.test.ts +76 -0
- package/src/__tests__/mongo.test.ts +48 -0
- package/src/__tests__/session.test.ts +103 -0
- package/src/__tests__/watch.test.ts +138 -0
- package/src/app.ts +235 -0
- package/src/bson.ts +6 -0
- package/src/credentials.ts +24 -0
- package/src/functions.ts +32 -0
- package/src/http.ts +92 -0
- package/src/index.ts +14 -0
- package/src/mongo.ts +63 -0
- package/src/session.native.ts +98 -0
- package/src/session.ts +59 -0
- package/src/types.ts +84 -0
- package/src/user.ts +39 -0
- package/src/watch.ts +150 -0
- package/tsconfig.json +34 -0
- package/tsconfig.spec.json +13 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { App } from '../app'
|
|
2
|
+
import { Credentials } from '../credentials'
|
|
3
|
+
|
|
4
|
+
describe('flowerbase-client auth', () => {
|
|
5
|
+
const originalFetch = global.fetch
|
|
6
|
+
const originalLocalStorage = (globalThis as typeof globalThis & { localStorage?: unknown }).localStorage
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
global.fetch = originalFetch
|
|
10
|
+
if (typeof originalLocalStorage === 'undefined') {
|
|
11
|
+
Reflect.deleteProperty(globalThis, 'localStorage')
|
|
12
|
+
} else {
|
|
13
|
+
Object.defineProperty(globalThis, 'localStorage', {
|
|
14
|
+
configurable: true,
|
|
15
|
+
value: originalLocalStorage
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('logs in with email/password', async () => {
|
|
21
|
+
global.fetch = jest
|
|
22
|
+
.fn()
|
|
23
|
+
.mockResolvedValueOnce({
|
|
24
|
+
ok: true,
|
|
25
|
+
text: async () => JSON.stringify({
|
|
26
|
+
access_token: 'access',
|
|
27
|
+
refresh_token: 'refresh',
|
|
28
|
+
user_id: 'user-1'
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
.mockResolvedValueOnce({
|
|
32
|
+
ok: true,
|
|
33
|
+
text: async () => JSON.stringify({ access_token: 'access-from-session' })
|
|
34
|
+
}) as unknown as typeof fetch
|
|
35
|
+
|
|
36
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
37
|
+
const user = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
38
|
+
|
|
39
|
+
expect(user.id).toBe('user-1')
|
|
40
|
+
expect(app.currentUser?.id).toBe('user-1')
|
|
41
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
42
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/login',
|
|
43
|
+
expect.objectContaining({ method: 'POST' })
|
|
44
|
+
)
|
|
45
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
46
|
+
'http://localhost:3000/api/client/v2.0/auth/session',
|
|
47
|
+
expect.objectContaining({
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: expect.objectContaining({ Authorization: 'Bearer refresh' })
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('logs in with anonymous provider', async () => {
|
|
55
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
56
|
+
ok: true,
|
|
57
|
+
text: async () => JSON.stringify({
|
|
58
|
+
access_token: 'access',
|
|
59
|
+
refresh_token: 'refresh',
|
|
60
|
+
user_id: 'anon-1'
|
|
61
|
+
})
|
|
62
|
+
}) as unknown as typeof fetch
|
|
63
|
+
|
|
64
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
65
|
+
const user = await app.logIn(Credentials.anonymous())
|
|
66
|
+
|
|
67
|
+
expect(user.id).toBe('anon-1')
|
|
68
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
69
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/anon-user/login',
|
|
70
|
+
expect.objectContaining({ method: 'POST' })
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('logs in with custom function provider', async () => {
|
|
75
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
76
|
+
ok: true,
|
|
77
|
+
text: async () => JSON.stringify({
|
|
78
|
+
access_token: 'access',
|
|
79
|
+
refresh_token: 'refresh',
|
|
80
|
+
user_id: 'custom-1'
|
|
81
|
+
})
|
|
82
|
+
}) as unknown as typeof fetch
|
|
83
|
+
|
|
84
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
85
|
+
const user = await app.logIn(Credentials.function({ token: 'abc' }))
|
|
86
|
+
|
|
87
|
+
expect(user.id).toBe('custom-1')
|
|
88
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
89
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/custom-function/login',
|
|
90
|
+
expect.objectContaining({ method: 'POST' })
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('supports register and reset endpoints', async () => {
|
|
95
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
96
|
+
ok: true,
|
|
97
|
+
text: async () => JSON.stringify({ status: 'ok' })
|
|
98
|
+
}) as unknown as typeof fetch
|
|
99
|
+
|
|
100
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
101
|
+
|
|
102
|
+
await app.emailPasswordAuth.registerUser({ email: 'john@doe.com', password: 'secret123' })
|
|
103
|
+
await app.emailPasswordAuth.sendResetPasswordEmail('john@doe.com')
|
|
104
|
+
await app.emailPasswordAuth.callResetPasswordFunction('john@doe.com', 'new-secret', 'extra')
|
|
105
|
+
await app.emailPasswordAuth.resetPassword({ token: 't1', tokenId: 't2', password: 'new-secret' })
|
|
106
|
+
|
|
107
|
+
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
108
|
+
1,
|
|
109
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/register',
|
|
110
|
+
expect.objectContaining({ method: 'POST' })
|
|
111
|
+
)
|
|
112
|
+
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
113
|
+
2,
|
|
114
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/reset/send',
|
|
115
|
+
expect.objectContaining({ method: 'POST' })
|
|
116
|
+
)
|
|
117
|
+
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
118
|
+
3,
|
|
119
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/reset/call',
|
|
120
|
+
expect.objectContaining({ method: 'POST' })
|
|
121
|
+
)
|
|
122
|
+
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
123
|
+
4,
|
|
124
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/reset',
|
|
125
|
+
expect.objectContaining({ method: 'POST' })
|
|
126
|
+
)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('calls session endpoint on app load when browser session exists', async () => {
|
|
130
|
+
const storage = new Map<string, string>()
|
|
131
|
+
storage.set(
|
|
132
|
+
'flowerbase:my-app:session',
|
|
133
|
+
JSON.stringify({ accessToken: 'old-access', refreshToken: 'refresh', userId: 'user-1' })
|
|
134
|
+
)
|
|
135
|
+
Object.defineProperty(globalThis, 'localStorage', {
|
|
136
|
+
configurable: true,
|
|
137
|
+
value: {
|
|
138
|
+
getItem: (key: string) => storage.get(key) ?? null,
|
|
139
|
+
setItem: (key: string, value: string) => {
|
|
140
|
+
storage.set(key, value)
|
|
141
|
+
},
|
|
142
|
+
removeItem: (key: string) => {
|
|
143
|
+
storage.delete(key)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
149
|
+
ok: true,
|
|
150
|
+
text: async () => JSON.stringify({ access_token: 'fresh-access' })
|
|
151
|
+
}) as unknown as typeof fetch
|
|
152
|
+
|
|
153
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
154
|
+
await app.getProfile().catch(() => undefined)
|
|
155
|
+
|
|
156
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
157
|
+
'http://localhost:3000/api/client/v2.0/auth/session',
|
|
158
|
+
expect.objectContaining({
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: expect.objectContaining({ Authorization: 'Bearer refresh' })
|
|
161
|
+
})
|
|
162
|
+
)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as Flowerbase from '../index'
|
|
2
|
+
|
|
3
|
+
describe('flowerbase-client compatibility surface', () => {
|
|
4
|
+
it('exposes Realm-like symbols', () => {
|
|
5
|
+
expect(typeof Flowerbase.App).toBe('function')
|
|
6
|
+
expect(typeof Flowerbase.Credentials.emailPassword).toBe('function')
|
|
7
|
+
expect(typeof Flowerbase.Credentials.anonymous).toBe('function')
|
|
8
|
+
expect(typeof Flowerbase.Credentials.function).toBe('function')
|
|
9
|
+
expect(typeof Flowerbase.BSON.ObjectId).toBe('function')
|
|
10
|
+
expect(Flowerbase.ObjectID).toBe(Flowerbase.ObjectId)
|
|
11
|
+
})
|
|
12
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { App } from '../app'
|
|
2
|
+
import { Credentials } from '../credentials'
|
|
3
|
+
|
|
4
|
+
describe('flowerbase-client functions', () => {
|
|
5
|
+
const originalFetch = global.fetch
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
global.fetch = originalFetch
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('calls dynamic function proxies', async () => {
|
|
12
|
+
global.fetch = jest
|
|
13
|
+
.fn()
|
|
14
|
+
.mockResolvedValueOnce({
|
|
15
|
+
ok: true,
|
|
16
|
+
text: async () => JSON.stringify({
|
|
17
|
+
access_token: 'access',
|
|
18
|
+
refresh_token: 'refresh',
|
|
19
|
+
user_id: 'user-1'
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
.mockResolvedValueOnce({
|
|
23
|
+
ok: true,
|
|
24
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
25
|
+
})
|
|
26
|
+
.mockResolvedValueOnce({
|
|
27
|
+
ok: true,
|
|
28
|
+
text: async () => JSON.stringify({ result: 42 })
|
|
29
|
+
}) as unknown as typeof fetch
|
|
30
|
+
|
|
31
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
32
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
33
|
+
|
|
34
|
+
const result = await app.currentUser!.functions.sum(40, 2)
|
|
35
|
+
expect(result).toEqual({ result: 42 })
|
|
36
|
+
|
|
37
|
+
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
38
|
+
3,
|
|
39
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/functions/call',
|
|
40
|
+
expect.objectContaining({
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: expect.objectContaining({ Authorization: 'Bearer access' })
|
|
43
|
+
})
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('throws function execution errors', async () => {
|
|
48
|
+
global.fetch = jest
|
|
49
|
+
.fn()
|
|
50
|
+
.mockResolvedValueOnce({
|
|
51
|
+
ok: true,
|
|
52
|
+
text: async () => JSON.stringify({
|
|
53
|
+
access_token: 'access',
|
|
54
|
+
refresh_token: 'refresh',
|
|
55
|
+
user_id: 'user-1'
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
.mockResolvedValueOnce({
|
|
59
|
+
ok: true,
|
|
60
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
61
|
+
})
|
|
62
|
+
.mockResolvedValueOnce({
|
|
63
|
+
ok: false,
|
|
64
|
+
status: 400,
|
|
65
|
+
text: async () => JSON.stringify({
|
|
66
|
+
error: '{"message":"boom","name":"Error"}',
|
|
67
|
+
error_code: 'FunctionExecutionError'
|
|
68
|
+
})
|
|
69
|
+
}) as unknown as typeof fetch
|
|
70
|
+
|
|
71
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
72
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
73
|
+
|
|
74
|
+
await expect(app.currentUser!.functions.explode()).rejects.toThrow('boom')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ObjectId } from 'bson'
|
|
2
|
+
import { App } from '../app'
|
|
3
|
+
import { Credentials } from '../credentials'
|
|
4
|
+
|
|
5
|
+
describe('flowerbase-client mongo service wrapper', () => {
|
|
6
|
+
const originalFetch = global.fetch
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
global.fetch = originalFetch
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('maps CRUD calls to mongodb-atlas service payload', async () => {
|
|
13
|
+
global.fetch = jest
|
|
14
|
+
.fn()
|
|
15
|
+
.mockResolvedValueOnce({
|
|
16
|
+
ok: true,
|
|
17
|
+
text: async () => JSON.stringify({
|
|
18
|
+
access_token: 'access',
|
|
19
|
+
refresh_token: 'refresh',
|
|
20
|
+
user_id: 'user-1'
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
.mockResolvedValue({
|
|
24
|
+
ok: true,
|
|
25
|
+
text: async () => JSON.stringify({ ok: true })
|
|
26
|
+
}) as unknown as typeof fetch
|
|
27
|
+
|
|
28
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
29
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
30
|
+
|
|
31
|
+
const collection = app.currentUser!.mongoClient('mongodb-atlas').db('testdb').collection('todos')
|
|
32
|
+
|
|
33
|
+
await collection.find({ done: false })
|
|
34
|
+
await collection.findOne({ done: false })
|
|
35
|
+
await collection.insertOne({ title: 'new task' })
|
|
36
|
+
await collection.updateOne({ _id: new ObjectId('507f1f77bcf86cd799439011') }, { $set: { done: true } })
|
|
37
|
+
await collection.updateMany({ done: false }, { $set: { done: true } })
|
|
38
|
+
await collection.deleteOne({ done: true })
|
|
39
|
+
|
|
40
|
+
expect((global.fetch as jest.Mock).mock.calls).toHaveLength(8)
|
|
41
|
+
const [url, request] = (global.fetch as jest.Mock).mock.calls[3]
|
|
42
|
+
expect(url).toBe('http://localhost:3000/api/client/v2.0/app/my-app/functions/call')
|
|
43
|
+
expect(request.method).toBe('POST')
|
|
44
|
+
const parsed = JSON.parse(request.body)
|
|
45
|
+
expect(parsed.service).toBe('mongodb-atlas')
|
|
46
|
+
expect(parsed.name).toBe('findOne')
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { App } from '../app'
|
|
2
|
+
import { Credentials } from '../credentials'
|
|
3
|
+
|
|
4
|
+
describe('flowerbase-client session', () => {
|
|
5
|
+
const originalFetch = global.fetch
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
global.fetch = originalFetch
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('refreshes access token', async () => {
|
|
12
|
+
global.fetch = jest
|
|
13
|
+
.fn()
|
|
14
|
+
.mockResolvedValueOnce({
|
|
15
|
+
ok: true,
|
|
16
|
+
text: async () => JSON.stringify({
|
|
17
|
+
access_token: 'access',
|
|
18
|
+
refresh_token: 'refresh',
|
|
19
|
+
user_id: 'user-1'
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
.mockResolvedValueOnce({
|
|
23
|
+
ok: true,
|
|
24
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
25
|
+
})
|
|
26
|
+
.mockResolvedValueOnce({
|
|
27
|
+
ok: true,
|
|
28
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
29
|
+
}) as unknown as typeof fetch
|
|
30
|
+
|
|
31
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
32
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
33
|
+
|
|
34
|
+
const token = await app.currentUser!.refreshAccessToken()
|
|
35
|
+
expect(token).toBe('access-2')
|
|
36
|
+
expect(global.fetch).toHaveBeenLastCalledWith(
|
|
37
|
+
'http://localhost:3000/api/client/v2.0/auth/session',
|
|
38
|
+
expect.objectContaining({
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: expect.objectContaining({ Authorization: 'Bearer refresh' })
|
|
41
|
+
})
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('clears session when refresh fails', async () => {
|
|
46
|
+
global.fetch = jest
|
|
47
|
+
.fn()
|
|
48
|
+
.mockResolvedValueOnce({
|
|
49
|
+
ok: true,
|
|
50
|
+
text: async () => JSON.stringify({
|
|
51
|
+
access_token: 'access',
|
|
52
|
+
refresh_token: 'refresh',
|
|
53
|
+
user_id: 'user-1'
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
.mockResolvedValueOnce({
|
|
57
|
+
ok: true,
|
|
58
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
59
|
+
})
|
|
60
|
+
.mockResolvedValueOnce({
|
|
61
|
+
ok: false,
|
|
62
|
+
status: 401,
|
|
63
|
+
text: async () => JSON.stringify({ message: 'Invalid refresh token provided' })
|
|
64
|
+
}) as unknown as typeof fetch
|
|
65
|
+
|
|
66
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
67
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
68
|
+
|
|
69
|
+
await expect(app.currentUser!.refreshAccessToken()).rejects.toThrow('Invalid refresh token provided')
|
|
70
|
+
expect(app.currentUser).toBeNull()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('revokes session on logout', async () => {
|
|
74
|
+
global.fetch = jest
|
|
75
|
+
.fn()
|
|
76
|
+
.mockResolvedValueOnce({
|
|
77
|
+
ok: true,
|
|
78
|
+
text: async () => JSON.stringify({
|
|
79
|
+
access_token: 'access',
|
|
80
|
+
refresh_token: 'refresh',
|
|
81
|
+
user_id: 'user-1'
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
.mockResolvedValueOnce({
|
|
85
|
+
ok: true,
|
|
86
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
87
|
+
})
|
|
88
|
+
.mockResolvedValueOnce({
|
|
89
|
+
ok: true,
|
|
90
|
+
text: async () => JSON.stringify({ status: 'ok' })
|
|
91
|
+
}) as unknown as typeof fetch
|
|
92
|
+
|
|
93
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
94
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
95
|
+
await app.currentUser!.logOut()
|
|
96
|
+
|
|
97
|
+
expect(global.fetch).toHaveBeenLastCalledWith(
|
|
98
|
+
'http://localhost:3000/api/client/v2.0/auth/session',
|
|
99
|
+
expect.objectContaining({ method: 'DELETE' })
|
|
100
|
+
)
|
|
101
|
+
expect(app.currentUser).toBeNull()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { App } from '../app'
|
|
2
|
+
import { Credentials } from '../credentials'
|
|
3
|
+
|
|
4
|
+
const streamFromLines = (lines: string[]) => {
|
|
5
|
+
const encoded = lines.map((line) => `${line}\n`).join('')
|
|
6
|
+
const bytes = new TextEncoder().encode(encoded)
|
|
7
|
+
|
|
8
|
+
return new ReadableStream<Uint8Array>({
|
|
9
|
+
start(controller) {
|
|
10
|
+
controller.enqueue(bytes)
|
|
11
|
+
controller.close()
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('flowerbase-client watch', () => {
|
|
17
|
+
const originalFetch = global.fetch
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
jest.useRealTimers()
|
|
21
|
+
global.fetch = originalFetch
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('receives SSE events through watch iterator', async () => {
|
|
25
|
+
global.fetch = jest
|
|
26
|
+
.fn()
|
|
27
|
+
.mockResolvedValueOnce({
|
|
28
|
+
ok: true,
|
|
29
|
+
text: async () => JSON.stringify({
|
|
30
|
+
access_token: 'access',
|
|
31
|
+
refresh_token: 'refresh',
|
|
32
|
+
user_id: 'user-1'
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
.mockResolvedValueOnce({
|
|
36
|
+
ok: true,
|
|
37
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
38
|
+
})
|
|
39
|
+
.mockResolvedValueOnce({
|
|
40
|
+
ok: true,
|
|
41
|
+
body: streamFromLines(['data: {"operationType":"insert","fullDocument":{"title":"A"}}', ''])
|
|
42
|
+
}) as unknown as typeof fetch
|
|
43
|
+
|
|
44
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
45
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
46
|
+
|
|
47
|
+
const iterator = app.currentUser!
|
|
48
|
+
.mongoClient('mongodb-atlas')
|
|
49
|
+
.db('testdb')
|
|
50
|
+
.collection('todos')
|
|
51
|
+
.watch()
|
|
52
|
+
|
|
53
|
+
const first = await iterator.next()
|
|
54
|
+
expect(first.done).toBe(false)
|
|
55
|
+
expect(first.value).toEqual({ operationType: 'insert', fullDocument: { title: 'A' } })
|
|
56
|
+
|
|
57
|
+
iterator.close()
|
|
58
|
+
|
|
59
|
+
const [url, request] = (global.fetch as jest.Mock).mock.calls[2]
|
|
60
|
+
expect(url).toContain('/functions/call?baas_request=')
|
|
61
|
+
expect(request.headers.Authorization).toBe('Bearer access')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('closes iterator on return', async () => {
|
|
65
|
+
global.fetch = jest
|
|
66
|
+
.fn()
|
|
67
|
+
.mockResolvedValueOnce({
|
|
68
|
+
ok: true,
|
|
69
|
+
text: async () => JSON.stringify({
|
|
70
|
+
access_token: 'access',
|
|
71
|
+
refresh_token: 'refresh',
|
|
72
|
+
user_id: 'user-1'
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
.mockResolvedValueOnce({
|
|
76
|
+
ok: true,
|
|
77
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
78
|
+
})
|
|
79
|
+
.mockResolvedValue({
|
|
80
|
+
ok: true,
|
|
81
|
+
body: streamFromLines([])
|
|
82
|
+
}) as unknown as typeof fetch
|
|
83
|
+
|
|
84
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
85
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
86
|
+
|
|
87
|
+
const iterator = app.currentUser!
|
|
88
|
+
.mongoClient('mongodb-atlas')
|
|
89
|
+
.db('testdb')
|
|
90
|
+
.collection('todos')
|
|
91
|
+
.watch()
|
|
92
|
+
|
|
93
|
+
iterator.close()
|
|
94
|
+
const result = await iterator.next()
|
|
95
|
+
expect(result.done).toBe(true)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('reconnects with backoff after network errors', async () => {
|
|
99
|
+
jest.useFakeTimers()
|
|
100
|
+
global.fetch = jest
|
|
101
|
+
.fn()
|
|
102
|
+
.mockResolvedValueOnce({
|
|
103
|
+
ok: true,
|
|
104
|
+
text: async () => JSON.stringify({
|
|
105
|
+
access_token: 'access',
|
|
106
|
+
refresh_token: 'refresh',
|
|
107
|
+
user_id: 'user-1'
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
.mockResolvedValueOnce({
|
|
111
|
+
ok: true,
|
|
112
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
113
|
+
})
|
|
114
|
+
.mockRejectedValueOnce(new Error('network'))
|
|
115
|
+
.mockResolvedValueOnce({
|
|
116
|
+
ok: true,
|
|
117
|
+
body: streamFromLines(['data: {"operationType":"update"}', ''])
|
|
118
|
+
}) as unknown as typeof fetch
|
|
119
|
+
|
|
120
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
121
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
122
|
+
|
|
123
|
+
const iterator = app.currentUser!
|
|
124
|
+
.mongoClient('mongodb-atlas')
|
|
125
|
+
.db('testdb')
|
|
126
|
+
.collection('todos')
|
|
127
|
+
.watch()
|
|
128
|
+
|
|
129
|
+
await jest.advanceTimersByTimeAsync(250)
|
|
130
|
+
const result = await iterator.next()
|
|
131
|
+
|
|
132
|
+
expect(result.done).toBe(false)
|
|
133
|
+
expect(result.value).toEqual({ operationType: 'update' })
|
|
134
|
+
expect((global.fetch as jest.Mock).mock.calls.length).toBeGreaterThanOrEqual(4)
|
|
135
|
+
|
|
136
|
+
iterator.close()
|
|
137
|
+
})
|
|
138
|
+
})
|