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.
79 lines
4.7 KiB
Go
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")}
|
|
}
|