Add tests for disabled property codepaths

Co-authored-by: ribtoks <505555+ribtoks@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-05 06:25:31 +00:00
parent 069475fa0c
commit a306142ecd
5 changed files with 449 additions and 0 deletions
+258
View File
@@ -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")
}
}
+48
View File
@@ -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")
+76
View File
@@ -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")
}
}
+5
View File
@@ -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
}
+62
View File
@@ -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)
}
}