Files
PrivateCaptcha/pkg/api/verify_test.go
Taras Kushnir 8112359ef9 Add more tests
2026-01-02 18:44:17 +02:00

856 lines
21 KiB
Go

package api
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"maps"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/common"
common_test "github.com/PrivateCaptcha/PrivateCaptcha/pkg/common/tests"
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/db"
dbgen "github.com/PrivateCaptcha/PrivateCaptcha/pkg/db/generated"
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/db/tests"
db_tests "github.com/PrivateCaptcha/PrivateCaptcha/pkg/db/tests"
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/puzzle"
)
func TestSerializeResponse(t *testing.T) {
v := VerifyResponseRecaptchaV3{
VerifyResponseRecaptchaV2: VerifyResponseRecaptchaV2{
Success: false,
ErrorCodes: []string{puzzle.VerifyErrorOther.String()},
ChallengeTS: common.JSONTimeNow(),
Hostname: "hostname.com",
},
Score: 0.5,
Action: "action",
}
_, err := json.Marshal(v)
if err != nil {
t.Fatal(err)
}
}
func verifySuite(response, secret, sitekey string) (*http.Response, error) {
srv := http.NewServeMux()
s.Setup("", true /*verbose*/, common.NoopMiddleware).Register(srv)
//srv.HandleFunc("/", catchAll)
req, err := http.NewRequest(http.MethodPost, "/"+common.VerifyEndpoint, strings.NewReader(response))
if err != nil {
return nil, err
}
req.Header.Set(common.HeaderAPIKey, secret)
req.Header.Set(cfg.Get(common.RateLimitHeaderKey).Value(), common_test.GenerateRandomIPv4())
if len(sitekey) > 0 {
req.Header.Set(common.HeaderSitekey, sitekey)
}
w := httptest.NewRecorder()
srv.ServeHTTP(w, req)
resp := w.Result()
return resp, nil
}
func siteVerifySuite(response, secret, sitekey string, headers ...map[string][]string) (*http.Response, error) {
srv := http.NewServeMux()
s.Setup("", true /*verbose*/, common.NoopMiddleware).Register(srv)
//srv.HandleFunc("/", catchAll)
data := url.Values{}
data.Set(common.ParamSecret, secret)
data.Set(common.ParamResponse, response)
if len(sitekey) > 0 {
data.Set(common.ParamSiteKey, sitekey)
}
encoded := data.Encode()
req, err := http.NewRequest(http.MethodPost, "/"+common.SiteVerifyEndpoint, strings.NewReader(encoded))
if err != nil {
return nil, err
}
req.Header.Set(common.HeaderContentType, common.ContentTypeURLEncoded)
req.Header.Set(cfg.Get(common.RateLimitHeaderKey).Value(), common_test.GenerateRandomIPv4())
for _, hh := range headers {
maps.Copy(req.Header, hh)
}
w := httptest.NewRecorder()
srv.ServeHTTP(w, req)
resp := w.Result()
return resp, nil
}
func solutionsSuite(ctx context.Context, sitekey, domain string) (string, string, error) {
resp, err := puzzleSuite(ctx, sitekey, domain)
if err != nil {
return "", "", err
}
if resp.StatusCode != http.StatusOK {
return "", "", fmt.Errorf("Unexpected puzzle status code %d", resp.StatusCode)
}
p, puzzleStr, err := parsePuzzle(resp)
if err != nil {
return puzzleStr, "", err
}
solver := &puzzle.ComputeSolver{}
solutions, err := solver.Solve(p)
if err != nil {
return puzzleStr, "", err
}
return puzzleStr, solutions.String(), nil
}
func setupVerifySuite(ctx context.Context, username string, scope dbgen.ApiKeyScope) (string, string, string, error) {
user, org, err := db_tests.CreateNewAccountForTest(ctx, store, username, testPlan)
if err != nil {
return "", "", "", err
}
property, _, err := store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, testPropertyDomain), org)
if err != nil {
return "", "", "", err
}
sitekey := db.UUIDToSiteKey(property.ExternalID)
puzzleStr, solutionsStr, err := solutionsSuite(ctx, sitekey, property.Domain)
if err != nil {
return "", "", "", err
}
keyParams := tests.CreateNewPuzzleAPIKeyParams(username+"-apikey", time.Now(), 1*time.Hour, 10.0 /*rps*/)
keyParams.Scope = scope
apikey, _, err := store.Impl().CreateAPIKey(ctx, user, keyParams)
if err != nil {
return "", "", "", err
}
return fmt.Sprintf("%s.%s", solutionsStr, puzzleStr), db.UUIDToSecret(apikey.ExternalID), sitekey, nil
}
func TestVerifyPuzzle(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
payload, apiKey, sitekey, err := setupVerifySuite(t.Context(), t.Name(), dbgen.ApiKeyScopePuzzle)
if err != nil {
t.Fatal(err)
}
resp, err := verifySuite(payload, apiKey, sitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected submit status code %d", resp.StatusCode)
}
}
func TestVerifyAnotherOrgScope(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Parallel()
ctx := t.Context()
user, org1, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
if err != nil {
t.Fatal(err)
}
property, _, err := store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, testPropertyDomain), org1)
if err != nil {
t.Fatal(err)
}
puzzleStr, solutionsStr, err := solutionsSuite(ctx, db.UUIDToSiteKey(property.ExternalID), property.Domain)
if err != nil {
t.Fatal(err)
}
payload := fmt.Sprintf("%s.%s", solutionsStr, puzzleStr)
org2, _, err := store.Impl().CreateNewOrganization(ctx, t.Name()+"-another-org", user.ID)
if err != nil {
t.Fatalf("Failed to create extra org: %v", err)
}
params := tests.CreateNewPuzzleAPIKeyParams(t.Name()+"-apikey", time.Now(), 1*time.Hour, 10.0 /*rps*/)
params.OrgID = db.Int(org2.ID)
apikey, _, err := store.Impl().CreateAPIKey(ctx, user, params)
if err != nil {
t.Fatal(err)
}
secret := db.UUIDToSecret(apikey.ExternalID)
sitekey := db.UUIDToSiteKey(property.ExternalID)
resp, err := verifySuite(payload, secret, sitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected verify status code %d", resp.StatusCode)
}
if err := checkVerifyError(resp, puzzle.OrgScopeError); err != nil {
t.Fatal(err)
}
}
func TestVerifyInvalidAPIKeyScope(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
payload, apiKey, sitekey, err := setupVerifySuite(t.Context(), t.Name(), dbgen.ApiKeyScopePortal)
if err != nil {
t.Fatal(err)
}
resp, err := verifySuite(payload, apiKey, sitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusForbidden {
t.Errorf("Unexpected submit status code %d", resp.StatusCode)
}
}
func TestVerifyPuzzleWrongExpectedSitekey(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
payload, apiKey, _, err := setupVerifySuite(t.Context(), t.Name(), dbgen.ApiKeyScopePuzzle)
if err != nil {
t.Fatal(err)
}
resp, err := verifySuite(payload, apiKey, db.UUIDToSiteKey(*randomUUID()))
if err != nil {
t.Fatal(err)
}
if err := checkVerifyError(resp, puzzle.InvalidPropertyError); err != nil {
t.Fatal(err)
}
}
func TestSiteVerifyPuzzle(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
payload, apiKey, sitekey, err := setupVerifySuite(t.Context(), t.Name(), dbgen.ApiKeyScopePuzzle)
if err != nil {
t.Fatal(err)
}
resp, err := siteVerifySuite(payload, apiKey, sitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected submit status code %d", resp.StatusCode)
}
}
func TestSiteVerifyPuzzleV3Compat(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
payload, apiKey, sitekey, err := setupVerifySuite(t.Context(), t.Name(), dbgen.ApiKeyScopePuzzle)
if err != nil {
t.Fatal(err)
}
resp, err := siteVerifySuite(payload, apiKey, sitekey, map[string][]string{common.HeaderCaptchaCompat: []string{recaptchaCompatV3}})
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected submit status code %d", resp.StatusCode)
}
}
func TestSiteVerifyWrongExpectedSitekey(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
payload, apiKey, _, err := setupVerifySuite(t.Context(), t.Name(), dbgen.ApiKeyScopePuzzle)
if err != nil {
t.Fatal(err)
}
resp, err := siteVerifySuite(payload, apiKey, db.UUIDToSiteKey(*randomUUID()))
if err != nil {
t.Fatal(err)
}
if err := checkSiteVerifyError(resp, puzzle.InvalidPropertyError); err != nil {
t.Fatal(err)
}
}
func checkSiteVerifyError(resp *http.Response, expected puzzle.VerifyError) error {
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
response := &VerifyResponseRecaptchaV2{}
err = json.Unmarshal(body, &response)
if err != nil {
return err
}
if expected == puzzle.VerifyNoError {
if !response.Success {
return errors.New("expected successful verification")
}
if len(response.ErrorCodes) > 0 {
return errors.New("error code present in response")
}
} else {
if len(response.ErrorCodes) == 0 {
return errors.New("no error code in response")
}
if response.ErrorCodes[0] != expected.String() {
return fmt.Errorf("Unexpected error code: %v", response.ErrorCodes[0])
}
}
return nil
}
func checkVerifyError(resp *http.Response, expected puzzle.VerifyError) error {
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
response := &VerificationResponse{}
err = json.Unmarshal(body, &response)
if err != nil {
return err
}
if expected == puzzle.VerifyNoError {
if !response.Success {
return errors.New("expected successful verification")
}
if response.Code != 0 {
return errors.New("error code present in response")
}
} else {
if response.Code == 0 {
return errors.New("no error code in response")
}
if response.Code != expected {
return fmt.Errorf("Unexpected error code: %v", response.Code.String())
}
}
return nil
}
func TestVerifyPuzzleReplay(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
payload, apiKey, sitekey, err := setupVerifySuite(t.Context(), t.Name(), dbgen.ApiKeyScopePuzzle)
if err != nil {
t.Fatal(err)
}
resp, err := verifySuite(payload, apiKey, sitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected submit status code %d", resp.StatusCode)
}
// now second time the same
resp, err = verifySuite(payload, apiKey, sitekey)
if err != nil {
t.Fatal(err)
}
if err := checkVerifyError(resp, puzzle.VerifiedBeforeError); err != nil {
t.Fatal(err)
}
}
// in this case "Site" part of the SiteVerify (reCAPTCHA compatibility) does not bring anything else
// except of checking _any_ error in the reCAPTCHA format
func TestSiteVerifyPuzzleReplay(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
payload, apiKey, sitekey, err := setupVerifySuite(t.Context(), t.Name(), dbgen.ApiKeyScopePuzzle)
if err != nil {
t.Fatal(err)
}
resp, err := siteVerifySuite(payload, apiKey, sitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected submit status code %d", resp.StatusCode)
}
// now second time the same
resp, err = siteVerifySuite(payload, apiKey, sitekey)
if err != nil {
t.Fatal(err)
}
if err := checkSiteVerifyError(resp, puzzle.VerifiedBeforeError); err != nil {
t.Fatal(err)
}
}
func TestVerifyPuzzleAllowReplay(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
payload, apiKey, sitekey, err := setupVerifySuite(t.Context(), t.Name(), dbgen.ApiKeyScopePuzzle)
if err != nil {
t.Fatal(err)
}
property, err := store.Impl().GetCachedPropertyBySitekey(t.Context(), sitekey, nil)
if err != nil {
t.Fatal(err)
}
const maxReplayCount = 3
// this should be still cached so we don't need to actually update DB
property.MaxReplayCount = maxReplayCount
for _ = range maxReplayCount {
resp, err := verifySuite(payload, apiKey, sitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected submit status code %d", resp.StatusCode)
}
if err := checkVerifyError(resp, puzzle.VerifyNoError); err != nil {
t.Fatal(err)
}
}
// now it should trigger an error
resp, err := verifySuite(payload, apiKey, sitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected submit status code %d", resp.StatusCode)
}
if err := checkVerifyError(resp, puzzle.VerifiedBeforeError); err != nil {
t.Fatal(err)
}
}
// same as successful test (TestVerifyPuzzle), but invalidates api key in cache
func TestVerifyCachePriority(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := t.Context()
user, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
if err != nil {
t.Fatal(err)
}
property, _, err := store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, testPropertyDomain), org)
if err != nil {
t.Fatal(err)
}
puzzleStr, solutionsStr, err := solutionsSuite(ctx, db.UUIDToSiteKey(property.ExternalID), property.Domain)
if err != nil {
t.Fatal(err)
}
apiKeyID := randomUUID()
secret := db.UUIDToSecret(*apiKeyID)
sitekey := db.UUIDToSiteKey(property.ExternalID)
cache.SetMissing(ctx, db.APIKeyCacheKey(secret))
resp, err := verifySuite(fmt.Sprintf("%s.%s", solutionsStr, puzzleStr), secret, sitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusForbidden {
t.Errorf("Unexpected submit status code %d", resp.StatusCode)
}
}
func TestVerifyInvalidSolutions(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := t.Context()
user, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
if err != nil {
t.Fatal(err)
}
property, _, err := store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, testPropertyDomain), org)
if err != nil {
t.Fatal(err)
}
puzzleStr, _, err := solutionsSuite(ctx, db.UUIDToSiteKey(property.ExternalID), property.Domain)
if err != nil {
t.Fatal(err)
}
solution := make([]byte, 16*puzzle.SolutionLength)
for i := 0; i < 16*puzzle.SolutionLength; i++ {
solution[i] = byte(i)
}
solutionsStr := (&puzzle.Solutions{Buffer: solution, Metadata: &puzzle.Metadata{}}).String()
payload := fmt.Sprintf("%s.%s", solutionsStr, puzzleStr)
params := tests.CreateNewPuzzleAPIKeyParams(t.Name()+"-apikey", time.Now(), 1*time.Hour, 10.0 /*rps*/)
apikey, _, err := store.Impl().CreateAPIKey(ctx, user, params)
if err != nil {
t.Fatal(err)
}
secret := db.UUIDToSecret(apikey.ExternalID)
sitekey := db.UUIDToSiteKey(property.ExternalID)
resp, err := verifySuite(payload, secret, sitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected verify status code %d", resp.StatusCode)
}
if err := checkVerifyError(resp, puzzle.InvalidSolutionError); err != nil {
t.Fatal(err)
}
}
func TestVerifyInvalidKey(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Parallel()
payload, _, sitekey, err := setupVerifySuite(t.Context(), t.Name(), dbgen.ApiKeyScopePuzzle)
if err != nil {
t.Fatal(err)
}
resp, err := verifySuite(payload, db.UUIDToSecret(*randomUUID()), sitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusForbidden {
t.Errorf("Unexpected submit status code %d", resp.StatusCode)
}
}
func TestSiteVerifyInvalidKey(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Parallel()
payload, _, sitekey, err := setupVerifySuite(t.Context(), t.Name(), dbgen.ApiKeyScopePuzzle)
if err != nil {
t.Fatal(err)
}
resp, err := siteVerifySuite(payload, db.UUIDToSecret(*randomUUID()), sitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusForbidden {
t.Errorf("Unexpected submit status code %d", resp.StatusCode)
}
}
func TestVerifyExpiredKey(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Parallel()
ctx := t.Context()
user, _, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
if err != nil {
t.Fatal(err)
}
apikey, _, err := store.Impl().CreateAPIKey(ctx, user, tests.CreateNewPuzzleAPIKeyParams(t.Name()+"-apikey", time.Now(), 1*time.Hour, 10.0 /*rps*/))
if err != nil {
t.Fatal(err)
}
_, err = store.Impl().UpdateAPIKey(ctx, user, apikey, time.Now().AddDate(0, 0, -1), true)
if err != nil {
t.Fatal(err)
}
resp, err := verifySuite("a.b.c", db.UUIDToSecret(apikey.ExternalID), "")
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusForbidden {
t.Errorf("Unexpected submit status code %d", resp.StatusCode)
}
}
func TestVerifyMaintenanceMode(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
// NOTE: this test cannot be run in parallel as it modifies the global DB state (maintenance mode)
// t.Parallel()
payload, apiKey, sitekey, err := setupVerifySuite(t.Context(), t.Name(), dbgen.ApiKeyScopePuzzle)
if err != nil {
t.Fatal(err)
}
cacheKey := db.PropertyBySitekeyCacheKey(sitekey)
cache.Delete(t.Context(), cacheKey)
store.UpdateConfig(true /*maintenance mode*/)
defer store.UpdateConfig(false /*maintenance mode*/)
resp, err := verifySuite(payload, apiKey, sitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected submit status code %d", resp.StatusCode)
}
if err := checkVerifyError(resp, puzzle.MaintenanceModeError); err != nil {
t.Fatal(err)
}
}
func verifyTestPropertySuite(t *testing.T, verifySitekey string, expectedCode puzzle.VerifyError) {
ctx := t.Context()
puzzleStr, solutionsStr, err := solutionsSuite(ctx, db.TestPropertySitekey, "localhost")
if err != nil {
t.Fatal(err)
}
payload := fmt.Sprintf("%s.%s", solutionsStr, puzzleStr)
user, _, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
if err != nil {
t.Fatal(err)
}
apikey, _, err := store.Impl().CreateAPIKey(ctx, user, tests.CreateNewPuzzleAPIKeyParams(t.Name()+"-apikey", time.Now(), 1*time.Hour, 10.0 /*rps*/))
if err != nil {
t.Fatal(err)
}
secret := db.UUIDToSecret(apikey.ExternalID)
resp, err := verifySuite(payload, secret, verifySitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected verify status code %d", resp.StatusCode)
}
if err := checkVerifyError(resp, expectedCode); err != nil {
t.Fatal(err)
}
}
func TestVerifyTestProperty(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
verifyTestPropertySuite(t, "" /*verify sitekey*/, puzzle.TestPropertyError)
}
func TestVerifyTestPropertyAgainstSitekey(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
verifyTestPropertySuite(t, db.UUIDToSiteKey(*randomUUID()), puzzle.InvalidPropertyError)
}
func TestVerifyTestShortcut(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := t.Context()
solver := &puzzle.ComputeSolver{}
solutions, _ := solver.Solve(s.Verifier.TestPuzzle)
var buf bytes.Buffer
buf.WriteString(solutions.String())
buf.Write([]byte("."))
s.Verifier.WriteTestPuzzle(&buf)
payload, err := s.Verifier.ParseSolutionPayload(ctx, buf.Bytes())
if err != nil {
t.Fatal(err)
}
if payload.Puzzle() != s.Verifier.TestPuzzle {
t.Fatal("verify result is not short circuited")
}
if result, _ := s.Verifier.Verify(ctx, payload, nil /*expectedOwner*/, time.Now().UTC()); result.Error != puzzle.TestPropertyError {
t.Errorf("Unexpected verification result: %v", result.Error.String())
}
}
func TestVerifyByOrgMember(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := t.Context()
owner, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name()+"_owner", testPlan)
if err != nil {
t.Fatal(err)
}
property, _, err := store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(owner.ID, testPropertyDomain), org)
if err != nil {
t.Fatal(err)
}
puzzleStr, solutionsStr, err := solutionsSuite(ctx, db.UUIDToSiteKey(property.ExternalID), property.Domain)
if err != nil {
t.Fatal(err)
}
payload := fmt.Sprintf("%s.%s", solutionsStr, puzzleStr)
member, _, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name()+"_member", testPlan)
params := tests.CreateNewPuzzleAPIKeyParams(t.Name()+"-apikey", time.Now(), 1*time.Hour, 10.0 /*rps*/)
params.OrgID = db.Int(org.ID)
apikey, _, err := store.Impl().CreateAPIKey(ctx, member, params)
if err != nil {
t.Fatal(err)
}
secret := db.UUIDToSecret(apikey.ExternalID)
sitekey := db.UUIDToSiteKey(property.ExternalID)
resp, err := verifySuite(payload, secret, sitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected verify status code %d", resp.StatusCode)
}
if err := checkVerifyError(resp, puzzle.WrongOwnerError); err != nil {
t.Fatal(err)
}
// join the org
if _, err := store.Impl().InviteUserToOrg(ctx, owner, org, member); err != nil {
t.Fatal(err)
}
if _, err := store.Impl().JoinOrg(ctx, org.ID, member); err != nil {
t.Fatal(err)
}
// now, after we join the org, should be OK
resp, err = verifySuite(payload, secret, sitekey)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected verify status code %d", resp.StatusCode)
}
if err := checkVerifyError(resp, puzzle.VerifyNoError); err != nil {
t.Fatal(err)
}
}