app_visibility_test.go RawHistoryFolder

1package domain_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)
12
13func visibilitySpec(interfaces ...domain.AppHTTPInterface) domain.AppSpec {
14	return domain.AppSpec{
15		Name: "blog",
16		Services: []domain.AppService{{
17			Name:      "web",
18			Image:     "registry.example.com/blog/web:1.4.2",
19			StopGrace: 10 * time.Second,
20			Readiness: domain.AppReadiness{Type: domain.AppReadinessHTTP, Path: "/healthz", Timeout: 30 * time.Second},
21			HTTP:      interfaces,
22		}},
23	}
24}
25
26// TestAppHTTPInterface_EffectiveVisibility proves the zero value normalizes
27// to public so manifests and stored state written before visibility existed
28// keep their meaning.
29func TestAppHTTPInterface_EffectiveVisibility(t *testing.T) {
30	absent := domain.AppHTTPInterface{Host: "blog.example.com", Port: 8080, TLS: "auto"}
31	assert.Equal(t, domain.AppVisibilityPublic, absent.EffectiveVisibility())
32	assert.True(t, absent.IsPublic())
33	assert.False(t, absent.IsInternal())
34
35	explicit := absent
36	explicit.Visibility = domain.AppVisibilityInternal
37	assert.Equal(t, domain.AppVisibilityInternal, explicit.EffectiveVisibility())
38	assert.True(t, explicit.IsInternal())
39	assert.False(t, explicit.IsPublic())
40}
41
42// TestAppSpec_ValidateVisibility proves public and absent visibility stay
43// valid, internal visibility accepts a port without host or tls, and every
44// malformed combination is rejected.
45func TestAppSpec_ValidateVisibility(t *testing.T) {
46	t.Run("absent visibility remains a valid public manifest", func(t *testing.T) {
47		require.NoError(t, visibilitySpec(domain.AppHTTPInterface{
48			Host: "blog.example.com", Port: 8080, TLS: "auto",
49		}).Validate())
50	})
51
52	t.Run("explicit public remains valid", func(t *testing.T) {
53		require.NoError(t, visibilitySpec(domain.AppHTTPInterface{
54			Host: "blog.example.com", Port: 8080, TLS: "auto", Visibility: domain.AppVisibilityPublic,
55		}).Validate())
56	})
57
58	t.Run("internal accepts a port without host or tls", func(t *testing.T) {
59		require.NoError(t, visibilitySpec(domain.AppHTTPInterface{
60			Port: 8080, Visibility: domain.AppVisibilityInternal,
61		}).Validate())
62	})
63
64	reject := map[string]domain.AppHTTPInterface{
65		"internal forbids host":           {Host: "blog.example.com", Port: 8080, Visibility: domain.AppVisibilityInternal},
66		"internal forbids tls":            {Port: 8080, TLS: "auto", Visibility: domain.AppVisibilityInternal},
67		"unknown visibility":              {Host: "blog.example.com", Port: 8080, TLS: "auto", Visibility: "vpn"},
68		"internal without port":           {Visibility: domain.AppVisibilityInternal},
69		"public without host":             {Port: 8080, TLS: "auto"},
70		"public without valid host":       {Host: "not a host", Port: 8080, TLS: "auto"},
71		"public without tls mode":         {Host: "blog.example.com", Port: 8080},
72		"public with unknown tls mode":    {Host: "blog.example.com", Port: 8080, TLS: "sometimes"},
73		"public with out of range port":   {Host: "blog.example.com", Port: 0, TLS: "auto"},
74		"internal with out of range port": {Port: -1, Visibility: domain.AppVisibilityInternal},
75	}
76	for name, iface := range reject {
77		t.Run(name, func(t *testing.T) {
78			err := visibilitySpec(iface).Validate()
79			require.Error(t, err)
80			assert.ErrorIs(t, err, domain.ErrInvalidAppSpec)
81		})
82	}
83}
84
85// TestAppService_InternallyOnlyPort proves a port is internal-only only
86// when internal HTTP declares it and no externally backed interface does.
87func TestAppService_InternallyOnlyPort(t *testing.T) {
88	internalOnly := domain.AppService{
89		HTTP: []domain.AppHTTPInterface{{Port: 8080, Visibility: domain.AppVisibilityInternal}},
90	}
91	assert.True(t, internalOnly.InternallyOnlyPort(8080))
92	assert.False(t, internalOnly.InternallyOnlyPort(8081))
93	assert.True(t, internalOnly.IsPublicHTTP() == false)
94
95	mixed := domain.AppService{
96		HTTP: []domain.AppHTTPInterface{
97			{Host: "blog.example.com", Port: 8080, TLS: "auto"},
98			{Port: 9090, Visibility: domain.AppVisibilityInternal},
99		},
100	}
101	assert.True(t, mixed.IsPublicHTTP())
102	assert.True(t, mixed.InternallyOnlyPort(9090))
103	assert.False(t, mixed.InternallyOnlyPort(8080))
104
105	// A TCP interface on the same port is externally backed: the port is
106	// never internal-only, even though an internal HTTP interface wants it.
107	// Validation rejects that combination, and the read helper must not
108	// treat it as internal-only either.
109	shadowed := domain.AppService{
110		HTTP: []domain.AppHTTPInterface{{Port: 8080, Visibility: domain.AppVisibilityInternal}},
111		TCP:  []domain.AppTCPInterface{{Port: 8080}},
112	}
113	assert.False(t, shadowed.InternallyOnlyPort(8080))
114}
115
116// TestAppSpec_ValidateInterfacePorts proves an internal HTTP port may not
117// collide with an externally backed interface on the same container port,
118// and that internal ports stay unique.
119func TestAppSpec_ValidateInterfacePorts(t *testing.T) {
120	collide := visibilitySpec(
121		domain.AppHTTPInterface{Port: 8080, Visibility: domain.AppVisibilityInternal},
122	)
123	collide.Services[0].Readiness.Port = 8080
124	collide.Services[0].TCP = []domain.AppTCPInterface{{Port: 8080}}
125	require.ErrorIs(t, collide.Validate(), domain.ErrInvalidAppSpec)
126
127	duplicate := visibilitySpec(
128		domain.AppHTTPInterface{Port: 8080, Visibility: domain.AppVisibilityInternal},
129		domain.AppHTTPInterface{Port: 8080, Visibility: domain.AppVisibilityInternal},
130	)
131	// An explicit port keeps this test on the duplicate-internal-port rule
132	// instead of short-circuiting on readiness-port ambiguity.
133	duplicate.Services[0].Readiness.Port = 8080
134	require.ErrorIs(t, duplicate.Validate(), domain.ErrInvalidAppSpec)
135}
136
137// TestAppSpec_ValidateMixedVisibilityReadiness proves one public HTTP
138// interface plus internal HTTP interfaces is a valid readiness target with
139// no explicit readiness.port, while interface sets that cannot select one
140// port stay strict.
141func TestAppSpec_ValidateMixedVisibilityReadiness(t *testing.T) {
142	require.NoError(t, visibilitySpec(
143		domain.AppHTTPInterface{Host: "blog.example.com", Port: 8080, TLS: "auto"},
144		domain.AppHTTPInterface{Port: 9090, Visibility: domain.AppVisibilityInternal},
145	).Validate())
146
147	tcpReadiness := visibilitySpec(
148		domain.AppHTTPInterface{Host: "blog.example.com", Port: 8080, TLS: "auto"},
149		domain.AppHTTPInterface{Port: 9090, Visibility: domain.AppVisibilityInternal},
150	)
151	tcpReadiness.Services[0].Readiness.Type = domain.AppReadinessTCP
152	require.NoError(t, tcpReadiness.Validate())
153
154	explicit := visibilitySpec(
155		domain.AppHTTPInterface{Host: "blog.example.com", Port: 8080, TLS: "auto"},
156		domain.AppHTTPInterface{Port: 9090, Visibility: domain.AppVisibilityInternal},
157	)
158	explicit.Services[0].Readiness.Port = 9090
159	require.NoError(t, explicit.Validate())
160
161	reject := map[string]func(spec domain.AppSpec){
162		"several internal ports": func(spec domain.AppSpec) {
163			spec.Services[0].HTTP = []domain.AppHTTPInterface{
164				{Port: 8080, Visibility: domain.AppVisibilityInternal},
165				{Port: 9090, Visibility: domain.AppVisibilityInternal},
166			}
167		},
168		"public http plus tcp": func(spec domain.AppSpec) {
169			spec.Services[0].TCP = []domain.AppTCPInterface{{Entrypoint: "tcp", Port: 9000, Publish: "9000"}}
170		},
171		"several public http ports": func(spec domain.AppSpec) {
172			spec.Services[0].HTTP = []domain.AppHTTPInterface{
173				{Host: "blog.example.com", Port: 8080, TLS: "auto"},
174				{Host: "www.example.com", Port: 8081, TLS: "auto"},
175			}
176		},
177	}
178	for name, mutate := range reject {
179		t.Run(name, func(t *testing.T) {
180			spec := visibilitySpec(
181				domain.AppHTTPInterface{Host: "blog.example.com", Port: 8080, TLS: "auto"},
182				domain.AppHTTPInterface{Port: 9090, Visibility: domain.AppVisibilityInternal},
183			)
184			mutate(spec)
185			err := spec.Validate()
186			require.Error(t, err)
187			assert.ErrorIs(t, err, domain.ErrInvalidAppSpec)
188			assert.Contains(t, err.Error(), "readiness.port is required")
189		})
190	}
191}

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