package postgres import ( "database/sql" "errors" "os" "strings" "testing" "time" "gardomatic.kleiax.de/internal/storage" _ "github.com/lib/pq" ) func TestDeleteUserAnonymizesAccountAndPreservesAuthoredContent(t *testing.T) { dsn := os.Getenv("GARDOMATIC_TEST_DB_DSN") if dsn == "" { t.Skip("set GARDOMATIC_TEST_DB_DSN to run PostgreSQL integration tests") } db, err := sql.Open("postgres", dsn) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = db.Close() }) if err = db.Ping(); err != nil { t.Fatal(err) } stamp := strings.ReplaceAll(time.Now().Format("150405.000000000"), ".", "") originalEmail := "delete-" + stamp + "@example.com" var ownerID, targetID, gardenID, entryID int if err = db.QueryRow(`INSERT INTO users (name,email,password_hash,activated) VALUES ('Owner',$1,'hash',true) RETURNING id`, "owner-delete-"+stamp+"@example.com").Scan(&ownerID); err != nil { t.Fatal(err) } if err = db.QueryRow(`INSERT INTO users (name,email,password_hash,activated) VALUES ('Personal Name',$1,'hash',true) RETURNING id`, originalEmail).Scan(&targetID); err != nil { t.Fatal(err) } if err = db.QueryRow(`INSERT INTO gardens (name) VALUES ('Deletion integration') RETURNING id`).Scan(&gardenID); err != nil { t.Fatal(err) } t.Cleanup(func() { _, _ = db.Exec(`DELETE FROM gardens WHERE id=$1`, gardenID) _, _ = db.Exec(`DELETE FROM users WHERE id IN ($1,$2)`, ownerID, targetID) }) if _, err = db.Exec(`INSERT INTO garden_members (garden_id,user_id,role) VALUES ($1,$2,'owner'),($1,$3,'member')`, gardenID, ownerID, targetID); err != nil { t.Fatal(err) } if err = db.QueryRow(`INSERT INTO journal_entries (garden_id,author_id,title) VALUES ($1,$2,'Bleibt erhalten') RETURNING id`, gardenID, targetID).Scan(&entryID); err != nil { t.Fatal(err) } users := UserModel{DB: db} if err = users.Delete(targetID); err != nil { t.Fatal(err) } if _, err = users.GetByEmail(originalEmail); !errors.Is(err, storage.ErrRecordNotFound) { t.Fatalf("deleted email still resolves: %v", err) } if _, err = users.GetByID(targetID); !errors.Is(err, storage.ErrRecordNotFound) { t.Fatalf("deleted user ID still resolves: %v", err) } var name, email string var activated bool var deletedAt time.Time if err = db.QueryRow(`SELECT name,email,activated,deleted_at FROM users WHERE id=$1`, targetID).Scan(&name, &email, &activated, &deletedAt); err != nil { t.Fatal(err) } if name != "Gelöschter Nutzer" || email == originalEmail || activated || deletedAt.IsZero() { t.Fatalf("account was not anonymized: name=%q email=%q activated=%t deleted_at=%v", name, email, activated, deletedAt) } var membershipCount, entryCount int if err = db.QueryRow(`SELECT count(*) FROM garden_members WHERE user_id=$1`, targetID).Scan(&membershipCount); err != nil { t.Fatal(err) } if err = db.QueryRow(`SELECT count(*) FROM journal_entries WHERE id=$1 AND author_id=$2`, entryID, targetID).Scan(&entryCount); err != nil { t.Fatal(err) } if membershipCount != 0 || entryCount != 1 { t.Fatalf("membership=%d preserved entries=%d", membershipCount, entryCount) } var replacementID int if err = db.QueryRow(`INSERT INTO users (name,email,password_hash,activated) VALUES ('Replacement',$1,'hash',true) RETURNING id`, originalEmail).Scan(&replacementID); err != nil { t.Fatalf("original email was not released: %v", err) } t.Cleanup(func() { _, _ = db.Exec(`DELETE FROM users WHERE id=$1`, replacementID) }) if err = users.Delete(ownerID); !errors.Is(err, storage.ErrConflict) { t.Fatalf("owner deletion error: got %v, want conflict", err) } }