feedBack/tests/test_correlation_id.py
Bret Mogilefsky af2949677a
rename: slopsmith → feedBack, byron → got-feedBack (#537)
* Update GitHub repo references from feedback* to feedBack*

* rename: slopsmith -> feedBack, byron -> got-feedBack

Renames across the entire codebase:
- slopsmith/Slopsmith/SLOPSMITH/SlopSmith -> feedBack/FeedBack/FEEDBACK/FeedBack
- byron/Byron/Byrongamatos -> got-feedBack/got-feedBack/got-feedBack
- /home/byron/ -> /opt/got-feedBack/
- byron@ougsoft.com -> hi@got-feedBack.org
- github.com/byrongamatos/ -> github.com/got-feedback/
- com.byron. -> com.got-feedback.
- SLOPSMITH_ env vars -> FEEDBACK_ with backward-compat fallback
- Protocol/storage strings migrated with read-old/write-new pattern
- window.slopsmith JS API -> window.feedBack (canonical) + backward-compat alias

Refs: #rename-slopsmith

* rename: complete regen against current main + fix backward-compat alias

Regenerated the slopsmith->feedBack / byron->got-feedBack rename on top of
current main (3 commits had landed since the branch: #572/#554/#574),
resolving the four content conflicts in favour of main's newer content
(autoplay/auto-exit, accuracy-badge, Virtuoso re-home, feedpak badge).

Completion fixes on top of the mechanical rename:
- Re-apply rename to post-branch content the original rename never saw:
  window.slopsmith(.Tour) consumers in lessons.js / notifications.js /
  onboarding-tour.js, and the matching JS + python tests (autoplay_exit,
  progression_*, test_feedpak_extension FEEDBACK_* env vars). The test env
  vars now match server.py (which reads FEEDBACK_SYNC_STARTUP /
  FEEDBACK_SKIP_STARTUP_TASKS), so the sync-startup test exercises the real
  path again.
- Restore the window.slopsmith backward-compat alias dropped during conflict
  resolution, and move the bus aliases to AFTER the _feedBackExisting merge
  block so they reference the fully-assembled object (also fixes the
  loop_api.test.js API-surface regex, which the original PR latently broke).
- Drop the stray empty data/web_library.db (runtime DB lives in CONFIG_DIR)
  and gitignore it.
- Fix stale tone-source test: feed[dB]ack -> fee[dB]ack to match shipped
  source labels.

Verified locally (org CI billing-blocked): JS 819/819 pass; pytest 1669
passed / 1683 collected with 0 import errors; zero residual slopsmith/byron
except the two intentional window.slopsmith aliases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* rename: implement advertised backward-compat + prune dead community plugins

Address gaps where PR #537's "Backward compatibility" section was advertised
but not implemented, and clean up the community plugin list.

Env vars (FEEDBACK_* canonical, legacy SLOPSMITH_* honoured):
- New lib/env_compat.py (getenv_compat / env_flag_compat) + tests. server.py
  (_env_flag + all FEEDBACK_* reads), diagnostics_hardware, gp2midi and
  tailwind_rebuild now resolve the legacy alias, so existing SLOPSMITH_UI /
  SLOPSMITH_PLUGINS_DIR / etc. deployments keep working.
- Fix the rename collapsing plugins/__init__.py and minigames/routes.py from
  `FEEDBACK_PLUGINS_DIR or SLOPSMITH_PLUGINS_DIR` into a redundant
  `FEEDBACK_ or FEEDBACK_` (the fallback was silently lost).

Storage (app.js update-channel):
- Read feedBack-update-channel, fall back to legacy slopsmith-update-channel,
  and clear the legacy key on write — so a user's update-channel preference
  survives the rename instead of resetting to "stable".

Community plugin list (README): the rename rewrote third-party repo URLs we
don't own. Probed every one; their owners never renamed, so:
- Restore the 13 live community plugins to their real slopsmith-* names.
- Prune 6 that are 404 to the public (topkoa splitscreen/stems, OmikronApex
  tuner, Jafz2001 nam-rig-builder, DeathlySin song-preview, Erikcb91 shuffle).
- Fix a pre-existing Guitar Theory clone-command typo (nam-tone -> guitar-theory).

Verified: env_compat 7/7, JS 819/819, pytest 1690 collected / 0 import errors,
rename-sensitive + startup suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: byrongamatos <xasiklas@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:03:01 +02:00

180 lines
7.0 KiB
Python

"""Integration test for CorrelationIdMiddleware wiring in server.py.
Verifies that every HTTP response carries the X-Request-ID header that was
introduced alongside the structured logging bootstrap (feedBack#155).
Requests that include an X-Request-ID should echo it; requests without it
should receive a server-generated ID. A cross-cutting end-to-end test
additionally asserts that the ID propagates into log output.
"""
import importlib
import io
import json
import logging
import sys
import uuid
import pytest
import structlog
from fastapi import FastAPI
from fastapi.testclient import TestClient
from asgi_correlation_id import CorrelationIdMiddleware
@pytest.fixture()
def client(tmp_path, monkeypatch):
"""Minimal server client with background I/O suppressed."""
monkeypatch.setenv("CONFIG_DIR", str(tmp_path))
monkeypatch.setenv("FEEDBACK_SYNC_STARTUP", "1")
sys.modules.pop("server", None)
server = importlib.import_module("server")
monkeypatch.setattr(server, "load_plugins", lambda *a, **kw: None)
monkeypatch.setattr(server, "startup_scan", lambda: None)
with TestClient(server.app) as tc:
try:
yield tc
finally:
conn = getattr(getattr(server, "meta_db", None), "conn", None)
if conn is not None:
conn.close()
def test_response_includes_x_request_id_header(client):
"""Every response must carry X-Request-ID regardless of the endpoint."""
r = client.get("/api/startup-status")
assert "x-request-id" in {k.lower() for k in r.headers}, (
f"X-Request-ID missing from response headers: {dict(r.headers)}"
)
def test_provided_x_request_id_is_echoed(client):
"""When the client sends a valid UUID X-Request-ID, the server echoes it."""
custom_id = str(uuid.uuid4())
r = client.get("/api/startup-status", headers={"X-Request-ID": custom_id})
assert r.headers.get("x-request-id") == custom_id
def test_opaque_proxy_id_is_echoed(client):
"""Non-UUID proxy-style request IDs must be propagated unchanged (validator=None)."""
opaque_id = "abc123def456"
r = client.get("/api/startup-status", headers={"X-Request-ID": opaque_id})
assert r.headers.get("x-request-id") == opaque_id, (
"Opaque proxy ID was replaced instead of propagated — "
"ensure CorrelationIdMiddleware is configured with validator=None"
)
def test_generated_x_request_id_is_nonempty(client):
"""When no X-Request-ID is sent, the server generates a non-empty one."""
r = client.get("/api/startup-status")
request_id = r.headers.get("x-request-id", "")
assert request_id, "Server-generated X-Request-ID must not be empty"
# ---------------------------------------------------------------------------
# End-to-end: middleware + logging_setup integration
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def _isolate_logging(isolate_logging):
"""Auto-use wrapper that pulls in the shared isolate_logging fixture."""
def test_request_id_appears_in_log_line(monkeypatch):
"""An HTTP request's X-Request-ID must appear as request_id in log output.
This is the end-to-end integration check: CorrelationIdMiddleware sets the
context var, and logging_setup._add_correlation_id reads it into the event
dict, so both pieces must be wired together for this test to pass.
"""
import logging_setup
monkeypatch.setenv("LOG_FORMAT", "json")
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
monkeypatch.delenv("LOG_FILE", raising=False)
logging_setup.configure_logging()
buf = io.StringIO()
for h in logging.getLogger("feedBack").handlers:
if isinstance(h, logging.StreamHandler) and not isinstance(
h, logging.FileHandler
):
h.stream = buf
# Minimal app that replicates the same middleware wiring as server.py.
mini_app = FastAPI()
mini_app.add_middleware(CorrelationIdMiddleware, validator=None)
@mini_app.get("/probe")
def probe():
logging.getLogger("feedBack.probe").info("probe_event")
return {"ok": True}
known_id = str(uuid.uuid4())
with TestClient(mini_app) as tc:
tc.get("/probe", headers={"X-Request-ID": known_id})
lines = [ln for ln in buf.getvalue().splitlines() if "probe_event" in ln]
assert lines, "No log line captured during HTTP request"
parsed = json.loads(lines[0])
assert parsed.get("request_id") == known_id, (
f"Log line request_id {parsed.get('request_id')!r} != {known_id!r}"
)
def test_server_app_request_id_propagated_to_logs(monkeypatch, tmp_path):
"""request_id must appear in log output from the real server.app.
Uses the actual server.app instance (with its CorrelationIdMiddleware) to
confirm no middleware-ordering regression can silence the correlation field.
A temporary route is added for the test and removed afterwards so that
server.app is not permanently modified.
"""
monkeypatch.setenv("LOG_FORMAT", "json")
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
monkeypatch.delenv("LOG_FILE", raising=False)
monkeypatch.setenv("CONFIG_DIR", str(tmp_path))
monkeypatch.setenv("FEEDBACK_SYNC_STARTUP", "1")
sys.modules.pop("server", None)
server_mod = importlib.import_module("server")
monkeypatch.setattr(server_mod, "load_plugins", lambda *a, **kw: None)
monkeypatch.setattr(server_mod, "startup_scan", lambda: None)
# Add a probe route to server.app so the test can trigger a log write.
@server_mod.app.get("/_test_log_probe")
def _probe():
logging.getLogger("feedBack.server").info("server_probe_event")
return {"ok": True}
known_id = str(uuid.uuid4())
buf = io.StringIO()
try:
with TestClient(server_mod.app) as tc:
# startup_events() has now run (including its configure_logging() call),
# so the feedBack handlers are freshly created. Redirect their stream
# to buf NOW, after startup, so we capture the probe request's output.
for h in logging.getLogger("feedBack").handlers:
if isinstance(h, logging.StreamHandler) and not isinstance(
h, logging.FileHandler
):
h.stream = buf
tc.get("/_test_log_probe", headers={"X-Request-ID": known_id})
finally:
# Remove the test route to avoid polluting server.app for other tests.
server_mod.app.router.routes = [
r for r in server_mod.app.router.routes
if getattr(r, "path", None) != "/_test_log_probe"
]
conn = getattr(getattr(server_mod, "meta_db", None), "conn", None)
if conn is not None:
conn.close()
lines = [ln for ln in buf.getvalue().splitlines() if "server_probe_event" in ln]
assert lines, "No log line captured from server.app probe route"
parsed = json.loads(lines[0])
assert parsed.get("request_id") == known_id, (
f"server.app log line request_id {parsed.get('request_id')!r} != {known_id!r}"
)