1package app
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "os/exec"
8 "path/filepath"
9 "testing"
10 "time"
11
12 "github.com/bnema/zerowrap"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15
16 "github.com/bnema/gordon/internal/adapters/out/domainsecrets"
17 "github.com/bnema/gordon/internal/domain"
18)
19
20func passCmd(args ...string) error {
21 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
22 defer cancel()
23 return exec.CommandContext(ctx, "pass", args...).Run()
24}
25
26func requirePass(t *testing.T) {
27 if err := passCmd("version"); err != nil {
28 t.Skip("pass not available")
29 }
30 if err := passCmd("ls"); err != nil {
31 t.Skip("pass store not initialized")
32 }
33}
34
35func cleanupPassDomain(domainName string, keys []string) {
36 safeDomain, err := domain.SanitizeDomainForEnvFile(domainName)
37 if err != nil {
38 return
39 }
40
41 for _, key := range keys {
42 path := fmt.Sprintf("%s/%s/%s", domainsecrets.PassDomainSecretsPath, safeDomain, key)
43 _ = passCmd("rm", "-f", path)
44 }
45
46 manifestPath := fmt.Sprintf("%s/%s/.keys", domainsecrets.PassDomainSecretsPath, safeDomain)
47 _ = passCmd("rm", "-f", manifestPath)
48}
49
50func TestMigrateEnvFile(t *testing.T) {
51 requirePass(t)
52
53 tmpDir, err := os.MkdirTemp("", "migrate-env-test-*")
54 require.NoError(t, err)
55 defer os.RemoveAll(tmpDir)
56
57 logger := zerowrap.Default()
58 store, err := domainsecrets.NewPassStore(logger)
59 require.NoError(t, err)
60
61 domainName := fmt.Sprintf("test.%d.example.com", time.Now().UnixNano())
62 keys := []string{"API_KEY", "DB_PASSWORD"}
63 defer cleanupPassDomain(domainName, keys)
64
65 envContent := "API_KEY=secret123\nDB_PASSWORD=pass456\n"
66 envFileName := domainName + ".env"
67 envFilePath := filepath.Join(tmpDir, envFileName)
68 err = os.WriteFile(envFilePath, []byte(envContent), 0600)
69 require.NoError(t, err)
70
71 err = migrateEnvFile(tmpDir, envFileName, store, logger)
72 require.NoError(t, err)
73
74 _, err = os.Stat(envFilePath)
75 assert.True(t, os.IsNotExist(err), "original .env file should be removed")
76
77 _, err = os.Stat(envFilePath + ".migrated")
78 assert.True(t, os.IsNotExist(err), ".env.migrated plaintext should not be preserved by default")
79
80 passKeys, err := store.ListKeys(domainName)
81 require.NoError(t, err)
82 assert.ElementsMatch(t, keys, passKeys)
83
84 values, err := store.GetAll(domainName)
85 require.NoError(t, err)
86 assert.Equal(t, "secret123", values["API_KEY"])
87 assert.Equal(t, "pass456", values["DB_PASSWORD"])
88}
89
90func cleanupPassAttachment(_ *testing.T, containerName string, keys []string) {
91 for _, key := range keys {
92 path := fmt.Sprintf("%s/%s/%s", domainsecrets.PassAttachmentPath, containerName, key)
93 _ = passCmd("rm", "-f", path)
94 }
95
96 manifestPath := fmt.Sprintf("%s/%s/.keys", domainsecrets.PassAttachmentPath, containerName)
97 _ = passCmd("rm", "-f", manifestPath)
98}
99
100func TestMigrateAttachmentEnvFile(t *testing.T) {
101 requirePass(t)
102
103 tmpDir, err := os.MkdirTemp("", "migrate-attachment-env-test-*")
104 require.NoError(t, err)
105 defer os.RemoveAll(tmpDir)
106
107 logger := zerowrap.Default()
108 store, err := domainsecrets.NewPassStore(logger)
109 require.NoError(t, err)
110
111 containerName := fmt.Sprintf("gitea-postgres-%d", time.Now().UnixNano())
112 envFileName := "gordon-" + containerName + ".env"
113 // extractContainerNameFromAttachmentFile strips the .env suffix, so the
114 // stored name includes the "gordon-" prefix from the filename.
115 storedContainerName := extractContainerNameFromAttachmentFile(envFileName)
116 keys := []string{"POSTGRES_USER", "POSTGRES_PASSWORD"}
117 defer cleanupPassAttachment(t, storedContainerName, keys)
118
119 envContent := "POSTGRES_USER=gitea\nPOSTGRES_PASSWORD=secret123\n"
120 envFilePath := filepath.Join(tmpDir, envFileName)
121 err = os.WriteFile(envFilePath, []byte(envContent), 0600)
122 require.NoError(t, err)
123
124 err = migrateAttachmentEnvFile(tmpDir, envFileName, store, logger)
125 require.NoError(t, err)
126
127 _, err = os.Stat(envFilePath)
128 assert.True(t, os.IsNotExist(err), "original .env file should be removed")
129
130 _, err = os.Stat(envFilePath + ".migrated")
131 assert.True(t, os.IsNotExist(err), ".env.migrated plaintext should not be preserved by default")
132
133 values, err := store.GetAllAttachment(storedContainerName)
134 require.NoError(t, err)
135 assert.Equal(t, "gitea", values["POSTGRES_USER"])
136 assert.Equal(t, "secret123", values["POSTGRES_PASSWORD"])
137}
138
139func TestIsAttachmentEnvFile(t *testing.T) {
140 tests := []struct {
141 name string
142 filename string
143 want bool
144 }{
145 {
146 name: "attachment env file",
147 filename: "gordon-gitea-postgres.env",
148 want: true,
149 },
150 {
151 name: "attachment env file with complex name",
152 filename: "gordon-git-example-com-gitea-postgres.env",
153 want: true,
154 },
155 {
156 name: "plain domain env file",
157 filename: "example.com.env",
158 want: false,
159 },
160 {
161 name: "migrated attachment file",
162 filename: "gordon-gitea-postgres.env.migrated",
163 want: false,
164 },
165 {
166 name: "non-env file",
167 filename: "config.yaml",
168 want: false,
169 },
170 {
171 name: "gordon prefix but no env suffix",
172 filename: "gordon-something",
173 want: false,
174 },
175 }
176
177 for _, tt := range tests {
178 t.Run(tt.name, func(t *testing.T) {
179 got := isAttachmentEnvFile(tt.filename)
180 assert.Equal(t, tt.want, got)
181 })
182 }
183}
184
185func TestExtractContainerNameFromAttachmentFile(t *testing.T) {
186 tests := []struct {
187 name string
188 filename string
189 want string
190 }{
191 {
192 name: "simple attachment file",
193 filename: "gordon-gitea-postgres.env",
194 want: "gordon-gitea-postgres",
195 },
196 {
197 name: "complex attachment file",
198 filename: "gordon-git-example-com-gitea-redis.env",
199 want: "gordon-git-example-com-gitea-redis",
200 },
201 {
202 name: "single word container",
203 filename: "gordon-redis.env",
204 want: "gordon-redis",
205 },
206 }
207
208 for _, tt := range tests {
209 t.Run(tt.name, func(t *testing.T) {
210 got := extractContainerNameFromAttachmentFile(tt.filename)
211 assert.Equal(t, tt.want, got)
212 })
213 }
214}