Cybersecurity-Projects/PROJECTS/intermediate/secrets-scanner/internal/hibp/client_test.go

218 lines
4.9 KiB
Go

/*
©AngelaMos | 2026
client_test.go
Tests for hibp/client.go
Tests:
sha1Hash produces correct uppercase hex SHA-1 for known input
Check() identifies breached secrets and returns the correct count
Check() correctly marks clean secrets as not breached
LRU cache prevents duplicate HTTP calls for the same secret
Non-200 server responses propagate as errors
429 responses trigger retries and succeed once the server recovers
All 3 retries exhausted returns a "retries exhausted" error
Cancelled context fails Check() before the HTTP call reaches the server
*/
package hibp
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSha1Hash(t *testing.T) {
t.Parallel()
got := sha1Hash("password")
assert.Equal(t,
"5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8",
got,
)
}
func TestClientCheckBreached(t *testing.T) {
t.Parallel()
hash := sha1Hash("password")
prefix := hash[:5]
suffix := hash[5:]
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/range/"+prefix, r.URL.Path)
w.WriteHeader(http.StatusOK)
fmt.Fprintf( //nolint:errcheck
w,
"%s:3861493\n",
suffix,
)
fmt.Fprintf( //nolint:errcheck
w,
"0000000000000000000000000000DEAD0:0\n",
)
}),
)
defer server.Close()
client := NewClient()
client.baseURL = server.URL + "/range/"
result, err := client.Check(context.Background(), "password")
require.NoError(t, err)
assert.True(t, result.Breached)
assert.Equal(t, 3861493, result.Count)
}
func TestClientCheckClean(t *testing.T) {
t.Parallel()
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, //nolint:errcheck
"0000000000000000000000000000AAAA0:5\n")
fmt.Fprintf( //nolint:errcheck
w,
"0000000000000000000000000000BBBB0:2\n",
)
}),
)
defer server.Close()
client := NewClient()
client.baseURL = server.URL + "/range/"
result, err := client.Check(
context.Background(), "unique_password_not_in_breach",
)
require.NoError(t, err)
assert.False(t, result.Breached)
assert.Equal(t, 0, result.Count)
}
func TestClientCachesResults(t *testing.T) {
t.Parallel()
callCount := 0
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
callCount++
hash := sha1Hash("cached_secret")
suffix := hash[5:]
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "%s:42\n", suffix) //nolint:errcheck
}),
)
defer server.Close()
client := NewClient()
client.baseURL = server.URL + "/range/"
result1, err := client.Check(
context.Background(), "cached_secret",
)
require.NoError(t, err)
assert.True(t, result1.Breached)
result2, err := client.Check(
context.Background(), "cached_secret",
)
require.NoError(t, err)
assert.True(t, result2.Breached)
assert.Equal(t, 42, result2.Count)
assert.Equal(t, 1, callCount)
}
func TestClientHandlesServerError(t *testing.T) {
t.Parallel()
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}),
)
defer server.Close()
client := NewClient()
client.baseURL = server.URL + "/range/"
_, err := client.Check(context.Background(), "test")
assert.Error(t, err)
}
func TestClientRetries429(t *testing.T) {
t.Parallel()
hash := sha1Hash("retryable")
suffix := hash[5:]
attempts := 0
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
attempts++
if attempts < 3 {
w.WriteHeader(http.StatusTooManyRequests)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "%s:10\n", suffix) //nolint:errcheck
}),
)
defer server.Close()
client := NewClient()
client.baseURL = server.URL + "/range/"
result, err := client.Check(context.Background(), "retryable")
require.NoError(t, err)
assert.True(t, result.Breached)
assert.Equal(t, 10, result.Count)
assert.Equal(t, 3, attempts)
}
func TestClientRetries429Exhausted(t *testing.T) {
t.Parallel()
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusTooManyRequests)
}),
)
defer server.Close()
client := NewClient()
client.baseURL = server.URL + "/range/"
_, err := client.Check(context.Background(), "always429")
require.Error(t, err)
assert.Contains(t, err.Error(), "retries exhausted")
}
func TestClientContextCancellation(t *testing.T) {
t.Parallel()
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "AAAA0:1\n") //nolint:errcheck
}),
)
defer server.Close()
client := NewClient()
client.baseURL = server.URL + "/range/"
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := client.Check(ctx, "test")
assert.Error(t, err)
}