Bound authentication response recognition
This commit is contained in:
@@ -125,3 +125,17 @@ func TestBundledSmokeSelectionUsesEffectiveConfiguration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBundledSmokeAuthenticationResponseOverflow(t *testing.T) {
|
||||||
|
transport := roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||||
|
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(strings.Repeat("x", (1<<20)+1)))}, nil
|
||||||
|
})
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{HTTPClient: &http.Client{Transport: transport}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
report, err := engine.SmokeTest(context.Background(), promptkit.SmokeTestRequest{Kind: promptkit.SmokeTestAuthentication, BackendID: "openrouter", APIKey: "private-key-marker"})
|
||||||
|
if err != nil || report == nil || !report.Completed || report.Reason != promptkit.SmokeTestReasonAuthenticationResponseLimitExceeded || report.Failure != promptkit.ExecutionFailureOther || report.Facts.CredentialsAccepted != nil {
|
||||||
|
t.Fatalf("overflow = %+v, %v", report, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -224,6 +224,10 @@ limit plus one, missing/inaccurate lengths, malformed bounded bodies, closure,
|
|||||||
401 without reading, cancellation, and redacted explicit overflow. Keep this
|
401 without reading, cancellation, and redacted explicit overflow. Keep this
|
||||||
fix independent of the later protocol relocation.
|
fix independent of the later protocol relocation.
|
||||||
|
|
||||||
|
Completion: authentication recognition reads at most 1 MiB plus one byte and
|
||||||
|
reports a distinct overflow reason without asserting credential rejection.
|
||||||
|
Focused authentication and public smoke tests pass on Go 1.27.3.
|
||||||
|
|
||||||
## Stage 8: Correct Artifact Result Ownership
|
## Stage 8: Correct Artifact Result Ownership
|
||||||
|
|
||||||
**Catalog:** 4. **Files:** public ArtifactReader GoDoc, adapter and ownership tests.
|
**Catalog:** 4. **Files:** public ArtifactReader GoDoc, adapter and ownership tests.
|
||||||
|
|||||||
@@ -112,12 +112,13 @@ type SmokeTestAttempt struct {
|
|||||||
type SmokeTestReason string
|
type SmokeTestReason string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SmokeTestReasonNone SmokeTestReason = ""
|
SmokeTestReasonNone SmokeTestReason = ""
|
||||||
SmokeTestReasonAuthenticationNotApplicable SmokeTestReason = "authentication_not_applicable"
|
SmokeTestReasonAuthenticationNotApplicable SmokeTestReason = "authentication_not_applicable"
|
||||||
SmokeTestReasonProbeNotConfigured SmokeTestReason = "probe_not_configured"
|
SmokeTestReasonProbeNotConfigured SmokeTestReason = "probe_not_configured"
|
||||||
SmokeTestReasonHTTPDiagnosticsUnavailable SmokeTestReason = "http_diagnostics_unavailable"
|
SmokeTestReasonHTTPDiagnosticsUnavailable SmokeTestReason = "http_diagnostics_unavailable"
|
||||||
SmokeTestReasonRequiredCredentialMissing SmokeTestReason = "required_credential_missing"
|
SmokeTestReasonRequiredCredentialMissing SmokeTestReason = "required_credential_missing"
|
||||||
SmokeTestReasonAuthenticationResponseUnrecognized SmokeTestReason = "authentication_response_unrecognized"
|
SmokeTestReasonAuthenticationResponseUnrecognized SmokeTestReason = "authentication_response_unrecognized"
|
||||||
|
SmokeTestReasonAuthenticationResponseLimitExceeded SmokeTestReason = "authentication_response_limit_exceeded"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CloneBackend returns a copy of a backend, including its mutable settings.
|
// CloneBackend returns a copy of a backend, including its mutable settings.
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ type authenticationProbe struct {
|
|||||||
client smokeHTTPGetter
|
client smokeHTTPGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxAuthenticationResponseBytes = 1 << 20
|
||||||
|
|
||||||
type authenticationProbeOutcome struct {
|
type authenticationProbeOutcome struct {
|
||||||
supported bool
|
supported bool
|
||||||
applicable *bool
|
applicable *bool
|
||||||
@@ -104,7 +106,17 @@ func (p authenticationProbe) runOpenRouterKey(
|
|||||||
outcome.failure = domain.ExecutionFailureMalformedResponse
|
outcome.failure = domain.ExecutionFailureMalformedResponse
|
||||||
return outcome, nil
|
return outcome, nil
|
||||||
}
|
}
|
||||||
recognized, bodyErr := recognizedOpenRouterKeyResponse(response.Body)
|
contents, bodyErr := io.ReadAll(io.LimitReader(response.Body, maxAuthenticationResponseBytes+1))
|
||||||
|
if len(contents) > maxAuthenticationResponseBytes {
|
||||||
|
outcome.reason = domain.SmokeTestReasonAuthenticationResponseLimitExceeded
|
||||||
|
outcome.failure = domain.ExecutionFailureOther
|
||||||
|
return outcome, nil
|
||||||
|
}
|
||||||
|
if bodyErr != nil {
|
||||||
|
outcome.failure = httpProbeFailure(ctx, bodyErr)
|
||||||
|
return outcome, nil
|
||||||
|
}
|
||||||
|
recognized, bodyErr := recognizedOpenRouterKeyResponse(bytes.NewReader(contents))
|
||||||
if bodyErr != nil {
|
if bodyErr != nil {
|
||||||
outcome.failure = httpProbeFailure(ctx, bodyErr)
|
outcome.failure = httpProbeFailure(ctx, bodyErr)
|
||||||
return outcome, nil
|
return outcome, nil
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -11,6 +12,57 @@ import (
|
|||||||
"go.maximumdirect.net/promptkit/internal/domain"
|
"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) {
|
||||||
|
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) {
|
func TestAuthenticationProbeApplicabilityAndHTTPAvailability(t *testing.T) {
|
||||||
client := reachabilityClient(t, func(*http.Request) (*http.Response, error) {
|
client := reachabilityClient(t, func(*http.Request) (*http.Response, error) {
|
||||||
t.Fatal("authentication applicability unexpectedly dispatched")
|
t.Fatal("authentication applicability unexpectedly dispatched")
|
||||||
|
|||||||
@@ -219,6 +219,9 @@ const (
|
|||||||
// SmokeTestReasonAuthenticationResponseUnrecognized marks an HTTP response
|
// SmokeTestReasonAuthenticationResponseUnrecognized marks an HTTP response
|
||||||
// whose authentication meaning could not be established.
|
// whose authentication meaning could not be established.
|
||||||
SmokeTestReasonAuthenticationResponseUnrecognized SmokeTestReason = "authentication_response_unrecognized"
|
SmokeTestReasonAuthenticationResponseUnrecognized SmokeTestReason = "authentication_response_unrecognized"
|
||||||
|
// SmokeTestReasonAuthenticationResponseLimitExceeded indicates that the
|
||||||
|
// authentication response exceeded the recognition allowance.
|
||||||
|
SmokeTestReasonAuthenticationResponseLimitExceeded SmokeTestReason = "authentication_response_limit_exceeded"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SmokeTest runs one built-in diagnostic against the engine's captured
|
// SmokeTest runs one built-in diagnostic against the engine's captured
|
||||||
|
|||||||
Reference in New Issue
Block a user