// run_group_bandwidth_test.go — covers the per-job bandwidth override // that operators can set via the Run-now form's "Limit bandwidth for // this run" disclosure (P2R-13b). package http import ( "context" "encoding/json" stdhttp "net/http" "net/url" "strings" "testing" "time" "github.com/coder/websocket" "github.com/oklog/ulid/v2" "gitea.dcglab.co.uk/steve/restic-manager/internal/api" "gitea.dcglab.co.uk/steve/restic-manager/internal/store" ) // TestRunSourceGroupBandwidthOverride: connect a fake agent, POST the // per-group Run-now endpoint with bandwidth_up_kbps=512, assert the // dispatched command.run carries it. func TestRunSourceGroupBandwidthOverride(t *testing.T) { t.Parallel() srv, ts, st := rawTestServer(t) hostID, token := enrolHostForWS(t, srv, st, "bw-host") // Pre-seed an init job so auto-init doesn't fire on hello and // pollute our envelope sequence. if err := st.CreateJob(context.Background(), store.Job{ ID: ulid.Make().String(), HostID: hostID, Kind: "init", ActorKind: "system", CreatedAt: time.Now().UTC(), }); err != nil { t.Fatalf("seed init: %v", err) } gid := ulid.Make().String() if err := st.CreateSourceGroup(context.Background(), &store.SourceGroup{ ID: gid, HostID: hostID, Name: "etc", Includes: []string{"/etc"}, }); err != nil { t.Fatalf("group: %v", err) } c := agentDial(t, srv, ts, hostID, token) sendHello(t, c, "bw-host") // Drain on-hello burst before issuing the run-now. _ = drainUntil(t, c, api.MsgScheduleSet) cookie := loginAsAdmin(t, st) form := url.Values{ "bandwidth_up_kbps": {"512"}, "bandwidth_down_kbps": {"256"}, } req, _ := stdhttp.NewRequest("POST", ts.URL+"/hosts/"+hostID+"/source-groups/"+gid+"/run", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "application/json") req.AddCookie(cookie) res, err := stdhttp.DefaultClient.Do(req) if err != nil { t.Fatalf("do: %v", err) } res.Body.Close() if res.StatusCode != stdhttp.StatusAccepted { t.Fatalf("status: got %d, want 202", res.StatusCode) } // Read the dispatched command.run; assert overrides are present. deadline := time.Now().Add(3 * time.Second) for time.Now().Before(deadline) { ctx, cancel := context.WithTimeout(context.Background(), 800*time.Millisecond) mt, raw, rerr := c.Read(ctx) cancel() if rerr != nil { break } if mt != websocket.MessageText { continue } var env api.Envelope _ = json.Unmarshal(raw, &env) if env.Type != api.MsgCommandRun { continue } var p api.CommandRunPayload if err := env.UnmarshalPayload(&p); err != nil { t.Fatalf("unmarshal: %v", err) } if p.Kind != api.JobBackup { continue } if p.BandwidthUpKBps == nil || *p.BandwidthUpKBps != 512 { t.Fatalf("BandwidthUpKBps: got %v, want 512", p.BandwidthUpKBps) } if p.BandwidthDownKBps == nil || *p.BandwidthDownKBps != 256 { t.Fatalf("BandwidthDownKBps: got %v, want 256", p.BandwidthDownKBps) } return } t.Fatal("timed out waiting for command.run with bandwidth override") } // TestRunSourceGroupBandwidthRejectsNegative: invalid value → 400. func TestRunSourceGroupBandwidthRejectsNegative(t *testing.T) { t.Parallel() _, url2, st := newTestServerWithHub(t) cookie := loginAsAdmin(t, st) hostID := makeHost(t, st, "bw-rej-host") gid := ulid.Make().String() if err := st.CreateSourceGroup(context.Background(), &store.SourceGroup{ ID: gid, HostID: hostID, Name: "etc", Includes: []string{"/etc"}, }); err != nil { t.Fatalf("group: %v", err) } form := url.Values{"bandwidth_up_kbps": {"-1"}} req, _ := stdhttp.NewRequest("POST", url2+"/hosts/"+hostID+"/source-groups/"+gid+"/run", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "application/json") req.AddCookie(cookie) res, err := stdhttp.DefaultClient.Do(req) if err != nil { t.Fatalf("do: %v", err) } defer res.Body.Close() if res.StatusCode != stdhttp.StatusBadRequest { t.Fatalf("status: got %d, want 400", res.StatusCode) } }