package services import ( "sync/atomic" "encoding/json" "testing" "time" "agent-desk/internal/models" "agent-desk/internal/pkg/dto/request" "agent-desk/internal/pkg/enums" "agent-desk/internal/repositories" "github.com/mlogclub/simple/sqls" "gorm.io/gorm" ) func TestChannelConfigCompareAndSwapRejectsStalePolicyAndSecretWrites(t *testing.T) { for _, action:=range []string{"editor", "secret-reset"} { t.Run(action,func(t *testing.T) { previousDB:=sqls.DB() db:=setupChannelServiceTestDB(t) t.Cleanup(func(){sqls.SetDB(previousDB)}) agent:=createChannelServiceTestAgent(t,db,1001) channel,err:=ChannelService.CreateChannel(request.CreateChannelRequest{ChannelType:enums.ChannelTypeWeb,AIAgentID:agent.ID,Name:"Policy channel",Status:int(enums.StatusOk),ConfigJSON:`{"requireSignedIdentity":false,"userTokenSecret":"test-old-secret"}`},channelServiceTestOperator()) if err!=nil {t.Fatal(err)} var injected atomic.Bool callbackName:="test:concurrent_channel_policy" // Commit another writer after the stale service read, just before its SQL. // Preserve updated_at deliberately: bytewise JSON CAS must also catch this. if err:=db.Callback().Update().Before("gorm:update").Register(callbackName,func(tx *gorm.DB){ if tx.Statement.Schema==nil || tx.Statement.Schema.Table!="t_channel" || !injected.CompareAndSwap(false,true){return} cfg,parseErr:=ChannelService.ParseWebChannelConfig(channel.ConfigJSON) if parseErr!=nil {tx.AddError(parseErr);return} cfg.RequireSignedIdentity=true cfg.UserTokenSecret="test-new-secret" encoded,encodeErr:=json.Marshal(cfg) if encodeErr!=nil {tx.AddError(encodeErr);return} if writeErr:=db.Session(&gorm.Session{NewDB:true}).Model(&models.Channel{}).Where("id = ?",channel.ID).Updates(map[string]any{"config_json":string(encoded),"updated_at":channel.UpdatedAt}).Error;writeErr!=nil{tx.AddError(writeErr)} });err!=nil{t.Fatal(err)} t.Cleanup(func(){_ = db.Callback().Update().Remove(callbackName)}) switch action { case "editor": err=ChannelService.UpdateChannel(request.UpdateChannelRequest{ID:channel.ID,CreateChannelRequest:request.CreateChannelRequest{ChannelType:enums.ChannelTypeWeb,AIAgentID:agent.ID,Name:"Old editor title",Status:int(enums.StatusOk),ConfigJSON:`{"title":"old editor"}`}},channelServiceTestOperator()) case "secret-reset": var secret string;secret,err=ChannelService.ResetUserTokenSecret(channel.ID,channelServiceTestOperator());if secret!=""{t.Fatal("failed reset returned an uncommitted secret")} } if !injected.Load() || err==nil {t.Fatal("stale writer was not rejected")} current:=ChannelService.Get(channel.ID) cfg,parseErr:=ChannelService.ParseWebChannelConfig(current.ConfigJSON) if parseErr!=nil || !cfg.RequireSignedIdentity || cfg.UserTokenSecret!="test-new-secret" {t.Fatal("stale writer replaced the concurrent policy/secret")} }) } } func TestChannelConfigCompareAndSwapUsesExactBytes(t *testing.T) { db,channel:=setupIdentitySecurityDB(t) channel.ConfigJSON=`{"userTokenSecret":"CaseSensitive"}` if err:=db.Model(channel).Updates(map[string]any{"config_json":channel.ConfigJSON,"updated_at":time.Now()}).Error;err!=nil{t.Fatal(err)} expected:=*channel if err:=db.Model(channel).UpdateColumn("config_json",`{"userTokenSecret":"casesensitive"}`).Error;err!=nil{t.Fatal(err)} updated,err:=repositories.ChannelRepository.UpdatesConfigIfUnchanged(db,&expected,map[string]any{"config_json":`{"requireSignedIdentity":false}`}) if err!=nil{t.Fatal(err)} if updated{t.Fatal("case-sensitive secret change was not treated as a conflict")} } func TestChannelConfigCompareAndSwapAllowsCurrentEditorAndReset(t *testing.T) { previousDB:=sqls.DB() db:=setupChannelServiceTestDB(t) t.Cleanup(func(){sqls.SetDB(previousDB)}) agent:=createChannelServiceTestAgent(t,db,1001) channel,err:=ChannelService.CreateChannel(request.CreateChannelRequest{ChannelType:enums.ChannelTypeWeb,AIAgentID:agent.ID,Name:"Current policy",Status:int(enums.StatusOk),ConfigJSON:`{"requireSignedIdentity":true}`},channelServiceTestOperator()) if err!=nil{t.Fatal(err)} err=ChannelService.UpdateChannel(request.UpdateChannelRequest{ID:channel.ID,CreateChannelRequest:request.CreateChannelRequest{ChannelType:enums.ChannelTypeWeb,AIAgentID:agent.ID,Name:"Current policy edited",Status:int(enums.StatusOk),ConfigJSON:`{"title":"Updated title"}`}},channelServiceTestOperator()) if err!=nil{t.Fatal(err)} secret,err:=ChannelService.ResetUserTokenSecret(channel.ID,channelServiceTestOperator()) if err!=nil || secret==""{t.Fatal("current secret reset failed")} cfg,err:=ChannelService.ParseWebChannelConfig(ChannelService.Get(channel.ID).ConfigJSON) if err!=nil || !cfg.RequireSignedIdentity || cfg.UserTokenSecret!=secret{t.Fatal("current update/reset lost policy or returned unsaved secret")} }