feat(cfold): canonicalize custom test layout
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
@ -1,73 +0,0 @@
|
||||
"""Shared discourse test helpers — admin user + API key + JSON HTTP.
|
||||
|
||||
The bitnamilegacy/discourse recipe (compose app env) sets ONLY the DB + SMTP vars — it does NOT seed
|
||||
an admin user, and Discourse's Admin API requires `Api-Key` + `Api-Username` headers (there is no
|
||||
auto-created API key). So the functional tests bootstrap their own admin by running Rails inside the
|
||||
`app` container (Discourse ships its Rails env at /opt/bitnami/discourse): find-or-create an admin
|
||||
user, then create an ApiKey and print its plaintext `.key` (Discourse returns the plaintext only at
|
||||
create time; it's stored hashed). Both the admin user and the key are class-B run-scoped — they live
|
||||
only in the per-run app and are destroyed at teardown.
|
||||
|
||||
`mint_admin(domain)` returns (api_key, api_username). Each call creates a fresh ApiKey (cheap;
|
||||
idempotent enough — the shared deployment's functional tests each mint their own), reusing the same
|
||||
admin user across calls. Uses `lifecycle.exec_in_app` (hardened exec with retry).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
|
||||
from harness import lifecycle # noqa: E402
|
||||
|
||||
# Rails snippet (single line): find-or-create an admin, create an ApiKey, print key + username as the
|
||||
# last two lines. SecureRandom is available in the Rails runtime. We mark the user active + approved
|
||||
# so the API accepts it. created_by_id must be set (ApiKey validates it).
|
||||
#
|
||||
# We also enable `allow_uncategorized_topics` (a standard Discourse feature, off by default since 3.x):
|
||||
# without it, POST /posts.json with no category 422s "Category can't be blank". This is config parity
|
||||
# with a real forum (the operator would either enable uncategorized or pick a category), not a test
|
||||
# weakening — the create-topic round-trip still posts a real topic and asserts a unique marker survives.
|
||||
_BOOTSTRAP_RB = (
|
||||
"SiteSetting.allow_uncategorized_topics = true; "
|
||||
"u = User.where(admin: true).order(:id).first; "
|
||||
"if u.nil?; "
|
||||
"u = User.create!(username: 'ccciadmin', name: 'CCCI Admin', "
|
||||
"email: 'ccciadmin@ccci.example.com', password: SecureRandom.hex(20), "
|
||||
"active: true, approved: true, trust_level: 1); "
|
||||
"u.update!(admin: true); u.activate; "
|
||||
"end; "
|
||||
"k = ApiKey.create!(description: 'ccci-run', created_by_id: u.id); "
|
||||
"puts 'CCCI_API_KEY=' + k.key; "
|
||||
"puts 'CCCI_API_USER=' + u.username"
|
||||
)
|
||||
|
||||
|
||||
def mint_admin(domain: str) -> tuple[str, str]:
|
||||
"""Bootstrap an admin + fresh API key via Rails in the app container. Returns (api_key, username)."""
|
||||
# `bin/rails` is `#!/usr/bin/env ruby`; the bitnami discourse image keeps ruby at
|
||||
# /opt/bitnami/ruby/bin, which is NOT on a login shell's PATH (`bash -lc` resets PATH from
|
||||
# /etc/profile → `env: 'ruby': No such file or directory`, rc=127). Use a non-login shell, discover
|
||||
# ruby (image-ENV PATH first, bitnami fallback), and invoke it explicitly so the shebang is moot.
|
||||
cmd = (
|
||||
"cd /opt/bitnami/discourse && "
|
||||
"RUBY=$(command -v ruby || echo /opt/bitnami/ruby/bin/ruby) && "
|
||||
f'RAILS_ENV=production "$RUBY" bin/rails runner "{_BOOTSTRAP_RB}"'
|
||||
)
|
||||
out = lifecycle.exec_in_app(domain, ["bash", "-c", cmd], service="app", timeout=240)
|
||||
key = user = None
|
||||
for line in out.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("CCCI_API_KEY="):
|
||||
key = line.split("=", 1)[1].strip()
|
||||
elif line.startswith("CCCI_API_USER="):
|
||||
user = line.split("=", 1)[1].strip()
|
||||
assert (
|
||||
key and user
|
||||
), f"could not bootstrap discourse admin/API key; rails output tail:\n{out[-1000:]}"
|
||||
return key, user
|
||||
|
||||
|
||||
def admin_headers(api_key: str, api_username: str) -> dict[str, str]:
|
||||
return {"Api-Key": api_key, "Api-Username": api_username}
|
||||
@ -1,70 +0,0 @@
|
||||
"""discourse — Q4.6 recipe-specific functional test (plan §4.3: "create the app's primary object — a
|
||||
topic — and read it back").
|
||||
|
||||
Exercises Discourse's core forum function end-to-end against the live per-run deploy, via the real
|
||||
Admin API:
|
||||
1. Wait for the Admin API to answer (/site.json 200 once Rails is serving).
|
||||
2. Bootstrap an admin + mint an API key (_discourse.mint_admin — the recipe seeds no admin).
|
||||
3. POST /posts.json to create a new topic (title + body carrying a unique marker).
|
||||
4. GET /t/<topic_id>.json to read it back and assert the title round-tripped; GET the post's raw
|
||||
and assert the unique body marker survived.
|
||||
|
||||
NOT health-only: a Discourse whose DB/Rails/posting path is broken fails here even though /srv/status
|
||||
returns 200. The marker is unique per run, so a stale/echoed response cannot pass.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
|
||||
import _discourse # noqa: E402
|
||||
from harness import http as harness_http # noqa: E402
|
||||
|
||||
|
||||
def test_create_topic_roundtrip(live_app):
|
||||
base = f"https://{live_app}"
|
||||
|
||||
# 1) Admin API ready (Rails serving JSON, not just /srv/status).
|
||||
harness_http.retry_http_get(f"{base}/site.json", expect_status=200, max_wait=120, interval=5)
|
||||
|
||||
# 2) Bootstrap admin + API key (recipe seeds no admin; mint via Rails in the app container).
|
||||
api_key, api_user = _discourse.mint_admin(live_app)
|
||||
hdrs = _discourse.admin_headers(api_key, api_user)
|
||||
|
||||
# 3) Create a topic with a unique marker in title + body (raw must be >= ~20 chars).
|
||||
# Discourse's `title_prettify` (on by default) capitalises the title's first letter, so we send a
|
||||
# title that already starts capitalised — that normalisation is then a no-op and the exact-equality
|
||||
# round-trip below stays faithful (the unique hex token is mid-string, untouched either way).
|
||||
uniq = uuid.uuid4().hex[:10]
|
||||
title = f"CCCI topic {uniq}"
|
||||
marker = f"ccci-body-marker-{uniq}-roundtrip-padding-text"
|
||||
status, body = harness_http.http_post(
|
||||
f"{base}/posts.json",
|
||||
data={"title": title, "raw": marker},
|
||||
headers=hdrs,
|
||||
timeout=60,
|
||||
)
|
||||
assert status in (200, 201) and isinstance(
|
||||
body, dict
|
||||
), f"create topic failed: HTTP {status}, body={body!r}"
|
||||
topic_id = body.get("topic_id")
|
||||
assert topic_id, f"create topic returned no topic_id: {body!r}"
|
||||
|
||||
# 4) Read the topic back and assert title + first-post body round-trip.
|
||||
status, got = harness_http.http_get(f"{base}/t/{topic_id}.json", headers=hdrs, timeout=30)
|
||||
assert status == 200 and isinstance(
|
||||
got, dict
|
||||
), f"read topic failed: HTTP {status}, body={got!r}"
|
||||
assert (
|
||||
got.get("title") == title
|
||||
), f"topic title did not round-trip: sent {title!r}, got {got.get('title')!r}"
|
||||
posts = (got.get("post_stream") or {}).get("posts") or []
|
||||
assert posts, f"topic has no posts on read-back: {got!r}"
|
||||
first_cooked = posts[0].get("cooked", "")
|
||||
assert (
|
||||
marker in first_cooked
|
||||
), f"topic body did not round-trip: marker {marker!r} not in first post {first_cooked!r}"
|
||||
@ -1,20 +0,0 @@
|
||||
"""discourse — health/readiness functional test (Phase 2).
|
||||
|
||||
SOURCE: no recipe-maintainer corpus exists for discourse (P2 vacuous — see PARITY.md). This is a
|
||||
Phase-2 health check aligned with the parity-port convention: /srv/status returns 200 only once the
|
||||
Rails app is actually serving (the recipe's HEALTH_PATH and the canonical "is discourse up" signal).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
|
||||
from harness import http as harness_http # noqa: E402
|
||||
|
||||
|
||||
def test_discourse_srv_status_ok(live_app):
|
||||
url = f"https://{live_app}/srv/status"
|
||||
status, _ = harness_http.retry_http_get(url, expect_status=200, max_wait=120, interval=5)
|
||||
assert status == 200, f"GET {url} HTTP {status} (expected 200 — discourse not serving)"
|
||||
@ -1,31 +0,0 @@
|
||||
"""discourse — Q4.6 recipe-specific functional test (2nd functional, beyond create-topic + health).
|
||||
|
||||
Asserts Discourse's Rails app emits its bootstrap JSON: GET /site.json returns 200 with the
|
||||
site-config envelope the Ember SPA needs (categories list + default trust levels). This distinguishes
|
||||
"the Discourse Rails backend is up and serving its API" from "a static/error page is served" — a
|
||||
wedged backend 5xxs, a misroute 404s. Complements test_create_topic (write path) with a read of the
|
||||
app's characteristic site config.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
|
||||
from harness import http as harness_http # noqa: E402
|
||||
|
||||
|
||||
def test_site_json_has_discourse_config(live_app):
|
||||
status, body = harness_http.retry_http_get(
|
||||
f"https://{live_app}/site.json", expect_status=200, max_wait=120, interval=5
|
||||
)
|
||||
assert status == 200 and isinstance(
|
||||
body, dict
|
||||
), f"GET /site.json failed: HTTP {status}, body type={type(body).__name__}"
|
||||
# /site.json carries Discourse-specific structure — `categories` (a list) and `groups` are always
|
||||
# present in a booted Discourse. A non-Discourse 200 (placeholder page) would not parse to this.
|
||||
assert "categories" in body, f"/site.json missing 'categories' key: keys={list(body)[:20]}"
|
||||
assert isinstance(
|
||||
body["categories"], list
|
||||
), f"/site.json 'categories' not a list: {type(body['categories']).__name__}"
|
||||
Reference in New Issue
Block a user