package gamemaster import ( "context" "net/http" "path/filepath" "runtime" "testing" "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) var expectedInternalOperationIDs = []string{ "internalHealthz", "internalReadyz", "internalRegisterRuntime", "internalGetRuntime", "internalListRuntimes", "internalForceNextTurn", "internalStopRuntime", "internalPatchRuntime", "internalBanishRace", "internalInvalidateMemberships", "internalGameLiveness", "internalListEngineVersions", "internalCreateEngineVersion", "internalGetEngineVersion", "internalUpdateEngineVersion", "internalDeprecateEngineVersion", "internalResolveEngineVersionImageRef", "internalExecuteCommands", "internalPutOrders", "internalGetReport", } // gmOwnedClosedSchemas lists every component schema for which Game Master // owns the wire shape and therefore must reject unknown fields. The list // is curated; the matching test fails if any schema in this list opens up. var gmOwnedClosedSchemas = []string{ "ProbeResponse", "LivenessResponse", "ImageRefResponse", "RegisterRuntimeMember", "RegisterRuntimeRequest", "RuntimeRecord", "RuntimeListResponse", "StopRuntimeRequest", "PatchRuntimeRequest", "EngineVersion", "EngineVersionListResponse", "CreateEngineVersionRequest", "UpdateEngineVersionRequest", "ErrorResponse", "ErrorBody", } // engineOwnedPassthroughSchemas lists every component schema that forwards // engine-owned payloads verbatim and therefore deliberately uses // `additionalProperties: true`. The matching test fails if any schema in // this list closes up. var engineOwnedPassthroughSchemas = []string{ "ExecuteCommandsRequest", "ExecuteCommandsResponse", "PutOrdersRequest", "PutOrdersResponse", "ReportResponse", } // 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) } // TestInternalSpecHasAllOperationIDs verifies that the spec declares every // operationId required by gamemaster/PLAN.md Stage 06 and no extras. Adding // a new operation requires updating expectedInternalOperationIDs in the same // patch as the spec change. func TestInternalSpecHasAllOperationIDs(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) got := make([]string, 0, len(expectedInternalOperationIDs)) for _, pathItem := range doc.Paths.Map() { for _, op := range pathItem.Operations() { require.NotEmpty(t, op.OperationID, "every operation must declare a non-empty operationId") got = append(got, op.OperationID) } } require.ElementsMatch(t, expectedInternalOperationIDs, got) } // TestInternalSpecRegisterRuntime verifies the register-runtime contract // used by Game Lobby after a successful container start. func TestInternalSpecRegisterRuntime(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) op := getOperation(t, doc, "/api/v1/internal/games/{game_id}/register-runtime", http.MethodPost) require.Equal(t, "internalRegisterRuntime", op.OperationID) assertOperationParameterRefs(t, op, "#/components/parameters/GameIDPath") assertSchemaRef(t, requestSchemaRef(t, op), "#/components/schemas/RegisterRuntimeRequest", "internalRegisterRuntime request") assertSchemaRef(t, responseSchemaRef(t, op, http.StatusOK), "#/components/schemas/RuntimeRecord", "internalRegisterRuntime 200") assertResponseRef(t, op, http.StatusBadRequest, "#/components/responses/InvalidRequestError") assertResponseRef(t, op, http.StatusNotFound, "#/components/responses/EngineVersionNotFoundError") assertResponseRef(t, op, http.StatusConflict, "#/components/responses/ConflictError") assertResponseRef(t, op, http.StatusBadGateway, "#/components/responses/EngineUnreachableError") assertResponseRef(t, op, http.StatusInternalServerError, "#/components/responses/InternalError") assertResponseRef(t, op, http.StatusServiceUnavailable, "#/components/responses/ServiceUnavailableError") req := componentSchemaRef(t, doc, "RegisterRuntimeRequest") assertRequiredFields(t, req, "engine_endpoint", "members", "target_engine_version", "turn_schedule") member := componentSchemaRef(t, doc, "RegisterRuntimeMember") assertRequiredFields(t, member, "user_id", "race_name") } // TestInternalSpecGetRuntime verifies the runtime read contract. func TestInternalSpecGetRuntime(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) op := getOperation(t, doc, "/api/v1/internal/runtimes/{game_id}", http.MethodGet) require.Equal(t, "internalGetRuntime", op.OperationID) assertOperationParameterRefs(t, op, "#/components/parameters/GameIDPath") assertSchemaRef(t, responseSchemaRef(t, op, http.StatusOK), "#/components/schemas/RuntimeRecord", "internalGetRuntime 200") assertResponseRef(t, op, http.StatusNotFound, "#/components/responses/NotFoundError") assertResponseRef(t, op, http.StatusInternalServerError, "#/components/responses/InternalError") } // TestInternalSpecListRuntimes verifies the list contract and the optional // status query parameter. func TestInternalSpecListRuntimes(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) op := getOperation(t, doc, "/api/v1/internal/runtimes", http.MethodGet) require.Equal(t, "internalListRuntimes", op.OperationID) assertOperationParameterRefs(t, op, "#/components/parameters/RuntimeStatusQuery") assertSchemaRef(t, responseSchemaRef(t, op, http.StatusOK), "#/components/schemas/RuntimeListResponse", "internalListRuntimes 200") assertResponseRef(t, op, http.StatusBadRequest, "#/components/responses/InvalidRequestError") assertResponseRef(t, op, http.StatusInternalServerError, "#/components/responses/InternalError") param := componentParameterRef(t, doc, "RuntimeStatusQuery") require.Equal(t, "status", param.Value.Name) require.Equal(t, "query", param.Value.In) require.False(t, param.Value.Required, "status filter must be optional") require.Equal(t, "#/components/schemas/RuntimeStatus", param.Value.Schema.Ref, "status filter schema must reference RuntimeStatus") } // TestInternalSpecForceNextTurn verifies the force-next-turn admin contract. func TestInternalSpecForceNextTurn(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) op := getOperation(t, doc, "/api/v1/internal/runtimes/{game_id}/force-next-turn", http.MethodPost) require.Equal(t, "internalForceNextTurn", op.OperationID) require.Nil(t, op.RequestBody, "internalForceNextTurn must have no request body") assertOperationParameterRefs(t, op, "#/components/parameters/GameIDPath") assertSchemaRef(t, responseSchemaRef(t, op, http.StatusOK), "#/components/schemas/RuntimeRecord", "internalForceNextTurn 200") assertResponseRef(t, op, http.StatusNotFound, "#/components/responses/NotFoundError") assertResponseRef(t, op, http.StatusConflict, "#/components/responses/ConflictError") assertResponseRef(t, op, http.StatusBadGateway, "#/components/responses/EngineUnreachableError") assertResponseRef(t, op, http.StatusInternalServerError, "#/components/responses/InternalError") } // TestInternalSpecStopRuntime verifies the stop admin contract. func TestInternalSpecStopRuntime(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) op := getOperation(t, doc, "/api/v1/internal/runtimes/{game_id}/stop", http.MethodPost) require.Equal(t, "internalStopRuntime", op.OperationID) assertOperationParameterRefs(t, op, "#/components/parameters/GameIDPath") assertSchemaRef(t, requestSchemaRef(t, op), "#/components/schemas/StopRuntimeRequest", "internalStopRuntime request") assertSchemaRef(t, responseSchemaRef(t, op, http.StatusOK), "#/components/schemas/RuntimeRecord", "internalStopRuntime 200") assertResponseRef(t, op, http.StatusBadRequest, "#/components/responses/InvalidRequestError") assertResponseRef(t, op, http.StatusNotFound, "#/components/responses/NotFoundError") assertResponseRef(t, op, http.StatusInternalServerError, "#/components/responses/InternalError") assertResponseRef(t, op, http.StatusServiceUnavailable, "#/components/responses/ServiceUnavailableError") req := componentSchemaRef(t, doc, "StopRuntimeRequest") assertRequiredFields(t, req, "reason") reason := req.Value.Properties["reason"] require.NotNil(t, reason) require.Equal(t, "#/components/schemas/StopReason", reason.Ref, "StopRuntimeRequest.reason must reference StopReason") } // TestInternalSpecPatchRuntime verifies the patch admin contract. func TestInternalSpecPatchRuntime(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) op := getOperation(t, doc, "/api/v1/internal/runtimes/{game_id}/patch", http.MethodPost) require.Equal(t, "internalPatchRuntime", op.OperationID) assertOperationParameterRefs(t, op, "#/components/parameters/GameIDPath") assertSchemaRef(t, requestSchemaRef(t, op), "#/components/schemas/PatchRuntimeRequest", "internalPatchRuntime request") assertSchemaRef(t, responseSchemaRef(t, op, http.StatusOK), "#/components/schemas/RuntimeRecord", "internalPatchRuntime 200") assertResponseRef(t, op, http.StatusBadRequest, "#/components/responses/InvalidRequestError") assertResponseRef(t, op, http.StatusNotFound, "#/components/responses/NotFoundError") assertResponseRef(t, op, http.StatusConflict, "#/components/responses/ConflictError") assertResponseRef(t, op, http.StatusInternalServerError, "#/components/responses/InternalError") assertResponseRef(t, op, http.StatusServiceUnavailable, "#/components/responses/ServiceUnavailableError") req := componentSchemaRef(t, doc, "PatchRuntimeRequest") assertRequiredFields(t, req, "version") } // TestInternalSpecBanishRace verifies the engine-side race banish contract // called by Game Lobby after a permanent membership removal. func TestInternalSpecBanishRace(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) op := getOperation(t, doc, "/api/v1/internal/games/{game_id}/race/{race_name}/banish", http.MethodPost) require.Equal(t, "internalBanishRace", op.OperationID) require.Nil(t, op.RequestBody, "internalBanishRace must have no request body; the race_name is on the path") assertOperationParameterRefs(t, op, "#/components/parameters/GameIDPath", "#/components/parameters/RaceNamePath", ) assertNoContentResponse(t, op, http.StatusNoContent) assertResponseRef(t, op, http.StatusNotFound, "#/components/responses/NotFoundError") assertResponseRef(t, op, http.StatusBadGateway, "#/components/responses/EngineUnreachableError") assertResponseRef(t, op, http.StatusInternalServerError, "#/components/responses/InternalError") } // TestInternalSpecInvalidateMemberships verifies the membership cache hook // called by Game Lobby on every roster mutation. func TestInternalSpecInvalidateMemberships(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) op := getOperation(t, doc, "/api/v1/internal/games/{game_id}/memberships/invalidate", http.MethodPost) require.Equal(t, "internalInvalidateMemberships", op.OperationID) require.Nil(t, op.RequestBody) assertOperationParameterRefs(t, op, "#/components/parameters/GameIDPath") assertNoContentResponse(t, op, http.StatusNoContent) assertResponseRef(t, op, http.StatusNotFound, "#/components/responses/NotFoundError") assertResponseRef(t, op, http.StatusInternalServerError, "#/components/responses/InternalError") } // TestInternalSpecGameLiveness verifies the liveness reply used by Lobby's // resume flow. func TestInternalSpecGameLiveness(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) op := getOperation(t, doc, "/api/v1/internal/games/{game_id}/liveness", http.MethodGet) require.Equal(t, "internalGameLiveness", op.OperationID) assertOperationParameterRefs(t, op, "#/components/parameters/GameIDPath") assertSchemaRef(t, responseSchemaRef(t, op, http.StatusOK), "#/components/schemas/LivenessResponse", "internalGameLiveness 200") assertResponseRef(t, op, http.StatusInternalServerError, "#/components/responses/InternalError") resp := componentSchemaRef(t, doc, "LivenessResponse") assertRequiredFields(t, resp, "ready", "status") status := resp.Value.Properties["status"] require.NotNil(t, status) require.Equal(t, "#/components/schemas/RuntimeStatus", status.Ref, "LivenessResponse.status must reference RuntimeStatus") } // TestInternalSpecEngineVersionsCRUD verifies all six engine version // registry operations: list, create, get, update, deprecate, resolve. func TestInternalSpecEngineVersionsCRUD(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) listOp := getOperation(t, doc, "/api/v1/internal/engine-versions", http.MethodGet) require.Equal(t, "internalListEngineVersions", listOp.OperationID) assertOperationParameterRefs(t, listOp, "#/components/parameters/EngineVersionStatusQuery") assertSchemaRef(t, responseSchemaRef(t, listOp, http.StatusOK), "#/components/schemas/EngineVersionListResponse", "internalListEngineVersions 200") createOp := getOperation(t, doc, "/api/v1/internal/engine-versions", http.MethodPost) require.Equal(t, "internalCreateEngineVersion", createOp.OperationID) assertSchemaRef(t, requestSchemaRef(t, createOp), "#/components/schemas/CreateEngineVersionRequest", "create request") assertSchemaRef(t, responseSchemaRef(t, createOp, http.StatusCreated), "#/components/schemas/EngineVersion", "internalCreateEngineVersion 201") assertResponseRef(t, createOp, http.StatusConflict, "#/components/responses/ConflictError") getOp := getOperation(t, doc, "/api/v1/internal/engine-versions/{version}", http.MethodGet) require.Equal(t, "internalGetEngineVersion", getOp.OperationID) assertOperationParameterRefs(t, getOp, "#/components/parameters/VersionPath") assertSchemaRef(t, responseSchemaRef(t, getOp, http.StatusOK), "#/components/schemas/EngineVersion", "internalGetEngineVersion 200") assertResponseRef(t, getOp, http.StatusNotFound, "#/components/responses/NotFoundError") updateOp := getOperation(t, doc, "/api/v1/internal/engine-versions/{version}", http.MethodPatch) require.Equal(t, "internalUpdateEngineVersion", updateOp.OperationID) assertOperationParameterRefs(t, updateOp, "#/components/parameters/VersionPath") assertSchemaRef(t, requestSchemaRef(t, updateOp), "#/components/schemas/UpdateEngineVersionRequest", "update request") assertSchemaRef(t, responseSchemaRef(t, updateOp, http.StatusOK), "#/components/schemas/EngineVersion", "internalUpdateEngineVersion 200") deprecateOp := getOperation(t, doc, "/api/v1/internal/engine-versions/{version}", http.MethodDelete) require.Equal(t, "internalDeprecateEngineVersion", deprecateOp.OperationID) assertNoContentResponse(t, deprecateOp, http.StatusNoContent) assertResponseRef(t, deprecateOp, http.StatusConflict, "#/components/responses/EngineVersionInUseError") resolveOp := getOperation(t, doc, "/api/v1/internal/engine-versions/{version}/image-ref", http.MethodGet) require.Equal(t, "internalResolveEngineVersionImageRef", resolveOp.OperationID) assertOperationParameterRefs(t, resolveOp, "#/components/parameters/VersionPath") assertSchemaRef(t, responseSchemaRef(t, resolveOp, http.StatusOK), "#/components/schemas/ImageRefResponse", "internalResolveEngineVersionImageRef 200") assertResponseRef(t, resolveOp, http.StatusNotFound, "#/components/responses/EngineVersionNotFoundError") createReq := componentSchemaRef(t, doc, "CreateEngineVersionRequest") assertRequiredFields(t, createReq, "version", "image_ref") } // TestInternalSpecHotPathContracts verifies the three Edge Gateway hot-path // operations and their pass-through schema treatment. func TestInternalSpecHotPathContracts(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) cmdOp := getOperation(t, doc, "/api/v1/internal/games/{game_id}/commands", http.MethodPost) require.Equal(t, "internalExecuteCommands", cmdOp.OperationID) assertOperationParameterRefs(t, cmdOp, "#/components/parameters/GameIDPath", "#/components/parameters/XUserIDHeader", ) assertSchemaRef(t, requestSchemaRef(t, cmdOp), "#/components/schemas/ExecuteCommandsRequest", "internalExecuteCommands request") assertSchemaRef(t, responseSchemaRef(t, cmdOp, http.StatusOK), "#/components/schemas/ExecuteCommandsResponse", "internalExecuteCommands 200") assertResponseRef(t, cmdOp, http.StatusForbidden, "#/components/responses/ForbiddenError") assertResponseRef(t, cmdOp, http.StatusBadGateway, "#/components/responses/EngineUnreachableError") orderOp := getOperation(t, doc, "/api/v1/internal/games/{game_id}/orders", http.MethodPost) require.Equal(t, "internalPutOrders", orderOp.OperationID) assertOperationParameterRefs(t, orderOp, "#/components/parameters/GameIDPath", "#/components/parameters/XUserIDHeader", ) assertSchemaRef(t, requestSchemaRef(t, orderOp), "#/components/schemas/PutOrdersRequest", "internalPutOrders request") assertSchemaRef(t, responseSchemaRef(t, orderOp, http.StatusOK), "#/components/schemas/PutOrdersResponse", "internalPutOrders 200") reportOp := getOperation(t, doc, "/api/v1/internal/games/{game_id}/reports/{turn}", http.MethodGet) require.Equal(t, "internalGetReport", reportOp.OperationID) assertOperationParameterRefs(t, reportOp, "#/components/parameters/GameIDPath", "#/components/parameters/TurnPath", "#/components/parameters/XUserIDHeader", ) require.Nil(t, reportOp.RequestBody, "internalGetReport must have no request body") assertSchemaRef(t, responseSchemaRef(t, reportOp, http.StatusOK), "#/components/schemas/ReportResponse", "internalGetReport 200") assertResponseRef(t, reportOp, http.StatusForbidden, "#/components/responses/ForbiddenError") assertResponseRef(t, reportOp, http.StatusBadGateway, "#/components/responses/EngineUnreachableError") } // TestInternalSpecProbes verifies the two probe operations. func TestInternalSpecProbes(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) for _, path := range []string{"/healthz", "/readyz"} { op := getOperation(t, doc, path, http.MethodGet) assertSchemaRef(t, responseSchemaRef(t, op, http.StatusOK), "#/components/schemas/ProbeResponse", op.OperationID+" 200") assertResponseRef(t, op, http.StatusServiceUnavailable, "#/components/responses/ServiceUnavailableError") } healthz := getOperation(t, doc, "/healthz", http.MethodGet) require.Equal(t, "internalHealthz", healthz.OperationID) readyz := getOperation(t, doc, "/readyz", http.MethodGet) require.Equal(t, "internalReadyz", readyz.OperationID) } // TestInternalSpecRuntimeRecordSchema verifies that RuntimeRecord declares // the required field set documented in gamemaster/README.md §Persistence // Layout, with the optional lifecycle timestamps present in properties. func TestInternalSpecRuntimeRecordSchema(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) schema := componentSchemaRef(t, doc, "RuntimeRecord") assertRequiredFields(t, schema, "game_id", "runtime_status", "engine_endpoint", "current_image_ref", "current_engine_version", "turn_schedule", "current_turn", "next_generation_at", "skip_next_tick", "engine_health_summary", "created_at", "updated_at", ) for _, optional := range []string{"started_at", "stopped_at", "finished_at"} { require.Contains(t, schema.Value.Properties, optional, "RuntimeRecord.%s must be present in properties", optional) } runtimeStatus := schema.Value.Properties["runtime_status"] require.NotNil(t, runtimeStatus) require.Equal(t, "#/components/schemas/RuntimeStatus", runtimeStatus.Ref, "RuntimeRecord.runtime_status must reference RuntimeStatus") } // TestInternalSpecEngineVersionSchema verifies the EngineVersion schema's // required field set and the deliberate `additionalProperties: true` on // the free-form `options` field. func TestInternalSpecEngineVersionSchema(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) schema := componentSchemaRef(t, doc, "EngineVersion") assertRequiredFields(t, schema, "version", "image_ref", "options", "status", "created_at", "updated_at") options := schema.Value.Properties["options"] require.NotNil(t, options) require.NotNil(t, options.Value.AdditionalProperties.Has, "EngineVersion.options must declare additionalProperties explicitly") require.True(t, *options.Value.AdditionalProperties.Has, "EngineVersion.options is free-form jsonb and must keep additionalProperties: true") status := schema.Value.Properties["status"] require.NotNil(t, status) require.Equal(t, "#/components/schemas/EngineVersionStatus", status.Ref, "EngineVersion.status must reference EngineVersionStatus") } // TestInternalSpecRuntimeStatusEnum verifies the seven-value RuntimeStatus // enum from gamemaster/README.md §Scope. func TestInternalSpecRuntimeStatusEnum(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) schema := componentSchemaRef(t, doc, "RuntimeStatus") got := stringEnumValues(t, schema) require.ElementsMatch(t, []string{ "starting", "running", "generation_in_progress", "generation_failed", "stopped", "engine_unreachable", "finished", }, got) } // TestInternalSpecEngineVersionStatusEnum verifies the EngineVersionStatus // enum from gamemaster/README.md §Engine Version Registry. func TestInternalSpecEngineVersionStatusEnum(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) schema := componentSchemaRef(t, doc, "EngineVersionStatus") got := stringEnumValues(t, schema) require.ElementsMatch(t, []string{"active", "deprecated"}, got) } // TestInternalSpecStopReasonEnum verifies the StopReason enum from // gamemaster/README.md §Lifecycles -> Stop. func TestInternalSpecStopReasonEnum(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) schema := componentSchemaRef(t, doc, "StopReason") got := stringEnumValues(t, schema) require.ElementsMatch(t, []string{"admin_request", "finished", "timeout"}, got) } // TestInternalSpecErrorEnvelope verifies the error envelope shape, which // must be identical to the Lobby and Runtime Manager envelopes. func TestInternalSpecErrorEnvelope(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) envelope := componentSchemaRef(t, doc, "ErrorResponse") assertRequiredFields(t, envelope, "error") assertAdditionalPropertiesFalse(t, envelope, "ErrorResponse") errRef := envelope.Value.Properties["error"] require.NotNil(t, errRef) require.Equal(t, "#/components/schemas/ErrorBody", errRef.Ref, "ErrorResponse.error must reference ErrorBody") body := componentSchemaRef(t, doc, "ErrorBody") assertRequiredFields(t, body, "code", "message") assertAdditionalPropertiesFalse(t, body, "ErrorBody") } // TestInternalSpecGMOwnedSchemasAreClosed verifies that every schema for // which Game Master owns the wire shape rejects unknown fields. func TestInternalSpecGMOwnedSchemasAreClosed(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) for _, name := range gmOwnedClosedSchemas { name := name t.Run(name, func(t *testing.T) { t.Parallel() schema := componentSchemaRef(t, doc, name) assertAdditionalPropertiesFalse(t, schema, name) }) } } // TestInternalSpecHotPathSchemasArePassthrough verifies that every engine // pass-through schema deliberately keeps `additionalProperties: true`. // The matching test guards against a refactor that closes these by mistake. func TestInternalSpecHotPathSchemasArePassthrough(t *testing.T) { t.Parallel() doc := loadInternalSpec(t) for _, name := range engineOwnedPassthroughSchemas { name := name t.Run(name, func(t *testing.T) { t.Parallel() schema := componentSchemaRef(t, doc, name) require.NotNil(t, schema.Value.AdditionalProperties.Has, "%s must declare additionalProperties explicitly", name) require.True(t, *schema.Value.AdditionalProperties.Has, "%s must keep additionalProperties: true (engine pass-through)", name) }) } } // loadInternalSpec loads and validates gamemaster/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 componentParameterRef(t *testing.T, doc *openapi3.T, name string) *openapi3.ParameterRef { t.Helper() if doc.Components.Parameters == nil { require.FailNow(t, "spec is missing component parameters") } ref := doc.Components.Parameters[name] if ref == nil { require.Failf(t, "test failed", "spec is missing component parameter %s", name) } return ref } 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 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) } func assertResponseRef(t *testing.T, op *openapi3.Operation, status int, want string) { t.Helper() ref := op.Responses.Status(status) if ref == nil { require.Failf(t, "test failed", "operation %s is missing %d response", op.OperationID, status) } require.Equal(t, want, ref.Ref, "operation %s response %d must reference %s", op.OperationID, status, want) } func assertNoContentResponse(t *testing.T, op *openapi3.Operation, status int) { t.Helper() ref := op.Responses.Status(status) if ref == nil || ref.Value == nil { require.Failf(t, "test failed", "operation %s is missing %d response", op.OperationID, status) } require.Empty(t, ref.Value.Content, "operation %s response %d must have no content body", op.OperationID, status) } func assertAdditionalPropertiesFalse(t *testing.T, schemaRef *openapi3.SchemaRef, name string) { t.Helper() require.NotNil(t, schemaRef.Value.AdditionalProperties.Has, "%s must declare additionalProperties explicitly", name) require.False(t, *schemaRef.Value.AdditionalProperties.Has, "%s must reject unknown fields (additionalProperties: false)", name) } func stringEnumValues(t *testing.T, schemaRef *openapi3.SchemaRef) []string { t.Helper() require.NotNil(t, schemaRef) got := make([]string, 0, len(schemaRef.Value.Enum)) for _, value := range schemaRef.Value.Enum { s, ok := value.(string) require.True(t, ok, "enum value %v is not a string", value) got = append(got, s) } return got }