The context was cancelled before its error was checked, so any failure looked like a timeout and private repositories never offered sign-in. Clone handling moves into cloneRepo, which checks for DeadlineExceeded specifically, with tests for success, failure with token masking, and timeout.
203 lines
7.0 KiB
Go
203 lines
7.0 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCloneRepo(t *testing.T) {
|
|
if _, err := exec.LookPath("git"); err != nil {
|
|
t.Skip("git not installed")
|
|
}
|
|
source := t.TempDir()
|
|
for _, args := range [][]string{{"init", "-q"}, {"-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "--allow-empty", "-m", "x"}} {
|
|
if out, err := exec.Command("git", append([]string{"-C", source}, args...)...).CombinedOutput(); err != nil {
|
|
t.Fatalf("git %v: %v %s", args, err, out)
|
|
}
|
|
}
|
|
if out, err := cloneRepo(source, filepath.Join(t.TempDir(), "ok"), ""); err != nil {
|
|
t.Fatalf("local clone failed: %v %s", err, out)
|
|
}
|
|
|
|
out, err := cloneRepo(filepath.Join(source, "missing-ghu_secret"), filepath.Join(t.TempDir(), "x"), "ghu_secret")
|
|
if err == nil || strings.Contains(out, "timed out") || strings.Contains(out, "ghu_secret") || !strings.Contains(out, "***") {
|
|
t.Fatalf("failed clone = %v %q; want a masked, non-timeout error", err, out)
|
|
}
|
|
|
|
defer func(old time.Duration) { cloneTimeout = old }(cloneTimeout)
|
|
cloneTimeout = time.Nanosecond
|
|
if out, err := cloneRepo(source, filepath.Join(t.TempDir(), "slow"), ""); err == nil || !strings.HasPrefix(out, "timed out") {
|
|
t.Fatalf("expired clone = %v %q; want timeout", err, out)
|
|
}
|
|
}
|