v0.1.10 8a38171

auth_test.go RawHistoryFolder

1package auth
2
3import (
4	"context"
5	"net"
6	"net/http"
7	"net/url"
8	"path/filepath"
9	"strings"
10	"testing"
11	"time"
12)
13
14func TestStoreRoundTrip(t *testing.T) {
15	dir := t.TempDir()
16	s := NewStore(filepath.Join(dir, "auth.json"))
17
18	if err := s.SetAPIKey("anthropic", "sk-ant-xyz"); err != nil {
19		t.Fatal(err)
20	}
21	c, err := s.Load()
22	if err != nil {
23		t.Fatal(err)
24	}
25	if c.Anthropic.APIKey != "sk-ant-xyz" {
26		t.Fatalf("got %+v", c)
27	}
28	if !c.Has("anthropic") || c.Method("anthropic") != "apikey" {
29		t.Fatalf("method mismatch: %q", c.Method("anthropic"))
30	}
31
32	tok := OAuthToken{AccessToken: "tok", RefreshToken: "ref", Expiry: time.Now().Add(1 * time.Hour)}
33	if err := s.SetOAuth("openai", tok); err != nil {
34		t.Fatal(err)
35	}
36	c, _ = s.Load()
37	if c.OpenAI.OAuth == nil || c.OpenAI.OAuth.AccessToken != "tok" {
38		t.Fatalf("got %+v", c.OpenAI)
39	}
40	if c.Method("openai") != "oauth" {
41		t.Fatalf("method=%q", c.Method("openai"))
42	}
43
44	if err := s.Clear("anthropic"); err != nil {
45		t.Fatal(err)
46	}
47	c, _ = s.Load()
48	if c.Has("anthropic") {
49		t.Fatalf("expected cleared, got %+v", c.Anthropic)
50	}
51}
52
53func TestPKCE(t *testing.T) {
54	p, err := NewPKCE()
55	if err != nil {
56		t.Fatal(err)
57	}
58	if len(p.Verifier) < 40 || len(p.Challenge) < 40 {
59		t.Fatalf("short pkce: %+v", p)
60	}
61	if p.Verifier == p.Challenge {
62		t.Fatal("verifier == challenge")
63	}
64}
65
66func TestAuthorizeURL(t *testing.T) {
67	p, _ := NewPKCE()
68	u, state, err := AnthropicOAuth.AuthorizeURL(p)
69	if err != nil {
70		t.Fatal(err)
71	}
72	parsed, err := url.Parse(u)
73	if err != nil {
74		t.Fatal(err)
75	}
76	q := parsed.Query()
77	if q.Get("client_id") != AnthropicOAuth.ClientID {
78		t.Fatal("missing client_id")
79	}
80	if q.Get("code_challenge_method") != "S256" {
81		t.Fatal("wrong challenge method")
82	}
83	// anthropic uses state == verifier
84	if q.Get("state") != p.Verifier {
85		t.Fatalf("state mismatch: %q vs verifier %q", q.Get("state"), p.Verifier)
86	}
87	if state != p.Verifier {
88		t.Fatalf("returned state != verifier")
89	}
90	if q.Get("redirect_uri") != AnthropicOAuth.RedirectURI() {
91		t.Fatalf("redirect_uri=%q", q.Get("redirect_uri"))
92	}
93	if q.Get("code") != "true" {
94		t.Fatalf("missing code=true extra arg")
95	}
96}
97
98func TestOpenAIAuthorizeURL(t *testing.T) {
99	p, _ := NewPKCE()
100	u, state, err := OpenAIOAuth.AuthorizeURL(p)
101	if err != nil {
102		t.Fatal(err)
103	}
104	parsed, _ := url.Parse(u)
105	q := parsed.Query()
106	if q.Get("redirect_uri") != "http://localhost:1455/auth/callback" {
107		t.Fatalf("redirect_uri=%q", q.Get("redirect_uri"))
108	}
109	if state == p.Verifier {
110		t.Fatal("openai state should be random, not verifier")
111	}
112	if q.Get("originator") != "zot" {
113		t.Fatalf("originator=%q", q.Get("originator"))
114	}
115}
116
117func TestServerIndexAndAPIKey(t *testing.T) {
118	s, err := NewServer()
119	if err != nil {
120		t.Fatal(err)
121	}
122	defer s.Shutdown(context.Background())
123
124	// Swap the probe out for a fake that always succeeds.
125	s.probeFn = func(ctx context.Context, provider, key string) error { return nil }
126
127	resp, err := http.Get(s.URL() + "/")
128	if err != nil {
129		t.Fatal(err)
130	}
131	defer resp.Body.Close()
132	if resp.StatusCode != 200 {
133		t.Fatalf("index status %d", resp.StatusCode)
134	}
135
136	resp, err = http.Get(s.URL() + "/apikey?provider=anthropic")
137	if err != nil {
138		t.Fatal(err)
139	}
140	resp.Body.Close()
141	if resp.StatusCode != 200 {
142		t.Fatalf("apikey form status %d", resp.StatusCode)
143	}
144
145	// POST a fake key and expect a success result on the channel.
146	_, err = http.PostForm(s.URL()+"/apikey", url.Values{
147		"provider": {"anthropic"},
148		"api_key":  {"sk-ant-test"},
149	})
150	if err != nil {
151		t.Fatal(err)
152	}
153	select {
154	case r := <-s.Result():
155		if r.Err != nil {
156			t.Fatalf("unexpected err: %v", r.Err)
157		}
158		if r.Provider != "anthropic" || r.APIKey != "sk-ant-test" || r.Method != "apikey" {
159			t.Fatalf("bad result: %+v", r)
160		}
161	case <-time.After(2 * time.Second):
162		t.Fatal("no result")
163	}
164}
165
166func TestServerCallback(t *testing.T) {
167	s, err := NewServer()
168	if err != nil {
169		t.Fatal(err)
170	}
171	defer s.Shutdown(context.Background())
172
173	u := s.URL() + "/callback?code=abc123&state=anthropic:xyz"
174	resp, err := http.Get(u)
175	if err != nil {
176		t.Fatal(err)
177	}
178	resp.Body.Close()
179
180	select {
181	case r := <-s.Result():
182		if r.Provider != "anthropic" || r.Code != "abc123" || r.State != "anthropic:xyz" || r.Method != "oauth" {
183			t.Fatalf("bad result: %+v", r)
184		}
185	case <-time.After(2 * time.Second):
186		t.Fatal("no result")
187	}
188}
189
190func TestCallbackServerRoundTrip(t *testing.T) {
191	// Use a provider config with an ephemeral port so tests stay hermetic.
192	op := OAuthProvider{
193		Name:         "anthropic",
194		RedirectHost: "127.0.0.1",
195		RedirectPort: 0, // let kernel pick
196		RedirectPath: "/callback",
197	}
198	// We need a real fixed port for CallbackServer.NewCallbackServer; find one.
199	l, err := net.Listen("tcp", "127.0.0.1:0")
200	if err != nil {
201		t.Fatal(err)
202	}
203	port := l.Addr().(*net.TCPAddr).Port
204	l.Close()
205	op.RedirectPort = port
206
207	cs, err := NewCallbackServer(op, "thestate")
208	if err != nil {
209		t.Fatal(err)
210	}
211	defer cs.Shutdown()
212
213	go func() {
214		time.Sleep(50 * time.Millisecond)
215		_, _ = http.Get(cs.URL() + "?code=abc&state=thestate")
216	}()
217	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
218	defer cancel()
219	r, err := cs.Result(ctx)
220	if err != nil {
221		t.Fatal(err)
222	}
223	if r.Code != "abc" || r.State != "thestate" {
224		t.Fatalf("bad result: %+v", r)
225	}
226}
227
228func TestCallbackServerStateMismatch(t *testing.T) {
229	l, _ := net.Listen("tcp", "127.0.0.1:0")
230	port := l.Addr().(*net.TCPAddr).Port
231	l.Close()
232	op := OAuthProvider{Name: "anthropic", RedirectHost: "127.0.0.1", RedirectPort: port, RedirectPath: "/callback"}
233	cs, err := NewCallbackServer(op, "expected")
234	if err != nil {
235		t.Fatal(err)
236	}
237	defer cs.Shutdown()
238
239	go func() {
240		time.Sleep(50 * time.Millisecond)
241		_, _ = http.Get(cs.URL() + "?code=abc&state=wrong")
242	}()
243	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
244	defer cancel()
245	r, err := cs.Result(ctx)
246	if err != nil {
247		t.Fatal(err)
248	}
249	if r.Err == nil || !strings.Contains(r.Err.Error(), "state") {
250		t.Fatalf("want state error, got %+v", r)
251	}
252}
253
254func TestServerCallbackError(t *testing.T) {
255	s, err := NewServer()
256	if err != nil {
257		t.Fatal(err)
258	}
259	defer s.Shutdown(context.Background())
260
261	_, _ = http.Get(s.URL() + "/callback?error=access_denied&state=openai:xyz")
262	select {
263	case r := <-s.Result():
264		if r.Err == nil || r.Provider != "openai" {
265			t.Fatalf("bad result: %+v", r)
266		}
267		if !strings.Contains(r.Err.Error(), "access_denied") {
268			t.Fatalf("missing reason: %v", r.Err)
269		}
270	case <-time.After(2 * time.Second):
271		t.Fatal("no result")
272	}
273}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close