@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,412 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter conformance suite for IOrgStore.
|
|
3
|
-
*
|
|
4
|
-
* Tests organization CRUD and org membership management to ensure all
|
|
5
|
-
* adapters behave identically.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect } from 'vitest';
|
|
9
|
-
import type { IOrgStore } from '../../organizations/interface.js';
|
|
10
|
-
import type { IComputeServerStore } from '../../computeServer/interface.js';
|
|
11
|
-
import type { IInviteStore } from '../../invites/interface.js';
|
|
12
|
-
import type { Organization, OrgMember, OrgRole, Invite, ComputeServerConfig } from '../../index.js';
|
|
13
|
-
import { DEFAULT_ORG_PERMISSIONS } from '../../organizations/schemas.js';
|
|
14
|
-
import { SYSTEM_CONTEXT } from '../../context.js';
|
|
15
|
-
import { makeSeedHelpers, makeUuid, noopSeedUser, type SeedUserFn } from './helpers.js';
|
|
16
|
-
|
|
17
|
-
export interface OrgStoreConformanceOptions {
|
|
18
|
-
/** Name to show in test output (e.g. "local-provider"). */
|
|
19
|
-
name: string;
|
|
20
|
-
/** Factory that returns a fresh, empty store per test. */
|
|
21
|
-
createStore: () => Promise<IOrgStore> | IOrgStore;
|
|
22
|
-
/**
|
|
23
|
-
* Hook adapters with user FK constraints use to seed `auth.users` before
|
|
24
|
-
* a conformance test references a user id. Returns the id the adapter
|
|
25
|
-
* actually stored — callers use that, not the suggested one.
|
|
26
|
-
*/
|
|
27
|
-
seedUser?: SeedUserFn;
|
|
28
|
-
/** If true, skip createOrg/deleteOrg tests (e.g. single-org providers). */
|
|
29
|
-
singleOrgMode?: boolean;
|
|
30
|
-
/** If true, run ctx-isolation tests (adapters with row-level security). */
|
|
31
|
-
ctxIsolation?: boolean;
|
|
32
|
-
/**
|
|
33
|
-
* When supplied, an extra `deleteOrg` cascade test runs that asserts
|
|
34
|
-
* pending invites and the org's compute override are cleaned up too.
|
|
35
|
-
* The factories must return stores wired to the same underlying state
|
|
36
|
-
* as `createStore` (i.e. the same data provider instance for local,
|
|
37
|
-
* the same Supabase project for the remote adapter).
|
|
38
|
-
*/
|
|
39
|
-
createCompanionStores?: () =>
|
|
40
|
-
| Promise<{ invites: IInviteStore; computeServer: IComputeServerStore }>
|
|
41
|
-
| { invites: IInviteStore; computeServer: IComputeServerStore };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function org(ownerId: string, overrides: Partial<Organization> = {}): Organization {
|
|
45
|
-
const now = new Date().toISOString();
|
|
46
|
-
return {
|
|
47
|
-
id: overrides.id ?? makeUuid(),
|
|
48
|
-
name: overrides.name ?? 'Test Org',
|
|
49
|
-
slug: overrides.slug ?? `test-${Math.random().toString(36).slice(2, 8)}`,
|
|
50
|
-
ownerId: overrides.ownerId ?? ownerId,
|
|
51
|
-
createdBy: overrides.createdBy ?? ownerId,
|
|
52
|
-
updatedBy: overrides.updatedBy ?? ownerId,
|
|
53
|
-
createdAt: overrides.createdAt ?? now,
|
|
54
|
-
updatedAt: overrides.updatedAt ?? now,
|
|
55
|
-
deletedAt: overrides.deletedAt ?? null,
|
|
56
|
-
...overrides
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function member(orgId: string, userId: string, role: OrgRole = 'member'): OrgMember {
|
|
61
|
-
const now = new Date().toISOString();
|
|
62
|
-
return {
|
|
63
|
-
orgId,
|
|
64
|
-
userId,
|
|
65
|
-
role,
|
|
66
|
-
permissions: [...DEFAULT_ORG_PERMISSIONS[role]],
|
|
67
|
-
joinedAt: now,
|
|
68
|
-
updatedAt: now,
|
|
69
|
-
updatedBy: userId,
|
|
70
|
-
deletedAt: null
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function runOrgStoreConformance(opts: OrgStoreConformanceOptions): void {
|
|
75
|
-
const {
|
|
76
|
-
name,
|
|
77
|
-
createStore,
|
|
78
|
-
seedUser = noopSeedUser,
|
|
79
|
-
singleOrgMode = false,
|
|
80
|
-
ctxIsolation = false,
|
|
81
|
-
createCompanionStores
|
|
82
|
-
} = opts;
|
|
83
|
-
const { seed, ctx } = makeSeedHelpers(seedUser);
|
|
84
|
-
|
|
85
|
-
describe(`IOrgStore conformance: ${name}`, () => {
|
|
86
|
-
// ============================================================================
|
|
87
|
-
// Organizations
|
|
88
|
-
// ============================================================================
|
|
89
|
-
|
|
90
|
-
if (!singleOrgMode) {
|
|
91
|
-
it('createOrg + getOrg returns the organization', async () => {
|
|
92
|
-
const store = await createStore();
|
|
93
|
-
const u1 = await seed();
|
|
94
|
-
const orgId = makeUuid();
|
|
95
|
-
await store.createOrg(ctx(u1), org(u1, { id: orgId, slug: 'acme' }));
|
|
96
|
-
const got = await store.getOrg(ctx(u1), orgId);
|
|
97
|
-
expect(got?.name).toBe('Test Org');
|
|
98
|
-
expect(got?.slug).toBe('acme');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('getOrg returns null for missing id', async () => {
|
|
102
|
-
const store = await createStore();
|
|
103
|
-
const u1 = await seed();
|
|
104
|
-
const got = await store.getOrg(ctx(u1), makeUuid());
|
|
105
|
-
expect(got).toBeNull();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('getOrgBySlug finds org by slug', async () => {
|
|
109
|
-
const store = await createStore();
|
|
110
|
-
const u1 = await seed();
|
|
111
|
-
const orgId = makeUuid();
|
|
112
|
-
await store.createOrg(ctx(u1), org(u1, { id: orgId, slug: 'acme-corp' }));
|
|
113
|
-
const got = await store.getOrgBySlug(ctx(u1), 'acme-corp');
|
|
114
|
-
expect(got?.id).toBe(orgId);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('getOrgBySlug returns null for missing slug', async () => {
|
|
118
|
-
const store = await createStore();
|
|
119
|
-
const u1 = await seed();
|
|
120
|
-
const got = await store.getOrgBySlug(ctx(u1), 'nonexistent');
|
|
121
|
-
expect(got).toBeNull();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('updateOrg modifies name and slug', async () => {
|
|
125
|
-
const store = await createStore();
|
|
126
|
-
const u1 = await seed();
|
|
127
|
-
const orgId = makeUuid();
|
|
128
|
-
await store.createOrg(ctx(u1), org(u1, { id: orgId, slug: 'old-slug' }));
|
|
129
|
-
await store.updateOrg(ctx(u1), orgId, { name: 'New Name', slug: 'new-slug' });
|
|
130
|
-
const got = await store.getOrg(ctx(u1), orgId);
|
|
131
|
-
expect(got?.name).toBe('New Name');
|
|
132
|
-
expect(got?.slug).toBe('new-slug');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('deleteOrg removes the organization', async () => {
|
|
136
|
-
const store = await createStore();
|
|
137
|
-
const u1 = await seed();
|
|
138
|
-
const orgId = makeUuid();
|
|
139
|
-
await store.createOrg(ctx(u1), org(u1, { id: orgId }));
|
|
140
|
-
await store.deleteOrg(ctx(u1), orgId);
|
|
141
|
-
const got = await store.getOrg(ctx(u1), orgId);
|
|
142
|
-
expect(got).toBeNull();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('listOrgs returns all organizations with pagination', async () => {
|
|
146
|
-
const store = await createStore();
|
|
147
|
-
const u1 = await seed();
|
|
148
|
-
for (let i = 0; i < 3; i++) {
|
|
149
|
-
await store.createOrg(ctx(u1), org(u1));
|
|
150
|
-
}
|
|
151
|
-
const page = await store.listOrgs(ctx(u1), { limit: 2 });
|
|
152
|
-
expect(page.items.length).toBe(2);
|
|
153
|
-
expect(page.nextCursor).toBeTruthy();
|
|
154
|
-
});
|
|
155
|
-
} else {
|
|
156
|
-
it('getOrg returns the default organization', async () => {
|
|
157
|
-
const store = await createStore();
|
|
158
|
-
const u1 = await seed();
|
|
159
|
-
const orgs = await store.listOrgs(ctx(u1));
|
|
160
|
-
expect(orgs.items.length > 0).toBe(true);
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ============================================================================
|
|
165
|
-
// Org Members
|
|
166
|
-
// ============================================================================
|
|
167
|
-
|
|
168
|
-
async function setupOrg(store: IOrgStore): Promise<{ u1: string; orgId: string }> {
|
|
169
|
-
const u1 = await seed();
|
|
170
|
-
const orgs = await store.listOrgs(ctx(u1));
|
|
171
|
-
let orgId: string;
|
|
172
|
-
if (singleOrgMode) {
|
|
173
|
-
orgId = orgs.items[0].id;
|
|
174
|
-
} else {
|
|
175
|
-
orgId = makeUuid();
|
|
176
|
-
await store.createOrg(ctx(u1), org(u1, { id: orgId }));
|
|
177
|
-
}
|
|
178
|
-
return { u1, orgId };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
it('addOrgMember + getOrgMember returns the member', async () => {
|
|
182
|
-
const store = await createStore();
|
|
183
|
-
const { u1, orgId } = await setupOrg(store);
|
|
184
|
-
const u2 = await seed();
|
|
185
|
-
await store.addOrgMember(ctx(u1), member(orgId, u2));
|
|
186
|
-
const got = await store.getOrgMember(ctx(u1), orgId, u2);
|
|
187
|
-
expect(got?.role).toBe('member');
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('getOrgMember returns null for missing member', async () => {
|
|
191
|
-
const store = await createStore();
|
|
192
|
-
const { u1, orgId } = await setupOrg(store);
|
|
193
|
-
const got = await store.getOrgMember(ctx(u1), orgId, makeUuid());
|
|
194
|
-
expect(got).toBeNull();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('updateOrgMemberRole changes role', async () => {
|
|
198
|
-
const store = await createStore();
|
|
199
|
-
const { u1, orgId } = await setupOrg(store);
|
|
200
|
-
const u2 = await seed();
|
|
201
|
-
await store.addOrgMember(ctx(u1), member(orgId, u2));
|
|
202
|
-
await store.updateOrgMemberRole(ctx(u1), orgId, u2, 'admin');
|
|
203
|
-
const got = await store.getOrgMember(ctx(u1), orgId, u2);
|
|
204
|
-
expect(got?.role).toBe('admin');
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('removeOrgMember deletes the member', async () => {
|
|
208
|
-
const store = await createStore();
|
|
209
|
-
const { u1, orgId } = await setupOrg(store);
|
|
210
|
-
const u2 = await seed();
|
|
211
|
-
await store.addOrgMember(ctx(u1), member(orgId, u2));
|
|
212
|
-
await store.removeOrgMember(ctx(u1), orgId, u2);
|
|
213
|
-
const got = await store.getOrgMember(ctx(u1), orgId, u2);
|
|
214
|
-
expect(got).toBeNull();
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('findUserMembership returns one (org, member) for a member', async () => {
|
|
218
|
-
const store = await createStore();
|
|
219
|
-
const { u1, orgId } = await setupOrg(store);
|
|
220
|
-
const u2 = await seed();
|
|
221
|
-
await store.addOrgMember(ctx(u1), member(orgId, u2));
|
|
222
|
-
|
|
223
|
-
// Looked up via SYSTEM_CONTEXT — the bootstrap path is the primary
|
|
224
|
-
// caller and runs before a per-user ctx exists.
|
|
225
|
-
const found = await store.findUserMembership(SYSTEM_CONTEXT, u2);
|
|
226
|
-
expect(found).not.toBeNull();
|
|
227
|
-
expect(found!.org.id).toBe(orgId);
|
|
228
|
-
expect(found!.member.userId).toBe(u2);
|
|
229
|
-
expect(found!.member.role).toBe('member');
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('findUserMembership returns null for a user with no memberships', async () => {
|
|
233
|
-
const store = await createStore();
|
|
234
|
-
await setupOrg(store); // ensure at least one org exists
|
|
235
|
-
const stranger = await seed();
|
|
236
|
-
|
|
237
|
-
const found = await store.findUserMembership(SYSTEM_CONTEXT, stranger);
|
|
238
|
-
expect(found).toBeNull();
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('findUserMembership skips soft-deleted memberships', async () => {
|
|
242
|
-
const store = await createStore();
|
|
243
|
-
const { u1, orgId } = await setupOrg(store);
|
|
244
|
-
const u2 = await seed();
|
|
245
|
-
await store.addOrgMember(ctx(u1), member(orgId, u2));
|
|
246
|
-
await store.removeOrgMember(ctx(u1), orgId, u2);
|
|
247
|
-
|
|
248
|
-
const found = await store.findUserMembership(SYSTEM_CONTEXT, u2);
|
|
249
|
-
expect(found).toBeNull();
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it('findUserMembership skips memberships in soft-deleted orgs', async () => {
|
|
253
|
-
const store = await createStore();
|
|
254
|
-
const { u1, orgId } = await setupOrg(store);
|
|
255
|
-
// u1 is a member of `orgId` — kill the org and verify the lookup
|
|
256
|
-
// returns null (membership in a dead org should not surface).
|
|
257
|
-
await store.deleteOrg(ctx(u1), orgId);
|
|
258
|
-
|
|
259
|
-
const found = await store.findUserMembership(SYSTEM_CONTEXT, u1);
|
|
260
|
-
expect(found).toBeNull();
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it('listOrgMembers returns members with pagination', async () => {
|
|
264
|
-
const store = await createStore();
|
|
265
|
-
const { u1, orgId } = await setupOrg(store);
|
|
266
|
-
for (let i = 0; i < 3; i++) {
|
|
267
|
-
const u = await seed();
|
|
268
|
-
await store.addOrgMember(ctx(u1), member(orgId, u));
|
|
269
|
-
}
|
|
270
|
-
const page = await store.listOrgMembers(ctx(u1), orgId, { limit: 2 });
|
|
271
|
-
expect(page.items.length).toBe(2);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// ============================================================================
|
|
275
|
-
// Audit fields + soft delete
|
|
276
|
-
// ============================================================================
|
|
277
|
-
|
|
278
|
-
if (!singleOrgMode) {
|
|
279
|
-
it('createOrg populates createdBy and updatedBy', async () => {
|
|
280
|
-
const store = await createStore();
|
|
281
|
-
const u1 = await seed();
|
|
282
|
-
const orgId = makeUuid();
|
|
283
|
-
await store.createOrg(ctx(u1), org(u1, { id: orgId, slug: `audit-${orgId.slice(0, 8)}` }));
|
|
284
|
-
const got = await store.getOrg(ctx(u1), orgId);
|
|
285
|
-
expect(got?.createdBy).toBe(u1);
|
|
286
|
-
expect(got?.updatedBy).toBe(u1);
|
|
287
|
-
expect(got?.deletedAt ?? null).toBeNull();
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('updateOrg advances updatedBy to the caller, preserves createdBy', async () => {
|
|
291
|
-
const store = await createStore();
|
|
292
|
-
const u1 = await seed();
|
|
293
|
-
const u2 = await seed();
|
|
294
|
-
const orgId = makeUuid();
|
|
295
|
-
await store.createOrg(ctx(u1), org(u1, { id: orgId, slug: `u-${orgId.slice(0, 8)}` }));
|
|
296
|
-
// Seed u2 as admin so (if auth is enforced) they can mutate the org.
|
|
297
|
-
await store.addOrgMember(ctx(u1), member(orgId, u2, 'admin'));
|
|
298
|
-
await store.updateOrg(ctx(u2), orgId, { name: 'Renamed' });
|
|
299
|
-
const got = await store.getOrg(ctx(u1), orgId);
|
|
300
|
-
expect(got?.updatedBy).toBe(u2);
|
|
301
|
-
expect(got?.createdBy).toBe(u1);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('deleteOrg soft-deletes — org stops appearing in reads', async () => {
|
|
305
|
-
const store = await createStore();
|
|
306
|
-
const u1 = await seed();
|
|
307
|
-
const orgId = makeUuid();
|
|
308
|
-
const slug = `sd-${orgId.slice(0, 8)}`;
|
|
309
|
-
await store.createOrg(ctx(u1), org(u1, { id: orgId, slug }));
|
|
310
|
-
await store.deleteOrg(ctx(u1), orgId);
|
|
311
|
-
expect(await store.getOrg(ctx(u1), orgId)).toBeNull();
|
|
312
|
-
expect(await store.getOrgBySlug(ctx(u1), slug)).toBeNull();
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it('deleteOrg cascades soft-delete to org members', async () => {
|
|
316
|
-
const store = await createStore();
|
|
317
|
-
const u1 = await seed();
|
|
318
|
-
const u2 = await seed();
|
|
319
|
-
const orgId = makeUuid();
|
|
320
|
-
await store.createOrg(ctx(u1), org(u1, { id: orgId, slug: `c-${orgId.slice(0, 8)}` }));
|
|
321
|
-
await store.addOrgMember(ctx(u1), member(orgId, u2));
|
|
322
|
-
await store.deleteOrg(ctx(u1), orgId);
|
|
323
|
-
expect(await store.getOrgMember(ctx(u1), orgId, u2)).toBeNull();
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
if (createCompanionStores) {
|
|
327
|
-
it('deleteOrg cascades into invites and the org compute override', async () => {
|
|
328
|
-
const store = await createStore();
|
|
329
|
-
const { invites, computeServer } = await createCompanionStores();
|
|
330
|
-
const u1 = await seed();
|
|
331
|
-
const orgId = makeUuid();
|
|
332
|
-
await store.createOrg(ctx(u1), org(u1, { id: orgId, slug: `cc-${orgId.slice(0, 8)}` }));
|
|
333
|
-
|
|
334
|
-
// Pending invite + org compute override.
|
|
335
|
-
const invite: Invite = {
|
|
336
|
-
id: makeUuid(),
|
|
337
|
-
tokenHash: `hash-${makeUuid()}`,
|
|
338
|
-
email: 'pending@example.com',
|
|
339
|
-
orgId,
|
|
340
|
-
orgRole: 'member',
|
|
341
|
-
orgPermissions: [],
|
|
342
|
-
invitedBy: u1,
|
|
343
|
-
createdAt: new Date().toISOString(),
|
|
344
|
-
expiresAt: new Date(Date.now() + 86_400_000).toISOString()
|
|
345
|
-
};
|
|
346
|
-
await invites.create(ctx(u1), invite);
|
|
347
|
-
const orgCtx = { ...ctx(u1), actingOrgId: orgId };
|
|
348
|
-
const server: ComputeServerConfig = {
|
|
349
|
-
id: makeUuid(),
|
|
350
|
-
scope: 'org',
|
|
351
|
-
ownerOrgId: orgId,
|
|
352
|
-
label: 'BYO',
|
|
353
|
-
serverUrl: 'https://compute.example.com',
|
|
354
|
-
apiKey: 'secret'
|
|
355
|
-
};
|
|
356
|
-
await computeServer.saveOrgServers(orgCtx, orgId, [server], server.id);
|
|
357
|
-
|
|
358
|
-
await store.deleteOrg(ctx(u1), orgId);
|
|
359
|
-
|
|
360
|
-
expect(await invites.getByTokenHash(ctx(u1), invite.tokenHash)).toBeNull();
|
|
361
|
-
const remainingConfig = await computeServer.getConfig(orgCtx);
|
|
362
|
-
const orgRows = remainingConfig.servers.filter(
|
|
363
|
-
(s) => s.scope === 'org' && s.ownerOrgId === orgId
|
|
364
|
-
);
|
|
365
|
-
expect(orgRows).toEqual([]);
|
|
366
|
-
expect(remainingConfig.orgDefaults?.[orgId]).toBeUndefined();
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
it('removeOrgMember soft-deletes — the member stops appearing in reads', async () => {
|
|
372
|
-
const store = await createStore();
|
|
373
|
-
const { u1, orgId } = await setupOrg(store);
|
|
374
|
-
const u2 = await seed();
|
|
375
|
-
await store.addOrgMember(ctx(u1), member(orgId, u2));
|
|
376
|
-
await store.removeOrgMember(ctx(u1), orgId, u2);
|
|
377
|
-
expect(await store.getOrgMember(ctx(u1), orgId, u2)).toBeNull();
|
|
378
|
-
const page = await store.listOrgMembers(ctx(u1), orgId, { limit: 100 });
|
|
379
|
-
expect(page.items.map((m) => m.userId)).not.toContain(u2);
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
if (ctxIsolation) {
|
|
383
|
-
it('ctx isolation: orgs created by one user are not visible to another', async () => {
|
|
384
|
-
const store = await createStore();
|
|
385
|
-
const userA = await seed();
|
|
386
|
-
const userB = await seed();
|
|
387
|
-
const orgA = makeUuid();
|
|
388
|
-
await store.createOrg(
|
|
389
|
-
ctx(userA),
|
|
390
|
-
org(userA, { id: orgA, slug: `org-a-${orgA.slice(0, 8)}` })
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
const page = await store.listOrgs(ctx(userB));
|
|
394
|
-
expect(page.items.map((o) => o.id)).not.toContain(orgA);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
it('ctx isolation: user cannot get an org they do not own', async () => {
|
|
398
|
-
const store = await createStore();
|
|
399
|
-
const userA = await seed();
|
|
400
|
-
const userB = await seed();
|
|
401
|
-
const orgA = makeUuid();
|
|
402
|
-
await store.createOrg(
|
|
403
|
-
ctx(userA),
|
|
404
|
-
org(userA, { id: orgA, slug: `org-a-${orgA.slice(0, 8)}` })
|
|
405
|
-
);
|
|
406
|
-
|
|
407
|
-
const got = await store.getOrg(ctx(userB), orgA);
|
|
408
|
-
expect(got).toBeNull();
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
}
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter conformance suite for IPlatformPermissionStore.
|
|
3
|
-
*
|
|
4
|
-
* Covers `getFor` / `getForBatch` / `set` / `hasInstanceAdmin` /
|
|
5
|
-
* `countInstanceAdminsExcluding`, plus the §2 sole-`instance_admin`
|
|
6
|
-
* invariant — `set` must return `'last_admin'` when dropping the only admin,
|
|
7
|
-
* and `countInstanceAdminsExcluding` must exclude the named user.
|
|
8
|
-
*
|
|
9
|
-
* These tests used to live on the auth-provider conformance suite when the
|
|
10
|
-
* invariant was enforced there; the move from `IAuthProvider` to
|
|
11
|
-
* `IPlatformPermissionStore` brought them with it.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
15
|
-
import type { IPlatformPermissionStore, RequestContext } from '../../index.js';
|
|
16
|
-
import { ALL_ORG_PERMISSIONS, ALL_PLATFORM_PERMISSIONS } from '../../index.js';
|
|
17
|
-
|
|
18
|
-
export interface PlatformPermissionStoreConformanceOptions {
|
|
19
|
-
name: string;
|
|
20
|
-
/**
|
|
21
|
-
* Build a fresh store and a `seedUser()` helper that creates a user the
|
|
22
|
-
* test can manage. Adapters with FK-enforced user IDs (Supabase) seed via
|
|
23
|
-
* the auth backend; local can return any uuid.
|
|
24
|
-
*/
|
|
25
|
-
createStore: () => Promise<{
|
|
26
|
-
store: IPlatformPermissionStore;
|
|
27
|
-
seedUser: () => Promise<string>;
|
|
28
|
-
}>;
|
|
29
|
-
cleanup?: () => Promise<void> | void;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function runPlatformPermissionStoreConformance(
|
|
33
|
-
opts: PlatformPermissionStoreConformanceOptions
|
|
34
|
-
): void {
|
|
35
|
-
const { name, createStore, cleanup } = opts;
|
|
36
|
-
|
|
37
|
-
function adminCtx(userId: string): RequestContext {
|
|
38
|
-
return {
|
|
39
|
-
userId,
|
|
40
|
-
platformPermissions: [...ALL_PLATFORM_PERMISSIONS],
|
|
41
|
-
orgPermissions: [...ALL_ORG_PERMISSIONS]
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function selfCtx(userId: string): RequestContext {
|
|
46
|
-
return {
|
|
47
|
-
userId,
|
|
48
|
-
platformPermissions: [],
|
|
49
|
-
orgPermissions: []
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
describe(`IPlatformPermissionStore conformance: ${name}`, () => {
|
|
54
|
-
beforeEach(() => {
|
|
55
|
-
// Every test creates its own store via createStore — no shared state.
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
afterEach(async () => {
|
|
59
|
-
if (cleanup) await cleanup();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('getFor returns empty array for users with no grants', async () => {
|
|
63
|
-
const { store, seedUser } = await createStore();
|
|
64
|
-
const userId = await seedUser();
|
|
65
|
-
const perms = await store.getFor(adminCtx(userId), userId);
|
|
66
|
-
expect(perms).toEqual([]);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('set then getFor round-trips every platform permission', async () => {
|
|
70
|
-
const { store, seedUser } = await createStore();
|
|
71
|
-
const userId = await seedUser();
|
|
72
|
-
const all = ['manage_compute', 'manage_instance_users', 'manage_updates'] as const;
|
|
73
|
-
const result = await store.set(adminCtx(userId), userId, [...all]);
|
|
74
|
-
expect(result).toBe('ok');
|
|
75
|
-
const fetched = await store.getFor(adminCtx(userId), userId);
|
|
76
|
-
expect(fetched).toEqual(expect.arrayContaining([...all]));
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('set returns not_found for unknown user', async () => {
|
|
80
|
-
const { store, seedUser } = await createStore();
|
|
81
|
-
const adminId = await seedUser();
|
|
82
|
-
// Random UUID that doesn't correspond to a real user
|
|
83
|
-
const ghost = '00000000-0000-4000-8000-000000000000';
|
|
84
|
-
const result = await store.set(adminCtx(adminId), ghost, ['instance_admin']);
|
|
85
|
-
expect(result).toBe('not_found');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('non-admin caller cannot set permissions for someone else', async () => {
|
|
89
|
-
const { store, seedUser } = await createStore();
|
|
90
|
-
const target = await seedUser();
|
|
91
|
-
const attacker = await seedUser();
|
|
92
|
-
await expect(store.set(selfCtx(attacker), target, ['instance_admin'])).rejects.toThrow();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('non-admin caller cannot read another user’s permissions', async () => {
|
|
96
|
-
const { store, seedUser } = await createStore();
|
|
97
|
-
const target = await seedUser();
|
|
98
|
-
const attacker = await seedUser();
|
|
99
|
-
await expect(store.getFor(selfCtx(attacker), target)).rejects.toThrow();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('user can read their own permissions', async () => {
|
|
103
|
-
const { store, seedUser } = await createStore();
|
|
104
|
-
const userId = await seedUser();
|
|
105
|
-
const perms = await store.getFor(selfCtx(userId), userId);
|
|
106
|
-
expect(perms).toEqual([]);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('hasInstanceAdmin returns false on a fresh instance, true after granting', async () => {
|
|
110
|
-
const { store, seedUser } = await createStore();
|
|
111
|
-
const adminId = await seedUser();
|
|
112
|
-
const sysCtx: RequestContext = {
|
|
113
|
-
userId: '',
|
|
114
|
-
platformPermissions: [],
|
|
115
|
-
orgPermissions: [],
|
|
116
|
-
system: true
|
|
117
|
-
};
|
|
118
|
-
expect(await store.hasInstanceAdmin(sysCtx)).toBe(false);
|
|
119
|
-
await store.set(adminCtx(adminId), adminId, ['instance_admin']);
|
|
120
|
-
expect(await store.hasInstanceAdmin(sysCtx)).toBe(true);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// §2 invariant — the load-bearing test.
|
|
124
|
-
it('set refuses to remove instance_admin from the sole admin', async () => {
|
|
125
|
-
const { store, seedUser } = await createStore();
|
|
126
|
-
const adminId = await seedUser();
|
|
127
|
-
await store.set(adminCtx(adminId), adminId, ['instance_admin']);
|
|
128
|
-
const result = await store.set(adminCtx(adminId), adminId, []);
|
|
129
|
-
expect(result).toBe('last_admin');
|
|
130
|
-
const after = await store.getFor(adminCtx(adminId), adminId);
|
|
131
|
-
expect(after).toContain('instance_admin');
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('set allows demotion when another instance_admin exists', async () => {
|
|
135
|
-
const { store, seedUser } = await createStore();
|
|
136
|
-
const adminA = await seedUser();
|
|
137
|
-
const adminB = await seedUser();
|
|
138
|
-
await store.set(adminCtx(adminA), adminA, ['instance_admin']);
|
|
139
|
-
await store.set(adminCtx(adminA), adminB, ['instance_admin']);
|
|
140
|
-
const result = await store.set(adminCtx(adminA), adminA, []);
|
|
141
|
-
expect(result).toBe('ok');
|
|
142
|
-
// adminB still holds it
|
|
143
|
-
const after = await store.getFor(adminCtx(adminA), adminB);
|
|
144
|
-
expect(after).toContain('instance_admin');
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('countInstanceAdminsExcluding excludes the named user', async () => {
|
|
148
|
-
const { store, seedUser } = await createStore();
|
|
149
|
-
const adminA = await seedUser();
|
|
150
|
-
const adminB = await seedUser();
|
|
151
|
-
await store.set(adminCtx(adminA), adminA, ['instance_admin']);
|
|
152
|
-
await store.set(adminCtx(adminA), adminB, ['instance_admin']);
|
|
153
|
-
const sysCtx: RequestContext = {
|
|
154
|
-
userId: '',
|
|
155
|
-
platformPermissions: [],
|
|
156
|
-
orgPermissions: [],
|
|
157
|
-
system: true
|
|
158
|
-
};
|
|
159
|
-
// Excluding adminA: one admin (adminB) remains
|
|
160
|
-
expect(await store.countInstanceAdminsExcluding(sysCtx, adminA)).toBe(1);
|
|
161
|
-
// Excluding adminB: one admin (adminA) remains
|
|
162
|
-
expect(await store.countInstanceAdminsExcluding(sysCtx, adminB)).toBe(1);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('countInstanceAdminsExcluding returns 0 when target is the sole admin', async () => {
|
|
166
|
-
const { store, seedUser } = await createStore();
|
|
167
|
-
const adminId = await seedUser();
|
|
168
|
-
await store.set(adminCtx(adminId), adminId, ['instance_admin']);
|
|
169
|
-
const sysCtx: RequestContext = {
|
|
170
|
-
userId: '',
|
|
171
|
-
platformPermissions: [],
|
|
172
|
-
orgPermissions: [],
|
|
173
|
-
system: true
|
|
174
|
-
};
|
|
175
|
-
expect(await store.countInstanceAdminsExcluding(sysCtx, adminId)).toBe(0);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('getForBatch returns a map keyed by userId', async () => {
|
|
179
|
-
const { store, seedUser } = await createStore();
|
|
180
|
-
const adminId = await seedUser();
|
|
181
|
-
const memberId = await seedUser();
|
|
182
|
-
await store.set(adminCtx(adminId), adminId, ['instance_admin']);
|
|
183
|
-
await store.set(adminCtx(adminId), memberId, ['manage_compute']);
|
|
184
|
-
const map = await store.getForBatch(adminCtx(adminId), [adminId, memberId]);
|
|
185
|
-
expect(map.get(adminId)).toContain('instance_admin');
|
|
186
|
-
expect(map.get(memberId)).toContain('manage_compute');
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('getForBatch from a non-admin caller is rejected', async () => {
|
|
190
|
-
const { store, seedUser } = await createStore();
|
|
191
|
-
const attacker = await seedUser();
|
|
192
|
-
const target = await seedUser();
|
|
193
|
-
await expect(store.getForBatch(selfCtx(attacker), [target])).rejects.toThrow();
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
}
|