package zitadel_test import ( "context" "encoding/base64" "encoding/json" "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" "gateway/internal/library/zitadel" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAuthorizeURL(t *testing.T) { t.Parallel() client, err := zitadel.NewClient(zitadel.Conf{ Issuer: testIssuerURL, OAuthClientID: testClientID, GoogleIdPID: "google-idp-1", }) require.NoError(t, err) raw, err := client.AuthorizeURL("https://app.example.com/callback", "state-abc", "google") require.NoError(t, err) u, err := url.Parse(raw) require.NoError(t, err) require.Equal(t, "https", u.Scheme) require.Equal(t, "zitadel.example.com", u.Host) require.Equal(t, "/oauth/v2/authorize", u.Path) q := u.Query() require.Equal(t, testClientID, q.Get("client_id")) require.Equal(t, "https://app.example.com/callback", q.Get("redirect_uri")) require.Equal(t, "code", q.Get("response_type")) require.Equal(t, "openid profile email", q.Get("scope")) require.Equal(t, "state-abc", q.Get("state")) require.Equal(t, "google-idp-1", q.Get("idp_id")) } func TestAuthorizeURLWithoutGoogleIdP(t *testing.T) { t.Parallel() client, err := zitadel.NewClient(zitadel.Conf{ Issuer: testIssuerURL, OAuthClientID: testClientID, }) require.NoError(t, err) raw, err := client.AuthorizeURL("https://app.example.com/callback", "state-abc", "google") require.NoError(t, err) require.NotContains(t, raw, "idp_id=") } func TestAuthorizeURLRequiresClientAndParams(t *testing.T) { t.Parallel() client, err := zitadel.NewClient(zitadel.Conf{Issuer: testIssuerURL}) require.NoError(t, err) _, err = client.AuthorizeURL("https://app/callback", "state", "google") require.Error(t, err) require.Contains(t, err.Error(), "oauth client id") _, err = client.AuthorizeURL("", "state", "google") require.Error(t, err) var nilClient *zitadel.Client _, err = nilClient.AuthorizeURL("https://app/callback", "state", "google") require.ErrorIs(t, err, zitadel.ErrNotConfigured) } func TestExchangeAuthorizationCode(t *testing.T) { t.Parallel() var gotForm url.Values srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/oauth/v2/token", r.URL.Path) body, err := io.ReadAll(r.Body) assert.NoError(t, err) gotForm, err = url.ParseQuery(string(body)) assert.NoError(t, err) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"access_token":"at","id_token":"id","expires_in":3600,"token_type":"Bearer"}`)) })) t.Cleanup(srv.Close) client, err := zitadel.NewClient(zitadel.Conf{ Issuer: srv.URL, OAuthClientID: testClientID, OAuthClientSecret: testSecret, }) require.NoError(t, err) tok, err := client.ExchangeAuthorizationCode( context.Background(), "auth-code-1", "https://app.example.com/callback", ) require.NoError(t, err) require.Equal(t, "at", tok.AccessToken) require.Equal(t, "id", tok.IDToken) require.Equal(t, "authorization_code", gotForm.Get("grant_type")) require.Equal(t, testClientID, gotForm.Get("client_id")) require.Equal(t, testSecret, gotForm.Get("client_secret")) require.Equal(t, "auth-code-1", gotForm.Get("code")) require.Equal(t, "https://app.example.com/callback", gotForm.Get("redirect_uri")) } func TestExchangeAuthorizationCodeInvalid(t *testing.T) { t.Parallel() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "invalid", http.StatusUnauthorized) })) t.Cleanup(srv.Close) client, err := zitadel.NewClient(zitadel.Conf{ Issuer: srv.URL, OAuthClientID: testClientID, OAuthClientSecret: testSecret, }) require.NoError(t, err) _, err = client.ExchangeAuthorizationCode(context.Background(), "bad", "https://app/callback") require.ErrorIs(t, err, zitadel.ErrInvalidCredentials) } func TestExchangeAuthorizationCodeRequiresParams(t *testing.T) { t.Parallel() client, err := zitadel.NewClient(zitadel.Conf{ Issuer: testIssuerURL, OAuthClientID: testClientID, OAuthClientSecret: testSecret, }) require.NoError(t, err) _, err = client.ExchangeAuthorizationCode(context.Background(), "", "https://app/callback") require.Error(t, err) var nilClient *zitadel.Client _, err = nilClient.ExchangeAuthorizationCode(context.Background(), "code", "https://app/callback") require.ErrorIs(t, err, zitadel.ErrNotConfigured) } func TestFetchUserInfo(t *testing.T) { t.Parallel() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/oidc/v1/userinfo", r.URL.Path) assert.Equal(t, "Bearer access-token", r.Header.Get("Authorization")) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{ "sub":"zitadel-sub-2", "email":"bob@example.com", "email_verified":true, "name":"Bob", "locale":"zh-tw" }`)) })) t.Cleanup(srv.Close) client, err := zitadel.NewClient(zitadel.Conf{Issuer: srv.URL}) require.NoError(t, err) claims, err := client.FetchUserInfo(context.Background(), "access-token") require.NoError(t, err) require.Equal(t, "zitadel-sub-2", claims.Sub) require.Equal(t, "bob@example.com", claims.Email) require.True(t, claims.EmailVerified) require.Equal(t, "Bob", claims.Name) require.Equal(t, "zh-tw", claims.Locale) } func TestFetchUserInfoUnauthorized(t *testing.T) { t.Parallel() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "invalid", http.StatusUnauthorized) })) t.Cleanup(srv.Close) client, err := zitadel.NewClient(zitadel.Conf{Issuer: srv.URL}) require.NoError(t, err) _, err = client.FetchUserInfo(context.Background(), "bad-token") require.ErrorIs(t, err, zitadel.ErrInvalidCredentials) } func TestFetchUserInfoMissingSub(t *testing.T) { t.Parallel() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"email":"nobody@example.com"}`)) })) t.Cleanup(srv.Close) client, err := zitadel.NewClient(zitadel.Conf{Issuer: srv.URL}) require.NoError(t, err) _, err = client.FetchUserInfo(context.Background(), "access-token") require.Error(t, err) require.Contains(t, err.Error(), "missing sub") } func TestParseIDTokenClaims(t *testing.T) { t.Parallel() payload, err := json.Marshal(map[string]any{ "sub": "sub-1", "email": "alice@example.com", "email_verified": true, "name": "Alice", "locale": "en-us", }) require.NoError(t, err) raw := strings.Join([]string{ base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"RS256"}`)), base64.RawURLEncoding.EncodeToString(payload), "signature", }, ".") claims, err := zitadel.ParseIDTokenClaims(raw) require.NoError(t, err) require.Equal(t, "sub-1", claims.Sub) require.Equal(t, "alice@example.com", claims.Email) require.True(t, claims.EmailVerified) require.Equal(t, "Alice", claims.Name) require.Equal(t, "en-us", claims.Locale) } func TestParseIDTokenClaimsErrors(t *testing.T) { t.Parallel() _, err := zitadel.ParseIDTokenClaims("") require.Error(t, err) _, err = zitadel.ParseIDTokenClaims("not-a-jwt") require.Error(t, err) require.Contains(t, err.Error(), "malformed") payload := base64.RawURLEncoding.EncodeToString([]byte(`{"email":"x@example.com"}`)) raw := "header." + payload + ".sig" _, err = zitadel.ParseIDTokenClaims(raw) require.Error(t, err) require.Contains(t, err.Error(), "missing sub") }