Initial commit
This commit is contained in:
16
scripts/init-db.sh
Executable file
16
scripts/init-db.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PG_BIN="${PG_BIN:-/opt/homebrew/opt/postgresql@17/bin}"
|
||||
|
||||
if ! "$PG_BIN/psql" -d postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname = 'queue'" | grep -q 1; then
|
||||
"$PG_BIN/psql" -d postgres -v ON_ERROR_STOP=1 -c "CREATE ROLE queue LOGIN PASSWORD 'queue'"
|
||||
fi
|
||||
|
||||
if ! "$PG_BIN/psql" -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname = 'queue'" | grep -q 1; then
|
||||
"$PG_BIN/createdb" -O queue queue
|
||||
fi
|
||||
|
||||
"$PG_BIN/psql" "postgres://queue:queue@localhost:5432/queue?sslmode=disable" -v ON_ERROR_STOP=1 -c "SELECT 1" >/dev/null
|
||||
echo "PostgreSQL development database is ready."
|
||||
|
||||
14
scripts/run-api.sh
Executable file
14
scripts/run-api.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR/server"
|
||||
|
||||
if [[ -f .env ]]; then
|
||||
set -a
|
||||
source .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
exec go run ./cmd/api
|
||||
|
||||
16
scripts/seed.sh
Executable file
16
scripts/seed.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR/server"
|
||||
|
||||
if [[ -f .env ]]; then
|
||||
set -a
|
||||
source .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
: "${ADMIN_PASSWORD:=ChangeMe123!}"
|
||||
export ADMIN_PASSWORD
|
||||
|
||||
exec go run ./cmd/seed
|
||||
61
scripts/smoke-real.sh
Executable file
61
scripts/smoke-real.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PG_BIN="${PG_BIN:-/opt/homebrew/opt/postgresql@17/bin}"
|
||||
SMOKE_PORT="${SMOKE_PORT:-18081}"
|
||||
TEST_DB="queue_phone_smoke_$$"
|
||||
API_PID=""
|
||||
API_LOG="${TMPDIR:-/tmp}/${TEST_DB}.log"
|
||||
API_BIN="${TMPDIR:-/tmp}/${TEST_DB}-api"
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "$API_PID" ]] && kill -0 "$API_PID" 2>/dev/null; then
|
||||
kill -TERM "$API_PID" 2>/dev/null || true
|
||||
wait "$API_PID" 2>/dev/null || true
|
||||
fi
|
||||
"$PG_BIN/dropdb" --if-exists --force "$TEST_DB" >/dev/null 2>&1 || true
|
||||
rm -f "$API_LOG" "$API_BIN"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
"$PG_BIN/createdb" -O queue "$TEST_DB"
|
||||
|
||||
set -a
|
||||
source "$ROOT_DIR/server/.env"
|
||||
set +a
|
||||
export APP_ENV=development
|
||||
export HTTP_ADDR=":$SMOKE_PORT"
|
||||
export DATABASE_URL="postgres://queue:queue@localhost:5432/$TEST_DB?sslmode=disable"
|
||||
export SESSION_COOKIE_NAME="queue_smoke_session_$$"
|
||||
export SESSION_COOKIE_SECURE=false
|
||||
export ADMIN_PASSWORD="${ADMIN_PASSWORD:-ChangeMe123!}"
|
||||
export ADMIN_USERNAME=smokeadmin
|
||||
export SEED_MODE=smoke
|
||||
export DEMO_DISPLAY_TOKEN="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
|
||||
|
||||
SEED_JSON="$(cd "$ROOT_DIR/server" && go run ./cmd/seed)"
|
||||
DISPLAY_TOKEN="$(printf '%s' "$SEED_JSON" | python3 -c 'import json, sys; projects=json.load(sys.stdin).get("projects", []); print(projects[0]["display_url"].rstrip("/").rsplit("/", 1)[-1])')"
|
||||
|
||||
(cd "$ROOT_DIR/server" && go build -o "$API_BIN" ./cmd/api)
|
||||
"$API_BIN" >"$API_LOG" 2>&1 &
|
||||
API_PID=$!
|
||||
|
||||
READY=0
|
||||
for _ in {1..50}; do
|
||||
if curl -fsS "http://127.0.0.1:$SMOKE_PORT/readyz" >/dev/null 2>&1; then
|
||||
READY=1
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
if [[ "$READY" != "1" ]]; then
|
||||
echo "Real smoke API did not become ready. Log follows:" >&2
|
||||
sed -n '1,160p' "$API_LOG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 "$ROOT_DIR/scripts/smoke.py" \
|
||||
--base-url "http://127.0.0.1:$SMOKE_PORT" \
|
||||
--admin-username="$ADMIN_USERNAME" \
|
||||
--display-token="$DISPLAY_TOKEN"
|
||||
439
scripts/smoke.py
Executable file
439
scripts/smoke.py
Executable file
@@ -0,0 +1,439 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the first-slice contract against the real local Go API and PostgreSQL."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import http.cookiejar
|
||||
import json
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self, base_url: str) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())
|
||||
)
|
||||
|
||||
def request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
body: dict | None = None,
|
||||
*,
|
||||
key: str | None = None,
|
||||
expected: int = 200,
|
||||
) -> dict:
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
headers = {"Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
if key:
|
||||
headers["Idempotency-Key"] = key
|
||||
request = urllib.request.Request(
|
||||
self.base_url + path, data=data, headers=headers, method=method
|
||||
)
|
||||
try:
|
||||
response = self.opener.open(request, timeout=10)
|
||||
status = response.status
|
||||
payload = json.loads(response.read() or b"{}")
|
||||
except urllib.error.HTTPError as error:
|
||||
status = error.code
|
||||
payload = json.loads(error.read() or b"{}")
|
||||
if status != expected:
|
||||
raise AssertionError(
|
||||
f"{method} {path}: expected {expected}, got {status}: {payload}"
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
def error_code(payload: dict) -> str | None:
|
||||
error = payload.get("error")
|
||||
return error.get("code") if isinstance(error, dict) else None
|
||||
|
||||
|
||||
def mapping_keys(value: object) -> set[str]:
|
||||
"""Collect every JSON object key recursively for privacy-boundary checks."""
|
||||
if isinstance(value, dict):
|
||||
keys = set(value)
|
||||
for child in value.values():
|
||||
keys.update(mapping_keys(child))
|
||||
return keys
|
||||
if isinstance(value, list):
|
||||
keys: set[str] = set()
|
||||
for child in value:
|
||||
keys.update(mapping_keys(child))
|
||||
return keys
|
||||
return set()
|
||||
|
||||
|
||||
def ticket_with_number(
|
||||
tickets: object,
|
||||
ticket_number: str,
|
||||
source: str,
|
||||
project_id: str | None = None,
|
||||
) -> dict:
|
||||
assert isinstance(tickets, list), f"{source} must be a ticket list"
|
||||
matches = [
|
||||
ticket
|
||||
for ticket in tickets
|
||||
if isinstance(ticket, dict)
|
||||
and ticket.get("ticket_number") == ticket_number
|
||||
and (project_id is None or ticket.get("project_id") == project_id)
|
||||
]
|
||||
assert len(matches) == 1, (
|
||||
f"{source} must contain exactly one ticket {ticket_number}, got {len(matches)}"
|
||||
)
|
||||
return matches[0]
|
||||
|
||||
|
||||
def assert_personal_continuity(
|
||||
ticket: object,
|
||||
*,
|
||||
phone: str,
|
||||
last_name: str,
|
||||
honorific: str,
|
||||
source: str,
|
||||
) -> dict:
|
||||
assert isinstance(ticket, dict), f"{source} must be a ticket object"
|
||||
assert ticket.get("phone") == phone, f"{source} changed the ticket phone"
|
||||
assert ticket.get("last_name") == last_name, (
|
||||
f"{source} changed the ticket last name"
|
||||
)
|
||||
assert ticket.get("honorific") == honorific, (
|
||||
f"{source} changed the ticket honorific"
|
||||
)
|
||||
return ticket
|
||||
|
||||
|
||||
def assert_public_phone_projection(payload: dict, phone: str, source: str) -> None:
|
||||
assert payload.get("phone_last4") == phone[-4:]
|
||||
phone_keys = {
|
||||
key for key in mapping_keys(payload) if "phone" in key.lower()
|
||||
}
|
||||
assert phone_keys == {"phone_last4"}, (
|
||||
f"{source} may expose only phone_last4, got {phone_keys}"
|
||||
)
|
||||
assert phone not in json.dumps(payload, ensure_ascii=False), (
|
||||
f"{source} leaked the complete phone"
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--base-url", default="http://127.0.0.1:8080")
|
||||
parser.add_argument("--username", default="staff")
|
||||
parser.add_argument("--admin-username", default="admin")
|
||||
parser.add_argument("--password", default="ChangeMe123!")
|
||||
parser.add_argument("--display-token", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
client = Client(args.base_url)
|
||||
health = client.request("GET", "/healthz")
|
||||
assert health["status"] == "ok"
|
||||
|
||||
client.request(
|
||||
"POST",
|
||||
"/api/staff/auth/login",
|
||||
{"username": args.username, "password": args.password},
|
||||
)
|
||||
me = client.request("GET", "/api/staff/auth/me")
|
||||
project_id = me["projects"][0]["id"]
|
||||
|
||||
snapshot = client.request("GET", f"/api/staff/projects/{project_id}/queue")
|
||||
assert snapshot.get("current_batch") is None, (
|
||||
"smoke requires a project with no active call batch; finish or miss the "
|
||||
"current batch before running it"
|
||||
)
|
||||
assert snapshot.get("waiting") == [], (
|
||||
"smoke requires a freshly rebuilt, dedicated database with no waiting "
|
||||
"tickets; rebuild and seed the smoke database before running it"
|
||||
)
|
||||
phone = "139" + str(int(time.time() * 1000))[-8:]
|
||||
last_name = "烟测"
|
||||
honorific = "游客"
|
||||
ticket_body = {
|
||||
"phone": phone,
|
||||
"last_name": last_name,
|
||||
"honorific": honorific,
|
||||
"allow_duplicate": False,
|
||||
}
|
||||
first_key = str(uuid.uuid4())
|
||||
first = client.request(
|
||||
"POST",
|
||||
f"/api/staff/projects/{project_id}/tickets",
|
||||
ticket_body,
|
||||
key=first_key,
|
||||
expected=201,
|
||||
)
|
||||
first_ticket = first["ticket"]
|
||||
assert first_ticket["ticket_number"]
|
||||
assert first_ticket["public_token"]
|
||||
assert_personal_continuity(
|
||||
first_ticket,
|
||||
phone=phone,
|
||||
last_name=last_name,
|
||||
honorific=honorific,
|
||||
source="create response",
|
||||
)
|
||||
|
||||
replay = client.request(
|
||||
"POST",
|
||||
f"/api/staff/projects/{project_id}/tickets",
|
||||
ticket_body,
|
||||
key=first_key,
|
||||
expected=201,
|
||||
)
|
||||
assert replay["ticket"]["id"] == first_ticket["id"]
|
||||
assert_personal_continuity(
|
||||
replay["ticket"],
|
||||
phone=phone,
|
||||
last_name=last_name,
|
||||
honorific=honorific,
|
||||
source="create idempotency replay",
|
||||
)
|
||||
|
||||
# Staff and admin are authenticated operational surfaces, so they must
|
||||
# expose the complete phone beside the exact ticket number.
|
||||
staff_snapshot = client.request(
|
||||
"GET", f"/api/staff/projects/{project_id}/queue"
|
||||
)
|
||||
staff_ticket = ticket_with_number(
|
||||
staff_snapshot["waiting"], first_ticket["ticket_number"], "staff queue"
|
||||
)
|
||||
assert_personal_continuity(
|
||||
staff_ticket,
|
||||
phone=phone,
|
||||
last_name=last_name,
|
||||
honorific=honorific,
|
||||
source="staff waiting queue",
|
||||
)
|
||||
|
||||
public = client.request(
|
||||
"GET", f"/api/public/status/{first_ticket['public_token']}"
|
||||
)
|
||||
assert public["ticket_number"] == first_ticket["ticket_number"]
|
||||
assert_public_phone_projection(public, phone, "public status")
|
||||
|
||||
client.request(
|
||||
"POST",
|
||||
"/api/admin/auth/login",
|
||||
{"username": args.admin_username, "password": args.password},
|
||||
)
|
||||
overview = client.request("GET", "/api/admin/overview")
|
||||
admin_ticket = ticket_with_number(
|
||||
overview.get("active_tickets"),
|
||||
first_ticket["ticket_number"],
|
||||
"admin active_tickets",
|
||||
project_id,
|
||||
)
|
||||
assert_personal_continuity(
|
||||
admin_ticket,
|
||||
phone=phone,
|
||||
last_name=last_name,
|
||||
honorific=honorific,
|
||||
source="admin active_tickets",
|
||||
)
|
||||
|
||||
duplicate = client.request(
|
||||
"POST",
|
||||
f"/api/staff/projects/{project_id}/tickets",
|
||||
ticket_body,
|
||||
key=str(uuid.uuid4()),
|
||||
expected=409,
|
||||
)
|
||||
assert error_code(duplicate) == "DUPLICATE_PHONE"
|
||||
|
||||
confirmed_body = {**ticket_body, "allow_duplicate": True}
|
||||
confirmed = client.request(
|
||||
"POST",
|
||||
f"/api/staff/projects/{project_id}/tickets",
|
||||
confirmed_body,
|
||||
key=str(uuid.uuid4()),
|
||||
expected=201,
|
||||
)
|
||||
confirmed_ticket = confirmed["ticket"]
|
||||
assert confirmed_ticket["id"] != first_ticket["id"]
|
||||
assert_personal_continuity(
|
||||
confirmed_ticket,
|
||||
phone=phone,
|
||||
last_name=last_name,
|
||||
honorific=honorific,
|
||||
source="confirmed duplicate create response",
|
||||
)
|
||||
|
||||
snapshot = client.request("GET", f"/api/staff/projects/{project_id}/queue")
|
||||
project = snapshot["project"]
|
||||
batch_size = int(
|
||||
project.get("call_batch_size") or project.get("batch_size") or 0
|
||||
)
|
||||
assert batch_size > 0, "project call batch size must be positive"
|
||||
created_tickets = [first_ticket, confirmed_ticket]
|
||||
while len(created_tickets) < batch_size + 1:
|
||||
extra = client.request(
|
||||
"POST",
|
||||
f"/api/staff/projects/{project_id}/tickets",
|
||||
confirmed_body,
|
||||
key=str(uuid.uuid4()),
|
||||
expected=201,
|
||||
)
|
||||
created_tickets.append(extra["ticket"])
|
||||
snapshot = client.request("GET", f"/api/staff/projects/{project_id}/queue")
|
||||
waiting_before_call = snapshot["waiting"]
|
||||
waiting_ids = {ticket["id"] for ticket in waiting_before_call}
|
||||
assert waiting_ids == {ticket["id"] for ticket in created_tickets}, (
|
||||
"dedicated smoke queue contains unexpected tickets"
|
||||
)
|
||||
for ticket in waiting_before_call:
|
||||
assert_personal_continuity(
|
||||
ticket,
|
||||
phone=phone,
|
||||
last_name=last_name,
|
||||
honorific=honorific,
|
||||
source="staff waiting queue before call-next",
|
||||
)
|
||||
stale_call = client.request(
|
||||
"POST",
|
||||
f"/api/staff/projects/{project_id}/call-next",
|
||||
{"expected_revision": max(0, int(snapshot["revision"]) - 1), "count": batch_size},
|
||||
key=str(uuid.uuid4()),
|
||||
expected=409,
|
||||
)
|
||||
assert error_code(stale_call) == "REVISION_CONFLICT"
|
||||
|
||||
expected_called_ids = [
|
||||
ticket["id"] for ticket in waiting_before_call[:batch_size]
|
||||
]
|
||||
call_key = str(uuid.uuid4())
|
||||
call_body = {"expected_revision": int(snapshot["revision"]), "count": batch_size}
|
||||
called = client.request(
|
||||
"POST",
|
||||
f"/api/staff/projects/{project_id}/call-next",
|
||||
call_body,
|
||||
key=call_key,
|
||||
)
|
||||
called_tickets = called["batch"]["tickets"]
|
||||
assert [ticket["id"] for ticket in called_tickets] == expected_called_ids
|
||||
assert len(called_tickets) == min(batch_size, len(waiting_before_call))
|
||||
assert called_tickets, "call-next must call at least one waiting ticket"
|
||||
for ticket in called_tickets:
|
||||
assert_personal_continuity(
|
||||
ticket,
|
||||
phone=phone,
|
||||
last_name=last_name,
|
||||
honorific=honorific,
|
||||
source="call-next response",
|
||||
)
|
||||
assert called["device_results"][0]["status"] == "SUCCESS"
|
||||
|
||||
call_replay = client.request(
|
||||
"POST",
|
||||
f"/api/staff/projects/{project_id}/call-next",
|
||||
call_body,
|
||||
key=call_key,
|
||||
)
|
||||
assert call_replay["batch"]["id"] == called["batch"]["id"]
|
||||
for ticket in call_replay["batch"]["tickets"]:
|
||||
assert_personal_continuity(
|
||||
ticket,
|
||||
phone=phone,
|
||||
last_name=last_name,
|
||||
honorific=honorific,
|
||||
source="call-next idempotency replay",
|
||||
)
|
||||
|
||||
active_snapshot = client.request(
|
||||
"GET", f"/api/staff/projects/{project_id}/queue"
|
||||
)
|
||||
current_batch = active_snapshot.get("current_batch")
|
||||
assert isinstance(current_batch, dict)
|
||||
assert current_batch["id"] == called["batch"]["id"]
|
||||
assert [ticket["id"] for ticket in current_batch["tickets"]] == (
|
||||
expected_called_ids
|
||||
)
|
||||
for ticket in current_batch["tickets"]:
|
||||
assert_personal_continuity(
|
||||
ticket,
|
||||
phone=phone,
|
||||
last_name=last_name,
|
||||
honorific=honorific,
|
||||
source="staff current_batch",
|
||||
)
|
||||
|
||||
second_called = client.request(
|
||||
"POST",
|
||||
f"/api/staff/projects/{project_id}/call-next",
|
||||
{"expected_revision": int(called["revision"]), "count": 1},
|
||||
key=str(uuid.uuid4()),
|
||||
)
|
||||
assert second_called["batch"]["id"] != called["batch"]["id"]
|
||||
assert len(second_called["batch"]["tickets"]) == 1
|
||||
final_staff_snapshot = client.request(
|
||||
"GET", f"/api/staff/projects/{project_id}/queue"
|
||||
)
|
||||
assert final_staff_snapshot["current_batch"]["id"] == second_called["batch"]["id"]
|
||||
assert final_staff_snapshot["metrics"]["waiting_count"] == 0
|
||||
completed_public = client.request(
|
||||
"GET", f"/api/public/status/{first_ticket['public_token']}"
|
||||
)
|
||||
assert completed_public["status"] == "COMPLETED"
|
||||
assert_public_phone_projection(completed_public, phone, "auto-completed public status")
|
||||
display = client.request(
|
||||
"GET", f"/api/display/{args.display_token}/snapshot"
|
||||
)
|
||||
serialized_display = json.dumps(display, ensure_ascii=False)
|
||||
display_keys = mapping_keys(display)
|
||||
assert "honorific" not in display_keys
|
||||
display_phone_keys = {
|
||||
key for key in display_keys if "phone" in key.lower()
|
||||
}
|
||||
forbidden_display_phone_keys = {"phone", "phone_last4", "phone_masked"}
|
||||
assert display_phone_keys.isdisjoint(forbidden_display_phone_keys)
|
||||
assert not display_phone_keys, (
|
||||
f"display leaked phone fields: {sorted(display_phone_keys)}"
|
||||
)
|
||||
assert phone not in serialized_display
|
||||
current_numbers = {
|
||||
ticket["ticket_number"]
|
||||
for ticket in (display.get("current_batch") or {}).get("tickets", [])
|
||||
}
|
||||
assert first_ticket["ticket_number"] not in current_numbers
|
||||
|
||||
overview = client.request("GET", "/api/admin/overview")
|
||||
project = next(item for item in overview["projects"] if item["id"] == project_id)
|
||||
assert project["waiting_count"] == 0
|
||||
current_admin_ticket = ticket_with_number(
|
||||
overview.get("active_tickets"),
|
||||
second_called["batch"]["tickets"][0]["ticket_number"],
|
||||
"admin active_tickets after automatic rotation",
|
||||
project_id,
|
||||
)
|
||||
assert_personal_continuity(
|
||||
current_admin_ticket,
|
||||
phone=phone,
|
||||
last_name=last_name,
|
||||
honorific=honorific,
|
||||
source="admin active_tickets after automatic rotation",
|
||||
)
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "ok",
|
||||
"project_id": project_id,
|
||||
"first_ticket": first_ticket["ticket_number"],
|
||||
"first_batch_id": called["batch"]["id"],
|
||||
"second_batch_id": second_called["batch"]["id"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
27
scripts/test-postgres.sh
Executable file
27
scripts/test-postgres.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PG_BIN="${PG_BIN:-/opt/homebrew/opt/postgresql@17/bin}"
|
||||
TEST_DB="queue_schema_test_$$"
|
||||
|
||||
cleanup() {
|
||||
"$PG_BIN/dropdb" --if-exists --force "$TEST_DB" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
"$PG_BIN/createdb" -O queue "$TEST_DB"
|
||||
(
|
||||
export TEST_DATABASE_URL="postgres://queue:queue@localhost:5432/$TEST_DB?sslmode=disable"
|
||||
cd "$ROOT_DIR/server"
|
||||
go test ./internal/database -run TestPostgresMigrationAndMaintenanceIntegration -count=1
|
||||
)
|
||||
|
||||
set -a
|
||||
source "$ROOT_DIR/server/.env"
|
||||
set +a
|
||||
export APP_ENV=development
|
||||
export DATABASE_URL="postgres://queue:queue@localhost:5432/$TEST_DB?sslmode=disable"
|
||||
export MIGRATE_ON_START=false
|
||||
export SUPER_ADMIN_PASSWORD='IntegrationOnlyPassword123!'
|
||||
(cd "$ROOT_DIR/server" && go run ./cmd/bootstrap-admin >/dev/null)
|
||||
Reference in New Issue
Block a user