Add QA test framework — API tests, infra health, test data seeding
Payfrit: - Tab API contract tests (bash + python) — all 13 endpoints - Param validation, response time, cross-env parity checks Grubflip: - API endpoint test stubs — menu, restaurant, order, auth - Ready to activate as Mike deploys endpoints Shared: - test_helpers.sh + test_helpers.py — HTTP helpers, pass/fail/skip, JSON output mode - Master test runner (scripts/run-all.sh) - Infrastructure health checker (disk, RAM, services, SSL certs) Test data: - Grubflip seed SQL (QA test restaurants, menus, orders) - Payfrit tab seeder script All Payfrit tab tests confirmed passing against dev + prod.
This commit is contained in:
parent
7d6d8c558e
commit
e6153ac4b7
16 changed files with 1031 additions and 2 deletions
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Generated reports
|
||||||
|
reports/*.txt
|
||||||
|
reports/*.json
|
||||||
|
reports/*.md
|
||||||
|
|
||||||
|
# Temp files
|
||||||
|
*.tmp
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Secrets (never commit)
|
||||||
|
.env
|
||||||
|
*.secret
|
||||||
|
credentials.*
|
||||||
47
README.md
47
README.md
|
|
@ -1,3 +1,46 @@
|
||||||
# payfrit-qa
|
# Payfrit QA Test Framework
|
||||||
|
|
||||||
QA test framework — API tests, infra health checks, test data seeding
|
Automated test suites for Payfrit and Grubflip APIs, infrastructure health, and deployment validation.
|
||||||
|
|
||||||
|
**Maintainer:** Luna (@luna) — QA
|
||||||
|
**Forgejo:** `payfrit/payfrit-qa`
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
payfrit/api/ — Payfrit Tab API contract tests
|
||||||
|
grubflip/api/ — Grubflip API test suite
|
||||||
|
shared/lib/ — Shared test utilities (bash + python)
|
||||||
|
test-data/ — Seed data and fixtures
|
||||||
|
scripts/ — CI runners, deployment checklists
|
||||||
|
reports/ — Generated test reports (gitignored)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all Payfrit API tests against dev
|
||||||
|
./scripts/run-all.sh payfrit dev
|
||||||
|
|
||||||
|
# Run all Grubflip API tests against dev
|
||||||
|
./scripts/run-all.sh grubflip dev
|
||||||
|
|
||||||
|
# Run infrastructure health check
|
||||||
|
./scripts/infra-health.sh
|
||||||
|
|
||||||
|
# Seed test data
|
||||||
|
./test-data/payfrit/seed-tabs.sh dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environments
|
||||||
|
|
||||||
|
| Name | Payfrit API | Grubflip API |
|
||||||
|
|------|-------------|--------------|
|
||||||
|
| dev | `https://dev.payfrit.com/api` | `https://dev.grubflip.com/api` |
|
||||||
|
| prod | `https://biz.payfrit.com/api` | `https://api.grubflip.com` |
|
||||||
|
|
||||||
|
## Test Conventions
|
||||||
|
|
||||||
|
- Every test script exits 0 on all-pass, 1 on any failure.
|
||||||
|
- JSON output mode available via `--json` flag for CI integration.
|
||||||
|
- Reports written to `reports/` with timestamp filenames.
|
||||||
|
|
|
||||||
151
grubflip/api/test_endpoints.py
Normal file
151
grubflip/api/test_endpoints.py
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
test_endpoints.py — Grubflip API test suite
|
||||||
|
|
||||||
|
Tests Grubflip API endpoints as they come online.
|
||||||
|
Structured as stubs that will be filled in as Mike deploys endpoints.
|
||||||
|
|
||||||
|
Current endpoint coverage (from Mike's task list):
|
||||||
|
- Menu endpoints (CRUD)
|
||||||
|
- Order endpoints
|
||||||
|
- Restaurant endpoints
|
||||||
|
- User endpoints
|
||||||
|
- Auth endpoints
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 grubflip/api/test_endpoints.py [dev|prod] [--json]
|
||||||
|
|
||||||
|
Author: Luna (@luna) — QA
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../.."))
|
||||||
|
from shared.lib.test_helpers import TestRunner, get_base_url
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
env = "dev"
|
||||||
|
json_mode = False
|
||||||
|
for arg in sys.argv[1:]:
|
||||||
|
if arg == "--json":
|
||||||
|
json_mode = True
|
||||||
|
elif arg in ("dev", "prod"):
|
||||||
|
env = arg
|
||||||
|
|
||||||
|
base = get_base_url("grubflip", env)
|
||||||
|
if not base:
|
||||||
|
print(f"Unknown environment: {env}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
t = TestRunner(f"grubflip-api-{env}", json_mode=json_mode)
|
||||||
|
|
||||||
|
# ─── Connectivity check ──────────────────────────────────
|
||||||
|
t.section("API CONNECTIVITY")
|
||||||
|
resp = t.http_get(base)
|
||||||
|
if resp["status"] == 0:
|
||||||
|
t.fail("API base reachable", resp["error"])
|
||||||
|
# If we can't even reach it, skip everything else
|
||||||
|
exit_code = t.summary()
|
||||||
|
sys.exit(exit_code)
|
||||||
|
else:
|
||||||
|
t.pass_(f"API base responds (HTTP {resp['status']}, {resp['time_ms']:.0f}ms)")
|
||||||
|
|
||||||
|
# ─── Menu endpoints ──────────────────────────────────────
|
||||||
|
t.section("MENU ENDPOINTS")
|
||||||
|
|
||||||
|
menu_endpoints = [
|
||||||
|
("GET", "/menus", "List menus"),
|
||||||
|
("GET", "/menus/1", "Get menu by ID"),
|
||||||
|
("GET", "/menu-items", "List menu items"),
|
||||||
|
("GET", "/menu-items/1", "Get menu item by ID"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for method, path, label in menu_endpoints:
|
||||||
|
url = f"{base}{path}"
|
||||||
|
if method == "GET":
|
||||||
|
resp = t.http_get(url)
|
||||||
|
else:
|
||||||
|
resp = t.http_post(url)
|
||||||
|
|
||||||
|
if resp["status"] == 0:
|
||||||
|
t.skip(label, f"unreachable: {resp['error']}")
|
||||||
|
elif resp["status"] == 404:
|
||||||
|
t.skip(label, "404 — endpoint not deployed yet")
|
||||||
|
elif resp["status"] in (200, 201, 400, 401, 422):
|
||||||
|
t.pass_(f"{label} — HTTP {resp['status']} ({resp['time_ms']:.0f}ms)")
|
||||||
|
else:
|
||||||
|
t.fail(label, f"HTTP {resp['status']}")
|
||||||
|
|
||||||
|
# ─── Restaurant endpoints ────────────────────────────────
|
||||||
|
t.section("RESTAURANT ENDPOINTS")
|
||||||
|
|
||||||
|
restaurant_endpoints = [
|
||||||
|
("GET", "/restaurants", "List restaurants"),
|
||||||
|
("GET", "/restaurants/1", "Get restaurant by ID"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for method, path, label in restaurant_endpoints:
|
||||||
|
url = f"{base}{path}"
|
||||||
|
resp = t.http_get(url)
|
||||||
|
if resp["status"] == 0:
|
||||||
|
t.skip(label, f"unreachable: {resp['error']}")
|
||||||
|
elif resp["status"] == 404:
|
||||||
|
t.skip(label, "404 — endpoint not deployed yet")
|
||||||
|
elif resp["status"] in (200, 400, 401, 403, 422):
|
||||||
|
t.pass_(f"{label} — HTTP {resp['status']} ({resp['time_ms']:.0f}ms)")
|
||||||
|
else:
|
||||||
|
t.fail(label, f"HTTP {resp['status']}")
|
||||||
|
|
||||||
|
# ─── Order endpoints ─────────────────────────────────────
|
||||||
|
t.section("ORDER ENDPOINTS")
|
||||||
|
|
||||||
|
order_endpoints = [
|
||||||
|
("GET", "/orders", "List orders"),
|
||||||
|
("GET", "/orders/1", "Get order by ID"),
|
||||||
|
("POST", "/orders", "Create order"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for method, path, label in order_endpoints:
|
||||||
|
url = f"{base}{path}"
|
||||||
|
if method == "POST":
|
||||||
|
resp = t.http_post(url, {"test": True})
|
||||||
|
else:
|
||||||
|
resp = t.http_get(url)
|
||||||
|
|
||||||
|
if resp["status"] == 0:
|
||||||
|
t.skip(label, f"unreachable: {resp['error']}")
|
||||||
|
elif resp["status"] == 404:
|
||||||
|
t.skip(label, "404 — endpoint not deployed yet")
|
||||||
|
elif resp["status"] in (200, 201, 400, 401, 422):
|
||||||
|
t.pass_(f"{label} — HTTP {resp['status']} ({resp['time_ms']:.0f}ms)")
|
||||||
|
else:
|
||||||
|
t.fail(label, f"HTTP {resp['status']}")
|
||||||
|
|
||||||
|
# ─── Auth endpoints ──────────────────────────────────────
|
||||||
|
t.section("AUTH ENDPOINTS")
|
||||||
|
|
||||||
|
auth_endpoints = [
|
||||||
|
("POST", "/auth/login", "Login"),
|
||||||
|
("POST", "/auth/register", "Register"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for method, path, label in auth_endpoints:
|
||||||
|
url = f"{base}{path}"
|
||||||
|
resp = t.http_post(url, {})
|
||||||
|
if resp["status"] == 0:
|
||||||
|
t.skip(label, f"unreachable: {resp['error']}")
|
||||||
|
elif resp["status"] == 404:
|
||||||
|
t.skip(label, "404 — endpoint not deployed yet")
|
||||||
|
elif resp["status"] in (200, 400, 401, 422):
|
||||||
|
t.pass_(f"{label} — HTTP {resp['status']} ({resp['time_ms']:.0f}ms)")
|
||||||
|
else:
|
||||||
|
t.fail(label, f"HTTP {resp['status']}")
|
||||||
|
|
||||||
|
exit_code = t.summary()
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
64
grubflip/api/test_endpoints.sh
Executable file
64
grubflip/api/test_endpoints.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================
|
||||||
|
# test_endpoints.sh — Grubflip API smoke tests (bash)
|
||||||
|
# Quick connectivity and endpoint existence checks.
|
||||||
|
# Usage: ./grubflip/api/test_endpoints.sh [dev|prod] [--json]
|
||||||
|
# Author: Luna (@luna) — QA
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
source "$REPO_ROOT/shared/lib/test_helpers.sh"
|
||||||
|
|
||||||
|
ENV="${1:-dev}"
|
||||||
|
case "$ENV" in
|
||||||
|
dev) BASE="https://dev.grubflip.com/api" ;;
|
||||||
|
prod) BASE="https://api.grubflip.com" ;;
|
||||||
|
*) echo "Usage: $0 [dev|prod] [--json]"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
$JSON_MODE || {
|
||||||
|
echo "============================================"
|
||||||
|
echo " GRUBFLIP API SMOKE TESTS ($ENV)"
|
||||||
|
echo " $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||||
|
echo "============================================"
|
||||||
|
}
|
||||||
|
|
||||||
|
section "CONNECTIVITY"
|
||||||
|
result=$(http_get "$BASE")
|
||||||
|
IFS='|' read -r code body ms <<< "$result"
|
||||||
|
if [ "$code" != "000" ] && [ -n "$code" ]; then
|
||||||
|
pass "API base responds (HTTP $code, ${ms}ms)"
|
||||||
|
else
|
||||||
|
fail "API base unreachable" "HTTP $code"
|
||||||
|
test_summary
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "ENDPOINT DISCOVERY"
|
||||||
|
ENDPOINTS=(
|
||||||
|
"GET|/menus|List menus"
|
||||||
|
"GET|/menus/1|Get menu"
|
||||||
|
"GET|/menu-items|List menu items"
|
||||||
|
"GET|/restaurants|List restaurants"
|
||||||
|
"GET|/restaurants/1|Get restaurant"
|
||||||
|
"GET|/orders|List orders"
|
||||||
|
"GET|/orders/1|Get order"
|
||||||
|
)
|
||||||
|
|
||||||
|
for entry in "${ENDPOINTS[@]}"; do
|
||||||
|
IFS='|' read -r method path label <<< "$entry"
|
||||||
|
result=$(http_get "$BASE$path")
|
||||||
|
IFS='|' read -r code body ms <<< "$result"
|
||||||
|
|
||||||
|
case "$code" in
|
||||||
|
000) skip "$label ($path)" "unreachable" ;;
|
||||||
|
404) skip "$label ($path)" "not deployed yet" ;;
|
||||||
|
200|201|400|401|422|403)
|
||||||
|
pass "$label ($path) — HTTP $code (${ms}ms)" ;;
|
||||||
|
*)
|
||||||
|
fail "$label ($path)" "HTTP $code" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
test_summary
|
||||||
0
payfrit/__init__.py
Normal file
0
payfrit/__init__.py
Normal file
108
payfrit/api/test_tabs.py
Normal file
108
payfrit/api/test_tabs.py
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
test_tabs.py — Payfrit Tab API contract tests (Python version)
|
||||||
|
|
||||||
|
Tests all 13 tab endpoints for:
|
||||||
|
- Endpoint existence (not 404)
|
||||||
|
- Param validation (correct ERROR field when called with no params)
|
||||||
|
- Response is valid JSON
|
||||||
|
- Response time < 500ms
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 payfrit/api/test_tabs.py [dev|prod] [--json]
|
||||||
|
|
||||||
|
Author: Luna (@luna) — QA
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add repo root to path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../.."))
|
||||||
|
from shared.lib.test_helpers import TestRunner, get_base_url
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
env = "dev"
|
||||||
|
json_mode = False
|
||||||
|
for arg in sys.argv[1:]:
|
||||||
|
if arg == "--json":
|
||||||
|
json_mode = True
|
||||||
|
elif arg in ("dev", "prod"):
|
||||||
|
env = arg
|
||||||
|
|
||||||
|
base = get_base_url("payfrit", env)
|
||||||
|
if not base:
|
||||||
|
print(f"Unknown environment: {env}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
t = TestRunner(f"payfrit-tabs-{env}", json_mode=json_mode)
|
||||||
|
|
||||||
|
# All 13 tab endpoints: (filename, expected_error_when_no_params)
|
||||||
|
TAB_ENDPOINTS = [
|
||||||
|
("open.php", "missing_UserID"),
|
||||||
|
("get.php", "missing_TabID"),
|
||||||
|
("getActive.php", "missing_UserID"),
|
||||||
|
("getPresence.php", "missing_BusinessID"),
|
||||||
|
("close.php", "missing_TabID"),
|
||||||
|
("cancel.php", "missing_TabID"),
|
||||||
|
("addOrder.php", "missing_TabID"),
|
||||||
|
("addMember.php", "missing_TabID"),
|
||||||
|
("removeMember.php", "missing_TabID"),
|
||||||
|
("approveOrder.php", "missing_TabID"),
|
||||||
|
("rejectOrder.php", "missing_TabID"),
|
||||||
|
("pendingOrders.php", "missing_TabID"),
|
||||||
|
("increaseAuth.php", "missing_TabID"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Endpoint existence & param validation ---
|
||||||
|
t.section(f"ENDPOINT EXISTENCE & PARAM VALIDATION ({len(TAB_ENDPOINTS)} endpoints)")
|
||||||
|
|
||||||
|
for endpoint, expected_error in TAB_ENDPOINTS:
|
||||||
|
url = f"{base}/tabs/{endpoint}"
|
||||||
|
resp = t.http_get(url)
|
||||||
|
|
||||||
|
if resp["status"] == 404 or resp["status"] == 0:
|
||||||
|
t.fail(f"tabs/{endpoint}", f"HTTP {resp['status']} — endpoint missing or unreachable")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if resp["json"] is None:
|
||||||
|
t.fail(f"tabs/{endpoint}", "response is not valid JSON")
|
||||||
|
continue
|
||||||
|
|
||||||
|
actual_error = resp["json"].get("ERROR", "")
|
||||||
|
if actual_error == expected_error:
|
||||||
|
t.pass_(f"tabs/{endpoint} — param validation OK ({resp['time_ms']:.0f}ms)")
|
||||||
|
else:
|
||||||
|
t.fail(f"tabs/{endpoint}", f"expected ERROR='{expected_error}', got='{actual_error}'")
|
||||||
|
|
||||||
|
# --- Response time ---
|
||||||
|
t.section("RESPONSE TIME (< 500ms threshold)")
|
||||||
|
MAX_MS = 500
|
||||||
|
|
||||||
|
for endpoint, _ in TAB_ENDPOINTS:
|
||||||
|
url = f"{base}/tabs/{endpoint}"
|
||||||
|
resp = t.http_get(url)
|
||||||
|
ms = resp["time_ms"]
|
||||||
|
if ms < MAX_MS:
|
||||||
|
t.pass_(f"tabs/{endpoint}: {ms:.0f}ms")
|
||||||
|
else:
|
||||||
|
t.fail(f"tabs/{endpoint}: {ms:.0f}ms", f"SLOW (limit {MAX_MS}ms)")
|
||||||
|
|
||||||
|
# --- Cross-environment parity ---
|
||||||
|
if env == "dev":
|
||||||
|
t.section("CROSS-ENVIRONMENT PARITY (dev vs prod)")
|
||||||
|
prod_base = get_base_url("payfrit", "prod")
|
||||||
|
for endpoint, _ in TAB_ENDPOINTS:
|
||||||
|
resp = t.http_get(f"{prod_base}/tabs/{endpoint}")
|
||||||
|
if resp["status"] not in (0, 404):
|
||||||
|
t.pass_(f"tabs/{endpoint} exists on prod (HTTP {resp['status']})")
|
||||||
|
else:
|
||||||
|
t.fail(f"tabs/{endpoint} MISSING on prod (HTTP {resp['status']})")
|
||||||
|
|
||||||
|
exit_code = t.summary()
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
105
payfrit/api/test_tabs.sh
Executable file
105
payfrit/api/test_tabs.sh
Executable file
|
|
@ -0,0 +1,105 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================
|
||||||
|
# test_tabs.sh — Payfrit Tab API Contract Tests
|
||||||
|
# Tests all 13 tab endpoints for existence, param validation,
|
||||||
|
# response format, and response time.
|
||||||
|
# Usage: ./payfrit/api/test_tabs.sh [dev|prod] [--json]
|
||||||
|
# Author: Luna (@luna) — QA
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
source "$REPO_ROOT/shared/lib/test_helpers.sh"
|
||||||
|
|
||||||
|
ENV="${1:-dev}"
|
||||||
|
case "$ENV" in
|
||||||
|
dev) BASE="https://dev.payfrit.com/api" ;;
|
||||||
|
prod) BASE="https://biz.payfrit.com/api" ;;
|
||||||
|
*) echo "Usage: $0 [dev|prod] [--json]"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
$JSON_MODE || {
|
||||||
|
echo "============================================"
|
||||||
|
echo " PAYFRIT TAB API CONTRACT TESTS ($ENV)"
|
||||||
|
echo " $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||||
|
echo "============================================"
|
||||||
|
}
|
||||||
|
|
||||||
|
# All 13 tab endpoints with their expected error when called with no params
|
||||||
|
TAB_ENDPOINTS=(
|
||||||
|
"open.php|missing_UserID"
|
||||||
|
"get.php|missing_TabID"
|
||||||
|
"getActive.php|missing_UserID"
|
||||||
|
"getPresence.php|missing_BusinessID"
|
||||||
|
"close.php|missing_TabID"
|
||||||
|
"cancel.php|missing_TabID"
|
||||||
|
"addOrder.php|missing_TabID"
|
||||||
|
"addMember.php|missing_TabID"
|
||||||
|
"removeMember.php|missing_TabID"
|
||||||
|
"approveOrder.php|missing_TabID"
|
||||||
|
"rejectOrder.php|missing_TabID"
|
||||||
|
"pendingOrders.php|missing_TabID"
|
||||||
|
"increaseAuth.php|missing_TabID"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ─── Endpoint existence & param validation ────────────────────
|
||||||
|
section "ENDPOINT EXISTENCE & PARAM VALIDATION (${#TAB_ENDPOINTS[@]} endpoints)"
|
||||||
|
|
||||||
|
for entry in "${TAB_ENDPOINTS[@]}"; do
|
||||||
|
IFS='|' read -r endpoint expected_error <<< "$entry"
|
||||||
|
url="$BASE/tabs/$endpoint"
|
||||||
|
result=$(http_get "$url")
|
||||||
|
IFS='|' read -r code body ms <<< "$result"
|
||||||
|
|
||||||
|
if [ "$code" = "404" ] || [ "$code" = "000" ]; then
|
||||||
|
fail "tabs/$endpoint" "HTTP $code — endpoint missing"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! is_valid_json "$body"; then
|
||||||
|
fail "tabs/$endpoint" "response is not valid JSON"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
actual_error=$(json_field "$body" "ERROR")
|
||||||
|
if [ "$actual_error" = "$expected_error" ]; then
|
||||||
|
pass "tabs/$endpoint — correct param validation (${ms}ms)"
|
||||||
|
else
|
||||||
|
fail "tabs/$endpoint" "expected error='$expected_error', got='$actual_error'"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ─── Response time checks ─────────────────────────────────────
|
||||||
|
section "RESPONSE TIME (< 500ms threshold)"
|
||||||
|
|
||||||
|
MAX_MS=500
|
||||||
|
for entry in "${TAB_ENDPOINTS[@]}"; do
|
||||||
|
IFS='|' read -r endpoint _ <<< "$entry"
|
||||||
|
url="$BASE/tabs/$endpoint"
|
||||||
|
result=$(http_get "$url")
|
||||||
|
IFS='|' read -r code body ms <<< "$result"
|
||||||
|
|
||||||
|
if [ "${ms:-0}" -lt "$MAX_MS" ] 2>/dev/null; then
|
||||||
|
pass "tabs/$endpoint response: ${ms}ms"
|
||||||
|
else
|
||||||
|
fail "tabs/$endpoint response: ${ms}ms (SLOW, limit ${MAX_MS}ms)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ─── Cross-environment parity ────────────────────────────────
|
||||||
|
if [ "$ENV" = "dev" ]; then
|
||||||
|
section "CROSS-ENVIRONMENT PARITY (dev vs prod)"
|
||||||
|
PROD_BASE="https://biz.payfrit.com/api"
|
||||||
|
for entry in "${TAB_ENDPOINTS[@]}"; do
|
||||||
|
IFS='|' read -r endpoint _ <<< "$entry"
|
||||||
|
prod_result=$(http_get "$PROD_BASE/tabs/$endpoint")
|
||||||
|
IFS='|' read -r prod_code _ _ <<< "$prod_result"
|
||||||
|
if [ "$prod_code" != "404" ] && [ "$prod_code" != "000" ]; then
|
||||||
|
pass "tabs/$endpoint exists on prod (HTTP $prod_code)"
|
||||||
|
else
|
||||||
|
fail "tabs/$endpoint MISSING on prod (HTTP $prod_code)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
test_summary
|
||||||
0
reports/.gitkeep
Normal file
0
reports/.gitkeep
Normal file
110
scripts/infra-health.sh
Executable file
110
scripts/infra-health.sh
Executable file
|
|
@ -0,0 +1,110 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================
|
||||||
|
# infra-health.sh — Infrastructure health checks
|
||||||
|
# Checks: disk, memory, apache, key services, SSL certs
|
||||||
|
# Usage: ./scripts/infra-health.sh [--json]
|
||||||
|
# Author: Luna (@luna) — QA
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
source "$REPO_ROOT/shared/lib/test_helpers.sh"
|
||||||
|
|
||||||
|
$JSON_MODE || {
|
||||||
|
echo "============================================"
|
||||||
|
echo " INFRASTRUCTURE HEALTH CHECK"
|
||||||
|
echo " $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||||
|
echo " Host: $(hostname)"
|
||||||
|
echo "============================================"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Disk ─────────────────────────────────────────────────────
|
||||||
|
section "DISK USAGE"
|
||||||
|
while read -r filesystem size used avail pct mount; do
|
||||||
|
pct_num=${pct%%%}
|
||||||
|
if [ "$pct_num" -lt 80 ]; then
|
||||||
|
pass "$mount: ${pct} used ($avail available)"
|
||||||
|
elif [ "$pct_num" -lt 90 ]; then
|
||||||
|
fail "$mount: ${pct} used — WARNING" "$avail remaining"
|
||||||
|
else
|
||||||
|
fail "$mount: ${pct} used — CRITICAL" "$avail remaining"
|
||||||
|
fi
|
||||||
|
done < <(df -h --output=source,size,used,avail,pcent,target -x tmpfs -x devtmpfs 2>/dev/null | tail -n +2)
|
||||||
|
|
||||||
|
# ─── Memory ───────────────────────────────────────────────────
|
||||||
|
section "MEMORY"
|
||||||
|
read -r total used free shared buff avail < <(free -m | awk '/^Mem:/ {print $2,$3,$4,$5,$6,$7}')
|
||||||
|
pct_used=$((used * 100 / total))
|
||||||
|
if [ "$pct_used" -lt 80 ]; then
|
||||||
|
pass "RAM: ${used}M / ${total}M (${pct_used}%) — ${avail}M available"
|
||||||
|
else
|
||||||
|
fail "RAM: ${used}M / ${total}M (${pct_used}%)" "only ${avail}M available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -r stotal sused sfree < <(free -m | awk '/^Swap:/ {print $2,$3,$4}')
|
||||||
|
if [ "${stotal:-0}" -gt 0 ]; then
|
||||||
|
spct=$((sused * 100 / stotal))
|
||||||
|
if [ "$spct" -lt 50 ]; then
|
||||||
|
pass "Swap: ${sused}M / ${stotal}M (${spct}%)"
|
||||||
|
else
|
||||||
|
fail "Swap: ${sused}M / ${stotal}M (${spct}%)" "high swap usage"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── Key services ─────────────────────────────────────────────
|
||||||
|
section "SERVICES"
|
||||||
|
for svc in apache2 mysql mariadb; do
|
||||||
|
if systemctl is-active --quiet "$svc" 2>/dev/null; then
|
||||||
|
pass "$svc is running"
|
||||||
|
elif systemctl list-unit-files "$svc.service" 2>/dev/null | grep -q "$svc"; then
|
||||||
|
fail "$svc is NOT running"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ─── SSL cert checks ─────────────────────────────────────────
|
||||||
|
section "SSL CERTIFICATES"
|
||||||
|
DOMAINS=("payfrit.com" "dev.payfrit.com" "biz.payfrit.com" "grubflip.com" "dev.grubflip.com")
|
||||||
|
for domain in "${DOMAINS[@]}"; do
|
||||||
|
expiry=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
|
||||||
|
cn=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null | openssl x509 -noout -subject 2>/dev/null | grep -oP 'CN\s*=\s*\K.*')
|
||||||
|
|
||||||
|
if [ -z "$expiry" ]; then
|
||||||
|
fail "$domain SSL" "could not retrieve certificate"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null)
|
||||||
|
now_epoch=$(date +%s)
|
||||||
|
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
|
||||||
|
|
||||||
|
if echo "$cn" | grep -qi "$domain"; then
|
||||||
|
cn_ok="CN matches"
|
||||||
|
else
|
||||||
|
cn_ok="CN MISMATCH: $cn"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$days_left" -gt 30 ]; then
|
||||||
|
pass "$domain — ${days_left}d until expiry, $cn_ok"
|
||||||
|
elif [ "$days_left" -gt 0 ]; then
|
||||||
|
fail "$domain — EXPIRING in ${days_left}d" "$cn_ok"
|
||||||
|
else
|
||||||
|
fail "$domain — EXPIRED" "$cn_ok"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ─── Website reachability ────────────────────────────────────
|
||||||
|
section "WEBSITE REACHABILITY"
|
||||||
|
SITES=("https://payfrit.com" "https://dev.payfrit.com" "https://biz.payfrit.com" "https://grubflip.com" "https://dev.grubflip.com")
|
||||||
|
for url in "${SITES[@]}"; do
|
||||||
|
result=$(http_get "$url")
|
||||||
|
IFS='|' read -r code body ms <<< "$result"
|
||||||
|
if [ "$code" = "200" ]; then
|
||||||
|
pass "$url — HTTP $code (${ms}ms)"
|
||||||
|
elif [ "$code" = "000" ]; then
|
||||||
|
fail "$url" "unreachable"
|
||||||
|
else
|
||||||
|
fail "$url" "HTTP $code (${ms}ms)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
test_summary
|
||||||
64
scripts/run-all.sh
Executable file
64
scripts/run-all.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================
|
||||||
|
# run-all.sh — Master test runner
|
||||||
|
# Usage: ./scripts/run-all.sh [payfrit|grubflip|all] [dev|prod] [--json]
|
||||||
|
# Author: Luna (@luna) — QA
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
REPORTS_DIR="$REPO_ROOT/reports"
|
||||||
|
|
||||||
|
PROJECT="${1:-all}"
|
||||||
|
ENV="${2:-dev}"
|
||||||
|
JSON_FLAG=""
|
||||||
|
for arg in "$@"; do [ "$arg" = "--json" ] && JSON_FLAG="--json"; done
|
||||||
|
|
||||||
|
TIMESTAMP=$(date -u '+%Y%m%d-%H%M%S')
|
||||||
|
REPORT_FILE="$REPORTS_DIR/${PROJECT}-${ENV}-${TIMESTAMP}.txt"
|
||||||
|
|
||||||
|
mkdir -p "$REPORTS_DIR"
|
||||||
|
|
||||||
|
echo "╔══════════════════════════════════════════════╗"
|
||||||
|
echo "║ PAYFRIT QA — TEST RUNNER ║"
|
||||||
|
echo "║ Project: $PROJECT | Env: $ENV "
|
||||||
|
echo "║ $(date -u '+%Y-%m-%d %H:%M:%S UTC') ║"
|
||||||
|
echo "╚══════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
EXIT_CODE=0
|
||||||
|
|
||||||
|
run_suite() {
|
||||||
|
local name="$1"
|
||||||
|
local cmd="$2"
|
||||||
|
echo "─── Running: $name ───"
|
||||||
|
if eval "$cmd"; then
|
||||||
|
echo " → $name: ALL PASSED"
|
||||||
|
else
|
||||||
|
echo " → $name: FAILURES DETECTED"
|
||||||
|
EXIT_CODE=1
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$PROJECT" = "payfrit" ] || [ "$PROJECT" = "all" ]; then
|
||||||
|
run_suite "Payfrit Tab API (bash)" "bash $REPO_ROOT/payfrit/api/test_tabs.sh $ENV $JSON_FLAG"
|
||||||
|
run_suite "Payfrit Tab API (python)" "python3 $REPO_ROOT/payfrit/api/test_tabs.py $ENV $JSON_FLAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$PROJECT" = "grubflip" ] || [ "$PROJECT" = "all" ]; then
|
||||||
|
run_suite "Grubflip API (bash)" "bash $REPO_ROOT/grubflip/api/test_endpoints.sh $ENV $JSON_FLAG"
|
||||||
|
run_suite "Grubflip API (python)" "python3 $REPO_ROOT/grubflip/api/test_endpoints.py $ENV $JSON_FLAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
if [ "$EXIT_CODE" -eq 0 ]; then
|
||||||
|
echo " ALL SUITES PASSED ✅"
|
||||||
|
else
|
||||||
|
echo " SOME SUITES HAD FAILURES ❌"
|
||||||
|
fi
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
|
||||||
|
exit $EXIT_CODE
|
||||||
0
shared/__init__.py
Normal file
0
shared/__init__.py
Normal file
0
shared/lib/__init__.py
Normal file
0
shared/lib/__init__.py
Normal file
152
shared/lib/test_helpers.py
Normal file
152
shared/lib/test_helpers.py
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
"""
|
||||||
|
test_helpers.py — Shared Python test utilities for Payfrit QA
|
||||||
|
Usage: from shared.lib.test_helpers import TestRunner
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
import ssl
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
# Skip SSL verification for dev/self-signed certs
|
||||||
|
SSL_CTX = ssl.create_default_context()
|
||||||
|
SSL_CTX.check_hostname = False
|
||||||
|
SSL_CTX.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TestResult:
|
||||||
|
name: str
|
||||||
|
status: str # pass, fail, skip
|
||||||
|
detail: str = ""
|
||||||
|
response_ms: float = 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestRunner:
|
||||||
|
"""Lightweight test runner with console + JSON output."""
|
||||||
|
|
||||||
|
def __init__(self, suite_name: str, json_mode: bool = False):
|
||||||
|
self.suite_name = suite_name
|
||||||
|
self.json_mode = json_mode
|
||||||
|
self.results: list[TestResult] = []
|
||||||
|
|
||||||
|
def pass_(self, name: str, detail: str = ""):
|
||||||
|
r = TestResult(name, "pass", detail)
|
||||||
|
self.results.append(r)
|
||||||
|
if not self.json_mode:
|
||||||
|
print(f" ✅ {name}{f' — {detail}' if detail else ''}")
|
||||||
|
|
||||||
|
def fail(self, name: str, detail: str = ""):
|
||||||
|
r = TestResult(name, "fail", detail)
|
||||||
|
self.results.append(r)
|
||||||
|
if not self.json_mode:
|
||||||
|
print(f" ❌ {name}{f' — {detail}' if detail else ''}")
|
||||||
|
|
||||||
|
def skip(self, name: str, reason: str = ""):
|
||||||
|
r = TestResult(name, "skip", reason)
|
||||||
|
self.results.append(r)
|
||||||
|
if not self.json_mode:
|
||||||
|
print(f" ⏭️ {name}{f' — {reason}' if reason else ''}")
|
||||||
|
|
||||||
|
def section(self, title: str):
|
||||||
|
if not self.json_mode:
|
||||||
|
print(f"\n━━━ {title} ━━━")
|
||||||
|
|
||||||
|
def http_get(self, url: str, timeout: int = 10) -> dict:
|
||||||
|
"""Returns dict with keys: status, body, json, time_ms, error"""
|
||||||
|
start = time.time()
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, method="GET")
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout, context=SSL_CTX) as resp:
|
||||||
|
body = resp.read().decode("utf-8", errors="replace")
|
||||||
|
elapsed = (time.time() - start) * 1000
|
||||||
|
result = {"status": resp.status, "body": body, "time_ms": elapsed, "error": None}
|
||||||
|
try:
|
||||||
|
result["json"] = json.loads(body)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
result["json"] = None
|
||||||
|
return result
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
elapsed = (time.time() - start) * 1000
|
||||||
|
body = e.read().decode("utf-8", errors="replace") if e.fp else ""
|
||||||
|
result = {"status": e.code, "body": body, "time_ms": elapsed, "error": str(e)}
|
||||||
|
try:
|
||||||
|
result["json"] = json.loads(body)
|
||||||
|
except (json.JSONDecodeError, Exception):
|
||||||
|
result["json"] = None
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
elapsed = (time.time() - start) * 1000
|
||||||
|
return {"status": 0, "body": "", "json": None, "time_ms": elapsed, "error": str(e)}
|
||||||
|
|
||||||
|
def http_post(self, url: str, data: dict = None, timeout: int = 10) -> dict:
|
||||||
|
"""POST JSON, returns same dict as http_get."""
|
||||||
|
payload = json.dumps(data or {}).encode("utf-8")
|
||||||
|
start = time.time()
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, data=payload, method="POST",
|
||||||
|
headers={"Content-Type": "application/json"})
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout, context=SSL_CTX) as resp:
|
||||||
|
body = resp.read().decode("utf-8", errors="replace")
|
||||||
|
elapsed = (time.time() - start) * 1000
|
||||||
|
result = {"status": resp.status, "body": body, "time_ms": elapsed, "error": None}
|
||||||
|
try:
|
||||||
|
result["json"] = json.loads(body)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
result["json"] = None
|
||||||
|
return result
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
elapsed = (time.time() - start) * 1000
|
||||||
|
body = e.read().decode("utf-8", errors="replace") if e.fp else ""
|
||||||
|
result = {"status": e.code, "body": body, "time_ms": elapsed, "error": str(e)}
|
||||||
|
try:
|
||||||
|
result["json"] = json.loads(body)
|
||||||
|
except (json.JSONDecodeError, Exception):
|
||||||
|
result["json"] = None
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
elapsed = (time.time() - start) * 1000
|
||||||
|
return {"status": 0, "body": "", "json": None, "time_ms": elapsed, "error": str(e)}
|
||||||
|
|
||||||
|
def summary(self) -> int:
|
||||||
|
"""Print summary, return exit code (0=all pass, 1=any fail)."""
|
||||||
|
passes = sum(1 for r in self.results if r.status == "pass")
|
||||||
|
fails = sum(1 for r in self.results if r.status == "fail")
|
||||||
|
skips = sum(1 for r in self.results if r.status == "skip")
|
||||||
|
total = len(self.results)
|
||||||
|
|
||||||
|
if self.json_mode:
|
||||||
|
out = {
|
||||||
|
"suite": self.suite_name,
|
||||||
|
"total": total, "pass": passes, "fail": fails, "skip": skips,
|
||||||
|
"results": [{"name": r.name, "status": r.status, "detail": r.detail} for r in self.results]
|
||||||
|
}
|
||||||
|
print(json.dumps(out, indent=2))
|
||||||
|
else:
|
||||||
|
print(f"\n{'━' * 42}")
|
||||||
|
print(f" TOTAL: {total} | PASS: {passes} | FAIL: {fails} | SKIP: {skips}")
|
||||||
|
print(f"{'━' * 42}")
|
||||||
|
|
||||||
|
return 0 if fails == 0 else 1
|
||||||
|
|
||||||
|
|
||||||
|
# Environment config
|
||||||
|
ENVS = {
|
||||||
|
"payfrit": {
|
||||||
|
"dev": "https://dev.payfrit.com/api",
|
||||||
|
"prod": "https://biz.payfrit.com/api",
|
||||||
|
},
|
||||||
|
"grubflip": {
|
||||||
|
"dev": "https://dev.grubflip.com/api",
|
||||||
|
"prod": "https://api.grubflip.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_base_url(project: str, env: str) -> str:
|
||||||
|
return ENVS.get(project, {}).get(env, "")
|
||||||
101
shared/lib/test_helpers.sh
Normal file
101
shared/lib/test_helpers.sh
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================
|
||||||
|
# test_helpers.sh — Shared test utilities
|
||||||
|
# Source this in any test script: source shared/lib/test_helpers.sh
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
PASS_COUNT=0
|
||||||
|
FAIL_COUNT=0
|
||||||
|
SKIP_COUNT=0
|
||||||
|
TEST_RESULTS=()
|
||||||
|
JSON_MODE=false
|
||||||
|
|
||||||
|
# Parse --json flag
|
||||||
|
for arg in "$@"; do
|
||||||
|
[ "$arg" = "--json" ] && JSON_MODE=true
|
||||||
|
done
|
||||||
|
|
||||||
|
pass() {
|
||||||
|
local msg="$1"
|
||||||
|
((PASS_COUNT++))
|
||||||
|
TEST_RESULTS+=("{\"status\":\"pass\",\"test\":\"$msg\"}")
|
||||||
|
$JSON_MODE || echo -e " ${GREEN}✅${NC} $msg"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
local msg="$1"
|
||||||
|
local detail="${2:-}"
|
||||||
|
((FAIL_COUNT++))
|
||||||
|
TEST_RESULTS+=("{\"status\":\"fail\",\"test\":\"$msg\",\"detail\":\"$detail\"}")
|
||||||
|
$JSON_MODE || echo -e " ${RED}❌${NC} $msg${detail:+ — $detail}"
|
||||||
|
}
|
||||||
|
|
||||||
|
skip() {
|
||||||
|
local msg="$1"
|
||||||
|
local reason="${2:-}"
|
||||||
|
((SKIP_COUNT++))
|
||||||
|
TEST_RESULTS+=("{\"status\":\"skip\",\"test\":\"$msg\",\"reason\":\"$reason\"}")
|
||||||
|
$JSON_MODE || echo -e " ${YELLOW}⏭️${NC} $msg${reason:+ — $reason}"
|
||||||
|
}
|
||||||
|
|
||||||
|
section() {
|
||||||
|
$JSON_MODE || echo -e "\n${CYAN}━━━ $1 ━━━${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP helper: returns "status_code|body|time_ms"
|
||||||
|
http_get() {
|
||||||
|
local url="$1"
|
||||||
|
local tmpfile=$(mktemp)
|
||||||
|
local http_code time_total body
|
||||||
|
http_code=$(curl -sk -o "$tmpfile" -w "%{http_code}" "$url" 2>/dev/null)
|
||||||
|
time_total=$(curl -sk -o /dev/null -w "%{time_total}" "$url" 2>/dev/null)
|
||||||
|
body=$(cat "$tmpfile")
|
||||||
|
rm -f "$tmpfile"
|
||||||
|
local ms=$(echo "$time_total * 1000" | bc 2>/dev/null | cut -d'.' -f1)
|
||||||
|
echo "${http_code}|${body}|${ms:-0}"
|
||||||
|
}
|
||||||
|
|
||||||
|
http_post() {
|
||||||
|
local url="$1"
|
||||||
|
local data="${2:-{}}"
|
||||||
|
local tmpfile=$(mktemp)
|
||||||
|
local http_code
|
||||||
|
http_code=$(curl -sk -X POST -o "$tmpfile" -w "%{http_code}" \
|
||||||
|
-H "Content-Type: application/json" -d "$data" "$url" 2>/dev/null)
|
||||||
|
local body=$(cat "$tmpfile")
|
||||||
|
rm -f "$tmpfile"
|
||||||
|
echo "${http_code}|${body}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# JSON field extractor (requires python3)
|
||||||
|
json_field() {
|
||||||
|
local json="$1"
|
||||||
|
local field="$2"
|
||||||
|
echo "$json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('$field',''))" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
is_valid_json() {
|
||||||
|
echo "$1" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print summary and exit with appropriate code
|
||||||
|
test_summary() {
|
||||||
|
local total=$((PASS_COUNT + FAIL_COUNT + SKIP_COUNT))
|
||||||
|
|
||||||
|
if $JSON_MODE; then
|
||||||
|
echo "{\"total\":$total,\"pass\":$PASS_COUNT,\"fail\":$FAIL_COUNT,\"skip\":$SKIP_COUNT,\"results\":[$(IFS=,; echo "${TEST_RESULTS[*]}")]}"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo -e " TOTAL: $total | ${GREEN}PASS: $PASS_COUNT${NC} | ${RED}FAIL: $FAIL_COUNT${NC} | ${YELLOW}SKIP: $SKIP_COUNT${NC}"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$FAIL_COUNT" -eq 0 ] && return 0 || return 1
|
||||||
|
}
|
||||||
45
test-data/grubflip/seed.sql
Normal file
45
test-data/grubflip/seed.sql
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
-- ============================================================
|
||||||
|
-- Grubflip test data seed
|
||||||
|
-- Run against grubflip_dev database
|
||||||
|
-- Author: Luna (@luna) — QA
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Test restaurant
|
||||||
|
INSERT IGNORE INTO restaurants (id, name, slug, address, phone, status, created_at)
|
||||||
|
VALUES
|
||||||
|
(9001, 'QA Test Restaurant', 'qa-test-restaurant', '123 Test St', '555-0100', 'active', NOW()),
|
||||||
|
(9002, 'QA Inactive Restaurant', 'qa-inactive', '456 Test Ave', '555-0200', 'inactive', NOW());
|
||||||
|
|
||||||
|
-- Test menu
|
||||||
|
INSERT IGNORE INTO menus (id, restaurant_id, name, status, created_at)
|
||||||
|
VALUES
|
||||||
|
(9001, 9001, 'QA Test Menu', 'active', NOW()),
|
||||||
|
(9002, 9001, 'QA Draft Menu', 'draft', NOW());
|
||||||
|
|
||||||
|
-- Test menu items
|
||||||
|
INSERT IGNORE INTO menu_items (id, menu_id, name, description, price, category, status, created_at)
|
||||||
|
VALUES
|
||||||
|
(9001, 9001, 'QA Burger', 'Test burger for QA', 9.99, 'Entrees', 'active', NOW()),
|
||||||
|
(9002, 9001, 'QA Fries', 'Test fries for QA', 4.99, 'Sides', 'active', NOW()),
|
||||||
|
(9003, 9001, 'QA Shake', 'Test milkshake for QA', 5.99, 'Drinks', 'active', NOW()),
|
||||||
|
(9004, 9002, 'QA Draft Item', 'Should not appear in active queries', 7.99, 'Entrees', 'draft', NOW());
|
||||||
|
|
||||||
|
-- Test users
|
||||||
|
INSERT IGNORE INTO users (id, email, name, phone, status, created_at)
|
||||||
|
VALUES
|
||||||
|
(9001, 'qa-user@test.payfrit.com', 'QA Test User', '555-0001', 'active', NOW()),
|
||||||
|
(9002, 'qa-admin@test.payfrit.com', 'QA Admin User', '555-0002', 'active', NOW());
|
||||||
|
|
||||||
|
-- Test orders
|
||||||
|
INSERT IGNORE INTO orders (id, user_id, restaurant_id, status, total, created_at)
|
||||||
|
VALUES
|
||||||
|
(9001, 9001, 9001, 'completed', 14.98, NOW()),
|
||||||
|
(9002, 9001, 9001, 'pending', 9.99, NOW()),
|
||||||
|
(9003, 9001, 9001, 'cancelled', 5.99, NOW());
|
||||||
|
|
||||||
|
-- Cleanup query (run manually when done testing):
|
||||||
|
-- DELETE FROM orders WHERE id BETWEEN 9001 AND 9099;
|
||||||
|
-- DELETE FROM menu_items WHERE id BETWEEN 9001 AND 9099;
|
||||||
|
-- DELETE FROM menus WHERE id BETWEEN 9001 AND 9099;
|
||||||
|
-- DELETE FROM restaurants WHERE id BETWEEN 9001 AND 9099;
|
||||||
|
-- DELETE FROM users WHERE id BETWEEN 9001 AND 9099;
|
||||||
72
test-data/payfrit/seed-tabs.sh
Executable file
72
test-data/payfrit/seed-tabs.sh
Executable file
|
|
@ -0,0 +1,72 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================
|
||||||
|
# seed-tabs.sh — Seed test data for tab API testing
|
||||||
|
# Creates test users and businesses via the API so tab tests
|
||||||
|
# can run with real data.
|
||||||
|
# Usage: ./test-data/payfrit/seed-tabs.sh [dev|prod]
|
||||||
|
# Author: Luna (@luna) — QA
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
ENV="${1:-dev}"
|
||||||
|
case "$ENV" in
|
||||||
|
dev) BASE="https://dev.payfrit.com/api" ;;
|
||||||
|
prod) echo "ERROR: Will not seed prod. Use dev only."; exit 1 ;;
|
||||||
|
*) echo "Usage: $0 [dev|prod]"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "============================================"
|
||||||
|
echo " TAB TEST DATA SEEDER ($ENV)"
|
||||||
|
echo " $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||||
|
echo "============================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test user/business IDs — these should already exist in dev DB
|
||||||
|
# If not, they need to be created via the user registration flow
|
||||||
|
TEST_USER_ID="QA_USER_001"
|
||||||
|
TEST_BUSINESS_ID="QA_BIZ_001"
|
||||||
|
|
||||||
|
echo "Test config:"
|
||||||
|
echo " User ID: $TEST_USER_ID"
|
||||||
|
echo " Business ID: $TEST_BUSINESS_ID"
|
||||||
|
echo " API Base: $BASE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Try to open a test tab
|
||||||
|
echo "Attempting to open a test tab..."
|
||||||
|
RESPONSE=$(curl -sk "$BASE/tabs/open.php?UserID=$TEST_USER_ID&BusinessID=$TEST_BUSINESS_ID" 2>/dev/null)
|
||||||
|
echo " Response: $RESPONSE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if we got a TabID back
|
||||||
|
TAB_ID=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('TabID',''))" 2>/dev/null)
|
||||||
|
if [ -n "$TAB_ID" ] && [ "$TAB_ID" != "None" ]; then
|
||||||
|
echo "✅ Tab created: $TAB_ID"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Try to get the tab
|
||||||
|
echo "Fetching tab..."
|
||||||
|
curl -sk "$BASE/tabs/get.php?TabID=$TAB_ID" 2>/dev/null | python3 -m json.tool 2>/dev/null
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Try to get active tabs for user
|
||||||
|
echo "Fetching active tabs for user..."
|
||||||
|
curl -sk "$BASE/tabs/getActive.php?UserID=$TEST_USER_ID" 2>/dev/null | python3 -m json.tool 2>/dev/null
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Clean up — cancel the test tab
|
||||||
|
echo "Cancelling test tab..."
|
||||||
|
curl -sk "$BASE/tabs/cancel.php?TabID=$TAB_ID" 2>/dev/null | python3 -m json.tool 2>/dev/null
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Seed complete — test tab created and cleaned up"
|
||||||
|
else
|
||||||
|
echo "⚠️ Could not create test tab. Response:"
|
||||||
|
echo "$RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE"
|
||||||
|
echo ""
|
||||||
|
echo "This may mean:"
|
||||||
|
echo " - Test user/business don't exist in the DB yet"
|
||||||
|
echo " - API requires auth that we don't have"
|
||||||
|
echo " - Endpoint logic changed"
|
||||||
|
echo ""
|
||||||
|
echo "For now, tests will use paramless validation (which works)."
|
||||||
|
fi
|
||||||
Loading…
Add table
Reference in a new issue