projection_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close