package internalhttp import ( "bytes" "context" "errors" "io" "net/http" "net/http/httptest" "path/filepath" "runtime" "strings" "sync" "testing" "time" "galaxy/rtmanager/internal/api/internalhttp/handlers" domainruntime "galaxy/rtmanager/internal/domain/runtime" "galaxy/rtmanager/internal/ports" "galaxy/rtmanager/internal/service/cleanupcontainer" "galaxy/rtmanager/internal/service/patchruntime" "galaxy/rtmanager/internal/service/restartruntime" "galaxy/rtmanager/internal/service/startruntime" "galaxy/rtmanager/internal/service/stopruntime" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" "github.com/getkin/kin-openapi/routers" "github.com/getkin/kin-openapi/routers/legacy" "github.com/stretchr/testify/require" ) // TestInternalRESTConformance loads the OpenAPI specification, drives // every runtime operation against the live internal HTTP listener // backed by stub services, and validates each response body against // the spec via `openapi3filter.ValidateResponse`. The test catches // drift between the wire shape produced by the handler layer and the // frozen contract; failure-path response shapes are validated by the // per-handler tests in `handlers/_test.go`. func TestInternalRESTConformance(t *testing.T) { t.Parallel() doc := loadConformanceSpec(t) router, err := legacy.NewRouter(doc) require.NoError(t, err) deps := newConformanceDeps(t) server, err := NewServer(newConformanceConfig(), Dependencies{ Logger: nil, Telemetry: nil, Readiness: nil, RuntimeRecords: deps.records, StartRuntime: deps.start, StopRuntime: deps.stop, RestartRuntime: deps.restart, PatchRuntime: deps.patch, CleanupContainer: deps.cleanup, }) require.NoError(t, err) cases := []conformanceCase{ { name: "internalListRuntimes", method: http.MethodGet, path: "/api/v1/internal/runtimes", }, { name: "internalGetRuntime", method: http.MethodGet, path: "/api/v1/internal/runtimes/" + conformanceGameID, }, { name: "internalStartRuntime", method: http.MethodPost, path: "/api/v1/internal/runtimes/" + conformanceGameID + "/start", contentType: "application/json", body: `{"image_ref":"galaxy/game:v1.2.3"}`, }, { name: "internalStopRuntime", method: http.MethodPost, path: "/api/v1/internal/runtimes/" + conformanceGameID + "/stop", contentType: "application/json", body: `{"reason":"admin_request"}`, }, { name: "internalRestartRuntime", method: http.MethodPost, path: "/api/v1/internal/runtimes/" + conformanceGameID + "/restart", }, { name: "internalPatchRuntime", method: http.MethodPost, path: "/api/v1/internal/runtimes/" + conformanceGameID + "/patch", contentType: "application/json", body: `{"image_ref":"galaxy/game:v1.2.4"}`, }, { name: "internalCleanupRuntimeContainer", method: http.MethodDelete, path: "/api/v1/internal/runtimes/" + conformanceGameID + "/container", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() runConformanceCase(t, server.handler, router, tc) }) } } // conformanceGameID is the path variable used for every per-game // conformance request. const conformanceGameID = "game-conformance" // conformanceServerURL mirrors the canonical `servers[0].url` entry in // `rtmanager/api/internal-openapi.yaml`. The legacy router matches // requests against this prefix; updating the spec's server URL // requires updating this constant. const conformanceServerURL = "http://localhost:8096" // conformanceCase describes one request the conformance test drives. type conformanceCase struct { name string method string path string contentType string body string } func runConformanceCase(t *testing.T, handler http.Handler, router routers.Router, tc conformanceCase) { t.Helper() // Drive the handler with the path-only form so the listener's // http.ServeMux matches the registered routes (which use raw paths, // without the OpenAPI server URL prefix). var bodyReader io.Reader if tc.body != "" { bodyReader = strings.NewReader(tc.body) } request := httptest.NewRequest(tc.method, tc.path, bodyReader) if tc.contentType != "" { request.Header.Set("Content-Type", tc.contentType) } request.Header.Set("X-Galaxy-Caller", "admin") recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) require.Equalf(t, http.StatusOK, recorder.Code, "operation %s returned %d: %s", tc.name, recorder.Code, recorder.Body.String()) // kin-openapi's legacy router requires the request URL to match a // `servers[].url` entry; rebuild the validation request with the // canonical local server URL declared in the spec. validationURL := conformanceServerURL + tc.path validationRequest := httptest.NewRequest(tc.method, validationURL, bodyReaderFor(tc.body)) if tc.contentType != "" { validationRequest.Header.Set("Content-Type", tc.contentType) } validationRequest.Header.Set("X-Galaxy-Caller", "admin") route, pathParams, err := router.FindRoute(validationRequest) require.NoError(t, err) requestInput := &openapi3filter.RequestValidationInput{ Request: validationRequest, PathParams: pathParams, Route: route, Options: &openapi3filter.Options{ IncludeResponseStatus: true, }, } require.NoError(t, openapi3filter.ValidateRequest(context.Background(), requestInput)) responseInput := &openapi3filter.ResponseValidationInput{ RequestValidationInput: requestInput, Status: recorder.Code, Header: recorder.Header(), Options: &openapi3filter.Options{ IncludeResponseStatus: true, }, } responseInput.SetBodyBytes(recorder.Body.Bytes()) require.NoError(t, openapi3filter.ValidateResponse(context.Background(), responseInput)) } func loadConformanceSpec(t *testing.T) *openapi3.T { t.Helper() _, thisFile, _, ok := runtime.Caller(0) require.True(t, ok) specPath := filepath.Join(filepath.Dir(thisFile), "..", "..", "..", "api", "internal-openapi.yaml") loader := openapi3.NewLoader() doc, err := loader.LoadFromFile(specPath) require.NoError(t, err) require.NoError(t, doc.Validate(context.Background())) return doc } func bodyReaderFor(raw string) io.Reader { if raw == "" { return http.NoBody } return bytes.NewBufferString(raw) } // conformanceDeps groups the stub collaborators handed to the listener. type conformanceDeps struct { records *conformanceRecords start *conformanceStart stop *conformanceStop restart *conformanceRestart patch *conformancePatch cleanup *conformanceCleanup } func newConformanceDeps(t *testing.T) *conformanceDeps { t.Helper() return &conformanceDeps{ records: newConformanceRecords(), start: &conformanceStart{}, stop: &conformanceStop{}, restart: &conformanceRestart{}, patch: &conformancePatch{}, cleanup: &conformanceCleanup{}, } } func newConformanceConfig() Config { return Config{ Addr: ":0", ReadHeaderTimeout: time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, IdleTimeout: time.Second, } } // conformanceRecord builds a canonical running record used by every // stub service. func conformanceRecord() domainruntime.RuntimeRecord { started := time.Date(2026, 4, 26, 13, 0, 0, 0, time.UTC) return domainruntime.RuntimeRecord{ GameID: conformanceGameID, Status: domainruntime.StatusRunning, CurrentContainerID: "container-conformance", CurrentImageRef: "galaxy/game:v1.2.3", EngineEndpoint: "http://galaxy-game-" + conformanceGameID + ":8080", StatePath: "/var/lib/galaxy/" + conformanceGameID, DockerNetwork: "galaxy-engine", StartedAt: &started, LastOpAt: started, CreatedAt: started, } } // conformanceRecords is an in-memory record store seeded with one // canonical record so the get / list endpoints have something to // return. type conformanceRecords struct { mu sync.Mutex stored map[string]domainruntime.RuntimeRecord } func newConformanceRecords() *conformanceRecords { return &conformanceRecords{ stored: map[string]domainruntime.RuntimeRecord{ conformanceGameID: conformanceRecord(), }, } } func (s *conformanceRecords) Get(_ context.Context, gameID string) (domainruntime.RuntimeRecord, error) { s.mu.Lock() defer s.mu.Unlock() record, ok := s.stored[gameID] if !ok { return domainruntime.RuntimeRecord{}, domainruntime.ErrNotFound } return record, nil } func (s *conformanceRecords) Upsert(_ context.Context, _ domainruntime.RuntimeRecord) error { return errors.New("not used in conformance test") } func (s *conformanceRecords) UpdateStatus(_ context.Context, _ ports.UpdateStatusInput) error { return errors.New("not used in conformance test") } func (s *conformanceRecords) ListByStatus(_ context.Context, _ domainruntime.Status) ([]domainruntime.RuntimeRecord, error) { return nil, errors.New("not used in conformance test") } func (s *conformanceRecords) List(_ context.Context) ([]domainruntime.RuntimeRecord, error) { s.mu.Lock() defer s.mu.Unlock() out := make([]domainruntime.RuntimeRecord, 0, len(s.stored)) for _, record := range s.stored { out = append(out, record) } return out, nil } // conformanceStart is the stub StartService used by the conformance // test. Every Handle call returns the canonical record. type conformanceStart struct{} func (s *conformanceStart) Handle(_ context.Context, _ startruntime.Input) (startruntime.Result, error) { return startruntime.Result{ Record: conformanceRecord(), Outcome: "success", }, nil } type conformanceStop struct{} func (s *conformanceStop) Handle(_ context.Context, _ stopruntime.Input) (stopruntime.Result, error) { rec := conformanceRecord() rec.Status = domainruntime.StatusStopped stopped := rec.LastOpAt.Add(time.Second) rec.StoppedAt = &stopped rec.LastOpAt = stopped return stopruntime.Result{Record: rec, Outcome: "success"}, nil } type conformanceRestart struct{} func (s *conformanceRestart) Handle(_ context.Context, _ restartruntime.Input) (restartruntime.Result, error) { return restartruntime.Result{Record: conformanceRecord(), Outcome: "success"}, nil } type conformancePatch struct{} func (s *conformancePatch) Handle(_ context.Context, in patchruntime.Input) (patchruntime.Result, error) { rec := conformanceRecord() if in.NewImageRef != "" { rec.CurrentImageRef = in.NewImageRef } return patchruntime.Result{Record: rec, Outcome: "success"}, nil } type conformanceCleanup struct{} func (s *conformanceCleanup) Handle(_ context.Context, _ cleanupcontainer.Input) (cleanupcontainer.Result, error) { rec := conformanceRecord() rec.Status = domainruntime.StatusRemoved rec.CurrentContainerID = "" removed := rec.LastOpAt.Add(time.Minute) rec.RemovedAt = &removed rec.LastOpAt = removed return cleanupcontainer.Result{Record: rec, Outcome: "success"}, nil } // Compile-time guards: the stubs must satisfy the handler-level // service ports plus ports.RuntimeRecordStore so the listener accepts // them. var ( _ handlers.StartService = (*conformanceStart)(nil) _ handlers.StopService = (*conformanceStop)(nil) _ handlers.RestartService = (*conformanceRestart)(nil) _ handlers.PatchService = (*conformancePatch)(nil) _ handlers.CleanupService = (*conformanceCleanup)(nil) _ ports.RuntimeRecordStore = (*conformanceRecords)(nil) )