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
|
## 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
|
## Client
|
||||||
|
|
||||||
```go
|
```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:
|
`NewClient` creates a standalone user with their own organization. `NewClientInOrg` adds a user to an existing owner's organization with a downgraded role.
|
||||||
- `c.Execute(query, variables, &result)` — GraphQL queries
|
|
||||||
- `c.ExecuteConnect(query, variables, &result)` — Connect API queries
|
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
|
- `c.GetOrganizationID()` — current org
|
||||||
|
|
||||||
## Factory pattern
|
## Factory pattern
|
||||||
|
|
||||||
Test data created via `factory.Create*` functions:
|
Test data created via `factory.Create*` or the builder pattern:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
// Simple — returns ID string
|
||||||
vendorID := factory.CreateVendor(c, factory.Attrs{"name": "Acme"})
|
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.SafeName(prefix)` — random unique names
|
||||||
- `factory.SafeEmail()` — random unique emails
|
- `factory.SafeEmail()` — random unique emails
|
||||||
- `factory.Attrs` map for overriding defaults
|
- `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
|
- Inline GraphQL queries as string constants
|
||||||
- Typed result structs per query
|
- Typed result structs with `json` tags per query
|
||||||
- `require.NoError` for mutation/query errors, `assert.Equal` for value checks
|
- 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