Files
promptkit/internal/usecase/smoke_authentication_test.go

311 lines
11 KiB
Go

package usecase
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
"testing"
"go.maximumdirect.net/promptkit/internal/domain"
)
type measuredAuthenticationBody struct {
reader io.Reader
read int
closed bool
}
func (b *measuredAuthenticationBody) Read(p []byte) (int, error) {
n, err := b.reader.Read(p)
b.read += n
return n, err
}
func (b *measuredAuthenticationBody) Close() error { b.closed = true; return nil }
func TestAuthenticationResponseRecognitionBound(t *testing.T) {
const maxAuthenticationResponseBytes = 1 << 20
valid := `{"data":{"label":"member","is_free_tier":false,"usage":0}}`
for _, test := range []struct {
name string
size int
declared int64
status int
overflow bool
}{
{"exact limit", maxAuthenticationResponseBytes, -1, 200, false},
{"overflow unknown length", maxAuthenticationResponseBytes + 10, -1, 200, true},
{"overflow understated length", maxAuthenticationResponseBytes + 10, 1, 200, true},
{"unauthorized does not read", maxAuthenticationResponseBytes + 10, -1, 401, false},
} {
t.Run(test.name, func(t *testing.T) {
body := &measuredAuthenticationBody{reader: strings.NewReader(valid + strings.Repeat(" ", test.size-len(valid)))}
client := reachabilityClient(t, func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: test.status, Header: make(http.Header), Body: body, ContentLength: test.declared}, nil
})
outcome, err := (authenticationProbe{client: client}).run(context.Background(), backendWithAuthenticationKind("openrouter_key"), domain.SmokeTestTarget{Endpoint: "https://example.invalid/key"})
if err != nil || !body.closed {
t.Fatalf("outcome = %+v, err = %v, closed = %v", outcome, err, body.closed)
}
if test.overflow {
if outcome.reason != domain.SmokeTestReasonAuthenticationResponseLimitExceeded || outcome.failure != domain.ExecutionFailureOther || outcome.credentialsAccepted != nil || body.read != maxAuthenticationResponseBytes+1 {
t.Fatalf("overflow = %+v, read = %d", outcome, body.read)
}
} else if test.status == 401 {
if body.read != 0 || outcome.credentialsAccepted == nil || *outcome.credentialsAccepted {
t.Fatalf("unauthorized = %+v, read = %d", outcome, body.read)
}
} else if outcome.credentialsAccepted == nil || !*outcome.credentialsAccepted {
t.Fatalf("exact limit = %+v", outcome)
}
})
}
}
func TestAuthenticationProbeApplicabilityAndHTTPAvailability(t *testing.T) {
client := reachabilityClient(t, func(*http.Request) (*http.Response, error) {
t.Fatal("authentication applicability unexpectedly dispatched")
return nil, nil
})
for _, tc := range []struct {
name string
backend domain.Backend
client bool
supported bool
applicable *bool
reason domain.SmokeTestReason
}{
{
name: "not configured",
backend: domain.Backend{},
reason: domain.SmokeTestReasonProbeNotConfigured,
},
{
name: "none works with injected client",
backend: backendWithAuthenticationKind("none"),
supported: true,
applicable: boolPointer(false),
reason: domain.SmokeTestReasonAuthenticationNotApplicable,
},
{
name: "HTTP adapter unavailable",
backend: backendWithAuthenticationKind("openrouter_key"),
reason: domain.SmokeTestReasonHTTPDiagnosticsUnavailable,
},
} {
t.Run(tc.name, func(t *testing.T) {
probe := authenticationProbe{}
if tc.client {
probe.client = client
}
outcome, err := probe.run(context.Background(), tc.backend, domain.SmokeTestTarget{})
if err != nil {
t.Fatal(err)
}
if outcome.supported != tc.supported || outcome.reason != tc.reason || outcome.evidence.DispatchInvoked {
t.Fatalf("outcome = %+v", outcome)
}
if !sameBool(outcome.applicable, tc.applicable) {
t.Fatalf("applicability = %v, want %v", outcome.applicable, tc.applicable)
}
})
}
}
func TestAuthenticationProbeInterpretsOpenRouterResponses(t *testing.T) {
validResponse := `{"data":{"label":"member","is_free_tier":false,"usage":0,"limit":12,"extra":{"ignored":true}},"other":"ignored"}`
for _, tc := range []struct {
name string
status int
body string
key string
wantAccepted *bool
wantFailure domain.ExecutionFailureKind
wantReason domain.SmokeTestReason
wantCredentials bool
}{
{
name: "recognized success",
status: http.StatusOK,
body: validResponse,
key: "synthetic-secret",
wantAccepted: boolPointer(true),
wantCredentials: true,
},
{
name: "anonymous unauthorized",
status: http.StatusUnauthorized,
wantAccepted: boolPointer(false),
},
{
name: "credential unauthorized",
status: http.StatusUnauthorized,
key: "synthetic-secret",
wantAccepted: boolPointer(false),
wantCredentials: true,
},
{
name: "unrecognized success",
status: http.StatusOK,
body: `{"data":{"label":"member","is_free_tier":false,"usage":"0"}}`,
key: "synthetic-secret",
wantFailure: domain.ExecutionFailureMalformedResponse,
wantReason: domain.SmokeTestReasonAuthenticationResponseUnrecognized,
wantCredentials: true,
},
{
name: "other status",
status: http.StatusNotFound,
key: "synthetic-secret",
wantFailure: domain.ExecutionFailureProviderStatus,
wantCredentials: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
var authorization string
client := reachabilityClient(t, func(request *http.Request) (*http.Response, error) {
if request.Method != http.MethodGet {
t.Errorf("method = %q, want GET", request.Method)
}
authorization = request.Header.Get("Authorization")
return &http.Response{
StatusCode: tc.status,
Header: make(http.Header),
Body: ioNopCloser(tc.body),
}, nil
})
backend := backendWithAuthenticationKind("openrouter_key")
settings := domain.ExecutionTarget{APIKey: tc.key, APIKeyRequired: true}
target := domain.SmokeTestTarget{
Endpoint: "https://endpoint-user:endpoint-pass@example.invalid/v1/key",
EffectiveSettings: settings,
}
outcome, err := (authenticationProbe{client: client}).run(context.Background(), backend, target)
if err != nil {
t.Fatal(err)
}
if outcome.credentialsAccepted == nil && tc.wantAccepted != nil ||
outcome.credentialsAccepted != nil && (tc.wantAccepted == nil || *outcome.credentialsAccepted != *tc.wantAccepted) {
t.Fatalf("accepted = %v, want %v", outcome.credentialsAccepted, tc.wantAccepted)
}
if outcome.failure != tc.wantFailure || outcome.reason != tc.wantReason {
t.Fatalf("failure/reason = %v/%q", outcome.failure, outcome.reason)
}
if !outcome.supported || outcome.applicable == nil || !*outcome.applicable ||
!outcome.evidence.ResponseReceived || outcome.evidence.HTTPStatus == nil || *outcome.evidence.HTTPStatus != tc.status {
t.Fatalf("response evidence = %+v", outcome)
}
if outcome.evidence.CredentialsSupplied != tc.wantCredentials ||
(authorization != "") != tc.wantCredentials {
t.Fatalf("credential evidence=%+v authorization=%q", outcome.evidence, authorization)
}
if strings.Contains(fmt.Sprintf("%+v", outcome), "synthetic-secret") ||
strings.Contains(outcome.evidence.Endpoint, "endpoint-pass") {
t.Fatalf("sensitive value escaped into outcome: %+v", outcome)
}
})
}
}
func TestAuthenticationProbeRecognizesOnlyOneCompleteJSONObject(t *testing.T) {
for _, tc := range []struct {
body string
accepted bool
}{
{`{"data":{"label":"member","is_free_tier":false,"usage":1}} {}`, false},
{`{"data":[]}`, false},
{`{"data":{"label":"member","is_free_tier":false,"usage":null}}`, false},
{`{"data":{"label":"member","is_free_tier":0,"usage":1}}`, false},
{`{"data":{"is_free_tier":false,"usage":1}}`, false},
{`{"data":{"label":null,"is_free_tier":false,"usage":1}}`, false},
{`{"data":{"label":"","is_free_tier":false,"usage":1e2,"ignored":true},"ignored":true}`, true},
} {
client := reachabilityClient(t, func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: http.StatusOK, Header: make(http.Header), Body: ioNopCloser(tc.body)}, nil
})
outcome, err := (authenticationProbe{client: client}).run(context.Background(), backendWithAuthenticationKind("openrouter_key"), domain.SmokeTestTarget{Endpoint: "https://example.invalid/key"})
if err != nil || (outcome.credentialsAccepted != nil && *outcome.credentialsAccepted) != tc.accepted {
t.Errorf("account response %s: outcome = %+v, error = %v", tc.body, outcome, err)
}
}
}
func TestAuthenticationProbeTreatsInterruptedResponseBodyAsTransportFailure(t *testing.T) {
client := reachabilityClient(t, func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: interruptedReadCloser{err: context.DeadlineExceeded},
}, nil
})
outcome, err := (authenticationProbe{client: client}).run(
context.Background(),
backendWithAuthenticationKind("openrouter_key"),
domain.SmokeTestTarget{Endpoint: "https://example.invalid/key"},
)
if err != nil {
t.Fatal(err)
}
if outcome.credentialsAccepted != nil || outcome.failure != domain.ExecutionFailureDeadline ||
!outcome.evidence.ResponseReceived || outcome.evidence.HTTPStatus == nil || *outcome.evidence.HTTPStatus != http.StatusOK {
t.Fatalf("interrupted response outcome = %+v", outcome)
}
}
func TestAuthenticationProbeReportsTransportFailureAndAllowsMissingRequiredKey(t *testing.T) {
t.Setenv("PROMPTKIT_AUTH_REQUIRED_KEY", "")
client := reachabilityClient(t, func(request *http.Request) (*http.Response, error) {
if request.Header.Get("Authorization") != "" {
t.Errorf("Authorization = %q, want absent", request.Header.Get("Authorization"))
}
return nil, errors.New("opaque transport failure")
})
target := domain.SmokeTestTarget{
Endpoint: "https://example.invalid/key",
EffectiveSettings: domain.ExecutionTarget{
APIKeyEnv: "PROMPTKIT_AUTH_REQUIRED_KEY",
APIKeyRequired: true,
},
}
outcome, err := (authenticationProbe{client: client}).run(context.Background(), backendWithAuthenticationKind("openrouter_key"), target)
if err != nil {
t.Fatal(err)
}
if outcome.credentialsAccepted != nil || outcome.failure != domain.ExecutionFailureTransport ||
!outcome.evidence.DispatchInvoked || !outcome.evidence.RequestObserved || outcome.evidence.CredentialsSupplied {
t.Fatalf("transport outcome = %+v", outcome)
}
}
func backendWithAuthenticationKind(kind string) domain.Backend {
return domain.Backend{SmokeTests: &domain.BackendSmokeTestConfig{
Authentication: &domain.AuthenticationProbeConfig{Kind: &kind},
}}
}
func sameBool(got, want *bool) bool {
if got == nil || want == nil {
return got == nil && want == nil
}
return *got == *want
}
type stringReadCloser struct{ *strings.Reader }
func ioNopCloser(value string) stringReadCloser {
return stringReadCloser{Reader: strings.NewReader(value)}
}
func (stringReadCloser) Close() error { return nil }
type interruptedReadCloser struct{ err error }
func (r interruptedReadCloser) Read([]byte) (int, error) { return 0, r.err }
func (interruptedReadCloser) Close() error { return nil }