test(api): 处理器测试的每次请求在包结束后对照 openapi 校验状态码、响应与 2xx 请求体,SetupAllowed 纳入一致性比对,补齐漏记的状态码并修正空列表回 null

This commit is contained in:
Lemon-miaow committed 2026-09-25 18:24:47 +08:00
1 parent 1054e62fa9
commit e7326e6315
11 files changed
+937 -28

No files matched your search

+174 -9
View File
@@ -12,15 +12,52 @@
# from (internalAPIRoutes / externalAPIRoutes in internal/api/api.go). A route
# added, removed, re-faced, or re-tiered without updating this file fails
# `go test ./...`. So path, method, face and tier are as trustworthy as the code.
# * The request/response BODY schemas below are hand-maintained from the Go
# handler types and are NOT yet schema-validated against live traffic. Treat
# them as documentation (SHAPE-ASSERTED), not as a contract test.
# * Which operations a setup-lockdown session may still use (x-felis-setup-allowed)
# is checked the same way against the SetupAllowed flag in those tables.
# * The named response schemas are compared field by field with the Go structs
# the handlers encode (internal/api/openapi_parity_test.go).
# * Every request the handler tests send is held to this file once the package
# has run (internal/api/openapi_contract_test.go): the operation (or
# x-felis-common-responses) must list the status that came back, a JSON response
# must fit the schema for that status and carry no property it does not name,
# and the JSON request behind a 2xx must fit the requestBody. Statuses and
# bodies no test reaches are still hand-maintained.
#
# The deployment zone (RootDomain, spec §2) never appears here — `example.test`
# is a placeholder, per the no-hardcoded-domain red line.
openapi: 3.1.0
# Answers any operation can give under the stated condition, declared once here
# instead of under every operation. when: any | internal (served on the internal
# face) | session (takes the session cookie) | setup-locked (takes the session
# cookie and is not x-felis-setup-allowed) | json-body (takes a JSON requestBody).
# code, when set, is the error code that answer carries.
x-felis-common-responses:
- status: 500
when: any
response: { $ref: '#/components/responses/InternalError' }
- status: 403
when: internal
code: wrong_caller
response: { $ref: '#/components/responses/WrongCaller' }
- status: 403
when: session
code: forbidden
response: { $ref: '#/components/responses/StaffOnlyHost' }
- status: 403
when: setup-locked
code: setup_required
response: { $ref: '#/components/responses/SetupRequired' }
- status: 413
when: json-body
code: too_large
response: { $ref: '#/components/responses/TooLarge' }
- status: 415
when: json-body
code: unsupported_media_type
response: { $ref: '#/components/responses/UnsupportedMediaType' }
info:
title: felis-api
version: 4.1.0
@@ -124,6 +161,41 @@ components:
responses:
NoContent:
description: Success, no body.
InternalError:
description: An unexpected failure (internal); the details are in the server log under the request id.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
WrongCaller:
description: A genuine service token for a caller this operation does not serve (wrong_caller).
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
StaffOnlyHost:
description: A session of a player (not admin or owner) on the operator console host, refused before any handler (forbidden).
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
SetupRequired:
description: The session belongs to an account still in first-run setup, which may use only the x-felis-setup-allowed operations until it has a durable sign-in (setup_required).
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
TooLarge:
description: The JSON body is over 1 MiB (too_large).
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
UnsupportedMediaType:
description: A body sent with a Content-Type other than application/json (unsupported_media_type).
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
InsufficientStorage:
description: The store this writes to is full — the backup archive (backup_store_full), the world volume (volume_full) or the upload area (uploads_full).
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
BadRequest:
description: Malformed or invalid request (validation, bad body, unknown field).
content:
@@ -976,6 +1048,11 @@ paths:
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'503':
description: The node is at its running-server cap (at_capacity).
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
/api/v1/internal/servers/{name}/status:
get:
@@ -1259,6 +1336,11 @@ paths:
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'409':
description: The linked account holds a staff role and is never reclaimed (protected_admin).
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
/api/v1/internal/player/blacklist/{mc_uuid}:
get:
@@ -1553,6 +1635,11 @@ paths:
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'503':
description: The node is at its running-server cap (at_capacity).
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
/api/v1/servers/{name}/stop:
post:
@@ -1697,11 +1784,21 @@ paths:
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
description: Unknown server.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'409':
description: Server not running.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'429':
description: This session already holds as many console streams as it may (too_many_streams).
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'503':
$ref: '#/components/responses/ServiceUnavailable'
@@ -2314,7 +2411,7 @@ paths:
required: [user_id, role]
properties:
user_id: { type: string }
role: { type: string, enum: [user, admin] }
role: { type: string, enum: [owner, admin, user] }
'400':
description: >-
Invalid email or missing assertion (bad_request); or the login could not be
@@ -2464,7 +2561,7 @@ paths:
required: [user_id, role]
properties:
user_id: { type: string }
role: { type: string, enum: [user, admin] }
role: { type: string, enum: [owner, admin, user] }
'400':
description: >-
Missing login_id or assertion (bad_request); or the login could not be
@@ -2599,7 +2696,7 @@ paths:
required: [user_id, role]
properties:
user_id: { type: string }
role: { type: string, enum: [user, admin] }
role: { type: string, enum: [owner, admin, user] }
'400':
description: >-
A valid email and code are required (bad_request); or the code is wrong,
@@ -2756,7 +2853,7 @@ paths:
required: [user_id, role]
properties:
user_id: { type: string }
role: { type: string, enum: [user, admin] }
role: { type: string, enum: [owner, admin, user] }
'400':
description: >-
request_id and code are required (bad_request); or the login could not be
@@ -2814,7 +2911,7 @@ paths:
properties:
user_id: { type: string }
username: { type: string }
role: { type: string, enum: [user, admin] }
role: { type: string, enum: [owner, admin, user] }
email: { type: string }
email_verified: { type: boolean }
has_passkey: { type: boolean }
@@ -2851,6 +2948,7 @@ paths:
API is fenced until setup completes).
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
responses:
'200':
@@ -2863,7 +2961,7 @@ paths:
properties:
user_id: { type: string }
username: { type: string }
role: { type: string, enum: [user, admin] }
role: { type: string, enum: [owner, admin, user] }
email: { type: string }
email_verified: { type: boolean }
has_passkey: { type: boolean }
@@ -2970,6 +3068,7 @@ paths:
access.
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
responses:
'200':
@@ -3003,6 +3102,11 @@ paths:
nudges unverified accounts through the email-OTP flow.
'401':
$ref: '#/components/responses/Unauthorized'
'503':
description: The account settings could not be read (auth_unavailable).
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
/api/v1/me/servers:
get:
@@ -3011,6 +3115,7 @@ paths:
summary: List the servers the caller owns or may claim.
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
responses:
'200':
@@ -3227,6 +3332,11 @@ paths:
safety_snapshot:
type: boolean
description: Whether a safety snapshot runs first. False when the request turned it off, or when this install cannot take one.
'400':
description: Malformed server name (bad_name) or request body.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'401':
$ref: '#/components/responses/Unauthorized'
'403':
@@ -3272,6 +3382,11 @@ paths:
properties:
name: { type: string }
status: { type: string, const: backing_up }
'400':
description: Malformed server name (bad_name).
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'401':
$ref: '#/components/responses/Unauthorized'
'403':
@@ -3286,8 +3401,15 @@ paths:
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'429':
description: Another backup of this server started within the cooldown (backup_cooldown).
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'503':
$ref: '#/components/responses/ServiceUnavailable'
'507':
$ref: '#/components/responses/InsufficientStorage'
# -------------------------------------------------- async job status (app) ---
/api/v1/servers/{name}/jobs:
@@ -3496,6 +3618,8 @@ paths:
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'507':
$ref: '#/components/responses/InsufficientStorage'
put:
tags: [files]
operationId: writeServerFile
@@ -3817,6 +3941,11 @@ paths:
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
description: Unknown user.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
put:
tags: [users]
operationId: setQuotas
@@ -3849,6 +3978,11 @@ paths:
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
description: Unknown user.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
/api/v1/users/{id}/sessions:
get:
@@ -4010,6 +4144,11 @@ paths:
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
description: Unknown user.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'409':
description: UUID is already linked to a different user.
content:
@@ -4055,6 +4194,7 @@ paths:
summary: Report account-link status and in-game instructions (web side, spec §10).
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
responses:
'200':
@@ -4077,6 +4217,7 @@ paths:
summary: Consume an in-game link code and bind the account.
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
requestBody:
required: true
@@ -4128,6 +4269,7 @@ paths:
session must have reauthed within 5 minutes (403 reauth_required).
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
requestBody:
required: true
@@ -4191,6 +4333,7 @@ paths:
consumed, or mismatched code is a 400.
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
requestBody:
required: true
@@ -4219,6 +4362,11 @@ paths:
schema: { $ref: '#/components/schemas/Error' }
'401':
$ref: '#/components/responses/Unauthorized'
'409':
description: Another account already proved this address (email_taken); the code is not consumed.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'429':
description: >-
Too many incorrect attempts on this code (otp_locked), or the account's
@@ -4241,6 +4389,7 @@ paths:
must have reauthed within 5 minutes (403 reauth_required).
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
requestBody:
required: true
@@ -4286,6 +4435,7 @@ paths:
instance.
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
responses:
'200':
@@ -4320,6 +4470,7 @@ paths:
configured.
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
requestBody:
required: true
@@ -4368,6 +4519,7 @@ paths:
not need the WebAuthn verifier, so it succeeds even where begin/finish report 503.
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
responses:
'200':
@@ -4400,6 +4552,7 @@ paths:
(403 reauth_required).
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
parameters:
- name: id
@@ -4439,6 +4592,7 @@ paths:
op-login or a passkey).
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
responses:
'200':
@@ -4467,6 +4621,7 @@ paths:
bound to a fresh reauth-purpose challenge.
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
responses:
'200':
@@ -4494,6 +4649,7 @@ paths:
clone check (a cloned authenticator is 400 passkey_login_invalid).
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
requestBody:
required: true
@@ -4529,6 +4685,7 @@ paths:
signing in again (403 staff_reauth).
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
responses:
'202':
@@ -4579,6 +4736,7 @@ paths:
summary: Redeem the reauth code and mark this session reauthed for 5 minutes.
x-felis-face: [external]
x-felis-tier: app
x-felis-setup-allowed: true
security: [{ sessionCookie: [] }]
requestBody:
required: true
@@ -5412,6 +5570,11 @@ paths:
content:
text/event-stream:
schema: { type: string }
'400':
description: Malformed build id (bad_request). The id becomes a label-selector value, so it is refused before any lookup.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'401':
$ref: '#/components/responses/Unauthorized'
'403':
@@ -5595,6 +5758,8 @@ paths:
content:
application/json:
schema: { $ref: '#/components/schemas/Submission' }
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':