Skip to content

Commit

Permalink
Refactor to support mocking google API (#6)
Browse files Browse the repository at this point in the history
* Refactor to support mocking google API

* make recaptcha struct attrs public for external mocking

* update readme
  • Loading branch information
cbarraford authored and ezzarghili committed Mar 6, 2018
1 parent b4e5104 commit 10856b0
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 28 deletions.
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?)
}
Expand All @@ -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?)
}
Expand All @@ -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.
Expand All @@ -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/)
[MIT](https://choosealicense.com/licenses/mit/)
60 changes: 36 additions & 24 deletions recaptcha.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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),
)
Expand Down
134 changes: 134 additions & 0 deletions recaptcha_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 10856b0

Please sign in to comment.