Files
Felis/internal/api/handlers_node_control_test.go
T

123 lines
4.3 KiB
Go

package api
import (
"context"
"errors"
"testing"
"felis.lolicon.best/internal/nodecontrol"
)
type fakeNodeControl struct {
tasks []nodecontrol.Task
calls int
err error
request nodecontrol.Request
actor string
}
func (f *fakeNodeControl) List(context.Context) ([]nodecontrol.Task, error) { return f.tasks, f.err }
func (f *fakeNodeControl) Get(context.Context, string) (nodecontrol.Task, error) {
if f.err != nil {
return nodecontrol.Task{}, f.err
}
if len(f.tasks) == 0 {
return nodecontrol.Task{}, nodecontrol.ErrNotFound
}
return f.tasks[0], nil
}
func (f *fakeNodeControl) Start(_ context.Context, r nodecontrol.Request, actor string) (nodecontrol.Task, error) {
f.calls++
f.request = r
f.actor = actor
return nodecontrol.Task{ID: "task", Request: r, State: "running"}, f.err
}
const nodeTaskPath = "/api/v1/settings/node-control/tasks"
const nodeTaskBody = `{"action":"approve","name":"worker-01","sshTarget":"worker-01","confirmMaintenance":true}`
func TestNodeControlOwnerBoundaryAndRetries(t *testing.T) {
for _, role := range []string{"user", "admin", "owner"} {
a := newTestAPI(newFakeRepo(), newFakeCluster())
a.External = staticExternal{p: &Principal{UserID: "actor", Role: role, ViaAdminAccess: role != "user"}}
control := &fakeNodeControl{tasks: []nodecontrol.Task{{ID: "task", State: "failed", Request: nodecontrol.Request{Action: "approve", Name: "worker-01", SSHTarget: "worker-01", ConfirmMaintenance: true}}}}
a.NodeControl = control
for _, tc := range []struct {
method, path, body string
want int
}{{"GET", "/api/v1/settings/node-control", "", 200}, {"GET", nodeTaskPath + "/task", "", 200}, {"POST", nodeTaskPath, nodeTaskBody, 202}, {"POST", nodeTaskPath + "/task/retry", "", 202}} {
w := do(a.ExternalHandler(), tc.method, tc.path, tc.body, jsonHeader)
want := tc.want
if role != "owner" {
want = 403
}
if w.Code != want {
t.Fatalf("%s %s: %d %s", role, tc.path, w.Code, w.Body.String())
}
}
if role != "owner" && control.calls != 0 {
t.Fatal("nonowner reached root operations")
}
if role == "owner" && (control.actor != "actor" || control.request.Name != "worker-01") {
t.Fatal("actor/scope not preserved")
}
}
}
func TestNodeControlErrorsAndStartGuard(t *testing.T) {
a := newTestAPI(newFakeRepo(), newFakeCluster())
a.External = staticExternal{p: &Principal{UserID: "owner", Role: "owner", ViaAdminAccess: true}}
control := &fakeNodeControl{}
a.NodeControl = control
w := do(a.ExternalHandler(), "POST", nodeTaskPath, `{"action":"approve","name":"x","sshTarget":"-exec","confirmMaintenance":true}`, jsonHeader)
if w.Code != 400 || control.calls != 0 {
t.Fatal(w.Code, w.Body.String())
}
for _, tc := range []struct {
err error
code int
}{{nodecontrol.ErrBusy, 409}, {errors.New("private host detail"), 503}} {
control.err = tc.err
w := do(a.ExternalHandler(), "POST", nodeTaskPath, nodeTaskBody, jsonHeader)
if w.Code != tc.code {
t.Fatal(w.Code, w.Body.String())
}
}
guard := NodeMaintenanceGuard(control)
if guard(context.Background()) == nil {
t.Fatal("host failure permitted start")
}
control.err = nil
control.tasks = []nodecontrol.Task{{State: "running"}}
if guard(context.Background()) == nil {
t.Fatal("maintenance permitted start")
}
control.tasks[0].State = "failed"
if err := guard(context.Background()); err != nil {
t.Fatal(err)
}
control.tasks[0].State = "succeeded"
w = do(a.ExternalHandler(), "POST", nodeTaskPath+"/task/retry", "", jsonHeader)
if w.Code != 409 {
t.Fatal(w.Code, w.Body.String())
}
}
func TestNodeControlRequiresFreshOwnerProof(t *testing.T) {
repo := newFakeRepo()
a := newTestAPI(repo, newFakeCluster())
p := &Principal{UserID: "owner", Role: "owner", ViaAdminAccess: true, ViaSession: true}
repo.passkeyCreds["owner-key"] = PasskeyCredential{ID: "owner-key", UserID: "owner", UserVerified: true}
a.External = staticExternal{p: p}
control := &fakeNodeControl{}
a.NodeControl = control
w := do(a.ExternalHandler(), "POST", nodeTaskPath, nodeTaskBody, jsonHeader)
if w.Code != 403 || decodeErr(t, w) != "reauth_required" || control.calls != 0 {
t.Fatal(w.Code, w.Body.String())
}
p.ReauthAt = a.now()
w = do(a.ExternalHandler(), "POST", nodeTaskPath, nodeTaskBody, jsonHeader)
if w.Code != 202 || control.calls != 1 {
t.Fatal(w.Code, w.Body.String())
}
}