3 new knowledge files, 31 chunks ingested: - curl-api-testing.md: API testing patterns, auth, error testing, k8s testing - tea-cli.md: Gitea CLI for issues, PRs, CI runs, releases - verify-done.md: definition of done checklist, verification workflow
161 lines
4.8 KiB
Markdown
161 lines
4.8 KiB
Markdown
# curl — API Testing & Verification
|
|
|
|
## Why Test With curl
|
|
- Every API endpoint must be testable with curl before marking a task done.
|
|
- curl is the ground truth — if curl can't hit it, the API doesn't work.
|
|
- Test locally first (`localhost`), then staging, then production.
|
|
- Save working curl commands as documentation for future reference.
|
|
|
|
## Basic Patterns
|
|
```bash
|
|
# GET request
|
|
curl -s http://localhost:8080/health | jq .
|
|
|
|
# POST with JSON body
|
|
curl -s -X POST http://localhost:8080/memory/ingest \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"project": "poimen", "text": "test record", "kind": "L1"}'
|
|
|
|
# PUT update
|
|
curl -s -X PUT http://localhost:8080/resource/123 \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"field": "new_value"}'
|
|
|
|
# DELETE
|
|
curl -s -X DELETE http://localhost:8080/resource/123
|
|
```
|
|
|
|
## Authentication
|
|
```bash
|
|
# Bearer token (JWT)
|
|
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8080/memory/query
|
|
|
|
# Get JWT from Authentik
|
|
TOKEN=$(curl -s -X POST https://authentik.riotpiao.com/application/o/token/ \
|
|
-d "grant_type=client_credentials" \
|
|
-d "client_id=$CLIENT_ID" \
|
|
-d "client_secret=$CLIENT_SECRET" | jq -r .access_token)
|
|
|
|
# API key header
|
|
curl -s -H "X-API-Key: $API_KEY" http://localhost:8080/endpoint
|
|
```
|
|
|
|
## Response Inspection
|
|
```bash
|
|
# Status code only
|
|
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health
|
|
|
|
# Headers + body
|
|
curl -sv http://localhost:8080/health 2>&1
|
|
|
|
# Response time
|
|
curl -s -o /dev/null -w "%{time_total}s" http://localhost:8080/health
|
|
|
|
# Follow redirects
|
|
curl -sL http://localhost:8080/old-path
|
|
|
|
# Pretty print JSON
|
|
curl -s http://localhost:8080/health | python3 -m json.tool
|
|
```
|
|
|
|
## Error Testing
|
|
```bash
|
|
# Expect 401 — missing auth
|
|
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/memory/query
|
|
# Should return: 401
|
|
|
|
# Expect 400 — bad request body
|
|
curl -s -X POST http://localhost:8080/memory/ingest \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"invalid": true}'
|
|
# Should return: 400
|
|
|
|
# Expect 429 — rate limit
|
|
for i in $(seq 1 200); do
|
|
curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $TOKEN" \
|
|
http://localhost:8080/memory/query -d '{"query":"test"}'
|
|
done | sort | uniq -c
|
|
# Should see 429 after limit exceeded
|
|
|
|
# Expect 404 — not found
|
|
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/nonexistent
|
|
```
|
|
|
|
## Kubernetes Service Testing
|
|
```bash
|
|
# Port-forward to test in-cluster service
|
|
kubectl port-forward -n poimen svc/poimen-memory 8080:8080 &
|
|
curl -s http://localhost:8080/health
|
|
|
|
# Direct pod exec curl
|
|
kubectl exec -n poimen deploy/poimen-memory -- curl -s http://localhost:8080/health
|
|
|
|
# Test from inside cluster (debug pod)
|
|
kubectl run -it --rm curl-test --image=curlimages/curl -- \
|
|
curl -s http://poimen-memory.poimen.svc.cluster.local:8080/health
|
|
|
|
# Test ingress from outside
|
|
curl -sk https://api.riotpiao.com/health
|
|
```
|
|
|
|
## Verification Checklist
|
|
After completing any API work, verify with curl:
|
|
1. **Happy path** — correct input returns expected output and status code
|
|
2. **Auth required** — missing token returns 401, bad token returns 403
|
|
3. **Validation** — bad input returns 400 with error message
|
|
4. **Not found** — missing resource returns 404
|
|
5. **Idempotency** — same request twice returns same result (for POST with idempotency key)
|
|
6. **Rate limiting** — excessive requests return 429
|
|
7. **Content type** — response has correct Content-Type header
|
|
8. **Latency** — response time within SLA (< 500ms for queries, < 50ms for health)
|
|
|
|
## Scripted Verification
|
|
```bash
|
|
#!/bin/bash
|
|
# verify-api.sh — run after any API change
|
|
BASE="http://localhost:8080"
|
|
PASS=0; FAIL=0
|
|
|
|
check() {
|
|
local desc="$1" expected="$2" actual="$3"
|
|
if [ "$expected" = "$actual" ]; then
|
|
echo "✓ $desc"; ((PASS++))
|
|
else
|
|
echo "✗ $desc (expected $expected, got $actual)"; ((FAIL++))
|
|
fi
|
|
}
|
|
|
|
check "health returns 200" "200" \
|
|
"$(curl -s -o /dev/null -w '%{http_code}' $BASE/health)"
|
|
|
|
check "query without auth returns 401" "401" \
|
|
"$(curl -s -o /dev/null -w '%{http_code}' -X POST $BASE/memory/query)"
|
|
|
|
check "ingest with bad body returns 400" "400" \
|
|
"$(curl -s -o /dev/null -w '%{http_code}' -X POST $BASE/memory/ingest \
|
|
-H 'Content-Type: application/json' -d '{}')"
|
|
|
|
echo "---"
|
|
echo "$PASS passed, $FAIL failed"
|
|
[ $FAIL -eq 0 ] && exit 0 || exit 1
|
|
```
|
|
|
|
## Data Piping
|
|
```bash
|
|
# Ingest from file
|
|
curl -s -X POST http://localhost:8080/memory/ingest \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-d @payload.json
|
|
|
|
# Query and filter with jq
|
|
curl -s -H "Authorization: Bearer $TOKEN" \
|
|
http://localhost:8080/memory/query \
|
|
-d '{"project":"poimen","query":"rust error handling"}' | \
|
|
jq '.results[] | {score, text}'
|
|
|
|
# Chain: query → pipe to next call
|
|
CHUNK_ID=$(curl -s ... | jq -r '.results[0].id')
|
|
curl -s http://localhost:8080/memory/chunk/$CHUNK_ID
|
|
```
|