package main // 🤖 GitHub code-flow door tests (github.go, #1112): a fake GitHub — one // httptest server playing both the token-exchange and /user endpoints — // swapped in via the package URL seams. The oidc suite covers the shared // anchor machinery; these pin the exchange contract, the resolve-or-create // path keying on the numeric id, and the GET half of the return trampoline. import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" ) // fakeGithub answers the exchange with a token whose validity it controls, // and /user with a fixed numeric id. Codes prefixed "bad" refuse. func fakeGithub(t *testing.T, userID int64) *httptest.Server { t.Helper() mux := http.NewServeMux() mux.HandleFunc("POST /login/oauth/access_token", func(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil || r.PostFormValue("client_secret") != "gh-secret" { w.Write([]byte(`{"error":"bad_client"}`)) return } if strings.HasPrefix(r.PostFormValue("code"), "bad") { w.Write([]byte(`{"error":"bad_verification_code"}`)) return } w.Write([]byte(`{"access_token":"gho_test","token_type":"bearer"}`)) }) mux.HandleFunc("GET /user", func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Bearer gho_test" { w.WriteHeader(http.StatusUnauthorized) return } fmt.Fprintf(w, `{"id":%d,"login":"someone"}`, userID) }) fake := httptest.NewServer(mux) t.Cleanup(fake.Close) return fake } func githubServer(t *testing.T, userID int64) (*httptest.Server, *Server) { t.Helper() ts, s := newTestServer(t) fake := fakeGithub(t, userID) oldTok, oldUser := githubTokenURL, githubUserURL githubTokenURL = fake.URL + "/login/oauth/access_token" githubUserURL = fake.URL + "/user" t.Cleanup(func() { githubTokenURL, githubUserURL = oldTok, oldUser }) s.github = &githubDoor{clientID: "gh-client", secret: "gh-secret", pepper: []byte("gh-pepper")} return ts, s } func githubVerifyReq(t *testing.T, ts *httptest.Server, headers map[string]string, body map[string]any) *http.Response { t.Helper() b, _ := json.Marshal(body) return do(t, "POST", ts.URL+"/api/oidc/github/verify", "", headers, b) } func TestGithubDisabledIs404(t *testing.T) { ts, _ := newTestServer(t) if resp := githubVerifyReq(t, ts, nil, map[string]any{"code": "x"}); resp.StatusCode != http.StatusNotFound { t.Fatalf("disabled verify: got %d, want 404", resp.StatusCode) } if resp := do(t, "GET", ts.URL+"/oidc/return?code=x&state=web.x", "", nil, nil); resp.StatusCode != http.StatusNotFound { t.Fatalf("disabled GET return: got %d, want 404", resp.StatusCode) } } func TestGithubSignInCreatesThenResolvesAccount(t *testing.T) { ts, _ := githubServer(t, 4242) var first struct { Token string `json:"token"` Proof string `json:"proof"` Created bool `json:"created"` } resp := githubVerifyReq(t, ts, nil, map[string]any{"code": "good-1"}) if resp.StatusCode != http.StatusOK { t.Fatalf("first sign-in: got %d, want 200", resp.StatusCode) } if err := json.NewDecoder(resp.Body).Decode(&first); err != nil || first.Token == "" || first.Proof == "" || !first.Created { t.Fatalf("first sign-in: bad body (%+v, err %v)", first, err) } // The same numeric id — through a different code — is the SAME account. var second struct { Token string `json:"token"` Created bool `json:"created"` } resp = githubVerifyReq(t, ts, nil, map[string]any{"code": "good-2"}) if resp.StatusCode != http.StatusOK { t.Fatalf("second sign-in: got %d, want 200", resp.StatusCode) } if err := json.NewDecoder(resp.Body).Decode(&second); err != nil || second.Created { t.Fatalf("second sign-in should resolve, not create (%+v, err %v)", second, err) } } func TestGithubBadCodeIs401(t *testing.T) { ts, _ := githubServer(t, 4242) if resp := githubVerifyReq(t, ts, nil, map[string]any{"code": "bad-code"}); resp.StatusCode != http.StatusUnauthorized { t.Fatalf("bad code: got %d, want 401", resp.StatusCode) } } func TestGithubReturnTrampoline(t *testing.T) { ts, _ := githubServer(t, 4242) client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }} resp, err := client.Get(ts.URL + "/oidc/return?code=abc&state=web.r1") if err != nil { t.Fatal(err) } if resp.StatusCode != http.StatusSeeOther { t.Fatalf("web state: got %d, want 303", resp.StatusCode) } if loc := resp.Header.Get("Location"); loc != "/#oauth=abc&state=web.r1" { t.Fatalf("web state location: %q", loc) } resp, err = client.Get(ts.URL + "/oidc/return?code=abc&state=mac.r2") if err != nil { t.Fatal(err) } if loc := resp.Header.Get("Location"); !strings.HasPrefix(loc, oidcReturnScheme+"#state=mac.r2&code=abc") { t.Fatalf("native state location: %q", loc) } } // The regression Paul hit live: the github anchor stored fine (and merges // moved it) but the listing/removal surfaces spoke only oidcProviderNames, // so it was invisible and un-removable. Pin the whole loop: sign in, LIST // the anchor, and confirm the removal ROUTE recognizes the kind (the // last-method 409 — not the unknown-kind 400). func TestGithubAnchorListsAndRoutesRemoval(t *testing.T) { ts, s := githubServer(t, 9001) var signin struct { Token string `json:"token"` Proof string `json:"proof"` } resp := githubVerifyReq(t, ts, nil, map[string]any{"code": "good"}) if resp.StatusCode != http.StatusOK { t.Fatalf("sign-in: got %d", resp.StatusCode) } if err := json.NewDecoder(resp.Body).Decode(&signin); err != nil { t.Fatal(err) } resp = do(t, "GET", ts.URL+"/api/anchors", signin.Token, nil, nil) if resp.StatusCode != http.StatusOK { t.Fatalf("list: got %d", resp.StatusCode) } var listing struct { Anchors []struct { Kind string `json:"kind"` ID string `json:"id"` } `json:"anchors"` } if err := json.NewDecoder(resp.Body).Decode(&listing); err != nil { t.Fatal(err) } ghID := "" for _, a := range listing.Anchors { if a.Kind == "github" { ghID = a.ID } } if ghID == "" { t.Fatalf("github anchor missing from listing: %+v", listing.Anchors) } _ = s // Its removal must ROUTE (the one method on the account → the // last-method 409, never the unknown-kind 400). resp = do(t, "DELETE", ts.URL+"/api/anchors/github/"+ghID, signin.Token, map[string]string{"X-Ceremony-Proof": signin.Proof}, nil) if resp.StatusCode != http.StatusConflict { t.Fatalf("remove route: got %d, want 409 (last method)", resp.StatusCode) } }