detached abad6c0

test(preview): add Update unit tests and deploy integration test

commit
abad6c010868b8d08da77710442ef6604280b5f9
author
brice · · 2026-03-18 19:42 UTC
committer
brice · · 2026-03-18 20:37 UTC
parent
03239fa
signature
unsigned
3 files · +137 -18
M internal/usecase/auto/preview/integration_test.go +90 -2
@@ -2,6 +2,7 @@ package preview
22
33 import (
44 "context"
5+ "fmt"
56 "testing"
67 "time"
78
@@ -11,11 +12,50 @@ import (
1112 "github.com/bnema/gordon/internal/domain"
1213 )
1314
15+type fakeDeployer struct {
16+ deployFn func(ctx context.Context, route domain.Route) (*domain.Container, error)
17+}
18+
19+func (f *fakeDeployer) Deploy(ctx context.Context, route domain.Route) (*domain.Container, error) {
20+ return f.deployFn(ctx, route)
21+}
22+
23+type fakeRouteManager struct {
24+ addRouteFn func(ctx context.Context, route domain.Route) error
25+ removeRouteFn func(ctx context.Context, domain string) error
26+ getRouteFn func(ctx context.Context, domain string) (*domain.Route, error)
27+}
28+
29+func (f *fakeRouteManager) AddRoute(ctx context.Context, route domain.Route) error {
30+ if f.addRouteFn != nil {
31+ return f.addRouteFn(ctx, route)
32+ }
33+ return nil
34+}
35+
36+func (f *fakeRouteManager) RemoveRoute(ctx context.Context, d string) error {
37+ if f.removeRouteFn != nil {
38+ return f.removeRouteFn(ctx, d)
39+ }
40+ return nil
41+}
42+
43+func (f *fakeRouteManager) GetRoute(ctx context.Context, d string) (*domain.Route, error) {
44+ if f.getRouteFn != nil {
45+ return f.getRouteFn(ctx, d)
46+ }
47+ return nil, fmt.Errorf("not found")
48+}
49+
50+func (f *fakeRouteManager) GetVolumeConfig() (bool, string, bool) {
51+ return true, "gordon", false
52+}
53+
1454 func TestPreviewLifecycle_Integration(t *testing.T) {
1555 store := &fakeStore{}
1656 svc := NewService(store, 2*time.Second)
1757
18- ctx := context.Background()
58+ ctx := t.Context()
1959
2060 // Create
2161 err := svc.CreatePreview(ctx, CreatePreviewRequest{
@@ -61,7 +101,7 @@ func TestPreviewLifecycle_CleanupExpired(t *testing.T) {
61101 store := &fakeStore{}
62102 svc := NewService(store, 1*time.Millisecond)
63103
64- ctx := context.Background()
104+ ctx := t.Context()
65105
66106 // Create with very short TTL
67107 err := svc.CreatePreview(ctx, CreatePreviewRequest{
@@ -91,3 +131,51 @@ func TestPreviewLifecycle_CleanupExpired(t *testing.T) {
91131 require.NoError(t, err)
92132 assert.Empty(t, all)
93133 }
134+
135+func TestPreviewLifecycle_WithDeploy(t *testing.T) {
136+ store := &fakeStore{}
137+ deployed := false
138+ routeAdded := ""
139+
140+ mockDeployer := &fakeDeployer{
141+ deployFn: func(_ context.Context, route domain.Route) (*domain.Container, error) {
142+ deployed = true
143+ return &domain.Container{Name: "gordon-" + route.Domain}, nil
144+ },
145+ }
146+ mockRouteManager := &fakeRouteManager{
147+ addRouteFn: func(_ context.Context, route domain.Route) error {
148+ routeAdded = route.Domain
149+ return nil
150+ },
151+ }
152+
153+ svc := NewService(store, 2*time.Second).
154+ WithDeployer(mockDeployer).
155+ WithRouteManager(mockRouteManager).
156+ WithRegistryDomain("reg.example.com")
157+
158+ ctx := t.Context()
159+ err := svc.CreatePreview(ctx, CreatePreviewRequest{
160+ Name: "test-feat",
161+ Domain: "myapp--test-feat.example.com",
162+ BaseRoute: "myapp.example.com",
163+ Image: "myapp:preview-test-feat",
164+ HTTPS: true,
165+ PreviewConfig: domain.PreviewConfig{
166+ TTL: 2 * time.Second,
167+ Separator: "--",
168+ DataCopy: false,
169+ },
170+ })
171+ require.NoError(t, err)
172+
173+ assert.True(t, deployed, "deployer should have been called")
174+ assert.Equal(t, "myapp--test-feat.example.com", routeAdded)
175+
176+ all, err := svc.List(ctx)
177+ require.NoError(t, err)
178+ require.Len(t, all, 1)
179+ assert.Equal(t, "running", all[0].Status)
180+ assert.Equal(t, []string{"gordon-myapp--test-feat.example.com"}, all[0].Containers)
181+}
@@ -2,6 +2,7 @@ package preview
2 2
3 import (3 import (
4 "context"4 "context"
5+ "fmt"
5 "testing"6 "testing"
6 "time"7 "time"
7 8
@@ -11,11 +12,50 @@ import (
11 "github.com/bnema/gordon/internal/domain"12 "github.com/bnema/gordon/internal/domain"
12 )13 )
13 14
15+type fakeDeployer struct {
16+ deployFn func(ctx context.Context, route domain.Route) (*domain.Container, error)
17+}
18+
19+func (f *fakeDeployer) Deploy(ctx context.Context, route domain.Route) (*domain.Container, error) {
20+ return f.deployFn(ctx, route)
21+}
22+
23+type fakeRouteManager struct {
24+ addRouteFn func(ctx context.Context, route domain.Route) error
25+ removeRouteFn func(ctx context.Context, domain string) error
26+ getRouteFn func(ctx context.Context, domain string) (*domain.Route, error)
27+}
28+
29+func (f *fakeRouteManager) AddRoute(ctx context.Context, route domain.Route) error {
30+ if f.addRouteFn != nil {
31+ return f.addRouteFn(ctx, route)
32+ }
33+ return nil
34+}
35+
36+func (f *fakeRouteManager) RemoveRoute(ctx context.Context, d string) error {
37+ if f.removeRouteFn != nil {
38+ return f.removeRouteFn(ctx, d)
39+ }
40+ return nil
41+}
42+
43+func (f *fakeRouteManager) GetRoute(ctx context.Context, d string) (*domain.Route, error) {
44+ if f.getRouteFn != nil {
45+ return f.getRouteFn(ctx, d)
46+ }
47+ return nil, fmt.Errorf("not found")
48+}
49+
50+func (f *fakeRouteManager) GetVolumeConfig() (bool, string, bool) {
51+ return true, "gordon", false
52+}
53+
14 func TestPreviewLifecycle_Integration(t *testing.T) {54 func TestPreviewLifecycle_Integration(t *testing.T) {
15 store := &fakeStore{}55 store := &fakeStore{}
16 svc := NewService(store, 2*time.Second)56 svc := NewService(store, 2*time.Second)
17 57
18- ctx := context.Background()58+ ctx := t.Context()
19 59
20 // Create60 // Create
21 err := svc.CreatePreview(ctx, CreatePreviewRequest{61 err := svc.CreatePreview(ctx, CreatePreviewRequest{
@@ -61,7 +101,7 @@ func TestPreviewLifecycle_CleanupExpired(t *testing.T) {
61 store := &fakeStore{}101 store := &fakeStore{}
62 svc := NewService(store, 1*time.Millisecond)102 svc := NewService(store, 1*time.Millisecond)
63 103
64- ctx := context.Background()104+ ctx := t.Context()
65 105
66 // Create with very short TTL106 // Create with very short TTL
67 err := svc.CreatePreview(ctx, CreatePreviewRequest{107 err := svc.CreatePreview(ctx, CreatePreviewRequest{
@@ -91,3 +131,51 @@ func TestPreviewLifecycle_CleanupExpired(t *testing.T) {
91 require.NoError(t, err)131 require.NoError(t, err)
92 assert.Empty(t, all)132 assert.Empty(t, all)
93 }133 }
134+
135+func TestPreviewLifecycle_WithDeploy(t *testing.T) {
136+ store := &fakeStore{}
137+ deployed := false
138+ routeAdded := ""
139+
140+ mockDeployer := &fakeDeployer{
141+ deployFn: func(_ context.Context, route domain.Route) (*domain.Container, error) {
142+ deployed = true
143+ return &domain.Container{Name: "gordon-" + route.Domain}, nil
144+ },
145+ }
146+ mockRouteManager := &fakeRouteManager{
147+ addRouteFn: func(_ context.Context, route domain.Route) error {
148+ routeAdded = route.Domain
149+ return nil
150+ },
151+ }
152+
153+ svc := NewService(store, 2*time.Second).
154+ WithDeployer(mockDeployer).
155+ WithRouteManager(mockRouteManager).
156+ WithRegistryDomain("reg.example.com")
157+
158+ ctx := t.Context()
159+ err := svc.CreatePreview(ctx, CreatePreviewRequest{
160+ Name: "test-feat",
161+ Domain: "myapp--test-feat.example.com",
162+ BaseRoute: "myapp.example.com",
163+ Image: "myapp:preview-test-feat",
164+ HTTPS: true,
165+ PreviewConfig: domain.PreviewConfig{
166+ TTL: 2 * time.Second,
167+ Separator: "--",
168+ DataCopy: false,
169+ },
170+ })
171+ require.NoError(t, err)
172+
173+ assert.True(t, deployed, "deployer should have been called")
174+ assert.Equal(t, "myapp--test-feat.example.com", routeAdded)
175+
176+ all, err := svc.List(ctx)
177+ require.NoError(t, err)
178+ require.Len(t, all, 1)
179+ assert.Equal(t, "running", all[0].Status)
180+ assert.Equal(t, []string{"gordon-myapp--test-feat.example.com"}, all[0].Containers)
181+}
M internal/usecase/auto/preview/service_test.go +44 -12
@@ -33,10 +33,10 @@ func TestPreviewService_Add(t *testing.T) {
3333 BaseRoute: "myapp.example.com",
3434 Image: "myapp:preview-feat",
3535 }
36- err := svc.Add(context.Background(), p)
36+ err := svc.Add(t.Context(), p)
3737 require.NoError(t, err)
3838
39- all, err := svc.List(context.Background())
39+ all, err := svc.List(t.Context())
4040 require.NoError(t, err)
4141 assert.Len(t, all, 1)
4242 assert.Equal(t, "feat", all[0].Name)
@@ -49,12 +49,12 @@ func TestPreviewService_Delete(t *testing.T) {
4949 },
5050 }
5151 svc := NewService(store, 48*time.Hour)
52- require.NoError(t, svc.Load(context.Background()))
52+ require.NoError(t, svc.Load(t.Context()))
5353
54- err := svc.Delete(context.Background(), "feat")
54+ err := svc.Delete(t.Context(), "feat")
5555 require.NoError(t, err)
5656
57- all, err := svc.List(context.Background())
57+ all, err := svc.List(t.Context())
5858 require.NoError(t, err)
5959 assert.Empty(t, all)
6060 }
@@ -66,13 +66,13 @@ func TestPreviewService_Get(t *testing.T) {
6666 },
6767 }
6868 svc := NewService(store, 48*time.Hour)
69- require.NoError(t, svc.Load(context.Background()))
69+ require.NoError(t, svc.Load(t.Context()))
7070
71- p, err := svc.Get(context.Background(), "feat")
71+ p, err := svc.Get(t.Context(), "feat")
7272 require.NoError(t, err)
7373 assert.Equal(t, "myapp--feat.example.com", p.Domain)
7474
75- _, err = svc.Get(context.Background(), "nonexistent")
75+ _, err = svc.Get(t.Context(), "nonexistent")
7676 assert.Error(t, err)
7777 }
7878
@@ -84,12 +84,12 @@ func TestPreviewService_Extend(t *testing.T) {
8484 },
8585 }
8686 svc := NewService(store, 48*time.Hour)
87- require.NoError(t, svc.Load(context.Background()))
87+ require.NoError(t, svc.Load(t.Context()))
8888
89- err := svc.Extend(context.Background(), "feat", 24*time.Hour)
89+ err := svc.Extend(t.Context(), "feat", 24*time.Hour)
9090 require.NoError(t, err)
9191
92- p, _ := svc.Get(context.Background(), "feat")
92+ p, _ := svc.Get(t.Context(), "feat")
9393 assert.True(t, p.ExpiresAt.After(now.Add(23*time.Hour)))
9494 }
9595
@@ -101,9 +101,41 @@ func TestPreviewService_GetExpired(t *testing.T) {
101101 },
102102 }
103103 svc := NewService(store, 48*time.Hour)
104- require.NoError(t, svc.Load(context.Background()))
104+ require.NoError(t, svc.Load(t.Context()))
105105
106106 expired := svc.GetExpired()
107107 assert.Len(t, expired, 1)
108108 assert.Equal(t, "expired", expired[0].Name)
109109 }
110+
111+func TestPreviewService_Update(t *testing.T) {
112+ store := &fakeStore{
113+ previews: []domain.PreviewRoute{
114+ {Name: "feat", Domain: "myapp--feat.example.com", Status: "deploying"},
115+ },
116+ }
117+ svc := NewService(store, 48*time.Hour)
118+ require.NoError(t, svc.Load(t.Context()))
119+
120+ updated := domain.PreviewRoute{
121+ Name: "feat",
122+ Domain: "myapp--feat.example.com",
123+ Status: "running",
124+ Containers: []string{"gordon-myapp--feat.example.com"},
125+ }
126+ err := svc.Update(t.Context(), updated)
127+ require.NoError(t, err)
128+
129+ p, err := svc.Get(t.Context(), "feat")
130+ require.NoError(t, err)
131+ assert.Equal(t, "running", p.Status)
132+ assert.Equal(t, []string{"gordon-myapp--feat.example.com"}, p.Containers)
133+}
134+
135+func TestPreviewService_Update_NotFound(t *testing.T) {
136+ store := &fakeStore{}
137+ svc := NewService(store, 48*time.Hour)
138+
139+ err := svc.Update(t.Context(), domain.PreviewRoute{Name: "nonexistent"})
140+ assert.ErrorIs(t, err, domain.ErrPreviewNotFound)
141+}
@@ -33,10 +33,10 @@ func TestPreviewService_Add(t *testing.T) {
33 BaseRoute: "myapp.example.com",33 BaseRoute: "myapp.example.com",
34 Image: "myapp:preview-feat",34 Image: "myapp:preview-feat",
35 }35 }
36- err := svc.Add(context.Background(), p)36+ err := svc.Add(t.Context(), p)
37 require.NoError(t, err)37 require.NoError(t, err)
38 38
39- all, err := svc.List(context.Background())39+ all, err := svc.List(t.Context())
40 require.NoError(t, err)40 require.NoError(t, err)
41 assert.Len(t, all, 1)41 assert.Len(t, all, 1)
42 assert.Equal(t, "feat", all[0].Name)42 assert.Equal(t, "feat", all[0].Name)
@@ -49,12 +49,12 @@ func TestPreviewService_Delete(t *testing.T) {
49 },49 },
50 }50 }
51 svc := NewService(store, 48*time.Hour)51 svc := NewService(store, 48*time.Hour)
52- require.NoError(t, svc.Load(context.Background()))52+ require.NoError(t, svc.Load(t.Context()))
53 53
54- err := svc.Delete(context.Background(), "feat")54+ err := svc.Delete(t.Context(), "feat")
55 require.NoError(t, err)55 require.NoError(t, err)
56 56
57- all, err := svc.List(context.Background())57+ all, err := svc.List(t.Context())
58 require.NoError(t, err)58 require.NoError(t, err)
59 assert.Empty(t, all)59 assert.Empty(t, all)
60 }60 }
@@ -66,13 +66,13 @@ func TestPreviewService_Get(t *testing.T) {
66 },66 },
67 }67 }
68 svc := NewService(store, 48*time.Hour)68 svc := NewService(store, 48*time.Hour)
69- require.NoError(t, svc.Load(context.Background()))69+ require.NoError(t, svc.Load(t.Context()))
70 70
71- p, err := svc.Get(context.Background(), "feat")71+ p, err := svc.Get(t.Context(), "feat")
72 require.NoError(t, err)72 require.NoError(t, err)
73 assert.Equal(t, "myapp--feat.example.com", p.Domain)73 assert.Equal(t, "myapp--feat.example.com", p.Domain)
74 74
75- _, err = svc.Get(context.Background(), "nonexistent")75+ _, err = svc.Get(t.Context(), "nonexistent")
76 assert.Error(t, err)76 assert.Error(t, err)
77 }77 }
78 78
@@ -84,12 +84,12 @@ func TestPreviewService_Extend(t *testing.T) {
84 },84 },
85 }85 }
86 svc := NewService(store, 48*time.Hour)86 svc := NewService(store, 48*time.Hour)
87- require.NoError(t, svc.Load(context.Background()))87+ require.NoError(t, svc.Load(t.Context()))
88 88
89- err := svc.Extend(context.Background(), "feat", 24*time.Hour)89+ err := svc.Extend(t.Context(), "feat", 24*time.Hour)
90 require.NoError(t, err)90 require.NoError(t, err)
91 91
92- p, _ := svc.Get(context.Background(), "feat")92+ p, _ := svc.Get(t.Context(), "feat")
93 assert.True(t, p.ExpiresAt.After(now.Add(23*time.Hour)))93 assert.True(t, p.ExpiresAt.After(now.Add(23*time.Hour)))
94 }94 }
95 95
@@ -101,9 +101,41 @@ func TestPreviewService_GetExpired(t *testing.T) {
101 },101 },
102 }102 }
103 svc := NewService(store, 48*time.Hour)103 svc := NewService(store, 48*time.Hour)
104- require.NoError(t, svc.Load(context.Background()))104+ require.NoError(t, svc.Load(t.Context()))
105 105
106 expired := svc.GetExpired()106 expired := svc.GetExpired()
107 assert.Len(t, expired, 1)107 assert.Len(t, expired, 1)
108 assert.Equal(t, "expired", expired[0].Name)108 assert.Equal(t, "expired", expired[0].Name)
109 }109 }
110+
111+func TestPreviewService_Update(t *testing.T) {
112+ store := &fakeStore{
113+ previews: []domain.PreviewRoute{
114+ {Name: "feat", Domain: "myapp--feat.example.com", Status: "deploying"},
115+ },
116+ }
117+ svc := NewService(store, 48*time.Hour)
118+ require.NoError(t, svc.Load(t.Context()))
119+
120+ updated := domain.PreviewRoute{
121+ Name: "feat",
122+ Domain: "myapp--feat.example.com",
123+ Status: "running",
124+ Containers: []string{"gordon-myapp--feat.example.com"},
125+ }
126+ err := svc.Update(t.Context(), updated)
127+ require.NoError(t, err)
128+
129+ p, err := svc.Get(t.Context(), "feat")
130+ require.NoError(t, err)
131+ assert.Equal(t, "running", p.Status)
132+ assert.Equal(t, []string{"gordon-myapp--feat.example.com"}, p.Containers)
133+}
134+
135+func TestPreviewService_Update_NotFound(t *testing.T) {
136+ store := &fakeStore{}
137+ svc := NewService(store, 48*time.Hour)
138+
139+ err := svc.Update(t.Context(), domain.PreviewRoute{Name: "nonexistent"})
140+ assert.ErrorIs(t, err, domain.ErrPreviewNotFound)
141+}
M internal/usecase/auto/preview/ticker_test.go +3 -4
@@ -1,7 +1,6 @@
11 package preview
22
33 import (
4- "context"
54 "testing"
65 "time"
76
@@ -19,13 +18,13 @@ func TestPreviewService_CleanupExpired(t *testing.T) {
1918 },
2019 }
2120 svc := NewService(store, 48*time.Hour)
22- require.NoError(t, svc.Load(context.Background()))
21+ require.NoError(t, svc.Load(t.Context()))
2322
24- expired := svc.CleanupExpired(context.Background())
23+ expired := svc.CleanupExpired(t.Context())
2524 assert.Len(t, expired, 1)
2625 assert.Equal(t, "expired", expired[0].Name)
2726
28- all, err := svc.List(context.Background())
27+ all, err := svc.List(t.Context())
2928 require.NoError(t, err)
3029 assert.Len(t, all, 1)
3130 assert.Equal(t, "active", all[0].Name)
@@ -1,7 +1,6 @@
1 package preview1 package preview
2 2
3 import (3 import (
4- "context"
5 "testing"4 "testing"
6 "time"5 "time"
7 6
@@ -19,13 +18,13 @@ func TestPreviewService_CleanupExpired(t *testing.T) {
19 },18 },
20 }19 }
21 svc := NewService(store, 48*time.Hour)20 svc := NewService(store, 48*time.Hour)
22- require.NoError(t, svc.Load(context.Background()))21+ require.NoError(t, svc.Load(t.Context()))
23 22
24- expired := svc.CleanupExpired(context.Background())23+ expired := svc.CleanupExpired(t.Context())
25 assert.Len(t, expired, 1)24 assert.Len(t, expired, 1)
26 assert.Equal(t, "expired", expired[0].Name)25 assert.Equal(t, "expired", expired[0].Name)
27 26
28- all, err := svc.List(context.Background())27+ all, err := svc.List(t.Context())
29 require.NoError(t, err)28 require.NoError(t, err)
30 assert.Len(t, all, 1)29 assert.Len(t, all, 1)
31 assert.Equal(t, "active", all[0].Name)30 assert.Equal(t, "active", all[0].Name)

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