oficina / memoria Public

Clone
git clone https://amadan.net/oficina/memoria

Plain git — no account needed to clone.

Download

Download this file

1package provider
2
3import (
4 "bufio"
5 "bytes"
6 "context"
7 "crypto/x509"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "io"
12 "log"
13 "net"
14 "net/http"
15 "net/http/httptest"
16 "os"
17 "reflect"
18 "regexp"
19 "strings"
20 "sync/atomic"
21 "testing"
22 "time"
23)
24
25// canary is planted in every response a test scripts. No error this
26// package returns may contain it: a provider's response body is content
27// (gate 31), and a JSON decode error quotes the byte it failed on.
28const canary = "CANARY-7f3a-passage-content"
29
30// stub serves one scripted reply and records what it was sent.
31type stub struct {
32 srv *httptest.Server
33 status int
34 body string
35 delay time.Duration
36 calls atomic.Int32
37 gotReq atomic.Value // chatRequest
38 gotKey atomic.Value // string
39 gotMethod atomic.Value // string
40 gotPath atomic.Value // string
41 // done is closed when the server sees the request's context end —
42 // which is how a test observes cancellation AT THE LISTENER, rather
43 // than trusting the error the client returned.
44 done chan struct{}
45}
46
47func newStub(t *testing.T, status int, body string) *stub {
48 t.Helper()
49 s := &stub{status: status, body: body, done: make(chan struct{})}
50 s.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
51 s.calls.Add(1)
52 s.gotMethod.Store(r.Method)
53 s.gotPath.Store(r.URL.Path)
54 s.gotKey.Store(r.Header.Get("Authorization"))
55 var req chatRequest
56 raw, _ := io.ReadAll(r.Body)
57 _ = json.Unmarshal(raw, &req)
58 s.gotReq.Store(req)
59 if s.delay > 0 {
60 select {
61 case <-time.After(s.delay):
62 case <-r.Context().Done():
63 close(s.done)
64 return
65 }
66 }
67 w.WriteHeader(s.status)
68 _, _ = io.WriteString(w, s.body)
69 }))
70 t.Cleanup(s.srv.Close)
71 return s
72}
73
74func (s *stub) client() *Client {
75 c, err := newClient("test-key", s.srv.URL)
76 if err != nil {
77 panic(err)
78 }
79 return c
80}
81
82// reply wraps model content in an OpenAI-compatible envelope.
83func reply(content, finish string) string {
84 b, _ := json.Marshal(map[string]any{
85 "choices": []map[string]any{{
86 "message": map[string]string{"role": "assistant", "content": content},
87 "finish_reason": finish,
88 }},
89 })
90 return string(b)
91}
92
93// allowedErrors is the complete set of error strings this package may
94// return, built from its own sentinels so it cannot drift from them. A
95// status error is the one parameterised case: the sentinel followed by
96// exactly a three-digit code.
97//
98// **An exact set, not a canary search.** The first version checked only
99// that the full canary was absent, and a review returned the first eight
100// bytes of the response in the status error — "CANARY-7" — and stayed
101// green. A pattern loose enough to admit "non-2xx" would admit lowercase
102// content too; an exact set admits nothing it does not name.
103var allowedErrors = map[string]bool{
104 ErrTooLarge.Error(): true, ErrMalformed.Error(): true, ErrTruncated.Error(): true,
105 ErrForbiddenHost.Error(): true, ErrTransport.Error(): true, ErrMissingKey.Error(): true,
106 context.Canceled.Error(): true, context.DeadlineExceeded.Error(): true,
107}
108
109var statusError = regexp.MustCompile(`^` + regexp.QuoteMeta(ErrStatus.Error()) + ` [0-9]{3}$`)
110
111func assertNoCanary(t *testing.T, err error) {
112 t.Helper()
113 if err != nil && !allowedErrors[err.Error()] && !statusError.MatchString(err.Error()) {
114 t.Errorf("error is not one of the fixed strings, so it may carry content: %q", err.Error())
115 }
116}
117
118// The presence half every negative test below depends on: a well-formed
119// reply is decoded into pointers, and the request is the one §8.9 says.
120func TestExtractAddressesHappyPath(t *testing.T) {
121 s := newStub(t, 200, reply(`{"addresses":[{"passage":1,"text":"12 Main Street, Dublin 2"}]}`, "stop"))
122 got, err := s.client().ExtractAddresses(context.Background(), []string{"first", "At 12 Main Street, Dublin 2 we"})
123 if err != nil {
124 t.Fatalf("extract: %v", err)
125 }
126 if len(got) != 1 || got[0].Passage != 1 || got[0].Text != "12 Main Street, Dublin 2" {
127 t.Fatalf("pointers = %+v", got)
128 }
129
130 if m := s.gotMethod.Load(); m != http.MethodPost {
131 t.Errorf("method = %v, want POST", m)
132 }
133 if p := s.gotPath.Load(); p != "/chat/completions" {
134 t.Errorf("path = %v, want /chat/completions", p)
135 }
136 req := s.gotReq.Load().(chatRequest)
137 if len(req.Messages) != 2 || req.Messages[0].Role != "system" || req.Messages[0].Content != instructions || instructions == "" {
138 t.Errorf("the system message is not the extraction instructions")
139 }
140 if req.Model != Model {
141 t.Errorf("model = %q, want the pinned %q", req.Model, Model)
142 }
143 if req.ResponseFormat["type"] != "json_object" || req.Temperature != 0 {
144 t.Errorf("response_format/temperature = %v/%v", req.ResponseFormat, req.Temperature)
145 }
146 if s.gotKey.Load().(string) != "Bearer test-key" {
147 t.Errorf("authorization header wrong")
148 }
149 // The passages go as indexed data, in the order given — the caller
150 // resolves Pointer.Passage against that order.
151 // Every index and every text, as given — a review blanked passage 0
152 // before serialising and a check of passage 1 alone stayed green.
153 var sent []sentPassage
154 want := []sentPassage{{0, "first"}, {1, "At 12 Main Street, Dublin 2 we"}}
155 if err := json.Unmarshal([]byte(req.Messages[1].Content), &sent); err != nil || !reflect.DeepEqual(sent, want) {
156 t.Errorf("passages sent = %+v, want %+v (err %v)", sent, want, err)
157 }
158}
159
160// "None found" is an empty list, and it is an answer, not an error.
161func TestAnEmptyListIsAnAnswer(t *testing.T) {
162 s := newStub(t, 200, reply(`{"addresses":[]}`, "stop"))
163 got, err := s.client().ExtractAddresses(context.Background(), []string{"x"})
164 if err != nil || len(got) != 0 {
165 t.Fatalf("got %+v, %v; want an empty answer", got, err)
166 }
167}
168
169func TestEveryFailureIsAnErrorAndCarriesNoContent(t *testing.T) {
170 big := `{"addresses":[{"passage":0,"text":"` + strings.Repeat("x", maxResponse) + `"}]}`
171 for _, c := range []struct {
172 name string
173 status int
174 body string
175 want error
176 }{
177 // An OpenAI-compatible 4xx echoes the input that caused it.
178 {"a 400 echoing the input", 400, `{"error":{"message":"bad input: ` + canary + `"}}`, ErrStatus},
179 {"a 500", 500, canary, ErrStatus},
180 // A capped body that parsed as a shorter list is the omission
181 // §8.9 cannot otherwise see; the cap has to be detectable.
182 {"a body past the cap", 200, reply(big, "stop"), ErrTooLarge},
183 // §8.9: a truncated response is rejected WHOLE.
184 {"truncated by the provider", 200, reply(`{"addresses":[{"passage":0,"text":"`+canary+`"}]}`, "length"), ErrTruncated},
185 {"a content filter stop", 200, reply(`{"addresses":[]}`, "content_filter"), ErrMalformed},
186 {"not json at all", 200, reply(canary, "stop"), ErrMalformed},
187 // A json.SyntaxError quotes the byte it choked on.
188 {"broken json quoting content", 200, reply(`{"addresses":[{"passage":0,"text":"`+canary+`}`, "stop"), ErrMalformed},
189 // The steerable parse: a duplicate key smuggles a second list.
190 {"a duplicate key", 200, reply(`{"addresses":[],"ADDRESSES":[{"passage":0,"text":"`+canary+`"}]}`, "stop"), ErrMalformed},
191 {"an unknown field", 200, reply(`{"addresses":[],"note":"`+canary+`"}`, "stop"), ErrMalformed},
192 {"trailing content", 200, reply(`{"addresses":[]}{"addresses":[{"passage":0,"text":"`+canary+`"}]}`, "stop"), ErrMalformed},
193 {"null is not empty", 200, reply(`{"addresses":null}`, "stop"), ErrMalformed},
194 {"a missing field", 200, reply(`{"addresses":[{"text":"`+canary+`"}]}`, "stop"), ErrMalformed},
195 // dec.More() is not an end-of-input check; the first version
196 // accepted both of these.
197 {"a stray closing brace", 200, reply(`{"addresses":[{"passage":0,"text":"`+canary+`"}]}}`, "stop"), ErrMalformed},
198 {"a stray closing bracket", 200, reply(`{"addresses":[]}]`, "stop"), ErrMalformed},
199 // encoding/json folds "ſ" to "s"; strings.ToLower does not. This
200 // decoded as passage 1 through the first version's duplicate check.
201 {"a long-s duplicate", 200, reply(`{"addresses":[{"passage":0,"pa\u017f\u017fage":1,"text":"`+canary+`"}]}`, "stop"), ErrMalformed},
202 {"a differently-cased key", 200, reply(`{"addresses":[{"PASSAGE":0,"text":"`+canary+`"}]}`, "stop"), ErrMalformed},
203 // The envelope, not the content: the last duplicate wins, which
204 // would turn a truncation into a clean stop.
205 {"a duplicate finish_reason", 200, `{"choices":[{"message":{"content":"{\"addresses\":[]}"},"finish_reason":"length","finish_reason":"stop"}]}`, ErrMalformed},
206 {"two choices", 200, `{"choices":[{"message":{"content":"{\"addresses\":[]}"},"finish_reason":"stop"},{"message":{"content":"{\"addresses\":[]}"},"finish_reason":"stop"}]}`, ErrMalformed},
207 } {
208 t.Run(c.name, func(t *testing.T) {
209 s := newStub(t, c.status, c.body)
210 got, err := s.client().ExtractAddresses(context.Background(), []string{"x"})
211 if !errors.Is(err, c.want) {
212 t.Fatalf("err = %v, want %v (got %+v)", err, c.want, got)
213 }
214 if got != nil {
215 t.Errorf("returned %+v alongside an error; a failure yields nothing", got)
216 }
217 assertNoCanary(t, err)
218 })
219 }
220}
221
222// §6.5's deadline, reused. A slow provider must not hold the page.
223func TestTheDeadlineHolds(t *testing.T) {
224 // Against a literal, not against Deadline itself: comparing the
225 // elapsed time to Deadline+slack passed when a review raised the
226 // constant to two seconds.
227 if Deadline != 800*time.Millisecond {
228 t.Fatalf("Deadline = %v; §8.9 and §6.5 say 800ms", Deadline)
229 }
230 s := newStub(t, 200, reply(`{"addresses":[]}`, "stop"))
231 s.delay = 5 * time.Second
232 start := time.Now()
233 _, err := s.client().ExtractAddresses(context.Background(), []string{"x"})
234 // DeadlineExceeded specifically: the store logs a deadline and stays
235 // silent on a cancellation, so a deadline reported as Canceled is an
236 // outage nobody hears about. A review swapped them and this stayed
237 // green while it checked only err != nil.
238 if !errors.Is(err, context.DeadlineExceeded) {
239 t.Fatalf("err = %v, want context.DeadlineExceeded", err)
240 }
241 if took := time.Since(start); took > 1200*time.Millisecond {
242 t.Errorf("took %v; the deadline is 800ms", took)
243 }
244}
245
246// An abandoned keystroke cancels the call rather than paying for it.
247func TestCancellationReachesTheProvider(t *testing.T) {
248 s := newStub(t, 200, reply(`{"addresses":[]}`, "stop"))
249 s.delay = 5 * time.Second
250 ctx, cancel := context.WithCancel(context.Background())
251 go func() { time.Sleep(50 * time.Millisecond); cancel() }()
252 start := time.Now()
253 if _, err := s.client().ExtractAddresses(ctx, []string{"x"}); !errors.Is(err, context.Canceled) {
254 t.Fatalf("err = %v, want context.Canceled", err)
255 }
256 if took := time.Since(start); took > 400*time.Millisecond {
257 t.Errorf("took %v after cancel", took)
258 }
259 // **At the listener.** The returned error proved only that the CLIENT
260 // stopped waiting; a review detached the request so it kept running in
261 // the background and this test stayed green. The server seeing its
262 // request context end is what shows the provider stopped too.
263 select {
264 case <-s.done:
265 case <-time.After(time.Second):
266 t.Error("the provider never saw the request cancelled; it is still paying for an abandoned keystroke")
267 }
268}
269
270// **What pays for this package's exemption from the outbound-network
271// gate.** Decision 13 makes TensorX the only provider; this makes the
272// client unable to reach anything else, whatever its base URL says — a
273// mis-set constant, a future config field, a redirect.
274//
275// Each subtest counts requests at the stub, so "refused" means nothing
276// reached the listener, and not merely that an error came back from
277// somewhere after a request had already left.
278//
279// The different-host case uses "localhost" against a server pinned to
280// "127.0.0.1": same scheme, same port, a live listener, so ONLY the host
281// pin can refuse it. notion's first version of this test used an address
282// that also failed the scheme check, and deleting the host pin left it
283// green — a neighbouring error path supplying the failure.
284func TestClientDialsOnlyTensorX(t *testing.T) {
285 t.Run("New is pinned to TensorX, https, port 443, no proxy", func(t *testing.T) {
286 c, err := New("k")
287 if err != nil {
288 t.Fatal(err)
289 }
290 pin := c.hc.Transport.(*pinnedTransport)
291 if c.base != BaseURL || pin.scheme != "https" || pin.authority != "api.tensorx.ai:443" {
292 t.Errorf("New dials %q pinned to %s://%s", c.base, pin.scheme, pin.authority)
293 }
294 // Structural, because behaviour cannot show it here: Go never
295 // proxies a request to 127.0.0.1, so an httptest server cannot
296 // tell a proxy-honouring transport from this one. A review showed
297 // the production constructor sending CONNECT to an HTTPS_PROXY
298 // listener while the host pin passed.
299 if tr, ok := pin.next.(*http.Transport); !ok || tr.Proxy != nil {
300 t.Errorf("the transport honours environment proxies: %T", pin.next)
301 }
302 })
303
304 t.Run("New refuses an empty credential", func(t *testing.T) {
305 if _, err := New(""); !errors.Is(err, ErrMissingKey) {
306 t.Errorf("err = %v, want ErrMissingKey", err)
307 }
308 })
309
310 // The first version pinned the host alone; another listener on the
311 // same host took the body, and ":8443" in BaseURL left every test green.
312 t.Run("another port on the same host is refused before dialling", func(t *testing.T) {
313 s := newStub(t, 200, reply(`{"addresses":[]}`, "stop"))
314 other := newStub(t, 200, reply(`{"addresses":[]}`, "stop"))
315 c := s.client()
316 c.base = other.srv.URL // same 127.0.0.1, different port
317 if _, err := c.ExtractAddresses(context.Background(), []string{"x"}); !errors.Is(err, ErrForbiddenHost) {
318 t.Fatalf("err = %v, want ErrForbiddenHost", err)
319 }
320 if n := other.calls.Load() + s.calls.Load(); n != 0 {
321 t.Errorf("%d requests arrived; the passages left", n)
322 }
323 })
324
325 t.Run("userinfo in the URL is refused", func(t *testing.T) {
326 s := newStub(t, 200, reply(`{"addresses":[]}`, "stop"))
327 c := s.client()
328 c.base = strings.Replace(s.srv.URL, "http://", "http://user:pw@", 1)
329 if _, err := c.ExtractAddresses(context.Background(), []string{"x"}); !errors.Is(err, ErrForbiddenHost) {
330 t.Fatalf("err = %v, want ErrForbiddenHost", err)
331 }
332 })
333
334 t.Run("another host is refused before dialling", func(t *testing.T) {
335 s := newStub(t, 200, reply(`{"addresses":[]}`, "stop"))
336 c := s.client()
337 c.base = strings.Replace(s.srv.URL, "127.0.0.1", "localhost", 1)
338 if c.base == s.srv.URL {
339 t.Fatal("could not build a different-host URL; this subtest proves nothing")
340 }
341 if _, err := c.ExtractAddresses(context.Background(), []string{"x"}); !errors.Is(err, ErrForbiddenHost) {
342 t.Fatalf("err = %v, want ErrForbiddenHost", err)
343 }
344 if n := s.calls.Load(); n != 0 {
345 t.Errorf("the stub received %d requests; the passages left", n)
346 }
347 })
348
349 t.Run("another scheme is refused before dialling", func(t *testing.T) {
350 s := newStub(t, 200, reply(`{"addresses":[]}`, "stop"))
351 c := s.client()
352 c.base = strings.Replace(s.srv.URL, "http://", "https://", 1)
353 if _, err := c.ExtractAddresses(context.Background(), []string{"x"}); !errors.Is(err, ErrForbiddenHost) {
354 t.Fatalf("err = %v, want ErrForbiddenHost", err)
355 }
356 if n := s.calls.Load(); n != 0 {
357 t.Errorf("the stub received %d requests", n)
358 }
359 })
360
361 // A redirect is refused outright, even to the same host: following it
362 // would resend the body — every passage — and, same-host, the key.
363 t.Run("a redirect is refused", func(t *testing.T) {
364 var hits atomic.Int32
365 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
366 if hits.Add(1) == 1 {
367 // The Location is provider-chosen bytes. A review returned
368 // this exact shape and the first version's error quoted it.
369 http.Redirect(w, r, "/"+canary, http.StatusTemporaryRedirect)
370 return
371 }
372 _, _ = io.WriteString(w, reply(`{"addresses":[]}`, "stop"))
373 }))
374 t.Cleanup(srv.Close)
375 c, err := newClient("k", srv.URL)
376 if err != nil {
377 t.Fatal(err)
378 }
379 _, err = c.ExtractAddresses(context.Background(), []string{"x"})
380 if !errors.Is(err, ErrForbiddenHost) {
381 t.Fatalf("err = %v, want ErrForbiddenHost", err)
382 }
383 assertNoCanary(t, err)
384 if n := hits.Load(); n != 1 {
385 t.Errorf("server saw %d requests; the redirect was followed", n)
386 }
387 })
388}
389
390// A response the HTTP layer cannot parse is a transport failure, and the
391// bytes it choked on are not in the error. Go's own header parser quotes
392// what it failed on; a review planted a canary in a malformed header and
393// the first version returned it. Served from a raw socket, because an
394// http.Server will not write a malformed header.
395func TestAMalformedResponseCarriesNoContent(t *testing.T) {
396 ln, err := net.Listen("tcp", "127.0.0.1:0")
397 if err != nil {
398 t.Fatal(err)
399 }
400 t.Cleanup(func() { ln.Close() })
401 go func() {
402 conn, err := ln.Accept()
403 if err != nil {
404 return
405 }
406 defer conn.Close()
407 // Consume the request line and headers, then answer badly.
408 br := bufio.NewReader(conn)
409 for {
410 line, err := br.ReadString('\n')
411 if err != nil || line == "\r\n" {
412 break
413 }
414 }
415 _, _ = conn.Write([]byte("HTTP/1.1 200 OK\r\n" + canary + " no colon here\r\n\r\n"))
416 }()
417 c, err := newClient("k", "http://"+ln.Addr().String())
418 if err != nil {
419 t.Fatal(err)
420 }
421 _, err = c.ExtractAddresses(context.Background(), []string{"x"})
422 if !errors.Is(err, ErrTransport) {
423 t.Fatalf("err = %v, want ErrTransport", err)
424 }
425 assertNoCanary(t, err)
426}
427
428// rejectAmbiguous is called DIRECTLY here, because through
429// ExtractAddresses two of its checks are unreachable: the content's
430// exact-key check rejects "paſſage" before the duplicate check runs, and
431// decodeAddresses has its own end-of-input check. Mutating either inside
432// rejectAmbiguous left every other test green (found 2026-09-27). They
433// still matter — for the ENVELOPE, whose keys are not enumerated — so
434// they are pinned where they can be reached (AGENTS.md: make a defence
435// reachable or delete it).
436func TestRejectAmbiguousDirectly(t *testing.T) {
437 for _, c := range []struct {
438 name, in string
439 exact map[string]bool
440 ok bool
441 }{
442 {"a clean document", `{"a":1,"b":[{"c":2}]}`, nil, true},
443 {"a stray closing brace", `{"a":1}}`, nil, false},
444 {"a stray closing bracket", `{"a":1}]`, nil, false},
445 {"two documents", `{"a":1} {"a":2}`, nil, false},
446 {"a plain duplicate", `{"a":1,"a":2}`, nil, false},
447 {"a case duplicate", `{"a":1,"A":2}`, nil, false},
448 // Go's decoder folds ſ to s; strings.ToLower does not. With no
449 // exact-key list — the envelope's case — only EqualFold sees it.
450 {"a long-s duplicate", `{"finish_reason":"length","finiſh_reaſon":"stop"}`, nil, false},
451 {"a nested long-s duplicate", `{"x":{"passage":0,"paſſage":1}}`, nil, false},
452 {"an exact key", `{"text":"a"}`, contentKeys, true},
453 {"a differently-cased key", `{"TEXT":"a"}`, contentKeys, false},
454 } {
455 if err := rejectAmbiguous([]byte(c.in), c.exact); (err == nil) != c.ok {
456 t.Errorf("%s: err = %v, want ok=%v", c.name, err, c.ok)
457 }
458 }
459}
460
461// And the long-s duplicate end to end in the envelope, where it would
462// otherwise turn a truncation into a clean stop.
463func TestALongSEnvelopeDuplicateIsRefused(t *testing.T) {
464 body := `{"choices":[{"message":{"content":"{\"addresses\":[]}"},"finish_reason":"length","finiſh_reaſon":"stop"}]}`
465 s := newStub(t, 200, body)
466 if _, err := s.client().ExtractAddresses(context.Background(), []string{"x"}); !errors.Is(err, ErrMalformed) {
467 t.Errorf("err = %v, want ErrMalformed: the duplicate would have decoded as a clean stop", err)
468 }
469}
470
471// **The standard library's own logger is a content path too.** net/http
472// reads idle pooled connections, and bytes arriving on one unsolicited
473// are reported with log.Printf, quoting them — bypassing every sanitised
474// error in this package. A review reproduced it over real TLS: a valid
475// response, then passage text on the same connection, then that text on
476// stderr. The client now disables keep-alives, so there is no idle
477// connection to read.
478//
479// Captures the STANDARD logger, because that is the channel. Served from
480// a raw socket that answers correctly and then keeps talking.
481func TestUnsolicitedBytesDoNotReachTheStandardLogger(t *testing.T) {
482 var logged bytes.Buffer
483 log.SetOutput(&logged)
484 t.Cleanup(func() { log.SetOutput(os.Stderr) })
485
486 ln, err := net.Listen("tcp", "127.0.0.1:0")
487 if err != nil {
488 t.Fatal(err)
489 }
490 t.Cleanup(func() { ln.Close() })
491 served := make(chan struct{})
492 go func() {
493 conn, err := ln.Accept()
494 if err != nil {
495 return
496 }
497 defer conn.Close()
498 br := bufio.NewReader(conn)
499 for {
500 line, err := br.ReadString('\n')
501 if err != nil || line == "\r\n" {
502 break
503 }
504 }
505 body := reply(`{"addresses":[]}`, "stop")
506 fmt.Fprintf(conn, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s", len(body), body)
507 // A moment for the client to finish and pool the connection, then
508 // talk unasked.
509 time.Sleep(50 * time.Millisecond)
510 _, _ = conn.Write([]byte(canary + " unsolicited team content"))
511 time.Sleep(150 * time.Millisecond)
512 close(served)
513 }()
514
515 c, err := newClient("k", "http://"+ln.Addr().String())
516 if err != nil {
517 t.Fatal(err)
518 }
519 if _, err := c.ExtractAddresses(context.Background(), []string{"x"}); err != nil {
520 t.Fatalf("the valid response failed: %v", err)
521 }
522 <-served
523 time.Sleep(50 * time.Millisecond) // let any read loop run
524 if strings.Contains(logged.String(), canary) {
525 t.Errorf("the standard logger carries provider bytes: %q", logged.String())
526 }
527}
528
529// **An untrusted certificate receives nothing, and a trusted one works.**
530// A review set InsecureSkipVerify on the transport and every test stayed
531// green, because every stub spoke plain HTTP. Both halves: without the
532// trusted half, a client that refused ALL TLS would pass.
533func TestTLSIsVerified(t *testing.T) {
534 var hits atomic.Int32
535 srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
536 hits.Add(1)
537 _, _ = io.WriteString(w, reply(`{"addresses":[]}`, "stop"))
538 }))
539 t.Cleanup(srv.Close)
540
541 untrusted, err := newClient("k", srv.URL) // system roots: the test CA is not among them
542 if err != nil {
543 t.Fatal(err)
544 }
545 if _, err := untrusted.ExtractAddresses(context.Background(), []string{"x"}); !errors.Is(err, ErrTransport) {
546 t.Fatalf("err = %v, want ErrTransport", err)
547 }
548 if n := hits.Load(); n != 0 {
549 t.Fatalf("%d requests reached a server whose certificate is not trusted", n)
550 }
551
552 pool := x509.NewCertPool()
553 pool.AddCert(srv.Certificate())
554 trusted, err := newClientWithRoots("k", srv.URL, pool)
555 if err != nil {
556 t.Fatal(err)
557 }
558 if _, err := trusted.ExtractAddresses(context.Background(), []string{"x"}); err != nil {
559 t.Fatalf("a trusted certificate failed: %v", err)
560 }
561 if n := hits.Load(); n != 1 {
562 t.Errorf("hits = %d, want 1", n)
563 }
564}
565