From 6ef0c66632edb6bf7319a68804d189c8620307bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rok=20Su=C5=A1nik?= Date: Mon, 2 Mar 2026 13:48:56 +0100 Subject: [PATCH 1/2] support serving risk intelligence fixtures --- friendly-captcha-sdk-testserver/README.md | 10 +- ...son => captcha_siteverify_test_cases.json} | 2 +- .../fixtures/fixture_test.go | 134 ++++++++++++----- .../fixtures/load.go | 42 +++++- ...risk_intelligence_retrieve_test_cases.json | 141 ++++++++++++++++++ friendly-captcha-sdk-testserver/main.go | 125 ++++++++++++---- .../model/model.go | 35 ++++- .../{siteverify.go => captcha_siteverify.go} | 2 +- .../wire/risk_intelligence_retrieve.go | 5 + 9 files changed, 411 insertions(+), 85 deletions(-) rename friendly-captcha-sdk-testserver/fixtures/{test_cases.json => captcha_siteverify_test_cases.json} (99%) create mode 100644 friendly-captcha-sdk-testserver/fixtures/risk_intelligence_retrieve_test_cases.json rename friendly-captcha-sdk-testserver/wire/{siteverify.go => captcha_siteverify.go} (85%) create mode 100644 friendly-captcha-sdk-testserver/wire/risk_intelligence_retrieve.go 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..690a50d 100644 --- a/friendly-captcha-sdk-testserver/fixtures/load.go +++ b/friendly-captcha-sdk-testserver/fixtures/load.go @@ -10,26 +10,52 @@ 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 + +// LoadCaptchaSiteverify loads captcha siteverify test cases from the embedded JSON file or from the provided filepath. +func LoadCaptchaSiteverify(filepath string) (model.CaptchaSiteverifyTestCasesFile, error) { + var err error + b := captchaSiteverifyTestCasesFileBytes + + // Only read from disk if a filepath was provided. + if filepath != "" { + b, err = os.ReadFile(filepath) + if err != nil { + return model.CaptchaSiteverifyTestCasesFile{}, fmt.Errorf("failed to read test cases: %w", err) + } + } + + var testCases model.CaptchaSiteverifyTestCasesFile + err = json.Unmarshal(b, &testCases) + if err != nil { + return model.CaptchaSiteverifyTestCasesFile{}, fmt.Errorf("failed to parse test cases as JSON: %w", err) + } + + return testCases, nil +} + +// LoadRiskIntelligenceRetrieve loads risk intelligence retrieve test cases from the embedded JSON file +// or from the provided filepath. +func LoadRiskIntelligenceRetrieve(filepath string) (model.RiskIntelligenceRetrieveTestCasesFile, error) { var err error - b := testCasesFileBytes + b := riskIntelligenceRetrieveTestCasesFileBytes // 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 model.RiskIntelligenceRetrieveTestCasesFile{}, fmt.Errorf("failed to read retrieve test cases: %w", err) } } - var testCases model.TestCasesFile + var testCases model.RiskIntelligenceRetrieveTestCasesFile err = json.Unmarshal(b, &testCases) if err != nil { - return model.TestCasesFile{}, fmt.Errorf("failed to parse test cases as JSON: %w", err) + return model.RiskIntelligenceRetrieveTestCasesFile{}, fmt.Errorf("failed to parse retrieve test cases as JSON: %w", err) } return testCases, nil 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..f4a500d 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 --retrieve-tests // Or just use the defaults: go run main.go serve const ( - defaultSiteverifyEndpoint = "/api/v2/captcha/siteverify" - defaultTestsJSONEndpoint = "/api/v1/tests" + defaultCaptchaSiteverifyEndpoint = "/api/v2/captcha/siteverify" + defaultRiskIntelligenceRetrieveEndpoint = "/api/v2/riskIntelligence/retrieve" + + // Legacy endpoint for backwards compatibility with SDKs that haven't been updated yet. + defaultLegacyTestsJSONEndpoint = "/api/v1/tests" + defaultCaptchaSiteverifyTestsJSONEndpoint = "/api/v1/captcha/siteverifyTests" + defaultRiskIntelligenceRetrieveTestsJSONEndpoint = "/api/v1/riskIntelligence/retrieveTests" + + expectedCaptchaSiteverifyTestsFileVersion = 2 + expectedRiskIntelligenceRetrieveTestsFileVersion = 1 ) var CLI struct { Serve struct { - Port int `help:"Port to listen on." default:"1090"` - Tests string `help:"Path to the test cases (JSON) file, leave empty to use the embedded tests." default:""` + Port int `help:"Port to listen on." default:"1090"` + Tests string `name:"siteverify-tests" help:"Path to captcha siteverify test cases (JSON), leave empty to use embedded tests." default:""` + RetrieveTests string `name:"retrieve-tests" help:"Path to risk intelligence retrieve test cases (JSON), leave empty to use embedded tests." default:""` } `cmd:"" help:"Start the SDK test server."` Version struct{} `cmd:"" help:"Print version information and exit."` } @@ -32,7 +41,7 @@ func main() { ctx := kong.Parse(&CLI) switch ctx.Command() { case "serve": - serve(CLI.Serve.Port, CLI.Serve.Tests) + serve(CLI.Serve.Port, CLI.Serve.Tests, CLI.Serve.RetrieveTests) case "version": fmt.Println(buildinfo.FullVersion()) default: @@ -40,60 +49,79 @@ func main() { } } -func serve(port int, testsPath string) { - tf, err := fixtures.Load(testsPath) +func serve(port int, siteverifytestsPath string, retrieveTestsPath string) { + captchaSiteverifyTestsFile, err := fixtures.LoadCaptchaSiteverify(siteverifytestsPath) + if err != nil { + panic(err) + } + riskIntelligenceRetrieveTestsFile, err := fixtures.LoadRiskIntelligenceRetrieve(retrieveTestsPath) if err != nil { panic(err) } - if tf.Version != 2 { - panic("Unsupported test file version") + if captchaSiteverifyTestsFile.Version != expectedCaptchaSiteverifyTestsFileVersion { + panic("Unsupported captcha siteverify test file version") + } + if riskIntelligenceRetrieveTestsFile.Version != expectedRiskIntelligenceRetrieveTestsFileVersion { + panic("Unsupported risk intelligence retrieve test file version") } - if len(tf.Tests) == 0 { - panic("No tests found") + if len(captchaSiteverifyTestsFile.Tests) == 0 { + panic("No captcha siteverify tests found") + } + if len(riskIntelligenceRetrieveTestsFile.Tests) == 0 { + panic("No risk intelligence retrieve tests found") } - responseToCase := make(map[string]model.TestCase) + responseToCaptchaSiteverifyCase := make(map[string]model.CaptchaSiteverifyTestCase) + for _, test := range captchaSiteverifyTestsFile.Tests { + responseToCaptchaSiteverifyCase[test.Response] = test + } - for _, test := range tf.Tests { - responseToCase[test.Response] = test + responseToRiskIntelligenceRetrieveCase := make(map[string]model.RiskIntelligenceRetrieveTestCase) + for _, test := range riskIntelligenceRetrieveTestsFile.Tests { + responseToRiskIntelligenceRetrieveCase[test.Token] = test } mux := http.NewServeMux() - handler := func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - + validateHeaders := func(w http.ResponseWriter, r *http.Request) bool { if r.Header.Get("X-Api-Key") == "" { http.Error(w, "Missing X-Api-Key header", http.StatusBadRequest) fmt.Println("Missing X-Api-Key header") - return + return false } ct := r.Header.Get("Content-Type") - if ct != "application/json" { fmt.Printf("Invalid content-type header %s\n", ct) http.Error(w, "Invalid content type header "+ct, http.StatusBadRequest) - return + return false } - // Check that `Frc-Sdk` header is present. if r.Header.Get("Frc-Sdk") == "" { fmt.Println("Missing Frc-Sdk header") http.Error(w, "Missing Frc-Sdk header", http.StatusBadRequest) + return false + } + return true + } + + captchaSiteverifyHandler := func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + if !validateHeaders(w, r) { return } - var req wire.SiteverifyRequest + var req wire.CaptchaSiteverifyRequest err := json.NewDecoder(r.Body).Decode(&req) if err != nil { http.Error(w, fmt.Sprintf("Failed to decode request: %s", err), http.StatusBadRequest) return } - testCase, ok := responseToCase[req.Response] + testCase, ok := responseToCaptchaSiteverifyCase[req.Response] if !ok { http.Error(w, fmt.Sprintf("No test case found for response: %s", req.Response), http.StatusBadRequest) return @@ -108,10 +136,38 @@ func serve(port int, testsPath string) { } } - mux.HandleFunc(defaultSiteverifyEndpoint, handler) - mux.HandleFunc(defaultTestsJSONEndpoint, func(w http.ResponseWriter, r *http.Request) { + riskIntelligenceRetrieveHandler := func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + if !validateHeaders(w, r) { + return + } + + var req wire.RiskIntelligenceRetrieveRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to decode request: %s", err), http.StatusBadRequest) + return + } + + testCase, ok := responseToRiskIntelligenceRetrieveCase[req.Token] + if !ok { + http.Error(w, fmt.Sprintf("No test case found for token: %s", req.Token), http.StatusBadRequest) + return + } + w.Header().Add("Content-Type", "application/json") + + w.WriteHeader(testCase.RiskIntelligenceRetrieveStatusCode) + _, err = w.Write([]byte(testCase.RiskIntelligenceRetrieveResponse)) + if err != nil { + fmt.Println("Failed to write response: ", err) + return + } + } + + serveJSON := func(w http.ResponseWriter, value any) { w.Header().Add("Content-Type", "application/json") - j, err := json.Marshal(tf) + j, err := json.Marshal(value) if err != nil { http.Error(w, fmt.Sprintf("Failed to marshal tests: %v", err), http.StatusInternalServerError) return @@ -121,9 +177,22 @@ func serve(port int, testsPath string) { fmt.Println("Failed to write response: ", err) return } + } + + mux.HandleFunc(defaultCaptchaSiteverifyEndpoint, captchaSiteverifyHandler) + mux.HandleFunc(defaultRiskIntelligenceRetrieveEndpoint, riskIntelligenceRetrieveHandler) + + mux.HandleFunc(defaultLegacyTestsJSONEndpoint, func(w http.ResponseWriter, r *http.Request) { + serveJSON(w, captchaSiteverifyTestsFile) + }) + mux.HandleFunc(defaultCaptchaSiteverifyTestsJSONEndpoint, func(w http.ResponseWriter, r *http.Request) { + serveJSON(w, captchaSiteverifyTestsFile) + }) + mux.HandleFunc(defaultRiskIntelligenceRetrieveTestsJSONEndpoint, func(w http.ResponseWriter, r *http.Request) { + serveJSON(w, riskIntelligenceRetrieveTestsFile) }) - fmt.Printf("Serving test cases version %d on port %d", tf.Version, port) + fmt.Printf("Serving test cases (captcha siteverify v%d, risk intelligence retrieve v%d) on port %d\n", captchaSiteverifyTestsFile.Version, riskIntelligenceRetrieveTestsFile.Version, port) err = http.ListenAndServe(fmt.Sprintf(":%d", port), mux) if err != nil { if err == http.ErrServerClosed { diff --git a/friendly-captcha-sdk-testserver/model/model.go b/friendly-captcha-sdk-testserver/model/model.go index 608a9ed..de6cded 100644 --- a/friendly-captcha-sdk-testserver/model/model.go +++ b/friendly-captcha-sdk-testserver/model/model.go @@ -5,23 +5,42 @@ import ( ) // What the SDK should conclude from the API response. -type TestCaseExpectation struct { +type CaptchaSiteverifyTestCaseExpectation struct { ShouldAccept bool `json:"should_accept"` WasAbleToVerify bool `json:"was_able_to_verify"` IsClientError bool `json:"is_client_error"` } -type TestCasesFile struct { - Version int `json:"version"` - Tests []TestCase `json:"tests"` +type CaptchaSiteverifyTestCasesFile struct { + Version int `json:"version"` + Tests []CaptchaSiteverifyTestCase `json:"tests"` } -type TestCase struct { +type CaptchaSiteverifyTestCase struct { Name string `json:"name"` Response string `json:"response"` Strict bool `json:"strict"` - SiteverifyResponse json.RawMessage `json:"siteverify_response"` - SiteverifyStatusCode int `json:"siteverify_status_code"` - Expectation TestCaseExpectation `json:"expectation"` + SiteverifyResponse json.RawMessage `json:"siteverify_response"` + SiteverifyStatusCode int `json:"siteverify_status_code"` + Expectation CaptchaSiteverifyTestCaseExpectation `json:"expectation"` +} + +type RiskIntelligenceRetrieveTestCaseExpectation struct { + WasAbleToRetrieve bool `json:"was_able_to_retrieve"` + IsClientError bool `json:"is_client_error"` +} + +type RiskIntelligenceRetrieveTestCasesFile struct { + Version int `json:"version"` + Tests []RiskIntelligenceRetrieveTestCase `json:"tests"` +} + +type RiskIntelligenceRetrieveTestCase struct { + Name string `json:"name"` + Token string `json:"token"` + + RiskIntelligenceRetrieveResponse json.RawMessage `json:"retrieve_response"` + RiskIntelligenceRetrieveStatusCode int `json:"retrieve_status_code"` + Expectation RiskIntelligenceRetrieveTestCaseExpectation `json:"expectation"` } diff --git a/friendly-captcha-sdk-testserver/wire/siteverify.go b/friendly-captcha-sdk-testserver/wire/captcha_siteverify.go similarity index 85% rename from friendly-captcha-sdk-testserver/wire/siteverify.go rename to friendly-captcha-sdk-testserver/wire/captcha_siteverify.go index 9dfdcc9..1a35116 100644 --- a/friendly-captcha-sdk-testserver/wire/siteverify.go +++ b/friendly-captcha-sdk-testserver/wire/captcha_siteverify.go @@ -1,6 +1,6 @@ package wire -type SiteverifyRequest struct { +type CaptchaSiteverifyRequest struct { // Optional, the sitekey that you want to make sure the puzzle was generated from. // // Not really used in this mock server. diff --git a/friendly-captcha-sdk-testserver/wire/risk_intelligence_retrieve.go b/friendly-captcha-sdk-testserver/wire/risk_intelligence_retrieve.go new file mode 100644 index 0000000..9372924 --- /dev/null +++ b/friendly-captcha-sdk-testserver/wire/risk_intelligence_retrieve.go @@ -0,0 +1,5 @@ +package wire + +type RiskIntelligenceRetrieveRequest struct { + Token string `json:"token"` +} From f965d0fd87309b5c7efbc358577410b58eabefb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rok=20Su=C5=A1nik?= Date: Mon, 2 Mar 2026 18:59:45 +0100 Subject: [PATCH 2/2] make loading generic --- .../fixtures/load.go | 46 +++++++------------ friendly-captcha-sdk-testserver/main.go | 4 +- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/friendly-captcha-sdk-testserver/fixtures/load.go b/friendly-captcha-sdk-testserver/fixtures/load.go index 690a50d..b40b24d 100644 --- a/friendly-captcha-sdk-testserver/fixtures/load.go +++ b/friendly-captcha-sdk-testserver/fixtures/load.go @@ -16,47 +16,35 @@ var captchaSiteverifyTestCasesFileBytes []byte //go:embed risk_intelligence_retrieve_test_cases.json var riskIntelligenceRetrieveTestCasesFileBytes []byte -// LoadCaptchaSiteverify loads captcha siteverify test cases from the embedded JSON file or from the provided filepath. -func LoadCaptchaSiteverify(filepath string) (model.CaptchaSiteverifyTestCasesFile, error) { +// 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 := captchaSiteverifyTestCasesFileBytes + b := embedded - // Only read from disk if a filepath was provided. if filepath != "" { b, err = os.ReadFile(filepath) if err != nil { - return model.CaptchaSiteverifyTestCasesFile{}, fmt.Errorf("failed to read test cases: %w", err) + return zero, fmt.Errorf("failed to read test cases: %w", err) } } - var testCases model.CaptchaSiteverifyTestCasesFile - err = json.Unmarshal(b, &testCases) + var out T + err = json.Unmarshal(b, &out) if err != nil { - return model.CaptchaSiteverifyTestCasesFile{}, 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 } -// LoadRiskIntelligenceRetrieve loads risk intelligence retrieve test cases from the embedded JSON file -// or from the provided filepath. -func LoadRiskIntelligenceRetrieve(filepath string) (model.RiskIntelligenceRetrieveTestCasesFile, error) { - var err error - b := riskIntelligenceRetrieveTestCasesFileBytes - - // Only read from disk if a filepath was provided. - if filepath != "" { - b, err = os.ReadFile(filepath) - if err != nil { - return model.RiskIntelligenceRetrieveTestCasesFile{}, fmt.Errorf("failed to read retrieve test cases: %w", err) - } - } - - var testCases model.RiskIntelligenceRetrieveTestCasesFile - err = json.Unmarshal(b, &testCases) - if err != nil { - return model.RiskIntelligenceRetrieveTestCasesFile{}, fmt.Errorf("failed to parse retrieve test cases as JSON: %w", err) - } +// 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) +} - return testCases, nil +// 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/main.go b/friendly-captcha-sdk-testserver/main.go index f4a500d..607fe43 100644 --- a/friendly-captcha-sdk-testserver/main.go +++ b/friendly-captcha-sdk-testserver/main.go @@ -52,11 +52,11 @@ func main() { func serve(port int, siteverifytestsPath string, retrieveTestsPath string) { captchaSiteverifyTestsFile, err := fixtures.LoadCaptchaSiteverify(siteverifytestsPath) if err != nil { - panic(err) + panic("failed to load captcha siteverify test cases: " + err.Error()) } riskIntelligenceRetrieveTestsFile, err := fixtures.LoadRiskIntelligenceRetrieve(retrieveTestsPath) if err != nil { - panic(err) + panic("failed to load risk intelligence retrieve test cases: " + err.Error()) } if captchaSiteverifyTestsFile.Version != expectedCaptchaSiteverifyTestsFileVersion {