Files
Nuno do Carmo febdc530e1 Feat/copilot login (#49)
* feat(auth): add Copilot login

Add experimental GitHub Copilot device login and copilot/* provider support for users with Copilot access but no OpenAI account.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(copilot): use responses for GPT-5

Route Copilot GPT-5 models through the Responses API because gpt-5.5 is not available on /chat/completions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(copilot): honor device flow timing

* docs(copilot): add auth helper docstrings

* fix(auth): address copilot review feedback

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-08 00:21:20 +03:00

125 lines
3.6 KiB
Go

package auth
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestCopilotStartDeviceFlow(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Fatalf("expected POST, got %s", r.Method)
}
if err := r.ParseForm(); err != nil {
t.Fatalf("ParseForm failed: %v", err)
}
if r.Form.Get("client_id") != "client-id" {
t.Fatalf("expected client id, got %q", r.Form.Get("client_id"))
}
if r.Form.Get("scope") != "read:user" {
t.Fatalf("expected scope, got %q", r.Form.Get("scope"))
}
_ = json.NewEncoder(w).Encode(map[string]any{
"device_code": "device-code",
"user_code": "USER-CODE",
"verification_uri": "https://github.com/login/device",
"expires_in": 600,
"interval": 1,
})
}))
defer server.Close()
client := NewCopilotOAuthClient()
client.ClientID = "client-id"
client.DeviceURL = server.URL
code, err := client.StartDeviceFlow(context.Background())
if err != nil {
t.Fatalf("StartDeviceFlow failed: %v", err)
}
if code.DeviceCode != "device-code" || code.UserCode != "USER-CODE" || code.Interval != 1 {
t.Fatalf("unexpected device code: %#v", code)
}
}
func TestCopilotPollDeviceToken(t *testing.T) {
polls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
polls++
if r.Method != http.MethodPost {
t.Fatalf("expected POST, got %s", r.Method)
}
if err := r.ParseForm(); err != nil {
t.Fatalf("ParseForm failed: %v", err)
}
if r.Form.Get("grant_type") != "urn:ietf:params:oauth:grant-type:device_code" {
t.Fatalf("unexpected grant type: %q", r.Form.Get("grant_type"))
}
if polls == 1 {
_ = json.NewEncoder(w).Encode(map[string]any{"error": "authorization_pending"})
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"access_token": "github-token"})
}))
defer server.Close()
client := NewCopilotOAuthClient()
client.ClientID = "client-id"
client.TokenURL = server.URL
client.PollTimeout = 5 * time.Second
client.ClientTimeout = time.Second
token, err := client.PollDeviceToken(context.Background(), &CopilotDeviceCode{
DeviceCode: "device-code",
ExpiresIn: 10,
Interval: 1,
})
if err != nil {
t.Fatalf("PollDeviceToken failed: %v", err)
}
if token != "github-token" {
t.Fatalf("expected github-token, got %q", token)
}
if polls != 2 {
t.Fatalf("expected 2 polls, got %d", polls)
}
}
func TestCopilotRefreshToken(t *testing.T) {
expiresAt := time.Now().Add(time.Hour).Unix()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Fatalf("expected GET, got %s", r.Method)
}
if r.Header.Get("Authorization") != "token github-token" {
t.Fatalf("unexpected authorization header: %q", r.Header.Get("Authorization"))
}
if r.Header.Get("User-Agent") != "kit" {
t.Fatalf("unexpected user agent: %q", r.Header.Get("User-Agent"))
}
_ = json.NewEncoder(w).Encode(map[string]any{
"token": "copilot-token",
"expires_at": expiresAt,
})
}))
defer server.Close()
client := NewCopilotOAuthClient()
client.CopilotURL = server.URL
creds, err := client.RefreshCopilotToken(context.Background(), "github-token")
if err != nil {
t.Fatalf("RefreshCopilotToken failed: %v", err)
}
if creds.GitHubToken != "github-token" || creds.CopilotAccessToken != "copilot-token" {
t.Fatalf("unexpected credentials: %#v", creds)
}
if creds.ExpiresAt != expiresAt {
t.Fatalf("expected expires_at %d, got %d", expiresAt, creds.ExpiresAt)
}
}