ScuttleBot

scuttlebot / internal / api / login_test.go
Blame History Raw 267 lines
1
package api_test
2
3
import (
4
"encoding/json"
5
"net/http"
6
"testing"
7
8
"github.com/conflicthq/scuttlebot/internal/api"
9
"github.com/conflicthq/scuttlebot/internal/auth"
10
"github.com/conflicthq/scuttlebot/internal/registry"
11
"net/http/httptest"
12
"path/filepath"
13
)
14
15
// newAdminStore creates an AdminStore backed by a temp file.
16
func newAdminStore(t *testing.T) *auth.AdminStore {
17
t.Helper()
18
s, err := auth.NewAdminStore(filepath.Join(t.TempDir(), "admins.json"))
19
if err != nil {
20
t.Fatalf("NewAdminStore: %v", err)
21
}
22
return s
23
}
24
25
// newTestServerWithAdmins creates a test server with admin auth configured.
26
func newTestServerWithAdmins(t *testing.T) (*httptest.Server, *auth.AdminStore) {
27
t.Helper()
28
admins := newAdminStore(t)
29
if err := admins.Add("admin", "hunter2"); err != nil {
30
t.Fatalf("Add admin: %v", err)
31
}
32
reg := registry.New(newMock(), []byte("test-signing-key"))
33
srv := api.New(reg, auth.TestStore(testToken), nil, nil, admins, nil, nil, nil, "", testLog)
34
return httptest.NewServer(srv.Handler()), admins
35
}
36
37
func TestLoginNoAdmins(t *testing.T) {
38
// When admins is nil, login returns 404.
39
reg := registry.New(newMock(), []byte("test-signing-key"))
40
srv := api.New(reg, auth.TestStore(testToken), nil, nil, nil, nil, nil, nil, "", testLog)
41
ts := httptest.NewServer(srv.Handler())
42
defer ts.Close()
43
44
resp := do(t, ts, "POST", "/login", map[string]any{"username": "admin", "password": "pw"}, nil)
45
defer resp.Body.Close()
46
if resp.StatusCode != http.StatusNotFound {
47
t.Errorf("expected 404 when no admins configured, got %d", resp.StatusCode)
48
}
49
}
50
51
func TestLoginValidCredentials(t *testing.T) {
52
ts, _ := newTestServerWithAdmins(t)
53
defer ts.Close()
54
55
resp := do(t, ts, "POST", "/login", map[string]any{"username": "admin", "password": "hunter2"}, nil)
56
defer resp.Body.Close()
57
if resp.StatusCode != http.StatusOK {
58
t.Fatalf("expected 200, got %d", resp.StatusCode)
59
}
60
61
var body map[string]string
62
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
63
t.Fatalf("decode: %v", err)
64
}
65
if body["token"] == "" {
66
t.Error("expected token in response")
67
}
68
if body["username"] != "admin" {
69
t.Errorf("expected username=admin, got %q", body["username"])
70
}
71
}
72
73
func TestLoginWrongPassword(t *testing.T) {
74
ts, _ := newTestServerWithAdmins(t)
75
defer ts.Close()
76
77
resp := do(t, ts, "POST", "/login", map[string]any{"username": "admin", "password": "wrong"}, nil)
78
defer resp.Body.Close()
79
if resp.StatusCode != http.StatusUnauthorized {
80
t.Errorf("expected 401, got %d", resp.StatusCode)
81
}
82
}
83
84
func TestLoginUnknownUser(t *testing.T) {
85
ts, _ := newTestServerWithAdmins(t)
86
defer ts.Close()
87
88
resp := do(t, ts, "POST", "/login", map[string]any{"username": "nobody", "password": "hunter2"}, nil)
89
defer resp.Body.Close()
90
if resp.StatusCode != http.StatusUnauthorized {
91
t.Errorf("expected 401, got %d", resp.StatusCode)
92
}
93
}
94
95
func TestLoginBadBody(t *testing.T) {
96
ts, _ := newTestServerWithAdmins(t)
97
defer ts.Close()
98
99
resp := do(t, ts, "POST", "/login", "not-json", nil)
100
defer resp.Body.Close()
101
if resp.StatusCode != http.StatusBadRequest {
102
t.Errorf("expected 400, got %d", resp.StatusCode)
103
}
104
}
105
106
func TestLoginRateLimit(t *testing.T) {
107
ts, _ := newTestServerWithAdmins(t)
108
defer ts.Close()
109
110
// 11 attempts from the same IP — the 11th should be rate-limited.
111
var lastStatus int
112
for i := 0; i < 11; i++ {
113
resp := do(t, ts, "POST", "/login", map[string]any{"username": "admin", "password": "wrong"}, nil)
114
lastStatus = resp.StatusCode
115
resp.Body.Close()
116
}
117
if lastStatus != http.StatusTooManyRequests {
118
t.Errorf("expected 429 on 11th attempt, got %d", lastStatus)
119
}
120
}
121
122
// --- admin management endpoints ---
123
124
func TestAdminList(t *testing.T) {
125
ts, _ := newTestServerWithAdmins(t)
126
defer ts.Close()
127
128
resp := do(t, ts, "GET", "/v1/admins", nil, authHeader())
129
defer resp.Body.Close()
130
if resp.StatusCode != http.StatusOK {
131
t.Fatalf("expected 200, got %d", resp.StatusCode)
132
}
133
134
var body map[string]any
135
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
136
t.Fatalf("decode: %v", err)
137
}
138
admins := body["admins"].([]any)
139
if len(admins) != 1 {
140
t.Errorf("expected 1 admin, got %d", len(admins))
141
}
142
}
143
144
func TestAdminAdd(t *testing.T) {
145
ts, _ := newTestServerWithAdmins(t)
146
defer ts.Close()
147
148
resp := do(t, ts, "POST", "/v1/admins", map[string]any{"username": "bob", "password": "passw0rd"}, authHeader())
149
defer resp.Body.Close()
150
if resp.StatusCode != http.StatusCreated {
151
t.Errorf("expected 201, got %d", resp.StatusCode)
152
}
153
154
// List should now have 2.
155
resp2 := do(t, ts, "GET", "/v1/admins", nil, authHeader())
156
defer resp2.Body.Close()
157
var body map[string]any
158
json.NewDecoder(resp2.Body).Decode(&body)
159
admins := body["admins"].([]any)
160
if len(admins) != 2 {
161
t.Errorf("expected 2 admins after add, got %d", len(admins))
162
}
163
}
164
165
func TestAdminAddDuplicate(t *testing.T) {
166
ts, _ := newTestServerWithAdmins(t)
167
defer ts.Close()
168
169
resp := do(t, ts, "POST", "/v1/admins", map[string]any{"username": "admin", "password": "pw"}, authHeader())
170
defer resp.Body.Close()
171
if resp.StatusCode != http.StatusConflict {
172
t.Errorf("expected 409 on duplicate, got %d", resp.StatusCode)
173
}
174
}
175
176
func TestAdminAddMissingFields(t *testing.T) {
177
ts, _ := newTestServerWithAdmins(t)
178
defer ts.Close()
179
180
resp := do(t, ts, "POST", "/v1/admins", map[string]any{"username": "bob"}, authHeader())
181
defer resp.Body.Close()
182
if resp.StatusCode != http.StatusBadRequest {
183
t.Errorf("expected 400 when password missing, got %d", resp.StatusCode)
184
}
185
}
186
187
func TestAdminRemove(t *testing.T) {
188
ts, admins := newTestServerWithAdmins(t)
189
defer ts.Close()
190
191
// Add a second admin first so we're not removing the only one.
192
_ = admins.Add("bob", "pw")
193
194
resp := do(t, ts, "DELETE", "/v1/admins/admin", nil, authHeader())
195
defer resp.Body.Close()
196
if resp.StatusCode != http.StatusNoContent {
197
t.Errorf("expected 204, got %d", resp.StatusCode)
198
}
199
200
resp2 := do(t, ts, "GET", "/v1/admins", nil, authHeader())
201
defer resp2.Body.Close()
202
var body map[string]any
203
json.NewDecoder(resp2.Body).Decode(&body)
204
remaining := body["admins"].([]any)
205
if len(remaining) != 1 {
206
t.Errorf("expected 1 admin remaining, got %d", len(remaining))
207
}
208
}
209
210
func TestAdminRemoveUnknown(t *testing.T) {
211
ts, _ := newTestServerWithAdmins(t)
212
defer ts.Close()
213
214
resp := do(t, ts, "DELETE", "/v1/admins/nobody", nil, authHeader())
215
defer resp.Body.Close()
216
if resp.StatusCode != http.StatusNotFound {
217
t.Errorf("expected 404, got %d", resp.StatusCode)
218
}
219
}
220
221
func TestAdminSetPassword(t *testing.T) {
222
ts, admins := newTestServerWithAdmins(t)
223
defer ts.Close()
224
225
resp := do(t, ts, "PUT", "/v1/admins/admin/password", map[string]any{"password": "newpass"}, authHeader())
226
defer resp.Body.Close()
227
if resp.StatusCode != http.StatusNoContent {
228
t.Errorf("expected 204, got %d", resp.StatusCode)
229
}
230
231
if !admins.Authenticate("admin", "newpass") {
232
t.Error("new password should authenticate")
233
}
234
}
235
236
func TestAdminSetPasswordMissing(t *testing.T) {
237
ts, _ := newTestServerWithAdmins(t)
238
defer ts.Close()
239
240
resp := do(t, ts, "PUT", "/v1/admins/admin/password", map[string]any{"password": ""}, authHeader())
241
defer resp.Body.Close()
242
if resp.StatusCode != http.StatusBadRequest {
243
t.Errorf("expected 400, got %d", resp.StatusCode)
244
}
245
}
246
247
func TestAdminEndpointsRequireAuth(t *testing.T) {
248
ts, _ := newTestServerWithAdmins(t)
249
defer ts.Close()
250
251
endpoints := []struct{ method, path string }{
252
{"GET", "/v1/admins"},
253
{"POST", "/v1/admins"},
254
{"DELETE", "/v1/admins/admin"},
255
{"PUT", "/v1/admins/admin/password"},
256
}
257
for _, e := range endpoints {
258
t.Run(e.method+" "+e.path, func(t *testing.T) {
259
resp := do(t, ts, e.method, e.path, nil, nil)
260
defer resp.Body.Close()
261
if resp.StatusCode != http.StatusUnauthorized {
262
t.Errorf("expected 401, got %d", resp.StatusCode)
263
}
264
})
265
}
266
}
267

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button