fix(ci): harden community image smoke
This commit is contained in:
+30
-3
@@ -286,17 +286,44 @@ jobs:
|
|||||||
CRANK_SESSION_SECRET=ci-session-secret
|
CRANK_SESSION_SECRET=ci-session-secret
|
||||||
CRANK_PASSWORD_PEPPER=ci-password-pepper
|
CRANK_PASSWORD_PEPPER=ci-password-pepper
|
||||||
CRANK_BOOTSTRAP_ADMIN_EMAIL=owner@crank.test
|
CRANK_BOOTSTRAP_ADMIN_EMAIL=owner@crank.test
|
||||||
CRANK_BOOTSTRAP_ADMIN_PASSWORD=ci-admin-password
|
|
||||||
CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME=CI Owner
|
CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME=CI Owner
|
||||||
CRANK_BASE_URL=http://127.0.0.1
|
CRANK_BASE_URL=http://127.0.0.1
|
||||||
|
CRANK_ENVIRONMENT=ci
|
||||||
|
CRANK_OUTBOUND_ALLOWED_HOSTS=admin-api
|
||||||
CRANK_PUBLISH_BIND=127.0.0.1
|
CRANK_PUBLISH_BIND=127.0.0.1
|
||||||
CRANK_ADMIN_PUBLISH_PORT=0
|
CRANK_ADMIN_PUBLISH_PORT=0
|
||||||
CRANK_MCP_PUBLISH_PORT=0
|
CRANK_MCP_PUBLISH_PORT=0
|
||||||
CRANK_UI_PUBLISH_PORT=0
|
CRANK_UI_PUBLISH_PORT=0
|
||||||
CRANK_DEMO_SEED=true
|
CRANK_DEMO_SEED=true
|
||||||
EOF
|
EOF
|
||||||
docker compose -f deploy/community/docker-compose.images.yml \
|
compose=(docker compose -f deploy/community/docker-compose.images.yml \
|
||||||
--env-file .tmp/community-smoke.env --profile local-db up -d --wait
|
--env-file .tmp/community-smoke.env --profile local-db)
|
||||||
|
"${compose[@]}" up -d --wait postgres
|
||||||
|
"${compose[@]}" run --rm migrate
|
||||||
|
bootstrap_json="$("${compose[@]}" run --rm --no-deps \
|
||||||
|
--entrypoint crank-migrate migrate admin-auth bootstrap-create \
|
||||||
|
--email owner@crank.test --display-name 'CI Owner')"
|
||||||
|
bootstrap_token="$(python3 -c \
|
||||||
|
'import json,sys; print(json.loads(sys.stdin.read())["bootstrap_token"])' \
|
||||||
|
<<<"$bootstrap_json")"
|
||||||
|
install -d -m 700 .tmp/community-bootstrap
|
||||||
|
printf '%s' "$bootstrap_token" > .tmp/community-bootstrap/token
|
||||||
|
printf '%s' 'ci-admin-password' > .tmp/community-bootstrap/password
|
||||||
|
printf '%s' 'ci-password-pepper' > .tmp/community-bootstrap/password-pepper
|
||||||
|
chmod 600 .tmp/community-bootstrap/token \
|
||||||
|
.tmp/community-bootstrap/password \
|
||||||
|
.tmp/community-bootstrap/password-pepper
|
||||||
|
"${compose[@]}" run --rm --no-deps \
|
||||||
|
-v "$PWD/.tmp/community-bootstrap:/run/bootstrap:ro" \
|
||||||
|
--entrypoint crank-migrate migrate admin-auth bootstrap-complete \
|
||||||
|
--token-file /run/bootstrap/token \
|
||||||
|
--password-file /run/bootstrap/password \
|
||||||
|
--password-pepper-file /run/bootstrap/password-pepper
|
||||||
|
rm -f .tmp/community-bootstrap/token \
|
||||||
|
.tmp/community-bootstrap/password \
|
||||||
|
.tmp/community-bootstrap/password-pepper
|
||||||
|
rmdir .tmp/community-bootstrap
|
||||||
|
"${compose[@]}" up -d --wait
|
||||||
|
|
||||||
- name: Run authenticated Community image smoke
|
- name: Run authenticated Community image smoke
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -217,8 +217,8 @@ jobs:
|
|||||||
CRANK_SESSION_SECRET=release-smoke-session
|
CRANK_SESSION_SECRET=release-smoke-session
|
||||||
CRANK_PASSWORD_PEPPER=release-smoke-pepper
|
CRANK_PASSWORD_PEPPER=release-smoke-pepper
|
||||||
CRANK_BOOTSTRAP_ADMIN_EMAIL=owner@crank.test
|
CRANK_BOOTSTRAP_ADMIN_EMAIL=owner@crank.test
|
||||||
CRANK_BOOTSTRAP_ADMIN_PASSWORD=release-smoke-password
|
|
||||||
CRANK_BASE_URL=http://127.0.0.1
|
CRANK_BASE_URL=http://127.0.0.1
|
||||||
|
CRANK_ENVIRONMENT=release-smoke
|
||||||
CRANK_PUBLISH_BIND=127.0.0.1
|
CRANK_PUBLISH_BIND=127.0.0.1
|
||||||
CRANK_ADMIN_PUBLISH_PORT=0
|
CRANK_ADMIN_PUBLISH_PORT=0
|
||||||
CRANK_MCP_PUBLISH_PORT=0
|
CRANK_MCP_PUBLISH_PORT=0
|
||||||
|
|||||||
@@ -18,21 +18,15 @@ FROM debian:bookworm-slim
|
|||||||
|
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||||
|
|
||||||
RUN test -s /etc/ssl/certs/ca-certificates.crt \
|
|
||||||
&& sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list.d/debian.sources \
|
|
||||||
&& grep -q '^URIs: https://deb.debian.org/' /etc/apt/sources.list.d/debian.sources \
|
|
||||||
&& ! grep -Eq '^[[:space:]]*URIs:[[:space:]]+http://' /etc/apt/sources.list.d/debian.sources \
|
|
||||||
&& apt-get -o Acquire::Retries=3 update \
|
|
||||||
&& apt-get -o Acquire::Retries=3 install -y --no-install-recommends ca-certificates curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /tmp/admin-api /usr/local/bin/admin-api
|
COPY --from=builder /tmp/admin-api /usr/local/bin/admin-api
|
||||||
COPY --from=builder /tmp/crank-migrate /usr/local/bin/crank-migrate
|
COPY --from=builder /tmp/crank-migrate /usr/local/bin/crank-migrate
|
||||||
COPY apps/admin-api/docker-entrypoint.sh /usr/local/bin/crank-admin-entrypoint
|
COPY apps/admin-api/docker-entrypoint.sh /usr/local/bin/crank-admin-entrypoint
|
||||||
|
COPY scripts/docker-http-healthcheck.sh /usr/local/bin/crank-http-healthcheck
|
||||||
|
|
||||||
RUN chmod 0755 /usr/local/bin/crank-admin-entrypoint
|
RUN test -s /etc/ssl/certs/ca-certificates.crt \
|
||||||
|
&& chmod 0755 /usr/local/bin/crank-admin-entrypoint /usr/local/bin/crank-http-healthcheck
|
||||||
|
|
||||||
ENV CRANK_ADMIN_BIND=0.0.0.0:3001
|
ENV CRANK_ADMIN_BIND=0.0.0.0:3001
|
||||||
ENV CRANK_STORAGE_ROOT=/var/lib/crank/storage
|
ENV CRANK_STORAGE_ROOT=/var/lib/crank/storage
|
||||||
|
|||||||
@@ -55,17 +55,13 @@ FROM debian:bookworm-slim
|
|||||||
|
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||||
|
|
||||||
RUN test -s /etc/ssl/certs/ca-certificates.crt \
|
|
||||||
&& sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list.d/debian.sources \
|
|
||||||
&& grep -q '^URIs: https://deb.debian.org/' /etc/apt/sources.list.d/debian.sources \
|
|
||||||
&& ! grep -Eq '^[[:space:]]*URIs:[[:space:]]+http://' /etc/apt/sources.list.d/debian.sources \
|
|
||||||
&& apt-get -o Acquire::Retries=3 update \
|
|
||||||
&& apt-get -o Acquire::Retries=3 install -y --no-install-recommends ca-certificates curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /tmp/mcp-server /usr/local/bin/mcp-server
|
COPY --from=builder /tmp/mcp-server /usr/local/bin/mcp-server
|
||||||
|
COPY scripts/docker-http-healthcheck.sh /usr/local/bin/crank-http-healthcheck
|
||||||
|
|
||||||
|
RUN test -s /etc/ssl/certs/ca-certificates.crt \
|
||||||
|
&& chmod 0755 /usr/local/bin/crank-http-healthcheck
|
||||||
|
|
||||||
ENV CRANK_MCP_BIND=0.0.0.0:3002
|
ENV CRANK_MCP_BIND=0.0.0.0:3002
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${CRANK_PUBLISH_BIND:-127.0.0.1}:${CRANK_ADMIN_PUBLISH_PORT:-3001}:3001"
|
- "${CRANK_PUBLISH_BIND:-127.0.0.1}:${CRANK_ADMIN_PUBLISH_PORT:-3001}:3001"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "--fail", "http://127.0.0.1:3001/ready"]
|
test: ["CMD", "/usr/local/bin/crank-http-healthcheck", "3001", "/ready"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -199,7 +199,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${CRANK_PUBLISH_BIND:-127.0.0.1}:${CRANK_MCP_PUBLISH_PORT:-3002}:3002"
|
- "${CRANK_PUBLISH_BIND:-127.0.0.1}:${CRANK_MCP_PUBLISH_PORT:-3002}:3002"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "--fail", "http://127.0.0.1:3002/ready"]
|
test: ["CMD", "/usr/local/bin/crank-http-healthcheck", "3002", "/ready"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${CRANK_PUBLISH_BIND:-127.0.0.1}:${CRANK_ADMIN_PUBLISH_PORT:-3001}:3001"
|
- "${CRANK_PUBLISH_BIND:-127.0.0.1}:${CRANK_ADMIN_PUBLISH_PORT:-3001}:3001"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "--fail", "http://127.0.0.1:3001/ready"]
|
test: ["CMD", "/usr/local/bin/crank-http-healthcheck", "3001", "/ready"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -202,7 +202,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${CRANK_PUBLISH_BIND:-127.0.0.1}:${CRANK_MCP_PUBLISH_PORT:-3002}:3002"
|
- "${CRANK_PUBLISH_BIND:-127.0.0.1}:${CRANK_MCP_PUBLISH_PORT:-3002}:3002"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "--fail", "http://127.0.0.1:3002/ready"]
|
test: ["CMD", "/usr/local/bin/crank-http-healthcheck", "3002", "/ready"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|||||||
+2
-2
@@ -127,7 +127,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${CRANK_PUBLISH_BIND:-127.0.0.1}:3001:3001"
|
- "${CRANK_PUBLISH_BIND:-127.0.0.1}:3001:3001"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "--fail", "http://127.0.0.1:3001/ready"]
|
test: ["CMD", "/usr/local/bin/crank-http-healthcheck", "3001", "/ready"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -190,7 +190,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${CRANK_PUBLISH_BIND:-127.0.0.1}:3002:3002"
|
- "${CRANK_PUBLISH_BIND:-127.0.0.1}:3002:3002"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "--fail", "http://127.0.0.1:3002/health"]
|
test: ["CMD", "/usr/local/bin/crank-http-healthcheck", "3002", "/health"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ class Client:
|
|||||||
def __init__(self, base_url: str, timeout_seconds: int) -> None:
|
def __init__(self, base_url: str, timeout_seconds: int) -> None:
|
||||||
self.base_url = base_url.rstrip("/")
|
self.base_url = base_url.rstrip("/")
|
||||||
self.timeout_seconds = timeout_seconds
|
self.timeout_seconds = timeout_seconds
|
||||||
|
self.csrf_token: str | None = None
|
||||||
cookie_jar = http.cookiejar.CookieJar()
|
cookie_jar = http.cookiejar.CookieJar()
|
||||||
self.opener = urllib.request.build_opener(
|
self.opener = urllib.request.build_opener(
|
||||||
urllib.request.HTTPCookieProcessor(cookie_jar)
|
urllib.request.HTTPCookieProcessor(cookie_jar)
|
||||||
@@ -156,6 +157,21 @@ class Client:
|
|||||||
request_headers = {"Accept": "application/json"}
|
request_headers = {"Accept": "application/json"}
|
||||||
if headers:
|
if headers:
|
||||||
request_headers.update(headers)
|
request_headers.update(headers)
|
||||||
|
if (
|
||||||
|
self.csrf_token
|
||||||
|
and method not in ("GET", "HEAD", "OPTIONS")
|
||||||
|
and (
|
||||||
|
path_or_url.startswith("/api/admin/")
|
||||||
|
or path_or_url.startswith("/api/auth/")
|
||||||
|
)
|
||||||
|
and path_or_url
|
||||||
|
not in (
|
||||||
|
"/api/auth/login",
|
||||||
|
"/api/auth/bootstrap/complete",
|
||||||
|
"/api/auth/session/csrf",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
request_headers.setdefault("x-csrf-token", self.csrf_token)
|
||||||
if payload is not None:
|
if payload is not None:
|
||||||
data = json.dumps(payload).encode("utf-8")
|
data = json.dumps(payload).encode("utf-8")
|
||||||
request_headers["Content-Type"] = "application/json"
|
request_headers["Content-Type"] = "application/json"
|
||||||
@@ -203,12 +219,21 @@ def admin_path(workspace_id: str, suffix: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def login(client: Client, email: str, password: str) -> None:
|
def login(client: Client, email: str, password: str) -> None:
|
||||||
client.request_json(
|
response = client.request_json(
|
||||||
"POST",
|
"POST",
|
||||||
"/api/auth/login",
|
"/api/auth/login",
|
||||||
{"email": email, "password": password},
|
{"email": email, "password": password},
|
||||||
expected=(200,),
|
expected=(200,),
|
||||||
)
|
)
|
||||||
|
session = require_object(response.body, "login")
|
||||||
|
csrf_token = session.get("csrf_token")
|
||||||
|
if (
|
||||||
|
not isinstance(csrf_token, str)
|
||||||
|
or not 32 <= len(csrf_token) <= 256
|
||||||
|
or not all(character.isalnum() or character in "-_." for character in csrf_token)
|
||||||
|
):
|
||||||
|
raise safe_error("login", "invalid_csrf_token")
|
||||||
|
client.csrf_token = csrf_token
|
||||||
|
|
||||||
|
|
||||||
def resolve_workspace(
|
def resolve_workspace(
|
||||||
@@ -326,6 +351,17 @@ def create_agent(client: Client, workspace_id: str, agent_slug: str) -> tuple[st
|
|||||||
raise safe_error("agent_create", "invalid_response") from error
|
raise safe_error("agent_create", "invalid_response") from error
|
||||||
|
|
||||||
|
|
||||||
|
def agent_etag(client: Client, workspace_id: str, agent_id: str) -> str:
|
||||||
|
response = client.request_json(
|
||||||
|
"GET",
|
||||||
|
admin_path(workspace_id, f"/agents/{agent_id}"),
|
||||||
|
)
|
||||||
|
etag = response.headers.get("ETag") if response.headers is not None else None
|
||||||
|
if not isinstance(etag, str) or len(etag) > 128 or not etag.startswith('"') or not etag.endswith('"'):
|
||||||
|
raise safe_error("agent_precondition", "invalid_response")
|
||||||
|
return etag
|
||||||
|
|
||||||
|
|
||||||
def edit_and_archive_operation(
|
def edit_and_archive_operation(
|
||||||
client: Client,
|
client: Client,
|
||||||
workspace_id: str,
|
workspace_id: str,
|
||||||
@@ -377,11 +413,13 @@ def bind_and_publish_agent(
|
|||||||
"enabled": True,
|
"enabled": True,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
headers={"If-Match": agent_etag(client, workspace_id, agent_id)},
|
||||||
)
|
)
|
||||||
published = client.request_json(
|
published = client.request_json(
|
||||||
"POST",
|
"POST",
|
||||||
admin_path(workspace_id, f"/agents/{agent_id}/publish"),
|
admin_path(workspace_id, f"/agents/{agent_id}/publish"),
|
||||||
{"version": agent_version},
|
{"version": agent_version},
|
||||||
|
headers={"If-Match": agent_etag(client, workspace_id, agent_id)},
|
||||||
).body
|
).body
|
||||||
try:
|
try:
|
||||||
published_version = int(published["published_version"])
|
published_version = int(published["published_version"])
|
||||||
@@ -436,8 +474,15 @@ def cleanup_smoke_assets(
|
|||||||
if agent_id:
|
if agent_id:
|
||||||
try:
|
try:
|
||||||
client.request_json(
|
client.request_json(
|
||||||
"DELETE",
|
"POST",
|
||||||
admin_path(workspace_id, f"/agents/{agent_id}"),
|
admin_path(workspace_id, f"/agents/{agent_id}/unpublish"),
|
||||||
|
headers={"If-Match": agent_etag(client, workspace_id, agent_id)},
|
||||||
|
expected=(200, 404),
|
||||||
|
)
|
||||||
|
client.request_json(
|
||||||
|
"POST",
|
||||||
|
admin_path(workspace_id, f"/agents/{agent_id}/archive"),
|
||||||
|
headers={"If-Match": agent_etag(client, workspace_id, agent_id)},
|
||||||
expected=(200, 404),
|
expected=(200, 404),
|
||||||
)
|
)
|
||||||
except SmokeError as error:
|
except SmokeError as error:
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
port="${1:?port is required}"
|
||||||
|
path="${2:?path is required}"
|
||||||
|
|
||||||
|
if [[ ! "$port" =~ ^[0-9]{1,5}$ ]] || (( port < 1 || port > 65535 )); then
|
||||||
|
exit 64
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$path" != /* || "$path" == *$'\r'* || "$path" == *$'\n'* ]]; then
|
||||||
|
exit 64
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bash provides /dev/tcp without adding a network client package to the runtime
|
||||||
|
# image. Compose still applies its own five-second timeout to the whole probe.
|
||||||
|
exec 3<>"/dev/tcp/127.0.0.1/${port}"
|
||||||
|
printf 'GET %s HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n' "$path" >&3
|
||||||
|
|
||||||
|
IFS=$'\r' read -r -t 2 status <&3
|
||||||
|
case "$status" in
|
||||||
|
'HTTP/1.0 200 '*|'HTTP/1.1 200 '*) ;;
|
||||||
|
*) exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -17,6 +17,48 @@ def load_smoke_module():
|
|||||||
|
|
||||||
|
|
||||||
class AuthenticatedProductSmokeTests(unittest.TestCase):
|
class AuthenticatedProductSmokeTests(unittest.TestCase):
|
||||||
|
def test_client_attaches_csrf_only_to_browser_api_mutations(self) -> None:
|
||||||
|
smoke = load_smoke_module()
|
||||||
|
|
||||||
|
class Response:
|
||||||
|
status = 200
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
def read(self, _limit):
|
||||||
|
return b"{}"
|
||||||
|
|
||||||
|
class Opener:
|
||||||
|
def __init__(self):
|
||||||
|
self.requests = []
|
||||||
|
|
||||||
|
def open(self, request, timeout):
|
||||||
|
self.requests.append((request, timeout))
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
client = smoke.Client("http://crank.test", 5)
|
||||||
|
opener = Opener()
|
||||||
|
client.opener = opener
|
||||||
|
client.csrf_token = "a" * 32
|
||||||
|
|
||||||
|
client.request_json("POST", "/api/admin/workspaces/ws/operations", {})
|
||||||
|
client.request_json("POST", "http://mcp.test/v1/ws/agent", {})
|
||||||
|
|
||||||
|
self.assertEqual(opener.requests[0][0].get_header("X-csrf-token"), "a" * 32)
|
||||||
|
self.assertIsNone(opener.requests[1][0].get_header("X-csrf-token"))
|
||||||
|
|
||||||
|
def test_login_keeps_server_issued_csrf_token(self) -> None:
|
||||||
|
smoke = load_smoke_module()
|
||||||
|
|
||||||
|
class FakeClient:
|
||||||
|
csrf_token = None
|
||||||
|
|
||||||
|
def request_json(self, *args, **kwargs):
|
||||||
|
return smoke.JsonResponse(200, {}, {"csrf_token": "b" * 32})
|
||||||
|
|
||||||
|
client = FakeClient()
|
||||||
|
smoke.login(client, "owner@crank.test", "safe-password")
|
||||||
|
self.assertEqual(client.csrf_token, "b" * 32)
|
||||||
|
|
||||||
def test_operation_payload_uses_internal_upstream(self) -> None:
|
def test_operation_payload_uses_internal_upstream(self) -> None:
|
||||||
smoke = load_smoke_module()
|
smoke = load_smoke_module()
|
||||||
|
|
||||||
@@ -136,6 +178,8 @@ class AuthenticatedProductSmokeTests(unittest.TestCase):
|
|||||||
return smoke.JsonResponse(200, {}, {"operation_id": "op_safe", "version": 7})
|
return smoke.JsonResponse(200, {}, {"operation_id": "op_safe", "version": 7})
|
||||||
if path.endswith("/operations/op_safe"):
|
if path.endswith("/operations/op_safe"):
|
||||||
return smoke.JsonResponse(200, {"ETag": '"safe-etag"'}, {"id": "op_safe"})
|
return smoke.JsonResponse(200, {"ETag": '"safe-etag"'}, {"id": "op_safe"})
|
||||||
|
if path.endswith("/agents/agent_safe"):
|
||||||
|
return smoke.JsonResponse(200, {"ETag": '"safe-agent-etag"'}, {"id": "agent_safe"})
|
||||||
if path.endswith("/publish") and "/operations/" in path:
|
if path.endswith("/publish") and "/operations/" in path:
|
||||||
return smoke.JsonResponse(200, {}, {"published_version": 7})
|
return smoke.JsonResponse(200, {}, {"published_version": 7})
|
||||||
if path.endswith("/agents"):
|
if path.endswith("/agents"):
|
||||||
@@ -158,6 +202,8 @@ class AuthenticatedProductSmokeTests(unittest.TestCase):
|
|||||||
self.assertEqual((agent_id, agent_version, published_agent_version), ("agent_safe", 3, 3))
|
self.assertEqual((agent_id, agent_version, published_agent_version), ("agent_safe", 3, 3))
|
||||||
binding = next(payload for _, path, payload, _ in client.requests if path.endswith("/bindings"))[0]
|
binding = next(payload for _, path, payload, _ in client.requests if path.endswith("/bindings"))[0]
|
||||||
self.assertEqual(binding["operation_version"], 7)
|
self.assertEqual(binding["operation_version"], 7)
|
||||||
|
binding_request = next(request for request in client.requests if request[1].endswith("/bindings"))
|
||||||
|
self.assertEqual(binding_request[3]["headers"], {"If-Match": '"safe-agent-etag"'})
|
||||||
publish = next(request for request in client.requests if request[1].endswith("/operations/op_safe/publish"))
|
publish = next(request for request in client.requests if request[1].endswith("/operations/op_safe/publish"))
|
||||||
self.assertEqual(publish[3]["headers"], {"If-Match": '"safe-etag"'})
|
self.assertEqual(publish[3]["headers"], {"If-Match": '"safe-etag"'})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user