125 lines
6.3 KiB
Go
125 lines
6.3 KiB
Go
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")
|
||
|
|
}
|
||
|
|
}
|