Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions friendly-captcha-sdk-testserver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,14 @@ Next, run the tests that talk to this test server in the SDK implementation.
You can pass some optional settings:

* `--port 1234` run the server on a custom port.
* `--tests some/path/my_test_cases_file.json` serve the test cases in a custom fixtures file.
* `--siteverify-tests some/path/my_captcha_siteverify_test_cases_file.json` serve custom captcha siteverify test cases (embedded by default).
* `--retrieve-tests some/path/my_risk_intelligence_retrieve_test_cases_file.json` serve custom risk intelligence retrieve test cases (embedded by default).

## Adding new sdk tests
The expected behavior of the SDK is defined in the [test_cases.json](./fixtures/test_cases.json) file.
The expected behavior of SDKs is defined in:

* [captcha_siteverify_test_cases.json](./fixtures/captcha_siteverify_test_cases.json)
* [risk_intelligence_retrieve_test_cases.json](./fixtures/risk_intelligence_retrieve_test_cases.json)

## Development

Expand All @@ -58,4 +62,4 @@ goreleaser --snapshot --skip=publish --clean
docker login
docker buildx create --use
docker buildx build --platform=linux/amd64,linux/arm64 -t friendlycaptcha/sdk-testserver:latest . --push
```
```
Original file line number Diff line number Diff line change
Expand Up @@ -472,4 +472,4 @@
}
}
]
}
}
134 changes: 98 additions & 36 deletions friendly-captcha-sdk-testserver/fixtures/fixture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ import (
"github.com/guregu/null/v6"
)

type SiteverifyResponse struct {
Success bool `json:"success"`
Data SiteverifyResponseSuccessData `json:"data,omitempty"`
type CaptchaSiteverifyResponse struct {
Success bool `json:"success"`
Data CaptchaSiteverifyResponseSuccessData `json:"data,omitempty"`
}

type SiteverifyResponseSuccessData struct {
type CaptchaSiteverifyResponseSuccessData struct {
EventID string `json:"event_id"`

Challenge SiteverifyResponseSuccessDataChallenge `json:"challenge"`
Challenge CaptchaSiteverifyResponseSuccessDataChallenge `json:"challenge"`

RiskIntelligence null.Value[RiskIntelligenceData] `json:"risk_intelligence"`
}

type SiteverifyResponseSuccessDataChallenge struct {
type CaptchaSiteverifyResponseSuccessDataChallenge struct {
Timestamp string `json:"timestamp"`
Origin string `json:"origin"`
}
Expand Down Expand Up @@ -52,15 +52,39 @@ type RiskIntelligenceDataClientBrowser struct {
ID string `json:"id"`
}

func TestFixtures(t *testing.T) {
type RiskIntelligenceRetrieveResponse struct {
Success bool `json:"success"`
Data RiskIntelligenceRetrieveResponseSuccessData `json:"data,omitempty"`
}

type RiskIntelligenceRetrieveResponseSuccessData struct {
RiskIntelligence null.Value[RiskIntelligenceData] `json:"risk_intelligence"`
Details RiskIntelligenceRetrieveResponseDetails `json:"details"`
}

type RiskIntelligenceRetrieveResponseDetails struct {
Timestamp string `json:"timestamp"`
ExpiresAt string `json:"expires_at"`
NumUses int64 `json:"num_uses"`
}

func TestCaptchaSiteverifyFixtures(t *testing.T) {
t.Parallel()

testCases, err := Load("")
testCases, err := LoadCaptchaSiteverify("")
if err != nil {
t.Fatalf("Failed to load embedded test cases: %v", err)
t.Fatalf("Failed to load embedded siteverify test cases: %v", err)
}

invalidJSONCases := map[string]bool{
"bad_response_200": true,
"bad_response_200_strict": true,
"bad_response_500": true,
"bad_response_400_strict": true,
"empty_string_response_200": true,
"empty_string_response_200_strict": true,
}

// For each fixture make sure it has a name, response and expectation.
for i, tc := range testCases.Tests {
if tc.Name == "" {
t.Errorf("Test case %d has an empty name", i)
Expand All @@ -69,46 +93,84 @@ func TestFixtures(t *testing.T) {
t.Errorf("Test case %d (%s) has an empty response", i, tc.Name)
}

// Parse as SiteverifyResponse to ensure it's valid JSON.
var svr SiteverifyResponse
var svr CaptchaSiteverifyResponse
err := json.Unmarshal(tc.SiteverifyResponse, &svr)
if err != nil {
// Some responses are completely invalid JSON (e.g., HTML error pages).
// We only validate the ones that are supposed to be valid JSON.

// We hardcode those that we expect to fail to parse:
invalidJSONCases := map[string]bool{
"bad_response_200": true,
"bad_response_200_strict": true,
"bad_response_500": true,
"bad_response_400_strict": true,
"empty_string_response_200": true,
"empty_string_response_200_strict": true,
}
if !invalidJSONCases[tc.Name] {
t.Errorf("Test case %d (%s) has invalid siteverify_response JSON: %v", i, tc.Name, err)
}
return
continue
}

if !svr.Success { // For non-success cases we don't validate further.
if !svr.Success {
continue
}
validateRiskIntelligenceData(t, i, tc.Name, svr.Data.RiskIntelligence)
}
}

ri := svr.Data.RiskIntelligence
func TestRiskIntelligenceRetrieveFixtures(t *testing.T) {
t.Parallel()

// If risk intelligence data is present, ensure it has expected fields.
if ri.Valid {
if ri.V.Client.HeaderUserAgent == "" {
t.Errorf("Test case %d (%s) has risk intelligence data with empty client header_user_agent", i, tc.Name)
}
testCases, err := LoadRiskIntelligenceRetrieve("")
if err != nil {
t.Fatalf("Failed to load embedded retrieve test cases: %v", err)
}

invalidJSONCases := map[string]bool{
"bad_response_200": true,
"bad_response_500": true,
"empty_string_response_200": true,
}

if ri.V.Client.Browser.Valid {
if ri.V.Client.Browser.V.ID == "" {
t.Errorf("Test case %d (%s) has risk intelligence data with empty browser id", i, tc.Name)
}
for i, tc := range testCases.Tests {
if tc.Name == "" {
t.Errorf("Test case %d has an empty name", i)
}
if tc.Token == "" {
t.Errorf("Test case %d (%s) has an empty token", i, tc.Name)
}

var rr RiskIntelligenceRetrieveResponse
err := json.Unmarshal(tc.RiskIntelligenceRetrieveResponse, &rr)
if err != nil {
if !invalidJSONCases[tc.Name] {
t.Errorf("Test case %d (%s) has invalid retrieve_response JSON: %v", i, tc.Name, err)
}
continue
}

if !rr.Success {
continue
}

if rr.Data.Details.Timestamp == "" {
t.Errorf("Test case %d (%s) has empty details.timestamp", i, tc.Name)
}
if rr.Data.Details.ExpiresAt == "" {
t.Errorf("Test case %d (%s) has empty details.expires_at", i, tc.Name)
}
validateRiskIntelligenceData(t, i, tc.Name, rr.Data.RiskIntelligence)
}
}

func validateRiskIntelligenceData(
t *testing.T,
testIndex int,
testName string,
ri null.Value[RiskIntelligenceData],
) {
t.Helper()

if !ri.Valid {
return
}

if ri.V.Client.HeaderUserAgent == "" {
t.Errorf("Test case %d (%s) has risk intelligence data with empty client header_user_agent", testIndex, testName)
}

if ri.V.Client.Browser.Valid && ri.V.Client.Browser.V.ID == "" {
t.Errorf("Test case %d (%s) has risk intelligence data with empty browser id", testIndex, testName)
}
}
36 changes: 25 additions & 11 deletions friendly-captcha-sdk-testserver/fixtures/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,41 @@ import (
"github.com/friendlycaptcha/friendly-captcha-sdk-tooling/friendly-captcha-sdk-testserver/model"
)

//go:embed test_cases.json
var testCasesFileBytes []byte
//go:embed captcha_siteverify_test_cases.json
var captchaSiteverifyTestCasesFileBytes []byte

// Load loads the test cases from the embedded JSON file or from the provided filepath.
func Load(filepath string) (model.TestCasesFile, error) {
//go:embed risk_intelligence_retrieve_test_cases.json
var riskIntelligenceRetrieveTestCasesFileBytes []byte

// load loads test cases from the embedded bytes or from the provided filepath.
// If filepath is non-empty, the file is read from disk; otherwise embedded is used.
func load[T any](filepath string, embedded []byte) (T, error) {
var zero T
var err error
b := testCasesFileBytes
b := embedded

// Only read from disk if a filepath was provided.
if filepath != "" {
b, err = os.ReadFile(filepath)
if err != nil {
return model.TestCasesFile{}, fmt.Errorf("failed to read test cases: %w", err)
return zero, fmt.Errorf("failed to read test cases: %w", err)
}
}

var testCases model.TestCasesFile
err = json.Unmarshal(b, &testCases)
var out T
err = json.Unmarshal(b, &out)
if err != nil {
return model.TestCasesFile{}, fmt.Errorf("failed to parse test cases as JSON: %w", err)
return zero, fmt.Errorf("failed to parse test cases as JSON: %w", err)
}

return testCases, nil
return out, nil
}

// LoadCaptchaSiteverify loads captcha siteverify test cases from the embedded JSON file or from the provided filepath.
func LoadCaptchaSiteverify(filepath string) (model.CaptchaSiteverifyTestCasesFile, error) {
return load[model.CaptchaSiteverifyTestCasesFile](filepath, captchaSiteverifyTestCasesFileBytes)
}

// LoadRiskIntelligenceRetrieve loads risk intelligence retrieve test cases from the embedded JSON file or from the provided filepath.
func LoadRiskIntelligenceRetrieve(filepath string) (model.RiskIntelligenceRetrieveTestCasesFile, error) {
return load[model.RiskIntelligenceRetrieveTestCasesFile](filepath, riskIntelligenceRetrieveTestCasesFileBytes)
}
Loading
Loading