Files
Memoh/internal/channel/lifecycle_test.go
T

295 lines
8.9 KiB
Go

package channel
import (
"context"
"errors"
"testing"
)
type fakeLifecycleStore struct {
resolveFunc func(ctx context.Context, botID string, channelType ChannelType) (ChannelConfig, error)
upsertFunc func(ctx context.Context, botID string, channelType ChannelType, req UpsertConfigRequest) (ChannelConfig, error)
statusFunc func(ctx context.Context, botID string, channelType ChannelType, disabled bool) (ChannelConfig, error)
deleteFunc func(ctx context.Context, botID string, channelType ChannelType) error
}
func (f *fakeLifecycleStore) ResolveEffectiveConfig(ctx context.Context, botID string, channelType ChannelType) (ChannelConfig, error) {
if f.resolveFunc == nil {
return ChannelConfig{}, ErrChannelConfigNotFound
}
return f.resolveFunc(ctx, botID, channelType)
}
func (f *fakeLifecycleStore) UpsertConfig(ctx context.Context, botID string, channelType ChannelType, req UpsertConfigRequest) (ChannelConfig, error) {
if f.upsertFunc == nil {
return ChannelConfig{}, nil
}
return f.upsertFunc(ctx, botID, channelType, req)
}
func (f *fakeLifecycleStore) UpdateConfigDisabled(ctx context.Context, botID string, channelType ChannelType, disabled bool) (ChannelConfig, error) {
if f.statusFunc == nil {
return ChannelConfig{}, ErrChannelConfigNotFound
}
return f.statusFunc(ctx, botID, channelType, disabled)
}
func (f *fakeLifecycleStore) DeleteConfig(ctx context.Context, botID string, channelType ChannelType) error {
if f.deleteFunc == nil {
return nil
}
return f.deleteFunc(ctx, botID, channelType)
}
type fakeConnectionController struct {
ensureFunc func(ctx context.Context, cfg ChannelConfig) error
removeFunc func(ctx context.Context, botID string, channelType ChannelType)
}
func (f *fakeConnectionController) EnsureConnection(ctx context.Context, cfg ChannelConfig) error {
if f.ensureFunc == nil {
return nil
}
return f.ensureFunc(ctx, cfg)
}
func (f *fakeConnectionController) RemoveConnection(ctx context.Context, botID string, channelType ChannelType) {
if f.removeFunc == nil {
return
}
f.removeFunc(ctx, botID, channelType)
}
func TestLifecycleUpsertDisabledRemovesConnection(t *testing.T) {
t.Parallel()
removeCalled := false
store := &fakeLifecycleStore{
upsertFunc: func(ctx context.Context, botID string, channelType ChannelType, req UpsertConfigRequest) (ChannelConfig, error) {
return ChannelConfig{ID: "cfg-1", BotID: botID, ChannelType: channelType, Disabled: true}, nil
},
}
controller := &fakeConnectionController{
removeFunc: func(ctx context.Context, botID string, channelType ChannelType) {
removeCalled = true
},
}
service := NewLifecycle(store, controller)
disabled := true
cfg, err := service.UpsertBotChannelConfig(context.Background(), "bot-1", ChannelType("telegram"), UpsertConfigRequest{
Credentials: map[string]any{"botToken": "x"},
Disabled: &disabled,
})
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if !cfg.Disabled {
t.Fatalf("expected disabled config")
}
if !removeCalled {
t.Fatalf("expected remove connection to be called")
}
}
func TestLifecycleUpsertEnableFailureRollsBackToPrevious(t *testing.T) {
t.Parallel()
previous := ChannelConfig{
ID: "cfg-prev",
BotID: "bot-1",
ChannelType: ChannelType("telegram"),
Credentials: map[string]any{"botToken": "old"},
Disabled: false,
}
newConfig := ChannelConfig{
ID: "cfg-new",
BotID: "bot-1",
ChannelType: ChannelType("telegram"),
Credentials: map[string]any{"botToken": "new"},
Disabled: false,
}
upsertCalls := 0
ensureCalls := 0
store := &fakeLifecycleStore{
resolveFunc: func(ctx context.Context, botID string, channelType ChannelType) (ChannelConfig, error) {
return previous, nil
},
upsertFunc: func(ctx context.Context, botID string, channelType ChannelType, req UpsertConfigRequest) (ChannelConfig, error) {
upsertCalls++
if upsertCalls == 1 {
return newConfig, nil
}
if token := ReadString(req.Credentials, "botToken"); token != "old" {
t.Fatalf("expected rollback credentials old, got %s", token)
}
return previous, nil
},
}
controller := &fakeConnectionController{
ensureFunc: func(ctx context.Context, cfg ChannelConfig) error {
ensureCalls++
if ensureCalls == 1 {
return errors.New("dial failed")
}
return nil
},
}
service := NewLifecycle(store, controller)
enabled := false
_, err := service.UpsertBotChannelConfig(context.Background(), "bot-1", ChannelType("telegram"), UpsertConfigRequest{
Credentials: map[string]any{"botToken": "new"},
Disabled: &enabled,
})
if err == nil {
t.Fatalf("expected error, got nil")
}
if upsertCalls != 2 {
t.Fatalf("expected 2 upsert calls (write + rollback), got %d", upsertCalls)
}
if ensureCalls != 2 {
t.Fatalf("expected 2 ensure calls (new + restore), got %d", ensureCalls)
}
}
func TestLifecycleUpsertEnableFailureWithoutPreviousDeletesNewConfig(t *testing.T) {
t.Parallel()
deleteCalls := 0
store := &fakeLifecycleStore{
resolveFunc: func(ctx context.Context, botID string, channelType ChannelType) (ChannelConfig, error) {
return ChannelConfig{}, ErrChannelConfigNotFound
},
upsertFunc: func(ctx context.Context, botID string, channelType ChannelType, req UpsertConfigRequest) (ChannelConfig, error) {
return ChannelConfig{
ID: "cfg-new",
BotID: botID,
ChannelType: channelType,
Credentials: map[string]any{"botToken": "new"},
}, nil
},
deleteFunc: func(ctx context.Context, botID string, channelType ChannelType) error {
deleteCalls++
return nil
},
}
controller := &fakeConnectionController{
ensureFunc: func(ctx context.Context, cfg ChannelConfig) error {
return errors.New("start failed")
},
}
service := NewLifecycle(store, controller)
enabled := false
_, err := service.UpsertBotChannelConfig(context.Background(), "bot-1", ChannelType("telegram"), UpsertConfigRequest{
Credentials: map[string]any{"botToken": "new"},
Disabled: &enabled,
})
if err == nil {
t.Fatalf("expected error, got nil")
}
if deleteCalls != 1 {
t.Fatalf("expected 1 delete call for rollback, got %d", deleteCalls)
}
}
func TestLifecycleDeleteStopsConnection(t *testing.T) {
t.Parallel()
removeCalled := false
store := &fakeLifecycleStore{
deleteFunc: func(ctx context.Context, botID string, channelType ChannelType) error {
return nil
},
}
controller := &fakeConnectionController{
removeFunc: func(ctx context.Context, botID string, channelType ChannelType) {
removeCalled = true
},
}
service := NewLifecycle(store, controller)
if err := service.DeleteBotChannelConfig(context.Background(), "bot-1", ChannelType("telegram")); err != nil {
t.Fatalf("expected no error, got %v", err)
}
if !removeCalled {
t.Fatalf("expected remove connection to be called")
}
}
func TestLifecycleSetBotChannelStatusDisable(t *testing.T) {
t.Parallel()
removeCalled := false
store := &fakeLifecycleStore{
statusFunc: func(ctx context.Context, botID string, channelType ChannelType, disabled bool) (ChannelConfig, error) {
if !disabled {
t.Fatalf("expected disabled=true update")
}
return ChannelConfig{ID: "cfg-1", BotID: botID, ChannelType: channelType, Disabled: true}, nil
},
}
controller := &fakeConnectionController{
removeFunc: func(ctx context.Context, botID string, channelType ChannelType) {
removeCalled = true
},
}
service := NewLifecycle(store, controller)
cfg, err := service.SetBotChannelStatus(context.Background(), "bot-1", ChannelType("telegram"), true)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if !cfg.Disabled {
t.Fatalf("expected disabled config")
}
if !removeCalled {
t.Fatalf("expected remove connection to be called")
}
}
func TestLifecycleSetBotChannelStatusEnableFailureRollsBack(t *testing.T) {
t.Parallel()
statusCalls := 0
removeCalled := false
store := &fakeLifecycleStore{
statusFunc: func(ctx context.Context, botID string, channelType ChannelType, disabled bool) (ChannelConfig, error) {
statusCalls++
if statusCalls == 1 && disabled {
t.Fatalf("first status update should enable config")
}
if statusCalls == 2 && !disabled {
t.Fatalf("second status update should rollback to disabled=true")
}
return ChannelConfig{
ID: "cfg-1",
BotID: botID,
ChannelType: channelType,
Disabled: disabled,
}, nil
},
}
controller := &fakeConnectionController{
ensureFunc: func(ctx context.Context, cfg ChannelConfig) error {
return errors.New("start failed")
},
removeFunc: func(ctx context.Context, botID string, channelType ChannelType) {
removeCalled = true
},
}
service := NewLifecycle(store, controller)
_, err := service.SetBotChannelStatus(context.Background(), "bot-1", ChannelType("telegram"), false)
if err == nil {
t.Fatalf("expected error, got nil")
}
if statusCalls != 2 {
t.Fatalf("expected 2 status updates, got %d", statusCalls)
}
if !removeCalled {
t.Fatalf("expected remove connection to be called on failed enable")
}
}