Skip to content

Commit b9194f1

Browse files
committed
test/compose: add unit test for project module
Signed-off-by: upsaurav12 <sauravup041103@gmail.com>
1 parent 33e6050 commit b9194f1

File tree

1 file changed

+370
-0
lines changed

1 file changed

+370
-0
lines changed

compose/project_test.go

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
package compose
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/compose-spec/compose-go/v2/types"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
const minimalCompose = `
15+
services:
16+
app:
17+
image: alpine
18+
`
19+
20+
const invalidCompose = `
21+
services:
22+
app: {}
23+
`
24+
25+
func writeComposeFile(t *testing.T, dir, name, content string) {
26+
t.Helper()
27+
28+
path := filepath.Join(dir, name)
29+
err := os.WriteFile(path, []byte(content), 0o644)
30+
if err != nil {
31+
t.Fatalf("failed to write compose file: %v", err)
32+
}
33+
}
34+
35+
func TestNewProjectFromComposeFile(t *testing.T) {
36+
ctx := context.Background()
37+
38+
tests := []struct {
39+
name string
40+
createFile bool
41+
fileName string
42+
explicitInput string
43+
content string
44+
expectErr bool
45+
expectDetected string
46+
}{
47+
{
48+
name: "no file and no explicit",
49+
expectErr: true,
50+
},
51+
{
52+
name: "auto detect default file",
53+
createFile: true,
54+
fileName: DefaultFileNames[0],
55+
content: minimalCompose,
56+
expectDetected: DefaultFileNames[0],
57+
},
58+
{
59+
name: "explicit file provided",
60+
createFile: true,
61+
fileName: "custom.yml",
62+
explicitInput: "custom.yml",
63+
content: minimalCompose,
64+
expectDetected: "custom.yml",
65+
},
66+
{
67+
name: "explicit file missing",
68+
explicitInput: "doesnotexist.yml",
69+
expectErr: true,
70+
},
71+
}
72+
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
workdir := t.TempDir()
76+
77+
if tt.createFile {
78+
writeComposeFile(t, workdir, tt.fileName, tt.content)
79+
}
80+
81+
project, err := NewProjectFromComposeFile(
82+
ctx,
83+
workdir,
84+
tt.explicitInput,
85+
)
86+
87+
if tt.expectErr {
88+
require.Error(t, err)
89+
return
90+
}
91+
92+
require.NoError(t, err)
93+
require.NotNil(t, project)
94+
95+
assert.Len(t, project.ComposeFiles, 1)
96+
assert.Equal(t, tt.expectDetected, project.ComposeFiles[0])
97+
assert.Equal(t, workdir, project.WorkingDir)
98+
})
99+
}
100+
}
101+
102+
func TestProjectValidate(t *testing.T) {
103+
ctx := context.Background()
104+
workdir := t.TempDir()
105+
106+
tests := []struct {
107+
name string
108+
content string
109+
expectErr bool
110+
}{
111+
{
112+
name: "valid service with image",
113+
content: minimalCompose,
114+
expectErr: false,
115+
},
116+
{
117+
name: "invalid service no image no build",
118+
content: invalidCompose,
119+
expectErr: true,
120+
},
121+
}
122+
123+
for _, tt := range tests {
124+
t.Run(tt.name, func(t *testing.T) {
125+
writeComposeFile(t, workdir, "compose.yml", tt.content)
126+
127+
project, err := NewProjectFromComposeFile(ctx, workdir, "compose.yml")
128+
if tt.expectErr {
129+
require.Error(t, err)
130+
return
131+
}
132+
133+
err = project.Validate(ctx)
134+
135+
if err != nil {
136+
assert.Error(t, err)
137+
} else {
138+
assert.NoError(t, err)
139+
}
140+
})
141+
}
142+
}
143+
144+
func TestAssignIPs(t *testing.T) {
145+
ctx := context.Background()
146+
147+
tests := []struct {
148+
name string
149+
project *Project
150+
expectErr bool
151+
expectAssigned bool
152+
}{
153+
{
154+
name: "dynamic ip assigned successfully",
155+
project: &Project{
156+
Project: &types.Project{
157+
Networks: types.Networks{
158+
"net1": {
159+
Name: "net1",
160+
External: false,
161+
Ipam: types.IPAMConfig{
162+
Config: []*types.IPAMPool{
163+
{Subnet: "192.168.1.0/29"},
164+
},
165+
},
166+
},
167+
},
168+
Services: types.Services{
169+
"app": {
170+
Name: "app",
171+
Networks: map[string]*types.ServiceNetworkConfig{
172+
"net1": {},
173+
},
174+
},
175+
},
176+
},
177+
},
178+
expectAssigned: true,
179+
},
180+
{
181+
name: "static ip respected",
182+
project: &Project{
183+
Project: &types.Project{
184+
Networks: types.Networks{
185+
"net1": {
186+
Name: "net1",
187+
External: false,
188+
Ipam: types.IPAMConfig{
189+
Config: []*types.IPAMPool{
190+
{Subnet: "192.168.1.0/29"},
191+
},
192+
},
193+
},
194+
},
195+
Services: types.Services{
196+
"app": {
197+
Name: "app",
198+
Networks: map[string]*types.ServiceNetworkConfig{
199+
"net1": {
200+
Ipv4Address: "192.168.1.5",
201+
},
202+
},
203+
},
204+
},
205+
},
206+
},
207+
expectAssigned: true,
208+
},
209+
{
210+
name: "invalid subnet",
211+
project: &Project{
212+
Project: &types.Project{
213+
Networks: types.Networks{
214+
"net1": {
215+
Name: "net1",
216+
Ipam: types.IPAMConfig{
217+
Config: []*types.IPAMPool{
218+
{Subnet: "invalid"},
219+
},
220+
},
221+
},
222+
},
223+
},
224+
},
225+
expectErr: true,
226+
},
227+
{
228+
name: "service references missing network",
229+
project: &Project{
230+
Project: &types.Project{
231+
Networks: types.Networks{},
232+
Services: types.Services{
233+
"app": {
234+
Name: "app",
235+
Networks: map[string]*types.ServiceNetworkConfig{
236+
"ghost": {},
237+
},
238+
},
239+
},
240+
},
241+
},
242+
expectErr: true,
243+
},
244+
{
245+
name: "subnet exhaustion",
246+
project: &Project{
247+
Project: &types.Project{
248+
Networks: types.Networks{
249+
"net1": {
250+
Name: "net1",
251+
Ipam: types.IPAMConfig{
252+
Config: []*types.IPAMPool{
253+
{Subnet: "10.0.0.0/31"},
254+
},
255+
},
256+
},
257+
},
258+
Services: types.Services{
259+
"a": {
260+
Name: "a",
261+
Networks: map[string]*types.ServiceNetworkConfig{
262+
"net1": {},
263+
},
264+
},
265+
"b": {
266+
Name: "b",
267+
Networks: map[string]*types.ServiceNetworkConfig{
268+
"net1": {},
269+
},
270+
},
271+
},
272+
},
273+
},
274+
expectErr: true,
275+
},
276+
}
277+
278+
for _, tt := range tests {
279+
t.Run(tt.name, func(t *testing.T) {
280+
err := tt.project.AssignIPs(ctx)
281+
282+
if tt.expectErr {
283+
require.Error(t, err)
284+
return
285+
}
286+
287+
require.NoError(t, err)
288+
289+
if tt.expectAssigned {
290+
for _, svc := range tt.project.Services {
291+
for _, netCfg := range svc.Networks {
292+
assert.NotEmpty(t, netCfg.Ipv4Address)
293+
}
294+
}
295+
}
296+
})
297+
}
298+
}
299+
300+
func TestServicesOrderedByDependencies(t *testing.T) {
301+
ctx := context.Background()
302+
303+
project := &Project{
304+
Project: &types.Project{
305+
Services: types.Services{
306+
"db": {Name: "db"},
307+
"api": {
308+
Name: "api",
309+
DependsOn: types.DependsOnConfig{
310+
"db": {Required: true},
311+
},
312+
},
313+
"frontend": {
314+
Name: "frontend",
315+
DependsOn: types.DependsOnConfig{
316+
"api": {Required: true},
317+
},
318+
},
319+
},
320+
},
321+
}
322+
323+
input := types.Services{
324+
"frontend": project.Services["frontend"],
325+
}
326+
327+
result := project.ServicesOrderedByDependencies(ctx, input, true)
328+
329+
require.Len(t, result, 3)
330+
331+
assert.Equal(t, "db", result[0].Name)
332+
assert.Equal(t, "api", result[1].Name)
333+
assert.Equal(t, "frontend", result[2].Name)
334+
}
335+
336+
func TestServicesReversedByDependencies(t *testing.T) {
337+
ctx := context.Background()
338+
339+
project := &Project{
340+
Project: &types.Project{
341+
Services: types.Services{
342+
"db": {Name: "db"},
343+
"api": {
344+
Name: "api",
345+
DependsOn: types.DependsOnConfig{
346+
"db": {Required: true},
347+
},
348+
},
349+
"frontend": {
350+
Name: "frontend",
351+
DependsOn: types.DependsOnConfig{
352+
"api": {Required: true},
353+
},
354+
},
355+
},
356+
},
357+
}
358+
359+
input := types.Services{
360+
"db": project.Services["db"],
361+
}
362+
363+
result := project.ServicesReversedByDependencies(ctx, input, true)
364+
365+
require.Len(t, result, 3)
366+
367+
assert.Equal(t, "frontend", result[0].Name)
368+
assert.Equal(t, "api", result[1].Name)
369+
assert.Equal(t, "db", result[2].Name)
370+
}

0 commit comments

Comments
 (0)