package golooseauth import ( "net/http" "net/http/httptest" "net/url" "testing" goloose "github.com/danyel/go-loose/client" "github.com/danyel/go-tell/internal/config" ) func TestNewWithoutCredentialsIsDisabled(t *testing.T) { auth, err := New(config.GoLooseConfig{Tenant: "tell", AuthDomain: "auth.dev", AppDomain: "tell.dev"}) if err != nil || auth != nil { t.Fatalf("New() = %#v, %v; want a disabled client", auth, err) } auth, err = New(config.GoLooseConfig{ClientID: "client"}) if err != nil || auth != nil { t.Fatalf("New() with a half-configured client = %#v, %v; want a disabled client", auth, err) } } func TestNewRejectsInvalidDomains(t *testing.T) { cases := map[string]config.GoLooseConfig{ "tenant with a dot": {Tenant: "tell.dev", AuthDomain: "auth.dev", AppDomain: "tell.dev", ClientID: "c", ClientSecret: "s"}, "empty tenant": {Tenant: " ", AuthDomain: "auth.dev", AppDomain: "tell.dev", ClientID: "c", ClientSecret: "s"}, "empty auth domain": {Tenant: "tell", ClientID: "c", ClientSecret: "s"}, "empty app domain": {Tenant: "tell", AuthDomain: "auth.dev", ClientID: "c", ClientSecret: "s"}, } for name, cfg := range cases { t.Run(name, func(t *testing.T) { if _, err := New(cfg); err == nil { t.Fatal("New() error = nil, want a configuration error") } }) } } func TestNewBuildsSingleHostBrowserClient(t *testing.T) { auth, err := New(config.GoLooseConfig{ Tenant: " Tell ", AuthDomain: ".auth.dev.", AppDomain: "Tell.Dev", ClientID: "client", ClientSecret: "secret", }) if err != nil { t.Fatal(err) } if auth.Tenant() != "https://tell.auth.dev" { t.Fatalf("Tenant() = %q", auth.Tenant()) } recorder := httptest.NewRecorder() auth.LoginHandler(recorder, httptest.NewRequest(http.MethodGet, LoginPath, nil)) if recorder.Code != http.StatusFound { t.Fatalf("login status = %d, body = %s", recorder.Code, recorder.Body.String()) } target, err := url.Parse(recorder.Header().Get("Location")) if err != nil { t.Fatal(err) } if target.Scheme+"://"+target.Host != "https://tell.auth.dev" || target.Path != "/connect/authorize" { t.Fatalf("unexpected login target: %s", target) } if target.Query().Get("client_id") != "client" || target.Query().Get("redirect_uri") != "https://tell.dev"+CallbackPath || target.Query().Get("state") == "" { t.Fatalf("unexpected login query: %s", target.RawQuery) } cookies := recorder.Result().Cookies() if len(cookies) != 1 || cookies[0].Name == SessionCookie || !cookies[0].HttpOnly { t.Fatalf("expected an HTTP-only state cookie scoped to the callback, got %#v", cookies) } } func TestSessionUserResolvesCookie(t *testing.T) { identity := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Bearer session-token" { http.Error(w, "missing bearer token", http.StatusUnauthorized) return } w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"id":"user-1","email":"owner@tell.dev","display_name":"Owner","tenant_slug":"tell"}`)) })) defer identity.Close() auth := testAuth(t, identity) request := httptest.NewRequest(http.MethodPut, "https://tell.dev/api/content/post", nil) if _, ok := auth.SessionUser(request); ok { t.Fatal("SessionUser() without a cookie = true, want false") } if _, ok := auth.SessionUser(httptest.NewRequest(http.MethodPut, "https://tell.dev/api/content/post", nil)); ok { t.Fatal("SessionUser() without a cookie = true, want false") } } func TestSessionUserRejectsRejectedSession(t *testing.T) { identity := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { http.Error(w, "no such session", http.StatusUnauthorized) })) defer identity.Close() auth := testAuth(t, identity) request := httptest.NewRequest(http.MethodPut, "https://tell.dev/api/content/post", nil) request.AddCookie(&http.Cookie{Name: SessionCookie, Value: "expired"}) if user, ok := auth.SessionUser(request); ok { t.Fatalf("SessionUser() with a rejected session = %#v, want no user", user) } } func TestSessionUserReturnsIdentity(t *testing.T) { identity := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"id":"user-1","email":"owner@tell.dev","display_name":"Owner","tenant_slug":"tell"}`)) })) defer identity.Close() auth := testAuth(t, identity) request := httptest.NewRequest(http.MethodPut, "https://tell.dev/api/content/post", nil) request.AddCookie(&http.Cookie{Name: SessionCookie, Value: "session-token"}) user, ok := auth.SessionUser(request) if !ok { t.Fatal("SessionUser() = false, want the session user") } if user.Email != "owner@tell.dev" || user.DisplayName != "Owner" || user.TenantSlug != "tell" { t.Fatalf("unexpected user: %#v", user) } } func TestNilAuthHasNoSession(t *testing.T) { var auth *Auth if user, ok := auth.SessionUser(httptest.NewRequest(http.MethodGet, "/", nil)); ok { t.Fatalf("SessionUser() on a disabled client = %#v, want no user", user) } } // testAuth wires an Auth to a local identity server, which is what the client // would talk to once the base URL points somewhere reachable in a test. func testAuth(t *testing.T, identity *httptest.Server) *Auth { t.Helper() browser, err := goloose.NewBrowserAuth(goloose.BrowserAuthConfig{ BaseURL: identity.URL, ClientID: "client", ClientSecret: "secret", RedirectURL: "https://tell.dev" + CallbackPath, LoginPath: LoginPath, CookieName: SessionCookie, HTTPClient: identity.Client(), }) if err != nil { t.Fatal(err) } return &Auth{BrowserAuth: browser, baseURL: identity.URL, httpClient: identity.Client()} }