Files
gitea-tea/internal/auth/oauth_test.go
T
bircni 88b91faa54 refactor: add credential and Git packages (#1123)
- `internal/auth`: credentials in the OS keyring or a private file, OAuth login with token refresh, SSH HTTP-signature signers (key files with passphrase prompt, ssh-agent keys and certificates), and the Git credential helper protocol.
- `internal/git`: cancellable Git subprocesses, repository and remote discovery (tracked remote via `branch.<name>.remote`), PR checkout by full ref, AGit push, branch cleanup.

Nothing imports these packages yet.

Reviewed-on: https://gitea.com/gitea/tea/pulls/1123
Reviewed-by: TheFox0x7 <95654+thefox0x7@noreply.gitea.com>
2026-10-11 17:09:37 +00:00

139 lines
5.3 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"sync"
"testing"
"time"
"gitea.dev/tea/internal/settings"
"github.com/stretchr/testify/require"
)
func TestOAuthLoginPKCEAndState(t *testing.T) {
var redirect, challenge string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/base/login/oauth/access_token", r.URL.Path)
require.NoError(t, r.ParseForm())
require.Equal(t, "authorization_code", r.Form.Get("grant_type"))
require.Equal(t, "valid-code", r.Form.Get("code"))
require.NotEmpty(t, r.Form.Get("code_verifier"))
require.Equal(t, redirect, r.Form.Get("redirect_uri"))
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"access_token": "access", "refresh_token": "refresh", "expires_in": 3600, "token_type": "Bearer"})
}))
defer server.Close()
credential, err := OAuthLogin(context.Background(), OAuthOptions{Instance: server.URL + "/base", ClientID: "configured-client", HTTP: server.Client(), Timeout: time.Second, Authorize: func(ctx context.Context, raw string) error {
u, err := url.Parse(raw)
if err != nil {
return err
}
require.Equal(t, "configured-client", u.Query().Get("client_id"))
require.Equal(t, "S256", u.Query().Get("code_challenge_method"))
redirect = u.Query().Get("redirect_uri")
callback, err := url.Parse(redirect)
require.NoError(t, err)
require.Equal(t, "127.0.0.1", callback.Hostname())
// Gitea's built-in tea application registers only the bare loopback URL.
require.Empty(t, callback.Path)
challenge = u.Query().Get("code_challenge")
require.NotEmpty(t, challenge)
// An unrelated callback cannot consume or satisfy the authorization flow.
response, err := http.Get(redirect + "?code=invalid&state=wrong")
if err != nil {
return err
}
response.Body.Close()
require.Equal(t, 400, response.StatusCode)
response, err = http.Get(redirect + "?code=valid-code&state=" + url.QueryEscape(u.Query().Get("state")))
if err != nil {
return err
}
response.Body.Close()
return nil
}})
require.NoError(t, err)
require.Equal(t, "access", credential.AccessToken)
require.Equal(t, "refresh", credential.RefreshToken)
}
func TestRefreshSerializesAndRetainsRefreshToken(t *testing.T) {
config := settings.New(t.TempDir())
keyring := &memoryKeyring{values: map[string]string{}}
credentials := &Store{Dir: config.Dir, Keyring: keyring}
manager := newTestManager(t, config, credentials)
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
require.NoError(t, r.ParseForm())
require.Equal(t, "old-refresh", r.Form.Get("refresh_token"))
require.Equal(t, "client", r.Form.Get("client_id"))
w.Header().Set("Content-Type", "application/json")
_, _ = fmt.Fprintln(w, `{"access_token":"new-access","expires_in":3600,"token_type":"Bearer"}`)
}))
defer server.Close()
now := time.Now()
require.NoError(t, manager.Register(server.URL, "alice", "file", "oauth", HostOptions{OAuthClientID: "client"}, Credential{AccessToken: "old-access", RefreshToken: "old-refresh", ExpiresAt: now.Add(-time.Minute)}))
var wg sync.WaitGroup
results := make(chan Credential, 8)
failures := make(chan error, 8)
for range 8 {
wg.Go(func() {
c, err := manager.Refresh(context.Background(), server.URL, "alice", server.Client(), false, now)
results <- c
failures <- err
})
}
wg.Wait()
close(results)
close(failures)
for err := range failures {
require.NoError(t, err)
}
for credential := range results {
require.Equal(t, "new-access", credential.AccessToken)
require.Equal(t, "old-refresh", credential.RefreshToken)
}
require.Equal(t, 1, requests)
}
func TestOAuthWithoutHTTPClientUsesDefaultClient(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = fmt.Fprintln(w, `{"access_token":"access","refresh_token":"refresh","expires_in":3600,"token_type":"Bearer"}`)
}))
defer server.Close()
credential, err := OAuthLogin(context.Background(), OAuthOptions{Instance: server.URL, Timeout: time.Second, Authorize: func(_ context.Context, raw string) error {
u, err := url.Parse(raw)
if err != nil {
return err
}
response, err := http.Get(u.Query().Get("redirect_uri") + "?code=code&state=" + url.QueryEscape(u.Query().Get("state")))
if err != nil {
return err
}
return response.Body.Close()
}})
require.NoError(t, err)
require.Equal(t, "access", credential.AccessToken)
config := settings.New(t.TempDir())
manager := newTestManager(t, config, &Store{Dir: config.Dir})
require.NoError(t, manager.Register(server.URL, "alice", "file", "oauth", HostOptions{}, Credential{AccessToken: "old", RefreshToken: "refresh", ExpiresAt: time.Now().Add(-time.Minute)}))
credential, err = manager.Refresh(context.Background(), server.URL, "alice", nil, false, time.Now())
require.NoError(t, err)
require.Equal(t, "access", credential.AccessToken)
}
func TestRefreshEndsBeforeSettingsLockWaitersGiveUp(t *testing.T) {
// A refresh holds the settings lock, so concurrent tea processes must wait longer than it can take.
require.Less(t, refreshTimeout, settings.LockTimeout)
}