2026-08-26 06:41:07 +08:00
package httpapi
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/concurrency"
"data-terminal-cloud/internal/identity"
"data-terminal-cloud/internal/saas"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestSaaSHouseholdAndSubjectWriteHandlers ( t * testing . T ) {
databaseURL := os . Getenv ( "TEST_DATABASE_URL" )
if databaseURL == "" {
t . Skip ( "TEST_DATABASE_URL is not set" )
}
pool , err := pgxpool . New ( context . Background (), databaseURL )
if err != nil {
t . Fatal ( err )
}
defer pool . Close ()
service := saas . NewService ( pool , audit . NewRepository (), nil , nil )
principal := identity . Principal { Issuer : "http://localhost:58180/realms/voice-local" , Subject : "00000000-0000-0000-0000-000000000101" }
var householdID , subjectID , factID , consentID string
defer func () {
if subjectID != "" {
if consentID != "" {
_ , _ = pool . Exec ( context . Background (), `DELETE FROM audit_event WHERE resource_id=$1::uuid` , consentID )
}
if factID != "" {
_ , _ = pool . Exec ( context . Background (), `DELETE FROM audit_event WHERE resource_id=$1::uuid` , factID )
}
_ , _ = pool . Exec ( context . Background (), `DELETE FROM consent_snapshot WHERE subject_id=(SELECT id FROM subject_profile WHERE public_id=$1::uuid)` , subjectID )
_ , _ = pool . Exec ( context . Background (), `DELETE FROM consent_record WHERE subject_id=(SELECT id FROM subject_profile WHERE public_id=$1::uuid)` , subjectID )
_ , _ = pool . Exec ( context . Background (), `DELETE FROM subject_fact WHERE subject_id=(SELECT id FROM subject_profile WHERE public_id=$1::uuid)` , subjectID )
_ , _ = pool . Exec ( context . Background (), `DELETE FROM audit_event WHERE resource_id=$1::uuid` , subjectID )
_ , _ = pool . Exec ( context . Background (), `DELETE FROM subject_profile WHERE public_id=$1::uuid` , subjectID )
}
if householdID != "" {
_ , _ = pool . Exec ( context . Background (), `DELETE FROM audit_event WHERE resource_id=$1::uuid` , householdID )
_ , _ = pool . Exec ( context . Background (), `DELETE FROM household_member WHERE household_id=(SELECT id FROM household WHERE public_id=$1::uuid)` , householdID )
_ , _ = pool . Exec ( context . Background (), `DELETE FROM household WHERE public_id=$1::uuid` , householdID )
}
}()
request := authenticatedRequest ( http . MethodPost , "/api/v1/households" , `{"name":"HTTP写接口集成家庭"}` , principal )
request . Header . Set ( "X-Tenant-Id" , "10000000-0000-0000-0000-000000000010" )
recorder := httptest . NewRecorder ()
CreateHouseholdHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusCreated || recorder . Header (). Get ( "ETag" ) != concurrency . FormatETag ( 1 ) {
t . Fatalf ( "create household status=%d headers=%v body=%s" , recorder . Code , recorder . Header (), recorder . Body . String ())
}
var householdResponse struct {
Data saas . Household `json:"data"`
}
if err := json . Unmarshal ( recorder . Body . Bytes (), & householdResponse ); err != nil {
t . Fatal ( err )
}
householdID = householdResponse . Data . ID
request = authenticatedRequest ( http . MethodPatch , "/api/v1/households/" + householdID , `{"name":"HTTP写接口集成家庭-修改"}` , principal )
request . SetPathValue ( "id" , householdID )
recorder = httptest . NewRecorder ()
PatchHouseholdHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusPreconditionRequired {
t . Fatalf ( "missing household ETag status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
request = authenticatedRequest ( http . MethodPatch , "/api/v1/households/" + householdID , `{"name":"HTTP写接口集成家庭-修改"}` , principal )
request . SetPathValue ( "id" , householdID )
request . Header . Set ( "If-Match" , concurrency . FormatETag ( 1 ))
recorder = httptest . NewRecorder ()
PatchHouseholdHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusOK || recorder . Header (). Get ( "ETag" ) != concurrency . FormatETag ( 2 ) {
t . Fatalf ( "patch household status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
request = authenticatedRequest ( http . MethodPost , "/api/v1/households/" + householdID + "/subjects" , `{"preferred_name":"HTTP集成长者","language":"zh-CN","timezone":"Asia/Shanghai","profile_data":{"synthetic":true},"conversation_preferences":{"pace":"slow"}}` , principal )
request . SetPathValue ( "id" , householdID )
recorder = httptest . NewRecorder ()
CreateSubjectHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusCreated || recorder . Header (). Get ( "ETag" ) != concurrency . FormatETag ( 1 ) {
t . Fatalf ( "create subject status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
var subjectResponse struct {
Data saas . Subject `json:"data"`
}
if err := json . Unmarshal ( recorder . Body . Bytes (), & subjectResponse ); err != nil {
t . Fatal ( err )
}
subjectID = subjectResponse . Data . ID
2026-10-08 10:06:44 +08:00
initialConsents , err := service . ListConsents ( context . Background (), principal , subjectID )
if err != nil || len ( initialConsents ) != 0 {
t . Fatalf ( "omitted consent selection granted permissions: %+v err=%v" , initialConsents , err )
}
request = authenticatedRequest ( http . MethodPost , "/api/v1/households/" + householdID + "/subjects" , `{"preferred_name":"未确认授权","language":"zh-CN","timezone":"Asia/Shanghai","profile_data":{},"conversation_preferences":{},"initial_consents":{"types":["personalized_memory"],"confirmed":false}}` , principal )
request . SetPathValue ( "id" , householdID )
recorder = httptest . NewRecorder ()
CreateSubjectHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusBadRequest {
t . Fatalf ( "unconfirmed selection status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
2026-08-26 06:41:07 +08:00
request = authenticatedRequest ( http . MethodPatch , "/api/v1/subjects/" + subjectID , `{"preferred_name":"HTTP集成长者-修改"}` , principal )
request . SetPathValue ( "id" , subjectID )
request . Header . Set ( "If-Match" , concurrency . FormatETag ( 1 ))
recorder = httptest . NewRecorder ()
PatchSubjectHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusOK || recorder . Header (). Get ( "ETag" ) != concurrency . FormatETag ( 2 ) {
t . Fatalf ( "patch subject status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
var patchedSubject struct {
Data saas . Subject `json:"data"`
}
if err := json . Unmarshal ( recorder . Body . Bytes (), & patchedSubject ); err != nil || patchedSubject . Data . PreferredName != "HTTP集成长者-修改" {
t . Fatalf ( "patched subject=%+v err=%v" , patchedSubject , err )
}
request = authenticatedRequest ( http . MethodPost , "/api/v1/subjects/" + subjectID + "/facts" , `{"fact_type":"hometown","content":"合成城市","source_type":"family_entry","source_ref":"http-integration://source","confirmation_status":"pending","sensitivity":"normal"}` , principal )
request . SetPathValue ( "id" , subjectID )
recorder = httptest . NewRecorder ()
CreateSubjectFactHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusCreated || recorder . Header (). Get ( "ETag" ) != concurrency . FormatETag ( 1 ) {
t . Fatalf ( "create fact status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
var factResponse struct {
Data saas . SubjectFact `json:"data"`
}
if err := json . Unmarshal ( recorder . Body . Bytes (), & factResponse ); err != nil {
t . Fatal ( err )
}
factID = factResponse . Data . ID
request = authenticatedRequest ( http . MethodPatch , "/api/v1/subjects/" + subjectID + "/facts/" + factID , `{"confirmation_status":"confirmed"}` , principal )
request . SetPathValue ( "id" , subjectID )
request . SetPathValue ( "fact_id" , factID )
request . Header . Set ( "If-Match" , concurrency . FormatETag ( 1 ))
recorder = httptest . NewRecorder ()
PatchSubjectFactHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusOK || recorder . Header (). Get ( "ETag" ) != concurrency . FormatETag ( 2 ) {
t . Fatalf ( "patch fact status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
request = authenticatedRequest ( http . MethodPost , "/api/v1/subjects/" + subjectID + "/consents" , `{"type":"story_extraction","scope":{"future":true},"policy_version":"local-v1","notice_digest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","evidence_ref":"http-integration://consent"}` , principal )
request . SetPathValue ( "id" , subjectID )
recorder = httptest . NewRecorder ()
GrantConsentHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusCreated {
t . Fatalf ( "grant consent status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
var consentResponse struct {
Data saas . Consent `json:"data"`
}
if err := json . Unmarshal ( recorder . Body . Bytes (), & consentResponse ); err != nil {
t . Fatal ( err )
}
consentID = consentResponse . Data . ID
request = authenticatedRequest ( http . MethodPost , "/api/v1/subjects/" + subjectID + "/consents/" + consentID + ":withdraw" , `` , principal )
request . SetPathValue ( "id" , subjectID )
request . SetPathValue ( "consent_id" , consentID )
recorder = httptest . NewRecorder ()
WithdrawConsentHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusOK {
t . Fatalf ( "withdraw consent status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
}
func authenticatedRequest ( method , target , body string , principal identity . Principal ) * http . Request {
request := httptest . NewRequest ( method , target , strings . NewReader ( body ))
request . Header . Set ( "Content-Type" , "application/json" )
return request . WithContext ( identity . WithPrincipal ( request . Context (), principal ))
}