proto(woo): per-browser Woo creds via X-Woo-* request headers

Prototype routing so 2–3 owners can share one store URL with different WP
application passwords. Device creds live in localStorage; server uses
request override → Settings fallback and never writes overrides into the
Settings row. Inference stays on shared Settings.

See PROTOTYPE.md for UAT with two browsers and known LAN forge risk.
This commit is contained in:
grokbot_ty
2026-09-26 12:26:21 -07:00
parent fc40c66c44
commit 2f6335ec26
8 changed files with 419 additions and 31 deletions
+113
View File
@@ -0,0 +1,113 @@
"""Prototype: per-request X-Woo-* headers build distinct WooClients without clobbering Settings."""
import base64
import os
import shutil
import tempfile
import pytest
from starlette.datastructures import Headers
import backend.db as db_mod
from backend.crypto import encrypt, decrypt
from backend.woo import (
WooClient,
client_for_request,
request_woo_override,
settings_woo_creds,
HEADER_WOO_USERNAME,
HEADER_WOO_APP_PASSWORD,
)
@pytest.fixture
def db_dir():
tmp = tempfile.mkdtemp(prefix="per_user_woo_")
os.environ["SLAB_DB_PATH"] = os.path.join(tmp, "t.db")
os.environ["SLAB_IMAGES_DIR"] = tmp
os.environ["WOO_BASE_URL"] = "https://store.example.com"
os.environ["SLAB_AES_KEY"] = base64.b64encode(b"0123456789abcdef0123456789abcdef").decode()
db_mod._reset_for_tests()
conn = db_mod.init_db()
conn.execute(
"INSERT OR REPLACE INTO settings(key, value, value_enc) VALUES ('woo_wp_username','shop-default',NULL)"
)
conn.execute(
"INSERT OR REPLACE INTO settings(key, value, value_enc) VALUES ('woo_app_password',NULL,?)",
(encrypt("shop-secret"),),
)
conn.commit()
yield conn
db_mod._reset_for_tests()
shutil.rmtree(tmp, ignore_errors=True)
def _auth_pair(client: WooClient) -> tuple[str, str]:
raw = base64.b64decode(client._auth).decode("utf-8")
user, password = raw.split(":", 1)
return user, password
def test_override_headers_produce_distinct_auth_without_clobbering_settings(db_dir):
conn = db_dir
before = settings_woo_creds(conn)
assert before[1] == "shop-default"
assert decrypt(before[2]) == "shop-secret"
h_a = Headers({HEADER_WOO_USERNAME: "alice", HEADER_WOO_APP_PASSWORD: "alice-app-pw"})
h_b = Headers({HEADER_WOO_USERNAME: "bob", HEADER_WOO_APP_PASSWORD: "bob-app-pw"})
client_a = client_for_request(conn, h_a)
client_b = client_for_request(conn, h_b)
client_fallback = client_for_request(conn, Headers({}))
assert _auth_pair(client_a) == ("alice", "alice-app-pw")
assert _auth_pair(client_b) == ("bob", "bob-app-pw")
assert _auth_pair(client_fallback) == ("shop-default", "shop-secret")
assert client_a._auth != client_b._auth
after = settings_woo_creds(conn)
assert after == before
assert decrypt(after[2]) == "shop-secret"
# Explicit: Settings row values unchanged
row_u = conn.execute("SELECT value FROM settings WHERE key='woo_wp_username'").fetchone()[0]
row_p = conn.execute("SELECT value_enc FROM settings WHERE key='woo_app_password'").fetchone()[0]
assert row_u == "shop-default"
assert decrypt(row_p) == "shop-secret"
def test_partial_headers_fall_back_to_settings(db_dir):
conn = db_dir
# Username only — incomplete override
assert request_woo_override(Headers({HEADER_WOO_USERNAME: "alice"})) is None
client = client_for_request(conn, Headers({HEADER_WOO_USERNAME: "alice"}))
assert _auth_pair(client) == ("shop-default", "shop-secret")
def test_test_woo_endpoint_uses_request_override(db_dir, monkeypatch):
"""API: POST /settings/test-woo with X-Woo-* builds client for that user."""
from fastapi.testclient import TestClient
from backend import app as app_mod
seen: list[str] = []
def fake_sync(client, conn):
seen.append(_auth_pair(client)[0])
return {"taxonomy_anchor": "x", "taxonomy_last_sync_at": "2026-01-01T00:00:00"}
monkeypatch.setattr(app_mod.taxonomy_mod, "sync", fake_sync)
tc = TestClient(app_mod.app)
r1 = tc.post(
"/api/v1/settings/test-woo",
headers={HEADER_WOO_USERNAME: "alice", HEADER_WOO_APP_PASSWORD: "pw-a"},
)
r2 = tc.post(
"/api/v1/settings/test-woo",
headers={HEADER_WOO_USERNAME: "bob", HEADER_WOO_APP_PASSWORD: "pw-b"},
)
r3 = tc.post("/api/v1/settings/test-woo")
assert r1.status_code == 200 and r1.json()["ok"] is True
assert r2.status_code == 200 and r2.json()["ok"] is True
assert r3.status_code == 200 and r3.json()["ok"] is True
assert seen == ["alice", "bob", "shop-default"]
assert settings_woo_creds(db_dir)[1] == "shop-default"