diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ee3b67d6..8ada16131 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,5 +42,5 @@ jobs: - name: Test run: go test ./... env: - DB_DRIVER: postgres - DB_DATA_SOURCE: postgres://postgres:postgres@localhost/postgres?sslmode=disable + SOFT_SERVE_DB_DRIVER: postgres + SOFT_SERVE_DB_DATA_SOURCE: postgres://postgres:postgres@localhost/postgres?sslmode=disable diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 50f4aa948..4b1b12a83 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -8,7 +8,10 @@ on: jobs: coverage: - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest] # TODO: add macos & windows + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -18,7 +21,24 @@ jobs: go-version: ^1 - name: Test - run: go test -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt ./... -timeout 5m + run: | + # We collect coverage data from two sources, + # 1) unit tests 2) integration tests + # + # https://go.dev/testing/coverage/ + # https://dustinspecker.com/posts/go-combined-unit-integration-code-coverage/ + # https://github.com/golang/go/issues/51430#issuecomment-1344711300 + mkdir -p coverage/unit + mkdir -p coverage/int + + # Collect unit tests coverage + go test -failfast -race -timeout 5m -skip=^TestScript -cover ./... -args -test.gocoverdir=$PWD/coverage/unit + + # Collect integration tests coverage + GOCOVERDIR=$PWD/coverage/int go test -failfast -race -timeout 5m -run=^TestScript ./... + + # Convert coverage data to legacy textfmt format to upload + go tool covdata textfmt -i=coverage/unit,coverage/int -o=coverage.txt - uses: codecov/codecov-action@v3 with: file: ./coverage.txt diff --git a/cmd/cmd.go b/cmd/cmd.go index 9aead6369..556594e15 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -33,7 +33,7 @@ func InitBackendContext(cmd *cobra.Command, _ []string) error { ctx = db.WithContext(ctx, dbx) dbstore := database.New(ctx, dbx) ctx = store.WithContext(ctx, dbstore) - be := backend.New(ctx, cfg, dbx) + be := backend.New(ctx, cfg, dbx, dbstore) ctx = backend.WithContext(ctx, be) cmd.SetContext(ctx) diff --git a/cmd/soft/serve/serve.go b/cmd/soft/serve/serve.go index 7f81dcf0f..d23ce65de 100644 --- a/cmd/soft/serve/serve.go +++ b/cmd/soft/serve/serve.go @@ -3,9 +3,12 @@ package serve import ( "context" "fmt" + "net/http" "os" "os/signal" "path/filepath" + "strconv" + "sync" "syscall" "time" @@ -80,10 +83,26 @@ var ( } done := make(chan os.Signal, 1) + doneOnce := sync.OnceFunc(func() { close(done) }) + lch := make(chan error, 1) + + // This endpoint is added for testing purposes + // It allows us to stop the server from the test suite. + // This is needed since Windows doesn't support signals. + if testRun, _ := strconv.ParseBool(os.Getenv("SOFT_SERVE_TESTRUN")); testRun { + h := s.HTTPServer.Server.Handler + s.HTTPServer.Server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/__stop" && r.Method == http.MethodHead { + doneOnce() + return + } + h.ServeHTTP(w, r) + }) + } + go func() { - defer close(lch) - defer close(done) + defer doneOnce() lch <- s.Start() }() diff --git a/cmd/soft/serve/server.go b/cmd/soft/serve/server.go index 223e55a69..33679091f 100644 --- a/cmd/soft/serve/server.go +++ b/cmd/soft/serve/server.go @@ -147,7 +147,7 @@ func (s *Server) Shutdown(ctx context.Context) error { for _, j := range jobs.List() { s.Cron.Remove(j.ID) } - s.Cron.Shutdown() + s.Cron.Stop() return nil }) // defer s.DB.Close() // nolint: errcheck diff --git a/go.mod b/go.mod index 7cd4a0cc4..3d1c87b75 100644 --- a/go.mod +++ b/go.mod @@ -41,8 +41,7 @@ require ( github.com/muesli/roff v0.1.0 github.com/prometheus/client_golang v1.17.0 github.com/robfig/cron/v3 v3.0.1 - github.com/rogpeppe/go-internal v1.11.0 - github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086 + github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c github.com/spf13/cobra v1.8.0 go.uber.org/automaxprocs v1.5.3 golang.org/x/crypto v0.16.0 diff --git a/go.sum b/go.sum index 7898d622c..b0d958006 100644 --- a/go.sum +++ b/go.sum @@ -167,10 +167,8 @@ github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086 h1:mncRSDOqYCng7jOD+Y6+IivdRI6Kzv2BLWYkWkdQfu0= -github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086/go.mod h1:YpdgDXpumPB/+EGmGTYHeiW/0QVFRzBYTNFaxWfPDk4= +github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c h1:fPpdjePK1atuOg28PXfNSqgwf9I/qD1Hlo39JFwKBXk= +github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= diff --git a/pkg/access/context_test.go b/pkg/access/context_test.go new file mode 100644 index 000000000..c4bcf4a60 --- /dev/null +++ b/pkg/access/context_test.go @@ -0,0 +1,20 @@ +package access + +import ( + "context" + "testing" +) + +func TestGoodFromContext(t *testing.T) { + ctx := WithContext(context.TODO(), AdminAccess) + if ac := FromContext(ctx); ac != AdminAccess { + t.Errorf("FromContext(ctx) => %d, want %d", ac, AdminAccess) + } +} + +func TestBadFromContext(t *testing.T) { + ctx := context.TODO() + if ac := FromContext(ctx); ac != -1 { + t.Errorf("FromContext(ctx) => %d, want %d", ac, -1) + } +} diff --git a/pkg/backend/backend.go b/pkg/backend/backend.go index 2d9eb2a05..ba8796b54 100644 --- a/pkg/backend/backend.go +++ b/pkg/backend/backend.go @@ -23,14 +23,13 @@ type Backend struct { } // New returns a new Soft Serve backend. -func New(ctx context.Context, cfg *config.Config, db *db.DB) *Backend { - dbstore := store.FromContext(ctx) +func New(ctx context.Context, cfg *config.Config, db *db.DB, st store.Store) *Backend { logger := log.FromContext(ctx).WithPrefix("backend") b := &Backend{ ctx: ctx, cfg: cfg, db: db, - store: dbstore, + store: st, logger: logger, manager: task.NewManager(ctx), } diff --git a/pkg/config/context_test.go b/pkg/config/context_test.go new file mode 100644 index 000000000..db7f8b257 --- /dev/null +++ b/pkg/config/context_test.go @@ -0,0 +1,29 @@ +package config + +import ( + "context" + "reflect" + "testing" +) + +func TestBadFromContext(t *testing.T) { + ctx := context.TODO() + if c := FromContext(ctx); c != nil { + t.Errorf("FromContext(ctx) => %v, want %v", c, nil) + } +} + +func TestGoodFromContext(t *testing.T) { + ctx := WithContext(context.TODO(), &Config{}) + if c := FromContext(ctx); c == nil { + t.Errorf("FromContext(ctx) => %v, want %v", c, &Config{}) + } +} + +func TestGoodFromContextWithDefaultConfig(t *testing.T) { + cfg := DefaultConfig() + ctx := WithContext(context.TODO(), cfg) + if c := FromContext(ctx); c == nil || !reflect.DeepEqual(c, cfg) { + t.Errorf("FromContext(ctx) => %v, want %v", c, cfg) + } +} diff --git a/pkg/config/file_test.go b/pkg/config/file_test.go new file mode 100644 index 000000000..81efad71c --- /dev/null +++ b/pkg/config/file_test.go @@ -0,0 +1,15 @@ +package config + +import "testing" + +func TestNewConfigFile(t *testing.T) { + for _, cfg := range []*Config{ + nil, + DefaultConfig(), + &Config{}, + } { + if s := newConfigFile(cfg); s == "" { + t.Errorf("newConfigFile(nil) => %q, want non-empty string", s) + } + } +} diff --git a/pkg/config/ssh.go b/pkg/config/ssh.go index 102b39141..151ac7877 100644 --- a/pkg/config/ssh.go +++ b/pkg/config/ssh.go @@ -1,8 +1,28 @@ package config -import "github.com/charmbracelet/keygen" +import ( + "errors" + + "github.com/charmbracelet/keygen" +) + +var ( + // ErrNilConfig is returned when a nil config is passed to a function. + ErrNilConfig = errors.New("nil config") + + // ErrEmptySSHKeyPath is returned when the SSH key path is empty. + ErrEmptySSHKeyPath = errors.New("empty SSH key path") +) // KeyPair returns the server's SSH key pair. -func (c SSHConfig) KeyPair() (*keygen.SSHKeyPair, error) { - return keygen.New(c.KeyPath, keygen.WithKeyType(keygen.Ed25519)) +func KeyPair(cfg *Config) (*keygen.SSHKeyPair, error) { + if cfg == nil { + return nil, ErrNilConfig + } + + if cfg.SSH.KeyPath == "" { + return nil, ErrEmptySSHKeyPath + } + + return keygen.New(cfg.SSH.KeyPath, keygen.WithKeyType(keygen.Ed25519)) } diff --git a/pkg/config/ssh_test.go b/pkg/config/ssh_test.go new file mode 100644 index 000000000..4f68ec149 --- /dev/null +++ b/pkg/config/ssh_test.go @@ -0,0 +1,26 @@ +package config + +import "testing" + +func TestBadSSHKeyPair(t *testing.T) { + for _, cfg := range []*Config{ + nil, + {}, + } { + if _, err := KeyPair(cfg); err == nil { + t.Errorf("cfg.SSH.KeyPair() => _, nil, want non-nil error") + } + } +} + +func TestGoodSSHKeyPair(t *testing.T) { + cfg := &Config{ + SSH: SSHConfig{ + KeyPath: "testdata/ssh_host_ed25519_key", + }, + } + + if _, err := KeyPair(cfg); err != nil { + t.Errorf("cfg.SSH.KeyPair() => _, %v, want nil error", err) + } +} diff --git a/pkg/cron/cron_test.go b/pkg/cron/cron_test.go new file mode 100644 index 000000000..c254191b2 --- /dev/null +++ b/pkg/cron/cron_test.go @@ -0,0 +1,31 @@ +package cron + +import ( + "bytes" + "context" + "fmt" + "testing" + + "github.com/charmbracelet/log" +) + +func TestCronLogger(t *testing.T) { + var buf bytes.Buffer + logger := log.New(&buf) + logger.SetLevel(log.DebugLevel) + clogger := cronLogger{logger} + clogger.Info("foo") + clogger.Error(fmt.Errorf("bar"), "test") + if buf.String() != "DEBU foo\nERRO test err=bar\n" { + t.Errorf("unexpected log output: %s", buf.String()) + } +} + +func TestSchedularAddRemove(t *testing.T) { + s := NewScheduler(context.TODO()) + id, err := s.AddFunc("* * * * *", func() {}) + if err != nil { + t.Fatal(err) + } + s.Remove(id) +} diff --git a/pkg/daemon/daemon.go b/pkg/daemon/daemon.go index eee10ec52..5d91565cd 100644 --- a/pkg/daemon/daemon.go +++ b/pkg/daemon/daemon.go @@ -150,30 +150,35 @@ func (d *GitDaemon) handleClient(conn net.Conn) { d.conns.Close(c) // nolint: errcheck }() - readc := make(chan struct{}, 1) + errc := make(chan error, 1) + s := pktline.NewScanner(c) go func() { if !s.Scan() { if err := s.Err(); err != nil { - if nerr, ok := err.(net.Error); ok && nerr.Timeout() { - d.fatal(c, git.ErrTimeout) - } else { - d.logger.Debugf("git: error scanning pktline: %v", err) - d.fatal(c, git.ErrSystemMalfunction) - } + errc <- err } - return } - readc <- struct{}{} + errc <- nil }() select { case <-ctx.Done(): if err := ctx.Err(); err != nil { d.logger.Debugf("git: connection context error: %v", err) + d.fatal(c, git.ErrTimeout) } return - case <-readc: + case err := <-errc: + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + d.fatal(c, git.ErrTimeout) + return + } else if err != nil { + d.logger.Debugf("git: error scanning pktline: %v", err) + d.fatal(c, git.ErrSystemMalfunction) + return + } + line := s.Bytes() split := bytes.SplitN(line, []byte{' '}, 2) if len(split) != 2 { diff --git a/pkg/daemon/daemon_test.go b/pkg/daemon/daemon_test.go index 7ebe31b53..88b4fdec7 100644 --- a/pkg/daemon/daemon_test.go +++ b/pkg/daemon/daemon_test.go @@ -8,6 +8,7 @@ import ( "os" "strings" "testing" + "time" "github.com/charmbracelet/soft-serve/pkg/backend" "github.com/charmbracelet/soft-serve/pkg/config" @@ -50,7 +51,7 @@ func TestMain(m *testing.M) { } datastore := database.New(ctx, dbx) ctx = store.WithContext(ctx, datastore) - be := backend.New(ctx, cfg, dbx) + be := backend.New(ctx, cfg, dbx, datastore) ctx = backend.WithContext(ctx, be) d, err := NewGitDaemon(ctx) if err != nil { @@ -78,9 +79,10 @@ func TestIdleTimeout(t *testing.T) { if err != nil { t.Fatal(err) } + time.Sleep(time.Second) _, err = readPktline(c) - if err != nil && err.Error() != git.ErrTimeout.Error() { - t.Fatalf("expected %q error, got %q", git.ErrTimeout, err) + if err == nil { + t.Errorf("expected error, got nil") } } @@ -94,7 +96,7 @@ func TestInvalidRepo(t *testing.T) { } _, err = readPktline(c) if err != nil && err.Error() != git.ErrInvalidRepo.Error() { - t.Fatalf("expected %q error, got %q", git.ErrInvalidRepo, err) + t.Errorf("expected %q error, got %q", git.ErrInvalidRepo, err) } } diff --git a/pkg/db/context_test.go b/pkg/db/context_test.go new file mode 100644 index 000000000..5da523975 --- /dev/null +++ b/pkg/db/context_test.go @@ -0,0 +1,28 @@ +package db_test + +import ( + "context" + "testing" + + "github.com/charmbracelet/soft-serve/pkg/db" + "github.com/charmbracelet/soft-serve/pkg/db/internal/test" +) + +func TestBadFromContext(t *testing.T) { + ctx := context.TODO() + if c := db.FromContext(ctx); c != nil { + t.Errorf("FromContext(ctx) => %v, want %v", c, nil) + } +} + +func TestGoodFromContext(t *testing.T) { + ctx := context.TODO() + dbx, err := test.OpenSqlite(ctx, t) + if err != nil { + t.Fatal(err) + } + ctx = db.WithContext(ctx, dbx) + if c := db.FromContext(ctx); c == nil { + t.Errorf("FromContext(ctx) => %v, want %v", c, dbx) + } +} diff --git a/pkg/db/db_test.go b/pkg/db/db_test.go new file mode 100644 index 000000000..3ca95ad37 --- /dev/null +++ b/pkg/db/db_test.go @@ -0,0 +1,17 @@ +package db + +import ( + "context" + "strings" + "testing" +) + +func TestOpenUnknownDriver(t *testing.T) { + _, err := Open(context.TODO(), "invalid", "") + if err == nil { + t.Error("Open(invalid) => nil, want error") + } + if !strings.Contains(err.Error(), "unknown driver") { + t.Errorf("Open(invalid) => %v, want error containing 'unknown driver'", err) + } +} diff --git a/pkg/db/errors_test.go b/pkg/db/errors_test.go new file mode 100644 index 000000000..0aba63457 --- /dev/null +++ b/pkg/db/errors_test.go @@ -0,0 +1,25 @@ +package db + +import ( + "database/sql" + "errors" + "fmt" + "testing" +) + +func TestWrapErrorBadNoRows(t *testing.T) { + for _, e := range []error{ + fmt.Errorf("foo"), + errors.New("bar"), + } { + if err := WrapError(e); err != e { + t.Errorf("WrapError(%v) => %v, want %v", e, err, e) + } + } +} + +func TestWrapErrorGoodNoRows(t *testing.T) { + if err := WrapError(sql.ErrNoRows); err != ErrRecordNotFound { + t.Errorf("WrapError(sql.ErrNoRows) => %v, want %v", err, ErrRecordNotFound) + } +} diff --git a/pkg/db/internal/test/test.go b/pkg/db/internal/test/test.go new file mode 100644 index 000000000..b40e2ad90 --- /dev/null +++ b/pkg/db/internal/test/test.go @@ -0,0 +1,29 @@ +package test + +import ( + "context" + "path/filepath" + "testing" + + "github.com/charmbracelet/soft-serve/pkg/db" +) + +// OpenSqlite opens a new temp SQLite database for testing. +// It removes the database file when the test is done using tb.Cleanup. +// If ctx is nil, context.TODO() is used. +func OpenSqlite(ctx context.Context, tb testing.TB) (*db.DB, error) { + if ctx == nil { + ctx = context.TODO() + } + dbpath := filepath.Join(tb.TempDir(), "test.db") + dbx, err := db.Open(ctx, "sqlite", dbpath) + if err != nil { + return nil, err + } + tb.Cleanup(func() { + if err := dbx.Close(); err != nil { + tb.Error(err) + } + }) + return dbx, nil +} diff --git a/pkg/db/migrate/migrate_test.go b/pkg/db/migrate/migrate_test.go new file mode 100644 index 000000000..bfc9d20f2 --- /dev/null +++ b/pkg/db/migrate/migrate_test.go @@ -0,0 +1,22 @@ +package migrate + +import ( + "context" + "testing" + + "github.com/charmbracelet/soft-serve/pkg/config" + "github.com/charmbracelet/soft-serve/pkg/db/internal/test" +) + +func TestMigrate(t *testing.T) { + // XXX: we need a config.Config in the context for the migrations to run + // properly. Some migrations depend on the config being present. + ctx := config.WithContext(context.TODO(), config.DefaultConfig()) + dbx, err := test.OpenSqlite(ctx, t) + if err != nil { + t.Fatal(err) + } + if err := Migrate(ctx, dbx); err != nil { + t.Errorf("Migrate() => %v, want nil error", err) + } +} diff --git a/pkg/git/git.go b/pkg/git/git.go index e523b90a2..d6c014296 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -2,6 +2,7 @@ package git import ( "context" + "errors" "fmt" "io" "path/filepath" @@ -13,6 +14,11 @@ import ( "github.com/go-git/go-git/v5/plumbing/format/pktline" ) +var ( + // ErrNoBranches is returned when a repo has no branches. + ErrNoBranches = errors.New("no branches found") +) + // WritePktline encodes and writes a pktline to the given writer. func WritePktline(w io.Writer, v ...interface{}) error { msg := fmt.Sprintln(v...) @@ -57,18 +63,18 @@ func EnsureWithin(reposDir string, repo string) error { // EnsureDefaultBranch ensures the repo has a default branch. // It will prefer choosing "main" or "master" if available. -func EnsureDefaultBranch(ctx context.Context, scmd ServiceCommand) error { - r, err := git.Open(scmd.Dir) +func EnsureDefaultBranch(ctx context.Context, repoPath string) error { + r, err := git.Open(repoPath) if err != nil { return err } brs, err := r.Branches() + if len(brs) == 0 { + return ErrNoBranches + } if err != nil { return err } - if len(brs) == 0 { - return fmt.Errorf("no branches found") - } // Rename the default branch to the first branch available _, err = r.HEAD() if err == git.ErrReferenceNotExist { diff --git a/pkg/git/git_test.go b/pkg/git/git_test.go index d95cb6497..4e4a476d2 100644 --- a/pkg/git/git_test.go +++ b/pkg/git/git_test.go @@ -2,8 +2,12 @@ package git import ( "bytes" + "context" + "errors" "fmt" "testing" + + "github.com/charmbracelet/soft-serve/git" ) func TestPktline(t *testing.T) { @@ -54,3 +58,40 @@ func TestPktline(t *testing.T) { }) } } + +func TestEnsureWithinBad(t *testing.T) { + tmp := t.TempDir() + for _, f := range []string{ + "..", + "../../../", + } { + if err := EnsureWithin(tmp, f); err == nil { + t.Errorf("EnsureWithin(%q, %q) => nil, want non-nil error", tmp, f) + } + } +} + +func TestEnsureWithinGood(t *testing.T) { + tmp := t.TempDir() + for _, f := range []string{ + tmp, + tmp + "/foo", + tmp + "/foo/bar", + } { + if err := EnsureWithin(tmp, f); err != nil { + t.Errorf("EnsureWithin(%q, %q) => %v, want nil error", tmp, f, err) + } + } +} + +func TestEnsureDefaultBranchEmpty(t *testing.T) { + tmp := t.TempDir() + r, err := git.Init(tmp, false) + if err != nil { + t.Fatal(err) + } + + if err := EnsureDefaultBranch(context.TODO(), r.Path); !errors.Is(err, ErrNoBranches) { + t.Errorf("EnsureDefaultBranch(%q) => %v, want ErrNoBranches", tmp, err) + } +} diff --git a/pkg/git/lfs.go b/pkg/git/lfs.go index 5aae027e5..7dc4b8b35 100644 --- a/pkg/git/lfs.go +++ b/pkg/git/lfs.go @@ -21,17 +21,8 @@ import ( "github.com/charmbracelet/soft-serve/pkg/proto" "github.com/charmbracelet/soft-serve/pkg/storage" "github.com/charmbracelet/soft-serve/pkg/store" - "github.com/rubyist/tracerx" ) -func init() { - // git-lfs-transfer uses tracerx for logging. - // use a custom key to avoid conflicts - // SOFT_SERVE_TRACE=1 to enable tracing git-lfs-transfer in soft-serve - tracerx.DefaultKey = "SOFT_SERVE" - tracerx.Prefix = "trace soft-serve-lfs-transfer: " -} - // lfsTransfer implements transfer.Backend. type lfsTransfer struct { ctx context.Context diff --git a/pkg/hooks/gen.go b/pkg/hooks/gen.go index 5eb16ebc2..467b2f263 100644 --- a/pkg/hooks/gen.go +++ b/pkg/hooks/gen.go @@ -3,7 +3,6 @@ package hooks import ( "bytes" "context" - "flag" "os" "path/filepath" "text/template" @@ -30,11 +29,6 @@ const ( // This function should be called by the backend when a repository is created. // TODO: support context. func GenerateHooks(_ context.Context, cfg *config.Config, repo string) error { - // TODO: support git hook tests. - if flag.Lookup("test.v") != nil { - log.WithPrefix("backend.hooks").Warn("refusing to set up hooks when in test") - return nil - } repo = utils.SanitizeRepo(repo) + ".git" hooksPath := filepath.Join(cfg.DataPath, "repos", repo, "hooks") if err := os.MkdirAll(hooksPath, os.ModePerm); err != nil { diff --git a/pkg/hooks/gen_test.go b/pkg/hooks/gen_test.go new file mode 100644 index 000000000..d9f03ee39 --- /dev/null +++ b/pkg/hooks/gen_test.go @@ -0,0 +1,40 @@ +package hooks + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/charmbracelet/soft-serve/git" + "github.com/charmbracelet/soft-serve/pkg/config" +) + +func TestGenerateHooks(t *testing.T) { + tmp := t.TempDir() + cfg := config.DefaultConfig() + cfg.DataPath = tmp + repoPath := filepath.Join(tmp, "repos", "test.git") + _, err := git.Init(repoPath, true) + if err != nil { + t.Fatal(err) + } + + if err := GenerateHooks(context.TODO(), cfg, "test.git"); err != nil { + t.Fatal(err) + } + + for _, hn := range []string{ + PreReceiveHook, + UpdateHook, + PostReceiveHook, + PostUpdateHook, + } { + if _, err := os.Stat(filepath.Join(repoPath, "hooks", hn)); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(repoPath, "hooks", hn+".d", "soft-serve")); err != nil { + t.Fatal(err) + } + } +} diff --git a/pkg/jwk/jwk.go b/pkg/jwk/jwk.go index b758f88aa..f7fe92404 100644 --- a/pkg/jwk/jwk.go +++ b/pkg/jwk/jwk.go @@ -32,7 +32,7 @@ func (p Pair) JWK() jose.JSONWebKey { // NewPair creates a new JSON Web Key pair. func NewPair(cfg *config.Config) (Pair, error) { - kp, err := cfg.SSH.KeyPair() + kp, err := config.KeyPair(cfg) if err != nil { return Pair{}, err } diff --git a/pkg/jwk/jwk_test.go b/pkg/jwk/jwk_test.go new file mode 100644 index 000000000..a8d95fbdb --- /dev/null +++ b/pkg/jwk/jwk_test.go @@ -0,0 +1,22 @@ +package jwk + +import ( + "errors" + "testing" + + "github.com/charmbracelet/soft-serve/pkg/config" +) + +func TestBadNewPair(t *testing.T) { + _, err := NewPair(nil) + if !errors.Is(err, config.ErrNilConfig) { + t.Errorf("NewPair(nil) => %v, want %v", err, config.ErrNilConfig) + } +} + +func TestGoodNewPair(t *testing.T) { + cfg := config.DefaultConfig() + if _, err := NewPair(cfg); err != nil { + t.Errorf("NewPair(cfg) => _, %v, want nil error", err) + } +} diff --git a/pkg/log/log.go b/pkg/log/log.go index aa9a6b418..b9134ead7 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -11,6 +11,9 @@ import ( // NewLogger returns a new logger with default settings. func NewLogger(cfg *config.Config) (*log.Logger, *os.File, error) { + if cfg == nil { + return nil, nil, config.ErrNilConfig + } logger := log.NewWithOptions(os.Stderr, log.Options{ ReportTimestamp: true, TimeFormat: time.DateOnly, @@ -37,7 +40,8 @@ func NewLogger(cfg *config.Config) (*log.Logger, *os.File, error) { var f *os.File if cfg.Log.Path != "" { - f, err := os.OpenFile(cfg.Log.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + var err error + f, err = os.OpenFile(cfg.Log.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return nil, nil, err } diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go new file mode 100644 index 000000000..131e05809 --- /dev/null +++ b/pkg/log/log_test.go @@ -0,0 +1,43 @@ +package log + +import ( + "path/filepath" + "testing" + + "github.com/charmbracelet/soft-serve/pkg/config" +) + +func TestGoodNewLogger(t *testing.T) { + for _, c := range []*config.Config{ + config.DefaultConfig(), + {}, + {Log: config.LogConfig{Path: filepath.Join(t.TempDir(), "logfile.txt")}}, + } { + _, f, err := NewLogger(c) + if err != nil { + t.Errorf("expected nil got %v", err) + } + if f != nil { + if err := f.Close(); err != nil { + t.Errorf("failed to close logger: %v", err) + } + } + } +} + +func TestBadNewLogger(t *testing.T) { + for _, c := range []*config.Config{ + nil, + {Log: config.LogConfig{Path: "\x00"}}, + } { + _, f, err := NewLogger(c) + if err == nil { + t.Errorf("expected error got nil") + } + if f != nil { + if err := f.Close(); err != nil { + t.Errorf("failed to close logger: %v", err) + } + } + } +} diff --git a/pkg/ssh/cmd/git.go b/pkg/ssh/cmd/git.go index 355d235d4..f6aea60ab 100644 --- a/pkg/ssh/cmd/git.go +++ b/pkg/ssh/cmd/git.go @@ -249,7 +249,7 @@ func gitRunE(cmd *cobra.Command, args []string) error { return git.ErrSystemMalfunction } - if err := git.EnsureDefaultBranch(ctx, scmd); err != nil { + if err := git.EnsureDefaultBranch(ctx, scmd.Dir); err != nil { logger.Error("failed to ensure default branch", "err", err, "repo", name) return git.ErrSystemMalfunction } diff --git a/pkg/ssh/session_test.go b/pkg/ssh/session_test.go index 845e17865..792759c16 100644 --- a/pkg/ssh/session_test.go +++ b/pkg/ssh/session_test.go @@ -76,7 +76,7 @@ func setup(tb testing.TB) (*gossh.Session, func() error) { } dbstore := database.New(ctx, dbx) ctx = store.WithContext(ctx, dbstore) - be := backend.New(ctx, cfg, dbx) + be := backend.New(ctx, cfg, dbx, dbstore) ctx = backend.WithContext(ctx, be) return testsession.New(tb, &ssh.Server{ Handler: ContextMiddleware(cfg, dbx, dbstore, be, log.Default())(bm.MiddlewareWithProgramHandler(SessionHandler, termenv.ANSI256)(func(s ssh.Session) { diff --git a/pkg/web/auth.go b/pkg/web/auth.go index 631660bd3..96b2bab85 100644 --- a/pkg/web/auth.go +++ b/pkg/web/auth.go @@ -136,7 +136,7 @@ var ErrInvalidToken = errors.New("invalid token") func parseJWT(ctx context.Context, bearer string) (*jwt.RegisteredClaims, error) { cfg := config.FromContext(ctx) logger := log.FromContext(ctx).WithPrefix("http.auth") - kp, err := cfg.SSH.KeyPair() + kp, err := config.KeyPair(cfg) if err != nil { return nil, err } diff --git a/pkg/web/git.go b/pkg/web/git.go index c03e56afd..8f1170bce 100644 --- a/pkg/web/git.go +++ b/pkg/web/git.go @@ -441,7 +441,7 @@ func serviceRpc(w http.ResponseWriter, r *http.Request) { } if service == git.ReceivePackService { - if err := git.EnsureDefaultBranch(ctx, cmd); err != nil { + if err := git.EnsureDefaultBranch(ctx, cmd.Dir); err != nil { logger.Errorf("failed to ensure default branch: %s", err) } } diff --git a/pkg/web/http.go b/pkg/web/http.go index 9e109a0e2..20fae6bd1 100644 --- a/pkg/web/http.go +++ b/pkg/web/http.go @@ -11,9 +11,10 @@ import ( // HTTPServer is an http server. type HTTPServer struct { - ctx context.Context - cfg *config.Config - server *http.Server + ctx context.Context + cfg *config.Config + + Server *http.Server } // NewHTTPServer creates a new HTTP server. @@ -23,7 +24,7 @@ func NewHTTPServer(ctx context.Context) (*HTTPServer, error) { s := &HTTPServer{ ctx: ctx, cfg: cfg, - server: &http.Server{ + Server: &http.Server{ Addr: cfg.HTTP.ListenAddr, Handler: NewRouter(ctx), ReadHeaderTimeout: time.Second * 10, @@ -38,18 +39,18 @@ func NewHTTPServer(ctx context.Context) (*HTTPServer, error) { // Close closes the HTTP server. func (s *HTTPServer) Close() error { - return s.server.Close() + return s.Server.Close() } // ListenAndServe starts the HTTP server. func (s *HTTPServer) ListenAndServe() error { if s.cfg.HTTP.TLSKeyPath != "" && s.cfg.HTTP.TLSCertPath != "" { - return s.server.ListenAndServeTLS(s.cfg.HTTP.TLSCertPath, s.cfg.HTTP.TLSKeyPath) + return s.Server.ListenAndServeTLS(s.cfg.HTTP.TLSCertPath, s.cfg.HTTP.TLSKeyPath) } - return s.server.ListenAndServe() + return s.Server.ListenAndServe() } // Shutdown gracefully shuts down the HTTP server. func (s *HTTPServer) Shutdown(ctx context.Context) error { - return s.server.Shutdown(ctx) + return s.Server.Shutdown(ctx) } diff --git a/pkg/webhook/content_type_test.go b/pkg/webhook/content_type_test.go new file mode 100644 index 000000000..1aff46e81 --- /dev/null +++ b/pkg/webhook/content_type_test.go @@ -0,0 +1,117 @@ +package webhook + +import "testing" + +func TestParseContentType(t *testing.T) { + tests := []struct { + name string + s string + want ContentType + err error + }{ + { + name: "JSON", + s: "application/json", + want: ContentTypeJSON, + }, + { + name: "Form", + s: "application/x-www-form-urlencoded", + want: ContentTypeForm, + }, + { + name: "Invalid", + s: "application/invalid", + err: ErrInvalidContentType, + want: -1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseContentType(tt.s) + if err != tt.err { + t.Errorf("ParseContentType() error = %v, wantErr %v", err, tt.err) + return + } + if got != tt.want { + t.Errorf("ParseContentType() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUnmarshalText(t *testing.T) { + tests := []struct { + name string + text []byte + want ContentType + wantErr bool + }{ + { + name: "JSON", + text: []byte("application/json"), + want: ContentTypeJSON, + }, + { + name: "Form", + text: []byte("application/x-www-form-urlencoded"), + want: ContentTypeForm, + }, + { + name: "Invalid", + text: []byte("application/invalid"), + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := new(ContentType) + if err := c.UnmarshalText(tt.text); (err != nil) != tt.wantErr { + t.Errorf("ContentType.UnmarshalText() error = %v, wantErr %v", err, tt.wantErr) + } + if *c != tt.want { + t.Errorf("ContentType.UnmarshalText() got = %v, want %v", *c, tt.want) + } + }) + } +} + +func TestMarshalText(t *testing.T) { + tests := []struct { + name string + c ContentType + want []byte + wantErr bool + }{ + { + name: "JSON", + c: ContentTypeJSON, + want: []byte("application/json"), + }, + { + name: "Form", + c: ContentTypeForm, + want: []byte("application/x-www-form-urlencoded"), + }, + { + name: "Invalid", + c: ContentType(-1), + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := tt.c.MarshalText() + if (err != nil) != tt.wantErr { + t.Errorf("ContentType.MarshalText() error = %v, wantErr %v", err, tt.wantErr) + return + } + if string(b) != string(tt.want) { + t.Errorf("ContentType.MarshalText() got = %v, want %v", string(b), string(tt.want)) + } + }) + } +} diff --git a/pkg/webhook/push.go b/pkg/webhook/push.go index 8af2c1398..6ef6062cd 100644 --- a/pkg/webhook/push.go +++ b/pkg/webhook/push.go @@ -2,6 +2,7 @@ package webhook import ( "context" + "errors" "fmt" gitm "github.com/aymanbagabas/git-module" @@ -75,7 +76,11 @@ func NewPushEvent(ctx context.Context, user proto.User, repo proto.Repository, r } payload.Repository.DefaultBranch, err = proto.RepositoryDefaultBranch(repo) - if err != nil { + // XXX: we check for ErrReferenceNotExist here because we don't want to + // return an error if the repo is an empty repo. + // This means that the repo doesn't have a default branch yet and this is + // the first push to it. + if err != nil && !errors.Is(err, git.ErrReferenceNotExist) { return PushEvent{}, err } diff --git a/testscript/script_test.go b/testscript/script_test.go index 94c6408bc..f7dd22eec 100644 --- a/testscript/script_test.go +++ b/testscript/script_test.go @@ -3,41 +3,65 @@ package testscript import ( "bytes" "context" - "database/sql" + "encoding/json" "flag" "fmt" "io" + "math/rand" "net" "net/http" "net/url" "os" + "os/exec" "path/filepath" + "runtime" "strings" - "sync" "testing" "time" "github.com/charmbracelet/keygen" - "github.com/charmbracelet/log" - "github.com/charmbracelet/soft-serve/cmd/soft/serve" - "github.com/charmbracelet/soft-serve/pkg/backend" "github.com/charmbracelet/soft-serve/pkg/config" "github.com/charmbracelet/soft-serve/pkg/db" - "github.com/charmbracelet/soft-serve/pkg/db/migrate" - logr "github.com/charmbracelet/soft-serve/pkg/log" - "github.com/charmbracelet/soft-serve/pkg/store" - "github.com/charmbracelet/soft-serve/pkg/store/database" "github.com/charmbracelet/soft-serve/pkg/test" "github.com/rogpeppe/go-internal/testscript" "github.com/spf13/cobra" "golang.org/x/crypto/ssh" ) -var update = flag.Bool("update", false, "update script files") +var ( + update = flag.Bool("update", false, "update script files") + binPath string +) + +func TestMain(m *testing.M) { + tmp, err := os.MkdirTemp("", "soft-serve*") + if err != nil { + fmt.Fprintf(os.Stderr, "failed to create temporary directory: %s", err) + os.Exit(1) + } + defer os.RemoveAll(tmp) + + binPath = filepath.Join(tmp, "soft") + if runtime.GOOS == "windows" { + binPath += ".exe" + } + + // Build the soft binary with -cover flag. + cmd := exec.Command("go", "build", "-race", "-cover", "-o", binPath, filepath.Join("..", "cmd", "soft")) + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "failed to build soft-serve binary: %s", err) + os.Exit(1) + } + + // Run tests + os.Exit(m.Run()) + + // Add binPath to PATH + os.Setenv("PATH", fmt.Sprintf("%s%c%s", os.Getenv("PATH"), os.PathListSeparator, filepath.Dir(binPath))) +} func TestScript(t *testing.T) { flag.Parse() - var lock sync.Mutex mkkey := func(name string) (string, *keygen.SSHKeyPair) { path := filepath.Join(t.TempDir(), name) @@ -53,21 +77,27 @@ func TestScript(t *testing.T) { _, user1 := mkkey("user1") testscript.Run(t, testscript.Params{ - Dir: "./testdata/", - UpdateScripts: *update, + Dir: "./testdata/", + UpdateScripts: *update, + RequireExplicitExec: true, Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ - "soft": cmdSoft(admin1.Signer()), - "usoft": cmdSoft(user1.Signer()), - "git": cmdGit(key), - "curl": cmdCurl, - "mkfile": cmdMkfile, - "envfile": cmdEnvfile, - "readfile": cmdReadfile, - "dos2unix": cmdDos2Unix, + "soft": cmdSoft(admin1.Signer()), + "usoft": cmdSoft(user1.Signer()), + "git": cmdGit(key), + "curl": cmdCurl, + "mkfile": cmdMkfile, + "envfile": cmdEnvfile, + "readfile": cmdReadfile, + "dos2unix": cmdDos2Unix, + "new-webhook": cmdNewWebhook, + "waitforserver": cmdWaitforserver, + "stopserver": cmdStopserver, }, Setup: func(e *testscript.Env) error { - data := t.TempDir() + // Add binPath to PATH + e.Setenv("PATH", fmt.Sprintf("%s%c%s", filepath.Dir(binPath), os.PathListSeparator, e.Getenv("PATH"))) + data := t.TempDir() sshPort := test.RandomPort() sshListen := fmt.Sprintf("localhost:%d", sshPort) gitPort := test.RandomPort() @@ -87,6 +117,20 @@ func TestScript(t *testing.T) { e.Setenv("SSH_KNOWN_HOSTS_FILE", filepath.Join(t.TempDir(), "known_hosts")) e.Setenv("SSH_KNOWN_CONFIG_FILE", filepath.Join(t.TempDir(), "config")) + // This is used to set up test specific configuration and http endpoints + e.Setenv("SOFT_SERVE_TESTRUN", "1") + + // Soft Serve debug environment variables + for _, env := range []string{ + "SOFT_SERVE_DEBUG", + "SOFT_SERVE_VERBOSE", + } { + if v, ok := os.LookupEnv(env); ok { + e.Setenv(env, v) + } + } + + // TODO: test different configs cfg := config.DefaultConfig() cfg.DataPath = data cfg.Name = serverName @@ -97,20 +141,16 @@ func TestScript(t *testing.T) { cfg.HTTP.ListenAddr = httpListen cfg.HTTP.PublicURL = "http://" + httpListen cfg.Stats.ListenAddr = statsListen - cfg.DB.Driver = "sqlite" cfg.LFS.Enabled = true - cfg.LFS.SSHEnabled = true - - dbDriver := os.Getenv("DB_DRIVER") - if dbDriver != "" { - cfg.DB.Driver = dbDriver - } + // cfg.LFS.SSHEnabled = true - dbDsn := os.Getenv("DB_DATA_SOURCE") - if dbDsn != "" { - cfg.DB.DataSource = dbDsn + // Parse os SOFT_SERVE environment variables + if err := cfg.ParseEnv(); err != nil { + return err } + // Override the database data source if we're using postgres + // so we can create a temporary database for the tests. if cfg.DB.Driver == "postgres" { err, cleanup := setupPostgres(e.T(), cfg) if err != nil { @@ -121,73 +161,12 @@ func TestScript(t *testing.T) { } } - if err := cfg.Validate(); err != nil { - return err - } - - ctx := config.WithContext(context.Background(), cfg) - - logger, f, err := logr.NewLogger(cfg) - if err != nil { - log.Errorf("failed to create logger: %v", err) - } - - ctx = log.WithContext(ctx, logger) - if f != nil { - defer f.Close() // nolint: errcheck - } - - dbx, err := db.Open(ctx, cfg.DB.Driver, cfg.DB.DataSource) - if err != nil { - return fmt.Errorf("open database: %w", err) - } - - if err := migrate.Migrate(ctx, dbx); err != nil { - return fmt.Errorf("migrate database: %w", err) - } - - ctx = db.WithContext(ctx, dbx) - datastore := database.New(ctx, dbx) - ctx = store.WithContext(ctx, datastore) - be := backend.New(ctx, cfg, dbx) - ctx = backend.WithContext(ctx, be) - - lock.Lock() - srv, err := serve.NewServer(ctx) - if err != nil { - lock.Unlock() - return err - } - lock.Unlock() - - go func() { - if err := srv.Start(); err != nil { - e.T().Fatal(err) - } - }() - - e.Defer(func() { - defer dbx.Close() // nolint: errcheck - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - lock.Lock() - defer lock.Unlock() - if err := srv.Shutdown(ctx); err != nil { - e.T().Fatal(err) - } - }) - - // wait until the server is up - for { - conn, _ := net.DialTimeout( - "tcp", - net.JoinHostPort("localhost", fmt.Sprintf("%d", sshPort)), - time.Second, - ) - if conn != nil { - conn.Close() - break + for _, env := range cfg.Environ() { + parts := strings.SplitN(env, "=", 2) + if len(parts) != 2 { + e.T().Fatal("invalid environment variable", env) } + e.Setenv(parts[0], parts[1]) } return nil @@ -318,6 +297,29 @@ func cmdEnvfile(ts *testscript.TestScript, neg bool, args []string) { } } +func cmdNewWebhook(ts *testscript.TestScript, neg bool, args []string) { + type webhookSite struct { + UUID string `json:"uuid"` + } + + if len(args) != 1 { + ts.Fatalf("usage: new-webhook ") + } + + const whSite = "https://webhook.site" + req, err := http.NewRequest(http.MethodPost, whSite+"/token", nil) + check(ts, err, neg) + + resp, err := http.DefaultClient.Do(req) + check(ts, err, neg) + + defer resp.Body.Close() + var site webhookSite + check(ts, json.NewDecoder(resp.Body).Decode(&site), neg) + + ts.Setenv(args[0], whSite+"/"+site.UUID) +} + func cmdCurl(ts *testscript.TestScript, neg bool, args []string) { var verbose bool var headers []string @@ -405,11 +407,35 @@ func cmdCurl(ts *testscript.TestScript, neg bool, args []string) { check(ts, cmd.Execute(), neg) } +func cmdWaitforserver(ts *testscript.TestScript, neg bool, args []string) { + // wait until the server is up + for { + conn, _ := net.DialTimeout( + "tcp", + net.JoinHostPort("localhost", fmt.Sprintf("%s", ts.Getenv("SSH_PORT"))), + time.Second, + ) + if conn != nil { + conn.Close() + break + } + } +} + +func cmdStopserver(ts *testscript.TestScript, neg bool, args []string) { + // stop the server + resp, err := http.DefaultClient.Head(fmt.Sprintf("%s/__stop", ts.Getenv("SOFT_SERVE_HTTP_PUBLIC_URL"))) + check(ts, err, neg) + defer resp.Body.Close() + time.Sleep(time.Second * 2) // Allow some time for the server to stop +} + func setupPostgres(t testscript.T, cfg *config.Config) (error, func()) { // Indicates postgres // Create a disposable database - dbName := fmt.Sprintf("softserve_test_%d", time.Now().UnixNano()) - dbDsn := os.Getenv("DB_DATA_SOURCE") + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + dbName := fmt.Sprintf("softserve_test_%d", rnd.Int63()) + dbDsn := cfg.DB.DataSource if dbDsn == "" { cfg.DB.DataSource = "postgres://postgres@localhost:5432/postgres?sslmode=disable" } @@ -419,7 +445,17 @@ func setupPostgres(t testscript.T, cfg *config.Config) (error, func()) { return err, nil } - connInfo := fmt.Sprintf("host=%s sslmode=disable", dbUrl.Hostname()) + scheme := dbUrl.Scheme + if scheme == "" { + scheme = "postgres" + } + + host := dbUrl.Hostname() + if host == "" { + host = "localhost" + } + + connInfo := fmt.Sprintf("host=%s sslmode=disable", host) username := dbUrl.User.Username() if username != "" { connInfo += fmt.Sprintf(" user=%s", username) @@ -431,6 +467,7 @@ func setupPostgres(t testscript.T, cfg *config.Config) (error, func()) { username = fmt.Sprintf("%s@", username) } else { connInfo += " user=postgres" + username = "postgres@" } port := dbUrl.Port() @@ -440,32 +477,31 @@ func setupPostgres(t testscript.T, cfg *config.Config) (error, func()) { } cfg.DB.DataSource = fmt.Sprintf("%s://%s%s%s/%s?sslmode=disable", - dbUrl.Scheme, + scheme, username, - dbUrl.Hostname(), + host, port, dbName, ) // Create the database - db, err := sql.Open(cfg.DB.Driver, connInfo) + dbx, err := db.Open(context.TODO(), cfg.DB.Driver, connInfo) if err != nil { return err, nil } - if _, err := db.Exec("CREATE DATABASE " + dbName); err != nil { + if _, err := dbx.Exec("CREATE DATABASE " + dbName); err != nil { return err, nil } return nil, func() { - db, err := sql.Open(cfg.DB.Driver, connInfo) + dbx, err := db.Open(context.TODO(), cfg.DB.Driver, connInfo) if err != nil { - t.Log("failed to open database", dbName, err) - return + t.Fatal("failed to open database", dbName, err) } - if _, err := db.Exec("DROP DATABASE " + dbName); err != nil { - t.Log("failed to drop database", dbName, err) + if _, err := dbx.Exec("DROP DATABASE " + dbName); err != nil { + t.Fatal("failed to drop database", dbName, err) } } } diff --git a/testscript/testdata/help.txtar b/testscript/testdata/help.txtar index 257ad2447..d6756ca02 100644 --- a/testscript/testdata/help.txtar +++ b/testscript/testdata/help.txtar @@ -1,9 +1,18 @@ # vi: set ft=conf [windows] dos2unix help.txt +# start soft serve +exec soft serve --sync-hooks & +# wait for server to start +waitforserver + soft --help cmpenv stdout help.txt +# stop the server +[windows] stopserver +[windows] ! stderr . + -- help.txt -- Soft Serve is a self-hostable Git server for the command line. diff --git a/testscript/testdata/http.txtar b/testscript/testdata/http.txtar index f2af369af..662e9b011 100644 --- a/testscript/testdata/http.txtar +++ b/testscript/testdata/http.txtar @@ -6,6 +6,11 @@ # convert crlf to lf on windows [windows] dos2unix http1.txt http2.txt http3.txt goget.txt gitclone.txt +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # create user soft user create user1 --key "$USER1_AUTHORIZED_KEY" @@ -133,6 +138,10 @@ stdout '404.*' curl http://$TOKEN@localhost:$HTTP_PORT/repo2.git?go-get=1 cmpenv stdout goget.txt +# stop the server +[windows] stopserver +[windows] ! stderr . + -- http1.txt -- {"transfer":"basic","objects":[{"oid":"","size":0,"error":{"code":422,"message":"invalid object"}}],"hash_algo":"sha256"} -- http2.txt -- diff --git a/testscript/testdata/jwt.txtar b/testscript/testdata/jwt.txtar index 07c605e94..7a257044e 100644 --- a/testscript/testdata/jwt.txtar +++ b/testscript/testdata/jwt.txtar @@ -1,5 +1,10 @@ # vi: set ft=conf +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # create user soft user create user1 --key "$USER1_AUTHORIZED_KEY" @@ -12,3 +17,7 @@ usoft jwt stdout '.*\..*\..*' usoft jwt repo stdout '.*\..*\..*' + +# stop the server +[windows] stopserver +[windows] ! stderr . diff --git a/testscript/testdata/mirror.txtar b/testscript/testdata/mirror.txtar index e1db9214d..d3cc1915d 100644 --- a/testscript/testdata/mirror.txtar +++ b/testscript/testdata/mirror.txtar @@ -3,6 +3,11 @@ # convert crlf to lf on windows [windows] dos2unix info1.txt info2.txt tree.txt +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # import a repo soft repo import --mirror charmbracelet/catwalk https://github.com/charmbracelet/catwalk.git @@ -74,6 +79,10 @@ cmp stdout info2.txt soft repo blob charmbracelet/test LICENSE stdout '.*Creative Commons.*' +# stop the server +[windows] stopserver +[windows] ! stderr . + -- info1.txt -- Project Name: diff --git a/testscript/testdata/repo-blob.txtar b/testscript/testdata/repo-blob.txtar index 770bd2d06..040160b17 100644 --- a/testscript/testdata/repo-blob.txtar +++ b/testscript/testdata/repo-blob.txtar @@ -3,6 +3,11 @@ # convert crlf to lf on windows [windows] dos2unix blob1.txt blob2.txt blob3.txt +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # create a repo soft repo create repo1 @@ -43,6 +48,9 @@ stderr 'revision does not exist' ! stdout . stderr 'revision does not exist' +# stop the server +[windows] stopserver + -- blob1.txt -- # Hello\n\nwelcome -- blob2.txt -- diff --git a/testscript/testdata/repo-collab.txtar b/testscript/testdata/repo-collab.txtar index 8f7d501b1..d692831bb 100644 --- a/testscript/testdata/repo-collab.txtar +++ b/testscript/testdata/repo-collab.txtar @@ -1,4 +1,10 @@ # vi: set ft=conf + +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # setup soft repo import test https://github.com/charmbracelet/catwalk.git soft user create foo --key "$USER1_AUTHORIZED_KEY" @@ -16,3 +22,7 @@ stdout 'foo' soft repo collab remove test foo soft repo collab list test ! stdout . + +# stop the server +[windows] stopserver +[windows] ! stderr . diff --git a/testscript/testdata/repo-commit.txtar b/testscript/testdata/repo-commit.txtar index dbf5c1bf8..70ca3da12 100644 --- a/testscript/testdata/repo-commit.txtar +++ b/testscript/testdata/repo-commit.txtar @@ -3,6 +3,11 @@ # convert crlf to lf on windows [windows] dos2unix commit1.txt +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # create a repo soft repo import basic1 https://github.com/git-fixtures/basic @@ -10,6 +15,10 @@ soft repo import basic1 https://github.com/git-fixtures/basic soft repo commit basic1 b8e471f58bcbca63b07bda20e428190409c2db47 cmp stdout commit1.txt +# stop the server +[windows] stopserver +[windows] ! stderr . + -- commit1.txt -- commit b8e471f58bcbca63b07bda20e428190409c2db47 Author: Daniel Ripolles diff --git a/testscript/testdata/repo-create.txtar b/testscript/testdata/repo-create.txtar index 794294a16..7b7f12d77 100644 --- a/testscript/testdata/repo-create.txtar +++ b/testscript/testdata/repo-create.txtar @@ -3,6 +3,11 @@ # convert crlf to lf on windows [windows] dos2unix readme.md branch_list.1.txt info.txt +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # create a repo soft repo create repo1 -d 'description' -H -p -n 'repo11' stderr 'Created repository repo1.*' @@ -114,6 +119,10 @@ stdout 'repo2' usoft repo delete repo2 ! exists $DATA_PATH/repos/repo2.git +# stop the server +[windows] stopserver +[windows] ! stderr . + -- readme.md -- # Project\nfoo diff --git a/testscript/testdata/repo-delete.txtar b/testscript/testdata/repo-delete.txtar index 2941dd48c..403c96043 100644 --- a/testscript/testdata/repo-delete.txtar +++ b/testscript/testdata/repo-delete.txtar @@ -1,5 +1,10 @@ # vi: set ft=conf +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + soft repo create repo1 soft repo create repo-to-delete soft repo delete repo-to-delete @@ -7,3 +12,7 @@ soft repo delete repo-to-delete stderr '.*not found.*' soft repo list stdout 'repo1' + +# stop the server +[windows] stopserver +[windows] ! stderr . diff --git a/testscript/testdata/repo-import.txtar b/testscript/testdata/repo-import.txtar index d85cdf379..84d4334b4 100644 --- a/testscript/testdata/repo-import.txtar +++ b/testscript/testdata/repo-import.txtar @@ -3,6 +3,11 @@ # convert crlf to lf on windows [windows] dos2unix repo3.txt +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # import private soft repo import --private repo1 https://github.com/charmbracelet/catwalk.git soft repo private repo1 @@ -18,6 +23,10 @@ soft repo import --name 'repo33' --description 'descriptive' repo3 https://githu soft repo info repo3 cmp stdout repo3.txt +# stop the server +[windows] stopserver +[windows] ! stderr . + -- repo3.txt -- Project Name: repo33 Repository: repo3 diff --git a/testscript/testdata/repo-perms.txtar b/testscript/testdata/repo-perms.txtar index 66341b9fe..1cc371e93 100644 --- a/testscript/testdata/repo-perms.txtar +++ b/testscript/testdata/repo-perms.txtar @@ -3,6 +3,11 @@ # convert crlf to lf on windows [windows] dos2unix info.txt +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # create a repo & user1 with admin soft repo create repo1 -p soft user create user1 -k "$USER1_AUTHORIZED_KEY" @@ -77,6 +82,10 @@ usoft repo delete repo1 usoft repo list ! stdout . +# stop the server +[windows] stopserver +[windows] ! stderr . + -- info.txt -- Project Name: proj Repository: repo1 diff --git a/testscript/testdata/repo-push.txtar b/testscript/testdata/repo-push.txtar new file mode 100644 index 000000000..cadb735b9 --- /dev/null +++ b/testscript/testdata/repo-push.txtar @@ -0,0 +1,18 @@ +# vi: set ft=conf + +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + +# create a repo +soft repo create repo-empty -d 'description' -H -p -n 'repo-empty' + +# clone repo +git clone ssh://localhost:$SSH_PORT/repo-empty repo-empty + +# push repo +! git -C repo-empty push origin HEAD + +# stop the server +[windows] stopserver diff --git a/testscript/testdata/repo-tree.txtar b/testscript/testdata/repo-tree.txtar index 0ccef0b9f..bc00ef031 100644 --- a/testscript/testdata/repo-tree.txtar +++ b/testscript/testdata/repo-tree.txtar @@ -3,6 +3,11 @@ # convert crlf to lf on windows [windows] dos2unix tree1.txt tree2.txt tree3.txt +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # create a repo soft repo create repo1 @@ -42,6 +47,9 @@ stderr 'file not found' ! stdout . stderr 'revision does not exist' +# stop the server +[windows] stopserver + -- tree1.txt -- drwxrwxrwx - folder -rw-r--r-- - .hidden diff --git a/testscript/testdata/repo-webhooks.txtar b/testscript/testdata/repo-webhooks.txtar index bee002e5c..fe393a5b6 100644 --- a/testscript/testdata/repo-webhooks.txtar +++ b/testscript/testdata/repo-webhooks.txtar @@ -1,27 +1,34 @@ # vi: set ft=conf +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # create a repo soft repo create repo-123 stderr 'Created repository repo-123.*' stdout ssh://localhost:$SSH_PORT/repo-123.git # create webhook -soft repo webhook create repo-123 https://webhook.site/794fa12b-08d4-4362-a0a9-a6f995f22e17 -e branch_tag_create -e branch_tag_delete -e collaborator -e push -e repository -e repository_visibility_change +new-webhook WH_REPO_123 +soft repo webhook create repo-123 $WH_REPO_123 -e branch_tag_create -e branch_tag_delete -e collaborator -e push -e repository -e repository_visibility_change # list webhooks soft repo webhook list repo-123 -stdout '1.*https://webhook.site/794fa12b-08d4-4362-a0a9-a6f995f22e17.*' +stdout '1.*webhook.site/.*' -# clone repo +# clone repo and commit files git clone ssh://localhost:$SSH_PORT/repo-123 repo-123 - -# create files mkfile ./repo-123/README.md 'foobar' git -C repo-123 add -A git -C repo-123 commit -m 'first' git -C repo-123 push origin HEAD # list webhook deliveries -# TODO: enable this test when githooks tests are fixed -# soft repo webhook deliver list repo-123 1 -# stdout '.*https://webhook.site/.*' +soft repo webhook deliver list repo-123 1 +stdout '✅.*push.*' + +# stop the server +[windows] stopserver +[windows] ! stderr . diff --git a/testscript/testdata/set-username.txtar b/testscript/testdata/set-username.txtar index 03c6e9ce2..30f1a8e00 100644 --- a/testscript/testdata/set-username.txtar +++ b/testscript/testdata/set-username.txtar @@ -3,6 +3,11 @@ # convert crlf to lf on windows [windows] dos2unix info1.txt info2.txt +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # get original username soft info cmpenv stdout info1.txt @@ -12,6 +17,10 @@ soft set-username test soft info cmpenv stdout info2.txt +# stop the server +[windows] stopserver +[windows] ! stderr . + -- info1.txt -- Username: admin Admin: true diff --git a/testscript/testdata/settings.txtar b/testscript/testdata/settings.txtar index 1340da343..1ceaec57d 100644 --- a/testscript/testdata/settings.txtar +++ b/testscript/testdata/settings.txtar @@ -1,4 +1,10 @@ # vi: set ft=conf + +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # check default allow-keyless soft settings allow-keyless true soft settings allow-keyless @@ -35,3 +41,6 @@ stdout 'admin-access.*' ! stdout . stderr . +# stop the server +[windows] stopserver + diff --git a/testscript/testdata/soft-browse.txtar b/testscript/testdata/soft-browse.txtar new file mode 100644 index 000000000..ca48d93d8 --- /dev/null +++ b/testscript/testdata/soft-browse.txtar @@ -0,0 +1,29 @@ +# vi: set ft=conf + +[windows] skip + +# clone repo +git clone https://github.com/charmbracelet/catwalk.git catwalk + +# run soft browse +ttyin input.txt +exec soft browse ./catwalk + +# cd and run soft +cd catwalk +ttyin ../input.txt +exec soft + +-- input.txt -- +jjkkdduu + +jjkkdduu + +jjkkdduu + +jjkkdduu + +jjkkdduu + +qqq + diff --git a/testscript/testdata/ssh.txtar b/testscript/testdata/ssh.txtar index b4e223739..3e652aeb2 100644 --- a/testscript/testdata/ssh.txtar +++ b/testscript/testdata/ssh.txtar @@ -2,6 +2,11 @@ [windows] dos2unix argserr1.txt argserr2.txt argserr3.txt invalidrepoerr.txt notauthorizederr.txt +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # create a user soft user create foo --key "$USER1_AUTHORIZED_KEY" @@ -87,6 +92,10 @@ stdout '.*header.*Bearer.*href.*expires_in.*expires_at.*' usoft git-lfs-authenticate repo2p upload stdout '.*header.*Bearer.*href.*expires_in.*expires_at.*' +# stop the server +[windows] stopserver +[windows] ! stderr . + -- argserr1.txt -- Error: accepts 1 arg(s), received 0 -- argserr2.txt -- diff --git a/testscript/testdata/token.txtar b/testscript/testdata/token.txtar index 3941b5e2a..821cea84d 100644 --- a/testscript/testdata/token.txtar +++ b/testscript/testdata/token.txtar @@ -1,5 +1,10 @@ # vi: set ft=conf +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # create user soft user create user1 --key "$USER1_AUTHORIZED_KEY" @@ -26,3 +31,6 @@ usoft token delete 1 stderr 'Access token deleted' ! usoft token delete 1 stderr 'token not found' + +# stop the server +[windows] stopserver diff --git a/testscript/testdata/user_management.txtar b/testscript/testdata/user_management.txtar index 8eade22e5..f65397090 100644 --- a/testscript/testdata/user_management.txtar +++ b/testscript/testdata/user_management.txtar @@ -3,6 +3,11 @@ # convert crlf to lf on windows [windows] dos2unix info.txt admin_key_list1.txt admin_key_list2.txt list1.txt list2.txt foo_info1.txt foo_info2.txt foo_info3.txt foo_info4.txt foo_info5.txt +# start soft serve +exec soft serve & +# wait for server to start +waitforserver + # add key to admin soft user add-pubkey admin "$ADMIN2_AUTHORIZED_KEY" soft user info admin @@ -63,6 +68,10 @@ soft user delete foo2 soft user list cmpenv stdout list1.txt +# stop the server +[windows] stopserver +[windows] ! stderr . + -- info.txt -- Username: admin