test(05-03): add failing tests for HTTPVerifier single-key verification
- 10 test cases covering live/dead/rate-limited/unknown/error classification
- Key substitution in header/body/URL via {{KEY}} template
- JSON metadata extraction via gjson paths
- HTTPS-only enforcement and per-call timeout
This commit is contained in:
189
pkg/verify/verifier_test.go
Normal file
189
pkg/verify/verifier_test.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package verify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/salvacybersec/keyhunter/pkg/engine"
|
||||
"github.com/salvacybersec/keyhunter/pkg/providers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newTestVerifier builds an HTTPVerifier whose transport trusts the given
|
||||
// httptest TLS server's self-signed cert.
|
||||
func newTestVerifier(t *testing.T, srv *httptest.Server, timeout time.Duration) *HTTPVerifier {
|
||||
t.Helper()
|
||||
v := NewHTTPVerifier(timeout)
|
||||
v.Client = srv.Client()
|
||||
v.Client.Timeout = timeout
|
||||
return v
|
||||
}
|
||||
|
||||
func testFinding(key string) engine.Finding {
|
||||
return engine.Finding{
|
||||
ProviderName: "testprov",
|
||||
KeyValue: key,
|
||||
KeyMasked: engine.MaskKey(key + "padding1234"),
|
||||
}
|
||||
}
|
||||
|
||||
func testProvider(spec providers.VerifySpec) providers.Provider {
|
||||
return providers.Provider{Name: "testprov", Verify: spec}
|
||||
}
|
||||
|
||||
func TestVerify_Live_200(t *testing.T) {
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
v := newTestVerifier(t, srv, 5*time.Second)
|
||||
p := testProvider(providers.VerifySpec{URL: srv.URL, Method: "GET"})
|
||||
res := v.Verify(context.Background(), testFinding("sk-test-keyvalue"), p)
|
||||
|
||||
assert.Equal(t, StatusLive, res.Status)
|
||||
assert.Equal(t, 200, res.HTTPCode)
|
||||
}
|
||||
|
||||
func TestVerify_Dead_401(t *testing.T) {
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
v := newTestVerifier(t, srv, 5*time.Second)
|
||||
p := testProvider(providers.VerifySpec{URL: srv.URL})
|
||||
res := v.Verify(context.Background(), testFinding("sk-test-keyvalue"), p)
|
||||
|
||||
assert.Equal(t, StatusDead, res.Status)
|
||||
assert.Equal(t, 401, res.HTTPCode)
|
||||
}
|
||||
|
||||
func TestVerify_RateLimited_429_WithRetryAfter(t *testing.T) {
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Retry-After", "30")
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
v := newTestVerifier(t, srv, 5*time.Second)
|
||||
p := testProvider(providers.VerifySpec{URL: srv.URL})
|
||||
res := v.Verify(context.Background(), testFinding("sk-test-keyvalue"), p)
|
||||
|
||||
assert.Equal(t, StatusRateLimited, res.Status)
|
||||
assert.Equal(t, 30*time.Second, res.RetryAfter)
|
||||
}
|
||||
|
||||
func TestVerify_MetadataExtraction(t *testing.T) {
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"organization":{"name":"Acme"},"tier":"plus"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
v := newTestVerifier(t, srv, 5*time.Second)
|
||||
p := testProvider(providers.VerifySpec{
|
||||
URL: srv.URL,
|
||||
MetadataPaths: map[string]string{"org": "organization.name", "tier": "tier"},
|
||||
})
|
||||
res := v.Verify(context.Background(), testFinding("sk-test-keyvalue"), p)
|
||||
|
||||
require.Equal(t, StatusLive, res.Status)
|
||||
assert.Equal(t, "Acme", res.Metadata["org"])
|
||||
assert.Equal(t, "plus", res.Metadata["tier"])
|
||||
}
|
||||
|
||||
func TestVerify_KeySubstitution_InHeader(t *testing.T) {
|
||||
var gotAuth string
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotAuth = r.Header.Get("Authorization")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
v := newTestVerifier(t, srv, 5*time.Second)
|
||||
p := testProvider(providers.VerifySpec{
|
||||
URL: srv.URL,
|
||||
Headers: map[string]string{"Authorization": "Bearer {{KEY}}"},
|
||||
})
|
||||
res := v.Verify(context.Background(), testFinding("sk-test-keyvalue"), p)
|
||||
|
||||
assert.Equal(t, StatusLive, res.Status)
|
||||
assert.Equal(t, "Bearer sk-test-keyvalue", gotAuth)
|
||||
}
|
||||
|
||||
func TestVerify_KeySubstitution_InBody(t *testing.T) {
|
||||
var gotBody string
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
gotBody = string(b)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
v := newTestVerifier(t, srv, 5*time.Second)
|
||||
p := testProvider(providers.VerifySpec{
|
||||
URL: srv.URL,
|
||||
Method: "POST",
|
||||
Body: `{"api_key":"{{KEY}}"}`,
|
||||
})
|
||||
res := v.Verify(context.Background(), testFinding("sk-test-keyvalue"), p)
|
||||
|
||||
assert.Equal(t, StatusLive, res.Status)
|
||||
assert.Equal(t, `{"api_key":"sk-test-keyvalue"}`, gotBody)
|
||||
}
|
||||
|
||||
func TestVerify_KeySubstitution_InURL(t *testing.T) {
|
||||
var gotKey string
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotKey = r.URL.Query().Get("key")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
v := newTestVerifier(t, srv, 5*time.Second)
|
||||
p := testProvider(providers.VerifySpec{URL: srv.URL + "/v1/models?key={{KEY}}"})
|
||||
res := v.Verify(context.Background(), testFinding("sk-test-keyvalue"), p)
|
||||
|
||||
assert.Equal(t, StatusLive, res.Status)
|
||||
assert.Equal(t, "sk-test-keyvalue", gotKey)
|
||||
}
|
||||
|
||||
func TestVerify_MissingURL_Unknown(t *testing.T) {
|
||||
v := NewHTTPVerifier(5 * time.Second)
|
||||
res := v.Verify(context.Background(), testFinding("sk-test-keyvalue"), testProvider(providers.VerifySpec{}))
|
||||
assert.Equal(t, StatusUnknown, res.Status)
|
||||
}
|
||||
|
||||
func TestVerify_HTTPRejected(t *testing.T) {
|
||||
v := NewHTTPVerifier(5 * time.Second)
|
||||
p := testProvider(providers.VerifySpec{URL: "http://example.com/verify"})
|
||||
res := v.Verify(context.Background(), testFinding("sk-test-keyvalue"), p)
|
||||
|
||||
assert.Equal(t, StatusError, res.Status)
|
||||
assert.True(t, strings.Contains(strings.ToLower(res.Error), "https"), "error should mention HTTPS: %q", res.Error)
|
||||
}
|
||||
|
||||
func TestVerify_Timeout(t *testing.T) {
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
v := newTestVerifier(t, srv, 50*time.Millisecond)
|
||||
p := testProvider(providers.VerifySpec{URL: srv.URL})
|
||||
res := v.Verify(context.Background(), testFinding("sk-test-keyvalue"), p)
|
||||
|
||||
assert.Equal(t, StatusError, res.Status)
|
||||
assert.True(t, regexp.MustCompile(`(?i)timeout|deadline|canceled`).MatchString(res.Error),
|
||||
"expected timeout-like error, got %q", res.Error)
|
||||
}
|
||||
Reference in New Issue
Block a user