Progress streams now stay open until a job ends, so a hung clone would otherwise hold its job and stream forever.
176 lines
5.8 KiB
Go
176 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func fakeGitHub(t *testing.T) *httptest.Server {
|
|
gh := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/login/oauth/access_token":
|
|
r.ParseForm()
|
|
if r.Form.Get("client_secret") != "secret" || r.Form.Get("code") != "good" {
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "bad_verification_code"})
|
|
return
|
|
}
|
|
json.NewEncoder(w).Encode(map[string]any{"access_token": "ghu_token", "expires_in": 28800})
|
|
case "/user":
|
|
if r.Header.Get("Authorization") != "Bearer ghu_token" {
|
|
w.WriteHeader(401)
|
|
return
|
|
}
|
|
json.NewEncoder(w).Encode(map[string]string{"login": "octocat"})
|
|
default:
|
|
w.WriteHeader(404)
|
|
}
|
|
}))
|
|
t.Cleanup(gh.Close)
|
|
return gh
|
|
}
|
|
|
|
func (c client) noRedirects() client {
|
|
jarClient := *c.http
|
|
jarClient.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
|
|
return client{c.t, c.url, &jarClient}
|
|
}
|
|
|
|
func (c client) location(path string) *url.URL {
|
|
c.t.Helper()
|
|
res, err := c.http.Get(c.url + path)
|
|
if err != nil {
|
|
c.t.Fatal(err)
|
|
}
|
|
res.Body.Close()
|
|
if res.StatusCode != http.StatusFound {
|
|
c.t.Fatalf("GET %s = %d, want redirect", path, res.StatusCode)
|
|
}
|
|
loc, _ := url.Parse(res.Header.Get("Location"))
|
|
return loc
|
|
}
|
|
|
|
type githubStatus struct {
|
|
Configured, Connected bool
|
|
Login, InstallURL string
|
|
}
|
|
|
|
func TestGitHubSignIn(t *testing.T) {
|
|
server, c := newTestServer(t)
|
|
c = c.noRedirects()
|
|
gh := fakeGitHub(t)
|
|
server.github = githubApp{clientID: "client", clientSecret: "secret", slug: "code-nav", webURL: gh.URL, apiURL: gh.URL}
|
|
|
|
var status githubStatus
|
|
c.json("GET", "/api/github/status", "", 200, &status)
|
|
if !status.Configured || status.Connected || status.InstallURL != gh.URL+"/apps/code-nav/installations/new" {
|
|
t.Fatalf("status before sign-in = %+v", status)
|
|
}
|
|
|
|
login := c.location("/api/github/login")
|
|
state := login.Query().Get("state")
|
|
if login.Path != "/login/oauth/authorize" || login.Query().Get("client_id") != "client" || state == "" {
|
|
t.Fatalf("authorize redirect = %s", login)
|
|
}
|
|
if got := c.location("/api/github/callback?code=good&state=forged").String(); got != "/?github=error" {
|
|
t.Fatalf("forged state redirect = %s", got)
|
|
}
|
|
// State is single use, so the forged attempt burned it.
|
|
if got := c.location("/api/github/callback?code=good&state=" + state).String(); got != "/?github=error" {
|
|
t.Fatalf("reused state redirect = %s", got)
|
|
}
|
|
|
|
state = c.location("/api/github/login").Query().Get("state")
|
|
if got := c.stranger().noRedirects().location("/api/github/callback?code=good&state=" + state).String(); got != "/?github=error" {
|
|
t.Fatalf("callback from another session = %s", got)
|
|
}
|
|
state = c.location("/api/github/login").Query().Get("state")
|
|
if got := c.location("/api/github/callback?code=good&state=" + state).String(); got != "/?github=connected" {
|
|
t.Fatalf("callback redirect = %s", got)
|
|
}
|
|
c.json("GET", "/api/github/status", "", 200, &status)
|
|
if !status.Connected || status.Login != "octocat" {
|
|
t.Fatalf("status after sign-in = %+v", status)
|
|
}
|
|
if raw, _ := json.Marshal(status); strings.Contains(string(raw), "ghu_token") {
|
|
t.Fatal("status leaked the token")
|
|
}
|
|
c.stranger().json("GET", "/api/github/status", "", 200, &status)
|
|
if status.Connected {
|
|
t.Fatal("another session sees the sign-in")
|
|
}
|
|
|
|
server.expire(time.Now().Add(9 * time.Hour))
|
|
c.json("GET", "/api/github/status", "", 200, &status)
|
|
if status.Connected || len(server.auth) != 0 {
|
|
t.Fatalf("expired token still present: %+v, %d entries", status, len(server.auth))
|
|
}
|
|
|
|
state = c.location("/api/github/login").Query().Get("state")
|
|
c.location("/api/github/callback?code=good&state=" + state)
|
|
c.json("POST", "/api/github/logout", "", 200, nil)
|
|
c.json("GET", "/api/github/status", "", 200, &status)
|
|
if status.Connected {
|
|
t.Fatal("still connected after logout")
|
|
}
|
|
}
|
|
|
|
func TestGitHubRoutesWhenUnconfigured(t *testing.T) {
|
|
server, c := newTestServer(t)
|
|
server.github = githubApp{}
|
|
var status githubStatus
|
|
c.json("GET", "/api/github/status", "", 200, &status)
|
|
if status.Configured {
|
|
t.Fatal("reported configured without credentials")
|
|
}
|
|
c.json("GET", "/api/github/login", "", 404, nil)
|
|
}
|
|
|
|
func TestGitCloneKeepsTokenOutOfArgsAndIgnoresHostCredentials(t *testing.T) {
|
|
if _, err := exec.LookPath("git"); err != nil {
|
|
t.Skip("git not installed")
|
|
}
|
|
cmd := gitClone(t.Context(), "https://github.com/o/r", t.TempDir(), "ghu_secret")
|
|
if strings.Contains(strings.Join(cmd.Args, " "), "ghu_secret") {
|
|
t.Fatal("token appears in git arguments")
|
|
}
|
|
read := func(key string) string {
|
|
probe := exec.Command("git", "config", "--get-all", key)
|
|
probe.Env = cmd.Env
|
|
out, _ := probe.Output()
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
if got := read("http.https://github.com/.extraheader"); got != "Authorization: Basic eC1hY2Nlc3MtdG9rZW46Z2h1X3NlY3JldA==" {
|
|
t.Errorf("extraheader = %q", got)
|
|
}
|
|
if got := read("credential.helper"); got != "" {
|
|
t.Errorf("credential.helper = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestCloneFailureMessages(t *testing.T) {
|
|
configured := githubApp{clientID: "id", clientSecret: "secret"}
|
|
private := "fatal: could not read Username for 'https://github.com': terminal prompts disabled"
|
|
for _, c := range []struct {
|
|
app githubApp
|
|
output string
|
|
signedIn bool
|
|
want string
|
|
}{
|
|
{configured, private, false, "signin"},
|
|
{configured, "remote: Repository not found.\nfatal: repository 'x' not found", true, "install"},
|
|
{githubApp{}, private, false, ""},
|
|
{configured, "fatal: unable to access: Could not resolve host: github.com", false, ""},
|
|
} {
|
|
message, got := c.app.cloneFailure(c.output, c.signedIn)
|
|
if got != c.want || message == "" {
|
|
t.Errorf("cloneFailure(%q, %v) = %q, %q; want %q", c.output, c.signedIn, message, got, c.want)
|
|
}
|
|
}
|
|
}
|