Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve docs, allow for easier mocking of verify result, minor fixes #8

Merged
merged 5 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"
)

// A ClientOption is a function that can be passed to NewClient to configure a new Client.
type ClientOption func(*Client) error

// A client for the Friendly Captcha API, see also the API docs at https://developer.friendlycaptcha.com
Expand Down Expand Up @@ -32,6 +33,7 @@ const (
euSiteverifyEndpointURL = "https://eu.frcapi.com/api/v2/captcha/siteverify"
)

// NewClient creates a new Friendly Captcha client with the given options.
func NewClient(opts ...ClientOption) (*Client, error) {
const (
defaultSiteverifyEndpoint = globalSiteverifyEndpointURL
Expand All @@ -52,19 +54,23 @@ func NewClient(opts ...ClientOption) (*Client, error) {
}

if c.APIKey == "" {
return nil, fmt.Errorf("you must set your Friendly Captcha API key using `WithAPIKey()` when creating a new client")
return nil, fmt.Errorf(
"you must set your Friendly Captcha API key using `WithAPIKey()` when creating a new client",
)
}

return c, nil
}

// WithAPIKey sets the API key for the client.
func WithAPIKey(apiKey string) ClientOption {
return func(c *Client) error {
c.APIKey = apiKey
return nil
}
}

// WithSitekey sets the sitekey for the client. This is optional.
func WithSitekey(sitekey string) ClientOption {
return func(c *Client) error {
c.Sitekey = sitekey
Expand All @@ -82,15 +88,16 @@ func WithStrictMode(strict bool) ClientOption {
}
}

// Takes a full URL, or the shorthands `"global"` or `"eu"` .
// Takes a full URL, or the shorthands `"global"` or `"eu"`.
func WithSiteverifyEndpoint(siteverifyEndpoint string) ClientOption {
if siteverifyEndpoint == "global" {
siteverifyEndpoint = globalSiteverifyEndpointURL
} else if siteverifyEndpoint == "eu" {
siteverifyEndpoint = euSiteverifyEndpointURL
}

return func(c *Client) error {
if siteverifyEndpoint == "global" {
siteverifyEndpoint = globalSiteverifyEndpointURL
} else if siteverifyEndpoint == "eu" {
siteverifyEndpoint = euSiteverifyEndpointURL
} else if siteverifyEndpoint == "" {
return fmt.Errorf("siteverifyEndpoint must not be empty")
}
c.SiteverifyEndpoint = siteverifyEndpoint
return nil
}
Expand Down
5 changes: 4 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ var ErrVerificationRequest = errors.New("verification request failed talking to
// This error signifies a non-200 response from the server. Usually this means that your API key was wrong.
// You should notify yourself if this happens, but it's usually still a good idea to accept the captcha even though
// we were unable to verify it: we don't want to lock users out.
var ErrVerificationFailedDueToClientError = errors.New("verification request failed due to a client error (check your credentials)")
var ErrVerificationFailedDueToClientError = errors.New(
"verification request failed due to a client error (check your credentials)",
)

// ErrorCode is an error code that the Friendly Captcha API can return.
type ErrorCode string

const (
Expand Down
40 changes: 37 additions & 3 deletions result.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import (
"fmt"
)

// VerifyResult wraps the response from the Friendly Captcha API when verifying a captcha, making it easier
// to work with. In the simplest case, you can just check `ShouldAccept` to see if the captcha was solved correctly or
// if you should accept it anyway (e.g. because the API was down).
type VerifyResult struct {
// Success is true if the captcha was solved correctly.
Success bool

// HTTP Response status code
// Status is the HTTP Response status code of the request to the Friendly Captcha API.
Status int

response VerifyResponse
Expand All @@ -18,6 +22,18 @@ type VerifyResult struct {
err error
}

// NewVerifyResult returns a new VerifyResult with the given response, status code, strict mode and error.
// This is generally only useful if you want to create a VerifyResult manually for testing purposes.
func NewVerifyResult(response VerifyResponse, status int, strict bool, err error) VerifyResult {
return VerifyResult{
Success: response.Success,
Status: status,
response: response,
strict: strict,
err: err,
}
}

// RequestError returns the error, if any (nil otherwise).
func (r VerifyResult) RequestError() error {
return r.err
Expand All @@ -31,6 +47,9 @@ func (r VerifyResult) Strict() bool {
return r.strict
}

// ShouldAccept returns true if you should allow the request to pass through.
// It is possible that verification wasn't possible, perhaps the API is unavailable. In that case this function will
// also return true, unless you enable `strict` mode for the client.
func (r VerifyResult) ShouldAccept() bool {
if r.WasAbleToVerify() {
return r.response.Success
Expand All @@ -39,15 +58,25 @@ func (r VerifyResult) ShouldAccept() bool {
if r.strict { // If Strict mode is enabled, we do not accept any captcha if there was an error.
return false
}
if errors.Is(r.err, ErrVerificationRequest) || errors.Is(r.err, ErrVerificationFailedDueToClientError) { // Failure to talk to Friendly Captcha verification API or client error (e.g. wrong API key)
if errors.Is(r.err, ErrVerificationRequest) ||

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I'd probably try break on the || here personally if the issue is long lines, I think the second errors.Is is hard to read now in that format. If the current version is added by golines then maybe it makes sense to keep as-is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is what golines made out of it.. Maybe there is some way to configure it, but for now I'll just follow its defaults

errors.Is(
r.err,
ErrVerificationFailedDueToClientError,
) { // Failure to talk to Friendly Captcha verification API or client error (e.g. wrong API key)
return true
}
return false
}

panic("Implementation error in friendly-captcha-go-sdk ShouldAccept: error should never be nil if success is false. " + fmt.Sprintf("%+v", r))
panic(
"Implementation error in friendly-captcha-go ShouldAccept: error should never be nil if success is false. " + fmt.Sprintf(
"%+v",
r,
),
)
}

// ShouldReject is the inverse of ShouldAccept.
func (r VerifyResult) ShouldReject() bool {
return !r.ShouldAccept()
}
Expand All @@ -66,14 +95,19 @@ func (r VerifyResult) IsErrorDueToClientError() bool {
return r.err != nil && errors.Is(r.err, ErrVerificationFailedDueToClientError)
}

// Response returns the response from the Friendly Captcha API.
func (r VerifyResult) Response() VerifyResponse {
return r.response
}

// HTTPStatusCode returns the HTTP status code of the response from the Friendly Captcha API.
func (r VerifyResult) HTTPStatusCode() int {
return r.Status
}

// WasAbleToVerify returns true if the captcha could be verified. If this is false, you should log the reason why
// and investigate (you can retrieve the error using the `RequestError` method). The `IsErrorDueToClientError` method
// will tell you if the error was due to a client error (e.g. wrong API key) - which will require your action to fix.
func (r VerifyResult) WasAbleToVerify() bool {
return r.Status == 200 && !r.IsRequestError()
}
3 changes: 2 additions & 1 deletion verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ func (frc *Client) VerifyCaptchaResponse(ctx context.Context, captchaResponse st
result.err = fmt.Errorf("%w: %v", ErrCreatingVerificationRequest, err)
return result
}
defer req.Body.Close()

req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Api-Key", frc.APIKey)
req.Header.Set("X-Frc-Sdk", fmt.Sprintf("friendly-captcha-go-sdk@%s", Version))
req.Header.Set("X-Frc-Sdk", fmt.Sprintf("friendly-captcha-go@%s", Version))

resp, err := frc.HTTPClient.Do(req)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion version.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package friendlycaptcha

var Version = "0.2.3"
var Version = "0.2.4"
8 changes: 8 additions & 0 deletions wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,35 @@ package friendlycaptcha

import "time"

// VerifyRequest is the request body for the /api/v2/captcha/siteverify endpoint. As a user of the SDK
// you generally don't need to create this struct yourself, instead you should use the Client's methods.
type VerifyRequest struct {
// The response value that the user submitted in the frc-captcha-response field.
Response string `json:"response"`
// Optional: the sitekey that you want to make sure the puzzle was generated from.
Sitekey string `json:"sitekey,omitempty"`
}

// VerifyResponseChallengeData is the data found in the challenge field of a VerifyResponse.
// It contains information about the challenge that was solved.
type VerifyResponseChallengeData struct {
Timestamp time.Time `json:"timestamp"`
Origin string `json:"origin"`
}

// VerifyResponseData is the data found in the data field of a VerifyResponse.
type VerifyResponseData struct {
Challenge VerifyResponseChallengeData `json:"challenge"`
}

// VerifyResponseError is the data found in the error field of a VerifyResponse in case of an error.
type VerifyResponseError struct {
ErrorCode ErrorCode `json:"error_code"`
Detail string `json:"detail"`
}

// VerifyResponse is the response body for the /api/v2/captcha/siteverify endpoint. This is what the Friendly
// Captcha API returns.
type VerifyResponse struct {
Success bool `json:"success"`

Expand Down
Loading