Skip to content
Talk to our solutions team

Meta End-to-End Testing

Audience: customer developers integrating against meta.svc, and kis.ai DevSecOps running the service in staging / production. Covers every server run mode, authenticated and unauthenticated testing with curl, and the OpenAPI spec (docs/openapi.yaml) importable into Bruno, Postman, Insomnia, or any OpenAPI-compatible client.

Meta serves a product’s files by path, as they are. Nothing below depends on what a file means: the paths are examples, and any file of any folder reads the same way. What a folder means is the business of the block that reads it.

  1. Prerequisites
  2. Request Headers Reference
  3. Server Run Modes
  4. Sample Product Tree
  5. Testing with curl
  6. Testing with the OpenAPI Spec
  7. DevSecOps Checklist
Terminal window
kvm install baas/meta.svc

Then serve a product directory:

Terminal window
meta.svc serve acme:demo:./my-product
Terminal window
brew install jq # macOS
apt-get install jq # Debian/Ubuntu
Terminal window
meta.svc version
meta.svc serve --help
HeaderRequiredDescription
X-Api-KeyWhen the server has an API key (meta serve -k <key>, or api.key in the config)The key travels in this header only, never in the query string (a query string ends up in access logs and proxies).
AuthorizationAlternative to X-Api-KeyBearer <token>: a tenant token issued by IAM. It carries the customer.
X-CustomerUnder an API keyThe customer whose products resolve. meta serve defaults it when it serves a single customer.
If-None-MatchFor conditional requestsAn ETag from a previous response: 304 when the content has not changed.
Content-TypeFor JSON bodiesapplication/json.

The product is always in the path (/product/{id}/…).

Shorthand used throughout this guide:

Terminal window
BASE="http://localhost:8080"
CUSTOMER="acme"
PRODUCT="kisai-demo"
APIKEY="kisai_A1B2C3D4"
A=(-H "X-Api-Key: $APIKEY")
C=(-H "X-Customer: $CUSTOMER")

When to use: Local development, quickest setup, no secrets.

Terminal window
meta.svc serve acme:kisai-demo:./my-product
  • Port defaults to 8080.
  • No X-Api-Key is checked.
  • A single customer is served, so X-Customer defaults to acme.
  • The server looks for edits on disk every --watch (default 2s) and sends a change event when the product changed.

When to use: Shared dev/CI environments where you need a lightweight secret gate.

Terminal window
meta.svc serve acme:kisai-demo:./my-product -k kisai_A1B2C3D4
  • A request without X-Api-Key: kisai_A1B2C3D4 gets 401. /health and /ready stay open: a probe carries no key.
  • Generate a real key for shared environments: openssl rand -hex 20.

Custom port:

Terminal window
meta.svc serve acme:kisai-demo:./my-product -k kisai_A1B2C3D4 -p 9090
BASE="http://localhost:9090"

When to use: Testing branch / tag access and the git operations.

A git repository at the path is detected automatically:

Terminal window
git -C my-product init
git -C my-product add .
git -C my-product commit -m "init"
git -C my-product tag v1.0.0
meta.svc serve acme:kisai-demo:./my-product -k kisai_A1B2C3D4

Without ?version, reads serve the working tree as it is. A branch or tag is read out of its commit; nothing is checked out:

Terminal window
curl "$BASE/product/$PRODUCT/file/config.yaml?version=v1.0.0&versionType=tag" "${A[@]}"
Terminal window
meta.svc serve \
acme:frontend:./frontend-product \
acme:backend:./backend-product \
-k kisai_A1B2C3D4

The path names the product:

Terminal window
curl "$BASE/product/frontend/files?path=/" "${A[@]}"
curl "$BASE/product/backend/files?path=/" "${A[@]}"

When to use: Testing customer isolation.

Terminal window
meta.svc serve \
acme:app:./acme-product \
contoso:app:./contoso-product \
-k kisai_A1B2C3D4

With two customers there is no default: X-Customer selects whose app resolves.

Terminal window
curl "$BASE/product/app/file/config.yaml" "${A[@]}" -H "X-Customer: acme"
curl "$BASE/product/app/file/config.yaml" "${A[@]}" -H "X-Customer: contoso"

When to use: Shipping a binary that carries its products.

Without a key (an unencrypted payload):

Terminal window
# Embed one or more product folders (folder[:entitlement]) into a copy of the binary.
# A folder's .git is never embedded.
meta.svc embed ./my-product ./docs-site:em1 --output embedded-meta
./embedded-meta serve

With a key (an encrypted payload):

Terminal window
meta.svc embed ./my-product --key kisai_LICENSE_KEY_HERE --output embedded-meta
./embedded-meta serve --license kisai_LICENSE_KEY_HERE

With no arguments and no embedded product:

Terminal window
meta.svc serve
# Error: no products specified and no embedded product found

Check what is embedded:

Terminal window
./embedded-meta embed-info # every product's manifest
./embedded-meta embed-info --license kisai_LICENSE_KEY_HERE # an encrypted payload's, once decrypted
curl "$BASE/ops/embedded-info" "${A[@]}"
# {"embedded":true,"manifest":{"name":"kisai-demo","files":42,"size":128000}}
# or, for several products
# {"embedded":true,"count":2,"manifests":[…]}
# or
# {"embedded":false,"message":"no product embedded"}

When to use: Running meta.svc as the platform service: customers’ products from git, a tenant token or an API key, the config service.

.meta.yaml
port: "8080"
api: # optional: requests then need X-Api-Key
key: ${env:META_API_KEY}
repowatchduration: 1m # how often each repo is fetched
cachetype: fs # memory | fs (mirrors on disk: survive restarts)
cachedir: /var/cache/meta
products: # customer → the products it has
acme:
- name: kisai-demo
url: https://x-access-token:${env:GIT_TOKEN}@git.example.com/acme/kisai-demo.git
type: remote
ref: main
readonly: true
Terminal window
meta.svc -f .meta.yaml # default mode
meta.svc -m product -f .meta.yaml # product mode: forge-authored products
meta.svc -m marketplace -f .meta.yaml # marketplace mode

A remote repository is mirrored and fetched, and served from its commits. Its credential is taken out of the URL: it authenticates every fetch and is never written to disk.

With a tenant token instead of an API key:

Terminal window
curl "$BASE/product/kisai-demo/file/config.yaml" -H "Authorization: Bearer $TOKEN"

The examples below assume this tree at ./my-product:

my-product/
├── config.yaml
├── data/
│ ├── customer.yaml
│ └── sub/
│ └── deep.yaml
└── content/
└── logo.svg
Terminal window
mkdir -p my-product/data/sub my-product/content
echo 'name: kisai-demo' > my-product/config.yaml
echo 'entity: customer' > my-product/data/customer.yaml
echo 'deep: 1' > my-product/data/sub/deep.yaml
echo '<svg/>' > my-product/content/logo.svg
meta.svc serve acme:kisai-demo:./my-product -k kisai_A1B2C3D4 --readonly=false
Terminal window
curl -s "$BASE/health"
curl -s "$BASE/ready" # 200 once the repositories have loaded
curl -s "$BASE/ops/repositories" "${A[@]}" "${C[@]}" | jq .
# {"repositories":{"products":{"acme":[{"name":"kisai-demo","type":"fs"}]},"marketplace":[]}}

An operator sees every customer’s products; anyone else sees their own.

One file, as it is:

Terminal window
curl -s "$BASE/product/$PRODUCT/file/config.yaml" "${A[@]}" "${C[@]}"
# name: kisai-demo

A listing (path is required; depth bounds it; listtype=files|folders narrows it):

Terminal window
curl -s "$BASE/product/$PRODUCT/files?path=/&depth=2" "${A[@]}" "${C[@]}" | jq .
# {"files":[…],"folders":[…]}

At a version (git repositories):

Terminal window
curl -s "$BASE/product/$PRODUCT/file/config.yaml?version=main&versionType=branch" "${A[@]}" "${C[@]}"
curl -s "$BASE/product/$PRODUCT/file/config.yaml?version=v1.0.0&versionType=tag" "${A[@]}" "${C[@]}"

Several files in one request. A missing file is an entry with error, not a failed batch:

Terminal window
curl -s -X POST "$BASE/product/$PRODUCT/batch" "${A[@]}" "${C[@]}" \
-H "Content-Type: application/json" \
-d '{"paths":["config.yaml","data/customer.yaml","nope.yaml"]}' | jq .
# {"files":[{"path":"config.yaml","content":"<base64>","hash":"…","size":17},
# …,
# {"path":"nope.yaml","content":"","hash":"","size":0,"error":"not found"}]}

An entry’s error says what failed (not found, invalid path, unreadable), never where on the server. content is base64:

Terminal window
curl -s -X POST "$BASE/product/$PRODUCT/batch" "${A[@]}" "${C[@]}" \
-H "Content-Type: application/json" -d '{"paths":["config.yaml"]}' \
| jq -r '.files[0].content' | base64 -d

A manifest: every file and folder under a path, hashed. Compare root_hash to tell whether anything under the path changed:

Terminal window
curl -s "$BASE/product/$PRODUCT/manifest?path=data" "${A[@]}" "${C[@]}" | jq .
# {"root_hash":"…","entries":[{"path":"data/customer.yaml","hash":"…","size":17,"mod_time":…,"is_dir":false},…]}

A folder the product does not have is an empty manifest, not an error.

fs/stat and fs/readdir describe the product’s files and folders (the meta client’s Stat and List). A file is read with file/{path…}; whether a path exists is fs/stat’s 200 or 404.

Terminal window
curl -s "$BASE/product/$PRODUCT/fs/stat/config.yaml" "${A[@]}" "${C[@]}" | jq .
# {"name":"config.yaml","size":17,"mode":420,"mod_time":1716400000000,"is_dir":false}
curl -s -o /dev/null -w '%{http_code}\n' "$BASE/product/$PRODUCT/fs/stat/does-not-exist.yaml" "${A[@]}" "${C[@]}"
# 404
curl -s "$BASE/product/$PRODUCT/fs/readdir/data" "${A[@]}" "${C[@]}" | jq .
# [{"name":"customer.yaml","is_dir":false,…},{"name":"sub","is_dir":true,…}]

mod_time is Unix milliseconds.

Every file read and manifest carries an ETag:

Terminal window
ETAG=$(curl -si "$BASE/product/$PRODUCT/file/config.yaml" "${A[@]}" "${C[@]}" \
| grep -i '^etag:' | tr -d '\r' | awk '{print $2}')
curl -s -o /dev/null -w "%{http_code}\n" "$BASE/product/$PRODUCT/file/config.yaml" \
"${A[@]}" "${C[@]}" -H "If-None-Match: $ETAG"
# 304
curl -s -o /dev/null -w "%{http_code}\n" "$BASE/product/$PRODUCT/file/config.yaml" \
"${A[@]}" "${C[@]}" -H 'If-None-Match: "stale"'
# 200

Server-sent events when asked for (Accept: text/event-stream; a browser EventSource asks), one JSON object per line (application/x-ndjson) otherwise:

Terminal window
curl -sN "$BASE/product/stream?id=$PRODUCT" "${A[@]}" "${C[@]}" -H "Accept: text/event-stream"
# event: snapshot
# data: {"hash":"…","ref":"","repo":"kisai-demo","ts":"…"}
curl -sN "$BASE/product/stream?id=$PRODUCT" "${A[@]}" "${C[@]}"
# {"data":{"hash":"…","ref":"","repo":"kisai-demo","ts":"…"},"event":"snapshot"}

In another terminal, edit a file: within --watch a repo-updated event arrives with the new hash and the previous one as prevHash. A write through the API publishes one at once. A ping arrives every 30 s. ref is empty for a folder, and the branch or tag a git repository serves otherwise.

Writes need a local checkout or folder that is not read-only (--readonly=false). A read-only repository answers 403, and a write that names a version 400: a write goes to the working tree.

Terminal window
# Create and remove a folder
curl -s -X PUT "$BASE/product/$PRODUCT/files?path=drafts" "${A[@]}" "${C[@]}" # 409 if it exists
curl -s -X DELETE "$BASE/product/$PRODUCT/files?path=drafts" "${A[@]}" "${C[@]}" # 404 if absent
# Upload: the path is the folder; each file lands at <folder>/<its filename>
curl -s -X POST "$BASE/product/$PRODUCT/file/data" "${A[@]}" "${C[@]}" \
-F "files=@./orders.yaml"
# again: 409 (it exists); add ?overwrite=true to replace it
# Upload a zip, expanded in place
curl -s -X POST "$BASE/product/$PRODUCT/zip/imports" "${A[@]}" "${C[@]}" -F "files=@./bundle.zip"

The product root cannot be removed (400), and nothing under .git can be written or removed.

A local git checkout (§3.3), not read-only:

Terminal window
curl -s "$BASE/product/$PRODUCT/git/refs" "${A[@]}" "${C[@]}" | jq . # {"branches":[…],"tags":[…]}
curl -s "$BASE/product/$PRODUCT/git/version" "${A[@]}" "${C[@]}" | jq . # the commit served
J=(-H "Content-Type: application/json")
curl -s -X POST "$BASE/product/$PRODUCT/git/status" "${A[@]}" "${C[@]}" "${J[@]}" -d '{}'
curl -s -X POST "$BASE/product/$PRODUCT/git/diff" "${A[@]}" "${C[@]}" "${J[@]}" -d '{}'
curl -s -X POST "$BASE/product/$PRODUCT/git/stage" "${A[@]}" "${C[@]}" "${J[@]}" -d '{"paths":["."]}'
curl -s -X POST "$BASE/product/$PRODUCT/git/commit" "${A[@]}" "${C[@]}" "${J[@]}" \
-d '{"message":"edit","username":"dev","useremail":"[email protected]"}'
# Write, stage and commit one file
curl -s -X PATCH "$BASE/product/$PRODUCT/git/commitfile" "${A[@]}" "${C[@]}" "${J[@]}" \
-d '{"path":"data/notes.yaml","file":"reviewed: true\n","message":"notes"}'
# Against the checkout's remote
curl -s -X POST "$BASE/product/$PRODUCT/git/fetch" "${A[@]}" "${C[@]}" "${J[@]}" -d '{}'
curl -s -X POST "$BASE/product/$PRODUCT/git/pull" "${A[@]}" "${C[@]}" "${J[@]}" -d '{}'
curl -s -X POST "$BASE/product/$PRODUCT/git/push" "${A[@]}" "${C[@]}" "${J[@]}" -d '{}'

A git operation that needs a working tree (everything but git/refs and git/version) against a folder or a remote repository is 400. git/version of a folder is 400 (it has no commit), and git/refs of a folder is empty.

A marketplace repository (marketplace mode, §3.7):

Terminal window
MARKET="kisai-components"
curl -s "$BASE/market/$MARKET/file/README.md" "${A[@]}" "${C[@]}"
curl -s "$BASE/market/$MARKET/git/zip" "${A[@]}" "${C[@]}" -o market.zip
curl -s "$BASE/market/$MARKET/fileorfolderzip/components/button" "${A[@]}" "${C[@]}" -o button.zip
curl -s -X PATCH "$BASE/market/$MARKET/paths" "${A[@]}" "${C[@]}" \
-H "Content-Type: application/json" -d '{"paths":["README.md","components/button/config.yaml"]}' -o some.zip

Every caller’s mistake answers its own 4xx; a 5xx is the server’s own failure.

Terminal window
code() { curl -s -o /dev/null -w "%{http_code}" "$@"; echo; }
code "$BASE/product/$PRODUCT/files?path=/" "${C[@]}" # 401: no key
code "$BASE/product/$PRODUCT/files?path=/" -H "X-Api-Key: wrong" "${C[@]}" # 401: wrong key
code "$BASE/product/nope/file/config.yaml" "${A[@]}" "${C[@]}" # 404: no such product
code "$BASE/product/$PRODUCT/file/nope.yaml" "${A[@]}" "${C[@]}" # 404: no such file
code "$BASE/product/$PRODUCT/file/.git/config" "${A[@]}" "${C[@]}" # 404: .git is never served
code "$BASE/product/$PRODUCT/file/..%2F..%2Fetc%2Fpasswd" "${A[@]}" "${C[@]}" # 400: never outside the product
code -X PUT "$BASE/product/$PRODUCT/files?path=x&version=main" "${A[@]}" "${C[@]}" # 400: a write names a version

The specification lives at docs/openapi.yaml (OpenAPI 3.0) in the meta service. It describes every route above: parameters, bodies and statuses. Import it into any OpenAPI-compatible client for a pre-populated collection:

  • Bruno: Import Collection → OpenAPI v3 → select the file.
  • Postman: Import → drag in the file; run with the Collection Runner or newman.
  • Insomnia: File → Import → Request Collection. Hoppscotch: Import/Export → Import from OpenAPI.

Set these environment variables:

VariableExampleUsed as
baseUrlhttp://localhost:8080server base URL
apikeykisai_A1B2C3D4X-Api-Key
customeracmeX-Customer
idkisai-demothe {id} path segment (the product)
pathconfig.yamlthe {path} path segment

Run through this before every production deployment.

Terminal window
[ "$(curl -s -o /dev/null -w '%{http_code}' "$BASE/ops/repositories")" = 401 ] \
&& echo "PASS" || echo "FAIL: auth not enforced"
[ "$(curl -s -o /dev/null -w '%{http_code}' "$BASE/ops/repositories" -H 'X-Api-Key: wrong')" = 401 ] \
&& echo "PASS" || echo "FAIL: a wrong key is accepted"
[ "$(curl -s -o /dev/null -w '%{http_code}' "$BASE/ops/repositories?apikey=$APIKEY")" = 401 ] \
&& echo "PASS" || echo "FAIL: a key in the query string is accepted"
[ "$(curl -s -o /dev/null -w '%{http_code}' "$BASE/ops/repositories" "${A[@]}" "${C[@]}")" = 200 ] \
&& echo "PASS" || echo "FAIL: the right key is refused"
Terminal window
# Serve two customers (§3.5), each with a product named "app" whose config.yaml differs
ACME=$(curl -s "$BASE/product/app/file/config.yaml" "${A[@]}" -H "X-Customer: acme")
CONTOSO=$(curl -s "$BASE/product/app/file/config.yaml" "${A[@]}" -H "X-Customer: contoso")
[ "$ACME" != "$CONTOSO" ] && echo "PASS: each customer reads its own" || echo "FAIL: customers share a product"
Terminal window
for P in ".git/config" ".GIT/config" "data/.git/config"; do
S=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/product/$PRODUCT/file/$P" "${A[@]}" "${C[@]}")
[ "$S" = 404 ] && echo "PASS: $P" || echo "FAIL [$S]: $P"
done
curl -s "$BASE/product/$PRODUCT/fs/readdir/" "${A[@]}" "${C[@]}" | grep -qi '"\.git"' \
&& echo "FAIL: .git listed" || echo "PASS: .git not listed"
Terminal window
for ENDPOINT in \
"/health" "/ready" "/ops/repositories" "/ops/embedded-info" \
"/product/$PRODUCT/files?path=/" "/product/$PRODUCT/manifest" \
"/product/$PRODUCT/file/config.yaml" "/product/$PRODUCT/file/does-not-exist.yaml"
do
S=$(curl -s -o /dev/null -w "%{http_code}" "$BASE$ENDPOINT" "${A[@]}" "${C[@]}")
[ "$S" -lt 500 ] && echo "PASS [$S]: $ENDPOINT" || echo "FAIL [$S]: $ENDPOINT"
done
Terminal window
ETAG=$(curl -si "$BASE/product/$PRODUCT/file/config.yaml" "${A[@]}" "${C[@]}" \
| grep -i '^etag:' | tr -d '\r' | awk '{print $2}')
S=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/product/$PRODUCT/file/config.yaml" \
"${A[@]}" "${C[@]}" -H "If-None-Match: $ETAG")
[ "$S" = 304 ] && echo "PASS: ETag caching works" || echo "FAIL: got $S, expected 304"
Terminal window
# Served with the default --readonly=true
S=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "$BASE/product/$PRODUCT/files?path=test-dir" "${A[@]}" "${C[@]}")
[ "$S" = 403 ] && echo "PASS: writes refused" || echo "FAIL: got $S, expected 403"
Terminal window
curl -sv https://meta.staging.kis.ai/health 2>&1 | grep -E "SSL|TLS|certificate|issuer"
curl -s --fail https://meta.staging.kis.ai/health && echo "PASS: TLS valid" || echo "FAIL: TLS error"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
failureThreshold: 2

Liveness never fails for repository problems: a restart cannot fix an expired git token. Readiness fails only when every product repository failed to load.