Skip to content

Review Moderation State Machine: State Transition Validation Through Integration Testing

Adityo Guni Waluyo

Integration tests prove the review moderation machine: a wrong 9.9/999 seed, Remove filling the audit trail, Restore clearing it, aggregates recomputed both ways.

TL;DR

Integration tests inject absurd aggregates and verify Remove triggers real recalculation through production logic, not test-only updates. The moderation flow atomically flips approved to removed, fills four audit columns, enforces whitelisted reasons, and blocks double removal with ErrNotFound, while restore reverses everything. Table-driven tests covering boundaries, duplication, and pagination turn state transitions into a guaranteed contract.

Review Moderation State Machine: State Transition Validation Through Integration Testing

When running integration tests, the system deliberately injects an extremely incorrect data aggregate, such as avg_rating 9.9 and review_count 999. When the Remove function is called once, this aggregate value is immediately overwritten by the production recalculation path. This proves that aggregate recalculation runs genuinely through production logic, not merely as a manual update inserted specifically for testing [1].

The initial guess assumed that deleting a review only changed the status column to removed and left the aggregate intact until a background process updated it. This assumption also presumed that audit columns were filled optionally or manually by developers during debugging.

In reality, the Remove(id, reason, note, actor) flow atomically moves the status from approved to removed. This process mandatorily fills four audit columns: removal_reason, removal_note, removed_by, and removed_at. The removal reason is strictly limited to six whitelist values (spam_promosi, judi_online, sara_kebencian, hoaks, data_pribadi, lainnya) with surrounding spaces automatically trimmed. The optional note is limited to a maximum of 1000 characters (1001 characters are rejected) and is also trimmed.

A second Remove call on an already-removed row returns ErrNotFound. This occurs because the UPDATE WHERE clause explicitly excludes the already-removed status, resulting in RowsAffected being zero. Conversely, the restore operation flips the status back to approved, while simultaneously clearing all four audit columns back to NULL. The aggregate is also recalculated in both directions: after removal, the average becomes 2 with a count of 1, and after restoration, the average becomes 3 with a count of 2. Reviews with pending or rejected status can also be removed through the same mechanism.

The review creation contract is designed with precise boundaries. Rating boundaries of 1 and 5 are accepted, whereas 0 and 6 trigger ErrInvalidRating without affecting data rows. Comment length is counted in runes, so 2000 multibyte characters are accepted, but 2001 are rejected. Empty comments, IP addresses, or user agents are stored as SQL NULL. Duplication based on user_id and entity_id is blocked at both the service and repository levels — where MySQL error 1062 is translated into ErrAlreadyReviewed.

Characterization Markers in State Transitions

Characterization testing records specific system behavior without moralizing [2]. A removed review still locks that user from reviewing the same entity, with an 'owner decision pending' status marker. The POST /reviews endpoint returns a 201 code with a body status of 'pending', while the stored row is 'approved' for instant publish. On the admin interface, a non-numeric ID yields a 404 response because ParseInt falls to 0, whereas a bad entity ID on the public API yields a 400 response.

Statistics Recalculation and Router Resilience

Statistics calculation rounds the average to 1 decimal using math.Round(x*10)/10 with 4.25->4.3, 4.24->4.2, 2.95->3.0. Pagination applies value clamping: page < 1 becomes 1, limit 0 becomes 10, and 51 becomes 10. Handler tests drive the real chi router via httptest with NewRequestWithContext + ResponseRecorder [3]. When the database is closed, every admin endpoint consistently returns a 500 code, ensuring no silent failures.

State Transitions as a Contract

Table-driven tests verify each state transition in isolation [4]. By validating every boundary condition and side effect, the review moderation state machine no longer relies on assumptions. Per-transition testing turns the audit trail from mere design intention into a guaranteed software contract [5].

Sources

Related articles