package dehub import ( "bytes" "errors" "io" "math/rand" "path/filepath" "testing" "dehub.dev/src/dehub.git/sigcred" "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 proj *Project cfg *Config } func newHarness(t *testing.T) *harness { rand := rand.New(rand.NewSource(0xb4eadb01)) return &harness{ t: t, rand: rand, proj: InitMemProject(), cfg: new(Config), } } func (h *harness) stage(tree map[string]string) { w, err := h.proj.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("removing %q: %v", path, err) } continue } dir := filepath.Dir(path) if err := fs.MkdirAll(dir, 0666); err != nil { h.t.Fatalf("making directory %q: %v", dir, err) } f, err := fs.Create(path) if err != nil { h.t.Fatalf("creating file %q: %v", path, err) } else if _, err := io.Copy(f, bytes.NewBufferString(content)); err != nil { h.t.Fatalf("writing to file %q: %v", path, err) } else if err := f.Close(); err != nil { h.t.Fatalf("closing file %q: %v", path, err) } else if _, err := w.Add(path); err != nil { h.t.Fatalf("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) stageNewAccount(accountID string, anon bool) sigcred.Signifier { sig, pubKeyBody := sigcred.TestSignifierPGP(accountID, anon, h.rand) if !anon { h.cfg.Accounts = append(h.cfg.Accounts, Account{ ID: accountID, Signifiers: []sigcred.SignifierUnion{{PGPPublicKey: &sigcred.SignifierPGP{ Body: string(pubKeyBody), }}}, }) h.stageCfg() } return sig } func (h *harness) stageAccessControls(aclYAML string) { if err := yaml.Unmarshal([]byte(aclYAML), &h.cfg.AccessControls); err != nil { h.t.Fatal(err) } h.stageCfg() } func (h *harness) checkout(branch plumbing.ReferenceName) { w, err := h.proj.GitRepo.Worktree() if err != nil { h.t.Fatal(err) } head, err := h.proj.GetHeadCommit() if errors.Is(err, ErrHeadIsZero) { // if HEAD is not resolvable to any hash than the Checkout method // doesn't work, just set HEAD manually. ref := plumbing.NewSymbolicReference(plumbing.HEAD, branch) if err := h.proj.GitRepo.Storer.SetReference(ref); err != nil { h.t.Fatal(err) } return } else if err != nil { h.t.Fatal(err) } _, err = h.proj.GitRepo.Storer.Reference(branch) if errors.Is(err, plumbing.ErrReferenceNotFound) { err = w.Checkout(&git.CheckoutOptions{ Hash: head.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.proj.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) } } type verifyExpectation int const ( verifyShouldSucceed verifyExpectation = 1 verifyShouldFail verifyExpectation = 0 verifySkip verifyExpectation = -1 ) func (h *harness) tryCommit( verifyExp verifyExpectation, payUn PayloadUnion, accountSig sigcred.Signifier, ) Commit { if accountSig != nil { var err error if payUn, err = h.proj.AccreditPayload(payUn, accountSig); err != nil { h.t.Fatalf("accrediting payload: %v", err) } } commit, err := h.proj.Commit(payUn) if err != nil { h.t.Fatalf("committing PayloadChange: %v", err) } else if verifyExp == verifySkip { return commit } branch, err := h.proj.ReferenceToBranchName(plumbing.HEAD) if err != nil { h.t.Fatalf("determining checked out branch: %v", err) } shouldSucceed := verifyExp > 0 err = h.proj.VerifyCommits(branch, []Commit{commit}) if shouldSucceed && err != nil { h.t.Fatalf("verifying commit %q: %v", commit.Hash, err) } else if shouldSucceed { return commit } else if !shouldSucceed && err == nil { h.t.Fatalf("verifying commit %q should have failed", commit.Hash) } var parentHash plumbing.Hash if commit.Object.NumParents() > 0 { parentHash = commit.Object.ParentHashes[0] } h.reset(parentHash, git.HardReset) return commit } func (h *harness) assertCommitChange( verifyExp verifyExpectation, msg string, sig sigcred.Signifier, ) Commit { payUn, err := h.proj.NewPayloadChange(msg) if err != nil { h.t.Fatalf("creating PayloadChange: %v", err) } return h.tryCommit(verifyExp, payUn, sig) } func TestHasStagedChanges(t *testing.T) { h := newHarness(t) rootSig := h.stageNewAccount("root", false) assertHasStaged := func(expHasStaged bool) { hasStaged, err := h.proj.HasStagedChanges() if err != nil { t.Fatalf("error calling HasStagedChanges: %v", err) } else if hasStaged != expHasStaged { t.Fatalf("expected HasStagedChanges to return %v", expHasStaged) } } // the harness starts with some staged changes assertHasStaged(true) h.stage(map[string]string{"foo": "bar"}) assertHasStaged(true) h.assertCommitChange(verifyShouldSucceed, "first commit", rootSig) assertHasStaged(false) h.stage(map[string]string{"foo": ""}) // delete foo assertHasStaged(true) h.assertCommitChange(verifyShouldSucceed, "second commit", rootSig) assertHasStaged(false) } // TestThisProjectStillVerifies opens this actual project and ensures that all // commits in it still verify. func TestThisProjectStillVerifies(t *testing.T) { proj, err := OpenProject(".") if err != nil { t.Fatalf("error opening repo: %v", err) } headCommit, err := proj.GetHeadCommit() if err != nil { t.Fatalf("getting repo head: %v", err) } allCommits, err := proj.GetCommitRange(plumbing.ZeroHash, headCommit.Hash) if err != nil { t.Fatalf("getting all commits (up to %q): %v", headCommit.Hash, err) } checkedOutBranch, err := proj.ReferenceToBranchName(plumbing.HEAD) if err != nil { t.Fatalf("error determining checked out branch: %v", err) } if err := proj.VerifyCommits(checkedOutBranch, allCommits); err != nil { t.Fatal(err) } } func TestShortHashResolving(t *testing.T) { // TODO ideally this test would test that conflicting hashes are noticed, // but that's hard... h := newHarness(t) rootSig := h.stageNewAccount("root", false) hash := h.assertCommitChange(verifyShouldSucceed, "first commit", rootSig).Hash hashStr := hash.String() t.Log(hashStr) for i := 2; i < len(hashStr); i++ { gotCommit, err := h.proj.GetCommitByRevision(plumbing.Revision(hashStr[:i])) if err != nil { t.Fatalf("resolving %q: %v", hashStr[:i], err) } else if gotCommit.Hash != hash { t.Fatalf("expected hash %q but got %q", gotCommit.Hash, hash) } } }