|
4 | 4 | import { afterEach, describe, expect, it, vi } from 'vitest' |
5 | 5 | import { OktaBlock } from '@/blocks/blocks/okta' |
6 | 6 | import { oktaActivateUserTool } from '@/tools/okta/activate_user' |
| 7 | +import { oktaClearUserSessionsTool } from '@/tools/okta/clear_user_sessions' |
| 8 | +import { oktaCreateUserTool } from '@/tools/okta/create_user' |
7 | 9 | import { oktaDeactivateUserTool } from '@/tools/okta/deactivate_user' |
| 10 | +import { oktaDeleteGroupRuleTool } from '@/tools/okta/delete_group_rule' |
8 | 11 | import { oktaDeleteUserTool } from '@/tools/okta/delete_user' |
| 12 | +import { oktaEnrollFactorTool } from '@/tools/okta/enroll_factor' |
9 | 13 | import { oktaGetLogsTool } from '@/tools/okta/get_logs' |
10 | 14 | import { oktaGetUserTool } from '@/tools/okta/get_user' |
| 15 | +import { oktaListAppsTool } from '@/tools/okta/list_apps' |
| 16 | +import { oktaRemoveUserFromAppTool } from '@/tools/okta/remove_user_from_app' |
| 17 | +import { oktaResetFactorTool } from '@/tools/okta/reset_factor' |
11 | 18 | import { oktaResetPasswordTool } from '@/tools/okta/reset_password' |
12 | 19 | import { oktaUpdateGroupTool } from '@/tools/okta/update_group' |
13 | 20 | import { oktaUpdateUserTool } from '@/tools/okta/update_user' |
@@ -201,6 +208,32 @@ describe('okta get_logs query building', () => { |
201 | 208 | const url = oktaGetLogsTool.request.url({ ...AUTH }) |
202 | 209 | expect(url).not.toContain('limit=') |
203 | 210 | }) |
| 211 | + |
| 212 | + /** |
| 213 | + * Okta documents `since` and `after` as mutually exclusive, and a scheduled |
| 214 | + * poll that persists the cursor normally also has a start time configured — |
| 215 | + * so the resume request would otherwise be one Okta rejects. |
| 216 | + */ |
| 217 | + it('drops since when a cursor is supplied', () => { |
| 218 | + const url = builtUrl(oktaGetLogsTool.request.url, { |
| 219 | + ...AUTH, |
| 220 | + since: '2026-08-01T00:00:00.000Z', |
| 221 | + after: 'CURSOR123', |
| 222 | + }) |
| 223 | + |
| 224 | + expect(url).toContain('after=CURSOR123') |
| 225 | + expect(url).not.toContain('since=') |
| 226 | + }) |
| 227 | + |
| 228 | + it('still sends since when no cursor is supplied', () => { |
| 229 | + const url = builtUrl(oktaGetLogsTool.request.url, { |
| 230 | + ...AUTH, |
| 231 | + since: '2026-08-01T00:00:00.000Z', |
| 232 | + }) |
| 233 | + |
| 234 | + expect(url).toContain('since=2026-08-01T00%3A00%3A00.000Z') |
| 235 | + expect(url).not.toContain('after=') |
| 236 | + }) |
204 | 237 | }) |
205 | 238 |
|
206 | 239 | describe('okta get_logs pagination termination', () => { |
@@ -303,6 +336,92 @@ describe('okta lifecycle flags are coerced rather than interpolated raw', () => |
303 | 336 | }) |
304 | 337 | }) |
305 | 338 |
|
| 339 | +describe('okta query-string flags are coerced rather than interpolated raw', () => { |
| 340 | + /** |
| 341 | + * Every one of these is `visibility: 'user-or-llm'` and typed `boolean` in |
| 342 | + * Okta's spec, so a direct or agent tool call can deliver `"yes"` for any of |
| 343 | + * them. Omission must still leave the parameter off entirely so Okta applies |
| 344 | + * its own documented default. |
| 345 | + */ |
| 346 | + const CASES: Array<{ |
| 347 | + name: string |
| 348 | + build: (params: Record<string, unknown>) => string |
| 349 | + param: string |
| 350 | + base: Record<string, unknown> |
| 351 | + }> = [ |
| 352 | + { |
| 353 | + name: 'remove_user_from_app.sendEmail', |
| 354 | + build: (params) => builtUrl(oktaRemoveUserFromAppTool.request.url, params), |
| 355 | + param: 'sendEmail', |
| 356 | + base: { ...AUTH, appId: '0oa1', userId: '00u1' }, |
| 357 | + }, |
| 358 | + { |
| 359 | + name: 'enroll_factor.activate', |
| 360 | + build: (params) => builtUrl(oktaEnrollFactorTool.request.url, params), |
| 361 | + param: 'activate', |
| 362 | + base: { ...AUTH, userId: '00u1', factorType: 'sms', provider: 'OKTA' }, |
| 363 | + }, |
| 364 | + { |
| 365 | + name: 'reset_factor.removeRecoveryEnrollment', |
| 366 | + build: (params) => builtUrl(oktaResetFactorTool.request.url, params), |
| 367 | + param: 'removeRecoveryEnrollment', |
| 368 | + base: { ...AUTH, userId: '00u1', factorId: 'fac1' }, |
| 369 | + }, |
| 370 | + { |
| 371 | + name: 'delete_group_rule.removeUsers', |
| 372 | + build: (params) => builtUrl(oktaDeleteGroupRuleTool.request.url, params), |
| 373 | + param: 'removeUsers', |
| 374 | + base: { ...AUTH, groupRuleId: '0pr1' }, |
| 375 | + }, |
| 376 | + { |
| 377 | + name: 'clear_user_sessions.oauthTokens', |
| 378 | + build: (params) => builtUrl(oktaClearUserSessionsTool.request.url, params), |
| 379 | + param: 'oauthTokens', |
| 380 | + base: { ...AUTH, userId: '00u1' }, |
| 381 | + }, |
| 382 | + { |
| 383 | + name: 'clear_user_sessions.forgetDevices', |
| 384 | + build: (params) => builtUrl(oktaClearUserSessionsTool.request.url, params), |
| 385 | + param: 'forgetDevices', |
| 386 | + base: { ...AUTH, userId: '00u1' }, |
| 387 | + }, |
| 388 | + { |
| 389 | + name: 'list_apps.includeNonDeleted', |
| 390 | + build: (params) => builtUrl(oktaListAppsTool.request.url, params), |
| 391 | + param: 'includeNonDeleted', |
| 392 | + base: { ...AUTH }, |
| 393 | + }, |
| 394 | + ] |
| 395 | + |
| 396 | + it.each(CASES)('$name coerces a stringy truthy to true', ({ build, param, base }) => { |
| 397 | + expect(build({ ...base, [param]: 'yes' })).toContain(`${param}=true`) |
| 398 | + }) |
| 399 | + |
| 400 | + it.each(CASES)('$name coerces a stringy falsy to false', ({ build, param, base }) => { |
| 401 | + expect(build({ ...base, [param]: 'false' })).toContain(`${param}=false`) |
| 402 | + }) |
| 403 | + |
| 404 | + it.each(CASES)('$name omits the param when undefined', ({ build, param, base }) => { |
| 405 | + expect(build(base)).not.toContain(`${param}=`) |
| 406 | + }) |
| 407 | + |
| 408 | + /** |
| 409 | + * `create_user.activate` is the one flag Okta itself defaults to `true`, so |
| 410 | + * omission must keep sending `true` rather than fall through the coercion. |
| 411 | + */ |
| 412 | + it('create_user.activate coerces a stringy value and still defaults to true', () => { |
| 413 | + const base = { ...AUTH, firstName: 'A', lastName: 'B', email: 'a@b.com' } |
| 414 | + |
| 415 | + expect(builtUrl(oktaCreateUserTool.request.url, base)).toContain('activate=true') |
| 416 | + expect(builtUrl(oktaCreateUserTool.request.url, { ...base, activate: 'yes' })).toContain( |
| 417 | + 'activate=true' |
| 418 | + ) |
| 419 | + expect(builtUrl(oktaCreateUserTool.request.url, { ...base, activate: 'false' })).toContain( |
| 420 | + 'activate=false' |
| 421 | + ) |
| 422 | + }) |
| 423 | +}) |
| 424 | + |
306 | 425 | describe('okta update_group declarative fallback', () => { |
307 | 426 | /** |
308 | 427 | * `PUT /api/v1/groups/{groupId}` replaces an extensible profile wholesale, so |
|
0 commit comments