service

package
v0.0.0-...-4c6a959 Latest Latest
Warning

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

Go to latest
Published: Oct 17, 2026 License: MIT Imports: 32 Imported by: 0

Documentation

Overview

Package service holds the business logic between handlers and storage.

Index

Constants

View Source
const (
	MaxNamedVariants  = 20
	MaxResponseRules  = 20
	MaxRuleConditions = 8
	MaxMockRoutes     = 50
)
View Source
const (
	StatMocksCreated   = "mocks_created"
	StatRequestsServed = "requests_served"
)

Stat keys used across the codebase. Constants keep us from typo-bumping the wrong counter and silently writing to a new row.

View Source
const MaxOpenAPISpecBytes = 1024 * 1024
View Source
const MaxSequenceSteps = 10

MaxSequenceSteps caps the EXTRA sequence steps per mock (the main response is step 1 on top of these). The create-form UI mirrors this cap.

View Source
const PathSuffixMaxLen = 255

PathSuffixMaxLen is the documented hard limit. 255 fits practical REST paths with plenty of headroom (a real `/api/v2/orgs/.../users/...` is usually < 100 chars).

View Source
const SpamPatternsOff = "off"

SpamPatternsOff is the QUICKMOCK_SPAM_PATTERNS_FILE value that disables the filter entirely.

Variables

View Source
var (
	ErrBodyTooLarge     = errors.New("body too large")
	ErrMockLimitReached = errors.New("mock limit reached")
	ErrNotFound         = errors.New("not found")
	ErrSpamBlocked      = errors.New("content blocked by spam filter")
	ErrPaymentBlocked   = errors.New("payment request blocked")
	ErrTokenRequired    = errors.New("admin token required")
	ErrTokenInvalid     = errors.New("admin token invalid")
	ErrTTLCapReached    = errors.New("ttl cap reached")
)

Errors returned by the service layer. Handlers map these to HTTP status codes + i18n message keys; nothing else should switch on them.

View Source
var MockTokens = []string{
	"{{mock.url}}",
}

MockTokens lists the {{mock.*}} tokens, which describe the mock serving the request rather than the request itself. Like RequestTokens they need live serving context and therefore stay out of SupportedTokens.

View Source
var RequestTokens = []string{
	"{{request.method}}",
	"{{request.path}}",
	"{{request.host}}",
	"{{request.ip}}",
	"{{request.query.id}}",
	"{{request.header.x-request-id}}",
	"{{request.body}}",
	"{{request.body.user.name}}",
}

RequestTokens lists the request.* echo tokens in the illustrative form the UI docs and README show them. query/header/body paths are user-defined, so these are examples, not an exhaustive set. Keep in sync with the UI tooltip and the README.

View Source
var SupportedTokens = []string{
	"{{faker.name}}",
	"{{faker.firstname}}",
	"{{faker.lastname}}",
	"{{faker.username}}",
	"{{faker.email}}",
	"{{faker.phone}}",
	"{{faker.url}}",
	"{{faker.ipv4}}",
	"{{faker.uuid}}",
	"{{faker.int}}",
	"{{faker.bool}}",
	"{{faker.word}}",
	"{{faker.sentence}}",
	"{{faker.color}}",
	"{{faker.company}}",
	"{{faker.city}}",
	"{{faker.price}}",
	"{{faker.lorem}}",
	"{{now.iso8601}}",
	"{{now.unix}}",
	"{{now.unix_ms}}",
	"{{now.date}}",
	"{{now.time}}",
	"{{now.rfc1123}}",
}

SupportedTokens lists the fixed-form faker/now tokens, every one of which resolves with no context at all — TestRenderResponseBody_AllSupportedTokensResolve asserts exactly that, so tokens needing context ({{seq}}) or carrying an argument ({{random.pick:a|b|c}}) stay out of this list even though RenderResponseBody substitutes them too. Keep in sync with the UI tooltip and the README.

Functions

func CurlSnippet

func CurlSnippet(m *model.Mock, baseURL string) string

CurlSnippet returns a one-liner cURL command for the detail page's "Copy as cURL" button. Always uses GET when the mock allows ANY.

func GenerateAdminToken

func GenerateAdminToken() (plain, hash string, err error)

GenerateAdminToken returns a new one-time admin token pair: plain is the value shown to the user exactly once (never persisted), hash is its SHA-256 hex digest stored in mocks.admin_token_hash.

func GenerateSlug

func GenerateSlug(ctx context.Context, checker SlugChecker) (string, error)

GenerateSlug returns a slugLength-character slug not currently in use, retrying up to 5 times on collision. Failing after 5 attempts is treated as a real problem (alphabet exhausted or DB acting up) and surfaced to the caller.

func HighlightJSON

func HighlightJSON(s string) template.HTML

HighlightJSON tokenises s as JSON and returns HTML where each token is wrapped in a <span> with a short class name (jk/js/jn/jb/jl/jp). CSS in style.css colours each class.

If s isn't valid JSON we don't try to be clever — we just escape the raw text so the caller's <pre> still renders cleanly without crashing.

Done server-side so the detail page works with JavaScript disabled and the rendered output is search-engine / share-preview friendly.

func IsReservedResponseHeader

func IsReservedResponseHeader(name string) bool

IsReservedResponseHeader reports whether name is in reservedHeaders. Exposed so the mock router can apply the same filter on the serve path as defence in depth against legacy DB rows written before the filter was tightened.

func LoadSpamPatterns

func LoadSpamPatterns(path string) ([]string, error)

LoadSpamPatterns resolves the pattern list for a QUICKMOCK_SPAM_PATTERNS_FILE value: "off" → nil (disabled), "" → embedded defaults, anything else → that file (one regex per line, # comments).

func MockURL

func MockURL(m *model.Mock, baseURL string) string

MockURL returns the canonical public URL of a mock, including the user's optional path suffix. Use this everywhere a URL is shown to a user — it keeps the labelled path consistent across the detail page, share view, cURL snippet, and code samples.

func ParseCurl

func ParseCurl(input string) (model.MockInput, error)

ParseCurl extracts method, headers, and body from a curl command. It understands the flags users actually paste: -X / --request, -H / --header, -d / --data / --data-raw / --data-binary, and -G. It deliberately ignores auth, cookie jars, output files, and certificate options.

The URL is detected but not retained — we only care about how the user would call the future mock, not where the curl was pointed.

func ParseOpenAPI

func ParseOpenAPI(raw string) ([]model.MockRoute, error)

ParseOpenAPI converts an OpenAPI 3.x JSON or YAML document into the multi-route shape accepted by POST /api/mocks. It intentionally focuses on response examples/schemas: security and server definitions describe the upstream API, not the anonymous Quickmock workspace.

func RenderResponseBodyForRequest

func RenderResponseBodyForRequest(body string, req *RequestData) string

RenderResponseBodyForRequest substitutes {{faker.*}} and {{now.*}} tokens (unknown tokens are left untouched), plus {{request.*}} echo tokens resolved against req. Request values are substituted after the faker/now pass and inserted verbatim — token-looking text inside a query param or body field is never re-expanded.

func SafeRedirectLocation

func SafeRedirectLocation(raw string) bool

SafeRedirectLocation reports whether raw is safe to serve as a mock's Location header. A mock is a trusted quickmock.dev URL to third-party fetchers (AI agents, CI jobs, webhook validators); an absolute Location turns that trust into an open redirect to any host, internal or external. Only a plain relative path is accepted:

  • exactly one leading '/' — "//host/path" and "/\host/path" are protocol-relative and backslash variants that browsers resolve to a host, so they are rejected even though url.Parse alone would not flag them;
  • no scheme and no host once parsed;
  • no control characters, DEL, or spaces, which would let the value smuggle extra response headers;
  • bounded length, to keep it a path rather than an encoded payload.

Exported so the mock router can re-run the same check on the serve path against rows written before this validation existed (see IsReservedResponseHeader for the same defence-in-depth pattern).

func VerifyAdminToken

func VerifyAdminToken(plain, hash string) bool

VerifyAdminToken reports whether plain hashes to hash, using a constant-time comparison. Any invalid input (empty plain/hash) returns false without panicking.

Types

type CodeSnippet

type CodeSnippet struct {
	Lang  string // "js" / "python" / "go" / "php"
	Label string // display label
	Code  string
}

CodeSnippet is one entry shown in the "Use this mock from your code" tabs.

func GenerateSnippets

func GenerateSnippets(m *model.Mock, baseURL string) []CodeSnippet

GenerateSnippets returns ready-to-paste client code for the four supported languages. The mock's URL is built from baseURL + slug; method defaults to GET when the mock allows ANY.

type LogWriter

type LogWriter struct {
	// contains filtered or unexported fields
}

LogWriter accepts request log rows on a buffered channel and writes them to Postgres in the background. If the channel is full, the row is dropped — the public mock router must never block on logging.

func NewLogWriter

func NewLogWriter(repo *repository.LogRepo, mocks *repository.MockRepo, stats *StatsCache, capacity int, logger *slog.Logger, broker *sse.Broker) *LogWriter

NewLogWriter creates a writer with `capacity` buffered slots. A capacity of ~1024 absorbs reasonable bursts while remaining tiny in memory.

func (*LogWriter) Shutdown

func (w *LogWriter) Shutdown(timeout time.Duration)

Shutdown closes the queue and waits for the consumer to finish.

func (*LogWriter) Start

func (w *LogWriter) Start(ctx context.Context)

Start spins up one consumer goroutine. Cancel ctx to stop accepting new items; Shutdown drains the remaining queue.

func (*LogWriter) Submit

func (w *LogWriter) Submit(l model.RequestLog)

Submit enqueues a log row. Drops on full queue.

type MockService

type MockService struct {
	// contains filtered or unexported fields
}

MockService is the only object handlers should call to manipulate mocks.

func NewMockService

func NewMockService(repo MockStore, logs *repository.LogRepo, stats *StatsCache, maxBody, maxMocks int, defaultTTL, maxTTL time.Duration, spam *SpamFilter) *MockService

func (*MockService) AuthorizeSlug

func (s *MockService) AuthorizeSlug(ctx context.Context, slug, adminToken string) (*model.Mock, error)

AuthorizeSlug fetches a mock by slug and checks adminToken against it, without mutating anything. It's the same Get-then-authorize sequence ClearLogs uses right above, exposed for read-only paths outside this file's own mutations — namely the log export handler, which needs the exact same token rule but isn't itself a MockService method.

func (*MockService) ClearLogs

func (s *MockService) ClearLogs(ctx context.Context, slug, adminToken string) error

ClearLogs purges the request_logs for a mock and resets the counter.

func (*MockService) Create

func (s *MockService) Create(ctx context.Context, in model.MockInput, creatorIP string) (*model.Mock, error)

Create validates input, applies defaults, generates a slug, and inserts.

creatorIP is required so we can enforce per-IP limits and rate-limit future requests. It is never shown in the UI.

func (*MockService) Delete

func (s *MockService) Delete(ctx context.Context, slug, adminToken string) error

Delete removes a mock and its logs.

func (*MockService) Extend

func (s *MockService) Extend(ctx context.Context, slug, adminToken string) (*model.Mock, error)

Extend pushes a mock's expiry one more defaultTTL into the future, capped at created_at + maxTTL. Returns ErrTTLCapReached without writing to storage if the mock is already at (or past) that cap — the caller keeps whatever expires_at the mock already has.

func (*MockService) Get

func (s *MockService) Get(ctx context.Context, slug string) (*model.Mock, error)

Get returns a mock by slug. ErrNotFound for unknown or expired.

func (*MockService) Update

func (s *MockService) Update(ctx context.Context, slug string, in model.MockInput, creatorIP, adminToken string) (*model.Mock, error)

Update applies the input to an existing mock, replacing every field.

type MockStore

type MockStore interface {
	Create(ctx context.Context, m *model.Mock) error
	BySlug(ctx context.Context, slug string) (*model.Mock, error)
	Update(ctx context.Context, m *model.Mock) error
	DeleteBySlug(ctx context.Context, slug string) error
	CountActiveByCreatorIP(ctx context.Context, ip string) (int, error)
	SlugExists(ctx context.Context, slug string) (bool, error)
}

MockStore is the subset of *repository.MockRepo the service needs. It is the same DI seam GenerateSlug already uses via SlugChecker above, and it is exported because NewMockService accepts it: tests in other packages — handler's, in particular — need to build a MockService over an in-memory fake, and without that the success path of creating a mock (POST / and POST /templates/:slug/create) could only be exercised against a live Postgres, which is to say never in CI.

*repository.MockRepo satisfies it, so production wiring is unchanged.

type RequestData

type RequestData struct {
	Method string
	Path   string
	IP     string
	Query  url.Values
	Header http.Header
	Body   []byte

	// Host is the authority the request was addressed to — Go keeps it in
	// r.Host, never in r.Header, so {{request.header.host}} cannot stand in
	// for it. Like every other request.* value it is echoed verbatim, and
	// like every other one it is client-controlled: a caller can send any
	// Host header it likes. Response bodies that must name the mock's own
	// address want MockURL below instead.
	Host string

	// MockURL is the mock's own public base URL (BaseURL + "/m/" + slug),
	// resolving {{mock.url}}. It comes from the server's configured base URL
	// rather than from the request, so it is the same address the UI offers
	// for copy-paste and cannot be steered by a forged Host header. It exists
	// for bodies that have to reference themselves — an OIDC discovery
	// document's issuer, a pagination "next" link — which is impossible to
	// write by hand because the slug only exists after creation.
	MockURL string

	// Seq supplies the {{seq}} token's next value. Callers wire it to the
	// shared repository.SeqCounter already used for response-sequence
	// stepping — nil leaves {{seq}} tokens untouched, the same fallback
	// every other token here uses when the data it needs isn't available.
	Seq func() uint64
}

RequestData carries the parts of an incoming request that request.* tokens can echo back into the response body, plus one per-hit extra unrelated to the request itself: Seq. Header values are substituted verbatim, including ones the inspector redacts in storage: nothing here is persisted, and the value goes only to the caller who sent it.

type SlugChecker

type SlugChecker interface {
	SlugExists(ctx context.Context, slug string) (bool, error)
}

SlugChecker is satisfied by *repository.MockRepo.SlugExists.

type SpamFilter

type SpamFilter struct {
	// contains filtered or unexported fields
}

SpamFilter blocks mock create/update when user-controlled content matches a known spam pattern (M4.4). Patterns are RE2 — linear-time matching, so user input can never ReDoS the service. A nil *SpamFilter is a no-op.

func NewSpamFilter

func NewSpamFilter(patterns []string, allowIPs []netip.Prefix, logger *slog.Logger) (*SpamFilter, error)

NewSpamFilter compiles patterns; an invalid one is a hard error — fail fast at startup. allowIPs is parsed by config.

func (*SpamFilter) Blocked

func (f *SpamFilter) Blocked(in *model.MockInput, creatorIP string) bool

Blocked reports whether any user-controlled field of in matches a spam pattern. Allowlisted creator IPs always pass (the false-positive escape hatch from the backlog AC).

type StatsCache

type StatsCache struct {
	// contains filtered or unexported fields
}

StatsCache wraps StatsRepo with a process-local cache. The home page reads the snapshot on every render — without the cache that's one DB round-trip per home page view. With the cache it's amortised to one query per `ttl` per process.

func NewStatsCache

func NewStatsCache(repo *repository.StatsRepo, ttl time.Duration, log *slog.Logger) *StatsCache

func (*StatsCache) BumpAsync

func (c *StatsCache) BumpAsync(key string, n int64)

BumpAsync increments key by n in a goroutine, never blocking the caller. Errors are logged. Use this on hot paths (mock creation, request hit).

func (*StatsCache) Snapshot

func (c *StatsCache) Snapshot(ctx context.Context) map[string]int64

Snapshot returns the current view of all counters. If the cache is fresh enough, returns the cached map; otherwise queries the DB and refreshes. Errors are logged and the stale snapshot is returned — never blocking the home page on a transient DB hiccup.

type ValidationError

type ValidationError struct {
	Field   string
	Message string // human-readable English fallback; not shown to users
}

ValidationError carries the offending field name. Handlers translate the field into a localized message.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Jump to

Keyboard shortcuts

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