mirror of
https://github.com/PrivateCaptcha/PrivateCaptcha.git
synced 2026-05-03 11:31:35 -05:00
Add tests for disabled property codepaths
Co-authored-by: ribtoks <505555+ribtoks@users.noreply.github.com>
This commit is contained in:
@@ -2063,3 +2063,261 @@ func buildManyPropertiesJSON(t *testing.T, count int) []byte {
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func TestApiGetPropertyDisabled(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := common.TraceContext(t.Context(), t.Name())
|
||||
|
||||
user, org, apiKey, err := setupAPISuite(ctx, t.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
property, _, err := server.BusinessDB.Impl().CreateNewProperty(ctx, db_test.CreateNewPropertyParams(user.ID, "example.com"), org)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// First verify we can get the property
|
||||
propertyID := server.IDHasher.Encrypt(int(property.ID))
|
||||
_, meta, err := requestResponseAPISuite[*apiPropertyOutput](ctx, nil,
|
||||
http.MethodGet,
|
||||
fmt.Sprintf("/%s/%s/%s/%s", common.OrgEndpoint, server.IDHasher.Encrypt(int(org.ID)),
|
||||
common.PropertyEndpoint, propertyID),
|
||||
apiKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !meta.Code.Success() {
|
||||
t.Fatalf("Expected success before disabling, got: %v", meta.Description)
|
||||
}
|
||||
|
||||
// Now disable the property
|
||||
if err := db_tests.DisableProperty(ctx, store, property.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Try to get the disabled property
|
||||
_, meta, err = requestResponseAPISuite[*apiPropertyOutput](ctx, nil,
|
||||
http.MethodGet,
|
||||
fmt.Sprintf("/%s/%s/%s/%s", common.OrgEndpoint, server.IDHasher.Encrypt(int(org.ID)),
|
||||
common.PropertyEndpoint, propertyID),
|
||||
apiKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Should return an error code for invalid/disabled property
|
||||
if meta.Code != common.StatusPropertyIDInvalidError {
|
||||
t.Errorf("Expected StatusPropertyIDInvalidError for disabled property, got: %v", meta.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiGetPropertiesExcludesDisabled(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := common.TraceContext(t.Context(), t.Name())
|
||||
|
||||
user, org, apiKey, err := setupAPISuite(ctx, t.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create 3 properties
|
||||
p1, _, err := server.BusinessDB.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, "example1.com"), org)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p2, _, err := server.BusinessDB.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, "example2.com"), org)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, err = server.BusinessDB.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, "example3.com"), org)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Disable p1 and p2
|
||||
if err := db_tests.DisableProperty(ctx, store, p1.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db_tests.DisableProperty(ctx, store, p2.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Get properties - should only return 1 (the enabled one)
|
||||
endpoint := fmt.Sprintf("/%s/%v/%s", common.OrgEndpoint, server.IDHasher.Encrypt(int(org.ID)), common.PropertiesEndpoint)
|
||||
properties, meta, err := requestResponseAPISuite[[]*apiOrgPropertyOutput](ctx, nil, http.MethodGet, endpoint, apiKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !meta.Code.Success() {
|
||||
t.Fatalf("Unexpected status code: %v", meta.Description)
|
||||
}
|
||||
|
||||
if len(properties) != 1 {
|
||||
t.Errorf("Expected 1 enabled property, got %d", len(properties))
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiUpdateDisabledProperty(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := common.TraceContext(t.Context(), t.Name())
|
||||
|
||||
user, org, apiKey, err := setupAPISuite(ctx, t.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
property, _, err := server.BusinessDB.Impl().CreateNewProperty(ctx, db_test.CreateNewPropertyParams(user.ID, "example.com"), org)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Disable the property
|
||||
if err := db_tests.DisableProperty(ctx, store, property.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Try to update the disabled property
|
||||
updates := []*apiUpdatePropertyInput{
|
||||
{
|
||||
ID: server.IDHasher.Encrypt(int(property.ID)),
|
||||
apiPropertySettings: apiPropertySettings{
|
||||
Name: "Updated Name",
|
||||
Level: int(common.DifficultyLevelHigh),
|
||||
Growth: string(dbgen.DifficultyGrowthMedium),
|
||||
ValiditySeconds: int(puzzle.ValidityDurations[3].Seconds()),
|
||||
MaxReplayCount: 100,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
output, meta, err := requestResponseAPISuite[*apiAsyncTaskOutput](ctx, updates,
|
||||
http.MethodPut,
|
||||
"/"+common.PropertiesEndpoint,
|
||||
apiKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !meta.Code.Success() {
|
||||
t.Fatalf("Unexpected status code: %v", meta.Description)
|
||||
}
|
||||
|
||||
// Wait for async task
|
||||
finished := false
|
||||
for i := 0; i < 20; i++ {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
result, meta, err := requestResponseAPISuite[*apiAsyncTaskResultOutput](ctx, nil, http.MethodGet, "/"+common.AsyncTaskEndpoint+"/"+output.ID, apiKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !meta.Code.Success() {
|
||||
t.Fatalf("Unexpected status code: %v", meta.Description)
|
||||
}
|
||||
|
||||
if result.Finished {
|
||||
finished = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !finished {
|
||||
t.Fatal("Async task did not complete within timeout")
|
||||
}
|
||||
|
||||
// Verify the property was NOT updated (still has original values)
|
||||
var updatedName string
|
||||
err = store.Pool.QueryRow(ctx, "SELECT name FROM backend.properties WHERE id = $1", property.ID).Scan(&updatedName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if updatedName == "Updated Name" {
|
||||
t.Error("Disabled property should not be updated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiDeleteDisabledProperty(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := common.TraceContext(t.Context(), t.Name())
|
||||
|
||||
user, org, apiKey, err := setupAPISuite(ctx, t.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
property, _, err := server.BusinessDB.Impl().CreateNewProperty(ctx, db_test.CreateNewPropertyParams(user.ID, "example.com"), org)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Disable the property
|
||||
if err := db_tests.DisableProperty(ctx, store, property.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Try to delete the disabled property
|
||||
idsToDelete := []string{server.IDHasher.Encrypt(int(property.ID))}
|
||||
|
||||
output, meta, err := requestResponseAPISuite[*apiAsyncTaskOutput](ctx, idsToDelete,
|
||||
http.MethodDelete,
|
||||
"/"+common.PropertiesEndpoint,
|
||||
apiKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !meta.Code.Success() {
|
||||
t.Fatalf("Unexpected status code: %v", meta.Description)
|
||||
}
|
||||
|
||||
// Wait for async task
|
||||
finished := false
|
||||
for i := 0; i < 20; i++ {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
result, meta, err := requestResponseAPISuite[*apiAsyncTaskResultOutput](ctx, nil, http.MethodGet, "/"+common.AsyncTaskEndpoint+"/"+output.ID, apiKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !meta.Code.Success() {
|
||||
t.Fatalf("Unexpected status code: %v", meta.Description)
|
||||
}
|
||||
|
||||
if result.Finished {
|
||||
finished = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !finished {
|
||||
t.Fatal("Async task did not complete within timeout")
|
||||
}
|
||||
|
||||
// Verify the property was NOT deleted (still exists without soft delete)
|
||||
var deletedAt *time.Time
|
||||
err = store.Pool.QueryRow(ctx, "SELECT deleted_at FROM backend.properties WHERE id = $1", property.ID).Scan(&deletedAt)
|
||||
if err != nil {
|
||||
t.Error("Disabled property should still exist, but got error:", err)
|
||||
}
|
||||
if deletedAt != nil {
|
||||
t.Error("Disabled property should not be soft-deleted")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,6 +357,54 @@ func TestGetPuzzleInvalidSitekeyLength(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPuzzleDisabledProperty(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)
|
||||
}
|
||||
|
||||
sitekey := db.UUIDToSiteKey(property.ExternalID)
|
||||
|
||||
// First verify the property works
|
||||
resp, err := puzzleSuite(ctx, sitekey, property.Domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK before disabling, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Now disable the property
|
||||
if err := db_tests.DisableProperty(ctx, store, property.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Clear cache so we fetch fresh data
|
||||
if found := cache.Delete(ctx, db.PropertyBySitekeyCacheKey(sitekey)); !found {
|
||||
t.Fatal("property was not found in cache")
|
||||
}
|
||||
|
||||
// Now the puzzle request should be forbidden
|
||||
resp, err = puzzleSuite(ctx, sitekey, property.Domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusForbidden {
|
||||
t.Errorf("Expected status Forbidden for disabled property, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecaptchaVerifyHandlerInvalidFormData(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
|
||||
@@ -1261,3 +1261,79 @@ func TestAPIKeyLastUsedAtUpdatedOnVerify(t *testing.T) {
|
||||
t.Errorf("last_used_at timestamp is too old: %v", updatedKey.LastUsedAt.Time)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func TestVerifyDisabledProperty(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)
|
||||
}
|
||||
|
||||
sitekey := db.UUIDToSiteKey(property.ExternalID)
|
||||
puzzleStr, solutionsStr, err := solutionsSuite(ctx, sitekey, property.Domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload := fmt.Sprintf("%s.%s", solutionsStr, puzzleStr)
|
||||
|
||||
keyParams := tests.CreateNewPuzzleAPIKeyParams(t.Name()+"-apikey", time.Now(), 1*time.Hour, 10.0 /*rps*/)
|
||||
apikey, _, err := store.Impl().CreateAPIKey(ctx, user, keyParams)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
secret := db.UUIDToSecret(apikey.ExternalID)
|
||||
|
||||
// First verify works
|
||||
resp, err := verifySuite(payload, secret, sitekey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK before disabling, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Now disable the property
|
||||
if err := db_tests.DisableProperty(ctx, store, property.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Clear cache
|
||||
cache.Delete(ctx, db.PropertyBySitekeyCacheKey(sitekey))
|
||||
|
||||
// Generate new puzzle and solution (can't reuse as property is disabled at puzzle endpoint too)
|
||||
// So we test by verifying the same (now stale) payload which will fail due to disabled property
|
||||
resp, err = verifySuite(payload, secret, sitekey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Should get forbidden or an error response
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Logf("Response body: %s, Status: %d", string(body), resp.StatusCode)
|
||||
|
||||
// Verify returns VerifyErrorOther for disabled properties
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// Direct HTTP error is also acceptable
|
||||
return
|
||||
}
|
||||
|
||||
var vr VerificationResponse
|
||||
if err := json.Unmarshal(body, &vr); err != nil {
|
||||
t.Fatalf("Failed to parse response: %v", err)
|
||||
}
|
||||
if vr.Success {
|
||||
t.Error("Expected verification to fail for disabled property")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,3 +128,8 @@ func CreatePropertyForOrg(ctx context.Context, store db.Implementor, org *dbgen.
|
||||
}, org)
|
||||
return property, err
|
||||
}
|
||||
|
||||
func DisableProperty(ctx context.Context, store *db.BusinessStore, propertyID int32) error {
|
||||
_, err := store.Pool.Exec(ctx, "UPDATE backend.properties SET enabled = FALSE WHERE id = $1", propertyID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1807,3 +1807,65 @@ func TestDeletePropertyCannotDelete(t *testing.T) {
|
||||
t.Errorf("Expected method not allowed (405), got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrgPropertyDisabled(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.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
|
||||
property, _, err := server.Store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, "example.com"), org)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new property: %v", err)
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
cookie, err := portal_tests.AuthenticateSuite(ctx, user.Email, srv, server.XSRF, server.Sessions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// First verify the property is accessible
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("/org/%s/property/%s",
|
||||
server.IDHasher.Encrypt(int(org.ID)),
|
||||
server.IDHasher.Encrypt(int(property.ID))), nil)
|
||||
req.AddCookie(cookie)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
srv.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status OK before disabling, got %d", w.Code)
|
||||
}
|
||||
|
||||
// Now disable the property
|
||||
if err := db_tests.DisableProperty(ctx, store, property.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Try to access the disabled property
|
||||
req = httptest.NewRequest("GET", fmt.Sprintf("/org/%s/property/%s",
|
||||
server.IDHasher.Encrypt(int(org.ID)),
|
||||
server.IDHasher.Encrypt(int(property.ID))), nil)
|
||||
req.AddCookie(cookie)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
srv.ServeHTTP(w, req)
|
||||
|
||||
// Should redirect to error page (status 303 See Other for forbidden)
|
||||
if w.Code != http.StatusSeeOther {
|
||||
t.Errorf("Expected redirect (303) for disabled property, got %d", w.Code)
|
||||
}
|
||||
|
||||
location, _ := w.Result().Location()
|
||||
if location == nil || !strings.Contains(location.String(), common.ErrorEndpoint) {
|
||||
t.Errorf("Expected redirect to error endpoint, got %v", location)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user