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) }