458 lines
27 KiB
Gherkin
458 lines
27 KiB
Gherkin
Feature: Developers authenticate against /v1 with named API keys instead of the jwt cookie
|
|
|
|
As a developer integrating with RenewCMS
|
|
I want to create named API keys and send them as a Bearer token
|
|
So that I can manage articles, media and users without the UI, with exactly the rights of the
|
|
user who created the key and never more
|
|
|
|
Background:
|
|
Given internal/domain/apikey defines the entity APIKey with ID uint32, UserID uint32, Name,
|
|
Prefix, HashedKey, ExpiresAt *time.Time, RevokedAt *time.Time, LastUsedAt *time.Time and
|
|
CreatedAt, with snake_case json tags
|
|
And internal/domain/apikey defines the sentinels apikey.ErrAPIKeyNotFound,
|
|
apikey.ErrAPIKeyInvalid and apikey.ErrExpiryNotInFuture
|
|
And apikey.ErrAPIKeyInvalid covers an unknown, malformed, expired or revoked key and a key
|
|
whose user is deleted or unverified
|
|
And internal/domain/apikey defines the interface Repository with Create, Get(id),
|
|
GetByHashedKey(hashedKey), ListByUser(userId), ListAll(), Revoke(id, at),
|
|
RevokeAllByUser(userId, at) and TouchLastUsed(id, at)
|
|
And RevokeAllByUser sets RevokedAt to at on every key of that user that is not yet revoked and
|
|
leaves the RevokedAt of already revoked keys and every other user's keys untouched
|
|
And internal/domain/apikey defines the interface KeyGenerator with Generate() returning the
|
|
raw key, its display prefix and its SHA-256 hash, and Hash(rawKey) returning the SHA-256 hash
|
|
And internal/infrastructure/hashing implements KeyGenerator, asserted with
|
|
var _ apikey.KeyGenerator = &SHA256KeyGenerator{}
|
|
And a generated raw key is "rcms_" followed by the base64url encoding, without padding, of 32
|
|
random bytes, so it matches ^rcms_[A-Za-z0-9_-]{43}$
|
|
And the display prefix is the first 12 characters of the raw key
|
|
And only the SHA-256 hash and the prefix are persisted, never the raw key
|
|
And the use cases CreateAPIKey, ListAPIKeys, RevokeAPIKey and AuthenticateAPIKey depend only
|
|
on apikey.Repository, apikey.KeyGenerator, user.Repository and an injected clock
|
|
And ResetPasswordUseCase additionally receives apikey.Repository and an injected clock, and
|
|
after a successful password reset calls RevokeAllByUser(userId, now); it never reaches
|
|
persistence directly
|
|
And persistence has a GORM model, a mapper and a repository for the entity, the model is
|
|
added to the AutoMigrate list in api/dependecyInjection.go, and wiring happens only in
|
|
api/dependecyInjection.go
|
|
And a new middleware resolves "Authorization: Bearer rcms_<...>" to a user and places a jwt in
|
|
the same jwtauth request context the cookie path fills, with the claims user_id and
|
|
token_version taken from the user's current record
|
|
And that middleware runs after jwtauth.Verifier and ahead of jwtauth.Authenticator on the
|
|
/article routes (public and protected), /user, /media, /settings and GET /auth/me
|
|
And jwtauth.Authenticator, RequireCurrentToken, RequireVerifiedUser, RequireOwner and every
|
|
handler stay unchanged
|
|
And a new middleware RequireSession, mounted ahead of jwtauth.Verifier, answers 403 with
|
|
apperr.SessionRequiredErrorMessage whenever the Authorization header is a Bearer token that
|
|
starts with "rcms_", whether that key is valid, expired, revoked, unknown or garbage
|
|
And RequireSession is mounted on the key management routes POST /v1/api-key, GET /v1/api-key
|
|
and DELETE /v1/api-key/{id}, on the session-only auth routes POST /v1/auth/verify and
|
|
DELETE /v1/auth/register, and on every /v1/system route (all under /v1/system/update)
|
|
And a request on those routes without a "Bearer rcms_" header passes through RequireSession
|
|
unchanged, so cookie-only and jwt-Bearer behaviour there does not change
|
|
And the public auth routes login, register, registration, logout, password-reset,
|
|
password-reset/confirm, invite and invite/accept do not use the key middleware or
|
|
RequireSession and behave exactly as they do without a key header
|
|
And the routes are mounted under /v1 inside the jwtauth.Verifier, jwtauth.Authenticator,
|
|
RequireCurrentToken and RequireVerifiedUser group:
|
|
POST /v1/api-key, GET /v1/api-key and DELETE /v1/api-key/{id}
|
|
And a Bearer key on a key-accepting route that cannot authenticate gets 401 with
|
|
apperr.UnauthorizedErrorMessage, and a request without an rcms_ key keeps the existing
|
|
jwtauth.Authenticator response, status 401, unchanged by this feature
|
|
And new apperr strings: APIKeyNotFoundErrorMessage = "API key not found",
|
|
APIKeyExpiryErrorMessage = "The expiry must be a date in the future." and
|
|
SessionRequiredErrorMessage = "This action needs a real session."
|
|
And the per-key rate limit is 120 requests per minute, as constants in api/router.go
|
|
And enableCorsIfConfigured allows the methods GET, POST, PUT, PATCH, DELETE and OPTIONS
|
|
And users: owner id 1 "marta" (verified), admin id 2 "dev" (verified), admin id 3 "pat"
|
|
(verified), unverified id 4 "quinn"
|
|
|
|
Scenario: Creating a key returns the full key exactly once
|
|
Given a valid jwt cookie for admin 2
|
|
When the client sends POST /v1/api-key with body {"name": "ci-deploy"}
|
|
Then the response status is 201
|
|
And the header Cache-Control is "no-store"
|
|
And the body has exactly the keys id, name, prefix, key, created_at, expires_at
|
|
And name is "ci-deploy", expires_at is null, and key matches ^rcms_[A-Za-z0-9_-]{43}$
|
|
And prefix equals the first 12 characters of key
|
|
And the stored record has user_id 2 and HashedKey equal to the SHA-256 of key
|
|
And neither the raw key nor the stored hash appears in any later response
|
|
|
|
Scenario: The key can't be retrieved afterwards
|
|
Given admin 2 created the key "ci-deploy" and holds its raw key
|
|
When the same jwt cookie sends GET /v1/api-key
|
|
Then the response status is 200
|
|
And the body is a JSON array with one entry whose keys are exactly id, user_id, name, prefix,
|
|
status, created_at, last_used_at, expires_at, revoked_at
|
|
And the entry has no "key" and no "hashed_key" field
|
|
And the raw key and its hash appear nowhere in the body
|
|
And the entry's prefix is the first 12 characters of the raw key
|
|
|
|
Scenario: Creating a key with an expiry
|
|
Given a valid jwt cookie for admin 2
|
|
When the client sends POST /v1/api-key with body
|
|
{"name": "temp", "expires_at": "<now + 30 days, RFC3339>"}
|
|
Then the response status is 201
|
|
And expires_at in the body equals the requested instant
|
|
|
|
Scenario Outline: Creating a key with an invalid body is refused and creates nothing
|
|
Given a valid jwt cookie for admin 2
|
|
When the client sends POST /v1/api-key with <body>
|
|
Then the response status is 400
|
|
And the response body is <message>
|
|
And no key exists in the repository
|
|
|
|
Examples:
|
|
| body | message |
|
|
| body not-json | apperr.BodyErrorMessage |
|
|
| body {} | text containing 'Name' and 'required' |
|
|
| body {"name": ""} | text containing 'Name' and 'required' |
|
|
| body {"name": "<61 characters>"} | text containing 'Name' and 'max' |
|
|
| body {"name": "x", "expires_at": "yesterday"} | apperr.BodyErrorMessage |
|
|
| body {"name": "x", "expires_at": "<now - 1h>"} | apperr.APIKeyExpiryErrorMessage |
|
|
|
|
Scenario: A request body can't choose the owner or the role of a new key
|
|
Given a valid jwt cookie for admin 2
|
|
When the client sends POST /v1/api-key with body {"name": "sneaky", "user_id": 1, "role": "owner"}
|
|
Then the response status is 201
|
|
And the stored key belongs to user 2
|
|
And using that key on POST /v1/user/invite returns 403 with apperr.OwnerRequiredErrorMessage
|
|
|
|
Scenario Outline: Key management needs a verified real session
|
|
Given <caller>
|
|
When the client sends POST /v1/api-key with body {"name": "x"}
|
|
Then the response status is <status>
|
|
And the response body is <message>
|
|
And no key exists in the repository
|
|
|
|
Examples:
|
|
| caller | status | message |
|
|
| no credentials | 401 | the existing Authenticator response |
|
|
| a jwt cookie for user 4 | 403 | apperr.VerificationRequiredErrorMessage |
|
|
| a stale token_version cookie | 401 | the existing Authenticator response |
|
|
|
|
Scenario Outline: A valid key acts as its creator on every key-accepting route
|
|
Given an active key created by admin 2
|
|
When the client sends <method> <path> with "Authorization: Bearer <that key>" and no cookie <body>
|
|
Then the response status is <status>
|
|
And the effect is attributed to user 2
|
|
|
|
Examples:
|
|
| method | path | body | status |
|
|
| GET | /v1/settings | | 200 |
|
|
| GET | /v1/user | | 200 |
|
|
| PATCH | /v1/user/me | and body {"display_name": "Dev"} | 200 |
|
|
| POST | /v1/article | and body {"title": "A valid title", "body": "x"} | 201 |
|
|
| PATCH | /v1/article/1/publish | | 200 |
|
|
| DELETE | /v1/article/1 | | 204 |
|
|
| GET | /v1/media | | 200 |
|
|
| DELETE | /v1/media/1 | | 204 |
|
|
|
|
Scenario: A key can read its creator through GET /v1/auth/me
|
|
Given an active key created by admin 2
|
|
When the client sends GET /v1/auth/me with that key and no cookie
|
|
Then the response status is 200
|
|
And the body is the same JSON the jwt cookie of admin 2 gets on GET /v1/auth/me
|
|
And the body describes user 2 "dev"
|
|
|
|
Scenario: A key that can't authenticate gets 401 on GET /v1/auth/me
|
|
When the client sends GET /v1/auth/me with "Authorization: Bearer rcms_unknown"
|
|
Then the response status is 401
|
|
And the response body is apperr.UnauthorizedErrorMessage
|
|
|
|
Scenario: A key sees offline articles on the public article routes, an anonymous caller does not
|
|
Given an offline article 5 and an online article 6
|
|
And an active key created by admin 2
|
|
When the client sends GET /v1/article with that key
|
|
Then the body lists articles 5 and 6
|
|
When the client sends GET /v1/article with no credentials
|
|
Then the body lists only article 6
|
|
When the client sends GET /v1/article/5 with that key
|
|
Then the response status is 200
|
|
When the client sends GET /v1/article/5 with no credentials
|
|
Then the response status is 404
|
|
|
|
Scenario: An invalid key on a public article route is refused, not treated as anonymous
|
|
When the client sends GET /v1/article with "Authorization: Bearer rcms_unknown"
|
|
Then the response status is 401
|
|
And the response body is apperr.UnauthorizedErrorMessage
|
|
|
|
Scenario: An owner's key can do owner-only things
|
|
Given an active key created by owner 1
|
|
When the client sends POST /v1/user/invite with that key and body {"email": "new@example.com"}
|
|
Then the response status is 201
|
|
And the invite's invited_by is 1
|
|
|
|
Scenario: An admin's key can not do owner-only things
|
|
Given an active key created by admin 2
|
|
When the client sends POST /v1/user/invite with that key and body {"email": "new@example.com"}
|
|
Then the response status is 403
|
|
And the response body is apperr.OwnerRequiredErrorMessage
|
|
And no invite exists
|
|
|
|
Scenario Outline: A key that can't authenticate gets the same 401 as a bad jwt
|
|
Given <state>
|
|
When the client sends GET /v1/settings with "Authorization: Bearer <credential>"
|
|
Then the response status is 401
|
|
And the response body is apperr.UnauthorizedErrorMessage
|
|
And no handler or use case behind the route runs
|
|
|
|
Examples:
|
|
| state | credential |
|
|
| no key stored for it | rcms_unknownunknownunknownunknownunknownunknown1 |
|
|
| a key whose expires_at is one second ago | that key |
|
|
| a key that was revoked | that key |
|
|
| a key of user 1 whose user record was deleted | that key |
|
|
| a key of an unverified user | that key |
|
|
| nothing after the prefix | rcms_ |
|
|
| a key with one character changed | that key, last character changed |
|
|
| a key's hash passed instead of the key | the stored HashedKey, status 401 only |
|
|
|
|
Scenario: Key behaviour does not change the jwt cookie path
|
|
Given a valid jwt cookie for admin 2
|
|
When the client sends GET /v1/settings with only that cookie
|
|
Then the response status is 200
|
|
When the client sends GET /v1/settings with an Authorization header "Bearer <a valid jwt>" and no cookie
|
|
Then the response status is 200
|
|
When the client sends GET /v1/settings with a stale token_version cookie
|
|
Then the response status is 401
|
|
|
|
Scenario: A Bearer key wins over the cookie, and a bad key is not rescued by a good cookie
|
|
Given an active key created by admin 3 and a valid jwt cookie for owner 1
|
|
When the client sends POST /v1/user/invite with both the key and the cookie
|
|
Then the response status is 403
|
|
When the client sends GET /v1/settings with "Authorization: Bearer rcms_unknown" and the owner cookie
|
|
Then the response status is 401
|
|
|
|
Scenario Outline: Session-only routes refuse any rcms_ credential with 403
|
|
Given <state>
|
|
When the client sends <method> <path> with "Authorization: Bearer <credential>" <body>
|
|
Then the response status is 403
|
|
And the response body is apperr.SessionRequiredErrorMessage
|
|
And no use case behind the route runs
|
|
And no key was created, listed or revoked
|
|
|
|
Examples:
|
|
| state | credential | method | path | body |
|
|
| an active key of owner 1 | that key | POST | /v1/api-key | and body {"name": "child"} |
|
|
| an active key of owner 1 | that key | GET | /v1/api-key | |
|
|
| an active key of owner 1 | that key | DELETE | /v1/api-key/1 | |
|
|
| an active key of owner 1 | that key | POST | /v1/auth/verify | and body {"code": "x"} |
|
|
| an active key of owner 1 | that key | DELETE | /v1/auth/register | |
|
|
| an active key of owner 1 | that key | GET | /v1/system/update | |
|
|
| an active key of owner 1 | that key | POST | /v1/system/update/check | |
|
|
| an active key of owner 1 | that key | POST | /v1/system/update/download | |
|
|
| an active key of owner 1 | that key | POST | /v1/system/update/restart | |
|
|
| a revoked key | that key | GET | /v1/api-key | |
|
|
| no key stored for it | rcms_unknownunknownunknownunknownunknownunknown1 | POST | /v1/system/update/check | |
|
|
| nothing | rcms_ | DELETE | /v1/auth/register | |
|
|
|
|
Scenario Outline: The session-only routes still work as before with a session
|
|
Given a valid jwt cookie for owner 1
|
|
When the client sends <method> <path> with only that cookie <body>
|
|
Then the response is the same as before this feature
|
|
And a request with no credentials still gets status 401 from the existing Authenticator
|
|
|
|
Examples:
|
|
| method | path | body |
|
|
| POST | /v1/api-key | and body {"name": "x"} |
|
|
| GET | /v1/api-key | |
|
|
| GET | /v1/system/update | |
|
|
| POST | /v1/system/update/check | |
|
|
|
|
Scenario Outline: A key never obtains a session through the public auth routes
|
|
Given an active key created by owner 1
|
|
When the client sends <method> <path> with that key and body <body>
|
|
Then the response is the same as without the key
|
|
And no jwt cookie is set unless the credentials in the body are valid
|
|
And the stored user and password are unchanged
|
|
|
|
Examples:
|
|
| method | path | body |
|
|
| POST | /v1/auth/login | {"username": "marta", "password": "wrong-pass-1"} |
|
|
| POST | /v1/auth/password-reset/confirm | {"email": "marta@example.com", "code": "nope", "password": "newpassword1"} |
|
|
|
|
Scenario: A successful password reset revokes every key of that user
|
|
Given user 2 has active keys "a" and "b", and each works on GET /v1/settings
|
|
And user 2's stored PasswordResetCode is the hash of "reset-code-1" with an unexpired expiration
|
|
When the client sends POST /v1/auth/password-reset/confirm with body
|
|
{"email": "dev@example.com", "code": "reset-code-1", "password": "newpassword1"}
|
|
Then the response status is 200
|
|
And GET /v1/settings with key "a" returns 401 with apperr.UnauthorizedErrorMessage
|
|
And GET /v1/settings with key "b" returns 401 with apperr.UnauthorizedErrorMessage
|
|
|
|
Scenario: A password reset leaves other users' keys untouched
|
|
Given user 2 has an active key "a" and user 3 has an active key "c"
|
|
And user 2 has a valid password reset code "reset-code-1"
|
|
When user 2's password reset is confirmed with "reset-code-1"
|
|
Then GET /v1/settings with key "c" returns 200
|
|
And key "c" has no revoked_at
|
|
|
|
Scenario: Keys revoked by a password reset show as revoked
|
|
Given user 2 has active keys "a" and "b", and a clock at T
|
|
And user 2 has a valid password reset code "reset-code-1"
|
|
When user 2's password reset is confirmed with "reset-code-1"
|
|
And admin 2 logs in again with the new password and sends GET /v1/api-key with the new cookie
|
|
Then the entries "a" and "b" have status "revoked" and revoked_at T
|
|
|
|
Scenario: A password reset does not move the revoked_at of a key that was already revoked
|
|
Given user 2 has a key "old" revoked at T0 and an active key "a"
|
|
And user 2 has a valid password reset code "reset-code-1"
|
|
When user 2's password reset is confirmed with "reset-code-1" at a later time T
|
|
Then key "old" still has revoked_at T0
|
|
And key "a" has revoked_at T
|
|
|
|
Scenario Outline: A failed password reset confirmation revokes nothing
|
|
Given user 2 has an active key "a"
|
|
And user 2's stored PasswordResetCode is the hash of "reset-code-1" with <expiration>
|
|
When the client sends POST /v1/auth/password-reset/confirm with <body>
|
|
Then the response status is not 200
|
|
And key "a" has no revoked_at
|
|
And GET /v1/settings with key "a" returns 200
|
|
|
|
Examples:
|
|
| expiration | body |
|
|
| an unexpired expiration | {"email": "dev@example.com", "code": "wrong", "password": "newpassword1"} |
|
|
| an expiration in the past | {"email": "dev@example.com", "code": "reset-code-1", "password": "newpassword1"} |
|
|
| an unexpired expiration | {"email": "nobody@example.com", "code": "reset-code-1", "password": "newpassword1"} |
|
|
|
|
Scenario: Listing as an admin shows only that admin's keys
|
|
Given keys "a" and "b" created by admin 2 and key "c" created by admin 3
|
|
When admin 2 sends GET /v1/api-key with a jwt cookie
|
|
Then the body lists exactly "a" and "b"
|
|
|
|
Scenario: Listing as the owner shows every user's keys, with the creator's id
|
|
Given the same three keys and key "d" created by owner 1
|
|
When owner 1 sends GET /v1/api-key with a jwt cookie
|
|
Then the body lists "a", "b", "c" and "d" newest first
|
|
And each entry has the user_id of its creator
|
|
|
|
Scenario Outline: The list reports each key's state
|
|
Given a key that is <state>
|
|
When its creator sends GET /v1/api-key with a jwt cookie
|
|
Then the entry's status is "<status>", revoked_at is <revoked_at>, and last_used_at is <last_used>
|
|
|
|
Examples:
|
|
| state | status | revoked_at | last_used |
|
|
| active, never used | active | null | null |
|
|
| active, used 5 minutes ago | active | null | that time |
|
|
| expired one hour ago | expired | null | unchanged |
|
|
| revoked | revoked | a time | unchanged |
|
|
| both expired and revoked | revoked | a time | unchanged |
|
|
|
|
Scenario: Revoking a key ends it immediately
|
|
Given an active key "ci-deploy" created by admin 2
|
|
And the key works on GET /v1/settings
|
|
When admin 2 sends DELETE /v1/api-key/<its id> with a jwt cookie
|
|
Then the response status is 204 and the body is empty
|
|
And GET /v1/settings with that key returns 401 with apperr.UnauthorizedErrorMessage
|
|
And GET /v1/api-key lists it with status "revoked"
|
|
|
|
Scenario: Revoking twice is idempotent
|
|
Given a key revoked at time T
|
|
When its creator sends DELETE /v1/api-key/<its id> again
|
|
Then the response status is 204
|
|
And revoked_at is still T
|
|
|
|
Scenario: An admin can't revoke someone else's key, and learns nothing about it
|
|
Given a key created by admin 3
|
|
When admin 2 sends DELETE /v1/api-key/<its id> with a jwt cookie
|
|
Then the response status is 404
|
|
And the response body is apperr.APIKeyNotFoundErrorMessage
|
|
And the key still works
|
|
|
|
Scenario: The owner can revoke any key
|
|
Given a key created by admin 3
|
|
When owner 1 sends DELETE /v1/api-key/<its id> with a jwt cookie
|
|
Then the response status is 204
|
|
And the key gets 401 on GET /v1/settings
|
|
|
|
Scenario Outline: Revoking with a bad id
|
|
Given a valid jwt cookie for admin 2
|
|
When the client sends DELETE /v1/api-key/<id>
|
|
Then the response status is <status>
|
|
And the response body is <message>
|
|
|
|
Examples:
|
|
| id | status | message |
|
|
| abc | 400 | apperr.IdUint32ErrorMessage |
|
|
| -1 | 400 | apperr.IdUint32ErrorMessage |
|
|
| 4294967296 | 400 | apperr.IdUint32ErrorMessage |
|
|
| 999 | 404 | apperr.APIKeyNotFoundErrorMessage |
|
|
|
|
Scenario: Using a key records last_used_at, at most once per minute
|
|
Given an active key never used, and a fake clock at T
|
|
When the key authenticates a request at T
|
|
Then last_used_at is T
|
|
When it authenticates again at T + 30s
|
|
Then no write happened and last_used_at is still T
|
|
When it authenticates again at T + 61s
|
|
Then last_used_at is T + 61s
|
|
|
|
Scenario: A failure to record last_used_at doesn't fail the request
|
|
Given an active key and a repository whose TouchLastUsed returns an error
|
|
When the client sends GET /v1/settings with that key
|
|
Then the response status is 200
|
|
|
|
Scenario: A rejected key never records last_used_at
|
|
Given an expired key and a revoked key
|
|
When each is used on GET /v1/settings
|
|
Then each gets 401 and no last_used_at is written
|
|
|
|
Scenario: Each key has its own rate limit
|
|
Given two active keys "a" and "b" and a limit of N requests per minute per key
|
|
When key "a" sends N+1 requests to GET /v1/settings
|
|
Then request N+1 gets 429 with apperr.TooManyRequestsErrorMessage and a Retry-After header
|
|
And key "b" still gets 200
|
|
And a jwt cookie request still gets 200
|
|
|
|
Scenario: The rate limit does not apply to unauthenticated requests
|
|
When the client sends N+1 requests with "Authorization: Bearer rcms_unknown"
|
|
Then every response is 401, never 429
|
|
|
|
Scenario: The CORS preflight allows PATCH
|
|
Given settings with CorsAllowedOrigins ["https://app.example.com"]
|
|
When the client sends OPTIONS /v1/article/1 with Origin "https://app.example.com" and
|
|
Access-Control-Request-Method "PATCH"
|
|
Then the response is a successful preflight
|
|
And Access-Control-Allow-Methods includes PATCH
|
|
And a preflight for method "TRACE" is not allowed
|
|
|
|
Scenario: Use case: creating a key hashes it and stores no plaintext
|
|
Given a fake KeyGenerator returning raw "rcms_fake", prefix "rcms_fake", hash "hash:rcms_fake"
|
|
When CreateAPIKey is called with user 2 and name "ci"
|
|
Then the repository's Create receives HashedKey "hash:rcms_fake", Prefix "rcms_fake", UserID 2
|
|
And the result carries the raw key, which is not stored
|
|
|
|
Scenario: Use case: authenticating returns the user and rejects every invalid case with one sentinel
|
|
Given a fake KeyGenerator and keys that are unknown, expired, revoked, and owned by a missing or
|
|
unverified user
|
|
When AuthenticateAPIKey is called with each raw key
|
|
Then every call returns apikey.ErrAPIKeyInvalid, asserted with errors.Is
|
|
And a key expiring exactly at the clock's now is expired
|
|
And a valid key returns the user from user.Repository
|
|
|
|
Scenario: Use case: a failing repository is not reported as an invalid key
|
|
Given a repository whose GetByHashedKey returns an unexpected error
|
|
When AuthenticateAPIKey is called
|
|
Then the error is returned and errors.Is(err, apikey.ErrAPIKeyInvalid) is false
|
|
And the middleware answers 500 "Internal server error"
|
|
|
|
Scenario: Use case: ResetPassword revokes the user's keys through the domain repository
|
|
Given a fake apikey.Repository holding active keys of users 2 and 3, a fake clock at T, and a
|
|
user.Repository fake with a valid reset code for user 2
|
|
When ResetPasswordUseCase.ResetPassword succeeds for user 2
|
|
Then the fake apikey.Repository received exactly one RevokeAllByUser(2, T)
|
|
And user 3's keys are untouched
|
|
|
|
Scenario: Use case: ResetPassword does not call RevokeAllByUser when the reset fails
|
|
Given a fake apikey.Repository and a user.Repository fake with a wrong or expired reset code
|
|
When ResetPasswordUseCase.ResetPassword is called
|
|
Then it returns the existing invalid or expired sentinel
|
|
And RevokeAllByUser is never called
|
|
|
|
Scenario: Use case: a failure to revoke keys after a reset is reported
|
|
Given a valid reset code and an apikey.Repository whose RevokeAllByUser returns an error
|
|
When ResetPasswordUseCase.ResetPassword is called
|
|
Then the use case returns that error
|
|
And the password was already changed
|