Documentation
¶
Overview ¶
Package forgetest provides a fluent testing API for forge HTTP handlers.
It offers request builders, response assertions, HTML parsing, session management, and RBAC testing utilities backed by an in-memory session store that is safe for parallel tests.
Basic Usage ¶
Create a test app with NewApp, build requests with Get/Post/etc., and assert responses with RequireStatus, RequireRedirect, or HTML assertions:
import (
"testing"
"net/http"
"github.com/dmitrymomot/forge"
"github.com/dmitrymomot/forge/forgetest"
)
func TestHandler(t *testing.T) {
t.Parallel()
handler := func(c forge.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
app := forgetest.NewApp(t, handler)
resp := forgetest.Get(t, app, "/").Do()
resp.RequireStatus(t, http.StatusOK)
if resp.Body() != "Hello, World!" {
t.Errorf("unexpected body: %s", resp.Body())
}
}
Sessions and RBAC ¶
Use AsUser to create a session with a user ID, WithRole to set the role, and WithSessionData to store arbitrary session data. WithRoles configures the app with RBAC permissions:
func TestProtectedEndpoint(t *testing.T) {
t.Parallel()
handler := func(c forge.Context) error {
if !c.Can("posts:write") {
return c.NoContent(http.StatusForbidden)
}
return c.String(http.StatusOK, "Post created")
}
app := forgetest.NewApp(t, handler,
forgetest.WithRoles(forge.RolePermissions{
"admin": {"posts:write"},
"viewer": {},
}),
)
// Admin user with permission.
resp := forgetest.Post(t, app, "/posts").
AsUser("user-123").
WithRole("admin").
Do()
resp.RequireStatus(t, http.StatusOK)
// Viewer without permission.
resp = forgetest.Post(t, app, "/posts").
AsUser("user-456").
WithRole("viewer").
Do()
resp.RequireStatus(t, http.StatusForbidden)
}
HTMX Support ¶
Use WithHTMX to mark a request as an HTMX request, and assert HTMX response headers with RequireHTMXTrigger, RequireHTMXRetarget, RequireHTMXReswap, etc.:
import (
"github.com/dmitrymomot/forge/pkg/htmx"
)
func TestHTMXPartial(t *testing.T) {
t.Parallel()
handler := func(c forge.Context) error {
partial := &forgetest.MockComponent{HTML: "<div>Partial content</div>"}
fullPage := &forgetest.MockComponent{HTML: "<html><body>Full page</body></html>"}
if c.IsHTMX() {
return c.Render(http.StatusOK, partial, htmx.WithTrigger("notification"))
}
return c.Render(http.StatusOK, fullPage)
}
app := forgetest.NewApp(t, handler)
// HTMX request gets partial content.
resp := forgetest.Get(t, app, "/partial").WithHTMX().Do()
resp.RequireStatus(t, http.StatusOK)
resp.RequireHTMXTrigger(t, "notification")
resp.HTML().RequireText(t, "div", "Partial content")
}
HTML Assertions ¶
Parse response HTML with resp.HTML() and assert element existence, text content, attributes, and counts using CSS selectors:
func TestHTMLRendering(t *testing.T) {
t.Parallel()
comp := &forgetest.MockComponent{
HTML: `<html>
<head><title>Dashboard</title></head>
<body>
<h1>Dashboard</h1>
<form action="/submit" method="post">
<input type="text" name="username" value="alice" />
<button type="submit">Submit</button>
</form>
<ul class="items">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</body>
</html>`,
}
handler := func(c forge.Context) error {
return c.Render(http.StatusOK, comp)
}
app := forgetest.NewApp(t, handler)
doc := forgetest.Get(t, app, "/").Do().HTML()
doc.RequireText(t, "h1", "Dashboard")
doc.RequireExactText(t, "title", "Dashboard")
doc.RequireAttr(t, "form", "action", "/submit")
doc.RequireValue(t, "input[name=username]", "alice")
doc.RequireCount(t, "ul.items li", 3)
doc.RequireExists(t, "button[type=submit]")
doc.RequireNotExists(t, ".error-message")
}
Form and JSON Requests ¶
Build POST requests with form data using WithForm, or JSON bodies using WithJSON:
func TestFormSubmission(t *testing.T) {
t.Parallel()
handler := func(c forge.Context) error {
username := c.Form("username")
if username == "" {
return c.NoContent(http.StatusBadRequest)
}
return c.Redirect(http.StatusSeeOther, "/dashboard")
}
app := forgetest.NewApp(t, handler)
resp := forgetest.Post(t, app, "/login").
WithForm("username", "alice").
WithForm("password", "secret").
Do()
resp.RequireRedirect(t, http.StatusSeeOther, "/dashboard")
}
func TestJSONAPI(t *testing.T) {
t.Parallel()
handler := func(c forge.Context) error {
var req struct {
Name string `json:"name"`
}
if _, err := c.Bind(&req); err != nil {
return c.NoContent(http.StatusBadRequest)
}
return c.JSON(http.StatusOK, map[string]string{"message": "Hello, " + req.Name})
}
app := forgetest.NewApp(t, handler)
resp := forgetest.Post(t, app, "/api/greet").
WithJSON(map[string]string{"name": "Alice"}).
Do()
resp.RequireStatus(t, http.StatusOK)
resp.RequireHeader(t, "Content-Type", "application/json")
}
Session Store Access ¶
Access the in-memory session store directly for advanced assertions:
import (
"context"
)
func TestSessionManagement(t *testing.T) {
t.Parallel()
handler := func(c forge.Context) error {
return c.String(http.StatusOK, "OK")
}
app := forgetest.NewApp(t, handler)
// Make request as user.
forgetest.Get(t, app, "/").AsUser("user-123").Do()
// Verify session was created.
count, _ := app.Store().CountByUserID(context.Background(), "user-123")
if count != 1 {
t.Errorf("expected 1 session, got %d", count)
}
}
Mock Components ¶
MockComponent renders static HTML or returns errors for testing component rendering:
func TestComponentRendering(t *testing.T) {
t.Parallel()
mock := &forgetest.MockComponent{
HTML: "<div>Mocked content</div>",
}
handler := func(c forge.Context) error {
return c.Render(http.StatusOK, mock)
}
app := forgetest.NewApp(t, handler)
resp := forgetest.Get(t, app, "/").Do()
resp.RequireStatus(t, http.StatusOK)
resp.HTML().RequireText(t, "div", "Mocked content")
}
Index ¶
- type App
- type Document
- func (d *Document) Find(selector string) *goquery.Selection
- func (d *Document) RequireAttr(t testing.TB, selector, attr, val string)
- func (d *Document) RequireCount(t testing.TB, selector string, n int)
- func (d *Document) RequireExactText(t testing.TB, selector, text string)
- func (d *Document) RequireExists(t testing.TB, selector string)
- func (d *Document) RequireNotExists(t testing.TB, selector string)
- func (d *Document) RequireText(t testing.TB, selector, substr string)
- func (d *Document) RequireValue(t testing.TB, selector, val string)
- type MemoryStore
- func (m *MemoryStore) Count() int
- func (m *MemoryStore) CountByUserID(_ context.Context, userID string) (int, error)
- func (m *MemoryStore) Create(_ context.Context, s *forge.Session) error
- func (m *MemoryStore) Delete(_ context.Context, id string) error
- func (m *MemoryStore) DeleteByUserID(_ context.Context, userID string) error
- func (m *MemoryStore) DeleteByUserIDExcept(_ context.Context, userID, exceptID string) error
- func (m *MemoryStore) DeleteOldestByUserID(_ context.Context, userID string) error
- func (m *MemoryStore) GetByID(id string) (*forge.Session, bool)
- func (m *MemoryStore) GetByTokenHash(_ context.Context, tokenHash string) (*forge.Session, error)
- func (m *MemoryStore) ListByUserID(_ context.Context, userID string) ([]*forge.Session, error)
- func (m *MemoryStore) Touch(_ context.Context, id string, lastActiveAt time.Time) error
- func (m *MemoryStore) Update(_ context.Context, s *forge.Session) error
- type MockComponent
- type Option
- type Request
- func (r *Request) AsUser(userID string) *Request
- func (r *Request) Do() *Response
- func (r *Request) WithCookie(name, value string) *Request
- func (r *Request) WithForm(key, value string) *Request
- func (r *Request) WithHTMX() *Request
- func (r *Request) WithHeader(key, value string) *Request
- func (r *Request) WithJSON(v any) *Request
- func (r *Request) WithRole(role string) *Request
- func (r *Request) WithSessionData(key string, value any) *Request
- type Response
- func (r *Response) Body() string
- func (r *Response) HTML() *Document
- func (r *Response) Header(key string) string
- func (r *Response) Recorder() *httptest.ResponseRecorder
- func (r *Response) RequireHTMXPushURL(t testing.TB, url string)
- func (r *Response) RequireHTMXRefresh(t testing.TB)
- func (r *Response) RequireHTMXReplaceURL(t testing.TB, url string)
- func (r *Response) RequireHTMXReswap(t testing.TB, strategy string)
- func (r *Response) RequireHTMXRetarget(t testing.TB, sel string)
- func (r *Response) RequireHTMXTrigger(t testing.TB, event string)
- func (r *Response) RequireHeader(t testing.TB, key, value string)
- func (r *Response) RequireRedirect(t testing.TB, code int, url string)
- func (r *Response) RequireStatus(t testing.TB, code int)
- func (r *Response) StatusCode() int
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type App ¶
type App struct {
// contains filtered or unexported fields
}
App wraps a forge.App with test helpers.
func (*App) Store ¶
func (a *App) Store() *MemoryStore
Store returns the in-memory session store for direct inspection in tests.
type Document ¶
type Document struct {
// contains filtered or unexported fields
}
Document wraps a goquery.Document for HTML assertion helpers.
func (*Document) Find ¶
Find returns the goquery.Selection for the given CSS selector. Use this for custom assertions beyond what the helper methods provide.
func (*Document) RequireAttr ¶
RequireAttr asserts that the first element matching the selector has an attribute with the given value.
func (*Document) RequireCount ¶
RequireCount asserts that exactly n elements match the CSS selector.
func (*Document) RequireExactText ¶
RequireExactText asserts that the first element matching the selector has exactly the given text (trimmed of leading/trailing whitespace).
func (*Document) RequireExists ¶
RequireExists asserts that at least one element matches the CSS selector.
func (*Document) RequireNotExists ¶
RequireNotExists asserts that no elements match the CSS selector.
func (*Document) RequireText ¶
RequireText asserts that the first element matching the selector contains substr in its text.
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is an in-memory session store for testing. It implements forge.SessionStore backed by maps with a read-write mutex for safe concurrent access in parallel tests.
func (*MemoryStore) Count ¶
func (m *MemoryStore) Count() int
Count returns the total number of sessions in the store.
func (*MemoryStore) CountByUserID ¶
CountByUserID returns the number of sessions for a user.
func (*MemoryStore) Delete ¶
func (m *MemoryStore) Delete(_ context.Context, id string) error
Delete removes a session by its ID.
func (*MemoryStore) DeleteByUserID ¶
func (m *MemoryStore) DeleteByUserID(_ context.Context, userID string) error
DeleteByUserID removes all sessions for a user.
func (*MemoryStore) DeleteByUserIDExcept ¶
func (m *MemoryStore) DeleteByUserIDExcept(_ context.Context, userID, exceptID string) error
DeleteByUserIDExcept removes all sessions for a user except the specified session ID.
func (*MemoryStore) DeleteOldestByUserID ¶
func (m *MemoryStore) DeleteOldestByUserID(_ context.Context, userID string) error
DeleteOldestByUserID removes the oldest session for a user (by LastActiveAt).
func (*MemoryStore) GetByID ¶
func (m *MemoryStore) GetByID(id string) (*forge.Session, bool)
GetByID retrieves a session by its ID. This is a test helper not part of the SessionStore interface, useful for inspecting session state in tests.
func (*MemoryStore) GetByTokenHash ¶
GetByTokenHash retrieves a session by its SHA-256 token hash.
func (*MemoryStore) ListByUserID ¶
ListByUserID retrieves all sessions for a user.
type MockComponent ¶
MockComponent implements forge.Component for testing. It renders static HTML or returns a configured error.
type Option ¶
type Option func(*appConfig)
Option configures the test App.
func WithErrorHandler ¶
func WithErrorHandler(h forge.ErrorHandler) Option
WithErrorHandler sets a custom error handler.
func WithMiddleware ¶
func WithMiddleware(mw ...forge.Middleware) Option
WithMiddleware adds middleware to the test app.
func WithOption ¶
WithOption passes through an arbitrary forge.Option for extensibility.
func WithRoles ¶
func WithRoles(perms forge.RolePermissions) Option
WithRoles enables RBAC with the given permissions. Roles are read from session data key "__forgetest_role", set via Request.WithRole().
type Request ¶
type Request struct {
// contains filtered or unexported fields
}
Request builds an HTTP request for testing forge handlers.
func (*Request) AsUser ¶
AsUser creates a session for the given user ID. The session is created in the store and a cookie is attached to the request.
func (*Request) WithCookie ¶
WithCookie adds a cookie to the request.
func (*Request) WithForm ¶
WithForm adds a form key-value pair. Sets Content-Type to application/x-www-form-urlencoded.
func (*Request) WithHeader ¶
WithHeader sets a custom request header.
type Response ¶
type Response struct {
// contains filtered or unexported fields
}
Response wraps an httptest.ResponseRecorder with assertion helpers.
func (*Response) HTML ¶
HTML parses the response body as HTML and returns a Document for assertions.
func (*Response) Recorder ¶
func (r *Response) Recorder() *httptest.ResponseRecorder
Recorder returns the underlying httptest.ResponseRecorder for advanced use.
func (*Response) RequireHTMXPushURL ¶
RequireHTMXPushURL asserts that the HX-Push-Url header matches.
func (*Response) RequireHTMXRefresh ¶
RequireHTMXRefresh asserts that the HX-Refresh header is "true".
func (*Response) RequireHTMXReplaceURL ¶
RequireHTMXReplaceURL asserts that the HX-Replace-Url header matches.
func (*Response) RequireHTMXReswap ¶
RequireHTMXReswap asserts that the HX-Reswap header matches.
func (*Response) RequireHTMXRetarget ¶
RequireHTMXRetarget asserts that the HX-Retarget header matches.
func (*Response) RequireHTMXTrigger ¶
RequireHTMXTrigger asserts that the HX-Trigger header contains the given event.
func (*Response) RequireHeader ¶
RequireHeader asserts that the response has a header with the given value.
func (*Response) RequireRedirect ¶
RequireRedirect asserts that the response has the given status code and a Location header matching the given URL.
func (*Response) RequireStatus ¶
RequireStatus asserts that the response has the given status code.
func (*Response) StatusCode ¶
StatusCode returns the response status code.