name: goravel-crud-test description: Generate and fix comprehensive CRUD tests for a Goravel entity. Covers all CRUD operations, sorting, pagination, search, and permissions. argument-hint: "[EntityName]" allowed-tools: Bash, Read, Write, Edit, Grep, Glob
Goravel CRUD Test Generator
Generate tests for $ARGUMENTS.
Step 1: Generate Test Template
go run . artisan make:crud-test --controller=$ARGUMENTS
This creates tests/feature/crud/<entity>_crud_test.go.
Step 2: Fix Generated Test (CRITICAL)
The generator produces a template with known issues. Fix in this order:
Fix 1: Permission Names
// WRONG (generator creates):
Slug: fmt.Sprintf("entitycontrollers_%s", perm)
// CORRECT (use service name from permission_constants.go):
Slug: fmt.Sprintf("entity_%s", perm)
Check app/auth/permission_constants.go for the exact ServiceRegistry value.
Fix 2: API Endpoints
// WRONG (generator creates):
s.makeRequest("POST", "/api/entitycontrollers", data)
// CORRECT (use hyphenated plural from routes/api.go):
s.makeRequest("POST", "/api/entity-names", data)
Fix 3: Table Names in Cleanup
// WRONG:
orm.Query().Exec("DELETE FROM entitycontrollers")
// CORRECT:
orm.Query().Exec("DELETE FROM entity_names")
Fix 4: Add Valid Test Data (snake_case Keys)
API request payloads MUST use snake_case keys (matching request struct form/json tags):
func (s *TestSuite) TestCreateEntity() {
data := map[string]interface{}{
"first_name": "Test", // snake_case — NOT "firstName"
"last_name": "Entity", // snake_case — NOT "lastName"
"description": "A test",
"status": "ACTIVE",
"birth_date": "1990-01-15", // snake_case — NOT "birthDate"
"tags": []string{"tag1"}, // Include arrays
}
resp, result := s.makeRequest("POST", "/api/entities", data)
s.Equal(http.StatusCreated, resp.StatusCode)
}
API responses return camelCase (from model json tags), so assertions use camelCase:
data := result["data"].(map[string]interface{})
s.Equal("Test", data["firstName"]) // camelCase in response
Fix 5: Initialize ALL Required Fields for ORM-Created Models
entity := &models.Entity{
Title: "Test",
Description: "Test",
Tags: []string{}, // MUST initialize arrays - NOT NULL constraint
CreatedBy: &createdByInt,
}
Common error: NOT NULL constraint failed: table.array_field
Fix: Always initialize JSON array fields to []string{} or []int{}.
Fix 6: carbon.DateTime in Tests
// Create date for test data:
date := *carbon.NewDateTime(carbon.Parse("2025-12-01"))
entity := &models.Entity{
Date: date, // Non-pointer carbon.DateTime
}
Fix 7: Foreign Key Dependencies
If entity has foreign keys, create parents in setup:
func (s *TestSuite) SetupTest() {
s.RefreshDatabase()
s.setupTestUser()
// Create parent entity
parent := &models.Parent{Name: "Test Parent"}
s.Nil(facades.Orm().Query().Create(parent))
s.testParent = parent
}
Step 3: Unauthenticated Request Tests
Fresh HTTP Client (CRITICAL)
The test suite's s.client has a cookie jar that holds the auth token from SetupTest() login. For unauthenticated tests, you MUST use a separate client without the cookie jar:
func (s *TestSuite) makeUnauthenticatedRequest(method, path string, body interface{}) (*http.Response, map[string]interface{}) {
// ... build request ...
// MUST use fresh client — s.client's cookie jar auto-sends auth token
unauthClient := &http.Client{
Timeout: 10 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := unauthClient.Do(req)
// ...
}
JWT Middleware Response Codes
The JWT middleware returns 302 redirect (not 401/403) for unauthenticated non-Inertia API requests. Test assertions should use NotEqual against success codes:
resp, _ := s.makeUnauthenticatedRequest("POST", "/api/entities", data)
s.NotEqual(http.StatusCreated, resp.StatusCode,
"Unauthenticated request should not create an entity")
Step 3b: Timestamp Precision in Sort Tests
TimestampsTz() creates timestamp(0) columns — second precision only. Records created within the same second have identical timestamps, making sort order indeterminate. Don't rely on millisecond sleeps for ordering:
// WRONG — 10ms sleep doesn't help with timestamp(0) precision
s.createTestEntity("First")
time.Sleep(10 * time.Millisecond)
s.createTestEntity("Second")
// CORRECT — use deterministic fields (name, ID) for sort order tests
// or accept any valid result for timestamp sorts
Step 4: Add Read-Only Field Tests (if applicable)
func (s *TestSuite) TestReadOnlyFieldCannotBeSet() {
data := s.getValidData()
data["score"] = 99 // Try to set read-only field
resp, result := s.makeRequest("POST", "/api/entities", data)
s.Equal(http.StatusCreated, resp.StatusCode)
var entity models.Entity
facades.Orm().Query().Find(&entity, result["data"].(map[string]interface{})["id"])
s.Equal(0, entity.Score, "read-only field should NOT be settable")
}
Step 4: Verify Test Compiles
Before running tests, ensure the test file compiles:
go vet ./tests/feature/crud/...
Step 5: Run Tests (TDD Loop)
APP_ENV=testing go test -v ./tests/feature/crud -run Test<Entity>CRUDTestSuite
Fix each failure, re-run, iterate until all pass.
Test Coverage Checklist
- Create with valid data
- Create validation errors (missing required fields)
- Get by ID (found)
- Get by ID (not found - 404)
- Update with valid data
- Delete (soft delete)
- Pagination (default page size, custom page size)
- Sorting (ascending, descending)
- Search by keyword
- Read-only fields cannot be set/updated (if applicable)
- Foreign key constraints (if applicable)
Next Step
After ALL tests pass, proceed to frontend work:
- Run
/inertia-typesto create TypeScript types and i18n translations - OR run
/inertia-scaffoldto generate all frontend components at once
Do NOT start UI work until all CRUD tests pass. Backend bugs (Bind issues, validation key mismatches, GORM mapping errors) are much easier to catch and fix via API tests than through the UI.