From 10856b0c6c9f48c132ee2d8efca59196e12a9efa Mon Sep 17 00:00:00 2001 From: Chad Barraford Date: Tue, 6 Mar 2018 11:23:57 -0300 Subject: [PATCH] Refactor to support mocking google API (#6) * Refactor to support mocking google API * make recaptcha struct attrs public for external mocking * update readme --- README.md | 15 ++++-- recaptcha.go | 60 ++++++++++++--------- recaptcha_test.go | 134 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+), 28 deletions(-) create mode 100644 recaptcha_test.go diff --git a/README.md b/README.md index 4f4b634..479441a 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,14 @@ To use it within your own code ```go import "github.com/ezzarghili/recaptcha-go" func main(){ - recaptcha.Init (recaptchaSecret) // get your secret from https://www.google.com/recaptcha/admin + captcha := recaptcha.NewReCAPTCHA(recaptchaSecret) // get your secret from https://www.google.com/recaptcha/admin } ``` Now everytime you need to verify a client request use ```go -success, err :=recaptcha.Verify(recaptchaResponse, ClientRemoteIP) +success, err := captcha.Verify(recaptchaResponse, ClientRemoteIP) if err !=nil { // do something with err (log?) } @@ -34,7 +34,7 @@ if err !=nil { or ```go -success, err :=recaptcha.VerifyNoRemoteIP(recaptchaResponse) +success, err := captcha.VerifyNoRemoteIP(recaptchaResponse) if err !=nil { // do something with err (log?) } @@ -47,6 +47,13 @@ Both `recaptcha.Verify` and `recaptcha.VerifyNoRemoteIP` return a `bool` and `er 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 +### Run Tests +Use the standard go means of running test. + +``` +go test +``` + ### Issues with this library If you have some problems with using this library, bug reports or enhancement please open an issue in the issues tracker. @@ -55,4 +62,4 @@ If you have some problems with using this library, bug reports or enhancement pl Let's go with something permitive should we ? -[MIT](https://choosealicense.com/licenses/mit/) \ No newline at end of file +[MIT](https://choosealicense.com/licenses/mit/) diff --git a/recaptcha.go b/recaptcha.go index 36d61eb..c1a9a02 100644 --- a/recaptcha.go +++ b/recaptcha.go @@ -4,13 +4,12 @@ import ( "bytes" "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "time" ) -var recaptchaSecret string - const reCAPTCHALink = "https://www.google.com/recaptcha/api/siteverify" type reCHAPTCHARequest struct { @@ -26,39 +25,52 @@ type reCHAPTCHAResponse struct { ErrorCodes []string `json:"error-codes,omitempty"` } -// Init initialize with the reCAPTCHA secret optained from https://www.google.com/recaptcha/admin -func Init(ReCAPTCHASecret string) { - recaptchaSecret = ReCAPTCHASecret +// custom client so we can mock in tests +type netClient interface { + Post(url string, contentType string, body io.Reader) (resp *http.Response, err error) +} + +type ReCAPTCHA struct { + Client netClient + Secret string + ReCAPTCHALink string +} + +// Create new ReCAPTCHA with the reCAPTCHA secret optained from https://www.google.com/recaptcha/admin +func NewReCAPTCHA(ReCAPTCHASecret string) (ReCAPTCHA, error) { + if ReCAPTCHASecret == "" { + 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, + }, + Secret: ReCAPTCHASecret, + ReCAPTCHALink: reCAPTCHALink, + }, nil } // Verify returns (true, nil) if no error the client answered the challenge correctly and have correct remoteIP -func Verify(challengeResponse string, remoteIP string) (bool, error) { - body := reCHAPTCHARequest{Secret: recaptchaSecret, Response: challengeResponse, RemoteIP: remoteIP} - return confirm(body) +func (r *ReCAPTCHA) Verify(challengeResponse string, remoteIP string) (bool, error) { + body := reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse, RemoteIP: remoteIP} + return r.confirm(body) } // VerifyNoRemoteIP returns (true, nil) if no error and the client answered the challenge correctly -func VerifyNoRemoteIP(challengeResponse string) (bool, error) { - body := reCHAPTCHARequest{Secret: recaptchaSecret, Response: challengeResponse} - return confirm(body) +func (r *ReCAPTCHA) VerifyNoRemoteIP(challengeResponse string) (bool, error) { + body := reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse} + return r.confirm(body) } -func confirm(recaptcha reCHAPTCHARequest) (Ok bool, Err error) { +func (r *ReCAPTCHA) confirm(recaptcha reCHAPTCHARequest) (Ok bool, Err error) { Ok, Err = false, nil - if recaptcha.Secret == "" { - Err = fmt.Errorf("recaptcha secret has not been set, please set recaptcha.Init(secret) before calling verification functions") - return - } - // 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 - netClient := &http.Client{ - Timeout: 10 * time.Second, - } formValue := []byte(`secret=` + recaptcha.Secret + `&response=` + recaptcha.Response) - response, err := netClient.Post( - reCAPTCHALink, + response, err := r.Client.Post( + r.ReCAPTCHALink, "application/x-www-form-urlencoded; charset=utf-8", bytes.NewBuffer(formValue), ) diff --git a/recaptcha_test.go b/recaptcha_test.go new file mode 100644 index 0000000..46e992e --- /dev/null +++ b/recaptcha_test.go @@ -0,0 +1,134 @@ +package recaptcha + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "strings" + "testing" + + . "gopkg.in/check.v1" +) + +func TestPackage(t *testing.T) { TestingT(t) } + +type ReCaptchaSuite struct{} + +var _ = Suite(&ReCaptchaSuite{}) + +type mockSuccessClient struct{} + +func (*mockSuccessClient) Post(url string, contentType string, body io.Reader) (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 +} + +type mockFailedClient struct{} + +func (*mockFailedClient) Post(url string, contentType string, body io.Reader) (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 +} + +type mockInvalidClient struct{} + +// bad json body +func (*mockInvalidClient) Post(url string, contentType string, body io.Reader) (resp *http.Response, err error) { + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + resp.Body = ioutil.NopCloser(strings.NewReader(` bogus json `)) + return +} + +type mockUnavailableClient struct{} + +func (*mockUnavailableClient) Post(url string, contentType string, body io.Reader) (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) TestNewReCAPTCHA(c *C) { + captcha, err := NewReCAPTCHA("my secret") + c.Assert(err, IsNil) + c.Check(captcha.Secret, Equals, "my secret") + c.Check(captcha.ReCAPTCHALink, Equals, reCAPTCHALink) + + captcha, err = NewReCAPTCHA("") + c.Assert(err, NotNil) +} + +func (s *ReCaptchaSuite) TestVerifyWithClientIP(c *C) { + captcha := ReCAPTCHA{ + Client: &mockSuccessClient{}, + } + + success, err := captcha.Verify("mycode", "127.0.0.1") + c.Assert(err, IsNil) + c.Check(success, Equals, true) + + captcha.Client = &mockFailedClient{} + success, err = captcha.Verify("mycode", "127.0.0.1") + c.Assert(err, IsNil) + c.Check(success, Equals, false) +} + +func (s *ReCaptchaSuite) TestVerifyWithoutClientIP(c *C) { + captcha := ReCAPTCHA{ + Client: &mockSuccessClient{}, + } + + success, err := captcha.VerifyNoRemoteIP("mycode") + c.Assert(err, IsNil) + c.Check(success, Equals, true) + + captcha.Client = &mockFailedClient{} + success, err = captcha.VerifyNoRemoteIP("mycode") + c.Assert(err, IsNil) + c.Check(success, Equals, false) +} + +func (s *ReCaptchaSuite) TestConfirm(c *C) { + // check that an invalid json body errors + captcha := ReCAPTCHA{ + Client: &mockInvalidClient{}, + } + body := reCHAPTCHARequest{Secret: "", Response: ""} + + success, err := captcha.confirm(body) + c.Assert(err, NotNil) + c.Check(success, Equals, false) + + captcha.Client = &mockUnavailableClient{} + success, err = captcha.confirm(body) + c.Assert(err, NotNil) + c.Check(success, Equals, false) +}