562 lines
19 KiB
Go
562 lines
19 KiB
Go
package docker
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
dockerclient "github.com/docker/docker/client"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"galaxy/rtmanager/internal/ports"
|
|
)
|
|
|
|
// newTestClient wires an httptest.Server backed Docker SDK client to our
|
|
// adapter. The handler is invoked for every Docker API request issued
|
|
// during the test; tests assert on path and method to route the
|
|
// response.
|
|
func newTestClient(t *testing.T, handler http.HandlerFunc) *Client {
|
|
t.Helper()
|
|
server := httptest.NewServer(handler)
|
|
t.Cleanup(server.Close)
|
|
|
|
docker, err := dockerclient.NewClientWithOpts(
|
|
dockerclient.WithHost(server.URL),
|
|
dockerclient.WithHTTPClient(server.Client()),
|
|
dockerclient.WithVersion("1.45"),
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = docker.Close() })
|
|
|
|
client, err := NewClient(Config{
|
|
Docker: docker,
|
|
LogDriver: "json-file",
|
|
LogOpts: "max-size=1m,max-file=3",
|
|
Clock: func() time.Time { return time.Date(2026, time.April, 27, 12, 0, 0, 0, time.UTC) },
|
|
})
|
|
require.NoError(t, err)
|
|
return client
|
|
}
|
|
|
|
func writeJSON(t *testing.T, w http.ResponseWriter, status int, body any) {
|
|
t.Helper()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
require.NoError(t, json.NewEncoder(w).Encode(body))
|
|
}
|
|
|
|
func writeNotFound(t *testing.T, w http.ResponseWriter, msg string) {
|
|
t.Helper()
|
|
writeJSON(t, w, http.StatusNotFound, map[string]string{"message": msg})
|
|
}
|
|
|
|
// Docker SDK uses /v1.45 prefix when client is pinned to API 1.45.
|
|
func dockerPath(suffix string) string {
|
|
return "/v1.45" + suffix
|
|
}
|
|
|
|
func TestNewClientValidatesConfig(t *testing.T) {
|
|
t.Run("nil docker client", func(t *testing.T) {
|
|
_, err := NewClient(Config{LogDriver: "json-file"})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "nil docker client")
|
|
})
|
|
t.Run("empty log driver", func(t *testing.T) {
|
|
docker, err := dockerclient.NewClientWithOpts(dockerclient.WithHost("tcp://127.0.0.1:65535"))
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = docker.Close() })
|
|
_, err = NewClient(Config{Docker: docker, LogDriver: " "})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "log driver")
|
|
})
|
|
}
|
|
|
|
func TestEnsureNetwork(t *testing.T) {
|
|
t.Run("present", func(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, http.MethodGet, r.Method)
|
|
require.Equal(t, dockerPath("/networks/galaxy-net"), r.URL.Path)
|
|
writeJSON(t, w, http.StatusOK, map[string]any{"Id": "net-1", "Name": "galaxy-net"})
|
|
})
|
|
require.NoError(t, client.EnsureNetwork(context.Background(), "galaxy-net"))
|
|
})
|
|
t.Run("missing", func(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
writeNotFound(t, w, "no such network")
|
|
})
|
|
err := client.EnsureNetwork(context.Background(), "missing")
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, ports.ErrNetworkMissing)
|
|
})
|
|
t.Run("transport error", func(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "boom", http.StatusInternalServerError)
|
|
})
|
|
err := client.EnsureNetwork(context.Background(), "x")
|
|
require.Error(t, err)
|
|
assert.NotErrorIs(t, err, ports.ErrNetworkMissing)
|
|
})
|
|
}
|
|
|
|
func TestInspectImage(t *testing.T) {
|
|
t.Run("present", func(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, http.MethodGet, r.Method)
|
|
require.Equal(t, dockerPath("/images/galaxy/game:test/json"), r.URL.Path)
|
|
writeJSON(t, w, http.StatusOK, map[string]any{
|
|
"Id": "sha256:abc",
|
|
"Config": map[string]any{
|
|
"Labels": map[string]string{
|
|
"com.galaxy.cpu_quota": "1.0",
|
|
"com.galaxy.memory": "512m",
|
|
"com.galaxy.pids_limit": "512",
|
|
},
|
|
},
|
|
})
|
|
})
|
|
got, err := client.InspectImage(context.Background(), "galaxy/game:test")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "galaxy/game:test", got.Ref)
|
|
assert.Equal(t, "1.0", got.Labels["com.galaxy.cpu_quota"])
|
|
assert.Equal(t, "512m", got.Labels["com.galaxy.memory"])
|
|
})
|
|
t.Run("not found", func(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
writeNotFound(t, w, "no such image")
|
|
})
|
|
_, err := client.InspectImage(context.Background(), "galaxy/missing:tag")
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, ports.ErrImageNotFound)
|
|
})
|
|
}
|
|
|
|
func TestInspectContainer(t *testing.T) {
|
|
t.Run("present", func(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, http.MethodGet, r.Method)
|
|
require.Equal(t, dockerPath("/containers/cont-1/json"), r.URL.Path)
|
|
writeJSON(t, w, http.StatusOK, map[string]any{
|
|
"Id": "cont-1",
|
|
"RestartCount": 2,
|
|
"State": map[string]any{
|
|
"Status": "running",
|
|
"OOMKilled": false,
|
|
"ExitCode": 0,
|
|
"StartedAt": "2026-04-27T11:00:00.5Z",
|
|
"FinishedAt": "0001-01-01T00:00:00Z",
|
|
"Health": map[string]any{"Status": "healthy"},
|
|
},
|
|
"Config": map[string]any{
|
|
"Image": "galaxy/game:test",
|
|
"Hostname": "galaxy-game-game-1",
|
|
"Labels": map[string]string{
|
|
"com.galaxy.owner": "rtmanager",
|
|
"com.galaxy.game_id": "game-1",
|
|
},
|
|
},
|
|
})
|
|
})
|
|
got, err := client.InspectContainer(context.Background(), "cont-1")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "cont-1", got.ID)
|
|
assert.Equal(t, 2, got.RestartCount)
|
|
assert.Equal(t, "running", got.Status)
|
|
assert.Equal(t, "healthy", got.Health)
|
|
assert.Equal(t, "galaxy/game:test", got.ImageRef)
|
|
assert.Equal(t, "galaxy-game-game-1", got.Hostname)
|
|
assert.Equal(t, "rtmanager", got.Labels["com.galaxy.owner"])
|
|
assert.False(t, got.StartedAt.IsZero())
|
|
})
|
|
t.Run("not found", func(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
writeNotFound(t, w, "no such container")
|
|
})
|
|
_, err := client.InspectContainer(context.Background(), "missing")
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, ports.ErrContainerNotFound)
|
|
})
|
|
}
|
|
|
|
func TestPullImagePolicies(t *testing.T) {
|
|
t.Run("if_missing/found skips pull", func(t *testing.T) {
|
|
hits := struct {
|
|
inspect atomic.Int32
|
|
pull atomic.Int32
|
|
}{}
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, "/json") && r.Method == http.MethodGet:
|
|
hits.inspect.Add(1)
|
|
writeJSON(t, w, http.StatusOK, map[string]any{"Id": "sha256:x"})
|
|
case strings.Contains(r.URL.Path, "/images/create"):
|
|
hits.pull.Add(1)
|
|
w.WriteHeader(http.StatusOK)
|
|
default:
|
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
|
}
|
|
})
|
|
require.NoError(t, client.PullImage(context.Background(), "alpine:3.21", ports.PullPolicyIfMissing))
|
|
assert.Equal(t, int32(1), hits.inspect.Load())
|
|
assert.Equal(t, int32(0), hits.pull.Load())
|
|
})
|
|
t.Run("if_missing/absent triggers pull", func(t *testing.T) {
|
|
hits := struct {
|
|
inspect atomic.Int32
|
|
pull atomic.Int32
|
|
}{}
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, "/json") && r.Method == http.MethodGet:
|
|
hits.inspect.Add(1)
|
|
writeNotFound(t, w, "no such image")
|
|
case strings.Contains(r.URL.Path, "/images/create"):
|
|
hits.pull.Add(1)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = io.WriteString(w, `{"status":"Pulling..."}`+"\n"+`{"status":"Done"}`+"\n")
|
|
default:
|
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
|
}
|
|
})
|
|
require.NoError(t, client.PullImage(context.Background(), "alpine:3.21", ports.PullPolicyIfMissing))
|
|
assert.Equal(t, int32(1), hits.inspect.Load())
|
|
assert.Equal(t, int32(1), hits.pull.Load())
|
|
})
|
|
t.Run("always pulls regardless of cache", func(t *testing.T) {
|
|
var pullCount atomic.Int32
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
require.Contains(t, r.URL.Path, "/images/create")
|
|
pullCount.Add(1)
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
require.NoError(t, client.PullImage(context.Background(), "alpine:3.21", ports.PullPolicyAlways))
|
|
assert.Equal(t, int32(1), pullCount.Load())
|
|
})
|
|
t.Run("never with absent image", func(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, http.MethodGet, r.Method)
|
|
writeNotFound(t, w, "no such image")
|
|
})
|
|
err := client.PullImage(context.Background(), "alpine:3.21", ports.PullPolicyNever)
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, ports.ErrImageNotFound)
|
|
})
|
|
t.Run("never with present image", func(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, http.MethodGet, r.Method)
|
|
writeJSON(t, w, http.StatusOK, map[string]any{"Id": "x"})
|
|
})
|
|
require.NoError(t, client.PullImage(context.Background(), "alpine:3.21", ports.PullPolicyNever))
|
|
})
|
|
t.Run("unknown policy", func(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
t.Fatal("must not call docker on unknown policy")
|
|
})
|
|
err := client.PullImage(context.Background(), "alpine:3.21", ports.PullPolicy("invalid"))
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestRunHappyPath(t *testing.T) {
|
|
calls := struct {
|
|
create atomic.Int32
|
|
start atomic.Int32
|
|
remove atomic.Int32
|
|
}{}
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/containers/create"):
|
|
calls.create.Add(1)
|
|
require.Equal(t, "galaxy-game-game-1", r.URL.Query().Get("name"))
|
|
writeJSON(t, w, http.StatusCreated, map[string]any{"Id": "cont-new", "Warnings": []string{}})
|
|
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/start"):
|
|
calls.start.Add(1)
|
|
require.Equal(t, dockerPath("/containers/cont-new/start"), r.URL.Path)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
case r.Method == http.MethodDelete && strings.HasPrefix(r.URL.Path, dockerPath("/containers/")):
|
|
calls.remove.Add(1)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
default:
|
|
t.Fatalf("unexpected %s %s", r.Method, r.URL.Path)
|
|
}
|
|
})
|
|
|
|
result, err := client.Run(context.Background(), ports.RunSpec{
|
|
Name: "galaxy-game-game-1",
|
|
Image: "galaxy/game:test",
|
|
Hostname: "galaxy-game-game-1",
|
|
Network: "galaxy-net",
|
|
Env: map[string]string{
|
|
"GAME_STATE_PATH": "/var/lib/galaxy-game",
|
|
"STORAGE_PATH": "/var/lib/galaxy-game",
|
|
},
|
|
Labels: map[string]string{"com.galaxy.owner": "rtmanager"},
|
|
LogDriver: "json-file",
|
|
BindMounts: []ports.BindMount{
|
|
{HostPath: "/var/lib/galaxy/games/game-1", MountPath: "/var/lib/galaxy-game"},
|
|
},
|
|
CPUQuota: 1.0,
|
|
Memory: "512m",
|
|
PIDsLimit: 512,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "cont-new", result.ContainerID)
|
|
assert.Equal(t, "http://galaxy-game-game-1:8080", result.EngineEndpoint)
|
|
assert.False(t, result.StartedAt.IsZero())
|
|
assert.Equal(t, int32(1), calls.create.Load())
|
|
assert.Equal(t, int32(1), calls.start.Load())
|
|
assert.Equal(t, int32(0), calls.remove.Load())
|
|
}
|
|
|
|
func TestRunStartFailureRemovesContainer(t *testing.T) {
|
|
calls := struct {
|
|
create atomic.Int32
|
|
start atomic.Int32
|
|
remove atomic.Int32
|
|
}{}
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/containers/create"):
|
|
calls.create.Add(1)
|
|
writeJSON(t, w, http.StatusCreated, map[string]any{"Id": "cont-x"})
|
|
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/start"):
|
|
calls.start.Add(1)
|
|
http.Error(w, `{"message":"insufficient host resources"}`, http.StatusInternalServerError)
|
|
case r.Method == http.MethodDelete && strings.HasPrefix(r.URL.Path, dockerPath("/containers/cont-x")):
|
|
calls.remove.Add(1)
|
|
require.Equal(t, "1", r.URL.Query().Get("force"))
|
|
w.WriteHeader(http.StatusNoContent)
|
|
default:
|
|
t.Fatalf("unexpected %s %s", r.Method, r.URL.Path)
|
|
}
|
|
})
|
|
|
|
_, err := client.Run(context.Background(), ports.RunSpec{
|
|
Name: "x",
|
|
Image: "img",
|
|
Hostname: "x",
|
|
Network: "n",
|
|
LogDriver: "json-file",
|
|
CPUQuota: 1.0,
|
|
Memory: "64m",
|
|
PIDsLimit: 64,
|
|
})
|
|
require.Error(t, err)
|
|
assert.Equal(t, int32(1), calls.create.Load())
|
|
assert.Equal(t, int32(1), calls.start.Load())
|
|
assert.Equal(t, int32(1), calls.remove.Load(), "adapter must roll back the partial container")
|
|
}
|
|
|
|
func TestRunRejectsInvalidSpec(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
t.Fatal("must not contact docker on invalid spec")
|
|
})
|
|
_, err := client.Run(context.Background(), ports.RunSpec{Name: "x"})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "image must not be empty")
|
|
}
|
|
|
|
func TestStop(t *testing.T) {
|
|
t.Run("graceful stop", func(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, http.MethodPost, r.Method)
|
|
require.Equal(t, dockerPath("/containers/cont-1/stop"), r.URL.Path)
|
|
require.Equal(t, "30", r.URL.Query().Get("t"))
|
|
w.WriteHeader(http.StatusNoContent)
|
|
})
|
|
require.NoError(t, client.Stop(context.Background(), "cont-1", 30*time.Second))
|
|
})
|
|
t.Run("missing container", func(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
writeNotFound(t, w, "no such container")
|
|
})
|
|
err := client.Stop(context.Background(), "missing", 30*time.Second)
|
|
assert.ErrorIs(t, err, ports.ErrContainerNotFound)
|
|
})
|
|
t.Run("negative timeout normalised to zero", func(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, "0", r.URL.Query().Get("t"))
|
|
w.WriteHeader(http.StatusNoContent)
|
|
})
|
|
require.NoError(t, client.Stop(context.Background(), "x", -5*time.Second))
|
|
})
|
|
}
|
|
|
|
func TestRemoveIsIdempotent(t *testing.T) {
|
|
t.Run("present", func(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, http.MethodDelete, r.Method)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
})
|
|
require.NoError(t, client.Remove(context.Background(), "cont-1"))
|
|
})
|
|
t.Run("missing", func(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
writeNotFound(t, w, "no such container")
|
|
})
|
|
require.NoError(t, client.Remove(context.Background(), "missing"))
|
|
})
|
|
}
|
|
|
|
func TestListAppliesLabelFilter(t *testing.T) {
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, http.MethodGet, r.Method)
|
|
require.Equal(t, dockerPath("/containers/json"), r.URL.Path)
|
|
require.Equal(t, "1", r.URL.Query().Get("all"))
|
|
|
|
filtersRaw := r.URL.Query().Get("filters")
|
|
require.NotEmpty(t, filtersRaw)
|
|
var args map[string]map[string]bool
|
|
require.NoError(t, json.Unmarshal([]byte(filtersRaw), &args))
|
|
require.True(t, args["label"]["com.galaxy.owner=rtmanager"])
|
|
|
|
writeJSON(t, w, http.StatusOK, []map[string]any{
|
|
{
|
|
"Id": "cont-a",
|
|
"Image": "galaxy/game:1.2.3",
|
|
"Names": []string{"/galaxy-game-game-1"},
|
|
"Labels": map[string]string{"com.galaxy.owner": "rtmanager"},
|
|
"State": "running",
|
|
"Created": int64(1700000000),
|
|
},
|
|
})
|
|
})
|
|
|
|
got, err := client.List(context.Background(), ports.ListFilter{
|
|
Labels: map[string]string{"com.galaxy.owner": "rtmanager"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, got, 1)
|
|
assert.Equal(t, "cont-a", got[0].ID)
|
|
assert.Equal(t, "galaxy/game:1.2.3", got[0].ImageRef)
|
|
assert.Equal(t, "galaxy-game-game-1", got[0].Hostname)
|
|
assert.Equal(t, "running", got[0].Status)
|
|
assert.False(t, got[0].StartedAt.IsZero())
|
|
assert.Equal(t, "rtmanager", got[0].Labels["com.galaxy.owner"])
|
|
}
|
|
|
|
func TestEventsListenDecodesContainerEvents(t *testing.T) {
|
|
mu := make(chan struct{})
|
|
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, http.MethodGet, r.Method)
|
|
require.Equal(t, dockerPath("/events"), r.URL.Path)
|
|
|
|
flusher, ok := w.(http.Flusher)
|
|
require.True(t, ok)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
flusher.Flush()
|
|
|
|
// Container start event
|
|
writeEvent(t, w, "container", "start", "cont-1", map[string]string{
|
|
"image": "galaxy/game:1.2.3",
|
|
"name": "galaxy-game-game-1",
|
|
"com.galaxy.game_id": "game-1",
|
|
}, time.Now())
|
|
flusher.Flush()
|
|
|
|
// Container die event with exit code 137
|
|
writeEvent(t, w, "container", "die", "cont-1", map[string]string{
|
|
"exitCode": "137",
|
|
}, time.Now())
|
|
flusher.Flush()
|
|
|
|
// Image event must be filtered out by adapter
|
|
writeEvent(t, w, "image", "pull", "img", nil, time.Now())
|
|
flusher.Flush()
|
|
|
|
<-mu
|
|
})
|
|
defer close(mu)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
events, _, err := client.EventsListen(ctx)
|
|
require.NoError(t, err)
|
|
|
|
got := []ports.DockerEvent{}
|
|
deadline := time.After(2 * time.Second)
|
|
for len(got) < 2 {
|
|
select {
|
|
case ev, ok := <-events:
|
|
if !ok {
|
|
t.Fatalf("events channel closed; got %d events", len(got))
|
|
}
|
|
got = append(got, ev)
|
|
case <-deadline:
|
|
t.Fatalf("did not receive expected events; have %d", len(got))
|
|
}
|
|
}
|
|
require.Len(t, got, 2)
|
|
assert.Equal(t, "start", got[0].Action)
|
|
assert.Equal(t, "cont-1", got[0].ContainerID)
|
|
assert.Equal(t, "game-1", got[0].Labels["com.galaxy.game_id"])
|
|
assert.Equal(t, "die", got[1].Action)
|
|
assert.Equal(t, 137, got[1].ExitCode)
|
|
}
|
|
|
|
func writeEvent(t *testing.T, w io.Writer, eventType, action, id string, attributes map[string]string, when time.Time) {
|
|
t.Helper()
|
|
payload := map[string]any{
|
|
"Type": eventType,
|
|
"Action": action,
|
|
"Actor": map[string]any{"ID": id, "Attributes": attributes},
|
|
"time": when.Unix(),
|
|
"timeNano": when.UnixNano(),
|
|
}
|
|
data, err := json.Marshal(payload)
|
|
require.NoError(t, err)
|
|
_, err = fmt.Fprintln(w, string(data))
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Sanity: parsing helpers.
|
|
func TestParseLogOpts(t *testing.T) {
|
|
got := parseLogOpts("max-size=1m,max-file=3, ,empty=,=novalue")
|
|
assert.Equal(t, "1m", got["max-size"])
|
|
assert.Equal(t, "3", got["max-file"])
|
|
assert.Equal(t, "", got["empty"])
|
|
_, hasNovalue := got["=novalue"]
|
|
assert.False(t, hasNovalue)
|
|
}
|
|
|
|
func TestParseDockerTime(t *testing.T) {
|
|
assert.True(t, parseDockerTime("").IsZero())
|
|
assert.True(t, parseDockerTime("not-a-date").IsZero())
|
|
parsed := parseDockerTime("2026-04-27T11:00:00.5Z")
|
|
assert.False(t, parsed.IsZero())
|
|
assert.Equal(t, time.UTC, parsed.Location())
|
|
}
|
|
|
|
func TestEnvMapToSliceDeterministicLength(t *testing.T) {
|
|
got := envMapToSlice(map[string]string{"A": "1", "B": "2"})
|
|
assert.Len(t, got, 2)
|
|
for _, kv := range got {
|
|
assert.Contains(t, []string{"A=1", "B=2"}, kv)
|
|
}
|
|
assert.Nil(t, envMapToSlice(nil))
|
|
}
|
|
|
|
// Compile-time sanity: make sure errors.Is wiring stays intact.
|
|
func TestSentinelErrorsAreDistinct(t *testing.T) {
|
|
require.True(t, errors.Is(ports.ErrNetworkMissing, ports.ErrNetworkMissing))
|
|
require.False(t, errors.Is(ports.ErrNetworkMissing, ports.ErrImageNotFound))
|
|
}
|
|
|
|
func TestURLPathEscapingForCharacters(t *testing.T) {
|
|
// Ensure the SDK URL path encodes special characters; the adapter
|
|
// passes raw inputs through and lets the SDK escape.
|
|
encoded := url.PathEscape("game-1")
|
|
assert.Equal(t, "game-1", encoded)
|
|
}
|