1package apps_test
2
3import (
4 "context"
5 "testing"
6
7 "github.com/bnema/zerowrap"
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/mock"
10 "github.com/stretchr/testify/require"
11
12 "github.com/bnema/gordon/internal/domain"
13 "github.com/bnema/gordon/internal/usecase/apps"
14 "github.com/bnema/gordon/internal/usecase/deployment"
15)
16
17func scopeDesired(names ...string) domain.AppDesiredRevision {
18 rev := domain.AppDesiredRevision{Revision: "rev-1", App: "blog"}
19 for _, name := range names {
20 rev.Spec.Services = append(rev.Spec.Services, domain.AppService{Name: name})
21 }
22 return rev
23}
24
25func scopeRevision(revision string, names ...string) domain.AppDesiredRevision {
26 rev := scopeDesired(names...)
27 rev.Revision = revision
28 return rev
29}
30
31func scopeActive(names ...string) domain.AppActive {
32 active := domain.AppActive{App: "blog", Services: map[string]domain.AppEffectiveService{}}
33 for _, name := range names {
34 active.Services[name] = domain.AppEffectiveService{}
35 }
36 return active
37}
38
39func TestAppServiceImpl_Deploy_MultiServiceRequiresScope(t *testing.T) {
40 store := newMockAppState(t)
41 deploy := newMockDeployEngine(t)
42 svc := apps.NewAppServiceImpl(store, deploy, newMockSecretWriter(t), zerowrap.Default())
43 t.Cleanup(func() { require.NoError(t, svc.Shutdown(context.Background())) })
44
45 // A first deploy has no ACTIVE yet: desired services still count.
46 store.EXPECT().LoadDesired(mock.Anything, "blog").Return(scopeDesired("web", "db"), true, nil)
47 store.EXPECT().LoadActive(mock.Anything, "blog").Return(domain.AppActive{}, false, nil)
48 deploy.startDeployFn = func(context.Context, deployment.DeployInput) (*deployment.StartDeployResult, error) {
49 t.Fatal("no claim may be made without a service scope")
50 return nil, nil
51 }
52
53 op, err := svc.Deploy(context.Background(), "blog", "", "", false, "key-1")
54 require.ErrorIs(t, err, domain.ErrAppServiceScope)
55 assert.Nil(t, op)
56 assert.Contains(t, err.Error(), "app blog has several services (db, web): pass --service NAME or --all")
57}
58
59func TestAppServiceImpl_Deploy_OldRevisionWithMultipleServicesRequiresScope(t *testing.T) {
60 store := newMockAppState(t)
61 deploy := newMockDeployEngine(t)
62 svc := apps.NewAppServiceImpl(store, deploy, newMockSecretWriter(t), zerowrap.Default())
63 t.Cleanup(func() { require.NoError(t, svc.Shutdown(context.Background())) })
64
65 // The head and ACTIVE know only one service, but the revision the
66 // deploy would activate declares two: the scope check must count the
67 // selected revision, not the head.
68 store.EXPECT().LoadRevision(mock.Anything, "blog", "rev-2").Return(scopeRevision("rev-2", "web", "db"), nil)
69 store.EXPECT().LoadActive(mock.Anything, "blog").Return(scopeActive("web"), true, nil)
70 deploy.startDeployFn = func(context.Context, deployment.DeployInput) (*deployment.StartDeployResult, error) {
71 t.Fatal("no claim may be made without a service scope")
72 return nil, nil
73 }
74
75 op, err := svc.Deploy(context.Background(), "blog", "rev-2", "", false, "key-1")
76 require.ErrorIs(t, err, domain.ErrAppServiceScope)
77 assert.Nil(t, op)
78 assert.Contains(t, err.Error(), "app blog has several services (db, web): pass --service NAME or --all")
79}
80
81func TestAppServiceImpl_Deploy_SingleServiceNeedsNoScope(t *testing.T) {
82 store := newMockAppState(t)
83 deploy := newMockDeployEngine(t)
84 svc := apps.NewAppServiceImpl(store, deploy, newMockSecretWriter(t), zerowrap.Default())
85 t.Cleanup(func() { require.NoError(t, svc.Shutdown(context.Background())) })
86
87 store.EXPECT().LoadDesired(mock.Anything, "blog").Return(scopeDesired("web"), true, nil)
88 store.EXPECT().LoadActive(mock.Anything, "blog").Return(scopeActive("web"), true, nil)
89 started := make(chan struct{}, 1)
90 deploy.startDeployFn = func(context.Context, deployment.DeployInput) (*deployment.StartDeployResult, error) {
91 started <- struct{}{}
92 return nil, domain.ErrAppStateConflict
93 }
94
95 _, err := svc.Deploy(context.Background(), "blog", "", "", false, "key-1")
96 require.ErrorIs(t, err, domain.ErrAppStateConflict)
97 require.Len(t, started, 1, "a single-service app deploys without --service or --all")
98}
99
100func TestAppServiceImpl_Deploy_AllSkipsScopeCheck(t *testing.T) {
101 store := newMockAppState(t)
102 deploy := newMockDeployEngine(t)
103 svc := apps.NewAppServiceImpl(store, deploy, newMockSecretWriter(t), zerowrap.Default())
104 t.Cleanup(func() { require.NoError(t, svc.Shutdown(context.Background())) })
105
106 var got deployment.DeployInput
107 deploy.startDeployFn = func(_ context.Context, input deployment.DeployInput) (*deployment.StartDeployResult, error) {
108 got = input
109 return nil, domain.ErrAppStateConflict
110 }
111
112 _, err := svc.Deploy(context.Background(), "blog", "", "", true, "key-1")
113 require.ErrorIs(t, err, domain.ErrAppStateConflict)
114 assert.Equal(t, deployment.DeployInput{App: "blog", Op: "key-1"}, got, "--all deploys every service")
115}
116
117func TestAppServiceImpl_Deploy_AllAndServiceAreExclusive(t *testing.T) {
118 svc := apps.NewAppServiceImpl(newMockAppState(t), newMockDeployEngine(t), newMockSecretWriter(t), zerowrap.Default())
119 t.Cleanup(func() { require.NoError(t, svc.Shutdown(context.Background())) })
120
121 _, err := svc.Deploy(context.Background(), "blog", "", "web", true, "key-1")
122 require.ErrorIs(t, err, domain.ErrAppServiceScope)
123}
124
125func TestAppServiceImpl_Restart_MultiServiceRequiresScope(t *testing.T) {
126 store := newMockAppState(t)
127 svc := apps.NewAppServiceImpl(store, newMockDeployEngine(t), newMockSecretWriter(t), zerowrap.Default())
128 t.Cleanup(func() { require.NoError(t, svc.Shutdown(context.Background())) })
129
130 store.EXPECT().LoadActive(mock.Anything, "blog").Return(scopeActive("web", "db"), true, nil)
131
132 // The engine mock has no Restart expectation: reaching it fails the test.
133 op, err := svc.Restart(context.Background(), "blog", "", false, "key-1")
134 require.ErrorIs(t, err, domain.ErrAppServiceScope)
135 assert.Nil(t, op)
136 assert.Contains(t, err.Error(), "(db, web)")
137}
138
139func TestAppServiceImpl_Restart_AllAndServiceAreExclusive(t *testing.T) {
140 svc := apps.NewAppServiceImpl(newMockAppState(t), newMockDeployEngine(t), newMockSecretWriter(t), zerowrap.Default())
141 t.Cleanup(func() { require.NoError(t, svc.Shutdown(context.Background())) })
142
143 _, err := svc.Restart(context.Background(), "blog", "web", true, "key-1")
144 require.ErrorIs(t, err, domain.ErrAppServiceScope)
145}