1package preview
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10
11 "github.com/bnema/gordon/internal/domain"
12)
13
14type fakeStore struct {
15 previews []domain.PreviewRoute
16}
17
18func (f *fakeStore) Load(_ context.Context) ([]domain.PreviewRoute, error) {
19 return f.previews, nil
20}
21func (f *fakeStore) Save(_ context.Context, p []domain.PreviewRoute) error {
22 f.previews = p
23 return nil
24}
25
26func TestPreviewService_Add(t *testing.T) {
27 store := &fakeStore{}
28 svc := NewService(store, 48*time.Hour)
29
30 p := domain.PreviewRoute{
31 Domain: "myapp--feat.example.com",
32 Name: "feat",
33 BaseRoute: "myapp.example.com",
34 Image: "myapp:preview-feat",
35 }
36 err := svc.Add(t.Context(), p)
37 require.NoError(t, err)
38
39 all, err := svc.List(t.Context())
40 require.NoError(t, err)
41 assert.Len(t, all, 1)
42 assert.Equal(t, "feat", all[0].Name)
43}
44
45func TestPreviewService_Delete(t *testing.T) {
46 store := &fakeStore{
47 previews: []domain.PreviewRoute{
48 {Name: "feat", Domain: "myapp--feat.example.com"},
49 },
50 }
51 svc := NewService(store, 48*time.Hour)
52 require.NoError(t, svc.Load(t.Context()))
53
54 err := svc.Delete(t.Context(), "feat")
55 require.NoError(t, err)
56
57 all, err := svc.List(t.Context())
58 require.NoError(t, err)
59 assert.Empty(t, all)
60}
61
62func TestPreviewService_Get(t *testing.T) {
63 store := &fakeStore{
64 previews: []domain.PreviewRoute{
65 {Name: "feat", Domain: "myapp--feat.example.com"},
66 },
67 }
68 svc := NewService(store, 48*time.Hour)
69 require.NoError(t, svc.Load(t.Context()))
70
71 p, err := svc.Get(t.Context(), "feat")
72 require.NoError(t, err)
73 assert.Equal(t, "myapp--feat.example.com", p.Domain)
74
75 _, err = svc.Get(t.Context(), "nonexistent")
76 assert.Error(t, err)
77}
78
79func TestPreviewService_Extend(t *testing.T) {
80 now := time.Now()
81 store := &fakeStore{
82 previews: []domain.PreviewRoute{
83 {Name: "feat", ExpiresAt: now.Add(1 * time.Hour)},
84 },
85 }
86 svc := NewService(store, 48*time.Hour)
87 require.NoError(t, svc.Load(t.Context()))
88
89 err := svc.Extend(t.Context(), "feat", 24*time.Hour)
90 require.NoError(t, err)
91
92 p, err := svc.Get(t.Context(), "feat")
93 require.NoError(t, err)
94 assert.True(t, p.ExpiresAt.After(now.Add(23*time.Hour)))
95}
96
97func TestPreviewService_GetExpired(t *testing.T) {
98 store := &fakeStore{
99 previews: []domain.PreviewRoute{
100 {Name: "expired", ExpiresAt: time.Now().Add(-1 * time.Hour)},
101 {Name: "active", ExpiresAt: time.Now().Add(1 * time.Hour)},
102 },
103 }
104 svc := NewService(store, 48*time.Hour)
105 require.NoError(t, svc.Load(t.Context()))
106
107 expired := svc.GetExpired()
108 assert.Len(t, expired, 1)
109 assert.Equal(t, "expired", expired[0].Name)
110}
111
112func TestPreviewService_Update(t *testing.T) {
113 store := &fakeStore{
114 previews: []domain.PreviewRoute{
115 {Name: "feat", Domain: "myapp--feat.example.com", Status: domain.PreviewStatusDeploying},
116 },
117 }
118 svc := NewService(store, 48*time.Hour)
119 require.NoError(t, svc.Load(t.Context()))
120
121 updated := domain.PreviewRoute{
122 Name: "feat",
123 Domain: "myapp--feat.example.com",
124 Status: domain.PreviewStatusRunning,
125 Containers: []string{"gordon-myapp--feat.example.com"},
126 }
127 err := svc.Update(t.Context(), updated)
128 require.NoError(t, err)
129
130 p, err := svc.Get(t.Context(), "feat")
131 require.NoError(t, err)
132 assert.Equal(t, domain.PreviewStatusRunning, p.Status)
133 assert.Equal(t, []string{"gordon-myapp--feat.example.com"}, p.Containers)
134}
135
136func TestPreviewService_Update_NotFound(t *testing.T) {
137 store := &fakeStore{}
138 svc := NewService(store, 48*time.Hour)
139
140 err := svc.Update(t.Context(), domain.PreviewRoute{Name: "nonexistent"})
141 assert.ErrorIs(t, err, domain.ErrPreviewNotFound)
142}
143
144type fakeRuntime struct {
145 containers []*domain.Container
146}
147
148func (f *fakeRuntime) ListContainers(_ context.Context, _ bool) ([]*domain.Container, error) {
149 return f.containers, nil
150}
151
152func TestPreviewService_CollectOrphans(t *testing.T) {
153 store := &fakeStore{
154 previews: []domain.PreviewRoute{
155 {Name: "tracked", Domain: "myapp--tracked.example.com"},
156 },
157 }
158 svc := NewService(store, 48*time.Hour)
159 require.NoError(t, svc.Load(t.Context()))
160
161 runtime := &fakeRuntime{
162 containers: []*domain.Container{
163 {
164 Name: "gordon-myapp--orphan.example.com",
165 Created: time.Now().Add(-72 * time.Hour),
166 Labels: map[string]string{
167 domain.LabelImage: "myapp:preview-orphan",
168 domain.LabelDomain: "myapp--orphan.example.com",
169 },
170 },
171 {
172 Name: "gordon-myapp--tracked.example.com",
173 Created: time.Now().Add(-72 * time.Hour),
174 Labels: map[string]string{
175 domain.LabelImage: "myapp:preview-tracked",
176 domain.LabelDomain: "myapp--tracked.example.com",
177 },
178 },
179 {
180 Name: "gordon-prod.example.com",
181 Created: time.Now().Add(-72 * time.Hour),
182 Labels: map[string]string{
183 domain.LabelImage: "myapp:latest",
184 domain.LabelDomain: "prod.example.com",
185 },
186 },
187 },
188 }
189
190 orphans := svc.CollectOrphans(t.Context(), runtime, []string{"preview-*"}, "--")
191 assert.Len(t, orphans, 1)
192 assert.Equal(t, "gordon-myapp--orphan.example.com", orphans[0].Name)
193}
194
195func TestPreviewService_CollectOrphans_NotExpired(t *testing.T) {
196 store := &fakeStore{}
197 svc := NewService(store, 48*time.Hour)
198 require.NoError(t, svc.Load(t.Context()))
199
200 runtime := &fakeRuntime{
201 containers: []*domain.Container{
202 {
203 Name: "gordon-myapp--recent.example.com",
204 Created: time.Now().Add(-12 * time.Hour),
205 Labels: map[string]string{
206 domain.LabelImage: "myapp:preview-recent",
207 domain.LabelDomain: "myapp--recent.example.com",
208 },
209 },
210 },
211 }
212
213 orphans := svc.CollectOrphans(t.Context(), runtime, []string{"preview-*"}, "--")
214 assert.Empty(t, orphans)
215}