| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 |
- //go:build unit
- package service
- import (
- "context"
- "errors"
- "strings"
- "testing"
- "time"
- "unicode/utf8"
- "github.com/stretchr/testify/require"
- )
- type duplicateChannelMonitorRepoStub struct {
- ChannelMonitorRepository
- source *ChannelMonitor
- created []*ChannelMonitor
- byOperation map[string]*ChannelMonitor
- nextID int64
- }
- func (r *duplicateChannelMonitorRepoStub) GetByID(_ context.Context, id int64) (*ChannelMonitor, error) {
- if r.source == nil || r.source.ID != id {
- return nil, ErrChannelMonitorNotFound
- }
- return r.source, nil
- }
- func (r *duplicateChannelMonitorRepoStub) Create(_ context.Context, monitor *ChannelMonitor) error {
- r.nextID++
- monitor.ID = 100 + r.nextID
- monitor.CreatedAt = time.Date(2026, time.July, 16, 8, 0, 0, 0, time.UTC)
- monitor.UpdatedAt = monitor.CreatedAt
- stored := *monitor
- stored.ExtraModels = append([]string(nil), monitor.ExtraModels...)
- stored.ExtraHeaders = cloneStringMap(monitor.ExtraHeaders)
- stored.BodyOverride = mustCloneJSONMap(monitor.BodyOverride)
- if monitor.TemplateID != nil {
- templateID := *monitor.TemplateID
- stored.TemplateID = &templateID
- }
- r.created = append(r.created, &stored)
- if stored.DuplicateOperationID != "" {
- if r.byOperation == nil {
- r.byOperation = make(map[string]*ChannelMonitor)
- }
- r.byOperation[stored.DuplicateOperationID] = &stored
- }
- return nil
- }
- func (r *duplicateChannelMonitorRepoStub) FindByDuplicateOperationID(_ context.Context, operationID string) (*ChannelMonitor, error) {
- monitor := r.byOperation[operationID]
- if monitor == nil {
- return nil, nil
- }
- cloned := *monitor
- cloned.ExtraModels = append([]string(nil), monitor.ExtraModels...)
- cloned.ExtraHeaders = cloneStringMap(monitor.ExtraHeaders)
- cloned.BodyOverride = mustCloneJSONMap(monitor.BodyOverride)
- return &cloned, nil
- }
- func cloneStringMap(source map[string]string) map[string]string {
- if source == nil {
- return nil
- }
- cloned := make(map[string]string, len(source))
- for key, value := range source {
- cloned[key] = value
- }
- return cloned
- }
- func mustCloneJSONMap(source map[string]any) map[string]any {
- if source == nil {
- return nil
- }
- cloned, err := cloneChannelMonitorJSONMap(source)
- if err != nil {
- panic(err)
- }
- return cloned
- }
- type duplicateChannelMonitorEncryptor struct {
- decryptErr error
- encryptErr error
- }
- func (e *duplicateChannelMonitorEncryptor) Encrypt(plaintext string) (string, error) {
- if e.encryptErr != nil {
- return "", e.encryptErr
- }
- return "NEW:" + plaintext, nil
- }
- func (e *duplicateChannelMonitorEncryptor) Decrypt(ciphertext string) (string, error) {
- if e.decryptErr != nil {
- return "", e.decryptErr
- }
- if !strings.HasPrefix(ciphertext, "OLD:") && !strings.HasPrefix(ciphertext, "NEW:") {
- return "", errors.New("invalid ciphertext")
- }
- return strings.TrimPrefix(strings.TrimPrefix(ciphertext, "OLD:"), "NEW:"), nil
- }
- func TestDuplicateChannelMonitorCopiesConfigurationAndResetsRuntimeState(t *testing.T) {
- lastCheckedAt := time.Date(2026, time.July, 15, 7, 0, 0, 0, time.UTC)
- templateID := int64(9)
- source := &ChannelMonitor{
- ID: 42,
- Name: "primary",
- Provider: MonitorProviderOpenAI,
- APIMode: MonitorAPIModeResponses,
- Endpoint: "https://api.example.com",
- APIKey: "OLD:top-secret",
- PrimaryModel: "gpt-5.4-mini",
- ExtraModels: []string{"gpt-5.4", "gpt-5.3"},
- GroupName: "production",
- Enabled: true,
- IntervalSeconds: 90,
- JitterSeconds: 15,
- LastCheckedAt: &lastCheckedAt,
- CreatedBy: 4,
- CreatedAt: lastCheckedAt.Add(-time.Hour),
- UpdatedAt: lastCheckedAt,
- TemplateID: &templateID,
- ExtraHeaders: map[string]string{"User-Agent": "Codex"},
- BodyOverrideMode: MonitorBodyOverrideModeMerge,
- BodyOverride: map[string]any{
- "metadata": map[string]any{"source": "original"},
- },
- }
- repo := &duplicateChannelMonitorRepoStub{source: source}
- service := NewChannelMonitorService(repo, &duplicateChannelMonitorEncryptor{})
- duplicate, err := service.Duplicate(context.Background(), source.ID, 77, "admin:77", "copy-primary")
- require.NoError(t, err)
- require.Len(t, repo.created, 1)
- stored := repo.created[0]
- require.NotEqual(t, source.ID, duplicate.ID)
- require.Equal(t, "primary (Copy)", duplicate.Name)
- require.Equal(t, source.Provider, duplicate.Provider)
- require.Equal(t, source.APIMode, duplicate.APIMode)
- require.Equal(t, source.Endpoint, duplicate.Endpoint)
- require.Equal(t, "top-secret", duplicate.APIKey)
- require.Equal(t, "NEW:top-secret", stored.APIKey)
- require.Equal(t, source.PrimaryModel, duplicate.PrimaryModel)
- require.Equal(t, source.ExtraModels, duplicate.ExtraModels)
- require.Equal(t, source.GroupName, duplicate.GroupName)
- require.Equal(t, source.IntervalSeconds, duplicate.IntervalSeconds)
- require.Equal(t, source.JitterSeconds, duplicate.JitterSeconds)
- require.Equal(t, source.TemplateID, duplicate.TemplateID)
- require.Equal(t, source.ExtraHeaders, duplicate.ExtraHeaders)
- require.Equal(t, source.BodyOverrideMode, duplicate.BodyOverrideMode)
- require.Equal(t, source.BodyOverride, duplicate.BodyOverride)
- require.False(t, duplicate.Enabled)
- require.Nil(t, duplicate.LastCheckedAt)
- require.Equal(t, int64(77), duplicate.CreatedBy)
- require.False(t, duplicate.APIKeyDecryptFailed)
- require.NotEmpty(t, duplicate.DuplicateOperationID)
- duplicate.ExtraModels[0] = "changed"
- duplicate.ExtraHeaders["User-Agent"] = "changed"
- duplicate.BodyOverride["metadata"].(map[string]any)["source"] = "changed"
- *duplicate.TemplateID = 10
- require.Equal(t, []string{"gpt-5.4", "gpt-5.3"}, source.ExtraModels)
- require.Equal(t, "Codex", source.ExtraHeaders["User-Agent"])
- require.Equal(t, "original", source.BodyOverride["metadata"].(map[string]any)["source"])
- require.Equal(t, int64(9), *source.TemplateID)
- require.Equal(t, "OLD:top-secret", source.APIKey)
- require.True(t, source.Enabled)
- require.Equal(t, &lastCheckedAt, source.LastCheckedAt)
- }
- func TestDuplicateChannelMonitorNamePreservesSuffixWithinSchemaLimit(t *testing.T) {
- name := duplicateChannelMonitorName(strings.Repeat("界", 100))
- require.Equal(t, 100, utf8.RuneCountInString(name))
- require.True(t, strings.HasSuffix(name, " (Copy)"))
- }
- func TestDuplicateChannelMonitorRejectsUndecryptableAPIKey(t *testing.T) {
- source := &ChannelMonitor{ID: 42, Name: "broken", APIKey: "OLD:broken"}
- repo := &duplicateChannelMonitorRepoStub{source: source}
- service := NewChannelMonitorService(repo, &duplicateChannelMonitorEncryptor{decryptErr: errors.New("wrong encryption key")})
- duplicate, err := service.Duplicate(context.Background(), source.ID, 77, "admin:77", "copy-broken")
- require.Nil(t, duplicate)
- require.ErrorIs(t, err, ErrChannelMonitorAPIKeyDecryptFailed)
- require.Empty(t, repo.created)
- require.Equal(t, "OLD:broken", source.APIKey)
- }
- func TestDuplicateChannelMonitorRecoversCommittedCopyForSameOperation(t *testing.T) {
- source := &ChannelMonitor{
- ID: 42,
- Name: "primary",
- Provider: MonitorProviderOpenAI,
- APIMode: MonitorAPIModeResponses,
- Endpoint: "https://api.example.com",
- APIKey: "OLD:top-secret",
- PrimaryModel: "gpt-5.4-mini",
- IntervalSeconds: 60,
- BodyOverrideMode: MonitorBodyOverrideModeOff,
- }
- repo := &duplicateChannelMonitorRepoStub{source: source}
- service := NewChannelMonitorService(repo, &duplicateChannelMonitorEncryptor{})
- first, err := service.Duplicate(context.Background(), source.ID, 77, "admin:77", "stable-key")
- require.NoError(t, err)
- retry, err := service.Duplicate(context.Background(), source.ID, 77, "admin:77", "stable-key")
- require.NoError(t, err)
- require.Len(t, repo.created, 1, "same operation must not create a second monitor")
- require.Equal(t, first.ID, retry.ID)
- require.Equal(t, "top-secret", retry.APIKey)
- require.Equal(t, first.DuplicateOperationID, retry.DuplicateOperationID)
- require.NotContains(t, retry.ExtraHeaders, ChannelMonitorDuplicateOperationIDMetadataKey)
- otherActor, err := service.Duplicate(context.Background(), source.ID, 88, "admin:88", "stable-key")
- require.NoError(t, err)
- require.NotEqual(t, first.ID, otherActor.ID)
- require.Len(t, repo.created, 2, "operation identity must include the actor scope")
- }
- func TestChannelMonitorDuplicateOperationMetadataKeyCannotBeSubmittedAsHeader(t *testing.T) {
- err := validateExtraHeaders(map[string]string{
- ChannelMonitorDuplicateOperationIDMetadataKey: "forged-operation",
- })
- require.Error(t, err)
- }
|