Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.
Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.
Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.
Expected source tree: 0a4a7528e9.
119 lines
5.1 KiB
Go
119 lines
5.1 KiB
Go
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")
|
|
}
|
|
}
|