@tellescope/sdk 1.250.2 → 1.252.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/lib/cjs/sdk.d.ts +9 -0
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +3 -0
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/tests/api_tests/account_switcher.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/account_switcher.test.js +1700 -306
- package/lib/cjs/tests/api_tests/account_switcher.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.js +337 -0
- package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_login.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/enduser_login.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_login.test.js +315 -0
- package/lib/cjs/tests/api_tests/enduser_login.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.js +287 -0
- package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.js +406 -0
- package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts +28 -0
- package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js +349 -0
- package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts +28 -0
- package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js +247 -0
- package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts +29 -0
- package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.js +278 -0
- package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts +24 -0
- package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js +201 -0
- package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts +2 -0
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.js +148 -0
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts +2 -0
- package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.js +88 -0
- package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js +373 -0
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js.map +1 -0
- package/lib/cjs/tests/setup.d.ts.map +1 -1
- package/lib/cjs/tests/setup.js +47 -32
- package/lib/cjs/tests/setup.js.map +1 -1
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +215 -159
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/sdk.d.ts +9 -0
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +3 -0
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/tests/api_tests/account_switcher.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/account_switcher.test.js +1702 -305
- package/lib/esm/tests/api_tests/account_switcher.test.js.map +1 -1
- package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.js +333 -0
- package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.js.map +1 -0
- package/lib/esm/tests/api_tests/enduser_login.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/enduser_login.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/enduser_login.test.js +308 -0
- package/lib/esm/tests/api_tests/enduser_login.test.js.map +1 -0
- package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.js +268 -0
- package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.js.map +1 -0
- package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.js +280 -0
- package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.js.map +1 -0
- package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.js +402 -0
- package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts +28 -0
- package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js +345 -0
- package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts +28 -0
- package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js +243 -0
- package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts +29 -0
- package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.js +271 -0
- package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts +24 -0
- package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js +194 -0
- package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.js +144 -0
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.js +84 -0
- package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.js.map +1 -0
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.js +369 -0
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.js.map +1 -0
- package/lib/esm/tests/setup.d.ts.map +1 -1
- package/lib/esm/tests/setup.js +47 -32
- package/lib/esm/tests/setup.js.map +1 -1
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +215 -159
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/sdk.ts +12 -0
- package/src/tests/api_tests/account_switcher.test.ts +1283 -0
- package/src/tests/api_tests/calendar_event_webhook_template.test.ts +204 -0
- package/src/tests/api_tests/enduser_login.test.ts +215 -0
- package/src/tests/api_tests/enduser_login_rate_limits.test.ts +178 -0
- package/src/tests/api_tests/push_forms_to_portal_group_completion.test.ts +223 -0
- package/src/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.ts +236 -0
- package/src/tests/api_tests/security/F-0005-ai-conversations-rbac.test.ts +154 -0
- package/src/tests/api_tests/security/F-0007-invite-user-enumeration.test.ts +198 -0
- package/src/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.ts +130 -0
- package/src/tests/api_tests/security/F-0013-sanitize-user-html.test.ts +109 -0
- package/src/tests/api_tests/security/F-0016-prototype-pollution.test.ts +50 -0
- package/src/tests/api_tests/set_fields_order_templates.test.ts +258 -0
- package/src/tests/setup.ts +8 -1
- package/src/tests/tests.ts +35 -5
- package/test_generated.pdf +0 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session, EnduserSession } from "../../sdk"
|
|
4
|
+
import { log_header, wait, async_test } from "@tellescope/testing"
|
|
5
|
+
import { Enduser } from "@tellescope/types-client"
|
|
6
|
+
import { setup_tests } from "../setup"
|
|
7
|
+
|
|
8
|
+
const host = process.env.API_URL || "http://localhost:8080"
|
|
9
|
+
|
|
10
|
+
const pollFor = async <T>(
|
|
11
|
+
fetchFn: () => Promise<T | undefined>,
|
|
12
|
+
evaluateFn: (result: T | undefined) => result is T,
|
|
13
|
+
description: string,
|
|
14
|
+
intervalMs = 500,
|
|
15
|
+
maxIterations = 30,
|
|
16
|
+
): Promise<T> => {
|
|
17
|
+
let lastResult: T | undefined
|
|
18
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
19
|
+
await wait(undefined, intervalMs)
|
|
20
|
+
lastResult = await fetchFn()
|
|
21
|
+
if (evaluateFn(lastResult)) return lastResult
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`Polling timeout: ${description} - waited ${maxIterations * intervalMs}ms`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const push_forms_to_portal_group_completion_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
27
|
+
log_header("Push Forms To Portal - Form Group Completed Trigger Tests")
|
|
28
|
+
|
|
29
|
+
const createdEnduserIds: string[] = []
|
|
30
|
+
const createdJourneyIds: string[] = []
|
|
31
|
+
const createdFormIds: string[] = []
|
|
32
|
+
const createdFormGroupIds: string[] = []
|
|
33
|
+
const createdTriggerIds: string[] = []
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// 1. Create two forms, each with a single text field
|
|
37
|
+
const formA = await sdk.api.forms.createOne({ title: 'Push To Portal Form A' })
|
|
38
|
+
createdFormIds.push(formA.id)
|
|
39
|
+
const fieldA = await sdk.api.form_fields.createOne({
|
|
40
|
+
formId: formA.id,
|
|
41
|
+
type: 'string',
|
|
42
|
+
title: 'FieldA',
|
|
43
|
+
previousFields: [{ type: 'root', info: {} }],
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const formB = await sdk.api.forms.createOne({ title: 'Push To Portal Form B' })
|
|
47
|
+
createdFormIds.push(formB.id)
|
|
48
|
+
const fieldB = await sdk.api.form_fields.createOne({
|
|
49
|
+
formId: formB.id,
|
|
50
|
+
type: 'string',
|
|
51
|
+
title: 'FieldB',
|
|
52
|
+
previousFields: [{ type: 'root', info: {} }],
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// 2. Create a form group containing both forms (shared across both submission flows)
|
|
56
|
+
const formGroup = await sdk.api.form_groups.createOne({
|
|
57
|
+
title: 'Push To Portal Test Group',
|
|
58
|
+
formIds: [formA.id, formB.id],
|
|
59
|
+
})
|
|
60
|
+
createdFormGroupIds.push(formGroup.id)
|
|
61
|
+
|
|
62
|
+
// Helper: run the full push-to-portal → submit → trigger flow with a configurable submitter.
|
|
63
|
+
// Each invocation creates its own trigger/journey/step/enduser and asserts its own tag.
|
|
64
|
+
// We test both the admin (user-session) submitter and the enduser (portal-session) submitter
|
|
65
|
+
// because they exercise different DB scopes in submit_form_response — and the regression QA caught
|
|
66
|
+
// was only triggered on the enduser-session path.
|
|
67
|
+
const runFlow = async ({ label, tag, submitAsEnduser } : { label: string, tag: string, submitAsEnduser: boolean }) => {
|
|
68
|
+
const trigger = await sdk.api.automation_triggers.createOne({
|
|
69
|
+
event: { type: 'Form Group Completed', info: { groupId: formGroup.id } },
|
|
70
|
+
action: { type: 'Add Tags', info: { tags: [tag] } },
|
|
71
|
+
status: 'Active',
|
|
72
|
+
title: `Form Group Completed - Push to Portal (${label})`,
|
|
73
|
+
})
|
|
74
|
+
createdTriggerIds.push(trigger.id)
|
|
75
|
+
|
|
76
|
+
const journey = await sdk.api.journeys.createOne({
|
|
77
|
+
title: `Push To Portal Trigger Journey (${label})`,
|
|
78
|
+
})
|
|
79
|
+
createdJourneyIds.push(journey.id)
|
|
80
|
+
|
|
81
|
+
const pushStep = await sdk.api.automation_steps.createOne({
|
|
82
|
+
journeyId: journey.id,
|
|
83
|
+
action: { type: 'pushFormsToPortal', info: { formGroupIds: [formGroup.id] } },
|
|
84
|
+
events: [{ type: 'onJourneyStart', info: {} }],
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const enduser = await sdk.api.endusers.createOne({ fname: 'PushPortal', lname: label })
|
|
88
|
+
createdEnduserIds.push(enduser.id)
|
|
89
|
+
|
|
90
|
+
await sdk.api.endusers.add_to_journey({
|
|
91
|
+
enduserIds: [enduser.id],
|
|
92
|
+
journeyId: journey.id,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const pushedResponses = await pollFor(
|
|
96
|
+
async () => {
|
|
97
|
+
const responses = await sdk.api.form_responses.getSome({
|
|
98
|
+
filter: { enduserId: enduser.id },
|
|
99
|
+
})
|
|
100
|
+
const pushed = responses.filter(r => !!r.pushedToPortalAt)
|
|
101
|
+
return pushed.length >= 2 ? pushed : undefined
|
|
102
|
+
},
|
|
103
|
+
(result): result is any[] => Array.isArray(result) && result.length >= 2,
|
|
104
|
+
`pushed-to-portal form_responses to be created by worker (${label})`,
|
|
105
|
+
500,
|
|
106
|
+
40,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
for (const fr of pushedResponses) {
|
|
110
|
+
if (!fr.pushedToPortalAt) {
|
|
111
|
+
throw new Error(`Expected pushedToPortalAt to be set on form_response ${fr.id} (${label})`)
|
|
112
|
+
}
|
|
113
|
+
if (fr.groupId !== pushStep.id) {
|
|
114
|
+
throw new Error(`Expected form_response.groupId (${fr.groupId}) to equal automation step id (${pushStep.id}) (${label})`)
|
|
115
|
+
}
|
|
116
|
+
if (fr.automationStepId !== pushStep.id) {
|
|
117
|
+
throw new Error(`Expected form_response.automationStepId (${fr.automationStepId}) to equal automation step id (${pushStep.id}) (${label})`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await async_test(
|
|
122
|
+
`Worker writes groupId === automationStepId and pushedToPortalAt set (${label})`,
|
|
123
|
+
async () => true,
|
|
124
|
+
{ onResult: r => r === true },
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
// Build the submitter session
|
|
128
|
+
let submitterApi: typeof sdk.api | EnduserSession['api']
|
|
129
|
+
if (submitAsEnduser) {
|
|
130
|
+
const { authToken } = await sdk.api.endusers.generate_auth_token({ id: enduser.id })
|
|
131
|
+
const enduserSDK = new EnduserSession({ host, authToken, businessId: sdk.userInfo.businessId })
|
|
132
|
+
submitterApi = enduserSDK.api
|
|
133
|
+
} else {
|
|
134
|
+
submitterApi = sdk.api
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const fr of pushedResponses) {
|
|
138
|
+
const isFormA = fr.formId === formA.id
|
|
139
|
+
const targetFieldId = isFormA ? fieldA.id : fieldB.id
|
|
140
|
+
const targetFieldTitle = isFormA ? 'FieldA' : 'FieldB'
|
|
141
|
+
await submitterApi.form_responses.submit_form_response({
|
|
142
|
+
accessCode: fr.accessCode as string,
|
|
143
|
+
responses: [{
|
|
144
|
+
fieldId: targetFieldId,
|
|
145
|
+
fieldTitle: targetFieldTitle,
|
|
146
|
+
answer: { type: 'string', value: 'pushed-portal-answer' },
|
|
147
|
+
}],
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await pollFor(
|
|
152
|
+
async () => {
|
|
153
|
+
const e = await sdk.api.endusers.getOne(enduser.id)
|
|
154
|
+
return e.tags?.includes(tag) ? e : undefined
|
|
155
|
+
},
|
|
156
|
+
(result): result is Enduser => !!result,
|
|
157
|
+
`Form Group Completed trigger to apply tag after push-to-portal submissions (${label})`,
|
|
158
|
+
500,
|
|
159
|
+
30,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
await async_test(
|
|
163
|
+
`Form Group Completed trigger fires for push-to-portal completion (${label})`,
|
|
164
|
+
() => sdk.api.endusers.getOne(enduser.id),
|
|
165
|
+
{ onResult: (e: Enduser) => !!e.tags?.includes(tag) },
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Admin submitter: simulates a staff user filling in the form on behalf of the patient
|
|
170
|
+
// (uses a user-scoped DB in submit_form_response).
|
|
171
|
+
await runFlow({
|
|
172
|
+
label: 'admin-submit',
|
|
173
|
+
tag: 'form-group-completed-push-admin',
|
|
174
|
+
submitAsEnduser: false,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Enduser submitter: simulates the patient submitting via the portal
|
|
178
|
+
// (uses an enduser-scoped DB in submit_form_response — exercises the path QA caught).
|
|
179
|
+
await runFlow({
|
|
180
|
+
label: 'enduser-submit',
|
|
181
|
+
tag: 'form-group-completed-push-enduser',
|
|
182
|
+
submitAsEnduser: true,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
} finally {
|
|
186
|
+
for (const id of createdTriggerIds) {
|
|
187
|
+
try { await sdk.api.automation_triggers.deleteOne(id) } catch (e) { /* ignore */ }
|
|
188
|
+
}
|
|
189
|
+
for (const id of createdEnduserIds) {
|
|
190
|
+
try { await sdk.api.endusers.deleteOne(id) } catch (e) { /* ignore */ }
|
|
191
|
+
}
|
|
192
|
+
for (const id of createdJourneyIds) {
|
|
193
|
+
try { await sdk.api.journeys.deleteOne(id) } catch (e) { /* ignore */ }
|
|
194
|
+
}
|
|
195
|
+
for (const id of createdFormGroupIds) {
|
|
196
|
+
try { await sdk.api.form_groups.deleteOne(id) } catch (e) { /* ignore */ }
|
|
197
|
+
}
|
|
198
|
+
for (const id of createdFormIds) {
|
|
199
|
+
try { await sdk.api.forms.deleteOne(id) } catch (e) { /* ignore */ }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (require.main === module) {
|
|
205
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
206
|
+
const sdk = new Session({ host })
|
|
207
|
+
const sdkNonAdmin = new Session({ host })
|
|
208
|
+
|
|
209
|
+
const runTests = async () => {
|
|
210
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
211
|
+
await push_forms_to_portal_group_completion_tests({ sdk, sdkNonAdmin })
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
runTests()
|
|
215
|
+
.then(() => {
|
|
216
|
+
console.log("✅ Push forms to portal group completion test suite completed successfully")
|
|
217
|
+
process.exit(0)
|
|
218
|
+
})
|
|
219
|
+
.catch((error) => {
|
|
220
|
+
console.error("❌ Push forms to portal group completion test suite failed:", error)
|
|
221
|
+
process.exit(1)
|
|
222
|
+
})
|
|
223
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session } from "../../../sdk"
|
|
4
|
+
import {
|
|
5
|
+
async_test,
|
|
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
|
+
const [nonAdminEmail, nonAdminPassword] = [process.env.NON_ADMIN_EMAIL, process.env.NON_ADMIN_PASSWORD]
|
|
14
|
+
|
|
15
|
+
const FULL_ACCESS = { create: 'All' as const, read: 'All' as const, update: 'All' as const, delete: 'All' as const }
|
|
16
|
+
|
|
17
|
+
// Schema fields tagged `redactions: ['all']` that must never appear in
|
|
18
|
+
// `/v1/data-sync` results. See packages/public/schema/src/schema.ts.
|
|
19
|
+
const REDACTABLE_FIELDS_BY_MODEL: Record<string, string[]> = {
|
|
20
|
+
users: ['hashedPass', 'hashedInviteCode'],
|
|
21
|
+
endusers: ['hashedPassword'],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type Violation = { modelName: string, recordId: string, leakedField: string }
|
|
25
|
+
|
|
26
|
+
const collectViolations = (results: { modelName: string, recordId: string, data: string }[]): Violation[] => {
|
|
27
|
+
const violations: Violation[] = []
|
|
28
|
+
for (const record of results) {
|
|
29
|
+
if (!record.data || record.data === 'deleted') continue
|
|
30
|
+
const fields = REDACTABLE_FIELDS_BY_MODEL[record.modelName]
|
|
31
|
+
if (!fields) continue
|
|
32
|
+
let parsed: any
|
|
33
|
+
try { parsed = JSON.parse(record.data) } catch { continue }
|
|
34
|
+
for (const f of fields) {
|
|
35
|
+
if (f in parsed && parsed[f] !== undefined && parsed[f] !== null && parsed[f] !== '') {
|
|
36
|
+
violations.push({ modelName: record.modelName, recordId: record.recordId, leakedField: f })
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return violations
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Regression test for F-0001 (security-audit/findings/F-0001-data-sync-bypasses-applyRedactions.md).
|
|
45
|
+
*
|
|
46
|
+
* The /v1/data-sync handler must apply the central applyRedactions() pipeline to
|
|
47
|
+
* every non-deleted record. The original bug: redactions were gated behind
|
|
48
|
+
* `if (session.fieldRedactions && session.fieldRedactions[record.modelName])`
|
|
49
|
+
* which meant any session without role-based field redactions (including all
|
|
50
|
+
* admins) received raw records — leaking schema-level `redactions: ['all']`
|
|
51
|
+
* fields (hashedPass, hashedPassword, hashedInviteCode).
|
|
52
|
+
*
|
|
53
|
+
* This test:
|
|
54
|
+
* 1. Configures a non-admin user with broad read access on users + endusers
|
|
55
|
+
* and NO fieldRedactions — the realistic "regular user with read access"
|
|
56
|
+
* condition that triggers the bypass.
|
|
57
|
+
* 2. Creates an enduser with a password to populate the sync stream.
|
|
58
|
+
* 3. Calls /v1/data-sync as the non-admin.
|
|
59
|
+
* 4. Asserts no returned record contains hashedPass / hashedPassword /
|
|
60
|
+
* hashedInviteCode.
|
|
61
|
+
*
|
|
62
|
+
* Pre-fix: assertion fails with leaked records.
|
|
63
|
+
* Post-fix: assertion passes.
|
|
64
|
+
*/
|
|
65
|
+
export const data_sync_redaction_bypass_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
66
|
+
log_header("F-0001: /v1/data-sync field-redaction bypass regression")
|
|
67
|
+
|
|
68
|
+
const roleName = `f0001-data-sync-bypass-${Date.now()}`
|
|
69
|
+
let testEnduserId: string | undefined
|
|
70
|
+
let rbapId: string | undefined
|
|
71
|
+
const originalRoles = sdkNonAdmin.userInfo.roles
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// 1. Create a role with broad read access but NO fieldRedactions.
|
|
75
|
+
// This is the realistic "regular user with read access" condition
|
|
76
|
+
// that triggers the bypass in the pre-fix handler.
|
|
77
|
+
const rbap = await sdk.api.role_based_access_permissions.createOne({
|
|
78
|
+
role: roleName,
|
|
79
|
+
permissions: {
|
|
80
|
+
...PROVIDER_PERMISSIONS,
|
|
81
|
+
users: FULL_ACCESS,
|
|
82
|
+
endusers: FULL_ACCESS,
|
|
83
|
+
},
|
|
84
|
+
// intentionally NO fieldRedactions — this is the exploit condition.
|
|
85
|
+
})
|
|
86
|
+
rbapId = rbap.id
|
|
87
|
+
|
|
88
|
+
// 2. Assign role to the non-admin user and re-authenticate so the new
|
|
89
|
+
// session reflects the role's permissions.
|
|
90
|
+
await sdk.api.users.updateOne(
|
|
91
|
+
sdkNonAdmin.userInfo.id,
|
|
92
|
+
{ roles: [roleName] },
|
|
93
|
+
{ replaceObjectFields: true },
|
|
94
|
+
)
|
|
95
|
+
await wait(undefined, 1500)
|
|
96
|
+
await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!)
|
|
97
|
+
|
|
98
|
+
// 3. Create a test enduser and set a password — this populates
|
|
99
|
+
// `hashedPassword` on the enduser record and writes a data_sync_records
|
|
100
|
+
// row for it.
|
|
101
|
+
const testEnduser = await sdk.api.endusers.createOne({
|
|
102
|
+
fname: 'F0001Target',
|
|
103
|
+
lname: 'Patient',
|
|
104
|
+
email: `f0001-target-${Date.now()}@example.com`,
|
|
105
|
+
})
|
|
106
|
+
testEnduserId = testEnduser.id
|
|
107
|
+
await sdk.api.endusers.set_password({ id: testEnduser.id, password: 'F0001TestPassword!123' })
|
|
108
|
+
|
|
109
|
+
// The non-admin user's own `hashedPass` is set from login and refreshed
|
|
110
|
+
// on every write to their user record (e.g., the role-assignment update
|
|
111
|
+
// above). No extra setup needed for users.hashedPass to be in the stream.
|
|
112
|
+
|
|
113
|
+
await wait(undefined, 500)
|
|
114
|
+
|
|
115
|
+
// 4. As the non-admin, call /v1/data-sync from epoch zero to capture all
|
|
116
|
+
// sync records the role can see.
|
|
117
|
+
const sync = await sdkNonAdmin.sync({ from: new Date(0) })
|
|
118
|
+
|
|
119
|
+
// 5. Walk every record. Fail if any contains a `redactions: ['all']` field.
|
|
120
|
+
const violations = collectViolations(sync.results)
|
|
121
|
+
|
|
122
|
+
// Belt-and-suspenders: at least one user record SHOULD be in the stream
|
|
123
|
+
// (the non-admin's own record). If the stream is empty, the assertion below
|
|
124
|
+
// would pass vacuously — guard against that.
|
|
125
|
+
const userRecordsInStream = sync.results.filter(r => r.modelName === 'users').length
|
|
126
|
+
await async_test(
|
|
127
|
+
"F-0001 guard: /v1/data-sync sync stream contains at least one user record",
|
|
128
|
+
async () => ({ userRecords: userRecordsInStream, totalRecords: sync.results.length }),
|
|
129
|
+
{ onResult: r => r.userRecords >= 1 },
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
await async_test(
|
|
133
|
+
"F-0001: /v1/data-sync must NOT return hashedPass / hashedPassword / hashedInviteCode (see security-audit/findings/F-0001)",
|
|
134
|
+
async () => ({
|
|
135
|
+
violationCount: violations.length,
|
|
136
|
+
violations: violations.slice(0, 10),
|
|
137
|
+
affectedModels: Array.from(new Set(violations.map(v => v.modelName))),
|
|
138
|
+
affectedFields: Array.from(new Set(violations.map(v => v.leakedField))),
|
|
139
|
+
}),
|
|
140
|
+
{ onResult: r => r.violationCount === 0 },
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
// ========================================================================
|
|
144
|
+
// Additional coverage for applyRedactions code paths reachable via /v1/data-sync.
|
|
145
|
+
// Each of these is a distinct branch in applyRedactions (routing.ts:1165-1238)
|
|
146
|
+
// and could regress independently of the F-0001 fix.
|
|
147
|
+
// ========================================================================
|
|
148
|
+
|
|
149
|
+
// Case A: schema-level `redactions: ['all']` must apply to ADMIN sessions too.
|
|
150
|
+
// Admins have no session.fieldRedactions, but `redactions: ['all']` is universal.
|
|
151
|
+
// Pre-fix: admin saw hashedPass via data-sync because applyRedactions was skipped entirely.
|
|
152
|
+
// Post-fix: applyRedactions always runs and `redactions: ['all']` strips for everyone.
|
|
153
|
+
const adminSync = await sdk.sync({ from: new Date(0) })
|
|
154
|
+
const adminViolations = collectViolations(adminSync.results)
|
|
155
|
+
await async_test(
|
|
156
|
+
"F-0001 coverage A: admin /v1/data-sync also strips redactions:['all'] fields (hashedPass etc.)",
|
|
157
|
+
async () => ({
|
|
158
|
+
violationCount: adminViolations.length,
|
|
159
|
+
violations: adminViolations.slice(0, 10),
|
|
160
|
+
}),
|
|
161
|
+
{ onResult: r => r.violationCount === 0 },
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
// Case B: `linkedAccountAccess` on users must be stripped when the caller is NOT
|
|
165
|
+
// the record's owner. This is a separate branch in applyRedactions (routing.ts:1220-1225)
|
|
166
|
+
// and protects against cross-user enumeration of who-requested-access-to-whom.
|
|
167
|
+
// The non-admin user reads other user records via data-sync; if any of those
|
|
168
|
+
// records have linkedAccountAccess set, it must be stripped on read.
|
|
169
|
+
const otherUsersInStream = sync.results.filter(
|
|
170
|
+
r => r.modelName === 'users' && r.recordId !== sdkNonAdmin.userInfo.id
|
|
171
|
+
)
|
|
172
|
+
const linkedAccountLeaks: { recordId: string }[] = []
|
|
173
|
+
for (const record of otherUsersInStream) {
|
|
174
|
+
if (!record.data || record.data === 'deleted') continue
|
|
175
|
+
try {
|
|
176
|
+
const parsed = JSON.parse(record.data)
|
|
177
|
+
if ('linkedAccountAccess' in parsed && parsed.linkedAccountAccess !== undefined) {
|
|
178
|
+
linkedAccountLeaks.push({ recordId: record.recordId })
|
|
179
|
+
}
|
|
180
|
+
} catch {}
|
|
181
|
+
}
|
|
182
|
+
await async_test(
|
|
183
|
+
"F-0001 coverage B: /v1/data-sync strips linkedAccountAccess from other users' records",
|
|
184
|
+
async () => ({
|
|
185
|
+
otherUserRecords: otherUsersInStream.length,
|
|
186
|
+
leakCount: linkedAccountLeaks.length,
|
|
187
|
+
leaks: linkedAccountLeaks.slice(0, 5),
|
|
188
|
+
}),
|
|
189
|
+
{ onResult: r => r.leakCount === 0 },
|
|
190
|
+
)
|
|
191
|
+
} finally {
|
|
192
|
+
// Cleanup: restore non-admin's original roles, delete role and test enduser.
|
|
193
|
+
try {
|
|
194
|
+
await sdk.api.users.updateOne(
|
|
195
|
+
sdkNonAdmin.userInfo.id,
|
|
196
|
+
{ roles: originalRoles ?? [] },
|
|
197
|
+
{ replaceObjectFields: true },
|
|
198
|
+
)
|
|
199
|
+
} catch {}
|
|
200
|
+
if (rbapId) {
|
|
201
|
+
try { await sdk.api.role_based_access_permissions.deleteOne(rbapId) } catch {}
|
|
202
|
+
}
|
|
203
|
+
if (testEnduserId) {
|
|
204
|
+
try { await sdk.api.endusers.deleteOne(testEnduserId) } catch {}
|
|
205
|
+
}
|
|
206
|
+
// Re-authenticate the non-admin to drop the exploit role from their JWT
|
|
207
|
+
// before subsequent tests run.
|
|
208
|
+
// Role restore above re-triggers deauthenticate_user; wait > 1s so the freshly minted
|
|
209
|
+
// token's (second-floored) iat lands after the deauth timestamp and isn't permanently
|
|
210
|
+
// rejected by is_logged_in's iat-slack check. Matches the in-test re-auth above.
|
|
211
|
+
await wait(undefined, 1500)
|
|
212
|
+
try { await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!) } catch {}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Allow running this test file independently
|
|
217
|
+
if (require.main === module) {
|
|
218
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
219
|
+
const sdk = new Session({ host })
|
|
220
|
+
const sdkNonAdmin = new Session({ host })
|
|
221
|
+
|
|
222
|
+
const runTests = async () => {
|
|
223
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
224
|
+
await data_sync_redaction_bypass_tests({ sdk, sdkNonAdmin })
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
runTests()
|
|
228
|
+
.then(() => {
|
|
229
|
+
console.log("✅ F-0001 data-sync redaction-bypass test suite completed successfully")
|
|
230
|
+
process.exit(0)
|
|
231
|
+
})
|
|
232
|
+
.catch((error) => {
|
|
233
|
+
console.error("❌ F-0001 data-sync redaction-bypass test suite failed:", error)
|
|
234
|
+
process.exit(1)
|
|
235
|
+
})
|
|
236
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { ObjectId } from 'bson'
|
|
4
|
+
import { Session } from "../../../sdk"
|
|
5
|
+
import {
|
|
6
|
+
async_test,
|
|
7
|
+
log_header,
|
|
8
|
+
wait,
|
|
9
|
+
} from "@tellescope/testing"
|
|
10
|
+
import { setup_tests } from "../../setup"
|
|
11
|
+
import { PROVIDER_PERMISSIONS } from "@tellescope/constants"
|
|
12
|
+
|
|
13
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
14
|
+
const [nonAdminEmail, nonAdminPassword] = [process.env.NON_ADMIN_EMAIL, process.env.NON_ADMIN_PASSWORD]
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Regression test for F-0005 (security-audit/findings/F-0005-ai-conversations-bypass-rbac.md).
|
|
18
|
+
*
|
|
19
|
+
* Both `ai_conversations.send_message` and `ai_conversations.generate_ai_decision` are
|
|
20
|
+
* registered with `noAccessPermissions: true` ([api.ts:22699, 22721](packages/private/api/api/v1/api.ts)).
|
|
21
|
+
* Their only access gate is `if (req.session.type === 'enduser') throw 403`. They must ALSO
|
|
22
|
+
* check `session.access?.ai_conversations?.<action>` (and `session.access?.endusers?.read`
|
|
23
|
+
* for generate_ai_decision) — the standard pattern used 16 lines earlier in the same file
|
|
24
|
+
* at api.ts:22680 for the background_errors handler.
|
|
25
|
+
*
|
|
26
|
+
* This test:
|
|
27
|
+
* 1. Creates a role with explicit NO_ACCESS for ai_conversations (and endusers).
|
|
28
|
+
* 2. Assigns the role to the non-admin user.
|
|
29
|
+
* 3. Calls each endpoint as the non-admin.
|
|
30
|
+
* 4. Asserts each endpoint returns a 403-equivalent error (not 200).
|
|
31
|
+
*
|
|
32
|
+
* Pre-fix:
|
|
33
|
+
* - send_message: 200 (or some downstream error from Bedrock) — NOT 403. Test fails.
|
|
34
|
+
* - generate_ai_decision: 200 with `{}` (handler responds early before access check). Test fails.
|
|
35
|
+
*
|
|
36
|
+
* Post-fix: both endpoints throw 403 before any work happens. Test passes.
|
|
37
|
+
*/
|
|
38
|
+
export const ai_conversations_rbac_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
39
|
+
log_header("F-0005: ai_conversations RBAC bypass regression")
|
|
40
|
+
|
|
41
|
+
const roleName = `f0005-ai-conversations-no-access-${Date.now()}`
|
|
42
|
+
let rbapId: string | undefined
|
|
43
|
+
const originalRoles = sdkNonAdmin.userInfo.roles
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// 1. Create a role that explicitly denies ai_conversations access AND endusers access.
|
|
47
|
+
// This is the realistic configuration the bypass affects: a tenant operator
|
|
48
|
+
// deliberately restricts a role from reading AI conversations / enduser PHI.
|
|
49
|
+
const rbap = await sdk.api.role_based_access_permissions.createOne({
|
|
50
|
+
role: roleName,
|
|
51
|
+
permissions: {
|
|
52
|
+
...PROVIDER_PERMISSIONS,
|
|
53
|
+
ai_conversations: { create: null, read: null, update: null, delete: null },
|
|
54
|
+
endusers: { create: null, read: null, update: null, delete: null },
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
rbapId = rbap.id
|
|
58
|
+
|
|
59
|
+
// 2. Assign the role to the non-admin user and re-authenticate so the new
|
|
60
|
+
// session reflects the role's denied permissions.
|
|
61
|
+
await sdk.api.users.updateOne(
|
|
62
|
+
sdkNonAdmin.userInfo.id,
|
|
63
|
+
{ roles: [roleName] },
|
|
64
|
+
{ replaceObjectFields: true },
|
|
65
|
+
)
|
|
66
|
+
await wait(undefined, 1500)
|
|
67
|
+
await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!)
|
|
68
|
+
|
|
69
|
+
// 3a. send_message must throw 403 (or equivalent access error). Pre-fix: succeeds (or
|
|
70
|
+
// fails downstream in Bedrock without 403). Post-fix: throws before any work.
|
|
71
|
+
await async_test(
|
|
72
|
+
"F-0005: ai_conversations.send_message must throw 403 when role denies ai_conversations",
|
|
73
|
+
() => sdkNonAdmin.api.ai_conversations.send_message({
|
|
74
|
+
message: 'F-0005 regression test',
|
|
75
|
+
type: 'Test',
|
|
76
|
+
} as any),
|
|
77
|
+
{
|
|
78
|
+
shouldError: true,
|
|
79
|
+
onError: (e: any) => {
|
|
80
|
+
// Accept any 4xx access-denial response — handler may use 403 (recommended)
|
|
81
|
+
// or 400 with "access" / "permission" in the message.
|
|
82
|
+
const msg = (e?.message ?? '').toLowerCase()
|
|
83
|
+
const status = e?.status ?? e?.code
|
|
84
|
+
return status === 403 || status === 401
|
|
85
|
+
|| msg.includes('access') || msg.includes('permission') || msg.includes('forbidden')
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
// 3b. generate_ai_decision must throw 403 BEFORE the early res.json({}) response.
|
|
91
|
+
// Pre-fix: handler responds 200 with {} immediately and processes in background.
|
|
92
|
+
// Post-fix: handler throws 403 before res.json({}).
|
|
93
|
+
await async_test(
|
|
94
|
+
"F-0005: ai_conversations.generate_ai_decision must throw 403 when role denies endusers/ai_conversations",
|
|
95
|
+
() => sdkNonAdmin.api.ai_conversations.generate_ai_decision({
|
|
96
|
+
enduserId: new ObjectId().toHexString(), // fake id — access check should fire first
|
|
97
|
+
automationStepId: new ObjectId().toHexString(), // fake id — access check should fire first
|
|
98
|
+
outcomes: ['yes', 'no'],
|
|
99
|
+
prompt: 'F-0005 regression test',
|
|
100
|
+
sources: [{ type: 'SMS', limit: 1 }], // non-empty so the validator passes; access check then fires
|
|
101
|
+
} as any),
|
|
102
|
+
{
|
|
103
|
+
shouldError: true,
|
|
104
|
+
onError: (e: any) => {
|
|
105
|
+
const msg = (e?.message ?? '').toLowerCase()
|
|
106
|
+
const status = e?.status ?? e?.code
|
|
107
|
+
return status === 403 || status === 401
|
|
108
|
+
|| msg.includes('access') || msg.includes('permission') || msg.includes('forbidden')
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
} finally {
|
|
113
|
+
// Cleanup: restore original roles, delete the test role.
|
|
114
|
+
try {
|
|
115
|
+
await sdk.api.users.updateOne(
|
|
116
|
+
sdkNonAdmin.userInfo.id,
|
|
117
|
+
{ roles: originalRoles ?? [] },
|
|
118
|
+
{ replaceObjectFields: true },
|
|
119
|
+
)
|
|
120
|
+
} catch {}
|
|
121
|
+
if (rbapId) {
|
|
122
|
+
try { await sdk.api.role_based_access_permissions.deleteOne(rbapId) } catch {}
|
|
123
|
+
}
|
|
124
|
+
// Re-authenticate the non-admin to drop the no-access role from their JWT
|
|
125
|
+
// before subsequent tests run.
|
|
126
|
+
// Role restore above re-triggers deauthenticate_user; wait > 1s so the freshly minted
|
|
127
|
+
// token's (second-floored) iat lands after the deauth timestamp and isn't permanently
|
|
128
|
+
// rejected by is_logged_in's iat-slack check. Matches the in-test re-auth above.
|
|
129
|
+
await wait(undefined, 1500)
|
|
130
|
+
try { await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!) } catch {}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Allow running this test file independently
|
|
135
|
+
if (require.main === module) {
|
|
136
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
137
|
+
const sdk = new Session({ host })
|
|
138
|
+
const sdkNonAdmin = new Session({ host })
|
|
139
|
+
|
|
140
|
+
const runTests = async () => {
|
|
141
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
142
|
+
await ai_conversations_rbac_tests({ sdk, sdkNonAdmin })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
runTests()
|
|
146
|
+
.then(() => {
|
|
147
|
+
console.log("✅ F-0005 ai_conversations RBAC test suite completed successfully")
|
|
148
|
+
process.exit(0)
|
|
149
|
+
})
|
|
150
|
+
.catch((error) => {
|
|
151
|
+
console.error("❌ F-0005 ai_conversations RBAC test suite failed:", error)
|
|
152
|
+
process.exit(1)
|
|
153
|
+
})
|
|
154
|
+
}
|