@svadmin/supabase 0.11.0 → 0.11.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/package.json +8 -4
- package/src/auth-provider.ts +59 -4
- package/src/supabase.test.ts +0 -325
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/supabase",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "Supabase adapters — DataProvider, AuthProvider, LiveProvider, AuditHandler",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"files": [
|
|
8
|
-
"src
|
|
8
|
+
"src/**/*.ts",
|
|
9
|
+
"!src/**/*.test.*",
|
|
10
|
+
"!src/**/*.spec.*",
|
|
11
|
+
"!src/**/vitest.config.*",
|
|
12
|
+
"!src/**/setupTest.*"
|
|
9
13
|
],
|
|
10
14
|
"main": "src/index.ts",
|
|
11
15
|
"types": "src/index.ts",
|
|
@@ -20,10 +24,10 @@
|
|
|
20
24
|
}
|
|
21
25
|
},
|
|
22
26
|
"peerDependencies": {
|
|
23
|
-
"@svadmin/core": "^0.25.
|
|
27
|
+
"@svadmin/core": "^0.25.3",
|
|
24
28
|
"@supabase/supabase-js": "^2.0.0",
|
|
25
29
|
"@refinedev/supabase": "^5.0.0",
|
|
26
|
-
"@supacloud/js": "^0.
|
|
30
|
+
"@supacloud/js": "^0.12.0"
|
|
27
31
|
},
|
|
28
32
|
"license": "MIT",
|
|
29
33
|
"author": "zuohuadong",
|
package/src/auth-provider.ts
CHANGED
|
@@ -3,6 +3,26 @@ import type { SupabaseClient } from '@supabase/supabase-js';
|
|
|
3
3
|
import type { AuthProvider, Identity, AuthActionResult, CheckResult } from '@svadmin/core';
|
|
4
4
|
import { audit } from '@svadmin/core';
|
|
5
5
|
|
|
6
|
+
const INVALID_REFRESH_TOKEN_MESSAGES = [
|
|
7
|
+
'refresh token is not valid',
|
|
8
|
+
'invalid refresh token',
|
|
9
|
+
'refresh_token_not_found',
|
|
10
|
+
'jwt expired',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function isInvalidRefreshTokenError(error: unknown): error is Error {
|
|
14
|
+
if (!(error instanceof Error)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const message = error.message.toLowerCase();
|
|
19
|
+
return INVALID_REFRESH_TOKEN_MESSAGES.some((fragment) => message.includes(fragment));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function clearInvalidSession(client: SupabaseClient): Promise<void> {
|
|
23
|
+
await client.auth.signOut({ scope: 'local' });
|
|
24
|
+
}
|
|
25
|
+
|
|
6
26
|
export function createSupabaseAuthProvider(client: SupabaseClient): AuthProvider {
|
|
7
27
|
return {
|
|
8
28
|
async login({ email, password }: Record<string, unknown>): Promise<AuthActionResult> {
|
|
@@ -23,16 +43,42 @@ export function createSupabaseAuthProvider(client: SupabaseClient): AuthProvider
|
|
|
23
43
|
},
|
|
24
44
|
|
|
25
45
|
async check(): Promise<CheckResult> {
|
|
26
|
-
const { data: { session } } = await client.auth.getSession();
|
|
46
|
+
const { data: { session }, error } = await client.auth.getSession();
|
|
47
|
+
if (error) {
|
|
48
|
+
if (isInvalidRefreshTokenError(error)) {
|
|
49
|
+
await clearInvalidSession(client);
|
|
50
|
+
return {
|
|
51
|
+
authenticated: false,
|
|
52
|
+
redirectTo: '/login',
|
|
53
|
+
logout: true,
|
|
54
|
+
error: { message: 'Session expired, please sign in again.' },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
authenticated: false,
|
|
60
|
+
redirectTo: '/login',
|
|
61
|
+
logout: true,
|
|
62
|
+
error: { message: 'Session validation failed.' },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
27
65
|
if (session) return { authenticated: true };
|
|
28
66
|
return { authenticated: false, redirectTo: '/login', logout: true };
|
|
29
67
|
},
|
|
30
68
|
|
|
31
69
|
async getIdentity(): Promise<Identity | null> {
|
|
32
|
-
const { data: { user } } = await client.auth.getUser();
|
|
70
|
+
const { data: { user }, error } = await client.auth.getUser();
|
|
71
|
+
if (error && isInvalidRefreshTokenError(error)) {
|
|
72
|
+
await clearInvalidSession(client);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
33
75
|
if (!user) return null;
|
|
34
76
|
|
|
35
|
-
const { data: { session } } = await client.auth.getSession();
|
|
77
|
+
const { data: { session }, error: sessionError } = await client.auth.getSession();
|
|
78
|
+
if (sessionError && isInvalidRefreshTokenError(sessionError)) {
|
|
79
|
+
await clearInvalidSession(client);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
36
82
|
|
|
37
83
|
return {
|
|
38
84
|
id: user.id,
|
|
@@ -44,7 +90,11 @@ export function createSupabaseAuthProvider(client: SupabaseClient): AuthProvider
|
|
|
44
90
|
},
|
|
45
91
|
|
|
46
92
|
async getPermissions(): Promise<unknown> {
|
|
47
|
-
const { data: { user } } = await client.auth.getUser();
|
|
93
|
+
const { data: { user }, error } = await client.auth.getUser();
|
|
94
|
+
if (error && isInvalidRefreshTokenError(error)) {
|
|
95
|
+
await clearInvalidSession(client);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
48
98
|
return user?.user_metadata?.role ?? 'user';
|
|
49
99
|
},
|
|
50
100
|
|
|
@@ -71,6 +121,11 @@ export function createSupabaseAuthProvider(client: SupabaseClient): AuthProvider
|
|
|
71
121
|
},
|
|
72
122
|
|
|
73
123
|
async onError(error: unknown): Promise<{ redirectTo?: string; logout?: boolean }> {
|
|
124
|
+
if (isInvalidRefreshTokenError(error)) {
|
|
125
|
+
await clearInvalidSession(client);
|
|
126
|
+
return { redirectTo: '/login', logout: true };
|
|
127
|
+
}
|
|
128
|
+
|
|
74
129
|
if (error instanceof Error && error.message.includes('401')) {
|
|
75
130
|
// Pre-flight check: race-condition guard
|
|
76
131
|
// Supabase might have just refreshed the token in the background right after our fetch failed
|
package/src/supabase.test.ts
DELETED
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
// @svadmin/supabase — Unit Tests
|
|
2
|
-
import { describe, test, expect, mock } from 'bun:test';
|
|
3
|
-
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
4
|
-
|
|
5
|
-
// ─── Mock @svadmin/core ───────────────────────────────────────────
|
|
6
|
-
mock.module('@svadmin/core', () => {
|
|
7
|
-
return {
|
|
8
|
-
audit: mock(() => {}),
|
|
9
|
-
};
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
// ─── Mock Refine Supabase Provider ────────────────────────────────
|
|
13
|
-
mock.module('@refinedev/supabase', () => {
|
|
14
|
-
return {
|
|
15
|
-
dataProvider: (..._args: any[]) => {
|
|
16
|
-
const mockDp: any = {
|
|
17
|
-
getList: async () => ({ data: [{ id: 1, title: 'Item 1' }], total: 1 }),
|
|
18
|
-
getOne: async () => ({ data: { id: 1, title: 'Item 1' } }),
|
|
19
|
-
create: async () => ({ data: { id: 2, title: 'New Item' } }),
|
|
20
|
-
update: async () => ({ data: { id: 1, title: 'Updated' } }),
|
|
21
|
-
deleteOne: async () => ({ data: { id: 1, title: 'Deleted' } }),
|
|
22
|
-
};
|
|
23
|
-
return mockDp;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// ─── Mock Supabase Client ──────────────────────────────────────────
|
|
29
|
-
function createMockSupabaseClient(overrides: Record<string, any> = {}) {
|
|
30
|
-
const client = {
|
|
31
|
-
auth: {
|
|
32
|
-
signInWithPassword: mock(async ({ email, password }) => {
|
|
33
|
-
if (password === 'bad') return { error: { message: 'Invalid credentials' } };
|
|
34
|
-
return { data: { user: { id: 'user-1' } }, error: null };
|
|
35
|
-
}),
|
|
36
|
-
signOut: mock(async () => ({ error: null })),
|
|
37
|
-
getSession: mock(async () => ({ data: { session: { access_token: 'valid-token' } } })),
|
|
38
|
-
getUser: mock(async () => ({
|
|
39
|
-
data: {
|
|
40
|
-
user: {
|
|
41
|
-
id: 'user-1',
|
|
42
|
-
email: 'admin@test.com',
|
|
43
|
-
user_metadata: { name: 'Admin', avatar_url: 'http://avatar', role: 'admin' }
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
})),
|
|
47
|
-
signUp: mock(async ({ email }) => {
|
|
48
|
-
if (email === 'bad@test.com') return { error: { message: 'Signup failed' } };
|
|
49
|
-
return { error: null };
|
|
50
|
-
}),
|
|
51
|
-
resetPasswordForEmail: mock(async () => ({ error: null })),
|
|
52
|
-
updateUser: mock(async () => ({ error: null })),
|
|
53
|
-
...(overrides.auth || {}),
|
|
54
|
-
},
|
|
55
|
-
channel: mock((name: string) => {
|
|
56
|
-
const c: any = {
|
|
57
|
-
name,
|
|
58
|
-
on: mock(() => c),
|
|
59
|
-
subscribe: mock(() => c),
|
|
60
|
-
unsubscribe: mock(() => c),
|
|
61
|
-
send: mock(() => c)
|
|
62
|
-
};
|
|
63
|
-
return c;
|
|
64
|
-
}),
|
|
65
|
-
};
|
|
66
|
-
// If there are top-level overrides besides auth, merge them
|
|
67
|
-
for (const [key, val] of Object.entries(overrides)) {
|
|
68
|
-
if (key !== 'auth') (client as any)[key] = val;
|
|
69
|
-
}
|
|
70
|
-
return client as unknown as SupabaseClient;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// ─── DataProvider Tests ──────────────────────────────────────────
|
|
75
|
-
describe('Supabase DataProvider', () => {
|
|
76
|
-
test('getList routes through refine-adapter', async () => {
|
|
77
|
-
const { createSupabaseDataProvider } = await import('./data-provider');
|
|
78
|
-
const dp = await createSupabaseDataProvider({} as any);
|
|
79
|
-
const result = await dp.getList({ resource: 'posts' });
|
|
80
|
-
expect(result.data).toHaveLength(1);
|
|
81
|
-
expect(result.total).toBe(1);
|
|
82
|
-
expect(result.data[0].id).toBe(1);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('create returns new record', async () => {
|
|
86
|
-
const { createSupabaseDataProvider } = await import('./data-provider');
|
|
87
|
-
const dp = await createSupabaseDataProvider({} as any);
|
|
88
|
-
const result = await dp.create({ resource: 'posts', variables: { title: 'New Item' } });
|
|
89
|
-
expect(result.data.id).toBe(2);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// ─── AuthProvider Tests ──────────────────────────────────────────
|
|
95
|
-
describe('Supabase AuthProvider', () => {
|
|
96
|
-
test('login success', async () => {
|
|
97
|
-
const { createSupabaseAuthProvider } = await import('./auth-provider');
|
|
98
|
-
const auth = createSupabaseAuthProvider(createMockSupabaseClient());
|
|
99
|
-
const result = await auth.login({ email: 'admin@test.com', password: 'pass' });
|
|
100
|
-
expect(result.success).toBe(true);
|
|
101
|
-
expect(result.redirectTo).toBe('/');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('login failure', async () => {
|
|
105
|
-
const { createSupabaseAuthProvider } = await import('./auth-provider');
|
|
106
|
-
const auth = createSupabaseAuthProvider(createMockSupabaseClient());
|
|
107
|
-
const result = await auth.login({ email: 'admin@test.com', password: 'bad' });
|
|
108
|
-
expect(result.success).toBe(false);
|
|
109
|
-
expect(result.error?.message).toBe('Invalid credentials');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('logout success', async () => {
|
|
113
|
-
const { createSupabaseAuthProvider } = await import('./auth-provider');
|
|
114
|
-
const auth = createSupabaseAuthProvider(createMockSupabaseClient());
|
|
115
|
-
const result = await auth.logout();
|
|
116
|
-
expect(result.success).toBe(true);
|
|
117
|
-
expect(result.redirectTo).toBe('/login');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('check returns authenticated when session exists', async () => {
|
|
121
|
-
const { createSupabaseAuthProvider } = await import('./auth-provider');
|
|
122
|
-
const auth = createSupabaseAuthProvider(createMockSupabaseClient());
|
|
123
|
-
const result = await auth.check();
|
|
124
|
-
expect(result.authenticated).toBe(true);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test('check returns unauthenticated when no session', async () => {
|
|
128
|
-
const { createSupabaseAuthProvider } = await import('./auth-provider');
|
|
129
|
-
const auth = createSupabaseAuthProvider(createMockSupabaseClient({
|
|
130
|
-
auth: { getSession: mock(async () => ({ data: { session: null } })) }
|
|
131
|
-
}));
|
|
132
|
-
const result = await auth.check();
|
|
133
|
-
expect(result.authenticated).toBe(false);
|
|
134
|
-
expect(result.redirectTo).toBe('/login');
|
|
135
|
-
expect(result.logout).toBe(true);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test('getIdentity surfaces user metadata', async () => {
|
|
139
|
-
const { createSupabaseAuthProvider } = await import('./auth-provider');
|
|
140
|
-
const auth = createSupabaseAuthProvider(createMockSupabaseClient());
|
|
141
|
-
const identity = await auth.getIdentity();
|
|
142
|
-
expect(identity).not.toBeNull();
|
|
143
|
-
expect(identity!.id).toBe('user-1');
|
|
144
|
-
expect(identity!.name).toBe('Admin');
|
|
145
|
-
expect(identity!.avatar).toBe('http://avatar');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test('getPermissions surfaces role from user_metadata', async () => {
|
|
149
|
-
const { createSupabaseAuthProvider } = await import('./auth-provider');
|
|
150
|
-
const auth = createSupabaseAuthProvider(createMockSupabaseClient());
|
|
151
|
-
const role = await auth.getPermissions?.();
|
|
152
|
-
expect(role).toBe('admin');
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test('onError returns logout true on 401 with no session', async () => {
|
|
156
|
-
const { createSupabaseAuthProvider } = await import('./auth-provider');
|
|
157
|
-
const client = createMockSupabaseClient({
|
|
158
|
-
auth: { getSession: mock(async () => ({ data: { session: null } })) }
|
|
159
|
-
});
|
|
160
|
-
const auth = createSupabaseAuthProvider(client);
|
|
161
|
-
const result = await auth.onError!(new Error('401 Unauthorized'));
|
|
162
|
-
expect(result.logout).toBe(true);
|
|
163
|
-
expect(result.redirectTo).toBe('/login');
|
|
164
|
-
expect(client.auth.signOut).toHaveBeenCalled();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test('onError swallows 401 if token actually exists (race condition guard)', async () => {
|
|
168
|
-
const { createSupabaseAuthProvider } = await import('./auth-provider');
|
|
169
|
-
const client = createMockSupabaseClient();
|
|
170
|
-
const auth = createSupabaseAuthProvider(client);
|
|
171
|
-
const result = await auth.onError!(new Error('401 Unauthorized'));
|
|
172
|
-
expect(result.logout).toBeUndefined();
|
|
173
|
-
expect(client.auth.signOut).not.toHaveBeenCalled();
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// ─── LiveProvider Tests ──────────────────────────────────────────
|
|
179
|
-
describe('Supabase LiveProvider', () => {
|
|
180
|
-
test('subscribe builds channel and binds events', async () => {
|
|
181
|
-
const { createSupabaseLiveProvider } = await import('./live-provider');
|
|
182
|
-
const client = createMockSupabaseClient();
|
|
183
|
-
const live = createSupabaseLiveProvider(client);
|
|
184
|
-
|
|
185
|
-
let callbackTriggered = false;
|
|
186
|
-
const unsub = live.subscribe({
|
|
187
|
-
resource: 'posts',
|
|
188
|
-
callback: (event) => { callbackTriggered = true; }
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
expect(client.channel).toHaveBeenCalledWith('live-posts');
|
|
192
|
-
const channelMock = (client.channel as ReturnType<typeof mock>).mock.results[0].value;
|
|
193
|
-
expect(channelMock.on).toHaveBeenCalled();
|
|
194
|
-
expect(channelMock.subscribe).toHaveBeenCalled();
|
|
195
|
-
|
|
196
|
-
unsub();
|
|
197
|
-
expect(channelMock.unsubscribe).toHaveBeenCalled();
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test('publish triggers broadcast send', async () => {
|
|
201
|
-
const { createSupabaseLiveProvider } = await import('./live-provider');
|
|
202
|
-
const client = createMockSupabaseClient();
|
|
203
|
-
const live = createSupabaseLiveProvider(client);
|
|
204
|
-
|
|
205
|
-
live.publish?.({ type: 'INSERT', resource: 'posts', payload: { a: 1 } });
|
|
206
|
-
|
|
207
|
-
const channelMock = (client.channel as ReturnType<typeof mock>).mock.results[0].value;
|
|
208
|
-
expect(channelMock.send).toHaveBeenCalled();
|
|
209
|
-
const sendArgs = channelMock.send.mock.calls[0][0];
|
|
210
|
-
expect(sendArgs.type).toBe('broadcast');
|
|
211
|
-
expect(sendArgs.event).toBe('live-event');
|
|
212
|
-
expect(sendArgs.payload.type).toBe('INSERT');
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// ─── SupaCloud Task Provider Tests ───────────────────────────────
|
|
218
|
-
describe('SupaCloud Task Provider', () => {
|
|
219
|
-
test('submit proxies to supacloud tasks client', async () => {
|
|
220
|
-
const { createSupaCloudTaskProvider } = await import('./supacloud');
|
|
221
|
-
const taskHandle = {
|
|
222
|
-
id: 'task-1',
|
|
223
|
-
wait: mock(async () => ({ id: 'task-1', status: 'done' })),
|
|
224
|
-
};
|
|
225
|
-
const supacloud = {
|
|
226
|
-
submit: mock(async () => taskHandle),
|
|
227
|
-
get: mock(async () => ({ id: 'task-1', status: 'done' })),
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const provider = createSupaCloudTaskProvider({ supacloud });
|
|
231
|
-
const result = await provider.submit('image.generate', {
|
|
232
|
-
body: { prompt: 'poster' },
|
|
233
|
-
idempotencyKey: 'job-1',
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
expect(supacloud.submit).toHaveBeenCalledWith('image.generate', {
|
|
237
|
-
body: { prompt: 'poster' },
|
|
238
|
-
idempotencyKey: 'job-1',
|
|
239
|
-
});
|
|
240
|
-
expect(result).toBe(taskHandle);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test('list normalizes object payload with data field', async () => {
|
|
244
|
-
const { createSupaCloudTaskProvider } = await import('./supacloud');
|
|
245
|
-
const supacloud: import('@svadmin/core').TaskProvider<{ id: string; status: string }> = {
|
|
246
|
-
submit: mock(async () => ({ wait: async () => ({ id: 'task-1', status: 'queued' }) })),
|
|
247
|
-
get: mock(async () => ({ id: 'task-1', status: 'queued' })),
|
|
248
|
-
list: mock(async () => ({ data: [{ id: 'task-1', status: 'queued' }] })),
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const provider = createSupaCloudTaskProvider({ supacloud });
|
|
252
|
-
if (!provider.list) throw new Error('provider.list should exist');
|
|
253
|
-
const tasks = await provider.list();
|
|
254
|
-
|
|
255
|
-
expect(tasks).toEqual({
|
|
256
|
-
data: [{ id: 'task-1', status: 'queued' }],
|
|
257
|
-
total: 1,
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
test('listDlq throws clear error when capability is missing', async () => {
|
|
262
|
-
const { createSupaCloudTaskProvider } = await import('./supacloud');
|
|
263
|
-
const supacloud: import('@svadmin/core').TaskProvider<{ id: string }> = {
|
|
264
|
-
submit: mock(async () => ({ wait: async () => ({ id: 'task-1' }) })),
|
|
265
|
-
get: mock(async () => ({ id: 'task-1' })),
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const provider = createSupaCloudTaskProvider({ supacloud });
|
|
269
|
-
if (!provider.listDlq) throw new Error('provider.listDlq should exist');
|
|
270
|
-
|
|
271
|
-
await expect(provider.listDlq()).rejects.toThrow('tasks.listDlq');
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// ─── SupaCloud Task LiveProvider Tests ───────────────────────────
|
|
277
|
-
describe('SupaCloud Task LiveProvider', () => {
|
|
278
|
-
test('subscribe requires taskId in liveParams', async () => {
|
|
279
|
-
const { createSupaCloudTaskLiveProvider } = await import('./supacloud');
|
|
280
|
-
const supacloud = {
|
|
281
|
-
subscribe: mock(() => ({ unsubscribe: mock(() => {}) })),
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
const live = createSupaCloudTaskLiveProvider({ supacloud });
|
|
285
|
-
|
|
286
|
-
expect(() =>
|
|
287
|
-
live.subscribe({
|
|
288
|
-
resource: 'tasks',
|
|
289
|
-
callback: () => {},
|
|
290
|
-
}),
|
|
291
|
-
).toThrow('liveParams.taskId');
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test('subscribe maps task updates into svadmin live events', async () => {
|
|
295
|
-
const { createSupaCloudTaskLiveProvider } = await import('./supacloud');
|
|
296
|
-
let receivedTaskCallback: ((task: Record<string, unknown>) => void) | undefined;
|
|
297
|
-
const unsubscribe = mock(() => {});
|
|
298
|
-
const supacloud = {
|
|
299
|
-
subscribe: mock((taskId: string, callback: (task: Record<string, unknown>) => void) => {
|
|
300
|
-
receivedTaskCallback = callback;
|
|
301
|
-
expect(taskId).toBe('task-42');
|
|
302
|
-
return { unsubscribe };
|
|
303
|
-
}),
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const live = createSupaCloudTaskLiveProvider({ supacloud });
|
|
307
|
-
const callback = mock(() => {});
|
|
308
|
-
const stop = live.subscribe({
|
|
309
|
-
resource: 'tasks',
|
|
310
|
-
liveParams: { taskId: 'task-42' },
|
|
311
|
-
callback,
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
receivedTaskCallback?.({ id: 'task-42', status: 'running' });
|
|
315
|
-
|
|
316
|
-
expect(callback).toHaveBeenCalledWith({
|
|
317
|
-
type: 'UPDATE',
|
|
318
|
-
resource: 'tasks',
|
|
319
|
-
payload: { id: 'task-42', status: 'running' },
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
stop();
|
|
323
|
-
expect(unsubscribe).toHaveBeenCalled();
|
|
324
|
-
});
|
|
325
|
-
});
|