package services import ( "encoding/json" "sync/atomic" "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") } }