@udibo/oauth2 0.14.0 → 0.15.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/README.md +45 -24
- package/{esm → dist}/adapters/hono/_common.d.ts +5 -5
- package/{esm → dist}/adapters/hono/_common.js +1 -1
- package/{esm → dist}/adapters/hono/authorization-server.d.ts +6 -7
- package/{esm → dist}/adapters/hono/authorization-server.js +0 -1
- package/{esm → dist}/adapters/hono/bff/auth-request-store.d.ts +2 -2
- package/{esm → dist}/adapters/hono/bff/auth-request-store.js +2 -2
- package/{esm → dist}/adapters/hono/bff/bff.d.ts +22 -12
- package/{esm → dist}/adapters/hono/bff/bff.js +35 -29
- package/{esm → dist}/adapters/hono/bff/cookie-prefix.js +1 -2
- package/{esm → dist}/adapters/hono/bff/mod.d.ts +5 -6
- package/{esm → dist}/adapters/hono/bff/mod.js +1 -2
- package/{esm → dist}/adapters/hono/bff/proxy.js +2 -1
- package/{esm → dist}/adapters/hono/bff/session-store.d.ts +2 -2
- package/{esm → dist}/adapters/hono/bff/session-store.js +7 -6
- package/{esm → dist}/adapters/hono/bff/testing.d.ts +7 -8
- package/{esm → dist}/adapters/hono/bff/testing.js +19 -22
- package/{esm → dist}/adapters/hono/identity.d.ts +1 -2
- package/{esm → dist}/adapters/hono/identity.js +10 -10
- package/{esm → dist}/adapters/hono/log.d.ts +0 -1
- package/{esm → dist}/adapters/hono/log.js +0 -6
- package/{esm → dist}/adapters/hono/resource-server.d.ts +5 -6
- package/{esm → dist}/adapters/hono/resource-server.js +0 -1
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +3 -0
- package/dist/cli/idp/config.d.ts +110 -0
- package/dist/cli/idp/config.js +269 -0
- package/dist/cli/idp/mod.d.ts +20 -0
- package/dist/cli/idp/mod.js +175 -0
- package/dist/cli/idp/pages.d.ts +56 -0
- package/dist/cli/idp/pages.js +190 -0
- package/dist/cli/idp/server.d.ts +93 -0
- package/dist/cli/idp/server.js +559 -0
- package/dist/cli/keygen.d.ts +13 -0
- package/dist/cli/keygen.js +30 -0
- package/dist/cli/mod.d.ts +34 -0
- package/dist/cli/mod.js +94 -0
- package/{esm → dist}/client/_http.d.ts +1 -1
- package/{esm → dist}/client/base.d.ts +1 -1
- package/{esm → dist}/client/bff-client.d.ts +1 -1
- package/{esm → dist}/client/bff-client.js +7 -6
- package/{esm → dist}/client/browser-storage.d.ts +1 -1
- package/{esm → dist}/client/check.js +1 -1
- package/{esm → dist}/client/direct-client.d.ts +6 -6
- package/{esm → dist}/client/direct-client.js +11 -13
- package/{esm → dist}/client/discovery-cache.d.ts +1 -1
- package/{esm → dist}/client/discovery-cache.js +2 -2
- package/{esm → dist}/client/events.d.ts +1 -1
- package/{esm → dist}/client/mod.d.ts +11 -12
- package/{esm → dist}/client/mod.js +1 -1
- package/{esm → dist}/errors.d.ts +1 -1
- package/{esm → dist}/errors.js +1 -1
- package/{esm → dist}/identity/events.d.ts +1 -1
- package/{esm → dist}/identity/external/_shared.d.ts +1 -1
- package/{esm → dist}/identity/external/_shared.js +1 -2
- package/{esm → dist}/identity/external/apple-client-secret.d.ts +1 -1
- package/{esm → dist}/identity/external/apple-client-secret.js +6 -7
- package/{esm → dist}/identity/external/apple.d.ts +5 -5
- package/{esm → dist}/identity/external/apple.js +10 -11
- package/{esm → dist}/identity/external/discord.d.ts +3 -3
- package/{esm → dist}/identity/external/discord.js +2 -2
- package/{esm → dist}/identity/external/flow.d.ts +1 -1
- package/{esm → dist}/identity/external/flow.js +2 -2
- package/{esm → dist}/identity/external/github.d.ts +3 -3
- package/{esm → dist}/identity/external/github.js +12 -17
- package/{esm → dist}/identity/external/google.d.ts +5 -5
- package/{esm → dist}/identity/external/google.js +2 -2
- package/{esm → dist}/identity/external/mod.d.ts +12 -13
- package/{esm → dist}/identity/external/mod.js +0 -1
- package/{esm → dist}/identity/external/oauth2.d.ts +1 -1
- package/{esm → dist}/identity/external/oauth2.js +8 -8
- package/{esm → dist}/identity/external/oidc.d.ts +4 -4
- package/{esm → dist}/identity/external/oidc.js +9 -8
- package/{esm → dist}/identity/hibp.d.ts +1 -1
- package/{esm → dist}/identity/hibp.js +1 -1
- package/{esm → dist}/identity/lockout.js +1 -2
- package/{esm → dist}/identity/mfa/mod.d.ts +4 -5
- package/{esm → dist}/identity/mfa/mod.js +1 -2
- package/{esm → dist}/identity/mfa/service.d.ts +3 -3
- package/{esm → dist}/identity/mfa/service.js +4 -3
- package/{esm → dist}/identity/mfa/totp.d.ts +1 -1
- package/{esm → dist}/identity/mfa/totp.js +2 -2
- package/{esm → dist}/identity/migration.d.ts +2 -3
- package/{esm → dist}/identity/migration.js +3 -4
- package/{esm → dist}/identity/mod.d.ts +14 -15
- package/{esm → dist}/identity/mod.js +0 -1
- package/{esm → dist}/identity/otp.d.ts +1 -1
- package/{esm → dist}/identity/otp.js +4 -2
- package/{esm → dist}/identity/password.js +8 -5
- package/{esm → dist}/identity/rate-limit.d.ts +1 -1
- package/{esm → dist}/identity/service.d.ts +10 -10
- package/{esm → dist}/identity/service.js +17 -18
- package/{esm → dist}/identity/session.js +2 -2
- package/{esm → dist}/identity/token-flow.js +3 -2
- package/{esm → dist}/models/authorization-code.d.ts +3 -3
- package/{esm → dist}/models/authorization.d.ts +1 -1
- package/{esm → dist}/models/authorization.js +1 -2
- package/{esm → dist}/models/device-authorization.d.ts +2 -2
- package/{esm → dist}/models/token.d.ts +3 -3
- package/{esm → dist}/react/callback.d.ts +1 -1
- package/{esm → dist}/react/callback.js +0 -1
- package/{esm → dist}/react/components/class-names.d.ts +1 -1
- package/{esm → dist}/react/components/class-names.js +3 -2
- package/{esm → dist}/react/components/internal.d.ts +2 -2
- package/{esm → dist}/react/components/internal.js +5 -11
- package/{esm → dist}/react/components/mfa-challenge-form.d.ts +2 -2
- package/{esm → dist}/react/components/mfa-challenge-form.js +4 -6
- package/{esm → dist}/react/components/mfa-enrollment-form.d.ts +2 -2
- package/{esm → dist}/react/components/mfa-enrollment-form.js +2 -6
- package/{esm → dist}/react/components/mod.d.ts +20 -21
- package/{esm → dist}/react/components/mod.js +1 -2
- package/{esm → dist}/react/components/request-password-reset-form.d.ts +2 -2
- package/{esm → dist}/react/components/request-password-reset-form.js +1 -3
- package/{esm → dist}/react/components/reset-password-form.d.ts +2 -2
- package/{esm → dist}/react/components/reset-password-form.js +2 -2
- package/{esm → dist}/react/components/sign-in-form.d.ts +2 -2
- package/{esm → dist}/react/components/sign-in-form.js +1 -5
- package/{esm → dist}/react/components/sign-up-form.d.ts +2 -2
- package/{esm → dist}/react/components/sign-up-form.js +4 -10
- package/{esm → dist}/react/components/types.d.ts +2 -2
- package/{esm → dist}/react/components/use-auth-form.d.ts +3 -3
- package/{esm → dist}/react/components/use-auth-form.js +4 -7
- package/{esm → dist}/react/components/user-menu.d.ts +2 -2
- package/{esm → dist}/react/components/user-menu.js +9 -14
- package/{esm → dist}/react/context.d.ts +1 -1
- package/{esm → dist}/react/mod.d.ts +6 -7
- package/{esm → dist}/react/mod.js +0 -1
- package/{esm → dist}/react/provider.d.ts +2 -2
- package/{esm → dist}/react/require-auth.js +1 -3
- package/{esm → dist}/react/testing.d.ts +2 -3
- package/{esm → dist}/react/testing.js +1 -2
- package/{esm → dist}/react/use-authorization.d.ts +1 -1
- package/{esm → dist}/react/use-oauth2.d.ts +8 -1
- package/{esm → dist}/react/use-oauth2.js +0 -1
- package/{esm → dist}/server/_clock-skew.js +2 -1
- package/{esm → dist}/server/authorization-server.d.ts +40 -41
- package/{esm → dist}/server/authorization-server.js +32 -42
- package/{esm → dist}/server/client-authentication.d.ts +2 -2
- package/{esm → dist}/server/grants/authorization-code.d.ts +9 -9
- package/{esm → dist}/server/grants/authorization-code.js +5 -6
- package/{esm → dist}/server/grants/client-credentials.d.ts +4 -4
- package/{esm → dist}/server/grants/device-authorization.d.ts +6 -6
- package/{esm → dist}/server/grants/device-authorization.js +4 -4
- package/{esm → dist}/server/grants/grant.d.ts +7 -7
- package/{esm → dist}/server/grants/grant.js +7 -7
- package/{esm → dist}/server/grants/password.d.ts +5 -5
- package/{esm → dist}/server/grants/refresh-token.d.ts +4 -4
- package/{esm → dist}/server/grants/refresh-token.js +5 -6
- package/{esm → dist}/server/introspection-token-reader.d.ts +5 -5
- package/{esm → dist}/server/introspection-token-reader.js +4 -4
- package/{esm → dist}/server/jwks-token-reader.d.ts +5 -5
- package/{esm → dist}/server/jwks-token-reader.js +4 -4
- package/{esm → dist}/server/mod.d.ts +12 -13
- package/{esm → dist}/server/mod.js +0 -1
- package/{esm → dist}/server/public-suffix/mod.d.ts +2 -3
- package/{esm → dist}/server/public-suffix/mod.js +1 -2
- package/{esm → dist}/server/public-suffix/rules.d.ts +1 -1
- package/{esm → dist}/server/public-suffix/rules.js +1 -1
- package/{esm → dist}/server/redirect-uri.js +5 -4
- package/{esm → dist}/server/resource-server.d.ts +11 -12
- package/{esm → dist}/server/resource-server.js +12 -14
- package/{esm → dist}/server/services/authorization-code.d.ts +5 -5
- package/{esm → dist}/server/services/client.d.ts +1 -1
- package/{esm → dist}/server/services/device-authorization.d.ts +3 -3
- package/{esm → dist}/server/services/token.d.ts +6 -6
- package/{esm → dist}/server/services/token.js +3 -1
- package/{esm → dist}/server/services/user.d.ts +1 -1
- package/{esm → dist}/server/signing-keys.d.ts +1 -1
- package/{esm → dist}/server/signing-keys.js +8 -2
- package/{esm → dist}/server/utils/hash.js +1 -1
- package/{esm → dist}/testing/contract/auth-request-storage.d.ts +1 -1
- package/{esm → dist}/testing/contract/auth-request-storage.js +24 -29
- package/{esm → dist}/testing/contract/authorization-code.d.ts +6 -6
- package/{esm → dist}/testing/contract/authorization-code.js +16 -17
- package/{esm → dist}/testing/contract/client.d.ts +4 -4
- package/{esm → dist}/testing/contract/client.js +14 -15
- package/{esm → dist}/testing/contract/device-authorization.d.ts +6 -6
- package/{esm → dist}/testing/contract/device-authorization.js +37 -39
- package/{esm → dist}/testing/contract/identity-session.d.ts +6 -1
- package/{esm → dist}/testing/contract/identity-session.js +44 -36
- package/{esm → dist}/testing/contract/identity-user-store.d.ts +8 -2
- package/{esm → dist}/testing/contract/identity-user-store.js +48 -46
- package/{esm → dist}/testing/contract/lockout-store.d.ts +1 -1
- package/{esm → dist}/testing/contract/lockout-store.js +23 -24
- package/{esm → dist}/testing/contract/mfa-store.d.ts +1 -1
- package/{esm → dist}/testing/contract/mfa-store.js +53 -54
- package/{esm → dist}/testing/contract/mod.d.ts +15 -16
- package/{esm → dist}/testing/contract/mod.js +0 -1
- package/{esm → dist}/testing/contract/otp-store.d.ts +1 -1
- package/{esm → dist}/testing/contract/otp-store.js +33 -34
- package/{esm → dist}/testing/contract/rate-limit-store.d.ts +1 -1
- package/{esm → dist}/testing/contract/rate-limit-store.js +15 -16
- package/{esm → dist}/testing/contract/tenant.js +361 -330
- package/{esm → dist}/testing/contract/token-flow-store.d.ts +1 -1
- package/{esm → dist}/testing/contract/token-flow-store.js +27 -32
- package/{esm → dist}/testing/contract/token-reader.d.ts +3 -3
- package/{esm → dist}/testing/contract/token-reader.js +6 -7
- package/{esm → dist}/testing/contract/token.d.ts +7 -7
- package/{esm → dist}/testing/contract/token.js +48 -53
- package/{esm → dist}/testing/contract/user.d.ts +2 -2
- package/{esm → dist}/testing/contract/user.js +8 -9
- package/{esm → dist}/testing/mod.d.ts +5 -6
- package/{esm → dist}/testing/mod.js +2 -3
- package/{esm → dist}/testing/server.d.ts +7 -6
- package/{esm → dist}/testing/server.js +2 -1
- package/{esm → dist}/testing/services.d.ts +11 -11
- package/{esm → dist}/testing/services.js +8 -8
- package/{esm → dist}/testing/tenant.d.ts +11 -7
- package/{esm → dist}/testing/tenant.js +109 -94
- package/dist/utils/_constant-time.d.ts +12 -0
- package/dist/utils/_constant-time.js +19 -0
- package/{esm → dist}/utils/_default-fetch.js +4 -1
- package/dist/utils/_delay.d.ts +13 -0
- package/dist/utils/_delay.js +28 -0
- package/dist/utils/_encoding.d.ts +39 -0
- package/dist/utils/_encoding.js +126 -0
- package/{esm → dist}/utils/authentication-context.d.ts +1 -1
- package/{esm → dist}/utils/crypto.d.ts +2 -3
- package/{esm → dist}/utils/crypto.js +3 -4
- package/{esm → dist}/utils/pkce.js +1 -1
- package/{esm → dist}/utils/text.js +1 -1
- package/{esm → dist}/utils/url.d.ts +0 -1
- package/{esm → dist}/utils/url.js +0 -1
- package/package.json +156 -33
- package/esm/_dnt.polyfills.d.ts +0 -31
- package/esm/_dnt.polyfills.js +0 -26
- package/esm/deps/jsr.io/@std/assert/1.0.19/almost_equals.d.ts +0 -29
- package/esm/deps/jsr.io/@std/assert/1.0.19/almost_equals.js +0 -47
- package/esm/deps/jsr.io/@std/assert/1.0.19/array_includes.d.ts +0 -28
- package/esm/deps/jsr.io/@std/assert/1.0.19/array_includes.js +0 -65
- package/esm/deps/jsr.io/@std/assert/1.0.19/assert.d.ts +0 -16
- package/esm/deps/jsr.io/@std/assert/1.0.19/assert.js +0 -23
- package/esm/deps/jsr.io/@std/assert/1.0.19/assertion_error.d.ts +0 -25
- package/esm/deps/jsr.io/@std/assert/1.0.19/assertion_error.js +0 -30
- package/esm/deps/jsr.io/@std/assert/1.0.19/equal.d.ts +0 -24
- package/esm/deps/jsr.io/@std/assert/1.0.19/equal.js +0 -193
- package/esm/deps/jsr.io/@std/assert/1.0.19/equals.d.ts +0 -36
- package/esm/deps/jsr.io/@std/assert/1.0.19/equals.js +0 -61
- package/esm/deps/jsr.io/@std/assert/1.0.19/exists.d.ts +0 -17
- package/esm/deps/jsr.io/@std/assert/1.0.19/exists.js +0 -27
- package/esm/deps/jsr.io/@std/assert/1.0.19/fail.d.ts +0 -14
- package/esm/deps/jsr.io/@std/assert/1.0.19/fail.js +0 -20
- package/esm/deps/jsr.io/@std/assert/1.0.19/false.d.ts +0 -18
- package/esm/deps/jsr.io/@std/assert/1.0.19/false.js +0 -23
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater.d.ts +0 -19
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater.js +0 -29
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.d.ts +0 -19
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.js +0 -29
- package/esm/deps/jsr.io/@std/assert/1.0.19/instance_of.d.ts +0 -22
- package/esm/deps/jsr.io/@std/assert/1.0.19/instance_of.js +0 -52
- package/esm/deps/jsr.io/@std/assert/1.0.19/is_error.d.ts +0 -24
- package/esm/deps/jsr.io/@std/assert/1.0.19/is_error.js +0 -53
- package/esm/deps/jsr.io/@std/assert/1.0.19/less.d.ts +0 -18
- package/esm/deps/jsr.io/@std/assert/1.0.19/less.js +0 -28
- package/esm/deps/jsr.io/@std/assert/1.0.19/less_or_equal.d.ts +0 -19
- package/esm/deps/jsr.io/@std/assert/1.0.19/less_or_equal.js +0 -29
- package/esm/deps/jsr.io/@std/assert/1.0.19/match.d.ts +0 -17
- package/esm/deps/jsr.io/@std/assert/1.0.19/match.js +0 -26
- package/esm/deps/jsr.io/@std/assert/1.0.19/mod.d.ts +0 -43
- package/esm/deps/jsr.io/@std/assert/1.0.19/mod.js +0 -45
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_equals.d.ts +0 -26
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_equals.js +0 -39
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_instance_of.d.ts +0 -19
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_instance_of.js +0 -29
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_match.d.ts +0 -17
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_match.js +0 -26
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.d.ts +0 -22
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.js +0 -32
- package/esm/deps/jsr.io/@std/assert/1.0.19/object_match.d.ts +0 -28
- package/esm/deps/jsr.io/@std/assert/1.0.19/object_match.js +0 -177
- package/esm/deps/jsr.io/@std/assert/1.0.19/rejects.d.ts +0 -41
- package/esm/deps/jsr.io/@std/assert/1.0.19/rejects.js +0 -53
- package/esm/deps/jsr.io/@std/assert/1.0.19/strict_equals.d.ts +0 -23
- package/esm/deps/jsr.io/@std/assert/1.0.19/strict_equals.js +0 -58
- package/esm/deps/jsr.io/@std/assert/1.0.19/string_includes.d.ts +0 -17
- package/esm/deps/jsr.io/@std/assert/1.0.19/string_includes.js +0 -26
- package/esm/deps/jsr.io/@std/assert/1.0.19/throws.d.ts +0 -44
- package/esm/deps/jsr.io/@std/assert/1.0.19/throws.js +0 -44
- package/esm/deps/jsr.io/@std/assert/1.0.19/unimplemented.d.ts +0 -14
- package/esm/deps/jsr.io/@std/assert/1.0.19/unimplemented.js +0 -20
- package/esm/deps/jsr.io/@std/assert/1.0.19/unreachable.d.ts +0 -14
- package/esm/deps/jsr.io/@std/assert/1.0.19/unreachable.js +0 -20
- package/esm/deps/jsr.io/@std/async/1.5.0/delay.d.ts +0 -44
- package/esm/deps/jsr.io/@std/async/1.5.0/delay.js +0 -84
- package/esm/deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.d.ts +0 -39
- package/esm/deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.js +0 -61
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common16.d.ts +0 -22
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common16.js +0 -51
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common32.d.ts +0 -34
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common32.js +0 -192
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common64.d.ts +0 -34
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common64.js +0 -113
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common_detach.d.ts +0 -3
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common_detach.js +0 -13
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_types.d.ts +0 -8
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_types.js +0 -2
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_validate_binary_like.d.ts +0 -1
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_validate_binary_like.js +0 -26
- package/esm/deps/jsr.io/@std/encoding/1.0.11/ascii85.d.ts +0 -60
- package/esm/deps/jsr.io/@std/encoding/1.0.11/ascii85.js +0 -152
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base32.d.ts +0 -39
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base32.js +0 -87
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base58.d.ts +0 -39
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base58.js +0 -131
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64.d.ts +0 -39
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64.js +0 -82
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64url.d.ts +0 -39
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64url.js +0 -72
- package/esm/deps/jsr.io/@std/encoding/1.0.11/hex.d.ts +0 -38
- package/esm/deps/jsr.io/@std/encoding/1.0.11/hex.js +0 -87
- package/esm/deps/jsr.io/@std/encoding/1.0.11/mod.d.ts +0 -97
- package/esm/deps/jsr.io/@std/encoding/1.0.11/mod.js +0 -99
- package/esm/deps/jsr.io/@std/encoding/1.0.11/varint.d.ts +0 -119
- package/esm/deps/jsr.io/@std/encoding/1.0.11/varint.js +0 -209
- package/esm/deps/jsr.io/@std/http/1.1.3/status.d.ts +0 -343
- package/esm/deps/jsr.io/@std/http/1.1.3/status.js +0 -343
- package/esm/deps/jsr.io/@std/internal/1.0.14/assertion_state.d.ts +0 -159
- package/esm/deps/jsr.io/@std/internal/1.0.14/assertion_state.js +0 -223
- package/esm/deps/jsr.io/@std/internal/1.0.14/build_message.d.ts +0 -79
- package/esm/deps/jsr.io/@std/internal/1.0.14/build_message.js +0 -116
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff.d.ts +0 -139
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff.js +0 -277
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff_str.d.ts +0 -98
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff_str.js +0 -181
- package/esm/deps/jsr.io/@std/internal/1.0.14/format.d.ts +0 -29
- package/esm/deps/jsr.io/@std/internal/1.0.14/format.js +0 -68
- package/esm/deps/jsr.io/@std/internal/1.0.14/styles.d.ts +0 -158
- package/esm/deps/jsr.io/@std/internal/1.0.14/styles.js +0 -206
- package/esm/deps/jsr.io/@std/internal/1.0.14/types.d.ts +0 -24
- package/esm/deps/jsr.io/@std/internal/1.0.14/types.js +0 -3
- package/esm/deps/jsr.io/@std/testing/1.0.20/_test_suite.d.ts +0 -91
- package/esm/deps/jsr.io/@std/testing/1.0.20/_test_suite.js +0 -347
- package/esm/deps/jsr.io/@std/testing/1.0.20/bdd.d.ts +0 -382
- package/esm/deps/jsr.io/@std/testing/1.0.20/bdd.js +0 -1110
- package/esm/deps/jsr.io/@udibo/http-error/0.11.1/mod.d.ts +0 -363
- package/esm/deps/jsr.io/@udibo/http-error/0.11.1/mod.js +0 -616
- package/esm/package.json +0 -3
- /package/{esm → dist}/adapters/hono/bff/cookie-prefix.d.ts +0 -0
- /package/{esm → dist}/adapters/hono/bff/proxy.d.ts +0 -0
- /package/{esm → dist}/client/_http.js +0 -0
- /package/{esm → dist}/client/base.js +0 -0
- /package/{esm → dist}/client/browser-storage.js +0 -0
- /package/{esm → dist}/client/check.d.ts +0 -0
- /package/{esm → dist}/client/events.js +0 -0
- /package/{esm → dist}/client/storage.d.ts +0 -0
- /package/{esm → dist}/client/storage.js +0 -0
- /package/{esm → dist}/identity/captcha.d.ts +0 -0
- /package/{esm → dist}/identity/captcha.js +0 -0
- /package/{esm → dist}/identity/delivery.d.ts +0 -0
- /package/{esm → dist}/identity/delivery.js +0 -0
- /package/{esm → dist}/identity/errors.d.ts +0 -0
- /package/{esm → dist}/identity/errors.js +0 -0
- /package/{esm → dist}/identity/events.js +0 -0
- /package/{esm → dist}/identity/external/_token-exchange.d.ts +0 -0
- /package/{esm → dist}/identity/external/_token-exchange.js +0 -0
- /package/{esm → dist}/identity/external/errors.d.ts +0 -0
- /package/{esm → dist}/identity/external/errors.js +0 -0
- /package/{esm → dist}/identity/external/provider.d.ts +0 -0
- /package/{esm → dist}/identity/external/provider.js +0 -0
- /package/{esm → dist}/identity/identifier.d.ts +0 -0
- /package/{esm → dist}/identity/identifier.js +0 -0
- /package/{esm → dist}/identity/lockout.d.ts +0 -0
- /package/{esm → dist}/identity/mfa/recovery-codes.d.ts +0 -0
- /package/{esm → dist}/identity/mfa/recovery-codes.js +0 -0
- /package/{esm → dist}/identity/password-policy.d.ts +0 -0
- /package/{esm → dist}/identity/password-policy.js +0 -0
- /package/{esm → dist}/identity/password.d.ts +0 -0
- /package/{esm → dist}/identity/rate-limit.js +0 -0
- /package/{esm → dist}/identity/session.d.ts +0 -0
- /package/{esm → dist}/identity/token-flow.d.ts +0 -0
- /package/{esm → dist}/models/authentication.d.ts +0 -0
- /package/{esm → dist}/models/authentication.js +0 -0
- /package/{esm → dist}/models/authorization-code.js +0 -0
- /package/{esm → dist}/models/client.d.ts +0 -0
- /package/{esm → dist}/models/client.js +0 -0
- /package/{esm → dist}/models/device-authorization.js +0 -0
- /package/{esm → dist}/models/responses.d.ts +0 -0
- /package/{esm → dist}/models/responses.js +0 -0
- /package/{esm → dist}/models/scope.d.ts +0 -0
- /package/{esm → dist}/models/scope.js +0 -0
- /package/{esm → dist}/models/token.js +0 -0
- /package/{esm → dist}/react/components/types.js +0 -0
- /package/{esm → dist}/react/context.js +0 -0
- /package/{esm → dist}/react/provider.js +0 -0
- /package/{esm → dist}/react/require-auth.d.ts +0 -0
- /package/{esm → dist}/react/use-authorization.js +0 -0
- /package/{esm → dist}/server/_clock-skew.d.ts +0 -0
- /package/{esm → dist}/server/client-authentication.js +0 -0
- /package/{esm → dist}/server/grants/client-credentials.js +0 -0
- /package/{esm → dist}/server/grants/password.js +0 -0
- /package/{esm → dist}/server/redirect-uri.d.ts +0 -0
- /package/{esm → dist}/server/services/authorization-code.js +0 -0
- /package/{esm → dist}/server/services/client.js +0 -0
- /package/{esm → dist}/server/services/device-authorization.js +0 -0
- /package/{esm → dist}/server/services/user.js +0 -0
- /package/{esm → dist}/server/utils/hash.d.ts +0 -0
- /package/{esm → dist}/testing/contract/_race.d.ts +0 -0
- /package/{esm → dist}/testing/contract/_race.js +0 -0
- /package/{esm → dist}/testing/contract/tenant.d.ts +0 -0
- /package/{esm → dist}/utils/_body.d.ts +0 -0
- /package/{esm → dist}/utils/_body.js +0 -0
- /package/{esm → dist}/utils/_buffer.d.ts +0 -0
- /package/{esm → dist}/utils/_buffer.js +0 -0
- /package/{esm → dist}/utils/_default-fetch.d.ts +0 -0
- /package/{esm → dist}/utils/_timing-safe.d.ts +0 -0
- /package/{esm → dist}/utils/_timing-safe.js +0 -0
- /package/{esm → dist}/utils/authentication-context.js +0 -0
- /package/{esm → dist}/utils/basic-auth.d.ts +0 -0
- /package/{esm → dist}/utils/basic-auth.js +0 -0
- /package/{esm → dist}/utils/pkce.d.ts +0 -0
- /package/{esm → dist}/utils/text.d.ts +0 -0
|
@@ -26,8 +26,7 @@
|
|
|
26
26
|
*
|
|
27
27
|
* @module
|
|
28
28
|
*/
|
|
29
|
-
import { assert,
|
|
30
|
-
import { afterAll, beforeAll, describe, it } from "../../deps/jsr.io/@std/testing/1.0.20/bdd.js";
|
|
29
|
+
import { afterAll, assert, beforeAll, describe, expect, it } from "vitest";
|
|
31
30
|
import { encodeBasicAuth } from "../../utils/basic-auth.js";
|
|
32
31
|
/**
|
|
33
32
|
* Registers the tenant contract suite. The permissions and resource type it
|
|
@@ -92,7 +91,7 @@ export function runTenantContractTests(options) {
|
|
|
92
91
|
headers: { authorization: clientAuth() },
|
|
93
92
|
body: new URLSearchParams({ token: accessToken }),
|
|
94
93
|
});
|
|
95
|
-
|
|
94
|
+
expect(response.status).toStrictEqual(200);
|
|
96
95
|
return await response.json();
|
|
97
96
|
}
|
|
98
97
|
async function ask(accessToken, path, body) {
|
|
@@ -107,27 +106,27 @@ export function runTenantContractTests(options) {
|
|
|
107
106
|
const text = await response.text();
|
|
108
107
|
return { status: response.status, body: text ? JSON.parse(text) : {} };
|
|
109
108
|
}
|
|
110
|
-
const sorted = (values) => [...values ?? []].sort();
|
|
109
|
+
const sorted = (values) => [...(values ?? [])].sort();
|
|
111
110
|
describe("introspection", () => {
|
|
112
111
|
it("reports tenant-wide permissions and no organization for a sign-in that picked none", async () => {
|
|
113
112
|
const { access_token } = await tenant.signIn(people.member);
|
|
114
113
|
const claims = await introspect(access_token);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
expect(claims.active).toStrictEqual(true);
|
|
115
|
+
expect(claims.sub).toStrictEqual(people.member);
|
|
116
|
+
expect(sorted(claims.permissions)).toStrictEqual(["contract:tenant"]);
|
|
117
|
+
expect(claims.org_id, "a sign-in with no organization names none").toBeFalsy();
|
|
119
118
|
});
|
|
120
119
|
it("picks a person's only organization when the sign-in names none", async () => {
|
|
121
120
|
const { access_token } = await tenant.signIn(people.solo);
|
|
122
121
|
const claims = await introspect(access_token);
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
expect(claims.org_id).toStrictEqual(organizations.other);
|
|
123
|
+
expect(sorted(claims.permissions)).toStrictEqual(["contract:other"]);
|
|
125
124
|
});
|
|
126
125
|
it("adds the picked organization and its permissions, and no other's", async () => {
|
|
127
126
|
const { access_token } = await tenant.signIn(people.member, organizations.home);
|
|
128
127
|
const claims = await introspect(access_token);
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
expect(claims.org_id).toStrictEqual(organizations.home);
|
|
129
|
+
expect(sorted(claims.permissions)).toStrictEqual([
|
|
131
130
|
"contract:home",
|
|
132
131
|
"contract:tenant",
|
|
133
132
|
]);
|
|
@@ -143,37 +142,37 @@ export function runTenantContractTests(options) {
|
|
|
143
142
|
refresh_token: issued.refresh_token,
|
|
144
143
|
}),
|
|
145
144
|
});
|
|
146
|
-
|
|
145
|
+
expect(response.status).toStrictEqual(200);
|
|
147
146
|
const refreshed = await response.json();
|
|
148
147
|
const claims = await introspect(refreshed.access_token);
|
|
149
|
-
|
|
148
|
+
expect(claims.org_id).toStrictEqual(organizations.other);
|
|
150
149
|
});
|
|
151
150
|
it("stops answering for an organization once the membership ends", async () => {
|
|
152
151
|
const { access_token } = await tenant.signIn(people.leaver, organizations.home);
|
|
153
152
|
await tenant.removeMember(organizations.home, people.leaver);
|
|
154
153
|
const claims = await introspect(access_token);
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
expect(claims.org_id).toBeFalsy();
|
|
155
|
+
expect(sorted(claims.permissions)).toStrictEqual([]);
|
|
157
156
|
});
|
|
158
157
|
});
|
|
159
158
|
describe("GET /api/memberships", () => {
|
|
160
159
|
it("lists the organizations the caller belongs to, and no others", async () => {
|
|
161
160
|
const { access_token } = await tenant.signIn(people.member);
|
|
162
161
|
const response = await send(new URL("/api/memberships", tenant.issuer), { headers: { authorization: `Bearer ${access_token}` } });
|
|
163
|
-
|
|
164
|
-
const body = await response.json();
|
|
165
|
-
|
|
162
|
+
expect(response.status).toStrictEqual(200);
|
|
163
|
+
const body = (await response.json());
|
|
164
|
+
expect(body.memberships.map((membership) => membership.org_id).sort()).toStrictEqual([organizations.home, organizations.other].sort());
|
|
166
165
|
for (const membership of body.memberships) {
|
|
167
|
-
|
|
166
|
+
expect(membership.roles).toStrictEqual(["member"]);
|
|
168
167
|
assert(membership.org_slug, "every membership names its slug");
|
|
169
168
|
assert(membership.name, "every membership names its organization");
|
|
170
169
|
}
|
|
171
|
-
|
|
170
|
+
expect(body.cursor).toStrictEqual(null);
|
|
172
171
|
});
|
|
173
172
|
it("refuses a caller with no bearer token", async () => {
|
|
174
173
|
const response = await send(new URL("/api/memberships", tenant.issuer));
|
|
175
174
|
await response.body?.cancel();
|
|
176
|
-
|
|
175
|
+
expect(response.status).toStrictEqual(401);
|
|
177
176
|
});
|
|
178
177
|
});
|
|
179
178
|
describe("POST /api/check", () => {
|
|
@@ -182,8 +181,8 @@ export function runTenantContractTests(options) {
|
|
|
182
181
|
const answer = await ask(access_token, "/api/check", {
|
|
183
182
|
permissions: ["contract:home", "contract:other", "contract:tenant"],
|
|
184
183
|
});
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
expect(answer.status).toStrictEqual(200);
|
|
185
|
+
expect(answer.body).toStrictEqual({
|
|
187
186
|
subject: people.member,
|
|
188
187
|
resource: { type: "organization", id: organizations.home },
|
|
189
188
|
results: {
|
|
@@ -198,7 +197,7 @@ export function runTenantContractTests(options) {
|
|
|
198
197
|
const answer = await ask(access_token, "/api/check", {
|
|
199
198
|
permissions: ["contract:home", "contract:tenant"],
|
|
200
199
|
});
|
|
201
|
-
|
|
200
|
+
expect(answer.body).toStrictEqual({
|
|
202
201
|
subject: people.member,
|
|
203
202
|
resource: null,
|
|
204
203
|
results: { "contract:home": false, "contract:tenant": true },
|
|
@@ -210,8 +209,8 @@ export function runTenantContractTests(options) {
|
|
|
210
209
|
permissions: ["contract:other"],
|
|
211
210
|
resource: { type: "organization", id: organizations.other },
|
|
212
211
|
});
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
expect(answer.status).toStrictEqual(200);
|
|
213
|
+
expect(answer.body.results).toStrictEqual({ "contract:other": true });
|
|
215
214
|
});
|
|
216
215
|
it("answers an organization the caller does not belong to from tenant-wide permissions alone", async () => {
|
|
217
216
|
const { access_token } = await tenant.signIn(people.member);
|
|
@@ -219,8 +218,8 @@ export function runTenantContractTests(options) {
|
|
|
219
218
|
permissions: ["contract:other", "contract:tenant"],
|
|
220
219
|
resource: { type: "organization", id: organizations.unjoined },
|
|
221
220
|
});
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
expect(answer.status).toStrictEqual(200);
|
|
222
|
+
expect(answer.body.results).toStrictEqual({
|
|
224
223
|
"contract:other": false,
|
|
225
224
|
"contract:tenant": true,
|
|
226
225
|
});
|
|
@@ -231,7 +230,7 @@ export function runTenantContractTests(options) {
|
|
|
231
230
|
permissions: ["contract:tenant"],
|
|
232
231
|
resource: { type: "organization", id: crypto.randomUUID() },
|
|
233
232
|
});
|
|
234
|
-
|
|
233
|
+
expect(answer.status).toStrictEqual(404);
|
|
235
234
|
});
|
|
236
235
|
it("answers a resource from a grant to the person and from one to their organization", async () => {
|
|
237
236
|
const { access_token } = await tenant.signIn(people.outsider);
|
|
@@ -239,8 +238,8 @@ export function runTenantContractTests(options) {
|
|
|
239
238
|
permissions: ["contract:read", "contract:write"],
|
|
240
239
|
resource: { type: "contract_document", id: "direct" },
|
|
241
240
|
});
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
expect(direct.status).toStrictEqual(200);
|
|
242
|
+
expect(direct.body.results).toStrictEqual({
|
|
244
243
|
"contract:read": true,
|
|
245
244
|
"contract:write": false,
|
|
246
245
|
});
|
|
@@ -248,7 +247,7 @@ export function runTenantContractTests(options) {
|
|
|
248
247
|
permissions: ["contract:write"],
|
|
249
248
|
resource: { type: "contract_document", id: "shared" },
|
|
250
249
|
});
|
|
251
|
-
|
|
250
|
+
expect(shared.body.results).toStrictEqual({ "contract:write": true });
|
|
252
251
|
});
|
|
253
252
|
it("leaves organization permissions out of a resource answer", async () => {
|
|
254
253
|
const { access_token } = await tenant.signIn(people.member, organizations.home);
|
|
@@ -256,7 +255,7 @@ export function runTenantContractTests(options) {
|
|
|
256
255
|
permissions: ["contract:home", "contract:tenant"],
|
|
257
256
|
resource: { type: "contract_document", id: "direct" },
|
|
258
257
|
});
|
|
259
|
-
|
|
258
|
+
expect(answer.body.results).toStrictEqual({
|
|
260
259
|
"contract:home": false,
|
|
261
260
|
"contract:tenant": true,
|
|
262
261
|
});
|
|
@@ -267,7 +266,7 @@ export function runTenantContractTests(options) {
|
|
|
267
266
|
permissions: ["contract:read"],
|
|
268
267
|
resource: { type: "contract_unregistered", id: "x" },
|
|
269
268
|
});
|
|
270
|
-
|
|
269
|
+
expect(answer.status).toStrictEqual(400);
|
|
271
270
|
});
|
|
272
271
|
it("refuses a caller with no bearer token", async () => {
|
|
273
272
|
const response = await send(new URL("/api/check", tenant.issuer), {
|
|
@@ -276,7 +275,7 @@ export function runTenantContractTests(options) {
|
|
|
276
275
|
body: JSON.stringify({ permissions: ["contract:tenant"] }),
|
|
277
276
|
});
|
|
278
277
|
await response.body?.cancel();
|
|
279
|
-
|
|
278
|
+
expect(response.status).toStrictEqual(401);
|
|
280
279
|
});
|
|
281
280
|
});
|
|
282
281
|
describe("POST /api/check/batch", () => {
|
|
@@ -286,8 +285,8 @@ export function runTenantContractTests(options) {
|
|
|
286
285
|
permissions: ["contract:read"],
|
|
287
286
|
resource: { type: "contract_document", ids: ["direct", "unseen"] },
|
|
288
287
|
});
|
|
289
|
-
|
|
290
|
-
|
|
288
|
+
expect(answer.status).toStrictEqual(200);
|
|
289
|
+
expect(answer.body).toStrictEqual({
|
|
291
290
|
subject: people.outsider,
|
|
292
291
|
resource: { type: "contract_document" },
|
|
293
292
|
results: {
|
|
@@ -336,13 +335,13 @@ export function runTenantContractTests(options) {
|
|
|
336
335
|
}
|
|
337
336
|
async function membershipsOf(accessToken) {
|
|
338
337
|
const reply = await call(accessToken, "GET", "/api/memberships");
|
|
339
|
-
|
|
338
|
+
expect(reply.status).toStrictEqual(200);
|
|
340
339
|
return reply.body.memberships;
|
|
341
340
|
}
|
|
342
341
|
async function createOrganization(accessToken) {
|
|
343
342
|
const slug = newSlug();
|
|
344
343
|
const reply = await call(accessToken, "POST", "/api/organizations", { name: `Contract ${slug}`, slug });
|
|
345
|
-
|
|
344
|
+
expect(reply.status, JSON.stringify(reply.body)).toStrictEqual(201);
|
|
346
345
|
return reply.body;
|
|
347
346
|
}
|
|
348
347
|
async function invite(accessToken, organizationId, email, role) {
|
|
@@ -350,15 +349,15 @@ export function runTenantContractTests(options) {
|
|
|
350
349
|
}
|
|
351
350
|
async function accept(accessToken, offerId) {
|
|
352
351
|
const reply = await call(accessToken, "POST", `/api/organizations/offers/${offerId}/accept`);
|
|
353
|
-
|
|
352
|
+
expect(reply.status, JSON.stringify(reply.body)).toStrictEqual(200);
|
|
354
353
|
return reply.body;
|
|
355
354
|
}
|
|
356
355
|
async function seat(managerToken, organizationId, invitee, role) {
|
|
357
356
|
const offered = await invite(managerToken, organizationId, invitee.email, role);
|
|
358
|
-
|
|
357
|
+
expect(offered.status, JSON.stringify(offered.body)).toStrictEqual(201);
|
|
359
358
|
assert(offered.body.status === "membership", offered.body.status);
|
|
360
359
|
const accepted = await accept(invitee.token, offered.body.membership.id);
|
|
361
|
-
|
|
360
|
+
expect(accepted.status).toStrictEqual("accepted");
|
|
362
361
|
}
|
|
363
362
|
async function membersOf(accessToken, organizationId) {
|
|
364
363
|
return await call(accessToken, "GET", `/api/organizations/${organizationId}/members`);
|
|
@@ -386,114 +385,127 @@ export function runTenantContractTests(options) {
|
|
|
386
385
|
});
|
|
387
386
|
async function invitationIdsOf(accessToken, organizationId) {
|
|
388
387
|
const listed = await call(accessToken, "GET", `/api/organizations/${organizationId}/invitations`);
|
|
389
|
-
|
|
388
|
+
expect(listed.status).toStrictEqual(200);
|
|
390
389
|
return listed.body.data.map((entry) => entry.id);
|
|
391
390
|
}
|
|
392
391
|
it("creates an organization its creator owns", async () => {
|
|
393
392
|
const creator = await person();
|
|
394
393
|
const slug = newSlug();
|
|
395
394
|
const created = await call(creator.token, "POST", "/api/organizations", { name: "Contract Created", slug });
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
395
|
+
expect(created.status).toStrictEqual(201);
|
|
396
|
+
expect(created.body.slug).toStrictEqual(slug);
|
|
397
|
+
expect(created.body.name).toStrictEqual("Contract Created");
|
|
399
398
|
const listed = await call(creator.token, "GET", "/api/organizations");
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
399
|
+
expect(listed.status).toStrictEqual(200);
|
|
400
|
+
expect(listed.body.data.map((entry) => entry.id)).toStrictEqual([
|
|
401
|
+
created.body.id,
|
|
402
|
+
]);
|
|
403
|
+
expect(await membershipsOf(creator.token)).toStrictEqual([
|
|
404
|
+
{
|
|
403
405
|
org_id: created.body.id,
|
|
404
406
|
org_slug: slug,
|
|
405
407
|
name: "Contract Created",
|
|
406
408
|
roles: ["owner"],
|
|
407
|
-
}
|
|
409
|
+
},
|
|
410
|
+
]);
|
|
408
411
|
});
|
|
409
412
|
it("refuses a slug another organization holds", async () => {
|
|
410
413
|
const reply = await call(seats.outsider.token, "POST", "/api/organizations", { name: "Contract Taken", slug: organization.slug });
|
|
411
|
-
|
|
414
|
+
expect(reply.status).toStrictEqual(409);
|
|
412
415
|
});
|
|
413
416
|
it("refuses a slug that is not lowercase letters, numbers and single hyphens", async () => {
|
|
414
417
|
const reply = await call(seats.outsider.token, "POST", "/api/organizations", { name: "Contract Shouting", slug: "Contract--Shouting" });
|
|
415
|
-
|
|
418
|
+
expect(reply.status).toStrictEqual(400);
|
|
416
419
|
});
|
|
417
420
|
it("lets a member of any tier read the organization and its members, and no cache keep them", async () => {
|
|
421
|
+
const organization = await createOrganization(seats.owner.token);
|
|
422
|
+
await seat(seats.owner.token, organization.id, seats.admin, "admin");
|
|
423
|
+
await seat(seats.owner.token, organization.id, seats.member, "member");
|
|
418
424
|
const read = await call(seats.member.token, "GET", `/api/organizations/${organization.id}`);
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
425
|
+
expect(read.status).toStrictEqual(200);
|
|
426
|
+
expect(read.body.id).toStrictEqual(organization.id);
|
|
427
|
+
expect(read.body.slug).toStrictEqual(organization.slug);
|
|
428
|
+
expect(read.headers.get("cache-control") ?? "").toContain("no-store");
|
|
423
429
|
const members = await membersOf(seats.member.token, organization.id);
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
430
|
+
expect(members.status).toStrictEqual(200);
|
|
431
|
+
expect(members.headers.get("cache-control") ?? "").toContain("no-store");
|
|
432
|
+
expect(members.body.data.map((row) => `${row.userId}:${row.role}`).sort()).toStrictEqual([
|
|
427
433
|
`${seats.admin.id}:admin`,
|
|
428
434
|
`${seats.member.id}:member`,
|
|
429
435
|
`${seats.owner.id}:owner`,
|
|
430
436
|
].sort());
|
|
431
|
-
|
|
437
|
+
expect(members.body.hasMore).toBeFalsy();
|
|
432
438
|
for (const row of members.body.data) {
|
|
433
439
|
assert(row.id, "every member row names its grant");
|
|
434
440
|
assert(row.name, "every member row names its person");
|
|
435
441
|
assert(row.acceptedAt, "a member row is an accepted grant");
|
|
436
442
|
}
|
|
437
|
-
|
|
438
|
-
?.email
|
|
443
|
+
expect(members.body.data.find((row) => row.userId === seats.member.id)
|
|
444
|
+
?.email).toStrictEqual(seats.member.email);
|
|
439
445
|
});
|
|
440
446
|
it("answers a non-member, an unknown id and a malformed id with the same 404", async () => {
|
|
441
447
|
const outsider = await call(seats.outsider.token, "GET", `/api/organizations/${organization.id}`);
|
|
442
448
|
const unknown = await call(seats.owner.token, "GET", `/api/organizations/${crypto.randomUUID()}`);
|
|
443
449
|
const malformed = await call(seats.owner.token, "GET", "/api/organizations/not-an-organization");
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
450
|
+
expect([
|
|
451
|
+
outsider.status,
|
|
452
|
+
unknown.status,
|
|
453
|
+
malformed.status,
|
|
454
|
+
]).toStrictEqual([404, 404, 404]);
|
|
455
|
+
expect(outsider.body.detail).toStrictEqual(unknown.body.detail);
|
|
456
|
+
expect(unknown.body.detail).toStrictEqual(malformed.body.detail);
|
|
447
457
|
const members = await membersOf(seats.outsider.token, organization.id);
|
|
448
|
-
|
|
458
|
+
expect(members.status).toStrictEqual(404);
|
|
449
459
|
});
|
|
450
460
|
it("lets an owner or admin rename the organization and a plain member not", async () => {
|
|
451
461
|
const renamed = await call(seats.admin.token, "PATCH", `/api/organizations/${organization.id}`, { name: "Contract Renamed" });
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
462
|
+
expect(renamed.status).toStrictEqual(200);
|
|
463
|
+
expect(renamed.body.name).toStrictEqual("Contract Renamed");
|
|
464
|
+
expect(renamed.body.slug).toStrictEqual(organization.slug);
|
|
455
465
|
for (const caller of [seats.member, seats.outsider]) {
|
|
456
466
|
const refused = await call(caller.token, "PATCH", `/api/organizations/${organization.id}`, { name: "Contract Hijacked" });
|
|
457
|
-
|
|
467
|
+
expect(refused.status).toStrictEqual(404);
|
|
458
468
|
}
|
|
459
469
|
});
|
|
460
470
|
it("lists the membership tiers an invitation may offer, to a manager only", async () => {
|
|
461
471
|
const roles = await call(seats.admin.token, "GET", `/api/organizations/${organization.id}/member-roles`);
|
|
462
|
-
|
|
463
|
-
|
|
472
|
+
expect(roles.status).toStrictEqual(200);
|
|
473
|
+
expect(roles.body.slice(0, 3)).toStrictEqual([
|
|
464
474
|
{ slug: "owner", name: "Owner" },
|
|
465
475
|
{ slug: "admin", name: "Admin" },
|
|
466
476
|
{ slug: "member", name: "Member" },
|
|
467
477
|
]);
|
|
468
478
|
const refused = await call(seats.member.token, "GET", `/api/organizations/${organization.id}/member-roles`);
|
|
469
|
-
|
|
479
|
+
expect(refused.status).toStrictEqual(404);
|
|
470
480
|
});
|
|
471
481
|
it("offers an existing person a pending membership that confers nothing until they accept it", async () => {
|
|
472
482
|
const invitee = await person();
|
|
473
483
|
const offered = await invite(seats.admin.token, organization.id, invitee.email, "member");
|
|
474
|
-
|
|
484
|
+
expect(offered.status).toStrictEqual(201);
|
|
475
485
|
assert(offered.body.status === "membership", offered.body.status);
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
486
|
+
expect(offered.body.membership.role).toStrictEqual("member");
|
|
487
|
+
expect(offered.body.membership.acceptedAt).toStrictEqual(null);
|
|
488
|
+
expect(await membershipsOf(invitee.token)).toStrictEqual([]);
|
|
479
489
|
const before = await membersOf(seats.admin.token, organization.id);
|
|
480
|
-
|
|
490
|
+
expect(before.body.data.some((row) => row.userId === invitee.id), "a pending grant is not a member").toBeFalsy();
|
|
481
491
|
const waiting = await call(invitee.token, "GET", "/api/organizations/offers");
|
|
482
|
-
|
|
492
|
+
expect(waiting.status).toStrictEqual(200);
|
|
483
493
|
const current = await call(seats.owner.token, "GET", `/api/organizations/${organization.id}`);
|
|
484
|
-
|
|
485
|
-
offers: [
|
|
494
|
+
expect(waiting.body).toStrictEqual({
|
|
495
|
+
offers: [
|
|
496
|
+
{
|
|
486
497
|
id: offered.body.membership.id,
|
|
487
498
|
organizationName: current.body.name,
|
|
488
499
|
roleName: "Member",
|
|
489
|
-
}
|
|
500
|
+
},
|
|
501
|
+
],
|
|
490
502
|
unverifiedEmail: null,
|
|
491
503
|
});
|
|
492
504
|
const accepted = await accept(invitee.token, offered.body.membership.id);
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
505
|
+
expect(accepted.status).toStrictEqual("accepted");
|
|
506
|
+
expect(accepted.membership?.organizationId).toStrictEqual(organization.id);
|
|
507
|
+
expect(accepted.membership?.role).toStrictEqual("member");
|
|
508
|
+
expect((await membershipsOf(invitee.token)).map((entry) => entry.roles)).toStrictEqual([["member"]]);
|
|
497
509
|
const after = await membersOf(seats.admin.token, organization.id);
|
|
498
510
|
assert(after.body.data.some((row) => row.userId === invitee.id));
|
|
499
511
|
});
|
|
@@ -502,24 +514,24 @@ export function runTenantContractTests(options) {
|
|
|
502
514
|
const offered = await invite(seats.owner.token, organization.id, invitee.email, "member");
|
|
503
515
|
assert(offered.body.status === "membership", offered.body.status);
|
|
504
516
|
const offerId = offered.body.membership.id;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
517
|
+
expect((await accept(seats.outsider.token, offerId)).status).toStrictEqual("invalid");
|
|
518
|
+
expect((await accept(seats.outsider.token, "not-an-offer")).status).toStrictEqual("invalid");
|
|
519
|
+
expect((await accept(invitee.token, offerId)).status).toStrictEqual("accepted");
|
|
508
520
|
});
|
|
509
521
|
it("refuses a second offer of a role an address already holds or was offered", async () => {
|
|
510
522
|
const stranger = newEmail();
|
|
511
523
|
const first = await invite(seats.admin.token, organization.id, stranger, "member");
|
|
512
|
-
|
|
524
|
+
expect(first.status).toStrictEqual(201);
|
|
513
525
|
const again = await invite(seats.admin.token, organization.id, stranger, "member");
|
|
514
|
-
|
|
526
|
+
expect(again.status).toStrictEqual(409);
|
|
515
527
|
const held = await invite(seats.admin.token, organization.id, seats.member.email, "member");
|
|
516
|
-
|
|
528
|
+
expect(held.status).toStrictEqual(409);
|
|
517
529
|
});
|
|
518
530
|
it("keeps the owner tier to owners", async () => {
|
|
519
531
|
const offered = await invite(seats.admin.token, organization.id, newEmail(), "owner");
|
|
520
|
-
|
|
532
|
+
expect(offered.status).toStrictEqual(403);
|
|
521
533
|
const revoked = await call(seats.admin.token, "DELETE", `/api/organizations/${organization.id}/members/${seats.owner.id}/owner`);
|
|
522
|
-
|
|
534
|
+
expect(revoked.status).toStrictEqual(403);
|
|
523
535
|
});
|
|
524
536
|
it("refuses a plain member every manager action with the 404 a stranger gets, and changes nothing", async () => {
|
|
525
537
|
const pending = await invite(seats.admin.token, organization.id, newEmail(), "member");
|
|
@@ -534,8 +546,8 @@ export function runTenantContractTests(options) {
|
|
|
534
546
|
for (const [method, path] of actions) {
|
|
535
547
|
const byMember = await call(seats.member.token, method, path);
|
|
536
548
|
const byStranger = await call(seats.outsider.token, method, path);
|
|
537
|
-
|
|
538
|
-
|
|
549
|
+
expect([byMember.status, byStranger.status], `${method} ${path}`).toStrictEqual([404, 404]);
|
|
550
|
+
expect(byMember.body.detail).toStrictEqual(byStranger.body.detail);
|
|
539
551
|
}
|
|
540
552
|
const members = await membersOf(seats.owner.token, organization.id);
|
|
541
553
|
assert(members.body.data.some((row) => row.userId === seats.admin.id && row.role === "admin"), "a plain member revoked a manager's tier");
|
|
@@ -544,10 +556,10 @@ export function runTenantContractTests(options) {
|
|
|
544
556
|
it("keeps a manager's actions to the organization the path names", async () => {
|
|
545
557
|
const base = `/api/organizations/${organization.id}`;
|
|
546
558
|
const withdrawn = await call(seats.owner.token, "DELETE", `${base}/invitations/${rivalInvitationId}`);
|
|
547
|
-
|
|
559
|
+
expect(withdrawn.status).toStrictEqual(404);
|
|
548
560
|
const revoked = await call(seats.owner.token, "DELETE", `${base}/members/${seats.rivalMember.id}/member`);
|
|
549
|
-
|
|
550
|
-
|
|
561
|
+
expect(revoked.status).toStrictEqual(404);
|
|
562
|
+
expect((await invitationIdsOf(seats.owner.token, organization.id)).includes(rivalInvitationId), "one organization's invitations listed another's").toBeFalsy();
|
|
551
563
|
const rivalBase = `/api/organizations/${rival.id}`;
|
|
552
564
|
for (const [method, path] of [
|
|
553
565
|
["DELETE", `${rivalBase}/invitations/${rivalInvitationId}`],
|
|
@@ -555,7 +567,7 @@ export function runTenantContractTests(options) {
|
|
|
555
567
|
["GET", `${rivalBase}/invitations`],
|
|
556
568
|
]) {
|
|
557
569
|
const refused = await call(seats.owner.token, method, path);
|
|
558
|
-
|
|
570
|
+
expect(refused.status, `${method} ${path}`).toStrictEqual(404);
|
|
559
571
|
}
|
|
560
572
|
assert((await invitationIdsOf(seats.rival.token, rival.id)).includes(rivalInvitationId), "another organization's manager withdrew this invitation");
|
|
561
573
|
const rivalMembers = await membersOf(seats.rival.token, rival.id);
|
|
@@ -563,11 +575,11 @@ export function runTenantContractTests(options) {
|
|
|
563
575
|
});
|
|
564
576
|
it("refuses an invitation from a plain member as it refuses a stranger", async () => {
|
|
565
577
|
const offered = await invite(seats.member.token, organization.id, newEmail(), "member");
|
|
566
|
-
|
|
578
|
+
expect(offered.status).toStrictEqual(404);
|
|
567
579
|
});
|
|
568
580
|
it("refuses a role the tenant has not defined", async () => {
|
|
569
581
|
const offered = await invite(seats.admin.token, organization.id, newEmail(), "contract-undefined-role");
|
|
570
|
-
|
|
582
|
+
expect(offered.status).toStrictEqual(400);
|
|
571
583
|
});
|
|
572
584
|
it("refuses a return_to on an origin with no registered redirect URI", async () => {
|
|
573
585
|
const reply = await call(seats.admin.token, "POST", `/api/organizations/${organization.id}/invitations`, {
|
|
@@ -575,25 +587,27 @@ export function runTenantContractTests(options) {
|
|
|
575
587
|
role: "member",
|
|
576
588
|
return_to: "https://unregistered.example/welcome",
|
|
577
589
|
});
|
|
578
|
-
|
|
590
|
+
expect(reply.status).toStrictEqual(400);
|
|
579
591
|
});
|
|
580
592
|
it("invites an address with no account, lowercased, for its verified holder to accept later", async () => {
|
|
581
593
|
const address = newEmail();
|
|
582
594
|
const offered = await invite(seats.admin.token, organization.id, address.toUpperCase(), "member");
|
|
583
|
-
|
|
595
|
+
expect(offered.status).toStrictEqual(201);
|
|
584
596
|
assert(offered.body.status === "invitation", offered.body.status);
|
|
585
|
-
|
|
597
|
+
expect(offered.body.invitation.email).toStrictEqual(address);
|
|
586
598
|
const invitationId = offered.body.invitation.id;
|
|
587
599
|
const listed = await call(seats.admin.token, "GET", `/api/organizations/${organization.id}/invitations?status=outstanding`);
|
|
588
|
-
|
|
600
|
+
expect(listed.status).toStrictEqual(200);
|
|
589
601
|
assert(listed.body.data.some((entry) => entry.id === invitationId));
|
|
590
602
|
const holder = await person({ email: address });
|
|
591
603
|
const waiting = await call(holder.token, "GET", "/api/organizations/offers");
|
|
592
|
-
|
|
604
|
+
expect(waiting.body.offers.map((offer) => offer.id)).toStrictEqual([
|
|
605
|
+
invitationId,
|
|
606
|
+
]);
|
|
593
607
|
const accepted = await accept(holder.token, invitationId);
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
608
|
+
expect(accepted.status).toStrictEqual("accepted");
|
|
609
|
+
expect(accepted.membership?.organizationId).toStrictEqual(organization.id);
|
|
610
|
+
expect((await membershipsOf(holder.token)).map((entry) => entry.org_id)).toStrictEqual([organization.id]);
|
|
597
611
|
});
|
|
598
612
|
it("hides an invitation from its address's unverified holder and names the address instead", async () => {
|
|
599
613
|
const address = newEmail();
|
|
@@ -601,14 +615,17 @@ export function runTenantContractTests(options) {
|
|
|
601
615
|
assert(offered.body.status === "invitation", offered.body.status);
|
|
602
616
|
const holder = await person({ email: address, emailVerified: false });
|
|
603
617
|
const waiting = await call(holder.token, "GET", "/api/organizations/offers");
|
|
604
|
-
|
|
605
|
-
|
|
618
|
+
expect(waiting.body).toStrictEqual({
|
|
619
|
+
offers: [],
|
|
620
|
+
unverifiedEmail: address,
|
|
621
|
+
});
|
|
622
|
+
expect((await accept(holder.token, offered.body.invitation.id)).status).toStrictEqual("wrong-account");
|
|
606
623
|
});
|
|
607
624
|
it("converts an invitation only for the address it names", async () => {
|
|
608
625
|
const offered = await invite(seats.admin.token, organization.id, newEmail(), "member");
|
|
609
626
|
assert(offered.body.status === "invitation", offered.body.status);
|
|
610
627
|
const other = await person();
|
|
611
|
-
|
|
628
|
+
expect((await accept(other.token, offered.body.invitation.id)).status).toStrictEqual("wrong-account");
|
|
612
629
|
});
|
|
613
630
|
it("withdraws an invitation so that it can no longer be accepted", async () => {
|
|
614
631
|
const address = newEmail();
|
|
@@ -616,35 +633,35 @@ export function runTenantContractTests(options) {
|
|
|
616
633
|
assert(offered.body.status === "invitation", offered.body.status);
|
|
617
634
|
const invitationId = offered.body.invitation.id;
|
|
618
635
|
const withdrawn = await call(seats.admin.token, "DELETE", `/api/organizations/${organization.id}/invitations/${invitationId}`);
|
|
619
|
-
|
|
636
|
+
expect(withdrawn.status).toStrictEqual(204);
|
|
620
637
|
const listed = await call(seats.admin.token, "GET", `/api/organizations/${organization.id}/invitations`);
|
|
621
|
-
|
|
638
|
+
expect(listed.body.data.some((entry) => entry.id === invitationId)).toBeFalsy();
|
|
622
639
|
const holder = await person({ email: address });
|
|
623
|
-
|
|
640
|
+
expect((await accept(holder.token, invitationId)).status).toStrictEqual("invalid");
|
|
624
641
|
});
|
|
625
642
|
it("ends a member's access when a manager revokes their tier", async () => {
|
|
626
643
|
const leaver = await person();
|
|
627
644
|
await seat(seats.admin.token, organization.id, leaver, "member");
|
|
628
645
|
const revoked = await call(seats.admin.token, "DELETE", `/api/organizations/${organization.id}/members/${leaver.id}/member`);
|
|
629
|
-
|
|
630
|
-
|
|
646
|
+
expect(revoked.status).toStrictEqual(204);
|
|
647
|
+
expect(await membershipsOf(leaver.token)).toStrictEqual([]);
|
|
631
648
|
const read = await call(leaver.token, "GET", `/api/organizations/${organization.id}`);
|
|
632
|
-
|
|
649
|
+
expect(read.status).toStrictEqual(404);
|
|
633
650
|
const again = await call(seats.admin.token, "DELETE", `/api/organizations/${organization.id}/members/${leaver.id}/member`);
|
|
634
|
-
|
|
651
|
+
expect(again.status).toStrictEqual(404);
|
|
635
652
|
});
|
|
636
653
|
it("never leaves an organization without an owner", async () => {
|
|
637
654
|
const reply = await call(seats.owner.token, "DELETE", `/api/organizations/${organization.id}/members/${seats.owner.id}/owner`);
|
|
638
|
-
|
|
655
|
+
expect(reply.status).toStrictEqual(409);
|
|
639
656
|
});
|
|
640
657
|
it("deletes an organization for its owner alone, and not while it holds a grant", async () => {
|
|
641
658
|
const doomed = await createOrganization(seats.owner.token);
|
|
642
659
|
await seat(seats.owner.token, doomed.id, seats.admin, "admin");
|
|
643
660
|
await seat(seats.owner.token, doomed.id, seats.member, "member");
|
|
644
661
|
const byAdmin = await call(seats.admin.token, "DELETE", `/api/organizations/${doomed.id}`);
|
|
645
|
-
|
|
662
|
+
expect(byAdmin.status).toStrictEqual(403);
|
|
646
663
|
const byMember = await call(seats.member.token, "DELETE", `/api/organizations/${doomed.id}`);
|
|
647
|
-
|
|
664
|
+
expect(byMember.status).toStrictEqual(404);
|
|
648
665
|
const holding = await createOrganization(seats.owner.token);
|
|
649
666
|
await tenant.grant({
|
|
650
667
|
resource: { type: "contract_document", id: `held-${holding.id}` },
|
|
@@ -652,16 +669,16 @@ export function runTenantContractTests(options) {
|
|
|
652
669
|
permissions: ["contract:read"],
|
|
653
670
|
});
|
|
654
671
|
const blocked = await call(seats.owner.token, "DELETE", `/api/organizations/${holding.id}`);
|
|
655
|
-
|
|
672
|
+
expect(blocked.status).toStrictEqual(409);
|
|
656
673
|
const deleted = await call(seats.owner.token, "DELETE", `/api/organizations/${doomed.id}`);
|
|
657
|
-
|
|
674
|
+
expect(deleted.status).toStrictEqual(204);
|
|
658
675
|
const gone = await call(seats.member.token, "GET", `/api/organizations/${doomed.id}`);
|
|
659
|
-
|
|
660
|
-
|
|
676
|
+
expect(gone.status).toStrictEqual(404);
|
|
677
|
+
expect((await membershipsOf(seats.member.token)).some((entry) => entry.org_id === doomed.id)).toBeFalsy();
|
|
661
678
|
});
|
|
662
679
|
it("refuses a caller with no bearer token", async () => {
|
|
663
680
|
const reply = await call(null, "GET", "/api/organizations");
|
|
664
|
-
|
|
681
|
+
expect(reply.status).toStrictEqual(401);
|
|
665
682
|
});
|
|
666
683
|
describe("application roles a manager grants a member", () => {
|
|
667
684
|
const APP_PERMISSION = "contract:app";
|
|
@@ -670,7 +687,7 @@ export function runTenantContractTests(options) {
|
|
|
670
687
|
beforeAll(async () => {
|
|
671
688
|
roleId = await tenant.addRole([APP_PERMISSION]);
|
|
672
689
|
const listed = await call(seats.admin.token, "GET", `/api/organizations/${organization.id}/member-roles`);
|
|
673
|
-
|
|
690
|
+
expect(listed.status).toStrictEqual(200);
|
|
674
691
|
const found = listed.body.find((entry) => entry.id === roleId);
|
|
675
692
|
assert(found, "member-roles does not list the role by its id");
|
|
676
693
|
option = found;
|
|
@@ -686,77 +703,79 @@ export function runTenantContractTests(options) {
|
|
|
686
703
|
}
|
|
687
704
|
async function heldBy(userId, managerToken = seats.admin.token, organizationId = organization.id) {
|
|
688
705
|
const reply = await call(managerToken, "GET", rolesPath(userId, organizationId));
|
|
689
|
-
|
|
706
|
+
expect(reply.status, JSON.stringify(reply.body)).toStrictEqual(200);
|
|
690
707
|
return reply.body;
|
|
691
708
|
}
|
|
692
709
|
async function holds(accessToken, organizationId) {
|
|
693
710
|
const reply = await ask(accessToken, "/api/check", {
|
|
694
711
|
permissions: [APP_PERMISSION],
|
|
695
|
-
...organizationId
|
|
712
|
+
...(organizationId
|
|
696
713
|
? { resource: { type: "organization", id: organizationId } }
|
|
697
|
-
: {},
|
|
714
|
+
: {}),
|
|
698
715
|
});
|
|
699
|
-
|
|
716
|
+
expect(reply.status).toStrictEqual(200);
|
|
700
717
|
return reply.body.results[APP_PERMISSION];
|
|
701
718
|
}
|
|
702
719
|
async function offersOf(accessToken) {
|
|
703
720
|
const reply = await call(accessToken, "GET", "/api/organizations/offers");
|
|
704
|
-
|
|
721
|
+
expect(reply.status).toStrictEqual(200);
|
|
705
722
|
return reply.body.offers.map((offer) => offer.id).sort();
|
|
706
723
|
}
|
|
707
724
|
it("names a tenant-wide role in member-roles by the id a grant takes, and no built-in tier by any", async () => {
|
|
708
|
-
|
|
725
|
+
expect(option.id).toStrictEqual(roleId);
|
|
709
726
|
assert(option.slug, "the role is listed by its slug");
|
|
710
727
|
assert(option.name, "the role is listed by its name");
|
|
711
728
|
const listed = await call(seats.owner.token, "GET", `/api/organizations/${organization.id}/member-roles`);
|
|
712
729
|
for (const slug of ["owner", "admin", "member"]) {
|
|
713
730
|
const tier = listed.body.find((entry) => entry.slug === slug);
|
|
714
731
|
assert(tier, `the built-in ${slug} tier is listed`);
|
|
715
|
-
|
|
732
|
+
expect("id" in tier, `the built-in ${slug} tier has an id`).toBeFalsy();
|
|
716
733
|
}
|
|
717
734
|
});
|
|
718
735
|
it("grants an accepted member a role that counts inside the organization only", async () => {
|
|
719
736
|
const member = await seated();
|
|
720
737
|
await seat(seats.rival.token, rival.id, member, "member");
|
|
721
|
-
|
|
738
|
+
expect(await holds(member.token, organization.id)).toBeFalsy();
|
|
722
739
|
const granted = await grantRole(seats.admin.token, member.id);
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
740
|
+
expect(granted.status, JSON.stringify(granted.body)).toStrictEqual(201);
|
|
741
|
+
expect(granted.body.subjectId).toStrictEqual(member.id);
|
|
742
|
+
expect(granted.body.roleId).toStrictEqual(roleId);
|
|
743
|
+
expect(granted.body.builtInRole).toStrictEqual(null);
|
|
744
|
+
expect(granted.body.scopeType).toStrictEqual("organization");
|
|
745
|
+
expect(granted.body.scopeId).toStrictEqual(organization.id);
|
|
746
|
+
expect(granted.body.created).toStrictEqual(true);
|
|
730
747
|
assert(await holds(member.token, organization.id));
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
748
|
+
expect(await holds(member.token), "a role held in an organization answered at tenant scope").toBeFalsy();
|
|
749
|
+
expect(await holds(member.token, rival.id), "a role held in one organization answered for another it belongs to").toBeFalsy();
|
|
750
|
+
expect(await heldBy(member.id)).toStrictEqual([
|
|
751
|
+
{
|
|
734
752
|
id: roleId,
|
|
735
753
|
builtInRole: null,
|
|
736
754
|
organizationId: null,
|
|
737
755
|
slug: option.slug,
|
|
738
756
|
name: option.name,
|
|
739
757
|
permissions: [APP_PERMISSION],
|
|
740
|
-
}
|
|
758
|
+
},
|
|
759
|
+
]);
|
|
741
760
|
const again = await grantRole(seats.owner.token, member.id);
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
761
|
+
expect(again.status).toStrictEqual(201);
|
|
762
|
+
expect(again.body.id).toStrictEqual(granted.body.id);
|
|
763
|
+
expect(again.body.created).toStrictEqual(false);
|
|
764
|
+
expect((await heldBy(member.id)).length).toStrictEqual(1);
|
|
746
765
|
});
|
|
747
766
|
it("revokes a role, after which the permission stops answering and the role is not found", async () => {
|
|
748
767
|
const member = await seated();
|
|
749
|
-
|
|
768
|
+
expect((await grantRole(seats.admin.token, member.id)).status).toStrictEqual(201);
|
|
750
769
|
const revoked = await call(seats.admin.token, "DELETE", `${rolesPath(member.id)}/${roleId}`);
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
770
|
+
expect(revoked.status).toStrictEqual(204);
|
|
771
|
+
expect(await holds(member.token, organization.id)).toBeFalsy();
|
|
772
|
+
expect(await heldBy(member.id)).toStrictEqual([]);
|
|
754
773
|
const again = await call(seats.admin.token, "DELETE", `${rolesPath(member.id)}/${roleId}`);
|
|
755
|
-
|
|
774
|
+
expect(again.status).toStrictEqual(404);
|
|
756
775
|
});
|
|
757
776
|
it("refuses a plain member and a stranger every verb with the 404 an unknown organization gets, and changes nothing", async () => {
|
|
758
777
|
const member = await seated();
|
|
759
|
-
|
|
778
|
+
expect((await grantRole(seats.admin.token, member.id)).status).toStrictEqual(201);
|
|
760
779
|
const unknown = crypto.randomUUID();
|
|
761
780
|
const verbs = [
|
|
762
781
|
["GET", (id) => rolesPath(member.id, id), undefined],
|
|
@@ -769,11 +788,11 @@ export function runTenantContractTests(options) {
|
|
|
769
788
|
];
|
|
770
789
|
for (const [method, path, body] of verbs) {
|
|
771
790
|
const onUnknown = await call(seats.owner.token, method, path(unknown), body);
|
|
772
|
-
|
|
791
|
+
expect(onUnknown.status, `${method} on an unknown id`).toStrictEqual(404);
|
|
773
792
|
for (const caller of [seats.member, seats.outsider]) {
|
|
774
793
|
const refused = await call(caller.token, method, path(organization.id), body);
|
|
775
|
-
|
|
776
|
-
|
|
794
|
+
expect(refused.status, `${method} below the manager tier`).toStrictEqual(404);
|
|
795
|
+
expect(refused.body.detail).toStrictEqual(onUnknown.body.detail);
|
|
777
796
|
}
|
|
778
797
|
}
|
|
779
798
|
assert(await holds(member.token, organization.id), "a refused revoke took the role away");
|
|
@@ -790,31 +809,33 @@ export function runTenantContractTests(options) {
|
|
|
790
809
|
]) {
|
|
791
810
|
const read = await call(seats.admin.token, "GET", rolesPath(userId));
|
|
792
811
|
const granted = await grantRole(seats.admin.token, userId);
|
|
793
|
-
|
|
812
|
+
expect([read.status, granted.status], userId).toStrictEqual([
|
|
813
|
+
404, 404,
|
|
814
|
+
]);
|
|
794
815
|
details.add(read.body.detail);
|
|
795
816
|
details.add(granted.body.detail);
|
|
796
817
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
818
|
+
expect(details.size, [...details].join(" | ")).toStrictEqual(1);
|
|
819
|
+
expect((await accept(pending.token, offered.body.membership.id)).status).toStrictEqual("accepted");
|
|
820
|
+
expect(await heldBy(pending.id)).toStrictEqual([]);
|
|
821
|
+
expect(await holds(pending.token, organization.id)).toBeFalsy();
|
|
801
822
|
});
|
|
802
823
|
it("refuses a role the tenant does not know with 404, and a grant that names no role with 400", async () => {
|
|
803
824
|
const member = await seated();
|
|
804
825
|
const unknown = await grantRole(seats.admin.token, member.id, crypto.randomUUID());
|
|
805
|
-
|
|
826
|
+
expect(unknown.status).toStrictEqual(404);
|
|
806
827
|
const unnamed = await call(seats.admin.token, "POST", rolesPath(member.id), {});
|
|
807
|
-
|
|
808
|
-
|
|
828
|
+
expect(unnamed.status).toStrictEqual(400);
|
|
829
|
+
expect(await heldBy(member.id)).toStrictEqual([]);
|
|
809
830
|
});
|
|
810
831
|
it("refuses a manager revoking a built-in role they do not hold there, and answers any other name with 404", async () => {
|
|
811
832
|
const member = await seated();
|
|
812
833
|
const asAdmin = await call(seats.admin.token, "DELETE", `${rolesPath(member.id)}/owner`);
|
|
813
|
-
|
|
834
|
+
expect(asAdmin.status).toStrictEqual(403);
|
|
814
835
|
const asOwner = await call(seats.owner.token, "DELETE", `${rolesPath(member.id)}/member`);
|
|
815
|
-
|
|
836
|
+
expect(asOwner.status, "an owner membership is not a built-in role held there").toStrictEqual(403);
|
|
816
837
|
const unnamed = await call(seats.owner.token, "DELETE", `${rolesPath(member.id)}/not-a-role`);
|
|
817
|
-
|
|
838
|
+
expect(unnamed.status).toStrictEqual(404);
|
|
818
839
|
});
|
|
819
840
|
it("ends the roles and open offers a person's last membership carried, so rejoining restores nothing", async () => {
|
|
820
841
|
const address = newEmail();
|
|
@@ -825,18 +846,17 @@ export function runTenantContractTests(options) {
|
|
|
825
846
|
await seat(seats.owner.token, organization.id, leaver, "member");
|
|
826
847
|
const ownerOffer = await invite(seats.owner.token, organization.id, address, "owner");
|
|
827
848
|
assert(ownerOffer.body.status === "membership", ownerOffer.body.status);
|
|
828
|
-
|
|
829
|
-
|
|
849
|
+
expect((await grantRole(seats.admin.token, leaver.id)).status).toStrictEqual(201);
|
|
850
|
+
expect(await offersOf(leaver.token)).toStrictEqual([invitationId, ownerOffer.body.membership.id].sort());
|
|
830
851
|
const revoked = await call(seats.admin.token, "DELETE", `/api/organizations/${organization.id}/members/${leaver.id}/member`);
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
.includes(invitationId), "an invitation to the leaver's address outlived their membership");
|
|
852
|
+
expect(revoked.status).toStrictEqual(204);
|
|
853
|
+
expect(await offersOf(leaver.token)).toStrictEqual([]);
|
|
854
|
+
expect((await invitationIdsOf(seats.owner.token, organization.id)).includes(invitationId), "an invitation to the leaver's address outlived their membership").toBeFalsy();
|
|
835
855
|
const read = await call(seats.admin.token, "GET", rolesPath(leaver.id));
|
|
836
|
-
|
|
856
|
+
expect(read.status).toStrictEqual(404);
|
|
837
857
|
await seat(seats.owner.token, organization.id, leaver, "member");
|
|
838
|
-
|
|
839
|
-
|
|
858
|
+
expect(await heldBy(leaver.id)).toStrictEqual([]);
|
|
859
|
+
expect(await holds(leaver.token, organization.id), "rejoining restored a role the last membership carried").toBeFalsy();
|
|
840
860
|
});
|
|
841
861
|
it("keeps a role revoke and a membership's end to the person and organization they name", async () => {
|
|
842
862
|
const address = newEmail();
|
|
@@ -847,19 +867,19 @@ export function runTenantContractTests(options) {
|
|
|
847
867
|
const bystander = await seated();
|
|
848
868
|
await seat(seats.rival.token, rival.id, bystander, "member");
|
|
849
869
|
for (const userId of [leaver.id, bystander.id]) {
|
|
850
|
-
|
|
870
|
+
expect((await grantRole(seats.admin.token, userId)).status).toStrictEqual(201);
|
|
851
871
|
}
|
|
852
|
-
|
|
853
|
-
.status
|
|
872
|
+
expect((await grantRole(seats.rival.token, bystander.id, roleId, rival.id))
|
|
873
|
+
.status).toStrictEqual(201);
|
|
854
874
|
const revoked = await call(seats.admin.token, "DELETE", `/api/organizations/${organization.id}/members/${leaver.id}/member`);
|
|
855
|
-
|
|
856
|
-
|
|
875
|
+
expect(revoked.status).toStrictEqual(204);
|
|
876
|
+
expect((await heldBy(bystander.id)).map((role) => role.id), "one person's membership ending took another's role").toStrictEqual([roleId]);
|
|
857
877
|
assert((await invitationIdsOf(seats.rival.token, rival.id)).includes(elsewhere.body.invitation.id), "a membership ending here withdrew an invitation elsewhere");
|
|
858
878
|
for (const status of [204, 404]) {
|
|
859
879
|
const taken = await call(seats.admin.token, "DELETE", `${rolesPath(bystander.id)}/${roleId}`);
|
|
860
|
-
|
|
880
|
+
expect(taken.status).toStrictEqual(status);
|
|
861
881
|
}
|
|
862
|
-
|
|
882
|
+
expect((await heldBy(bystander.id, seats.rival.token, rival.id)).map((role) => role.id), "a revoke here took the role held in another organization").toStrictEqual([roleId]);
|
|
863
883
|
});
|
|
864
884
|
it("withdraws only the offer named from someone who is not a member", async () => {
|
|
865
885
|
const invitee = await person();
|
|
@@ -870,27 +890,25 @@ export function runTenantContractTests(options) {
|
|
|
870
890
|
offers.push(offered.body.membership.id);
|
|
871
891
|
}
|
|
872
892
|
const withdrawn = await call(seats.owner.token, "DELETE", `/api/organizations/${organization.id}/members/${invitee.id}/member`);
|
|
873
|
-
|
|
874
|
-
|
|
893
|
+
expect(withdrawn.status).toStrictEqual(204);
|
|
894
|
+
expect(await offersOf(invitee.token)).toStrictEqual([offers[1]]);
|
|
875
895
|
});
|
|
876
896
|
it("ends nothing while the person keeps another accepted role there", async () => {
|
|
877
897
|
const stayer = await seated();
|
|
878
898
|
await seat(seats.owner.token, organization.id, stayer, "admin");
|
|
879
899
|
const ownerOffer = await invite(seats.owner.token, organization.id, stayer.email, "owner");
|
|
880
900
|
assert(ownerOffer.body.status === "membership", ownerOffer.body.status);
|
|
881
|
-
|
|
901
|
+
expect((await grantRole(seats.owner.token, stayer.id)).status).toStrictEqual(201);
|
|
882
902
|
const revoked = await call(seats.owner.token, "DELETE", `/api/organizations/${organization.id}/members/${stayer.id}/member`);
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
roleId,
|
|
886
|
-
]);
|
|
903
|
+
expect(revoked.status).toStrictEqual(204);
|
|
904
|
+
expect((await heldBy(stayer.id)).map((role) => role.id)).toStrictEqual([roleId]);
|
|
887
905
|
assert(await holds(stayer.token, organization.id));
|
|
888
|
-
|
|
906
|
+
expect(await offersOf(stayer.token)).toStrictEqual([
|
|
889
907
|
ownerOffer.body.membership.id,
|
|
890
908
|
]);
|
|
891
909
|
const withdrawn = await call(seats.owner.token, "DELETE", `/api/organizations/${organization.id}/members/${stayer.id}/owner`);
|
|
892
|
-
|
|
893
|
-
|
|
910
|
+
expect(withdrawn.status).toStrictEqual(204);
|
|
911
|
+
expect(await offersOf(stayer.token)).toStrictEqual([]);
|
|
894
912
|
assert(await holds(stayer.token, organization.id), "withdrawing a pending offer ended a role");
|
|
895
913
|
});
|
|
896
914
|
});
|
|
@@ -899,27 +917,27 @@ export function runTenantContractTests(options) {
|
|
|
899
917
|
it("answers a new person's own metadata bucket, empty, and no cache keeps it", async () => {
|
|
900
918
|
const caller = await person();
|
|
901
919
|
const reply = await call(caller.token, "GET", "/api/account");
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
920
|
+
expect(reply.status).toStrictEqual(200);
|
|
921
|
+
expect(reply.body).toStrictEqual({ userMetadata: {} });
|
|
922
|
+
expect(reply.headers.get("cache-control") ?? "").toContain("no-store");
|
|
905
923
|
});
|
|
906
924
|
it("merges a patch shallowly, deleting keys set to null and keeping keys it does not name", async () => {
|
|
907
925
|
const caller = await person();
|
|
908
926
|
const first = await call(caller.token, "PATCH", "/api/account", {
|
|
909
927
|
userMetadata: { theme: "dark", locale: "en", nested: { a: 1 } },
|
|
910
928
|
});
|
|
911
|
-
|
|
912
|
-
|
|
929
|
+
expect(first.status).toStrictEqual(200);
|
|
930
|
+
expect(first.body).toStrictEqual({
|
|
913
931
|
userMetadata: { theme: "dark", locale: "en", nested: { a: 1 } },
|
|
914
932
|
});
|
|
915
933
|
const second = await call(caller.token, "PATCH", "/api/account", {
|
|
916
934
|
userMetadata: { locale: null, nested: { b: 2 } },
|
|
917
935
|
});
|
|
918
|
-
|
|
936
|
+
expect(second.body).toStrictEqual({
|
|
919
937
|
userMetadata: { theme: "dark", nested: { b: 2 } },
|
|
920
938
|
});
|
|
921
939
|
const read = await call(caller.token, "GET", "/api/account");
|
|
922
|
-
|
|
940
|
+
expect(read.body).toStrictEqual(second.body);
|
|
923
941
|
});
|
|
924
942
|
it("refuses anything but a userMetadata object", async () => {
|
|
925
943
|
const caller = await person();
|
|
@@ -929,7 +947,7 @@ export function runTenantContractTests(options) {
|
|
|
929
947
|
{ userMetadata: {}, metadata: { role: "admin" } },
|
|
930
948
|
]) {
|
|
931
949
|
const reply = await call(caller.token, "PATCH", "/api/account", body);
|
|
932
|
-
|
|
950
|
+
expect(reply.status, JSON.stringify(body)).toStrictEqual(400);
|
|
933
951
|
}
|
|
934
952
|
});
|
|
935
953
|
it("keeps the bucket to eight levels of nesting", async () => {
|
|
@@ -938,11 +956,11 @@ export function runTenantContractTests(options) {
|
|
|
938
956
|
const deepest = await call(caller.token, "PATCH", "/api/account", {
|
|
939
957
|
userMetadata: nested(8),
|
|
940
958
|
});
|
|
941
|
-
|
|
959
|
+
expect(deepest.status).toStrictEqual(200);
|
|
942
960
|
const tooDeep = await call(caller.token, "PATCH", "/api/account", {
|
|
943
961
|
userMetadata: nested(9),
|
|
944
962
|
});
|
|
945
|
-
|
|
963
|
+
expect(tooDeep.status).toStrictEqual(400);
|
|
946
964
|
});
|
|
947
965
|
it("measures the metadata limit in UTF-8 bytes and preserves the bucket on refusal", async () => {
|
|
948
966
|
const caller = await person();
|
|
@@ -950,9 +968,9 @@ export function runTenantContractTests(options) {
|
|
|
950
968
|
const reply = await call(caller.token, "PATCH", "/api/account", {
|
|
951
969
|
userMetadata: { notes },
|
|
952
970
|
});
|
|
953
|
-
|
|
971
|
+
expect(reply.status).toStrictEqual(400);
|
|
954
972
|
const read = await call(caller.token, "GET", "/api/account");
|
|
955
|
-
|
|
973
|
+
expect(read.body).toStrictEqual({ userMetadata: {} });
|
|
956
974
|
});
|
|
957
975
|
it("keeps the merged bucket to sixteen kilobytes, however small each patch is", async () => {
|
|
958
976
|
const caller = await person();
|
|
@@ -960,23 +978,23 @@ export function runTenantContractTests(options) {
|
|
|
960
978
|
const first = await call(caller.token, "PATCH", "/api/account", {
|
|
961
979
|
userMetadata: { first: half },
|
|
962
980
|
});
|
|
963
|
-
|
|
981
|
+
expect(first.status).toStrictEqual(200);
|
|
964
982
|
const second = await call(caller.token, "PATCH", "/api/account", {
|
|
965
983
|
userMetadata: { second: half },
|
|
966
984
|
});
|
|
967
|
-
|
|
985
|
+
expect(second.status).toStrictEqual(400);
|
|
968
986
|
const read = await call(caller.token, "GET", "/api/account");
|
|
969
|
-
|
|
987
|
+
expect(read.body).toStrictEqual({ userMetadata: { first: half } });
|
|
970
988
|
});
|
|
971
989
|
it("refuses a caller with no bearer token", async () => {
|
|
972
990
|
const reply = await call(null, "GET", "/api/account");
|
|
973
|
-
|
|
991
|
+
expect(reply.status).toStrictEqual(401);
|
|
974
992
|
});
|
|
975
993
|
});
|
|
976
994
|
describe("the account sessions API", () => {
|
|
977
995
|
async function sessionsOf(accessToken) {
|
|
978
996
|
const reply = await call(accessToken, "GET", "/api/account/sessions");
|
|
979
|
-
|
|
997
|
+
expect(reply.status).toStrictEqual(200);
|
|
980
998
|
return reply.body.sessions;
|
|
981
999
|
}
|
|
982
1000
|
const currentOf = (sessions) => sessions.filter((session) => session.current).map(({ id }) => id);
|
|
@@ -995,9 +1013,9 @@ export function runTenantContractTests(options) {
|
|
|
995
1013
|
const first = await tenant.signIn(id);
|
|
996
1014
|
const second = await tenant.signIn(id);
|
|
997
1015
|
const listed = await sessionsOf(second.access_token);
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1016
|
+
expect(listed.length).toStrictEqual(2);
|
|
1017
|
+
expect(currentOf(listed)).toStrictEqual([listed[0].id]);
|
|
1018
|
+
expect(Object.keys(listed[0]).sort()).toStrictEqual([
|
|
1001
1019
|
"createdAt",
|
|
1002
1020
|
"current",
|
|
1003
1021
|
"id",
|
|
@@ -1006,20 +1024,20 @@ export function runTenantContractTests(options) {
|
|
|
1006
1024
|
"userAgent",
|
|
1007
1025
|
]);
|
|
1008
1026
|
const fromFirst = await sessionsOf(first.access_token);
|
|
1009
|
-
|
|
1010
|
-
|
|
1027
|
+
expect(fromFirst.map((session) => session.id).sort()).toStrictEqual(listed.map((session) => session.id).sort());
|
|
1028
|
+
expect(currentOf(fromFirst)).toStrictEqual([listed[1].id]);
|
|
1011
1029
|
});
|
|
1012
1030
|
it("leaves an earlier sign-in's credential live when another browser signs in", async () => {
|
|
1013
1031
|
const id = await tenant.addUser([], { email: newEmail() });
|
|
1014
1032
|
const first = await tenant.signIn(id);
|
|
1015
1033
|
const second = await tenant.signIn(id);
|
|
1016
|
-
|
|
1017
|
-
|
|
1034
|
+
expect((await introspect(first.access_token)).active).toStrictEqual(true);
|
|
1035
|
+
expect((await introspect(second.access_token)).active).toStrictEqual(true);
|
|
1018
1036
|
const fromFirst = await sessionsOf(first.access_token);
|
|
1019
1037
|
const fromSecond = await sessionsOf(second.access_token);
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1038
|
+
expect(fromFirst.length).toStrictEqual(2);
|
|
1039
|
+
expect(currentOf(fromFirst).length).toStrictEqual(1);
|
|
1040
|
+
expect(currentOf(fromFirst)[0] === currentOf(fromSecond)[0], "two browsers' sign-ins shared one login session").toBeFalsy();
|
|
1023
1041
|
});
|
|
1024
1042
|
it("keeps a refreshed credential on the session it was issued from", async () => {
|
|
1025
1043
|
const id = await tenant.addUser([], { email: newEmail() });
|
|
@@ -1027,17 +1045,17 @@ export function runTenantContractTests(options) {
|
|
|
1027
1045
|
await tenant.signIn(id);
|
|
1028
1046
|
const issuedFrom = currentOf(await sessionsOf(first.access_token));
|
|
1029
1047
|
const response = await refresh(first.refresh_token);
|
|
1030
|
-
|
|
1048
|
+
expect(response.status).toStrictEqual(200);
|
|
1031
1049
|
const refreshed = await response.json();
|
|
1032
|
-
|
|
1050
|
+
expect(currentOf(await sessionsOf(refreshed.access_token))).toStrictEqual(issuedFrom);
|
|
1033
1051
|
});
|
|
1034
1052
|
it("refuses to end the caller's own session", async () => {
|
|
1035
1053
|
const id = await tenant.addUser([], { email: newEmail() });
|
|
1036
1054
|
const { access_token } = await tenant.signIn(id);
|
|
1037
1055
|
const [own] = currentOf(await sessionsOf(access_token));
|
|
1038
1056
|
const reply = await call(access_token, "DELETE", `/api/account/sessions/${own}`);
|
|
1039
|
-
|
|
1040
|
-
|
|
1057
|
+
expect(reply.status).toStrictEqual(409);
|
|
1058
|
+
expect(reply.body.reason).toStrictEqual("current_session");
|
|
1041
1059
|
});
|
|
1042
1060
|
it("ends another of the caller's sessions and every token issued from it", async () => {
|
|
1043
1061
|
const id = await tenant.addUser([], { email: newEmail() });
|
|
@@ -1045,23 +1063,29 @@ export function runTenantContractTests(options) {
|
|
|
1045
1063
|
const current = await tenant.signIn(id);
|
|
1046
1064
|
const [otherSession] = currentOf(await sessionsOf(other.access_token));
|
|
1047
1065
|
const ended = await call(current.access_token, "DELETE", `/api/account/sessions/${otherSession}`);
|
|
1048
|
-
|
|
1049
|
-
|
|
1066
|
+
expect(ended.status).toStrictEqual(204);
|
|
1067
|
+
expect((await introspect(other.access_token)).active).toStrictEqual(false);
|
|
1050
1068
|
const refused = await refresh(other.refresh_token);
|
|
1051
1069
|
await refused.body?.cancel();
|
|
1052
|
-
|
|
1070
|
+
expect(refused.status).toStrictEqual(400);
|
|
1053
1071
|
const remaining = await sessionsOf(current.access_token);
|
|
1054
|
-
|
|
1072
|
+
expect(remaining.map((session) => session.current)).toStrictEqual([
|
|
1073
|
+
true,
|
|
1074
|
+
]);
|
|
1055
1075
|
});
|
|
1056
1076
|
it("answers another person's session, an unknown id and a malformed id with 404", async () => {
|
|
1057
1077
|
const caller = await person();
|
|
1058
1078
|
const stranger = await person();
|
|
1059
1079
|
const [theirs] = currentOf(await sessionsOf(stranger.token));
|
|
1060
|
-
for (const sessionId of [
|
|
1080
|
+
for (const sessionId of [
|
|
1081
|
+
theirs,
|
|
1082
|
+
crypto.randomUUID(),
|
|
1083
|
+
"not-a-session",
|
|
1084
|
+
]) {
|
|
1061
1085
|
const reply = await call(caller.token, "DELETE", `/api/account/sessions/${sessionId}`);
|
|
1062
|
-
|
|
1086
|
+
expect(reply.status, sessionId).toStrictEqual(404);
|
|
1063
1087
|
}
|
|
1064
|
-
|
|
1088
|
+
expect((await sessionsOf(stranger.token)).length).toStrictEqual(1);
|
|
1065
1089
|
});
|
|
1066
1090
|
it("ends every other session and keeps the caller's", async () => {
|
|
1067
1091
|
const id = await tenant.addUser([], { email: newEmail() });
|
|
@@ -1069,11 +1093,13 @@ export function runTenantContractTests(options) {
|
|
|
1069
1093
|
await tenant.signIn(id);
|
|
1070
1094
|
const kept = await tenant.signIn(id);
|
|
1071
1095
|
const reply = await call(kept.access_token, "POST", "/api/account/sessions/revoke-others");
|
|
1072
|
-
|
|
1073
|
-
|
|
1096
|
+
expect(reply.status).toStrictEqual(200);
|
|
1097
|
+
expect(reply.body).toStrictEqual({ revoked: 2 });
|
|
1074
1098
|
const remaining = await sessionsOf(kept.access_token);
|
|
1075
|
-
|
|
1076
|
-
|
|
1099
|
+
expect(remaining.map((session) => session.current)).toStrictEqual([
|
|
1100
|
+
true,
|
|
1101
|
+
]);
|
|
1102
|
+
expect((await introspect(first.access_token)).active).toStrictEqual(false);
|
|
1077
1103
|
});
|
|
1078
1104
|
it("ends a login session when a token issued from it is revoked", async () => {
|
|
1079
1105
|
const id = await tenant.addUser([], { email: newEmail() });
|
|
@@ -1089,33 +1115,35 @@ export function runTenantContractTests(options) {
|
|
|
1089
1115
|
}),
|
|
1090
1116
|
});
|
|
1091
1117
|
await response.body?.cancel();
|
|
1092
|
-
|
|
1093
|
-
|
|
1118
|
+
expect(response.status).toStrictEqual(200);
|
|
1119
|
+
expect((await introspect(revoked.access_token)).active).toStrictEqual(false);
|
|
1094
1120
|
const remaining = await sessionsOf(kept.access_token);
|
|
1095
|
-
|
|
1121
|
+
expect(remaining.map((session) => session.current)).toStrictEqual([
|
|
1122
|
+
true,
|
|
1123
|
+
]);
|
|
1096
1124
|
});
|
|
1097
1125
|
it("revokes a login session's earlier credential when the same browser signs in again", async () => {
|
|
1098
1126
|
const id = await tenant.addUser([], { email: newEmail() });
|
|
1099
1127
|
const first = await tenant.signIn(id);
|
|
1100
1128
|
const again = await tenant.signIn(id, undefined, { sameBrowser: true });
|
|
1101
|
-
|
|
1129
|
+
expect((await introspect(first.access_token)).active).toStrictEqual(false);
|
|
1102
1130
|
const refused = await refresh(first.refresh_token);
|
|
1103
1131
|
await refused.body?.cancel();
|
|
1104
|
-
|
|
1105
|
-
|
|
1132
|
+
expect(refused.status).toStrictEqual(400);
|
|
1133
|
+
expect((await introspect(again.access_token)).active).toStrictEqual(true);
|
|
1106
1134
|
const listed = await sessionsOf(again.access_token);
|
|
1107
|
-
|
|
1135
|
+
expect(listed.map((session) => session.current)).toStrictEqual([true]);
|
|
1108
1136
|
});
|
|
1109
1137
|
it("refuses a body that names anything", async () => {
|
|
1110
1138
|
const caller = await person();
|
|
1111
1139
|
const reply = await call(caller.token, "POST", "/api/account/sessions/revoke-others", { userId: caller.id });
|
|
1112
|
-
|
|
1140
|
+
expect(reply.status).toStrictEqual(400);
|
|
1113
1141
|
});
|
|
1114
1142
|
});
|
|
1115
1143
|
describe("the linked accounts API", () => {
|
|
1116
1144
|
async function linkedOf(accessToken) {
|
|
1117
1145
|
const reply = await call(accessToken, "GET", "/api/account/linked-accounts");
|
|
1118
|
-
|
|
1146
|
+
expect(reply.status).toStrictEqual(200);
|
|
1119
1147
|
return reply.body;
|
|
1120
1148
|
}
|
|
1121
1149
|
it("lists a person's linked accounts and whether they have a password", async () => {
|
|
@@ -1126,13 +1154,13 @@ export function runTenantContractTests(options) {
|
|
|
1126
1154
|
email,
|
|
1127
1155
|
});
|
|
1128
1156
|
const body = await linkedOf(caller.token);
|
|
1129
|
-
|
|
1157
|
+
expect(body.hasPassword).toStrictEqual(true);
|
|
1130
1158
|
assert(Array.isArray(body.connectable));
|
|
1131
|
-
|
|
1159
|
+
expect(body.identities.length).toStrictEqual(1);
|
|
1132
1160
|
const [identity] = body.identities;
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1161
|
+
expect(identity.id).toStrictEqual(linked);
|
|
1162
|
+
expect(identity.provider).toStrictEqual("github");
|
|
1163
|
+
expect(identity.email).toStrictEqual(email);
|
|
1136
1164
|
assert(identity.displayName, "a linked account names its provider");
|
|
1137
1165
|
assert(identity.createdAt, "a linked account says when it was linked");
|
|
1138
1166
|
});
|
|
@@ -1143,8 +1171,8 @@ export function runTenantContractTests(options) {
|
|
|
1143
1171
|
email: null,
|
|
1144
1172
|
});
|
|
1145
1173
|
const reply = await call(caller.token, "DELETE", `/api/account/linked-accounts/${linked}`);
|
|
1146
|
-
|
|
1147
|
-
|
|
1174
|
+
expect(reply.status).toStrictEqual(204);
|
|
1175
|
+
expect((await linkedOf(caller.token)).identities).toStrictEqual([]);
|
|
1148
1176
|
});
|
|
1149
1177
|
it("refuses to disconnect a person's last way to sign in", async () => {
|
|
1150
1178
|
const id = await tenant.addUser([], {
|
|
@@ -1156,11 +1184,11 @@ export function runTenantContractTests(options) {
|
|
|
1156
1184
|
email: null,
|
|
1157
1185
|
});
|
|
1158
1186
|
const { access_token } = await tenant.signIn(id);
|
|
1159
|
-
|
|
1187
|
+
expect((await linkedOf(access_token)).hasPassword).toStrictEqual(false);
|
|
1160
1188
|
const reply = await call(access_token, "DELETE", `/api/account/linked-accounts/${linked}`);
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1189
|
+
expect(reply.status).toStrictEqual(409);
|
|
1190
|
+
expect(reply.body.reason).toStrictEqual("last_method");
|
|
1191
|
+
expect((await linkedOf(access_token)).identities.map((entry) => entry.id)).toStrictEqual([linked]);
|
|
1164
1192
|
});
|
|
1165
1193
|
it("answers another person's linked account, an unknown id and a malformed id with 404", async () => {
|
|
1166
1194
|
const caller = await person();
|
|
@@ -1171,9 +1199,9 @@ export function runTenantContractTests(options) {
|
|
|
1171
1199
|
});
|
|
1172
1200
|
for (const identityId of [theirs, crypto.randomUUID(), "not-an-id"]) {
|
|
1173
1201
|
const reply = await call(caller.token, "DELETE", `/api/account/linked-accounts/${identityId}`);
|
|
1174
|
-
|
|
1202
|
+
expect(reply.status, identityId).toStrictEqual(404);
|
|
1175
1203
|
}
|
|
1176
|
-
|
|
1204
|
+
expect((await linkedOf(stranger.token)).identities.map((entry) => entry.id)).toStrictEqual([theirs]);
|
|
1177
1205
|
});
|
|
1178
1206
|
});
|
|
1179
1207
|
describe("client credentials and the resource-grants API", () => {
|
|
@@ -1200,8 +1228,8 @@ export function runTenantContractTests(options) {
|
|
|
1200
1228
|
: { authorization: encodeBasicAuth(client.id, client.secret) },
|
|
1201
1229
|
body: new URLSearchParams({
|
|
1202
1230
|
grant_type: "client_credentials",
|
|
1203
|
-
...client.secret === undefined ? { client_id: client.id } : {},
|
|
1204
|
-
...scope === undefined ? {} : { scope },
|
|
1231
|
+
...(client.secret === undefined ? { client_id: client.id } : {}),
|
|
1232
|
+
...(scope === undefined ? {} : { scope }),
|
|
1205
1233
|
}),
|
|
1206
1234
|
});
|
|
1207
1235
|
const text = await response.text();
|
|
@@ -1213,7 +1241,7 @@ export function runTenantContractTests(options) {
|
|
|
1213
1241
|
}
|
|
1214
1242
|
async function issuedTo(client, scope) {
|
|
1215
1243
|
const issued = await machineToken(client, scope);
|
|
1216
|
-
|
|
1244
|
+
expect(issued.status, JSON.stringify(issued.body)).toStrictEqual(200);
|
|
1217
1245
|
assert(issued.body.access_token);
|
|
1218
1246
|
return issued.body.access_token;
|
|
1219
1247
|
}
|
|
@@ -1223,7 +1251,7 @@ export function runTenantContractTests(options) {
|
|
|
1223
1251
|
headers: { authorization: encodeBasicAuth(client.id, client.secret) },
|
|
1224
1252
|
body: new URLSearchParams({ token: accessToken }),
|
|
1225
1253
|
});
|
|
1226
|
-
|
|
1254
|
+
expect(response.status).toStrictEqual(200);
|
|
1227
1255
|
return await response.json();
|
|
1228
1256
|
}
|
|
1229
1257
|
function grantsOn(accessToken, query) {
|
|
@@ -1252,22 +1280,22 @@ export function runTenantContractTests(options) {
|
|
|
1252
1280
|
});
|
|
1253
1281
|
it("issues a machine token with no refresh token, naming no person and no organization", async () => {
|
|
1254
1282
|
const issued = await machineToken(reader, ORGANIZATIONS_READ);
|
|
1255
|
-
|
|
1256
|
-
|
|
1283
|
+
expect(issued.status, JSON.stringify(issued.body)).toStrictEqual(200);
|
|
1284
|
+
expect(issued.body.token_type).toStrictEqual("Bearer");
|
|
1257
1285
|
assert(issued.body.access_token);
|
|
1258
|
-
|
|
1286
|
+
expect("refresh_token" in issued.body, "a machine token has no refresh token").toBeFalsy();
|
|
1259
1287
|
const claims = await introspectAs(reader, issued.body.access_token);
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1288
|
+
expect(claims.active).toStrictEqual(true);
|
|
1289
|
+
expect(claims.client_id).toStrictEqual(reader.id);
|
|
1290
|
+
expect(claims.scope).toStrictEqual(ORGANIZATIONS_READ);
|
|
1291
|
+
expect(claims.sub, "a machine token names no person").toBeFalsy();
|
|
1292
|
+
expect(claims.username, "a machine token names no person").toBeFalsy();
|
|
1293
|
+
expect(claims.org_id, "a machine token belongs to no organization").toBeFalsy();
|
|
1294
|
+
expect(sorted(claims.permissions)).toStrictEqual([]);
|
|
1267
1295
|
});
|
|
1268
1296
|
it("answers another application introspecting a machine token that it is inactive", async () => {
|
|
1269
1297
|
const claims = await introspectAs(unassigned, readerToken);
|
|
1270
|
-
|
|
1298
|
+
expect(claims).toStrictEqual({ active: false });
|
|
1271
1299
|
});
|
|
1272
1300
|
it("refuses the grant to a public application, which has no secret to authenticate with", async () => {
|
|
1273
1301
|
const open = await tenant.addMachineClient({
|
|
@@ -1275,23 +1303,23 @@ export function runTenantContractTests(options) {
|
|
|
1275
1303
|
permissions: [],
|
|
1276
1304
|
confidential: false,
|
|
1277
1305
|
});
|
|
1278
|
-
|
|
1306
|
+
expect(open.secret, "a public application is issued no secret").toBeFalsy();
|
|
1279
1307
|
const refused = await machineToken(open, ORGANIZATIONS_READ);
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1308
|
+
expect(refused.status, JSON.stringify(refused.body)).toStrictEqual(401);
|
|
1309
|
+
expect(refused.body.error).toStrictEqual("invalid_client");
|
|
1310
|
+
expect(refused.body.access_token).toBeFalsy();
|
|
1283
1311
|
});
|
|
1284
1312
|
it("refuses the grant to an application not registered for it, and refuses a machine token an OIDC scope or one outside its allowlist", async () => {
|
|
1285
1313
|
const interactive = await machineToken(tenant.client, ORGANIZATIONS_READ);
|
|
1286
|
-
|
|
1287
|
-
|
|
1314
|
+
expect(interactive.status).toStrictEqual(401);
|
|
1315
|
+
expect(interactive.body.error).toStrictEqual("unauthorized_client");
|
|
1288
1316
|
for (const scope of [
|
|
1289
1317
|
"openid",
|
|
1290
1318
|
`${ORGANIZATIONS_READ} identity:organizations:write`,
|
|
1291
1319
|
]) {
|
|
1292
1320
|
const refused = await machineToken(reader, scope);
|
|
1293
|
-
|
|
1294
|
-
|
|
1321
|
+
expect(refused.status, scope).toStrictEqual(400);
|
|
1322
|
+
expect(refused.body.error, scope).toStrictEqual("invalid_scope");
|
|
1295
1323
|
}
|
|
1296
1324
|
});
|
|
1297
1325
|
it("accepts organization_id on the refresh_token grant alone", async () => {
|
|
@@ -1305,45 +1333,45 @@ export function runTenantContractTests(options) {
|
|
|
1305
1333
|
}),
|
|
1306
1334
|
});
|
|
1307
1335
|
const body = await response.json();
|
|
1308
|
-
|
|
1309
|
-
|
|
1336
|
+
expect(response.status, JSON.stringify(body)).toStrictEqual(400);
|
|
1337
|
+
expect(body.error).toStrictEqual("invalid_request");
|
|
1310
1338
|
});
|
|
1311
1339
|
it("refuses a machine token wherever the tenant answers for a person", async () => {
|
|
1312
1340
|
const checked = await ask(readerToken, "/api/check", {
|
|
1313
1341
|
permissions: ["contract:tenant"],
|
|
1314
1342
|
});
|
|
1315
|
-
|
|
1316
|
-
|
|
1343
|
+
expect(checked.status).toStrictEqual(403);
|
|
1344
|
+
expect(checked.body.reason).toStrictEqual("machine_token");
|
|
1317
1345
|
const batch = await ask(readerToken, "/api/check/batch", {
|
|
1318
1346
|
permissions: ["contract:read"],
|
|
1319
1347
|
resource: { type: "contract_document", ids: ["direct"] },
|
|
1320
1348
|
});
|
|
1321
|
-
|
|
1322
|
-
|
|
1349
|
+
expect(batch.status).toStrictEqual(403);
|
|
1350
|
+
expect(batch.body.reason).toStrictEqual("machine_token");
|
|
1323
1351
|
for (const path of ["/api/memberships", "/api/account"]) {
|
|
1324
1352
|
const reply = await call(readerToken, "GET", path);
|
|
1325
|
-
|
|
1326
|
-
|
|
1353
|
+
expect(reply.status, path).toStrictEqual(403);
|
|
1354
|
+
expect(reply.body.reason, path).toStrictEqual("machine_token");
|
|
1327
1355
|
}
|
|
1328
1356
|
const organizations = await call(readerToken, "GET", "/api/organizations");
|
|
1329
|
-
|
|
1357
|
+
expect(organizations.status).toStrictEqual(403);
|
|
1330
1358
|
const userinfo = await send(userinfoEndpoint, {
|
|
1331
1359
|
headers: { authorization: `Bearer ${readerToken}` },
|
|
1332
1360
|
});
|
|
1333
1361
|
await userinfo.body?.cancel();
|
|
1334
|
-
|
|
1362
|
+
expect(userinfo.status).toStrictEqual(401);
|
|
1335
1363
|
});
|
|
1336
1364
|
it("lists who holds a grant on a resource, roles named, and an empty list where nobody does", async () => {
|
|
1337
1365
|
const direct = await grantsOn(readerToken, DIRECT);
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1366
|
+
expect(direct.status, JSON.stringify(direct.body)).toStrictEqual(200);
|
|
1367
|
+
expect(direct.headers.get("cache-control") ?? "").toContain("no-store");
|
|
1368
|
+
expect(direct.body.resource).toStrictEqual({
|
|
1341
1369
|
type: "contract_document",
|
|
1342
1370
|
id: "direct",
|
|
1343
1371
|
});
|
|
1344
|
-
|
|
1372
|
+
expect(direct.body.grants.length).toStrictEqual(1);
|
|
1345
1373
|
const [held] = direct.body.grants;
|
|
1346
|
-
|
|
1374
|
+
expect(Object.keys(held).sort()).toStrictEqual([
|
|
1347
1375
|
"builtInRole",
|
|
1348
1376
|
"id",
|
|
1349
1377
|
"roleId",
|
|
@@ -1355,27 +1383,30 @@ export function runTenantContractTests(options) {
|
|
|
1355
1383
|
"subjectRole",
|
|
1356
1384
|
"subjectType",
|
|
1357
1385
|
]);
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1386
|
+
expect(held.subjectType).toStrictEqual("user");
|
|
1387
|
+
expect(held.subjectId).toStrictEqual(people.outsider);
|
|
1388
|
+
expect(held.subjectRole).toStrictEqual(null);
|
|
1389
|
+
expect(held.subjectActive).toStrictEqual(true);
|
|
1362
1390
|
assert(held.subjectName, "a grant names its holder");
|
|
1363
1391
|
assert(held.id, "a grant has an id");
|
|
1364
1392
|
assert(held.roleId, "a grant names its role");
|
|
1365
1393
|
assert(held.roleSlug, "a grant names its role's slug");
|
|
1366
1394
|
assert(held.roleName, "a grant names its role's name");
|
|
1367
|
-
|
|
1395
|
+
expect(held.builtInRole).toStrictEqual(null);
|
|
1368
1396
|
const shared = await grantsOn(readerToken, "type=contract_document&id=shared");
|
|
1369
|
-
|
|
1370
|
-
|
|
1397
|
+
expect(shared.status).toStrictEqual(200);
|
|
1398
|
+
expect(shared.body.grants.map((grant) => [
|
|
1399
|
+
grant.subjectType,
|
|
1400
|
+
grant.subjectId,
|
|
1401
|
+
])).toStrictEqual([["organization", organizations.home]]);
|
|
1371
1402
|
const nobody = await grantsOn(readerToken, `type=contract_document&id=nobody-${crypto.randomUUID()}`);
|
|
1372
|
-
|
|
1373
|
-
|
|
1403
|
+
expect(nobody.status).toStrictEqual(200);
|
|
1404
|
+
expect(nobody.body.grants).toStrictEqual([]);
|
|
1374
1405
|
});
|
|
1375
1406
|
it("lists the grants on one resource in role-name order", async () => {
|
|
1376
1407
|
const ranked = await grantsOn(readerToken, RANKED);
|
|
1377
|
-
|
|
1378
|
-
|
|
1408
|
+
expect(ranked.status, JSON.stringify(ranked.body)).toStrictEqual(200);
|
|
1409
|
+
expect(ranked.body.grants.map((grant) => grant.subjectId).sort()).toStrictEqual([
|
|
1379
1410
|
people.member,
|
|
1380
1411
|
people.outsider,
|
|
1381
1412
|
people.leaver,
|
|
@@ -1383,47 +1414,47 @@ export function runTenantContractTests(options) {
|
|
|
1383
1414
|
organizations.home,
|
|
1384
1415
|
].sort());
|
|
1385
1416
|
const names = ranked.body.grants.map((grant) => grant.roleName);
|
|
1386
|
-
|
|
1387
|
-
|
|
1417
|
+
expect(new Set(names).size, "each grant confers a role of its own").toStrictEqual(names.length);
|
|
1418
|
+
expect(names).toStrictEqual([...names].sort((a, b) => a.localeCompare(b)));
|
|
1388
1419
|
});
|
|
1389
1420
|
it("hides the listing from a machine token without the permission or the scope, and refuses a person's token and no token", async () => {
|
|
1390
1421
|
const withoutPermission = await issuedTo(unassigned, ORGANIZATIONS_READ);
|
|
1391
|
-
|
|
1422
|
+
expect((await grantsOn(withoutPermission, DIRECT)).status).toStrictEqual(404);
|
|
1392
1423
|
const withoutScope = await issuedTo(reader);
|
|
1393
|
-
|
|
1424
|
+
expect((await grantsOn(withoutScope, DIRECT)).status).toStrictEqual(404);
|
|
1394
1425
|
const { access_token } = await tenant.signIn(people.member);
|
|
1395
1426
|
const person = await call(access_token, "GET", `/api/resource-grants?${DIRECT}`);
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1427
|
+
expect(person.status).toStrictEqual(403);
|
|
1428
|
+
expect(person.body.reason).toStrictEqual("user_token");
|
|
1429
|
+
expect((await grantsOn(null, DIRECT)).status).toStrictEqual(401);
|
|
1399
1430
|
});
|
|
1400
1431
|
function refusedFields(reply) {
|
|
1401
|
-
|
|
1432
|
+
expect(reply.status, JSON.stringify(reply.body)).toStrictEqual(400);
|
|
1402
1433
|
const { fieldErrors } = reply.body;
|
|
1403
1434
|
return Object.keys(fieldErrors ?? {});
|
|
1404
1435
|
}
|
|
1405
1436
|
it("refuses a resource type the tenant has not registered, and a resource missing its id", async () => {
|
|
1406
1437
|
const unregistered = await grantsOn(readerToken, "type=contract_unregistered&id=direct");
|
|
1407
|
-
|
|
1438
|
+
expect(refusedFields(unregistered)).toStrictEqual(["type"]);
|
|
1408
1439
|
const missing = await grantsOn(readerToken, "type=contract_document");
|
|
1409
|
-
|
|
1440
|
+
expect(refusedFields(missing)).toStrictEqual(["resource"]);
|
|
1410
1441
|
});
|
|
1411
1442
|
it("bounds the type to 64 characters and the id to 255, refusing ASCII control characters and no other", async () => {
|
|
1412
1443
|
const long = await grantsOn(readerToken, `type=${"t".repeat(65)}&id=direct`);
|
|
1413
|
-
|
|
1444
|
+
expect(refusedFields(long)).toStrictEqual(["resource"]);
|
|
1414
1445
|
const longest = await grantsOn(readerToken, `type=${"t".repeat(64)}&id=direct`);
|
|
1415
|
-
|
|
1446
|
+
expect(refusedFields(longest)).toStrictEqual(["type"]);
|
|
1416
1447
|
const query = (id) => `type=contract_document&id=${encodeURIComponent(id)}`;
|
|
1417
1448
|
const wide = await grantsOn(readerToken, query("i".repeat(256)));
|
|
1418
|
-
|
|
1449
|
+
expect(refusedFields(wide)).toStrictEqual(["resource"]);
|
|
1419
1450
|
const widest = await grantsOn(readerToken, query("i".repeat(255)));
|
|
1420
|
-
|
|
1421
|
-
|
|
1451
|
+
expect(widest.status, JSON.stringify(widest.body)).toStrictEqual(200);
|
|
1452
|
+
expect(widest.body.grants).toStrictEqual([]);
|
|
1422
1453
|
const control = await grantsOn(readerToken, query("tab\there"));
|
|
1423
|
-
|
|
1454
|
+
expect(refusedFields(control)).toStrictEqual(["resource"]);
|
|
1424
1455
|
const unicode = await grantsOn(readerToken, query("next\u0085line"));
|
|
1425
|
-
|
|
1426
|
-
|
|
1456
|
+
expect(unicode.status, JSON.stringify(unicode.body)).toStrictEqual(200);
|
|
1457
|
+
expect(unicode.body.grants).toStrictEqual([]);
|
|
1427
1458
|
});
|
|
1428
1459
|
});
|
|
1429
1460
|
});
|