139 lines
6.2 KiB
Go
139 lines
6.2 KiB
Go
package privacy
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
|
|
"data-terminal-cloud/internal/cache"
|
|
"data-terminal-cloud/internal/database"
|
|
"data-terminal-cloud/internal/outbox"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
const (
|
|
privacyDeleteSubjectID = "90000000-0000-0000-0000-000000000741"
|
|
privacyDeleteSubjectPublicID = "90000000-0000-0000-0000-000000000742"
|
|
privacyDeleteRequestID = "90000000-0000-0000-0000-000000000743"
|
|
privacyDeleteRequestPublicID = "90000000-0000-0000-0000-000000000744"
|
|
)
|
|
|
|
type privacyDeleteStore struct {
|
|
removed []string
|
|
}
|
|
|
|
func (s *privacyDeleteStore) Remove(_ context.Context, reference string) error {
|
|
s.removed = append(s.removed, reference)
|
|
return nil
|
|
}
|
|
|
|
func TestDeleteProcessorReconcilesAllSubsystemsAndCreatesTombstone(t *testing.T) {
|
|
databaseURL := os.Getenv("TEST_DATABASE_URL")
|
|
redisAddress := os.Getenv("TEST_REDIS_ADDR")
|
|
if databaseURL == "" || redisAddress == "" {
|
|
t.Skip("TEST_DATABASE_URL and TEST_REDIS_ADDR are required")
|
|
}
|
|
ctx := context.Background()
|
|
pool, err := database.Open(ctx, databaseURL, "privacy-delete-test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer pool.Close()
|
|
cleanupPrivacyDeleteFixture(t, ctx, pool)
|
|
defer cleanupPrivacyDeleteFixture(t, context.Background(), pool)
|
|
|
|
if _, err := pool.Exec(ctx, `INSERT INTO subject_profile (
|
|
id,public_id,tenant_id,household_id,preferred_name,status
|
|
) VALUES (
|
|
$1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001',
|
|
'10000000-0000-0000-0000-000000000201','隐私删除集成长者','active'
|
|
)`, privacyDeleteSubjectID, privacyDeleteSubjectPublicID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `INSERT INTO privacy_request (
|
|
id,public_id,tenant_id,subject_id,request_type,scope,requested_by,idempotency_key,request_digest
|
|
) VALUES (
|
|
$1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001',$3::uuid,
|
|
'DELETE','{"reason_code":"subject_request"}'::jsonb,
|
|
'10000000-0000-0000-0000-000000000101','privacy-delete-integration',repeat('9',64)
|
|
)`, privacyDeleteRequestID, privacyDeleteRequestPublicID, privacyDeleteSubjectID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `INSERT INTO export_task (
|
|
id,public_id,tenant_id,subject_id,requested_by,scope,status,object_ref,object_size,content_digest,
|
|
expires_at,completed_at,idempotency_key,request_digest
|
|
) VALUES (
|
|
'90000000-0000-0000-0000-000000000745','90000000-0000-0000-0000-000000000746',
|
|
'10000000-0000-0000-0000-000000000001',$1::uuid,'10000000-0000-0000-0000-000000000101',
|
|
'{}'::jsonb,'READY','privacy-delete/export.zip',128,repeat('a',64),now()+interval '1 hour',now(),
|
|
'privacy-delete-export-integration',repeat('b',64)
|
|
)`, privacyDeleteSubjectID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `INSERT INTO privacy_subtask (
|
|
public_id,privacy_request_id,subsystem,target_type,target_ref
|
|
) SELECT gen_random_uuid(),$1::uuid,subsystem,'subject_data',subsystem
|
|
FROM unnest(ARRAY['redis','export_files','object_storage','pgvector','provider_index','postgresql']) AS subsystem`, privacyDeleteRequestID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
redisClient := cache.Open(redisAddress, "", 0)
|
|
defer redisClient.Close()
|
|
store := &privacyDeleteStore{}
|
|
processor, err := NewDeleteProcessor(pool, redisClient, store)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := processor.Handle(ctx, outbox.Event{AggregateID: privacyDeleteRequestID}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var requestStatus, subjectStatus, preferredName string
|
|
var completed, skipped, tombstones, audits int
|
|
if err := pool.QueryRow(ctx, `SELECT status FROM privacy_request WHERE id=$1::uuid`, privacyDeleteRequestID).Scan(&requestStatus); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT status,preferred_name FROM subject_profile WHERE id=$1::uuid`, privacyDeleteSubjectID).Scan(&subjectStatus, &preferredName); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FILTER(WHERE status='COMPLETED'),count(*) FILTER(WHERE status='SKIPPED') FROM privacy_subtask WHERE privacy_request_id=$1::uuid`, privacyDeleteRequestID).Scan(&completed, &skipped); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM tombstone WHERE deletion_request_id=$1::uuid`, privacyDeleteRequestID).Scan(&tombstones); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM audit_event WHERE action='privacy.delete.complete' AND resource_id=$1::uuid`, privacyDeleteRequestPublicID).Scan(&audits); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var exportStatus string
|
|
var exportReference *string
|
|
if err := pool.QueryRow(ctx, `SELECT status,object_ref FROM export_task WHERE id='90000000-0000-0000-0000-000000000745'::uuid`).Scan(&exportStatus, &exportReference); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if requestStatus != "COMPLETED" || subjectStatus != "deleted" || preferredName != "[已删除]" || completed != 5 || skipped != 1 || tombstones != 1 || audits != 1 || len(store.removed) != 1 || store.removed[0] != "privacy-delete/export.zip" || exportStatus != "EXPIRED" || exportReference != nil {
|
|
t.Fatalf("request=%s subject=%s name=%q completed=%d skipped=%d tombstones=%d audits=%d removed=%v", requestStatus, subjectStatus, preferredName, completed, skipped, tombstones, audits, store.removed)
|
|
}
|
|
if err := processor.Handle(ctx, outbox.Event{AggregateID: privacyDeleteRequestID}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := processor.Handle(ctx, outbox.Event{AggregateID: "90000000-0000-0000-0000-000000000799"}); err != nil {
|
|
t.Fatalf("missing deletion request error=%v", err)
|
|
}
|
|
}
|
|
|
|
func cleanupPrivacyDeleteFixture(t *testing.T, ctx context.Context, pool *pgxpool.Pool) {
|
|
t.Helper()
|
|
statements := []string{
|
|
`DELETE FROM tombstone WHERE deletion_request_id='90000000-0000-0000-0000-000000000743'::uuid`,
|
|
`DELETE FROM audit_event WHERE action='privacy.delete.complete' AND resource_id='90000000-0000-0000-0000-000000000744'::uuid`,
|
|
`DELETE FROM privacy_request WHERE id='90000000-0000-0000-0000-000000000743'::uuid`,
|
|
`DELETE FROM export_task WHERE id='90000000-0000-0000-0000-000000000745'::uuid`,
|
|
`DELETE FROM subject_profile WHERE id='90000000-0000-0000-0000-000000000741'::uuid`,
|
|
}
|
|
for _, statement := range statements {
|
|
if _, err := pool.Exec(ctx, statement); err != nil {
|
|
t.Fatalf("cleanup privacy delete fixture: %v", err)
|
|
}
|
|
}
|
|
}
|