Detailed changes
@@ -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
@@ -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
@@ -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)
@@ -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()
}()
@@ -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
@@ -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
@@ -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=
@@ -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)
+ }
+}
@@ -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),
}
@@ -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)
+ }
+}
@@ -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)
+ }
+ }
+}
@@ -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))
}
@@ -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)
+ }
+}
@@ -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)
+}
@@ -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 {
@@ -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)
}
}
@@ -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)
+ }
+}
@@ -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)
+ }
+}
@@ -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)
+ }
+}
@@ -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
+}
@@ -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)
+ }
+}
@@ -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 {
@@ -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)
+ }
+}
@@ -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
@@ -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 {
@@ -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)
+ }
+ }
+}
@@ -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
}
@@ -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)
+ }
+}
@@ -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
}
@@ -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)
+ }
+ }
+ }
+}
@@ -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
}
@@ -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) {
@@ -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
}
@@ -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)
}
}
@@ -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)
}
@@ -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))
+ }
+ })
+ }
+}
@@ -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
}
@@ -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 <env-name>")
+ }
+
+ 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)
}
}
}
@@ -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.
@@ -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 --
@@ -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 .
@@ -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:
@@ -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 --
@@ -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 .
@@ -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
@@ -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
@@ -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 .
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 .
@@ -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
@@ -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
+
@@ -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
+
@@ -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 --
@@ -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
@@ -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