198 lines
5.9 KiB
Go
198 lines
5.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"galaxy/rtmanager/internal/domain/runtime"
|
|
"galaxy/rtmanager/internal/ports"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// fixedClock is the wall-clock used to build canonical sample records
|
|
// across the handler tests. UTC Sunday 1pm 2026-04-26 is far enough in
|
|
// the future to be obvious in test output.
|
|
var fixedClock = time.Date(2026, 4, 26, 13, 0, 0, 0, time.UTC)
|
|
|
|
// sampleRunningRecord returns a canonical running record used by every
|
|
// happy-path test in this package.
|
|
func sampleRunningRecord(t *testing.T) runtime.RuntimeRecord {
|
|
t.Helper()
|
|
started := fixedClock
|
|
return runtime.RuntimeRecord{
|
|
GameID: "game-test",
|
|
Status: runtime.StatusRunning,
|
|
CurrentContainerID: "container-test",
|
|
CurrentImageRef: "galaxy/game:v1.2.3",
|
|
EngineEndpoint: "http://galaxy-game-game-test:8080",
|
|
StatePath: "/var/lib/galaxy/game-test",
|
|
DockerNetwork: "galaxy-engine",
|
|
StartedAt: &started,
|
|
LastOpAt: fixedClock,
|
|
CreatedAt: fixedClock,
|
|
}
|
|
}
|
|
|
|
// sampleStoppedRecord returns a canonical stopped record useful for
|
|
// cleanup-handler and list-handler tests.
|
|
func sampleStoppedRecord(t *testing.T) runtime.RuntimeRecord {
|
|
t.Helper()
|
|
started := fixedClock
|
|
stopped := fixedClock.Add(time.Minute)
|
|
return runtime.RuntimeRecord{
|
|
GameID: "game-stopped",
|
|
Status: runtime.StatusStopped,
|
|
CurrentContainerID: "container-stopped",
|
|
CurrentImageRef: "galaxy/game:v1.2.3",
|
|
EngineEndpoint: "http://galaxy-game-game-stopped:8080",
|
|
StatePath: "/var/lib/galaxy/game-stopped",
|
|
DockerNetwork: "galaxy-engine",
|
|
StartedAt: &started,
|
|
StoppedAt: &stopped,
|
|
LastOpAt: stopped,
|
|
CreatedAt: fixedClock,
|
|
}
|
|
}
|
|
|
|
// drive routes one request through a full mux configured by Register.
|
|
// It returns the captured ResponseRecorder so tests can assert on
|
|
// status, headers, and body.
|
|
func drive(t *testing.T, deps Dependencies, method, path string, headers http.Header, body io.Reader) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
|
|
mux := http.NewServeMux()
|
|
Register(mux, deps)
|
|
|
|
request := httptest.NewRequest(method, path, body)
|
|
for key, values := range headers {
|
|
for _, value := range values {
|
|
request.Header.Add(key, value)
|
|
}
|
|
}
|
|
|
|
recorder := httptest.NewRecorder()
|
|
mux.ServeHTTP(recorder, request)
|
|
return recorder
|
|
}
|
|
|
|
// decodeRecordResponse asserts that the response carried a 200 with
|
|
// the canonical content type and decodes the record body.
|
|
func decodeRecordResponse(t *testing.T, rec *httptest.ResponseRecorder) runtimeRecordResponse {
|
|
t.Helper()
|
|
require.Equalf(t, http.StatusOK, rec.Code, "expected 200, got body: %s", rec.Body.String())
|
|
require.Equal(t, JSONContentType, rec.Header().Get("Content-Type"))
|
|
|
|
var resp runtimeRecordResponse
|
|
require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
|
|
return resp
|
|
}
|
|
|
|
// decodeErrorBody asserts the canonical error envelope and decodes it.
|
|
func decodeErrorBody(t *testing.T, rec *httptest.ResponseRecorder, wantStatus int) errorBody {
|
|
t.Helper()
|
|
require.Equalf(t, wantStatus, rec.Code, "expected %d, got body: %s", wantStatus, rec.Body.String())
|
|
require.Equal(t, JSONContentType, rec.Header().Get("Content-Type"))
|
|
|
|
var resp errorResponse
|
|
require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
|
|
return resp.Error
|
|
}
|
|
|
|
// fakeRuntimeRecords is an in-memory ports.RuntimeRecordStore used by
|
|
// list / get tests. It is intentionally minimal — services use their
|
|
// own fakes in `internal/service/<op>/service_test.go` and do not
|
|
// share this helper.
|
|
type fakeRuntimeRecords struct {
|
|
mu sync.Mutex
|
|
stored map[string]runtime.RuntimeRecord
|
|
listErr error
|
|
getErr error
|
|
}
|
|
|
|
func newFakeRuntimeRecords() *fakeRuntimeRecords {
|
|
return &fakeRuntimeRecords{stored: map[string]runtime.RuntimeRecord{}}
|
|
}
|
|
|
|
func (s *fakeRuntimeRecords) put(record runtime.RuntimeRecord) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.stored[record.GameID] = record
|
|
}
|
|
|
|
func (s *fakeRuntimeRecords) Get(_ context.Context, gameID string) (runtime.RuntimeRecord, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.getErr != nil {
|
|
return runtime.RuntimeRecord{}, s.getErr
|
|
}
|
|
record, ok := s.stored[gameID]
|
|
if !ok {
|
|
return runtime.RuntimeRecord{}, runtime.ErrNotFound
|
|
}
|
|
return record, nil
|
|
}
|
|
|
|
func (s *fakeRuntimeRecords) Upsert(_ context.Context, _ runtime.RuntimeRecord) error {
|
|
return errors.New("not used in handler tests")
|
|
}
|
|
|
|
func (s *fakeRuntimeRecords) UpdateStatus(_ context.Context, _ ports.UpdateStatusInput) error {
|
|
return errors.New("not used in handler tests")
|
|
}
|
|
|
|
func (s *fakeRuntimeRecords) ListByStatus(_ context.Context, _ runtime.Status) ([]runtime.RuntimeRecord, error) {
|
|
return nil, errors.New("not used in handler tests")
|
|
}
|
|
|
|
func (s *fakeRuntimeRecords) List(_ context.Context) ([]runtime.RuntimeRecord, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.listErr != nil {
|
|
return nil, s.listErr
|
|
}
|
|
if len(s.stored) == 0 {
|
|
return nil, nil
|
|
}
|
|
records := make([]runtime.RuntimeRecord, 0, len(s.stored))
|
|
for _, record := range s.stored {
|
|
records = append(records, record)
|
|
}
|
|
return records, nil
|
|
}
|
|
|
|
// jsonHeaders returns the default headers used by tests that send a
|
|
// JSON body.
|
|
func jsonHeaders() http.Header {
|
|
h := http.Header{}
|
|
h.Set("Content-Type", "application/json")
|
|
return h
|
|
}
|
|
|
|
// withCaller adds the X-Galaxy-Caller header to h and returns h. The
|
|
// helper exists to keep test cases readable when the header is the
|
|
// only difference between two table rows.
|
|
func withCaller(h http.Header, value string) http.Header {
|
|
if h == nil {
|
|
h = http.Header{}
|
|
}
|
|
h.Set(callerHeader, value)
|
|
return h
|
|
}
|
|
|
|
// strReader builds an io.Reader from raw JSON.
|
|
func strReader(raw string) io.Reader {
|
|
return strings.NewReader(raw)
|
|
}
|
|
|
|
// Compile-time assertions that the in-memory fake satisfies the port.
|
|
var _ ports.RuntimeRecordStore = (*fakeRuntimeRecords)(nil)
|