From 980c1d99961945da210298c3740602a116d04ab9 Mon Sep 17 00:00:00 2001 From: mohamed ez-zarghili Date: Wed, 26 Dec 2018 01:33:25 +0000 Subject: [PATCH] introducing recaptcha v3 api and adding more options for verification (#14) --- README.md | 80 ++++++-- recaptcha.go | 133 ++++++++++--- recaptcha_test.go | 490 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 638 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 5eca29d..112672c 100644 --- a/README.md +++ b/README.md @@ -2,53 +2,101 @@ [![Build Status](https://travis-ci.org/ezzarghili/recaptcha-go.svg?branch=master)](https://travis-ci.org/ezzarghili/recaptcha-go) -Google reCAPTCHA v2 form submittion in golang +Google reCAPTCHA v2 & v3 form submittion verification in golang ## Usage -Install the package in your environment by using a stable API version, see latest version in release page +The API has changed form last version hence the new major version change. +Old API is still available using the package `gopkg.in/ezzarghili/recaptcha-go.v2` although it does not provide all options available in this version +As always install the package in your environment by using a stable API version, see latest version in release page. ```bash -go get gopkg.in/ezzarghili/recaptcha-go.v2 +go get -u gopkg.in/ezzarghili/recaptcha-go.v3 ``` +### recaptcha v2 API +```go +import "gopkg.in/ezzarghili/recaptcha-go.v3" +func main(){ + captcha := recaptcha.NewReCAPTCHA(recaptchaSecret, recaptcha.V2, timeout) // for v2 API get your secret from https://www.google.com/recaptcha/admin +} +``` + +Now everytime you need to verify a V2 API client with no special options request use + +```go +err := captcha.Verify(recaptchaResponse) +if err != nil { + // do something with err (log?) +} +// proceed +``` +For specific options use the `VerifyWithOptions` method +Availavle options for the v2 api are: -To use it within your own code +```go + Hostname string + ApkPackageName string + ResponseTime float64 + RemoteIP string +``` +Other v3 options are ignored and method will return nil when succeeded + +```go +err := captcha.VerifyWithOptions(recaptchaResponse, , VerifyOption{RemoteIP: "123.123.123.123"}) +if err != nil { + // do something with err (log?) +} +// proceed +``` +### recaptcha v3 API ```go -import "github.com/ezzarghili/recaptcha-go" +import "github.com/ezzarghili/recaptcha-go.v3" func main(){ - captcha := recaptcha.NewReCAPTCHA(recaptchaSecret) // get your secret from https://www.google.com/recaptcha/admin + captcha := recaptcha.NewReCAPTCHA(recaptchaSecret, recaptcha.V3, timeout) // for v3 API use https://g.co/recaptcha/v3 (apperently the same admin UI at the time of writing) } ``` -Now everytime you need to verify a client request use +Now everytime you need to verify a V2 API client with no special options request use ```go -success, err := captcha.Verify(recaptchaResponse, ClientRemoteIP) -if err !=nil { +err := captcha.Verify(recaptchaResponse) +if err != nil { // do something with err (log?) } -// proceed with success (true|false) +// proceed ``` +For specific options use the `VerifyWithOptions` method +Availavle options for the v3 api are: -or +```go + Treshold float32 + Action string + Hostname string + ApkPackageName string + ResponseTime float64 + RemoteIP string +``` ```go -success, err := captcha.VerifyNoRemoteIP(recaptchaResponse) -if err !=nil { +err := captcha.VerifyWithOptions(recaptchaResponse, , VerifyOption{Action: "hompage", Treshold: 0.8}) +if err != nil { // do something with err (log?) } -// proceed with success (true|false) +// proceed ``` while `recaptchaResponse` is the form value with name `g-recaptcha-response` sent back by recaptcha server and set for you in the form when user answers the challenge -Both `recaptcha.Verify` and `recaptcha.VerifyNoRemoteIP` return a `bool` and `error` values `(bool, error)` +Both `recaptcha.Verify` and `recaptcha.VerifyWithOptions` return a `error` or `nil` if successful + +Use the `error` to check for issues with the secret, connection with the server, options mismatches and incorrect solution. -Use the `error` to check for issues with the secret and connection in the server, and use the `bool` value to verify if the client answered the challenge correctly +This version made timeout explcit to make sure users have the possiblity to set the underling http client timeout suitable for their implemetation. ### Run Tests Use the standard go means of running test. +You can also check examples of usable in the tests. ``` go test diff --git a/recaptcha.go b/recaptcha.go index 09a4838..8260d35 100644 --- a/recaptcha.go +++ b/recaptcha.go @@ -11,6 +11,17 @@ import ( const reCAPTCHALink = "https://www.google.com/recaptcha/api/siteverify" +// VERSION the recaptcha api version +type VERSION int8 + +const ( + // V2 recaptcha api v2 + V2 VERSION = iota + // V3 recaptcha api v3, more details can be found here : https://developers.google.com/recaptcha/docs/v3 + V3 + DEFAULT_TRESHOLD float32 = 0.5 +) + type reCHAPTCHARequest struct { Secret string `json:"secret"` Response string `json:"response"` @@ -18,10 +29,13 @@ type reCHAPTCHARequest struct { } type reCHAPTCHAResponse struct { - Success bool `json:"success"` - ChallengeTS time.Time `json:"challenge_ts"` - Hostname string `json:"hostname"` - ErrorCodes []string `json:"error-codes,omitempty"` + Success bool `json:"success"` + ChallengeTS time.Time `json:"challenge_ts"` + Hostname string `json:"hostname,omitempty"` + ApkPackageName string `json:"apk_package_name,omitempty"` + Action string `json:"action,omitempty"` + Score float32 `json:"score,omitempty"` + ErrorCodes []string `json:"error-codes,omitempty"` } // custom client so we can mock in tests @@ -29,43 +43,74 @@ type netClient interface { PostForm(url string, formValues url.Values) (resp *http.Response, err error) } +type clock interface { + Since(t time.Time) time.Duration +} + +type realClock struct { +} + +func (realClock) Since(t time.Time) time.Duration { + return time.Since(t) +} + +// ReCAPTCHA recpatcha holder struct, make adding mocking code simpler type ReCAPTCHA struct { Client netClient Secret string ReCAPTCHALink string + Version VERSION + Timeout uint + horloge clock } -// Create new ReCAPTCHA with the reCAPTCHA secret optained from https://www.google.com/recaptcha/admin -func NewReCAPTCHA(ReCAPTCHASecret string) (ReCAPTCHA, error) { +// NewReCAPTCHA Create new ReCAPTCHA with the v2 reCAPTCHA secret optained from https://www.google.com/recaptcha/admin +// or https://www.google.com/recaptcha/admin +func NewReCAPTCHA(ReCAPTCHASecret string, version VERSION, timeout uint) (ReCAPTCHA, error) { if ReCAPTCHASecret == "" { - return ReCAPTCHA{}, fmt.Errorf("Recaptcha secret cannot be blank.") + return ReCAPTCHA{}, fmt.Errorf("recaptcha secret cannot be blank") } return ReCAPTCHA{ Client: &http.Client{ - // Go http client does not set a default timeout for request, so we need - // to set one for worse cases when the server hang, we need to make this available in the API - // to make it possible this library's users to change it, for now a 10s timeout seems reasonable - Timeout: 10 * time.Second, + Timeout: time.Duration(timeout) * time.Second, }, + horloge: &realClock{}, Secret: ReCAPTCHASecret, ReCAPTCHALink: reCAPTCHALink, + Timeout: timeout, + Version: version, }, nil } // Verify returns (true, nil) if no error the client answered the challenge correctly and have correct remoteIP -func (r *ReCAPTCHA) Verify(challengeResponse string, remoteIP string) (bool, error) { - body := reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse, RemoteIP: remoteIP} - return r.confirm(body) +func (r *ReCAPTCHA) Verify(challengeResponse string) error { + body := reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse} + return r.confirm(body, VerifyOption{}) } -// VerifyNoRemoteIP returns (true, nil) if no error and the client answered the challenge correctly -func (r *ReCAPTCHA) VerifyNoRemoteIP(challengeResponse string) (bool, error) { - body := reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse} - return r.confirm(body) +// VerifyOption verification options expected for the challenge +type VerifyOption struct { + Treshold float32 // ignored in v2 recaptcha + Action string // ignored in v2 recaptcha + Hostname string + ApkPackageName string + ResponseTime float64 + RemoteIP string } -func (r *ReCAPTCHA) confirm(recaptcha reCHAPTCHARequest) (Ok bool, Err error) { - Ok, Err = false, nil +// VerifyWithOptions returns (true, nil) if no error the client answered the challenge correctly and have correct remoteIP +func (r *ReCAPTCHA) VerifyWithOptions(challengeResponse string, options VerifyOption) error { + var body reCHAPTCHARequest + if options.RemoteIP == "" { + body = reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse} + } else { + body = reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse, RemoteIP: options.RemoteIP} + } + return r.confirm(body, options) +} + +func (r *ReCAPTCHA) confirm(recaptcha reCHAPTCHARequest, options VerifyOption) (Err error) { + Err = nil var formValues url.Values if recaptcha.RemoteIP != "" { formValues = url.Values{"secret": {recaptcha.Secret}, "remoteip": {recaptcha.RemoteIP}, "response": {recaptcha.Response}} @@ -74,21 +119,61 @@ func (r *ReCAPTCHA) confirm(recaptcha reCHAPTCHARequest) (Ok bool, Err error) { } response, err := r.Client.PostForm(r.ReCAPTCHALink, formValues) if err != nil { - Err = fmt.Errorf("error posting to recaptcha endpoint: %s", err) + Err = fmt.Errorf("error posting to recaptcha endpoint: '%s'", err) return } defer response.Body.Close() resultBody, err := ioutil.ReadAll(response.Body) if err != nil { - Err = fmt.Errorf("couldn't read response body: %s", err) + Err = fmt.Errorf("couldn't read response body: '%s'", err) return } var result reCHAPTCHAResponse err = json.Unmarshal(resultBody, &result) if err != nil { - Err = fmt.Errorf("invalid response body json: %s", err) + Err = fmt.Errorf("invalid response body json: '%s'", err) + return + } + + if options.Hostname != "" && options.Hostname != result.Hostname { + Err = fmt.Errorf("invalid response hostname '%s', while expecting '%s'", result.Hostname, options.Hostname) + return + } + + if options.ApkPackageName != "" && options.ApkPackageName != result.ApkPackageName { + Err = fmt.Errorf("invalid response ApkPackageName '%s', while expecting '%s'", result.ApkPackageName, options.ApkPackageName) return } - Ok = result.Success + + if options.ResponseTime != 0 { + duration := r.horloge.Since(result.ChallengeTS).Seconds() + if options.ResponseTime < duration { + Err = fmt.Errorf("time spent in resolving challenge '%f', while expecting maximum '%f'", duration, options.ResponseTime) + return + } + } + if r.Version == V3 { + if options.Action != "" && options.Action != result.Action { + Err = fmt.Errorf("invalid response action '%s', while expecting '%s'", result.Action, options.Action) + return + } + if options.Treshold != 0 && options.Treshold >= result.Score { + Err = fmt.Errorf("received score '%f', while expecting minimum '%f'", result.Score, options.Treshold) + return + } + if options.Treshold == 0 && DEFAULT_TRESHOLD >= result.Score { + Err = fmt.Errorf("received score '%f', while expecting minimum '%f'", result.Score, DEFAULT_TRESHOLD) + return + } + } + if result.ErrorCodes != nil { + Err = fmt.Errorf("remote error codes: %v", result.ErrorCodes) + return + } + if !result.Success && recaptcha.RemoteIP != "" { + Err = fmt.Errorf("invalid challenge solution or remote IP") + } else if !result.Success { + Err = fmt.Errorf("invalid challenge solution") + } return } diff --git a/recaptcha_test.go b/recaptcha_test.go index 0968a25..bc1d910 100644 --- a/recaptcha_test.go +++ b/recaptcha_test.go @@ -7,6 +7,7 @@ import ( "net/url" "strings" "testing" + "time" . "gopkg.in/check.v1" ) @@ -17,16 +18,67 @@ type ReCaptchaSuite struct{} var _ = Suite(&ReCaptchaSuite{}) -type mockSuccessClient struct{} +func (s *ReCaptchaSuite) TestNewReCAPTCHA(c *C) { + captcha, err := NewReCAPTCHA("my secret", V2, 10) + c.Assert(err, IsNil) + c.Check(captcha.Secret, Equals, "my secret") + c.Check(captcha.Version, Equals, V2) + c.Check(captcha.Timeout, Equals, (uint)(10)) + c.Check(captcha.ReCAPTCHALink, Equals, reCAPTCHALink) -func (*mockSuccessClient) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + captcha, err = NewReCAPTCHA("", V2, 10) + c.Assert(err, NotNil) +} + +type mockInvalidClient struct{} +type mockUnavailableClient struct{} + +func (*mockInvalidClient) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` bogus json `)) + return +} + +func (*mockUnavailableClient) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "Not Found", + StatusCode: 404, + } + resp.Body = ioutil.NopCloser(nil) + err = fmt.Errorf("Unable to connect to server") + return +} + +func (s *ReCaptchaSuite) TestConfirm(c *C) { + captcha := ReCAPTCHA{ + Client: &mockInvalidClient{}, + } + body := reCHAPTCHARequest{Secret: "", Response: ""} + + err := captcha.confirm(body, VerifyOption{}) + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, "invalid response body json:.*") + + captcha.Client = &mockUnavailableClient{} + err = captcha.confirm(body, VerifyOption{}) + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, "error posting to recaptcha endpoint:.*") + +} + +type mockInvalidSolutionClient struct{} + +func (*mockInvalidSolutionClient) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { resp = &http.Response{ Status: "200 OK", StatusCode: 200, } resp.Body = ioutil.NopCloser(strings.NewReader(` { - "success": true, + "success": false, "challenge_ts": "2018-03-06T03:41:29+00:00", "hostname": "test.com" } @@ -34,9 +86,34 @@ func (*mockSuccessClient) PostForm(url string, formValues url.Values) (resp *htt return } -type mockFailedClient struct{} +func (s *ReCaptchaSuite) TestVerifyInvalidSolutionNoRemoteIp(c *C) { + captcha := ReCAPTCHA{ + Client: &mockInvalidSolutionClient{}, + } -func (*mockFailedClient) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + err := captcha.Verify("mycode") + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, "invalid challenge solution") +} + +type mockSuccessClientNoOptions struct{} +type mockFailedClientNoOptions struct{} + +func (*mockSuccessClientNoOptions) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": true, + "challenge_ts": "2018-03-06T03:41:29+00:00", + "hostname": "test.com" + } + `)) + return +} +func (*mockFailedClientNoOptions) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { resp = &http.Response{ Status: "200 OK", StatusCode: 200, @@ -46,72 +123,434 @@ func (*mockFailedClient) PostForm(url string, formValues url.Values) (resp *http "success": false, "challenge_ts": "2018-03-06T03:41:29+00:00", "hostname": "test.com", - "error-codes": ["bad-request"] + "error-codes": ["invalid-input-response","bad-request"] } `)) return } +func (s *ReCaptchaSuite) TestVerifyWithoutOptions(c *C) { + captcha := ReCAPTCHA{ + Client: &mockSuccessClientNoOptions{}, + } -type mockInvalidClient struct{} + err := captcha.Verify("mycode") + c.Assert(err, IsNil) -// bad json body -func (*mockInvalidClient) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + captcha.Client = &mockFailedClientNoOptions{} + err = captcha.Verify("mycode") + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, "remote error codes:.*") + +} + +type mockSuccessClientWithRemoteIPOption struct{} +type mockFailClientWithRemoteIPOption struct{} + +func (*mockSuccessClientWithRemoteIPOption) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { resp = &http.Response{ Status: "200 OK", StatusCode: 200, } - resp.Body = ioutil.NopCloser(strings.NewReader(` bogus json `)) + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": true, + "challenge_ts": "2018-03-06T03:41:29+00:00", + "hostname": "test.com" + } + `)) return } +func (*mockFailClientWithRemoteIPOption) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": false, + "challenge_ts": "2018-03-06T03:41:29+00:00", + "hostname": "test.com" + } + `)) + return +} +func (s *ReCaptchaSuite) TestVerifyWithRemoteIPOption(c *C) { + captcha := ReCAPTCHA{ + Client: &mockSuccessClientWithRemoteIPOption{}, + } -type mockUnavailableClient struct{} + err := captcha.VerifyWithOptions("mycode", VerifyOption{RemoteIP: "123.123.123.123"}) + c.Assert(err, IsNil) -func (*mockUnavailableClient) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + captcha.Client = &mockFailClientWithRemoteIPOption{} + err = captcha.VerifyWithOptions("mycode", VerifyOption{RemoteIP: "123.123.123.123"}) + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, "invalid challenge solution or remote IP") + +} + +type mockSuccessClientWithHostnameOption struct{} +type mockFailClientWithHostnameOption struct{} + +func (*mockSuccessClientWithHostnameOption) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { resp = &http.Response{ - Status: "Not Found", - StatusCode: 404, + Status: "200 OK", + StatusCode: 200, } - resp.Body = ioutil.NopCloser(nil) - err = fmt.Errorf("Unable to connect to server") + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": true, + "challenge_ts": "2018-03-06T03:41:29+00:00", + "hostname": "test.com" + } + `)) + return +} +func (*mockFailClientWithHostnameOption) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": true, + "challenge_ts": "2018-03-06T03:41:29+00:00", + "hostname": "test2.com" + } + `)) return } +func (s *ReCaptchaSuite) TestVerifyWithHostnameOption(c *C) { + captcha := ReCAPTCHA{ + Client: &mockSuccessClientWithHostnameOption{}, + } + + err := captcha.VerifyWithOptions("mycode", VerifyOption{Hostname: "test.com"}) + c.Assert(err, IsNil) + + captcha.Client = &mockFailClientWithHostnameOption{} + err = captcha.VerifyWithOptions("mycode", VerifyOption{Hostname: "test.com"}) + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, "invalid response hostname 'test2.com', while expecting 'test.com'") + +} + +type mockClockWithinRespenseTime struct{} +type mockClockOverRespenseTime struct{} + +func (*mockClockWithinRespenseTime) Since(t time.Time) time.Duration { + return 1 * time.Second +} + +func (*mockClockOverRespenseTime) Since(t time.Time) time.Duration { + return 8 * time.Second +} + +func (s *ReCaptchaSuite) TestVerifyWithResponseOption(c *C) { + captcha := ReCAPTCHA{ + Client: &mockSuccessClientNoOptions{}, + horloge: &mockClockWithinRespenseTime{}, + } + + err := captcha.VerifyWithOptions("mycode", VerifyOption{ResponseTime: 5}) + c.Assert(err, IsNil) + + captcha.horloge = &mockClockOverRespenseTime{} + err = captcha.VerifyWithOptions("mycode", VerifyOption{ResponseTime: 5}) + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, "time spent in resolving challenge '8.000000', while expecting maximum '5.000000'") + +} + +type mockSuccessClientWithApkPackageNameOption struct{} +type mockFailClientWithApkPackageNameOption struct{} + +func (*mockSuccessClientWithApkPackageNameOption) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": true, + "challenge_ts": "2018-03-06T03:41:29+00:00", + "apk_package_name": "com.test.app" + } + `)) + return +} +func (*mockFailClientWithApkPackageNameOption) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": true, + "challenge_ts": "2018-03-06T03:41:29+00:00", + "apk_package_name": "com.test.app2" + } + `)) + return +} +func (s *ReCaptchaSuite) TestVerifyWithApkPackageNameOption(c *C) { + captcha := ReCAPTCHA{ + Client: &mockSuccessClientWithApkPackageNameOption{}, + } + + err := captcha.VerifyWithOptions("mycode", VerifyOption{ApkPackageName: "com.test.app"}) + c.Assert(err, IsNil) + + captcha.Client = &mockFailClientWithApkPackageNameOption{} + err = captcha.VerifyWithOptions("mycode", VerifyOption{ApkPackageName: "com.test.app"}) + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, "invalid response ApkPackageName 'com.test.app2', while expecting 'com.test.app'") + +} + +type mockV3SuccessClientWithActionOption struct{} +type mockV3FailClientWithActionOption struct{} + +func (*mockV3SuccessClientWithActionOption) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": true, + "challenge_ts": "2018-03-06T03:41:29+00:00", + "action": "homepage", + "score": 1 + } + `)) + return +} +func (*mockV3FailClientWithActionOption) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": true, + "challenge_ts": "2018-03-06T03:41:29+00:00", + "action": "homepage2", + "score": 1 + + } + `)) + return +} +func (s *ReCaptchaSuite) TestV3VerifyWithActionOption(c *C) { + captcha := ReCAPTCHA{ + Client: &mockV3SuccessClientWithActionOption{}, + Version: V3, + } + + err := captcha.VerifyWithOptions("mycode", VerifyOption{Action: "homepage"}) + c.Assert(err, IsNil) + + captcha.Client = &mockV3FailClientWithActionOption{} + err = captcha.VerifyWithOptions("mycode", VerifyOption{Action: "homepage"}) + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, "invalid response action 'homepage2', while expecting 'homepage'") + +} + +type mockV3SuccessClientWithTresholdOption struct{} +type mockV3FailClientWithTresholdOption struct{} + +func (*mockV3SuccessClientWithTresholdOption) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": true, + "challenge_ts": "2018-03-06T03:41:29+00:00", + "score": 0.8 + } + `)) + return +} +func (*mockV3FailClientWithTresholdOption) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": true, + "challenge_ts": "2018-03-06T03:41:29+00:00", + "score": 0.23 + } + `)) + return +} +func (s *ReCaptchaSuite) TestV3VerifyWithTresholdOption(c *C) { + captcha := ReCAPTCHA{ + Client: &mockV3SuccessClientWithTresholdOption{}, + Version: V3, + } + + err := captcha.VerifyWithOptions("mycode", VerifyOption{Treshold: 0.6}) + c.Assert(err, IsNil) + + captcha.Client = &mockV3FailClientWithTresholdOption{} + err = captcha.VerifyWithOptions("mycode", VerifyOption{Treshold: 0.6}) + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, "received score '0.230000', while expecting minimum '0.600000'") + err = captcha.VerifyWithOptions("mycode", VerifyOption{}) + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, "received score '0.230000', while expecting minimum '0.500000'") +} + +type mockV2SuccessClientWithV3IgnoreOptions struct{} + +func (*mockV2SuccessClientWithV3IgnoreOptions) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": true, + "challenge_ts": "2018-03-06T03:41:29+00:00", + } + `)) + return +} +func (s *ReCaptchaSuite) TestV2VerifyWithV3IgnoreOptions(c *C) { + captcha := ReCAPTCHA{ + Client: &mockV3SuccessClientWithTresholdOption{}, + Version: V2, + } + err := captcha.VerifyWithOptions("mycode", VerifyOption{Action: "homepage", Treshold: 0.5}) + c.Assert(err, IsNil) +} + +/* +func (*mockSuccessClient) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": true, + "challenge_ts": "2018-03-06T03:41:29+00:00", + "hostname": "test.com" + } + `)) + return +} + +func (*mockSuccessClientHostnameOption) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": true, + "challenge_ts": "2018-03-06T03:41:29+00:00", + "hostname": "valid.com" + } + `)) + return +} + +func (*mockFailedClientHostnameOption) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": true, + "challenge_ts": "2018-03-06T03:41:29+00:00", + "hostname": "invalid.com" + } + `)) + return +} + +func (*mockFailedClient) PostForm(url string, formValues url.Values) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` + { + "success": false, + "challenge_ts": "2018-03-06T03:41:29+00:00", + "hostname": "test.com", + "error-codes": ["bad-request"] + } + `)) + return +} + +// bad json body func (s *ReCaptchaSuite) TestNewReCAPTCHA(c *C) { - captcha, err := NewReCAPTCHA("my secret") + captcha, err := NewReCAPTCHA("my secret", V2, 10) c.Assert(err, IsNil) c.Check(captcha.Secret, Equals, "my secret") + c.Check(captcha.Version, Equals, V2) + c.Check(captcha.Timeout, Equals, (uint)(10)) c.Check(captcha.ReCAPTCHALink, Equals, reCAPTCHALink) - captcha, err = NewReCAPTCHA("") + captcha, err = NewReCAPTCHA("", V2, 10) c.Assert(err, NotNil) } -func (s *ReCaptchaSuite) TestVerifyWithClientIP(c *C) { +func (s *ReCaptchaSuite) TestVerifyV2WithoutClientIP(c *C) { captcha := ReCAPTCHA{ - Client: &mockSuccessClient{}, + Client: &mockSuccessClient{}, + Version: V2, } - success, err := captcha.Verify("mycode", "127.0.0.1") + success, err := captcha.Verify("mycode") c.Assert(err, IsNil) c.Check(success, Equals, true) captcha.Client = &mockFailedClient{} - success, err = captcha.Verify("mycode", "127.0.0.1") + success, err = captcha.Verify("mycode") + c.Assert(err, IsNil) + c.Check(success, Equals, false) +} + +func (s *ReCaptchaSuite) TestV3VerifyWithCHostnameOption(c *C) { + captcha := ReCAPTCHA{ + Client: &mockSuccessClientHostnameOption{}, + Version: V3, + } + + success, err := captcha.VerifyWithOptions("mycode", VerifyOption{Hostname: "valid.com"}) c.Assert(err, IsNil) + c.Check(success, Equals, true) + + captcha.Client = &mockFailedClientHostnameOption{} + success, err = captcha.VerifyWithOptions("mycode", VerifyOption{Hostname: "valid.com"}) + c.Assert(err, NotNil) c.Check(success, Equals, false) } -func (s *ReCaptchaSuite) TestVerifyWithoutClientIP(c *C) { +/* +func (s *ReCaptchaSuite) TestVerifyWithClientIP(c *C) { captcha := ReCAPTCHA{ Client: &mockSuccessClient{}, } - success, err := captcha.VerifyNoRemoteIP("mycode") + success, err := captcha.Verify("mycode", "127.0.0.1") c.Assert(err, IsNil) c.Check(success, Equals, true) captcha.Client = &mockFailedClient{} - success, err = captcha.VerifyNoRemoteIP("mycode") + success, err = captcha.Verify("mycode", "127.0.0.1") c.Assert(err, IsNil) c.Check(success, Equals, false) } @@ -132,3 +571,4 @@ func (s *ReCaptchaSuite) TestConfirm(c *C) { c.Assert(err, NotNil) c.Check(success, Equals, false) } +*/