Parent directory

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}