e2e/ghimport_test.go

v1.14.0
gitbay/e2e/ghimport_test.go history · blame · raw

156 lines · 6177 bytes

  1package e2e
  2
  3import (
  4	"fmt"
  5	"net/http"
  6	"net/http/httptest"
  7	"os"
  8	"path/filepath"
  9	"strings"
 10	"testing"
 11)
 12
 13// fakeGitHub serves just enough of the GitHub REST API for the importer.
 14func fakeGitHub(t *testing.T) *httptest.Server {
 15	t.Helper()
 16	mux := http.NewServeMux()
 17	auth := func(w http.ResponseWriter, r *http.Request) bool {
 18		if r.Header.Get("Authorization") != "Bearer sekrit" {
 19			w.WriteHeader(401)
 20			return false
 21		}
 22		return true
 23	}
 24	mux.HandleFunc("/repos/octo/legacy/issues", func(w http.ResponseWriter, r *http.Request) {
 25		if !auth(w, r) {
 26			return
 27		}
 28		if r.URL.Query().Get("page") != "1" {
 29			fmt.Fprint(w, "[]")
 30			return
 31		}
 32		fmt.Fprint(w, `[
 33		 {"number":1,"title":"old bug","body":"it crashed","state":"closed",
 34		  "created_at":"2019-03-04T10:00:00Z","user":{"login":"octofan"},
 35		  "labels":[{"name":"bug"}],"comments":0},
 36		 {"number":2,"title":"add feature","body":"the patch","state":"closed",
 37		  "created_at":"2020-06-01T10:00:00Z","user":{"login":"drive-by"},
 38		  "labels":[],"comments":1,"pull_request":{}},
 39		 {"number":3,"title":"still open","body":"discuss","state":"open",
 40		  "created_at":"2021-01-01T10:00:00Z","user":{"login":"octofan"},
 41		  "labels":[],"comments":2}
 42		]`)
 43	})
 44	mux.HandleFunc("/repos/octo/legacy/pulls/2", func(w http.ResponseWriter, r *http.Request) {
 45		if !auth(w, r) {
 46			return
 47		}
 48		fmt.Fprint(w, `{"merged_at":"2020-06-02T10:00:00Z",
 49		 "head":{"sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","ref":"feature"},
 50		 "base":{"sha":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","ref":"main"}}`)
 51	})
 52	comments := func(payload string) http.HandlerFunc {
 53		return func(w http.ResponseWriter, r *http.Request) {
 54			if !auth(w, r) {
 55				return
 56			}
 57			if r.URL.Query().Get("page") != "1" {
 58				fmt.Fprint(w, "[]")
 59				return
 60			}
 61			fmt.Fprint(w, payload)
 62		}
 63	}
 64	mux.HandleFunc("/repos/octo/legacy/issues/2/comments", comments(
 65		`[{"id":101,"body":"nice patch","created_at":"2020-06-01T11:00:00Z","user":{"login":"maintainer"}}]`))
 66	mux.HandleFunc("/repos/octo/legacy/issues/3/comments", comments(
 67		`[{"id":102,"body":"me too","created_at":"2021-01-02T10:00:00Z","user":{"login":"other"}},
 68		  {"id":103,"body":"still happening","created_at":"2021-02-01T10:00:00Z","user":{"login":"octofan"}}]`))
 69	srv := httptest.NewServer(mux)
 70	t.Cleanup(srv.Close)
 71	return srv
 72}
 73
 74func TestGitHubIssueImport(t *testing.T) {
 75	// allow_local lets --api-base reach the loopback fake; a default
 76	// instance refuses it (see the SSRF check at the end).
 77	inst := startInstanceWith(t, "[webhooks]\nallow_local = true\n")
 78	aliceKey := inst.newKey(t, "alice")
 79	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
 80
 81	if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
 82		t.Fatalf("repo create: %s", errOut)
 83	}
 84	work := t.TempDir()
 85	env := inst.gitEnv(aliceKey)
 86	mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
 87	dir := filepath.Join(work, "w")
 88	os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\n"), 0o644)
 89	mustGit(t, dir, env, "checkout", "-q", "-b", "main")
 90	mustGit(t, dir, env, "add", ".")
 91	mustGit(t, dir, env, "commit", "-q", "-m", "base")
 92	mustGit(t, dir, env, "push", "-q", "origin", "main")
 93
 94	gh := fakeGitHub(t)
 95	out, errOut, code := inst.ssh(t, aliceKey, "sekrit\n", "repo", "import-issues", "alice/app",
 96		"--from", "octo/legacy", "--token-stdin", "--api-base", gh.URL)
 97	if code != 0 {
 98		t.Fatalf("import: %s", errOut)
 99	}
100	if !strings.Contains(out, "imported 2 issues, 1 merge requests, 3 comments") {
101		t.Fatalf("summary: %s", out)
102	}
103
104	// Issue #1 (GitHub #1): closed, labeled, attributed.
105	out, _, _ = inst.ssh(t, aliceKey, "", "issue", "show", "alice/app", "1", "--json")
106	if !strings.Contains(out, "old bug") || !strings.Contains(out, `"state":"closed"`) ||
107		!strings.Contains(out, `"labels":["bug"]`) ||
108		!strings.Contains(out, "imported issue github.com/octo/legacy#1") ||
109		!strings.Contains(out, "@octofan, 2019-03-04") {
110		t.Fatalf("issue 1: %s", out)
111	}
112	// Issue #2 (GitHub #3): open, two attributed comments.
113	out, _, _ = inst.ssh(t, aliceKey, "", "issue", "show", "alice/app", "2", "--json")
114	if !strings.Contains(out, "still open") || !strings.Contains(out, `"state":"open"`) ||
115		!strings.Contains(out, "me too") || !strings.Contains(out, "@other, 2021-01-02") {
116		t.Fatalf("issue 2: %s", out)
117	}
118	// MR !1 (GitHub PR #2): merged, discussion imported.
119	out, _, _ = inst.ssh(t, aliceKey, "", "mr", "show", "alice/app", "1", "--json")
120	if !strings.Contains(out, "add feature") || !strings.Contains(out, `"state":"merged"`) ||
121		!strings.Contains(out, "imported pull request github.com/octo/legacy#2") ||
122		!strings.Contains(out, "nice patch") {
123		t.Fatalf("mr 1: %s", out)
124	}
125
126	// Re-running imports nothing new — fully resumable.
127	out, _, code = inst.ssh(t, aliceKey, "sekrit\n", "repo", "import-issues", "alice/app",
128		"--from", "octo/legacy", "--token-stdin", "--api-base", gh.URL)
129	if code != 0 || !strings.Contains(out, "imported 0 issues, 0 merge requests, 0 comments (3 items already imported)") {
130		t.Fatalf("re-run: %s", out)
131	}
132	out, _, _ = inst.ssh(t, aliceKey, "", "issue", "list", "alice/app", "--state", "all")
133	if strings.Count(out, "\n") != 2 {
134		t.Fatalf("issues duplicated:\n%s", out)
135	}
136
137	// A wrong token surfaces the API error.
138	if _, errOut, code := inst.ssh(t, aliceKey, "wrong\n", "repo", "import-issues", "alice/app",
139		"--from", "octo/legacy", "--token-stdin", "--api-base", gh.URL); code == 0 || !strings.Contains(errOut, "401") {
140		t.Fatalf("bad token: exit %d, %s", code, errOut)
141	}
142}
143
144func TestGitHubImportSSRFGuard(t *testing.T) {
145	inst := startInstance(t) // allow_local off: default posture
146	aliceKey := inst.newKey(t, "alice")
147	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
148	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
149		t.Fatal("repo create failed")
150	}
151	_, errOut, code := inst.ssh(t, aliceKey, "", "repo", "import-issues", "alice/app",
152		"--from", "octo/legacy", "--api-base", "http://127.0.0.1:9999")
153	if code != 2 || !strings.Contains(errOut, "SSRF") {
154		t.Fatalf("local api-base allowed: exit %d, %s", code, errOut)
155	}
156}