Refuse draft_week jobs on the generic jobs endpoint
A kind can now be made service-only: Runner.Enqueue, and so POST /api/v1/jobs, refuses it with 400 service_only_kind, and only its typed Kind enqueues it. draft_week is service-only, so StartDraft and its one-draft-in-flight-per-week rule is the only way to start a draft; a confirmed draft_week posted to /api/v1/jobs can no longer run beside the week's unconfirmed draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,8 @@ type jobCreated struct {
|
||||
}
|
||||
|
||||
// postJob serves POST /api/v1/jobs: it enqueues a job of a registered kind
|
||||
// and returns 202 with its id, which GET /api/v1/jobs/{id} reports on.
|
||||
// that is not service-only and returns 202 with its id, which
|
||||
// GET /api/v1/jobs/{id} reports on.
|
||||
func postJob(logger *slog.Logger, runner *jobs.Runner) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := decodeJobRequest(http.MaxBytesReader(w, r.Body, maxJobBody))
|
||||
@@ -43,6 +44,9 @@ func postJob(logger *slog.Logger, runner *jobs.Runner) http.HandlerFunc {
|
||||
case errors.Is(err, jobs.ErrUnknownKind):
|
||||
writeError(w, r, logger, http.StatusBadRequest, "unknown_kind", "unknown job kind")
|
||||
return
|
||||
case errors.Is(err, jobs.ErrServiceOnly):
|
||||
writeError(w, r, logger, http.StatusBadRequest, "service_only_kind", "job kind is started only by its own endpoint")
|
||||
return
|
||||
case errors.Is(err, jobs.ErrInvalidPayload):
|
||||
writeError(w, r, logger, http.StatusBadRequest, "invalid_payload", "payload does not match the job kind")
|
||||
return
|
||||
|
||||
@@ -204,6 +204,32 @@ func TestPostPlanDraft(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDraftWeekNotEnqueuedThroughJobs checks POST /api/v1/jobs refuses a
|
||||
// draft_week job, so a confirmed draft cannot run beside the week's
|
||||
// unconfirmed one: POST /plans/{weekStart}/draft is the only way to start
|
||||
// a draft.
|
||||
func TestDraftWeekNotEnqueuedThroughJobs(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := newPlansRouter(t)
|
||||
|
||||
rec, _ := callDinners(t, e.h, http.MethodPost, plansPath+"2026-10-12/draft", "{}")
|
||||
if rec.Code != http.StatusAccepted {
|
||||
t.Fatalf("POST draft = %d: %s", rec.Code, rec.Body)
|
||||
}
|
||||
rec, body := callDinners(t, e.h, http.MethodPost, "/api/v1/jobs",
|
||||
`{"kind":"`+plan.KindDraftWeek+`","payload":{"week_start":"2026-10-12","confirm":true}}`)
|
||||
if rec.Code != http.StatusBadRequest || errorCode(body) != "service_only_kind" {
|
||||
t.Errorf("POST job %s = %d %v, want 400 service_only_kind", plan.KindDraftWeek, rec.Code, body)
|
||||
}
|
||||
var queued int
|
||||
if err := e.st.Reader.QueryRowContext(t.Context(), `SELECT COUNT(*) FROM jobs WHERE kind = ?`, plan.KindDraftWeek).Scan(&queued); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if queued != 1 {
|
||||
t.Errorf("%d draft jobs, want only the one POST draft enqueued", queued)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimultaneousPostPlanDraftsShareOneJob(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := newPlansRouter(t)
|
||||
|
||||
Reference in New Issue
Block a user