Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,14 @@ def test_global_guardrail_blocks_key_without_team_opt_out(
exercised_on=["chat_completions"],
)
def test_team_with_disable_flag_bypasses_global_guardrail(
self, client: GuardrailsClient, resources: ResourceManager
self, client: GuardrailsClient, resources: ResourceManager, scoped_key: str
) -> None:
banned = unique_marker()
guardrail_id = client.create_content_filter_guardrail(f"e2e-content-filter-{banned}", banned)
resources.defer(lambda: client.delete_guardrail(guardrail_id))

_assert_eventually_blocked(client, scoped_key, banned)

team_id = client.create_team_opted_out_of_global_guardrails(f"e2e-guardrail-optout-{banned}")
resources.defer(lambda: client.delete_team(team_id))
key = client.create_key_in_team(team_id)
Expand All @@ -87,6 +89,6 @@ def test_team_with_disable_flag_bypasses_global_guardrail(
chat = unwrap(client.chat(key, MODEL, _prompt_with(banned)))

assert chat.choices, (
f"team opted out of global guardrails, so the banned keyword must pass "
f"through and the call must succeed, but no choices came back: {chat}"
f"the same prompt was just blocked for a key outside the team, so the opt-out "
f"is what must let it through, but no choices came back: {chat}"
)
41 changes: 26 additions & 15 deletions tests/e2e/management/test_config_misc_endpoints_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class CacheSettingsValue(BaseModel):
type: str
host: str = ""
port: str = ""
namespace: str | None = None


class CacheSettingsUpdateBody(BaseModel):
Expand All @@ -141,6 +142,7 @@ class CacheCurrentValues(BaseModel):
type: str | None = None
host: str | None = None
port: str | None = None
namespace: str | None = None


class CacheGetResponse(BaseModel):
Expand Down Expand Up @@ -374,40 +376,49 @@ class TestCacheSettings:
def test_update_persists_cache_backend_to_get(
self, client: ManagementClient, resources: ResourceManager
) -> None:
"""Exercise the update route without changing global state: capture the live
cache backend and write exactly that back, so the config the proxy ends on is
byte-for-byte the one it started with. A teardown restore of the same captured
settings is the safety net if the body fails partway. The update route is only
meaningful against a configured cache, so an unconfigured proxy fails loudly
here rather than being silently switched to redis."""
"""Write a value the proxy did not already hold, so a route that silently
dropped the write cannot pass. The only field changed is the key namespace, a
label prefixed to cache keys that leaves the backend connection untouched, and
teardown restores the captured original so a shared proxy keeps the config it
started with. The update route is only meaningful against a configured cache,
so an unconfigured proxy fails loudly here rather than being silently switched
to redis."""
before = self._read_settings(client)
assert before.type is not None, (
"GET /cache/settings reported no cache type; refusing to invent one and mutate the shared proxy"
)
captured = CacheSettingsValue(type=before.type, host=before.host or "", port=before.port or "")
captured = CacheSettingsValue(
type=before.type, host=before.host or "", port=before.port or "", namespace=before.namespace
)
resources.defer(lambda: self._write_settings(client, captured))

namespace = f"e2e-cache-ns-{unique_marker()}"
target = CacheSettingsValue(type=captured.type, host=captured.host, port=captured.port, namespace=namespace)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Partial update clears cache settings

When the shared proxy has cache settings beyond type, host, port, and namespace, this four-field update clears the omitted non-secret fields because /cache/settings replaces them rather than preserving them. The deferred restore uses the same partial model, so settings such as TTL, database, SSL, or Sentinel configuration remain removed; when the original namespace is null, serialization also omits it and leaves the generated namespace active, changing cache connectivity or key behavior for later tests.

updated = unwrap(
client.proxy.transport.post(
"/cache/settings",
headers=client.proxy.transport.master,
json=CacheSettingsUpdateBody(cache_settings=captured),
json=CacheSettingsUpdateBody(cache_settings=target),
response_type=CacheUpdateResponse,
)
)
assert updated.status == "success", f"/cache/settings update status {updated.status!r}, expected 'success'"
assert updated.settings.type == captured.type, (
f"/cache/settings echoed type {updated.settings.type!r}, wrote {captured.type!r}"
assert updated.settings.namespace == namespace, (
f"/cache/settings echoed namespace {updated.settings.namespace!r}, wrote {namespace!r}"
)

def reflected() -> CacheCurrentValues | None:
current = self._read_settings(client)
return current if current.type == captured.type else None
return current if current.namespace == namespace else None

after = _poll(client, reflected, f"/cache/settings never reported type {captured.type!r} after the update")
assert after.host == captured.host and after.port == captured.port, (
f"/cache/settings persisted host/port {after.host!r}/{after.port!r}, "
f"wrote {captured.host!r}/{captured.port!r}"
after = _poll(
client,
reflected,
f"/cache/settings still reports namespace {before.namespace!r} instead of the written {namespace!r}",
)
assert after.type == captured.type and after.host == captured.host and after.port == captured.port, (
f"/cache/settings persisted type/host/port {after.type!r}/{after.host!r}/{after.port!r}, "
f"wrote {captured.type!r}/{captured.host!r}/{captured.port!r}"
)

@staticmethod
Expand Down
Loading