feat(server/m7-user-mgmt): U2.5 auto-approve handler wiring

Wires the auto_approve_requests user flag into the request creation
flow. When the flag is set (admin enabled it via /admin/users from
U2-T3), POST /api/requests transitions the just-created pending row
through Service.Approve inline, which dispatches to Lidarr the same
way a manual admin approve does.

Failures inside Approve (ErrLidarrDisabled, ErrDefaultsIncomplete,
network/Lidarr errors) leave the row pending — same fallback as
manual approve hitting the same path. The user gets a 201 either
way; admin can still resolve the row in /admin/requests if it
stayed pending. The handler logs the auto-approve failure with
user_id + request_id for observability.

The "actor" id passed to Approve is the user's own ID — they're
acting under the privilege the admin granted them via the toggle.
The trail of "admin set the flag" already lives in audit_log
(ActionAutoApproveToggle from U2-T2). Adding a per-auto-approval
audit entry is a future enhancement; for v1 the toggle audit plus
the request row's status transition is enough.

Test verifies the Lidarr-disabled fallback contract: a user with
auto_approve=true and Lidarr unavailable still gets 201 Created
with status=pending (no crash, no 500). The "auto-approve actually
succeeds" test path requires a Lidarr stub; deferred until a
broader Lidarr test fixture lands.
This commit is contained in:
2026-05-07 18:46:01 -04:00
parent 081b8e62d5
commit 045ba6975f
2 changed files with 65 additions and 0 deletions
+26
View File
@@ -154,6 +154,32 @@ func (h *handlers) handleCreateRequest(w http.ResponseWriter, r *http.Request) {
return
}
// Auto-approve flow (#355 / #376 U2.5): when the user has the
// auto_approve_requests flag set by an admin, transition the
// just-created pending row through Approve so it dispatches to
// Lidarr immediately. Failures here (Lidarr disabled, defaults
// missing, network error) leave the row pending — same fallback
// as a manual admin approve that hits the same error path. The
// user gets a 201 with whatever status Approve produced; admin
// can still resolve the row in /admin/requests if it stayed
// pending.
//
// The "actor" id passed to Approve is the user themselves — the
// auto-approval is the user acting under the privilege the admin
// granted them via the toggle. The trail of "admin set the flag"
// already lives in audit_log (ActionAutoApproveToggle from U2).
if user.AutoApproveRequests {
approved, aerr := h.lidarrRequests.Approve(r.Context(), row.ID, user.ID, lidarrrequests.ApproveOverrides{})
if aerr == nil {
row = approved
} else {
h.logger.Warn("api: auto-approve failed; request stays pending",
"user_id", uuidToString(user.ID),
"request_id", uuidToString(row.ID),
"err", aerr)
}
}
writeJSON(w, http.StatusCreated, requestViewFrom(row))
}
+39
View File
@@ -85,6 +85,45 @@ func createArtistRequest(t *testing.T, h *handlers, user dbq.User, artistMBID, a
return rv
}
// TestHandleCreateRequest_AutoApprove_LidarrDisabled_StaysPending verifies
// that a user with auto_approve_requests=true still receives a 201 Created
// when Lidarr is disabled in the test environment — the auto-approve attempt
// fails inside Service.Approve with ErrLidarrDisabled, which the handler
// catches and logs, leaving the row in pending state. This is the U2.5
// fallback contract: a misconfigured Lidarr must not break request creation.
func TestHandleCreateRequest_AutoApprove_LidarrDisabled_StaysPending(t *testing.T) {
h, pool := testHandlers(t)
resetLidarrState(t, h)
alice := seedUser(t, pool, "alice", "pw", false)
if _, err := pool.Exec(context.Background(),
"UPDATE users SET auto_approve_requests = true WHERE id = $1", alice.ID); err != nil {
t.Fatalf("set auto_approve: %v", err)
}
// Re-fetch so the User row carried into the request context has the flag.
refreshed, err := dbq.New(pool).GetUserByID(context.Background(), alice.ID)
if err != nil {
t.Fatalf("reload user: %v", err)
}
if !refreshed.AutoApproveRequests {
t.Fatalf("auto_approve flag did not persist — bailing")
}
body := `{"kind":"artist","lidarr_artist_mbid":"auto-mbid-1","artist_name":"Autoband"}`
w := doCreateRequest(h, refreshed, body)
if w.Code != http.StatusCreated {
t.Fatalf("status = %d, want 201; body = %s", w.Code, w.Body.String())
}
var rv requestView
if err := json.Unmarshal(w.Body.Bytes(), &rv); err != nil {
t.Fatalf("decode: %v", err)
}
if rv.Status != "pending" {
t.Errorf("status = %q, want pending (auto-approve falls back when Lidarr disabled)", rv.Status)
}
}
// TestHandleCreateRequest_Artist_HappyPath verifies a valid artist request
// returns 201 with kind=artist and status=pending.
func TestHandleCreateRequest_Artist_HappyPath(t *testing.T) {