diff --git a/.secrets.baseline b/.secrets.baseline index a314502342..ffd50233a2 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1,9 +1,9 @@ { "exclude": { - "files": "(?x)( package-lock\\.json$ |Cargo\\.lock$ |uv\\.lock$ |go\\.sum$ |mcpgateway/sri_hashes\\.json$ )|^\\.secrets\\.baseline$|^.secrets.baseline$", + "files": "(?x)( package-lock\\.json$ |Cargo\\.lock$ |uv\\.lock$ |go\\.sum$ |mcpgateway/sri_hashes\\.json$ )|^.secrets.baseline$", "lines": null }, - "generated_at": "2026-09-18T18:21:05Z", + "generated_at": "2026-09-22T10:20:38Z", "plugins_used": [ { "name": "AWSKeyDetector" diff --git a/Makefile b/Makefile index 93ffa72bd7..87e8a8cdf7 100644 --- a/Makefile +++ b/Makefile @@ -7273,6 +7273,10 @@ PLAYWRIGHT_SCREENSHOTS := $(PLAYWRIGHT_DIR)/screenshots PLAYWRIGHT_VIDEOS := $(PLAYWRIGHT_DIR)/videos PLAYWRIGHT_SLOWMO ?= 750 TEST_BASE_URL ?= http://localhost:8080 +# Auth env for Playwright runs against the docker-compose stack: must match +# the gateway's signing secret and bootstrap admin password (docker-compose.yml). +JWT_SECRET_KEY ?= my-test-key-but-now-longer-than-32-bytes +PLATFORM_ADMIN_PASSWORD ?= changeme ZAP_BASE_URL ?= http://localhost:8090 ZAP_API_KEY ?= changeme # URL ZAP uses internally to spider the app. nginx exposes port 80 on mcpnet @@ -7348,6 +7352,7 @@ define run_playwright_test @$(MAKE) --no-print-directory playwright-preflight $(if $(strip $(2)),@mkdir -p $(2),) @$(if $(strip $(3)),$(3),) TEST_BASE_URL='$(TEST_BASE_URL)' \ + JWT_SECRET_KEY='$(JWT_SECRET_KEY)' PLATFORM_ADMIN_PASSWORD='$(PLATFORM_ADMIN_PASSWORD)' \ $(UV_BIN) run pytest -p playwright $(4) \ --browser chromium \ $(if $(filter fail,$(5)),|| { echo '❌ UI tests failed!'; exit 1; },|| true) diff --git a/tests/playwright/conftest.py b/tests/playwright/conftest.py index db4b249601..9cca95588a 100644 --- a/tests/playwright/conftest.py +++ b/tests/playwright/conftest.py @@ -381,15 +381,12 @@ def admin_api(playwright: Playwright) -> Generator[APIRequestContext, None, None if not token and not DISABLE_JWT_FALLBACK: # Priority 3: Locally-signed JWT fallback try: - token = _create_jwt_token( - {"sub": ADMIN_EMAIL}, - user_data={ - "email": ADMIN_EMAIL, - "full_name": "Test Admin", - "is_admin": True, - "auth_provider": "test", - }, + token = make_test_jwt( + ADMIN_EMAIL, + is_admin=True, teams=None, # Admin bypass: null teams with is_admin=true + auth_provider="test", + user_data={"email": ADMIN_EMAIL, "is_admin": True, "auth_provider": "test", "full_name": "Test Admin"}, ) except Exception: pass # Use empty if generation fails diff --git a/tests/playwright/entities/test_gateways_extended.py b/tests/playwright/entities/test_gateways_extended.py index 4d917639c2..cd77cd10d9 100644 --- a/tests/playwright/entities/test_gateways_extended.py +++ b/tests/playwright/entities/test_gateways_extended.py @@ -567,13 +567,24 @@ def test_edit_modal_auth_type_bearer_fields(self, gateways_page: GatewaysPage): gateways_page.close_edit_modal() - def test_edit_modal_auth_type_oauth_fields(self, gateways_page: GatewaysPage): + def test_edit_modal_auth_type_oauth_fields(self, gateways_page: GatewaysPage, admin_api): """Test that selecting OAuth in edit modal shows OAuth fields.""" + # Open a gateway whose auth type is NOT oauth: row 0 can be an + # OAuth-configured leftover from other suites, which legitimately + # renders OAuth fields on open and breaks the initial assertion. + resp = admin_api.get("/gateways") + assert resp.status == 200, f"Failed to list gateways: {resp.status}" + non_oauth = [g for g in resp.json() if g.get("authType") != "oauth"] + if not non_oauth: + pytest.skip("No non-OAuth gateway available") + gw_name = non_oauth[0]["name"] + gateways_page.navigate_to_gateways_tab() gateways_page.wait_for_gateways_table_loaded() _skip_if_no_gateways(gateways_page) - _open_edit_or_skip(gateways_page, 0) + gateways_page.search_gateways(gw_name) + gateways_page.open_edit_modal_by_name(gw_name) # Wait for editGateway() to finish populating the form before asserting field # states — the modal becomes visible before the async fetch resolves, so without diff --git a/tests/playwright/entities/test_servers_extended.py b/tests/playwright/entities/test_servers_extended.py index 6d8e9bbbe9..887eabf775 100644 --- a/tests/playwright/entities/test_servers_extended.py +++ b/tests/playwright/entities/test_servers_extended.py @@ -961,12 +961,31 @@ def test_select_all_resources_survives_search(self, servers_page: ServersPage): @pytest.mark.ui @pytest.mark.e2e - def test_select_all_prompts_survives_search(self, servers_page: ServersPage): + def test_select_all_prompts_survives_search(self, servers_page: ServersPage, admin_api): """Regression test for #3257: Select All prompts + search + clear preserves selections. Clicks Select All prompts, captures store size, searches, clears, then asserts the in-memory store size is unchanged. """ + # Guarantee at least one prompt is visible in the modal: a DB holding + # only team-scoped prompts from other suites leaves the prompt list + # empty and the wait below times out instead of reaching the skip. + prompt_name = f"sel-all-prompts-{uuid.uuid4().hex[:8]}" + resp = admin_api.post( + "/prompts/", + data={ + "prompt": { + "name": prompt_name, + "description": "select-all regression prompt", + "template": "Hello {{name}}", + "arguments": [{"name": "name", "description": "Name", "required": True}], + }, + "team_id": None, + }, + ) + assert resp.status in (200, 201), f"Failed to create prompt: {resp.status} {resp.text()}" + prompt_id = resp.json()["id"] + servers_page.navigate_to_servers_tab() servers_page.wait_for_visible(servers_page.add_server_form) server_name = self._create_server_with_tools(servers_page) @@ -1036,3 +1055,4 @@ def test_select_all_prompts_survives_search(self, servers_page: ServersPage): expect(servers_page.edit_server_modal).to_be_hidden(timeout=5000) finally: cleanup_server(servers_page.page, server_name) + admin_api.delete(f"/prompts/{prompt_id}") diff --git a/tests/playwright/pages/gateways_page.py b/tests/playwright/pages/gateways_page.py index 410469dc76..ee5b9c4f22 100644 --- a/tests/playwright/pages/gateways_page.py +++ b/tests/playwright/pages/gateways_page.py @@ -1270,6 +1270,20 @@ def open_edit_modal(self, gateway_index: int = 0) -> None: edit_btn = gateway_row.locator('button[role="menuitem"]:has-text("Edit")') self._click_and_wait_for_gateway_fetch(edit_btn, "gateway-edit-modal") + def open_edit_modal_by_name(self, name: str) -> None: + """Click Edit on the row for a named gateway (search-filter-safe). + + Client-side search filters hide non-matching rows but leave them in the + table body, so index-based selection can land on a hidden row. This + locates the row by its visible gateway name instead. + """ + self.page.wait_for_selector('#gateways-table-body tr[id*="gateway-row"]', state="attached", timeout=15000) + self.page.wait_for_function("typeof window.Admin?.viewGateway === 'function'", timeout=10000) + gateway_row = self.gateways_table_body.locator("tr", has_text=name).first + self._open_action_dropdown(gateway_row) + edit_btn = gateway_row.locator('button[role="menuitem"]:has-text("Edit")') + self._click_and_wait_for_gateway_fetch(edit_btn, "gateway-edit-modal") + def close_edit_modal(self) -> None: """Close the edit modal via Cancel.""" self.edit_modal_cancel_btn.click() diff --git a/tests/playwright/test_rbac_permissions.py b/tests/playwright/test_rbac_permissions.py index 46bc256e82..a96c40d98f 100644 --- a/tests/playwright/test_rbac_permissions.py +++ b/tests/playwright/test_rbac_permissions.py @@ -1194,7 +1194,7 @@ def test_developer_rpc_tools_call_not_denied(self, playwright: Playwright, admin "tool": { "name": tool_name, "description": "RPC RBAC regression test tool (#3515)", - "url": "https://httpbin.org/get", + "url": "http://fast_time_server:9080/nonexistent", # local 404: fast, deterministic, non-retryable (httpbin.org is an external dependency that can degrade into retryable 503s, pushing the call past the client timeout) "integration_type": "REST", "input_schema": {}, "visibility": "team",