@tellescope/sdk 1.250.2 → 1.251.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/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/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 +370 -0
- package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.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 +179 -158
- 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/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/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 +366 -0
- package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.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 +179 -158
- 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/enduser_login.test.ts +215 -0
- package/src/tests/api_tests/push_forms_to_portal_group_completion.test.ts +198 -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 +18 -5
- package/test_generated.pdf +0 -0
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
2
13
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
14
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
15
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -35,388 +46,1771 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
35
46
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
47
|
}
|
|
37
48
|
};
|
|
38
|
-
var
|
|
39
|
-
|
|
49
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
50
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
51
|
+
if (ar || !(i in from)) {
|
|
52
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
53
|
+
ar[i] = from[i];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
40
57
|
};
|
|
41
58
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
59
|
exports.account_switcher_tests = void 0;
|
|
43
60
|
require('source-map-support').install();
|
|
44
|
-
var crypto_1 = __importDefault(require("crypto"));
|
|
45
61
|
var sdk_1 = require("../../sdk");
|
|
46
62
|
var testing_1 = require("@tellescope/testing");
|
|
47
|
-
var utilities_1 = require("@tellescope/utilities");
|
|
48
63
|
var setup_1 = require("../setup");
|
|
49
64
|
var host = process.env.API_URL || 'http://localhost:8080';
|
|
50
|
-
var
|
|
51
|
-
var
|
|
52
|
-
|
|
53
|
-
var
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
var RAND = function () { return Math.random().toString(36).slice(2, 10); };
|
|
66
|
+
var decode_jwt = function (token) {
|
|
67
|
+
try {
|
|
68
|
+
var part = token.split('.')[1];
|
|
69
|
+
var json = Buffer.from(part.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8');
|
|
70
|
+
return JSON.parse(json);
|
|
71
|
+
}
|
|
72
|
+
catch (_a) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var passOnAnyResult = { shouldError: false, onResult: function () { return true; } };
|
|
56
77
|
var account_switcher_tests = function (_a) {
|
|
57
78
|
var sdk = _a.sdk, sdkNonAdmin = _a.sdkNonAdmin;
|
|
58
79
|
return __awaiter(void 0, void 0, void 0, function () {
|
|
59
|
-
var
|
|
60
|
-
var
|
|
61
|
-
return __generator(this, function (
|
|
62
|
-
switch (
|
|
80
|
+
var adminId, nonAdminId, adminEmail, nonAdminEmail, nonAdminBusinessId, adminBusinessId, NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD, get_user, __lac_resetCounter, sanitize_marker_tags, set_linkedAccountAccess, clear_linkedAccountAccess, cleanup_marker_tags, ensureOrgToggleEnabled, emailRejectMatcher, seedEmail, createdSeedUserId, _b, adminAfterRequest, pendingFromNonAdmin, fakeEntry, validatorRejectMatcher, mutations, _loop_1, _i, mutations_1, _c, label, mutated, adminWithPending, seededPending, unauthedSdk, is401Rejection, unverifiedEmail, unverifiedUserId, created, _d, _e, adminPre, preLen, adminBeforeAccept, pendingEntry, adminWithReq, pendingForNonAdmin, nonAdminAfterReq, pendingFromAdmin, i, _f, adminForF, pendingForF, _g, adminPendingState, pendingNA, switchedToken, switchedUser, decoded, switchedSdk, originalAdminFname, g6EnduserId, ce, e_1, _h, state, newPending, oSeedState, oPending, oBefore, oBeforeLen, i4State, i4Pending, i4Accepted, i4Entry, kSeedState, kPending, kSwitchedToken, kSwitchedUser, kSwitchedSdk, kBackToken, kBackDecoded, kBackSdk, userCEmail, userCRecord, userCId, userCToken, sdkC, userCState, pendingForC, aAsBResp, aAsBSdk, aAsBDecoded, chainedToken, chainedDecoded, chainedSdk, _j, lSeedState, lPending, lSwitchResp, lSwitchedSdk, lBackResp, lBackSdk, enduserEmail, enduserRec, enduserAuthToken, sdkAsEnduser, isEnduserRejection, _k, i, _l, _m;
|
|
81
|
+
var _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4;
|
|
82
|
+
return __generator(this, function (_5) {
|
|
83
|
+
switch (_5.label) {
|
|
63
84
|
case 0:
|
|
64
85
|
(0, testing_1.log_header)("Account Switcher Tests");
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
adminId = sdk.userInfo.id;
|
|
87
|
+
nonAdminId = sdkNonAdmin.userInfo.id;
|
|
88
|
+
adminEmail = sdk.userInfo.email;
|
|
89
|
+
nonAdminEmail = sdkNonAdmin.userInfo.email;
|
|
90
|
+
nonAdminBusinessId = sdkNonAdmin.userInfo.businessId;
|
|
91
|
+
adminBusinessId = sdk.userInfo.businessId;
|
|
92
|
+
NON_ADMIN_EMAIL = process.env.NON_ADMIN_EMAIL;
|
|
93
|
+
NON_ADMIN_PASSWORD = process.env.NON_ADMIN_PASSWORD;
|
|
94
|
+
if (!(NON_ADMIN_EMAIL && NON_ADMIN_PASSWORD)) {
|
|
95
|
+
throw new Error("NON_ADMIN_EMAIL and NON_ADMIN_PASSWORD must be set to run account_switcher_tests");
|
|
96
|
+
}
|
|
97
|
+
get_user = function (s, id) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
98
|
+
return [2 /*return*/, s.api.users.getOne(id)
|
|
99
|
+
// The standard CRUD update path rate-limits identical updates to the same record at
|
|
100
|
+
// 3/30s (routing.ts:2631). Cleanup PATCHes between sections all carry the same
|
|
101
|
+
// { linkedAccountAccess: [] } payload and would trip it. Workaround: include a rotating
|
|
102
|
+
// marker tag so every write has a unique JSON payload. Markers are stripped at end of run.
|
|
103
|
+
];
|
|
104
|
+
}); }); };
|
|
105
|
+
__lac_resetCounter = 0;
|
|
106
|
+
sanitize_marker_tags = function (tags) {
|
|
107
|
+
if (tags === void 0) { tags = []; }
|
|
108
|
+
return tags.filter(function (t) { return typeof t === 'string' && !t.startsWith('__lac_'); });
|
|
109
|
+
};
|
|
110
|
+
set_linkedAccountAccess = function (s, ownerId, entries) { return __awaiter(void 0, void 0, void 0, function () {
|
|
111
|
+
var me, newTags;
|
|
112
|
+
var _a;
|
|
113
|
+
return __generator(this, function (_b) {
|
|
114
|
+
switch (_b.label) {
|
|
115
|
+
case 0:
|
|
116
|
+
__lac_resetCounter++;
|
|
117
|
+
return [4 /*yield*/, s.api.users.getOne(ownerId)];
|
|
118
|
+
case 1:
|
|
119
|
+
me = _b.sent();
|
|
120
|
+
newTags = __spreadArray(__spreadArray([], sanitize_marker_tags((_a = me === null || me === void 0 ? void 0 : me.tags) !== null && _a !== void 0 ? _a : []), true), ["__lac_".concat(__lac_resetCounter)], false);
|
|
121
|
+
return [4 /*yield*/, s.api.users.updateOne(ownerId, {
|
|
122
|
+
linkedAccountAccess: entries,
|
|
123
|
+
tags: newTags,
|
|
124
|
+
}, { replaceObjectFields: true })];
|
|
125
|
+
case 2:
|
|
126
|
+
_b.sent();
|
|
127
|
+
return [2 /*return*/];
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}); };
|
|
131
|
+
clear_linkedAccountAccess = function (s, ownerId) { return __awaiter(void 0, void 0, void 0, function () {
|
|
132
|
+
var me;
|
|
133
|
+
var _a;
|
|
134
|
+
return __generator(this, function (_b) {
|
|
135
|
+
switch (_b.label) {
|
|
136
|
+
case 0: return [4 /*yield*/, s.api.users.getOne(ownerId)];
|
|
137
|
+
case 1:
|
|
138
|
+
me = _b.sent();
|
|
139
|
+
if (!(((_a = me === null || me === void 0 ? void 0 : me.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).length))
|
|
140
|
+
return [2 /*return*/];
|
|
141
|
+
return [4 /*yield*/, set_linkedAccountAccess(s, ownerId, [])];
|
|
142
|
+
case 2:
|
|
143
|
+
_b.sent();
|
|
144
|
+
return [2 /*return*/];
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}); };
|
|
148
|
+
cleanup_marker_tags = function (s, ownerId) { return __awaiter(void 0, void 0, void 0, function () {
|
|
149
|
+
var me, cleaned, _a;
|
|
150
|
+
var _b, _c;
|
|
151
|
+
return __generator(this, function (_d) {
|
|
152
|
+
switch (_d.label) {
|
|
153
|
+
case 0:
|
|
154
|
+
_d.trys.push([0, 4, , 5]);
|
|
155
|
+
return [4 /*yield*/, s.api.users.getOne(ownerId)];
|
|
156
|
+
case 1:
|
|
157
|
+
me = _d.sent();
|
|
158
|
+
cleaned = sanitize_marker_tags((_b = me === null || me === void 0 ? void 0 : me.tags) !== null && _b !== void 0 ? _b : []);
|
|
159
|
+
if (!(((_c = me === null || me === void 0 ? void 0 : me.tags) !== null && _c !== void 0 ? _c : []).length !== cleaned.length)) return [3 /*break*/, 3];
|
|
160
|
+
return [4 /*yield*/, s.api.users.updateOne(ownerId, { tags: cleaned }, { replaceObjectFields: true })];
|
|
161
|
+
case 2:
|
|
162
|
+
_d.sent();
|
|
163
|
+
_d.label = 3;
|
|
164
|
+
case 3: return [3 /*break*/, 5];
|
|
165
|
+
case 4:
|
|
166
|
+
_a = _d.sent();
|
|
167
|
+
return [3 /*break*/, 5];
|
|
168
|
+
case 5: return [2 /*return*/];
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}); };
|
|
172
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
|
|
83
173
|
case 1:
|
|
84
|
-
|
|
85
|
-
return [4 /*yield*/,
|
|
174
|
+
_5.sent();
|
|
175
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdkNonAdmin, nonAdminId)
|
|
176
|
+
// The feature is opt-in per org. Enable it on the test business so the rest of the suite
|
|
177
|
+
// exercises the feature (the O section below explicitly toggles it off to verify gating).
|
|
178
|
+
];
|
|
86
179
|
case 2:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
180
|
+
_5.sent();
|
|
181
|
+
ensureOrgToggleEnabled = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
182
|
+
var org;
|
|
183
|
+
return __generator(this, function (_a) {
|
|
184
|
+
switch (_a.label) {
|
|
185
|
+
case 0: return [4 /*yield*/, sdk.api.organizations.getOne(adminBusinessId)];
|
|
186
|
+
case 1:
|
|
187
|
+
org = _a.sent();
|
|
188
|
+
if (!((org === null || org === void 0 ? void 0 : org.accountSwitchingEnabled) !== true)) return [3 /*break*/, 4];
|
|
189
|
+
return [4 /*yield*/, sdk.api.organizations.updateOne(adminBusinessId, { accountSwitchingEnabled: true })];
|
|
190
|
+
case 2:
|
|
191
|
+
_a.sent();
|
|
192
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
193
|
+
case 3:
|
|
194
|
+
_a.sent();
|
|
195
|
+
_a.label = 4;
|
|
196
|
+
case 4: return [2 /*return*/];
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}); };
|
|
200
|
+
return [4 /*yield*/, ensureOrgToggleEnabled()
|
|
201
|
+
// ============================================================
|
|
202
|
+
// A. Email immutability
|
|
203
|
+
// ============================================================
|
|
204
|
+
];
|
|
90
205
|
case 3:
|
|
91
|
-
|
|
92
|
-
|
|
206
|
+
_5.sent();
|
|
207
|
+
// ============================================================
|
|
208
|
+
// A. Email immutability
|
|
209
|
+
// ============================================================
|
|
210
|
+
(0, testing_1.log_header)("A. Email immutability");
|
|
211
|
+
emailRejectMatcher = function (e) { return (e.statusCode === 400
|
|
212
|
+
|| /(updates|disabled|readonly|cannot)/i.test(e.message || '')); };
|
|
213
|
+
return [4 /*yield*/, (0, testing_1.async_test)('A1. Self PATCH of own email rejected', function () { return sdkNonAdmin.api.users.updateOne(nonAdminId, { email: "evil-".concat(RAND(), "@tellescope.com") }); }, { shouldError: true, onError: emailRejectMatcher })];
|
|
93
214
|
case 4:
|
|
94
|
-
|
|
95
|
-
return [4 /*yield*/, sdk.api.users.
|
|
215
|
+
_5.sent();
|
|
216
|
+
return [4 /*yield*/, (0, testing_1.async_test)('A2. Admin PATCH of another user email rejected', function () { return sdk.api.users.updateOne(nonAdminId, { email: "admin-rename-".concat(RAND(), "@tellescope.com") }); }, { shouldError: true, onError: emailRejectMatcher })];
|
|
96
217
|
case 5:
|
|
97
|
-
|
|
98
|
-
return [4 /*yield*/, sdk.api.users.
|
|
218
|
+
_5.sent();
|
|
219
|
+
return [4 /*yield*/, (0, testing_1.async_test)('A3. Admin PATCH of own email rejected', function () { return sdk.api.users.updateOne(adminId, { email: "admin-self-".concat(RAND(), "@tellescope.com") }); }, { shouldError: true, onError: emailRejectMatcher })];
|
|
99
220
|
case 6:
|
|
100
|
-
|
|
101
|
-
return [4 /*yield*/,
|
|
102
|
-
|
|
103
|
-
userPrefixSwitcher = _l.sent();
|
|
104
|
-
return [4 /*yield*/, sdk.api.users.createOne({ email: emailNonAdminPlus, verifiedEmail: true })
|
|
105
|
-
// Created with verifiedEmail: false so we can observe propagation from a verified source
|
|
221
|
+
_5.sent();
|
|
222
|
+
return [4 /*yield*/, (0, testing_1.async_test)('A4. verifiedEmail/email unchanged after rejected updates', function () { return get_user(sdk, nonAdminId); }, { shouldError: false, onResult: function (u) { return u.verifiedEmail === true && u.email === nonAdminEmail; } })
|
|
223
|
+
// A5: email IS settable on user creation
|
|
106
224
|
];
|
|
225
|
+
case 7:
|
|
226
|
+
_5.sent();
|
|
227
|
+
seedEmail = "seed-".concat(RAND(), "@tellescope.com");
|
|
228
|
+
createdSeedUserId = '';
|
|
229
|
+
return [4 /*yield*/, (0, testing_1.async_test)('A5. Admin can set email on user creation', function () { return sdk.api.users.createOne({ email: seedEmail, fname: 'Seed', lname: 'User' }); }, { shouldError: false, onResult: function (u) { createdSeedUserId = u.id; return u.email === seedEmail; } })];
|
|
107
230
|
case 8:
|
|
108
|
-
|
|
109
|
-
return [
|
|
110
|
-
|
|
111
|
-
];
|
|
231
|
+
_5.sent();
|
|
232
|
+
if (!createdSeedUserId) return [3 /*break*/, 12];
|
|
233
|
+
_5.label = 9;
|
|
112
234
|
case 9:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return [4 /*yield*/, sdk.api.users.createOne({ email: emailPlusLocked, verifiedEmail: true })];
|
|
235
|
+
_5.trys.push([9, 11, , 12]);
|
|
236
|
+
return [4 /*yield*/, sdk.api.users.deleteOne(createdSeedUserId)];
|
|
116
237
|
case 10:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return [4 /*yield*/, sdk.api.users.updateOne(userPlusLocked.id, { lockedOutUntil: 0 })
|
|
120
|
-
// Restrictive role + user assigned to that role (used to verify no privilege escalation)
|
|
121
|
-
];
|
|
238
|
+
_5.sent();
|
|
239
|
+
return [3 /*break*/, 12];
|
|
122
240
|
case 11:
|
|
123
|
-
|
|
124
|
-
return [
|
|
125
|
-
role: "acctsw_restrictive_".concat(suffix),
|
|
126
|
-
permissions: {
|
|
127
|
-
users: { read: null, create: null, update: null, delete: null },
|
|
128
|
-
endusers: { read: null, create: null, update: null, delete: null },
|
|
129
|
-
},
|
|
130
|
-
})];
|
|
241
|
+
_b = _5.sent();
|
|
242
|
+
return [3 /*break*/, 12];
|
|
131
243
|
case 12:
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
244
|
+
// ============================================================
|
|
245
|
+
// B. linkedAccountAccess PATCH-self validator
|
|
246
|
+
// ============================================================
|
|
247
|
+
(0, testing_1.log_header)("B. linkedAccountAccess PATCH-self validator");
|
|
248
|
+
// Seed: nonAdmin requests access to admin
|
|
249
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
135
250
|
case 13:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
];
|
|
251
|
+
// Seed: nonAdmin requests access to admin
|
|
252
|
+
_5.sent();
|
|
253
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
140
254
|
case 14:
|
|
141
|
-
|
|
142
|
-
return [4 /*yield*/,
|
|
255
|
+
_5.sent();
|
|
256
|
+
return [4 /*yield*/, get_user(sdk, adminId)];
|
|
143
257
|
case 15:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
258
|
+
adminAfterRequest = _5.sent();
|
|
259
|
+
pendingFromNonAdmin = ((_o = adminAfterRequest.linkedAccountAccess) !== null && _o !== void 0 ? _o : []).find(function (e) { return e.userId === nonAdminId; });
|
|
260
|
+
(0, testing_1.assert)(!!pendingFromNonAdmin && pendingFromNonAdmin.status === 'pending', 'no pending entry seeded for B tests', 'B-seed pending entry present');
|
|
261
|
+
// B14: PATCH self with array unchanged is a no-op
|
|
262
|
+
return [4 /*yield*/, (0, testing_1.async_test)('B14. PATCH self with linkedAccountAccess unchanged succeeds', function () { return sdk.api.users.updateOne(adminId, { linkedAccountAccess: adminAfterRequest.linkedAccountAccess }, { replaceObjectFields: true }); }, passOnAnyResult)
|
|
263
|
+
// B5/B6: cannot add entries
|
|
264
|
+
];
|
|
149
265
|
case 16:
|
|
150
|
-
|
|
151
|
-
|
|
266
|
+
// B14: PATCH self with array unchanged is a no-op
|
|
267
|
+
_5.sent();
|
|
268
|
+
fakeEntry = {
|
|
269
|
+
userId: '000000000000000000000099',
|
|
270
|
+
email: 'fake@tellescope.com',
|
|
271
|
+
fname: 'Fake', lname: 'User', orgName: 'Fake Org',
|
|
272
|
+
status: 'accepted',
|
|
273
|
+
createdAt: new Date(),
|
|
274
|
+
requestExpiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
275
|
+
};
|
|
276
|
+
validatorRejectMatcher = function (e) { return (e.statusCode === 400
|
|
277
|
+
|| e.statusCode === 404
|
|
278
|
+
// "No updates provided" fires when unknown fields (e.g. the removed accountAccessGrantedTo) get
|
|
279
|
+
// stripped by the schema and nothing valid remains — that's a legitimate rejection mechanism.
|
|
280
|
+
|| /(linkedAccountAccess|owner|add entries|mutate|immutable|status can only|legacy|accountAccessGrantedTo|replaced|Could not find|No updates provided)/i.test(e.message || '')); };
|
|
281
|
+
return [4 /*yield*/, (0, testing_1.async_test)('B5. Cannot add accepted entry via PATCH', function () { var _a; return sdk.api.users.updateOne(adminId, { linkedAccountAccess: __spreadArray(__spreadArray([], ((_a = adminAfterRequest.linkedAccountAccess) !== null && _a !== void 0 ? _a : []), true), [fakeEntry], false) }, { replaceObjectFields: true }); }, { shouldError: true, onError: validatorRejectMatcher })];
|
|
152
282
|
case 17:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
(0, testing_1.assert)(linkedIds.includes(userPlusB.id), 'userPlusB missing from linked accounts', 'get_linked_accounts: includes plus sibling B');
|
|
157
|
-
(0, testing_1.assert)(linkedIds.includes(userPlusUppercase.id), 'uppercase plus variant missing from linked accounts', 'get_linked_accounts: case-insensitive base match');
|
|
158
|
-
(0, testing_1.assert)(linkedIds.includes(userPlusInjection.id), 'regex-special plus variant missing from linked accounts', 'get_linked_accounts: regex special chars do not break matching');
|
|
159
|
-
(0, testing_1.assert)(linkedIds.includes(userCrossOrg.id), 'cross-org sibling missing from linked accounts', 'get_linked_accounts: includes cross-org plus sibling');
|
|
160
|
-
(0, testing_1.assert)(!linkedIds.includes(sdk.userInfo.id), 'caller present in linked accounts', 'get_linked_accounts: excludes caller');
|
|
161
|
-
(0, testing_1.assert)(!linkedIds.includes(userOtherBase.id), 'unrelated-base user present in linked accounts', 'get_linked_accounts: excludes unrelated base email');
|
|
162
|
-
(0, testing_1.assert)(!linkedIds.includes(userPrefixSwitcher.id), 'prefix-extended user present in linked accounts (anchored regex broken?)', 'get_linked_accounts: anchored regex excludes prefix-extended local part');
|
|
163
|
-
(0, testing_1.assert)(!linkedIds.includes(userPlusLocked.id), 'locked user present in linked accounts', 'get_linked_accounts: excludes locked target');
|
|
164
|
-
sample = (linkedFromAdmin.linkedAccounts || []).find(function (a) { return a.id === userPlusB.id; });
|
|
165
|
-
(0, testing_1.assert)(!!sample
|
|
166
|
-
&& typeof sample.email === 'string'
|
|
167
|
-
&& typeof sample.orgName === 'string'
|
|
168
|
-
&& typeof sample.requiresMFA === 'boolean', 'linked account entry missing required fields', 'get_linked_accounts: entries have id, email, orgName, requiresMFA');
|
|
169
|
-
// noAccessPermissions: non-admin can call without 403
|
|
170
|
-
return [4 /*yield*/, (0, testing_1.async_test)('get_linked_accounts callable by non-admin (noAccessPermissions)', function () { return sdkNonAdmin.api.users.get_linked_accounts(); }, { onResult: function (r) { return Array.isArray(r.linkedAccounts); } })
|
|
171
|
-
// ====================== B. switch_account ======================
|
|
172
|
-
// 1. Same-org happy path: sdk (admin) → userPlusB
|
|
173
|
-
// Capture the source token in a separate Session BEFORE the switch so we can verify
|
|
174
|
-
// the original JWT is invalidated (Step 13) without losing access via `sdk` itself.
|
|
283
|
+
_5.sent();
|
|
284
|
+
return [4 /*yield*/, (0, testing_1.async_test)('B6. Cannot add pending entry via PATCH', function () { var _a; return sdk.api.users.updateOne(adminId, { linkedAccountAccess: __spreadArray(__spreadArray([], ((_a = adminAfterRequest.linkedAccountAccess) !== null && _a !== void 0 ? _a : []), true), [__assign(__assign({}, fakeEntry), { status: 'pending' })], false) }, { replaceObjectFields: true }); }, { shouldError: true, onError: validatorRejectMatcher })
|
|
285
|
+
// B7-B13: cannot mutate immutable fields
|
|
175
286
|
];
|
|
176
287
|
case 18:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
288
|
+
_5.sent();
|
|
289
|
+
mutations = [
|
|
290
|
+
['userId', __assign(__assign({}, pendingFromNonAdmin), { userId: '000000000000000000000099' })],
|
|
291
|
+
['email', __assign(__assign({}, pendingFromNonAdmin), { email: 'mutated@tellescope.com' })],
|
|
292
|
+
['fname', __assign(__assign({}, pendingFromNonAdmin), { fname: 'MutatedF' })],
|
|
293
|
+
['lname', __assign(__assign({}, pendingFromNonAdmin), { lname: 'MutatedL' })],
|
|
294
|
+
['orgName', __assign(__assign({}, pendingFromNonAdmin), { orgName: 'MutatedOrg' })],
|
|
295
|
+
['createdAt', __assign(__assign({}, pendingFromNonAdmin), { createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24) })],
|
|
296
|
+
['requestExpiresAt', __assign(__assign({}, pendingFromNonAdmin), { requestExpiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365) })],
|
|
297
|
+
];
|
|
298
|
+
_loop_1 = function (label, mutated) {
|
|
299
|
+
return __generator(this, function (_6) {
|
|
300
|
+
switch (_6.label) {
|
|
301
|
+
case 0: return [4 /*yield*/, (0, testing_1.async_test)("B7-13. Cannot mutate linkedAccountAccess entry ".concat(label), function () { return sdk.api.users.updateOne(adminId, { linkedAccountAccess: [mutated] }, { replaceObjectFields: true }); }, { shouldError: true, onError: validatorRejectMatcher })];
|
|
302
|
+
case 1:
|
|
303
|
+
_6.sent();
|
|
304
|
+
return [2 /*return*/];
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
_i = 0, mutations_1 = mutations;
|
|
309
|
+
_5.label = 19;
|
|
181
310
|
case 19:
|
|
182
|
-
|
|
183
|
-
|
|
311
|
+
if (!(_i < mutations_1.length)) return [3 /*break*/, 22];
|
|
312
|
+
_c = mutations_1[_i], label = _c[0], mutated = _c[1];
|
|
313
|
+
return [5 /*yield**/, _loop_1(label, mutated)];
|
|
184
314
|
case 20:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
(0, testing_1.assert)(((_d = switchResult.user) === null || _d === void 0 ? void 0 : _d.id) === userPlusB.id, 'switch_account returned wrong user id', 'switch_account: returns target user record');
|
|
188
|
-
(0, testing_1.assert)((((_e = switchResult.user) === null || _e === void 0 ? void 0 : _e.email) || '').toLowerCase() === emailPlusB.toLowerCase(), 'switch_account returned wrong email', 'switch_account: returns target email');
|
|
189
|
-
// User model stores password hash in `hashedPass` (not `hashedPassword` — that's Enduser).
|
|
190
|
-
// Both are asserted as belt-and-suspenders in case field names drift.
|
|
191
|
-
(0, testing_1.assert)(!((_f = switchResult.user) === null || _f === void 0 ? void 0 : _f.hashedPass), "switch_account response leaked hashedPass: ".concat(JSON.stringify(switchResult.user.hashedPass)), 'switch_account: response redacts hashedPass');
|
|
192
|
-
(0, testing_1.assert)(!((_g = switchResult.user) === null || _g === void 0 ? void 0 : _g.hashedPassword), "switch_account response leaked hashedPassword: ".concat(JSON.stringify(switchResult.user.hashedPassword)), 'switch_account: response has no hashedPassword (defense-in-depth)');
|
|
193
|
-
switchedSession_1 = new sdk_1.Session({ host: host, authToken: switchResult.authToken });
|
|
194
|
-
return [4 /*yield*/, (0, testing_1.async_test)('switch_account: new token authenticates', function () { return switchedSession_1.test_authenticated(); }, { expectedResult: 'Authenticated!' })];
|
|
315
|
+
_5.sent();
|
|
316
|
+
_5.label = 21;
|
|
195
317
|
case 21:
|
|
196
|
-
|
|
197
|
-
return [
|
|
198
|
-
case 22:
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
318
|
+
_i++;
|
|
319
|
+
return [3 /*break*/, 19];
|
|
320
|
+
case 22:
|
|
321
|
+
// B3: pending -> accepted allowed
|
|
322
|
+
return [4 /*yield*/, (0, testing_1.async_test)('B3. Owner can flip pending -> accepted', function () { return set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, pendingFromNonAdmin), { status: 'accepted' })]); }, passOnAnyResult)
|
|
323
|
+
// B4: accepted -> pending rejected
|
|
324
|
+
];
|
|
202
325
|
case 23:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
(0, testing_1.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return [4 /*yield*/, (0, testing_1.async_test)('switch_account: source JWT invalidated after switch', function () { return preSwitchTokenSession_1.test_authenticated(); }, { shouldError: true, onError: function (e) { var _a; return /unauth|expired|invalid/i.test((e === null || e === void 0 ? void 0 : e.message) || ((_a = e === null || e === void 0 ? void 0 : e.toString) === null || _a === void 0 ? void 0 : _a.call(e)) || String(e)); } })
|
|
210
|
-
// Re-authenticate sdk so subsequent tests in this suite work (its prior token is now expired).
|
|
326
|
+
// B3: pending -> accepted allowed
|
|
327
|
+
_5.sent();
|
|
328
|
+
// B4: accepted -> pending rejected
|
|
329
|
+
return [4 /*yield*/, (0, testing_1.async_test)('B4. Cannot flip accepted -> pending', function () { return set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, pendingFromNonAdmin), { status: 'pending' })]); }, { shouldError: true, onError: validatorRejectMatcher })
|
|
330
|
+
// B11/B12: non-owner PATCH of another user's linkedAccountAccess. Use a NON-empty
|
|
331
|
+
// payload so the rate-limit key is unique from later admin-owned `[]` clears.
|
|
211
332
|
];
|
|
212
333
|
case 24:
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
334
|
+
// B4: accepted -> pending rejected
|
|
335
|
+
_5.sent();
|
|
336
|
+
// B11/B12: non-owner PATCH of another user's linkedAccountAccess. Use a NON-empty
|
|
337
|
+
// payload so the rate-limit key is unique from later admin-owned `[]` clears.
|
|
338
|
+
return [4 /*yield*/, (0, testing_1.async_test)('B11/B12. Non-owner PATCH of another user linkedAccountAccess rejected', function () { return sdkNonAdmin.api.users.updateOne(adminId, { linkedAccountAccess: [__assign(__assign({}, pendingFromNonAdmin), { status: 'accepted' })] }, { replaceObjectFields: true }); }, { shouldError: true, onError: validatorRejectMatcher })
|
|
339
|
+
// B15: legacy field no longer accepted
|
|
218
340
|
];
|
|
219
341
|
case 25:
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
342
|
+
// B11/B12: non-owner PATCH of another user's linkedAccountAccess. Use a NON-empty
|
|
343
|
+
// payload so the rate-limit key is unique from later admin-owned `[]` clears.
|
|
344
|
+
_5.sent();
|
|
345
|
+
// B15: legacy field no longer accepted
|
|
346
|
+
return [4 /*yield*/, (0, testing_1.async_test)('B15. Legacy accountAccessGrantedTo PATCH is rejected', function () { return sdk.api.users.updateOne(adminId, { accountAccessGrantedTo: [nonAdminId] }); }, { shouldError: true, onError: validatorRejectMatcher })
|
|
347
|
+
// B1: Owner can remove a pending entry (re-seed first)
|
|
348
|
+
];
|
|
223
349
|
case 26:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return [4 /*yield*/,
|
|
350
|
+
// B15: legacy field no longer accepted
|
|
351
|
+
_5.sent();
|
|
352
|
+
// B1: Owner can remove a pending entry (re-seed first)
|
|
353
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
|
|
228
354
|
case 27:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return [4 /*yield*/, (0, testing_1.async_test)('switch_account: cross-org session cannot see source-org user (tenant isolation)', function () { return xorgSession_1.api.users.getOne(sdk.userInfo.id); }, testing_1.handleAnyError)
|
|
233
|
-
// Re-auth sdk: previous switch invalidated its token (Step 13)
|
|
234
|
-
];
|
|
355
|
+
// B1: Owner can remove a pending entry (re-seed first)
|
|
356
|
+
_5.sent();
|
|
357
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
235
358
|
case 28:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// Re-auth sdk: previous switch invalidated its token (Step 13)
|
|
239
|
-
return [4 /*yield*/, sdk.authenticate(TEST_EMAIL, TEST_PASSWORD)
|
|
240
|
-
// 3. Permission scoping (no privilege escalation): sdk (admin) → userReadOnly,
|
|
241
|
-
// then attempt an admin-only operation with the new token. Must fail.
|
|
242
|
-
];
|
|
359
|
+
_5.sent();
|
|
360
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
243
361
|
case 29:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
case 30:
|
|
248
|
-
downgradeSwitch = _l.sent();
|
|
249
|
-
downgradedSession_1 = new sdk_1.Session({ host: host, authToken: downgradeSwitch.authToken });
|
|
250
|
-
return [4 /*yield*/, (0, testing_1.async_test)('switch_account: downgraded session cannot perform admin op (no privilege escalation)', function () { return downgradedSession_1.api.users.updateOne(userPlusA.id, { roles: ['Admin'] }, { replaceObjectFields: true }); }, testing_1.handleAnyError)
|
|
251
|
-
// Re-auth sdk: previous switch invalidated its token (Step 13)
|
|
362
|
+
_5.sent();
|
|
363
|
+
return [4 /*yield*/, (0, testing_1.async_test)('B1. Owner can remove a pending entry', function () { return set_linkedAccountAccess(sdk, adminId, []); }, passOnAnyResult)
|
|
364
|
+
// B2: Owner can remove an accepted entry
|
|
252
365
|
];
|
|
366
|
+
case 30:
|
|
367
|
+
_5.sent();
|
|
368
|
+
// B2: Owner can remove an accepted entry
|
|
369
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
253
370
|
case 31:
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return [4 /*yield*/,
|
|
257
|
-
// 3b. Self-switch attempt MUST be rejected (Step 12): no-op that wastes rate-limit budget
|
|
258
|
-
// and creates spurious audit logs.
|
|
259
|
-
];
|
|
371
|
+
// B2: Owner can remove an accepted entry
|
|
372
|
+
_5.sent();
|
|
373
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
260
374
|
case 32:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// 3b. Self-switch attempt MUST be rejected (Step 12): no-op that wastes rate-limit budget
|
|
264
|
-
// and creates spurious audit logs.
|
|
265
|
-
return [4 /*yield*/, (0, testing_1.async_test)('switch_account: rejects self-switch', function () { return sdk.api.users.switch_account({ targetUserId: sdk.userInfo.id }); }, { shouldError: true, onError: function (e) { var _a; return /own account/i.test((e === null || e === void 0 ? void 0 : e.message) || ((_a = e === null || e === void 0 ? void 0 : e.toString) === null || _a === void 0 ? void 0 : _a.call(e)) || String(e)); } })
|
|
266
|
-
// 4. Cross-base attack: sdk (admin) → userOtherBase (different base) MUST be rejected.
|
|
267
|
-
];
|
|
375
|
+
_5.sent();
|
|
376
|
+
return [4 /*yield*/, get_user(sdk, adminId)];
|
|
268
377
|
case 33:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
// 4. Cross-base attack: sdk (admin) → userOtherBase (different base) MUST be rejected.
|
|
273
|
-
return [4 /*yield*/, (0, testing_1.async_test)('switch_account: rejects cross-base target', function () { return sdk.api.users.switch_account({ targetUserId: userOtherBase.id }); }, testing_1.handleAnyError)];
|
|
378
|
+
adminWithPending = _5.sent();
|
|
379
|
+
seededPending = ((_p = adminWithPending.linkedAccountAccess) !== null && _p !== void 0 ? _p : []).find(function (e) { return e.userId === nonAdminId; });
|
|
380
|
+
return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, seededPending), { status: 'accepted' })])];
|
|
274
381
|
case 34:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
//
|
|
382
|
+
_5.sent();
|
|
383
|
+
return [4 /*yield*/, (0, testing_1.async_test)('B2. Owner can remove an accepted entry', function () { return set_linkedAccountAccess(sdk, adminId, []); }, passOnAnyResult)
|
|
384
|
+
// ============================================================
|
|
385
|
+
// C. request_linked_account_access
|
|
386
|
+
// ============================================================
|
|
279
387
|
];
|
|
280
388
|
case 35:
|
|
281
|
-
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
389
|
+
_5.sent();
|
|
390
|
+
// ============================================================
|
|
391
|
+
// C. request_linked_account_access
|
|
392
|
+
// ============================================================
|
|
393
|
+
(0, testing_1.log_header)("C. request_linked_account_access");
|
|
394
|
+
unauthedSdk = new sdk_1.Session({ host: host });
|
|
395
|
+
is401Rejection = function (e) { return ((e === null || e === void 0 ? void 0 : e.statusCode) === 401
|
|
396
|
+
|| (typeof e === 'string' && /^unauthenticated$/i.test(e))
|
|
397
|
+
|| /^unauthenticated$/i.test((e === null || e === void 0 ? void 0 : e.message) || '')); };
|
|
398
|
+
return [4 /*yield*/, (0, testing_1.async_test)('C1. Unauthenticated request returns 401', function () { return unauthedSdk.api.users.request_linked_account_access({ targetEmail: adminEmail }); }, { shouldError: true, onError: is401Rejection })];
|
|
286
399
|
case 36:
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
// 6. Nonexistent target: must reject without leaking existence
|
|
290
|
-
return [4 /*yield*/, (0, testing_1.async_test)('switch_account: rejects nonexistent target', function () { return sdk.api.users.switch_account({ targetUserId: '000000000000000000000000' }); }, testing_1.handleAnyError)
|
|
291
|
-
// 7. Malformed targetUserId: validation error (does not reach handler / rate limit)
|
|
292
|
-
];
|
|
400
|
+
_5.sent();
|
|
401
|
+
return [4 /*yield*/, (0, testing_1.async_test)('C2. Non-existent email returns {} (no error)', function () { return sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: "nobody-".concat(RAND(), "@tellescope.example") }); }, passOnAnyResult)];
|
|
293
402
|
case 37:
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return [4 /*yield*/, (0, testing_1.async_test)('switch_account: rejects malformed targetUserId', function () { return sdk.api.users.switch_account({ targetUserId: 'not-an-objectid' }); }, testing_1.handleAnyError)
|
|
298
|
-
// 8. noAccessPermissions on switch: non-admin can switch to its own plus sibling
|
|
403
|
+
_5.sent();
|
|
404
|
+
return [4 /*yield*/, (0, testing_1.async_test)('C2. No record written for non-existent email', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) { var _a; return ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).length === 0; } })
|
|
405
|
+
// C3: unverified email -> treated as no-match. Verify via admin-created user.
|
|
299
406
|
];
|
|
300
407
|
case 38:
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// Re-auth sdkNonAdmin: previous switch invalidated its token (Step 13)
|
|
306
|
-
];
|
|
408
|
+
_5.sent();
|
|
409
|
+
unverifiedEmail = "unverified-".concat(RAND(), "@tellescope.com");
|
|
410
|
+
unverifiedUserId = '';
|
|
411
|
+
_5.label = 39;
|
|
307
412
|
case 39:
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// Re-auth sdkNonAdmin: previous switch invalidated its token (Step 13)
|
|
311
|
-
return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
|
|
312
|
-
// 9. Unauthenticated request: must reject before reaching handler
|
|
313
|
-
];
|
|
413
|
+
_5.trys.push([39, 41, , 42]);
|
|
414
|
+
return [4 /*yield*/, sdk.api.users.createOne({ email: unverifiedEmail, fname: 'Unv', lname: 'User' })];
|
|
314
415
|
case 40:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
return [4 /*yield*/, (0, testing_1.async_test)('switch_account: rejects unauthenticated caller', function () { return noAuthSession_1.api.users.switch_account({ targetUserId: userPlusA.id }); }, testing_1.handleAnyError)
|
|
319
|
-
// 10. Audit log written for the same-org switch performed in step 1
|
|
320
|
-
];
|
|
416
|
+
created = _5.sent();
|
|
417
|
+
unverifiedUserId = created.id;
|
|
418
|
+
return [3 /*break*/, 42];
|
|
321
419
|
case 41:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
case 42:
|
|
326
|
-
// 10. Audit log written for the same-org switch performed in step 1
|
|
327
|
-
_l.sent();
|
|
328
|
-
return [4 /*yield*/, sdk.api.user_logs.getSome({ limit: 1000 })];
|
|
420
|
+
_d = _5.sent();
|
|
421
|
+
return [3 /*break*/, 42];
|
|
422
|
+
case 42: return [4 /*yield*/, (0, testing_1.async_test)('C3. Unverified email treated as no-match', function () { return sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: unverifiedEmail }); }, passOnAnyResult)];
|
|
329
423
|
case 43:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
var _a, _b, _c;
|
|
334
|
-
return ((_a = l === null || l === void 0 ? void 0 : l.info) === null || _a === void 0 ? void 0 : _a.event) === 'account_switch'
|
|
335
|
-
&& ((_b = l === null || l === void 0 ? void 0 : l.info) === null || _b === void 0 ? void 0 : _b.targetUserId) === userPlusB.id
|
|
336
|
-
&& ((_c = l === null || l === void 0 ? void 0 : l.info) === null || _c === void 0 ? void 0 : _c.sourceUserId) === sdk.userInfo.id;
|
|
337
|
-
});
|
|
338
|
-
(0, testing_1.assert)(!!switchLog, 'no account_switch audit log found for sdk → userPlusB', 'switch_account: HIPAA audit log written');
|
|
339
|
-
if (switchLog) {
|
|
340
|
-
(0, testing_1.assert)(!!switchLog.info.sourceBusinessId && !!switchLog.info.targetBusinessId, 'audit log missing businessIds', 'switch_account: audit log captures both businessIds');
|
|
341
|
-
(0, testing_1.assert)((switchLog.info.targetEmail || '').toLowerCase() === emailPlusB.toLowerCase(), 'audit log targetEmail mismatch', 'switch_account: audit log captures targetEmail');
|
|
342
|
-
}
|
|
343
|
-
return [4 /*yield*/, sdk.api.users.getOne(userUnverifTarget.id)];
|
|
424
|
+
_5.sent();
|
|
425
|
+
if (!unverifiedUserId) return [3 /*break*/, 48];
|
|
426
|
+
return [4 /*yield*/, (0, testing_1.async_test)('C3. No record written on unverified target', function () { return get_user(sdk, unverifiedUserId); }, { shouldError: false, onResult: function (u) { var _a; return ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).length === 0; } })];
|
|
344
427
|
case 44:
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return [4 /*yield*/, sdk.api.users.switch_account({ targetUserId: userUnverifTarget.id })];
|
|
428
|
+
_5.sent();
|
|
429
|
+
_5.label = 45;
|
|
348
430
|
case 45:
|
|
349
|
-
|
|
350
|
-
return [4 /*yield*/, sdk.
|
|
431
|
+
_5.trys.push([45, 47, , 48]);
|
|
432
|
+
return [4 /*yield*/, sdk.api.users.deleteOne(unverifiedUserId)];
|
|
351
433
|
case 46:
|
|
352
|
-
|
|
353
|
-
return [
|
|
434
|
+
_5.sent();
|
|
435
|
+
return [3 /*break*/, 48];
|
|
354
436
|
case 47:
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
_l.label = 48;
|
|
361
|
-
case 48:
|
|
362
|
-
if (!(i < 15)) return [3 /*break*/, 54];
|
|
363
|
-
_l.label = 49;
|
|
437
|
+
_e = _5.sent();
|
|
438
|
+
return [3 /*break*/, 48];
|
|
439
|
+
case 48:
|
|
440
|
+
// C5: self-request
|
|
441
|
+
return [4 /*yield*/, (0, testing_1.async_test)('C5. Self-request returns {} and writes nothing', function () { return sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: nonAdminEmail }); }, passOnAnyResult)];
|
|
364
442
|
case 49:
|
|
365
|
-
|
|
366
|
-
|
|
443
|
+
// C5: self-request
|
|
444
|
+
_5.sent();
|
|
445
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
367
446
|
case 50:
|
|
368
|
-
|
|
369
|
-
return [4 /*yield*/,
|
|
447
|
+
_5.sent();
|
|
448
|
+
return [4 /*yield*/, (0, testing_1.async_test)('C5. No record written on self-request', function () { return get_user(sdkNonAdmin, nonAdminId); }, { shouldError: false, onResult: function (u) { var _a; return ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).length === 0; } })
|
|
449
|
+
// C4: valid match
|
|
450
|
+
];
|
|
370
451
|
case 51:
|
|
371
|
-
|
|
372
|
-
|
|
452
|
+
_5.sent();
|
|
453
|
+
// C4: valid match
|
|
454
|
+
return [4 /*yield*/, (0, testing_1.async_test)('C4. Valid email request returns {}', function () { return sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail }); }, passOnAnyResult)];
|
|
373
455
|
case 52:
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (/too many|rate/i.test(msg)) {
|
|
378
|
-
rateLimitTripped = true;
|
|
379
|
-
return [3 /*break*/, 54];
|
|
380
|
-
}
|
|
381
|
-
return [3 /*break*/, 53];
|
|
456
|
+
// C4: valid match
|
|
457
|
+
_5.sent();
|
|
458
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
382
459
|
case 53:
|
|
383
|
-
|
|
384
|
-
return [
|
|
460
|
+
_5.sent();
|
|
461
|
+
return [4 /*yield*/, (0, testing_1.async_test)('C4. Pending entry written with requester snapshot', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) {
|
|
462
|
+
var _a;
|
|
463
|
+
var e = ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).find(function (x) { return x.userId === nonAdminId; });
|
|
464
|
+
if (!e)
|
|
465
|
+
return false;
|
|
466
|
+
var expiresOk = (new Date(e.requestExpiresAt).getTime() - Date.now()) > 6 * 24 * 60 * 60 * 1000;
|
|
467
|
+
return e.status === 'pending' && e.email === nonAdminEmail && !!e.createdAt && expiresOk;
|
|
468
|
+
} })
|
|
469
|
+
// C6: idempotent
|
|
470
|
+
];
|
|
385
471
|
case 54:
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
return [4 /*yield*/, sdk.authenticate(TEST_EMAIL, TEST_PASSWORD).catch(function () { return null; })];
|
|
472
|
+
_5.sent();
|
|
473
|
+
return [4 /*yield*/, get_user(sdk, adminId)];
|
|
389
474
|
case 55:
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
return [
|
|
475
|
+
adminPre = _5.sent();
|
|
476
|
+
preLen = ((_q = adminPre.linkedAccountAccess) !== null && _q !== void 0 ? _q : []).length;
|
|
477
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
393
478
|
case 56:
|
|
394
|
-
|
|
395
|
-
return [4 /*yield*/,
|
|
396
|
-
userPlusA && cleanup(function () { return sdk.api.users.deleteOne(userPlusA.id); }),
|
|
397
|
-
userPlusB && cleanup(function () { return sdk.api.users.deleteOne(userPlusB.id); }),
|
|
398
|
-
userPlusLocked && cleanup(function () { return sdk.api.users.deleteOne(userPlusLocked.id); }),
|
|
399
|
-
userPlusUppercase && cleanup(function () { return sdk.api.users.deleteOne(userPlusUppercase.id); }),
|
|
400
|
-
userPlusInjection && cleanup(function () { return sdk.api.users.deleteOne(userPlusInjection.id); }),
|
|
401
|
-
userReadOnly && cleanup(function () { return sdk.api.users.deleteOne(userReadOnly.id); }),
|
|
402
|
-
userOtherBase && cleanup(function () { return sdk.api.users.deleteOne(userOtherBase.id); }),
|
|
403
|
-
userPrefixSwitcher && cleanup(function () { return sdk.api.users.deleteOne(userPrefixSwitcher.id); }),
|
|
404
|
-
userNonAdminPlus && cleanup(function () { return sdk.api.users.deleteOne(userNonAdminPlus.id); }),
|
|
405
|
-
userUnverifTarget && cleanup(function () { return sdk.api.users.deleteOne(userUnverifTarget.id); }),
|
|
406
|
-
userCrossOrg && cleanup(function () { return sdkOther.api.users.deleteOne(userCrossOrg.id); }),
|
|
407
|
-
restrictiveRole && cleanup(function () { return sdk.api.role_based_access_permissions.deleteOne(restrictiveRole.id); }),
|
|
408
|
-
])];
|
|
479
|
+
_5.sent();
|
|
480
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
409
481
|
case 57:
|
|
410
|
-
|
|
411
|
-
return [
|
|
412
|
-
|
|
482
|
+
_5.sent();
|
|
483
|
+
return [4 /*yield*/, (0, testing_1.async_test)('C6. Duplicate request inside window does not duplicate the entry', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) { var _a; return ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).length === preLen; } })
|
|
484
|
+
// C7: existing accepted -> no-op
|
|
485
|
+
];
|
|
486
|
+
case 58:
|
|
487
|
+
_5.sent();
|
|
488
|
+
return [4 /*yield*/, get_user(sdk, adminId)];
|
|
489
|
+
case 59:
|
|
490
|
+
adminBeforeAccept = _5.sent();
|
|
491
|
+
pendingEntry = ((_r = adminBeforeAccept.linkedAccountAccess) !== null && _r !== void 0 ? _r : []).find(function (e) { return e.userId === nonAdminId; });
|
|
492
|
+
return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, pendingEntry), { status: 'accepted' })])];
|
|
493
|
+
case 60:
|
|
494
|
+
_5.sent();
|
|
495
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
496
|
+
case 61:
|
|
497
|
+
_5.sent();
|
|
498
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
499
|
+
case 62:
|
|
500
|
+
_5.sent();
|
|
501
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
502
|
+
case 63:
|
|
503
|
+
_5.sent();
|
|
504
|
+
return [4 /*yield*/, (0, testing_1.async_test)('C7. Re-requesting on accepted entry is a no-op', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) {
|
|
505
|
+
var _a;
|
|
506
|
+
var entries = ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []);
|
|
507
|
+
var e = entries.find(function (x) { return x.userId === nonAdminId; });
|
|
508
|
+
return entries.length === 1 && (e === null || e === void 0 ? void 0 : e.status) === 'accepted';
|
|
509
|
+
} })
|
|
510
|
+
// Reset
|
|
511
|
+
];
|
|
512
|
+
case 64:
|
|
513
|
+
_5.sent();
|
|
514
|
+
// Reset
|
|
515
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)
|
|
516
|
+
// C9: email case-insensitivity (whitespace is rejected at the schema validator;
|
|
517
|
+
// case-insensitive matching happens after emailValidator lowercases the input).
|
|
518
|
+
];
|
|
519
|
+
case 65:
|
|
520
|
+
// Reset
|
|
521
|
+
_5.sent();
|
|
522
|
+
// C9: email case-insensitivity (whitespace is rejected at the schema validator;
|
|
523
|
+
// case-insensitive matching happens after emailValidator lowercases the input).
|
|
524
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail.toUpperCase() })];
|
|
525
|
+
case 66:
|
|
526
|
+
// C9: email case-insensitivity (whitespace is rejected at the schema validator;
|
|
527
|
+
// case-insensitive matching happens after emailValidator lowercases the input).
|
|
528
|
+
_5.sent();
|
|
529
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
530
|
+
case 67:
|
|
531
|
+
_5.sent();
|
|
532
|
+
return [4 /*yield*/, (0, testing_1.async_test)('C9. Email case-insensitive matching', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) { var _a; return ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).some(function (e) { return e.userId === nonAdminId; }); } })
|
|
533
|
+
// C10 is the request_linked_account_access rate-limit test. Because it exhausts admin's
|
|
534
|
+
// `request-linked-${adminId}` counter for 60s, and E5/E6/E7 setup needs admin to send a
|
|
535
|
+
// single request_linked_account_access (to be the source-side in the role-flipped lockout
|
|
536
|
+
// tests), C10 is intentionally relocated to the bottom of the suite (after I4) so no
|
|
537
|
+
// downstream test depends on admin's request_linked quota.
|
|
538
|
+
// ============================================================
|
|
539
|
+
// D. get_linked_accounts
|
|
540
|
+
// ============================================================
|
|
541
|
+
];
|
|
542
|
+
case 68:
|
|
543
|
+
_5.sent();
|
|
544
|
+
// C10 is the request_linked_account_access rate-limit test. Because it exhausts admin's
|
|
545
|
+
// `request-linked-${adminId}` counter for 60s, and E5/E6/E7 setup needs admin to send a
|
|
546
|
+
// single request_linked_account_access (to be the source-side in the role-flipped lockout
|
|
547
|
+
// tests), C10 is intentionally relocated to the bottom of the suite (after I4) so no
|
|
548
|
+
// downstream test depends on admin's request_linked quota.
|
|
549
|
+
// ============================================================
|
|
550
|
+
// D. get_linked_accounts
|
|
551
|
+
// ============================================================
|
|
552
|
+
(0, testing_1.log_header)("D. get_linked_accounts");
|
|
553
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
|
|
554
|
+
case 69:
|
|
555
|
+
_5.sent();
|
|
556
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
557
|
+
case 70:
|
|
558
|
+
_5.sent();
|
|
559
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
560
|
+
case 71:
|
|
561
|
+
_5.sent();
|
|
562
|
+
return [4 /*yield*/, get_user(sdk, adminId)];
|
|
563
|
+
case 72:
|
|
564
|
+
adminWithReq = _5.sent();
|
|
565
|
+
pendingForNonAdmin = ((_s = adminWithReq.linkedAccountAccess) !== null && _s !== void 0 ? _s : []).find(function (e) { return e.userId === nonAdminId; });
|
|
566
|
+
return [4 /*yield*/, (0, testing_1.async_test)('D2. Pending entry not returned', function () { return sdkNonAdmin.api.users.get_linked_accounts(); }, { shouldError: false, onResult: function (r) { var _a; return !((_a = r.linkedAccounts) !== null && _a !== void 0 ? _a : []).some(function (a) { return a.id === adminId; }); } })];
|
|
567
|
+
case 73:
|
|
568
|
+
_5.sent();
|
|
569
|
+
return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, pendingForNonAdmin), { status: 'accepted' })])];
|
|
570
|
+
case 74:
|
|
571
|
+
_5.sent();
|
|
572
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
573
|
+
case 75:
|
|
574
|
+
_5.sent();
|
|
575
|
+
return [4 /*yield*/, (0, testing_1.async_test)('D1. Accepted entry is returned', function () { return sdkNonAdmin.api.users.get_linked_accounts(); }, { shouldError: false, onResult: function (r) { var _a; return ((_a = r.linkedAccounts) !== null && _a !== void 0 ? _a : []).some(function (a) { return a.id === adminId; }); } })];
|
|
576
|
+
case 76:
|
|
577
|
+
_5.sent();
|
|
578
|
+
return [4 /*yield*/, (0, testing_1.async_test)('D5. Returned row has expected identity fields', function () { return sdkNonAdmin.api.users.get_linked_accounts(); }, { shouldError: false, onResult: function (r) {
|
|
579
|
+
var _a;
|
|
580
|
+
var row = ((_a = r.linkedAccounts) !== null && _a !== void 0 ? _a : []).find(function (a) { return a.id === adminId; });
|
|
581
|
+
return !!row && typeof row.email === 'string' && row.email.length > 0 && typeof row.orgName === 'string' && typeof row.requiresMFA === 'boolean';
|
|
582
|
+
} })];
|
|
583
|
+
case 77:
|
|
584
|
+
_5.sent();
|
|
585
|
+
return [4 /*yield*/, (0, testing_1.async_test)('D3. Self is excluded', function () { return sdkNonAdmin.api.users.get_linked_accounts(); }, { shouldError: false, onResult: function (r) { var _a; return !((_a = r.linkedAccounts) !== null && _a !== void 0 ? _a : []).some(function (a) { return a.id === nonAdminId; }); } })];
|
|
586
|
+
case 78:
|
|
587
|
+
_5.sent();
|
|
588
|
+
return [4 /*yield*/, (0, testing_1.async_test)('D6. Empty result for caller with no grants directed at them', function () { return sdk.api.users.get_linked_accounts(); }, { shouldError: false, onResult: function (r) { return Array.isArray(r.linkedAccounts) && r.linkedAccounts.length === 0; } })
|
|
589
|
+
// ============================================================
|
|
590
|
+
// E. switch_account — grant + accessibility
|
|
591
|
+
// ============================================================
|
|
592
|
+
];
|
|
593
|
+
case 79:
|
|
594
|
+
_5.sent();
|
|
595
|
+
// ============================================================
|
|
596
|
+
// E. switch_account — grant + accessibility
|
|
597
|
+
// ============================================================
|
|
598
|
+
(0, testing_1.log_header)("E. switch_account — grant + accessibility");
|
|
599
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
|
|
600
|
+
case 80:
|
|
601
|
+
_5.sent();
|
|
602
|
+
return [4 /*yield*/, (0, testing_1.async_test)('E1. No entry -> 403', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 403 || (e.message || '').includes('not granted'); } })];
|
|
603
|
+
case 81:
|
|
604
|
+
_5.sent();
|
|
605
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
606
|
+
case 82:
|
|
607
|
+
_5.sent();
|
|
608
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
609
|
+
case 83:
|
|
610
|
+
_5.sent();
|
|
611
|
+
return [4 /*yield*/, (0, testing_1.async_test)('E2. Only pending -> 403', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 403 || (e.message || '').includes('not granted'); } })];
|
|
612
|
+
case 84:
|
|
613
|
+
_5.sent();
|
|
614
|
+
return [4 /*yield*/, (0, testing_1.async_test)('E3. Self-switch -> 400', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: nonAdminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 400 || (e.message || '').includes('own account'); } })];
|
|
615
|
+
case 85:
|
|
616
|
+
_5.sent();
|
|
617
|
+
return [4 /*yield*/, (0, testing_1.async_test)('E4. Nonexistent target -> 404', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: '000000000000000000000000' }); }, { shouldError: true, onError: function (e) { return e.statusCode === 404 || (e.message || '').includes('not found'); } })
|
|
618
|
+
// E9. Malformed targetUserId -> 400 (mongoIdStringRequired schema validator)
|
|
619
|
+
];
|
|
620
|
+
case 86:
|
|
621
|
+
_5.sent();
|
|
622
|
+
// E9. Malformed targetUserId -> 400 (mongoIdStringRequired schema validator)
|
|
623
|
+
return [4 /*yield*/, (0, testing_1.async_test)('E9. Malformed targetUserId -> 400', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: 'not-a-mongo-id' }); }, { shouldError: true, onError: function (e) { return e.statusCode === 400 || /(invalid|mongoId|parsing|format)/i.test(e.message || ''); } })
|
|
624
|
+
// E5/E6/E7. Locked target -> 401. Flip roles: admin (only user who can write locked* fields) is the
|
|
625
|
+
// SOURCE, nonAdmin is the TARGET. nonAdmin grants admin access first.
|
|
626
|
+
// Run BEFORE E10's rate-limit exhaustion so admin's switch counter is still fresh here.
|
|
627
|
+
];
|
|
628
|
+
case 87:
|
|
629
|
+
// E9. Malformed targetUserId -> 400 (mongoIdStringRequired schema validator)
|
|
630
|
+
_5.sent();
|
|
631
|
+
// E5/E6/E7. Locked target -> 401. Flip roles: admin (only user who can write locked* fields) is the
|
|
632
|
+
// SOURCE, nonAdmin is the TARGET. nonAdmin grants admin access first.
|
|
633
|
+
// Run BEFORE E10's rate-limit exhaustion so admin's switch counter is still fresh here.
|
|
634
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdkNonAdmin, nonAdminId)];
|
|
635
|
+
case 88:
|
|
636
|
+
// E5/E6/E7. Locked target -> 401. Flip roles: admin (only user who can write locked* fields) is the
|
|
637
|
+
// SOURCE, nonAdmin is the TARGET. nonAdmin grants admin access first.
|
|
638
|
+
// Run BEFORE E10's rate-limit exhaustion so admin's switch counter is still fresh here.
|
|
639
|
+
_5.sent();
|
|
640
|
+
return [4 /*yield*/, sdk.api.users.request_linked_account_access({ targetEmail: nonAdminEmail })];
|
|
641
|
+
case 89:
|
|
642
|
+
_5.sent();
|
|
643
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
644
|
+
case 90:
|
|
645
|
+
_5.sent();
|
|
646
|
+
return [4 /*yield*/, get_user(sdkNonAdmin, nonAdminId)];
|
|
647
|
+
case 91:
|
|
648
|
+
nonAdminAfterReq = _5.sent();
|
|
649
|
+
pendingFromAdmin = ((_t = nonAdminAfterReq.linkedAccountAccess) !== null && _t !== void 0 ? _t : []).find(function (e) { return e.userId === adminId; });
|
|
650
|
+
if (!pendingFromAdmin) return [3 /*break*/, 94];
|
|
651
|
+
return [4 /*yield*/, set_linkedAccountAccess(sdkNonAdmin, nonAdminId, [__assign(__assign({}, pendingFromAdmin), { status: 'accepted' })])];
|
|
652
|
+
case 92:
|
|
653
|
+
_5.sent();
|
|
654
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
655
|
+
case 93:
|
|
656
|
+
_5.sent();
|
|
657
|
+
_5.label = 94;
|
|
658
|
+
case 94:
|
|
659
|
+
// E5: lockedOutUntil in the future
|
|
660
|
+
return [4 /*yield*/, sdk.api.users.updateOne(nonAdminId, { lockedOutUntil: Date.now() + 60000 })];
|
|
661
|
+
case 95:
|
|
662
|
+
// E5: lockedOutUntil in the future
|
|
663
|
+
_5.sent();
|
|
664
|
+
return [4 /*yield*/, (0, testing_1.async_test)('E5. Target with lockedOutUntil > now -> 401', function () { return sdk.api.users.switch_account({ targetUserId: nonAdminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 401 || /(locked|not accessible)/i.test(e.message || ''); } })
|
|
665
|
+
// E6: lockedOutUntil === 0 (indefinite lock)
|
|
666
|
+
];
|
|
667
|
+
case 96:
|
|
668
|
+
_5.sent();
|
|
669
|
+
// E6: lockedOutUntil === 0 (indefinite lock)
|
|
670
|
+
return [4 /*yield*/, sdk.api.users.updateOne(nonAdminId, { lockedOutUntil: 0 })];
|
|
671
|
+
case 97:
|
|
672
|
+
// E6: lockedOutUntil === 0 (indefinite lock)
|
|
673
|
+
_5.sent();
|
|
674
|
+
return [4 /*yield*/, (0, testing_1.async_test)('E6. Target with lockedOutUntil === 0 -> 401', function () { return sdk.api.users.switch_account({ targetUserId: nonAdminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 401 || /(locked|not accessible)/i.test(e.message || ''); } })
|
|
675
|
+
// E7: failedLoginAttempts >= 10
|
|
676
|
+
];
|
|
677
|
+
case 98:
|
|
678
|
+
_5.sent();
|
|
679
|
+
// E7: failedLoginAttempts >= 10
|
|
680
|
+
return [4 /*yield*/, sdk.api.users.updateOne(nonAdminId, { lockedOutUntil: -1, failedLoginAttempts: 10 })];
|
|
681
|
+
case 99:
|
|
682
|
+
// E7: failedLoginAttempts >= 10
|
|
683
|
+
_5.sent();
|
|
684
|
+
return [4 /*yield*/, (0, testing_1.async_test)('E7. Target with failedLoginAttempts >= 10 -> 401', function () { return sdk.api.users.switch_account({ targetUserId: nonAdminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 401 || /(locked|not accessible|failed login)/i.test(e.message || ''); } })
|
|
685
|
+
// Restore nonAdmin to a healthy state. Setting lockedOutUntil to 0/future in E5/E6 triggered
|
|
686
|
+
// deauthenticate_user(nonAdminId) via routing.ts:2742-2752, writing `deauthenticated-${id}`
|
|
687
|
+
// to cache with the current timestamp. is_logged_in rejects any token whose iat falls within
|
|
688
|
+
// the 1s slack window after that timestamp — so a re-auth too quickly afterward produces a
|
|
689
|
+
// token that gets immediately invalidated. Wait > 1s past E6's deauth before re-authing.
|
|
690
|
+
];
|
|
691
|
+
case 100:
|
|
692
|
+
_5.sent();
|
|
693
|
+
// Restore nonAdmin to a healthy state. Setting lockedOutUntil to 0/future in E5/E6 triggered
|
|
694
|
+
// deauthenticate_user(nonAdminId) via routing.ts:2742-2752, writing `deauthenticated-${id}`
|
|
695
|
+
// to cache with the current timestamp. is_logged_in rejects any token whose iat falls within
|
|
696
|
+
// the 1s slack window after that timestamp — so a re-auth too quickly afterward produces a
|
|
697
|
+
// token that gets immediately invalidated. Wait > 1s past E6's deauth before re-authing.
|
|
698
|
+
return [4 /*yield*/, sdk.api.users.updateOne(nonAdminId, { lockedOutUntil: -1, failedLoginAttempts: 0 })];
|
|
699
|
+
case 101:
|
|
700
|
+
// Restore nonAdmin to a healthy state. Setting lockedOutUntil to 0/future in E5/E6 triggered
|
|
701
|
+
// deauthenticate_user(nonAdminId) via routing.ts:2742-2752, writing `deauthenticated-${id}`
|
|
702
|
+
// to cache with the current timestamp. is_logged_in rejects any token whose iat falls within
|
|
703
|
+
// the 1s slack window after that timestamp — so a re-auth too quickly afterward produces a
|
|
704
|
+
// token that gets immediately invalidated. Wait > 1s past E6's deauth before re-authing.
|
|
705
|
+
_5.sent();
|
|
706
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 1500)];
|
|
707
|
+
case 102:
|
|
708
|
+
_5.sent();
|
|
709
|
+
return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)];
|
|
710
|
+
case 103:
|
|
711
|
+
_5.sent();
|
|
712
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdkNonAdmin, nonAdminId)
|
|
713
|
+
// E10. switch_account rate limit (20/min). Each failed switch consumes a quota slot
|
|
714
|
+
// (rate-limit check runs first; source token not invalidated). E5-E7 above already burned
|
|
715
|
+
// 3 slots from admin's switch-account counter, so prime 17 more to reach the limit, then
|
|
716
|
+
// assert the next call is 429. Keep this LAST in E — exhausts admin's switch counter.
|
|
717
|
+
];
|
|
718
|
+
case 104:
|
|
719
|
+
_5.sent();
|
|
720
|
+
i = 0;
|
|
721
|
+
_5.label = 105;
|
|
722
|
+
case 105:
|
|
723
|
+
if (!(i < 17)) return [3 /*break*/, 110];
|
|
724
|
+
_5.label = 106;
|
|
725
|
+
case 106:
|
|
726
|
+
_5.trys.push([106, 108, , 109]);
|
|
727
|
+
return [4 /*yield*/, sdk.api.users.switch_account({ targetUserId: '000000000000000000000000' })];
|
|
728
|
+
case 107:
|
|
729
|
+
_5.sent();
|
|
730
|
+
return [3 /*break*/, 109];
|
|
731
|
+
case 108:
|
|
732
|
+
_f = _5.sent();
|
|
733
|
+
return [3 /*break*/, 109];
|
|
734
|
+
case 109:
|
|
735
|
+
i++;
|
|
736
|
+
return [3 /*break*/, 105];
|
|
737
|
+
case 110: return [4 /*yield*/, (0, testing_1.async_test)('E10. Switch beyond 20/min is rate-limited (429)', function () { return sdk.api.users.switch_account({ targetUserId: '000000000000000000000000' }); }, { shouldError: true, onError: function (e) { return e.statusCode === 429 || (e.message || '').toLowerCase().includes('rate'); } })
|
|
738
|
+
// ============================================================
|
|
739
|
+
// F. switch_account — enforceMFA gap
|
|
740
|
+
// ============================================================
|
|
741
|
+
];
|
|
742
|
+
case 111:
|
|
743
|
+
_5.sent();
|
|
744
|
+
// ============================================================
|
|
745
|
+
// F. switch_account — enforceMFA gap
|
|
746
|
+
// ============================================================
|
|
747
|
+
(0, testing_1.log_header)("F. switch_account — enforceMFA gap");
|
|
748
|
+
// Set up accepted grant: nonAdmin requests, admin accepts. (E section cleared admin's array.)
|
|
749
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
|
|
750
|
+
case 112:
|
|
751
|
+
// Set up accepted grant: nonAdmin requests, admin accepts. (E section cleared admin's array.)
|
|
752
|
+
_5.sent();
|
|
753
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
754
|
+
case 113:
|
|
755
|
+
_5.sent();
|
|
756
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
757
|
+
case 114:
|
|
758
|
+
_5.sent();
|
|
759
|
+
return [4 /*yield*/, get_user(sdk, adminId)];
|
|
760
|
+
case 115:
|
|
761
|
+
adminForF = _5.sent();
|
|
762
|
+
pendingForF = ((_u = adminForF.linkedAccountAccess) !== null && _u !== void 0 ? _u : []).find(function (e) { return e.userId === nonAdminId; });
|
|
763
|
+
if (!pendingForF) return [3 /*break*/, 118];
|
|
764
|
+
return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, pendingForF), { status: 'accepted' })])];
|
|
765
|
+
case 116:
|
|
766
|
+
_5.sent();
|
|
767
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
768
|
+
case 117:
|
|
769
|
+
_5.sent();
|
|
770
|
+
_5.label = 118;
|
|
771
|
+
case 118:
|
|
772
|
+
// Enable enforceMFA on the test business; admin (target) currently has no MFA configured.
|
|
773
|
+
return [4 /*yield*/, sdk.api.organizations.updateOne(adminBusinessId, { enforceMFA: true })];
|
|
774
|
+
case 119:
|
|
775
|
+
// Enable enforceMFA on the test business; admin (target) currently has no MFA configured.
|
|
776
|
+
_5.sent();
|
|
777
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)
|
|
778
|
+
// F1: target with no MFA but org enforces -> 403
|
|
779
|
+
];
|
|
780
|
+
case 120:
|
|
781
|
+
_5.sent();
|
|
782
|
+
// F1: target with no MFA but org enforces -> 403
|
|
783
|
+
return [4 /*yield*/, (0, testing_1.async_test)('F1. enforceMFA on org + target MFA not configured -> 403', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 403 || /(MFA configuration|enforceMFA)/i.test(e.message || ''); } })
|
|
784
|
+
// F2: admin configures MFA, switch now succeeds — but the switched JWT has requiresMFA: true.
|
|
785
|
+
];
|
|
786
|
+
case 121:
|
|
787
|
+
// F1: target with no MFA but org enforces -> 403
|
|
788
|
+
_5.sent();
|
|
789
|
+
// F2: admin configures MFA, switch now succeeds — but the switched JWT has requiresMFA: true.
|
|
790
|
+
return [4 /*yield*/, sdk.api.users.configure_MFA({ disable: false })];
|
|
791
|
+
case 122:
|
|
792
|
+
// F2: admin configures MFA, switch now succeeds — but the switched JWT has requiresMFA: true.
|
|
793
|
+
_5.sent();
|
|
794
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
795
|
+
case 123:
|
|
796
|
+
_5.sent();
|
|
797
|
+
return [4 /*yield*/, (0, testing_1.async_test)('F2. After target configures MFA, switch succeeds and switched JWT has requiresMFA: true', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: false, onResult: function (r) {
|
|
798
|
+
var decoded = decode_jwt(r.authToken);
|
|
799
|
+
return !!r.authToken && (decoded === null || decoded === void 0 ? void 0 : decoded.requiresMFA) === true;
|
|
800
|
+
} })
|
|
801
|
+
// Teardown F: revert enforceMFA first (configure_MFA(disable=true) refuses while enforced),
|
|
802
|
+
// then disable MFA on admin. Re-auth nonAdmin since its source token was invalidated by F2's switch.
|
|
803
|
+
];
|
|
804
|
+
case 124:
|
|
805
|
+
_5.sent();
|
|
806
|
+
// Teardown F: revert enforceMFA first (configure_MFA(disable=true) refuses while enforced),
|
|
807
|
+
// then disable MFA on admin. Re-auth nonAdmin since its source token was invalidated by F2's switch.
|
|
808
|
+
return [4 /*yield*/, sdk.api.organizations.updateOne(adminBusinessId, { enforceMFA: false })];
|
|
809
|
+
case 125:
|
|
810
|
+
// Teardown F: revert enforceMFA first (configure_MFA(disable=true) refuses while enforced),
|
|
811
|
+
// then disable MFA on admin. Re-auth nonAdmin since its source token was invalidated by F2's switch.
|
|
812
|
+
_5.sent();
|
|
813
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
814
|
+
case 126:
|
|
815
|
+
_5.sent();
|
|
816
|
+
_5.label = 127;
|
|
817
|
+
case 127:
|
|
818
|
+
_5.trys.push([127, 129, , 130]);
|
|
819
|
+
return [4 /*yield*/, sdk.api.users.configure_MFA({ disable: true })];
|
|
820
|
+
case 128:
|
|
821
|
+
_5.sent();
|
|
822
|
+
return [3 /*break*/, 130];
|
|
823
|
+
case 129:
|
|
824
|
+
_g = _5.sent();
|
|
825
|
+
return [3 /*break*/, 130];
|
|
826
|
+
case 130: return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
|
|
827
|
+
// ============================================================
|
|
828
|
+
// G. switch_account — JWT + audit
|
|
829
|
+
// ============================================================
|
|
830
|
+
];
|
|
831
|
+
case 131:
|
|
832
|
+
_5.sent();
|
|
833
|
+
// ============================================================
|
|
834
|
+
// G. switch_account — JWT + audit
|
|
835
|
+
// ============================================================
|
|
836
|
+
(0, testing_1.log_header)("G. switch_account — JWT + audit");
|
|
837
|
+
// Set up an accepted grant. clear_linkedAccountAccess removes F's accepted entry, which
|
|
838
|
+
// writes the `grant-revoked-${adminId}-${nonAdminId}` cache key. is_logged_in compares that
|
|
839
|
+
// key against the new JWT's iat with a 1s slack (accounts for JWT iat second-rounding), so
|
|
840
|
+
// we must ensure the new grant is minted well past that window or the freshly-switched
|
|
841
|
+
// token gets rejected as if it were a pre-revocation token. Wait > 1s between the cleanup
|
|
842
|
+
// and the new switch.
|
|
843
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
|
|
844
|
+
case 132:
|
|
845
|
+
// Set up an accepted grant. clear_linkedAccountAccess removes F's accepted entry, which
|
|
846
|
+
// writes the `grant-revoked-${adminId}-${nonAdminId}` cache key. is_logged_in compares that
|
|
847
|
+
// key against the new JWT's iat with a 1s slack (accounts for JWT iat second-rounding), so
|
|
848
|
+
// we must ensure the new grant is minted well past that window or the freshly-switched
|
|
849
|
+
// token gets rejected as if it were a pre-revocation token. Wait > 1s between the cleanup
|
|
850
|
+
// and the new switch.
|
|
851
|
+
_5.sent();
|
|
852
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 1500)];
|
|
853
|
+
case 133:
|
|
854
|
+
_5.sent();
|
|
855
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
856
|
+
case 134:
|
|
857
|
+
_5.sent();
|
|
858
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
859
|
+
case 135:
|
|
860
|
+
_5.sent();
|
|
861
|
+
return [4 /*yield*/, get_user(sdk, adminId)];
|
|
862
|
+
case 136:
|
|
863
|
+
adminPendingState = _5.sent();
|
|
864
|
+
pendingNA = ((_v = adminPendingState.linkedAccountAccess) !== null && _v !== void 0 ? _v : []).find(function (e) { return e.userId === nonAdminId; });
|
|
865
|
+
return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, pendingNA), { status: 'accepted' })])];
|
|
866
|
+
case 137:
|
|
867
|
+
_5.sent();
|
|
868
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
869
|
+
case 138:
|
|
870
|
+
_5.sent();
|
|
871
|
+
switchedToken = '';
|
|
872
|
+
switchedUser = null;
|
|
873
|
+
return [4 /*yield*/, (0, testing_1.async_test)('G0. Switch succeeds when accepted grant exists', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: false, onResult: function (r) {
|
|
874
|
+
var _a;
|
|
875
|
+
switchedToken = r.authToken;
|
|
876
|
+
switchedUser = r.user;
|
|
877
|
+
return typeof r.authToken === 'string' && r.authToken.length > 0 && ((_a = r.user) === null || _a === void 0 ? void 0 : _a.id) === adminId;
|
|
878
|
+
} })];
|
|
879
|
+
case 139:
|
|
880
|
+
_5.sent();
|
|
881
|
+
decoded = decode_jwt(switchedToken);
|
|
882
|
+
(0, testing_1.assert)(!!decoded, 'JWT decode failed', 'G1. JWT decoded');
|
|
883
|
+
(0, testing_1.assert)((decoded === null || decoded === void 0 ? void 0 : decoded.id) === adminId, "JWT.id ".concat(decoded === null || decoded === void 0 ? void 0 : decoded.id, " != expected ").concat(adminId), 'G1. JWT.id == target');
|
|
884
|
+
(0, testing_1.assert)((decoded === null || decoded === void 0 ? void 0 : decoded.actorUserId) === nonAdminId, "JWT.actorUserId mismatch", 'G1. JWT.actorUserId == source');
|
|
885
|
+
(0, testing_1.assert)((decoded === null || decoded === void 0 ? void 0 : decoded.actorEmail) === nonAdminEmail, "JWT.actorEmail mismatch", 'G1. JWT.actorEmail');
|
|
886
|
+
(0, testing_1.assert)((decoded === null || decoded === void 0 ? void 0 : decoded.actorBusinessId) === nonAdminBusinessId, "JWT.actorBusinessId mismatch", 'G1. JWT.actorBusinessId');
|
|
887
|
+
return [4 /*yield*/, (0, testing_1.async_test)('G2. Pre-switch nonAdmin token is invalidated', function () { return sdkNonAdmin.test_authenticated(); }, { shouldError: true, onError: function () { return true; } })];
|
|
888
|
+
case 140:
|
|
889
|
+
_5.sent();
|
|
890
|
+
switchedSdk = new sdk_1.Session({ host: host });
|
|
891
|
+
switchedSdk.setAuthToken(switchedToken);
|
|
892
|
+
switchedSdk.setUserInfo(switchedUser);
|
|
893
|
+
return [4 /*yield*/, (0, testing_1.async_test)('G3. Switched token authenticates', function () { return switchedSdk.test_authenticated(); }, { shouldError: false, onResult: function (r) { return r === 'Authenticated!'; } })];
|
|
894
|
+
case 141:
|
|
895
|
+
_5.sent();
|
|
896
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 500)];
|
|
897
|
+
case 142:
|
|
898
|
+
_5.sent();
|
|
899
|
+
return [4 /*yield*/, (0, testing_1.async_test)('G4. user_logs has account_switch event with full info', function () { return sdk.api.user_logs.getOne({ resourceId: adminId, resource: 'users', action: 'update' }); }, { shouldError: false, onResult: function (log) {
|
|
900
|
+
var _a;
|
|
901
|
+
var info = (_a = log === null || log === void 0 ? void 0 : log.info) !== null && _a !== void 0 ? _a : {};
|
|
902
|
+
return (log === null || log === void 0 ? void 0 : log.userId) === nonAdminId
|
|
903
|
+
&& info.event === 'account_switch'
|
|
904
|
+
&& info.sourceUserId === nonAdminId
|
|
905
|
+
&& info.sourceEmail === nonAdminEmail
|
|
906
|
+
&& info.sourceBusinessId === nonAdminBusinessId
|
|
907
|
+
&& info.targetUserId === adminId
|
|
908
|
+
&& info.targetEmail === adminEmail
|
|
909
|
+
&& info.targetBusinessId === adminBusinessId;
|
|
910
|
+
} })
|
|
911
|
+
// G5: downstream user_log under switched session carries actorUserId.
|
|
912
|
+
// Capture original fname so we can restore it during cleanup — downstream tests
|
|
913
|
+
// (e.g. Calendar RSVPs) compare userInfo.fname against server-side values.
|
|
914
|
+
];
|
|
915
|
+
case 143:
|
|
916
|
+
_5.sent();
|
|
917
|
+
originalAdminFname = sdk.userInfo.fname;
|
|
918
|
+
return [4 /*yield*/, switchedSdk.api.users.updateOne(adminId, { fname: "Switched-".concat(RAND()) })];
|
|
919
|
+
case 144:
|
|
920
|
+
_5.sent();
|
|
921
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 500)];
|
|
922
|
+
case 145:
|
|
923
|
+
_5.sent();
|
|
924
|
+
return [4 /*yield*/, (0, testing_1.async_test)('G5. Downstream user_log under switched session has actorUserId', function () { return sdk.api.user_logs.getSome({ filter: { resourceId: adminId, resource: 'users', action: 'update' } }); }, { shouldError: false, onResult: function (logs) { return (logs !== null && logs !== void 0 ? logs : []).some(function (l) { return l.actorUserId === nonAdminId && l.userId === adminId; }); } })
|
|
925
|
+
// G6. PHI-adjacent collection: create an enduser through the switched session and assert
|
|
926
|
+
// the auto-emitted CRUD user_log carries actorUserId (exercises routing.ts inline insert
|
|
927
|
+
// path, not just storeUserLog or the users-collection update path).
|
|
928
|
+
];
|
|
929
|
+
case 146:
|
|
930
|
+
_5.sent();
|
|
931
|
+
g6EnduserId = '';
|
|
932
|
+
_5.label = 147;
|
|
933
|
+
case 147:
|
|
934
|
+
_5.trys.push([147, 149, , 150]);
|
|
935
|
+
return [4 /*yield*/, switchedSdk.api.endusers.createOne({ fname: 'Switch', lname: 'Test', email: "switch-test-".concat(RAND(), "@tellescope.example") })];
|
|
936
|
+
case 148:
|
|
937
|
+
ce = _5.sent();
|
|
938
|
+
g6EnduserId = ce.id;
|
|
939
|
+
return [3 /*break*/, 150];
|
|
940
|
+
case 149:
|
|
941
|
+
e_1 = _5.sent();
|
|
942
|
+
return [3 /*break*/, 150];
|
|
943
|
+
case 150: return [4 /*yield*/, (0, testing_1.wait)(undefined, 500)];
|
|
944
|
+
case 151:
|
|
945
|
+
_5.sent();
|
|
946
|
+
return [4 /*yield*/, (0, testing_1.async_test)('G6. Downstream enduser create under switched session has actorUserId', function () { return sdk.api.user_logs.getSome({ filter: { resource: 'endusers', action: 'create' } }); }, { shouldError: false, onResult: function (logs) { return (logs !== null && logs !== void 0 ? logs : []).some(function (l) { return l.actorUserId === nonAdminId && l.userId === adminId && (g6EnduserId ? l.resourceId === g6EnduserId : true); }); } })];
|
|
947
|
+
case 152:
|
|
948
|
+
_5.sent();
|
|
949
|
+
if (!g6EnduserId) return [3 /*break*/, 156];
|
|
950
|
+
_5.label = 153;
|
|
951
|
+
case 153:
|
|
952
|
+
_5.trys.push([153, 155, , 156]);
|
|
953
|
+
return [4 /*yield*/, sdk.api.endusers.deleteOne(g6EnduserId)];
|
|
954
|
+
case 154:
|
|
955
|
+
_5.sent();
|
|
956
|
+
return [3 /*break*/, 156];
|
|
957
|
+
case 155:
|
|
958
|
+
_h = _5.sent();
|
|
959
|
+
return [3 /*break*/, 156];
|
|
960
|
+
case 156:
|
|
961
|
+
// G7. Cross-org boundary assertion: the switched JWT operates in the TARGET's business.
|
|
962
|
+
// If a future change ever gates cross-org switching, this assertion will fire.
|
|
963
|
+
(0, testing_1.assert)((decoded === null || decoded === void 0 ? void 0 : decoded.businessId) === adminBusinessId, "JWT.businessId mismatch (got ".concat(decoded === null || decoded === void 0 ? void 0 : decoded.businessId, ", expected ").concat(adminBusinessId, ")"), 'G7. JWT.businessId == target businessId');
|
|
964
|
+
// ============================================================
|
|
965
|
+
// H. Real-time revocation
|
|
966
|
+
// ============================================================
|
|
967
|
+
(0, testing_1.log_header)("H. Real-time revocation");
|
|
968
|
+
return [4 /*yield*/, (0, testing_1.async_test)('H1. Baseline: switched session reads OK', function () { return switchedSdk.test_authenticated(); }, { shouldError: false, onResult: function (r) { return r === 'Authenticated!'; } })
|
|
969
|
+
// Revoke
|
|
970
|
+
];
|
|
971
|
+
case 157:
|
|
972
|
+
_5.sent();
|
|
973
|
+
// Revoke
|
|
974
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
|
|
975
|
+
case 158:
|
|
976
|
+
// Revoke
|
|
977
|
+
_5.sent();
|
|
978
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 750)];
|
|
979
|
+
case 159:
|
|
980
|
+
_5.sent();
|
|
981
|
+
return [4 /*yield*/, (0, testing_1.async_test)('H2. Switched session 401 after revoke', function () { return switchedSdk.test_authenticated(); }, { shouldError: true, onError: is401Rejection })];
|
|
982
|
+
case 160:
|
|
983
|
+
_5.sent();
|
|
984
|
+
return [4 /*yield*/, (0, testing_1.async_test)('H3. Owner own session still works (no over-broad invalidation)', function () { return sdk.test_authenticated(); }, { shouldError: false, onResult: function (r) { return r === 'Authenticated!'; } })
|
|
985
|
+
// H6. After revoke, a brand-new switch_account attempt is also rejected (covers the
|
|
986
|
+
// net-new path, complementing H2 which covers the already-minted session path).
|
|
987
|
+
// nonAdmin's source token from G0 was invalidated by that successful switch; re-auth first.
|
|
988
|
+
];
|
|
989
|
+
case 161:
|
|
990
|
+
_5.sent();
|
|
991
|
+
// H6. After revoke, a brand-new switch_account attempt is also rejected (covers the
|
|
992
|
+
// net-new path, complementing H2 which covers the already-minted session path).
|
|
993
|
+
// nonAdmin's source token from G0 was invalidated by that successful switch; re-auth first.
|
|
994
|
+
return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)];
|
|
995
|
+
case 162:
|
|
996
|
+
// H6. After revoke, a brand-new switch_account attempt is also rejected (covers the
|
|
997
|
+
// net-new path, complementing H2 which covers the already-minted session path).
|
|
998
|
+
// nonAdmin's source token from G0 was invalidated by that successful switch; re-auth first.
|
|
999
|
+
_5.sent();
|
|
1000
|
+
return [4 /*yield*/, (0, testing_1.async_test)('H6. New switch_account after revoke -> 403', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 403 || (e.message || '').includes('not granted'); } })
|
|
1001
|
+
// H5: reject of pending entry does not write a stale revocation key
|
|
1002
|
+
];
|
|
1003
|
+
case 163:
|
|
1004
|
+
_5.sent();
|
|
1005
|
+
// H5: reject of pending entry does not write a stale revocation key
|
|
1006
|
+
return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)];
|
|
1007
|
+
case 164:
|
|
1008
|
+
// H5: reject of pending entry does not write a stale revocation key
|
|
1009
|
+
_5.sent();
|
|
1010
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
1011
|
+
case 165:
|
|
1012
|
+
_5.sent();
|
|
1013
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
1014
|
+
case 166:
|
|
1015
|
+
_5.sent();
|
|
1016
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)]; // reject pending
|
|
1017
|
+
case 167:
|
|
1018
|
+
_5.sent(); // reject pending
|
|
1019
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
1020
|
+
case 168:
|
|
1021
|
+
_5.sent();
|
|
1022
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
1023
|
+
case 169:
|
|
1024
|
+
_5.sent();
|
|
1025
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
1026
|
+
case 170:
|
|
1027
|
+
_5.sent();
|
|
1028
|
+
return [4 /*yield*/, get_user(sdk, adminId)];
|
|
1029
|
+
case 171:
|
|
1030
|
+
state = _5.sent();
|
|
1031
|
+
newPending = ((_w = state.linkedAccountAccess) !== null && _w !== void 0 ? _w : []).find(function (e) { return e.userId === nonAdminId; });
|
|
1032
|
+
return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, newPending), { status: 'accepted' })])];
|
|
1033
|
+
case 172:
|
|
1034
|
+
_5.sent();
|
|
1035
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 500)];
|
|
1036
|
+
case 173:
|
|
1037
|
+
_5.sent();
|
|
1038
|
+
return [4 /*yield*/, (0, testing_1.async_test)('H5. New switched session works after a prior reject (no stale revocation)', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: false, onResult: function (r) { return typeof r.authToken === 'string' && r.authToken.length > 0; } })];
|
|
1039
|
+
case 174:
|
|
1040
|
+
_5.sent();
|
|
1041
|
+
return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
|
|
1042
|
+
// ============================================================
|
|
1043
|
+
// O. Org-toggle gating (accountSwitchingEnabled)
|
|
1044
|
+
// ============================================================
|
|
1045
|
+
];
|
|
1046
|
+
case 175:
|
|
1047
|
+
_5.sent();
|
|
1048
|
+
// ============================================================
|
|
1049
|
+
// O. Org-toggle gating (accountSwitchingEnabled)
|
|
1050
|
+
// ============================================================
|
|
1051
|
+
(0, testing_1.log_header)("O. Org-toggle gating");
|
|
1052
|
+
// Pre-stage an accepted entry while the toggle is ON (it currently is).
|
|
1053
|
+
// Wait > 1s after the cleanup so the (adminId, nonAdminId) revocation key from H sits
|
|
1054
|
+
// clearly before the new switch's iat — is_logged_in's 1s slack would otherwise reject
|
|
1055
|
+
// freshly minted tokens as if they were pre-revocation.
|
|
1056
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
|
|
1057
|
+
case 176:
|
|
1058
|
+
// Pre-stage an accepted entry while the toggle is ON (it currently is).
|
|
1059
|
+
// Wait > 1s after the cleanup so the (adminId, nonAdminId) revocation key from H sits
|
|
1060
|
+
// clearly before the new switch's iat — is_logged_in's 1s slack would otherwise reject
|
|
1061
|
+
// freshly minted tokens as if they were pre-revocation.
|
|
1062
|
+
_5.sent();
|
|
1063
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 1500)];
|
|
1064
|
+
case 177:
|
|
1065
|
+
_5.sent();
|
|
1066
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
1067
|
+
case 178:
|
|
1068
|
+
_5.sent();
|
|
1069
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
1070
|
+
case 179:
|
|
1071
|
+
_5.sent();
|
|
1072
|
+
return [4 /*yield*/, get_user(sdk, adminId)];
|
|
1073
|
+
case 180:
|
|
1074
|
+
oSeedState = _5.sent();
|
|
1075
|
+
oPending = ((_x = oSeedState.linkedAccountAccess) !== null && _x !== void 0 ? _x : []).find(function (e) { return e.userId === nonAdminId; });
|
|
1076
|
+
if (!oPending) return [3 /*break*/, 183];
|
|
1077
|
+
return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, oPending), { status: 'accepted' })])];
|
|
1078
|
+
case 181:
|
|
1079
|
+
_5.sent();
|
|
1080
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
1081
|
+
case 182:
|
|
1082
|
+
_5.sent();
|
|
1083
|
+
_5.label = 183;
|
|
1084
|
+
case 183:
|
|
1085
|
+
// Toggle OFF.
|
|
1086
|
+
return [4 /*yield*/, sdk.api.organizations.updateOne(adminBusinessId, { accountSwitchingEnabled: false })];
|
|
1087
|
+
case 184:
|
|
1088
|
+
// Toggle OFF.
|
|
1089
|
+
_5.sent();
|
|
1090
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)
|
|
1091
|
+
// O1. request_linked_account_access silently no-ops while toggle is off.
|
|
1092
|
+
];
|
|
1093
|
+
case 185:
|
|
1094
|
+
_5.sent();
|
|
1095
|
+
return [4 /*yield*/, get_user(sdk, adminId)];
|
|
1096
|
+
case 186:
|
|
1097
|
+
oBefore = _5.sent();
|
|
1098
|
+
oBeforeLen = ((_y = oBefore.linkedAccountAccess) !== null && _y !== void 0 ? _y : []).length;
|
|
1099
|
+
return [4 /*yield*/, (0, testing_1.async_test)('O1. request_linked_account_access returns {} while toggle is off', function () { return sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail }); }, { shouldError: false, onResult: function () { return true; } })];
|
|
1100
|
+
case 187:
|
|
1101
|
+
_5.sent();
|
|
1102
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
1103
|
+
case 188:
|
|
1104
|
+
_5.sent();
|
|
1105
|
+
return [4 /*yield*/, (0, testing_1.async_test)('O1. No new record written while toggle is off', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) { var _a; return ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).length === oBeforeLen; } })
|
|
1106
|
+
// O2. switch_account on a pre-existing accepted grant -> 403 while toggle is off.
|
|
1107
|
+
];
|
|
1108
|
+
case 189:
|
|
1109
|
+
_5.sent();
|
|
1110
|
+
// O2. switch_account on a pre-existing accepted grant -> 403 while toggle is off.
|
|
1111
|
+
return [4 /*yield*/, (0, testing_1.async_test)('O2. switch_account -> 403 while target org has toggle off', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 403 || /(organization has not enabled|switching)/i.test(e.message || ''); } })
|
|
1112
|
+
// O5. With the toggle off, the SOURCE-org check fires first (before the target check),
|
|
1113
|
+
// so switch_account responds with "Your organization has not enabled..." rather than
|
|
1114
|
+
// "Target organization has not enabled...". Single-org fixture means source==target here;
|
|
1115
|
+
// the error-message assertion is what proves the source-side gate is actually firing
|
|
1116
|
+
// (without it, O2 would have passed under the old target-only implementation too).
|
|
1117
|
+
];
|
|
1118
|
+
case 190:
|
|
1119
|
+
// O2. switch_account on a pre-existing accepted grant -> 403 while toggle is off.
|
|
1120
|
+
_5.sent();
|
|
1121
|
+
// O5. With the toggle off, the SOURCE-org check fires first (before the target check),
|
|
1122
|
+
// so switch_account responds with "Your organization has not enabled..." rather than
|
|
1123
|
+
// "Target organization has not enabled...". Single-org fixture means source==target here;
|
|
1124
|
+
// the error-message assertion is what proves the source-side gate is actually firing
|
|
1125
|
+
// (without it, O2 would have passed under the old target-only implementation too).
|
|
1126
|
+
return [4 /*yield*/, (0, testing_1.async_test)('O5. switch_account error message names the actor org (source-side check fires)', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: true, onError: function (e) { return /your organization/i.test(e.message || ''); } })
|
|
1127
|
+
// Toggle ON again.
|
|
1128
|
+
];
|
|
1129
|
+
case 191:
|
|
1130
|
+
// O5. With the toggle off, the SOURCE-org check fires first (before the target check),
|
|
1131
|
+
// so switch_account responds with "Your organization has not enabled..." rather than
|
|
1132
|
+
// "Target organization has not enabled...". Single-org fixture means source==target here;
|
|
1133
|
+
// the error-message assertion is what proves the source-side gate is actually firing
|
|
1134
|
+
// (without it, O2 would have passed under the old target-only implementation too).
|
|
1135
|
+
_5.sent();
|
|
1136
|
+
// Toggle ON again.
|
|
1137
|
+
return [4 /*yield*/, sdk.api.organizations.updateOne(adminBusinessId, { accountSwitchingEnabled: true })];
|
|
1138
|
+
case 192:
|
|
1139
|
+
// Toggle ON again.
|
|
1140
|
+
_5.sent();
|
|
1141
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)
|
|
1142
|
+
// O3. switch_account now succeeds (same accepted grant as before).
|
|
1143
|
+
];
|
|
1144
|
+
case 193:
|
|
1145
|
+
_5.sent();
|
|
1146
|
+
// O3. switch_account now succeeds (same accepted grant as before).
|
|
1147
|
+
return [4 /*yield*/, (0, testing_1.async_test)('O3. switch_account succeeds once toggle is re-enabled', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: false, onResult: function (r) { return typeof r.authToken === 'string' && r.authToken.length > 0; } })
|
|
1148
|
+
// nonAdmin's source token was invalidated by the switch; re-auth for subsequent tests.
|
|
1149
|
+
];
|
|
1150
|
+
case 194:
|
|
1151
|
+
// O3. switch_account now succeeds (same accepted grant as before).
|
|
1152
|
+
_5.sent();
|
|
1153
|
+
// nonAdmin's source token was invalidated by the switch; re-auth for subsequent tests.
|
|
1154
|
+
return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
|
|
1155
|
+
// O4. request_linked_account_access writes a record once toggle is re-enabled.
|
|
1156
|
+
];
|
|
1157
|
+
case 195:
|
|
1158
|
+
// nonAdmin's source token was invalidated by the switch; re-auth for subsequent tests.
|
|
1159
|
+
_5.sent();
|
|
1160
|
+
// O4. request_linked_account_access writes a record once toggle is re-enabled.
|
|
1161
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
|
|
1162
|
+
case 196:
|
|
1163
|
+
// O4. request_linked_account_access writes a record once toggle is re-enabled.
|
|
1164
|
+
_5.sent();
|
|
1165
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
1166
|
+
case 197:
|
|
1167
|
+
_5.sent();
|
|
1168
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
1169
|
+
case 198:
|
|
1170
|
+
_5.sent();
|
|
1171
|
+
return [4 /*yield*/, (0, testing_1.async_test)('O4. request_linked_account_access writes a pending entry once toggle is on', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) { var _a; return ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).some(function (e) { return e.userId === nonAdminId && e.status === 'pending'; }); } })
|
|
1172
|
+
// ============================================================
|
|
1173
|
+
// I. Cross-cutting / regressions
|
|
1174
|
+
// ============================================================
|
|
1175
|
+
];
|
|
1176
|
+
case 199:
|
|
1177
|
+
_5.sent();
|
|
1178
|
+
// ============================================================
|
|
1179
|
+
// I. Cross-cutting / regressions
|
|
1180
|
+
// ============================================================
|
|
1181
|
+
(0, testing_1.log_header)("I. Cross-cutting / regressions");
|
|
1182
|
+
return [4 /*yield*/, (0, testing_1.async_test)('I1. Legacy users.updateOne({ accountAccessGrantedTo: [...] }) rejected', function () { return sdk.api.users.updateOne(adminId, { accountAccessGrantedTo: [nonAdminId] }); }, { shouldError: true, onError: function (e) { return e.statusCode === 400 || /(accountAccessGrantedTo|legacy|replaced|No updates provided)/i.test(e.message || ''); } })];
|
|
1183
|
+
case 200:
|
|
1184
|
+
_5.sent();
|
|
1185
|
+
return [4 /*yield*/, (0, testing_1.async_test)('I2. After full revoke, get_linked_accounts no longer shows A', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1186
|
+
var r;
|
|
1187
|
+
var _a;
|
|
1188
|
+
return __generator(this, function (_b) {
|
|
1189
|
+
switch (_b.label) {
|
|
1190
|
+
case 0: return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
|
|
1191
|
+
case 1:
|
|
1192
|
+
_b.sent();
|
|
1193
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
1194
|
+
case 2:
|
|
1195
|
+
_b.sent();
|
|
1196
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.get_linked_accounts()];
|
|
1197
|
+
case 3:
|
|
1198
|
+
r = _b.sent();
|
|
1199
|
+
return [2 /*return*/, ((_a = r.linkedAccounts) !== null && _a !== void 0 ? _a : []).find(function (a) { return a.id === adminId; })];
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
}); }, { shouldError: false, onResult: function (r) { return r === undefined; } })
|
|
1203
|
+
// I4. Accepted-grant expiration semantics — lock in the chosen behavior.
|
|
1204
|
+
// The switch handler does NOT check requestExpiresAt for accepted entries: that field
|
|
1205
|
+
// only governs the *pending* TTL. If this changes, this test will fail; that's the
|
|
1206
|
+
// signal to make the behavior choice deliberately. (Simulating an actually-expired
|
|
1207
|
+
// accepted grant requires either waiting 7 days or mutating requestExpiresAt — the
|
|
1208
|
+
// schema validator blocks the latter, so we lock in the behavior by inspection.)
|
|
1209
|
+
// I2 above did clear_linkedAccountAccess → wait > 1s past the resulting revocation key
|
|
1210
|
+
// before re-granting; otherwise is_logged_in's 1s slack rejects the new switched token.
|
|
1211
|
+
];
|
|
1212
|
+
case 201:
|
|
1213
|
+
_5.sent();
|
|
1214
|
+
// I4. Accepted-grant expiration semantics — lock in the chosen behavior.
|
|
1215
|
+
// The switch handler does NOT check requestExpiresAt for accepted entries: that field
|
|
1216
|
+
// only governs the *pending* TTL. If this changes, this test will fail; that's the
|
|
1217
|
+
// signal to make the behavior choice deliberately. (Simulating an actually-expired
|
|
1218
|
+
// accepted grant requires either waiting 7 days or mutating requestExpiresAt — the
|
|
1219
|
+
// schema validator blocks the latter, so we lock in the behavior by inspection.)
|
|
1220
|
+
// I2 above did clear_linkedAccountAccess → wait > 1s past the resulting revocation key
|
|
1221
|
+
// before re-granting; otherwise is_logged_in's 1s slack rejects the new switched token.
|
|
1222
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 1500)];
|
|
1223
|
+
case 202:
|
|
1224
|
+
// I4. Accepted-grant expiration semantics — lock in the chosen behavior.
|
|
1225
|
+
// The switch handler does NOT check requestExpiresAt for accepted entries: that field
|
|
1226
|
+
// only governs the *pending* TTL. If this changes, this test will fail; that's the
|
|
1227
|
+
// signal to make the behavior choice deliberately. (Simulating an actually-expired
|
|
1228
|
+
// accepted grant requires either waiting 7 days or mutating requestExpiresAt — the
|
|
1229
|
+
// schema validator blocks the latter, so we lock in the behavior by inspection.)
|
|
1230
|
+
// I2 above did clear_linkedAccountAccess → wait > 1s past the resulting revocation key
|
|
1231
|
+
// before re-granting; otherwise is_logged_in's 1s slack rejects the new switched token.
|
|
1232
|
+
_5.sent();
|
|
1233
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
1234
|
+
case 203:
|
|
1235
|
+
_5.sent();
|
|
1236
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
1237
|
+
case 204:
|
|
1238
|
+
_5.sent();
|
|
1239
|
+
return [4 /*yield*/, get_user(sdk, adminId)];
|
|
1240
|
+
case 205:
|
|
1241
|
+
i4State = _5.sent();
|
|
1242
|
+
i4Pending = ((_z = i4State.linkedAccountAccess) !== null && _z !== void 0 ? _z : []).find(function (e) { return e.userId === nonAdminId; });
|
|
1243
|
+
if (!i4Pending) return [3 /*break*/, 208];
|
|
1244
|
+
return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, i4Pending), { status: 'accepted' })])];
|
|
1245
|
+
case 206:
|
|
1246
|
+
_5.sent();
|
|
1247
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
1248
|
+
case 207:
|
|
1249
|
+
_5.sent();
|
|
1250
|
+
_5.label = 208;
|
|
1251
|
+
case 208: return [4 /*yield*/, get_user(sdk, adminId)];
|
|
1252
|
+
case 209:
|
|
1253
|
+
i4Accepted = _5.sent();
|
|
1254
|
+
i4Entry = ((_0 = i4Accepted.linkedAccountAccess) !== null && _0 !== void 0 ? _0 : []).find(function (e) { return e.userId === nonAdminId; });
|
|
1255
|
+
return [4 /*yield*/, (0, testing_1.async_test)('I4. Switch succeeds on a future-dated accepted grant (expiration-ignored semantics locked in by inspection)', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: false, onResult: function (r) { return (typeof r.authToken === 'string'
|
|
1256
|
+
&& r.authToken.length > 0
|
|
1257
|
+
&& !!i4Entry
|
|
1258
|
+
&& new Date(i4Entry.requestExpiresAt).getTime() > Date.now()); } })
|
|
1259
|
+
// Re-auth nonAdmin since the switch invalidated its source token
|
|
1260
|
+
];
|
|
1261
|
+
case 210:
|
|
1262
|
+
_5.sent();
|
|
1263
|
+
// Re-auth nonAdmin since the switch invalidated its source token
|
|
1264
|
+
return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
|
|
1265
|
+
// ============================================================
|
|
1266
|
+
// K. Actor-identity model (chained-switch + switch-back semantics)
|
|
1267
|
+
// ============================================================
|
|
1268
|
+
// Premise: the real operator is always the actor (session.actorUserId || session.id).
|
|
1269
|
+
// Grant checks, audit attribution, and request authorship all derive from realActor —
|
|
1270
|
+
// never from the proxy identity. Verified here via:
|
|
1271
|
+
// - get_linked_accounts returns the actor's grants + the actor's own account (switch-back).
|
|
1272
|
+
// - Validator rejects ALL linkedAccountAccess writes from switched sessions.
|
|
1273
|
+
// - request_linked_account_access uses actor's email for self-check (proxy-email no-op
|
|
1274
|
+
// would otherwise create a self-targeted pending entry).
|
|
1275
|
+
// - switch_account back to the actor mints a JWT with actor* claims cleared.
|
|
1276
|
+
// - Audit log for switch-back records event=account_switch_back with proxySessionId.
|
|
1277
|
+
];
|
|
1278
|
+
case 211:
|
|
1279
|
+
// Re-auth nonAdmin since the switch invalidated its source token
|
|
1280
|
+
_5.sent();
|
|
1281
|
+
// ============================================================
|
|
1282
|
+
// K. Actor-identity model (chained-switch + switch-back semantics)
|
|
1283
|
+
// ============================================================
|
|
1284
|
+
// Premise: the real operator is always the actor (session.actorUserId || session.id).
|
|
1285
|
+
// Grant checks, audit attribution, and request authorship all derive from realActor —
|
|
1286
|
+
// never from the proxy identity. Verified here via:
|
|
1287
|
+
// - get_linked_accounts returns the actor's grants + the actor's own account (switch-back).
|
|
1288
|
+
// - Validator rejects ALL linkedAccountAccess writes from switched sessions.
|
|
1289
|
+
// - request_linked_account_access uses actor's email for self-check (proxy-email no-op
|
|
1290
|
+
// would otherwise create a self-targeted pending entry).
|
|
1291
|
+
// - switch_account back to the actor mints a JWT with actor* claims cleared.
|
|
1292
|
+
// - Audit log for switch-back records event=account_switch_back with proxySessionId.
|
|
1293
|
+
(0, testing_1.log_header)("K. Actor-identity model");
|
|
1294
|
+
// Seed an accepted grant so nonAdmin can switch into admin. Wait > 1s past the prior
|
|
1295
|
+
// clear_linkedAccountAccess so is_logged_in's iat-vs-revoked-key slack doesn't trip.
|
|
1296
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
|
|
1297
|
+
case 212:
|
|
1298
|
+
// Seed an accepted grant so nonAdmin can switch into admin. Wait > 1s past the prior
|
|
1299
|
+
// clear_linkedAccountAccess so is_logged_in's iat-vs-revoked-key slack doesn't trip.
|
|
1300
|
+
_5.sent();
|
|
1301
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 1500)];
|
|
1302
|
+
case 213:
|
|
1303
|
+
_5.sent();
|
|
1304
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
1305
|
+
case 214:
|
|
1306
|
+
_5.sent();
|
|
1307
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
1308
|
+
case 215:
|
|
1309
|
+
_5.sent();
|
|
1310
|
+
return [4 /*yield*/, get_user(sdk, adminId)];
|
|
1311
|
+
case 216:
|
|
1312
|
+
kSeedState = _5.sent();
|
|
1313
|
+
kPending = ((_1 = kSeedState.linkedAccountAccess) !== null && _1 !== void 0 ? _1 : []).find(function (e) { return e.userId === nonAdminId; });
|
|
1314
|
+
return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, kPending), { status: 'accepted' })])];
|
|
1315
|
+
case 217:
|
|
1316
|
+
_5.sent();
|
|
1317
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)
|
|
1318
|
+
// K1. Establish a switched session: nonAdmin → admin.
|
|
1319
|
+
];
|
|
1320
|
+
case 218:
|
|
1321
|
+
_5.sent();
|
|
1322
|
+
kSwitchedToken = '';
|
|
1323
|
+
kSwitchedUser = null;
|
|
1324
|
+
return [4 /*yield*/, (0, testing_1.async_test)('K1. nonAdmin switches into admin (sets up actor-identity scenario)', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: false, onResult: function (r) {
|
|
1325
|
+
var _a;
|
|
1326
|
+
kSwitchedToken = r.authToken;
|
|
1327
|
+
kSwitchedUser = r.user;
|
|
1328
|
+
return typeof r.authToken === 'string' && r.authToken.length > 0 && ((_a = r.user) === null || _a === void 0 ? void 0 : _a.id) === adminId;
|
|
1329
|
+
} })];
|
|
1330
|
+
case 219:
|
|
1331
|
+
_5.sent();
|
|
1332
|
+
kSwitchedSdk = new sdk_1.Session({ host: host });
|
|
1333
|
+
kSwitchedSdk.setAuthToken(kSwitchedToken);
|
|
1334
|
+
kSwitchedSdk.setUserInfo(kSwitchedUser);
|
|
1335
|
+
// K2. get_linked_accounts from a switched session returns the actor's own account first
|
|
1336
|
+
// (the switch-back entry) and excludes the current proxy identity (admin) — querying
|
|
1337
|
+
// against realActor=nonAdmin would normally surface admin (it has nonAdmin: accepted),
|
|
1338
|
+
// but the caller IS already admin so switching there would no-op. K8b separately covers
|
|
1339
|
+
// the case where the list DOES include actor-grants that aren't the current proxy.
|
|
1340
|
+
return [4 /*yield*/, (0, testing_1.async_test)('K2. get_linked_accounts from switched session: actor first, current proxy excluded', function () { return kSwitchedSdk.api.users.get_linked_accounts(); }, { shouldError: false, onResult: function (r) {
|
|
1341
|
+
var _a;
|
|
1342
|
+
var accounts = ((_a = r === null || r === void 0 ? void 0 : r.linkedAccounts) !== null && _a !== void 0 ? _a : []);
|
|
1343
|
+
if (accounts.length === 0)
|
|
1344
|
+
return false;
|
|
1345
|
+
if (accounts[0].id !== nonAdminId)
|
|
1346
|
+
return false;
|
|
1347
|
+
// Current proxy (admin) must NOT appear, even though admin granted nonAdmin.
|
|
1348
|
+
var hasSelfProxy = accounts.some(function (a) { return a.id === adminId; });
|
|
1349
|
+
return !hasSelfProxy;
|
|
1350
|
+
} })
|
|
1351
|
+
// K3. Validator rejects ALL linkedAccountAccess PATCHes from a switched session — even
|
|
1352
|
+
// ones the proxy identity (admin = ownerId) would normally be authorized to make. Closes
|
|
1353
|
+
// the self-approval / silent-revoke hole. Use the marker-tag helper to ensure a unique
|
|
1354
|
+
// payload (avoid the 3/30s identical-update rate limit colliding with this case).
|
|
1355
|
+
];
|
|
1356
|
+
case 220:
|
|
1357
|
+
// K2. get_linked_accounts from a switched session returns the actor's own account first
|
|
1358
|
+
// (the switch-back entry) and excludes the current proxy identity (admin) — querying
|
|
1359
|
+
// against realActor=nonAdmin would normally surface admin (it has nonAdmin: accepted),
|
|
1360
|
+
// but the caller IS already admin so switching there would no-op. K8b separately covers
|
|
1361
|
+
// the case where the list DOES include actor-grants that aren't the current proxy.
|
|
1362
|
+
_5.sent();
|
|
1363
|
+
// K3. Validator rejects ALL linkedAccountAccess PATCHes from a switched session — even
|
|
1364
|
+
// ones the proxy identity (admin = ownerId) would normally be authorized to make. Closes
|
|
1365
|
+
// the self-approval / silent-revoke hole. Use the marker-tag helper to ensure a unique
|
|
1366
|
+
// payload (avoid the 3/30s identical-update rate limit colliding with this case).
|
|
1367
|
+
return [4 /*yield*/, (0, testing_1.async_test)('K3. Validator rejects linkedAccountAccess PATCH from switched session', function () { return set_linkedAccountAccess(kSwitchedSdk, adminId, [__assign(__assign({}, kPending), { status: 'accepted' })]); }, { shouldError: true, onError: function (e) { return (e.statusCode === 400
|
|
1368
|
+
|| /(switched session|actorUserId|while acting)/i.test(e.message || '')); } })
|
|
1369
|
+
// K4. request_linked_account_access from switched session uses the actor's email for the
|
|
1370
|
+
// self-check. Calling with targetEmail = nonAdminEmail (the actor's own email) → silent
|
|
1371
|
+
// {} and NO write. Old behavior would have used session.email=adminEmail for the self-
|
|
1372
|
+
// check, mismatched, looked up nonAdmin, and created a pending entry on nonAdmin.
|
|
1373
|
+
];
|
|
1374
|
+
case 221:
|
|
1375
|
+
// K3. Validator rejects ALL linkedAccountAccess PATCHes from a switched session — even
|
|
1376
|
+
// ones the proxy identity (admin = ownerId) would normally be authorized to make. Closes
|
|
1377
|
+
// the self-approval / silent-revoke hole. Use the marker-tag helper to ensure a unique
|
|
1378
|
+
// payload (avoid the 3/30s identical-update rate limit colliding with this case).
|
|
1379
|
+
_5.sent();
|
|
1380
|
+
// K4. request_linked_account_access from switched session uses the actor's email for the
|
|
1381
|
+
// self-check. Calling with targetEmail = nonAdminEmail (the actor's own email) → silent
|
|
1382
|
+
// {} and NO write. Old behavior would have used session.email=adminEmail for the self-
|
|
1383
|
+
// check, mismatched, looked up nonAdmin, and created a pending entry on nonAdmin.
|
|
1384
|
+
return [4 /*yield*/, (0, testing_1.async_test)('K4. request_linked_account_access from switched session uses actor email for self-check', function () { return kSwitchedSdk.api.users.request_linked_account_access({ targetEmail: nonAdminEmail }); }, passOnAnyResult)];
|
|
1385
|
+
case 222:
|
|
1386
|
+
// K4. request_linked_account_access from switched session uses the actor's email for the
|
|
1387
|
+
// self-check. Calling with targetEmail = nonAdminEmail (the actor's own email) → silent
|
|
1388
|
+
// {} and NO write. Old behavior would have used session.email=adminEmail for the self-
|
|
1389
|
+
// check, mismatched, looked up nonAdmin, and created a pending entry on nonAdmin.
|
|
1390
|
+
_5.sent();
|
|
1391
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
1392
|
+
case 223:
|
|
1393
|
+
_5.sent();
|
|
1394
|
+
return [4 /*yield*/, (0, testing_1.async_test)('K4. No pending entry created on actor record (actor identity is the requester)', function () { return get_user(sdk, nonAdminId); }, { shouldError: false, onResult: function (u) { var _a; return !((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).length; } })
|
|
1395
|
+
// K5. switch-back: nonAdmin (acting as admin) returns to nonAdmin. No grant lookup
|
|
1396
|
+
// (target === realActor); resulting JWT has all actor* claims cleared.
|
|
1397
|
+
];
|
|
1398
|
+
case 224:
|
|
1399
|
+
_5.sent();
|
|
1400
|
+
kBackToken = '';
|
|
1401
|
+
return [4 /*yield*/, (0, testing_1.async_test)('K5. Switch-back to actor succeeds (no grant lookup required)', function () { return kSwitchedSdk.api.users.switch_account({ targetUserId: nonAdminId }); }, { shouldError: false, onResult: function (r) {
|
|
1402
|
+
var _a;
|
|
1403
|
+
kBackToken = r.authToken;
|
|
1404
|
+
return typeof r.authToken === 'string' && r.authToken.length > 0 && ((_a = r.user) === null || _a === void 0 ? void 0 : _a.id) === nonAdminId;
|
|
1405
|
+
} })];
|
|
1406
|
+
case 225:
|
|
1407
|
+
_5.sent();
|
|
1408
|
+
kBackDecoded = decode_jwt(kBackToken);
|
|
1409
|
+
(0, testing_1.assert)((kBackDecoded === null || kBackDecoded === void 0 ? void 0 : kBackDecoded.id) === nonAdminId, "JWT.id ".concat(kBackDecoded === null || kBackDecoded === void 0 ? void 0 : kBackDecoded.id, " != ").concat(nonAdminId), 'K5. JWT.id == actor');
|
|
1410
|
+
(0, testing_1.assert)(!(kBackDecoded === null || kBackDecoded === void 0 ? void 0 : kBackDecoded.actorUserId), "JWT still carries actorUserId=".concat(kBackDecoded === null || kBackDecoded === void 0 ? void 0 : kBackDecoded.actorUserId, " after switch-back"), 'K5. JWT.actorUserId cleared');
|
|
1411
|
+
(0, testing_1.assert)(!(kBackDecoded === null || kBackDecoded === void 0 ? void 0 : kBackDecoded.actorEmail), "JWT still carries actorEmail after switch-back", 'K5. JWT.actorEmail cleared');
|
|
1412
|
+
(0, testing_1.assert)(!(kBackDecoded === null || kBackDecoded === void 0 ? void 0 : kBackDecoded.actorBusinessId), "JWT still carries actorBusinessId after switch-back", 'K5. JWT.actorBusinessId cleared');
|
|
1413
|
+
kBackSdk = new sdk_1.Session({ host: host });
|
|
1414
|
+
kBackSdk.setAuthToken(kBackToken);
|
|
1415
|
+
return [4 /*yield*/, (0, testing_1.async_test)('K5b. Switched-back token authenticates', function () { return kBackSdk.test_authenticated(); }, { shouldError: false, onResult: function (r) { return r === 'Authenticated!'; } })
|
|
1416
|
+
// K6. Audit log for the switch-back: event=account_switch_back, userId=realActor (nonAdmin),
|
|
1417
|
+
// proxySessionId=admin (the proxy identity that issued the request).
|
|
1418
|
+
];
|
|
1419
|
+
case 226:
|
|
1420
|
+
_5.sent();
|
|
1421
|
+
// K6. Audit log for the switch-back: event=account_switch_back, userId=realActor (nonAdmin),
|
|
1422
|
+
// proxySessionId=admin (the proxy identity that issued the request).
|
|
1423
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 500)];
|
|
1424
|
+
case 227:
|
|
1425
|
+
// K6. Audit log for the switch-back: event=account_switch_back, userId=realActor (nonAdmin),
|
|
1426
|
+
// proxySessionId=admin (the proxy identity that issued the request).
|
|
1427
|
+
_5.sent();
|
|
1428
|
+
return [4 /*yield*/, (0, testing_1.async_test)('K6. user_logs has account_switch_back event with realActor as userId + proxySessionId', function () { return sdk.api.user_logs.getSome({ filter: { resourceId: nonAdminId, resource: 'users', action: 'update' } }); }, { shouldError: false, onResult: function (logs) { return (logs !== null && logs !== void 0 ? logs : []).some(function (l) {
|
|
1429
|
+
var _a;
|
|
1430
|
+
var info = (_a = l === null || l === void 0 ? void 0 : l.info) !== null && _a !== void 0 ? _a : {};
|
|
1431
|
+
return (l === null || l === void 0 ? void 0 : l.userId) === nonAdminId
|
|
1432
|
+
&& info.event === 'account_switch_back'
|
|
1433
|
+
&& info.sourceUserId === nonAdminId
|
|
1434
|
+
&& info.proxySessionId === adminId
|
|
1435
|
+
&& info.targetUserId === nonAdminId;
|
|
1436
|
+
}); } })
|
|
1437
|
+
// K7. From the now-clean nonAdmin session (kBackSdk, minted by the K5 switch-back),
|
|
1438
|
+
// switching back INTO admin works normally — grant unchanged, chain restarted with no
|
|
1439
|
+
// leftover actor* state. Use kBackSdk because sdkNonAdmin's original token was invalidated
|
|
1440
|
+
// by the K1 switch and we haven't re-authed it yet.
|
|
1441
|
+
];
|
|
1442
|
+
case 228:
|
|
1443
|
+
_5.sent();
|
|
1444
|
+
// K7. From the now-clean nonAdmin session (kBackSdk, minted by the K5 switch-back),
|
|
1445
|
+
// switching back INTO admin works normally — grant unchanged, chain restarted with no
|
|
1446
|
+
// leftover actor* state. Use kBackSdk because sdkNonAdmin's original token was invalidated
|
|
1447
|
+
// by the K1 switch and we haven't re-authed it yet.
|
|
1448
|
+
return [4 /*yield*/, (0, testing_1.async_test)('K7. Clean nonAdmin session can mint a fresh switch into admin (chain restarted)', function () { return kBackSdk.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: false, onResult: function (r) { return typeof r.authToken === 'string' && r.authToken.length > 0; } })
|
|
1449
|
+
// Re-auth nonAdmin: its original token died in K1, and kBackSdk's token died in K7.
|
|
1450
|
+
];
|
|
1451
|
+
case 229:
|
|
1452
|
+
// K7. From the now-clean nonAdmin session (kBackSdk, minted by the K5 switch-back),
|
|
1453
|
+
// switching back INTO admin works normally — grant unchanged, chain restarted with no
|
|
1454
|
+
// leftover actor* state. Use kBackSdk because sdkNonAdmin's original token was invalidated
|
|
1455
|
+
// by the K1 switch and we haven't re-authed it yet.
|
|
1456
|
+
_5.sent();
|
|
1457
|
+
// Re-auth nonAdmin: its original token died in K1, and kBackSdk's token died in K7.
|
|
1458
|
+
return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
|
|
1459
|
+
// ============================================================
|
|
1460
|
+
// K8. Chained switch A→B→C preserves the original actor (not the proxy)
|
|
1461
|
+
// ============================================================
|
|
1462
|
+
// Spec: from a switched A-as-B session, switching to C must mint a JWT with
|
|
1463
|
+
// actorUserId=A — never actorUserId=B. The B hop is a UI affordance and must not become
|
|
1464
|
+
// the new "actor" on subsequent switches. Requires a third user C with a direct grant
|
|
1465
|
+
// from C to A; admin creates C and uses generate_auth_token to bootstrap a C-session
|
|
1466
|
+
// for accepting the grant.
|
|
1467
|
+
];
|
|
1468
|
+
case 230:
|
|
1469
|
+
// Re-auth nonAdmin: its original token died in K1, and kBackSdk's token died in K7.
|
|
1470
|
+
_5.sent();
|
|
1471
|
+
// ============================================================
|
|
1472
|
+
// K8. Chained switch A→B→C preserves the original actor (not the proxy)
|
|
1473
|
+
// ============================================================
|
|
1474
|
+
// Spec: from a switched A-as-B session, switching to C must mint a JWT with
|
|
1475
|
+
// actorUserId=A — never actorUserId=B. The B hop is a UI affordance and must not become
|
|
1476
|
+
// the new "actor" on subsequent switches. Requires a third user C with a direct grant
|
|
1477
|
+
// from C to A; admin creates C and uses generate_auth_token to bootstrap a C-session
|
|
1478
|
+
// for accepting the grant.
|
|
1479
|
+
(0, testing_1.log_header)("K8. Chained switching preserves original actor");
|
|
1480
|
+
userCEmail = "switch-c-".concat(RAND(), "@tellescope.example");
|
|
1481
|
+
return [4 /*yield*/, sdk.api.users.createOne({
|
|
1482
|
+
email: userCEmail,
|
|
1483
|
+
fname: 'Chained',
|
|
1484
|
+
lname: 'Target',
|
|
1485
|
+
notificationEmailsDisabled: true,
|
|
1486
|
+
verifiedEmail: true,
|
|
1487
|
+
})];
|
|
1488
|
+
case 231:
|
|
1489
|
+
userCRecord = _5.sent();
|
|
1490
|
+
userCId = userCRecord.id;
|
|
1491
|
+
return [4 /*yield*/, sdk.api.users.generate_auth_token({ id: userCId })];
|
|
1492
|
+
case 232:
|
|
1493
|
+
userCToken = (_5.sent()).authToken;
|
|
1494
|
+
sdkC = new sdk_1.Session({ host: host });
|
|
1495
|
+
sdkC.setAuthToken(userCToken);
|
|
1496
|
+
// A (nonAdmin) requests access to C; C accepts (validator requires session.id === ownerId).
|
|
1497
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: userCEmail })];
|
|
1498
|
+
case 233:
|
|
1499
|
+
// A (nonAdmin) requests access to C; C accepts (validator requires session.id === ownerId).
|
|
1500
|
+
_5.sent();
|
|
1501
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
1502
|
+
case 234:
|
|
1503
|
+
_5.sent();
|
|
1504
|
+
return [4 /*yield*/, get_user(sdkC, userCId)];
|
|
1505
|
+
case 235:
|
|
1506
|
+
userCState = _5.sent();
|
|
1507
|
+
pendingForC = ((_2 = userCState.linkedAccountAccess) !== null && _2 !== void 0 ? _2 : []).find(function (e) { return e.userId === nonAdminId; });
|
|
1508
|
+
(0, testing_1.assert)(!!pendingForC, 'K8 setup: pending entry should exist on C from nonAdmin', 'K8 setup: pending on C');
|
|
1509
|
+
return [4 /*yield*/, set_linkedAccountAccess(sdkC, userCId, [__assign(__assign({}, pendingForC), { status: 'accepted' })])];
|
|
1510
|
+
case 236:
|
|
1511
|
+
_5.sent();
|
|
1512
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)
|
|
1513
|
+
// Establish the A-as-B switched session. The admin→nonAdmin accepted grant from K1 is
|
|
1514
|
+
// still in place (K7's switch consumed a slot but didn't remove the grant).
|
|
1515
|
+
];
|
|
1516
|
+
case 237:
|
|
1517
|
+
_5.sent();
|
|
1518
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.switch_account({ targetUserId: adminId })];
|
|
1519
|
+
case 238:
|
|
1520
|
+
aAsBResp = _5.sent();
|
|
1521
|
+
aAsBSdk = new sdk_1.Session({ host: host });
|
|
1522
|
+
aAsBSdk.setAuthToken(aAsBResp.authToken);
|
|
1523
|
+
aAsBSdk.setUserInfo(aAsBResp.user);
|
|
1524
|
+
aAsBDecoded = decode_jwt(aAsBResp.authToken);
|
|
1525
|
+
(0, testing_1.assert)((aAsBDecoded === null || aAsBDecoded === void 0 ? void 0 : aAsBDecoded.id) === adminId && (aAsBDecoded === null || aAsBDecoded === void 0 ? void 0 : aAsBDecoded.actorUserId) === nonAdminId, "K8 setup: A-as-B token should carry id=admin, actorUserId=nonAdmin (got id=".concat(aAsBDecoded === null || aAsBDecoded === void 0 ? void 0 : aAsBDecoded.id, ", actorUserId=").concat(aAsBDecoded === null || aAsBDecoded === void 0 ? void 0 : aAsBDecoded.actorUserId, ")"), 'K8 setup: A-as-B JWT confirmed');
|
|
1526
|
+
chainedToken = '';
|
|
1527
|
+
return [4 /*yield*/, (0, testing_1.async_test)('K8. Chained switch A→B→C succeeds when C granted access to the actor', function () { return aAsBSdk.api.users.switch_account({ targetUserId: userCId }); }, { shouldError: false, onResult: function (r) {
|
|
1528
|
+
var _a;
|
|
1529
|
+
chainedToken = r.authToken;
|
|
1530
|
+
return typeof r.authToken === 'string' && r.authToken.length > 0 && ((_a = r.user) === null || _a === void 0 ? void 0 : _a.id) === userCId;
|
|
1531
|
+
} })];
|
|
1532
|
+
case 239:
|
|
1533
|
+
_5.sent();
|
|
1534
|
+
chainedDecoded = decode_jwt(chainedToken);
|
|
1535
|
+
(0, testing_1.assert)((chainedDecoded === null || chainedDecoded === void 0 ? void 0 : chainedDecoded.id) === userCId, "chained JWT.id ".concat(chainedDecoded === null || chainedDecoded === void 0 ? void 0 : chainedDecoded.id, " != ").concat(userCId), 'K8. chained JWT.id == C');
|
|
1536
|
+
(0, testing_1.assert)((chainedDecoded === null || chainedDecoded === void 0 ? void 0 : chainedDecoded.actorUserId) === nonAdminId, "chained JWT.actorUserId is ".concat(chainedDecoded === null || chainedDecoded === void 0 ? void 0 : chainedDecoded.actorUserId, " \u2014 must remain nonAdmin (the original actor), NOT admin (the proxy hop)"), 'K8. chained JWT.actorUserId == A (original actor preserved, NOT B)');
|
|
1537
|
+
(0, testing_1.assert)((chainedDecoded === null || chainedDecoded === void 0 ? void 0 : chainedDecoded.actorEmail) === nonAdminEmail, "chained JWT.actorEmail ".concat(chainedDecoded === null || chainedDecoded === void 0 ? void 0 : chainedDecoded.actorEmail, " != ").concat(nonAdminEmail), 'K8. chained JWT.actorEmail preserved');
|
|
1538
|
+
(0, testing_1.assert)((chainedDecoded === null || chainedDecoded === void 0 ? void 0 : chainedDecoded.actorBusinessId) === nonAdminBusinessId, "chained JWT.actorBusinessId mismatch", 'K8. chained JWT.actorBusinessId preserved');
|
|
1539
|
+
// Audit log: the chained switch's user_log must attribute to the original actor (nonAdmin),
|
|
1540
|
+
// and record proxySessionId=adminId so the B-hop is reconstructable.
|
|
1541
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 500)];
|
|
1542
|
+
case 240:
|
|
1543
|
+
// Audit log: the chained switch's user_log must attribute to the original actor (nonAdmin),
|
|
1544
|
+
// and record proxySessionId=adminId so the B-hop is reconstructable.
|
|
1545
|
+
_5.sent();
|
|
1546
|
+
return [4 /*yield*/, (0, testing_1.async_test)('K8. Chained switch audit log attributes to actor with proxySessionId=B', function () { return sdk.api.user_logs.getSome({ filter: { resourceId: userCId, resource: 'users', action: 'update' } }); }, { shouldError: false, onResult: function (logs) { return (logs !== null && logs !== void 0 ? logs : []).some(function (l) {
|
|
1547
|
+
var _a;
|
|
1548
|
+
var info = (_a = l === null || l === void 0 ? void 0 : l.info) !== null && _a !== void 0 ? _a : {};
|
|
1549
|
+
return (l === null || l === void 0 ? void 0 : l.userId) === nonAdminId
|
|
1550
|
+
&& info.event === 'account_switch'
|
|
1551
|
+
&& info.sourceUserId === nonAdminId
|
|
1552
|
+
&& info.proxySessionId === adminId
|
|
1553
|
+
&& info.targetUserId === userCId;
|
|
1554
|
+
}); } })
|
|
1555
|
+
// K8b. From the chained C-session, get_linked_accounts must reflect the ACTOR's
|
|
1556
|
+
// perspective: actor's own account first (switch-back), plus all accounts that have
|
|
1557
|
+
// granted access to the actor (B/admin). The current proxy identity (C) must NOT appear
|
|
1558
|
+
// — caller is already in that session.
|
|
1559
|
+
];
|
|
1560
|
+
case 241:
|
|
1561
|
+
_5.sent();
|
|
1562
|
+
chainedSdk = new sdk_1.Session({ host: host });
|
|
1563
|
+
chainedSdk.setAuthToken(chainedToken);
|
|
1564
|
+
return [4 /*yield*/, (0, testing_1.async_test)('K8b. get_linked_accounts from chained C-session reflects actor + actor-grants, excludes current proxy', function () { return chainedSdk.api.users.get_linked_accounts(); }, { shouldError: false, onResult: function (r) {
|
|
1565
|
+
var _a;
|
|
1566
|
+
var accounts = ((_a = r === null || r === void 0 ? void 0 : r.linkedAccounts) !== null && _a !== void 0 ? _a : []);
|
|
1567
|
+
if (accounts.length === 0)
|
|
1568
|
+
return false;
|
|
1569
|
+
// Switch-back entry (actor's own account) is first.
|
|
1570
|
+
if (accounts[0].id !== nonAdminId)
|
|
1571
|
+
return false;
|
|
1572
|
+
// Admin appears because admin has nonAdmin: accepted in linkedAccountAccess.
|
|
1573
|
+
var hasAdmin = accounts.some(function (a) { return a.id === adminId; });
|
|
1574
|
+
// The current proxy identity (userC) must NOT appear — it's not switchable from itself.
|
|
1575
|
+
var hasSelfProxy = accounts.some(function (a) { return a.id === userCId; });
|
|
1576
|
+
return hasAdmin && !hasSelfProxy;
|
|
1577
|
+
} })
|
|
1578
|
+
// Cleanup K8: delete C (also clears the linkedAccountAccess that the chained switch
|
|
1579
|
+
// consumed), and re-auth nonAdmin since the K8 setup switch invalidated its token.
|
|
1580
|
+
];
|
|
1581
|
+
case 242:
|
|
1582
|
+
_5.sent();
|
|
1583
|
+
_5.label = 243;
|
|
1584
|
+
case 243:
|
|
1585
|
+
_5.trys.push([243, 245, , 246]);
|
|
1586
|
+
return [4 /*yield*/, sdk.api.users.deleteOne(userCId)];
|
|
1587
|
+
case 244:
|
|
1588
|
+
_5.sent();
|
|
1589
|
+
return [3 /*break*/, 246];
|
|
1590
|
+
case 245:
|
|
1591
|
+
_j = _5.sent();
|
|
1592
|
+
return [3 /*break*/, 246];
|
|
1593
|
+
case 246: return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
|
|
1594
|
+
// ============================================================
|
|
1595
|
+
// L. linkedAccountAccess read-side redaction (owner-only metadata)
|
|
1596
|
+
// ============================================================
|
|
1597
|
+
// The grant list reveals who's been requesting access to whom. Only the owner needs to
|
|
1598
|
+
// see it (to act on pending requests / inspect accepted grants). Cross-user reads,
|
|
1599
|
+
// switched-session reads, and the switch_account response.user must all redact the field.
|
|
1600
|
+
];
|
|
1601
|
+
case 247:
|
|
1602
|
+
_5.sent();
|
|
1603
|
+
// ============================================================
|
|
1604
|
+
// L. linkedAccountAccess read-side redaction (owner-only metadata)
|
|
1605
|
+
// ============================================================
|
|
1606
|
+
// The grant list reveals who's been requesting access to whom. Only the owner needs to
|
|
1607
|
+
// see it (to act on pending requests / inspect accepted grants). Cross-user reads,
|
|
1608
|
+
// switched-session reads, and the switch_account response.user must all redact the field.
|
|
1609
|
+
(0, testing_1.log_header)("L. linkedAccountAccess read-side redaction");
|
|
1610
|
+
// Seed an accepted grant so admin's record has a non-empty linkedAccountAccess for the
|
|
1611
|
+
// redaction assertions to be meaningful (a missing field is indistinguishable from a
|
|
1612
|
+
// redacted-empty field otherwise).
|
|
1613
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
|
|
1614
|
+
case 248:
|
|
1615
|
+
// Seed an accepted grant so admin's record has a non-empty linkedAccountAccess for the
|
|
1616
|
+
// redaction assertions to be meaningful (a missing field is indistinguishable from a
|
|
1617
|
+
// redacted-empty field otherwise).
|
|
1618
|
+
_5.sent();
|
|
1619
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 1500)];
|
|
1620
|
+
case 249:
|
|
1621
|
+
_5.sent();
|
|
1622
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
|
|
1623
|
+
case 250:
|
|
1624
|
+
_5.sent();
|
|
1625
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
|
|
1626
|
+
case 251:
|
|
1627
|
+
_5.sent();
|
|
1628
|
+
return [4 /*yield*/, get_user(sdk, adminId)];
|
|
1629
|
+
case 252:
|
|
1630
|
+
lSeedState = _5.sent();
|
|
1631
|
+
lPending = ((_3 = lSeedState.linkedAccountAccess) !== null && _3 !== void 0 ? _3 : []).find(function (e) { return e.userId === nonAdminId; });
|
|
1632
|
+
return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, lPending), { status: 'accepted' })])];
|
|
1633
|
+
case 253:
|
|
1634
|
+
_5.sent();
|
|
1635
|
+
return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)
|
|
1636
|
+
// L1. Owner reading own record from a non-switched session: field IS visible.
|
|
1637
|
+
];
|
|
1638
|
+
case 254:
|
|
1639
|
+
_5.sent();
|
|
1640
|
+
// L1. Owner reading own record from a non-switched session: field IS visible.
|
|
1641
|
+
return [4 /*yield*/, (0, testing_1.async_test)('L1. Owner reading own record sees linkedAccountAccess', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) { return Array.isArray(u === null || u === void 0 ? void 0 : u.linkedAccountAccess) && u.linkedAccountAccess.length > 0; } })
|
|
1642
|
+
// L2. Cross-user read (non-admin reads admin): field is redacted.
|
|
1643
|
+
];
|
|
1644
|
+
case 255:
|
|
1645
|
+
// L1. Owner reading own record from a non-switched session: field IS visible.
|
|
1646
|
+
_5.sent();
|
|
1647
|
+
// L2. Cross-user read (non-admin reads admin): field is redacted.
|
|
1648
|
+
return [4 /*yield*/, (0, testing_1.async_test)('L2. Cross-user read (nonAdmin reads admin) redacts linkedAccountAccess', function () { return get_user(sdkNonAdmin, adminId); }, { shouldError: false, onResult: function (u) { return (u === null || u === void 0 ? void 0 : u.linkedAccountAccess) === undefined; } })
|
|
1649
|
+
// L3. Switched session reading the proxy's own record: still redacted (session.actorUserId
|
|
1650
|
+
// is set, so callerIsRealOwner is false even though value.id === session.id).
|
|
1651
|
+
];
|
|
1652
|
+
case 256:
|
|
1653
|
+
// L2. Cross-user read (non-admin reads admin): field is redacted.
|
|
1654
|
+
_5.sent();
|
|
1655
|
+
return [4 /*yield*/, sdkNonAdmin.api.users.switch_account({ targetUserId: adminId })];
|
|
1656
|
+
case 257:
|
|
1657
|
+
lSwitchResp = _5.sent();
|
|
1658
|
+
lSwitchedSdk = new sdk_1.Session({ host: host });
|
|
1659
|
+
lSwitchedSdk.setAuthToken(lSwitchResp.authToken);
|
|
1660
|
+
return [4 /*yield*/, (0, testing_1.async_test)('L3. Switched session reading proxy record (admin) redacts linkedAccountAccess', function () { return get_user(lSwitchedSdk, adminId); }, { shouldError: false, onResult: function (u) { return (u === null || u === void 0 ? void 0 : u.linkedAccountAccess) === undefined; } })
|
|
1661
|
+
// L4. switch_account response.user does NOT include linkedAccountAccess (the response
|
|
1662
|
+
// bypasses applyRedactions, so the handler strips the field explicitly).
|
|
1663
|
+
];
|
|
1664
|
+
case 258:
|
|
1665
|
+
_5.sent();
|
|
1666
|
+
// L4. switch_account response.user does NOT include linkedAccountAccess (the response
|
|
1667
|
+
// bypasses applyRedactions, so the handler strips the field explicitly).
|
|
1668
|
+
(0, testing_1.assert)((lSwitchResp === null || lSwitchResp === void 0 ? void 0 : lSwitchResp.user) && lSwitchResp.user.id === adminId && lSwitchResp.user.linkedAccountAccess === undefined, "switch_account response.user.linkedAccountAccess should be undefined; got ".concat(JSON.stringify((_4 = lSwitchResp === null || lSwitchResp === void 0 ? void 0 : lSwitchResp.user) === null || _4 === void 0 ? void 0 : _4.linkedAccountAccess)), 'L4. switch_account response.user has linkedAccountAccess redacted');
|
|
1669
|
+
return [4 /*yield*/, lSwitchedSdk.api.users.switch_account({ targetUserId: nonAdminId })];
|
|
1670
|
+
case 259:
|
|
1671
|
+
lBackResp = _5.sent();
|
|
1672
|
+
lBackSdk = new sdk_1.Session({ host: host });
|
|
1673
|
+
lBackSdk.setAuthToken(lBackResp.authToken);
|
|
1674
|
+
return [4 /*yield*/, (0, testing_1.async_test)('L5. Owner-in-real-session can read their own linkedAccountAccess after switch-back', function () { return get_user(lBackSdk, nonAdminId); }, { shouldError: false, onResult: function (u) { return Array.isArray(u === null || u === void 0 ? void 0 : u.linkedAccountAccess); } })
|
|
1675
|
+
// L cleanup: re-auth nonAdmin (lBackSdk's token wasn't invalidated, but we want a clean
|
|
1676
|
+
// sdkNonAdmin for the remaining sections).
|
|
1677
|
+
];
|
|
1678
|
+
case 260:
|
|
1679
|
+
_5.sent();
|
|
1680
|
+
// L cleanup: re-auth nonAdmin (lBackSdk's token wasn't invalidated, but we want a clean
|
|
1681
|
+
// sdkNonAdmin for the remaining sections).
|
|
1682
|
+
return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
|
|
1683
|
+
// ============================================================
|
|
1684
|
+
// M. Enduser sessions are rejected on user-only endpoints
|
|
1685
|
+
// ============================================================
|
|
1686
|
+
// The three new endpoints are registered as customActions on the `users` model with no
|
|
1687
|
+
// `allowEnduser` and no `enduserAction` declared. With only the user-type auth path active,
|
|
1688
|
+
// is_logged_in's type check (authentication.ts:587 — `if (userInfo.type !== type) return false`)
|
|
1689
|
+
// rejects the enduser JWT and checkAccess returns 401 "Unauthenticated" before businessOnly
|
|
1690
|
+
// even runs. Tests here are negative assertions guarding against future drift — e.g. if
|
|
1691
|
+
// someone adds `allowEnduser` to a custom action by accident.
|
|
1692
|
+
];
|
|
1693
|
+
case 261:
|
|
1694
|
+
// L cleanup: re-auth nonAdmin (lBackSdk's token wasn't invalidated, but we want a clean
|
|
1695
|
+
// sdkNonAdmin for the remaining sections).
|
|
1696
|
+
_5.sent();
|
|
1697
|
+
// ============================================================
|
|
1698
|
+
// M. Enduser sessions are rejected on user-only endpoints
|
|
1699
|
+
// ============================================================
|
|
1700
|
+
// The three new endpoints are registered as customActions on the `users` model with no
|
|
1701
|
+
// `allowEnduser` and no `enduserAction` declared. With only the user-type auth path active,
|
|
1702
|
+
// is_logged_in's type check (authentication.ts:587 — `if (userInfo.type !== type) return false`)
|
|
1703
|
+
// rejects the enduser JWT and checkAccess returns 401 "Unauthenticated" before businessOnly
|
|
1704
|
+
// even runs. Tests here are negative assertions guarding against future drift — e.g. if
|
|
1705
|
+
// someone adds `allowEnduser` to a custom action by accident.
|
|
1706
|
+
(0, testing_1.log_header)("M. Enduser sessions rejected on user endpoints");
|
|
1707
|
+
enduserEmail = "switch-enduser-".concat(RAND(), "@tellescope.example");
|
|
1708
|
+
return [4 /*yield*/, sdk.api.endusers.createOne({
|
|
1709
|
+
email: enduserEmail,
|
|
1710
|
+
fname: 'Switch',
|
|
1711
|
+
lname: 'Enduser',
|
|
1712
|
+
})];
|
|
1713
|
+
case 262:
|
|
1714
|
+
enduserRec = _5.sent();
|
|
1715
|
+
return [4 /*yield*/, sdk.api.endusers.generate_auth_token({ id: enduserRec.id })
|
|
1716
|
+
// Use a plain Session with the enduser token so we can hit the user-only routes. The
|
|
1717
|
+
// EnduserSession's .api shape doesn't include these methods, but the underlying HTTP
|
|
1718
|
+
// routes do exist server-side — we want to confirm the server-side rejection fires
|
|
1719
|
+
// regardless of what client SDK is used.
|
|
1720
|
+
];
|
|
1721
|
+
case 263:
|
|
1722
|
+
enduserAuthToken = (_5.sent()).authToken;
|
|
1723
|
+
sdkAsEnduser = new sdk_1.Session({ host: host });
|
|
1724
|
+
sdkAsEnduser.setAuthToken(enduserAuthToken);
|
|
1725
|
+
isEnduserRejection = is401Rejection;
|
|
1726
|
+
return [4 /*yield*/, (0, testing_1.async_test)('M1. Enduser session is rejected on get_linked_accounts', function () { return sdkAsEnduser.api.users.get_linked_accounts(); }, { shouldError: true, onError: isEnduserRejection })];
|
|
1727
|
+
case 264:
|
|
1728
|
+
_5.sent();
|
|
1729
|
+
return [4 /*yield*/, (0, testing_1.async_test)('M2. Enduser session is rejected on switch_account', function () { return sdkAsEnduser.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: true, onError: isEnduserRejection })];
|
|
1730
|
+
case 265:
|
|
1731
|
+
_5.sent();
|
|
1732
|
+
return [4 /*yield*/, (0, testing_1.async_test)('M3. Enduser session is rejected on request_linked_account_access', function () { return sdkAsEnduser.api.users.request_linked_account_access({ targetEmail: adminEmail }); }, { shouldError: true, onError: isEnduserRejection })
|
|
1733
|
+
// Cleanup the inline enduser.
|
|
1734
|
+
];
|
|
1735
|
+
case 266:
|
|
1736
|
+
_5.sent();
|
|
1737
|
+
_5.label = 267;
|
|
1738
|
+
case 267:
|
|
1739
|
+
_5.trys.push([267, 269, , 270]);
|
|
1740
|
+
return [4 /*yield*/, sdk.api.endusers.deleteOne(enduserRec.id)];
|
|
1741
|
+
case 268:
|
|
1742
|
+
_5.sent();
|
|
1743
|
+
return [3 /*break*/, 270];
|
|
1744
|
+
case 269:
|
|
1745
|
+
_k = _5.sent();
|
|
1746
|
+
return [3 /*break*/, 270];
|
|
1747
|
+
case 270:
|
|
1748
|
+
// ============================================================
|
|
1749
|
+
// C10. request_linked_account_access rate limit (placed last — exhausts admin's quota for 60s)
|
|
1750
|
+
// ============================================================
|
|
1751
|
+
(0, testing_1.log_header)("C10. request_linked_account_access rate limit (placed last)");
|
|
1752
|
+
i = 0;
|
|
1753
|
+
_5.label = 271;
|
|
1754
|
+
case 271:
|
|
1755
|
+
if (!(i < 30)) return [3 /*break*/, 276];
|
|
1756
|
+
_5.label = 272;
|
|
1757
|
+
case 272:
|
|
1758
|
+
_5.trys.push([272, 274, , 275]);
|
|
1759
|
+
return [4 /*yield*/, sdk.api.users.request_linked_account_access({ targetEmail: "rl-".concat(RAND(), "@tellescope.example") })];
|
|
1760
|
+
case 273:
|
|
1761
|
+
_5.sent();
|
|
1762
|
+
return [3 /*break*/, 275];
|
|
1763
|
+
case 274:
|
|
1764
|
+
_l = _5.sent();
|
|
1765
|
+
return [3 /*break*/, 275];
|
|
1766
|
+
case 275:
|
|
1767
|
+
i++;
|
|
1768
|
+
return [3 /*break*/, 271];
|
|
1769
|
+
case 276: return [4 /*yield*/, (0, testing_1.async_test)('C10. 31st request inside one minute is rate-limited (429)', function () { return sdk.api.users.request_linked_account_access({ targetEmail: "rl-".concat(RAND(), "@tellescope.example") }); }, { shouldError: true, onError: function (e) { return e.statusCode === 429 || (e.message || '').toLowerCase().includes('rate'); } })
|
|
1770
|
+
// ============================================================
|
|
1771
|
+
// J. Cleanup
|
|
1772
|
+
// ============================================================
|
|
1773
|
+
];
|
|
1774
|
+
case 277:
|
|
1775
|
+
_5.sent();
|
|
1776
|
+
// ============================================================
|
|
1777
|
+
// J. Cleanup
|
|
1778
|
+
// ============================================================
|
|
1779
|
+
(0, testing_1.log_header)("J. Cleanup");
|
|
1780
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
|
|
1781
|
+
case 278:
|
|
1782
|
+
_5.sent();
|
|
1783
|
+
return [4 /*yield*/, clear_linkedAccountAccess(sdkNonAdmin, nonAdminId)];
|
|
1784
|
+
case 279:
|
|
1785
|
+
_5.sent();
|
|
1786
|
+
return [4 /*yield*/, cleanup_marker_tags(sdk, adminId)];
|
|
1787
|
+
case 280:
|
|
1788
|
+
_5.sent();
|
|
1789
|
+
return [4 /*yield*/, cleanup_marker_tags(sdkNonAdmin, nonAdminId)
|
|
1790
|
+
// Restore admin's fname (G5 mutated it server-side; downstream tests compare userInfo.fname to the server value).
|
|
1791
|
+
];
|
|
1792
|
+
case 281:
|
|
1793
|
+
_5.sent();
|
|
1794
|
+
if (!originalAdminFname) return [3 /*break*/, 285];
|
|
1795
|
+
_5.label = 282;
|
|
1796
|
+
case 282:
|
|
1797
|
+
_5.trys.push([282, 284, , 285]);
|
|
1798
|
+
return [4 /*yield*/, sdk.api.users.updateOne(adminId, { fname: originalAdminFname })];
|
|
1799
|
+
case 283:
|
|
1800
|
+
_5.sent();
|
|
1801
|
+
return [3 /*break*/, 285];
|
|
1802
|
+
case 284:
|
|
1803
|
+
_m = _5.sent();
|
|
1804
|
+
return [3 /*break*/, 285];
|
|
1805
|
+
case 285: return [2 /*return*/];
|
|
413
1806
|
}
|
|
414
1807
|
});
|
|
415
1808
|
});
|
|
416
1809
|
};
|
|
417
1810
|
exports.account_switcher_tests = account_switcher_tests;
|
|
1811
|
+
// Allow running this test file independently
|
|
418
1812
|
if (require.main === module) {
|
|
419
|
-
console.log("
|
|
1813
|
+
console.log("Using API URL: ".concat(host));
|
|
420
1814
|
var sdk_2 = new sdk_1.Session({ host: host });
|
|
421
1815
|
var sdkNonAdmin_1 = new sdk_1.Session({ host: host });
|
|
422
1816
|
var runTests = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
@@ -434,11 +1828,11 @@ if (require.main === module) {
|
|
|
434
1828
|
}); };
|
|
435
1829
|
runTests()
|
|
436
1830
|
.then(function () {
|
|
437
|
-
console.log("
|
|
1831
|
+
console.log("Account switcher test suite completed successfully");
|
|
438
1832
|
process.exit(0);
|
|
439
1833
|
})
|
|
440
1834
|
.catch(function (error) {
|
|
441
|
-
console.error("
|
|
1835
|
+
console.error("Account switcher test suite failed:", error);
|
|
442
1836
|
process.exit(1);
|
|
443
1837
|
});
|
|
444
1838
|
}
|