1package provider
2
3import (
4 "context"
5 "net/http"
6 "net/url"
7 "testing"
8 "time"
9)
10
11type mockRoundTripper struct {
12 lastReq *http.Request
13}
14
15func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
16 m.lastReq = req
17 return &http.Response{
18 StatusCode: http.StatusOK,
19 Body: http.NoBody,
20 }, nil
21}
22
23func TestCopilotHostRewrite(t *testing.T) {
24 // Seed the cache with a mock token that carries a non-default proxy endpoint
25 // Ensure the expiresAt time is far in the future so that it bypasses the exchange check
26 const pat = "ghp_mock_token_for_test"
27 copilotCache.mu.Lock()
28 copilotCache.tokens[pat] = copilotToken{
29 value: "mock-jwt-with;proxy-ep=enterprise.copilot.proxy",
30 expiresAt: time.Now().Add(1 * time.Hour),
31 baseURL: "https://api.enterprise.copilot.proxy",
32 }
33 copilotCache.mu.Unlock()
34 t.Cleanup(func() {
35 copilotCache.mu.Lock()
36 delete(copilotCache.tokens, pat)
37 copilotCache.mu.Unlock()
38 })
39
40 mockRT := &mockRoundTripper{}
41 transport := &copilotRefreshTransport{
42 inner: mockRT,
43 pat: pat,
44 }
45
46 reqUrl, err := url.Parse("https://api.individual.githubcopilot.com/chat/completions")
47 if err != nil {
48 t.Fatalf("failed to parse test URL: %v", err)
49 }
50
51 req := &http.Request{
52 Method: "POST",
53 URL: reqUrl,
54 Header: make(http.Header),
55 }
56 req = req.WithContext(context.Background())
57
58 _, err = transport.RoundTrip(req)
59 if err != nil {
60 t.Fatalf("RoundTrip failed: %v", err)
61 }
62
63 if mockRT.lastReq == nil {
64 t.Fatal("RoundTrip did not forward the request")
65 }
66
67 // Verify both URL host and Host header are rewritten correctly to prevent 421 Misdirected Request
68 wantHost := "api.enterprise.copilot.proxy"
69 if mockRT.lastReq.URL.Host != wantHost {
70 t.Errorf("expected URL.Host to be %q, got %q", wantHost, mockRT.lastReq.URL.Host)
71 }
72 if mockRT.lastReq.Host != wantHost {
73 t.Errorf("expected Host header (Request.Host) to be %q, got %q", wantHost, mockRT.lastReq.Host)
74 }
75}
76
77func TestCopilotRoutesModernGPTToResponses(t *testing.T) {
78 client, ok := NewGithubCopilotClient("pat").(*copilotClient)
79 if !ok {
80 t.Fatal("NewGithubCopilotClient did not return a copilotClient")
81 }
82 codex, ok := client.router.byAPI[APIResponses].(*codexClient)
83 if !ok {
84 t.Fatal("github-copilot Responses client is not a codexClient")
85 }
86 const want = "https://api.individual.githubcopilot.com/responses"
87 if codex.baseURL != want {
88 t.Errorf("github-copilot Responses baseURL = %q, want %q", codex.baseURL, want)
89 }
90 for _, id := range []string{"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra"} {
91 m, err := FindModel("github-copilot", id)
92 if err != nil {
93 t.Fatalf("FindModel(%q): %v", id, err)
94 }
95 if m.API != APIResponses {
96 t.Errorf("model %q API = %q, want %q", id, m.API, APIResponses)
97 }
98 }
99}