v3.0.0-alpha.1 6fdddb8

app_service_test.go RawHistoryFolder

1package apps_test
2
3import (
4	"context"
5	"errors"
6	"testing"
7	"time"
8
9	"github.com/bnema/zerowrap"
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/mock"
12	"github.com/stretchr/testify/require"
13
14	"github.com/bnema/gordon/internal/domain"
15	"github.com/bnema/gordon/internal/usecase/apps"
16)
17
18func secretSpec() domain.AppSpec {
19	return domain.AppSpec{
20		Name: "blog",
21		Services: []domain.AppService{
22			{
23				Name:      "web",
24				Image:     "img:1",
25				StopGrace: 10 * time.Second,
26				Readiness: domain.AppReadiness{Type: "none", Timeout: 30 * time.Second},
27				HTTP:      []domain.AppHTTPInterface{{Host: "blog.example.com", Port: 8080, TLS: "auto"}},
28				Secrets:   map[string]string{"DATABASE_URL": "database-url"},
29			},
30		},
31	}
32}
33
34func TestAppServiceImpl_SecretsRoundTrip(t *testing.T) {
35	ctx := context.Background()
36	store := newMockAppState(t)
37	deploy := newMockDeployEngine(t)
38	secrets := newMockSecretWriter(t)
39
40	// Apply persists desired state through the store mock.
41	store.EXPECT().Recover(mock.Anything).Return(nil).Once()
42	store.EXPECT().LoadCheckpoint(mock.Anything).Return(domain.AppStoreCheckpoint{}, nil).Once()
43	store.EXPECT().LoadDesired(mock.Anything, "blog").Return(domain.AppDesiredRevision{}, false, nil).Once()
44	store.EXPECT().LoadActive(mock.Anything, "blog").Return(domain.AppActive{}, false, nil).Once()
45	store.EXPECT().StageApply(mock.Anything, mock.Anything).Return(nil).Once()
46	store.EXPECT().CommitApply(mock.Anything, "blog", mock.Anything).Return(nil).Once()
47	store.EXPECT().MaterializeApply(mock.Anything, "blog", mock.Anything).Return(nil).Once()
48	store.EXPECT().CollectGarbage(mock.Anything, "blog", mock.Anything).Return(nil).Once()
49
50	svc := apps.NewAppServiceImpl(store, deploy, secrets, zerowrap.Default())
51	_, _, err := svc.Apply(ctx, secretSpec(), []byte("m"), false)
52	require.NoError(t, err)
53
54	// SetSecrets resolves registered names from desired state.
55	desired := domain.AppDesiredRevision{App: "blog", Revision: "rev-1", Spec: secretSpec()}
56	store.EXPECT().LoadDesired(mock.Anything, "blog").Return(desired, true, nil).Once()
57	store.EXPECT().LoadActive(mock.Anything, "blog").Return(domain.AppActive{}, false, nil).Once()
58	store.EXPECT().LoadOwnership(mock.Anything, "blog").Return(domain.AppOwnership{App: "blog", ID: "app-uuid-1"}, nil).Once()
59	secrets.EXPECT().SetSecret(mock.Anything, "gordon/apps/app-uuid-1/web/database-url", "v").Return(nil).Once()
60	require.NoError(t, svc.SetSecrets(ctx, "blog", "web", map[string]string{"DATABASE_URL": "v"}))
61
62	// Delete refused while still referenced (names + reference check).
63	desiredWithSecret := desired
64	store.EXPECT().LoadDesired(mock.Anything, "blog").Return(desiredWithSecret, true, nil).Once()
65	store.EXPECT().LoadActive(mock.Anything, "blog").Return(domain.AppActive{}, false, nil).Once()
66	store.EXPECT().LoadDesired(mock.Anything, "blog").Return(desiredWithSecret, true, nil).Once()
67	require.ErrorIs(t, svc.DeleteSecret(ctx, "blog", "web", "DATABASE_URL"), domain.ErrAppStateConflict)
68
69	// Unknown key refused.
70	store.EXPECT().LoadDesired(mock.Anything, "blog").Return(desired, true, nil).Once()
71	store.EXPECT().LoadActive(mock.Anything, "blog").Return(domain.AppActive{}, false, nil).Once()
72	require.ErrorIs(t, svc.SetSecrets(ctx, "blog", "web", map[string]string{"NOPE": "v"}), domain.ErrAppSecretMissing)
73}
74
75func TestAppServiceImpl_OperationByKey(t *testing.T) {
76	ctx := context.Background()
77	store := newMockAppState(t)
78	deploy := newMockDeployEngine(t)
79	secrets := newMockSecretWriter(t)
80	svc := apps.NewAppServiceImpl(store, deploy, secrets, zerowrap.Default())
81
82	op := domain.AppOperation{Op: "op-1", Kind: "deploy", App: "blog", StartedAt: time.Now().UTC()}
83	store.EXPECT().LoadOperation(mock.Anything, "blog", "op-1").Return(op, nil).Once()
84	loaded, err := svc.OperationByKey(ctx, "blog", "op-1")
85	require.NoError(t, err)
86	assert.Equal(t, "op-1", loaded.Op)
87
88	store.EXPECT().LoadOperation(mock.Anything, "blog", "missing").Return(domain.AppOperation{}, domain.ErrAppOperationNotFound).Once()
89	_, err = svc.OperationByKey(ctx, "blog", "missing")
90	require.ErrorIs(t, err, domain.ErrAppOperationNotFound)
91}
92
93func TestAppServiceImpl_ListSecrets_MissingAppNotFound(t *testing.T) {
94	ctx := context.Background()
95	store := newMockAppState(t)
96	svc := apps.NewAppServiceImpl(store, newMockDeployEngine(t), newMockSecretWriter(t), zerowrap.Default())
97
98	store.EXPECT().AppExists(mock.Anything, "blog").Return(false, nil).Once()
99	_, err := svc.ListSecrets(ctx, "blog", "")
100	require.ErrorIs(t, err, domain.ErrAppNotFound)
101}
102
103func TestAppServiceImpl_ListSecrets_OwnershipOnlyAppStaysLive(t *testing.T) {
104	ctx := context.Background()
105	store := newMockAppState(t)
106	svc := apps.NewAppServiceImpl(store, newMockDeployEngine(t), newMockSecretWriter(t), zerowrap.Default())
107
108	store.EXPECT().AppExists(mock.Anything, "blog").Return(true, nil).Once()
109	store.EXPECT().LoadDesired(mock.Anything, "blog").Return(domain.AppDesiredRevision{}, false, nil).Once()
110	store.EXPECT().LoadActive(mock.Anything, "blog").Return(domain.AppActive{}, false, nil).Once()
111	entries, err := svc.ListSecrets(ctx, "blog", "")
112	require.NoError(t, err)
113	assert.Empty(t, entries)
114}
115
116func TestAppServiceImpl_ListSecrets_WrapsLoadErrors(t *testing.T) {
117	ctx := context.Background()
118	store := newMockAppState(t)
119	svc := apps.NewAppServiceImpl(store, newMockDeployEngine(t), newMockSecretWriter(t), zerowrap.Default())
120
121	store.EXPECT().AppExists(mock.Anything, "blog").Return(true, nil).Once()
122	store.EXPECT().LoadDesired(mock.Anything, "blog").Return(domain.AppDesiredRevision{}, false, errors.New("boom")).Once()
123	_, err := svc.ListSecrets(ctx, "blog", "")
124	require.ErrorContains(t, err, "load desired")
125	require.ErrorContains(t, err, "boom")
126}

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