test(approvals): t-paliad-216 SuggestChanges service + handler error mapping
Service-level (real DB, gated on TEST_DATABASE_URL like the rest of the
approval suite):
- HappyPath: OLD row → changes_requested; NEW row pending with
previous_request_id back-pointer; entity reflects counter payload;
two project_events emitted (changes_suggested + requested).
- NoOpRejected: identical counter + empty note → ErrSuggestionRequiresChange.
- NoteOnlyAccepted: identical counter + non-empty note succeeds; entity
keeps the original counter values.
- SelfApprovalBlocked: original requester cannot suggest on their own row.
- RequestNotPending: already-decided row rejects suggest-changes.
- LifecycleInvalid: create-lifecycle pending → ErrSuggestionLifecycleInvalid.
- OriginalRequesterCanApproveCounter: m's Q6 model — after the approver
suggests changes, the ORIGINAL REQUESTER (now no longer the new row's
requested_by) can approve the counter themselves provided their
profession qualifies.
- CounterApproverCannotSelfApprove: 4-Augen still holds — the suggesting
approver cannot approve their own counter (ErrSelfApproval on the new row).
Handler-level (pure-Go, no DB):
- SuggestionRequiresChange400: error code mapping.
- SuggestionLifecycleInvalid400: error code mapping.
This commit is contained in:
@@ -82,6 +82,44 @@ func TestMapApprovalError_MissReturnsFalse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMapApprovalError_SuggestionRequiresChange400 pins t-paliad-216:
|
||||
// a no-op suggest-changes (no counter diff + no note) surfaces as a 400
|
||||
// with code suggestion_requires_change so the frontend can disable the
|
||||
// submit button instead of letting the user click into a dead-end alert.
|
||||
func TestMapApprovalError_SuggestionRequiresChange400(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
if !mapApprovalError(w, services.ErrSuggestionRequiresChange) {
|
||||
t.Fatal("mapApprovalError returned false for ErrSuggestionRequiresChange")
|
||||
}
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400", w.Code)
|
||||
}
|
||||
var body map[string]string
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &body)
|
||||
if body["code"] != "suggestion_requires_change" {
|
||||
t.Errorf("code = %q, want suggestion_requires_change", body["code"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestMapApprovalError_SuggestionLifecycleInvalid400 pins t-paliad-216:
|
||||
// suggest-changes on a create/delete lifecycle is rejected with a clean
|
||||
// 400 + code suggestion_lifecycle_invalid so the frontend can hide the
|
||||
// button for those rows.
|
||||
func TestMapApprovalError_SuggestionLifecycleInvalid400(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
if !mapApprovalError(w, services.ErrSuggestionLifecycleInvalid) {
|
||||
t.Fatal("mapApprovalError returned false for ErrSuggestionLifecycleInvalid")
|
||||
}
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400", w.Code)
|
||||
}
|
||||
var body map[string]string
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &body)
|
||||
if body["code"] != "suggestion_lifecycle_invalid" {
|
||||
t.Errorf("code = %q, want suggestion_lifecycle_invalid", body["code"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseInboxFilter_DropsUnknownStatus pins t-paliad-160 §D regression
|
||||
// hardening: a stray ?status=foo from a stale frontend build (or an
|
||||
// attacker scoping us out of our own list) must NOT shadow rows out of
|
||||
|
||||
@@ -946,3 +946,393 @@ func TestApprovalService_ViewerFlags(t *testing.T) {
|
||||
t.Error("ListSubmittedByUser: viewer_is_requester = false on self-authored row, want true")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SuggestChanges — t-paliad-216 Slice A. The fourth approval action: the
|
||||
// approver authors a counter-proposal which becomes a NEW pending row
|
||||
// requested by the approver. 4-Augen still holds via the standard
|
||||
// self-approval guard.
|
||||
// ============================================================================
|
||||
|
||||
// seedPendingUpdate spins up the {policy, deadline, pending update
|
||||
// request} triple SuggestChanges needs. Returns the deadline id, the
|
||||
// pending request id, and the pre-image due_date (so callers can assert
|
||||
// applyRevert restored it correctly).
|
||||
func (e *approvalTestEnv) seedPendingUpdate(t *testing.T) (uuid.UUID, uuid.UUID, time.Time) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
e.seedPolicy(EntityTypeDeadline, LifecycleUpdate, "associate")
|
||||
|
||||
originalDue := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
|
||||
deadlineID := e.seedDeadline(originalDue)
|
||||
newDue := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
tx, err := e.pool.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("begin: %v", err)
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
`UPDATE paliad.deadlines SET due_date = $1 WHERE id = $2`,
|
||||
newDue, deadlineID); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf("UPDATE pre-submit: %v", err)
|
||||
}
|
||||
preImage := map[string]any{"due_date": "2026-06-01"}
|
||||
payload := map[string]any{"due_date": "2026-06-15"}
|
||||
reqID, err := e.approvals.SubmitUpdate(ctx, tx, e.projectID, deadlineID, e.requester, EntityTypeDeadline, preImage, payload)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf("SubmitUpdate: %v", err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
t.Fatalf("commit: %v", err)
|
||||
}
|
||||
if reqID == nil {
|
||||
t.Fatal("SubmitUpdate returned nil request id")
|
||||
}
|
||||
return deadlineID, *reqID, originalDue
|
||||
}
|
||||
|
||||
// TestApprovalService_SuggestChanges_HappyPath: approver suggests a
|
||||
// different due_date + note. Expected end state:
|
||||
// - OLD request: status='changes_requested', decision_note set,
|
||||
// counter_payload set, decided_by=approver.
|
||||
// - Entity: approval_status='pending', pending_request_id points at
|
||||
// a NEW pending row, due_date == approver's counter_payload value.
|
||||
// - NEW request: status='pending', requested_by=approver,
|
||||
// payload=counter_payload, previous_request_id=OLD.
|
||||
// - Two project_events emitted: *_approval_changes_suggested and
|
||||
// *_approval_requested.
|
||||
func TestApprovalService_SuggestChanges_HappyPath(t *testing.T) {
|
||||
env := setupApprovalTest(t)
|
||||
defer env.cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
deadlineID, oldReqID, _ := env.seedPendingUpdate(t)
|
||||
|
||||
counterDue := time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC)
|
||||
counter := map[string]any{"due_date": "2026-06-20"}
|
||||
newReqID, err := env.approvals.SuggestChanges(ctx, oldReqID, env.approver, counter, "Bitte später, Raumkonflikt am 15.6.")
|
||||
if err != nil {
|
||||
t.Fatalf("SuggestChanges: %v", err)
|
||||
}
|
||||
if newReqID == nil {
|
||||
t.Fatal("expected new request id, got nil")
|
||||
}
|
||||
if *newReqID == oldReqID {
|
||||
t.Fatal("new request id must differ from old")
|
||||
}
|
||||
|
||||
// OLD row.
|
||||
oldRow := struct {
|
||||
Status string `db:"status"`
|
||||
DecidedBy *uuid.UUID `db:"decided_by"`
|
||||
DecidedAt *time.Time `db:"decided_at"`
|
||||
DecisionNote *string `db:"decision_note"`
|
||||
CounterPayload []byte `db:"counter_payload"`
|
||||
PreviousRequest *uuid.UUID `db:"previous_request_id"`
|
||||
DecisionKind *string `db:"decision_kind"`
|
||||
}{}
|
||||
if err := env.pool.GetContext(ctx, &oldRow,
|
||||
`SELECT status, decided_by, decided_at, decision_note, counter_payload,
|
||||
previous_request_id, decision_kind
|
||||
FROM paliad.approval_requests WHERE id = $1`, oldReqID); err != nil {
|
||||
t.Fatalf("read old row: %v", err)
|
||||
}
|
||||
if oldRow.Status != RequestStatusChangesRequested {
|
||||
t.Errorf("old row status = %q, want %q", oldRow.Status, RequestStatusChangesRequested)
|
||||
}
|
||||
if oldRow.DecidedBy == nil || *oldRow.DecidedBy != env.approver {
|
||||
t.Errorf("old row decided_by = %v, want %v", oldRow.DecidedBy, env.approver)
|
||||
}
|
||||
if oldRow.DecisionNote == nil || *oldRow.DecisionNote == "" {
|
||||
t.Error("old row decision_note should be set")
|
||||
}
|
||||
if len(oldRow.CounterPayload) == 0 {
|
||||
t.Error("old row counter_payload should be set")
|
||||
}
|
||||
if oldRow.PreviousRequest != nil {
|
||||
t.Errorf("old row previous_request_id = %v, want NULL", oldRow.PreviousRequest)
|
||||
}
|
||||
if oldRow.DecisionKind == nil || (*oldRow.DecisionKind != DecisionKindPeer && *oldRow.DecisionKind != DecisionKindAdminOverride) {
|
||||
t.Errorf("old row decision_kind = %v, want peer or admin_override", oldRow.DecisionKind)
|
||||
}
|
||||
|
||||
// NEW row.
|
||||
newRow := struct {
|
||||
Status string `db:"status"`
|
||||
RequestedBy uuid.UUID `db:"requested_by"`
|
||||
Payload []byte `db:"payload"`
|
||||
PreviousRequestID *uuid.UUID `db:"previous_request_id"`
|
||||
LifecycleEvent string `db:"lifecycle_event"`
|
||||
}{}
|
||||
if err := env.pool.GetContext(ctx, &newRow,
|
||||
`SELECT status, requested_by, payload, previous_request_id, lifecycle_event
|
||||
FROM paliad.approval_requests WHERE id = $1`, *newReqID); err != nil {
|
||||
t.Fatalf("read new row: %v", err)
|
||||
}
|
||||
if newRow.Status != RequestStatusPending {
|
||||
t.Errorf("new row status = %q, want pending", newRow.Status)
|
||||
}
|
||||
if newRow.RequestedBy != env.approver {
|
||||
t.Errorf("new row requested_by = %v, want %v (approver)", newRow.RequestedBy, env.approver)
|
||||
}
|
||||
if newRow.PreviousRequestID == nil || *newRow.PreviousRequestID != oldReqID {
|
||||
t.Errorf("new row previous_request_id = %v, want %v", newRow.PreviousRequestID, oldReqID)
|
||||
}
|
||||
if newRow.LifecycleEvent != LifecycleUpdate {
|
||||
t.Errorf("new row lifecycle = %q, want update", newRow.LifecycleEvent)
|
||||
}
|
||||
|
||||
// Entity: pending, due_date == counter.
|
||||
entity := struct {
|
||||
Status string `db:"approval_status"`
|
||||
PendingRequest *uuid.UUID `db:"pending_request_id"`
|
||||
DueDate time.Time `db:"due_date"`
|
||||
}{}
|
||||
if err := env.pool.GetContext(ctx, &entity,
|
||||
`SELECT approval_status, pending_request_id, due_date FROM paliad.deadlines WHERE id = $1`,
|
||||
deadlineID); err != nil {
|
||||
t.Fatalf("read entity: %v", err)
|
||||
}
|
||||
if entity.Status != "pending" {
|
||||
t.Errorf("entity approval_status = %q, want pending", entity.Status)
|
||||
}
|
||||
if entity.PendingRequest == nil || *entity.PendingRequest != *newReqID {
|
||||
t.Errorf("entity pending_request_id = %v, want %v", entity.PendingRequest, *newReqID)
|
||||
}
|
||||
if !entity.DueDate.Equal(counterDue) {
|
||||
t.Errorf("entity due_date = %v, want %v (counter)", entity.DueDate, counterDue)
|
||||
}
|
||||
|
||||
// Two project_events: one *_approval_changes_suggested + one *_approval_requested
|
||||
// for the NEW row.
|
||||
var nSuggested, nRequested int
|
||||
if err := env.pool.GetContext(ctx, &nSuggested,
|
||||
`SELECT COUNT(*) FROM paliad.project_events
|
||||
WHERE project_id = $1 AND event_type = 'deadline_approval_changes_suggested'`,
|
||||
env.projectID); err != nil {
|
||||
t.Fatalf("count changes_suggested events: %v", err)
|
||||
}
|
||||
if nSuggested != 1 {
|
||||
t.Errorf("expected 1 deadline_approval_changes_suggested event, got %d", nSuggested)
|
||||
}
|
||||
if err := env.pool.GetContext(ctx, &nRequested,
|
||||
`SELECT COUNT(*) FROM paliad.project_events
|
||||
WHERE project_id = $1 AND event_type = 'deadline_approval_requested'`,
|
||||
env.projectID); err != nil {
|
||||
t.Fatalf("count requested events: %v", err)
|
||||
}
|
||||
// Two requested events expected: one from the original SubmitUpdate +
|
||||
// one from the SuggestChanges spawn.
|
||||
if nRequested != 2 {
|
||||
t.Errorf("expected 2 deadline_approval_requested events (original + spawn), got %d", nRequested)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApprovalService_SuggestChanges_NoOpRejected: identical counter +
|
||||
// empty note returns ErrSuggestionRequiresChange.
|
||||
func TestApprovalService_SuggestChanges_NoOpRejected(t *testing.T) {
|
||||
env := setupApprovalTest(t)
|
||||
defer env.cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
_, oldReqID, _ := env.seedPendingUpdate(t)
|
||||
|
||||
// Same payload as the original SubmitUpdate. No note.
|
||||
identical := map[string]any{"due_date": "2026-06-15"}
|
||||
_, err := env.approvals.SuggestChanges(ctx, oldReqID, env.approver, identical, "")
|
||||
if !errors.Is(err, ErrSuggestionRequiresChange) {
|
||||
t.Errorf("no-op suggest: got %v, want ErrSuggestionRequiresChange", err)
|
||||
}
|
||||
|
||||
// Empty counter, empty note → also rejected.
|
||||
_, err = env.approvals.SuggestChanges(ctx, oldReqID, env.approver, nil, "")
|
||||
if !errors.Is(err, ErrSuggestionRequiresChange) {
|
||||
t.Errorf("empty suggest: got %v, want ErrSuggestionRequiresChange", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApprovalService_SuggestChanges_NoteOnlyAccepted: when the counter
|
||||
// is unchanged but a non-empty note is present, the call succeeds. The
|
||||
// new row's payload equals the OLD payload (the approver said "I want a
|
||||
// fresh look from someone else; here's why", without a different value).
|
||||
func TestApprovalService_SuggestChanges_NoteOnlyAccepted(t *testing.T) {
|
||||
env := setupApprovalTest(t)
|
||||
defer env.cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
deadlineID, oldReqID, _ := env.seedPendingUpdate(t)
|
||||
|
||||
identical := map[string]any{"due_date": "2026-06-15"}
|
||||
newReqID, err := env.approvals.SuggestChanges(ctx, oldReqID, env.approver, identical, "Bitte nochmal prüfen.")
|
||||
if err != nil {
|
||||
t.Fatalf("note-only suggest: %v", err)
|
||||
}
|
||||
if newReqID == nil {
|
||||
t.Fatal("expected new request id, got nil")
|
||||
}
|
||||
|
||||
// Entity's due_date stays at 2026-06-15 (the original counter == original payload).
|
||||
var got time.Time
|
||||
if err := env.pool.GetContext(ctx, &got,
|
||||
`SELECT due_date FROM paliad.deadlines WHERE id = $1`, deadlineID); err != nil {
|
||||
t.Fatalf("read due_date: %v", err)
|
||||
}
|
||||
want := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
if !got.Equal(want) {
|
||||
t.Errorf("entity due_date = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApprovalService_SuggestChanges_SelfApprovalBlocked: the original
|
||||
// requester cannot suggest changes on their own row (would equal
|
||||
// self-approval).
|
||||
func TestApprovalService_SuggestChanges_SelfApprovalBlocked(t *testing.T) {
|
||||
env := setupApprovalTest(t)
|
||||
defer env.cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
_, oldReqID, _ := env.seedPendingUpdate(t)
|
||||
|
||||
counter := map[string]any{"due_date": "2026-06-20"}
|
||||
_, err := env.approvals.SuggestChanges(ctx, oldReqID, env.requester, counter, "")
|
||||
if !errors.Is(err, ErrSelfApproval) {
|
||||
t.Errorf("self suggest: got %v, want ErrSelfApproval", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApprovalService_SuggestChanges_RequestNotPending: a row already
|
||||
// decided (approved/rejected/revoked/changes_requested) rejects further
|
||||
// suggest-changes calls.
|
||||
func TestApprovalService_SuggestChanges_RequestNotPending(t *testing.T) {
|
||||
env := setupApprovalTest(t)
|
||||
defer env.cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
_, oldReqID, _ := env.seedPendingUpdate(t)
|
||||
|
||||
// Approve first.
|
||||
if err := env.approvals.Approve(ctx, oldReqID, env.approver, "ok"); err != nil {
|
||||
t.Fatalf("Approve: %v", err)
|
||||
}
|
||||
|
||||
counter := map[string]any{"due_date": "2026-06-20"}
|
||||
_, err := env.approvals.SuggestChanges(ctx, oldReqID, env.approver, counter, "too late")
|
||||
if !errors.Is(err, ErrRequestNotPending) {
|
||||
t.Errorf("decided row suggest: got %v, want ErrRequestNotPending", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApprovalService_SuggestChanges_LifecycleInvalid: lifecycle ∉
|
||||
// (update, complete) rejects with ErrSuggestionLifecycleInvalid. A
|
||||
// create-lifecycle pending request is the easiest to set up.
|
||||
func TestApprovalService_SuggestChanges_LifecycleInvalid(t *testing.T) {
|
||||
env := setupApprovalTest(t)
|
||||
defer env.cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
env.seedPolicy(EntityTypeDeadline, LifecycleCreate, "associate")
|
||||
deadlineID := env.seedDeadline(time.Now().AddDate(0, 0, 14))
|
||||
|
||||
tx, err := env.pool.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("begin: %v", err)
|
||||
}
|
||||
reqID, err := env.approvals.SubmitCreate(ctx, tx, env.projectID, deadlineID, env.requester, EntityTypeDeadline, map[string]any{"due_date": "2026-05-20"})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf("SubmitCreate: %v", err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
t.Fatalf("commit: %v", err)
|
||||
}
|
||||
|
||||
counter := map[string]any{"due_date": "2026-06-01"}
|
||||
_, err = env.approvals.SuggestChanges(ctx, *reqID, env.approver, counter, "different date")
|
||||
if !errors.Is(err, ErrSuggestionLifecycleInvalid) {
|
||||
t.Errorf("create-lifecycle suggest: got %v, want ErrSuggestionLifecycleInvalid", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApprovalService_SuggestChanges_OriginalRequesterCanApproveCounter:
|
||||
// the cleanest verification of m's Q6 mental model — after the approver
|
||||
// suggests changes, the ORIGINAL REQUESTER is no longer the new row's
|
||||
// requested_by and can now approve the counter themselves (provided
|
||||
// their profession is sufficient). For this test we promote the requester
|
||||
// to 'partner' profession so they pass the canApprove gate.
|
||||
func TestApprovalService_SuggestChanges_OriginalRequesterCanApproveCounter(t *testing.T) {
|
||||
env := setupApprovalTest(t)
|
||||
defer env.cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
// Promote the requester so they qualify as an approver of the counter.
|
||||
// The original Submit was theirs (excluded as requested_by); for the
|
||||
// counter their role lets them sign off.
|
||||
if _, err := env.pool.ExecContext(ctx,
|
||||
`UPDATE paliad.users SET profession='partner' WHERE id = $1`, env.requester); err != nil {
|
||||
t.Fatalf("promote requester profession: %v", err)
|
||||
}
|
||||
if _, err := env.pool.ExecContext(ctx,
|
||||
`UPDATE paliad.users SET profession='partner' WHERE id = $1`, env.approver); err != nil {
|
||||
t.Fatalf("promote approver profession: %v", err)
|
||||
}
|
||||
|
||||
deadlineID, oldReqID, _ := env.seedPendingUpdate(t)
|
||||
|
||||
counter := map[string]any{"due_date": "2026-06-22"}
|
||||
newReqID, err := env.approvals.SuggestChanges(ctx, oldReqID, env.approver, counter, "Lieber den 22.")
|
||||
if err != nil {
|
||||
t.Fatalf("SuggestChanges: %v", err)
|
||||
}
|
||||
|
||||
// Original requester approves the counter.
|
||||
if err := env.approvals.Approve(ctx, *newReqID, env.requester, "Ja, passt."); err != nil {
|
||||
t.Fatalf("original requester approves counter: %v", err)
|
||||
}
|
||||
|
||||
// Entity is back to approved with the counter date.
|
||||
row := struct {
|
||||
Status string `db:"approval_status"`
|
||||
ApprovedBy *uuid.UUID `db:"approved_by"`
|
||||
DueDate time.Time `db:"due_date"`
|
||||
}{}
|
||||
if err := env.pool.GetContext(ctx, &row,
|
||||
`SELECT approval_status, approved_by, due_date FROM paliad.deadlines WHERE id = $1`,
|
||||
deadlineID); err != nil {
|
||||
t.Fatalf("read entity: %v", err)
|
||||
}
|
||||
if row.Status != "approved" {
|
||||
t.Errorf("entity approval_status = %q, want approved", row.Status)
|
||||
}
|
||||
if row.ApprovedBy == nil || *row.ApprovedBy != env.requester {
|
||||
t.Errorf("approved_by = %v, want %v (original requester)", row.ApprovedBy, env.requester)
|
||||
}
|
||||
want := time.Date(2026, 6, 22, 0, 0, 0, 0, time.UTC)
|
||||
if !row.DueDate.Equal(want) {
|
||||
t.Errorf("due_date = %v, want %v", row.DueDate, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApprovalService_SuggestChanges_CounterApproverCannotSelfApprove:
|
||||
// after suggest-changes, the approver who suggested (= new row's
|
||||
// requested_by) is blocked from approving their own counter — 4-Augen
|
||||
// still holds.
|
||||
func TestApprovalService_SuggestChanges_CounterApproverCannotSelfApprove(t *testing.T) {
|
||||
env := setupApprovalTest(t)
|
||||
defer env.cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
_, oldReqID, _ := env.seedPendingUpdate(t)
|
||||
|
||||
counter := map[string]any{"due_date": "2026-06-22"}
|
||||
newReqID, err := env.approvals.SuggestChanges(ctx, oldReqID, env.approver, counter, "")
|
||||
if err != nil {
|
||||
t.Fatalf("SuggestChanges: %v", err)
|
||||
}
|
||||
|
||||
if err := env.approvals.Approve(ctx, *newReqID, env.approver, ""); !errors.Is(err, ErrSelfApproval) {
|
||||
t.Errorf("counter author self-approves: got %v, want ErrSelfApproval", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user