feat: game lobby service

This commit is contained in:
Ilia Denisov
2026-04-25 23:20:55 +02:00
committed by GitHub
parent 32dc29359a
commit 48b0056b49
336 changed files with 57074 additions and 1418 deletions
+634
View File
@@ -0,0 +1,634 @@
package lobby
import (
"context"
"encoding/json"
"net/http"
"path/filepath"
"runtime"
"testing"
"github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
)
// TestPublicOpenAPISpecValidates loads public-openapi.yaml and verifies it
// is a syntactically valid OpenAPI 3.0 document.
func TestPublicOpenAPISpecValidates(t *testing.T) {
t.Parallel()
loadPublicSpec(t)
}
// TestInternalOpenAPISpecValidates loads internal-openapi.yaml and verifies
// it is a syntactically valid OpenAPI 3.0 document.
func TestInternalOpenAPISpecValidates(t *testing.T) {
t.Parallel()
loadInternalSpec(t)
}
// TestPublicSpecFreezesGameCreateContract verifies that the game-create
// operation has a stable operationId, correct request and response schema
// references, and the expected required fields on CreateGameRequest.
func TestPublicSpecFreezesGameCreateContract(t *testing.T) {
t.Parallel()
doc := loadPublicSpec(t)
op := getOperation(t, doc, "/api/v1/lobby/games", http.MethodPost)
require.Equal(t, "createGame", op.OperationID)
assertOperationParameterRefs(t, op, "#/components/parameters/XUserID")
assertSchemaRef(t, requestSchemaRef(t, op), "#/components/schemas/CreateGameRequest", "createGame request")
assertSchemaRef(t, responseSchemaRef(t, op, http.StatusCreated), "#/components/schemas/GameRecord", "createGame 201")
assertSchemaRef(t, responseSchemaRef(t, op, http.StatusBadRequest), "#/components/schemas/ErrorResponse", "createGame 400")
assertSchemaRef(t, responseSchemaRef(t, op, http.StatusForbidden), "#/components/schemas/ErrorResponse", "createGame 403")
assertSchemaRef(t, responseSchemaRef(t, op, http.StatusUnprocessableEntity), "#/components/schemas/ErrorResponse", "createGame 422")
req := componentSchemaRef(t, doc, "CreateGameRequest")
assertRequiredFields(t, req,
"game_name", "game_type",
"min_players", "max_players",
"start_gap_hours", "start_gap_players",
"enrollment_ends_at", "turn_schedule", "target_engine_version",
)
}
// TestPublicSpecFreezesGameRecordSchema verifies that GameRecord carries the
// full frozen field set from README.md and that optional fields are not
// listed as required.
func TestPublicSpecFreezesGameRecordSchema(t *testing.T) {
t.Parallel()
doc := loadPublicSpec(t)
schema := componentSchemaRef(t, doc, "GameRecord")
assertRequiredFields(t, schema,
"game_id", "game_name", "game_type", "owner_user_id", "status",
"min_players", "max_players", "start_gap_hours", "start_gap_players",
"enrollment_ends_at", "turn_schedule", "target_engine_version",
"created_at", "updated_at",
"current_turn", "runtime_status", "engine_health_summary",
)
// Optional fields must be present in properties but not in required.
for _, opt := range []string{"description", "started_at", "finished_at"} {
require.Contains(t, schema.Value.Properties, opt, "GameRecord.%s must be in properties", opt)
}
}
// TestPublicSpecFreezesStatusEnums verifies that the game_status enum in
// GameRecord contains the full frozen 9-value set.
func TestPublicSpecFreezesStatusEnums(t *testing.T) {
t.Parallel()
doc := loadPublicSpec(t)
assertStringEnum(t, componentSchemaRef(t, doc, "GameRecord"), "status",
"draft", "enrollment_open", "ready_to_start", "starting",
"start_failed", "running", "paused", "finished", "cancelled",
)
assertStringEnum(t, componentSchemaRef(t, doc, "GameRecord"), "game_type",
"public", "private",
)
}
// TestPublicSpecFreezesGameLifecycleContracts verifies that every state
// transition command has the correct operationId and returns a GameRecord on
// success.
func TestPublicSpecFreezesGameLifecycleContracts(t *testing.T) {
t.Parallel()
doc := loadPublicSpec(t)
cases := []struct {
path string
operationID string
}{
{"/api/v1/lobby/games/{game_id}/open-enrollment", "openEnrollment"},
{"/api/v1/lobby/games/{game_id}/ready-to-start", "manualReadyToStart"},
{"/api/v1/lobby/games/{game_id}/start", "startGame"},
{"/api/v1/lobby/games/{game_id}/pause", "pauseGame"},
{"/api/v1/lobby/games/{game_id}/resume", "resumeGame"},
{"/api/v1/lobby/games/{game_id}/cancel", "cancelGame"},
{"/api/v1/lobby/games/{game_id}/retry-start", "retryStart"},
}
for _, tc := range cases {
tc := tc
t.Run(tc.operationID, func(t *testing.T) {
t.Parallel()
op := getOperation(t, doc, tc.path, http.MethodPost)
require.Equal(t, tc.operationID, op.OperationID)
assertSchemaRef(t, responseSchemaRef(t, op, http.StatusOK), "#/components/schemas/GameRecord",
tc.operationID+" 200")
})
}
}
// TestPublicSpecFreezesApplicationContracts verifies the three application
// operations: submit, approve, and reject.
func TestPublicSpecFreezesApplicationContracts(t *testing.T) {
t.Parallel()
doc := loadPublicSpec(t)
submitOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/applications", http.MethodPost)
require.Equal(t, "submitApplication", submitOp.OperationID)
assertSchemaRef(t, requestSchemaRef(t, submitOp), "#/components/schemas/SubmitApplicationRequest", "submit request")
assertSchemaRef(t, responseSchemaRef(t, submitOp, http.StatusCreated), "#/components/schemas/ApplicationRecord", "submit 201")
req := componentSchemaRef(t, doc, "SubmitApplicationRequest")
assertRequiredFields(t, req, "race_name")
approveOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/applications/{application_id}/approve", http.MethodPost)
require.Equal(t, "approveApplication", approveOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, approveOp, http.StatusOK), "#/components/schemas/MembershipRecord", "approve 200")
rejectOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/applications/{application_id}/reject", http.MethodPost)
require.Equal(t, "rejectApplication", rejectOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, rejectOp, http.StatusOK), "#/components/schemas/ApplicationRecord", "reject 200")
appRecord := componentSchemaRef(t, doc, "ApplicationRecord")
assertRequiredFields(t, appRecord,
"application_id", "game_id", "applicant_user_id", "race_name", "status", "created_at",
)
assertStringEnum(t, appRecord, "status", "submitted", "approved", "rejected")
}
// TestPublicSpecFreezesInviteContracts verifies the four invite operations:
// create, redeem, decline, and revoke.
func TestPublicSpecFreezesInviteContracts(t *testing.T) {
t.Parallel()
doc := loadPublicSpec(t)
createOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/invites", http.MethodPost)
require.Equal(t, "createInvite", createOp.OperationID)
assertSchemaRef(t, requestSchemaRef(t, createOp), "#/components/schemas/CreateInviteRequest", "create request")
assertSchemaRef(t, responseSchemaRef(t, createOp, http.StatusCreated), "#/components/schemas/InviteRecord", "create 201")
req := componentSchemaRef(t, doc, "CreateInviteRequest")
assertRequiredFields(t, req, "invitee_user_id")
redeemOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/invites/{invite_id}/redeem", http.MethodPost)
require.Equal(t, "redeemInvite", redeemOp.OperationID)
assertSchemaRef(t, requestSchemaRef(t, redeemOp), "#/components/schemas/RedeemInviteRequest", "redeem request")
assertSchemaRef(t, responseSchemaRef(t, redeemOp, http.StatusOK), "#/components/schemas/MembershipRecord", "redeem 200")
declineOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/invites/{invite_id}/decline", http.MethodPost)
require.Equal(t, "declineInvite", declineOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, declineOp, http.StatusOK), "#/components/schemas/InviteRecord", "decline 200")
revokeOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/invites/{invite_id}/revoke", http.MethodPost)
require.Equal(t, "revokeInvite", revokeOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, revokeOp, http.StatusOK), "#/components/schemas/InviteRecord", "revoke 200")
inviteRecord := componentSchemaRef(t, doc, "InviteRecord")
assertRequiredFields(t, inviteRecord,
"invite_id", "game_id", "inviter_user_id", "invitee_user_id", "status", "created_at", "expires_at",
)
assertStringEnum(t, inviteRecord, "status", "created", "redeemed", "declined", "revoked", "expired")
// race_name is optional on InviteRecord (set only at redeem time).
require.Contains(t, inviteRecord.Value.Properties, "race_name", "InviteRecord.race_name must be in properties")
}
// TestPublicSpecFreezesMembershipContracts verifies the membership list,
// remove, and block operations.
func TestPublicSpecFreezesMembershipContracts(t *testing.T) {
t.Parallel()
doc := loadPublicSpec(t)
listOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/memberships", http.MethodGet)
require.Equal(t, "listMemberships", listOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, listOp, http.StatusOK), "#/components/schemas/MembershipListResponse", "list 200")
removeOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/memberships/{membership_id}/remove", http.MethodPost)
require.Equal(t, "removeMember", removeOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, removeOp, http.StatusOK), "#/components/schemas/MembershipRecord", "remove 200")
blockOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/memberships/{membership_id}/block", http.MethodPost)
require.Equal(t, "blockMember", blockOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, blockOp, http.StatusOK), "#/components/schemas/MembershipRecord", "block 200")
memberRecord := componentSchemaRef(t, doc, "MembershipRecord")
assertRequiredFields(t, memberRecord,
"membership_id", "game_id", "user_id", "race_name", "status", "joined_at",
)
assertStringEnum(t, memberRecord, "status", "active", "removed", "blocked")
// removed_at is optional.
require.Contains(t, memberRecord.Value.Properties, "removed_at", "MembershipRecord.removed_at must be in properties")
}
// TestPublicSpecFreezesMyListContracts verifies that the three user-facing
// list endpoints have correct operationIds, pagination parameters, and
// response schema references.
func TestPublicSpecFreezesMyListContracts(t *testing.T) {
t.Parallel()
doc := loadPublicSpec(t)
myGamesOp := getOperation(t, doc, "/api/v1/lobby/my/games", http.MethodGet)
require.Equal(t, "listMyGames", myGamesOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, myGamesOp, http.StatusOK), "#/components/schemas/GameListResponse", "my/games 200")
myAppsOp := getOperation(t, doc, "/api/v1/lobby/my/applications", http.MethodGet)
require.Equal(t, "listMyApplications", myAppsOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, myAppsOp, http.StatusOK), "#/components/schemas/MyApplicationListResponse", "my/applications 200")
myInvitesOp := getOperation(t, doc, "/api/v1/lobby/my/invites", http.MethodGet)
require.Equal(t, "listMyInvites", myInvitesOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, myInvitesOp, http.StatusOK), "#/components/schemas/MyInviteListResponse", "my/invites 200")
myAppItem := componentSchemaRef(t, doc, "MyApplicationItem")
assertRequiredFields(t, myAppItem,
"application_id", "game_id", "applicant_user_id", "race_name",
"status", "created_at", "game_name", "game_type",
)
myInviteItem := componentSchemaRef(t, doc, "MyInviteItem")
assertRequiredFields(t, myInviteItem,
"invite_id", "game_id", "inviter_user_id", "invitee_user_id",
"status", "created_at", "expires_at", "game_name", "inviter_name",
)
}
// TestPublicSpecFreezesMyRaceNamesContract verifies that the
// self-service GET endpoint and its response schemas are wired with the
// frozen field set.
func TestPublicSpecFreezesMyRaceNamesContract(t *testing.T) {
t.Parallel()
doc := loadPublicSpec(t)
op := getOperation(t, doc, "/api/v1/lobby/my/race-names", http.MethodGet)
require.Equal(t, "listMyRaceNames", op.OperationID)
assertOperationParameterRefs(t, op, "#/components/parameters/XUserID")
assertSchemaRef(t, responseSchemaRef(t, op, http.StatusOK),
"#/components/schemas/MyRaceNamesResponse", "listMyRaceNames 200")
resp := componentSchemaRef(t, doc, "MyRaceNamesResponse")
assertRequiredFields(t, resp, "registered", "pending", "reservations")
pending := componentSchemaRef(t, doc, "PendingRaceName")
assertRequiredFields(t, pending,
"canonical_key", "race_name", "source_game_id", "eligible_until_ms")
require.Contains(t, pending.Value.Properties, "reserved_at_ms",
"PendingRaceName.reserved_at_ms must be in properties")
reservation := componentSchemaRef(t, doc, "RaceNameReservation")
assertRequiredFields(t, reservation,
"canonical_key", "race_name", "game_id", "game_status")
require.Contains(t, reservation.Value.Properties, "reserved_at_ms",
"RaceNameReservation.reserved_at_ms must be in properties")
}
// TestPublicSpecFreezesErrorExamples verifies that the component response
// examples use the stable error codes defined in README.md.
func TestPublicSpecFreezesErrorExamples(t *testing.T) {
t.Parallel()
doc := loadPublicSpec(t)
cases := []struct {
response string
example string
wantCode string
}{
{"InvalidRequestError", "invalidRequest", "invalid_request"},
{"ForbiddenError", "forbidden", "forbidden"},
{"NotFoundError", "notFound", "subject_not_found"},
{"ConflictError", "conflict", "conflict"},
{"InternalError", "internal", "internal_error"},
{"ServiceUnavailableError", "unavailable", "service_unavailable"},
}
for _, tc := range cases {
tc := tc
t.Run(tc.response, func(t *testing.T) {
t.Parallel()
val := responseExampleValue(t, doc, tc.response, tc.example)
payload, err := json.Marshal(val)
require.NoError(t, err)
var envelope struct {
Error struct {
Code string `json:"code"`
} `json:"error"`
}
require.NoError(t, json.Unmarshal(payload, &envelope))
require.Equal(t, tc.wantCode, envelope.Error.Code)
})
}
// DomainPreconditionError must contain both eligibility_denied and name_taken examples.
eligibilityVal := responseExampleValue(t, doc, "DomainPreconditionError", "eligibilityDenied")
eligibilityPayload, err := json.Marshal(eligibilityVal)
require.NoError(t, err)
require.Contains(t, string(eligibilityPayload), "eligibility_denied")
nameTakenVal := responseExampleValue(t, doc, "DomainPreconditionError", "nameTaken")
nameTakenPayload, err := json.Marshal(nameTakenVal)
require.NoError(t, err)
require.Contains(t, string(nameTakenPayload), "name_taken")
}
// TestInternalSpecFreezesGMReadContracts verifies the GM-facing read
// endpoints: internal game get and internal membership list.
func TestInternalSpecFreezesGMReadContracts(t *testing.T) {
t.Parallel()
doc := loadInternalSpec(t)
getOp := getOperation(t, doc, "/api/v1/internal/games/{game_id}", http.MethodGet)
require.Equal(t, "internalGetGame", getOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, getOp, http.StatusOK), "#/components/schemas/GameRecord", "internalGetGame 200")
listOp := getOperation(t, doc, "/api/v1/internal/games/{game_id}/memberships", http.MethodGet)
require.Equal(t, "internalListMemberships", listOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, listOp, http.StatusOK), "#/components/schemas/MembershipListResponse", "internalListMemberships 200")
}
// TestInternalSpecFreezesAdminMirroredRoutes verifies that a representative
// subset of admin-mirrored routes exist with the expected operationIds and
// response schemas.
func TestInternalSpecFreezesAdminMirroredRoutes(t *testing.T) {
t.Parallel()
doc := loadInternalSpec(t)
createOp := getOperation(t, doc, "/api/v1/lobby/games", http.MethodPost)
require.Equal(t, "adminCreateGame", createOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, createOp, http.StatusCreated), "#/components/schemas/GameRecord", "adminCreateGame 201")
cancelOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/cancel", http.MethodPost)
require.Equal(t, "adminCancelGame", cancelOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, cancelOp, http.StatusOK), "#/components/schemas/GameRecord", "adminCancelGame 200")
approveOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/applications/{application_id}/approve", http.MethodPost)
require.Equal(t, "adminApproveApplication", approveOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, approveOp, http.StatusOK), "#/components/schemas/MembershipRecord", "adminApproveApplication 200")
rejectOp := getOperation(t, doc, "/api/v1/lobby/games/{game_id}/applications/{application_id}/reject", http.MethodPost)
require.Equal(t, "adminRejectApplication", rejectOp.OperationID)
assertSchemaRef(t, responseSchemaRef(t, rejectOp, http.StatusOK), "#/components/schemas/ApplicationRecord", "adminRejectApplication 200")
}
// TestPublicSpecDeclaresAllRegisteredRoutes asserts that every HTTP route
// registered by lobby/internal/api/publichttp is declared in
// public-openapi.yaml. The route table mirrors the mux.HandleFunc calls
// in publichttp/{server,games,applications,invites,memberships,mylists,
// pause_resume,racenames,ready_to_start,start}.go and must be updated
// whenever a new public route is registered.
func TestPublicSpecDeclaresAllRegisteredRoutes(t *testing.T) {
t.Parallel()
doc := loadPublicSpec(t)
for _, r := range publicHTTPRoutes() {
t.Run(r.Method+" "+r.Path, func(t *testing.T) {
t.Parallel()
getOperation(t, doc, r.Path, r.Method)
})
}
}
// TestInternalSpecDeclaresAllRegisteredRoutes asserts that every HTTP route
// registered by lobby/internal/api/internalhttp is declared in
// internal-openapi.yaml. The route table mirrors the mux.HandleFunc calls
// in internalhttp/{server,games,applications,memberships,pause_resume,
// ready_to_start,start}.go and must be updated whenever a new internal
// route is registered.
func TestInternalSpecDeclaresAllRegisteredRoutes(t *testing.T) {
t.Parallel()
doc := loadInternalSpec(t)
for _, r := range internalHTTPRoutes() {
t.Run(r.Method+" "+r.Path, func(t *testing.T) {
t.Parallel()
getOperation(t, doc, r.Path, r.Method)
})
}
}
type httpRoute struct {
Method string
Path string
}
func publicHTTPRoutes() []httpRoute {
return []httpRoute{
{http.MethodGet, "/healthz"},
{http.MethodGet, "/readyz"},
{http.MethodPost, "/api/v1/lobby/games"},
{http.MethodGet, "/api/v1/lobby/games"},
{http.MethodGet, "/api/v1/lobby/games/{game_id}"},
{http.MethodPatch, "/api/v1/lobby/games/{game_id}"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/open-enrollment"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/cancel"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/applications"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/applications/{application_id}/approve"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/applications/{application_id}/reject"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/invites"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/invites/{invite_id}/redeem"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/invites/{invite_id}/decline"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/invites/{invite_id}/revoke"},
{http.MethodGet, "/api/v1/lobby/games/{game_id}/memberships"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/memberships/{membership_id}/remove"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/memberships/{membership_id}/block"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/pause"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/resume"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/ready-to-start"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/start"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/retry-start"},
{http.MethodPost, "/api/v1/lobby/race-names/register"},
{http.MethodGet, "/api/v1/lobby/my/games"},
{http.MethodGet, "/api/v1/lobby/my/applications"},
{http.MethodGet, "/api/v1/lobby/my/invites"},
{http.MethodGet, "/api/v1/lobby/my/race-names"},
}
}
func internalHTTPRoutes() []httpRoute {
return []httpRoute{
{http.MethodGet, "/healthz"},
{http.MethodGet, "/readyz"},
{http.MethodGet, "/api/v1/internal/games/{game_id}"},
{http.MethodGet, "/api/v1/internal/games/{game_id}/memberships"},
{http.MethodPost, "/api/v1/lobby/games"},
{http.MethodGet, "/api/v1/lobby/games"},
{http.MethodGet, "/api/v1/lobby/games/{game_id}"},
{http.MethodPatch, "/api/v1/lobby/games/{game_id}"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/open-enrollment"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/cancel"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/applications/{application_id}/approve"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/applications/{application_id}/reject"},
{http.MethodGet, "/api/v1/lobby/games/{game_id}/memberships"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/memberships/{membership_id}/remove"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/memberships/{membership_id}/block"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/pause"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/resume"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/ready-to-start"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/start"},
{http.MethodPost, "/api/v1/lobby/games/{game_id}/retry-start"},
}
}
// loadPublicSpec loads and validates lobby/api/public-openapi.yaml relative
// to this test file.
func loadPublicSpec(t *testing.T) *openapi3.T {
t.Helper()
return loadSpec(t, filepath.Join("api", "public-openapi.yaml"))
}
// loadInternalSpec loads and validates lobby/api/internal-openapi.yaml
// relative to this test file.
func loadInternalSpec(t *testing.T) *openapi3.T {
t.Helper()
return loadSpec(t, filepath.Join("api", "internal-openapi.yaml"))
}
func loadSpec(t *testing.T, rel string) *openapi3.T {
t.Helper()
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
require.FailNow(t, "runtime.Caller failed")
}
specPath := filepath.Join(filepath.Dir(thisFile), rel)
loader := openapi3.NewLoader()
doc, err := loader.LoadFromFile(specPath)
if err != nil {
require.Failf(t, "test failed", "load spec %s: %v", specPath, err)
}
if doc == nil {
require.Failf(t, "test failed", "load spec %s: returned nil document", specPath)
}
if err := doc.Validate(context.Background()); err != nil {
require.Failf(t, "test failed", "validate spec %s: %v", specPath, err)
}
return doc
}
func getOperation(t *testing.T, doc *openapi3.T, path, method string) *openapi3.Operation {
t.Helper()
if doc.Paths == nil {
require.FailNow(t, "spec is missing paths")
}
pathItem := doc.Paths.Value(path)
if pathItem == nil {
require.Failf(t, "test failed", "spec is missing path %s", path)
}
op := pathItem.GetOperation(method)
if op == nil {
require.Failf(t, "test failed", "spec is missing %s operation for path %s", method, path)
}
return op
}
func requestSchemaRef(t *testing.T, op *openapi3.Operation) *openapi3.SchemaRef {
t.Helper()
if op.RequestBody == nil || op.RequestBody.Value == nil {
require.FailNow(t, "operation is missing request body")
}
mt := op.RequestBody.Value.Content.Get("application/json")
if mt == nil || mt.Schema == nil {
require.FailNow(t, "operation is missing application/json request schema")
}
return mt.Schema
}
func responseSchemaRef(t *testing.T, op *openapi3.Operation, status int) *openapi3.SchemaRef {
t.Helper()
ref := op.Responses.Status(status)
if ref == nil || ref.Value == nil {
require.Failf(t, "test failed", "operation is missing %d response", status)
}
mt := ref.Value.Content.Get("application/json")
if mt == nil || mt.Schema == nil {
require.Failf(t, "test failed", "operation is missing application/json schema for %d response", status)
}
return mt.Schema
}
func componentSchemaRef(t *testing.T, doc *openapi3.T, name string) *openapi3.SchemaRef {
t.Helper()
if doc.Components.Schemas == nil {
require.FailNow(t, "spec is missing component schemas")
}
ref := doc.Components.Schemas[name]
if ref == nil {
require.Failf(t, "test failed", "spec is missing component schema %s", name)
}
return ref
}
func responseExampleValue(t *testing.T, doc *openapi3.T, responseName, exampleName string) any {
t.Helper()
ref := doc.Components.Responses[responseName]
if ref == nil || ref.Value == nil {
require.Failf(t, "test failed", "spec is missing component response %s", responseName)
}
mt := ref.Value.Content.Get("application/json")
if mt == nil {
require.Failf(t, "test failed", "response %s is missing application/json content", responseName)
}
exRef := mt.Examples[exampleName]
if exRef == nil || exRef.Value == nil {
require.Failf(t, "test failed", "response %s is missing example %s", responseName, exampleName)
}
return exRef.Value.Value
}
func assertSchemaRef(t *testing.T, schemaRef *openapi3.SchemaRef, want, name string) {
t.Helper()
require.NotNil(t, schemaRef, "%s schema ref", name)
require.Equal(t, want, schemaRef.Ref, "%s schema ref", name)
}
func assertRequiredFields(t *testing.T, schemaRef *openapi3.SchemaRef, fields ...string) {
t.Helper()
require.NotNil(t, schemaRef)
require.ElementsMatch(t, fields, schemaRef.Value.Required)
}
func assertStringEnum(t *testing.T, schemaRef *openapi3.SchemaRef, property string, values ...string) {
t.Helper()
require.NotNil(t, schemaRef)
propRef := schemaRef.Value.Properties[property]
require.NotNil(t, propRef, "schema property %s", property)
got := make([]string, 0, len(propRef.Value.Enum))
for _, v := range propRef.Value.Enum {
got = append(got, v.(string))
}
require.ElementsMatch(t, values, got)
}
func assertOperationParameterRefs(t *testing.T, op *openapi3.Operation, refs ...string) {
t.Helper()
got := make([]string, 0, len(op.Parameters))
for _, p := range op.Parameters {
got = append(got, p.Ref)
}
require.ElementsMatch(t, refs, got)
}