|
1
|
package manager_test |
|
2
|
|
|
3
|
import ( |
|
4
|
"context" |
|
5
|
"fmt" |
|
6
|
"os" |
|
7
|
"path/filepath" |
|
8
|
"testing" |
|
9
|
|
|
10
|
"github.com/conflicthq/scuttlebot/internal/bots/manager" |
|
11
|
"log/slog" |
|
12
|
) |
|
13
|
|
|
14
|
var testLog = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) |
|
15
|
|
|
16
|
// stubProvisioner records RegisterAccount/ChangePassword calls. |
|
17
|
type stubProvisioner struct { |
|
18
|
accounts map[string]string |
|
19
|
failOn string // if set, RegisterAccount returns an error for this nick |
|
20
|
} |
|
21
|
|
|
22
|
func newStub() *stubProvisioner { |
|
23
|
return &stubProvisioner{accounts: make(map[string]string)} |
|
24
|
} |
|
25
|
|
|
26
|
func (p *stubProvisioner) RegisterAccount(name, pass string) error { |
|
27
|
if p.failOn == name { |
|
28
|
return fmt.Errorf("ACCOUNT_EXISTS") |
|
29
|
} |
|
30
|
if _, ok := p.accounts[name]; ok { |
|
31
|
return fmt.Errorf("ACCOUNT_EXISTS") |
|
32
|
} |
|
33
|
p.accounts[name] = pass |
|
34
|
return nil |
|
35
|
} |
|
36
|
|
|
37
|
func (p *stubProvisioner) ChangePassword(name, pass string) error { |
|
38
|
if _, ok := p.accounts[name]; !ok { |
|
39
|
return fmt.Errorf("ACCOUNT_DOES_NOT_EXIST") |
|
40
|
} |
|
41
|
p.accounts[name] = pass |
|
42
|
return nil |
|
43
|
} |
|
44
|
|
|
45
|
// stubChannels returns a fixed list of channels. |
|
46
|
type stubChannels struct { |
|
47
|
channels []string |
|
48
|
err error |
|
49
|
} |
|
50
|
|
|
51
|
func (c *stubChannels) ListChannels() ([]string, error) { |
|
52
|
return c.channels, c.err |
|
53
|
} |
|
54
|
|
|
55
|
func newManager(t *testing.T) *manager.Manager { |
|
56
|
t.Helper() |
|
57
|
return manager.New( |
|
58
|
"127.0.0.1:6667", |
|
59
|
t.TempDir(), |
|
60
|
newStub(), |
|
61
|
&stubChannels{channels: []string{"#fleet", "#ops"}}, |
|
62
|
testLog, |
|
63
|
) |
|
64
|
} |
|
65
|
|
|
66
|
// scribeSpec returns a minimal enabled scribe BotSpec. |
|
67
|
func scribeSpec() manager.BotSpec { |
|
68
|
return manager.BotSpec{ |
|
69
|
ID: "scribe", |
|
70
|
Nick: "scribe", |
|
71
|
Enabled: true, |
|
72
|
Config: map[string]any{"dir": "/tmp/scribe-test-logs"}, |
|
73
|
} |
|
74
|
} |
|
75
|
|
|
76
|
func TestSyncStartsEnabledBot(t *testing.T) { |
|
77
|
m := newManager(t) |
|
78
|
ctx, cancel := context.WithCancel(context.Background()) |
|
79
|
defer cancel() |
|
80
|
|
|
81
|
m.Sync(ctx, []manager.BotSpec{scribeSpec()}) |
|
82
|
|
|
83
|
running := m.Running() |
|
84
|
if len(running) != 1 || running[0] != "scribe" { |
|
85
|
t.Errorf("expected [scribe] running, got %v", running) |
|
86
|
} |
|
87
|
} |
|
88
|
|
|
89
|
func TestSyncDisabledBotNotStarted(t *testing.T) { |
|
90
|
m := newManager(t) |
|
91
|
ctx, cancel := context.WithCancel(context.Background()) |
|
92
|
defer cancel() |
|
93
|
|
|
94
|
spec := scribeSpec() |
|
95
|
spec.Enabled = false |
|
96
|
m.Sync(ctx, []manager.BotSpec{spec}) |
|
97
|
|
|
98
|
if len(m.Running()) != 0 { |
|
99
|
t.Errorf("expected no bots running, got %v", m.Running()) |
|
100
|
} |
|
101
|
} |
|
102
|
|
|
103
|
func TestSyncStopsDisabledBot(t *testing.T) { |
|
104
|
m := newManager(t) |
|
105
|
ctx, cancel := context.WithCancel(context.Background()) |
|
106
|
defer cancel() |
|
107
|
|
|
108
|
// Start it. |
|
109
|
m.Sync(ctx, []manager.BotSpec{scribeSpec()}) |
|
110
|
if len(m.Running()) != 1 { |
|
111
|
t.Fatalf("bot should be running before disable") |
|
112
|
} |
|
113
|
|
|
114
|
// Disable it. |
|
115
|
spec := scribeSpec() |
|
116
|
spec.Enabled = false |
|
117
|
m.Sync(ctx, []manager.BotSpec{spec}) |
|
118
|
|
|
119
|
if len(m.Running()) != 0 { |
|
120
|
t.Errorf("expected bot stopped after disable, got %v", m.Running()) |
|
121
|
} |
|
122
|
} |
|
123
|
|
|
124
|
func TestSyncIdempotent(t *testing.T) { |
|
125
|
m := newManager(t) |
|
126
|
ctx, cancel := context.WithCancel(context.Background()) |
|
127
|
defer cancel() |
|
128
|
|
|
129
|
spec := scribeSpec() |
|
130
|
m.Sync(ctx, []manager.BotSpec{spec}) |
|
131
|
m.Sync(ctx, []manager.BotSpec{spec}) // second call — should not start a second copy |
|
132
|
|
|
133
|
if len(m.Running()) != 1 { |
|
134
|
t.Errorf("expected exactly 1 running bot, got %v", m.Running()) |
|
135
|
} |
|
136
|
} |
|
137
|
|
|
138
|
func TestPasswordPersistence(t *testing.T) { |
|
139
|
dir := t.TempDir() |
|
140
|
prov := newStub() |
|
141
|
m1 := manager.New("127.0.0.1:6667", dir, prov, &stubChannels{}, testLog) |
|
142
|
|
|
143
|
ctx, cancel := context.WithCancel(context.Background()) |
|
144
|
m1.Sync(ctx, []manager.BotSpec{scribeSpec()}) |
|
145
|
cancel() |
|
146
|
|
|
147
|
// Passwords file should exist. |
|
148
|
pwPath := filepath.Join(dir, "bot_passwords.json") |
|
149
|
if _, err := os.Stat(pwPath); err != nil { |
|
150
|
t.Fatalf("passwords file not created: %v", err) |
|
151
|
} |
|
152
|
|
|
153
|
// Load a second manager from the same dir — it should reuse the same password |
|
154
|
// (ensureAccount will call ChangePassword, not RegisterAccount, because the stub |
|
155
|
// already has the account from the first run). |
|
156
|
m2 := manager.New("127.0.0.1:6667", dir, prov, &stubChannels{}, testLog) |
|
157
|
ctx2, cancel2 := context.WithCancel(context.Background()) |
|
158
|
defer cancel2() |
|
159
|
|
|
160
|
// Should not panic and should be able to start the bot. |
|
161
|
m2.Sync(ctx2, []manager.BotSpec{scribeSpec()}) |
|
162
|
if len(m2.Running()) != 1 { |
|
163
|
t.Errorf("second manager: expected 1 running bot, got %v", m2.Running()) |
|
164
|
} |
|
165
|
} |
|
166
|
|
|
167
|
func TestSyncOracleStarts(t *testing.T) { |
|
168
|
// Oracle now starts with default config (no API key — it won't respond to |
|
169
|
// summaries but the bot itself connects to IRC and runs). |
|
170
|
m := newManager(t) |
|
171
|
ctx, cancel := context.WithCancel(context.Background()) |
|
172
|
defer cancel() |
|
173
|
|
|
174
|
spec := manager.BotSpec{ID: "oracle", Nick: "oracle", Enabled: true} |
|
175
|
m.Sync(ctx, []manager.BotSpec{spec}) |
|
176
|
|
|
177
|
running := m.Running() |
|
178
|
found := false |
|
179
|
for _, nick := range running { |
|
180
|
if nick == "oracle" { |
|
181
|
found = true |
|
182
|
} |
|
183
|
} |
|
184
|
if !found { |
|
185
|
t.Errorf("expected oracle to be in Running, got %v", running) |
|
186
|
} |
|
187
|
} |
|
188
|
|
|
189
|
func TestSyncMultipleBots(t *testing.T) { |
|
190
|
m := newManager(t) |
|
191
|
ctx, cancel := context.WithCancel(context.Background()) |
|
192
|
defer cancel() |
|
193
|
|
|
194
|
specs := []manager.BotSpec{ |
|
195
|
scribeSpec(), |
|
196
|
{ID: "snitch", Nick: "snitch", Enabled: true}, |
|
197
|
} |
|
198
|
m.Sync(ctx, []manager.BotSpec{specs[0], specs[1]}) |
|
199
|
|
|
200
|
running := m.Running() |
|
201
|
if len(running) != 2 { |
|
202
|
t.Errorf("expected 2 running bots, got %v", running) |
|
203
|
} |
|
204
|
} |
|
205
|
|