diff --git a/friendly-captcha-sdk-testserver/README.md b/friendly-captcha-sdk-testserver/README.md index a476955..7ae6f44 100644 --- a/friendly-captcha-sdk-testserver/README.md +++ b/friendly-captcha-sdk-testserver/README.md @@ -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 @@ -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 -``` \ No newline at end of file +``` diff --git a/friendly-captcha-sdk-testserver/fixtures/test_cases.json b/friendly-captcha-sdk-testserver/fixtures/captcha_siteverify_test_cases.json similarity index 99% rename from friendly-captcha-sdk-testserver/fixtures/test_cases.json rename to friendly-captcha-sdk-testserver/fixtures/captcha_siteverify_test_cases.json index 887acad..2de9939 100644 --- a/friendly-captcha-sdk-testserver/fixtures/test_cases.json +++ b/friendly-captcha-sdk-testserver/fixtures/captcha_siteverify_test_cases.json @@ -472,4 +472,4 @@ } } ] -} +} \ No newline at end of file diff --git a/friendly-captcha-sdk-testserver/fixtures/fixture_test.go b/friendly-captcha-sdk-testserver/fixtures/fixture_test.go index a410137..4b1bf9b 100644 --- a/friendly-captcha-sdk-testserver/fixtures/fixture_test.go +++ b/friendly-captcha-sdk-testserver/fixtures/fixture_test.go @@ -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"` } @@ -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) @@ -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) } } diff --git a/friendly-captcha-sdk-testserver/fixtures/load.go b/friendly-captcha-sdk-testserver/fixtures/load.go index 20d41b3..b40b24d 100644 --- a/friendly-captcha-sdk-testserver/fixtures/load.go +++ b/friendly-captcha-sdk-testserver/fixtures/load.go @@ -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) } diff --git a/friendly-captcha-sdk-testserver/fixtures/risk_intelligence_retrieve_test_cases.json b/friendly-captcha-sdk-testserver/fixtures/risk_intelligence_retrieve_test_cases.json new file mode 100644 index 0000000..bdab39d --- /dev/null +++ b/friendly-captcha-sdk-testserver/fixtures/risk_intelligence_retrieve_test_cases.json @@ -0,0 +1,141 @@ +{ + "version": 1, + "tests": [ + { + "name": "success", + "token": "dc2fe149-04ce-45e5-a317-41425942abba", + "retrieve_response": { + "success": true, + "data": { + "risk_intelligence": { + "risk_scores": { + "overall": 3, + "network": 3, + "browser": 2 + }, + "client": { + "header_user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + "browser": { + "id": "chrome" + } + } + }, + "details": { + "timestamp": "2023-08-04T13:01:25Z", + "expires_at": "2023-08-04T13:06:25Z", + "num_uses": 1 + } + } + }, + "retrieve_status_code": 200, + "expectation": { + "was_able_to_retrieve": true, + "is_client_error": false + } + }, + { + "name": "success_with_null_client_browser", + "token": "c4b0031a-68c1-481e-a40e-fe526b0e0398", + "retrieve_response": { + "success": true, + "data": { + "risk_intelligence": { + "risk_scores": { + "overall": 1, + "network": 1, + "browser": 1 + }, + "client": { + "header_user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + "browser": null + } + }, + "details": { + "timestamp": "2023-08-04T13:01:25Z", + "expires_at": "2023-08-04T13:06:25Z", + "num_uses": 1 + } + } + }, + "retrieve_status_code": 200, + "expectation": { + "was_able_to_retrieve": true, + "is_client_error": false + } + }, + { + "name": "auth_required", + "token": "50aa9a4c-c42a-4086-b313-6f045af3a9c7", + "retrieve_response": { + "success": false, + "error": { + "error_code": "auth_required", + "detail": "" + } + }, + "retrieve_status_code": 401, + "expectation": { + "was_able_to_retrieve": false, + "is_client_error": true + } + }, + { + "name": "auth_invalid", + "token": "5c05b5d9-1e08-4331-93ef-10aded015949", + "retrieve_response": { + "success": false, + "error": { + "error_code": "auth_invalid", + "detail": "" + } + }, + "retrieve_status_code": 401, + "expectation": { + "was_able_to_retrieve": false, + "is_client_error": true + } + }, + { + "name": "bad_request", + "token": "689ca2eb-f1e9-42e5-8245-2e8c9ecb1974", + "retrieve_response": { + "success": false, + "error": { + "error_code": "bad_request", + "detail": "" + } + }, + "retrieve_status_code": 400, + "expectation": { + "was_able_to_retrieve": false, + "is_client_error": true + } + }, + { + "name": "token_expired", + "token": "c0cf28c8-891b-4643-989d-47899339e5e7", + "retrieve_response": { + "success": false, + "error": { + "error_code": "token_expired", + "detail": "" + } + }, + "retrieve_status_code": 400, + "expectation": { + "was_able_to_retrieve": false, + "is_client_error": true + } + }, + { + "name": "bad_response_500", + "token": "12f35538-ed0e-43e4-9504-3379a22983bf", + "retrieve_response": "
Something went horribly wrong", + "retrieve_status_code": 500, + "expectation": { + "was_able_to_retrieve": false, + "is_client_error": false + } + } + ] +} \ No newline at end of file diff --git a/friendly-captcha-sdk-testserver/main.go b/friendly-captcha-sdk-testserver/main.go index eccf48d..607fe43 100644 --- a/friendly-captcha-sdk-testserver/main.go +++ b/friendly-captcha-sdk-testserver/main.go @@ -12,18 +12,27 @@ import ( "github.com/friendlycaptcha/friendly-captcha-sdk-tooling/friendly-captcha-sdk-testserver/wire" ) -// Usage: go run main.go serve --port 1090 --tests ./test_cases.json +// Usage: go run main.go serve --port 1090 --siteverify-tests