package dehub import ( "bytes" "dehub/accessctl" "dehub/sigcred" "errors" "io" "math/rand" "path/filepath" "runtime/debug" "testing" "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing" yaml "gopkg.in/yaml.v2" ) type harness struct { t *testing.T rand *rand.Rand repo *Repo cfg *Config sig sigcred.SignifierInterface } func newHarness(t *testing.T) *harness { rand := rand.New(rand.NewSource(0xb4eadb01)) sig, pubKeyBody := sigcred.SignifierPGPTmp("root", rand) pubKeyPath := filepath.Join(DehubDir, "root.asc") cfg := &Config{ Accounts: []Account{{ ID: "root", Signifiers: []sigcred.Signifier{{PGPPublicKeyFile: &sigcred.SignifierPGPFile{ Path: pubKeyPath, }}}, }}, AccessControls: []accessctl.BranchAccessControl{ { BranchPattern: "**", ChangeAccessControls: []accessctl.ChangeAccessControl{ { FilePathPattern: "**", Condition: accessctl.Condition{ Signature: &accessctl.ConditionSignature{ AccountIDs: []string{"root"}, Count: "100%", }, }, }, }, }, }, } cfgBody, err := yaml.Marshal(cfg) if err != nil { t.Fatal(err) } h := &harness{ t: t, rand: rand, repo: InitMemRepo(), cfg: cfg, sig: sig, } h.stage(map[string]string{ ConfigPath: string(cfgBody), pubKeyPath: string(pubKeyBody), }) return h } func (h *harness) stage(tree map[string]string) { w, err := h.repo.GitRepo.Worktree() if err != nil { h.t.Fatal(err) } fs := w.Filesystem for path, content := range tree { if content == "" { if _, err := w.Remove(path); err != nil { h.t.Fatalf("error removing %q: %v", path, err) } continue } dir := filepath.Dir(path) if err := fs.MkdirAll(dir, 0666); err != nil { h.t.Fatalf("error making directory %q: %v", dir, err) } f, err := fs.Create(path) if err != nil { h.t.Fatalf("error creating file %q: %v", path, err) } else if _, err := io.Copy(f, bytes.NewBufferString(content)); err != nil { h.t.Fatalf("error writing to file %q: %v", path, err) } else if err := f.Close(); err != nil { h.t.Fatalf("error closing file %q: %v", path, err) } else if _, err := w.Add(path); err != nil { h.t.Fatalf("error adding file %q to index: %v", path, err) } } } func (h *harness) stageCfg() { cfgBody, err := yaml.Marshal(h.cfg) if err != nil { h.t.Fatal(err) } h.stage(map[string]string{ConfigPath: string(cfgBody)}) } func (h *harness) checkout(branch plumbing.ReferenceName) { w, err := h.repo.GitRepo.Worktree() if err != nil { h.t.Fatal(err) } head, err := h.repo.GetGitHead() if err != nil { h.t.Fatal(err) } _, err = h.repo.GitRepo.Branch(branch.Short()) if errors.Is(err, git.ErrBranchNotFound) { err = w.Checkout(&git.CheckoutOptions{ Hash: head.GitCommit.Hash, Branch: branch, Create: true, }) } else if err != nil { h.t.Fatalf("checking if branch already exists: %v", branch) } else { err = w.Checkout(&git.CheckoutOptions{ Branch: branch, }) } if err != nil { h.t.Fatalf("checking out branch: %v", err) } } func (h *harness) reset(to plumbing.Hash, mode git.ResetMode) { w, err := h.repo.GitRepo.Worktree() if err != nil { h.t.Fatal(err) } err = w.Reset(&git.ResetOptions{ Commit: to, Mode: mode, }) if err != nil { h.t.Fatal(err) } } func (h *harness) tryCommit( shouldSucceed bool, commit Commit, accountID string, accountSig sigcred.SignifierInterface, ) GitCommit { if accountSig != nil { var err error if commit, err = h.repo.AccreditCommit(commit, accountSig); err != nil { h.t.Fatalf("accrediting commit: %v", err) } } gitCommit, err := h.repo.Commit(commit, accountID) if err != nil { h.t.Fatalf("failed to commit ChangeCommit: %v", err) } branch, err := h.repo.CheckedOutBranch() if err != nil { h.t.Fatalf("determining checked out branch: %v", err) } err = h.repo.VerifyCommits(branch, []GitCommit{gitCommit}) if shouldSucceed && err != nil { h.t.Fatalf("verifying commit %q: %v", gitCommit.GitCommit.Hash, err) } else if shouldSucceed { return gitCommit } else if !shouldSucceed && err == nil { h.t.Fatalf("verifying commit %q should have failed", gitCommit.GitCommit.Hash) } if gitCommit.GitCommit.NumParents() == 0 { h.t.Fatalf("unverifiable commit %q has no parents, but it should", gitCommit.GitCommit.NumParents()) } h.reset(gitCommit.GitCommit.ParentHashes[0], git.HardReset) return gitCommit } func (h *harness) changeCommit( msg string, accountID string, sig sigcred.SignifierInterface, ) GitCommit { commit, err := h.repo.NewCommitChange(msg) if err != nil { h.t.Fatalf("creating ChangeCommit: %v", err) } return h.tryCommit(true, commit, accountID, sig) } func TestHasStagedChanges(t *testing.T) { harness := newHarness(t) assertHasStaged := func(expHasStaged bool) { hasStaged, err := harness.repo.HasStagedChanges() if err != nil { debug.PrintStack() t.Fatalf("error calling HasStagedChanges: %v", err) } else if hasStaged != expHasStaged { debug.PrintStack() t.Fatalf("expected HasStagedChanges to return %v", expHasStaged) } } // the harness starts with some staged changes assertHasStaged(true) harness.stage(map[string]string{"foo": "bar"}) assertHasStaged(true) harness.changeCommit("first commit", "root", harness.sig) assertHasStaged(false) harness.stage(map[string]string{"foo": ""}) // delete foo assertHasStaged(true) harness.changeCommit("second commit", "root", harness.sig) assertHasStaged(false) } // TestOldConfig tests that having an older, now malformed, Config doesn't mess // with the current parsing, as long as the default access controls still work. func TestOldConfig(t *testing.T) { harness := newHarness(t) // overwrite the currently staged config file with an older form harness.stage(map[string]string{ConfigPath: ` --- accounts: - id: root signifiers: - type: pgp_public_key_file path: ".dehub/root.asc" access_controls: - pattern: "**" condition: type: signature account_ids: - root count: 0 `}) // this commit should be created and verify fine harness.changeCommit("first commit, this is going great", "root", harness.sig) // this commit should not be verifiable, because toot isn't in accounts and // the default access controls should be being used harness.stage(map[string]string{"foo": "no rules!"}) badCommit, err := harness.repo.NewCommitChange("ain't no laws") if err != nil { t.Fatalf("creating CommitChange: %v", err) } harness.tryCommit(false, badCommit, "toot", nil) // make a commit fixing the config. everything should still be fine. harness.stage(map[string]string{ConfigPath: ` --- accounts: - id: root signifiers: - type: pgp_public_key_file path: ".dehub/root.asc" `}) harness.changeCommit("Fix the config!", "root", harness.sig) } // TestThisRepoStillVerifies opens this actual repository and ensures that all // commits in it still verify, given this codebase. func TestThisRepoStillVerifies(t *testing.T) { repo, err := OpenRepo(".") if err != nil { t.Fatalf("error opening repo: %v", err) } headGitCommit, err := repo.GetGitHead() if err != nil { t.Fatalf("getting repo head: %v", err) } allCommits, err := repo.GetGitCommitRange(plumbing.ZeroHash, headGitCommit.GitCommit.Hash) if err != nil { t.Fatalf("getting all commits (up to %q): %v", headGitCommit.GitCommit.Hash, err) } checkedOutBranch, err := repo.CheckedOutBranch() if err != nil { t.Fatalf("error determining checked out branch: %v", err) } if err := repo.VerifyCommits(checkedOutBranch, allCommits); err != nil { t.Fatal(err) } }