package forge import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "net/url" "os/exec" "strconv" "strings" "testing" ) func gistLogin(t *testing.T, s *Server, h http.Handler, name string) (User, string) { t.Helper() u, err := s.store.CreateUser(name, "correct horse") if err != nil { t.Fatal(err) } w := post(h, "/login", url.Values{"name": {name}, "password": {"correct horse"}, "email": {name + "@example.org"}}, "") c := session(w) if c == "" { t.Fatalf("login %s: %d", name, w.Code) } return u, c } func newGist(t *testing.T, h http.Handler, cookie string, form url.Values) string { t.Helper() w := post(h, "/gists", form, cookie) id := strings.TrimPrefix(w.Header().Get("Location"), "/gist/") if w.Code != 303 || !gistID.MatchString(id) { t.Fatalf("create gist: %d %s", w.Code, w.Body.String()) } return id } func TestGists(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") } s, h := testServer(t) alice, ac := gistLogin(t, s, h, "alice") bob, bc := gistLogin(t, s, h, "bob") files := url.Values{"name": {"hello.go", ""}, "content": {"package main\r\n", "plain notes\n"}, "orig": {"", ""}, "description": {"first"}} if w := post(h, "/gists", files, ac); w.Code != 400 || !strings.Contains(w.Body.String(), "a day old") { t.Fatalf("young account created a gist: %d", w.Code) } age(t, s, alice) age(t, s, bob) id := newGist(t, h, ac, files) page := get(h, "/gist/"+id, bc).Body.String() for _, want := range []string{"hello.go", "gistfile1.txt", "first", "/report?kind=gist"} { if !strings.Contains(page, want) { t.Errorf("gist page lacks %q", want) } } raw := pagesGet(h, "gist.example.org", "/raw/"+id+"/hello.go") if raw.Body.String() != "package main\n" || !strings.HasPrefix(raw.Header().Get("Content-Type"), "text/plain") || !strings.Contains(raw.Header().Get("Content-Security-Policy"), "sandbox") || len(raw.Result().Cookies()) > 0 { t.Errorf("raw: %d %q %v", raw.Code, raw.Body.String(), raw.Header()) } if w := pagesGet(h, "gist.example.org", "/gists"); w.Code == 200 { t.Error("raw host serves the web app") } if w := pagesGet(h, "gist.example.org", "/embed/"+id); !strings.Contains(w.Body.String(), "hello.go") || !strings.Contains(w.Header().Get("Content-Security-Policy"), "frame-ancestors *") { t.Errorf("embed: %d %v", w.Code, w.Header()) } secret := newGist(t, h, ac, url.Values{"name": {"key.txt"}, "content": {"hidden"}, "orig": {""}, "visibility": {"secret"}}) for path, cookie := range map[string]string{"/gists": ac, "/~alice/gists": bc} { if strings.Contains(get(h, path, cookie).Body.String(), secret) { t.Errorf("%s lists a secret gist", path) } } if !strings.Contains(get(h, "/~alice/gists", ac).Body.String(), secret) { t.Error("owner cannot see their secret gist") } for _, path := range []string{"/gists/new", "/gist/" + id + "/edit", "/gists", "/~alice/gists?tab=starred"} { if w := get(h, path, ac); w.Code != 200 { t.Errorf("%s: %d", path, w.Code) } } head := s.gistHead(Gist{ID: id, Owner: alice}) edit := url.Values{"head": {head}, "name": {"hello.go", "gistfile1.txt"}, "orig": {"hello.go", "gistfile1.txt"}, "content": {"package main\n\nfunc main() {}\n", ""}, "description": {"second"}} if w := post(h, "/gist/"+id+"/edit", edit, bc); w.Code != 403 { t.Errorf("non-owner edited: %d", w.Code) } if w := post(h, "/gist/"+id+"/edit", edit, ac); w.Code != 303 { t.Fatalf("edit: %d %s", w.Code, w.Body.String()) } if w := post(h, "/gist/"+id+"/edit", edit, ac); w.Code != 400 { t.Errorf("stale edit accepted: %d", w.Code) } log := get(h, "/gist/"+id+"/revisions", bc).Body.String() if strings.Count(log, `class="revision"`) != 2 || !strings.Contains(log, "func main() {}") { t.Error("revisions do not show both commits") } if !strings.Contains(get(h, "/gist/"+id+"?rev="+head, bc).Body.String(), "gistfile1.txt") { t.Error("old revision not viewable") } post(h, "/gist/"+id+"/star", nil, bc) post(h, "/gist/"+id+"/comment", url.Values{"body": {"nice **work**"}}, bc) if w := post(h, "/gist/"+id+"/fork", nil, bc); w.Code != 303 { t.Fatalf("fork: %d", w.Code) } g, err := s.store.Gist(id) if err != nil || g.Stars != 1 || g.Forks != 1 || g.Comments != 1 { t.Errorf("counts: %+v %v", g, err) } forks, _ := s.store.GistForks(id) if len(forks) != 1 || forks[0].Owner.ID != bob.ID { t.Fatalf("forks: %+v", forks) } if !strings.Contains(get(h, "/gist/"+id, ac).Body.String(), "work") { t.Error("comment not rendered") } t.Setenv("GITPOCKET_PUSHER", strconv.FormatInt(bob.ID, 10)) update := func(ref, tree string) int { gd := Git{Dir: gistDir(s.root, "alice", id)} out, err := gd.runEnv(ident("T", "t@example.org", timeNow()), []byte("x"), "commit-tree", tree, "-p", head) if err != nil { t.Fatal(err) } var errw bytes.Buffer return gistPreReceive(s.root, id, strings.NewReader(head+" "+strings.TrimSpace(string(out))+" "+ref+"\n"), &errw) } gd := Git{Dir: gistDir(s.root, "alice", id)} blob, _ := gd.run([]byte("x"), "hash-object", "-w", "--stdin") flat, _ := gd.run([]byte("100644 blob "+strings.TrimSpace(string(blob))+"\tok.txt\x00"), "mktree", "-z") nested, _ := gd.run([]byte("040000 tree "+strings.TrimSpace(string(flat))+"\tdir\x00"), "mktree", "-z") if update("refs/heads/main", strings.TrimSpace(string(flat))) == 0 { t.Error("non-owner push accepted") } t.Setenv("GITPOCKET_PUSHER", strconv.FormatInt(alice.ID, 10)) if update("refs/heads/main", strings.TrimSpace(string(flat))) != 0 { t.Error("owner push refused") } if update("refs/heads/other", strings.TrimSpace(string(flat))) == 0 { t.Error("push to another branch accepted") } if update("refs/heads/main", strings.TrimSpace(string(nested))) == 0 { t.Error("nested tree accepted") } token, err := s.store.CreateToken(alice.ID, "cli", scopeAll) if err != nil { t.Fatal(err) } body := `{"description":"api","visibility":"public","files":[{"name":"a.md","content":"# hi"}]}` r := httptest.NewRequest("POST", "/api/gists", strings.NewReader(body)) r.Header.Set("Content-Type", "application/json") r.Header.Set("Authorization", "Bearer "+token) r.RemoteAddr = "192.0.2.1:1000" w := httptest.NewRecorder() h.ServeHTTP(w, r) var made struct { ID string Files []struct{ Name, Content string } } if w.Code != 201 || json.Unmarshal(w.Body.Bytes(), &made) != nil || len(made.Files) != 1 || made.Files[0].Content != "# hi" { t.Fatalf("api create: %d %s", w.Code, w.Body.String()) } if w := get(h, "/api/gists/"+made.ID, ""); w.Code != 200 || !strings.Contains(w.Body.String(), `"a.md"`) { t.Errorf("api get: %d", w.Code) } r = httptest.NewRequest("POST", "/api/gists?name=x.txt", strings.NewReader("raw upload")) r.RemoteAddr = "192.0.2.1:1000" w = httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != 401 { t.Errorf("anonymous api create: %d", w.Code) } if w := get(h, "/gist/"+id+"/revisions?from=1", bc); w.Code != 200 || !strings.Contains(w.Body.String(), "Newer") { t.Errorf("revision paging: %d", w.Code) } if w := get(h, "/admin?s=gists", ac); w.Code != 403 && w.Code != 404 { t.Errorf("admin gists open to a non-admin: %d", w.Code) } s.store.SetAdmin("alice", true) if w := get(h, "/admin?s=gists", ac); w.Code != 200 || !strings.Contains(w.Body.String(), "/gist/"+id) { t.Errorf("admin gists: %d", w.Code) } if w := get(h, "/search?type=gists&q=hello", bc); !strings.Contains(w.Body.String(), "/gist/"+id) { t.Error("gist search misses hello.go") } if w := post(h, "/gist/"+id+"/delete", nil, bc); w.Code != 403 { t.Errorf("non-owner deleted: %d", w.Code) } if w := post(h, "/gist/"+id+"/delete", nil, ac); w.Code != 303 { t.Errorf("delete: %d", w.Code) } if _, err := s.store.Gist(id); err == nil { t.Error("gist survived deletion") } }