@memberjunction/server 5.9.0 → 5.10.0
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/dist/agents/skip-agent.d.ts.map +1 -1
- package/dist/agents/skip-agent.js +1 -0
- package/dist/agents/skip-agent.js.map +1 -1
- package/dist/agents/skip-sdk.d.ts +6 -0
- package/dist/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +5 -3
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/generated/generated.d.ts +3 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +13 -0
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/PubSubManager.d.ts.map +1 -1
- package/dist/generic/PubSubManager.js +0 -1
- package/dist/generic/PubSubManager.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +16 -4
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/resolvers/CurrentUserContextResolver.d.ts +18 -0
- package/dist/resolvers/CurrentUserContextResolver.d.ts.map +1 -0
- package/dist/resolvers/CurrentUserContextResolver.js +54 -0
- package/dist/resolvers/CurrentUserContextResolver.js.map +1 -0
- package/dist/test-dynamic-plugin.d.ts +6 -0
- package/dist/test-dynamic-plugin.d.ts.map +1 -0
- package/dist/test-dynamic-plugin.js +18 -0
- package/dist/test-dynamic-plugin.js.map +1 -0
- package/package.json +59 -59
- package/src/__tests__/bcsaas-integration.test.ts +455 -0
- package/src/__tests__/middleware-integration.test.ts +877 -0
- package/src/__tests__/mjapi-bootstrap.test.ts +29 -0
- package/src/agents/skip-agent.ts +1 -0
- package/src/agents/skip-sdk.ts +13 -3
- package/src/generated/generated.ts +10 -0
- package/src/generic/PubSubManager.ts +0 -1
- package/src/generic/ResolverBase.ts +17 -4
- package/src/index.ts +1 -0
- package/src/resolvers/CurrentUserContextResolver.ts +39 -0
- package/src/test-dynamic-plugin.ts +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/server",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.10.0",
|
|
4
4
|
"description": "MemberJunction: This project provides API access via GraphQL to the common data store.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -27,62 +27,62 @@
|
|
|
27
27
|
"@as-integrations/express5": "^1.0.0",
|
|
28
28
|
"@graphql-tools/schema": "latest",
|
|
29
29
|
"@graphql-tools/utils": "^11.0.0",
|
|
30
|
-
"@memberjunction/actions": "5.
|
|
31
|
-
"@memberjunction/actions-base": "5.
|
|
32
|
-
"@memberjunction/actions-apollo": "5.
|
|
33
|
-
"@memberjunction/actions-bizapps-accounting": "5.
|
|
34
|
-
"@memberjunction/actions-bizapps-crm": "5.
|
|
35
|
-
"@memberjunction/actions-bizapps-formbuilders": "5.
|
|
36
|
-
"@memberjunction/actions-bizapps-lms": "5.
|
|
37
|
-
"@memberjunction/actions-bizapps-social": "5.
|
|
38
|
-
"@memberjunction/ai": "5.
|
|
39
|
-
"@memberjunction/ai-mcp-client": "5.
|
|
40
|
-
"@memberjunction/ai-agent-manager": "5.
|
|
41
|
-
"@memberjunction/ai-agent-manager-actions": "5.
|
|
42
|
-
"@memberjunction/ai-agents": "5.
|
|
43
|
-
"@memberjunction/ai-core-plus": "5.
|
|
44
|
-
"@memberjunction/ai-prompts": "5.
|
|
45
|
-
"@memberjunction/ai-provider-bundle": "5.
|
|
46
|
-
"@memberjunction/ai-vectors-pinecone": "5.
|
|
47
|
-
"@memberjunction/aiengine": "5.
|
|
48
|
-
"@memberjunction/communication-ms-graph": "5.
|
|
49
|
-
"@memberjunction/communication-sendgrid": "5.
|
|
50
|
-
"@memberjunction/communication-types": "5.
|
|
51
|
-
"@memberjunction/component-registry-client-sdk": "5.
|
|
52
|
-
"@memberjunction/config": "5.
|
|
53
|
-
"@memberjunction/core": "5.
|
|
54
|
-
"@memberjunction/core-actions": "5.
|
|
55
|
-
"@memberjunction/core-entities": "5.
|
|
56
|
-
"@memberjunction/core-entities-server": "5.
|
|
57
|
-
"@memberjunction/data-context": "5.
|
|
58
|
-
"@memberjunction/data-context-server": "5.
|
|
59
|
-
"@memberjunction/doc-utils": "5.
|
|
60
|
-
"@memberjunction/api-keys": "5.
|
|
61
|
-
"@memberjunction/encryption": "5.
|
|
62
|
-
"@memberjunction/entity-communications-base": "5.
|
|
63
|
-
"@memberjunction/entity-communications-server": "5.
|
|
64
|
-
"@memberjunction/external-change-detection": "5.
|
|
65
|
-
"@memberjunction/generic-database-provider": "5.
|
|
66
|
-
"@memberjunction/global": "5.
|
|
67
|
-
"@memberjunction/graphql-dataprovider": "5.
|
|
68
|
-
"@memberjunction/integration-engine": "5.
|
|
69
|
-
"@memberjunction/integration-schema-builder": "5.
|
|
70
|
-
"@memberjunction/interactive-component-types": "5.
|
|
71
|
-
"@memberjunction/computer-use-engine": "5.
|
|
72
|
-
"@memberjunction/notifications": "5.
|
|
73
|
-
"@memberjunction/queue": "5.
|
|
74
|
-
"@memberjunction/redis-provider": "5.
|
|
75
|
-
"@memberjunction/scheduling-actions": "5.
|
|
76
|
-
"@memberjunction/scheduling-base-types": "5.
|
|
77
|
-
"@memberjunction/scheduling-engine": "5.
|
|
78
|
-
"@memberjunction/scheduling-engine-base": "5.
|
|
79
|
-
"@memberjunction/skip-types": "5.
|
|
80
|
-
"@memberjunction/sqlserver-dataprovider": "5.
|
|
81
|
-
"@memberjunction/storage": "5.
|
|
82
|
-
"@memberjunction/templates": "5.
|
|
83
|
-
"@memberjunction/testing-engine": "5.
|
|
84
|
-
"@memberjunction/testing-engine-base": "5.
|
|
85
|
-
"@memberjunction/version-history": "5.
|
|
30
|
+
"@memberjunction/actions": "5.10.0",
|
|
31
|
+
"@memberjunction/actions-base": "5.10.0",
|
|
32
|
+
"@memberjunction/actions-apollo": "5.10.0",
|
|
33
|
+
"@memberjunction/actions-bizapps-accounting": "5.10.0",
|
|
34
|
+
"@memberjunction/actions-bizapps-crm": "5.10.0",
|
|
35
|
+
"@memberjunction/actions-bizapps-formbuilders": "5.10.0",
|
|
36
|
+
"@memberjunction/actions-bizapps-lms": "5.10.0",
|
|
37
|
+
"@memberjunction/actions-bizapps-social": "5.10.0",
|
|
38
|
+
"@memberjunction/ai": "5.10.0",
|
|
39
|
+
"@memberjunction/ai-mcp-client": "5.10.0",
|
|
40
|
+
"@memberjunction/ai-agent-manager": "5.10.0",
|
|
41
|
+
"@memberjunction/ai-agent-manager-actions": "5.10.0",
|
|
42
|
+
"@memberjunction/ai-agents": "5.10.0",
|
|
43
|
+
"@memberjunction/ai-core-plus": "5.10.0",
|
|
44
|
+
"@memberjunction/ai-prompts": "5.10.0",
|
|
45
|
+
"@memberjunction/ai-provider-bundle": "5.10.0",
|
|
46
|
+
"@memberjunction/ai-vectors-pinecone": "5.10.0",
|
|
47
|
+
"@memberjunction/aiengine": "5.10.0",
|
|
48
|
+
"@memberjunction/communication-ms-graph": "5.10.0",
|
|
49
|
+
"@memberjunction/communication-sendgrid": "5.10.0",
|
|
50
|
+
"@memberjunction/communication-types": "5.10.0",
|
|
51
|
+
"@memberjunction/component-registry-client-sdk": "5.10.0",
|
|
52
|
+
"@memberjunction/config": "5.10.0",
|
|
53
|
+
"@memberjunction/core": "5.10.0",
|
|
54
|
+
"@memberjunction/core-actions": "5.10.0",
|
|
55
|
+
"@memberjunction/core-entities": "5.10.0",
|
|
56
|
+
"@memberjunction/core-entities-server": "5.10.0",
|
|
57
|
+
"@memberjunction/data-context": "5.10.0",
|
|
58
|
+
"@memberjunction/data-context-server": "5.10.0",
|
|
59
|
+
"@memberjunction/doc-utils": "5.10.0",
|
|
60
|
+
"@memberjunction/api-keys": "5.10.0",
|
|
61
|
+
"@memberjunction/encryption": "5.10.0",
|
|
62
|
+
"@memberjunction/entity-communications-base": "5.10.0",
|
|
63
|
+
"@memberjunction/entity-communications-server": "5.10.0",
|
|
64
|
+
"@memberjunction/external-change-detection": "5.10.0",
|
|
65
|
+
"@memberjunction/generic-database-provider": "5.10.0",
|
|
66
|
+
"@memberjunction/global": "5.10.0",
|
|
67
|
+
"@memberjunction/graphql-dataprovider": "5.10.0",
|
|
68
|
+
"@memberjunction/integration-engine": "5.10.0",
|
|
69
|
+
"@memberjunction/integration-schema-builder": "5.10.0",
|
|
70
|
+
"@memberjunction/interactive-component-types": "5.10.0",
|
|
71
|
+
"@memberjunction/computer-use-engine": "5.10.0",
|
|
72
|
+
"@memberjunction/notifications": "5.10.0",
|
|
73
|
+
"@memberjunction/queue": "5.10.0",
|
|
74
|
+
"@memberjunction/redis-provider": "5.10.0",
|
|
75
|
+
"@memberjunction/scheduling-actions": "5.10.0",
|
|
76
|
+
"@memberjunction/scheduling-base-types": "5.10.0",
|
|
77
|
+
"@memberjunction/scheduling-engine": "5.10.0",
|
|
78
|
+
"@memberjunction/scheduling-engine-base": "5.10.0",
|
|
79
|
+
"@memberjunction/skip-types": "5.10.0",
|
|
80
|
+
"@memberjunction/sqlserver-dataprovider": "5.10.0",
|
|
81
|
+
"@memberjunction/storage": "5.10.0",
|
|
82
|
+
"@memberjunction/templates": "5.10.0",
|
|
83
|
+
"@memberjunction/testing-engine": "5.10.0",
|
|
84
|
+
"@memberjunction/testing-engine-base": "5.10.0",
|
|
85
|
+
"@memberjunction/version-history": "5.10.0",
|
|
86
86
|
"@types/compression": "^1.8.1",
|
|
87
87
|
"@types/cors": "^2.8.19",
|
|
88
88
|
"@types/jsonwebtoken": "9.0.10",
|
|
@@ -107,8 +107,8 @@
|
|
|
107
107
|
"jwks-rsa": "^3.2.2",
|
|
108
108
|
"lru-cache": "^11.2.5",
|
|
109
109
|
"mssql": "^12.2.0",
|
|
110
|
-
"@memberjunction/postgresql-dataprovider": "5.
|
|
111
|
-
"@memberjunction/sql-dialect": "5.
|
|
110
|
+
"@memberjunction/postgresql-dataprovider": "5.10.0",
|
|
111
|
+
"@memberjunction/sql-dialect": "5.10.0",
|
|
112
112
|
"pg": "^8.13.3",
|
|
113
113
|
"@types/pg": "^8.11.11",
|
|
114
114
|
"reflect-metadata": "0.2.2",
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BCSaaS Integration Tests — Phase 4, 5, 6
|
|
3
|
+
*
|
|
4
|
+
* Tests the BCSaaS middle-layer plugin architecture against a RUNNING MJAPI instance
|
|
5
|
+
* that has BCSaaS loaded via DynamicPackageLoader and a database with BCSaaS entities.
|
|
6
|
+
*
|
|
7
|
+
* Test data setup (mj_test database):
|
|
8
|
+
* - Organizations: Acme Corp (11111111-...), Beta Inc (22222222-...), Acme West Division (33333333-...)
|
|
9
|
+
* - Contacts: System User (AAAAAAAA-...) linked to MJ System user, Other User (BBBBBBBB-...)
|
|
10
|
+
* - OrgContacts: System→Acme (Owner), System→Beta (Member), Other→Beta (Admin)
|
|
11
|
+
* - ContactRoles: Owner, Admin, Member, Viewer
|
|
12
|
+
*
|
|
13
|
+
* Environment variables:
|
|
14
|
+
* MJAPI_URL Base URL of the running MJAPI instance (required)
|
|
15
|
+
* MJAPI_SYSTEM_API_KEY System API key (x-mj-api-key header)
|
|
16
|
+
*/
|
|
17
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
18
|
+
|
|
19
|
+
// ─── Configuration ─────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const BASE_URL = process.env.MJAPI_URL ?? '';
|
|
22
|
+
const SYSTEM_API_KEY = process.env.MJAPI_SYSTEM_API_KEY ?? '';
|
|
23
|
+
const GRAPHQL_PATH = process.env.MJAPI_GRAPHQL_PATH ?? '/';
|
|
24
|
+
|
|
25
|
+
const GRAPHQL_URL = `${BASE_URL}${GRAPHQL_PATH}`;
|
|
26
|
+
const HAS_SERVER = !!BASE_URL;
|
|
27
|
+
const HAS_SYSTEM_KEY = !!SYSTEM_API_KEY;
|
|
28
|
+
|
|
29
|
+
// Timeout for HTTP requests (some first-request queries are slow due to connection pool warmup)
|
|
30
|
+
const REQUEST_TIMEOUT = 60_000;
|
|
31
|
+
|
|
32
|
+
// ─── Test Data IDs ─────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const ACME_ORG_ID = '11111111-1111-1111-1111-111111111111';
|
|
35
|
+
const BETA_ORG_ID = '22222222-2222-2222-2222-222222222222';
|
|
36
|
+
const SYSTEM_CONTACT_ID = 'AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA';
|
|
37
|
+
const NONEXISTENT_ORG_ID = '99999999-9999-9999-9999-999999999999';
|
|
38
|
+
|
|
39
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
interface FetchResult {
|
|
42
|
+
status: number;
|
|
43
|
+
headers: Headers;
|
|
44
|
+
body: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function fetchJson(url: string, options: RequestInit = {}): Promise<FetchResult> {
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch(url, { ...options, signal: controller.signal });
|
|
52
|
+
let body: Record<string, unknown> = {};
|
|
53
|
+
try {
|
|
54
|
+
body = (await response.json()) as Record<string, unknown>;
|
|
55
|
+
} catch {
|
|
56
|
+
// Non-JSON response — body stays empty
|
|
57
|
+
}
|
|
58
|
+
return { status: response.status, headers: response.headers, body };
|
|
59
|
+
} finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function systemKeyHeaders(extra: Record<string, string> = {}): Record<string, string> {
|
|
65
|
+
return {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
'x-mj-api-key': SYSTEM_API_KEY,
|
|
68
|
+
...extra,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function makeRunViewQuery(entityName: string, extraFilter = '', maxRows = 100): string {
|
|
73
|
+
return JSON.stringify({
|
|
74
|
+
query: `query RunDynamicView($input: RunDynamicViewInput!) {
|
|
75
|
+
RunDynamicView(input: $input) {
|
|
76
|
+
Results { PrimaryKey { FieldName Value } EntityID Data }
|
|
77
|
+
RowCount TotalRowCount ExecutionTime ErrorMessage
|
|
78
|
+
}
|
|
79
|
+
}`,
|
|
80
|
+
variables: {
|
|
81
|
+
input: {
|
|
82
|
+
EntityName: entityName,
|
|
83
|
+
ExtraFilter: extraFilter,
|
|
84
|
+
OrderBy: '',
|
|
85
|
+
MaxRows: maxRows,
|
|
86
|
+
ResultType: 'simple',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
operationName: 'RunDynamicView',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface RunViewData {
|
|
94
|
+
Results: Array<{ Data: string; PrimaryKey: Array<{ FieldName: string; Value: string }> }>;
|
|
95
|
+
RowCount: number;
|
|
96
|
+
TotalRowCount: number;
|
|
97
|
+
ErrorMessage: string | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function extractRunViewData(body: Record<string, unknown>): RunViewData | null {
|
|
101
|
+
const data = body.data as Record<string, RunViewData> | null;
|
|
102
|
+
if (!data?.RunDynamicView) return null;
|
|
103
|
+
return data.RunDynamicView;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseResults(rvData: RunViewData): Record<string, unknown>[] {
|
|
107
|
+
return rvData.Results.map(r => JSON.parse(r.Data) as Record<string, unknown>);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Phase 4: BCSaaS as Middle-Layer Plugin ─────────────────────────────────
|
|
111
|
+
|
|
112
|
+
describe('Phase 4: BCSaaS as Middle-Layer Plugin', () => {
|
|
113
|
+
beforeAll(() => {
|
|
114
|
+
if (!HAS_SERVER) {
|
|
115
|
+
console.warn('MJAPI_URL not set — skipping BCSaaS integration tests');
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('4.4 BCTenantContext resolution', () => {
|
|
120
|
+
it.skipIf(!HAS_SERVER || !HAS_SYSTEM_KEY)(
|
|
121
|
+
'should resolve tenant context for authenticated system user',
|
|
122
|
+
async () => {
|
|
123
|
+
// The system user (not.set@nowhere.com) has a Contact linked via LinkedUserID.
|
|
124
|
+
// bcTenantContextMiddleware finds the Contact, loads org memberships.
|
|
125
|
+
// Verify by querying BC: Contacts (non-scoped) — success means middleware ran.
|
|
126
|
+
const { status, body } = await fetchJson(GRAPHQL_URL, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: systemKeyHeaders(),
|
|
129
|
+
body: makeRunViewQuery('BC: Contacts'),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(status).toBe(200);
|
|
133
|
+
const rvData = extractRunViewData(body);
|
|
134
|
+
if (rvData) {
|
|
135
|
+
expect(rvData.ErrorMessage).toBeNull();
|
|
136
|
+
// Should see both contacts (BC: Contacts has no OrganizationID — unfiltered)
|
|
137
|
+
expect(rvData.RowCount).toBe(2);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
REQUEST_TIMEOUT
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('4.6 Multi-org user resolution', () => {
|
|
145
|
+
it.skipIf(!HAS_SERVER || !HAS_SYSTEM_KEY)(
|
|
146
|
+
'should access both orgs the user belongs to',
|
|
147
|
+
async () => {
|
|
148
|
+
// System user belongs to Acme Corp (Owner) and Beta Inc (Member).
|
|
149
|
+
// Verify multi-org by switching org context and confirming both work.
|
|
150
|
+
// Query with Acme context:
|
|
151
|
+
const acmeResult = await fetchJson(GRAPHQL_URL, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: systemKeyHeaders({ 'x-organization-id': ACME_ORG_ID }),
|
|
154
|
+
body: makeRunViewQuery('BC: Organization Contacts'),
|
|
155
|
+
});
|
|
156
|
+
expect(acmeResult.status).toBe(200);
|
|
157
|
+
|
|
158
|
+
// Query with Beta context:
|
|
159
|
+
const betaResult = await fetchJson(GRAPHQL_URL, {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
headers: systemKeyHeaders({ 'x-organization-id': BETA_ORG_ID }),
|
|
162
|
+
body: makeRunViewQuery('BC: Organization Contacts'),
|
|
163
|
+
});
|
|
164
|
+
expect(betaResult.status).toBe(200);
|
|
165
|
+
|
|
166
|
+
const acmeData = extractRunViewData(acmeResult.body);
|
|
167
|
+
const betaData = extractRunViewData(betaResult.body);
|
|
168
|
+
|
|
169
|
+
if (acmeData && betaData) {
|
|
170
|
+
// Acme has 1 org contact (System user as Owner)
|
|
171
|
+
const acmeContacts = parseResults(acmeData);
|
|
172
|
+
expect(acmeContacts.length).toBe(1);
|
|
173
|
+
expect(acmeContacts[0].OrganizationID).toBe(ACME_ORG_ID);
|
|
174
|
+
|
|
175
|
+
// Beta has 2 org contacts (System as Member + Other as Admin)
|
|
176
|
+
const betaContacts = parseResults(betaData);
|
|
177
|
+
expect(betaContacts.length).toBe(2);
|
|
178
|
+
for (const c of betaContacts) {
|
|
179
|
+
expect(c.OrganizationID).toBe(BETA_ORG_ID);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
REQUEST_TIMEOUT
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('4.7 Org selector via X-Organization-ID header', () => {
|
|
188
|
+
it.skipIf(!HAS_SERVER || !HAS_SYSTEM_KEY)(
|
|
189
|
+
'should switch active org via X-Organization-ID header',
|
|
190
|
+
async () => {
|
|
191
|
+
// When X-Organization-ID is set to Beta Inc, the request should succeed.
|
|
192
|
+
const { status, body } = await fetchJson(GRAPHQL_URL, {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: systemKeyHeaders({ 'x-organization-id': BETA_ORG_ID }),
|
|
195
|
+
body: JSON.stringify({ query: '{ __typename }' }),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(status).toBe(200);
|
|
199
|
+
const data = body.data as Record<string, string> | null;
|
|
200
|
+
expect(data?.__typename).toBe('Query');
|
|
201
|
+
},
|
|
202
|
+
REQUEST_TIMEOUT
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
it.skipIf(!HAS_SERVER || !HAS_SYSTEM_KEY)(
|
|
206
|
+
'should reject X-Organization-ID for org user has no access to',
|
|
207
|
+
async () => {
|
|
208
|
+
// System user does NOT belong to org 99999999-...
|
|
209
|
+
const { status, body } = await fetchJson(GRAPHQL_URL, {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: systemKeyHeaders({ 'x-organization-id': NONEXISTENT_ORG_ID }),
|
|
212
|
+
body: JSON.stringify({ query: '{ __typename }' }),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(status).toBe(403);
|
|
216
|
+
const error = body as { error?: string; message?: string };
|
|
217
|
+
expect(error.error).toBe('Forbidden');
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('4.8 MJ TenantContext populated', () => {
|
|
223
|
+
it.skipIf(!HAS_SERVER || !HAS_SYSTEM_KEY)(
|
|
224
|
+
'should populate TenantContext with org data',
|
|
225
|
+
async () => {
|
|
226
|
+
// When context is set to Acme, org-scoped queries should be filtered.
|
|
227
|
+
// This proves TenantContext is populated and hooks can read it.
|
|
228
|
+
const { status, body } = await fetchJson(GRAPHQL_URL, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers: systemKeyHeaders({ 'x-organization-id': ACME_ORG_ID }),
|
|
231
|
+
body: makeRunViewQuery('BC: Organization Contacts'),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(status).toBe(200);
|
|
235
|
+
const rvData = extractRunViewData(body);
|
|
236
|
+
if (rvData) {
|
|
237
|
+
expect(rvData.ErrorMessage).toBeNull();
|
|
238
|
+
// Hook used TenantContext.organizationID to filter — only Acme contacts
|
|
239
|
+
const results = parseResults(rvData);
|
|
240
|
+
expect(results.length).toBe(1);
|
|
241
|
+
expect(results[0].OrganizationID).toBe(ACME_ORG_ID);
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
REQUEST_TIMEOUT
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('4.12 GraphQL context has tenant', () => {
|
|
249
|
+
it.skipIf(!HAS_SERVER || !HAS_SYSTEM_KEY)(
|
|
250
|
+
'should authenticate and execute GraphQL with tenant context',
|
|
251
|
+
async () => {
|
|
252
|
+
const { status, body } = await fetchJson(GRAPHQL_URL, {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
headers: systemKeyHeaders(),
|
|
255
|
+
body: JSON.stringify({ query: '{ __typename }' }),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(status).toBe(200);
|
|
259
|
+
const data = body.data as Record<string, string> | null;
|
|
260
|
+
expect(data?.__typename).toBe('Query');
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ─── Phase 5: BCSaaS Hook Integration ───────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
describe('Phase 5: BCSaaS Hook Integration', () => {
|
|
269
|
+
describe('5.3 BCSaaS PreRunView filter uses org context', () => {
|
|
270
|
+
it.skipIf(!HAS_SERVER || !HAS_SYSTEM_KEY)(
|
|
271
|
+
'should filter org-scoped entities by OrganizationID from default context',
|
|
272
|
+
async () => {
|
|
273
|
+
// Default org for System user is Acme Corp (first membership).
|
|
274
|
+
// BC: Organization Contacts (org-scoped) should only return Acme contacts.
|
|
275
|
+
const { status, body } = await fetchJson(GRAPHQL_URL, {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
headers: systemKeyHeaders(),
|
|
278
|
+
body: makeRunViewQuery('BC: Organization Contacts'),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
expect(status).toBe(200);
|
|
282
|
+
const rvData = extractRunViewData(body);
|
|
283
|
+
if (rvData && rvData.RowCount > 0) {
|
|
284
|
+
const results = parseResults(rvData);
|
|
285
|
+
for (const row of results) {
|
|
286
|
+
expect(row.OrganizationID).toBe(ACME_ORG_ID);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
REQUEST_TIMEOUT
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
it.skipIf(!HAS_SERVER || !HAS_SYSTEM_KEY)(
|
|
294
|
+
'should filter by selected org when X-Organization-ID is set',
|
|
295
|
+
async () => {
|
|
296
|
+
const { status, body } = await fetchJson(GRAPHQL_URL, {
|
|
297
|
+
method: 'POST',
|
|
298
|
+
headers: systemKeyHeaders({ 'x-organization-id': BETA_ORG_ID }),
|
|
299
|
+
body: makeRunViewQuery('BC: Organization Contacts'),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
expect(status).toBe(200);
|
|
303
|
+
const rvData = extractRunViewData(body);
|
|
304
|
+
if (rvData && rvData.RowCount > 0) {
|
|
305
|
+
const results = parseResults(rvData);
|
|
306
|
+
for (const row of results) {
|
|
307
|
+
expect(row.OrganizationID).toBe(BETA_ORG_ID);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
REQUEST_TIMEOUT
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe('5.3b Non-scoped entities pass through unfiltered', () => {
|
|
316
|
+
it.skipIf(!HAS_SERVER || !HAS_SYSTEM_KEY)(
|
|
317
|
+
'should NOT filter entities without OrganizationID field',
|
|
318
|
+
async () => {
|
|
319
|
+
// BC: Contacts does NOT have OrganizationID — should return all 2 contacts
|
|
320
|
+
const { status, body } = await fetchJson(GRAPHQL_URL, {
|
|
321
|
+
method: 'POST',
|
|
322
|
+
headers: systemKeyHeaders(),
|
|
323
|
+
body: makeRunViewQuery('BC: Contacts'),
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
expect(status).toBe(200);
|
|
327
|
+
const rvData = extractRunViewData(body);
|
|
328
|
+
if (rvData) {
|
|
329
|
+
expect(rvData.ErrorMessage).toBeNull();
|
|
330
|
+
expect(rvData.RowCount).toBe(2);
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
REQUEST_TIMEOUT
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('5.6 Hook priority ordering', () => {
|
|
338
|
+
it.skipIf(!HAS_SERVER || !HAS_SYSTEM_KEY)(
|
|
339
|
+
'should execute BCSaaS hook at priority 100 for org filtering',
|
|
340
|
+
async () => {
|
|
341
|
+
// BCSaaS hooks at priority 100 with namespace 'mj:tenantFilter'.
|
|
342
|
+
// Verify the hook runs by checking org filtering works.
|
|
343
|
+
const { status, body } = await fetchJson(GRAPHQL_URL, {
|
|
344
|
+
method: 'POST',
|
|
345
|
+
headers: systemKeyHeaders({ 'x-organization-id': BETA_ORG_ID }),
|
|
346
|
+
body: makeRunViewQuery('BC: Organization Contacts'),
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
expect(status).toBe(200);
|
|
350
|
+
const rvData = extractRunViewData(body);
|
|
351
|
+
if (rvData && rvData.RowCount > 0) {
|
|
352
|
+
const results = parseResults(rvData);
|
|
353
|
+
for (const row of results) {
|
|
354
|
+
expect(row.OrganizationID).toBe(BETA_ORG_ID);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
REQUEST_TIMEOUT
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe('5.7-5.8 MJ multi-tenancy disabled, BCSaaS handles all', () => {
|
|
363
|
+
it.skipIf(!HAS_SERVER || !HAS_SYSTEM_KEY)(
|
|
364
|
+
'should handle tenant filtering entirely through BCSaaS hooks',
|
|
365
|
+
async () => {
|
|
366
|
+
// MJ config-driven MT is disabled. BCSaaS hooks handle all filtering.
|
|
367
|
+
const { status, body } = await fetchJson(GRAPHQL_URL, {
|
|
368
|
+
method: 'POST',
|
|
369
|
+
headers: systemKeyHeaders({ 'x-organization-id': ACME_ORG_ID }),
|
|
370
|
+
body: makeRunViewQuery('BC: Organization Contacts'),
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
expect(status).toBe(200);
|
|
374
|
+
const rvData = extractRunViewData(body);
|
|
375
|
+
if (rvData && rvData.RowCount > 0) {
|
|
376
|
+
const results = parseResults(rvData);
|
|
377
|
+
// Acme Corp has 1 org contact (System user)
|
|
378
|
+
expect(results.length).toBe(1);
|
|
379
|
+
expect(results[0].OrganizationID).toBe(ACME_ORG_ID);
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
REQUEST_TIMEOUT
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// ─── Phase 6: Multi-Layer Stacking ──────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
describe('Phase 6: Multi-Layer Stacking', () => {
|
|
390
|
+
describe('6.5 BCSaaS hooks coexist with base MJ', () => {
|
|
391
|
+
it.skipIf(!HAS_SERVER || !HAS_SYSTEM_KEY)(
|
|
392
|
+
'should run BCSaaS middleware and hooks without conflicting with MJ base',
|
|
393
|
+
async () => {
|
|
394
|
+
// Full pipeline: MJ auth → BCSaaS tenant → hooks → GraphQL
|
|
395
|
+
// BC: Organizations has no OrganizationID field, so it's unfiltered — all 3 orgs.
|
|
396
|
+
const { status, body } = await fetchJson(GRAPHQL_URL, {
|
|
397
|
+
method: 'POST',
|
|
398
|
+
headers: systemKeyHeaders(),
|
|
399
|
+
body: makeRunViewQuery('BC: Organizations'),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
expect(status).toBe(200);
|
|
403
|
+
const rvData = extractRunViewData(body);
|
|
404
|
+
if (rvData) {
|
|
405
|
+
expect(rvData.ErrorMessage).toBeNull();
|
|
406
|
+
expect(rvData.RowCount).toBe(3);
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
REQUEST_TIMEOUT
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe('6.6 Middleware execution order', () => {
|
|
414
|
+
it.skipIf(!HAS_SERVER || !HAS_SYSTEM_KEY)(
|
|
415
|
+
'should execute: MJ Auth → BCSaaS Tenant → GraphQL in correct order',
|
|
416
|
+
async () => {
|
|
417
|
+
// Order verified by:
|
|
418
|
+
// 1. Auth succeeds (200, not 401)
|
|
419
|
+
// 2. Tenant context resolved (org filtering works on scoped entity)
|
|
420
|
+
// 3. GraphQL query returns filtered data
|
|
421
|
+
const { status, body } = await fetchJson(GRAPHQL_URL, {
|
|
422
|
+
method: 'POST',
|
|
423
|
+
headers: systemKeyHeaders({ 'x-organization-id': ACME_ORG_ID }),
|
|
424
|
+
body: makeRunViewQuery('BC: Organization Contacts'),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
expect(status).toBe(200);
|
|
428
|
+
const rvData = extractRunViewData(body);
|
|
429
|
+
if (rvData) {
|
|
430
|
+
expect(rvData.ErrorMessage).toBeNull();
|
|
431
|
+
if (rvData.RowCount > 0) {
|
|
432
|
+
const results = parseResults(rvData);
|
|
433
|
+
for (const row of results) {
|
|
434
|
+
expect(row.OrganizationID).toBe(ACME_ORG_ID);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
REQUEST_TIMEOUT
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
it.skipIf(!HAS_SERVER || !HAS_SYSTEM_KEY)(
|
|
443
|
+
'should return 401 when no auth is provided (MJ auth blocks before BCSaaS)',
|
|
444
|
+
async () => {
|
|
445
|
+
const { status } = await fetchJson(GRAPHQL_URL, {
|
|
446
|
+
method: 'POST',
|
|
447
|
+
headers: { 'Content-Type': 'application/json' },
|
|
448
|
+
body: makeRunViewQuery('BC: Organizations'),
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
expect(status).toBe(401);
|
|
452
|
+
}
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
});
|