1package preview
2
3import (
4 "context"
5 "fmt"
6 "testing"
7 "time"
8
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"
11
12 "github.com/bnema/gordon/internal/domain"
13)
14
15type fakeDeployer struct {
16 deployFn func(ctx context.Context, route domain.Route) (*domain.Container, error)
17}
18
19func (f *fakeDeployer) Deploy(ctx context.Context, route domain.Route) (*domain.Container, error) {
20 return f.deployFn(ctx, route)
21}
22
23type 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
29func (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
36func (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
43func (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("route %q: %w", d, domain.ErrRouteNotFound)
48}
49
50func (f *fakeRouteManager) GetVolumeConfig() (bool, string, bool) {
51 return true, "gordon", false
52}
53
54func TestPreviewLifecycle_Integration(t *testing.T) {
55 store := &fakeStore{}
56 svc := NewService(store, 2*time.Second)
57
58 ctx := t.Context()
59
60 // Create
61 err := svc.CreatePreview(ctx, CreatePreviewRequest{
62 Name: "test-feat",
63 Domain: "myapp--test-feat.example.com",
64 BaseRoute: "myapp.example.com",
65 Image: "myapp:preview-test-feat",
66 HTTPS: true,
67 PreviewConfig: domain.PreviewConfig{
68 TTL: 2 * time.Second,
69 Separator: "--",
70 DataCopy: false,
71 },
72 })
73 require.NoError(t, err)
74
75 // Verify created
76 all, err := svc.List(ctx)
77 require.NoError(t, err)
78 require.Len(t, all, 1)
79 assert.Equal(t, "test-feat", all[0].Name)
80 assert.Equal(t, "myapp--test-feat.example.com", all[0].Domain)
81 assert.Equal(t, "myapp.example.com", all[0].BaseRoute)
82
83 // Extend
84 err = svc.Extend(ctx, "test-feat", 1*time.Hour)
85 require.NoError(t, err)
86
87 p, err := svc.Get(ctx, "test-feat")
88 require.NoError(t, err)
89 assert.True(t, p.ExpiresAt.After(time.Now().Add(55*time.Minute)))
90
91 // Delete
92 err = svc.Delete(ctx, "test-feat")
93 require.NoError(t, err)
94
95 all, err = svc.List(ctx)
96 require.NoError(t, err)
97 assert.Empty(t, all)
98}
99
100func TestPreviewLifecycle_CleanupExpired(t *testing.T) {
101 store := &fakeStore{}
102 svc := NewService(store, 1*time.Millisecond)
103
104 ctx := t.Context()
105
106 // Create with very short TTL
107 err := svc.CreatePreview(ctx, CreatePreviewRequest{
108 Name: "short-lived",
109 Domain: "myapp--short-lived.example.com",
110 BaseRoute: "myapp.example.com",
111 Image: "myapp:preview-short-lived",
112 HTTPS: true,
113 PreviewConfig: domain.PreviewConfig{
114 TTL: 1 * time.Millisecond,
115 Separator: "--",
116 DataCopy: false,
117 },
118 })
119 require.NoError(t, err)
120
121 // Wait for expiry
122 time.Sleep(10 * time.Millisecond)
123
124 // Cleanup should find it
125 expired := svc.CleanupExpired(ctx)
126 assert.Len(t, expired, 1)
127 assert.Equal(t, "short-lived", expired[0].Name)
128
129 // Should be gone
130 all, err := svc.List(ctx)
131 require.NoError(t, err)
132 assert.Empty(t, all)
133}
134
135type fakeEnvLoader struct {
136 loadEnvFn func(ctx context.Context, domain string) ([]string, error)
137}
138
139func (f *fakeEnvLoader) LoadEnv(ctx context.Context, domain string) ([]string, error) {
140 if f.loadEnvFn != nil {
141 return f.loadEnvFn(ctx, domain)
142 }
143 return nil, nil
144}
145
146func (f *fakeEnvLoader) CreateEnvFile(_ context.Context, _ string) error { return nil }
147func (f *fakeEnvLoader) EnvFileExists(_ string) (bool, error) { return false, nil }
148
149func TestPreviewLifecycle_WithEnvInheritance(t *testing.T) {
150 store := &fakeStore{}
151 var deployedRoute domain.Route
152
153 mockDeployer := &fakeDeployer{
154 deployFn: func(_ context.Context, route domain.Route) (*domain.Container, error) {
155 deployedRoute = route
156 return &domain.Container{Name: "gordon-" + route.Domain}, nil
157 },
158 }
159 mockRouteManager := &fakeRouteManager{}
160 mockEnvLoader := &fakeEnvLoader{
161 loadEnvFn: func(_ context.Context, d string) ([]string, error) {
162 if d == "myapp.example.com" {
163 return []string{"DB_HOST=prod-db", "API_KEY=secret123"}, nil
164 }
165 return nil, nil
166 },
167 }
168
169 svc := NewService(store, 2*time.Second).
170 WithDeployer(mockDeployer).
171 WithRouteManager(mockRouteManager).
172 WithEnvLoader(mockEnvLoader).
173 WithRegistryDomain("reg.example.com")
174
175 ctx := t.Context()
176 err := svc.CreatePreview(ctx, CreatePreviewRequest{
177 Name: "test-feat",
178 Domain: "myapp--test-feat.example.com",
179 BaseRoute: "myapp.example.com",
180 Image: "myapp:preview-test-feat",
181 HTTPS: true,
182 PreviewConfig: domain.PreviewConfig{
183 TTL: 2 * time.Second,
184 Separator: "--",
185 EnvCopy: true,
186 },
187 })
188 require.NoError(t, err)
189
190 // Deployer should receive base route's env vars via Route.Env
191 assert.Equal(t, []string{"DB_HOST=prod-db", "API_KEY=secret123"}, deployedRoute.Env)
192}
193
194func TestPreviewLifecycle_WithDeploy(t *testing.T) {
195 store := &fakeStore{}
196 deployed := false
197 routeAdded := ""
198
199 mockDeployer := &fakeDeployer{
200 deployFn: func(_ context.Context, route domain.Route) (*domain.Container, error) {
201 deployed = true
202 return &domain.Container{Name: "gordon-" + route.Domain}, nil
203 },
204 }
205 mockRouteManager := &fakeRouteManager{
206 addRouteFn: func(_ context.Context, route domain.Route) error {
207 routeAdded = route.Domain
208 return nil
209 },
210 }
211
212 svc := NewService(store, 2*time.Second).
213 WithDeployer(mockDeployer).
214 WithRouteManager(mockRouteManager).
215 WithRegistryDomain("reg.example.com")
216
217 ctx := t.Context()
218 err := svc.CreatePreview(ctx, CreatePreviewRequest{
219 Name: "test-feat",
220 Domain: "myapp--test-feat.example.com",
221 BaseRoute: "myapp.example.com",
222 Image: "myapp:preview-test-feat",
223 HTTPS: true,
224 PreviewConfig: domain.PreviewConfig{
225 TTL: 2 * time.Second,
226 Separator: "--",
227 DataCopy: false,
228 },
229 })
230 require.NoError(t, err)
231
232 assert.True(t, deployed, "deployer should have been called")
233 assert.Equal(t, "myapp--test-feat.example.com", routeAdded)
234
235 all, err := svc.List(ctx)
236 require.NoError(t, err)
237 require.Len(t, all, 1)
238 assert.Equal(t, domain.PreviewStatusRunning, all[0].Status)
239 assert.Equal(t, []string{"gordon-myapp--test-feat.example.com"}, all[0].Containers)
240}