Files
agent-desk-ng/internal/services/channel_policy_concurrency_test.go
mono 63e0457886 fix: harden identity, live access, drafts and setup (review only)
Source-reviewed hardening changes and regression tests. UNBUILT / UNTESTED: not a deployable release. Go formatting, compilation, automated regression tests, full frontend validation and staging acceptance remain required. See docs/hardening/VALIDATION.md. Main remains the original clean snapshot; no upstream Git history imported.
2026-10-16 12:15:20 +00:00

79 lines
4.7 KiB
Go

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")}
}