@memberjunction/server 6.1.0-edge.5 → 6.1.0-edge.7
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/README.md +30 -11
- package/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/SessionJanitor.js +2 -2
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/auth/exampleNewUserSubClass.d.ts.map +1 -1
- package/dist/auth/exampleNewUserSubClass.js +9 -4
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +2 -8
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +4 -12
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/auth/principals.d.ts +92 -0
- package/dist/auth/principals.d.ts.map +1 -0
- package/dist/auth/principals.js +193 -0
- package/dist/auth/principals.js.map +1 -0
- package/dist/config.d.ts +106 -12
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +41 -4
- package/dist/config.js.map +1 -1
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts +30 -0
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts.map +1 -1
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js +58 -5
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +7607 -6853
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +25141 -21645
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +152 -2
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +389 -10
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +26 -2
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/generic/refusalExtensions.d.ts +52 -0
- package/dist/generic/refusalExtensions.d.ts.map +1 -0
- package/dist/generic/refusalExtensions.js +16 -0
- package/dist/generic/refusalExtensions.js.map +1 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +131 -93
- package/dist/index.js.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.js +2 -12
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
- package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -1
- package/dist/realtimeWidget/visitorIdentity.js +20 -5
- package/dist/realtimeWidget/visitorIdentity.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +34 -5
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +64 -7
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
- package/dist/resolvers/FileCategoryResolver.js +1 -2
- package/dist/resolvers/FileCategoryResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +2 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -2
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +80 -4
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +13 -8
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +45 -8
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +11 -2
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
- package/dist/resolvers/UserViewResolver.js +8 -4
- package/dist/resolvers/UserViewResolver.js.map +1 -1
- package/dist/rest/EntityCRUDHandler.d.ts +2 -1
- package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
- package/dist/rest/EntityCRUDHandler.js +3 -18
- package/dist/rest/EntityCRUDHandler.js.map +1 -1
- package/dist/rest/RESTEndpointHandler.d.ts +2 -1
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +3 -16
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/rest/RealtimeSdpBrokerHandler.d.ts +23 -0
- package/dist/rest/RealtimeSdpBrokerHandler.d.ts.map +1 -0
- package/dist/rest/RealtimeSdpBrokerHandler.js +78 -0
- package/dist/rest/RealtimeSdpBrokerHandler.js.map +1 -0
- package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
- package/dist/serverExtensionReservedRoots.js +2 -3
- package/dist/serverExtensionReservedRoots.js.map +1 -1
- package/package.json +92 -91
- package/src/__tests__/EntityCRUDHandler.test.ts +18 -5
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +118 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +20 -0
- package/src/__tests__/RealtimeSdpBrokerHandler.test.ts +191 -0
- package/src/__tests__/RemoteBrowserAudioStream.test.ts +81 -2
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +190 -3
- package/src/__tests__/ResolverBase.readableFieldsTransport.test.ts +106 -0
- package/src/__tests__/UserViewResolver.updateWhereClause.test.ts +114 -0
- package/src/__tests__/WidgetSessionService.test.ts +4 -1
- package/src/__tests__/generatedGraphQLSchema.structural.test.ts +46 -0
- package/src/__tests__/newUserRolesDefault.test.ts +81 -0
- package/src/__tests__/newUsers.test.ts +36 -14
- package/src/__tests__/principals.callSites.test.ts +112 -0
- package/src/__tests__/principals.logging.test.ts +160 -0
- package/src/__tests__/principals.test.ts +305 -0
- package/src/__tests__/resolverBase.fls.test.ts +483 -0
- package/src/__tests__/resolverBase.recordChangesFls.test.ts +358 -0
- package/src/__tests__/resolverBase.refusalExtensions.test.ts +101 -0
- package/src/__tests__/resolverBase.refusalMessage.test.ts +88 -0
- package/src/__tests__/serverExtensionReservedRoots.test.ts +79 -11
- package/src/__tests__/visitorIdentity.resolve.test.ts +61 -0
- package/src/agentSessions/SessionJanitor.ts +2 -2
- package/src/auth/exampleNewUserSubClass.ts +9 -6
- package/src/auth/magicLink/MagicLinkService.ts +2 -8
- package/src/auth/newUsers.ts +7 -15
- package/src/auth/principals.ts +257 -0
- package/src/config.ts +44 -4
- package/src/entitySubclasses/MJEntityPermissionEntityServer.server.ts +65 -6
- package/src/generated/graphql-schemas/__mj.ts +18966 -16452
- package/src/generic/ResolverBase.ts +467 -9
- package/src/generic/RunViewResolver.ts +28 -2
- package/src/generic/refusalExtensions.ts +59 -0
- package/src/index.ts +154 -102
- package/src/realtimeWidget/WidgetSessionService.ts +2 -14
- package/src/realtimeWidget/visitorIdentity.ts +21 -6
- package/src/resolvers/EntityRecordNameResolver.ts +74 -8
- package/src/resolvers/FileCategoryResolver.ts +2 -3
- package/src/resolvers/IntegrationDiscoveryResolver.ts +2 -0
- package/src/resolvers/QuerySystemUserResolver.ts +1 -1
- package/src/resolvers/RealtimeClientSessionResolver.ts +128 -2
- package/src/resolvers/RemoteBrowserActionResolver.ts +47 -8
- package/src/resolvers/SyncRolesUsersResolver.ts +11 -2
- package/src/resolvers/UserViewResolver.ts +8 -4
- package/src/rest/EntityCRUDHandler.ts +3 -19
- package/src/rest/RESTEndpointHandler.ts +3 -17
- package/src/rest/RealtimeSdpBrokerHandler.ts +101 -0
- package/src/serverExtensionReservedRoots.ts +2 -3
- package/dist/resolvers/RingCentralTelephonyResolver.d.ts +0 -27
- package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +0 -1
- package/dist/resolvers/RingCentralTelephonyResolver.js +0 -87
- package/dist/resolvers/RingCentralTelephonyResolver.js.map +0 -1
- package/dist/resolvers/TeamsMeetingsResolver.d.ts +0 -28
- package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +0 -1
- package/dist/resolvers/TeamsMeetingsResolver.js +0 -91
- package/dist/resolvers/TeamsMeetingsResolver.js.map +0 -1
- package/dist/resolvers/TelephonyResolver.d.ts +0 -26
- package/dist/resolvers/TelephonyResolver.d.ts.map +0 -1
- package/dist/resolvers/TelephonyResolver.js +0 -86
- package/dist/resolvers/TelephonyResolver.js.map +0 -1
- package/dist/resolvers/VonageTelephonyResolver.d.ts +0 -26
- package/dist/resolvers/VonageTelephonyResolver.d.ts.map +0 -1
- package/dist/resolvers/VonageTelephonyResolver.js +0 -86
- package/dist/resolvers/VonageTelephonyResolver.js.map +0 -1
- package/dist/telephony/RingCentralTelephonyService.d.ts +0 -115
- package/dist/telephony/RingCentralTelephonyService.d.ts.map +0 -1
- package/dist/telephony/RingCentralTelephonyService.js +0 -262
- package/dist/telephony/RingCentralTelephonyService.js.map +0 -1
- package/dist/telephony/TeamsMeetingsRouter.d.ts +0 -40
- package/dist/telephony/TeamsMeetingsRouter.d.ts.map +0 -1
- package/dist/telephony/TeamsMeetingsRouter.js +0 -96
- package/dist/telephony/TeamsMeetingsRouter.js.map +0 -1
- package/dist/telephony/TeamsMeetingsService.d.ts +0 -113
- package/dist/telephony/TeamsMeetingsService.d.ts.map +0 -1
- package/dist/telephony/TeamsMeetingsService.js +0 -196
- package/dist/telephony/TeamsMeetingsService.js.map +0 -1
- package/dist/telephony/TwilioTelephonyRouter.d.ts +0 -36
- package/dist/telephony/TwilioTelephonyRouter.d.ts.map +0 -1
- package/dist/telephony/TwilioTelephonyRouter.js +0 -143
- package/dist/telephony/TwilioTelephonyRouter.js.map +0 -1
- package/dist/telephony/TwilioTelephonyService.d.ts +0 -95
- package/dist/telephony/TwilioTelephonyService.d.ts.map +0 -1
- package/dist/telephony/TwilioTelephonyService.js +0 -193
- package/dist/telephony/TwilioTelephonyService.js.map +0 -1
- package/dist/telephony/VonageTelephonyRouter.d.ts +0 -41
- package/dist/telephony/VonageTelephonyRouter.d.ts.map +0 -1
- package/dist/telephony/VonageTelephonyRouter.js +0 -201
- package/dist/telephony/VonageTelephonyRouter.js.map +0 -1
- package/dist/telephony/VonageTelephonyService.d.ts +0 -90
- package/dist/telephony/VonageTelephonyService.d.ts.map +0 -1
- package/dist/telephony/VonageTelephonyService.js +0 -191
- package/dist/telephony/VonageTelephonyService.js.map +0 -1
- package/dist/telephony/calendar-scheduler.d.ts +0 -67
- package/dist/telephony/calendar-scheduler.d.ts.map +0 -1
- package/dist/telephony/calendar-scheduler.js +0 -133
- package/dist/telephony/calendar-scheduler.js.map +0 -1
- package/dist/telephony/index.d.ts +0 -29
- package/dist/telephony/index.d.ts.map +0 -1
- package/dist/telephony/index.js +0 -38
- package/dist/telephony/index.js.map +0 -1
- package/dist/telephony/media-upgrade-router.d.ts +0 -47
- package/dist/telephony/media-upgrade-router.d.ts.map +0 -1
- package/dist/telephony/media-upgrade-router.js +0 -72
- package/dist/telephony/media-upgrade-router.js.map +0 -1
- package/dist/telephony/ringcentral-runtime.d.ts +0 -14
- package/dist/telephony/ringcentral-runtime.d.ts.map +0 -1
- package/dist/telephony/ringcentral-runtime.js +0 -18
- package/dist/telephony/ringcentral-runtime.js.map +0 -1
- package/dist/telephony/teams-meetings-runtime.d.ts +0 -16
- package/dist/telephony/teams-meetings-runtime.d.ts.map +0 -1
- package/dist/telephony/teams-meetings-runtime.js +0 -20
- package/dist/telephony/teams-meetings-runtime.js.map +0 -1
- package/dist/telephony/teamsAcsMediaRegistry.d.ts +0 -69
- package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +0 -1
- package/dist/telephony/teamsAcsMediaRegistry.js +0 -118
- package/dist/telephony/teamsAcsMediaRegistry.js.map +0 -1
- package/dist/telephony/telephony-runtime.d.ts +0 -14
- package/dist/telephony/telephony-runtime.d.ts.map +0 -1
- package/dist/telephony/telephony-runtime.js +0 -18
- package/dist/telephony/telephony-runtime.js.map +0 -1
- package/dist/telephony/twilioMediaRegistry.d.ts +0 -60
- package/dist/telephony/twilioMediaRegistry.d.ts.map +0 -1
- package/dist/telephony/twilioMediaRegistry.js +0 -106
- package/dist/telephony/twilioMediaRegistry.js.map +0 -1
- package/dist/telephony/vonage-runtime.d.ts +0 -14
- package/dist/telephony/vonage-runtime.d.ts.map +0 -1
- package/dist/telephony/vonage-runtime.js +0 -18
- package/dist/telephony/vonage-runtime.js.map +0 -1
- package/dist/telephony/vonageMediaRegistry.d.ts +0 -78
- package/dist/telephony/vonageMediaRegistry.d.ts.map +0 -1
- package/dist/telephony/vonageMediaRegistry.js +0 -158
- package/dist/telephony/vonageMediaRegistry.js.map +0 -1
- package/src/__tests__/media-upgrade-router.test.ts +0 -43
- package/src/__tests__/teams-meetings-service.test.ts +0 -65
- package/src/__tests__/teamsAcsMediaRegistry.test.ts +0 -82
- package/src/__tests__/twilio-telephony-service.test.ts +0 -23
- package/src/__tests__/twilioMediaRegistry.test.ts +0 -103
- package/src/__tests__/vonageMediaRegistry.test.ts +0 -180
- package/src/resolvers/RingCentralTelephonyResolver.ts +0 -64
- package/src/resolvers/TeamsMeetingsResolver.ts +0 -68
- package/src/resolvers/TelephonyResolver.ts +0 -63
- package/src/resolvers/VonageTelephonyResolver.ts +0 -63
- package/src/telephony/RingCentralTelephonyService.ts +0 -342
- package/src/telephony/TeamsMeetingsRouter.ts +0 -124
- package/src/telephony/TeamsMeetingsService.ts +0 -268
- package/src/telephony/TwilioTelephonyRouter.ts +0 -184
- package/src/telephony/TwilioTelephonyService.ts +0 -261
- package/src/telephony/VonageTelephonyRouter.ts +0 -239
- package/src/telephony/VonageTelephonyService.ts +0 -259
- package/src/telephony/calendar-scheduler.ts +0 -176
- package/src/telephony/index.ts +0 -43
- package/src/telephony/media-upgrade-router.ts +0 -79
- package/src/telephony/ringcentral-runtime.ts +0 -22
- package/src/telephony/teams-meetings-runtime.ts +0 -24
- package/src/telephony/teamsAcsMediaRegistry.ts +0 -152
- package/src/telephony/telephony-runtime.ts +0 -22
- package/src/telephony/twilioMediaRegistry.ts +0 -137
- package/src/telephony/vonage-runtime.ts +0 -22
- package/src/telephony/vonageMediaRegistry.ts +0 -200
|
@@ -21,7 +21,7 @@ import type { UserInfo } from '@memberjunction/core';
|
|
|
21
21
|
const {
|
|
22
22
|
mockConfig,
|
|
23
23
|
mockCacheUsers,
|
|
24
|
-
|
|
24
|
+
mockSystemUserId,
|
|
25
25
|
mockRefresh,
|
|
26
26
|
mockGetEntityObject,
|
|
27
27
|
mockRunViewFn,
|
|
@@ -39,6 +39,7 @@ const {
|
|
|
39
39
|
Name: string;
|
|
40
40
|
Email: string;
|
|
41
41
|
Type: string;
|
|
42
|
+
IsActive?: boolean;
|
|
42
43
|
}
|
|
43
44
|
return {
|
|
44
45
|
mockConfig: {
|
|
@@ -55,7 +56,11 @@ const {
|
|
|
55
56
|
},
|
|
56
57
|
},
|
|
57
58
|
mockCacheUsers: [] as HoistedCacheUser[],
|
|
58
|
-
|
|
59
|
+
// The system user's ID as the cache reports it. A well-formed UUID that matches NO fixture
|
|
60
|
+
// below, so the resolver's system rung stays inert unless a test opts into it. Well-formed
|
|
61
|
+
// matters: `UUIDsEqual` is a string compare with no shape check, so a malformed value here
|
|
62
|
+
// would read as real data while being unrepresentable in the column it stands for.
|
|
63
|
+
mockSystemUserId: { value: '00000000-0000-0000-0000-00000000d1f5' },
|
|
59
64
|
mockRefresh: vi.fn(),
|
|
60
65
|
mockGetEntityObject: vi.fn(),
|
|
61
66
|
mockRunViewFn: vi.fn(),
|
|
@@ -76,10 +81,12 @@ vi.mock('../config.js', () => ({ configInfo: mockConfig }));
|
|
|
76
81
|
|
|
77
82
|
vi.mock('@memberjunction/generic-database-provider', () => {
|
|
78
83
|
const instance = {
|
|
79
|
-
UserByName: mockUserByName,
|
|
80
84
|
get Users() {
|
|
81
85
|
return mockCacheUsers;
|
|
82
86
|
},
|
|
87
|
+
get SYSTEM_USER_ID() {
|
|
88
|
+
return mockSystemUserId.value;
|
|
89
|
+
},
|
|
83
90
|
Refresh: mockRefresh,
|
|
84
91
|
GetSystemUser: vi.fn(),
|
|
85
92
|
};
|
|
@@ -234,7 +241,9 @@ function entitiesOf(name: string): MockEntity[] {
|
|
|
234
241
|
return createdEntities.filter((e) => e.TestEntityName === name);
|
|
235
242
|
}
|
|
236
243
|
|
|
237
|
-
|
|
244
|
+
/** The cache row for the configured context user; `CONTEXT_USER` is the same object, typed. */
|
|
245
|
+
const CONTEXT_USER_ROW = { ID: 'sys-1', Name: 'system@test.com', Email: 'system@test.com', Type: 'Owner', IsActive: true };
|
|
246
|
+
const CONTEXT_USER = CONTEXT_USER_ROW as unknown as UserInfo;
|
|
238
247
|
|
|
239
248
|
function resetConfig(): void {
|
|
240
249
|
mockConfig.userHandling = {
|
|
@@ -261,14 +270,22 @@ beforeEach(() => {
|
|
|
261
270
|
getEntityObjectCalls = [];
|
|
262
271
|
saveBehavior = {};
|
|
263
272
|
entitySeq = 0;
|
|
273
|
+
// defineProperty rather than assignment: one test replaces this with a throwing getter.
|
|
274
|
+
Object.defineProperty(mockSystemUserId, 'value', {
|
|
275
|
+
value: '00000000-0000-0000-0000-00000000d1f5',
|
|
276
|
+
writable: true,
|
|
277
|
+
configurable: true,
|
|
278
|
+
});
|
|
279
|
+
// The configured context user must be IN the cache: resolution reads the cache directly rather
|
|
280
|
+
// than going through a UserByName stub that answered regardless of what the cache held.
|
|
264
281
|
mockCacheUsers.length = 0;
|
|
282
|
+
mockCacheUsers.push(CONTEXT_USER_ROW);
|
|
265
283
|
|
|
266
284
|
mockRolesArray.length = 0;
|
|
267
285
|
mockRolesArray.push({ ID: 'role-ui', Name: 'UI' }, { ID: 'role-dev', Name: 'Developer' });
|
|
268
286
|
mockApplicationsArray.length = 0;
|
|
269
287
|
mockApplicationsArray.push({ ID: 'app-crm', Name: ' CRM ' }, { ID: 'app-admin', Name: 'Admin' });
|
|
270
288
|
|
|
271
|
-
mockUserByName.mockImplementation((name: string) => (name === 'system@test.com' ? CONTEXT_USER : undefined));
|
|
272
289
|
mockGetEntityObject.mockImplementation(async (entityName: string, contextUser?: UserInfo) => {
|
|
273
290
|
const entity = makeMockEntity(entityName);
|
|
274
291
|
createdEntities.push(entity);
|
|
@@ -290,7 +307,6 @@ describe('NewUserBase.createNewUser', () => {
|
|
|
290
307
|
const user = await create();
|
|
291
308
|
|
|
292
309
|
expect(user).not.toBeNull();
|
|
293
|
-
expect(mockUserByName).toHaveBeenCalledWith('system@test.com');
|
|
294
310
|
expect(getEntityObjectCalls[0]).toEqual({ entityName: 'MJ: Users', contextUser: CONTEXT_USER });
|
|
295
311
|
// Role records are scoped to the same creation context user
|
|
296
312
|
for (const call of getEntityObjectCalls) {
|
|
@@ -299,9 +315,9 @@ describe('NewUserBase.createNewUser', () => {
|
|
|
299
315
|
});
|
|
300
316
|
|
|
301
317
|
it('falls back to an Owner-typed cache user (case-insensitive, trimmed) when the configured user is missing', async () => {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
mockCacheUsers.push({ ID: 'u-1', Name: 'Plain', Email: 'p@x.com', Type: 'User' }, owner);
|
|
318
|
+
const owner = { ID: 'owner-9', Name: 'Fallback Owner', Email: 'owner@x.com', Type: ' OWNER ', IsActive: true };
|
|
319
|
+
mockCacheUsers.length = 0; // the configured user is absent from this deployment
|
|
320
|
+
mockCacheUsers.push({ ID: 'u-1', Name: 'Plain', Email: 'p@x.com', Type: 'User', IsActive: true }, owner);
|
|
305
321
|
|
|
306
322
|
const user = await create();
|
|
307
323
|
|
|
@@ -311,8 +327,8 @@ describe('NewUserBase.createNewUser', () => {
|
|
|
311
327
|
});
|
|
312
328
|
|
|
313
329
|
it('returns null without opening a transaction when no context user can be resolved at all', async () => {
|
|
314
|
-
|
|
315
|
-
mockCacheUsers.push({ ID: 'u-1', Name: 'Plain', Email: 'p@x.com', Type: 'User' });
|
|
330
|
+
mockCacheUsers.length = 0; // no configured user, no system user, no Owner
|
|
331
|
+
mockCacheUsers.push({ ID: 'u-1', Name: 'Plain', Email: 'p@x.com', Type: 'User', IsActive: true });
|
|
316
332
|
|
|
317
333
|
const user = await create();
|
|
318
334
|
|
|
@@ -322,8 +338,12 @@ describe('NewUserBase.createNewUser', () => {
|
|
|
322
338
|
});
|
|
323
339
|
|
|
324
340
|
it('returns undefined (outer catch) when context resolution throws', async () => {
|
|
325
|
-
|
|
326
|
-
|
|
341
|
+
// A cache that cannot report its own system user id fails resolution outright.
|
|
342
|
+
Object.defineProperty(mockSystemUserId, 'value', {
|
|
343
|
+
get() {
|
|
344
|
+
throw new Error('cache exploded');
|
|
345
|
+
},
|
|
346
|
+
configurable: true,
|
|
327
347
|
});
|
|
328
348
|
|
|
329
349
|
const user = await create();
|
|
@@ -604,7 +624,9 @@ describe('verifyUserRecord', () => {
|
|
|
604
624
|
const user = await verifyUserRecord('new@example.com', 'New', 'User');
|
|
605
625
|
|
|
606
626
|
expect(user).toBeUndefined();
|
|
607
|
-
|
|
627
|
+
// The cache is no longer empty at rest (it holds the configured context user), so
|
|
628
|
+
// assert the thing that actually matters: the failed user was not added to it.
|
|
629
|
+
expect(mockCacheUsers.some((u) => u.Email === 'new@example.com')).toBe(false);
|
|
608
630
|
});
|
|
609
631
|
});
|
|
610
632
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* There is ONE resolver for the three config-named context users — issue #4209, second half.
|
|
3
|
+
*
|
|
4
|
+
* The bug was not that a lookup used the wrong column; it was that the lookup was written out by
|
|
5
|
+
* hand at every consumer. Six copies existed across MJServer in three mutually inconsistent
|
|
6
|
+
* variants, and two of them could not resolve the value MJ itself ships. `auth/principals.ts`
|
|
7
|
+
* exists to make that impossible, and this test is what keeps it that way: any file that reads
|
|
8
|
+
* one of the three settings must go through the ladder rather than growing a fourth variant.
|
|
9
|
+
*
|
|
10
|
+
* Source-scanning rather than behavioural because the failure mode is *a new call site*, which no
|
|
11
|
+
* behavioural test of the existing call sites can see. Same technique as
|
|
12
|
+
* `AdhocQueryResolver.scopeGuard.test.ts` and `serverExtensionReservedRoots.test.ts`.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import { readdirSync, readFileSync, statSync } from 'fs';
|
|
16
|
+
import { dirname, join, relative, sep } from 'path';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
|
|
19
|
+
const SRC_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
|
|
21
|
+
/** The three settings that name a principal in config. `config.ts` DEFINES them; it does not resolve them. */
|
|
22
|
+
const CONTEXT_USER_SETTINGS = [
|
|
23
|
+
'contextUserForNewUserCreation',
|
|
24
|
+
'contextUserForProvisioning',
|
|
25
|
+
'contextUserForLookup',
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Files that legitimately mention a setting without resolving it, as paths RELATIVE TO `src/`.
|
|
30
|
+
* Deliberately not basenames: exempting every file called `config.ts` anywhere under `src/` would
|
|
31
|
+
* let a future `realtimeWidget/config.ts` hand-roll a lookup and still pass this guard.
|
|
32
|
+
*/
|
|
33
|
+
const NOT_CONSUMERS = ['config.ts', 'auth/principals.ts'];
|
|
34
|
+
|
|
35
|
+
/** `src/`-relative, forward-slashed, so the assertions below read the same on every platform. */
|
|
36
|
+
function relativeToSrc(absolutePath: string): string {
|
|
37
|
+
return relative(SRC_ROOT, absolutePath).split(sep).join('/');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function everyTsFileUnder(dir: string): string[] {
|
|
41
|
+
return readdirSync(dir).flatMap((name) => {
|
|
42
|
+
const full = join(dir, name);
|
|
43
|
+
if (statSync(full).isDirectory()) {
|
|
44
|
+
return name === '__tests__' || name === 'generated' ? [] : everyTsFileUnder(full);
|
|
45
|
+
}
|
|
46
|
+
return name.endsWith('.ts') && !name.endsWith('.d.ts') ? [full] : [];
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Files that READ one of the settings — i.e. resolve it to a user, rather than declaring it. */
|
|
51
|
+
function consumersOfAContextUserSetting(): { path: string; source: string }[] {
|
|
52
|
+
return everyTsFileUnder(SRC_ROOT)
|
|
53
|
+
.filter((p) => !NOT_CONSUMERS.includes(relativeToSrc(p)))
|
|
54
|
+
.map((path) => ({ path: relativeToSrc(path), source: readFileSync(path, 'utf8') }))
|
|
55
|
+
.filter(({ source }) => CONTEXT_USER_SETTINGS.some((s) => source.includes(`.${s}`)));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('context-user resolution has exactly one implementation', () => {
|
|
59
|
+
it('finds the known consumers, so the scan itself is not silently matching nothing', () => {
|
|
60
|
+
const found = consumersOfAContextUserSetting().map((c) => c.path);
|
|
61
|
+
|
|
62
|
+
expect(found).toContain('auth/newUsers.ts');
|
|
63
|
+
expect(found).toContain('auth/magicLink/MagicLinkService.ts');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('routes every consumer through auth/principals.ts', () => {
|
|
67
|
+
const handRolled = consumersOfAContextUserSetting()
|
|
68
|
+
.filter(({ source }) => !source.includes('ResolveConfiguredPrincipal'))
|
|
69
|
+
.map(({ path }) => path);
|
|
70
|
+
|
|
71
|
+
expect(handRolled).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('leaves no consumer matching the setting against a single column by hand', () => {
|
|
75
|
+
// The two shapes that produced #4209: `UserByName(candidate)` and `u.Email === candidate`.
|
|
76
|
+
//
|
|
77
|
+
// Scoped to the statement that READS the setting, not the whole file: MagicLinkService
|
|
78
|
+
// also resolves the INVITEE by Email (`provisionUser`, ~270 lines away), which is tier-1
|
|
79
|
+
// identity and correct. A file-wide grep would condemn it, and a guard that cries wolf
|
|
80
|
+
// gets deleted.
|
|
81
|
+
const COLUMN_MATCH = /UserByName\s*\(|\.(Email|Name)\b[^\n]*===/;
|
|
82
|
+
const handRolled = consumersOfAContextUserSetting()
|
|
83
|
+
.filter(({ source }) =>
|
|
84
|
+
source.split('\n').some(
|
|
85
|
+
(line) => CONTEXT_USER_SETTINGS.some((s) => line.includes(`.${s}`)) && COLUMN_MATCH.test(line),
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
.map(({ path }) => path);
|
|
89
|
+
|
|
90
|
+
expect(handRolled).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('the ladder is reachable by the integrators the example is written for', () => {
|
|
95
|
+
// `exampleNewUserSubClass.ts` exists to be COPIED into a deployment's own package — its header
|
|
96
|
+
// says so, and it tells you to uncomment `@RegisterClass` in your code. Now that it resolves
|
|
97
|
+
// through `ResolveConfiguredPrincipal`, an integrator who copies it needs to be able to import
|
|
98
|
+
// that. `package.json` publishes only `"."`, so the package index is the whole public surface,
|
|
99
|
+
// and this guard cannot see downstream code — it can only make sure the door exists.
|
|
100
|
+
it('exports ResolveConfiguredPrincipal from the package index', () => {
|
|
101
|
+
const index = readFileSync(join(SRC_ROOT, 'index.ts'), 'utf8');
|
|
102
|
+
|
|
103
|
+
expect(index).toMatch(/ResolveConfiguredPrincipal/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('exports the types a caller needs to hold the result', () => {
|
|
107
|
+
const index = readFileSync(join(SRC_ROOT, 'index.ts'), 'utf8');
|
|
108
|
+
|
|
109
|
+
expect(index).toMatch(/ResolvablePrincipal/);
|
|
110
|
+
expect(index).toMatch(/PrincipalResolution/);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log policy for context-user resolution — the second half of issue #4209.
|
|
3
|
+
*
|
|
4
|
+
* The defect had two costs. One was attribution (covered by principals.test.ts). The other was
|
|
5
|
+
* volume: because the shipped default could never resolve, EVERY magic-link redeem and EVERY
|
|
6
|
+
* auto-provisioned user emitted an error-level line, on every host that took the default. Real
|
|
7
|
+
* errors were buried under it.
|
|
8
|
+
*
|
|
9
|
+
* Resolution is a pure function; this file covers the impure shell around it — reading the user
|
|
10
|
+
* cache and deciding how often a misconfiguration is worth saying out loud.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
13
|
+
|
|
14
|
+
const { mockLogError, mockUsers, SYSTEM_USER_ID } = vi.hoisted(() => ({
|
|
15
|
+
mockLogError: vi.fn(),
|
|
16
|
+
mockUsers: [] as Array<Record<string, unknown>>,
|
|
17
|
+
SYSTEM_USER_ID: 'ECAFCCEC-6A37-EF11-86D4-000D3A4E707E',
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('@memberjunction/core', () => ({
|
|
21
|
+
LogError: mockLogError,
|
|
22
|
+
UserInfo: class {},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock('@memberjunction/generic-database-provider', () => ({
|
|
26
|
+
UserCache: {
|
|
27
|
+
get Users() {
|
|
28
|
+
return mockUsers;
|
|
29
|
+
},
|
|
30
|
+
Instance: {
|
|
31
|
+
get Users() {
|
|
32
|
+
return mockUsers;
|
|
33
|
+
},
|
|
34
|
+
get SYSTEM_USER_ID() {
|
|
35
|
+
return SYSTEM_USER_ID;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
/** Stock seed: the system user MJ's own default is aiming at, plus a human Owner. */
|
|
42
|
+
const SYSTEM = { ID: SYSTEM_USER_ID, Name: 'System', Email: 'not.set@nowhere.com', Type: 'Owner ', IsActive: true };
|
|
43
|
+
const HOST_ADMIN = { ID: 'AAAA1111-0000-0000-0000-000000000001', Name: 'jane.admin@acme.com', Email: 'jane.admin@acme.com', Type: 'Owner', IsActive: true };
|
|
44
|
+
|
|
45
|
+
/** Fresh module per test: the de-duplication state is module-scoped by design. */
|
|
46
|
+
async function loadSubject() {
|
|
47
|
+
vi.resetModules();
|
|
48
|
+
return import('../auth/principals.js');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.clearAllMocks();
|
|
53
|
+
mockUsers.length = 0;
|
|
54
|
+
mockUsers.push(SYSTEM, HOST_ADMIN);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('ResolveConfiguredPrincipal', () => {
|
|
58
|
+
it('resolves MJ\'s own shipped default without logging anything', async () => {
|
|
59
|
+
const { ResolveConfiguredPrincipal } = await loadSubject();
|
|
60
|
+
|
|
61
|
+
const user = ResolveConfiguredPrincipal('not.set@nowhere.com', 'MagicLink');
|
|
62
|
+
|
|
63
|
+
expect(user).toBe(SYSTEM);
|
|
64
|
+
expect(mockLogError).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('logs an unresolvable candidate ONCE however many times it is asked for', async () => {
|
|
68
|
+
const { ResolveConfiguredPrincipal } = await loadSubject();
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < 25; i++) {
|
|
71
|
+
ResolveConfiguredPrincipal('ghost@nowhere.example', 'MagicLink');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
expect(mockLogError).toHaveBeenCalledTimes(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('names both the purpose and the candidate so the operator can find the setting', async () => {
|
|
78
|
+
const { ResolveConfiguredPrincipal } = await loadSubject();
|
|
79
|
+
|
|
80
|
+
ResolveConfiguredPrincipal('ghost@nowhere.example', 'MagicLink');
|
|
81
|
+
|
|
82
|
+
const logged = String(mockLogError.mock.calls[0][0]);
|
|
83
|
+
expect(logged).toContain('MagicLink');
|
|
84
|
+
expect(logged).toContain('ghost@nowhere.example');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('still reports a DIFFERENT misconfiguration rather than swallowing it', async () => {
|
|
88
|
+
const { ResolveConfiguredPrincipal } = await loadSubject();
|
|
89
|
+
|
|
90
|
+
ResolveConfiguredPrincipal('ghost@nowhere.example', 'MagicLink');
|
|
91
|
+
ResolveConfiguredPrincipal('other@nowhere.example', 'MagicLink');
|
|
92
|
+
|
|
93
|
+
expect(mockLogError).toHaveBeenCalledTimes(2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('reports the same candidate separately for a different purpose', async () => {
|
|
97
|
+
const { ResolveConfiguredPrincipal } = await loadSubject();
|
|
98
|
+
|
|
99
|
+
ResolveConfiguredPrincipal('ghost@nowhere.example', 'MagicLink');
|
|
100
|
+
ResolveConfiguredPrincipal('ghost@nowhere.example', 'Widget');
|
|
101
|
+
|
|
102
|
+
expect(mockLogError).toHaveBeenCalledTimes(2);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('does not treat an unset candidate as a misconfiguration', async () => {
|
|
106
|
+
const { ResolveConfiguredPrincipal } = await loadSubject();
|
|
107
|
+
|
|
108
|
+
const user = ResolveConfiguredPrincipal(undefined, 'NewUser');
|
|
109
|
+
|
|
110
|
+
expect(user).toBe(SYSTEM);
|
|
111
|
+
expect(mockLogError).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('returns null and says so when the deployment offers no principal at all', async () => {
|
|
115
|
+
mockUsers.length = 0;
|
|
116
|
+
const { ResolveConfiguredPrincipal } = await loadSubject();
|
|
117
|
+
|
|
118
|
+
const user = ResolveConfiguredPrincipal('ghost@nowhere.example', 'MagicLink');
|
|
119
|
+
|
|
120
|
+
expect(user).toBeNull();
|
|
121
|
+
expect(mockLogError).toHaveBeenCalledTimes(1);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('the misconfiguration report stays bounded WITHOUT regressing to per-request logging', () => {
|
|
126
|
+
it('publishes its bound, so this test cannot silently pass against a different one', async () => {
|
|
127
|
+
const { MAX_REPORTED_MISCONFIGURATIONS } = await loadSubject();
|
|
128
|
+
|
|
129
|
+
expect(typeof MAX_REPORTED_MISCONFIGURATIONS).toBe('number');
|
|
130
|
+
expect(MAX_REPORTED_MISCONFIGURATIONS).toBeGreaterThan(0);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('still says a NEW misconfiguration exactly once after the bound is reached', async () => {
|
|
134
|
+
// The bound exists so a caller passing a per-request candidate cannot grow the tracker
|
|
135
|
+
// forever. But whatever it does at the limit must not be "log every time" — that is
|
|
136
|
+
// precisely the per-request error line issue #4209 was filed about, reintroduced for the
|
|
137
|
+
// one caller the bound was added for.
|
|
138
|
+
const { ResolveConfiguredPrincipal, MAX_REPORTED_MISCONFIGURATIONS } = await loadSubject();
|
|
139
|
+
expect(typeof MAX_REPORTED_MISCONFIGURATIONS).toBe('number'); // else the loop below is a no-op and this passes for free
|
|
140
|
+
for (let i = 0; i < MAX_REPORTED_MISCONFIGURATIONS + 5; i++) {
|
|
141
|
+
ResolveConfiguredPrincipal(`filler-${i}@nowhere.example`, 'MagicLink');
|
|
142
|
+
}
|
|
143
|
+
mockLogError.mockClear();
|
|
144
|
+
|
|
145
|
+
ResolveConfiguredPrincipal('late@nowhere.example', 'MagicLink');
|
|
146
|
+
ResolveConfiguredPrincipal('late@nowhere.example', 'MagicLink');
|
|
147
|
+
ResolveConfiguredPrincipal('late@nowhere.example', 'MagicLink');
|
|
148
|
+
|
|
149
|
+
expect(mockLogError).toHaveBeenCalledTimes(1);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('does not grow without bound', async () => {
|
|
153
|
+
const { ResolveConfiguredPrincipal, ReportedMisconfigurationCount, MAX_REPORTED_MISCONFIGURATIONS } = await loadSubject();
|
|
154
|
+
for (let i = 0; i < MAX_REPORTED_MISCONFIGURATIONS * 3; i++) {
|
|
155
|
+
ResolveConfiguredPrincipal(`filler-${i}@nowhere.example`, 'MagicLink');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
expect(ReportedMisconfigurationCount()).toBeLessThanOrEqual(MAX_REPORTED_MISCONFIGURATIONS);
|
|
159
|
+
});
|
|
160
|
+
});
|