253 lines
8.7 KiB
Go
253 lines
8.7 KiB
Go
package usecase
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
|
)
|
|
|
|
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 _, body := range []string{
|
|
`{"data":{"label":"member","is_free_tier":false,"usage":1}} {}`,
|
|
`{"data":[]}`,
|
|
`{"data":{"label":"member","is_free_tier":false,"usage":null}}`,
|
|
`{"data":{"label":"member","is_free_tier":0,"usage":1}}`,
|
|
`{"data":{"is_free_tier":false,"usage":1}}`,
|
|
`{"data":{"label":null,"is_free_tier":false,"usage":1}}`,
|
|
} {
|
|
if recognized, err := recognizedOpenRouterKeyResponse(strings.NewReader(body)); err != nil || recognized {
|
|
t.Errorf("recognized invalid account response %s", body)
|
|
}
|
|
}
|
|
if recognized, err := recognizedOpenRouterKeyResponse(strings.NewReader(`{"data":{"label":"","is_free_tier":false,"usage":1e2,"ignored":true},"ignored":true}`)); err != nil || !recognized {
|
|
t.Fatal("valid minimal identifying response was rejected")
|
|
}
|
|
}
|
|
|
|
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 }
|