-
Notifications
You must be signed in to change notification settings - Fork 43
OSAC-706: E2E tests for catalog item lifecycle #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import pytest | ||
|
|
||
| from tests.core.runner import env | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def cluster_template() -> str: | ||
| return env("OSAC_CLUSTER_TEMPLATE", "osac.templates.ocp_4_17_small") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import subprocess | ||
| import time | ||
| from uuid import uuid4 | ||
|
|
||
| import pytest | ||
|
|
||
| from tests.core.grpc_client import GRPCClient | ||
| from tests.core.osac_cli import OsacCLI | ||
|
|
||
|
|
||
| def _unique_name(prefix: str) -> str: | ||
| return f"{prefix}-{uuid4().hex[:8]}" | ||
|
|
||
|
|
||
| def _wait_cluster_removed(grpc: GRPCClient, cluster_id: str, timeout: int = 30) -> None: | ||
| deadline = time.monotonic() + timeout | ||
| while cluster_id in grpc.list_cluster_ids(): | ||
| if time.monotonic() > deadline: | ||
| break | ||
| time.sleep(2) | ||
|
|
||
|
|
||
| def test_catalog_item_crud(grpc: GRPCClient, cluster_template: str) -> None: | ||
| name = _unique_name("e2e-cat") | ||
| catalog_item_id = grpc.create_cluster_catalog_item(name=name, template=cluster_template, published=True) | ||
| try: | ||
| assert catalog_item_id in grpc.list_cluster_catalog_item_ids() | ||
|
|
||
| item = grpc.get_cluster_catalog_item(catalog_item_id=catalog_item_id) | ||
| obj = item["object"] | ||
| assert obj["title"] == name | ||
| assert obj["template"] == cluster_template | ||
| assert obj["published"] is True | ||
|
|
||
| grpc.delete_cluster_catalog_item(catalog_item_id=catalog_item_id) | ||
| catalog_item_id = "" | ||
|
|
||
| assert catalog_item_id not in grpc.list_cluster_catalog_item_ids() | ||
|
Comment on lines
+37
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deletion verification checks wrong value (always passes). Line 38 sets Risk severity: Critical — The test cannot detect catalog item deletion failures, allowing broken delete operations to pass silently and accumulate leaked resources. 🐛 Proposed fix to verify actual catalog item ID grpc.delete_cluster_catalog_item(catalog_item_id=catalog_item_id)
+ deleted_id = catalog_item_id
catalog_item_id = ""
- assert catalog_item_id not in grpc.list_cluster_catalog_item_ids()
+ assert deleted_id not in grpc.list_cluster_catalog_item_ids()🤖 Prompt for AI Agents |
||
| finally: | ||
| if catalog_item_id: | ||
| grpc.delete_cluster_catalog_item(catalog_item_id=catalog_item_id) | ||
|
|
||
|
|
||
| def test_unpublished_catalog_item_not_visible_in_public_api(grpc: GRPCClient, cluster_template: str) -> None: | ||
| name = _unique_name("e2e-unpub") | ||
| catalog_item_id = grpc.create_cluster_catalog_item(name=name, template=cluster_template, published=False) | ||
| try: | ||
| assert catalog_item_id not in grpc.list_cluster_catalog_item_ids() | ||
|
|
||
| output, rc = grpc.call_unchecked(service="osac.public.v1.ClusterCatalogItems/Get", data={"id": catalog_item_id}) | ||
| assert rc != 0, f"Expected Get to fail for unpublished item, got: {output}" | ||
| assert "not published" in output.lower() or "not found" in output.lower() | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| finally: | ||
| grpc.delete_cluster_catalog_item(catalog_item_id=catalog_item_id) | ||
|
|
||
|
|
||
| def test_create_cluster_with_catalog_item(grpc: GRPCClient, cli: OsacCLI, cluster_template: str) -> None: | ||
| name = _unique_name("e2e-cat") | ||
| catalog_item_id = grpc.create_cluster_catalog_item(name=name, template=cluster_template, published=True) | ||
| cluster_id = "" | ||
| try: | ||
| cluster_name = _unique_name("e2e-cluster") | ||
| cluster_id = cli.create_cluster_with_catalog_item(catalog_item=catalog_item_id, name=cluster_name) | ||
|
|
||
| assert cluster_id in grpc.list_cluster_ids() | ||
|
|
||
| cluster = grpc.get_cluster(cluster_id=cluster_id) | ||
| assert cluster["object"]["spec"]["catalogItem"] == catalog_item_id | ||
| finally: | ||
| if cluster_id: | ||
| cli.delete_cluster(uuid=cluster_id) | ||
| _wait_cluster_removed(grpc, cluster_id) | ||
| grpc.delete_cluster_catalog_item(catalog_item_id=catalog_item_id) | ||
|
tzvatot marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def test_create_cluster_with_unpublished_catalog_item_fails( | ||
| grpc: GRPCClient, cli: OsacCLI, cluster_template: str | ||
| ) -> None: | ||
| name = _unique_name("e2e-unpub") | ||
| catalog_item_id = grpc.create_cluster_catalog_item(name=name, template=cluster_template, published=False) | ||
| try: | ||
| cluster_name = _unique_name("e2e-cluster") | ||
| with pytest.raises(subprocess.CalledProcessError): | ||
| cli.create_cluster_with_catalog_item(catalog_item=catalog_item_id, name=cluster_name) | ||
| finally: | ||
| grpc.delete_cluster_catalog_item(catalog_item_id=catalog_item_id) | ||
|
|
||
|
|
||
| @pytest.mark.xfail(reason="Server does not yet block deletion of referenced catalog items") | ||
| def test_delete_catalog_item_blocked_when_referenced(grpc: GRPCClient, cli: OsacCLI, cluster_template: str) -> None: | ||
| name = _unique_name("e2e-ref") | ||
| catalog_item_id = grpc.create_cluster_catalog_item(name=name, template=cluster_template, published=True) | ||
| cluster_id = "" | ||
| try: | ||
| cluster_name = _unique_name("e2e-cluster") | ||
| cluster_id = cli.create_cluster_with_catalog_item(catalog_item=catalog_item_id, name=cluster_name) | ||
|
|
||
| output, rc = grpc.call_unchecked( | ||
| service="osac.private.v1.ClusterCatalogItems/Delete", data={"id": catalog_item_id} | ||
| ) | ||
| assert rc != 0, f"Expected delete to be blocked, got: {output}" | ||
| finally: | ||
| if cluster_id: | ||
| cli.delete_cluster(uuid=cluster_id) | ||
| _wait_cluster_removed(grpc, cluster_id) | ||
| grpc.delete_cluster_catalog_item(catalog_item_id=catalog_item_id) | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing timeout exception masks cluster removal failures.
_wait_cluster_removedbreaks silently when the deadline is exceeded without raising an exception. This allows teardown to proceed to catalog item deletion even when the cluster is still registered, causing downstream gRPC referential integrity failures that mask the actual removal problem.Risk severity: Major — Silent timeouts produce misleading test failures and can leave orphaned catalog items when the real issue is cluster deletion/deregistration lag.
🛡️ Proposed fix to raise TimeoutError on deadline breach
def _wait_cluster_removed(grpc: GRPCClient, cluster_id: str, timeout: int = 30) -> None: deadline = time.monotonic() + timeout while cluster_id in grpc.list_cluster_ids(): if time.monotonic() > deadline: - break + raise TimeoutError(f"Cluster {cluster_id} still present after {timeout}s") time.sleep(2)🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added to #70
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alosadagrande, thanks for tracking it in#70. The silentbreakwill stay a potential source of confusing failures until that's addressed — worth keeping an eye on it.🧠 Learnings used