Files
galaxy-game/gamemaster/internal/adapters/rtmclient/client_test.go
T
2026-05-03 07:59:03 +02:00

157 lines
5.3 KiB
Go

package rtmclient
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"galaxy/gamemaster/internal/ports"
)
func newTestClient(t *testing.T, baseURL string, timeout time.Duration) *Client {
t.Helper()
client, err := NewClient(Config{BaseURL: baseURL, RequestTimeout: timeout})
require.NoError(t, err)
t.Cleanup(func() { _ = client.Close() })
return client
}
func TestNewClientValidatesConfig(t *testing.T) {
cases := map[string]Config{
"empty base url": {BaseURL: "", RequestTimeout: time.Second},
"non-absolute": {BaseURL: "rtm:8096", RequestTimeout: time.Second},
"zero timeout": {BaseURL: "http://rtm:8096", RequestTimeout: 0},
"negative timeout": {BaseURL: "http://rtm:8096", RequestTimeout: -time.Second},
}
for name, cfg := range cases {
t.Run(name, func(t *testing.T) {
_, err := NewClient(cfg)
require.Error(t, err)
})
}
}
func TestStopHappyPath(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, "/api/v1/internal/runtimes/game-1/stop", r.URL.Path)
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
var got stopRequestEnvelope
require.NoError(t, json.Unmarshal(body, &got))
assert.Equal(t, "admin_request", got.Reason)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"game_id":"game-1","status":"stopped"}`))
}))
defer server.Close()
client := newTestClient(t, server.URL, time.Second)
require.NoError(t, client.Stop(context.Background(), "game-1", "admin_request"))
}
func TestStopRejectsBadInput(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
t.Fatal("must not contact rtm on bad input")
}))
defer server.Close()
client := newTestClient(t, server.URL, time.Second)
require.Error(t, client.Stop(context.Background(), " ", "admin_request"))
require.Error(t, client.Stop(context.Background(), "g", " "))
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := client.Stop(ctx, "g", "admin_request")
require.Error(t, err)
assert.True(t, errors.Is(err, context.Canceled))
}
func TestStopInternalError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"error":{"code":"internal_error","message":"boom"}}`))
}))
defer server.Close()
client := newTestClient(t, server.URL, time.Second)
err := client.Stop(context.Background(), "g", "admin_request")
require.Error(t, err)
assert.True(t, errors.Is(err, ports.ErrRTMUnavailable))
assert.Contains(t, err.Error(), "internal_error")
}
func TestStopTimeoutMapsToUnavailable(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
time.Sleep(120 * time.Millisecond)
_, _ = w.Write([]byte(`{}`))
}))
defer server.Close()
client := newTestClient(t, server.URL, 30*time.Millisecond)
err := client.Stop(context.Background(), "g", "admin_request")
require.Error(t, err)
assert.True(t, errors.Is(err, ports.ErrRTMUnavailable))
}
func TestPatchHappyPath(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, "/api/v1/internal/runtimes/g/patch", r.URL.Path)
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
var got patchRequestEnvelope
require.NoError(t, json.Unmarshal(body, &got))
assert.Equal(t, "galaxy/game:1.2.4", got.ImageRef)
_, _ = w.Write([]byte(`{"game_id":"g","status":"running"}`))
}))
defer server.Close()
client := newTestClient(t, server.URL, time.Second)
require.NoError(t, client.Patch(context.Background(), "g", "galaxy/game:1.2.4"))
}
func TestPatchSemverConflictMapsToUnavailable(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusConflict)
_, _ = w.Write([]byte(`{"error":{"code":"semver_patch_only","message":"cross-major patch not allowed"}}`))
}))
defer server.Close()
client := newTestClient(t, server.URL, time.Second)
err := client.Patch(context.Background(), "g", "galaxy/game:2.0.0")
require.Error(t, err)
assert.True(t, errors.Is(err, ports.ErrRTMUnavailable))
assert.Contains(t, err.Error(), "semver_patch_only")
}
func TestPatchRejectsBadInput(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
t.Fatal("must not contact rtm on bad input")
}))
defer server.Close()
client := newTestClient(t, server.URL, time.Second)
require.Error(t, client.Patch(context.Background(), " ", "galaxy/game:1.0.0"))
require.Error(t, client.Patch(context.Background(), "g", " "))
}
func TestCloseIsIdempotent(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{}`))
}))
defer server.Close()
client := newTestClient(t, server.URL, time.Second)
require.NoError(t, client.Stop(context.Background(), "g", "admin_request"))
require.NoError(t, client.Close())
require.NoError(t, client.Close())
}