190 lines
6.2 KiB
Go
190 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestGoogleOAuthCallbackCreatesSessionAndUser(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mux := http.NewServeMux()
|
|
provider := httptest.NewServer(mux)
|
|
defer provider.Close()
|
|
|
|
mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
t.Fatalf("parse token form failed: %v", err)
|
|
}
|
|
if got := r.Form.Get("code"); got != "ok-code" {
|
|
t.Fatalf("code = %q, want %q", got, "ok-code")
|
|
}
|
|
if got := r.Form.Get("client_id"); got != "client-id" {
|
|
t.Fatalf("client_id = %q, want %q", got, "client-id")
|
|
}
|
|
if got := r.Form.Get("client_secret"); got != "client-secret" {
|
|
t.Fatalf("client_secret = %q, want %q", got, "client-secret")
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"access_token": "google-access-token"})
|
|
})
|
|
|
|
mux.HandleFunc("/userinfo", func(w http.ResponseWriter, r *http.Request) {
|
|
if got := r.Header.Get("Authorization"); got != "Bearer google-access-token" {
|
|
t.Fatalf("authorization = %q, want bearer token", got)
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"sub": "google-sub-1",
|
|
"email": "alice@example.com",
|
|
"email_verified": true,
|
|
})
|
|
})
|
|
|
|
s := makeTestServer(t, func(cfg *Config) {
|
|
cfg.GoogleAuthEnabled = true
|
|
cfg.GoogleClientID = "client-id"
|
|
cfg.GoogleClientSecret = "client-secret"
|
|
cfg.GoogleAuthURL = provider.URL + "/auth"
|
|
cfg.GoogleTokenURL = provider.URL + "/token"
|
|
cfg.GoogleUserInfoURL = provider.URL + "/userinfo"
|
|
})
|
|
|
|
startReq := httptest.NewRequest(http.MethodGet, "/api/auth/google/start", nil)
|
|
startReq.Host = "file.example.com"
|
|
startRec := httptest.NewRecorder()
|
|
s.handleGoogleAuthStart(startRec, startReq)
|
|
|
|
if startRec.Code != http.StatusFound {
|
|
t.Fatalf("start status = %d, want %d", startRec.Code, http.StatusFound)
|
|
}
|
|
stateCookie := cookieByName(startRec.Result().Cookies(), googleOAuthLoginStateCookie)
|
|
if stateCookie == nil || stateCookie.Value == "" {
|
|
t.Fatal("missing oauth state cookie")
|
|
}
|
|
|
|
redir := startRec.Result().Header.Get("Location")
|
|
parsed, err := url.Parse(redir)
|
|
if err != nil {
|
|
t.Fatalf("parse redirect url failed: %v", err)
|
|
}
|
|
if parsed.Query().Get("state") != stateCookie.Value {
|
|
t.Fatalf("redirect state mismatch")
|
|
}
|
|
|
|
cbReq := httptest.NewRequest(http.MethodGet, "/api/auth/google/callback?code=ok-code&state="+url.QueryEscape(stateCookie.Value), nil)
|
|
cbReq.Host = "file.example.com"
|
|
cbReq.AddCookie(stateCookie)
|
|
cbRec := httptest.NewRecorder()
|
|
s.handleGoogleAuthCallback(cbRec, cbReq)
|
|
|
|
if cbRec.Code != http.StatusFound {
|
|
t.Fatalf("callback status = %d, want %d", cbRec.Code, http.StatusFound)
|
|
}
|
|
if got := cbRec.Header().Get("Location"); got != "/drive" {
|
|
t.Fatalf("callback redirect = %q, want %q", got, "/drive")
|
|
}
|
|
if cookieByName(cbRec.Result().Cookies(), "access_token") == nil {
|
|
t.Fatal("callback missing access_token cookie")
|
|
}
|
|
if cookieByName(cbRec.Result().Cookies(), "refresh_token") == nil {
|
|
t.Fatal("callback missing refresh_token cookie")
|
|
}
|
|
|
|
var count int
|
|
var googleSub string
|
|
err = s.db.QueryRow(`SELECT COUNT(*), COALESCE(MAX(google_sub), '') FROM users WHERE email = ?`, "alice@example.com").Scan(&count, &googleSub)
|
|
if err != nil {
|
|
t.Fatalf("query user failed: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("users with google email = %d, want 1", count)
|
|
}
|
|
if strings.TrimSpace(googleSub) != "google-sub-1" {
|
|
t.Fatalf("google_sub = %q, want %q", googleSub, "google-sub-1")
|
|
}
|
|
}
|
|
|
|
func TestGoogleOAuthCallbackLinksGoogleToExistingUser(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mux := http.NewServeMux()
|
|
provider := httptest.NewServer(mux)
|
|
defer provider.Close()
|
|
|
|
mux.HandleFunc("/token", func(w http.ResponseWriter, _ *http.Request) {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"access_token": "google-link-token"})
|
|
})
|
|
|
|
mux.HandleFunc("/userinfo", func(w http.ResponseWriter, _ *http.Request) {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"sub": "google-sub-link-1",
|
|
"email": "other@example.com",
|
|
"email_verified": true,
|
|
})
|
|
})
|
|
|
|
s := makeTestServer(t, func(cfg *Config) {
|
|
cfg.GoogleAuthEnabled = true
|
|
cfg.GoogleClientID = "client-id"
|
|
cfg.GoogleClientSecret = "client-secret"
|
|
cfg.GoogleAuthURL = provider.URL + "/auth"
|
|
cfg.GoogleTokenURL = provider.URL + "/token"
|
|
cfg.GoogleUserInfoURL = provider.URL + "/userinfo"
|
|
})
|
|
|
|
user, err := s.createUser("alice", "password123", "dracula", "auto")
|
|
if err != nil {
|
|
t.Fatalf("create user failed: %v", err)
|
|
}
|
|
|
|
loginReq := httptest.NewRequest(http.MethodGet, "/api/auth/login-link", nil)
|
|
loginRec := httptest.NewRecorder()
|
|
if err := s.issueUserSession(loginRec, loginReq, user.ID); err != nil {
|
|
t.Fatalf("issueUserSession failed: %v", err)
|
|
}
|
|
accessCookie := cookieByName(loginRec.Result().Cookies(), "access_token")
|
|
if accessCookie == nil || accessCookie.Value == "" {
|
|
t.Fatal("missing access cookie")
|
|
}
|
|
|
|
startReq := httptest.NewRequest(http.MethodGet, "/api/user/google/link/start", nil)
|
|
startReq.Host = "file.example.com"
|
|
startReq.AddCookie(accessCookie)
|
|
startRec := httptest.NewRecorder()
|
|
s.handleGoogleLinkStart(startRec, startReq)
|
|
|
|
if startRec.Code != http.StatusFound {
|
|
t.Fatalf("link start status = %d, want %d", startRec.Code, http.StatusFound)
|
|
}
|
|
stateCookie := cookieByName(startRec.Result().Cookies(), googleOAuthLinkStateCookie)
|
|
if stateCookie == nil || stateCookie.Value == "" {
|
|
t.Fatal("missing link state cookie")
|
|
}
|
|
|
|
cbReq := httptest.NewRequest(http.MethodGet, "/api/auth/google/callback?code=ok-code&state="+url.QueryEscape(stateCookie.Value), nil)
|
|
cbReq.Host = "file.example.com"
|
|
cbReq.AddCookie(stateCookie)
|
|
cbReq.AddCookie(accessCookie)
|
|
cbRec := httptest.NewRecorder()
|
|
s.handleGoogleAuthCallback(cbRec, cbReq)
|
|
|
|
if cbRec.Code != http.StatusFound {
|
|
t.Fatalf("callback status = %d, want %d", cbRec.Code, http.StatusFound)
|
|
}
|
|
if got := cbRec.Header().Get("Location"); got != "/drive" {
|
|
t.Fatalf("callback redirect = %q, want %q", got, "/drive")
|
|
}
|
|
|
|
var googleSub string
|
|
err = s.db.QueryRow(`SELECT COALESCE(google_sub, '') FROM users WHERE id = ?`, user.ID).Scan(&googleSub)
|
|
if err != nil {
|
|
t.Fatalf("query user google_sub failed: %v", err)
|
|
}
|
|
if strings.TrimSpace(googleSub) != "google-sub-link-1" {
|
|
t.Fatalf("google_sub = %q, want %q", googleSub, "google-sub-link-1")
|
|
}
|
|
}
|