flowtest

package
v0.1.16 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 30, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package flowtest is the multi-step flow-test engine (FLOWTEST-S1) — the last piece of the productivity-confidence layer: persisted, re-runnable scenarios ("login as role X → create → attach → assert") that a deploy can re-run for a PASS/FAIL regression verdict anchored to the schema version it ran against.

It GENERALIZES the molds STATE-AUDIT-V1 §4 mapped — it is not a new idea:

  • the FORMAT is api-cert.postman_collection.json formalized: steps as DATA (method/path/body/headers + assertions) with CAPTURED variables flowing between steps ({{token}} from the login, {{cita_id}} from the create);
  • the RUNNER is the acceptance-test/DevHub pattern: execute in order against the live app, emit each step's PASS/FAIL live (SSE), final verdict;
  • the ASSERTIONS are the e2e/httpexpect vocabulary: status, field exists, field equals/contains.

A flow authenticates as a TENANT USER (a real POST /auth/login step, or a flow-level role that pre-mints a tenant JWT) — never the super-admin — so it exercises the REAL RBAC the app enforces.

Index

Constants

This section is empty.

Variables

View Source
var ErrDuplicateName = errors.New("a flow with that name already exists")

ErrDuplicateName marks a create/rename colliding with an existing flow name.

View Source
var ErrFlowNotFound = errors.New("flow not found")

ErrFlowNotFound marks a lookup of a flow the tenant does not have.

Functions

func DeleteFlow

func DeleteFlow(ctx context.Context, pool *pgxpool.Pool, tenantID, id string) error

DeleteFlow removes one flow (its runs stay — the regression trail survives).

func EnsureTables

func EnsureTables(ctx context.Context, pool *pgxpool.Pool) error

EnsureTables creates the flow tables idempotently (the outbox/schema_history pattern — existing databases predate the canonical DDL in migrations/001).

func SaveRun

func SaveRun(ctx context.Context, pool *pgxpool.Pool, run *Run) error

SaveRun persists an execution's verdict + full per-step results, anchored to the schema version it ran against.

Types

type Assert

type Assert struct {
	Path  string `json:"path"`
	Op    string `json:"op"`
	Value string `json:"value,omitempty"`
}

Assert checks one field of the response JSON, addressed by dot-path. Ops (FLOWTEST-POWER-S1 — the full vocabulary, mirrored by the Studio assertion controls):

  • "exists" / "not_exists" — the field is (not) present. not_exists is how a GraphQL step asserts success ("errors" not_exists — GraphQL is always 200).
  • "eq" / "ne" — the stringified value equals / differs from Value ({{var}} substitution applies to Value, so a response field can be compared against a captured variable).
  • "contains" — substring on the stringified value.
  • "gt" / "gte" / "lt" / "lte" — numeric comparison (both sides must parse as numbers; a non-numeric side is a clear failure, never a silent pass).
  • "len" — the field is an array (or string) whose length equals Value.

type Emit

type Emit func(event string, payload any)

Emit receives live events while a flow runs: ("step", StepResult) after each step, so a UI can stream the run (the DevHub SSE pattern). nil is fine.

type Expect

type Expect struct {
	Status  int      `json:"status"`
	Asserts []Assert `json:"asserts,omitempty"`
}

Expect is a step's assertions: the HTTP status plus optional field checks.

type Flow

type Flow struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	// Role, when set, pre-mints a tenant JWT for that RBAC role into {{token}}
	// before step 1 (the convenience path). A flow that wants to exercise the
	// REAL login instead makes step 1 a POST /auth/login and captures
	// {{token}} from the response — a capture overwrites the pre-minted var.
	Role  string `json:"role,omitempty"`
	Steps []Step `json:"steps"`
}

Flow is one named multi-step scenario — the persistable "flow as data".

func (*Flow) Validate

func (f *Flow) Validate() error

Validate rejects a malformed flow with an actionable error (the same fail-at-save philosophy as the schema validator — a bad step never becomes a confusing runtime failure).

type FlowResult

type FlowResult struct {
	Name       string       `json:"name"`
	Pass       bool         `json:"pass"`
	Steps      []StepResult `json:"steps"`
	StepsTotal int          `json:"steps_total"`
	StepsFail  int          `json:"steps_failed"`
	DurationMS int64        `json:"duration_ms"`
}

FlowResult is one flow's verdict with every step's outcome.

type Run

type Run struct {
	ID            int64           `json:"id"`
	TenantID      string          `json:"tenant_id"`
	SchemaVersion int             `json:"schema_version"`
	Scope         string          `json:"scope"` // "suite" or the flow name
	Pass          bool            `json:"pass"`
	FlowsTotal    int             `json:"flows_total"`
	FlowsFailed   int             `json:"flows_failed"`
	StepsTotal    int             `json:"steps_total"`
	StepsFailed   int             `json:"steps_failed"`
	Results       json.RawMessage `json:"results,omitempty"` // []FlowResult
	CreatedAt     time.Time       `json:"created_at"`
}

Run is one persisted execution (a single flow or the whole suite), anchored to the schema version it ran against — the regression trail.

func GetRun

func GetRun(ctx context.Context, pool *pgxpool.Pool, tenantID string, id int64) (*Run, error)

GetRun returns one run WITH its full results.

func ListRuns

func ListRuns(ctx context.Context, pool *pgxpool.Pool, tenantID string, limit int) ([]Run, error)

ListRuns returns the tenant's run history, newest first (without the heavy per-step results; fetch one run for the detail).

type Runner

type Runner struct {
	// Handler is the live data-plane router (App.currentRouter).
	Handler http.Handler
	// JWTSecret mints the flow-level Role token — the SAME tenant-JWT contract
	// the engine validates (never a super-admin credential).
	JWTSecret string
	// HostSuffix builds the request Host (<tenant><HostSuffix>) the tenant
	// middleware resolves. Default ".flows.internal".
	HostSuffix string
}

Runner executes flows against the app's REAL router (the same handler the listener serves — tenant middleware, rate limit, cache, JWT, RBAC, generated routes), in-process: no loopback socket, no parallel path, and after a hot-swap it runs against the CURRENT surface. The acceptance-test pattern, server-side.

func (*Runner) Run

func (r *Runner) Run(ctx context.Context, tenantID string, flow *Flow, emit Emit) *FlowResult

Run executes flow for tenantID. State flows between steps via captured variables; the first failing step stops the flow (later steps depend on its state) and the remaining steps are reported as skipped.

type Step

type Step struct {
	Name    string            `json:"name"`
	Method  string            `json:"method"`
	Path    string            `json:"path"` // e.g. /api/citas or /auth/login
	Body    string            `json:"body,omitempty"`
	Headers map[string]string `json:"headers,omitempty"`
	// Upload, when set, sends a multipart file (POST /api/files) instead of Body.
	Upload *Upload `json:"upload,omitempty"`
	Expect Expect  `json:"expect"`
	// Capture maps {{variable}} names to dot-paths into the response JSON
	// (e.g. "cita_id": "id", "token": "token", "first": "data.0.id").
	Capture map[string]string `json:"capture,omitempty"`
}

Step is one request + its expectations + what it captures for later steps. {{var}} placeholders are substituted in Path, Body and header values.

type StepResult

type StepResult struct {
	Index      int               `json:"index"`
	Name       string            `json:"name"`
	Method     string            `json:"method"`
	Path       string            `json:"path"` // after variable substitution
	Skipped    bool              `json:"skipped,omitempty"`
	Pass       bool              `json:"pass"`
	Status     int               `json:"status"`
	Expected   int               `json:"expected"`
	Failures   []string          `json:"failures,omitempty"`
	BodySample string            `json:"body_sample,omitempty"` // the response body (capped ~2 KB) — every step, pass or fail
	Captured   map[string]string `json:"captured,omitempty"`
	DurationMS int64             `json:"duration_ms"`
}

StepResult is one executed step's outcome — always with the exact detail (expected vs got) so a red step is actionable.

type StoredFlow

type StoredFlow struct {
	ID        string    `json:"id"`
	TenantID  string    `json:"tenant_id"`
	Name      string    `json:"name"`
	Steps     int       `json:"steps"`
	Flow      *Flow     `json:"flow,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

StoredFlow is a persisted flow with its identity.

func GetFlow

func GetFlow(ctx context.Context, pool *pgxpool.Pool, tenantID, id string) (*StoredFlow, error)

GetFlow returns one flow WITH its steps.

func ListFlows

func ListFlows(ctx context.Context, pool *pgxpool.Pool, tenantID string) ([]StoredFlow, error)

ListFlows returns the tenant's flows, name order (the suite runs in this order — deterministic, like the fan-out's tenant enumeration).

func SaveFlow

func SaveFlow(ctx context.Context, pool *pgxpool.Pool, tenantID, id string, f *Flow) (*StoredFlow, error)

SaveFlow inserts (id=="") or updates a flow. The flow must already be Validate()d by the caller.

type Upload

type Upload struct {
	Field    string `json:"field,omitempty"` // form field; default "file"
	Filename string `json:"filename"`
	Content  string `json:"content"` // inline text content (e.g. "%PDF-1.4 ...")
}

Upload describes a multipart file part built from inline text content.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL