Files

125 lines
6.3 KiB
Go
Raw Permalink Normal View History

package onboarding
import (
"fmt"
"strings"
"testing"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/saas"
"github.com/google/uuid"
)
func TestProfileCreationRollsBackWhenMemoryConsentFails(t *testing.T) {
f := newFixture(t)
name := "test_initial_memory_" + strings.ReplaceAll(uuid.NewString(), "-", "")
// Fail the second grant for this synthetic tenant to verify that the first
// grant, the new household, profile, audit records and replay result all roll back.
_, err := f.s.pool.Exec(f.ctx, fmt.Sprintf(`CREATE FUNCTION %s() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.tenant_id='%s'::uuid AND NEW.consent_type='personalized_memory' THEN RAISE EXCEPTION 'synthetic consent failure'; END IF; RETURN NEW; END $$;
CREATE TRIGGER %s BEFORE INSERT ON consent_record FOR EACH ROW EXECUTE FUNCTION %s();`, name, f.tenant, name, name))
must(t, err)
t.Cleanup(func() {
_, _ = f.s.pool.Exec(f.ctx, fmt.Sprintf("DROP TRIGGER IF EXISTS %s ON consent_record; DROP FUNCTION IF EXISTS %s();", name, name))
})
in := ProfileInput{HouseholdName: "授权失败家庭", Name: "授权失败老人", InitialConsents: saas.InitialConsentSelection{Types: []string{saas.ConsentTranscriptStorage, saas.ConsentPersonalMemory}, Confirmed: true}}
requestID := uuid.NewString()
_, err = f.s.CreateProfile(f.ctx, f.web, requestID, requestJSON(in), in)
if err == nil {
t.Fatal("profile creation succeeded despite failed consent")
}
var profiles, households, audits int
must(t, f.s.pool.QueryRow(f.ctx, `SELECT
(SELECT count(*) FROM subject_profile WHERE tenant_id=$1::uuid AND preferred_name=$2),
(SELECT count(*) FROM household WHERE tenant_id=$1::uuid AND name=$3),
(SELECT count(*) FROM audit_event WHERE request_id=$4)`, f.tenant, in.Name, in.HouseholdName, requestID).Scan(&profiles, &households, &audits))
if profiles != 0 || households != 0 || audits != 0 {
t.Fatalf("partial creation survived: profiles=%d households=%d audits=%d", profiles, households, audits)
}
_, err = f.s.pool.Exec(f.ctx, fmt.Sprintf("DROP TRIGGER %s ON consent_record; DROP FUNCTION %s();", name, name))
must(t, err)
_, err = f.s.CreateProfile(f.ctx, f.web, requestID, requestJSON(in), in)
must(t, err)
}
func TestProfileCreationIncludesMemoryConsentAndReplay(t *testing.T) {
f := newFixture(t)
in := ProfileInput{HouseholdName: "选项授权测试家庭", Name: "选项授权测试老人", InitialConsents: saas.InitialConsentSelection{Types: []string{saas.ConsentTranscriptStorage, saas.ConsentPersonalMemory}, Confirmed: true}}
requestID := uuid.NewString()
created, err := f.s.CreateProfile(f.ctx, f.web, requestID, requestJSON(in), in)
must(t, err)
replayed, err := f.s.CreateProfile(f.ctx, f.web, requestID, requestJSON(in), in)
must(t, err)
if replayed["elder_id"] != created["elder_id"] {
t.Fatal("profile replay created another elder")
}
var count, audits int
var enabled bool
must(t, f.s.pool.QueryRow(f.ctx, `SELECT
(SELECT count(*) FROM consent_record c WHERE c.subject_id=sp.id),
(SELECT count(*) FROM audit_event WHERE request_id=$2 AND action='consent.grant'),
(SELECT effective_consents @> '{"voice_chat":true,"transcript_storage":true,"personalized_memory":true,"raw_audio_storage":false,"story_extraction":false,"family_sharing":false}'::jsonb AND jsonb_array_length(source_consent_ids)=2 FROM consent_snapshot WHERE subject_id=sp.id ORDER BY snapshot_version DESC LIMIT 1)
FROM subject_profile sp WHERE sp.public_id=$1::uuid`, created["elder_id"], requestID).Scan(&count, &audits, &enabled))
if count != 2 || audits != 2 || !enabled {
t.Fatalf("default consents count=%d audits=%d enabled=%v", count, audits, enabled)
}
}
func TestMemoryWithdrawalSurvivesDeviceBinding(t *testing.T) {
f := newFixture(t)
api := saas.NewService(f.s.pool, audit.NewRepository(), nil, nil)
consents, err := api.ListConsents(f.ctx, f.user, f.elder)
must(t, err)
for _, c := range consents {
_, err = api.WithdrawConsent(f.ctx, f.user, f.elder, c.ID, uuid.NewString())
must(t, err)
}
f.bound()
var enabled bool
must(t, f.s.pool.QueryRow(f.ctx, `SELECT call_memory_authorized(id) FROM subject_profile WHERE public_id=$1::uuid`, f.elder).Scan(&enabled))
if enabled {
t.Fatal("device binding restored withdrawn memory consent")
}
}
func TestCreationGrantsOnlyExplicitlySelectedPermissions(t *testing.T) {
f := newFixture(t)
for _, tc := range []struct {
name string
selected []string
}{
{"legacy", nil},
{"subtitle_only", []string{saas.ConsentTranscriptStorage}},
{"all", []string{saas.ConsentTranscriptStorage, saas.ConsentRawAudioStorage, saas.ConsentStoryExtraction, saas.ConsentPersonalMemory, saas.ConsentFamilySharing}},
} {
t.Run(tc.name, func(t *testing.T) {
in := ProfileInput{HouseholdName: "授权选择测试" + tc.name, Name: "测试老人", InitialConsents: saas.InitialConsentSelection{Types: tc.selected, Confirmed: len(tc.selected) > 0}}
created, err := f.s.CreateProfile(f.ctx, f.web, uuid.NewString(), requestJSON(in), in)
must(t, err)
var kinds []string
var sourceCount int
must(t, f.s.pool.QueryRow(f.ctx, `SELECT ARRAY(SELECT consent_type FROM consent_record WHERE subject_id=sp.id ORDER BY consent_type), (SELECT jsonb_array_length(source_consent_ids) FROM consent_snapshot WHERE subject_id=sp.id ORDER BY snapshot_version DESC LIMIT 1) FROM subject_profile sp WHERE public_id=$1::uuid`, created["elder_id"]).Scan(&kinds, &sourceCount))
if len(kinds) != len(tc.selected) || sourceCount != len(tc.selected) {
t.Fatalf("grants=%v sourceCount=%d wanted=%v", kinds, sourceCount, tc.selected)
}
for _, kind := range kinds {
found := false
for _, selected := range tc.selected {
found = found || kind == selected
}
if !found {
t.Fatalf("unselected permission granted: %s", kind)
}
}
})
}
bad := ProfileInput{HouseholdName: "未确认家庭", Name: "未确认老人", InitialConsents: saas.InitialConsentSelection{Types: []string{saas.ConsentPersonalMemory}}}
if _, err := f.s.CreateProfile(f.ctx, f.web, uuid.NewString(), requestJSON(bad), bad); err == nil {
t.Fatal("unconfirmed consent was accepted")
}
var count int
must(t, f.s.pool.QueryRow(f.ctx, `SELECT count(*) FROM household WHERE tenant_id=$1::uuid AND name=$2`, f.tenant, bad.HouseholdName).Scan(&count))
if count != 0 {
t.Fatal("unconfirmed selection left an orphan household")
}
}