1package domain_test
2
3import (
4 "testing"
5
6 "github.com/stretchr/testify/assert"
7 "github.com/stretchr/testify/require"
8
9 "github.com/bnema/gordon/internal/domain"
10)
11
12func TestAppSpec_ValidateBindsOK(t *testing.T) {
13 spec := validSpec()
14 spec.Services[0].Binds = []domain.AppBind{
15 {Name: "config", Path: "/etc/app.conf", ReadOnly: true},
16 {Name: "data", Path: "/var/lib/app"},
17 }
18 require.NoError(t, spec.Validate())
19}
20
21func TestAppSpec_ValidateBindErrors(t *testing.T) {
22 cases := []struct {
23 name string
24 mutate func(*domain.AppSpec)
25 wantErr string
26 }{
27 {"bad bind name", func(s *domain.AppSpec) {
28 s.Services[0].Binds = []domain.AppBind{{Name: "Bad_Name", Path: "/data"}}
29 }, "bind name"},
30 {"reserved separator name", func(s *domain.AppSpec) {
31 s.Services[0].Binds = []domain.AppBind{{Name: "a--b", Path: "/data"}}
32 }, "reserved separator"},
33 {"relative destination", func(s *domain.AppSpec) {
34 s.Services[0].Binds = []domain.AppBind{{Name: "data", Path: "relative/path"}}
35 }, "must be absolute"},
36 {"unnormalized destination", func(s *domain.AppSpec) {
37 s.Services[0].Binds = []domain.AppBind{{Name: "data", Path: "/var/../data"}}
38 }, "normalized clean path"},
39 {"trailing slash destination", func(s *domain.AppSpec) {
40 s.Services[0].Binds = []domain.AppBind{{Name: "data", Path: "/data/"}}
41 }, "normalized clean path"},
42 {"sensitive destination", func(s *domain.AppSpec) {
43 s.Services[0].Binds = []domain.AppBind{{Name: "dev", Path: "/dev"}}
44 }, "sensitive container path"},
45 {"duplicate bind name", func(s *domain.AppSpec) {
46 s.Services[0].Binds = []domain.AppBind{
47 {Name: "data", Path: "/data"},
48 {Name: "data", Path: "/other"},
49 }
50 }, "duplicate bind name"},
51 {"duplicate bind destination", func(s *domain.AppSpec) {
52 s.Services[0].Binds = []domain.AppBind{
53 {Name: "one", Path: "/data"},
54 {Name: "two", Path: "/data"},
55 }
56 }, "duplicate bind destination"},
57 {"collision with volume", func(s *domain.AppSpec) {
58 s.Services[0].Volumes = []domain.AppVolume{{Name: "data", Path: "/data"}}
59 s.Services[0].Binds = []domain.AppBind{{Name: "data-bind", Path: "/data"}}
60 }, "collides with a declared volume"},
61 }
62 for _, tc := range cases {
63 t.Run(tc.name, func(t *testing.T) {
64 spec := validSpec()
65 tc.mutate(&spec)
66 err := spec.Validate()
67 require.Error(t, err)
68 assert.ErrorIs(t, err, domain.ErrInvalidAppSpec)
69 assert.Contains(t, err.Error(), tc.wantErr)
70 })
71 }
72}
73
74func TestValidateBindName(t *testing.T) {
75 assert.NoError(t, domain.ValidateBindName("data"))
76 assert.NoError(t, domain.ValidateBindName("app.config"))
77 assert.ErrorIs(t, domain.ValidateBindName(""), domain.ErrInvalidAppSpec)
78 assert.ErrorIs(t, domain.ValidateBindName("Bad"), domain.ErrInvalidAppSpec)
79 assert.ErrorIs(t, domain.ValidateBindName("a--b"), domain.ErrInvalidAppSpec)
80}
81
82func TestValidateBindDestination(t *testing.T) {
83 valid := []string{"/data", "/etc/app.conf", "/var/lib/app"}
84 for _, dest := range valid {
85 assert.NoError(t, domain.ValidateBindDestination(dest), dest)
86 }
87 invalid := []string{"", "data", "/data/", "/data/../etc", "//data", "/./data", "/", "/proc", "/sys", "/dev", "/boot"}
88 for _, dest := range invalid {
89 err := domain.ValidateBindDestination(dest)
90 require.Error(t, err, dest)
91 assert.ErrorIs(t, err, domain.ErrInvalidAppSpec, dest)
92 }
93}
94
95func TestIsSensitiveBindDestination(t *testing.T) {
96 for _, dest := range []string{"/", "/proc", "/sys", "/dev", "/boot"} {
97 assert.True(t, domain.IsSensitiveBindDestination(dest), dest)
98 }
99 assert.False(t, domain.IsSensitiveBindDestination("/etc/app.conf"))
100 assert.True(t, domain.IsSensitiveBindDestination("/proc/self"))
101 assert.True(t, domain.IsSensitiveBindDestination("/dev/shm"))
102}
103
104func TestDiffAppSpec_Binds(t *testing.T) {
105 desired := validSpec()
106 desired.Services[0].Binds = []domain.AppBind{{Name: "data", Path: "/data", ReadOnly: true}}
107 effective := validSpec()
108 effective.Services[0].Binds = []domain.AppBind{{Name: "data", Path: "/data"}}
109 diff := domain.DiffAppSpec(desired, effective)
110 assert.Contains(t, diff.Changed, "service/web/binds")
111}