1package apptraffic_test
2
3import (
4 "testing"
5 "time"
6
7 "github.com/stretchr/testify/assert"
8 "github.com/stretchr/testify/require"
9
10 "github.com/bnema/gordon/internal/domain"
11 "github.com/bnema/gordon/internal/usecase/apptraffic"
12)
13
14// l4 builds installation entrypoint policies. An empty address omits the
15// entrypoint, so tests exercise only the transports they configure.
16func l4(tcpAddress, udpAddress string) map[string]apptraffic.EntrypointPolicy {
17 policies := map[string]apptraffic.EntrypointPolicy{}
18 if tcpAddress != "" {
19 policies["tcp"] = apptraffic.EntrypointPolicy{
20 Name: "tcp", Address: tcpAddress, Protocol: domain.EntryPointProtocolTCP,
21 TrustedCIDRs: []string{"10.0.0.0/8"},
22 }
23 }
24 if udpAddress != "" {
25 policies["udp"] = apptraffic.EntrypointPolicy{
26 Name: "udp", Address: udpAddress, Protocol: domain.EntryPointProtocolUDP,
27 }
28 }
29 return policies
30}
31
32func webActive() domain.AppActive {
33 return domain.AppActive{
34 App: "blog",
35 Services: map[string]domain.AppEffectiveService{
36 "web": {
37 EffectiveRevision: "rev-1",
38 Image: "img:1",
39 Container: "c-web",
40 BackendBinds: map[int]int{8080: 18080},
41 Spec: domain.AppService{
42 HTTP: []domain.AppHTTPInterface{{Host: "blog.example.com", Port: 8080, TLS: "auto"}},
43 },
44 },
45 },
46 }
47}
48
49func TestProject_HTTP(t *testing.T) {
50 entries, err := apptraffic.Project("blog", webActive(), nil)
51 require.NoError(t, err)
52 require.Len(t, entries, 1)
53 assert.Equal(t, "http", entries[0].Kind)
54 assert.Equal(t, "blog.example.com", entries[0].Host)
55 assert.Equal(t, "auto", entries[0].TLSMode)
56 assert.Equal(t, 8080, entries[0].Backend.ContainerPort)
57 assert.True(t, entries[0].Backend.Resolved())
58 assert.Equal(t, "127.0.0.1", entries[0].Backend.Host)
59 assert.Equal(t, 18080, entries[0].Backend.Port)
60 assert.Equal(t, "c-web", entries[0].Backend.ContainerID)
61 assert.Empty(t, entries[0].Entrypoint)
62}
63
64func TestProject_TCPUDP(t *testing.T) {
65 active := domain.AppActive{
66 App: "game",
67 Services: map[string]domain.AppEffectiveService{
68 "server": {
69 EffectiveRevision: "rev-1",
70 Image: "img:1",
71 Spec: domain.AppService{
72 TCP: []domain.AppTCPInterface{{Entrypoint: "tcp", Port: 25565, Publish: "0.0.0.0:25565"}},
73 UDP: []domain.AppUDPInterface{{Entrypoint: "udp", Port: 28015, Publish: "0.0.0.0:28015"}},
74 },
75 },
76 },
77 }
78 entries, err := apptraffic.Project("game", active, l4("0.0.0.0:25565", "0.0.0.0:28015"))
79 require.NoError(t, err)
80 require.Len(t, entries, 2)
81 assert.Equal(t, "tcp", entries[0].Kind)
82 assert.Equal(t, "udp", entries[1].Kind)
83 assert.Equal(t, 25565, entries[0].BindPort)
84 assert.Equal(t, "tcp", entries[0].Entrypoint)
85}
86
87// TestProject_SamePortTCPUDPResolvesDistinctBackends proves the same
88// container port number on TCP and UDP resolves to two distinct loopback
89// backends from their respective bind maps.
90func TestProject_SamePortTCPUDPResolvesDistinctBackends(t *testing.T) {
91 active := domain.AppActive{
92 App: "game",
93 Services: map[string]domain.AppEffectiveService{
94 "server": {
95 EffectiveRevision: "rev-1",
96 Image: "img:1",
97 Container: "c-game",
98 BackendBinds: map[int]int{9000: 19000},
99 UDPBackendBinds: map[int]int{9000: 19001},
100 Spec: domain.AppService{
101 TCP: []domain.AppTCPInterface{{Entrypoint: "tcp", Port: 9000, Publish: "0.0.0.0:9000"}},
102 UDP: []domain.AppUDPInterface{{Entrypoint: "udp", Port: 9000, Publish: "0.0.0.0:9000"}},
103 },
104 },
105 },
106 }
107 entries, err := apptraffic.Project("game", active, l4("0.0.0.0:9000", "0.0.0.0:9000"))
108 require.NoError(t, err)
109 require.Len(t, entries, 2)
110 byKind := map[string]apptraffic.RouteEntry{}
111 for _, entry := range entries {
112 byKind[entry.Kind] = entry
113 }
114 require.Contains(t, byKind, "tcp")
115 require.Contains(t, byKind, "udp")
116 assert.True(t, byKind["tcp"].Backend.Resolved())
117 assert.True(t, byKind["udp"].Backend.Resolved())
118 assert.Equal(t, 9000, byKind["tcp"].Backend.ContainerPort)
119 assert.Equal(t, 9000, byKind["udp"].Backend.ContainerPort)
120 assert.Equal(t, 19000, byKind["tcp"].Backend.Port)
121 assert.Equal(t, 19001, byKind["udp"].Backend.Port)
122 assert.NotEqual(t, byKind["tcp"].Backend.Port, byKind["udp"].Backend.Port)
123}
124
125func TestProject_TCPAsOrdinaryRCON(t *testing.T) {
126 // RCON is ordinary TCP: no special kind or policy tag.
127 active := domain.AppActive{
128 App: "rust",
129 Services: map[string]domain.AppEffectiveService{
130 "server": {
131 EffectiveRevision: "rev-1",
132 Image: "img:1",
133 Spec: domain.AppService{
134 TCP: []domain.AppTCPInterface{{Entrypoint: "tcp", Port: 28016, Publish: "0.0.0.0:28016"}},
135 },
136 },
137 },
138 }
139 entries, err := apptraffic.Project("rust", active, l4("0.0.0.0:28016", ""))
140 require.NoError(t, err)
141 require.Len(t, entries, 1)
142 assert.Equal(t, "tcp", entries[0].Kind)
143 assert.Equal(t, 28016, entries[0].Backend.ContainerPort)
144 assert.False(t, entries[0].Backend.Resolved(), "no recorded bind: fail closed")
145}
146
147// TestProject_RejectsPublishMismatch proves a declared bind that differs
148// from the entrypoint listener is refused rather than silently widened to
149// the entrypoint address.
150func TestProject_RejectsPublishMismatch(t *testing.T) {
151 cases := []struct {
152 name string
153 entrypoint apptraffic.EntrypointPolicy
154 publish string
155 }{
156 {
157 name: "loopback-vs-wildcard",
158 entrypoint: apptraffic.EntrypointPolicy{Name: "tcp", Address: "0.0.0.0:25432", Protocol: domain.EntryPointProtocolTCP},
159 publish: "127.0.0.1:15432",
160 },
161 {
162 name: "same-host-different-port",
163 entrypoint: apptraffic.EntrypointPolicy{Name: "tcp", Address: "0.0.0.0:25432", Protocol: domain.EntryPointProtocolTCP},
164 publish: "0.0.0.0:15432",
165 },
166 {
167 name: "protocol-mismatch",
168 entrypoint: apptraffic.EntrypointPolicy{Name: "tcp", Address: "0.0.0.0:25432", Protocol: domain.EntryPointProtocolUDP},
169 publish: "0.0.0.0:25432",
170 },
171 }
172 for _, tc := range cases {
173 t.Run(tc.name, func(t *testing.T) {
174 active := domain.AppActive{
175 App: "game",
176 Services: map[string]domain.AppEffectiveService{
177 "server": {
178 EffectiveRevision: "rev-1", Image: "img:1",
179 Spec: domain.AppService{
180 TCP: []domain.AppTCPInterface{{Entrypoint: "tcp", Port: 25432, Publish: tc.publish}},
181 },
182 },
183 },
184 }
185 _, err := apptraffic.Project("game", active, map[string]apptraffic.EntrypointPolicy{"tcp": tc.entrypoint})
186 require.ErrorIs(t, err, domain.ErrAppTrafficProjection)
187 })
188 }
189}
190
191func TestProject_UnknownEntrypoint(t *testing.T) {
192 active := domain.AppActive{
193 App: "game",
194 Services: map[string]domain.AppEffectiveService{
195 "server": {
196 EffectiveRevision: "rev-1",
197 Image: "img:1",
198 Spec: domain.AppService{
199 TCP: []domain.AppTCPInterface{{Entrypoint: "nope", Port: 1, Publish: "1"}},
200 },
201 },
202 },
203 }
204 _, err := apptraffic.Project("game", active, l4("0.0.0.0:25565", ""))
205 require.ErrorIs(t, err, domain.ErrAppTrafficProjection)
206}
207
208func TestProject_GameExample(t *testing.T) {
209 active := domain.AppActive{
210 App: "rust",
211 Services: map[string]domain.AppEffectiveService{
212 "server": {
213 EffectiveRevision: "rev-1", Image: "img:1",
214 Spec: domain.AppService{
215 Readiness: domain.AppReadiness{Type: "log", Path: "/data/logs/server.log", Contains: "ready", Timeout: 5 * time.Minute},
216 UDP: []domain.AppUDPInterface{{Entrypoint: "udp", Port: 28015, Publish: "0.0.0.0:28015"}},
217 TCP: []domain.AppTCPInterface{{Entrypoint: "tcp", Port: 28016, Publish: "0.0.0.0:28016"}},
218 },
219 },
220 },
221 }
222 entries, err := apptraffic.Project("rust", active, l4("0.0.0.0:28016", "0.0.0.0:28015"))
223 require.NoError(t, err)
224 require.Len(t, entries, 2)
225}