server_test.go
6090 bytes
1package serve
2
3import (
4 "bytes"
5 "context"
6 "os"
7 "path/filepath"
8 "runtime"
9 "strings"
10 "testing"
11
12 "charm.land/log/v2"
13 "github.com/charmbracelet/soft-serve/pkg/access"
14 "github.com/charmbracelet/soft-serve/pkg/backend"
15 "github.com/charmbracelet/soft-serve/pkg/config"
16 "github.com/charmbracelet/soft-serve/pkg/db"
17 "github.com/charmbracelet/soft-serve/pkg/db/migrate"
18 "github.com/charmbracelet/soft-serve/pkg/proto"
19 "github.com/charmbracelet/soft-serve/pkg/store"
20 "github.com/charmbracelet/soft-serve/pkg/store/database"
21 "github.com/matryer/is"
22 _ "modernc.org/sqlite" // sqlite driver
23)
24
25func setupTestBackend(tb testing.TB) (context.Context, *config.Config, *backend.Backend) {
26 tb.Helper()
27 ctx, cfg, be, _ := setupTestBackendWithDB(tb)
28 return ctx, cfg, be
29}
30
31func setupTestBackendWithDB(tb testing.TB) (context.Context, *config.Config, *backend.Backend, *db.DB) {
32 tb.Helper()
33 is := is.New(tb)
34
35 cfg := config.DefaultConfig()
36 cfg.DataPath = tb.TempDir()
37 is.NoErr(cfg.Validate())
38
39 ctx := context.Background()
40 ctx = config.WithContext(ctx, cfg)
41
42 is.NoErr(os.MkdirAll(cfg.DataPath, os.ModePerm))
43 dbx, err := db.Open(ctx, cfg.DB.Driver, cfg.DB.DataSource)
44 is.NoErr(err)
45 tb.Cleanup(func() { dbx.Close() }) //nolint: errcheck
46
47 is.NoErr(migrate.Migrate(ctx, dbx))
48
49 dbstore := database.New(ctx, dbx)
50 ctx = store.WithContext(ctx, dbstore)
51 be := backend.New(ctx, cfg, dbx, dbstore)
52
53 return ctx, cfg, be, dbx
54}
55
56func TestWarnIfAnonAdminAccess(t *testing.T) {
57 cases := []struct {
58 name string
59 allowKeyless bool
60 anonAccess access.AccessLevel
61 wantWarning bool
62 }{
63 {"defaults: keyless disabled, read-only anon", false, access.ReadOnlyAccess, false},
64 {"keyless allowed but anon access below admin", true, access.ReadWriteAccess, false},
65 {"admin anon access but keyless disallowed", false, access.AdminAccess, false},
66 {"keyless allowed with admin anon access: the dangerous combo", true, access.AdminAccess, true},
67 }
68
69 for _, c := range cases {
70 t.Run(c.name, func(t *testing.T) {
71 is := is.New(t)
72 ctx, cfg, be := setupTestBackend(t)
73
74 allow := c.allowKeyless
75 cfg.AllowKeyless = &allow
76 cfg.AnonAccess = &c.anonAccess
77
78 var buf bytes.Buffer
79 logger := log.New(&buf)
80
81 warnIfAnonAdminAccess(ctx, be, logger)
82
83 gotWarning := strings.Contains(buf.String(), "WARNING")
84 is.Equal(gotWarning, c.wantWarning)
85 })
86 }
87}
88
89func TestEnsureDefaultRepoDisabled(t *testing.T) {
90 is := is.New(t)
91 ctx, cfg, be := setupTestBackend(t)
92
93 cfg.DefaultRepo = ""
94 ensureDefaultRepo(ctx, cfg, be, log.New(os.Stderr))
95
96 repos, err := be.Repositories(ctx)
97 is.NoErr(err)
98 is.Equal(len(repos), 0)
99}
100
101func TestEnsureDefaultRepoCreatesWhenMissing(t *testing.T) {
102 is := is.New(t)
103 ctx, cfg, be := setupTestBackend(t)
104
105 cfg.DefaultRepo = "gitops"
106 ensureDefaultRepo(ctx, cfg, be, log.New(os.Stderr))
107
108 r, err := be.Repository(ctx, "gitops")
109 is.NoErr(err)
110 is.Equal(r.Name(), "gitops")
111 is.Equal(r.IsPrivate(), false)
112}
113
114func TestEnsureDefaultRepoIsIdempotent(t *testing.T) {
115 is := is.New(t)
116 ctx, cfg, be := setupTestBackend(t)
117
118 cfg.DefaultRepo = "gitops"
119 logger := log.New(os.Stderr)
120
121 ensureDefaultRepo(ctx, cfg, be, logger)
122 r, err := be.Repository(ctx, "gitops")
123 is.NoErr(err)
124 createdAt := r.CreatedAt()
125
126 // Restart-safety: a second boot against the same data must not error or
127 // touch the existing repository.
128 ensureDefaultRepo(ctx, cfg, be, logger)
129
130 r, err = be.Repository(ctx, "gitops")
131 is.NoErr(err)
132 is.Equal(r.CreatedAt(), createdAt)
133
134 repos, err := be.Repositories(ctx)
135 is.NoErr(err)
136 is.Equal(len(repos), 1)
137}
138
139func TestEnsureDefaultRepoInvalidNameSkipped(t *testing.T) {
140 is := is.New(t)
141 ctx, cfg, be := setupTestBackend(t)
142
143 cfg.DefaultRepo = "../not valid!"
144 ensureDefaultRepo(ctx, cfg, be, log.New(os.Stderr))
145
146 repos, err := be.Repositories(ctx)
147 is.NoErr(err)
148 is.Equal(len(repos), 0)
149}
150
151func TestEnsureDefaultRepoLooksUpExisting(t *testing.T) {
152 is := is.New(t)
153 ctx, cfg, be := setupTestBackend(t)
154
155 owner, err := be.UserByID(ctx, 1)
156 is.NoErr(err)
157 _, err = be.CreateRepository(ctx, "gitops", owner, proto.RepositoryOptions{
158 Private: true,
159 })
160 is.NoErr(err)
161
162 cfg.DefaultRepo = "gitops"
163 ensureDefaultRepo(ctx, cfg, be, log.New(os.Stderr))
164
165 r, err := be.Repository(ctx, "gitops")
166 is.NoErr(err)
167 is.Equal(r.IsPrivate(), true)
168}
169
170func TestEnsureDefaultRepoOwnerLookupFailsSkipped(t *testing.T) {
171 is := is.New(t)
172 ctx, cfg, be := setupTestBackend(t)
173
174 // Delete the built-in admin account (ID 1) so UserByID cannot find it.
175 // ensureDefaultRepo must log and return, not panic.
176 is.NoErr(be.DeleteUser(ctx, "admin"))
177
178 cfg.DefaultRepo = "gitops"
179 ensureDefaultRepo(ctx, cfg, be, log.New(os.Stderr))
180
181 repos, err := be.Repositories(ctx)
182 is.NoErr(err)
183 is.Equal(len(repos), 0)
184}
185
186func TestEnsureDefaultRepoLookupErrorSkipped(t *testing.T) {
187 is := is.New(t)
188 ctx, cfg, be, dbx := setupTestBackendWithDB(t)
189
190 // Create the repo directory on disk so Repository() passes os.Stat,
191 // then close the DB so the lookup behind it fails with a non-not-found error.
192 repoDir := filepath.Join(cfg.DataPath, "repos", "gitops.git")
193 is.NoErr(os.MkdirAll(repoDir, os.ModePerm))
194 is.NoErr(dbx.Close())
195
196 cfg.DefaultRepo = "gitops"
197 ensureDefaultRepo(ctx, cfg, be, log.New(os.Stderr))
198
199 _, err := be.UserByID(ctx, 1)
200 is.True(err != nil) // db is closed, sanity check we broke it correctly
201}
202
203func TestEnsureDefaultRepoCreateErrorSkipped(t *testing.T) {
204 if runtime.GOOS == "windows" || os.Geteuid() == 0 {
205 t.Skip("requires enforceable directory permissions as a non-root user")
206 }
207
208 is := is.New(t)
209 ctx, cfg, be := setupTestBackend(t)
210
211 // Make the "repos" directory read-only so CreateRepository's git.Init
212 // fails for a reason other than the repo already existing.
213 reposDir := filepath.Join(cfg.DataPath, "repos")
214 is.NoErr(os.MkdirAll(reposDir, os.ModePerm))
215 is.NoErr(os.Chmod(reposDir, 0o555))
216 t.Cleanup(func() { os.Chmod(reposDir, 0o755) }) //nolint: errcheck
217
218 cfg.DefaultRepo = "gitops"
219 ensureDefaultRepo(ctx, cfg, be, log.New(os.Stderr))
220
221 repos, err := be.Repositories(ctx)
222 is.NoErr(err)
223 is.Equal(len(repos), 0)
224}