40d76a13ac74589c8395067af64f58d5a3f96b43
- Author
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Committer
- GitHub <noreply@github.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
2index 7ee3b67d6a8835efefc2fb9f1f70ee0af2dad5b7..8ada1613150b81ce2f1bc5490ca48edc349c6296 100644
3--- a/.github/workflows/build.yml
4+++ b/.github/workflows/build.yml
5@@ -42,5 +42,5 @@ jobs:
6 - name: Test
7 run: go test ./...
8 env:
9- DB_DRIVER: postgres
10- DB_DATA_SOURCE: postgres://postgres:postgres@localhost/postgres?sslmode=disable
11+ SOFT_SERVE_DB_DRIVER: postgres
12+ SOFT_SERVE_DB_DATA_SOURCE: postgres://postgres:postgres@localhost/postgres?sslmode=disable
13diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
14index 50f4aa9481391a7febf7fa9dbac233c739b56809..4b1b12a83f5624f16d158e9f039f62a8d79c24fc 100644
15--- a/.github/workflows/coverage.yml
16+++ b/.github/workflows/coverage.yml
17@@ -8,7 +8,10 @@ on:
18
19 jobs:
20 coverage:
21- runs-on: ubuntu-latest
22+ strategy:
23+ matrix:
24+ os: [ubuntu-latest] # TODO: add macos & windows
25+ runs-on: ${{ matrix.os }}
26 steps:
27 - uses: actions/checkout@v4
28
29@@ -18,7 +21,24 @@ jobs:
30 go-version: ^1
31
32 - name: Test
33- run: go test -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt ./... -timeout 5m
34+ run: |
35+ # We collect coverage data from two sources,
36+ # 1) unit tests 2) integration tests
37+ #
38+ # https://go.dev/testing/coverage/
39+ # https://dustinspecker.com/posts/go-combined-unit-integration-code-coverage/
40+ # https://github.com/golang/go/issues/51430#issuecomment-1344711300
41+ mkdir -p coverage/unit
42+ mkdir -p coverage/int
43+
44+ # Collect unit tests coverage
45+ go test -failfast -race -timeout 5m -skip=^TestScript -cover ./... -args -test.gocoverdir=$PWD/coverage/unit
46+
47+ # Collect integration tests coverage
48+ GOCOVERDIR=$PWD/coverage/int go test -failfast -race -timeout 5m -run=^TestScript ./...
49+
50+ # Convert coverage data to legacy textfmt format to upload
51+ go tool covdata textfmt -i=coverage/unit,coverage/int -o=coverage.txt
52 - uses: codecov/codecov-action@v3
53 with:
54 file: ./coverage.txt
55diff --git a/cmd/cmd.go b/cmd/cmd.go
56index 9aead6369d7faa2932dcea9d4dcf3ef8766336ee..556594e15a49e1de8910e475914d96de5192b6a5 100644
57--- a/cmd/cmd.go
58+++ b/cmd/cmd.go
59@@ -33,7 +33,7 @@ func InitBackendContext(cmd *cobra.Command, _ []string) error {
60 ctx = db.WithContext(ctx, dbx)
61 dbstore := database.New(ctx, dbx)
62 ctx = store.WithContext(ctx, dbstore)
63- be := backend.New(ctx, cfg, dbx)
64+ be := backend.New(ctx, cfg, dbx, dbstore)
65 ctx = backend.WithContext(ctx, be)
66
67 cmd.SetContext(ctx)
68diff --git a/cmd/soft/serve/serve.go b/cmd/soft/serve/serve.go
69index 7f81dcf0f741085ff41f03229a786b76091d0eb5..d23ce65de3b5c4c7f1883b6f384edbf646b30824 100644
70--- a/cmd/soft/serve/serve.go
71+++ b/cmd/soft/serve/serve.go
72@@ -3,9 +3,12 @@ package serve
73 import (
74 "context"
75 "fmt"
76+ "net/http"
77 "os"
78 "os/signal"
79 "path/filepath"
80+ "strconv"
81+ "sync"
82 "syscall"
83 "time"
84
85@@ -80,10 +83,26 @@ var (
86 }
87
88 done := make(chan os.Signal, 1)
89+ doneOnce := sync.OnceFunc(func() { close(done) })
90+
91 lch := make(chan error, 1)
92+
93+ // This endpoint is added for testing purposes
94+ // It allows us to stop the server from the test suite.
95+ // This is needed since Windows doesn't support signals.
96+ if testRun, _ := strconv.ParseBool(os.Getenv("SOFT_SERVE_TESTRUN")); testRun {
97+ h := s.HTTPServer.Server.Handler
98+ s.HTTPServer.Server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
99+ if r.URL.Path == "/__stop" && r.Method == http.MethodHead {
100+ doneOnce()
101+ return
102+ }
103+ h.ServeHTTP(w, r)
104+ })
105+ }
106+
107 go func() {
108- defer close(lch)
109- defer close(done)
110+ defer doneOnce()
111 lch <- s.Start()
112 }()
113
114diff --git a/cmd/soft/serve/server.go b/cmd/soft/serve/server.go
115index 223e55a69fb5c907feda0b1bb3f657a0216cb2de..33679091f1f177303b4651e842cdf20b01c0bc81 100644
116--- a/cmd/soft/serve/server.go
117+++ b/cmd/soft/serve/server.go
118@@ -147,7 +147,7 @@ func (s *Server) Shutdown(ctx context.Context) error {
119 for _, j := range jobs.List() {
120 s.Cron.Remove(j.ID)
121 }
122- s.Cron.Shutdown()
123+ s.Cron.Stop()
124 return nil
125 })
126 // defer s.DB.Close() // nolint: errcheck
127diff --git a/go.mod b/go.mod
128index 7cd4a0cc406ac7eb761af917c5d4b63d5c15c28d..3d1c87b75243f6994a25f0da16415ca86a0ac6d4 100644
129--- a/go.mod
130+++ b/go.mod
131@@ -41,8 +41,7 @@ require (
132 github.com/muesli/roff v0.1.0
133 github.com/prometheus/client_golang v1.17.0
134 github.com/robfig/cron/v3 v3.0.1
135- github.com/rogpeppe/go-internal v1.11.0
136- github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086
137+ github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c
138 github.com/spf13/cobra v1.8.0
139 go.uber.org/automaxprocs v1.5.3
140 golang.org/x/crypto v0.16.0
141diff --git a/go.sum b/go.sum
142index 7898d622cd0488074b0763452abce3549d8b061e..b0d9580068e48188bdd52f19e47100c060d835ff 100644
143--- a/go.sum
144+++ b/go.sum
145@@ -167,10 +167,8 @@ github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
146 github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
147 github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
148 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
149-github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
150-github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
151-github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086 h1:mncRSDOqYCng7jOD+Y6+IivdRI6Kzv2BLWYkWkdQfu0=
152-github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086/go.mod h1:YpdgDXpumPB/+EGmGTYHeiW/0QVFRzBYTNFaxWfPDk4=
153+github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c h1:fPpdjePK1atuOg28PXfNSqgwf9I/qD1Hlo39JFwKBXk=
154+github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
155 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
156 github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
157 github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
158diff --git a/pkg/access/context_test.go b/pkg/access/context_test.go
159new file mode 100644
160index 0000000000000000000000000000000000000000..c4bcf4a60cec848b52f95076d1e804b9886c736b
161--- /dev/null
162+++ b/pkg/access/context_test.go
163@@ -0,0 +1,20 @@
164+package access
165+
166+import (
167+ "context"
168+ "testing"
169+)
170+
171+func TestGoodFromContext(t *testing.T) {
172+ ctx := WithContext(context.TODO(), AdminAccess)
173+ if ac := FromContext(ctx); ac != AdminAccess {
174+ t.Errorf("FromContext(ctx) => %d, want %d", ac, AdminAccess)
175+ }
176+}
177+
178+func TestBadFromContext(t *testing.T) {
179+ ctx := context.TODO()
180+ if ac := FromContext(ctx); ac != -1 {
181+ t.Errorf("FromContext(ctx) => %d, want %d", ac, -1)
182+ }
183+}
184diff --git a/pkg/backend/backend.go b/pkg/backend/backend.go
185index 2d9eb2a05538e7a2ffa5ebc568f14d5e610f39f9..ba8796b54546620df6b8377704ee5d76b1c10283 100644
186--- a/pkg/backend/backend.go
187+++ b/pkg/backend/backend.go
188@@ -23,14 +23,13 @@ type Backend struct {
189 }
190
191 // New returns a new Soft Serve backend.
192-func New(ctx context.Context, cfg *config.Config, db *db.DB) *Backend {
193- dbstore := store.FromContext(ctx)
194+func New(ctx context.Context, cfg *config.Config, db *db.DB, st store.Store) *Backend {
195 logger := log.FromContext(ctx).WithPrefix("backend")
196 b := &Backend{
197 ctx: ctx,
198 cfg: cfg,
199 db: db,
200- store: dbstore,
201+ store: st,
202 logger: logger,
203 manager: task.NewManager(ctx),
204 }
205diff --git a/pkg/config/context_test.go b/pkg/config/context_test.go
206new file mode 100644
207index 0000000000000000000000000000000000000000..db7f8b257539b7c374cca9579c3b3d79d3182706
208--- /dev/null
209+++ b/pkg/config/context_test.go
210@@ -0,0 +1,29 @@
211+package config
212+
213+import (
214+ "context"
215+ "reflect"
216+ "testing"
217+)
218+
219+func TestBadFromContext(t *testing.T) {
220+ ctx := context.TODO()
221+ if c := FromContext(ctx); c != nil {
222+ t.Errorf("FromContext(ctx) => %v, want %v", c, nil)
223+ }
224+}
225+
226+func TestGoodFromContext(t *testing.T) {
227+ ctx := WithContext(context.TODO(), &Config{})
228+ if c := FromContext(ctx); c == nil {
229+ t.Errorf("FromContext(ctx) => %v, want %v", c, &Config{})
230+ }
231+}
232+
233+func TestGoodFromContextWithDefaultConfig(t *testing.T) {
234+ cfg := DefaultConfig()
235+ ctx := WithContext(context.TODO(), cfg)
236+ if c := FromContext(ctx); c == nil || !reflect.DeepEqual(c, cfg) {
237+ t.Errorf("FromContext(ctx) => %v, want %v", c, cfg)
238+ }
239+}
240diff --git a/pkg/config/file_test.go b/pkg/config/file_test.go
241new file mode 100644
242index 0000000000000000000000000000000000000000..81efad71ceba05003f8cf85f0b6d8d7415284f13
243--- /dev/null
244+++ b/pkg/config/file_test.go
245@@ -0,0 +1,15 @@
246+package config
247+
248+import "testing"
249+
250+func TestNewConfigFile(t *testing.T) {
251+ for _, cfg := range []*Config{
252+ nil,
253+ DefaultConfig(),
254+ &Config{},
255+ } {
256+ if s := newConfigFile(cfg); s == "" {
257+ t.Errorf("newConfigFile(nil) => %q, want non-empty string", s)
258+ }
259+ }
260+}
261diff --git a/pkg/config/ssh.go b/pkg/config/ssh.go
262index 102b391418ac86f1d0f963fc7701591bdd233ce0..151ac7877abdfa39070244198f1de37131a5f8fa 100644
263--- a/pkg/config/ssh.go
264+++ b/pkg/config/ssh.go
265@@ -1,8 +1,28 @@
266 package config
267
268-import "github.com/charmbracelet/keygen"
269+import (
270+ "errors"
271+
272+ "github.com/charmbracelet/keygen"
273+)
274+
275+var (
276+ // ErrNilConfig is returned when a nil config is passed to a function.
277+ ErrNilConfig = errors.New("nil config")
278+
279+ // ErrEmptySSHKeyPath is returned when the SSH key path is empty.
280+ ErrEmptySSHKeyPath = errors.New("empty SSH key path")
281+)
282
283 // KeyPair returns the server's SSH key pair.
284-func (c SSHConfig) KeyPair() (*keygen.SSHKeyPair, error) {
285- return keygen.New(c.KeyPath, keygen.WithKeyType(keygen.Ed25519))
286+func KeyPair(cfg *Config) (*keygen.SSHKeyPair, error) {
287+ if cfg == nil {
288+ return nil, ErrNilConfig
289+ }
290+
291+ if cfg.SSH.KeyPath == "" {
292+ return nil, ErrEmptySSHKeyPath
293+ }
294+
295+ return keygen.New(cfg.SSH.KeyPath, keygen.WithKeyType(keygen.Ed25519))
296 }
297diff --git a/pkg/config/ssh_test.go b/pkg/config/ssh_test.go
298new file mode 100644
299index 0000000000000000000000000000000000000000..4f68ec149cd2dc69c8656d902be61a773fc8d723
300--- /dev/null
301+++ b/pkg/config/ssh_test.go
302@@ -0,0 +1,26 @@
303+package config
304+
305+import "testing"
306+
307+func TestBadSSHKeyPair(t *testing.T) {
308+ for _, cfg := range []*Config{
309+ nil,
310+ {},
311+ } {
312+ if _, err := KeyPair(cfg); err == nil {
313+ t.Errorf("cfg.SSH.KeyPair() => _, nil, want non-nil error")
314+ }
315+ }
316+}
317+
318+func TestGoodSSHKeyPair(t *testing.T) {
319+ cfg := &Config{
320+ SSH: SSHConfig{
321+ KeyPath: "testdata/ssh_host_ed25519_key",
322+ },
323+ }
324+
325+ if _, err := KeyPair(cfg); err != nil {
326+ t.Errorf("cfg.SSH.KeyPair() => _, %v, want nil error", err)
327+ }
328+}
329diff --git a/pkg/cron/cron_test.go b/pkg/cron/cron_test.go
330new file mode 100644
331index 0000000000000000000000000000000000000000..c254191b2e45e889b5d2ce3f10fc1c7f16949c83
332--- /dev/null
333+++ b/pkg/cron/cron_test.go
334@@ -0,0 +1,31 @@
335+package cron
336+
337+import (
338+ "bytes"
339+ "context"
340+ "fmt"
341+ "testing"
342+
343+ "github.com/charmbracelet/log"
344+)
345+
346+func TestCronLogger(t *testing.T) {
347+ var buf bytes.Buffer
348+ logger := log.New(&buf)
349+ logger.SetLevel(log.DebugLevel)
350+ clogger := cronLogger{logger}
351+ clogger.Info("foo")
352+ clogger.Error(fmt.Errorf("bar"), "test")
353+ if buf.String() != "DEBU foo\nERRO test err=bar\n" {
354+ t.Errorf("unexpected log output: %s", buf.String())
355+ }
356+}
357+
358+func TestSchedularAddRemove(t *testing.T) {
359+ s := NewScheduler(context.TODO())
360+ id, err := s.AddFunc("* * * * *", func() {})
361+ if err != nil {
362+ t.Fatal(err)
363+ }
364+ s.Remove(id)
365+}
366diff --git a/pkg/daemon/daemon.go b/pkg/daemon/daemon.go
367index eee10ec525754cd7007fd9c77b71ac833e7b8bc4..5d91565cdd58861267ce911267a6d00dc3922c2a 100644
368--- a/pkg/daemon/daemon.go
369+++ b/pkg/daemon/daemon.go
370@@ -150,30 +150,35 @@ func (d *GitDaemon) handleClient(conn net.Conn) {
371 d.conns.Close(c) // nolint: errcheck
372 }()
373
374- readc := make(chan struct{}, 1)
375+ errc := make(chan error, 1)
376+
377 s := pktline.NewScanner(c)
378 go func() {
379 if !s.Scan() {
380 if err := s.Err(); err != nil {
381- if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
382- d.fatal(c, git.ErrTimeout)
383- } else {
384- d.logger.Debugf("git: error scanning pktline: %v", err)
385- d.fatal(c, git.ErrSystemMalfunction)
386- }
387+ errc <- err
388 }
389- return
390 }
391- readc <- struct{}{}
392+ errc <- nil
393 }()
394
395 select {
396 case <-ctx.Done():
397 if err := ctx.Err(); err != nil {
398 d.logger.Debugf("git: connection context error: %v", err)
399+ d.fatal(c, git.ErrTimeout)
400 }
401 return
402- case <-readc:
403+ case err := <-errc:
404+ if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
405+ d.fatal(c, git.ErrTimeout)
406+ return
407+ } else if err != nil {
408+ d.logger.Debugf("git: error scanning pktline: %v", err)
409+ d.fatal(c, git.ErrSystemMalfunction)
410+ return
411+ }
412+
413 line := s.Bytes()
414 split := bytes.SplitN(line, []byte{' '}, 2)
415 if len(split) != 2 {
416diff --git a/pkg/daemon/daemon_test.go b/pkg/daemon/daemon_test.go
417index 7ebe31b5358b7e5de7813cec41ca4cbdf790734b..88b4fdec7a9e2de2d4c9621b549c57c269f11ddb 100644
418--- a/pkg/daemon/daemon_test.go
419+++ b/pkg/daemon/daemon_test.go
420@@ -8,6 +8,7 @@ import (
421 "os"
422 "strings"
423 "testing"
424+ "time"
425
426 "github.com/charmbracelet/soft-serve/pkg/backend"
427 "github.com/charmbracelet/soft-serve/pkg/config"
428@@ -50,7 +51,7 @@ func TestMain(m *testing.M) {
429 }
430 datastore := database.New(ctx, dbx)
431 ctx = store.WithContext(ctx, datastore)
432- be := backend.New(ctx, cfg, dbx)
433+ be := backend.New(ctx, cfg, dbx, datastore)
434 ctx = backend.WithContext(ctx, be)
435 d, err := NewGitDaemon(ctx)
436 if err != nil {
437@@ -78,9 +79,10 @@ func TestIdleTimeout(t *testing.T) {
438 if err != nil {
439 t.Fatal(err)
440 }
441+ time.Sleep(time.Second)
442 _, err = readPktline(c)
443- if err != nil && err.Error() != git.ErrTimeout.Error() {
444- t.Fatalf("expected %q error, got %q", git.ErrTimeout, err)
445+ if err == nil {
446+ t.Errorf("expected error, got nil")
447 }
448 }
449
450@@ -94,7 +96,7 @@ func TestInvalidRepo(t *testing.T) {
451 }
452 _, err = readPktline(c)
453 if err != nil && err.Error() != git.ErrInvalidRepo.Error() {
454- t.Fatalf("expected %q error, got %q", git.ErrInvalidRepo, err)
455+ t.Errorf("expected %q error, got %q", git.ErrInvalidRepo, err)
456 }
457 }
458
459diff --git a/pkg/db/context_test.go b/pkg/db/context_test.go
460new file mode 100644
461index 0000000000000000000000000000000000000000..5da52397583b46773da8c736e9dcdd9922f60b99
462--- /dev/null
463+++ b/pkg/db/context_test.go
464@@ -0,0 +1,28 @@
465+package db_test
466+
467+import (
468+ "context"
469+ "testing"
470+
471+ "github.com/charmbracelet/soft-serve/pkg/db"
472+ "github.com/charmbracelet/soft-serve/pkg/db/internal/test"
473+)
474+
475+func TestBadFromContext(t *testing.T) {
476+ ctx := context.TODO()
477+ if c := db.FromContext(ctx); c != nil {
478+ t.Errorf("FromContext(ctx) => %v, want %v", c, nil)
479+ }
480+}
481+
482+func TestGoodFromContext(t *testing.T) {
483+ ctx := context.TODO()
484+ dbx, err := test.OpenSqlite(ctx, t)
485+ if err != nil {
486+ t.Fatal(err)
487+ }
488+ ctx = db.WithContext(ctx, dbx)
489+ if c := db.FromContext(ctx); c == nil {
490+ t.Errorf("FromContext(ctx) => %v, want %v", c, dbx)
491+ }
492+}
493diff --git a/pkg/db/db_test.go b/pkg/db/db_test.go
494new file mode 100644
495index 0000000000000000000000000000000000000000..3ca95ad3767ee459251ac7d229da68018105d07d
496--- /dev/null
497+++ b/pkg/db/db_test.go
498@@ -0,0 +1,17 @@
499+package db
500+
501+import (
502+ "context"
503+ "strings"
504+ "testing"
505+)
506+
507+func TestOpenUnknownDriver(t *testing.T) {
508+ _, err := Open(context.TODO(), "invalid", "")
509+ if err == nil {
510+ t.Error("Open(invalid) => nil, want error")
511+ }
512+ if !strings.Contains(err.Error(), "unknown driver") {
513+ t.Errorf("Open(invalid) => %v, want error containing 'unknown driver'", err)
514+ }
515+}
516diff --git a/pkg/db/errors_test.go b/pkg/db/errors_test.go
517new file mode 100644
518index 0000000000000000000000000000000000000000..0aba634572abca53a9337a08f022ed333a797d12
519--- /dev/null
520+++ b/pkg/db/errors_test.go
521@@ -0,0 +1,25 @@
522+package db
523+
524+import (
525+ "database/sql"
526+ "errors"
527+ "fmt"
528+ "testing"
529+)
530+
531+func TestWrapErrorBadNoRows(t *testing.T) {
532+ for _, e := range []error{
533+ fmt.Errorf("foo"),
534+ errors.New("bar"),
535+ } {
536+ if err := WrapError(e); err != e {
537+ t.Errorf("WrapError(%v) => %v, want %v", e, err, e)
538+ }
539+ }
540+}
541+
542+func TestWrapErrorGoodNoRows(t *testing.T) {
543+ if err := WrapError(sql.ErrNoRows); err != ErrRecordNotFound {
544+ t.Errorf("WrapError(sql.ErrNoRows) => %v, want %v", err, ErrRecordNotFound)
545+ }
546+}
547diff --git a/pkg/db/internal/test/test.go b/pkg/db/internal/test/test.go
548new file mode 100644
549index 0000000000000000000000000000000000000000..b40e2ad90ea73be4c6887e41564e1ff5958b0277
550--- /dev/null
551+++ b/pkg/db/internal/test/test.go
552@@ -0,0 +1,29 @@
553+package test
554+
555+import (
556+ "context"
557+ "path/filepath"
558+ "testing"
559+
560+ "github.com/charmbracelet/soft-serve/pkg/db"
561+)
562+
563+// OpenSqlite opens a new temp SQLite database for testing.
564+// It removes the database file when the test is done using tb.Cleanup.
565+// If ctx is nil, context.TODO() is used.
566+func OpenSqlite(ctx context.Context, tb testing.TB) (*db.DB, error) {
567+ if ctx == nil {
568+ ctx = context.TODO()
569+ }
570+ dbpath := filepath.Join(tb.TempDir(), "test.db")
571+ dbx, err := db.Open(ctx, "sqlite", dbpath)
572+ if err != nil {
573+ return nil, err
574+ }
575+ tb.Cleanup(func() {
576+ if err := dbx.Close(); err != nil {
577+ tb.Error(err)
578+ }
579+ })
580+ return dbx, nil
581+}
582diff --git a/pkg/db/migrate/migrate_test.go b/pkg/db/migrate/migrate_test.go
583new file mode 100644
584index 0000000000000000000000000000000000000000..bfc9d20f223bbfbb3a6e3fb6bf17894268659380
585--- /dev/null
586+++ b/pkg/db/migrate/migrate_test.go
587@@ -0,0 +1,22 @@
588+package migrate
589+
590+import (
591+ "context"
592+ "testing"
593+
594+ "github.com/charmbracelet/soft-serve/pkg/config"
595+ "github.com/charmbracelet/soft-serve/pkg/db/internal/test"
596+)
597+
598+func TestMigrate(t *testing.T) {
599+ // XXX: we need a config.Config in the context for the migrations to run
600+ // properly. Some migrations depend on the config being present.
601+ ctx := config.WithContext(context.TODO(), config.DefaultConfig())
602+ dbx, err := test.OpenSqlite(ctx, t)
603+ if err != nil {
604+ t.Fatal(err)
605+ }
606+ if err := Migrate(ctx, dbx); err != nil {
607+ t.Errorf("Migrate() => %v, want nil error", err)
608+ }
609+}
610diff --git a/pkg/git/git.go b/pkg/git/git.go
611index e523b90a279dde5590614f20c419b24a135aa42d..d6c014296f1e1b2dfc539c26d316256a3e1996be 100644
612--- a/pkg/git/git.go
613+++ b/pkg/git/git.go
614@@ -2,6 +2,7 @@ package git
615
616 import (
617 "context"
618+ "errors"
619 "fmt"
620 "io"
621 "path/filepath"
622@@ -13,6 +14,11 @@ import (
623 "github.com/go-git/go-git/v5/plumbing/format/pktline"
624 )
625
626+var (
627+ // ErrNoBranches is returned when a repo has no branches.
628+ ErrNoBranches = errors.New("no branches found")
629+)
630+
631 // WritePktline encodes and writes a pktline to the given writer.
632 func WritePktline(w io.Writer, v ...interface{}) error {
633 msg := fmt.Sprintln(v...)
634@@ -57,18 +63,18 @@ func EnsureWithin(reposDir string, repo string) error {
635
636 // EnsureDefaultBranch ensures the repo has a default branch.
637 // It will prefer choosing "main" or "master" if available.
638-func EnsureDefaultBranch(ctx context.Context, scmd ServiceCommand) error {
639- r, err := git.Open(scmd.Dir)
640+func EnsureDefaultBranch(ctx context.Context, repoPath string) error {
641+ r, err := git.Open(repoPath)
642 if err != nil {
643 return err
644 }
645 brs, err := r.Branches()
646+ if len(brs) == 0 {
647+ return ErrNoBranches
648+ }
649 if err != nil {
650 return err
651 }
652- if len(brs) == 0 {
653- return fmt.Errorf("no branches found")
654- }
655 // Rename the default branch to the first branch available
656 _, err = r.HEAD()
657 if err == git.ErrReferenceNotExist {
658diff --git a/pkg/git/git_test.go b/pkg/git/git_test.go
659index d95cb6497daed360740ccfb50a5a79c2d2944712..4e4a476d297661781249bdb8b9303a63c22bc580 100644
660--- a/pkg/git/git_test.go
661+++ b/pkg/git/git_test.go
662@@ -2,8 +2,12 @@ package git
663
664 import (
665 "bytes"
666+ "context"
667+ "errors"
668 "fmt"
669 "testing"
670+
671+ "github.com/charmbracelet/soft-serve/git"
672 )
673
674 func TestPktline(t *testing.T) {
675@@ -54,3 +58,40 @@ func TestPktline(t *testing.T) {
676 })
677 }
678 }
679+
680+func TestEnsureWithinBad(t *testing.T) {
681+ tmp := t.TempDir()
682+ for _, f := range []string{
683+ "..",
684+ "../../../",
685+ } {
686+ if err := EnsureWithin(tmp, f); err == nil {
687+ t.Errorf("EnsureWithin(%q, %q) => nil, want non-nil error", tmp, f)
688+ }
689+ }
690+}
691+
692+func TestEnsureWithinGood(t *testing.T) {
693+ tmp := t.TempDir()
694+ for _, f := range []string{
695+ tmp,
696+ tmp + "/foo",
697+ tmp + "/foo/bar",
698+ } {
699+ if err := EnsureWithin(tmp, f); err != nil {
700+ t.Errorf("EnsureWithin(%q, %q) => %v, want nil error", tmp, f, err)
701+ }
702+ }
703+}
704+
705+func TestEnsureDefaultBranchEmpty(t *testing.T) {
706+ tmp := t.TempDir()
707+ r, err := git.Init(tmp, false)
708+ if err != nil {
709+ t.Fatal(err)
710+ }
711+
712+ if err := EnsureDefaultBranch(context.TODO(), r.Path); !errors.Is(err, ErrNoBranches) {
713+ t.Errorf("EnsureDefaultBranch(%q) => %v, want ErrNoBranches", tmp, err)
714+ }
715+}
716diff --git a/pkg/git/lfs.go b/pkg/git/lfs.go
717index 5aae027e52acf27d2503193cd0ce773e255c23b0..7dc4b8b353ea480501eab855fee922d2a94f74ee 100644
718--- a/pkg/git/lfs.go
719+++ b/pkg/git/lfs.go
720@@ -21,17 +21,8 @@ import (
721 "github.com/charmbracelet/soft-serve/pkg/proto"
722 "github.com/charmbracelet/soft-serve/pkg/storage"
723 "github.com/charmbracelet/soft-serve/pkg/store"
724- "github.com/rubyist/tracerx"
725 )
726
727-func init() {
728- // git-lfs-transfer uses tracerx for logging.
729- // use a custom key to avoid conflicts
730- // SOFT_SERVE_TRACE=1 to enable tracing git-lfs-transfer in soft-serve
731- tracerx.DefaultKey = "SOFT_SERVE"
732- tracerx.Prefix = "trace soft-serve-lfs-transfer: "
733-}
734-
735 // lfsTransfer implements transfer.Backend.
736 type lfsTransfer struct {
737 ctx context.Context
738diff --git a/pkg/hooks/gen.go b/pkg/hooks/gen.go
739index 5eb16ebc2fe47baae43332d8575bb6f642feca7d..467b2f263a32263a016c08f39ea9357d19b2f98c 100644
740--- a/pkg/hooks/gen.go
741+++ b/pkg/hooks/gen.go
742@@ -3,7 +3,6 @@ package hooks
743 import (
744 "bytes"
745 "context"
746- "flag"
747 "os"
748 "path/filepath"
749 "text/template"
750@@ -30,11 +29,6 @@ const (
751 // This function should be called by the backend when a repository is created.
752 // TODO: support context.
753 func GenerateHooks(_ context.Context, cfg *config.Config, repo string) error {
754- // TODO: support git hook tests.
755- if flag.Lookup("test.v") != nil {
756- log.WithPrefix("backend.hooks").Warn("refusing to set up hooks when in test")
757- return nil
758- }
759 repo = utils.SanitizeRepo(repo) + ".git"
760 hooksPath := filepath.Join(cfg.DataPath, "repos", repo, "hooks")
761 if err := os.MkdirAll(hooksPath, os.ModePerm); err != nil {
762diff --git a/pkg/hooks/gen_test.go b/pkg/hooks/gen_test.go
763new file mode 100644
764index 0000000000000000000000000000000000000000..d9f03ee39849364157d177d30a13ed85a94b25f1
765--- /dev/null
766+++ b/pkg/hooks/gen_test.go
767@@ -0,0 +1,40 @@
768+package hooks
769+
770+import (
771+ "context"
772+ "os"
773+ "path/filepath"
774+ "testing"
775+
776+ "github.com/charmbracelet/soft-serve/git"
777+ "github.com/charmbracelet/soft-serve/pkg/config"
778+)
779+
780+func TestGenerateHooks(t *testing.T) {
781+ tmp := t.TempDir()
782+ cfg := config.DefaultConfig()
783+ cfg.DataPath = tmp
784+ repoPath := filepath.Join(tmp, "repos", "test.git")
785+ _, err := git.Init(repoPath, true)
786+ if err != nil {
787+ t.Fatal(err)
788+ }
789+
790+ if err := GenerateHooks(context.TODO(), cfg, "test.git"); err != nil {
791+ t.Fatal(err)
792+ }
793+
794+ for _, hn := range []string{
795+ PreReceiveHook,
796+ UpdateHook,
797+ PostReceiveHook,
798+ PostUpdateHook,
799+ } {
800+ if _, err := os.Stat(filepath.Join(repoPath, "hooks", hn)); err != nil {
801+ t.Fatal(err)
802+ }
803+ if _, err := os.Stat(filepath.Join(repoPath, "hooks", hn+".d", "soft-serve")); err != nil {
804+ t.Fatal(err)
805+ }
806+ }
807+}
808diff --git a/pkg/jwk/jwk.go b/pkg/jwk/jwk.go
809index b758f88aa815d6a58e86db64e8f87f02a2564651..f7fe924041a39f1ae76bc6afec95ced6ca81cbec 100644
810--- a/pkg/jwk/jwk.go
811+++ b/pkg/jwk/jwk.go
812@@ -32,7 +32,7 @@ func (p Pair) JWK() jose.JSONWebKey {
813
814 // NewPair creates a new JSON Web Key pair.
815 func NewPair(cfg *config.Config) (Pair, error) {
816- kp, err := cfg.SSH.KeyPair()
817+ kp, err := config.KeyPair(cfg)
818 if err != nil {
819 return Pair{}, err
820 }
821diff --git a/pkg/jwk/jwk_test.go b/pkg/jwk/jwk_test.go
822new file mode 100644
823index 0000000000000000000000000000000000000000..a8d95fbdb9f79eccbfe99d598a96c726c619c77f
824--- /dev/null
825+++ b/pkg/jwk/jwk_test.go
826@@ -0,0 +1,22 @@
827+package jwk
828+
829+import (
830+ "errors"
831+ "testing"
832+
833+ "github.com/charmbracelet/soft-serve/pkg/config"
834+)
835+
836+func TestBadNewPair(t *testing.T) {
837+ _, err := NewPair(nil)
838+ if !errors.Is(err, config.ErrNilConfig) {
839+ t.Errorf("NewPair(nil) => %v, want %v", err, config.ErrNilConfig)
840+ }
841+}
842+
843+func TestGoodNewPair(t *testing.T) {
844+ cfg := config.DefaultConfig()
845+ if _, err := NewPair(cfg); err != nil {
846+ t.Errorf("NewPair(cfg) => _, %v, want nil error", err)
847+ }
848+}
849diff --git a/pkg/log/log.go b/pkg/log/log.go
850index aa9a6b4181b3b0fcd9458ddd66a460f55b7d6d34..b9134ead76d6d5a96a413cfea5d2202647054374 100644
851--- a/pkg/log/log.go
852+++ b/pkg/log/log.go
853@@ -11,6 +11,9 @@ import (
854
855 // NewLogger returns a new logger with default settings.
856 func NewLogger(cfg *config.Config) (*log.Logger, *os.File, error) {
857+ if cfg == nil {
858+ return nil, nil, config.ErrNilConfig
859+ }
860 logger := log.NewWithOptions(os.Stderr, log.Options{
861 ReportTimestamp: true,
862 TimeFormat: time.DateOnly,
863@@ -37,7 +40,8 @@ func NewLogger(cfg *config.Config) (*log.Logger, *os.File, error) {
864
865 var f *os.File
866 if cfg.Log.Path != "" {
867- f, err := os.OpenFile(cfg.Log.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
868+ var err error
869+ f, err = os.OpenFile(cfg.Log.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
870 if err != nil {
871 return nil, nil, err
872 }
873diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go
874new file mode 100644
875index 0000000000000000000000000000000000000000..131e05809e85eb5b98945c928413ff777e93439a
876--- /dev/null
877+++ b/pkg/log/log_test.go
878@@ -0,0 +1,43 @@
879+package log
880+
881+import (
882+ "path/filepath"
883+ "testing"
884+
885+ "github.com/charmbracelet/soft-serve/pkg/config"
886+)
887+
888+func TestGoodNewLogger(t *testing.T) {
889+ for _, c := range []*config.Config{
890+ config.DefaultConfig(),
891+ {},
892+ {Log: config.LogConfig{Path: filepath.Join(t.TempDir(), "logfile.txt")}},
893+ } {
894+ _, f, err := NewLogger(c)
895+ if err != nil {
896+ t.Errorf("expected nil got %v", err)
897+ }
898+ if f != nil {
899+ if err := f.Close(); err != nil {
900+ t.Errorf("failed to close logger: %v", err)
901+ }
902+ }
903+ }
904+}
905+
906+func TestBadNewLogger(t *testing.T) {
907+ for _, c := range []*config.Config{
908+ nil,
909+ {Log: config.LogConfig{Path: "\x00"}},
910+ } {
911+ _, f, err := NewLogger(c)
912+ if err == nil {
913+ t.Errorf("expected error got nil")
914+ }
915+ if f != nil {
916+ if err := f.Close(); err != nil {
917+ t.Errorf("failed to close logger: %v", err)
918+ }
919+ }
920+ }
921+}
922diff --git a/pkg/ssh/cmd/git.go b/pkg/ssh/cmd/git.go
923index 355d235d42a5d5fa04f77338015363fbd09bb4c9..f6aea60ab150f8995e6cb1b28fa5c39766bf028f 100644
924--- a/pkg/ssh/cmd/git.go
925+++ b/pkg/ssh/cmd/git.go
926@@ -249,7 +249,7 @@ func gitRunE(cmd *cobra.Command, args []string) error {
927 return git.ErrSystemMalfunction
928 }
929
930- if err := git.EnsureDefaultBranch(ctx, scmd); err != nil {
931+ if err := git.EnsureDefaultBranch(ctx, scmd.Dir); err != nil {
932 logger.Error("failed to ensure default branch", "err", err, "repo", name)
933 return git.ErrSystemMalfunction
934 }
935diff --git a/pkg/ssh/session_test.go b/pkg/ssh/session_test.go
936index 845e178654b591257c3c0184a264a0d4673d3f78..792759c167361c4bd9c10783dda279983e6bc890 100644
937--- a/pkg/ssh/session_test.go
938+++ b/pkg/ssh/session_test.go
939@@ -76,7 +76,7 @@ func setup(tb testing.TB) (*gossh.Session, func() error) {
940 }
941 dbstore := database.New(ctx, dbx)
942 ctx = store.WithContext(ctx, dbstore)
943- be := backend.New(ctx, cfg, dbx)
944+ be := backend.New(ctx, cfg, dbx, dbstore)
945 ctx = backend.WithContext(ctx, be)
946 return testsession.New(tb, &ssh.Server{
947 Handler: ContextMiddleware(cfg, dbx, dbstore, be, log.Default())(bm.MiddlewareWithProgramHandler(SessionHandler, termenv.ANSI256)(func(s ssh.Session) {
948diff --git a/pkg/web/auth.go b/pkg/web/auth.go
949index 631660bd3c64611c0ebc234ba9861184f1c54714..96b2bab85fcc94200525088d0bf37bac31942bf8 100644
950--- a/pkg/web/auth.go
951+++ b/pkg/web/auth.go
952@@ -136,7 +136,7 @@ var ErrInvalidToken = errors.New("invalid token")
953 func parseJWT(ctx context.Context, bearer string) (*jwt.RegisteredClaims, error) {
954 cfg := config.FromContext(ctx)
955 logger := log.FromContext(ctx).WithPrefix("http.auth")
956- kp, err := cfg.SSH.KeyPair()
957+ kp, err := config.KeyPair(cfg)
958 if err != nil {
959 return nil, err
960 }
961diff --git a/pkg/web/git.go b/pkg/web/git.go
962index c03e56afd591285aeb34146f9d0211dd01b9db73..8f1170bce71edda69e81f171415776b4800b1474 100644
963--- a/pkg/web/git.go
964+++ b/pkg/web/git.go
965@@ -441,7 +441,7 @@ func serviceRpc(w http.ResponseWriter, r *http.Request) {
966 }
967
968 if service == git.ReceivePackService {
969- if err := git.EnsureDefaultBranch(ctx, cmd); err != nil {
970+ if err := git.EnsureDefaultBranch(ctx, cmd.Dir); err != nil {
971 logger.Errorf("failed to ensure default branch: %s", err)
972 }
973 }
974diff --git a/pkg/web/http.go b/pkg/web/http.go
975index 9e109a0e2b880bf60300f5ac8ecb0d40cbff5225..20fae6bd176edff577947b56e14722f8f53d8819 100644
976--- a/pkg/web/http.go
977+++ b/pkg/web/http.go
978@@ -11,9 +11,10 @@ import (
979
980 // HTTPServer is an http server.
981 type HTTPServer struct {
982- ctx context.Context
983- cfg *config.Config
984- server *http.Server
985+ ctx context.Context
986+ cfg *config.Config
987+
988+ Server *http.Server
989 }
990
991 // NewHTTPServer creates a new HTTP server.
992@@ -23,7 +24,7 @@ func NewHTTPServer(ctx context.Context) (*HTTPServer, error) {
993 s := &HTTPServer{
994 ctx: ctx,
995 cfg: cfg,
996- server: &http.Server{
997+ Server: &http.Server{
998 Addr: cfg.HTTP.ListenAddr,
999 Handler: NewRouter(ctx),
1000 ReadHeaderTimeout: time.Second * 10,
1001@@ -38,18 +39,18 @@ func NewHTTPServer(ctx context.Context) (*HTTPServer, error) {
1002
1003 // Close closes the HTTP server.
1004 func (s *HTTPServer) Close() error {
1005- return s.server.Close()
1006+ return s.Server.Close()
1007 }
1008
1009 // ListenAndServe starts the HTTP server.
1010 func (s *HTTPServer) ListenAndServe() error {
1011 if s.cfg.HTTP.TLSKeyPath != "" && s.cfg.HTTP.TLSCertPath != "" {
1012- return s.server.ListenAndServeTLS(s.cfg.HTTP.TLSCertPath, s.cfg.HTTP.TLSKeyPath)
1013+ return s.Server.ListenAndServeTLS(s.cfg.HTTP.TLSCertPath, s.cfg.HTTP.TLSKeyPath)
1014 }
1015- return s.server.ListenAndServe()
1016+ return s.Server.ListenAndServe()
1017 }
1018
1019 // Shutdown gracefully shuts down the HTTP server.
1020 func (s *HTTPServer) Shutdown(ctx context.Context) error {
1021- return s.server.Shutdown(ctx)
1022+ return s.Server.Shutdown(ctx)
1023 }
1024diff --git a/pkg/webhook/content_type_test.go b/pkg/webhook/content_type_test.go
1025new file mode 100644
1026index 0000000000000000000000000000000000000000..1aff46e810bf9a555d63f88c0f554a105f81dbaa
1027--- /dev/null
1028+++ b/pkg/webhook/content_type_test.go
1029@@ -0,0 +1,117 @@
1030+package webhook
1031+
1032+import "testing"
1033+
1034+func TestParseContentType(t *testing.T) {
1035+ tests := []struct {
1036+ name string
1037+ s string
1038+ want ContentType
1039+ err error
1040+ }{
1041+ {
1042+ name: "JSON",
1043+ s: "application/json",
1044+ want: ContentTypeJSON,
1045+ },
1046+ {
1047+ name: "Form",
1048+ s: "application/x-www-form-urlencoded",
1049+ want: ContentTypeForm,
1050+ },
1051+ {
1052+ name: "Invalid",
1053+ s: "application/invalid",
1054+ err: ErrInvalidContentType,
1055+ want: -1,
1056+ },
1057+ }
1058+
1059+ for _, tt := range tests {
1060+ t.Run(tt.name, func(t *testing.T) {
1061+ got, err := ParseContentType(tt.s)
1062+ if err != tt.err {
1063+ t.Errorf("ParseContentType() error = %v, wantErr %v", err, tt.err)
1064+ return
1065+ }
1066+ if got != tt.want {
1067+ t.Errorf("ParseContentType() got = %v, want %v", got, tt.want)
1068+ }
1069+ })
1070+ }
1071+}
1072+
1073+func TestUnmarshalText(t *testing.T) {
1074+ tests := []struct {
1075+ name string
1076+ text []byte
1077+ want ContentType
1078+ wantErr bool
1079+ }{
1080+ {
1081+ name: "JSON",
1082+ text: []byte("application/json"),
1083+ want: ContentTypeJSON,
1084+ },
1085+ {
1086+ name: "Form",
1087+ text: []byte("application/x-www-form-urlencoded"),
1088+ want: ContentTypeForm,
1089+ },
1090+ {
1091+ name: "Invalid",
1092+ text: []byte("application/invalid"),
1093+ wantErr: true,
1094+ },
1095+ }
1096+
1097+ for _, tt := range tests {
1098+ t.Run(tt.name, func(t *testing.T) {
1099+ c := new(ContentType)
1100+ if err := c.UnmarshalText(tt.text); (err != nil) != tt.wantErr {
1101+ t.Errorf("ContentType.UnmarshalText() error = %v, wantErr %v", err, tt.wantErr)
1102+ }
1103+ if *c != tt.want {
1104+ t.Errorf("ContentType.UnmarshalText() got = %v, want %v", *c, tt.want)
1105+ }
1106+ })
1107+ }
1108+}
1109+
1110+func TestMarshalText(t *testing.T) {
1111+ tests := []struct {
1112+ name string
1113+ c ContentType
1114+ want []byte
1115+ wantErr bool
1116+ }{
1117+ {
1118+ name: "JSON",
1119+ c: ContentTypeJSON,
1120+ want: []byte("application/json"),
1121+ },
1122+ {
1123+ name: "Form",
1124+ c: ContentTypeForm,
1125+ want: []byte("application/x-www-form-urlencoded"),
1126+ },
1127+ {
1128+ name: "Invalid",
1129diff --git a/pkg/webhook/push.go b/pkg/webhook/push.go
1130index 8af2c13982fcfa24f3e9e0168bd7ba857efa3bf6..6ef6062cdd9e4acd499ec09421e18fb273535802 100644
1131--- a/pkg/webhook/push.go
1132+++ b/pkg/webhook/push.go
1133@@ -2,6 +2,7 @@ package webhook
1134
1135 import (
1136 "context"
1137+ "errors"
1138 "fmt"
1139
1140 gitm "github.com/aymanbagabas/git-module"
1141@@ -75,7 +76,11 @@ func NewPushEvent(ctx context.Context, user proto.User, repo proto.Repository, r
1142 }
1143
1144 payload.Repository.DefaultBranch, err = proto.RepositoryDefaultBranch(repo)
1145- if err != nil {
1146+ // XXX: we check for ErrReferenceNotExist here because we don't want to
1147+ // return an error if the repo is an empty repo.
1148+ // This means that the repo doesn't have a default branch yet and this is
1149+ // the first push to it.
1150+ if err != nil && !errors.Is(err, git.ErrReferenceNotExist) {
1151 return PushEvent{}, err
1152 }
1153
1154diff --git a/testscript/script_test.go b/testscript/script_test.go
1155index 94c6408bc2810ac0aede32b5eee2fd2f55c2dc2b..f7dd22eec65b699667e357bf2bbd348e9cae17fd 100644
1156--- a/testscript/script_test.go
1157+++ b/testscript/script_test.go
1158@@ -3,41 +3,65 @@ package testscript
1159 import (
1160 "bytes"
1161 "context"
1162- "database/sql"
1163+ "encoding/json"
1164 "flag"
1165 "fmt"
1166 "io"
1167+ "math/rand"
1168 "net"
1169 "net/http"
1170 "net/url"
1171 "os"
1172+ "os/exec"
1173 "path/filepath"
1174+ "runtime"
1175 "strings"
1176- "sync"
1177 "testing"
1178 "time"
1179
1180 "github.com/charmbracelet/keygen"
1181- "github.com/charmbracelet/log"
1182- "github.com/charmbracelet/soft-serve/cmd/soft/serve"
1183- "github.com/charmbracelet/soft-serve/pkg/backend"
1184 "github.com/charmbracelet/soft-serve/pkg/config"
1185 "github.com/charmbracelet/soft-serve/pkg/db"
1186- "github.com/charmbracelet/soft-serve/pkg/db/migrate"
1187- logr "github.com/charmbracelet/soft-serve/pkg/log"
1188- "github.com/charmbracelet/soft-serve/pkg/store"
1189- "github.com/charmbracelet/soft-serve/pkg/store/database"
1190 "github.com/charmbracelet/soft-serve/pkg/test"
1191 "github.com/rogpeppe/go-internal/testscript"
1192 "github.com/spf13/cobra"
1193 "golang.org/x/crypto/ssh"
1194 )
1195
1196-var update = flag.Bool("update", false, "update script files")
1197+var (
1198+ update = flag.Bool("update", false, "update script files")
1199+ binPath string
1200+)
1201+
1202+func TestMain(m *testing.M) {
1203+ tmp, err := os.MkdirTemp("", "soft-serve*")
1204+ if err != nil {
1205+ fmt.Fprintf(os.Stderr, "failed to create temporary directory: %s", err)
1206+ os.Exit(1)
1207+ }
1208+ defer os.RemoveAll(tmp)
1209+
1210+ binPath = filepath.Join(tmp, "soft")
1211+ if runtime.GOOS == "windows" {
1212+ binPath += ".exe"
1213+ }
1214+
1215+ // Build the soft binary with -cover flag.
1216+ cmd := exec.Command("go", "build", "-race", "-cover", "-o", binPath, filepath.Join("..", "cmd", "soft"))
1217+ if err := cmd.Run(); err != nil {
1218+ fmt.Fprintf(os.Stderr, "failed to build soft-serve binary: %s", err)
1219+ os.Exit(1)
1220+ }
1221+
1222+ // Run tests
1223+ os.Exit(m.Run())
1224+
1225+ // Add binPath to PATH
1226+ os.Setenv("PATH", fmt.Sprintf("%s%c%s", os.Getenv("PATH"), os.PathListSeparator, filepath.Dir(binPath)))
1227+}
1228
1229 func TestScript(t *testing.T) {
1230 flag.Parse()
1231- var lock sync.Mutex
1232
1233 mkkey := func(name string) (string, *keygen.SSHKeyPair) {
1234 path := filepath.Join(t.TempDir(), name)
1235@@ -53,21 +77,27 @@ func TestScript(t *testing.T) {
1236 _, user1 := mkkey("user1")
1237
1238 testscript.Run(t, testscript.Params{
1239- Dir: "./testdata/",
1240- UpdateScripts: *update,
1241+ Dir: "./testdata/",
1242+ UpdateScripts: *update,
1243+ RequireExplicitExec: true,
1244 Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
1245- "soft": cmdSoft(admin1.Signer()),
1246- "usoft": cmdSoft(user1.Signer()),
1247- "git": cmdGit(key),
1248- "curl": cmdCurl,
1249- "mkfile": cmdMkfile,
1250- "envfile": cmdEnvfile,
1251- "readfile": cmdReadfile,
1252- "dos2unix": cmdDos2Unix,
1253+ "soft": cmdSoft(admin1.Signer()),
1254+ "usoft": cmdSoft(user1.Signer()),
1255+ "git": cmdGit(key),
1256+ "curl": cmdCurl,
1257+ "mkfile": cmdMkfile,
1258diff --git a/testscript/testdata/help.txtar b/testscript/testdata/help.txtar
1259index 257ad244799114f510f9c470075575d8d0ba411d..d6756ca02fde1607ffa23add93e1b95adc8de89f 100644
1260--- a/testscript/testdata/help.txtar
1261+++ b/testscript/testdata/help.txtar
1262@@ -1,9 +1,18 @@
1263 # vi: set ft=conf
1264 [windows] dos2unix help.txt
1265
1266+# start soft serve
1267+exec soft serve --sync-hooks &
1268+# wait for server to start
1269+waitforserver
1270+
1271 soft --help
1272 cmpenv stdout help.txt
1273
1274+# stop the server
1275+[windows] stopserver
1276+[windows] ! stderr .
1277+
1278 -- help.txt --
1279 Soft Serve is a self-hostable Git server for the command line.
1280
1281diff --git a/testscript/testdata/http.txtar b/testscript/testdata/http.txtar
1282index f2af369af97c6658c0bc9cffc02983ef61ee4ce1..662e9b011695f478763f7b9767691b265510e2a1 100644
1283--- a/testscript/testdata/http.txtar
1284+++ b/testscript/testdata/http.txtar
1285@@ -6,6 +6,11 @@
1286 # convert crlf to lf on windows
1287 [windows] dos2unix http1.txt http2.txt http3.txt goget.txt gitclone.txt
1288
1289+# start soft serve
1290+exec soft serve &
1291+# wait for server to start
1292+waitforserver
1293+
1294 # create user
1295 soft user create user1 --key "$USER1_AUTHORIZED_KEY"
1296
1297@@ -133,6 +138,10 @@ stdout '404.*'
1298 curl http://$TOKEN@localhost:$HTTP_PORT/repo2.git?go-get=1
1299 cmpenv stdout goget.txt
1300
1301+# stop the server
1302+[windows] stopserver
1303+[windows] ! stderr .
1304+
1305 -- http1.txt --
1306 {"transfer":"basic","objects":[{"oid":"","size":0,"error":{"code":422,"message":"invalid object"}}],"hash_algo":"sha256"}
1307 -- http2.txt --
1308diff --git a/testscript/testdata/jwt.txtar b/testscript/testdata/jwt.txtar
1309index 07c605e948938d4f293d13c875f274d433887016..7a257044ecf0ead8e1676c51b23276b99f171a0f 100644
1310--- a/testscript/testdata/jwt.txtar
1311+++ b/testscript/testdata/jwt.txtar
1312@@ -1,5 +1,10 @@
1313 # vi: set ft=conf
1314
1315+# start soft serve
1316+exec soft serve &
1317+# wait for server to start
1318+waitforserver
1319+
1320 # create user
1321 soft user create user1 --key "$USER1_AUTHORIZED_KEY"
1322
1323@@ -12,3 +17,7 @@ usoft jwt
1324 stdout '.*\..*\..*'
1325 usoft jwt repo
1326 stdout '.*\..*\..*'
1327+
1328+# stop the server
1329+[windows] stopserver
1330+[windows] ! stderr .
1331diff --git a/testscript/testdata/mirror.txtar b/testscript/testdata/mirror.txtar
1332index e1db9214d4f96288b5561b0ed8c41d413b5d66ff..d3cc1915de47e519a678468fedcf749277e5dc82 100644
1333--- a/testscript/testdata/mirror.txtar
1334+++ b/testscript/testdata/mirror.txtar
1335@@ -3,6 +3,11 @@
1336 # convert crlf to lf on windows
1337 [windows] dos2unix info1.txt info2.txt tree.txt
1338
1339+# start soft serve
1340+exec soft serve &
1341+# wait for server to start
1342+waitforserver
1343+
1344 # import a repo
1345 soft repo import --mirror charmbracelet/catwalk https://github.com/charmbracelet/catwalk.git
1346
1347@@ -74,6 +79,10 @@ cmp stdout info2.txt
1348 soft repo blob charmbracelet/test LICENSE
1349 stdout '.*Creative Commons.*'
1350
1351+# stop the server
1352+[windows] stopserver
1353+[windows] ! stderr .
1354+
1355
1356 -- info1.txt --
1357 Project Name:
1358diff --git a/testscript/testdata/repo-blob.txtar b/testscript/testdata/repo-blob.txtar
1359index 770bd2d067f5de2096b8a0b37c36f073a515e577..040160b17a9b9a29c020cb07502a534b5f2d0db6 100644
1360--- a/testscript/testdata/repo-blob.txtar
1361+++ b/testscript/testdata/repo-blob.txtar
1362@@ -3,6 +3,11 @@
1363 # convert crlf to lf on windows
1364 [windows] dos2unix blob1.txt blob2.txt blob3.txt
1365
1366+# start soft serve
1367+exec soft serve &
1368+# wait for server to start
1369+waitforserver
1370+
1371 # create a repo
1372 soft repo create repo1
1373
1374@@ -43,6 +48,9 @@ stderr 'revision does not exist'
1375 ! stdout .
1376 stderr 'revision does not exist'
1377
1378+# stop the server
1379+[windows] stopserver
1380+
1381 -- blob1.txt --
1382 # Hello\n\nwelcome
1383 -- blob2.txt --
1384diff --git a/testscript/testdata/repo-collab.txtar b/testscript/testdata/repo-collab.txtar
1385index 8f7d501b1922831d34cf3cbf72b44cb47507262c..d692831bbe59933d6069331182407e069308bc99 100644
1386--- a/testscript/testdata/repo-collab.txtar
1387+++ b/testscript/testdata/repo-collab.txtar
1388@@ -1,4 +1,10 @@
1389 # vi: set ft=conf
1390+
1391+# start soft serve
1392+exec soft serve &
1393+# wait for server to start
1394+waitforserver
1395+
1396 # setup
1397 soft repo import test https://github.com/charmbracelet/catwalk.git
1398 soft user create foo --key "$USER1_AUTHORIZED_KEY"
1399@@ -16,3 +22,7 @@ stdout 'foo'
1400 soft repo collab remove test foo
1401 soft repo collab list test
1402 ! stdout .
1403+
1404+# stop the server
1405+[windows] stopserver
1406+[windows] ! stderr .
1407diff --git a/testscript/testdata/repo-commit.txtar b/testscript/testdata/repo-commit.txtar
1408index dbf5c1bf80f5db690a1c30d4157834ca7a1e2316..70ca3da125876d954a739e262542166a37f11754 100644
1409--- a/testscript/testdata/repo-commit.txtar
1410+++ b/testscript/testdata/repo-commit.txtar
1411@@ -3,6 +3,11 @@
1412 # convert crlf to lf on windows
1413 [windows] dos2unix commit1.txt
1414
1415+# start soft serve
1416+exec soft serve &
1417+# wait for server to start
1418+waitforserver
1419+
1420 # create a repo
1421 soft repo import basic1 https://github.com/git-fixtures/basic
1422
1423@@ -10,6 +15,10 @@ soft repo import basic1 https://github.com/git-fixtures/basic
1424 soft repo commit basic1 b8e471f58bcbca63b07bda20e428190409c2db47
1425 cmp stdout commit1.txt
1426
1427+# stop the server
1428+[windows] stopserver
1429+[windows] ! stderr .
1430+
1431 -- commit1.txt --
1432 commit b8e471f58bcbca63b07bda20e428190409c2db47
1433 Author: Daniel Ripolles
1434diff --git a/testscript/testdata/repo-create.txtar b/testscript/testdata/repo-create.txtar
1435index 794294a16e195b7509babc6bc2e26ed30980f3c5..7b7f12d77f17eae7c66b02704e79a16d19638b17 100644
1436--- a/testscript/testdata/repo-create.txtar
1437+++ b/testscript/testdata/repo-create.txtar
1438@@ -3,6 +3,11 @@
1439 # convert crlf to lf on windows
1440 [windows] dos2unix readme.md branch_list.1.txt info.txt
1441
1442+# start soft serve
1443+exec soft serve &
1444+# wait for server to start
1445+waitforserver
1446+
1447 # create a repo
1448 soft repo create repo1 -d 'description' -H -p -n 'repo11'
1449 stderr 'Created repository repo1.*'
1450@@ -114,6 +119,10 @@ stdout 'repo2'
1451 usoft repo delete repo2
1452 ! exists $DATA_PATH/repos/repo2.git
1453
1454+# stop the server
1455+[windows] stopserver
1456+[windows] ! stderr .
1457+
1458
1459 -- readme.md --
1460 # Project\nfoo
1461diff --git a/testscript/testdata/repo-delete.txtar b/testscript/testdata/repo-delete.txtar
1462index 2941dd48c252029887d21f7c81570b2968d6f1c8..403c96043cd4ec46e448b17954ad346e7cceeca4 100644
1463--- a/testscript/testdata/repo-delete.txtar
1464+++ b/testscript/testdata/repo-delete.txtar
1465@@ -1,5 +1,10 @@
1466 # vi: set ft=conf
1467
1468+# start soft serve
1469+exec soft serve &
1470+# wait for server to start
1471+waitforserver
1472+
1473 soft repo create repo1
1474 soft repo create repo-to-delete
1475 soft repo delete repo-to-delete
1476@@ -7,3 +12,7 @@ soft repo delete repo-to-delete
1477 stderr '.*not found.*'
1478 soft repo list
1479 stdout 'repo1'
1480+
1481+# stop the server
1482+[windows] stopserver
1483+[windows] ! stderr .
1484diff --git a/testscript/testdata/repo-import.txtar b/testscript/testdata/repo-import.txtar
1485index d85cdf3790187d2144f21087b39db2e07e9453dc..84d4334b4505990626c3f2d19cae19062b6963ae 100644
1486--- a/testscript/testdata/repo-import.txtar
1487+++ b/testscript/testdata/repo-import.txtar
1488@@ -3,6 +3,11 @@
1489 # convert crlf to lf on windows
1490 [windows] dos2unix repo3.txt
1491
1492+# start soft serve
1493+exec soft serve &
1494+# wait for server to start
1495+waitforserver
1496+
1497 # import private
1498 soft repo import --private repo1 https://github.com/charmbracelet/catwalk.git
1499 soft repo private repo1
1500@@ -18,6 +23,10 @@ soft repo import --name 'repo33' --description 'descriptive' repo3 https://githu
1501 soft repo info repo3
1502 cmp stdout repo3.txt
1503
1504+# stop the server
1505+[windows] stopserver
1506+[windows] ! stderr .
1507+
1508 -- repo3.txt --
1509 Project Name: repo33
1510 Repository: repo3
1511diff --git a/testscript/testdata/repo-perms.txtar b/testscript/testdata/repo-perms.txtar
1512index 66341b9fecea85d043d3722bcc11aa89c2189fca..1cc371e93c404a92d6c21d31ed5f8cbd275b1d2a 100644
1513--- a/testscript/testdata/repo-perms.txtar
1514+++ b/testscript/testdata/repo-perms.txtar
1515@@ -3,6 +3,11 @@
1516 # convert crlf to lf on windows
1517 [windows] dos2unix info.txt
1518
1519+# start soft serve
1520+exec soft serve &
1521+# wait for server to start
1522+waitforserver
1523+
1524 # create a repo & user1 with admin
1525 soft repo create repo1 -p
1526 soft user create user1 -k "$USER1_AUTHORIZED_KEY"
1527@@ -77,6 +82,10 @@ usoft repo delete repo1
1528 usoft repo list
1529 ! stdout .
1530
1531+# stop the server
1532+[windows] stopserver
1533+[windows] ! stderr .
1534+
1535 -- info.txt --
1536 Project Name: proj
1537 Repository: repo1
1538diff --git a/testscript/testdata/repo-push.txtar b/testscript/testdata/repo-push.txtar
1539new file mode 100644
1540index 0000000000000000000000000000000000000000..cadb735b95349f71161515f034d420a4cb8f2100
1541--- /dev/null
1542+++ b/testscript/testdata/repo-push.txtar
1543@@ -0,0 +1,18 @@
1544+# vi: set ft=conf
1545+
1546+# start soft serve
1547+exec soft serve &
1548+# wait for server to start
1549+waitforserver
1550+
1551+# create a repo
1552+soft repo create repo-empty -d 'description' -H -p -n 'repo-empty'
1553+
1554+# clone repo
1555+git clone ssh://localhost:$SSH_PORT/repo-empty repo-empty
1556+
1557+# push repo
1558+! git -C repo-empty push origin HEAD
1559+
1560+# stop the server
1561+[windows] stopserver