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