@selvajs/platform 0.14.2 → 0.15.0-beta.1
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/LICENSE +1 -1
- package/dist/auth.d.ts +60 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +12 -0
- package/dist/auth.js.map +1 -0
- package/dist/compute.d.ts +47 -0
- package/dist/compute.d.ts.map +1 -0
- package/dist/compute.js +11 -0
- package/dist/compute.js.map +1 -0
- package/dist/computeServer/index.d.ts +2 -0
- package/dist/computeServer/index.d.ts.map +1 -1
- package/dist/computeServer/index.js +6 -0
- package/dist/computeServer/index.js.map +1 -1
- package/dist/computeServer/interface.d.ts +10 -0
- package/dist/computeServer/interface.d.ts.map +1 -1
- package/dist/computeServer/secrets.d.ts +53 -0
- package/dist/computeServer/secrets.d.ts.map +1 -0
- package/dist/computeServer/secrets.js +67 -0
- package/dist/computeServer/secrets.js.map +1 -0
- package/dist/context.d.ts +7 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +13 -0
- package/dist/context.js.map +1 -1
- package/dist/data/interface.d.ts +33 -0
- package/dist/data/interface.d.ts.map +1 -1
- package/dist/definitions/interface.d.ts +18 -0
- package/dist/definitions/interface.d.ts.map +1 -1
- package/dist/definitions/schemas.d.ts +1 -0
- package/dist/definitions/schemas.d.ts.map +1 -1
- package/dist/definitions/schemas.js +3 -0
- package/dist/definitions/schemas.js.map +1 -1
- package/dist/definitions/service.d.ts +62 -0
- package/dist/definitions/service.d.ts.map +1 -0
- package/dist/definitions/service.js +149 -0
- package/dist/definitions/service.js.map +1 -0
- package/dist/definitions/types.d.ts +24 -1
- package/dist/definitions/types.d.ts.map +1 -1
- package/dist/definitions.d.ts +146 -0
- package/dist/definitions.d.ts.map +1 -0
- package/dist/definitions.js +23 -0
- package/dist/definitions.js.map +1 -0
- package/dist/errors/interface.d.ts +44 -0
- package/dist/errors/interface.d.ts.map +1 -0
- package/dist/errors/interface.js +9 -0
- package/dist/errors/interface.js.map +1 -0
- package/dist/events/interface.d.ts +23 -0
- package/dist/events/interface.d.ts.map +1 -1
- package/dist/events/interface.js.map +1 -1
- package/dist/index.d.ts +14 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/invites/schemas.d.ts +24 -0
- package/dist/invites/schemas.d.ts.map +1 -0
- package/dist/invites/schemas.js +17 -0
- package/dist/invites/schemas.js.map +1 -0
- package/dist/metrics/interface.d.ts +6 -0
- package/dist/metrics/interface.d.ts.map +1 -1
- package/dist/metrics/interface.js.map +1 -1
- package/dist/organizations/index.d.ts +4 -3
- package/dist/organizations/index.d.ts.map +1 -1
- package/dist/organizations/index.js +2 -1
- package/dist/organizations/index.js.map +1 -1
- package/dist/organizations/interface.d.ts +1 -1
- package/dist/organizations/interface.d.ts.map +1 -1
- package/dist/organizations/paths.d.ts +22 -0
- package/dist/organizations/paths.d.ts.map +1 -0
- package/dist/organizations/paths.js +43 -0
- package/dist/organizations/paths.js.map +1 -0
- package/dist/organizations/schemas.d.ts +10 -0
- package/dist/organizations/schemas.d.ts.map +1 -1
- package/dist/organizations/schemas.js +6 -0
- package/dist/organizations/schemas.js.map +1 -1
- package/dist/organizations/types.d.ts +10 -1
- package/dist/organizations/types.d.ts.map +1 -1
- package/dist/organizations.d.ts +93 -0
- package/dist/organizations.d.ts.map +1 -0
- package/dist/organizations.js +14 -0
- package/dist/organizations.js.map +1 -0
- package/dist/solveCache/interface.d.ts +86 -0
- package/dist/solveCache/interface.d.ts.map +1 -0
- package/dist/solveCache/interface.js +12 -0
- package/dist/solveCache/interface.js.map +1 -0
- package/dist/storage/assetClasses.d.ts +59 -0
- package/dist/storage/assetClasses.d.ts.map +1 -0
- package/dist/storage/assetClasses.js +86 -0
- package/dist/storage/assetClasses.js.map +1 -0
- package/dist/storage/cacheBust.d.ts +21 -0
- package/dist/storage/cacheBust.d.ts.map +1 -0
- package/dist/storage/cacheBust.js +29 -0
- package/dist/storage/cacheBust.js.map +1 -0
- package/dist/storage/image.d.ts +6 -1
- package/dist/storage/image.d.ts.map +1 -1
- package/dist/storage/image.js +8 -3
- package/dist/storage/image.js.map +1 -1
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/testing/adapterSuite.d.ts +33 -0
- package/dist/testing/adapterSuite.d.ts.map +1 -0
- package/dist/testing/adapterSuite.js +166 -0
- package/dist/testing/adapterSuite.js.map +1 -0
- package/dist/testing/suites/definitionStoreSuite.d.ts.map +1 -1
- package/dist/testing/suites/definitionStoreSuite.js +87 -0
- package/dist/testing/suites/definitionStoreSuite.js.map +1 -1
- package/dist/testing/suites/eventSinkSuite.js +1 -0
- package/dist/testing/suites/eventSinkSuite.js.map +1 -1
- package/dist/testing/suites/orgStoreSuite.d.ts.map +1 -1
- package/dist/testing/suites/orgStoreSuite.js +12 -0
- package/dist/testing/suites/orgStoreSuite.js.map +1 -1
- package/dist/testing/suites/platformPermissionStoreSuite.d.ts +9 -0
- package/dist/testing/suites/platformPermissionStoreSuite.d.ts.map +1 -1
- package/dist/testing/suites/platformPermissionStoreSuite.js +34 -1
- package/dist/testing/suites/platformPermissionStoreSuite.js.map +1 -1
- package/dist/testing/suites/runner.d.ts +21 -0
- package/dist/testing/suites/runner.d.ts.map +1 -0
- package/dist/testing/suites/runner.js +7 -0
- package/dist/testing/suites/runner.js.map +1 -0
- package/dist/testing/suites/storageProviderSuite.d.ts.map +1 -1
- package/dist/testing/suites/storageProviderSuite.js +18 -0
- package/dist/testing/suites/storageProviderSuite.js.map +1 -1
- package/package.json +9 -24
- package/src/access/index.ts +0 -23
- package/src/access/rules.ts +0 -276
- package/src/auth/index.ts +0 -8
- package/src/auth/interface.ts +0 -279
- package/src/auth/types.ts +0 -32
- package/src/bindings/index.ts +0 -2
- package/src/bindings/interface.ts +0 -61
- package/src/computeServer/index.ts +0 -17
- package/src/computeServer/interface.ts +0 -62
- package/src/computeServer/types.ts +0 -74
- package/src/computeServer/utils.ts +0 -112
- package/src/config.ts +0 -99
- package/src/context.ts +0 -66
- package/src/data/index.ts +0 -1
- package/src/data/interface.ts +0 -70
- package/src/definitions/index.ts +0 -26
- package/src/definitions/interface.ts +0 -78
- package/src/definitions/paths.ts +0 -31
- package/src/definitions/schemas.ts +0 -64
- package/src/definitions/types.ts +0 -111
- package/src/errors.ts +0 -16
- package/src/events/audit.ts +0 -67
- package/src/events/index.ts +0 -9
- package/src/events/interface.ts +0 -102
- package/src/index.ts +0 -219
- package/src/invites/index.ts +0 -2
- package/src/invites/interface.ts +0 -38
- package/src/invites/types.ts +0 -39
- package/src/metrics/interface.ts +0 -86
- package/src/organizations/index.ts +0 -14
- package/src/organizations/interface.ts +0 -68
- package/src/organizations/schemas.ts +0 -52
- package/src/organizations/types.ts +0 -31
- package/src/pagination.ts +0 -45
- package/src/permissions/index.ts +0 -3
- package/src/permissions/interface.ts +0 -60
- package/src/permissions/types.ts +0 -21
- package/src/platformProjects/index.ts +0 -3
- package/src/platformProjects/interface.ts +0 -27
- package/src/platformProjects/schemas.ts +0 -13
- package/src/platformProjects/types.ts +0 -22
- package/src/projects/index.ts +0 -4
- package/src/projects/interface.ts +0 -58
- package/src/projects/schemas.ts +0 -28
- package/src/projects/types.ts +0 -36
- package/src/shareLinks/index.ts +0 -5
- package/src/shareLinks/interface.ts +0 -35
- package/src/shareLinks/schemas.ts +0 -21
- package/src/shareLinks/types.ts +0 -34
- package/src/storage/image.ts +0 -77
- package/src/storage/index.ts +0 -9
- package/src/storage/interface.ts +0 -27
- package/src/testing/index.ts +0 -47
- package/src/testing/suites/authProviderSuite.ts +0 -207
- package/src/testing/suites/computeServerStoreSuite.ts +0 -195
- package/src/testing/suites/definitionStoreSuite.ts +0 -585
- package/src/testing/suites/emailLinkAuthSuite.ts +0 -83
- package/src/testing/suites/eventSinkSuite.ts +0 -379
- package/src/testing/suites/helpers.ts +0 -87
- package/src/testing/suites/inviteStoreSuite.ts +0 -143
- package/src/testing/suites/orgStoreSuite.ts +0 -412
- package/src/testing/suites/platformPermissionStoreSuite.ts +0 -196
- package/src/testing/suites/platformProjectGrantStoreSuite.ts +0 -166
- package/src/testing/suites/projectStoreSuite.ts +0 -381
- package/src/testing/suites/shareLinkStoreSuite.ts +0 -203
- package/src/testing/suites/solveMetricSinkSuite.ts +0 -135
- package/src/testing/suites/storageProviderSuite.ts +0 -175
- package/src/testing/suites/userProfileStoreSuite.ts +0 -134
- package/src/userProfile/index.ts +0 -3
- package/src/userProfile/interface.ts +0 -48
- package/src/userProfile/types.ts +0 -24
- package/src/utils/audit.ts +0 -46
- package/src/utils/index.ts +0 -2
|
@@ -1,585 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter conformance suite for IDefinitionStore.
|
|
3
|
-
*
|
|
4
|
-
* The goal is to prove every adapter behaves identically from the consuming
|
|
5
|
-
* app's perspective — `ctx` scoping, `pending` filtering, version CRUD +
|
|
6
|
-
* reference protection (Permissions spec §6), and error shapes.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect } from 'vitest';
|
|
10
|
-
import type { IDefinitionStore } from '../../definitions/interface.js';
|
|
11
|
-
import type { DefinitionRecord, DefinitionVersion } from '../../definitions/types.js';
|
|
12
|
-
import { makeCtx, makeUuid } from './helpers.js';
|
|
13
|
-
import type { RequestContext } from '../../context.js';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Scope the suite needs to reference from each test. Adapters that enforce
|
|
17
|
-
* FK constraints (Supabase) return real uuids they've seeded; adapters that
|
|
18
|
-
* don't (local JSON) can omit this and the suite uses the defaults.
|
|
19
|
-
*/
|
|
20
|
-
export interface DefinitionTestScope {
|
|
21
|
-
/** Primary owner. Every record and ctx in the suite uses this id. */
|
|
22
|
-
ownerId: string;
|
|
23
|
-
/**
|
|
24
|
-
* Optional access token for `ownerId`. Required for adapters that enforce
|
|
25
|
-
* auth at the DB layer (Supabase RLS); ignored by local providers.
|
|
26
|
-
*/
|
|
27
|
-
ownerSessionToken?: string;
|
|
28
|
-
/** Default project id. Returned by `primaryProjectId()` in the suite. */
|
|
29
|
-
projectId: string;
|
|
30
|
-
/**
|
|
31
|
-
* Second project id used by `listByProject` tests to prove filtering.
|
|
32
|
-
* Must reference a real row for FK-enforcing adapters.
|
|
33
|
-
*/
|
|
34
|
-
secondaryProjectId: string;
|
|
35
|
-
/** Optional third user id for ctx-isolation tests. Required when ctxIsolation is set. */
|
|
36
|
-
secondaryUserId?: string;
|
|
37
|
-
/** Token for `secondaryUserId`, when used. */
|
|
38
|
-
secondaryUserSessionToken?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface DefinitionStoreConformanceOptions {
|
|
42
|
-
/** Name to show in the test output (e.g. "local-provider"). */
|
|
43
|
-
name: string;
|
|
44
|
-
/** Factory that returns a fresh, empty store per test. */
|
|
45
|
-
createStore: () => Promise<IDefinitionStore> | IDefinitionStore;
|
|
46
|
-
/**
|
|
47
|
-
* Per-test scope factory. Called once in each `it` block. Adapters with FK
|
|
48
|
-
* constraints seed the necessary `auth.users` / `projects` rows here and
|
|
49
|
-
* return the generated ids; adapters without FKs can return stub ids.
|
|
50
|
-
*/
|
|
51
|
-
createScope?: () => Promise<DefinitionTestScope> | DefinitionTestScope;
|
|
52
|
-
/**
|
|
53
|
-
* Set to true for adapters that enforce per-user ctx scoping (e.g. Supabase RLS).
|
|
54
|
-
* When false, ctx-isolation tests are skipped (local JSON adapter shares all records).
|
|
55
|
-
*/
|
|
56
|
-
ctxIsolation?: boolean;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const DEFAULT_SCOPE: DefinitionTestScope = {
|
|
60
|
-
ownerId: 'user-1',
|
|
61
|
-
projectId: 'project-1',
|
|
62
|
-
secondaryProjectId: 'project-2',
|
|
63
|
-
secondaryUserId: 'user-b'
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// Suite-wide token map. `scopeFor` populates it from the scope; `ctx`
|
|
67
|
-
// auto-attaches the matching token so adapters with DB-side auth (Supabase
|
|
68
|
-
// RLS) run as the right user. Local providers ignore tokens entirely.
|
|
69
|
-
const tokens = new Map<string, string>();
|
|
70
|
-
|
|
71
|
-
function ctx(
|
|
72
|
-
userId: string,
|
|
73
|
-
opts: {
|
|
74
|
-
actingOrgId?: string;
|
|
75
|
-
platformPermissions?: RequestContext['platformPermissions'];
|
|
76
|
-
orgPermissions?: RequestContext['orgPermissions'];
|
|
77
|
-
} = {}
|
|
78
|
-
): RequestContext {
|
|
79
|
-
return makeCtx(userId, { ...opts, sessionToken: tokens.get(userId) });
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function recordTokensFromScope(scope: DefinitionTestScope): void {
|
|
83
|
-
if (scope.ownerSessionToken) tokens.set(scope.ownerId, scope.ownerSessionToken);
|
|
84
|
-
if (scope.secondaryUserId && scope.secondaryUserSessionToken) {
|
|
85
|
-
tokens.set(scope.secondaryUserId, scope.secondaryUserSessionToken);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function record(
|
|
90
|
-
scope: DefinitionTestScope,
|
|
91
|
-
overrides: Partial<DefinitionRecord> = {}
|
|
92
|
-
): DefinitionRecord {
|
|
93
|
-
const now = new Date().toISOString();
|
|
94
|
-
return {
|
|
95
|
-
guid: overrides.guid ?? makeUuid(),
|
|
96
|
-
projectId: overrides.projectId ?? scope.projectId,
|
|
97
|
-
ownerId: overrides.ownerId ?? scope.ownerId,
|
|
98
|
-
createdBy: overrides.createdBy ?? scope.ownerId,
|
|
99
|
-
updatedBy: overrides.updatedBy ?? scope.ownerId,
|
|
100
|
-
displayName: overrides.displayName ?? 'Test',
|
|
101
|
-
status: overrides.status ?? 'published',
|
|
102
|
-
solveCount: overrides.solveCount ?? 0,
|
|
103
|
-
liveVersionId: overrides.liveVersionId ?? null,
|
|
104
|
-
draftVersionId: overrides.draftVersionId ?? null,
|
|
105
|
-
createdAt: overrides.createdAt ?? now,
|
|
106
|
-
updatedAt: overrides.updatedAt ?? now,
|
|
107
|
-
deletedAt: overrides.deletedAt ?? null,
|
|
108
|
-
...overrides
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function version(
|
|
113
|
-
definitionId: string,
|
|
114
|
-
versionNumber: number,
|
|
115
|
-
uploadedBy: string,
|
|
116
|
-
overrides: Partial<DefinitionVersion> = {}
|
|
117
|
-
): DefinitionVersion {
|
|
118
|
-
return {
|
|
119
|
-
id: overrides.id ?? makeUuid(),
|
|
120
|
-
definitionId,
|
|
121
|
-
versionNumber,
|
|
122
|
-
fileExt: overrides.fileExt ?? 'gh',
|
|
123
|
-
fileKey: overrides.fileKey ?? `definitions/${definitionId}/versions/v${versionNumber}.gh`,
|
|
124
|
-
originalFilename: overrides.originalFilename,
|
|
125
|
-
uploadedBy,
|
|
126
|
-
uploadedAt: overrides.uploadedAt ?? new Date().toISOString(),
|
|
127
|
-
...overrides
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function runDefinitionStoreConformance(opts: DefinitionStoreConformanceOptions): void {
|
|
132
|
-
const { name, createStore, createScope, ctxIsolation = false } = opts;
|
|
133
|
-
const scopeFor = async (): Promise<DefinitionTestScope> => {
|
|
134
|
-
const s = createScope ? await createScope() : DEFAULT_SCOPE;
|
|
135
|
-
recordTokensFromScope(s);
|
|
136
|
-
return s;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
describe(`IDefinitionStore conformance: ${name}`, () => {
|
|
140
|
-
it('create + get returns the record', async () => {
|
|
141
|
-
const store = await createStore();
|
|
142
|
-
const scope = await scopeFor();
|
|
143
|
-
const guid = makeUuid();
|
|
144
|
-
const rec = record(scope, { guid });
|
|
145
|
-
await store.create(ctx(scope.ownerId), rec);
|
|
146
|
-
const got = await store.get(ctx(scope.ownerId), guid);
|
|
147
|
-
expect(got?.guid).toBe(guid);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('get returns null for missing guid', async () => {
|
|
151
|
-
const store = await createStore();
|
|
152
|
-
const scope = await scopeFor();
|
|
153
|
-
const got = await store.get(ctx(scope.ownerId), makeUuid());
|
|
154
|
-
expect(got).toBeNull();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('list filters pending by default', async () => {
|
|
158
|
-
const store = await createStore();
|
|
159
|
-
const scope = await scopeFor();
|
|
160
|
-
const pub = makeUuid();
|
|
161
|
-
const pend = makeUuid();
|
|
162
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid: pub, status: 'published' }));
|
|
163
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid: pend, status: 'pending' }));
|
|
164
|
-
|
|
165
|
-
const page = await store.list(ctx(scope.ownerId));
|
|
166
|
-
const guids = page.items.map((r) => r.guid);
|
|
167
|
-
expect(guids).toContain(pub);
|
|
168
|
-
expect(guids).not.toContain(pend);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('list with includePending returns pending too', async () => {
|
|
172
|
-
const store = await createStore();
|
|
173
|
-
const scope = await scopeFor();
|
|
174
|
-
const pub = makeUuid();
|
|
175
|
-
const pend = makeUuid();
|
|
176
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid: pub, status: 'published' }));
|
|
177
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid: pend, status: 'pending' }));
|
|
178
|
-
|
|
179
|
-
const page = await store.list(ctx(scope.ownerId), { includePending: true });
|
|
180
|
-
const guids = page.items.map((r) => r.guid);
|
|
181
|
-
expect(guids).toContain(pub);
|
|
182
|
-
expect(guids).toContain(pend);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('list with statuses filter returns only matching statuses', async () => {
|
|
186
|
-
const store = await createStore();
|
|
187
|
-
const scope = await scopeFor();
|
|
188
|
-
const pub = makeUuid();
|
|
189
|
-
const dft = makeUuid();
|
|
190
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid: pub, status: 'published' }));
|
|
191
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid: dft, status: 'draft' }));
|
|
192
|
-
|
|
193
|
-
const page = await store.list(ctx(scope.ownerId), { statuses: ['draft'] });
|
|
194
|
-
const guids = page.items.map((r) => r.guid);
|
|
195
|
-
expect(guids).toContain(dft);
|
|
196
|
-
expect(guids).not.toContain(pub);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('listByProject filters by projectId', async () => {
|
|
200
|
-
const store = await createStore();
|
|
201
|
-
const scope = await scopeFor();
|
|
202
|
-
const a = makeUuid();
|
|
203
|
-
const b = makeUuid();
|
|
204
|
-
await store.create(
|
|
205
|
-
ctx(scope.ownerId),
|
|
206
|
-
record(scope, { guid: a, projectId: scope.projectId })
|
|
207
|
-
);
|
|
208
|
-
await store.create(
|
|
209
|
-
ctx(scope.ownerId),
|
|
210
|
-
record(scope, { guid: b, projectId: scope.secondaryProjectId })
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
const page = await store.listByProject(ctx(scope.ownerId), scope.projectId);
|
|
214
|
-
expect(page.items.map((r) => r.guid)).toEqual([a]);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('update applies patch and bumps updatedAt', async () => {
|
|
218
|
-
const store = await createStore();
|
|
219
|
-
const scope = await scopeFor();
|
|
220
|
-
const guid = makeUuid();
|
|
221
|
-
const created = record(scope, { guid, displayName: 'Old' });
|
|
222
|
-
await store.create(ctx(scope.ownerId), created);
|
|
223
|
-
|
|
224
|
-
await store.update(ctx(scope.ownerId), guid, { displayName: 'New' });
|
|
225
|
-
const got = await store.get(ctx(scope.ownerId), guid);
|
|
226
|
-
expect(got?.displayName).toBe('New');
|
|
227
|
-
expect(got!.updatedAt >= created.updatedAt).toBe(true);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('update on missing guid throws ProviderError with status 404', async () => {
|
|
231
|
-
const store = await createStore();
|
|
232
|
-
const scope = await scopeFor();
|
|
233
|
-
let thrown: unknown;
|
|
234
|
-
try {
|
|
235
|
-
await store.update(ctx(scope.ownerId), makeUuid(), { displayName: 'X' });
|
|
236
|
-
} catch (err) {
|
|
237
|
-
thrown = err;
|
|
238
|
-
}
|
|
239
|
-
expect(thrown).toBeTruthy();
|
|
240
|
-
const e = thrown as { statusCode?: number };
|
|
241
|
-
expect(e.statusCode).toBe(404);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('status flip pending → published via update', async () => {
|
|
245
|
-
const store = await createStore();
|
|
246
|
-
const scope = await scopeFor();
|
|
247
|
-
const guid = makeUuid();
|
|
248
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid, status: 'pending' }));
|
|
249
|
-
|
|
250
|
-
const beforeFlip = await store.list(ctx(scope.ownerId));
|
|
251
|
-
expect(beforeFlip.items.map((r) => r.guid)).not.toContain(guid);
|
|
252
|
-
|
|
253
|
-
await store.update(ctx(scope.ownerId), guid, { status: 'published' });
|
|
254
|
-
const afterFlip = await store.list(ctx(scope.ownerId));
|
|
255
|
-
expect(afterFlip.items.map((r) => r.guid)).toContain(guid);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('incrementSolveCount increases solveCount by 1', async () => {
|
|
259
|
-
const store = await createStore();
|
|
260
|
-
const scope = await scopeFor();
|
|
261
|
-
const guid = makeUuid();
|
|
262
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid, solveCount: 5 }));
|
|
263
|
-
await store.incrementSolveCount(ctx(scope.ownerId), guid);
|
|
264
|
-
const got = await store.get(ctx(scope.ownerId), guid);
|
|
265
|
-
expect(got?.solveCount).toBe(6);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('incrementSolveCount is a no-op for missing guid', async () => {
|
|
269
|
-
const store = await createStore();
|
|
270
|
-
const scope = await scopeFor();
|
|
271
|
-
let threw = false;
|
|
272
|
-
try {
|
|
273
|
-
await store.incrementSolveCount(ctx(scope.ownerId), makeUuid());
|
|
274
|
-
} catch {
|
|
275
|
-
threw = true;
|
|
276
|
-
}
|
|
277
|
-
expect(threw).toBe(false);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it('update advances updatedBy to the caller', async () => {
|
|
281
|
-
const store = await createStore();
|
|
282
|
-
const scope = await scopeFor();
|
|
283
|
-
const guid = makeUuid();
|
|
284
|
-
const editor = scope.secondaryUserId ?? makeUuid();
|
|
285
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid }));
|
|
286
|
-
// Editing as a different user — updatedBy should advance.
|
|
287
|
-
await store.update(ctx(editor), guid, { displayName: 'Renamed' });
|
|
288
|
-
const got = await store.get(ctx(scope.ownerId), guid);
|
|
289
|
-
expect(got?.updatedBy).toBe(editor);
|
|
290
|
-
expect(got?.createdBy).toBe(scope.ownerId);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// ============================================================================
|
|
294
|
-
// Versions (Permissions spec §6)
|
|
295
|
-
// ============================================================================
|
|
296
|
-
|
|
297
|
-
it('createVersion + listVersions: rows return newest-first by versionNumber', async () => {
|
|
298
|
-
const store = await createStore();
|
|
299
|
-
const scope = await scopeFor();
|
|
300
|
-
const guid = makeUuid();
|
|
301
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid }));
|
|
302
|
-
|
|
303
|
-
const v1 = version(guid, 1, scope.ownerId);
|
|
304
|
-
const v2 = version(guid, 2, scope.ownerId);
|
|
305
|
-
await store.createVersion(ctx(scope.ownerId), v1);
|
|
306
|
-
await store.createVersion(ctx(scope.ownerId), v2);
|
|
307
|
-
|
|
308
|
-
const page = await store.listVersions(ctx(scope.ownerId), guid);
|
|
309
|
-
expect(page.items.map((v) => v.versionNumber)).toEqual([2, 1]);
|
|
310
|
-
expect(page.items.map((v) => v.id)).toEqual([v2.id, v1.id]);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it('getVersion returns the row by id', async () => {
|
|
314
|
-
const store = await createStore();
|
|
315
|
-
const scope = await scopeFor();
|
|
316
|
-
const guid = makeUuid();
|
|
317
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid }));
|
|
318
|
-
const v1 = version(guid, 1, scope.ownerId);
|
|
319
|
-
await store.createVersion(ctx(scope.ownerId), v1);
|
|
320
|
-
|
|
321
|
-
const got = await store.getVersion(ctx(scope.ownerId), v1.id);
|
|
322
|
-
expect(got?.id).toBe(v1.id);
|
|
323
|
-
expect(got?.versionNumber).toBe(1);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it('setVersionSchema persists a cached schema on the version', async () => {
|
|
327
|
-
const store = await createStore();
|
|
328
|
-
const scope = await scopeFor();
|
|
329
|
-
const guid = makeUuid();
|
|
330
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid }));
|
|
331
|
-
const v1 = version(guid, 1, scope.ownerId);
|
|
332
|
-
await store.createVersion(ctx(scope.ownerId), v1);
|
|
333
|
-
expect((await store.getVersion(ctx(scope.ownerId), v1.id))?.schema).toBeUndefined();
|
|
334
|
-
|
|
335
|
-
const schema = {
|
|
336
|
-
name: 'Test',
|
|
337
|
-
inputs: [],
|
|
338
|
-
outputs: []
|
|
339
|
-
} as unknown as Parameters<typeof store.setVersionSchema>[2];
|
|
340
|
-
await store.setVersionSchema(ctx(scope.ownerId), v1.id, schema);
|
|
341
|
-
|
|
342
|
-
const got = await store.getVersion(ctx(scope.ownerId), v1.id);
|
|
343
|
-
expect(got?.schema).toEqual(schema);
|
|
344
|
-
expect(got?.schemaExtractedAt).toBeTypeOf('string');
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it('setVersionSchema is a no-op for a missing version', async () => {
|
|
348
|
-
const store = await createStore();
|
|
349
|
-
const scope = await scopeFor();
|
|
350
|
-
const schema = { name: 'X', inputs: [], outputs: [] } as unknown as Parameters<
|
|
351
|
-
typeof store.setVersionSchema
|
|
352
|
-
>[2];
|
|
353
|
-
await expect(
|
|
354
|
-
store.setVersionSchema(ctx(scope.ownerId), makeUuid(), schema)
|
|
355
|
-
).resolves.toBeUndefined();
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it('setLiveVersion + setDraftVersion repoint channels', async () => {
|
|
359
|
-
const store = await createStore();
|
|
360
|
-
const scope = await scopeFor();
|
|
361
|
-
const guid = makeUuid();
|
|
362
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid }));
|
|
363
|
-
const v1 = version(guid, 1, scope.ownerId);
|
|
364
|
-
const v2 = version(guid, 2, scope.ownerId);
|
|
365
|
-
await store.createVersion(ctx(scope.ownerId), v1);
|
|
366
|
-
await store.createVersion(ctx(scope.ownerId), v2);
|
|
367
|
-
|
|
368
|
-
await store.setDraftVersion(ctx(scope.ownerId), guid, v2.id);
|
|
369
|
-
let got = await store.get(ctx(scope.ownerId), guid);
|
|
370
|
-
expect(got?.draftVersionId).toBe(v2.id);
|
|
371
|
-
expect(got?.liveVersionId).toBeNull();
|
|
372
|
-
|
|
373
|
-
await store.setLiveVersion(ctx(scope.ownerId), guid, v1.id);
|
|
374
|
-
got = await store.get(ctx(scope.ownerId), guid);
|
|
375
|
-
expect(got?.liveVersionId).toBe(v1.id);
|
|
376
|
-
expect(got?.draftVersionId).toBe(v2.id);
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
it('attachInitialVersion: bootstraps both pointers + status atomically', async () => {
|
|
380
|
-
const store = await createStore();
|
|
381
|
-
const scope = await scopeFor();
|
|
382
|
-
const guid = makeUuid();
|
|
383
|
-
// Mirrors what DefinitionService.create writes: status='pending',
|
|
384
|
-
// no pointers — the state attachInitialVersion is meant to advance.
|
|
385
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid, status: 'pending' }));
|
|
386
|
-
const v1 = version(guid, 1, scope.ownerId);
|
|
387
|
-
await store.createVersion(ctx(scope.ownerId), v1);
|
|
388
|
-
|
|
389
|
-
await store.attachInitialVersion(ctx(scope.ownerId), guid, v1.id);
|
|
390
|
-
|
|
391
|
-
const got = await store.get(ctx(scope.ownerId), guid);
|
|
392
|
-
expect(got?.status).toBe('draft');
|
|
393
|
-
expect(got?.liveVersionId).toBe(v1.id);
|
|
394
|
-
expect(got?.draftVersionId).toBe(v1.id);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
it('attachInitialVersion: rejects a version belonging to another definition', async () => {
|
|
398
|
-
const store = await createStore();
|
|
399
|
-
const scope = await scopeFor();
|
|
400
|
-
const guidA = makeUuid();
|
|
401
|
-
const guidB = makeUuid();
|
|
402
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid: guidA, status: 'pending' }));
|
|
403
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid: guidB, status: 'pending' }));
|
|
404
|
-
const vForB = version(guidB, 1, scope.ownerId);
|
|
405
|
-
await store.createVersion(ctx(scope.ownerId), vForB);
|
|
406
|
-
|
|
407
|
-
let thrown: unknown;
|
|
408
|
-
try {
|
|
409
|
-
await store.attachInitialVersion(ctx(scope.ownerId), guidA, vForB.id);
|
|
410
|
-
} catch (err) {
|
|
411
|
-
thrown = err;
|
|
412
|
-
}
|
|
413
|
-
expect((thrown as { statusCode?: number })?.statusCode).toBe(404);
|
|
414
|
-
|
|
415
|
-
// And A's state must still be the bootstrap-pending shape.
|
|
416
|
-
const got = await store.get(ctx(scope.ownerId), guidA);
|
|
417
|
-
expect(got?.status).toBe('pending');
|
|
418
|
-
expect(got?.liveVersionId).toBeNull();
|
|
419
|
-
expect(got?.draftVersionId).toBeNull();
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
it('setLiveVersion rejects a version belonging to another definition', async () => {
|
|
423
|
-
const store = await createStore();
|
|
424
|
-
const scope = await scopeFor();
|
|
425
|
-
const guidA = makeUuid();
|
|
426
|
-
const guidB = makeUuid();
|
|
427
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid: guidA }));
|
|
428
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid: guidB }));
|
|
429
|
-
const vForB = version(guidB, 1, scope.ownerId);
|
|
430
|
-
await store.createVersion(ctx(scope.ownerId), vForB);
|
|
431
|
-
|
|
432
|
-
let thrown: unknown;
|
|
433
|
-
try {
|
|
434
|
-
await store.setLiveVersion(ctx(scope.ownerId), guidA, vForB.id);
|
|
435
|
-
} catch (err) {
|
|
436
|
-
thrown = err;
|
|
437
|
-
}
|
|
438
|
-
expect((thrown as { statusCode?: number })?.statusCode).toBe(404);
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it('deleteVersion: succeeds when not referenced by live or draft', async () => {
|
|
442
|
-
const store = await createStore();
|
|
443
|
-
const scope = await scopeFor();
|
|
444
|
-
const guid = makeUuid();
|
|
445
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid }));
|
|
446
|
-
const v1 = version(guid, 1, scope.ownerId);
|
|
447
|
-
const v2 = version(guid, 2, scope.ownerId);
|
|
448
|
-
await store.createVersion(ctx(scope.ownerId), v1);
|
|
449
|
-
await store.createVersion(ctx(scope.ownerId), v2);
|
|
450
|
-
await store.setLiveVersion(ctx(scope.ownerId), guid, v2.id);
|
|
451
|
-
await store.setDraftVersion(ctx(scope.ownerId), guid, v2.id);
|
|
452
|
-
|
|
453
|
-
await store.deleteVersion(ctx(scope.ownerId), v1.id);
|
|
454
|
-
const remaining = await store.listVersions(ctx(scope.ownerId), guid);
|
|
455
|
-
expect(remaining.items.map((v) => v.id)).toEqual([v2.id]);
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it('deleteVersion: throws 409 when the version is referenced by liveVersionId', async () => {
|
|
459
|
-
const store = await createStore();
|
|
460
|
-
const scope = await scopeFor();
|
|
461
|
-
const guid = makeUuid();
|
|
462
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid }));
|
|
463
|
-
const v1 = version(guid, 1, scope.ownerId);
|
|
464
|
-
await store.createVersion(ctx(scope.ownerId), v1);
|
|
465
|
-
await store.setLiveVersion(ctx(scope.ownerId), guid, v1.id);
|
|
466
|
-
|
|
467
|
-
let thrown: unknown;
|
|
468
|
-
try {
|
|
469
|
-
await store.deleteVersion(ctx(scope.ownerId), v1.id);
|
|
470
|
-
} catch (err) {
|
|
471
|
-
thrown = err;
|
|
472
|
-
}
|
|
473
|
-
expect((thrown as { statusCode?: number })?.statusCode).toBe(409);
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
it('delete removes the record', async () => {
|
|
477
|
-
const store = await createStore();
|
|
478
|
-
const scope = await scopeFor();
|
|
479
|
-
const guid = makeUuid();
|
|
480
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid }));
|
|
481
|
-
await store.delete(ctx(scope.ownerId), guid);
|
|
482
|
-
const got = await store.get(ctx(scope.ownerId), guid);
|
|
483
|
-
expect(got).toBeNull();
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
// ============================================================================
|
|
487
|
-
// Audit fields + soft delete
|
|
488
|
-
// ============================================================================
|
|
489
|
-
|
|
490
|
-
it('create populates createdBy/updatedBy and deletedAt=null', async () => {
|
|
491
|
-
const store = await createStore();
|
|
492
|
-
const scope = await scopeFor();
|
|
493
|
-
const guid = makeUuid();
|
|
494
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid }));
|
|
495
|
-
const got = await store.get(ctx(scope.ownerId), guid);
|
|
496
|
-
expect(got?.createdBy).toBe(scope.ownerId);
|
|
497
|
-
expect(got?.updatedBy).toBe(scope.ownerId);
|
|
498
|
-
expect(got?.deletedAt ?? null).toBeNull();
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
it('versioning scaffold: liveVersionId/draftVersionId default to null on create', async () => {
|
|
502
|
-
const store = await createStore();
|
|
503
|
-
const scope = await scopeFor();
|
|
504
|
-
const guid = makeUuid();
|
|
505
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid }));
|
|
506
|
-
const got = await store.get(ctx(scope.ownerId), guid);
|
|
507
|
-
expect(got?.liveVersionId ?? null).toBeNull();
|
|
508
|
-
expect(got?.draftVersionId ?? null).toBeNull();
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
it('delete soft-deletes — record excluded from list/listByProject', async () => {
|
|
512
|
-
const store = await createStore();
|
|
513
|
-
const scope = await scopeFor();
|
|
514
|
-
const guid = makeUuid();
|
|
515
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid, status: 'published' }));
|
|
516
|
-
await store.delete(ctx(scope.ownerId), guid);
|
|
517
|
-
const all = await store.list(ctx(scope.ownerId), { limit: 500 });
|
|
518
|
-
expect(all.items.map((r) => r.guid)).not.toContain(guid);
|
|
519
|
-
const byProject = await store.listByProject(ctx(scope.ownerId), scope.projectId, {
|
|
520
|
-
limit: 500
|
|
521
|
-
});
|
|
522
|
-
expect(byProject.items.map((r) => r.guid)).not.toContain(guid);
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
it('listPublic returns published records, not pending', async () => {
|
|
526
|
-
const store = await createStore();
|
|
527
|
-
const scope = await scopeFor();
|
|
528
|
-
const pub = makeUuid();
|
|
529
|
-
const pend = makeUuid();
|
|
530
|
-
const dft = makeUuid();
|
|
531
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid: pub, status: 'published' }));
|
|
532
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid: pend, status: 'pending' }));
|
|
533
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid: dft, status: 'draft' }));
|
|
534
|
-
|
|
535
|
-
const page = await store.listPublic(ctx(scope.ownerId));
|
|
536
|
-
expect(page.items.map((r) => r.guid)).toContain(pub);
|
|
537
|
-
expect(page.items.map((r) => r.guid)).not.toContain(pend);
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
it('pagination respects limit and nextCursor', async () => {
|
|
541
|
-
const store = await createStore();
|
|
542
|
-
const scope = await scopeFor();
|
|
543
|
-
for (let i = 0; i < 5; i++) {
|
|
544
|
-
await store.create(ctx(scope.ownerId), record(scope, { displayName: `N${i}` }));
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const first = await store.list(ctx(scope.ownerId), { limit: 2 });
|
|
548
|
-
expect(first.items.length).toBe(2);
|
|
549
|
-
expect(first.nextCursor).toBeTruthy();
|
|
550
|
-
|
|
551
|
-
const second = await store.list(ctx(scope.ownerId), { limit: 2, cursor: first.nextCursor });
|
|
552
|
-
expect(second.items.length).toBe(2);
|
|
553
|
-
const firstGuids = first.items.map((r) => r.guid);
|
|
554
|
-
for (const item of second.items) {
|
|
555
|
-
expect(firstGuids).not.toContain(item.guid);
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
if (ctxIsolation) {
|
|
560
|
-
it('ctx isolation: records created by one user are not visible to another', async () => {
|
|
561
|
-
const store = await createStore();
|
|
562
|
-
const scope = await scopeFor();
|
|
563
|
-
const otherUser = scope.secondaryUserId;
|
|
564
|
-
if (!otherUser) throw new Error('ctxIsolation requires scope.secondaryUserId');
|
|
565
|
-
const guid = makeUuid();
|
|
566
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid }));
|
|
567
|
-
|
|
568
|
-
const page = await store.list(ctx(otherUser));
|
|
569
|
-
expect(page.items.map((r) => r.guid)).not.toContain(guid);
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
it('ctx isolation: user cannot get a record they do not own', async () => {
|
|
573
|
-
const store = await createStore();
|
|
574
|
-
const scope = await scopeFor();
|
|
575
|
-
const otherUser = scope.secondaryUserId;
|
|
576
|
-
if (!otherUser) throw new Error('ctxIsolation requires scope.secondaryUserId');
|
|
577
|
-
const guid = makeUuid();
|
|
578
|
-
await store.create(ctx(scope.ownerId), record(scope, { guid }));
|
|
579
|
-
|
|
580
|
-
const got = await store.get(ctx(otherUser), guid);
|
|
581
|
-
expect(got).toBeNull();
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
});
|
|
585
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter conformance suite for IEmailLinkAuth.
|
|
3
|
-
*
|
|
4
|
-
* Tests the contract surface — input validation, error mapping, and rejection
|
|
5
|
-
* of obviously invalid tokens. The full round-trip (real email click) can't
|
|
6
|
-
* be exercised here without intercepting the IdP's mail delivery, so we test
|
|
7
|
-
* what's verifiable in-process and trust the IdP's own integration tests
|
|
8
|
-
* for token correctness.
|
|
9
|
-
*
|
|
10
|
-
* Adapters that don't broker email links should NOT run this suite.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { describe, it, expect } from 'vitest';
|
|
14
|
-
import type { IEmailLinkAuth } from '../../auth/index.js';
|
|
15
|
-
|
|
16
|
-
export interface EmailLinkAuthConformanceOptions {
|
|
17
|
-
/** Name to show in test output (e.g. "SupabaseEmailLinkAuth"). */
|
|
18
|
-
name: string;
|
|
19
|
-
/**
|
|
20
|
-
* Factory returning a fresh `IEmailLinkAuth` instance. May be configured
|
|
21
|
-
* with self-signup enabled or disabled — the suite's tests are robust to
|
|
22
|
-
* either, since they don't assume new users get created.
|
|
23
|
-
*
|
|
24
|
-
* `validEmail` is an address the adapter will accept (must pass adapter
|
|
25
|
-
* validation; doesn't need to actually receive mail). `invalidEmail` is
|
|
26
|
-
* one the adapter must reject as malformed.
|
|
27
|
-
*/
|
|
28
|
-
createAdapter: () => Promise<{
|
|
29
|
-
adapter: IEmailLinkAuth;
|
|
30
|
-
validEmail: string;
|
|
31
|
-
invalidEmail: string;
|
|
32
|
-
}>;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function runEmailLinkAuthConformance(opts: EmailLinkAuthConformanceOptions): void {
|
|
36
|
-
const { name, createAdapter } = opts;
|
|
37
|
-
|
|
38
|
-
describe(`IEmailLinkAuth conformance: ${name}`, () => {
|
|
39
|
-
it('verifyMagicLink returns null for an empty string', async () => {
|
|
40
|
-
const { adapter } = await createAdapter();
|
|
41
|
-
expect(await adapter.verifyMagicLink('')).toBeNull();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('verifyMagicLink returns null for a URL without a token', async () => {
|
|
45
|
-
const { adapter } = await createAdapter();
|
|
46
|
-
expect(await adapter.verifyMagicLink('https://example.com/callback')).toBeNull();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('verifyMagicLink returns null for a URL with garbage token_hash', async () => {
|
|
50
|
-
const { adapter } = await createAdapter();
|
|
51
|
-
const url =
|
|
52
|
-
'https://example.com/auth/email/callback?token_hash=NOT-A-REAL-TOKEN&type=magiclink';
|
|
53
|
-
expect(await adapter.verifyMagicLink(url)).toBeNull();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('verifyMagicLink returns null for an unrecognized verification type', async () => {
|
|
57
|
-
const { adapter } = await createAdapter();
|
|
58
|
-
const url = 'https://example.com/auth/email/callback?token_hash=abc&type=not-a-valid-type';
|
|
59
|
-
expect(await adapter.verifyMagicLink(url)).toBeNull();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('sendMagicLink rejects obviously invalid emails as { ok: false }', async () => {
|
|
63
|
-
const { adapter, invalidEmail } = await createAdapter();
|
|
64
|
-
const result = await adapter.sendMagicLink(
|
|
65
|
-
invalidEmail,
|
|
66
|
-
'https://example.com/auth/email/callback'
|
|
67
|
-
);
|
|
68
|
-
expect(result.ok).toBe(false);
|
|
69
|
-
if (!result.ok) {
|
|
70
|
-
expect(['invalid_email', 'rate_limited']).toContain(result.reason);
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// `sendMagicLink` for a valid email is intentionally NOT tested here:
|
|
75
|
-
// it requires real Supabase project credentials with email delivery
|
|
76
|
-
// enabled, and we'd be asserting on side-effects (mail sent) we can't
|
|
77
|
-
// observe. Adapter-specific integration tests cover that path.
|
|
78
|
-
void '';
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// keep `createAdapter` referenced for adapters that have nothing more to test
|
|
82
|
-
void createAdapter;
|
|
83
|
-
}
|