e2e/ghimport_test.go
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}