267 lines
9.3 KiB
Go
267 lines
9.3 KiB
Go
package main
|
|
|
|
// deleteremote_test.go — the command that deletes a repository on a server mgsh
|
|
// does not own.
|
|
//
|
|
// Nothing it does can be undone from here, so the tests are less about the happy
|
|
// path than about the ways it must refuse: no target named, an unknown target, a
|
|
// question answered with no, a repository that is not there. Each of those has
|
|
// to leave the server untouched, which is asserted on the requests the fake
|
|
// provider received — the DELETE either went out or it did not.
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// mirrorServer is a fake Gitea holding one repository owned by "mike".
|
|
func mirrorServer(t *testing.T, project string) *fakeProvider {
|
|
t.Helper()
|
|
f := newFakeProvider(t)
|
|
f.route("GET /api/v1/user", 200, `{"login":"mike"}`)
|
|
f.route("GET /api/v1/repos/mike/"+project, 200, `{}`)
|
|
f.route("DELETE /api/v1/repos/mike/"+project, 204, "")
|
|
return f
|
|
}
|
|
|
|
// mirrorProject points the globals at a project with f as its single mirror
|
|
// target, and answers every y/n question with answer. It returns the project
|
|
// directory and the recorded questions.
|
|
func mirrorProject(t *testing.T, f *fakeProvider, name string, answer bool) (string, *[]string) {
|
|
t.Helper()
|
|
dir := useProject(t, name)
|
|
cfg.Remotes = []RemoteTarget{{Name: "gitea", URL: f.URL, Key: "tok", Type: "gitea"}}
|
|
return dir, fakeAnswers(t, answer)
|
|
}
|
|
|
|
// deleted reports whether the DELETE for project actually went to the server.
|
|
func deleted(f *fakeProvider, project string) bool {
|
|
return f.find("DELETE /api/v1/repos/mike/"+project) != nil
|
|
}
|
|
|
|
// TestDeleteRepoPerProvider pins the request each provider expects. GitLab
|
|
// answers 202 — the deletion is only scheduled — which still counts as success.
|
|
func TestDeleteRepoPerProvider(t *testing.T) {
|
|
for _, c := range []struct {
|
|
typ, path string
|
|
code int
|
|
}{
|
|
{"gitea", "/api/v1/repos/mike/mgsh", 204},
|
|
{"github", "/api/v3/repos/mike/mgsh", 204},
|
|
{"gitlab", "/api/v4/projects/mike%2Fmgsh", 202},
|
|
} {
|
|
f := newFakeProvider(t)
|
|
f.route("DELETE "+c.path, c.code, "")
|
|
|
|
if err := newRemoteAPI(f.URL, "tok", c.typ).deleteRepo("mike", "mgsh"); err != nil {
|
|
t.Fatalf("%s: deleteRepo: %v (requests: %v)", c.typ, err, f.paths())
|
|
}
|
|
if f.find("DELETE "+c.path) == nil {
|
|
t.Errorf("%s: expected DELETE %s, got %v", c.typ, c.path, f.paths())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDeleteRepoNamesTheMissingPermission: deleting needs more of a token than
|
|
// pushing does, so a 403 here is the likeliest failure of all. Passing the bare
|
|
// status on would send people to the wrong setting.
|
|
func TestDeleteRepoNamesTheMissingPermission(t *testing.T) {
|
|
f := newFakeProvider(t)
|
|
f.route("DELETE /api/v3/repos/mike/mgsh", 403, `{"message":"Must have admin rights"}`)
|
|
|
|
err := newRemoteAPI(f.URL, "tok", "github").deleteRepo("mike", "mgsh")
|
|
if err == nil || !strings.Contains(err.Error(), "delete_repo") {
|
|
t.Errorf("403 error = %v, want it to name the delete_repo scope", err)
|
|
}
|
|
}
|
|
|
|
// TestDeleteRepoTreatsUnroutedAsFailure: anything the server did not accept must
|
|
// surface as an error, or a repository that is still there looks deleted.
|
|
func TestDeleteRepoReportsFailure(t *testing.T) {
|
|
f := newFakeProvider(t) // everything 404s
|
|
if err := newRemoteAPI(f.URL, "tok", "gitea").deleteRepo("mike", "mgsh"); err == nil {
|
|
t.Error("deleteRepo accepted a 404 as success")
|
|
}
|
|
}
|
|
|
|
// TestDeleteRemoteNeedsATarget is the one that matters most: `pushremote` with
|
|
// no @name means "every configured server", and inheriting that here would wipe
|
|
// the project off all of them at once.
|
|
func TestDeleteRemoteNeedsATarget(t *testing.T) {
|
|
f := mirrorServer(t, "notes")
|
|
_, asked := mirrorProject(t, f, "notes", true) // even a standing "yes"
|
|
|
|
out := captureStdout(t, func() { runCommand("deleteremote") })
|
|
|
|
if len(f.got) != 0 {
|
|
t.Fatalf("deleteremote talked to the server without being told where: %v", f.paths())
|
|
}
|
|
if len(*asked) != 0 {
|
|
t.Errorf("asked %q although no target was named", *asked)
|
|
}
|
|
if !strings.Contains(out, "@gitea") {
|
|
t.Errorf("output does not say which targets exist: %q", out)
|
|
}
|
|
}
|
|
|
|
// TestDeleteRemoteRejectsUnknownTarget: a mistyped server name must not fall
|
|
// back to some other target.
|
|
func TestDeleteRemoteRejectsUnknownTarget(t *testing.T) {
|
|
f := mirrorServer(t, "notes")
|
|
_, asked := mirrorProject(t, f, "notes", true)
|
|
|
|
out := captureStdout(t, func() { runCommand("deleteremote @gitae") })
|
|
|
|
if len(f.got) != 0 {
|
|
t.Fatalf("an unknown target still reached a server: %v", f.paths())
|
|
}
|
|
if len(*asked) != 0 {
|
|
t.Errorf("asked %q for an unknown target", *asked)
|
|
}
|
|
if !strings.Contains(out, "gitae") {
|
|
t.Errorf("unknown target not reported: %q", out)
|
|
}
|
|
}
|
|
|
|
// TestDeleteRemoteKeepsRepoWhenDeclined: the question is the last guard, so a
|
|
// "no" has to stop the DELETE, not just the message about it.
|
|
func TestDeleteRemoteKeepsRepoWhenDeclined(t *testing.T) {
|
|
f := mirrorServer(t, "notes")
|
|
_, asked := mirrorProject(t, f, "notes", false)
|
|
|
|
captureStdout(t, func() { runCommand("deleteremote @gitea") })
|
|
|
|
if len(*asked) != 1 {
|
|
t.Fatalf("questions asked = %q, want exactly one", *asked)
|
|
}
|
|
if !strings.Contains((*asked)[0], "mike/notes") {
|
|
t.Errorf("question %q does not name the repository being deleted", (*asked)[0])
|
|
}
|
|
if deleted(f, "notes") {
|
|
t.Fatalf("deleted the repository after the user declined: %v", f.paths())
|
|
}
|
|
}
|
|
|
|
// TestDeleteRemoteWithoutRepositoryAsksNothing: a project that was never
|
|
// mirrored (or a typo in the project name) is a no-op, not a question.
|
|
func TestDeleteRemoteWithoutRepositoryAsksNothing(t *testing.T) {
|
|
f := newFakeProvider(t)
|
|
f.route("GET /api/v1/user", 200, `{"login":"mike"}`) // the repo lookup 404s
|
|
_, asked := mirrorProject(t, f, "notes", true)
|
|
|
|
out := captureStdout(t, func() { runCommand("deleteremote @gitea") })
|
|
|
|
if len(*asked) != 0 {
|
|
t.Errorf("asked %q about a repository that is not there", *asked)
|
|
}
|
|
if deleted(f, "notes") {
|
|
t.Error("sent a DELETE for a repository the server does not have")
|
|
}
|
|
if !strings.Contains(out, "nothing to delete") {
|
|
t.Errorf("output = %q, want it to say there is nothing to delete", out)
|
|
}
|
|
}
|
|
|
|
// TestDeleteRemoteByHostAndGitRemoteCleanup covers the whole accepted path: the
|
|
// server picked by its host rather than its configured name, the DELETE sent,
|
|
// and the now-dangling git remote dropped — while an unrelated remote stays.
|
|
func TestDeleteRemoteByHostAndGitRemoteCleanup(t *testing.T) {
|
|
f := mirrorServer(t, "notes")
|
|
dir, asked := mirrorProject(t, f, "notes", true)
|
|
|
|
mustGit(t, dir, "init", "-q")
|
|
mustGit(t, dir, "remote", "add", "gitea", f.URL+"/mike/notes.git")
|
|
mustGit(t, dir, "remote", "add", "origin", "git@git.example:notes.git")
|
|
|
|
captureStdout(t, func() { runCommand("deleteremote " + remoteHost(f.URL)) })
|
|
|
|
if len(*asked) != 1 {
|
|
t.Fatalf("questions asked = %q, want exactly one", *asked)
|
|
}
|
|
if !deleted(f, "notes") {
|
|
t.Fatalf("no DELETE sent, requests: %v", f.paths())
|
|
}
|
|
remotes, err := gitCapture(dir, "remote")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
names := splitLines(strings.TrimSpace(remotes))
|
|
for _, n := range names {
|
|
if n == "gitea" {
|
|
t.Errorf("git remote gitea survived the deletion: %q", names)
|
|
}
|
|
}
|
|
if len(names) != 1 || names[0] != "origin" {
|
|
t.Errorf("git remotes = %q, want origin left alone", names)
|
|
}
|
|
}
|
|
|
|
// TestDeleteRemoteKeepsARepointedGitRemote: the local remote is only dropped
|
|
// while it still points at what was deleted. One the user has since aimed
|
|
// somewhere else is theirs.
|
|
func TestDeleteRemoteKeepsARepointedGitRemote(t *testing.T) {
|
|
f := mirrorServer(t, "notes")
|
|
dir, _ := mirrorProject(t, f, "notes", true)
|
|
|
|
mustGit(t, dir, "init", "-q")
|
|
mustGit(t, dir, "remote", "add", "gitea", "https://elsewhere.example/mike/notes.git")
|
|
|
|
captureStdout(t, func() { runCommand("deleteremote @gitea") })
|
|
|
|
if !deleted(f, "notes") {
|
|
t.Fatalf("no DELETE sent, requests: %v", f.paths())
|
|
}
|
|
url, err := gitCapture(dir, "remote", "get-url", "gitea")
|
|
if err != nil {
|
|
t.Fatalf("git remote gitea was removed although it pointed elsewhere: %v", err)
|
|
}
|
|
if strings.TrimSpace(url) != "https://elsewhere.example/mike/notes.git" {
|
|
t.Errorf("git remote url = %q, want it untouched", strings.TrimSpace(url))
|
|
}
|
|
}
|
|
|
|
// TestRemoteMatches: a target answers to its configured name and to the host of
|
|
// its url, in any case and with or without the '@'.
|
|
func TestRemoteMatches(t *testing.T) {
|
|
tgt := RemoteTarget{Name: "gitea", URL: "https://git.example.com:3000/root"}
|
|
for _, c := range []struct {
|
|
sel string
|
|
want bool
|
|
}{
|
|
{"gitea", true},
|
|
{"GITEA", true},
|
|
{"@gitea", true},
|
|
{"git.example.com", true},
|
|
{"https://git.example.com/mike/x", true},
|
|
{"git@git.example.com:mike/x.git", true},
|
|
{"example.com", false}, // a suffix is not the host
|
|
{"other", false},
|
|
{"@", false},
|
|
{"", false},
|
|
} {
|
|
if got := remoteMatches(tgt, c.sel); got != c.want {
|
|
t.Errorf("remoteMatches(%q) = %v, want %v", c.sel, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestParseRemoteSelectors: everything on the line selects a server, '@' or not
|
|
// — the command takes nothing else that a bare word could be confused with.
|
|
func TestParseRemoteSelectors(t *testing.T) {
|
|
for _, c := range []struct {
|
|
in string
|
|
want []string
|
|
}{
|
|
{"", nil},
|
|
{"@gitea", []string{"gitea"}},
|
|
{"git.example.com", []string{"git.example.com"}},
|
|
{"@gitea git.example.com", []string{"gitea", "git.example.com"}},
|
|
{"@", nil},
|
|
} {
|
|
got := parseRemoteSelectors(c.in)
|
|
if strings.Join(got, ",") != strings.Join(c.want, ",") {
|
|
t.Errorf("parseRemoteSelectors(%q) = %v, want %v", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|