1package apps_test
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "github.com/bnema/zerowrap"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12
13 outmocks "github.com/bnema/gordon/internal/boundaries/out/mocks"
14 "github.com/bnema/gordon/internal/domain"
15 "github.com/bnema/gordon/internal/usecase/apps"
16)
17
18func bindPolicy(root, source string, apps, services []string) domain.AppBindPolicy {
19 return domain.AppBindPolicy{
20 Name: "config",
21 Source: source,
22 Root: root,
23 AllowedApps: apps,
24 AllowedServices: services,
25 }
26}
27
28func TestApply_AcceptsBindsUnderAdministrativePolicy(t *testing.T) {
29 root := t.TempDir()
30 source := filepath.Join(root, "binds")
31 require.NoError(t, os.MkdirAll(source, 0o755))
32
33 store := outmocks.NewMockAppState(t)
34 spec := testSpec("blog")
35 spec.Services[0].Binds = []domain.AppBind{{Name: "config", Path: "/etc/app.conf"}}
36 applySuccess(store, spec)
37
38 svc := apps.NewService(store, zerowrap.Default()).
39 WithBindPolicies(map[string]domain.AppBindPolicy{
40 "config": bindPolicy(root, source, []string{"blog"}, []string{"web"}),
41 })
42 _, _, err := svc.Apply(context.Background(), spec, []byte("manifest"), false)
43 require.NoError(t, err)
44}
45
46func TestApply_RejectsBindWithoutPolicyBeforeStoreAccess(t *testing.T) {
47 store := outmocks.NewMockAppState(t)
48 spec := testSpec("blog")
49 spec.Services[0].Binds = []domain.AppBind{{Name: "config", Path: "/etc/app.conf"}}
50 svc := apps.NewService(store, zerowrap.Default())
51
52 _, _, err := svc.Apply(context.Background(), spec, []byte("manifest"), false)
53
54 require.ErrorIs(t, err, domain.ErrBindPolicy)
55 assert.Contains(t, err.Error(), "blog")
56 assert.Contains(t, err.Error(), "web")
57 assert.Contains(t, err.Error(), "config")
58}
59
60func TestApply_RejectsBindOutsideAllowlists(t *testing.T) {
61 root := t.TempDir()
62 source := filepath.Join(root, "binds")
63 require.NoError(t, os.MkdirAll(source, 0o755))
64 spec := testSpec("blog")
65 spec.Services[0].Binds = []domain.AppBind{{Name: "config", Path: "/etc/app.conf"}}
66
67 t.Run("app not allowed", func(t *testing.T) {
68 store := outmocks.NewMockAppState(t)
69 svc := apps.NewService(store, zerowrap.Default()).
70 WithBindPolicies(map[string]domain.AppBindPolicy{
71 "config": bindPolicy(root, source, []string{"other"}, []string{"web"}),
72 })
73 _, _, err := svc.Apply(context.Background(), spec, []byte("manifest"), false)
74 require.ErrorIs(t, err, domain.ErrBindPolicy)
75 assert.NotContains(t, err.Error(), source)
76 })
77
78 t.Run("service not allowed", func(t *testing.T) {
79 store := outmocks.NewMockAppState(t)
80 svc := apps.NewService(store, zerowrap.Default()).
81 WithBindPolicies(map[string]domain.AppBindPolicy{
82 "config": bindPolicy(root, source, []string{"blog"}, []string{"other"}),
83 })
84 _, _, err := svc.Apply(context.Background(), spec, []byte("manifest"), false)
85 require.ErrorIs(t, err, domain.ErrBindPolicy)
86 assert.NotContains(t, err.Error(), source)
87 })
88}
89
90func TestApply_RejectsBindSourceOutsideRoot(t *testing.T) {
91 root := t.TempDir()
92 outside := t.TempDir()
93 source := filepath.Join(outside, "binds")
94 require.NoError(t, os.MkdirAll(source, 0o755))
95 store := outmocks.NewMockAppState(t)
96 spec := testSpec("blog")
97 spec.Services[0].Binds = []domain.AppBind{{Name: "config", Path: "/etc/app.conf"}}
98 svc := apps.NewService(store, zerowrap.Default()).
99 WithBindPolicies(map[string]domain.AppBindPolicy{
100 "config": bindPolicy(root, source, []string{"blog"}, []string{"web"}),
101 })
102
103 _, _, err := svc.Apply(context.Background(), spec, []byte("manifest"), false)
104
105 require.ErrorIs(t, err, domain.ErrBindPolicy)
106 assert.NotContains(t, err.Error(), source, "apply errors must never leak the policy source path")
107 assert.NotContains(t, err.Error(), outside)
108}
109
110// TestApply_BindPoliciesReplaceOnReload proves a reload-style replacement takes
111// effect immediately: the second apply is refused before any store access.
112func TestApply_BindPoliciesReplaceOnReload(t *testing.T) {
113 root := t.TempDir()
114 source := filepath.Join(root, "binds")
115 require.NoError(t, os.MkdirAll(source, 0o755))
116 spec := testSpec("blog")
117 spec.Services[0].Binds = []domain.AppBind{{Name: "config", Path: "/etc/app.conf"}}
118
119 store := outmocks.NewMockAppState(t)
120 applySuccess(store, spec)
121 svc := apps.NewService(store, zerowrap.Default()).
122 WithBindPolicies(map[string]domain.AppBindPolicy{
123 "config": bindPolicy(root, source, []string{"blog"}, []string{"web"}),
124 })
125 _, _, err := svc.Apply(context.Background(), spec, []byte("manifest"), false)
126 require.NoError(t, err)
127
128 svc.SetBindPolicies(map[string]domain.AppBindPolicy{
129 "config": bindPolicy(root, source, []string{"other"}, []string{"web"}),
130 })
131 _, _, err = svc.Apply(context.Background(), spec, []byte("manifest"), false)
132 require.ErrorIs(t, err, domain.ErrBindPolicy)
133}