// Package testserver runs a real barerepo in a test, so the other two repos can drive this one. package testserver import ( "context" "fmt" "net/http/httptest" "os" "os/exec" "path/filepath" "strings" "sync" "testing" "github.com/barerepo/server/internal/cache" "github.com/barerepo/server/internal/config" "github.com/barerepo/server/internal/gitread" "github.com/barerepo/server/internal/httpd" "github.com/barerepo/server/internal/repocfg" "github.com/barerepo/server/internal/store" "github.com/barerepo/server/internal/thread" "github.com/barerepo/server/internal/token" "github.com/barerepo/server/internal/transport" ) // Instance is one running barerepo, torn down when the test ends. type Instance struct { URL string Cfg config.Config DB *store.DB } var ( buildOnce sync.Once buildPath string buildErr error ) // Binary builds the server once per test binary, because the hooks are this program calling itself. func Binary(tb testing.TB) string { tb.Helper() buildOnce.Do(func() { dir, err := os.MkdirTemp("", "barerepo-testserver-") if err != nil { buildErr = err return } buildPath = filepath.Join(dir, "barerepo") out, err := exec.Command("go", "build", "-o", buildPath, "github.com/barerepo/server/cmd/barerepo").CombinedOutput() if err != nil { buildErr = fmt.Errorf("building the server: %v\n%s", err, out) } }) if buildErr != nil { tb.Fatal(buildErr) } return buildPath } // New starts a barerepo on a loopback port with its own disk and database. func New(tb testing.TB) *Instance { tb.Helper() bin := Binary(tb) root := tb.TempDir() cfg := config.Default() cfg.Paths.Repos = filepath.Join(root, "repos") cfg.Paths.Cache = filepath.Join(root, "cache") cfg.Paths.Artifacts = filepath.Join(root, "artifacts") cfg.Database.URL = "sqlite://" + filepath.Join(root, "barerepo.db") cfg.Path = filepath.Join(root, "barerepo.toml") body := fmt.Sprintf("[paths]\nrepos = %q\ncache = %q\nartifacts = %q\n\n[database]\nurl = %q\n", cfg.Paths.Repos, cfg.Paths.Cache, cfg.Paths.Artifacts, cfg.Database.URL) if err := os.WriteFile(cfg.Path, []byte(body), 0o600); err != nil { tb.Fatal(err) } for _, d := range []string{cfg.Paths.Repos, cfg.Paths.Cache, cfg.Paths.Artifacts} { if err := os.MkdirAll(d, 0o750); err != nil { tb.Fatal(err) } } // The server writes this on every key change, and the hook checks signatures against it. store.SignersPath = filepath.Join(cfg.Paths.Cache, "allowed_signers") db, err := store.Open(context.Background(), cfg.Database.URL) if err != nil { tb.Fatal(err) } tb.Cleanup(func() { db.Close() }) shared := cache.New(cfg.Paths.Cache) gitread.Cache = shared repocfg.Cache = shared thread.Cache = shared httpd.Cache = shared srv := &httpd.Server{Cfg: cfg, DB: db, Transport: &transport.Server{Cfg: cfg, DB: db, Bin: bin}} ts := httptest.NewServer(srv.Handler()) tb.Cleanup(ts.Close) cfg.Server.ExternalURL = ts.URL srv.Cfg = cfg srv.Transport.Cfg = cfg body += fmt.Sprintf("\n[server]\nexternal_url = %q\n", ts.URL) if err := os.WriteFile(cfg.Path, []byte(body), 0o600); err != nil { tb.Fatal(err) } return &Instance{URL: ts.URL, Cfg: cfg, DB: db} } // Account makes an account with a fresh key and hands back a token for git over https. func (in *Instance) Account(tb testing.TB, name string) string { tb.Helper() dir := tb.TempDir() key := filepath.Join(dir, "id") if out, err := exec.Command("ssh-keygen", "-t", "ed25519", "-N", "", "-C", name, "-f", key, "-q").CombinedOutput(); err != nil { tb.Fatalf("ssh-keygen: %v\n%s", err, out) } pub, err := os.ReadFile(key + ".pub") if err != nil { tb.Fatal(err) } if _, err := in.DB.CreateAccount(context.Background(), name, string(pub), false); err != nil { tb.Fatal(err) } tok, _, err := in.DB.CreateToken(context.Background(), token.Git, name, "", "test") if err != nil { tb.Fatal(err) } return tok } // RunnerToken issues a token scoped to one repository, which is what chapter 15 requires. func (in *Instance) RunnerToken(tb testing.TB, account, owner, name string) string { tb.Helper() tok, _, err := in.DB.CreateToken(context.Background(), token.Runner, account, owner+"/"+name, "test runner") if err != nil { tb.Fatal(err) } return tok } // Repo pushes a first commit, which is how a repository comes into being here. func (in *Instance) Repo(tb testing.TB, tok, owner, name string) string { tb.Helper() work := tb.TempDir() write := func(path, body string) { full := filepath.Join(work, path) if err := os.MkdirAll(filepath.Dir(full), 0o750); err != nil { tb.Fatal(err) } if err := os.WriteFile(full, []byte(body), 0o600); err != nil { tb.Fatal(err) } } write("config.go", "package main\n") write(".barerepo/config", "[repo]\nvisibility = \"public\"\n") for _, args := range [][]string{ {"init", "-q", "-b", "master"}, {"add", "-A"}, {"commit", "-qm", "first"}, {"push", "-q", in.URLFor(tok, "/"+owner+"/"+name), "master"}, } { cmd := exec.Command("git", args...) cmd.Dir = work cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=tester", "GIT_AUTHOR_EMAIL=t@x", "GIT_COMMITTER_NAME=tester", "GIT_COMMITTER_EMAIL=t@x", "GIT_TERMINAL_PROMPT=0", "GIT_CONFIG_COUNT=1", "GIT_CONFIG_KEY_0=credential.helper", "GIT_CONFIG_VALUE_0=") if out, err := cmd.CombinedOutput(); err != nil { tb.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out) } } return work } // URLFor is the clone url with a token in it, which is how a test pushes without an agent. func (in *Instance) URLFor(tok, path string) string { return strings.Replace(in.URL, "://", "://x:"+tok+"@", 1) + path }