Expand e2e CLAUDE.md with full test patterns
Document client setup, factory pattern, test structure, authorization testing, assertion helpers, and file organization. Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
160
e2e/CLAUDE.md
160
e2e/CLAUDE.md
@@ -14,35 +14,173 @@ make test-e2e
|
||||
|
||||
## Test setup
|
||||
|
||||
`testutil.Setup()` starts `bin/probod` as a subprocess (once per test run) and waits for the GraphQL endpoint to be healthy.
|
||||
`testutil.Setup()` starts `bin/probod` as a subprocess (once per test run via `sync.Once`) and waits for the GraphQL endpoint to be healthy. No explicit teardown is needed — each test gets its own organization/user, so tests never interfere with each other.
|
||||
|
||||
## Client
|
||||
|
||||
```go
|
||||
c := testutil.NewClient(t, testutil.RoleOwner)
|
||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||
admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner)
|
||||
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
|
||||
```
|
||||
|
||||
The client carries organization/tenant context and provides:
|
||||
- `c.Execute(query, variables, &result)` — GraphQL queries
|
||||
- `c.ExecuteConnect(query, variables, &result)` — Connect API queries
|
||||
`NewClient` creates a standalone user with their own organization. `NewClientInOrg` adds a user to an existing owner's organization with a downgraded role.
|
||||
|
||||
The client provides:
|
||||
- `c.Execute(query, variables, &result)` — Console API (authenticated)
|
||||
- `c.ExecuteConnect(query, variables, &result)` — Connect API (sign-up, sign-in)
|
||||
- `c.ExecuteShouldFail(query, variables, &result)` — expects an error
|
||||
- `c.GetOrganizationID()` — current org
|
||||
|
||||
## Factory pattern
|
||||
|
||||
Test data created via `factory.Create*` functions:
|
||||
Test data created via `factory.Create*` or the builder pattern:
|
||||
|
||||
```go
|
||||
// Simple — returns ID string
|
||||
vendorID := factory.CreateVendor(c, factory.Attrs{"name": "Acme"})
|
||||
userID := factory.CreateUser(c)
|
||||
|
||||
// Builder — chainable for optional fields
|
||||
vendorID := factory.NewVendor(owner).
|
||||
WithName("Test").
|
||||
WithDescription("Desc").
|
||||
Create()
|
||||
```
|
||||
|
||||
- `factory.SafeName(prefix)` — random unique names
|
||||
- `factory.SafeEmail()` — random unique emails
|
||||
- `factory.Attrs` map for overriding defaults
|
||||
|
||||
## Test structure
|
||||
## Writing a test
|
||||
|
||||
- Always `t.Parallel()` at the test function level
|
||||
Every test follows this structure:
|
||||
|
||||
```go
|
||||
func TestVendor_Create(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||
|
||||
t.Run(
|
||||
"create a vendor",
|
||||
func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const query = `
|
||||
mutation CreateVendor($input: CreateVendorInput!) {
|
||||
createVendor(input: $input) {
|
||||
vendorEdge {
|
||||
node {
|
||||
id
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var result struct {
|
||||
CreateVendor struct {
|
||||
VendorEdge struct {
|
||||
Node struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
} `json:"node"`
|
||||
} `json:"vendorEdge"`
|
||||
} `json:"createVendor"`
|
||||
}
|
||||
|
||||
name := factory.SafeName("vendor")
|
||||
|
||||
err := owner.Execute(
|
||||
query,
|
||||
map[string]any{
|
||||
"input": map[string]any{
|
||||
"organizationId": owner.GetOrganizationID(),
|
||||
"name": name,
|
||||
"description": "A test vendor",
|
||||
},
|
||||
},
|
||||
&result,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, result.CreateVendor.VendorEdge.Node.ID)
|
||||
assert.Equal(t, name, result.CreateVendor.VendorEdge.Node.Name)
|
||||
},
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Key rules:
|
||||
- Always `t.Parallel()` at both test function and subtest level
|
||||
- Inline GraphQL queries as string constants
|
||||
- Typed result structs per query
|
||||
- `require.NoError` for mutation/query errors, `assert.Equal` for value checks
|
||||
- Typed result structs with `json` tags per query
|
||||
- Variables as `map[string]any`
|
||||
- `require.NoError` for GraphQL call errors, `assert.Equal` for value checks
|
||||
|
||||
## Authorization testing
|
||||
|
||||
Test that roles are properly enforced and tenants are isolated:
|
||||
|
||||
```go
|
||||
t.Run(
|
||||
"viewer cannot create vendor",
|
||||
func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
|
||||
err := viewer.Execute(query, variables, &result)
|
||||
testutil.RequireForbiddenError(t, err)
|
||||
},
|
||||
)
|
||||
|
||||
t.Run(
|
||||
"other org cannot access vendor",
|
||||
func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
otherOwner := testutil.NewClient(t, testutil.RoleOwner)
|
||||
err := otherOwner.Execute(query, variables, &result)
|
||||
require.Error(t, err)
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## Assertion helpers
|
||||
|
||||
| Helper | Purpose |
|
||||
|--------|---------|
|
||||
| `RequireForbiddenError(t, err)` | Verifies FORBIDDEN error code |
|
||||
| `RequireErrorCode(t, err, code)` | Checks specific GraphQL error code |
|
||||
| `AssertTimestampsOnCreate(t, created, updated)` | `createdAt == updatedAt` |
|
||||
| `AssertTimestampsOnUpdate(t, created, updated)` | `createdAt` unchanged, `updatedAt` advances |
|
||||
| `AssertFirstPage(t, pageInfo)` | First page of a paginated result |
|
||||
| `AssertLastPage(t, pageInfo)` | Last page of a paginated result |
|
||||
| `AssertOrderedAscending(t, items)` | Items in ascending order |
|
||||
| `AssertOrderedDescending(t, items)` | Items in descending order |
|
||||
| `AssertNodeNotAccessible(t, client, id)` | Tenant isolation check |
|
||||
|
||||
## File organization
|
||||
|
||||
```
|
||||
e2e/
|
||||
├── console/ # Test files (package console_test)
|
||||
│ ├── vendor_test.go
|
||||
│ ├── framework_test.go
|
||||
│ ├── audit_test.go
|
||||
│ └── ...
|
||||
└── internal/
|
||||
├── factory/
|
||||
│ └── factory.go # Test data builders
|
||||
└── testutil/
|
||||
├── testutil.go # Server setup/teardown
|
||||
├── client.go # Client and auth
|
||||
├── graphql.go # GraphQL request/response
|
||||
├── assert.go # Assertion helpers
|
||||
└── mailpit.go # Email service integration
|
||||
```
|
||||
|
||||
One test file per entity (e.g. `vendor_test.go`). Test function names follow `TestEntity_Operation` (e.g. `TestVendor_Create`, `TestVendor_Update`).
|
||||
|
||||
Reference in New Issue
Block a user