1package domain
2
3import (
4 "testing"
5 "time"
6
7 "github.com/stretchr/testify/require"
8)
9
10func TestStandaloneServiceValidateRequiresNameAndImageWhenEnabled(t *testing.T) {
11 svc := StandaloneService{Enabled: true, Image: "redis:7"}
12 require.ErrorContains(t, svc.Validate(), "service name")
13
14 svc = StandaloneService{Enabled: true, Name: "cache"}
15 require.ErrorContains(t, svc.Validate(), "image")
16}
17
18func TestStandaloneServiceValidateRejectsPaddedNames(t *testing.T) {
19 svc := StandaloneService{Name: " cache ", Image: "redis:7", Enabled: true}
20 require.ErrorContains(t, svc.Validate(), "leading or trailing whitespace")
21
22 svc = StandaloneService{
23 Name: "cache",
24 Image: "redis:7",
25 Enabled: true,
26 Ports: []StandaloneServicePort{{Name: " admin ", Container: 6379, Protocol: NetworkProtocolTCP}},
27 }
28 require.ErrorContains(t, svc.Validate(), "leading or trailing whitespace")
29}
30
31func TestStandaloneServiceValidateAllowsDisabledServiceWithoutImage(t *testing.T) {
32 svc := StandaloneService{Name: "cache", Enabled: false}
33 require.NoError(t, svc.Validate())
34}
35
36func TestStandaloneServiceValidatePorts(t *testing.T) {
37 valid := StandaloneService{
38 Name: "game",
39 Image: "game:latest",
40 Enabled: true,
41 Ports: []StandaloneServicePort{
42 {Name: "game", Container: 28015, Protocol: NetworkProtocolUDP, Publish: "127.0.0.1:38015"},
43 {Name: "rcon", Container: 28016, Protocol: NetworkProtocolTCP, Publish: "127.0.0.1:38016", Private: true},
44 },
45 }
46 require.NoError(t, valid.Validate())
47
48 duplicate := valid
49 duplicate.Ports = append(duplicate.Ports, StandaloneServicePort{Name: "game", Container: 28017, Protocol: NetworkProtocolUDP})
50 require.ErrorContains(t, duplicate.Validate(), "duplicate port")
51
52 badProtocol := valid
53 badProtocol.Ports = []StandaloneServicePort{{Name: "game", Container: 28015, Protocol: NetworkProtocol("sctp")}}
54 require.ErrorContains(t, badProtocol.Validate(), "protocol")
55
56 badPublish := valid
57 badPublish.Ports = []StandaloneServicePort{{Name: "game", Container: 28015, Protocol: NetworkProtocolUDP, Publish: "127.0.0.1:not-a-port"}}
58 require.ErrorContains(t, badPublish.Validate(), "valid port")
59
60 conflictingVisibility := valid
61 conflictingVisibility.Ports = []StandaloneServicePort{{Name: "rcon", Container: 28016, Protocol: NetworkProtocolTCP, Private: true, Public: true}}
62 require.ErrorContains(t, conflictingVisibility.Validate(), "cannot be both private and public")
63
64 privateNonLoopback := valid
65 privateNonLoopback.Ports = []StandaloneServicePort{{Name: "rcon", Container: 28016, Protocol: NetworkProtocolTCP, Private: true, Publish: "0.0.0.0:28016"}}
66 require.ErrorContains(t, privateNonLoopback.Validate(), "must be loopback")
67}
68
69func TestStandaloneServiceValidateVolumesAndEnvFile(t *testing.T) {
70 svc := StandaloneService{Name: "cache", Image: "redis:7", Enabled: true}
71 require.NoError(t, svc.Validate(), "volumes may be omitted")
72
73 svc.Volumes = []StandaloneServiceVolume{{Source: "cache-data", Target: "var/lib/redis"}}
74 require.ErrorContains(t, svc.Validate(), "absolute")
75
76 svc.Volumes = []StandaloneServiceVolume{{Source: "cache-data", Target: `C:\\data`}}
77 require.ErrorContains(t, svc.Validate(), "absolute")
78
79 svc.Volumes = nil
80 svc.EnvFile = " "
81 require.ErrorContains(t, svc.Validate(), "env_file")
82}
83
84func TestStandaloneServiceValidateReadiness(t *testing.T) {
85 svc := StandaloneService{Name: "game", Image: "game:latest", Enabled: true}
86 for _, readinessType := range []string{"", "none", "tcp", "log"} {
87 t.Run(readinessType, func(t *testing.T) {
88 candidate := svc
89 candidate.Readiness = StandaloneServiceReadiness{Type: readinessType, Path: "/tmp/server.log", Contains: "ready", Timeout: time.Minute}
90 require.NoError(t, candidate.Validate())
91 })
92 }
93
94 svc.Readiness = StandaloneServiceReadiness{Type: "udp"}
95 require.ErrorContains(t, svc.Validate(), "readiness")
96
97 for _, timeout := range []time.Duration{0, -time.Second} {
98 svc.Readiness = StandaloneServiceReadiness{Type: "tcp", Timeout: timeout, TimeoutSet: true}
99 require.ErrorContains(t, svc.Validate(), "readiness timeout")
100 }
101
102 svc.Readiness = StandaloneServiceReadiness{Type: "log", Path: "/tmp/server.log"}
103 require.ErrorContains(t, svc.Validate(), "contains")
104
105 svc.Readiness = StandaloneServiceReadiness{Type: "log", Contains: "ready"}
106 require.ErrorContains(t, svc.Validate(), "path")
107}
108
109func TestStandaloneServiceWithDefaultsCleanup(t *testing.T) {
110 svc := StandaloneService{Name: "game", Image: "game:latest", Enabled: true}.WithDefaults()
111 require.True(t, svc.Cleanup.PreserveVolumes)
112 require.True(t, svc.Cleanup.RemoveContainer)
113 require.NoError(t, svc.Validate())
114
115 svc = StandaloneService{Name: "game", Image: "game:latest", Enabled: true, Cleanup: StandaloneServiceCleanup{RemoveContainer: true}}.WithDefaults()
116 require.False(t, svc.Cleanup.PreserveVolumes)
117 require.True(t, svc.Cleanup.RemoveContainer)
118
119 svc = StandaloneService{Name: "game", Image: "game:latest", Enabled: true, Cleanup: StandaloneServiceCleanup{PreserveVolumes: true}}.WithDefaults()
120 require.True(t, svc.Cleanup.PreserveVolumes)
121 require.False(t, svc.Cleanup.RemoveContainer)
122}
123
124func TestStandaloneServiceValidateRejectsDuplicateVolumeTargets(t *testing.T) {
125 svc := StandaloneService{
126 Name: "cache",
127 Image: "redis:7",
128 Enabled: true,
129 Volumes: []StandaloneServiceVolume{
130 {Source: "cache-data", Target: "/data"},
131 {Source: "cache-other", Target: "/data"},
132 },
133 }
134 require.ErrorContains(t, svc.Validate(), "duplicate volume target")
135}