v3.0.0-alpha.1 6fdddb8

service_test.go RawHistoryFolder

1package deployment_test
2
3import (
4	"context"
5	"testing"
6	"time"
7
8	"github.com/bnema/zerowrap"
9	"github.com/stretchr/testify/assert"
10	"github.com/stretchr/testify/mock"
11	"github.com/stretchr/testify/require"
12
13	outmocks "github.com/bnema/gordon/internal/boundaries/out/mocks"
14	"github.com/bnema/gordon/internal/domain"
15	"github.com/bnema/gordon/internal/usecase/deployment"
16)
17
18func testRevision(app string, services ...domain.AppService) domain.AppDesiredRevision {
19	for i := range services {
20		if services[i].StopGrace == 0 {
21			services[i].StopGrace = 10 * time.Second
22		}
23		if services[i].Readiness.Timeout == 0 {
24			services[i].Readiness.Timeout = 30 * time.Second
25		}
26	}
27	return domain.AppDesiredRevision{
28		Revision: "rev-1", App: app,
29		Spec: domain.AppSpec{Name: app, Env: map[string]string{}, Services: services},
30	}
31}
32
33func webService() domain.AppService {
34	return domain.AppService{
35		Name: "web", Image: "docker.io/example/web:1.4.2",
36		Readiness: domain.AppReadiness{Type: "http", Path: "/healthz"},
37		HTTP:      []domain.AppHTTPInterface{{Host: "blog.example.com", Port: 8080, TLS: "auto"}},
38		Secrets:   map[string]string{"DATABASE_URL": "database-url"},
39	}
40}
41
42func preflightService(
43	t *testing.T,
44	state *outmocks.MockAppState,
45	runtime *outmocks.MockContainerRuntime,
46	images *outmocks.MockImageResolver,
47	secrets *outmocks.MockSecretProvider,
48) *deployment.Service {
49	t.Helper()
50	// Every mutation claims a journal only when no other operation of the
51	// app is unfinished. These tests start clean.
52	state.EXPECT().LoadLatestOperation(mock.Anything, mock.Anything).
53		Return(domain.AppOperation{}, false, nil).Maybe()
54	return deployment.NewService(deployment.Deps{
55		State: state, Runtime: runtime, Images: images, Secrets: secrets,
56		ImagePolicy: domain.ImageSourcePolicy{AllowedRegistries: []string{"registry.example.com"}},
57	}, zerowrap.Default()).WithProbeDeps(deployment.NewTestProbeDeps(runtime,
58		func(context.Context, string, string) (int, error) { return 200, nil },
59		func(context.Context, string) error { return nil },
60	))
61}
62
63func TestComputeOutcome_TerminalResults(t *testing.T) {
64	deployed := deployment.ServiceResult{Result: "deployed"}
65	failed := deployment.ServiceResult{Result: "failed", Error: "boom"}
66	assert.Equal(t, "success", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": deployed}))
67	assert.Equal(t, "failed", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": failed}))
68	assert.Equal(t, "partial", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": deployed, "b": failed}))
69	assert.Equal(t, "failed", deployment.ComputeOutcome(map[string]deployment.ServiceResult{}))
70}
71
72// TestReconcileBoot_ContinuesAfterAppFailure proves R1: a failing app
73// never blocks verification of the following apps, and failures are
74// aggregated instead of aborting at the first one.
75func TestReconcileBoot_ContinuesAfterAppFailure(t *testing.T) {
76	ctx := context.Background()
77	state := outmocks.NewMockAppState(t)
78	runtime := outmocks.NewMockContainerRuntime(t)
79	images := outmocks.NewMockImageResolver(t)
80	secrets := outmocks.NewMockSecretProvider(t)
81	svcSvc := preflightService(t, state, runtime, images, secrets)
82
83	badEff := domain.AppEffectiveService{
84		Container:    "ctr-bad",
85		Spec:         webService(),
86		BackendBinds: map[int]int{8080: 32768},
87	}
88	badActive := domain.AppActive{App: "bad", Services: map[string]domain.AppEffectiveService{"web": badEff}}
89	goodEff := domain.AppEffectiveService{
90		Container: "ctr-good",
91		Spec:      webService(),
92	}
93	goodActive := domain.AppActive{App: "good", Services: map[string]domain.AppEffectiveService{"web": goodEff}}
94
95	state.EXPECT().Recover(mock.Anything).Return(nil)
96	state.EXPECT().ListApps(mock.Anything).Return([]string{"bad", "good"}, nil).Once()
97	// bad app: Start fails bind verification, binds withdrawn.
98	state.EXPECT().LoadIntent(mock.Anything, "bad").Return(domain.AppStopIntent{App: "bad"}, nil).Once()
99	state.EXPECT().LoadActive(mock.Anything, "bad").Return(badActive, true, nil)
100	state.EXPECT().LoadLatestOperation(mock.Anything, "bad").Return(domain.AppOperation{}, false, nil).Maybe()
101	state.EXPECT().SaveOperation(mock.Anything, mock.Anything).Return(nil)
102	state.EXPECT().SaveIntent(mock.Anything, mock.Anything).Return(nil)
103	state.EXPECT().LoadRecoveryInhibitions(mock.Anything, "bad").Return(nil, nil).Once()
104	runtime.EXPECT().IsContainerRunning(mock.Anything, "ctr-bad").Return(true, nil).Once()
105	runtime.EXPECT().GetContainerBackendBinds(mock.Anything, "ctr-bad", mock.Anything).Return(nil, assert.AnError).Once()
106	// No traffic boundary is wired here, so the fail-closed bind
107	// withdrawal is a no-op and deployment writes no ACTIVE binds itself.
108	// good app: still verified after the bad one failed.
109	state.EXPECT().LoadIntent(mock.Anything, "good").Return(domain.AppStopIntent{App: "good"}, nil).Once()
110	// Start load + bind-refresh reload.
111	state.EXPECT().LoadActive(mock.Anything, "good").Return(goodActive, true, nil)
112	state.EXPECT().LoadLatestOperation(mock.Anything, "good").Return(domain.AppOperation{}, false, nil).Maybe()
113	state.EXPECT().LoadRecoveryInhibitions(mock.Anything, "good").Return(nil, nil).Once()
114	runtime.EXPECT().IsContainerRunning(mock.Anything, "ctr-good").Return(true, nil).Once()
115	runtime.EXPECT().GetContainerBackendBinds(mock.Anything, "ctr-good", mock.Anything).Return([]domain.ContainerBackendBind{{ContainerPort: 8080, HostPort: 32777, Protocol: domain.NetworkProtocolTCP}}, nil).Once()
116	state.EXPECT().RegisterBackendBinds(mock.Anything, mock.Anything).Return(nil).Once()
117	// Bind refresh persists the new bind (differs from recorded nil).
118	state.EXPECT().SaveActive(mock.Anything, mock.Anything).Return(nil).Once()
119
120	err := svcSvc.ReconcileBoot(ctx)
121	require.Error(t, err)
122	assert.Contains(t, err.Error(), `"bad"`)
123}

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