@tellescope/sdk 1.252.0 → 1.252.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.
Files changed (52) hide show
  1. package/lib/cjs/tests/api_tests/calendar_canvas_coding_clear.test.d.ts +6 -0
  2. package/lib/cjs/tests/api_tests/calendar_canvas_coding_clear.test.d.ts.map +1 -0
  3. package/lib/cjs/tests/api_tests/calendar_canvas_coding_clear.test.js +139 -0
  4. package/lib/cjs/tests/api_tests/calendar_canvas_coding_clear.test.js.map +1 -0
  5. package/lib/cjs/tests/api_tests/integrations_redacted.test.d.ts.map +1 -1
  6. package/lib/cjs/tests/api_tests/integrations_redacted.test.js +30 -20
  7. package/lib/cjs/tests/api_tests/integrations_redacted.test.js.map +1 -1
  8. package/lib/cjs/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.d.ts +32 -0
  9. package/lib/cjs/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.d.ts.map +1 -0
  10. package/lib/cjs/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.js +237 -0
  11. package/lib/cjs/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.js.map +1 -0
  12. package/lib/cjs/tests/api_tests/security/F-0076-self-admin-role-assignment.test.d.ts +38 -0
  13. package/lib/cjs/tests/api_tests/security/F-0076-self-admin-role-assignment.test.d.ts.map +1 -0
  14. package/lib/cjs/tests/api_tests/security/F-0076-self-admin-role-assignment.test.js +222 -0
  15. package/lib/cjs/tests/api_tests/security/F-0076-self-admin-role-assignment.test.js.map +1 -0
  16. package/lib/cjs/tests/api_tests/user_portal_settings.test.d.ts +6 -0
  17. package/lib/cjs/tests/api_tests/user_portal_settings.test.d.ts.map +1 -0
  18. package/lib/cjs/tests/api_tests/user_portal_settings.test.js +301 -0
  19. package/lib/cjs/tests/api_tests/user_portal_settings.test.js.map +1 -0
  20. package/lib/cjs/tests/tests.d.ts.map +1 -1
  21. package/lib/cjs/tests/tests.js +154 -142
  22. package/lib/cjs/tests/tests.js.map +1 -1
  23. package/lib/esm/tests/api_tests/calendar_canvas_coding_clear.test.d.ts +6 -0
  24. package/lib/esm/tests/api_tests/calendar_canvas_coding_clear.test.d.ts.map +1 -0
  25. package/lib/esm/tests/api_tests/calendar_canvas_coding_clear.test.js +135 -0
  26. package/lib/esm/tests/api_tests/calendar_canvas_coding_clear.test.js.map +1 -0
  27. package/lib/esm/tests/api_tests/integrations_redacted.test.d.ts.map +1 -1
  28. package/lib/esm/tests/api_tests/integrations_redacted.test.js +30 -20
  29. package/lib/esm/tests/api_tests/integrations_redacted.test.js.map +1 -1
  30. package/lib/esm/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.d.ts +32 -0
  31. package/lib/esm/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.d.ts.map +1 -0
  32. package/lib/esm/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.js +233 -0
  33. package/lib/esm/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.js.map +1 -0
  34. package/lib/esm/tests/api_tests/security/F-0076-self-admin-role-assignment.test.d.ts +38 -0
  35. package/lib/esm/tests/api_tests/security/F-0076-self-admin-role-assignment.test.d.ts.map +1 -0
  36. package/lib/esm/tests/api_tests/security/F-0076-self-admin-role-assignment.test.js +218 -0
  37. package/lib/esm/tests/api_tests/security/F-0076-self-admin-role-assignment.test.js.map +1 -0
  38. package/lib/esm/tests/api_tests/user_portal_settings.test.d.ts +6 -0
  39. package/lib/esm/tests/api_tests/user_portal_settings.test.d.ts.map +1 -0
  40. package/lib/esm/tests/api_tests/user_portal_settings.test.js +297 -0
  41. package/lib/esm/tests/api_tests/user_portal_settings.test.js.map +1 -0
  42. package/lib/esm/tests/tests.d.ts.map +1 -1
  43. package/lib/esm/tests/tests.js +154 -142
  44. package/lib/esm/tests/tests.js.map +1 -1
  45. package/lib/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +10 -10
  47. package/src/tests/api_tests/integrations_redacted.test.ts +8 -0
  48. package/src/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.ts +161 -0
  49. package/src/tests/api_tests/security/F-0076-self-admin-role-assignment.test.ts +165 -0
  50. package/src/tests/api_tests/user_portal_settings.test.ts +217 -0
  51. package/src/tests/tests.ts +6 -0
  52. package/test_generated.pdf +0 -0
@@ -0,0 +1,161 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session } from "../../../sdk"
4
+ import {
5
+ assert,
6
+ log_header,
7
+ wait,
8
+ } from "@tellescope/testing"
9
+ import { setup_tests } from "../../setup"
10
+ import { PROVIDER_PERMISSIONS } from "@tellescope/constants"
11
+
12
+ const host = process.env.API_URL || 'http://localhost:8080' as const
13
+
14
+ // Separate tenant (different businessId), reusing the same hardcoded apiKey that
15
+ // multi_tenant_tests relies on in tests.ts.
16
+ const OTHER_TENANT_API_KEY = "ba745e25162bb95a795c5fa1af70df188d93c4d3aac9c48b34a5c8c9dd7b80f7"
17
+
18
+ /**
19
+ * Tenant-boundary guard for cascade_role_rename (relates to security-audit finding F-0053,
20
+ * which was investigated and closed as a FALSE POSITIVE — see that file for the code trace).
21
+ *
22
+ * The `cascade_role_rename` side-effect handler
23
+ * ([event_handlers_v2/role_based_access_permissions.ts](packages/private/api/api/v1/event_handlers_v2/role_based_access_permissions.ts))
24
+ * runs when a `role_based_access_permissions` doc's `role` field changes. It finds every user
25
+ * with the old role name (via `DBUnrestricted.users`) and rewrites their `roles` array, then
26
+ * deauthenticates them. F-0053 hypothesized this query was globally cross-tenant. It is NOT:
27
+ * `DBUnrestricted` bypasses per-user/per-role RBAC but is STILL scoped to the acting session's
28
+ * `businessId` (see `modifyFilterForAccessConstraint` injecting `{ businessId }` at
29
+ * database.ts:1761-1763, reached via the `unrestricted: true` branch at database.ts:2137-2144).
30
+ *
31
+ * This test locks that boundary in place so a future refactor of `DBUnrestricted` semantics
32
+ * can't silently turn the cascade into a cross-tenant write:
33
+ * 1. Tenant A creates a role `ROLE_OLD` and assigns it to a Tenant A user (positive control).
34
+ * 2. Tenant B (separate businessId) has a user whose roles include the SAME `ROLE_OLD`.
35
+ * 3. Tenant A renames the role `ROLE_OLD` -> `ROLE_NEW`.
36
+ * 4. Assert the Tenant B user's roles are UNCHANGED (still `[ROLE_OLD]`) <-- guards the tenant boundary.
37
+ * 5. Assert the Tenant A user's roles ARE renamed to `[ROLE_NEW]` <-- same-tenant cascade works.
38
+ *
39
+ * Expected on current (correct) code: BOTH assertions pass. A regression that drops the
40
+ * `businessId` scoping would flip assertion #4 to red (Tenant B user becomes `[ROLE_NEW]`).
41
+ *
42
+ * A collision-proof unique role name (timestamped) is used so the test never touches real roles.
43
+ */
44
+ export const cascade_role_rename_cross_tenant_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
45
+ log_header("F-0053: cascade role rename cross-tenant regression")
46
+
47
+ const stamp = Date.now()
48
+ const ROLE_OLD = `XTenantRename_${stamp}`
49
+ const ROLE_NEW = `${ROLE_OLD}_renamed`
50
+
51
+ // Tenant B = a genuinely separate businessId (the apiKey's org). admin is required to create /
52
+ // set roles on users, so the apiKey user must be an Admin in its org.
53
+ const sdkOther = new Session({ host, apiKey: OTHER_TENANT_API_KEY })
54
+
55
+ let rbapId: string | undefined
56
+ // We create dedicated throwaway users in BOTH tenants and delete them in `finally`. We never
57
+ // mutate any pre-existing user (an earlier version of this test clobbered the apiKey's own
58
+ // admin user via getSome+replaceObjectFields — see F-0053 finding notes). controlUser lives in
59
+ // Tenant A and SHOULD be renamed by the same-tenant cascade; victimUser lives in Tenant B and
60
+ // must be UNTOUCHED.
61
+ let controlUserId: string | undefined
62
+ let victimUserId: string | undefined
63
+
64
+ try {
65
+ const tenantABusinessId = sdk.userInfo.businessId
66
+
67
+ // 1. Tenant A: create the role to be renamed.
68
+ const rbap = await sdk.api.role_based_access_permissions.createOne({
69
+ role: ROLE_OLD,
70
+ permissions: { ...PROVIDER_PERMISSIONS },
71
+ })
72
+ rbapId = rbap.id
73
+
74
+ // 2. Tenant A: create a throwaway control user holding ROLE_OLD (notification emails off).
75
+ const controlUser = await sdk.api.users.createOne({
76
+ email: `f0053-control-${stamp}@example.com`,
77
+ notificationEmailsDisabled: true,
78
+ } as any)
79
+ controlUserId = controlUser.id
80
+ await sdk.api.users.updateOne(controlUserId, { roles: [ROLE_OLD] }, { replaceObjectFields: true })
81
+
82
+ // 3. Tenant B: create a throwaway victim user holding the SAME ROLE_OLD.
83
+ const victimUser = await sdkOther.api.users.createOne({
84
+ email: `f0053-victim-${stamp}@example.com`,
85
+ notificationEmailsDisabled: true,
86
+ } as any)
87
+ victimUserId = victimUser.id
88
+ await sdkOther.api.users.updateOne(victimUserId, { roles: [ROLE_OLD] }, { replaceObjectFields: true })
89
+
90
+ // 4. Setup sanity: tenants are distinct and both users actually hold ROLE_OLD before the rename.
91
+ // (Distinguishes a setup failure from the security assertion below.)
92
+ assert(
93
+ victimUser.businessId !== tenantABusinessId,
94
+ `Victim user shares businessId with Tenant A (${victimUser.businessId}) — not a cross-tenant scenario`,
95
+ 'F-0053 setup: tenants are distinct',
96
+ )
97
+ const victimBefore = await sdkOther.api.users.getOne(victimUserId)
98
+ const controlBefore = await sdk.api.users.getOne(controlUserId)
99
+ assert(
100
+ JSON.stringify(victimBefore.roles) === JSON.stringify([ROLE_OLD])
101
+ && JSON.stringify(controlBefore.roles) === JSON.stringify([ROLE_OLD]),
102
+ `Setup failed: expected both users to hold [${ROLE_OLD}] (victim=${JSON.stringify(victimBefore.roles)}, control=${JSON.stringify(controlBefore.roles)})`,
103
+ 'F-0053 setup: both users hold ROLE_OLD',
104
+ )
105
+
106
+ // 5. Tenant A renames the role. This triggers cascade_role_rename.
107
+ await sdk.api.role_based_access_permissions.updateOne(rbapId, { role: ROLE_NEW })
108
+ await wait(undefined, 1500) // let the side effect run
109
+
110
+ // 6. SECURITY ASSERTION — the Tenant B victim must be untouched by Tenant A's rename.
111
+ const victimAfter = await sdkOther.api.users.getOne(victimUserId)
112
+ assert(
113
+ JSON.stringify(victimAfter.roles) === JSON.stringify([ROLE_OLD]),
114
+ `CROSS-TENANT LEAK: Tenant B victim roles changed to ${JSON.stringify(victimAfter.roles)} `
115
+ + `after Tenant A renamed its role. Expected [${ROLE_OLD}].`,
116
+ 'F-0053: Tenant B user roles unaffected by other-tenant role rename',
117
+ )
118
+
119
+ // 7. POSITIVE CONTROL — the Tenant A control user SHOULD be renamed (same-tenant cascade intact).
120
+ const controlAfter = await sdk.api.users.getOne(controlUserId)
121
+ assert(
122
+ JSON.stringify(controlAfter.roles) === JSON.stringify([ROLE_NEW]),
123
+ `Same-tenant cascade broken: Tenant A control roles are ${JSON.stringify(controlAfter.roles)}, `
124
+ + `expected [${ROLE_NEW}].`,
125
+ 'F-0053: Tenant A user roles renamed by same-tenant cascade',
126
+ )
127
+ } finally {
128
+ // Cleanup: delete both throwaway users and the role doc. Never touches pre-existing users.
129
+ if (victimUserId) {
130
+ try { await sdkOther.api.users.deleteOne(victimUserId) } catch {}
131
+ }
132
+ if (controlUserId) {
133
+ try { await sdk.api.users.deleteOne(controlUserId) } catch {}
134
+ }
135
+ if (rbapId) {
136
+ try { await sdk.api.role_based_access_permissions.deleteOne(rbapId) } catch {}
137
+ }
138
+ }
139
+ }
140
+
141
+ // Allow running this test file independently
142
+ if (require.main === module) {
143
+ console.log(`🌐 Using API URL: ${host}`)
144
+ const sdk = new Session({ host })
145
+ const sdkNonAdmin = new Session({ host })
146
+
147
+ const runTests = async () => {
148
+ await setup_tests(sdk, sdkNonAdmin)
149
+ await cascade_role_rename_cross_tenant_tests({ sdk, sdkNonAdmin })
150
+ }
151
+
152
+ runTests()
153
+ .then(() => {
154
+ console.log("✅ F-0053 cascade role rename cross-tenant test suite completed successfully")
155
+ process.exit(0)
156
+ })
157
+ .catch((error) => {
158
+ console.error("❌ F-0053 cascade role rename cross-tenant test suite failed:", error)
159
+ process.exit(1)
160
+ })
161
+ }
@@ -0,0 +1,165 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session } from "../../../sdk"
4
+ import {
5
+ assert,
6
+ log_header,
7
+ wait,
8
+ } from "@tellescope/testing"
9
+ import { setup_tests } from "../../setup"
10
+
11
+ const host = process.env.API_URL || 'http://localhost:8080' as const
12
+
13
+ /**
14
+ * Self-role privilege-escalation guard (relates to security-audit finding F-0076, which was
15
+ * investigated and closed as a FALSE POSITIVE — see that file for the full code trace).
16
+ *
17
+ * F-0076 hypothesized that a non-admin staff user could `PATCH /v1/users/{their-own-id}` with
18
+ * `{ roles: ['Admin'] }` and self-promote to Admin, because the FIRST `users` relationship
19
+ * constraint ("Only admin users can set the admin role",
20
+ * [schema.ts:3446](packages/public/schema/src/schema.ts#L3446)) has a self-exception
21
+ * (`if (_id === session.id) return`).
22
+ *
23
+ * That analysis missed the SECOND constraint, "Only admin users can update user roles"
24
+ * ([schema.ts:3486](packages/public/schema/src/schema.ts#L3486)), which has NO self-exception.
25
+ * Relationship constraints are AND-evaluated — `validateRelationshipConstraints`
26
+ * ([routing.ts:1240-1252](packages/private/api/api/modules/routing.ts#L1240)) loops the whole
27
+ * array and throws 400 on the FIRST evaluator that returns a string. So a non-admin self-update
28
+ * that includes `roles` passes constraint #1 (self-exception) but is rejected by constraint #2.
29
+ * The self-promotion is blocked.
30
+ *
31
+ * This test locks that boundary in place so a future refactor of the role constraints can't
32
+ * silently reintroduce the escalation. A dedicated throwaway non-admin user is used as the
33
+ * "attacker" (we never mutate the shared sdkNonAdmin's roles):
34
+ * 1. Admin creates a throwaway user and assigns it a non-admin role (`['Provider']`).
35
+ * 2. Authenticate AS that user via a freshly-minted auth token.
36
+ * 3. As the attacker, attempt four self-role mutations — ['Admin'], ['Provider','Admin'],
37
+ * an arbitrary role, and [] — and assert EACH is blocked. <-- the security assertions
38
+ * 4. Confirm (as admin) the attacker's roles are still ['Provider'] — nothing slipped through.
39
+ * 5. Positive control: admin CAN update the throwaway user's roles. <-- guards against an
40
+ * over-restrictive regression that would block legitimate admin role management.
41
+ *
42
+ * Expected on current (correct) code: all assertions pass. A regression that made the self-update
43
+ * path role-writable by non-admins would flip the step-3/step-4 assertions to red.
44
+ */
45
+ export const self_admin_role_assignment_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
46
+ log_header("F-0076: self-admin role assignment privilege-escalation regression")
47
+
48
+ const stamp = Date.now()
49
+ const NON_ADMIN_ROLE = 'Provider'
50
+
51
+ // Assert that a self-role-update attempt is rejected by the relationship constraints.
52
+ const expect_blocked = async (fn: () => Promise<any>, description: string) => {
53
+ try {
54
+ await fn()
55
+ assert(false, `${description} - SELF-ROLE ESCALATION SUCCEEDED (expected it to be blocked)`)
56
+ } catch (e: any) {
57
+ // CRUD relationship-constraint failures surface as 400 { message, info } via SDK parseError.
58
+ assert(
59
+ e?.code === 400 || e?.statusCode === 400 || typeof e?.message === 'string',
60
+ `${description} - expected a block error, got: ${JSON.stringify(e)}`,
61
+ description,
62
+ )
63
+ }
64
+ }
65
+
66
+ let attackerId: string | undefined
67
+
68
+ try {
69
+ // 1. Admin creates a throwaway non-admin user (notification emails off, timestamped email).
70
+ // verifiedEmail is set at creation (admin-only, updatesDisabled after) so the attacker can
71
+ // drive an authenticated session — otherwise actions are blocked on email-verification, not
72
+ // on the role constraint we're actually testing.
73
+ const attacker = await sdk.api.users.createOne({
74
+ email: `f0076-attacker-${stamp}@example.com`,
75
+ notificationEmailsDisabled: true,
76
+ verifiedEmail: true,
77
+ } as any)
78
+ attackerId = attacker.id
79
+ await sdk.api.users.updateOne(attackerId, { roles: [NON_ADMIN_ROLE] }, { replaceObjectFields: true })
80
+ await wait(undefined, 2000) // role change triggers a logout; let it propagate before minting a token
81
+
82
+ // Setup sanity: the attacker holds exactly the non-admin role and is NOT an admin.
83
+ const attackerBefore = await sdk.api.users.getOne(attackerId)
84
+ assert(
85
+ JSON.stringify(attackerBefore.roles) === JSON.stringify([NON_ADMIN_ROLE]),
86
+ `Setup failed: expected attacker to hold [${NON_ADMIN_ROLE}], got ${JSON.stringify(attackerBefore.roles)}`,
87
+ 'F-0076 setup: attacker holds a non-admin role',
88
+ )
89
+
90
+ // 2. Authenticate AS the attacker (no password needed — admin mints an auth token).
91
+ const sdkAttacker = new Session({
92
+ host,
93
+ authToken: (await sdk.api.users.generate_auth_token({ id: attackerId })).authToken,
94
+ })
95
+ await sdkAttacker.refresh_session() // populate userInfo from the freshly-minted token
96
+ assert(
97
+ sdkAttacker.userInfo.id === attackerId && !(sdkAttacker.userInfo.roles ?? []).includes('Admin'),
98
+ `Setup failed: attacker session is not the expected non-admin user`,
99
+ 'F-0076 setup: authenticated as the non-admin attacker',
100
+ )
101
+
102
+ // 3. SECURITY ASSERTIONS — every self-role mutation by the non-admin must be blocked.
103
+ await expect_blocked(
104
+ () => sdkAttacker.api.users.updateOne(attackerId!, { roles: ['Admin'] }, { replaceObjectFields: true }),
105
+ 'F-0076: non-admin self-update to [Admin] is blocked',
106
+ )
107
+ await expect_blocked(
108
+ () => sdkAttacker.api.users.updateOne(attackerId!, { roles: [NON_ADMIN_ROLE, 'Admin'] }, { replaceObjectFields: true }),
109
+ 'F-0076: non-admin self-update to [Provider, Admin] is blocked',
110
+ )
111
+ await expect_blocked(
112
+ () => sdkAttacker.api.users.updateOne(attackerId!, { roles: [`Arbitrary_${stamp}`] }, { replaceObjectFields: true }),
113
+ 'F-0076: non-admin self-update to an arbitrary role is blocked',
114
+ )
115
+ await expect_blocked(
116
+ () => sdkAttacker.api.users.updateOne(attackerId!, { roles: [] }, { replaceObjectFields: true }),
117
+ 'F-0076: non-admin self-update to [] (would grant defaults) is blocked',
118
+ )
119
+
120
+ // 4. STATE ASSERTION — nothing slipped through; roles are still the original non-admin role.
121
+ const attackerAfter = await sdk.api.users.getOne(attackerId)
122
+ assert(
123
+ JSON.stringify(attackerAfter.roles) === JSON.stringify([NON_ADMIN_ROLE]),
124
+ `ESCALATION LEAK: attacker roles changed to ${JSON.stringify(attackerAfter.roles)} `
125
+ + `after self-update attempts. Expected [${NON_ADMIN_ROLE}].`,
126
+ 'F-0076: attacker roles unchanged after all self-escalation attempts',
127
+ )
128
+
129
+ // 5. POSITIVE CONTROL — an Admin CAN update the user's roles (mechanism is not over-restricted).
130
+ await sdk.api.users.updateOne(attackerId, { roles: [NON_ADMIN_ROLE] }, { replaceObjectFields: true })
131
+ const afterAdminUpdate = await sdk.api.users.getOne(attackerId)
132
+ assert(
133
+ JSON.stringify(afterAdminUpdate.roles) === JSON.stringify([NON_ADMIN_ROLE]),
134
+ `Admin role update failed: roles are ${JSON.stringify(afterAdminUpdate.roles)}, expected [${NON_ADMIN_ROLE}]`,
135
+ 'F-0076: admin can manage user roles (positive control)',
136
+ )
137
+ } finally {
138
+ // Cleanup: delete the throwaway attacker. Never touches pre-existing users.
139
+ if (attackerId) {
140
+ try { await sdk.api.users.deleteOne(attackerId) } catch {}
141
+ }
142
+ }
143
+ }
144
+
145
+ // Allow running this test file independently
146
+ if (require.main === module) {
147
+ console.log(`🌐 Using API URL: ${host}`)
148
+ const sdk = new Session({ host })
149
+ const sdkNonAdmin = new Session({ host })
150
+
151
+ const runTests = async () => {
152
+ await setup_tests(sdk, sdkNonAdmin)
153
+ await self_admin_role_assignment_tests({ sdk, sdkNonAdmin })
154
+ }
155
+
156
+ runTests()
157
+ .then(() => {
158
+ console.log("✅ F-0076 self-admin role assignment test suite completed successfully")
159
+ process.exit(0)
160
+ })
161
+ .catch((error) => {
162
+ console.error("❌ F-0076 self-admin role assignment test suite failed:", error)
163
+ process.exit(1)
164
+ })
165
+ }
@@ -0,0 +1,217 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session, EnduserSession } from "../../sdk"
4
+ import {
5
+ assert,
6
+ async_test,
7
+ handleAnyError,
8
+ log_header,
9
+ } from "@tellescope/testing"
10
+ import { setup_tests } from "../setup"
11
+
12
+ const host = process.env.API_URL || 'http://localhost:8080' as const
13
+ const businessId = '60398b1131a295e64f084ff6'
14
+
15
+ // Main test function that can be called independently
16
+ export const user_portal_settings_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
17
+ log_header("User portalSettings Tests")
18
+
19
+ // Operate on a throwaway user so we never mutate existing users' records.
20
+ const testUser = await sdk.api.users.createOne({
21
+ email: `portal_settings_test_${Date.now()}@test.tellescope.com`,
22
+ })
23
+
24
+ // throwaway enduser used to confirm enduser-visibility of portalSettings
25
+ let testEnduserId: string | undefined
26
+ let enduserSDK: EnduserSession | undefined
27
+
28
+ try {
29
+ // ===== Valid: string values =====
30
+ await async_test(
31
+ 'portalSettings - string values accepted',
32
+ async () => {
33
+ await sdk.api.users.updateOne(testUser.id, { portalSettings: { theme: 'dark' } }, { replaceObjectFields: true })
34
+ const updated = await sdk.api.users.getOne(testUser.id)
35
+ return updated.portalSettings?.theme
36
+ },
37
+ { onResult: (r) => r === 'dark' }
38
+ )
39
+
40
+ // ===== Valid: boolean values + round-trip as real booleans =====
41
+ await async_test(
42
+ 'portalSettings - boolean values accepted and round-trip as booleans',
43
+ async () => {
44
+ await sdk.api.users.updateOne(
45
+ testUser.id,
46
+ { portalSettings: { showNameInSecureMessaging: true, showAvatar: false } },
47
+ { replaceObjectFields: true }
48
+ )
49
+ const updated = await sdk.api.users.getOne(testUser.id)
50
+ return updated.portalSettings
51
+ },
52
+ {
53
+ onResult: (r) =>
54
+ r?.showNameInSecureMessaging === true &&
55
+ r?.showAvatar === false &&
56
+ // assert real booleans, not coerced strings
57
+ typeof r?.showNameInSecureMessaging === 'boolean' &&
58
+ typeof r?.showAvatar === 'boolean',
59
+ }
60
+ )
61
+
62
+ // ===== Valid: mixed string + boolean values, strings stay strings =====
63
+ await async_test(
64
+ 'portalSettings - mixed string and boolean values',
65
+ async () => {
66
+ await sdk.api.users.updateOne(
67
+ testUser.id,
68
+ { portalSettings: { theme: 'light', showAvatar: true } },
69
+ { replaceObjectFields: true }
70
+ )
71
+ const updated = await sdk.api.users.getOne(testUser.id)
72
+ return updated.portalSettings
73
+ },
74
+ {
75
+ onResult: (r) =>
76
+ r?.theme === 'light' &&
77
+ typeof r?.theme === 'string' &&
78
+ r?.showAvatar === true &&
79
+ typeof r?.showAvatar === 'boolean',
80
+ }
81
+ )
82
+
83
+ // ===== Valid: empty object (zero-iteration loop passes) =====
84
+ await async_test(
85
+ 'portalSettings - empty object accepted',
86
+ async () => {
87
+ await sdk.api.users.updateOne(testUser.id, { portalSettings: {} }, { replaceObjectFields: true })
88
+ const updated = await sdk.api.users.getOne(testUser.id)
89
+ return updated.portalSettings
90
+ },
91
+ { onResult: (r) => !!r && typeof r === 'object' && Object.keys(r).length === 0 }
92
+ )
93
+
94
+ // ===== Invalid: value string > 250 chars =====
95
+ await async_test(
96
+ 'portalSettings - value string > 250 chars rejected',
97
+ () => sdk.api.users.updateOne(
98
+ testUser.id,
99
+ { portalSettings: { tooLong: 'a'.repeat(251) } },
100
+ { replaceObjectFields: true }
101
+ ),
102
+ handleAnyError
103
+ )
104
+
105
+ // ===== Invalid: key > 250 chars =====
106
+ await async_test(
107
+ 'portalSettings - key > 250 chars rejected',
108
+ () => sdk.api.users.updateOne(
109
+ testUser.id,
110
+ { portalSettings: { ['a'.repeat(251)]: 'x' } },
111
+ { replaceObjectFields: true }
112
+ ),
113
+ handleAnyError
114
+ )
115
+
116
+ // ===== Invalid: nested object value (disallowed type) =====
117
+ await async_test(
118
+ 'portalSettings - nested object value rejected',
119
+ () => sdk.api.users.updateOne(
120
+ testUser.id,
121
+ { portalSettings: { k: { nested: 1 } as any } },
122
+ { replaceObjectFields: true }
123
+ ),
124
+ handleAnyError
125
+ )
126
+
127
+ // ===== Invalid: array value (disallowed type) =====
128
+ await async_test(
129
+ 'portalSettings - array value rejected',
130
+ () => sdk.api.users.updateOne(
131
+ testUser.id,
132
+ { portalSettings: { k: [1, 2] as any } },
133
+ { replaceObjectFields: true }
134
+ ),
135
+ handleAnyError
136
+ )
137
+
138
+ // ===== Number value (secondary): orValidator tries boolean then string;
139
+ // stringValidator250's escapeString throws on non-strings, so a number is
140
+ // rejected by both branches => API validation error. =====
141
+ await async_test(
142
+ 'portalSettings - number value rejected',
143
+ () => sdk.api.users.updateOne(
144
+ testUser.id,
145
+ { portalSettings: { k: 1 as any } },
146
+ { replaceObjectFields: true }
147
+ ),
148
+ handleAnyError
149
+ )
150
+
151
+ // ===== Enduser visibility: portalSettings readable by endusers, un-redacted =====
152
+ await async_test(
153
+ 'portalSettings - readable by enduser (un-redacted)',
154
+ async () => {
155
+ // set a known value on the throwaway user
156
+ await sdk.api.users.updateOne(
157
+ testUser.id,
158
+ { portalSettings: { showNameInSecureMessaging: true, theme: 'dark' } },
159
+ { replaceObjectFields: true }
160
+ )
161
+
162
+ // create + authenticate a throwaway enduser to read as a patient
163
+ const testEnduser = await sdk.api.endusers.createOne({
164
+ email: `portal_settings_enduser_${Date.now()}@test.tellescope.com`,
165
+ })
166
+ testEnduserId = testEnduser.id
167
+ await sdk.api.endusers.set_password({ id: testEnduser.id, password: 'TestPassword123!' })
168
+
169
+ enduserSDK = new EnduserSession({ host, businessId })
170
+ await enduserSDK.authenticate(testEnduser.email!, 'TestPassword123!')
171
+
172
+ const asEnduser = await enduserSDK.api.users.getOne(testUser.id)
173
+ return asEnduser.portalSettings
174
+ },
175
+ {
176
+ onResult: (r) =>
177
+ // field is present and un-redacted for endusers
178
+ r?.showNameInSecureMessaging === true && r?.theme === 'dark',
179
+ }
180
+ )
181
+
182
+ console.log("✅ All User portalSettings tests passed!")
183
+ } finally {
184
+ try {
185
+ if (enduserSDK) {
186
+ await enduserSDK.api.endusers.logout().catch(() => {})
187
+ }
188
+ if (testEnduserId) {
189
+ await sdk.api.endusers.deleteOne(testEnduserId)
190
+ }
191
+ } finally {
192
+ await sdk.api.users.deleteOne(testUser.id)
193
+ }
194
+ }
195
+ }
196
+
197
+ // Allow running this test file independently
198
+ if (require.main === module) {
199
+ console.log(`🌐 Using API URL: ${host}`)
200
+ const sdk = new Session({ host })
201
+ const sdkNonAdmin = new Session({ host })
202
+
203
+ const runTests = async () => {
204
+ await setup_tests(sdk, sdkNonAdmin)
205
+ await user_portal_settings_tests({ sdk, sdkNonAdmin })
206
+ }
207
+
208
+ runTests()
209
+ .then(() => {
210
+ console.log("✅ User portalSettings test suite completed successfully")
211
+ process.exit(0)
212
+ })
213
+ .catch((error) => {
214
+ console.error("❌ User portalSettings test suite failed:", error)
215
+ process.exit(1)
216
+ })
217
+ }
@@ -36,6 +36,7 @@ import {
36
36
 
37
37
  import { Session, APIQuery, EnduserSession } from "../sdk"
38
38
  import { enduser_observations_acknowledge_tests } from "./api_tests/enduser_observations_acknowledge.test"
39
+ import { user_portal_settings_tests } from "./api_tests/user_portal_settings.test"
39
40
  import { integrations_redacted_tests } from "./api_tests/integrations_redacted.test"
40
41
  import { get_some_projection_tests } from "./api_tests/get_some_projection.test"
41
42
  import { mdb_sort_tests } from "./api_tests/mdb_sort.test"
@@ -91,6 +92,8 @@ import { no_access_permission_checks_tests } from "./api_tests/no_access_permiss
91
92
  import { field_redaction_tests } from "./api_tests/field_redaction.test";
92
93
  import { data_sync_redaction_bypass_tests } from "./api_tests/security/F-0001-data-sync-redaction-bypass.test";
93
94
  import { ai_conversations_rbac_tests } from "./api_tests/security/F-0005-ai-conversations-rbac.test";
95
+ import { cascade_role_rename_cross_tenant_tests } from "./api_tests/security/F-0053-cascade-role-rename-cross-tenant.test";
96
+ import { self_admin_role_assignment_tests } from "./api_tests/security/F-0076-self-admin-role-assignment.test";
94
97
  import { invite_user_enumeration_tests } from "./api_tests/security/F-0007-invite-user-enumeration.test";
95
98
  import { handle_incoming_communication_cross_tenant_tests } from "./api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test";
96
99
  import { sanitize_user_html_xss_tests } from "./api_tests/security/F-0013-sanitize-user-html.test";
@@ -14336,6 +14339,8 @@ const ip_address_form_tests = async () => {
14336
14339
  await enduser_login_rate_limits_tests({ sdk, sdkNonAdmin })
14337
14340
  await data_sync_redaction_bypass_tests({ sdk, sdkNonAdmin })
14338
14341
  await ai_conversations_rbac_tests({ sdk, sdkNonAdmin })
14342
+ await cascade_role_rename_cross_tenant_tests({ sdk, sdkNonAdmin })
14343
+ await self_admin_role_assignment_tests({ sdk, sdkNonAdmin })
14339
14344
  await sanitize_user_html_xss_tests()
14340
14345
  await prototype_pollution_tests()
14341
14346
  await automation_trigger_tests()
@@ -14393,6 +14398,7 @@ const ip_address_form_tests = async () => {
14393
14398
  await inbox_threads_loading_tests()
14394
14399
  await load_inbox_data_tests({ sdk, sdkNonAdmin })
14395
14400
  await enduser_observations_acknowledge_tests({ sdk, sdkNonAdmin })
14401
+ await user_portal_settings_tests({ sdk, sdkNonAdmin })
14396
14402
  await create_user_notifications_trigger_tests({ sdk })
14397
14403
  await group_mms_active_tests()
14398
14404
  await auto_reply_tests()
Binary file