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:
Bryan Frimin
2026-03-15 14:27:42 +01:00
parent 9a37283e24
commit 0ca6b6775d

View File

@@ -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`).