@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,379 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter conformance suite for the event-sink contract (Permissions spec §9).
|
|
3
|
-
*
|
|
4
|
-
* Verifies that each provider's stores emit the expected `DomainEvent` shape
|
|
5
|
-
* after the corresponding mutation succeeds, and that emit happens AFTER the
|
|
6
|
-
* write (so a failed write does not emit). Both reference providers run the
|
|
7
|
-
* same suite — drift between Local and Supabase emission shape gets caught
|
|
8
|
-
* immediately.
|
|
9
|
-
*
|
|
10
|
-
* Local stays on `NoopEventSink`; Supabase wires `SupabaseEventSink` so events
|
|
11
|
-
* persist to `public.audit_events`. The suite guarantees the seam exists and
|
|
12
|
-
* the shape is consistent so future webhook / audit / analytics consumers can
|
|
13
|
-
* plug in by swapping the sink.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
17
|
-
import type {
|
|
18
|
-
IDataProvider,
|
|
19
|
-
DomainEvent,
|
|
20
|
-
IEventSink,
|
|
21
|
-
Organization,
|
|
22
|
-
Project,
|
|
23
|
-
OrgMember,
|
|
24
|
-
DefinitionRecord,
|
|
25
|
-
DefinitionVersion,
|
|
26
|
-
ShareLink,
|
|
27
|
-
RequestContext
|
|
28
|
-
} from '../../index.js';
|
|
29
|
-
import { ALL_ORG_PERMISSIONS, ALL_PLATFORM_PERMISSIONS } from '../../index.js';
|
|
30
|
-
import { makeUuid } from './helpers.js';
|
|
31
|
-
|
|
32
|
-
/** Records every event for assertion. Resets per test via `clear()`. */
|
|
33
|
-
export class RecordingEventSink implements IEventSink {
|
|
34
|
-
readonly events: DomainEvent[] = [];
|
|
35
|
-
|
|
36
|
-
async emit(event: DomainEvent): Promise<void> {
|
|
37
|
-
this.events.push(event);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
clear(): void {
|
|
41
|
-
this.events.length = 0;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
last(): DomainEvent | undefined {
|
|
45
|
-
return this.events[this.events.length - 1];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
ofType<T extends DomainEvent['type']>(type: T): Extract<DomainEvent, { type: T }>[] {
|
|
49
|
-
return this.events.filter((e): e is Extract<DomainEvent, { type: T }> => e.type === type);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface EventSinkConformanceOptions {
|
|
54
|
-
name: string;
|
|
55
|
-
/**
|
|
56
|
-
* Build a fresh data provider wired to the given sink. Adapters that need
|
|
57
|
-
* to seed FK-related state (Supabase: `auth.users` rows) do it here.
|
|
58
|
-
*/
|
|
59
|
-
createProvider: (sink: RecordingEventSink) => Promise<IDataProvider> | IDataProvider;
|
|
60
|
-
/**
|
|
61
|
-
* The actor user id that mutations will be performed as. The suite needs
|
|
62
|
-
* this to be a real id in the adapter's auth backend (Supabase has FK).
|
|
63
|
-
* Local can return any uuid.
|
|
64
|
-
*
|
|
65
|
-
* `sessionToken` is required for adapters with DB-side auth (Supabase RLS)
|
|
66
|
-
* so the per-test `ctx` runs as that authenticated user. Local can omit.
|
|
67
|
-
*/
|
|
68
|
-
createActorId: () => Promise<{ userId: string; sessionToken?: string }>;
|
|
69
|
-
/**
|
|
70
|
-
* Optional hook for cleaning up after the test (delete tempdir, truncate
|
|
71
|
-
* Supabase tables, etc.). Called once at the end of each test.
|
|
72
|
-
*/
|
|
73
|
-
cleanup?: () => Promise<void> | void;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function runEventSinkConformance(opts: EventSinkConformanceOptions): void {
|
|
77
|
-
const { name, createProvider, createActorId, cleanup } = opts;
|
|
78
|
-
|
|
79
|
-
describe(`IEventSink conformance: ${name}`, () => {
|
|
80
|
-
let sink: RecordingEventSink;
|
|
81
|
-
let provider: IDataProvider;
|
|
82
|
-
let actorId: string;
|
|
83
|
-
let ctx: RequestContext;
|
|
84
|
-
|
|
85
|
-
beforeEach(async () => {
|
|
86
|
-
sink = new RecordingEventSink();
|
|
87
|
-
provider = await createProvider(sink);
|
|
88
|
-
const seeded = await createActorId();
|
|
89
|
-
actorId = seeded.userId;
|
|
90
|
-
ctx = {
|
|
91
|
-
userId: actorId,
|
|
92
|
-
platformPermissions: [...ALL_PLATFORM_PERMISSIONS],
|
|
93
|
-
orgPermissions: [...ALL_ORG_PERMISSIONS],
|
|
94
|
-
adapterContext: seeded.sessionToken ? { sessionToken: seeded.sessionToken } : undefined
|
|
95
|
-
};
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Clean up after each test if the adapter supplies a hook. Always
|
|
99
|
-
// registered (calling an undefined cleanup is the no-op path).
|
|
100
|
-
afterEach(async () => {
|
|
101
|
-
if (cleanup) await cleanup();
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('createOrg emits org.created', async () => {
|
|
105
|
-
const orgId = makeUuid();
|
|
106
|
-
await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
|
|
107
|
-
|
|
108
|
-
const created = sink.ofType('org.created');
|
|
109
|
-
expect(created).toHaveLength(1);
|
|
110
|
-
expect(created[0]).toMatchObject({ orgId, actorId });
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('deleteOrg emits org.deleted', async () => {
|
|
114
|
-
const orgId = makeUuid();
|
|
115
|
-
await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
|
|
116
|
-
sink.clear();
|
|
117
|
-
|
|
118
|
-
await provider.orgs.deleteOrg(ctx, orgId);
|
|
119
|
-
|
|
120
|
-
expect(sink.ofType('org.deleted')).toMatchObject([{ orgId, actorId }]);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('addOrgMember emits org_member.added', async () => {
|
|
124
|
-
const orgId = makeUuid();
|
|
125
|
-
const { userId: newMemberId } = await createActorId();
|
|
126
|
-
await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
|
|
127
|
-
sink.clear();
|
|
128
|
-
|
|
129
|
-
await provider.orgs.addOrgMember(ctx, makeOrgMember({ orgId, userId: newMemberId }));
|
|
130
|
-
|
|
131
|
-
expect(sink.ofType('org_member.added')).toMatchObject([
|
|
132
|
-
{ orgId, userId: newMemberId, actorId }
|
|
133
|
-
]);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('removeOrgMember emits org_member.removed', async () => {
|
|
137
|
-
const orgId = makeUuid();
|
|
138
|
-
const { userId: newMemberId } = await createActorId();
|
|
139
|
-
await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
|
|
140
|
-
await provider.orgs.addOrgMember(ctx, makeOrgMember({ orgId, userId: newMemberId }));
|
|
141
|
-
sink.clear();
|
|
142
|
-
|
|
143
|
-
await provider.orgs.removeOrgMember(ctx, orgId, newMemberId);
|
|
144
|
-
|
|
145
|
-
expect(sink.ofType('org_member.removed')).toMatchObject([
|
|
146
|
-
{ orgId, userId: newMemberId, actorId }
|
|
147
|
-
]);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('createProject emits project.created with the parent orgId', async () => {
|
|
151
|
-
const orgId = makeUuid();
|
|
152
|
-
const projectId = makeUuid();
|
|
153
|
-
await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
|
|
154
|
-
sink.clear();
|
|
155
|
-
|
|
156
|
-
await provider.projects.createProject(
|
|
157
|
-
ctx,
|
|
158
|
-
makeProject({ id: projectId, orgId, ownerId: actorId })
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
expect(sink.ofType('project.created')).toMatchObject([{ projectId, orgId, actorId }]);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('deleteProject emits project.deleted', async () => {
|
|
165
|
-
const orgId = makeUuid();
|
|
166
|
-
const projectId = makeUuid();
|
|
167
|
-
await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
|
|
168
|
-
await provider.projects.createProject(
|
|
169
|
-
ctx,
|
|
170
|
-
makeProject({ id: projectId, orgId, ownerId: actorId })
|
|
171
|
-
);
|
|
172
|
-
sink.clear();
|
|
173
|
-
|
|
174
|
-
await provider.projects.deleteProject(ctx, projectId);
|
|
175
|
-
|
|
176
|
-
expect(sink.ofType('project.deleted')).toMatchObject([{ projectId, actorId }]);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('createDefinition + setLiveVersion emit created + published', async () => {
|
|
180
|
-
const orgId = makeUuid();
|
|
181
|
-
const projectId = makeUuid();
|
|
182
|
-
const definitionId = makeUuid();
|
|
183
|
-
const versionId = makeUuid();
|
|
184
|
-
|
|
185
|
-
await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
|
|
186
|
-
await provider.projects.createProject(
|
|
187
|
-
ctx,
|
|
188
|
-
makeProject({ id: projectId, orgId, ownerId: actorId })
|
|
189
|
-
);
|
|
190
|
-
sink.clear();
|
|
191
|
-
|
|
192
|
-
await provider.definitions.create(
|
|
193
|
-
ctx,
|
|
194
|
-
makeDefinition({ guid: definitionId, projectId, ownerId: actorId })
|
|
195
|
-
);
|
|
196
|
-
await provider.definitions.createVersion(
|
|
197
|
-
ctx,
|
|
198
|
-
makeVersion({ id: versionId, definitionId, uploadedBy: actorId })
|
|
199
|
-
);
|
|
200
|
-
await provider.definitions.setLiveVersion(ctx, definitionId, versionId);
|
|
201
|
-
|
|
202
|
-
expect(sink.ofType('definition.created')).toMatchObject([
|
|
203
|
-
{ definitionId, projectId, actorId }
|
|
204
|
-
]);
|
|
205
|
-
expect(sink.ofType('definition_version.created')).toMatchObject([
|
|
206
|
-
{ versionId, definitionId, actorId }
|
|
207
|
-
]);
|
|
208
|
-
expect(sink.ofType('definition.published')).toMatchObject([
|
|
209
|
-
{ definitionId, versionId, actorId }
|
|
210
|
-
]);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('share-link create + revoke emit minted + revoked', async () => {
|
|
214
|
-
const orgId = makeUuid();
|
|
215
|
-
const projectId = makeUuid();
|
|
216
|
-
const definitionId = makeUuid();
|
|
217
|
-
const linkId = makeUuid();
|
|
218
|
-
|
|
219
|
-
await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
|
|
220
|
-
await provider.projects.createProject(
|
|
221
|
-
ctx,
|
|
222
|
-
makeProject({ id: projectId, orgId, ownerId: actorId })
|
|
223
|
-
);
|
|
224
|
-
await provider.definitions.create(
|
|
225
|
-
ctx,
|
|
226
|
-
makeDefinition({ guid: definitionId, projectId, ownerId: actorId })
|
|
227
|
-
);
|
|
228
|
-
sink.clear();
|
|
229
|
-
|
|
230
|
-
await provider.shareLinks.create(
|
|
231
|
-
ctx,
|
|
232
|
-
makeShareLink({ id: linkId, definitionId, createdBy: actorId })
|
|
233
|
-
);
|
|
234
|
-
await provider.shareLinks.revoke(ctx, linkId);
|
|
235
|
-
|
|
236
|
-
expect(sink.ofType('share_link.minted')).toMatchObject([{ linkId, definitionId, actorId }]);
|
|
237
|
-
expect(sink.ofType('share_link.revoked')).toMatchObject([{ linkId, actorId }]);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('failed write does not emit', async () => {
|
|
241
|
-
const orgId = makeUuid();
|
|
242
|
-
await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
|
|
243
|
-
sink.clear();
|
|
244
|
-
// Duplicate-id create must throw and must not emit.
|
|
245
|
-
await expect(
|
|
246
|
-
provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }))
|
|
247
|
-
).rejects.toThrow();
|
|
248
|
-
expect(sink.ofType('org.created')).toHaveLength(0);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it('system-context mutations emit with actorId="system"', async () => {
|
|
252
|
-
const sysCtx: RequestContext = {
|
|
253
|
-
userId: '',
|
|
254
|
-
platformPermissions: [...ALL_PLATFORM_PERMISSIONS],
|
|
255
|
-
orgPermissions: [...ALL_ORG_PERMISSIONS],
|
|
256
|
-
system: true
|
|
257
|
-
};
|
|
258
|
-
const orgId = makeUuid();
|
|
259
|
-
await provider.orgs.createOrg(sysCtx, makeOrg({ id: orgId, ownerId: actorId }));
|
|
260
|
-
|
|
261
|
-
const created = sink.ofType('org.created');
|
|
262
|
-
expect(created).toHaveLength(1);
|
|
263
|
-
expect(created[0].actorId).toBe('system');
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// ============================================================================
|
|
269
|
-
// Test data helpers
|
|
270
|
-
// ============================================================================
|
|
271
|
-
function nowIso(): string {
|
|
272
|
-
return new Date().toISOString();
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function makeOrg(overrides: { id: string; ownerId: string }): Organization {
|
|
276
|
-
const now = nowIso();
|
|
277
|
-
return {
|
|
278
|
-
id: overrides.id,
|
|
279
|
-
name: `Org ${overrides.id.slice(0, 6)}`,
|
|
280
|
-
slug: `org-${overrides.id.slice(0, 8)}`,
|
|
281
|
-
ownerId: overrides.ownerId,
|
|
282
|
-
createdBy: overrides.ownerId,
|
|
283
|
-
updatedBy: overrides.ownerId,
|
|
284
|
-
createdAt: now,
|
|
285
|
-
updatedAt: now,
|
|
286
|
-
deletedAt: null
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function makeOrgMember(overrides: { orgId: string; userId: string }): OrgMember {
|
|
291
|
-
const now = nowIso();
|
|
292
|
-
return {
|
|
293
|
-
orgId: overrides.orgId,
|
|
294
|
-
userId: overrides.userId,
|
|
295
|
-
role: 'member',
|
|
296
|
-
permissions: [],
|
|
297
|
-
joinedAt: now,
|
|
298
|
-
updatedAt: now,
|
|
299
|
-
updatedBy: overrides.userId,
|
|
300
|
-
deletedAt: null
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function makeProject(overrides: { id: string; orgId: string; ownerId: string }): Project {
|
|
305
|
-
const now = nowIso();
|
|
306
|
-
return {
|
|
307
|
-
id: overrides.id,
|
|
308
|
-
orgId: overrides.orgId,
|
|
309
|
-
name: `Project ${overrides.id.slice(0, 6)}`,
|
|
310
|
-
slug: `proj-${overrides.id.slice(0, 8)}`,
|
|
311
|
-
visibility: 'private',
|
|
312
|
-
autoJoinOnUpload: false,
|
|
313
|
-
ownerId: overrides.ownerId,
|
|
314
|
-
createdBy: overrides.ownerId,
|
|
315
|
-
updatedBy: overrides.ownerId,
|
|
316
|
-
createdAt: now,
|
|
317
|
-
updatedAt: now,
|
|
318
|
-
deletedAt: null
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function makeDefinition(overrides: {
|
|
323
|
-
guid: string;
|
|
324
|
-
projectId: string;
|
|
325
|
-
ownerId: string;
|
|
326
|
-
}): DefinitionRecord {
|
|
327
|
-
const now = nowIso();
|
|
328
|
-
return {
|
|
329
|
-
guid: overrides.guid,
|
|
330
|
-
projectId: overrides.projectId,
|
|
331
|
-
ownerId: overrides.ownerId,
|
|
332
|
-
createdBy: overrides.ownerId,
|
|
333
|
-
updatedBy: overrides.ownerId,
|
|
334
|
-
displayName: `Def ${overrides.guid.slice(0, 6)}`,
|
|
335
|
-
status: 'published',
|
|
336
|
-
solveCount: 0,
|
|
337
|
-
liveVersionId: null,
|
|
338
|
-
draftVersionId: null,
|
|
339
|
-
createdAt: now,
|
|
340
|
-
updatedAt: now,
|
|
341
|
-
deletedAt: null
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function makeVersion(overrides: {
|
|
346
|
-
id: string;
|
|
347
|
-
definitionId: string;
|
|
348
|
-
uploadedBy: string;
|
|
349
|
-
}): DefinitionVersion {
|
|
350
|
-
return {
|
|
351
|
-
id: overrides.id,
|
|
352
|
-
definitionId: overrides.definitionId,
|
|
353
|
-
versionNumber: 1,
|
|
354
|
-
fileExt: 'gh',
|
|
355
|
-
fileKey: `defs/${overrides.definitionId}/v/${overrides.id}/file.gh`,
|
|
356
|
-
uploadedBy: overrides.uploadedBy,
|
|
357
|
-
uploadedAt: nowIso()
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function makeShareLink(overrides: {
|
|
362
|
-
id: string;
|
|
363
|
-
definitionId: string;
|
|
364
|
-
createdBy: string;
|
|
365
|
-
}): ShareLink {
|
|
366
|
-
return {
|
|
367
|
-
id: overrides.id,
|
|
368
|
-
definitionId: overrides.definitionId,
|
|
369
|
-
channel: 'live',
|
|
370
|
-
tokenHash: `hash-${overrides.id}`,
|
|
371
|
-
createdBy: overrides.createdBy,
|
|
372
|
-
createdAt: nowIso(),
|
|
373
|
-
expiresAt: null,
|
|
374
|
-
revokedAt: null,
|
|
375
|
-
allowSolve: true,
|
|
376
|
-
maxSolves: null,
|
|
377
|
-
solveCount: 0
|
|
378
|
-
};
|
|
379
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import type { RequestContext } from '../../context.js';
|
|
2
|
-
import { ALL_ORG_PERMISSIONS } from '../../organizations/schemas.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Build a test `RequestContext`. Defaults to a platform admin to keep existing
|
|
6
|
-
* suites permissive; tests that verify permission gating should pass explicit
|
|
7
|
-
* platform/org permission arrays.
|
|
8
|
-
*
|
|
9
|
-
* `sessionToken`, when supplied, is stuffed into `adapterContext.sessionToken`
|
|
10
|
-
* so adapters that enforce auth at the DB layer (Supabase RLS) can run the
|
|
11
|
-
* call as a real authenticated user. Local providers ignore it.
|
|
12
|
-
*/
|
|
13
|
-
export function makeCtx(
|
|
14
|
-
userId: string,
|
|
15
|
-
opts: {
|
|
16
|
-
actingOrgId?: string;
|
|
17
|
-
platformPermissions?: RequestContext['platformPermissions'];
|
|
18
|
-
orgPermissions?: RequestContext['orgPermissions'];
|
|
19
|
-
sessionToken?: string;
|
|
20
|
-
} = {}
|
|
21
|
-
): RequestContext {
|
|
22
|
-
return {
|
|
23
|
-
userId,
|
|
24
|
-
actingOrgId: opts.actingOrgId,
|
|
25
|
-
platformPermissions: opts.platformPermissions ?? ['instance_admin'],
|
|
26
|
-
orgPermissions: opts.orgPermissions ?? [...ALL_ORG_PERMISSIONS],
|
|
27
|
-
adapterContext: opts.sessionToken ? { sessionToken: opts.sessionToken } : undefined
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function makeUuid(): string {
|
|
32
|
-
return crypto.randomUUID();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Optional hook adapters pass when their backend enforces referential
|
|
37
|
-
* integrity on user ids (e.g. Supabase `auth.users`). Returns the id the
|
|
38
|
-
* adapter actually stored AND, optionally, a session token the suite can
|
|
39
|
-
* thread back into `makeCtx({ sessionToken })` so the resulting calls run
|
|
40
|
-
* as that user.
|
|
41
|
-
*
|
|
42
|
-
* Adapters without user FKs (local JSON) return the suggested id as-is and
|
|
43
|
-
* omit the token. Adapters with auth (Supabase) ignore the suggested id,
|
|
44
|
-
* create a fresh user, sign in as them, and return the real id + access JWT.
|
|
45
|
-
*
|
|
46
|
-
* The conformance suite calls this to get a usable id *before* constructing
|
|
47
|
-
* any record that references a user. It never assumes the suggested id is
|
|
48
|
-
* the one used.
|
|
49
|
-
*/
|
|
50
|
-
export type SeedUserResult = { userId: string; sessionToken?: string };
|
|
51
|
-
export type SeedUserFn = (suggestedId: string) => Promise<SeedUserResult>;
|
|
52
|
-
|
|
53
|
-
/** Default no-op for adapters that don't need to seed users. Echoes the id back, no token. */
|
|
54
|
-
export const noopSeedUser: SeedUserFn = async (id) => ({ userId: id });
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Wraps a `SeedUserFn` so suites can keep their existing `seed() → userId` and
|
|
58
|
-
* `ctx(userId)` shape while transparently threading session tokens for adapters
|
|
59
|
-
* that return them. Build one per test (Map is mutable & local).
|
|
60
|
-
*/
|
|
61
|
-
export function makeSeedHelpers(seedUser: SeedUserFn) {
|
|
62
|
-
const tokens = new Map<string, string>();
|
|
63
|
-
|
|
64
|
-
async function seed(): Promise<string> {
|
|
65
|
-
const result = await seedUser(makeUuid());
|
|
66
|
-
if (result.sessionToken) tokens.set(result.userId, result.sessionToken);
|
|
67
|
-
return result.userId;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Register a token for a user that wasn't created via `seed()` (e.g. one returned by `createStore`). */
|
|
71
|
-
function registerToken(userId: string, sessionToken: string): void {
|
|
72
|
-
tokens.set(userId, sessionToken);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function ctx(
|
|
76
|
-
userId: string,
|
|
77
|
-
opts: {
|
|
78
|
-
actingOrgId?: string;
|
|
79
|
-
platformPermissions?: RequestContext['platformPermissions'];
|
|
80
|
-
orgPermissions?: RequestContext['orgPermissions'];
|
|
81
|
-
} = {}
|
|
82
|
-
): RequestContext {
|
|
83
|
-
return makeCtx(userId, { ...opts, sessionToken: tokens.get(userId) });
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return { seed, ctx, registerToken, tokens };
|
|
87
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter conformance suite for IInviteStore.
|
|
3
|
-
*
|
|
4
|
-
* Invites are the handoff between "admin wants to add Alice" and "Alice has
|
|
5
|
-
* an account + an org membership". The contract is small but there are a few
|
|
6
|
-
* state-machine rules adapters must honor:
|
|
7
|
-
*
|
|
8
|
-
* - `getByTokenHash` returns null for missing, expired, or already-consumed
|
|
9
|
-
* invites. The store sees only HMAC digests; the route layer hashes raw
|
|
10
|
-
* tokens before lookup.
|
|
11
|
-
* - `markAccepted` is idempotent (no-op if already accepted).
|
|
12
|
-
* - `revoke` removes pending invites but must not un-consume accepted ones.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { describe, it, expect } from 'vitest';
|
|
16
|
-
import type { IInviteStore } from '../../invites/interface.js';
|
|
17
|
-
import type { Invite } from '../../invites/types.js';
|
|
18
|
-
import { SYSTEM_CONTEXT } from '../../context.js';
|
|
19
|
-
import { makeSeedHelpers, makeUuid, noopSeedUser, type SeedUserFn } from './helpers.js';
|
|
20
|
-
|
|
21
|
-
export interface InviteTestScope {
|
|
22
|
-
/** User that creates invites. Must be seeded + have `manage_org_members` in the org. */
|
|
23
|
-
adminId: string;
|
|
24
|
-
/**
|
|
25
|
-
* Optional access token for `adminId`. Required for adapters with DB-side
|
|
26
|
-
* auth (Supabase RLS); local providers ignore it.
|
|
27
|
-
*/
|
|
28
|
-
adminSessionToken?: string;
|
|
29
|
-
/** Org invites are issued for. Must be seeded with the admin as a member. */
|
|
30
|
-
orgId: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface InviteStoreConformanceOptions {
|
|
34
|
-
name: string;
|
|
35
|
-
createStore: () => Promise<IInviteStore> | IInviteStore;
|
|
36
|
-
createScope?: () => Promise<InviteTestScope> | InviteTestScope;
|
|
37
|
-
seedUser?: SeedUserFn;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function invite(scope: InviteTestScope, overrides: Partial<Invite> = {}): Invite {
|
|
41
|
-
const now = new Date();
|
|
42
|
-
return {
|
|
43
|
-
id: overrides.id ?? makeUuid(),
|
|
44
|
-
// Conformance treats `tokenHash` as opaque — the suite stores whatever
|
|
45
|
-
// the adapter receives and looks it up by the same value. The route
|
|
46
|
-
// layer is the one that mints raw tokens and HMACs them; the store
|
|
47
|
-
// contract is "lookup by stored digest".
|
|
48
|
-
tokenHash:
|
|
49
|
-
overrides.tokenHash ?? `hash-${makeUuid()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
50
|
-
email: overrides.email ?? `invitee-${Math.random().toString(36).slice(2, 6)}@test.local`,
|
|
51
|
-
orgId: overrides.orgId ?? scope.orgId,
|
|
52
|
-
orgRole: overrides.orgRole ?? 'member',
|
|
53
|
-
orgPermissions: overrides.orgPermissions ?? [],
|
|
54
|
-
invitedBy: overrides.invitedBy ?? scope.adminId,
|
|
55
|
-
createdAt: overrides.createdAt ?? now.toISOString(),
|
|
56
|
-
expiresAt: overrides.expiresAt ?? new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(),
|
|
57
|
-
acceptedAt: overrides.acceptedAt,
|
|
58
|
-
acceptedByUserId: overrides.acceptedByUserId
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const DEFAULT_SCOPE: InviteTestScope = {
|
|
63
|
-
adminId: 'admin-1',
|
|
64
|
-
orgId: 'org-1'
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export function runInviteStoreConformance(opts: InviteStoreConformanceOptions): void {
|
|
68
|
-
const { name, createStore, createScope, seedUser = noopSeedUser } = opts;
|
|
69
|
-
const { ctx, registerToken } = makeSeedHelpers(seedUser);
|
|
70
|
-
const scopeFor = async (): Promise<InviteTestScope> => {
|
|
71
|
-
const s = createScope ? await createScope() : DEFAULT_SCOPE;
|
|
72
|
-
if (s.adminSessionToken) registerToken(s.adminId, s.adminSessionToken);
|
|
73
|
-
return s;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
describe(`IInviteStore conformance: ${name}`, () => {
|
|
77
|
-
it('create + getByTokenHash round-trips', async () => {
|
|
78
|
-
const store = await createStore();
|
|
79
|
-
const scope = await scopeFor();
|
|
80
|
-
const inv = invite(scope);
|
|
81
|
-
await store.create(ctx(scope.adminId), inv);
|
|
82
|
-
const got = await store.getByTokenHash(SYSTEM_CONTEXT, inv.tokenHash);
|
|
83
|
-
expect(got?.id).toBe(inv.id);
|
|
84
|
-
expect(got?.email).toBe(inv.email);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('getByTokenHash returns null for unknown hash', async () => {
|
|
88
|
-
const store = await createStore();
|
|
89
|
-
const got = await store.getByTokenHash(SYSTEM_CONTEXT, 'nope-' + makeUuid());
|
|
90
|
-
expect(got).toBeNull();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('getByTokenHash returns null for expired invite', async () => {
|
|
94
|
-
const store = await createStore();
|
|
95
|
-
const scope = await scopeFor();
|
|
96
|
-
const inv = invite(scope, {
|
|
97
|
-
expiresAt: new Date(Date.now() - 60_000).toISOString()
|
|
98
|
-
});
|
|
99
|
-
await store.create(ctx(scope.adminId), inv);
|
|
100
|
-
const got = await store.getByTokenHash(SYSTEM_CONTEXT, inv.tokenHash);
|
|
101
|
-
expect(got).toBeNull();
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('markAccepted sets acceptedAt + acceptedByUserId; getByTokenHash then returns null', async () => {
|
|
105
|
-
const store = await createStore();
|
|
106
|
-
const scope = await scopeFor();
|
|
107
|
-
const { userId: acceptor } = await seedUser(makeUuid());
|
|
108
|
-
const inv = invite(scope);
|
|
109
|
-
await store.create(ctx(scope.adminId), inv);
|
|
110
|
-
await store.markAccepted(SYSTEM_CONTEXT, inv.id, acceptor);
|
|
111
|
-
const got = await store.getByTokenHash(SYSTEM_CONTEXT, inv.tokenHash);
|
|
112
|
-
expect(got).toBeNull();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('markAccepted is idempotent (second call is a no-op)', async () => {
|
|
116
|
-
const store = await createStore();
|
|
117
|
-
const scope = await scopeFor();
|
|
118
|
-
const { userId: acceptor } = await seedUser(makeUuid());
|
|
119
|
-
const inv = invite(scope);
|
|
120
|
-
await store.create(ctx(scope.adminId), inv);
|
|
121
|
-
await store.markAccepted(SYSTEM_CONTEXT, inv.id, acceptor);
|
|
122
|
-
await store.markAccepted(SYSTEM_CONTEXT, inv.id, acceptor);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('listByOrg returns invites for the org', async () => {
|
|
126
|
-
const store = await createStore();
|
|
127
|
-
const scope = await scopeFor();
|
|
128
|
-
for (let i = 0; i < 3; i++) await store.create(ctx(scope.adminId), invite(scope));
|
|
129
|
-
const page = await store.listByOrg(ctx(scope.adminId), scope.orgId, { limit: 100 });
|
|
130
|
-
expect(page.items.length).toBeGreaterThanOrEqual(3);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('revoke removes a pending invite', async () => {
|
|
134
|
-
const store = await createStore();
|
|
135
|
-
const scope = await scopeFor();
|
|
136
|
-
const inv = invite(scope);
|
|
137
|
-
await store.create(ctx(scope.adminId), inv);
|
|
138
|
-
await store.revoke(ctx(scope.adminId), inv.id);
|
|
139
|
-
const got = await store.getByTokenHash(SYSTEM_CONTEXT, inv.tokenHash);
|
|
140
|
-
expect(got).toBeNull();
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
}
|