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": bc, "/~alice/gists": bc} {
if strings.Contains(get(h, path, cookie).Body.String(), secret) {
t.Errorf("%s lists a secret gist", path)
}
}
for _, path := range []string{"/gists", "/~alice/gists"} {
if body := get(h, path, ac).Body.String(); !strings.Contains(body, secret) || !strings.Contains(body, `private`) {
t.Errorf("%s hides the owner's private gist", path)
}
}
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")
}
}