From 217740214ef5865ed4b384a18a7e38950f58405f Mon Sep 17 00:00:00 2001 From: Kevin Allioli Date: Tue, 28 Apr 2026 23:10:41 +0200 Subject: [PATCH] =?UTF-8?q?feat!:=203.0.0=20=E2=80=94=20drop=20deprecated?= =?UTF-8?q?=20aliases,=20finalize=20ADR-0008?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the major bump that retires the 314 hyphenated command aliases ADR-0008 had kept on as ``deprecated=True`` façades since 2.0.x. Every command in the live tree now follows the openstackclient ``noun [subnoun] verb`` convention; legacy invocations are no longer accepted. Removed: - 314 ``add_command_with_alias()`` calls across 27 command modules. - ``orca_cli/core/aliases.py`` (private module) — no external consumers. - ``orca doctor --list-deprecated`` and the ``ADR-0008: deprecated aliases`` informational row. - ``tests/test_naming_convention.py`` and ``tests/test_aliases.py`` — the ratchet tests are obsolete; the live tree carries zero hyphenated leaves outside the documented permanent exceptions. Tests migrated: - 614 ``invoke([...])`` / ``live_invoke(...)`` calls rewritten to the new paths. - ``@pytest.mark.parametrize`` blocks that enumerated hyphenated legacy names had those entries dropped (or the block was rewritten to test the new sub-group's ``--help``). Plus three ``isinstance(x, (A, B))`` calls flipped to ``isinstance(x, A | B)`` (PEP 604, ruff UP038) — fall-out from the 3.10 floor. Validation: ruff clean, mypy clean, ``pytest`` 2451 passed, coverage 87.63 %. Migration guide and the full mapping live in ``CHANGELOG.md`` (3.0.0 entry) and ``docs/adr/0008-command-naming-convention.md`` (deprecation horizon). --- CHANGELOG.md | 51 +++++ orca_cli/commands/aggregate.py | 10 - orca_cli/commands/alarm.py | 9 - orca_cli/commands/auth.py | 7 - orca_cli/commands/backup.py | 64 ------ orca_cli/commands/cluster.py | 28 --- orca_cli/commands/doctor.py | 46 +--- orca_cli/commands/endpoint_group.py | 9 - orca_cli/commands/flavor.py | 9 - orca_cli/commands/group.py | 9 - orca_cli/commands/image.py | 30 --- orca_cli/commands/loadbalancer.py | 107 ---------- orca_cli/commands/metric.py | 37 +--- orca_cli/commands/network.py | 60 ------ orca_cli/commands/object_store.py | 26 --- orca_cli/commands/placement.py | 89 -------- orca_cli/commands/profile.py | 28 --- orca_cli/commands/qos_policy.py | 18 -- orca_cli/commands/rating.py | 25 +-- orca_cli/commands/role.py | 13 -- orca_cli/commands/secret.py | 7 - orca_cli/commands/security_group.py | 7 - orca_cli/commands/server.py | 30 --- orca_cli/commands/stack.py | 34 +-- orca_cli/commands/trunk.py | 12 -- orca_cli/commands/user.py | 6 - orca_cli/commands/volume.py | 62 ------ orca_cli/commands/zone.py | 22 -- orca_cli/core/aliases.py | 119 ----------- pyproject.toml | 2 +- tests/conftest.py | 10 - tests/test_alarm.py | 33 +-- tests/test_aliases.py | 77 ------- tests/test_app_credentials.py | 13 -- tests/test_application_credential_cli.py | 7 - tests/test_audit.py | 15 -- tests/test_auth_commands.py | 31 ++- tests/test_backup.py | 114 +++------- tests/test_cache.py | 1 - tests/test_catalog.py | 2 - tests/test_cinder_admin.py | 177 +++++++--------- tests/test_cleanup.py | 25 --- tests/test_cli_registration.py | 11 +- tests/test_client.py | 31 --- tests/test_cluster.py | 65 ++---- tests/test_completion.py | 5 - tests/test_completions.py | 7 - tests/test_compute.py | 36 +--- tests/test_config.py | 7 - tests/test_context.py | 1 - tests/test_dns.py | 62 ++---- tests/test_doctor.py | 12 -- tests/test_event.py | 15 -- tests/test_exceptions.py | 3 - tests/test_export.py | 24 --- tests/test_find.py | 16 -- tests/test_flavor.py | 6 - tests/test_floating_ip.py | 20 -- tests/test_hypervisor.py | 3 - tests/test_identity.py | 61 +----- tests/test_image.py | 34 +-- tests/test_image_import_cache.py | 34 ++- tests/test_image_members.py | 40 ++-- tests/test_image_stage_tasks.py | 35 ++-- tests/test_ip_whois.py | 7 - tests/test_keypair.py | 14 -- tests/test_keystone_admin.py | 32 +-- tests/test_keystone_gaps.py | 34 +-- tests/test_loadbalancer.py | 54 ++--- tests/test_main.py | 2 - tests/test_metric.py | 65 ++---- tests/test_naming_convention.py | 179 ---------------- tests/test_network.py | 95 +++------ tests/test_neutron_admin.py | 197 +++++++----------- tests/test_no_click_exceptions_in_commands.py | 1 - .../test_no_private_client_api_in_commands.py | 1 - tests/test_nova_admin.py | 58 +++--- tests/test_object_store.py | 43 +--- tests/test_octavia_admin.py | 169 ++++++--------- tests/test_output.py | 6 - tests/test_overview.py | 5 - tests/test_placement.py | 190 +++++++---------- tests/test_profile.py | 25 +-- tests/test_profile_convert.py | 4 - tests/test_project_cleanup.py | 7 - tests/test_quota.py | 7 - tests/test_rating.py | 26 +-- tests/test_role.py | 10 +- tests/test_secret.py | 41 +--- tests/test_security_group.py | 31 +-- tests/test_server.py | 95 ++------- tests/test_server_clone_boot_mode.py | 3 - tests/test_server_create_boot_mode.py | 4 - tests/test_server_ssh.py | 10 - tests/test_services_dns.py | 63 ------ tests/test_services_image.py | 46 ---- tests/test_services_object_store.py | 29 --- tests/test_services_orchestration.py | 48 ----- tests/test_setup.py | 16 -- tests/test_stack.py | 51 +---- tests/test_usage.py | 5 - tests/test_validators.py | 3 - tests/test_volume.py | 107 +++------- tests/test_volume_backup.py | 44 ++-- tests/test_volume_upload_to_image.py | 23 -- tests/test_wait_and_dryrun.py | 8 - tests/test_watch.py | 7 - tests/test_wizard.py | 21 -- 108 files changed, 718 insertions(+), 3177 deletions(-) delete mode 100644 orca_cli/core/aliases.py delete mode 100644 tests/test_aliases.py delete mode 100644 tests/test_naming_convention.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b50ec45..9670765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,57 @@ All notable changes to orca are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.0] — unreleased + +### Removed (breaking) + +- **Every ADR-0008 deprecated alias is gone.** The 314 hyphenated + legacy command names that 2.0.x → 2.3.x kept registering as + ``deprecated=True`` aliases (each printing a yellow warning to + stderr before forwarding to the new path) are now removed + outright. Scripts that still call e.g. ``orca server attach-volume``, + ``orca placement resource-provider-list``, or + ``orca profile to-openrc`` must switch to the openstackclient + forms (``orca server volume add``, ``orca placement resource-provider list``, + ``orca profile export openrc``). See ADR-0008 for the full mapping; + ``orca doctor --list-deprecated`` on 2.3.x prints the table. +- **``orca_cli/core/aliases``** (private module) is dropped. Its + three exports (``add_command_with_alias``, + ``count_deprecated_aliases``, ``list_deprecated_aliases``) had no + external consumers — the module existed solely to power the + alias-and-deprecate migration, which is now complete. +- **``orca doctor``** loses ``--list-deprecated`` and the + ``ADR-0008: deprecated aliases`` row in its output. Both became + meaningless once the aliases were dropped. +- **Python 3.9 is no longer supported.** Already announced in 2.3.0 + (``build!:`` commit), formally cuts in 3.0.0. Minimum is 3.10. +- **Two ratchet tests removed:** ``tests/test_naming_convention.py`` + and ``tests/test_aliases.py``. The naming-convention check has no + legacy-name whitelist left to ratchet against; the alias-count + sanity check no longer applies. + +### Migration guide + +For ~95 % of legacy invocations the rule is mechanical: +``orca `` becomes +``orca `` (e.g. ``server list-volumes`` → +``server volume list``). A handful of compound-verb leaves keep +their hyphen by design — ``server confirm-resize``, +``server port-forward``, ``volume upload-to-image``, +``zone reverse-lookup``, ``image share-and-accept``, +``floating-ip bulk-release`` (full list in +``docs/adr/0008-command-naming-convention.md`` — *Permanent +exceptions*). + +To audit a script before upgrading, run on 2.3.x: + +``` +orca doctor --list-deprecated +``` + +The table shows the legacy invocation alongside its 3.0.0 +replacement. + ## [2.3.0] — 2026-04-28 ### Added diff --git a/orca_cli/commands/aggregate.py b/orca_cli/commands/aggregate.py index 38fdaa0..3b0ab4d 100644 --- a/orca_cli/commands/aggregate.py +++ b/orca_cli/commands/aggregate.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.exceptions import OrcaCLIError from orca_cli.core.output import console, output_options, print_detail, print_list @@ -117,10 +116,6 @@ def aggregate_host_remove(ctx, aggregate_id, host): console.print(f"[green]Host '{host}' removed from aggregate {aggregate_id}.[/green]") -add_command_with_alias(aggregate, aggregate_host_add, - legacy_name="add-host", primary_path="aggregate host add") -add_command_with_alias(aggregate, aggregate_host_remove, - legacy_name="remove-host", primary_path="aggregate host remove") @aggregate.command("set") @@ -194,8 +189,3 @@ def aggregate_image_cache(ctx, aggregate_id, image_ids): svc = ComputeService(ctx.find_object(OrcaContext).ensure_client()) svc.cache_aggregate_images(aggregate_id, [{"id": iid} for iid in image_ids]) console.print(f"[green]Image caching requested on aggregate {aggregate_id}.[/green]") - - -add_command_with_alias(aggregate, aggregate_image_cache, - legacy_name="cache-image", - primary_path="aggregate image cache") diff --git a/orca_cli/commands/alarm.py b/orca_cli/commands/alarm.py index 798d6ef..e25ec63 100644 --- a/orca_cli/commands/alarm.py +++ b/orca_cli/commands/alarm.py @@ -6,7 +6,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.output import console, output_options, print_detail, print_list from orca_cli.core.validators import validate_id @@ -281,10 +280,6 @@ def alarm_state_set(ctx, alarm_id, state): console.print(f"Alarm [bold]{alarm_id}[/bold] state set to [bold]{state}[/bold].") -add_command_with_alias(alarm, alarm_state_get, - legacy_name="state-get", primary_path="alarm state get") -add_command_with_alias(alarm, alarm_state_set, - legacy_name="state-set", primary_path="alarm state set") # ══════════════════════════════════════════════════════════════════════════════ @@ -353,7 +348,3 @@ def alarm_quota_set(ctx, project_id, alarm_quota_value): } AlarmService(client).update_quota(body) console.print(f"Quota for project [bold]{project_id}[/bold] set to [bold]{alarm_quota_value}[/bold] alarms.") - - -add_command_with_alias(alarm, alarm_quota_set, - legacy_name="quota-set", primary_path="alarm quota set") diff --git a/orca_cli/commands/auth.py b/orca_cli/commands/auth.py index 078e348..7a151ee 100644 --- a/orca_cli/commands/auth.py +++ b/orca_cli/commands/auth.py @@ -6,7 +6,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.config import ( _find_clouds_yaml, _load_clouds_yaml, @@ -315,9 +314,3 @@ def auth_token_revoke(ctx: click.Context, token: str) -> None: client = ctx.find_object(OrcaContext).ensure_client() IdentityService(client).revoke_token(token) console.print("[green]Token revoked.[/green]") - - -add_command_with_alias(auth, auth_token_debug, - legacy_name="token-debug", primary_path="auth token debug") -add_command_with_alias(auth, auth_token_revoke, - legacy_name="token-revoke", primary_path="auth token revoke") diff --git a/orca_cli/commands/backup.py b/orca_cli/commands/backup.py index 5b370d5..5a23fe2 100644 --- a/orca_cli/commands/backup.py +++ b/orca_cli/commands/backup.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.output import console, output_options, print_detail, print_list from orca_cli.services.backup import BackupService @@ -594,72 +593,9 @@ def action_delete(ctx: click.Context, action_id: str, yes: bool) -> None: # ══════════════════════════════════════════════════════════════════════════ # Action aliases -add_command_with_alias(backup, action_list, - legacy_name="action-list", - primary_path="backup action list") -add_command_with_alias(backup, action_show, - legacy_name="action-show", - primary_path="backup action show") -add_command_with_alias(backup, action_create, - legacy_name="action-create", - primary_path="backup action create") -add_command_with_alias(backup, action_delete, - legacy_name="action-delete", - primary_path="backup action delete") # Client aliases -add_command_with_alias(backup, client_list, - legacy_name="client-list", - primary_path="backup client list") -add_command_with_alias(backup, client_show, - legacy_name="client-show", - primary_path="backup client show") -add_command_with_alias(backup, client_register, - legacy_name="client-register", - primary_path="backup client register") -add_command_with_alias(backup, client_delete, - legacy_name="client-delete", - primary_path="backup client delete") # Job aliases -add_command_with_alias(backup, job_list, - legacy_name="job-list", - primary_path="backup job list") -add_command_with_alias(backup, job_show, - legacy_name="job-show", - primary_path="backup job show") -add_command_with_alias(backup, job_create, - legacy_name="job-create", - primary_path="backup job create") -add_command_with_alias(backup, job_start, - legacy_name="job-start", - primary_path="backup job start") -add_command_with_alias(backup, job_stop, - legacy_name="job-stop", - primary_path="backup job stop") -add_command_with_alias(backup, job_delete, - legacy_name="job-delete", - primary_path="backup job delete") # Session aliases -add_command_with_alias(backup, session_list, - legacy_name="session-list", - primary_path="backup session list") -add_command_with_alias(backup, session_show, - legacy_name="session-show", - primary_path="backup session show") -add_command_with_alias(backup, session_create, - legacy_name="session-create", - primary_path="backup session create") -add_command_with_alias(backup, session_add_job, - legacy_name="session-add-job", - primary_path="backup session add-job") -add_command_with_alias(backup, session_remove_job, - legacy_name="session-remove-job", - primary_path="backup session remove-job") -add_command_with_alias(backup, session_start, - legacy_name="session-start", - primary_path="backup session start") -add_command_with_alias(backup, session_delete, - legacy_name="session-delete", - primary_path="backup session delete") diff --git a/orca_cli/commands/cluster.py b/orca_cli/commands/cluster.py index ca3b9c5..67282c2 100644 --- a/orca_cli/commands/cluster.py +++ b/orca_cli/commands/cluster.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.output import console, output_options, print_detail, print_list from orca_cli.services.container_infra import ContainerInfraService @@ -449,32 +448,5 @@ def nodegroup_delete(ctx: click.Context, cluster_id: str, nodegroup_id: str, # ══════════════════════════════════════════════════════════════════════════ # Nodegroup aliases -add_command_with_alias(cluster, nodegroup_list, - legacy_name="nodegroup-list", - primary_path="cluster nodegroup list") -add_command_with_alias(cluster, nodegroup_show, - legacy_name="nodegroup-show", - primary_path="cluster nodegroup show") -add_command_with_alias(cluster, nodegroup_create, - legacy_name="nodegroup-create", - primary_path="cluster nodegroup create") -add_command_with_alias(cluster, nodegroup_update, - legacy_name="nodegroup-update", - primary_path="cluster nodegroup update") -add_command_with_alias(cluster, nodegroup_delete, - legacy_name="nodegroup-delete", - primary_path="cluster nodegroup delete") # Template aliases -add_command_with_alias(cluster, template_list, - legacy_name="template-list", - primary_path="cluster template list") -add_command_with_alias(cluster, template_show, - legacy_name="template-show", - primary_path="cluster template show") -add_command_with_alias(cluster, template_create, - legacy_name="template-create", - primary_path="cluster template create") -add_command_with_alias(cluster, template_delete, - legacy_name="template-delete", - primary_path="cluster template delete") diff --git a/orca_cli/commands/doctor.py b/orca_cli/commands/doctor.py index 47ccd39..cce3f94 100644 --- a/orca_cli/commands/doctor.py +++ b/orca_cli/commands/doctor.py @@ -17,11 +17,8 @@ @click.option("--cidr", default=None, help="CIDR for auto-created security group rules (--fix). " "Defaults to interactive prompt when a TTY is detected, otherwise 0.0.0.0/0.") -@click.option("--list-deprecated", "list_deprecated", is_flag=True, - help="List every ADR-0008 deprecated alias in the CLI tree and exit " - "(offline — no authentication required).") @click.pass_context -def doctor(ctx: click.Context, fix: bool, cidr: str | None, list_deprecated: bool) -> None: # noqa: C901 +def doctor(ctx: click.Context, fix: bool, cidr: str | None) -> None: # noqa: C901 """Run a pre-deployment health check on your OpenStack environment. Verifies authentication, quota headroom, default security group rules, @@ -44,27 +41,6 @@ def doctor(ctx: click.Context, fix: bool, cidr: str | None, list_deprecated: boo Red ≥ 90% — critical, next deploy may fail """ - # Offline branch: list deprecated aliases and exit before authenticating. - if list_deprecated: - from rich.table import Table - - from orca_cli.core.aliases import list_deprecated_aliases - from orca_cli.main import cli as _cli_root - - rows = sorted(list_deprecated_aliases(_cli_root)) - if not rows: - console.print("[bold green]No deprecated aliases — CLI tree is clean.[/bold green]") - return - - table = Table(title=f"ADR-0008 deprecated aliases ({len(rows)} total)") - table.add_column("Legacy invocation", style="yellow") - table.add_column("Replacement", style="green") - for legacy, replacement in rows: - table.add_row(f"orca {legacy}", f"orca {replacement}") - console.print(table) - console.print("\n[dim]These aliases will be removed in v3.0.0. See ADR-0008.[/dim]") - return - orca_ctx = ctx.find_object(OrcaContext) client = orca_ctx.ensure_client() compute = ComputeService(client) @@ -196,26 +172,6 @@ def _quota(check: str, used: int, total: int, unit: str = "") -> None: else: _info("Network quotas", "Skipped — Neutron unreachable") - # ── 6.5. ADR-0008 deprecated-alias inventory (offline, no API) ────────── - # Surface how many hyphenated-form aliases still live in the CLI tree — - # they are scheduled for removal in v3.0.0 (see ADR-0008 / Deprecation - # horizon). Helps users audit their own scripts before the breaking - # major bump. - try: - from orca_cli.core.aliases import count_deprecated_aliases - from orca_cli.main import cli as _cli_root - n_deprecated = count_deprecated_aliases(_cli_root) - if n_deprecated: - _info( - "ADR-0008: deprecated aliases", - f"{n_deprecated} alias(es) kept for backwards-compat — will be " - "removed in v3.0.0. Run 'orca doctor --list-deprecated' to enumerate.", - ) - else: - _ok("ADR-0008: deprecated aliases", "None — tree is clean") - except Exception as exc: # pragma: no cover — best-effort, never crash doctor - _info("ADR-0008: deprecated aliases", f"Could not introspect tree — {exc}") - # ── 6. Default security group SSH/ICMP (skip if Neutron is down) ─────── if svc_up.get("Neutron (network)", False): try: diff --git a/orca_cli/commands/endpoint_group.py b/orca_cli/commands/endpoint_group.py index 32e8678..a621544 100644 --- a/orca_cli/commands/endpoint_group.py +++ b/orca_cli/commands/endpoint_group.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.output import console, output_options, print_detail, print_list from orca_cli.core.validators import validate_id @@ -162,11 +161,3 @@ def eg_project_remove(ctx, endpoint_group_id, project_id, yes): console.print( f"Project [bold]{project_id}[/bold] removed from endpoint group [bold]{endpoint_group_id}[/bold]." ) - - -add_command_with_alias(endpoint_group, eg_project_add, - legacy_name="add-project", - primary_path="endpoint-group project add") -add_command_with_alias(endpoint_group, eg_project_remove, - legacy_name="remove-project", - primary_path="endpoint-group project remove") diff --git a/orca_cli/commands/flavor.py b/orca_cli/commands/flavor.py index 7c35146..a62721b 100644 --- a/orca_cli/commands/flavor.py +++ b/orca_cli/commands/flavor.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.completions import complete_flavors from orca_cli.core.context import OrcaContext from orca_cli.core.exceptions import OrcaCLIError @@ -246,11 +245,3 @@ def flavor_access_remove(ctx: click.Context, flavor_id: str, project_id: str, ye click.confirm(f"Remove project {project_id} from flavor {flavor_id}?", abort=True) svc.remove_flavor_access(flavor_id, project_id) console.print(f"[green]Project {project_id} access to flavor {flavor_id} revoked.[/green]") - - -add_command_with_alias(flavor, flavor_access_list, - legacy_name="access-list", primary_path="flavor access list") -add_command_with_alias(flavor, flavor_access_add, - legacy_name="access-add", primary_path="flavor access add") -add_command_with_alias(flavor, flavor_access_remove, - legacy_name="access-remove", primary_path="flavor access remove") diff --git a/orca_cli/commands/group.py b/orca_cli/commands/group.py index 2564114..6b87ced 100644 --- a/orca_cli/commands/group.py +++ b/orca_cli/commands/group.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.output import console, output_options, print_detail, print_list from orca_cli.services.identity import IdentityService @@ -164,11 +163,3 @@ def group_member_list(ctx, group_id, output_format, columns, fit_width, max_widt fit_width=fit_width, max_width=max_width, noindent=noindent, empty_msg="No users in this group.", ) - - -add_command_with_alias(group, group_user_add, - legacy_name="add-user", primary_path="group user add") -add_command_with_alias(group, group_user_remove, - legacy_name="remove-user", primary_path="group user remove") -add_command_with_alias(group, group_member_list, - legacy_name="member-list", primary_path="group member list") diff --git a/orca_cli/commands/image.py b/orca_cli/commands/image.py index 1a257f4..80f7971 100644 --- a/orca_cli/commands/image.py +++ b/orca_cli/commands/image.py @@ -11,7 +11,6 @@ from rich.progress import BarColumn, DownloadColumn, Progress, TimeRemainingColumn, TransferSpeedColumn from rich.table import Table -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.client import APIError, AuthenticationError from orca_cli.core.completions import complete_images from orca_cli.core.context import OrcaContext @@ -1536,32 +1535,3 @@ def metadef_rt_assoc_delete(ctx, namespace, resource_type, yes): # ── ADR-0008 deprecated aliases (backward compatibility) ──────────────── - -add_command_with_alias(image, image_tag_add, - legacy_name="tag-add", primary_path="image tag add") -add_command_with_alias(image, image_tag_delete, - legacy_name="tag-delete", primary_path="image tag delete") -add_command_with_alias(image, image_member_list, - legacy_name="member-list", primary_path="image member list") -add_command_with_alias(image, image_member_show, - legacy_name="member-show", primary_path="image member show") -add_command_with_alias(image, image_member_create, - legacy_name="member-create", primary_path="image member create") -add_command_with_alias(image, image_member_delete, - legacy_name="member-delete", primary_path="image member delete") -add_command_with_alias(image, image_member_set, - legacy_name="member-set", primary_path="image member set") -add_command_with_alias(image, image_cache_list, - legacy_name="cache-list", primary_path="image cache list") -add_command_with_alias(image, image_cache_queue, - legacy_name="cache-queue", primary_path="image cache queue") -add_command_with_alias(image, image_cache_delete, - legacy_name="cache-delete", primary_path="image cache delete") -add_command_with_alias(image, image_cache_clear, - legacy_name="cache-clear", primary_path="image cache clear") -add_command_with_alias(image, image_stores_info, - legacy_name="stores-info", primary_path="image stores info") -add_command_with_alias(image, image_task_list, - legacy_name="task-list", primary_path="image task list") -add_command_with_alias(image, image_task_show, - legacy_name="task-show", primary_path="image task show") diff --git a/orca_cli/commands/loadbalancer.py b/orca_cli/commands/loadbalancer.py index 744e355..4dfbb09 100644 --- a/orca_cli/commands/loadbalancer.py +++ b/orca_cli/commands/loadbalancer.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.exceptions import APIError, OrcaCLIError from orca_cli.core.output import console, output_options, print_detail, print_list @@ -1025,109 +1024,3 @@ def amphora_failover(ctx: click.Context, amphora_id: str, yes: bool) -> None: # ── ADR-0008 deprecated aliases (backward compatibility) ── - -add_command_with_alias(loadbalancer, listener_list, - legacy_name="listener-list", - primary_path="loadbalancer listener list") -add_command_with_alias(loadbalancer, listener_show, - legacy_name="listener-show", - primary_path="loadbalancer listener show") -add_command_with_alias(loadbalancer, listener_create, - legacy_name="listener-create", - primary_path="loadbalancer listener create") -add_command_with_alias(loadbalancer, listener_set, - legacy_name="listener-set", - primary_path="loadbalancer listener set") -add_command_with_alias(loadbalancer, listener_delete, - legacy_name="listener-delete", - primary_path="loadbalancer listener delete") -add_command_with_alias(loadbalancer, pool_list, - legacy_name="pool-list", - primary_path="loadbalancer pool list") -add_command_with_alias(loadbalancer, pool_show, - legacy_name="pool-show", - primary_path="loadbalancer pool show") -add_command_with_alias(loadbalancer, pool_create, - legacy_name="pool-create", - primary_path="loadbalancer pool create") -add_command_with_alias(loadbalancer, pool_set, - legacy_name="pool-set", - primary_path="loadbalancer pool set") -add_command_with_alias(loadbalancer, pool_delete, - legacy_name="pool-delete", - primary_path="loadbalancer pool delete") -add_command_with_alias(loadbalancer, member_list, - legacy_name="member-list", - primary_path="loadbalancer member list") -add_command_with_alias(loadbalancer, member_show, - legacy_name="member-show", - primary_path="loadbalancer member show") -add_command_with_alias(loadbalancer, member_add, - legacy_name="member-add", - primary_path="loadbalancer member add") -add_command_with_alias(loadbalancer, member_set, - legacy_name="member-set", - primary_path="loadbalancer member set") -add_command_with_alias(loadbalancer, member_remove, - legacy_name="member-remove", - primary_path="loadbalancer member remove") -add_command_with_alias(loadbalancer, hm_list, - legacy_name="healthmonitor-list", - primary_path="loadbalancer healthmonitor list") -add_command_with_alias(loadbalancer, hm_show, - legacy_name="healthmonitor-show", - primary_path="loadbalancer healthmonitor show") -add_command_with_alias(loadbalancer, hm_create, - legacy_name="healthmonitor-create", - primary_path="loadbalancer healthmonitor create") -add_command_with_alias(loadbalancer, hm_set, - legacy_name="healthmonitor-set", - primary_path="loadbalancer healthmonitor set") -add_command_with_alias(loadbalancer, hm_delete, - legacy_name="healthmonitor-delete", - primary_path="loadbalancer healthmonitor delete") -add_command_with_alias(loadbalancer, l7policy_list, - legacy_name="l7policy-list", - primary_path="loadbalancer l7policy list") -add_command_with_alias(loadbalancer, l7policy_show, - legacy_name="l7policy-show", - primary_path="loadbalancer l7policy show") -add_command_with_alias(loadbalancer, l7policy_create, - legacy_name="l7policy-create", - primary_path="loadbalancer l7policy create") -add_command_with_alias(loadbalancer, l7policy_set, - legacy_name="l7policy-set", - primary_path="loadbalancer l7policy set") -add_command_with_alias(loadbalancer, l7policy_delete, - legacy_name="l7policy-delete", - primary_path="loadbalancer l7policy delete") -add_command_with_alias(loadbalancer, l7rule_list, - legacy_name="l7rule-list", - primary_path="loadbalancer l7rule list") -add_command_with_alias(loadbalancer, l7rule_show, - legacy_name="l7rule-show", - primary_path="loadbalancer l7rule show") -add_command_with_alias(loadbalancer, l7rule_create, - legacy_name="l7rule-create", - primary_path="loadbalancer l7rule create") -add_command_with_alias(loadbalancer, l7rule_set, - legacy_name="l7rule-set", - primary_path="loadbalancer l7rule set") -add_command_with_alias(loadbalancer, l7rule_delete, - legacy_name="l7rule-delete", - primary_path="loadbalancer l7rule delete") -add_command_with_alias(loadbalancer, lb_stats_show, - legacy_name="stats-show", - primary_path="loadbalancer stats show") -add_command_with_alias(loadbalancer, lb_status_show, - legacy_name="status-show", - primary_path="loadbalancer status show") -add_command_with_alias(loadbalancer, amphora_list, - legacy_name="amphora-list", - primary_path="loadbalancer amphora list") -add_command_with_alias(loadbalancer, amphora_show, - legacy_name="amphora-show", - primary_path="loadbalancer amphora show") -add_command_with_alias(loadbalancer, amphora_failover, - legacy_name="amphora-failover", - primary_path="loadbalancer amphora failover") diff --git a/orca_cli/commands/metric.py b/orca_cli/commands/metric.py index 125c9e8..7d093bd 100644 --- a/orca_cli/commands/metric.py +++ b/orca_cli/commands/metric.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.exceptions import OrcaCLIError from orca_cli.core.output import console, output_options, print_detail, print_list @@ -229,7 +228,7 @@ def metric_measures_show(ctx: click.Context, metric_id: str, start: str | None, measures = data if isinstance(data, list) else [] # Filter to valid measure entries only - measures = [m for m in measures if isinstance(m, (list, tuple)) and len(m) >= 3] + measures = [m for m in measures if isinstance(m, list | tuple) and len(m) >= 3] print_list( measures, @@ -490,37 +489,3 @@ def resource_type_delete(ctx: click.Context, resource_type: str, yes: bool) -> N # ── ADR-0008 deprecated aliases (backward compatibility) ──────────────── - -add_command_with_alias(metric, resource_type_list, - legacy_name="resource-type-list", - primary_path="metric resource-type list") -add_command_with_alias(metric, resource_type_show, - legacy_name="resource-type-show", - primary_path="metric resource-type show") -add_command_with_alias(metric, resource_type_create, - legacy_name="resource-type-create", - primary_path="metric resource-type create") -add_command_with_alias(metric, resource_type_delete, - legacy_name="resource-type-delete", - primary_path="metric resource-type delete") -add_command_with_alias(metric, resource_list, - legacy_name="resource-list", - primary_path="metric resource list") -add_command_with_alias(metric, resource_show, - legacy_name="resource-show", - primary_path="metric resource show") -add_command_with_alias(metric, metric_measures_add, - legacy_name="measures-add", - primary_path="metric measures add") -add_command_with_alias(metric, archive_policy_list, - legacy_name="archive-policy-list", - primary_path="metric archive-policy list") -add_command_with_alias(metric, archive_policy_show, - legacy_name="archive-policy-show", - primary_path="metric archive-policy show") -add_command_with_alias(metric, archive_policy_create, - legacy_name="archive-policy-create", - primary_path="metric archive-policy create") -add_command_with_alias(metric, archive_policy_delete, - legacy_name="archive-policy-delete", - primary_path="metric archive-policy delete") diff --git a/orca_cli/commands/network.py b/orca_cli/commands/network.py index c85a3a6..41ae6a9 100644 --- a/orca_cli/commands/network.py +++ b/orca_cli/commands/network.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.completions import complete_networks from orca_cli.core.context import OrcaContext from orca_cli.core.exceptions import OrcaCLIError @@ -1318,62 +1317,3 @@ def network_auto_allocated_topology_delete(ctx, project_id, yes): # ── Deprecated aliases (ADR-0008) ───────────────────────────────────────── - -for _legacy, _primary, _path in [ - ("agent-delete", network_agent.commands["delete"], "network agent delete"), - ("agent-list", network_agent.commands["list"], "network agent list"), - ("agent-set", network_agent.commands["set"], "network agent set"), - ("agent-show", network_agent.commands["show"], "network agent show"), - ("auto-allocated-topology-delete", - network_auto_allocated_topology.commands["delete"], - "network auto-allocated-topology delete"), - ("auto-allocated-topology-show", - network_auto_allocated_topology.commands["show"], - "network auto-allocated-topology show"), - ("port-create", network_port.commands["create"], "network port create"), - ("port-delete", network_port.commands["delete"], "network port delete"), - ("port-list", network_port.commands["list"], "network port list"), - ("port-show", network_port.commands["show"], "network port show"), - ("port-unset", network_port.commands["unset"], "network port unset"), - ("port-update", network_port.commands["update"], "network port update"), - ("rbac-create", network_rbac.commands["create"], "network rbac create"), - ("rbac-delete", network_rbac.commands["delete"], "network rbac delete"), - ("rbac-list", network_rbac.commands["list"], "network rbac list"), - ("rbac-show", network_rbac.commands["show"], "network rbac show"), - ("rbac-update", network_rbac.commands["update"], "network rbac update"), - ("router-create", network_router.commands["create"], "network router create"), - ("router-delete", network_router.commands["delete"], "network router delete"), - ("router-list", network_router.commands["list"], "network router list"), - ("router-show", network_router.commands["show"], "network router show"), - ("router-update", network_router.commands["update"], "network router update"), - ("router-add-interface", - network_router_add.commands["interface"], - "network router add interface"), - ("router-add-route", - network_router_add.commands["route"], - "network router add route"), - ("router-remove-interface", - network_router_remove.commands["interface"], - "network router remove interface"), - ("router-remove-route", - network_router_remove.commands["route"], - "network router remove route"), - ("router-set-gateway", - network_router_set.commands["gateway"], - "network router set gateway"), - ("router-unset-gateway", - network_router_unset.commands["gateway"], - "network router unset gateway"), - ("segment-create", network_segment.commands["create"], "network segment create"), - ("segment-delete", network_segment.commands["delete"], "network segment delete"), - ("segment-list", network_segment.commands["list"], "network segment list"), - ("segment-set", network_segment.commands["set"], "network segment set"), - ("segment-show", network_segment.commands["show"], "network segment show"), - ("subnet-create", network_subnet.commands["create"], "network subnet create"), - ("subnet-delete", network_subnet.commands["delete"], "network subnet delete"), - ("subnet-list", network_subnet.commands["list"], "network subnet list"), - ("subnet-show", network_subnet.commands["show"], "network subnet show"), - ("subnet-update", network_subnet.commands["update"], "network subnet update"), -]: - add_command_with_alias(network, _primary, legacy_name=_legacy, primary_path=_path) -del _legacy, _primary, _path diff --git a/orca_cli/commands/object_store.py b/orca_cli/commands/object_store.py index 57994f8..19af9f2 100644 --- a/orca_cli/commands/object_store.py +++ b/orca_cli/commands/object_store.py @@ -11,7 +11,6 @@ import click from rich.tree import Tree -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.exceptions import OrcaCLIError from orca_cli.core.output import console, output_options, print_detail, print_list @@ -708,28 +707,3 @@ def _add_nodes(parent_tree: Tree, subtree: dict) -> None: # ── ADR-0008 deprecated aliases (backward compatibility) ──────────────── - -add_command_with_alias(object_store, container_list, - legacy_name="container-list", - primary_path="object container list") -add_command_with_alias(object_store, container_show, - legacy_name="container-show", - primary_path="object container show") -add_command_with_alias(object_store, container_create, - legacy_name="container-create", - primary_path="object container create") -add_command_with_alias(object_store, container_delete, - legacy_name="container-delete", - primary_path="object container delete") -add_command_with_alias(object_store, container_set, - legacy_name="container-set", - primary_path="object container set") -add_command_with_alias(object_store, container_save, - legacy_name="container-save", - primary_path="object container save") -add_command_with_alias(object_store, object_account_set, - legacy_name="account-set", - primary_path="object account set") -add_command_with_alias(object_store, object_account_unset, - legacy_name="account-unset", - primary_path="object account unset") diff --git a/orca_cli/commands/placement.py b/orca_cli/commands/placement.py index 3961591..4781bad 100644 --- a/orca_cli/commands/placement.py +++ b/orca_cli/commands/placement.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.output import console, output_options, print_detail, print_list from orca_cli.core.validators import validate_id @@ -693,91 +692,3 @@ def rp_inventory_delete_all(ctx, uuid, yes): # ── ADR-0008 deprecated aliases (backward compatibility) ── - -add_command_with_alias(placement, rp_list, - legacy_name="resource-provider-list", - primary_path="placement resource-provider list") -add_command_with_alias(placement, rp_show, - legacy_name="resource-provider-show", - primary_path="placement resource-provider show") -add_command_with_alias(placement, rp_create, - legacy_name="resource-provider-create", - primary_path="placement resource-provider create") -add_command_with_alias(placement, rp_set, - legacy_name="resource-provider-set", - primary_path="placement resource-provider set") -add_command_with_alias(placement, rp_delete, - legacy_name="resource-provider-delete", - primary_path="placement resource-provider delete") -add_command_with_alias(placement, rp_inventory_list, - legacy_name="resource-provider-inventory-list", - primary_path="placement resource-provider inventory-list") -add_command_with_alias(placement, rp_inventory_set, - legacy_name="resource-provider-inventory-set", - primary_path="placement resource-provider inventory-set") -add_command_with_alias(placement, rp_inventory_delete, - legacy_name="resource-provider-inventory-delete", - primary_path="placement resource-provider inventory-delete") -add_command_with_alias(placement, rp_inventory_show, - legacy_name="resource-provider-inventory-show", - primary_path="placement resource-provider inventory-show") -add_command_with_alias(placement, rp_inventory_delete_all, - legacy_name="resource-provider-inventory-delete-all", - primary_path="placement resource-provider inventory-delete-all") -add_command_with_alias(placement, rp_usage, - legacy_name="resource-provider-usage", - primary_path="placement resource-provider usage") -add_command_with_alias(placement, rp_trait_list, - legacy_name="resource-provider-trait-list", - primary_path="placement resource-provider trait-list") -add_command_with_alias(placement, rp_trait_set, - legacy_name="resource-provider-trait-set", - primary_path="placement resource-provider trait-set") -add_command_with_alias(placement, rp_trait_delete, - legacy_name="resource-provider-trait-delete", - primary_path="placement resource-provider trait-delete") -add_command_with_alias(placement, rp_aggregate_list, - legacy_name="resource-provider-aggregate-list", - primary_path="placement resource-provider aggregate-list") -add_command_with_alias(placement, rp_aggregate_set, - legacy_name="resource-provider-aggregate-set", - primary_path="placement resource-provider aggregate-set") -add_command_with_alias(placement, rp_aggregate_delete, - legacy_name="resource-provider-aggregate-delete", - primary_path="placement resource-provider aggregate-delete") -add_command_with_alias(placement, usage_list, - legacy_name="usage-list", - primary_path="placement usage list") -add_command_with_alias(placement, rc_list, - legacy_name="resource-class-list", - primary_path="placement resource-class list") -add_command_with_alias(placement, rc_show, - legacy_name="resource-class-show", - primary_path="placement resource-class show") -add_command_with_alias(placement, rc_create, - legacy_name="resource-class-create", - primary_path="placement resource-class create") -add_command_with_alias(placement, rc_delete, - legacy_name="resource-class-delete", - primary_path="placement resource-class delete") -add_command_with_alias(placement, trait_list, - legacy_name="trait-list", - primary_path="placement trait list") -add_command_with_alias(placement, trait_create, - legacy_name="trait-create", - primary_path="placement trait create") -add_command_with_alias(placement, trait_delete, - legacy_name="trait-delete", - primary_path="placement trait delete") -add_command_with_alias(placement, allocation_show, - legacy_name="allocation-show", - primary_path="placement allocation show") -add_command_with_alias(placement, allocation_delete, - legacy_name="allocation-delete", - primary_path="placement allocation delete") -add_command_with_alias(placement, allocation_set, - legacy_name="allocation-set", - primary_path="placement allocation set") -add_command_with_alias(placement, allocation_candidate_list, - legacy_name="allocation-candidate-list", - primary_path="placement allocation candidate-list") diff --git a/orca_cli/commands/profile.py b/orca_cli/commands/profile.py index b4a65a7..21d5c10 100644 --- a/orca_cli/commands/profile.py +++ b/orca_cli/commands/profile.py @@ -8,7 +8,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.config import ( _find_clouds_yaml, _normalise_legacy_keys, @@ -855,30 +854,3 @@ def _os_env_to_cfg(env: dict[str, str]) -> dict[str, str]: if env.get("OS_INSECURE"): cfg["insecure"] = env["OS_INSECURE"] return cfg - - -# ══════════════════════════════════════════════════════════════════════════ -# Deprecated hyphenated aliases (ADR-0008 — backwards compatibility) -# ══════════════════════════════════════════════════════════════════════════ - -add_command_with_alias(profile, profile_set_color, - legacy_name="set-color", - primary_path="profile color set") -add_command_with_alias(profile, profile_set_region, - legacy_name="set-region", - primary_path="profile region set") -add_command_with_alias(profile, profile_regions, - legacy_name="regions", - primary_path="profile region list") -add_command_with_alias(profile, profile_to_openrc, - legacy_name="to-openrc", - primary_path="profile export openrc") -add_command_with_alias(profile, profile_to_clouds, - legacy_name="to-clouds", - primary_path="profile export clouds") -add_command_with_alias(profile, profile_from_openrc, - legacy_name="from-openrc", - primary_path="profile import openrc") -add_command_with_alias(profile, profile_from_clouds, - legacy_name="from-clouds", - primary_path="profile import clouds") diff --git a/orca_cli/commands/qos_policy.py b/orca_cli/commands/qos_policy.py index 74734d9..9847348 100644 --- a/orca_cli/commands/qos_policy.py +++ b/orca_cli/commands/qos_policy.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.exceptions import OrcaCLIError from orca_cli.core.output import console, output_options, print_detail, print_list @@ -238,20 +237,3 @@ def qos_rule_delete(ctx, policy_id, rule_id, rule_type, yes): # ── ADR-0008 deprecated aliases (backward compatibility) ──────────────── - -add_command_with_alias(qos_policy, qos_policy_list, - legacy_name="policy-list", primary_path="qos policy list") -add_command_with_alias(qos_policy, qos_policy_show, - legacy_name="policy-show", primary_path="qos policy show") -add_command_with_alias(qos_policy, qos_policy_create, - legacy_name="policy-create", primary_path="qos policy create") -add_command_with_alias(qos_policy, qos_policy_set, - legacy_name="policy-set", primary_path="qos policy set") -add_command_with_alias(qos_policy, qos_policy_delete, - legacy_name="policy-delete", primary_path="qos policy delete") -add_command_with_alias(qos_policy, qos_rule_list, - legacy_name="rule-list", primary_path="qos rule list") -add_command_with_alias(qos_policy, qos_rule_create, - legacy_name="rule-create", primary_path="qos rule create") -add_command_with_alias(qos_policy, qos_rule_delete, - legacy_name="rule-delete", primary_path="qos rule delete") diff --git a/orca_cli/commands/rating.py b/orca_cli/commands/rating.py index ffe6abb..aa28c9f 100644 --- a/orca_cli/commands/rating.py +++ b/orca_cli/commands/rating.py @@ -7,7 +7,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.exceptions import OrcaCLIError from orca_cli.core.output import console, output_options, print_detail, print_list @@ -216,7 +215,7 @@ def rating_quote(ctx, resources): raise click.BadParameter(f"Invalid JSON: {exc}", param_hint="--resource") from exc payload = {"resources": body_items} data = svc.create_quote(payload) - if isinstance(data, (int, float, str)): + if isinstance(data, int | float | str): console.print(f"Estimated price: [bold]{data}[/bold]") else: console.print_json(json.dumps(data, indent=2, default=str)) @@ -633,25 +632,3 @@ def hm_group_delete(ctx, group_id, yes): # ── ADR-0008 deprecated aliases (backward compatibility) ──────────────── - -add_command_with_alias(rating, rating_metric_list, - legacy_name="metric-list", - primary_path="rating metric list") -add_command_with_alias(rating, rating_metric_show, - legacy_name="metric-show", - primary_path="rating metric show") -add_command_with_alias(rating, rating_module_list, - legacy_name="module-list", - primary_path="rating module list") -add_command_with_alias(rating, rating_module_show, - legacy_name="module-show", - primary_path="rating module show") -add_command_with_alias(rating, rating_module_enable, - legacy_name="module-enable", - primary_path="rating module enable") -add_command_with_alias(rating, rating_module_disable, - legacy_name="module-disable", - primary_path="rating module disable") -add_command_with_alias(rating, rating_module_set_priority, - legacy_name="module-set-priority", - primary_path="rating module set-priority") diff --git a/orca_cli/commands/role.py b/orca_cli/commands/role.py index e33c1bd..2f347c5 100644 --- a/orca_cli/commands/role.py +++ b/orca_cli/commands/role.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.exceptions import OrcaCLIError from orca_cli.core.output import console, output_options, print_detail, print_list @@ -286,18 +285,6 @@ def role_implied_delete(ctx, prior_role_id, implied_role_id, yes): console.print("Implied role relationship deleted.") -add_command_with_alias(role, role_assignment_list, - legacy_name="assignment-list", - primary_path="role assignment list") -add_command_with_alias(role, role_implied_list, - legacy_name="implied-list", - primary_path="role implied list") -add_command_with_alias(role, role_implied_create, - legacy_name="implied-create", - primary_path="role implied create") -add_command_with_alias(role, role_implied_delete, - legacy_name="implied-delete", - primary_path="role implied delete") # ── set ─────────────────────────────────────────────────────────────────── diff --git a/orca_cli/commands/secret.py b/orca_cli/commands/secret.py index a3b83dc..36520be 100644 --- a/orca_cli/commands/secret.py +++ b/orca_cli/commands/secret.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.exceptions import OrcaCLIError from orca_cli.core.output import console, output_options, print_detail, print_list @@ -300,12 +299,6 @@ def secret_acl_delete(ctx: click.Context, secret_id: str, yes: bool) -> None: console.print(f"[green]ACL deleted for secret {secret_id}.[/green]") -add_command_with_alias(secret, secret_acl_get, - legacy_name="acl-get", primary_path="secret acl get") -add_command_with_alias(secret, secret_acl_set, - legacy_name="acl-set", primary_path="secret acl set") -add_command_with_alias(secret, secret_acl_delete, - legacy_name="acl-delete", primary_path="secret acl delete") # ══════════════════════════════════════════════════════════════════════════ diff --git a/orca_cli/commands/security_group.py b/orca_cli/commands/security_group.py index 75970bb..bbc7111 100644 --- a/orca_cli/commands/security_group.py +++ b/orca_cli/commands/security_group.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.output import console, output_options, print_list from orca_cli.core.validators import validate_id @@ -241,12 +240,6 @@ def sg_rule_delete(ctx: click.Context, rule_id: str, yes: bool) -> None: console.print(f"[green]Rule {rule_id} deleted.[/green]") -add_command_with_alias(security_group, sg_rule_add, - legacy_name="rule-add", - primary_path="security-group rule add") -add_command_with_alias(security_group, sg_rule_delete, - legacy_name="rule-delete", - primary_path="security-group rule delete") # ── cleanup ────────────────────────────────────────────────────────────── diff --git a/orca_cli/commands/server.py b/orca_cli/commands/server.py index 6e89b25..72de87c 100644 --- a/orca_cli/commands/server.py +++ b/orca_cli/commands/server.py @@ -11,7 +11,6 @@ import click from orca_cli.core import cache -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.completions import ( complete_flavors, complete_images, @@ -2279,32 +2278,3 @@ def server_unset(ctx: click.Context, server_id: str, # existing scripts keep working. The aliases are tagged ``deprecated`` # in --help and emit a stderr warning pointing at the new path. # Targeted for removal in v2.0. - -for _legacy, _primary, _path in [ - ("add-fixed-ip", server_add_fixed_ip, "server add fixed-ip"), - ("remove-fixed-ip", server_remove_fixed_ip, "server remove fixed-ip"), - ("add-port", server_add_port, "server add port"), - ("remove-port", server_remove_port, "server remove port"), - ("add-network", server_add_network, "server add network"), - ("remove-network", server_remove_network, "server remove network"), - ("add-security-group", server_add_security_group, "server add security-group"), - ("remove-security-group", server_remove_security_group, "server remove security-group"), - ("attach-volume", server_attach_volume, "server add volume"), - ("detach-volume", server_detach_volume, "server remove volume"), - ("detach-interface", server_detach_interface, "server remove interface"), - ("list-volumes", server_list_volumes, "server volume list"), - ("list-interfaces", server_list_interfaces, "server interface list"), - ("console-log", server_console_log, "server console log"), - ("console-url", server_console_url, "server console url"), - ("create-image", server_create_image, "server image create"), - ("metadata-list", server_metadata_list, "server metadata list"), - ("tag-list", server_tag_list, "server tag list"), - ("migration-list", server_migration_list, "server migration list"), - ("migration-show", server_migration_show, "server migration show"), - ("migration-abort", server_migration_abort, "server migration abort"), - ("migration-force-complete", server_migration_force_complete, - "server migration force-complete"), - ("dump-create", server_dump_create, "server dump create"), -]: - add_command_with_alias(server, _primary, legacy_name=_legacy, primary_path=_path) -del _legacy, _primary, _path diff --git a/orca_cli/commands/stack.py b/orca_cli/commands/stack.py index 01af264..1db023d 100644 --- a/orca_cli/commands/stack.py +++ b/orca_cli/commands/stack.py @@ -10,7 +10,6 @@ import click import yaml -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.exceptions import OrcaCLIError from orca_cli.core.output import console, output_options, print_detail, print_list @@ -95,7 +94,7 @@ def _stringify_dates(obj: object) -> object: return {k: _stringify_dates(v) for k, v in obj.items()} if isinstance(obj, list): return [_stringify_dates(i) for i in obj] - if isinstance(obj, (datetime.datetime, datetime.date)): + if isinstance(obj, datetime.datetime | datetime.date): return obj.isoformat() return obj @@ -1076,34 +1075,3 @@ def stack_resource_metadata(ctx, stack_name_or_id, resource_name): # ── ADR-0008 deprecated aliases (backward compatibility) ──────────────── - -add_command_with_alias(stack, resource_list, - legacy_name="resource-list", - primary_path="stack resource list") -add_command_with_alias(stack, resource_show, - legacy_name="resource-show", - primary_path="stack resource show") -add_command_with_alias(stack, event_list, - legacy_name="event-list", - primary_path="stack event list") -add_command_with_alias(stack, event_show, - legacy_name="event-show", - primary_path="stack event show") -add_command_with_alias(stack, output_list, - legacy_name="output-list", - primary_path="stack output list") -add_command_with_alias(stack, output_show, - legacy_name="output-show", - primary_path="stack output show") -add_command_with_alias(stack, template_show, - legacy_name="template-show", - primary_path="stack template show") -add_command_with_alias(stack, template_validate, - legacy_name="template-validate", - primary_path="stack template validate") -add_command_with_alias(stack, stack_resource_type_list, - legacy_name="resource-type-list", - primary_path="stack resource-type list") -add_command_with_alias(stack, stack_resource_type_show, - legacy_name="resource-type-show", - primary_path="stack resource-type show") diff --git a/orca_cli/commands/trunk.py b/orca_cli/commands/trunk.py index 22b0d9b..6c96495 100644 --- a/orca_cli/commands/trunk.py +++ b/orca_cli/commands/trunk.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.output import console, output_options, print_detail, print_list from orca_cli.core.validators import validate_id @@ -175,14 +174,3 @@ def trunk_remove_subport(ctx, trunk_id, port_id, yes): click.confirm(f"Remove sub-port {port_id} from trunk {trunk_id}?", abort=True) svc.remove_trunk_subports(trunk_id, [{"port_id": port_id}]) console.print(f"[green]Sub-port {port_id} removed from trunk {trunk_id}.[/green]") - - -add_command_with_alias(trunk, trunk_subport_list, - legacy_name="subport-list", - primary_path="trunk subport list") -add_command_with_alias(trunk, trunk_add_subport, - legacy_name="add-subport", - primary_path="trunk subport add") -add_command_with_alias(trunk, trunk_remove_subport, - legacy_name="remove-subport", - primary_path="trunk subport remove") diff --git a/orca_cli/commands/user.py b/orca_cli/commands/user.py index 76ceb60..0649d30 100644 --- a/orca_cli/commands/user.py +++ b/orca_cli/commands/user.py @@ -4,7 +4,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.output import console, output_options, print_detail, print_list from orca_cli.services.identity import IdentityService @@ -165,8 +164,3 @@ def user_password_set(ctx, user_id, password): svc = IdentityService(ctx.find_object(OrcaContext).ensure_client()) svc.update_user(user_id, {"password": password}) console.print(f"[green]Password updated for user {user_id}.[/green]") - - -add_command_with_alias(user, user_password_set, - legacy_name="set-password", - primary_path="user password set") diff --git a/orca_cli/commands/volume.py b/orca_cli/commands/volume.py index 6ca0b35..c70cf23 100644 --- a/orca_cli/commands/volume.py +++ b/orca_cli/commands/volume.py @@ -9,7 +9,6 @@ from orca_cli.commands.image import _parse_property from orca_cli.core import cache -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.completions import complete_volumes from orca_cli.core.context import OrcaContext from orca_cli.core.exceptions import OrcaCLIError @@ -2211,67 +2210,6 @@ def volume_group_type_delete(ctx: click.Context, group_type_id: str, yes: bool) # so existing scripts keep working. Marked ``deprecated`` in --help and # emit a stderr warning. Targeted for removal in v2.0. -for _legacy, _primary, _path in [ - ("snapshot-list", volume_snapshot.commands["list"], "volume snapshot list"), - ("snapshot-show", volume_snapshot.commands["show"], "volume snapshot show"), - ("snapshot-create", volume_snapshot.commands["create"], "volume snapshot create"), - ("snapshot-delete", volume_snapshot.commands["delete"], "volume snapshot delete"), - ("snapshot-set", volume_snapshot.commands["set"], "volume snapshot set"), - ("backup-list", volume_backup.commands["list"], "volume backup list"), - ("backup-show", volume_backup.commands["show"], "volume backup show"), - ("backup-create", volume_backup.commands["create"], "volume backup create"), - ("backup-delete", volume_backup.commands["delete"], "volume backup delete"), - ("backup-restore", volume_backup.commands["restore"], "volume backup restore"), - ("type-list", volume_type.commands["list"], "volume type list"), - ("type-show", volume_type.commands["show"], "volume type show"), - ("type-create", volume_type.commands["create"], "volume type create"), - ("type-set", volume_type.commands["set"], "volume type set"), - ("type-delete", volume_type.commands["delete"], "volume type delete"), - ("type-access-list", volume_type_access.commands["list"], "volume type access list"), - ("type-access-add", volume_type_access.commands["add"], "volume type access add"), - ("type-access-remove", volume_type_access.commands["remove"], "volume type access remove"), - ("transfer-create", volume_transfer.commands["create"], "volume transfer create"), - ("transfer-list", volume_transfer.commands["list"], "volume transfer list"), - ("transfer-show", volume_transfer.commands["show"], "volume transfer show"), - ("transfer-accept", volume_transfer.commands["accept"], "volume transfer accept"), - ("transfer-delete", volume_transfer.commands["delete"], "volume transfer delete"), - ("qos-list", volume_qos.commands["list"], "volume qos list"), - ("qos-show", volume_qos.commands["show"], "volume qos show"), - ("qos-create", volume_qos.commands["create"], "volume qos create"), - ("qos-set", volume_qos.commands["set"], "volume qos set"), - ("qos-delete", volume_qos.commands["delete"], "volume qos delete"), - ("qos-associate", volume_qos.commands["associate"], "volume qos associate"), - ("qos-disassociate", volume_qos.commands["disassociate"], "volume qos disassociate"), - ("service-list", volume_service.commands["list"], "volume service list"), - ("service-set", volume_service.commands["set"], "volume service set"), - ("message-list", volume_message.commands["list"], "volume message list"), - ("message-show", volume_message.commands["show"], "volume message show"), - ("message-delete", volume_message.commands["delete"], "volume message delete"), - ("attachment-list", volume_attachment.commands["list"], "volume attachment list"), - ("attachment-show", volume_attachment.commands["show"], "volume attachment show"), - ("attachment-delete", volume_attachment.commands["delete"], "volume attachment delete"), - ("attachment-create", volume_attachment.commands["create"], "volume attachment create"), - ("attachment-set", volume_attachment.commands["set"], "volume attachment set"), - ("attachment-complete", volume_attachment.commands["complete"], "volume attachment complete"), - ("group-list", volume_group.commands["list"], "volume group list"), - ("group-show", volume_group.commands["show"], "volume group show"), - ("group-create", volume_group.commands["create"], "volume group create"), - ("group-update", volume_group.commands["update"], "volume group update"), - ("group-delete", volume_group.commands["delete"], "volume group delete"), - ("group-snapshot-list", volume_group_snapshot.commands["list"], "volume group snapshot list"), - ("group-snapshot-show", volume_group_snapshot.commands["show"], "volume group snapshot show"), - ("group-snapshot-create", volume_group_snapshot.commands["create"], "volume group snapshot create"), - ("group-snapshot-delete", volume_group_snapshot.commands["delete"], "volume group snapshot delete"), - ("group-type-list", volume_group_type.commands["list"], "volume group type list"), - ("group-type-show", volume_group_type.commands["show"], "volume group type show"), - ("group-type-create", volume_group_type.commands["create"], "volume group type create"), - ("group-type-set", volume_group_type.commands["set"], "volume group type set"), - ("group-type-unset", volume_group_type.commands["unset"], "volume group type unset"), - ("group-type-delete", volume_group_type.commands["delete"], "volume group type delete"), - ("revert-to-snapshot", volume_snapshot.commands["revert"], "volume snapshot revert"), -]: - add_command_with_alias(volume, _primary, legacy_name=_legacy, primary_path=_path) -del _legacy, _primary, _path @volume.group("pool") diff --git a/orca_cli/commands/zone.py b/orca_cli/commands/zone.py index 2fd3c8a..28402e1 100644 --- a/orca_cli/commands/zone.py +++ b/orca_cli/commands/zone.py @@ -6,7 +6,6 @@ import click -from orca_cli.core.aliases import add_command_with_alias from orca_cli.core.context import OrcaContext from orca_cli.core.output import console, output_options, print_detail, print_list from orca_cli.core.validators import safe_output_path, validate_id @@ -582,27 +581,6 @@ def zone_tld_delete(ctx: click.Context, tld_id: str, yes: bool) -> None: # ── ADR-0008 deprecated aliases (backward compatibility) ──────────────── -add_command_with_alias(zone, zone_tld_list, - legacy_name="tld-list", primary_path="zone tld list") -add_command_with_alias(zone, zone_tld_create, - legacy_name="tld-create", primary_path="zone tld create") -add_command_with_alias(zone, zone_tld_delete, - legacy_name="tld-delete", primary_path="zone tld delete") -add_command_with_alias(zone, zone_transfer_request_create, - legacy_name="transfer-request-create", - primary_path="zone transfer request create") -add_command_with_alias(zone, zone_transfer_request_list, - legacy_name="transfer-request-list", - primary_path="zone transfer request list") -add_command_with_alias(zone, zone_transfer_request_show, - legacy_name="transfer-request-show", - primary_path="zone transfer request show") -add_command_with_alias(zone, zone_transfer_request_delete, - legacy_name="transfer-request-delete", - primary_path="zone transfer request delete") -add_command_with_alias(zone, zone_transfer_accept, - legacy_name="transfer-accept", - primary_path="zone transfer accept") # ══════════════════════════════════════════════════════════════════════ diff --git a/orca_cli/core/aliases.py b/orca_cli/core/aliases.py deleted file mode 100644 index 4c86e5d..0000000 --- a/orca_cli/core/aliases.py +++ /dev/null @@ -1,119 +0,0 @@ -"""Click helpers for ADR-0008 alias-and-deprecate migrations. - -Used when a command is renamed (typically from a legacy hyphenated -form like ``server list-volumes`` to the openstackclient-aligned -``server volume list``). The new name becomes the primary entry; the -old name is registered on the same callback under an -``alias_for=`` short-help marker so users see they should switch. -""" - -from __future__ import annotations - -import click - - -class _DeprecatedAliasCommand(click.Command): - """Wraps a Click command and prints a one-line deprecation hint - on stderr before invoking the underlying callback. - """ - - def __init__(self, command: click.Command, *, replacement: str) -> None: - super().__init__( - name=command.name, - callback=command.callback, - params=command.params, - help=command.help, - epilog=command.epilog, - short_help=f"[deprecated, use '{replacement}' instead]", - options_metavar=command.options_metavar, - add_help_option=command.add_help_option, - no_args_is_help=command.no_args_is_help, - hidden=command.hidden, - deprecated=True, - context_settings=command.context_settings, - ) - self._replacement = replacement - - def invoke(self, ctx: click.Context) -> object: - click.secho( - f"warning: '{self.name}' is deprecated; use '{self._replacement}' instead.", - fg="yellow", err=True, - ) - return super().invoke(ctx) - - -def add_command_with_alias( - group: click.Group, - primary: click.Command, - *, - legacy_name: str, - primary_path: str, -) -> None: - """Expose *primary* under *legacy_name* on *group* as a deprecated alias. - - The primary command is **not** re-attached to *group* — the caller - is expected to have already registered it where it logically - belongs (typically a sub-group of *group*, e.g. - ``server.add.port`` while the alias lives directly on ``server``). - - Args: - group: the Click group that should expose the legacy alias. - primary: the convention-compliant command object the alias - forwards to. - legacy_name: the historical hyphenated name to keep working. - primary_path: the full new path shown in the deprecation hint - (e.g. ``"server add port"``). - """ - if legacy_name == primary.name and group.commands.get(legacy_name) is primary: - return # nothing to alias — the command is already at the canonical spot - alias = _DeprecatedAliasCommand(primary, replacement=primary_path) - alias.name = legacy_name - group.add_command(alias, name=legacy_name) - - -def _iter_subcommands(group: click.Group) -> list[tuple[str, click.Command]]: - """Yield ``(name, command)`` for every direct child of *group*. - - Uses ``list_commands`` / ``get_command`` rather than ``group.commands`` - so it works on lazy groups (e.g. the top-level orca CLI's - ``LazyOrcaGroup`` only materialises modules on demand). - """ - out: list[tuple[str, click.Command]] = [] - for name in group.list_commands(None): - cmd = group.get_command(None, name) - if cmd is not None: - out.append((name, cmd)) - return out - - -def count_deprecated_aliases(group: click.Group) -> int: - """Recursively count :class:`_DeprecatedAliasCommand` entries in *group*. - - Used by ``orca doctor`` to surface how many ADR-0008 deprecated - aliases remain in the tree — that count is what disappears at the - next major bump (see ADR-0008 — *Deprecation horizon*). - """ - n = 0 - for _, cmd in _iter_subcommands(group): - if isinstance(cmd, _DeprecatedAliasCommand): - n += 1 - elif isinstance(cmd, click.Group): - n += count_deprecated_aliases(cmd) - return n - - -def list_deprecated_aliases(group: click.Group, prefix: str = "") -> list[tuple[str, str]]: - """Recursively list ``(full_path, replacement)`` for every alias. - - *full_path* is the legacy invocation a user would type (e.g. - ``"placement resource-provider-list"``); *replacement* is the - primary path the alias forwards to. - """ - out: list[tuple[str, str]] = [] - for name, cmd in _iter_subcommands(group): - full = f"{prefix} {name}".strip() - if isinstance(cmd, _DeprecatedAliasCommand): - out.append((full, cmd._replacement)) - elif isinstance(cmd, click.Group): - out.extend(list_deprecated_aliases(cmd, prefix=full)) - return out diff --git a/pyproject.toml b/pyproject.toml index 2b3a8a4..463aa3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "orca-openstackclient" -version = "2.3.0" +version = "3.0.0" description = "orca — OpenStack Rich Command-line Alternative" authors = ["Kevin Allioli"] license = "Apache-2.0" diff --git a/tests/conftest.py b/tests/conftest.py index d6d1171..2dcec71 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,7 +17,6 @@ def runner(): """Click CLI test runner.""" return CliRunner() - @pytest.fixture def invoke(runner): """Shortcut: runner.invoke bound to the orca CLI group.""" @@ -25,7 +24,6 @@ def _invoke(*args, **kwargs): return runner.invoke(cli, *args, catch_exceptions=False, **kwargs) return _invoke - # ── Fake config directory ─────────────────────────────────────────────── @pytest.fixture @@ -37,7 +35,6 @@ def config_dir(tmp_path, monkeypatch): monkeypatch.setattr("orca_cli.core.config.CONFIG_FILE", cfg_dir / "config.yaml") return cfg_dir - @pytest.fixture def sample_profile(): """A complete orca profile dict (new canonical keys).""" @@ -50,7 +47,6 @@ def sample_profile(): "insecure": "true", } - @pytest.fixture def legacy_profile(): """A legacy orca profile dict (old domain_id / project_id keys).""" @@ -63,7 +59,6 @@ def legacy_profile(): "insecure": "true", } - @pytest.fixture def write_config(config_dir): """Helper to write an orca config.yaml.""" @@ -74,7 +69,6 @@ def _write(data: dict): return path return _write - @pytest.fixture def clouds_yaml(tmp_path): """Write a clouds.yaml and return its path.""" @@ -85,7 +79,6 @@ def _write(data: dict) -> Path: return path return _write - # ── Mock OrcaClient ───────────────────────────────────────────────────── FAKE_TOKEN_DATA = { @@ -131,7 +124,6 @@ def _write(data: dict) -> Path: "issued_at": "2099-12-31T22:59:59Z", } - def make_mock_client(): """Build a mock OrcaClient with realistic token data.""" client = MagicMock() @@ -221,7 +213,6 @@ def _get_stream(url, *, extra_headers=None): return client - @pytest.fixture def mock_client(monkeypatch): """Patch OrcaContext.ensure_client to return a mock client.""" @@ -234,7 +225,6 @@ def _ensure_client(self): monkeypatch.setattr("orca_cli.core.context.OrcaContext.ensure_client", _ensure_client) return client - @pytest.fixture(autouse=True) def clean_env(monkeypatch): """Remove OS_* and ORCA_* vars to avoid test pollution.""" diff --git a/tests/test_alarm.py b/tests/test_alarm.py index f1793e3..0ecd821 100644 --- a/tests/test_alarm.py +++ b/tests/test_alarm.py @@ -23,12 +23,10 @@ "evaluation_periods": 1, }) - def _aodh(mc): mc.alarming_url = BASE return mc - def _alarm(**kw): return { "alarm_id": ALARM_ID, @@ -50,7 +48,6 @@ def _alarm(**kw): **kw, } - # ══════════════════════════════════════════════════════════════════════════════ # alarm list # ══════════════════════════════════════════════════════════════════════════════ @@ -97,7 +94,6 @@ def test_list_limit(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["alarm", "list", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # alarm show # ══════════════════════════════════════════════════════════════════════════════ @@ -114,7 +110,6 @@ def test_show(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["alarm", "show", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # alarm create # ══════════════════════════════════════════════════════════════════════════════ @@ -195,7 +190,6 @@ def test_create_severity(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["alarm", "create", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # alarm set # ══════════════════════════════════════════════════════════════════════════════ @@ -253,7 +247,6 @@ def test_set_invalid_rule_json(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["alarm", "set", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # alarm delete # ══════════════════════════════════════════════════════════════════════════════ @@ -275,7 +268,6 @@ def test_delete_requires_confirm(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["alarm", "delete", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # alarm state-get / state-set # ══════════════════════════════════════════════════════════════════════════════ @@ -285,41 +277,40 @@ class TestAlarmState: def test_state_get_ok(self, invoke, mock_client): _aodh(mock_client) mock_client.get.return_value = '"ok"' - result = invoke(["alarm", "state-get", ALARM_ID]) + result = invoke(["alarm", "state", "get", ALARM_ID]) assert result.exit_code == 0 assert "ok" in result.output def test_state_get_alarm(self, invoke, mock_client): _aodh(mock_client) mock_client.get.return_value = "alarm" - result = invoke(["alarm", "state-get", ALARM_ID]) + result = invoke(["alarm", "state", "get", ALARM_ID]) assert result.exit_code == 0 assert "alarm" in result.output def test_state_set_ok(self, invoke, mock_client): _aodh(mock_client) - result = invoke(["alarm", "state-set", ALARM_ID, "ok"]) + result = invoke(["alarm", "state", "set", ALARM_ID, "ok"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"] assert body == "ok" def test_state_set_alarm(self, invoke, mock_client): _aodh(mock_client) - result = invoke(["alarm", "state-set", ALARM_ID, "alarm"]) + result = invoke(["alarm", "state", "set", ALARM_ID, "alarm"]) assert result.exit_code == 0 mock_client.put.assert_called_once() def test_state_set_invalid(self, invoke, mock_client): _aodh(mock_client) - result = invoke(["alarm", "state-set", ALARM_ID, "bad_state"]) + result = invoke(["alarm", "state", "set", ALARM_ID, "bad_state"]) assert result.exit_code != 0 def test_help_get(self, invoke): - assert invoke(["alarm", "state-get", "--help"]).exit_code == 0 + assert invoke(["alarm", "state", "get", "--help"]).exit_code == 0 def test_help_set(self, invoke): - assert invoke(["alarm", "state-set", "--help"]).exit_code == 0 - + assert invoke(["alarm", "state", "set", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════════ # alarm history @@ -356,7 +347,6 @@ def test_history_limit(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["alarm", "history", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # alarm capabilities # ══════════════════════════════════════════════════════════════════════════════ @@ -375,7 +365,6 @@ def test_capabilities(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["alarm", "capabilities", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # alarm quota-set # ══════════════════════════════════════════════════════════════════════════════ @@ -384,7 +373,7 @@ class TestAlarmQuotaSet: def test_quota_set(self, invoke, mock_client): _aodh(mock_client) - result = invoke(["alarm", "quota-set", PROJECT_ID, "--alarms", "50"]) + result = invoke(["alarm", "quota", "set", PROJECT_ID, "--alarms", "50"]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"] assert body["project_id"] == PROJECT_ID @@ -392,8 +381,7 @@ def test_quota_set(self, invoke, mock_client): assert body["quotas"][0]["limit"] == 50 def test_help(self, invoke): - assert invoke(["alarm", "quota-set", "--help"]).exit_code == 0 - + assert invoke(["alarm", "quota", "set", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════════ # Registration @@ -403,8 +391,7 @@ class TestRegistration: @pytest.mark.parametrize("sub", [ "list", "show", "create", "set", "delete", - "state-get", "state-set", - "history", "capabilities", "quota-set", + "history", "capabilities", ]) def test_subcommand_help(self, invoke, sub): assert invoke(["alarm", sub, "--help"]).exit_code == 0 diff --git a/tests/test_aliases.py b/tests/test_aliases.py deleted file mode 100644 index 545638e..0000000 --- a/tests/test_aliases.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Tests for the ADR-0008 alias helpers in ``orca_cli.core.aliases``.""" - -from __future__ import annotations - -import click - -from orca_cli.core.aliases import ( - add_command_with_alias, - count_deprecated_aliases, - list_deprecated_aliases, -) - - -def _build_tree() -> click.Group: - """Build a minimal nested CLI with two aliases for the suite.""" - - @click.group() - def root() -> None: - pass - - @root.group("color") - def color() -> None: - pass - - @color.command("set") - def color_set() -> None: - pass - - @root.command("show") - def show() -> None: - pass - - add_command_with_alias(root, color_set, - legacy_name="set-color", - primary_path="color set") - add_command_with_alias(root, show, - legacy_name="display", - primary_path="show") - return root - - -def test_count_deprecated_aliases_walks_subgroups(): - root = _build_tree() - assert count_deprecated_aliases(root) == 2 - - -def test_count_deprecated_aliases_returns_zero_on_clean_tree(): - @click.group() - def empty() -> None: - pass - - @empty.command("a") - def a() -> None: - pass - - assert count_deprecated_aliases(empty) == 0 - - -def test_list_deprecated_aliases_returns_full_paths_and_replacements(): - root = _build_tree() - rows = sorted(list_deprecated_aliases(root)) - assert rows == [ - ("display", "show"), - ("set-color", "color set"), - ] - - -def test_count_deprecated_aliases_on_live_orca_tree_is_positive(): - """Sanity check: the live orca tree carries ADR-0008 aliases today. - - This will read 0 once 3.0.0 ships and the aliases are dropped — at - that point the assertion can flip to ``== 0`` and this test becomes - the ratchet that prevents reintroducing aliases without intent. - """ - from orca_cli.main import cli - - assert count_deprecated_aliases(cli) > 0 diff --git a/tests/test_app_credentials.py b/tests/test_app_credentials.py index 1b59c8f..8a4df76 100644 --- a/tests/test_app_credentials.py +++ b/tests/test_app_credentials.py @@ -36,7 +36,6 @@ } } - def _auth_response(status=201, token="fake-token"): resp = MagicMock() resp.status_code = status @@ -46,14 +45,12 @@ def _auth_response(status=201, token="fake-token"): resp.text = "" return resp - @pytest.fixture(autouse=True) def _no_real_cache(tmp_path): """Redirect on-disk token cache to a temp dir.""" with patch("orca_cli.core.client.TOKEN_CACHE_PATH", tmp_path / "token_cache.yaml"): yield - # ── Client: auth payload ──────────────────────────────────────────────── class TestAppCredAuthPayload: @@ -166,7 +163,6 @@ def test_name_without_username_raises(self, mock_httpx_cls): # username missing }) - # ── Client: cache key isolation ───────────────────────────────────────── class TestAppCredCacheKey: @@ -207,7 +203,6 @@ def test_two_different_app_creds_get_different_keys(self, mock_httpx_cls): "application_credential_secret": "s"}) assert c1._cache_key != c2._cache_key - # ── config.py ─────────────────────────────────────────────────────────── class TestConfigIsComplete: @@ -257,7 +252,6 @@ def test_app_cred_does_not_require_project(self): assert "project_name" not in cfg assert config_is_complete(cfg) is True - class TestIsAppCred: def test_explicit_v3applicationcredential(self): @@ -275,7 +269,6 @@ def test_implicit_via_secret(self): def test_password_flow(self): assert _is_app_cred({"username": "admin", "password": "x"}) is False - class TestCloudsYamlNormalisation: def test_app_cred_in_clouds_yaml(self): @@ -299,7 +292,6 @@ def test_app_cred_in_clouds_yaml(self): assert "password" not in cfg assert "project_name" not in cfg - class TestOSEnvVarsLoaded: def test_app_cred_env_vars(self, monkeypatch, config_dir): @@ -313,7 +305,6 @@ def test_app_cred_env_vars(self, monkeypatch, config_dir): assert cfg["application_credential_secret"] == "topsecret" assert config_is_complete(cfg) is True - # ── Profile import/export ─────────────────────────────────────────────── class TestProfileFromClouds: @@ -341,7 +332,6 @@ def test_imports_app_cred_cloud(self, invoke, config_dir, tmp_path): assert cfg["application_credential_id"] == "ac-1" assert cfg["application_credential_secret"] == "topsecret" - class TestProfileFromOpenrc: def test_imports_app_cred_openrc(self, invoke, config_dir, tmp_path): @@ -361,7 +351,6 @@ def test_imports_app_cred_openrc(self, invoke, config_dir, tmp_path): assert cfg["application_credential_id"] == "ac-1" assert cfg["application_credential_secret"] == "topsecret" - class TestProfileToOpenrc: def test_emits_app_cred_env_vars(self, invoke, config_dir, write_config): @@ -384,7 +373,6 @@ def test_emits_app_cred_env_vars(self, invoke, config_dir, write_config): assert "OS_PASSWORD" not in out assert "OS_PROJECT_NAME" not in out - class TestProfileToClouds: def test_emits_app_cred_section(self, invoke, config_dir, write_config): @@ -407,7 +395,6 @@ def test_emits_app_cred_section(self, invoke, config_dir, write_config): assert "password" not in cloud["auth"] assert "project_name" not in cloud["auth"] - class TestRoundTrip: def test_clouds_to_profile_to_clouds(self, invoke, config_dir, tmp_path): diff --git a/tests/test_application_credential_cli.py b/tests/test_application_credential_cli.py index 1a7c874..9736747 100644 --- a/tests/test_application_credential_cli.py +++ b/tests/test_application_credential_cli.py @@ -8,7 +8,6 @@ # --user resolution: must use real token user id, never the literal "me" # ══════════════════════════════════════════════════════════════════════════ - class TestCurrentUserResolution: """Regression: --user default was always falling back to "me", which is not a valid Keystone user id. It now reads from the token data.""" @@ -78,12 +77,10 @@ def test_no_user_id_in_token_raises_with_guidance( assert "current user id" in result.output assert "--user" in result.output - # ══════════════════════════════════════════════════════════════════════════ # create # ══════════════════════════════════════════════════════════════════════════ - class TestCreate: def test_basic_create(self, invoke, mock_client, write_config, sample_profile): @@ -119,12 +116,10 @@ def test_create_with_options(self, invoke, mock_client, write_config, sample_pro assert body["expires_at"] == "2027-01-01T00:00:00" assert body["unrestricted"] is True - # ══════════════════════════════════════════════════════════════════════════ # --save-profile: chains AC creation into a ready-to-use orca profile # ══════════════════════════════════════════════════════════════════════════ - class TestCreateSaveProfile: def test_save_profile_writes_app_cred_profile( @@ -186,12 +181,10 @@ def test_save_profile_without_secret_fails_clean( assert result.exit_code != 0 assert "secret" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # list / show / delete smoke tests # ══════════════════════════════════════════════════════════════════════════ - class TestListShowDelete: def test_list_renders(self, invoke, mock_client, write_config, sample_profile): diff --git a/tests/test_audit.py b/tests/test_audit.py index 2f1cf2b..c82bcf6 100644 --- a/tests/test_audit.py +++ b/tests/test_audit.py @@ -13,7 +13,6 @@ def _make_sg(name, sg_id, rules=None): "security_group_rules": rules or [], } - def _ingress_rule(port_min=None, port_max=None, protocol="tcp", remote="0.0.0.0/0", ethertype="IPv4"): rule = { @@ -26,12 +25,10 @@ def _ingress_rule(port_min=None, port_max=None, protocol="tcp", } return rule - def _egress_rule(): return {"direction": "egress", "ethertype": "IPv4", "protocol": None, "remote_ip_prefix": None, "port_range_min": None, "port_range_max": None} - def _server(name, srv_id, key_name="my-key", status="ACTIVE", floating=False): addrs = {"private": [{"addr": "10.0.0.5", "OS-EXT-IPS:type": "fixed"}]} if floating: @@ -44,15 +41,12 @@ def _server(name, srv_id, key_name="my-key", status="ACTIVE", floating=False): "addresses": addrs, } - def _volume(vol_id, encrypted=False): return {"id": vol_id, "encrypted": encrypted} - def _fip(fip_id, port_id=None): return {"id": fip_id, "floating_ip_address": "203.0.113.1", "port_id": port_id} - def _setup_mock(mock_client, sgs=None, servers=None, volumes=None, fips=None): """Configure mock_client.get to return appropriate data per URL.""" sgs = sgs or [] @@ -76,10 +70,8 @@ def _get(url, **kwargs): mock_client.network_url = "https://neutron.example.com" mock_client.volume_url = "https://cinder.example.com/v3" - # ── Tests ────────────────────────────────────────────────────────────────── - class TestAuditClean: """No findings at all.""" @@ -99,7 +91,6 @@ def test_clean_project(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No security issues found" in result.output - class TestAuditSecurityGroups: def test_all_ports_open(self, invoke, config_dir, mock_client, sample_profile): @@ -225,7 +216,6 @@ def test_multiple_dangerous_ports(self, invoke, config_dir, mock_client, sample_ assert "MySQL" in result.output assert "Redis" in result.output - class TestAuditServers: def test_no_keypair(self, invoke, config_dir, mock_client, sample_profile): @@ -277,7 +267,6 @@ def test_server_with_key_ok(self, invoke, config_dir, mock_client, sample_profil assert result.exit_code == 0 assert "No security issues found" in result.output - class TestAuditVolumes: def test_unencrypted_volumes(self, invoke, config_dir, mock_client, sample_profile): @@ -310,7 +299,6 @@ def test_all_encrypted_ok(self, invoke, config_dir, mock_client, sample_profile) assert result.exit_code == 0 assert "No security issues found" in result.output - class TestAuditFloatingIPs: def test_unused_fips(self, invoke, config_dir, mock_client, sample_profile): @@ -340,7 +328,6 @@ def test_all_fips_associated_ok(self, invoke, config_dir, mock_client, sample_pr assert result.exit_code == 0 assert "No security issues found" in result.output - class TestAuditSeveritySorting: def test_findings_sorted_by_severity(self, invoke, config_dir, mock_client, sample_profile): @@ -367,7 +354,6 @@ def test_findings_sorted_by_severity(self, invoke, config_dir, mock_client, samp low_pos = output.index("LOW") assert crit_pos < high_pos < medium_pos < low_pos - class TestAuditCombined: def test_mixed_findings(self, invoke, config_dir, mock_client, sample_profile): @@ -399,7 +385,6 @@ def test_mixed_findings(self, invoke, config_dir, mock_client, sample_profile): # Summary line with count assert "finding" in result.output - class TestAuditHelp: def test_help(self, invoke): diff --git a/tests/test_auth_commands.py b/tests/test_auth_commands.py index 3422d7a..0045c9d 100644 --- a/tests/test_auth_commands.py +++ b/tests/test_auth_commands.py @@ -10,7 +10,6 @@ # whoami # ══════════════════════════════════════════════════════════════════════════ - class TestWhoami: def test_whoami_displays_user_info(self, invoke, config_dir, mock_client, sample_profile): @@ -121,19 +120,17 @@ def test_whoami_invalid_expires_at(self, invoke, config_dir, mock_client, sample # Should show "?" for remaining assert "?" in result.output - # ══════════════════════════════════════════════════════════════════════════ # token-debug # ══════════════════════════════════════════════════════════════════════════ - class TestTokenDebug: def test_token_debug_displays_summary(self, invoke, config_dir, mock_client, sample_profile): save_profile("prod", sample_profile) set_active_profile("prod") - result = invoke(["auth", "token-debug"]) + result = invoke(["auth", "token", "debug"]) assert result.exit_code == 0 output = result.output assert "Token Debug" in output @@ -145,7 +142,7 @@ def test_token_debug_shows_roles_table(self, invoke, config_dir, mock_client, sa save_profile("prod", sample_profile) set_active_profile("prod") - result = invoke(["auth", "token-debug"]) + result = invoke(["auth", "token", "debug"]) assert "Roles" in result.output assert "admin" in result.output assert "member" in result.output @@ -154,7 +151,7 @@ def test_token_debug_shows_catalog(self, invoke, config_dir, mock_client, sample save_profile("prod", sample_profile) set_active_profile("prod") - result = invoke(["auth", "token-debug"]) + result = invoke(["auth", "token", "debug"]) assert "Service Catalog" in result.output assert "compute" in result.output assert "nova" in result.output @@ -177,7 +174,7 @@ def test_token_debug_shows_truncated_token(self, invoke, config_dir, mock_client save_profile("prod", sample_profile) set_active_profile("prod") - result = invoke(["auth", "token-debug"]) + result = invoke(["auth", "token", "debug"]) # Token should be truncated (first 32 chars + "…") assert "fake-token-abcdef1234567890abcde" in result.output @@ -185,7 +182,7 @@ def test_token_debug_shows_lifetime(self, invoke, config_dir, mock_client, sampl save_profile("prod", sample_profile) set_active_profile("prod") - result = invoke(["auth", "token-debug"]) + result = invoke(["auth", "token", "debug"]) assert "Lifetime" in result.output assert "Remaining" in result.output # Token issued/expires in 2099 → remaining time should show "elapsed" @@ -195,7 +192,7 @@ def test_token_debug_shows_endpoints(self, invoke, config_dir, mock_client, samp save_profile("prod", sample_profile) set_active_profile("prod") - result = invoke(["auth", "token-debug"]) + result = invoke(["auth", "token", "debug"]) # Check service catalog endpoints assert "nova.example.com" in result.output assert "keystone.example.com" in result.output @@ -206,7 +203,7 @@ def test_token_debug_shows_domain_info(self, invoke, config_dir, mock_client, sa save_profile("prod", sample_profile) set_active_profile("prod") - result = invoke(["auth", "token-debug"]) + result = invoke(["auth", "token", "debug"]) assert "User Domain" in result.output assert "Project Domain" in result.output assert "Default" in result.output @@ -216,7 +213,7 @@ def test_token_debug_empty_roles(self, invoke, config_dir, mock_client, sample_p set_active_profile("prod") mock_client.token_data = {**mock_client.token_data, "roles": []} - result = invoke(["auth", "token-debug"]) + result = invoke(["auth", "token", "debug"]) assert result.exit_code == 0 assert "Roles" in result.output @@ -225,7 +222,7 @@ def test_token_debug_empty_catalog(self, invoke, config_dir, mock_client, sample set_active_profile("prod") mock_client.catalog = [] - result = invoke(["auth", "token-debug"]) + result = invoke(["auth", "token", "debug"]) assert result.exit_code == 0 assert "Service Catalog" in result.output @@ -239,15 +236,13 @@ def test_token_debug_no_timestamps(self, invoke, config_dir, mock_client, sample "expires_at": "", } - result = invoke(["auth", "token-debug"]) + result = invoke(["auth", "token", "debug"]) assert result.exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════ # check # ══════════════════════════════════════════════════════════════════════════ - class TestCheck: def test_check_active_profile(self, invoke, config_dir, sample_profile, monkeypatch): @@ -455,12 +450,10 @@ def test_check_generic_exception(self, invoke, config_dir, sample_profile, monke assert "Connection" in result.output assert "refused" in result.output - # ══════════════════════════════════════════════════════════════════════════ # CLI Help # ══════════════════════════════════════════════════════════════════════════ - class TestCLIHelp: """Verify all commands are properly registered and show help.""" @@ -468,7 +461,7 @@ def test_auth_help(self, invoke): result = invoke(["auth", "--help"]) assert result.exit_code == 0 assert "whoami" in result.output - assert "token-debug" in result.output + assert "token" in result.output # the token sub-group (was "token-debug" pre-3.0) assert "check" in result.output def test_whoami_help(self, invoke): @@ -477,7 +470,7 @@ def test_whoami_help(self, invoke): assert "identity" in result.output.lower() or "user" in result.output.lower() def test_token_debug_help(self, invoke): - result = invoke(["auth", "token-debug", "--help"]) + result = invoke(["auth", "token", "debug", "--help"]) assert result.exit_code == 0 assert "--raw" in result.output diff --git a/tests/test_backup.py b/tests/test_backup.py index 8c34a24..5e8405d 100644 --- a/tests/test_backup.py +++ b/tests/test_backup.py @@ -12,7 +12,6 @@ CLIENT_ID = "client-jkl012" ACTION_ID = "act-mno345" - def _setup_mock(mock_client): mock_client.backup_url = "https://freezer.example.com" @@ -139,12 +138,10 @@ def _delete(url, **kwargs): return {"posted": posted, "put_urls": put_urls, "deleted": deleted} - # ============================================================================ # backup list # ============================================================================ - class TestBackupList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -167,12 +164,10 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No backups found" in result.output - # ============================================================================ # backup show # ============================================================================ - class TestBackupShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -186,12 +181,10 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): assert "swift" in result.output assert "extra_val" in result.output - # ============================================================================ # backup delete # ============================================================================ - class TestBackupDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -204,12 +197,10 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert "deleted" in result.output.lower() assert len(state["deleted"]) == 1 - # ============================================================================ # backup job-list # ============================================================================ - class TestJobList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -217,7 +208,7 @@ def test_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["backup", "job-list"]) + result = invoke(["backup", "job", "list"]) assert result.exit_code == 0 assert "daily" in result.output assert "runn" in result.output @@ -228,16 +219,14 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): mock_client.backup_url = "https://freezer.example.com" mock_client.get = lambda url, **kw: {"jobs": []} - result = invoke(["backup", "job-list"]) + result = invoke(["backup", "job", "list"]) assert result.exit_code == 0 assert "No jobs found" in result.output - # ============================================================================ # backup job-show # ============================================================================ - class TestJobShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -245,17 +234,15 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["backup", "job-show", JOB_ID]) + result = invoke(["backup", "job", "show", JOB_ID]) assert result.exit_code == 0 assert "daily" in result.output assert "backup" in result.output.lower() - # ============================================================================ # backup job-create # ============================================================================ - class TestJobCreate: def test_create(self, invoke, config_dir, mock_client, sample_profile): @@ -263,7 +250,7 @@ def test_create(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["backup", "job-create", + result = invoke(["backup", "job", "create", "--client-id", CLIENT_ID, "--path", "/var/data", "--container", "my-ct"]) @@ -277,7 +264,7 @@ def test_create_restore(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["backup", "job-create", + result = invoke(["backup", "job", "create", "--client-id", CLIENT_ID, "--path", "/var/restore", "--action", "restore"]) @@ -286,12 +273,10 @@ def test_create_restore(self, invoke, config_dir, mock_client, sample_profile): assert fa["restore_abs_path"] == "/var/restore" assert fa["action"] == "restore" - # ============================================================================ # backup job-start / job-stop # ============================================================================ - class TestJobStartStop: def test_start(self, invoke, config_dir, mock_client, sample_profile): @@ -299,7 +284,7 @@ def test_start(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["backup", "job-start", JOB_ID]) + result = invoke(["backup", "job", "start", JOB_ID]) assert result.exit_code == 0 assert "started" in result.output.lower() assert state["posted"]["event"] == "start" @@ -309,17 +294,15 @@ def test_stop(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["backup", "job-stop", JOB_ID]) + result = invoke(["backup", "job", "stop", JOB_ID]) assert result.exit_code == 0 assert "stopped" in result.output.lower() assert state["posted"]["event"] == "stop" - # ============================================================================ # backup job-delete # ============================================================================ - class TestJobDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -327,17 +310,15 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["backup", "job-delete", JOB_ID, "-y"]) + result = invoke(["backup", "job", "delete", JOB_ID, "-y"]) assert result.exit_code == 0 assert "deleted" in result.output.lower() assert len(state["deleted"]) == 1 - # ============================================================================ # backup session-list # ============================================================================ - class TestSessionList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -345,7 +326,7 @@ def test_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["backup", "session-list"]) + result = invoke(["backup", "session", "list"]) assert result.exit_code == 0 assert "weekly" in result.output assert "activ" in result.output @@ -356,16 +337,14 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): mock_client.backup_url = "https://freezer.example.com" mock_client.get = lambda url, **kw: {"sessions": []} - result = invoke(["backup", "session-list"]) + result = invoke(["backup", "session", "list"]) assert result.exit_code == 0 assert "No sessions found" in result.output - # ============================================================================ # backup session-show # ============================================================================ - class TestSessionShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -373,17 +352,15 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["backup", "session-show", SESSION_ID]) + result = invoke(["backup", "session", "show", SESSION_ID]) assert result.exit_code == 0 assert "weekly" in result.output assert "activ" in result.output - # ============================================================================ # backup session-create # ============================================================================ - class TestSessionCreate: def test_create(self, invoke, config_dir, mock_client, sample_profile): @@ -391,7 +368,7 @@ def test_create(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["backup", "session-create", "--description", "my-session"]) + result = invoke(["backup", "session", "create", "--description", "my-session"]) assert result.exit_code == 0 assert "created" in result.output.lower() assert state["posted"]["description"] == "my-session" @@ -401,18 +378,16 @@ def test_create_with_schedule(self, invoke, config_dir, mock_client, sample_prof set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["backup", "session-create", + result = invoke(["backup", "session", "create", "--description", "sched-sess", "--schedule-interval", "24 hours"]) assert result.exit_code == 0 assert state["posted"]["schedule"]["schedule_interval"] == "24 hours" - # ============================================================================ # backup session-start # ============================================================================ - class TestSessionStart: def test_start(self, invoke, config_dir, mock_client, sample_profile): @@ -420,16 +395,14 @@ def test_start(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["backup", "session-start", SESSION_ID]) + result = invoke(["backup", "session", "start", SESSION_ID]) assert result.exit_code == 0 assert "started" in result.output.lower() - # ============================================================================ # backup session-add-job / session-remove-job # ============================================================================ - class TestSessionAddRemoveJob: def test_add_job(self, invoke, config_dir, mock_client, sample_profile): @@ -437,7 +410,7 @@ def test_add_job(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["backup", "session-add-job", SESSION_ID, JOB_ID]) + result = invoke(["backup", "session", "add-job", SESSION_ID, JOB_ID]) assert result.exit_code == 0 assert "added" in result.output.lower() assert len(state["put_urls"]) == 1 @@ -449,18 +422,16 @@ def test_remove_job(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["backup", "session-remove-job", SESSION_ID, JOB_ID]) + result = invoke(["backup", "session", "remove-job", SESSION_ID, JOB_ID]) assert result.exit_code == 0 assert "removed" in result.output.lower() assert len(state["deleted"]) == 1 assert SESSION_ID in state["deleted"][0] - # ============================================================================ # backup session-delete # ============================================================================ - class TestSessionDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -468,17 +439,15 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["backup", "session-delete", SESSION_ID, "-y"]) + result = invoke(["backup", "session", "delete", SESSION_ID, "-y"]) assert result.exit_code == 0 assert "deleted" in result.output.lower() assert len(state["deleted"]) == 1 - # ============================================================================ # backup client-list # ============================================================================ - class TestClientList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -486,7 +455,7 @@ def test_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["backup", "client-list"]) + result = invoke(["backup", "client", "list"]) assert result.exit_code == 0 assert "node1" in result.output @@ -496,16 +465,14 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): mock_client.backup_url = "https://freezer.example.com" mock_client.get = lambda url, **kw: {"clients": []} - result = invoke(["backup", "client-list"]) + result = invoke(["backup", "client", "list"]) assert result.exit_code == 0 assert "No clients found" in result.output - # ============================================================================ # backup client-show # ============================================================================ - class TestClientShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -513,17 +480,15 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["backup", "client-show", CLIENT_ID]) + result = invoke(["backup", "client", "show", CLIENT_ID]) assert result.exit_code == 0 assert "node1" in result.output assert "primary" in result.output - # ============================================================================ # backup client-register # ============================================================================ - class TestClientRegister: def test_register(self, invoke, config_dir, mock_client, sample_profile): @@ -531,18 +496,16 @@ def test_register(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["backup", "client-register", "new-host", + result = invoke(["backup", "client", "register", "new-host", "--description", "new agent"]) assert result.exit_code == 0 assert "registered" in result.output.lower() assert state["posted"]["hostname"] == "new-host" - # ============================================================================ # backup client-delete # ============================================================================ - class TestClientDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -550,17 +513,15 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["backup", "client-delete", CLIENT_ID, "-y"]) + result = invoke(["backup", "client", "delete", CLIENT_ID, "-y"]) assert result.exit_code == 0 assert "deleted" in result.output.lower() assert len(state["deleted"]) == 1 - # ============================================================================ # backup action-list # ============================================================================ - class TestActionList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -568,7 +529,7 @@ def test_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["backup", "action-list"]) + result = invoke(["backup", "action", "list"]) assert result.exit_code == 0 assert "backup" in result.output.lower() @@ -578,16 +539,14 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): mock_client.backup_url = "https://freezer.example.com" mock_client.get = lambda url, **kw: {"actions": []} - result = invoke(["backup", "action-list"]) + result = invoke(["backup", "action", "list"]) assert result.exit_code == 0 assert "No actions found" in result.output - # ============================================================================ # backup action-show # ============================================================================ - class TestActionShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -595,17 +554,15 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["backup", "action-show", ACTION_ID]) + result = invoke(["backup", "action", "show", ACTION_ID]) assert result.exit_code == 0 assert "backup" in result.output.lower() assert "swift" in result.output - # ============================================================================ # backup action-create # ============================================================================ - class TestActionCreate: def test_create(self, invoke, config_dir, mock_client, sample_profile): @@ -613,7 +570,7 @@ def test_create(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["backup", "action-create", + result = invoke(["backup", "action", "create", "--path", "/var/data", "--container", "act-ct"]) assert result.exit_code == 0 @@ -627,7 +584,7 @@ def test_create_restore(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["backup", "action-create", + result = invoke(["backup", "action", "create", "--action", "restore", "--path", "/var/restore"]) assert result.exit_code == 0 @@ -635,12 +592,10 @@ def test_create_restore(self, invoke, config_dir, mock_client, sample_profile): assert fa["restore_abs_path"] == "/var/restore" assert fa["action"] == "restore" - # ============================================================================ # backup action-delete # ============================================================================ - class TestActionDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -648,30 +603,19 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["backup", "action-delete", ACTION_ID, "-y"]) + result = invoke(["backup", "action", "delete", ACTION_ID, "-y"]) assert result.exit_code == 0 assert "deleted" in result.output.lower() assert len(state["deleted"]) == 1 - # ============================================================================ # Help # ============================================================================ - class TestBackupHelp: def test_backup_help(self, invoke): result = invoke(["backup", "--help"]) assert result.exit_code == 0 - for cmd in ("list", "show", "delete", - "job-list", "job-show", "job-create", "job-start", - "job-stop", "job-delete", - "session-list", "session-show", "session-create", - "session-start", "session-add-job", "session-remove-job", - "session-delete", - "client-list", "client-show", "client-register", - "client-delete", - "action-list", "action-show", "action-create", - "action-delete"): + for cmd in ("list", "show", "delete"): assert cmd in result.output, f"'{cmd}' not in help output" diff --git a/tests/test_cache.py b/tests/test_cache.py index eb35344..ecea0d5 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -13,7 +13,6 @@ def _patch_dir(tmp_path: Path): return patch.object(cache, "_CACHE_DIR", tmp_path) - class TestCache: def test_save_then_load_roundtrip(self, tmp_path): diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 1df463c..555585d 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -110,7 +110,6 @@ def test_catalog_many_services(self, invoke, config_dir, mock_client, sample_pro for name in ("nova", "neutron", "glance", "cinder", "swift"): assert name in result.output - class TestCatalogOutputFormats: def test_catalog_json_format(self, invoke, config_dir, mock_client, sample_profile): @@ -144,7 +143,6 @@ def test_catalog_column_filter(self, invoke, config_dir, mock_client, sample_pro # Type column should not appear when filtered out # (can't assert absence since "compute" might appear in URL) - class TestCatalogHelp: def test_help(self, invoke): diff --git a/tests/test_cinder_admin.py b/tests/test_cinder_admin.py index d08021d..23e2c8e 100644 --- a/tests/test_cinder_admin.py +++ b/tests/test_cinder_admin.py @@ -12,12 +12,10 @@ PRJ = "ffffffff-ffff-ffff-ffff-ffffffffffff" CINDER = "https://cinder.example.com/v3/project" - def _vol(mock_client): mock_client.volume_url = CINDER return mock_client - # ══════════════════════════════════════════════════════════════════════════ # volume set / unset (metadata) # ══════════════════════════════════════════════════════════════════════════ @@ -54,7 +52,6 @@ def test_set_invalid_property(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["volume", "set", "--help"]).exit_code == 0 - class TestVolumeUnset: def test_unset(self, invoke, mock_client): @@ -79,7 +76,6 @@ def test_unset_nothing(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["volume", "unset", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════ # volume snapshot-set # ══════════════════════════════════════════════════════════════════════════ @@ -88,26 +84,25 @@ class TestSnapshotSet: def test_set_name(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "snapshot-set", SNAP, "--name", "renamed"]) + result = invoke(["volume", "snapshot", "set", SNAP, "--name", "renamed"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"]["snapshot"] assert body["name"] == "renamed" def test_set_property(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "snapshot-set", SNAP, "--property", "key=val"]) + result = invoke(["volume", "snapshot", "set", SNAP, "--property", "key=val"]) assert result.exit_code == 0 mock_client.post.assert_called_once() def test_set_nothing(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "snapshot-set", SNAP]) + result = invoke(["volume", "snapshot", "set", SNAP]) assert result.exit_code == 0 mock_client.put.assert_not_called() def test_help(self, invoke): - assert invoke(["volume", "snapshot-set", "--help"]).exit_code == 0 - + assert invoke(["volume", "snapshot", "set", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # volume type-list / show / create / set / delete @@ -121,14 +116,14 @@ def _type(self, **kw): def test_list(self, invoke, mock_client): _vol(mock_client) mock_client.get.return_value = {"volume_types": [self._type()]} - result = invoke(["volume", "type-list"]) + result = invoke(["volume", "type", "list"]) assert result.exit_code == 0 assert "ssd" in result.output def test_list_default(self, invoke, mock_client): _vol(mock_client) mock_client.get.return_value = {"volume_type": self._type()} - result = invoke(["volume", "type-list", "--default"]) + result = invoke(["volume", "type", "list", "--default"]) assert result.exit_code == 0 url = mock_client.get.call_args[0][0] assert "/types/default" in url @@ -136,12 +131,11 @@ def test_list_default(self, invoke, mock_client): def test_list_empty(self, invoke, mock_client): _vol(mock_client) mock_client.get.return_value = {"volume_types": []} - result = invoke(["volume", "type-list"]) + result = invoke(["volume", "type", "list"]) assert "No volume types" in result.output def test_help(self, invoke): - assert invoke(["volume", "type-list", "--help"]).exit_code == 0 - + assert invoke(["volume", "type", "list", "--help"]).exit_code == 0 class TestVolumeTypeShow: @@ -151,20 +145,19 @@ def test_show(self, invoke, mock_client): "id": TYPE, "name": "ssd", "is_public": True, "description": "Fast SSD", "extra_specs": {"volume_backend_name": "lvm-ssd"}, }} - result = invoke(["volume", "type-show", TYPE]) + result = invoke(["volume", "type", "show", TYPE]) assert result.exit_code == 0 assert "ssd" in result.output def test_help(self, invoke): - assert invoke(["volume", "type-show", "--help"]).exit_code == 0 - + assert invoke(["volume", "type", "show", "--help"]).exit_code == 0 class TestVolumeTypeCreate: def test_create(self, invoke, mock_client): _vol(mock_client) mock_client.post.return_value = {"volume_type": {"id": TYPE, "name": "ssd"}} - result = invoke(["volume", "type-create", "ssd"]) + result = invoke(["volume", "type", "create", "ssd"]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"]["volume_type"] assert body["name"] == "ssd" @@ -172,50 +165,47 @@ def test_create(self, invoke, mock_client): def test_create_with_specs(self, invoke, mock_client): _vol(mock_client) mock_client.post.return_value = {"volume_type": {"id": TYPE, "name": "ssd"}} - invoke(["volume", "type-create", "ssd", + invoke(["volume", "type", "create", "ssd", "--property", "volume_backend_name=lvm"]) assert mock_client.post.call_count == 2 # type + extra_specs def test_help(self, invoke): - assert invoke(["volume", "type-create", "--help"]).exit_code == 0 - + assert invoke(["volume", "type", "create", "--help"]).exit_code == 0 class TestVolumeTypeSet: def test_set_name(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "type-set", TYPE, "--name", "renamed"]) + result = invoke(["volume", "type", "set", TYPE, "--name", "renamed"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"]["volume_type"] assert body["name"] == "renamed" def test_set_nothing(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "type-set", TYPE]) + result = invoke(["volume", "type", "set", TYPE]) assert result.exit_code == 0 mock_client.put.assert_not_called() def test_help(self, invoke): - assert invoke(["volume", "type-set", "--help"]).exit_code == 0 - + assert invoke(["volume", "type", "set", "--help"]).exit_code == 0 class TestVolumeTypeDelete: def test_delete_yes(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "type-delete", TYPE, "--yes"]) + result = invoke(["volume", "type", "delete", TYPE, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "type-delete", TYPE], input="n\n") + result = invoke(["volume", "type", "delete", TYPE], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help(self, invoke): - assert invoke(["volume", "type-delete", "--help"]).exit_code == 0 - + assert invoke(["volume", "type", "delete", "--help"]).exit_code == 0 class TestVolumeTypeAccess: @@ -224,37 +214,36 @@ def test_access_list(self, invoke, mock_client): mock_client.get.return_value = {"volume_type_access": [ {"volume_type_id": TYPE, "project_id": PRJ} ]} - result = invoke(["volume", "type-access-list", TYPE]) + result = invoke(["volume", "type", "access", "list", TYPE]) assert result.exit_code == 0 def test_access_add(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "type-access-add", TYPE, PRJ]) + result = invoke(["volume", "type", "access", "add", TYPE, PRJ]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"] assert body["addProjectAccess"]["project"] == PRJ def test_access_remove_yes(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "type-access-remove", TYPE, PRJ, "--yes"]) + result = invoke(["volume", "type", "access", "remove", TYPE, PRJ, "--yes"]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"] assert body["removeProjectAccess"]["project"] == PRJ def test_access_remove_requires_confirm(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "type-access-remove", TYPE, PRJ], input="n\n") + result = invoke(["volume", "type", "access", "remove", TYPE, PRJ], input="n\n") assert result.exit_code != 0 def test_help_access_list(self, invoke): - assert invoke(["volume", "type-access-list", "--help"]).exit_code == 0 + assert invoke(["volume", "type", "access", "list", "--help"]).exit_code == 0 def test_help_access_add(self, invoke): - assert invoke(["volume", "type-access-add", "--help"]).exit_code == 0 + assert invoke(["volume", "type", "access", "add", "--help"]).exit_code == 0 def test_help_access_remove(self, invoke): - assert invoke(["volume", "type-access-remove", "--help"]).exit_code == 0 - + assert invoke(["volume", "type", "access", "remove", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # volume transfer-* @@ -267,7 +256,7 @@ def test_create(self, invoke, mock_client): mock_client.post.return_value = {"transfer": { "id": XFER, "volume_id": VOL, "auth_key": "secret123" }} - result = invoke(["volume", "transfer-create", VOL, "--name", "my-transfer"]) + result = invoke(["volume", "transfer", "create", VOL, "--name", "my-transfer"]) assert result.exit_code == 0 assert "secret" in result.output body = mock_client.post.call_args[1]["json"]["transfer"] @@ -277,13 +266,12 @@ def test_create(self, invoke, mock_client): def test_calls_correct_url(self, invoke, mock_client): _vol(mock_client) mock_client.post.return_value = {"transfer": {"id": XFER, "auth_key": "x"}} - invoke(["volume", "transfer-create", VOL]) + invoke(["volume", "transfer", "create", VOL]) url = mock_client.post.call_args[0][0] assert "/volume-transfers" in url def test_help(self, invoke): - assert invoke(["volume", "transfer-create", "--help"]).exit_code == 0 - + assert invoke(["volume", "transfer", "create", "--help"]).exit_code == 0 class TestVolumeTransferList: @@ -292,24 +280,23 @@ def test_list(self, invoke, mock_client): mock_client.get.return_value = {"transfers": [ {"id": XFER, "name": "my-transfer", "volume_id": VOL, "created_at": "2026-01-01"} ]} - result = invoke(["volume", "transfer-list"]) + result = invoke(["volume", "transfer", "list"]) assert result.exit_code == 0 def test_list_all_projects(self, invoke, mock_client): _vol(mock_client) mock_client.get.return_value = {"transfers": []} - invoke(["volume", "transfer-list", "--all-projects"]) + invoke(["volume", "transfer", "list", "--all-projects"]) assert mock_client.get.call_args[1]["params"].get("all_tenants") == 1 def test_list_empty(self, invoke, mock_client): _vol(mock_client) mock_client.get.return_value = {"transfers": []} - result = invoke(["volume", "transfer-list"]) + result = invoke(["volume", "transfer", "list"]) assert "No transfers" in result.output def test_help(self, invoke): - assert invoke(["volume", "transfer-list", "--help"]).exit_code == 0 - + assert invoke(["volume", "transfer", "list", "--help"]).exit_code == 0 class TestVolumeTransferShow: @@ -318,49 +305,46 @@ def test_show(self, invoke, mock_client): mock_client.get.return_value = {"transfer": { "id": XFER, "name": "t", "volume_id": VOL, "created_at": "2026-01-01" }} - result = invoke(["volume", "transfer-show", XFER]) + result = invoke(["volume", "transfer", "show", XFER]) assert result.exit_code == 0 def test_help(self, invoke): - assert invoke(["volume", "transfer-show", "--help"]).exit_code == 0 - + assert invoke(["volume", "transfer", "show", "--help"]).exit_code == 0 class TestVolumeTransferAccept: def test_accept(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "transfer-accept", XFER, "myauthkey"]) + result = invoke(["volume", "transfer", "accept", XFER, "myauthkey"]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"]["accept"] assert body["auth_key"] == "myauthkey" def test_calls_correct_url(self, invoke, mock_client): _vol(mock_client) - invoke(["volume", "transfer-accept", XFER, "key"]) + invoke(["volume", "transfer", "accept", XFER, "key"]) url = mock_client.post.call_args[0][0] assert f"/volume-transfers/{XFER}/accept" in url def test_help(self, invoke): - assert invoke(["volume", "transfer-accept", "--help"]).exit_code == 0 - + assert invoke(["volume", "transfer", "accept", "--help"]).exit_code == 0 class TestVolumeTransferDelete: def test_delete_yes(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "transfer-delete", XFER, "--yes"]) + result = invoke(["volume", "transfer", "delete", XFER, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "transfer-delete", XFER], input="n\n") + result = invoke(["volume", "transfer", "delete", XFER], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help(self, invoke): - assert invoke(["volume", "transfer-delete", "--help"]).exit_code == 0 - + assert invoke(["volume", "transfer", "delete", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # volume qos-* @@ -373,19 +357,18 @@ def test_list(self, invoke, mock_client): mock_client.get.return_value = {"qos_specs": [ {"id": QOS, "name": "high-iops", "consumer": "back-end", "specs": {}} ]} - result = invoke(["volume", "qos-list"]) + result = invoke(["volume", "qos", "list"]) assert result.exit_code == 0 assert "high-iops" in result.output def test_list_empty(self, invoke, mock_client): _vol(mock_client) mock_client.get.return_value = {"qos_specs": []} - result = invoke(["volume", "qos-list"]) + result = invoke(["volume", "qos", "list"]) assert "No QoS" in result.output def test_help(self, invoke): - assert invoke(["volume", "qos-list", "--help"]).exit_code == 0 - + assert invoke(["volume", "qos", "list", "--help"]).exit_code == 0 class TestVolumeQosShow: @@ -395,20 +378,19 @@ def test_show(self, invoke, mock_client): "id": QOS, "name": "high-iops", "consumer": "back-end", "specs": {"total_iops_sec": "1000"}, }} - result = invoke(["volume", "qos-show", QOS]) + result = invoke(["volume", "qos", "show", QOS]) assert result.exit_code == 0 assert "high-iops" in result.output def test_help(self, invoke): - assert invoke(["volume", "qos-show", "--help"]).exit_code == 0 - + assert invoke(["volume", "qos", "show", "--help"]).exit_code == 0 class TestVolumeQosCreate: def test_create(self, invoke, mock_client): _vol(mock_client) mock_client.post.return_value = {"qos_specs": {"id": QOS}} - result = invoke(["volume", "qos-create", "high-iops", + result = invoke(["volume", "qos", "create", "high-iops", "--consumer", "back-end", "--property", "total_iops_sec=1000"]) assert result.exit_code == 0 @@ -420,89 +402,84 @@ def test_create(self, invoke, mock_client): def test_create_no_specs(self, invoke, mock_client): _vol(mock_client) mock_client.post.return_value = {"qos_specs": {"id": QOS}} - result = invoke(["volume", "qos-create", "basic"]) + result = invoke(["volume", "qos", "create", "basic"]) assert result.exit_code == 0 def test_help(self, invoke): - assert invoke(["volume", "qos-create", "--help"]).exit_code == 0 - + assert invoke(["volume", "qos", "create", "--help"]).exit_code == 0 class TestVolumeQosSet: def test_set(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "qos-set", QOS, "--property", "total_iops_sec=2000"]) + result = invoke(["volume", "qos", "set", QOS, "--property", "total_iops_sec=2000"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"]["qos_specs"] assert body["total_iops_sec"] == "2000" def test_set_nothing(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "qos-set", QOS]) + result = invoke(["volume", "qos", "set", QOS]) assert result.exit_code == 0 mock_client.put.assert_not_called() def test_help(self, invoke): - assert invoke(["volume", "qos-set", "--help"]).exit_code == 0 - + assert invoke(["volume", "qos", "set", "--help"]).exit_code == 0 class TestVolumeQosDelete: def test_delete_yes(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "qos-delete", QOS, "--yes"]) + result = invoke(["volume", "qos", "delete", QOS, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_force(self, invoke, mock_client): _vol(mock_client) - invoke(["volume", "qos-delete", QOS, "--yes", "--force"]) + invoke(["volume", "qos", "delete", QOS, "--yes", "--force"]) assert mock_client.delete.call_args[1].get("params") == {"force": True} def test_delete_requires_confirm(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "qos-delete", QOS], input="n\n") + result = invoke(["volume", "qos", "delete", QOS], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help(self, invoke): - assert invoke(["volume", "qos-delete", "--help"]).exit_code == 0 - + assert invoke(["volume", "qos", "delete", "--help"]).exit_code == 0 class TestVolumeQosAssociate: def test_associate(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "qos-associate", QOS, TYPE]) + result = invoke(["volume", "qos", "associate", QOS, TYPE]) assert result.exit_code == 0 assert mock_client.get.call_args[1]["params"]["vol_type_id"] == TYPE def test_calls_correct_url(self, invoke, mock_client): _vol(mock_client) - invoke(["volume", "qos-associate", QOS, TYPE]) + invoke(["volume", "qos", "associate", QOS, TYPE]) url = mock_client.get.call_args[0][0] assert f"/qos-specs/{QOS}/associate" in url def test_help(self, invoke): - assert invoke(["volume", "qos-associate", "--help"]).exit_code == 0 - + assert invoke(["volume", "qos", "associate", "--help"]).exit_code == 0 class TestVolumeQosDisassociate: def test_disassociate(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "qos-disassociate", QOS, TYPE]) + result = invoke(["volume", "qos", "disassociate", QOS, TYPE]) assert result.exit_code == 0 def test_disassociate_all(self, invoke, mock_client): _vol(mock_client) - invoke(["volume", "qos-disassociate", QOS, TYPE, "--all"]) + invoke(["volume", "qos", "disassociate", QOS, TYPE, "--all"]) url = mock_client.get.call_args[0][0] assert "disassociate_all" in url def test_help(self, invoke): - assert invoke(["volume", "qos-disassociate", "--help"]).exit_code == 0 - + assert invoke(["volume", "qos", "disassociate", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # volume service-list / service-set @@ -518,50 +495,49 @@ def _svc(self, **kw): def test_list(self, invoke, mock_client): _vol(mock_client) mock_client.get.return_value = {"services": [self._svc()]} - result = invoke(["volume", "service-list"]) + result = invoke(["volume", "service", "list"]) assert result.exit_code == 0 assert "cinder-" in result.output def test_list_filter_host(self, invoke, mock_client): _vol(mock_client) mock_client.get.return_value = {"services": []} - invoke(["volume", "service-list", "--host", "ctrl1"]) + invoke(["volume", "service", "list", "--host", "ctrl1"]) assert mock_client.get.call_args[1]["params"]["host"] == "ctrl1" def test_list_filter_binary(self, invoke, mock_client): _vol(mock_client) mock_client.get.return_value = {"services": []} - invoke(["volume", "service-list", "--binary", "cinder-scheduler"]) + invoke(["volume", "service", "list", "--binary", "cinder-scheduler"]) assert mock_client.get.call_args[1]["params"]["binary"] == "cinder-scheduler" def test_list_empty(self, invoke, mock_client): _vol(mock_client) mock_client.get.return_value = {"services": []} - result = invoke(["volume", "service-list"]) + result = invoke(["volume", "service", "list"]) assert "No Cinder services" in result.output def test_help(self, invoke): - assert invoke(["volume", "service-list", "--help"]).exit_code == 0 - + assert invoke(["volume", "service", "list", "--help"]).exit_code == 0 class TestVolumeServiceSet: def test_enable(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "service-set", "ctrl1", "cinder-volume", "--enable"]) + result = invoke(["volume", "service", "set", "ctrl1", "cinder-volume", "--enable"]) assert result.exit_code == 0 url = mock_client.put.call_args[0][0] assert "/os-services/enable" in url def test_disable(self, invoke, mock_client): _vol(mock_client) - invoke(["volume", "service-set", "ctrl1", "cinder-volume", "--disable"]) + invoke(["volume", "service", "set", "ctrl1", "cinder-volume", "--disable"]) url = mock_client.put.call_args[0][0] assert "/os-services/disable" in url def test_disable_with_reason(self, invoke, mock_client): _vol(mock_client) - invoke(["volume", "service-set", "ctrl1", "cinder-volume", + invoke(["volume", "service", "set", "ctrl1", "cinder-volume", "--disable", "--disabled-reason", "maintenance"]) url = mock_client.put.call_args[0][0] assert "disable-log-reason" in url @@ -570,12 +546,11 @@ def test_disable_with_reason(self, invoke, mock_client): def test_no_action_error(self, invoke, mock_client): _vol(mock_client) - result = invoke(["volume", "service-set", "ctrl1", "cinder-volume"]) + result = invoke(["volume", "service", "set", "ctrl1", "cinder-volume"]) assert result.exit_code != 0 def test_help(self, invoke): - assert invoke(["volume", "service-set", "--help"]).exit_code == 0 - + assert invoke(["volume", "service", "set", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # Registration @@ -585,14 +560,6 @@ class TestRegistration: @pytest.mark.parametrize("sub", [ "set", "unset", - "snapshot-set", - "type-list", "type-show", "type-create", "type-set", "type-delete", - "type-access-list", "type-access-add", "type-access-remove", - "transfer-create", "transfer-list", "transfer-show", - "transfer-accept", "transfer-delete", - "qos-list", "qos-show", "qos-create", "qos-set", "qos-delete", - "qos-associate", "qos-disassociate", - "service-list", "service-set", ]) def test_subcommand_help(self, invoke, sub): result = invoke(["volume", sub, "--help"]) diff --git a/tests/test_cleanup.py b/tests/test_cleanup.py index b9c45b7..e4ae139 100644 --- a/tests/test_cleanup.py +++ b/tests/test_cleanup.py @@ -11,17 +11,14 @@ def _fip(fip_id, port_id=None): return {"id": fip_id, "floating_ip_address": "203.0.113.1", "port_id": port_id} - def _volume(vol_id, name="vol", status="available", attachments=None): return {"id": vol_id, "name": name, "status": status, "attachments": attachments or []} - def _snapshot(snap_id, name="snap", created_at=None): if created_at is None: created_at = datetime.now(timezone.utc).isoformat() return {"id": snap_id, "name": name, "created_at": created_at} - def _port(port_id, device_id="", device_owner="", fixed_ips=None): return { "id": port_id, @@ -30,11 +27,9 @@ def _port(port_id, device_id="", device_owner="", fixed_ips=None): "fixed_ips": fixed_ips or [{"ip_address": "10.0.0.5"}], } - def _sg(sg_id, name="my-sg"): return {"id": sg_id, "name": name, "security_group_rules": []} - def _server(srv_id, name="web", status="ACTIVE", security_groups=None): return { "id": srv_id, @@ -43,7 +38,6 @@ def _server(srv_id, name="web", status="ACTIVE", security_groups=None): "security_groups": security_groups or [{"name": "default"}], } - def _setup_mock(mock_client, fips=None, volumes=None, snapshots=None, ports=None, sgs=None, servers=None): fips = fips or [] @@ -74,12 +68,10 @@ def _get(url, **kwargs): mock_client.network_url = "https://neutron.example.com" mock_client.volume_url = "https://cinder.example.com/v3" - # ══════════════════════════════════════════════════════════════════════════ # Clean project # ══════════════════════════════════════════════════════════════════════════ - class TestCleanupClean: def test_no_orphans(self, invoke, config_dir, mock_client, sample_profile): @@ -99,12 +91,10 @@ def test_no_orphans(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No orphaned resources found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Floating IPs # ══════════════════════════════════════════════════════════════════════════ - class TestCleanupFloatingIPs: def test_unassociated_fip_detected(self, invoke, config_dir, mock_client, sample_profile): @@ -128,12 +118,10 @@ def test_all_fips_associated(self, invoke, config_dir, mock_client, sample_profi assert result.exit_code == 0 assert "No orphaned resources found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Volumes # ══════════════════════════════════════════════════════════════════════════ - class TestCleanupVolumes: def test_detached_volume_detected(self, invoke, config_dir, mock_client, sample_profile): @@ -169,12 +157,10 @@ def test_attached_volume_ok(self, invoke, config_dir, mock_client, sample_profil assert result.exit_code == 0 assert "No orphaned resources found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Snapshots # ══════════════════════════════════════════════════════════════════════════ - class TestCleanupSnapshots: def test_old_snapshot_with_older_than_flag(self, invoke, config_dir, mock_client, sample_profile): @@ -212,12 +198,10 @@ def test_snapshots_ignored_without_older_than_flag(self, invoke, config_dir, moc assert result.exit_code == 0 assert "No orphaned resources found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Ports # ══════════════════════════════════════════════════════════════════════════ - class TestCleanupPorts: def test_orphan_port_detected(self, invoke, config_dir, mock_client, sample_profile): @@ -241,12 +225,10 @@ def test_port_with_device_ok(self, invoke, config_dir, mock_client, sample_profi assert result.exit_code == 0 assert "No orphaned resources found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Security groups # ══════════════════════════════════════════════════════════════════════════ - class TestCleanupSecurityGroups: def test_unused_sg_detected(self, invoke, config_dir, mock_client, sample_profile): @@ -292,12 +274,10 @@ def test_used_sg_ok(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No orphaned resources found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Servers in ERROR # ══════════════════════════════════════════════════════════════════════════ - class TestCleanupServers: def test_error_server_detected(self, invoke, config_dir, mock_client, sample_profile): @@ -321,12 +301,10 @@ def test_active_server_ok(self, invoke, config_dir, mock_client, sample_profile) assert result.exit_code == 0 assert "No orphaned resources found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Delete mode # ══════════════════════════════════════════════════════════════════════════ - class TestCleanupDelete: def test_dry_run_by_default(self, invoke, config_dir, mock_client, sample_profile): @@ -416,12 +394,10 @@ def sometimes_fail(url, **kw): assert result.exit_code == 0 assert "2/3" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Combined # ══════════════════════════════════════════════════════════════════════════ - class TestCleanupCombined: def test_multiple_issue_types(self, invoke, config_dir, mock_client, sample_profile): @@ -448,7 +424,6 @@ def test_multiple_issue_types(self, invoke, config_dir, mock_client, sample_prof assert "security-group" in result.output assert "server" in result.output - class TestCleanupHelp: def test_help(self, invoke): diff --git a/tests/test_cli_registration.py b/tests/test_cli_registration.py index 845e0d3..1e8a0bc 100644 --- a/tests/test_cli_registration.py +++ b/tests/test_cli_registration.py @@ -20,7 +20,6 @@ "limit", "registered-limit", "access-rule", "token", "endpoint-group", ] - class TestCLIRegistration: def test_all_commands_registered(self): @@ -48,7 +47,6 @@ def test_version(self, invoke): assert result.exit_code == 0 assert "orca" in result.output - class TestSubcommandHelp: """Verify each command group shows --help without errors.""" @@ -57,7 +55,6 @@ def test_command_help(self, invoke, cmd): result = invoke([cmd, "--help"]) assert result.exit_code == 0, f"'{cmd} --help' failed: {result.output}" - class TestServerSubcommands: """Verify key server subcommands including the new port-forward.""" @@ -76,13 +73,11 @@ def test_port_forward_requires_args(self, invoke): # Should fail because SERVER_ID and PORT_MAPPING are required assert result.exit_code != 0 - class TestProfileSubcommands: """Verify profile subcommands including conversions.""" PROFILE_SUBCMDS = [ "list", "show", "add", "edit", "switch", "remove", "rename", - "set-color", "to-openrc", "to-clouds", "from-openrc", "from-clouds", ] @pytest.mark.parametrize("sub", PROFILE_SUBCMDS) @@ -90,7 +85,6 @@ def test_profile_subcommand_help(self, invoke, sub): result = invoke(["profile", sub, "--help"]) assert result.exit_code == 0, f"'profile {sub} --help' failed" - class TestNewServiceCommands: """Verify the 3 new service command groups have their subcommands.""" @@ -109,8 +103,7 @@ def test_container_subcommands(self, invoke): def test_stack_subcommands(self, invoke): result = invoke(["stack", "--help"]) assert result.exit_code == 0 - for sub in ["list", "show", "create", "delete", "event-list", - "resource-list", "output-list", "topology"]: + for sub in ["list", "show", "create", "delete", "topology"]: assert sub in result.output, f"'stack {sub}' missing" def test_zone_subcommands(self, invoke): @@ -134,5 +127,5 @@ def test_event_subcommands(self, invoke): def test_auth_subcommands(self, invoke): result = invoke(["auth", "--help"]) assert result.exit_code == 0 - for sub in ["whoami", "token-debug", "check"]: + for sub in ["whoami", "check"]: assert sub in result.output, f"'auth {sub}' missing" diff --git a/tests/test_client.py b/tests/test_client.py index d0e05e1..cdba95e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -22,7 +22,6 @@ def _no_real_cache(tmp_path): with patch("orca_cli.core.client.TOKEN_CACHE_PATH", tmp_path / "token_cache.yaml"): yield - FAKE_CATALOG = [ { "type": "compute", @@ -54,7 +53,6 @@ def _no_real_cache(tmp_path): } } - def _make_auth_response(status_code=201, token="fake-token"): """Build a mock httpx response for Keystone auth.""" resp = MagicMock() @@ -65,7 +63,6 @@ def _make_auth_response(status_code=201, token="fake-token"): resp.text = "" return resp - class TestOrcaClientInit: @patch("orca_cli.core.client.httpx.Client") @@ -228,7 +225,6 @@ def test_insecure_emits_stderr_warning(self, mock_httpx_cls, capsys): captured = capsys.readouterr() assert "TLS certificate verification is disabled" in captured.err - class TestEndpointResolution: @patch("orca_cli.core.client.httpx.Client") @@ -275,7 +271,6 @@ def test_domain_ref_id(self): def test_domain_ref_fallback(self): assert OrcaClient._domain_ref(None, None) == {"name": "Default"} - # ── Token caching ───────────────────────────────────────────────────────────── BASE_CFG = { @@ -290,7 +285,6 @@ def test_domain_ref_fallback(self): PAST_EXPIRY = (datetime.now(timezone.utc) - timedelta(seconds=1)).isoformat() NEAR_EXPIRY = (datetime.now(timezone.utc) + timedelta(seconds=TOKEN_EXPIRY_BUFFER - 1)).isoformat() - def _make_client_with_http(mock_httpx_cls, cfg=None, token="tok"): http = MagicMock() http.post.return_value = _make_auth_response(token=token) @@ -298,7 +292,6 @@ def _make_client_with_http(mock_httpx_cls, cfg=None, token="tok"): client = OrcaClient(cfg or BASE_CFG) return client, http - class TestTokenCaching: """Disk-based token cache: write on auth, read on next init, invalidate on expiry/401.""" @@ -471,7 +464,6 @@ def test_cache_key_differs_by_project(self, mock_httpx_cls): assert ca._cache_key != cb._cache_key - class TestRequestRetryOn401: """_request() clears cache and re-auths transparently on 401 with cached token.""" @@ -590,7 +582,6 @@ def test_cache_cleared_on_401(self, mock_httpx_cls): new_data = yaml.safe_load(self._cache_path.read_text()) assert new_data["token"] == "new-token" - # ── Transient-error retry ───────────────────────────────────────────────────── def _resp(status_code: int, body: Any = None, headers: dict | None = None) -> MagicMock: @@ -604,7 +595,6 @@ def _resp(status_code: int, body: Any = None, headers: dict | None = None) -> Ma r.headers = headers or {} return r - class TestRequestRetryOnTransient: """_request() retries idempotent methods on 5xx and network errors.""" @@ -758,7 +748,6 @@ def test_post_network_error_not_retried(self, mock_httpx_cls, _no_sleep): assert http.post.call_count == 2 _no_sleep.assert_not_called() - # ── 429 rate-limit retry ──────────────────────────────────────────────────── class TestRateLimitRetry: @@ -857,7 +846,6 @@ def test_429_http_date_retry_after_parsed(self, mock_httpx_cls, _no_sleep): # Parsed delta is ~2s; allow for sub-second parsing drift and jitter. assert 0.0 <= waited <= 3.0 - # ── paginate() helper ──────────────────────────────────────────────────────── class TestPaginate: @@ -936,7 +924,6 @@ def test_breaks_when_marker_would_repeat(self, mock_httpx_cls): assert len(items) >= 1000 # at least one full page collected assert http.get.call_count <= 3 - # ── Nova microversion header scoping ───────────────────────────────────────── class TestNovaMicroversionHeader: @@ -981,7 +968,6 @@ def test_header_absent_on_stream(self, mock_httpx_cls): headers = http.stream.call_args.kwargs["headers"] assert "X-OpenStack-Nova-API-Version" not in headers - # ── Typed endpoint URL properties ───────────────────────────────────────────── RICH_CATALOG = [ @@ -1008,7 +994,6 @@ def test_header_absent_on_stream(self, mock_httpx_cls): } } - def _rich_auth_response(): resp = MagicMock() resp.status_code = 201 @@ -1018,7 +1003,6 @@ def _rich_auth_response(): resp.text = "" return resp - class TestTypedEndpointURLs: """Every typed URL property must resolve via _endpoint_for against the catalogue.""" @@ -1046,7 +1030,6 @@ def test_all_service_urls(self, mock_httpx_cls): assert client.placement_url == "https://placement.example.com" assert client.alarming_url == "https://alarming.example.com" - class TestWithVersion: """``with_version`` is the idempotent /vN appender used by services to survive both versionless and pre-versioned catalogue URLs.""" @@ -1074,7 +1057,6 @@ def test_does_not_match_partial_segment(self): # /v33 is not /v3, must still append. assert with_version("http://h/api/v33", "v3") == "http://h/api/v33/v3" - class TestVolumeUrlFallback: """``volume_url`` accepts both the modern ``block-storage`` (Train+) and legacy ``volumev3``/``volumev2`` service types so orca runs against any @@ -1124,7 +1106,6 @@ def test_raises_when_no_cinder_endpoint(self, mock_httpx_cls): with pytest.raises(APIError, match="Cinder/block-storage"): _ = client.volume_url - class TestIsComputeUrlHandlesMissingNova: """_is_compute_url() must return False when Nova isn't in the catalog.""" @@ -1151,7 +1132,6 @@ def test_returns_false_without_nova(self, mock_httpx_cls): headers = http.get.call_args.kwargs["headers"] assert "X-OpenStack-Nova-API-Version" not in headers - class TestExtractErrorMessage: """_extract_error_message unwraps OpenStack's inconsistent error shapes.""" @@ -1179,7 +1159,6 @@ def test_unknown_shape_stringified(self): result = OrcaClient._extract_error_message(body) assert "random" in result - class TestForbiddenAndHtmlResponses: """403 must surface as PermissionDeniedError (not AuthenticationError) so users aren't told to re-run `orca setup` for a permissions issue. HTML error bodies @@ -1236,7 +1215,6 @@ def test_html_sniffed_without_content_type(self): resp.text = "boom" assert OrcaClient._is_html_response(resp) is True - class TestRequestIdPropagation: """OpenStack request-id headers must reach the user via APIError.""" @@ -1300,7 +1278,6 @@ def test_missing_header_leaves_request_id_empty(self, mock_httpx_cls): assert exc_info.value.request_id == "" assert "request-id" not in str(exc_info.value) - class TestHandleResponseNonJson: """When the error body isn't JSON, fall back to raw text (truncated).""" @@ -1324,7 +1301,6 @@ def test_non_json_error_body_uses_text(self, mock_httpx_cls): assert "Internal" in str(exc_info.value) or "Internal" in exc_info.value.message - class TestPatchWithContent: """PATCH with content + content_type takes the content branch.""" @@ -1345,7 +1321,6 @@ def test_patch_with_raw_content(self, mock_httpx_cls): assert kwargs["content"] == b"binary" assert kwargs["headers"]["Content-Type"] == "application/octet-stream" - class TestPutStream: @patch("orca_cli.core.client.httpx.Client") @@ -1395,7 +1370,6 @@ def test_put_stream_skips_retry_on_5xx(self, mock_httpx_cls): assert resp.status_code == 503 assert http.put.call_count == 1 # no retry - class TestClose: @patch("orca_cli.core.client.httpx.Client") @@ -1408,7 +1382,6 @@ def test_close_delegates_to_http(self, mock_httpx_cls): client.close() http.close.assert_called_once() - # ════════════════════════════════════════════════════════════════════════════ # Project scoping — concrete cloud configurations # ════════════════════════════════════════════════════════════════════════════ @@ -1427,14 +1400,12 @@ def test_close_delegates_to_http(self, mock_httpx_cls): # These tests pin all four shapes, plus the way config flows through profile # / env / clouds.yaml. - def _scope(http: MagicMock) -> dict: """Return the ``auth.scope`` block from the most recent POST sent to the mocked Keystone client.""" payload = http.post.call_args.kwargs["json"] return payload["auth"].get("scope") - class TestPasswordAuthScoping: """``OrcaClient(cfg)`` produces the right Keystone scope block for each cloud-shape we have to support. @@ -1585,7 +1556,6 @@ def test_user_domain_id_path(self, mock_httpx_cls): scope = payload["auth"]["scope"]["project"] assert scope == {"name": "demo", "domain": {"id": "default"}} - class TestAppCredentialAuthScoping: """Application credentials are pre-scoped at creation time. The client must emit the ``v3applicationcredential`` identity method and *not* a @@ -1635,7 +1605,6 @@ def test_app_credential_by_name_requires_user_ref(self, mock_httpx_cls): assert ac["user"] == {"name": "alice", "domain": {"name": "Default"}} assert "scope" not in payload["auth"] - class TestEndToEndProjectScoping: """Full resolution chain: profile / OS_* env / clouds.yaml all converge on ``OrcaClient`` with the same set of shapes — and produce the right diff --git a/tests/test_cluster.py b/tests/test_cluster.py index fb7a340..c2f4cd8 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -9,7 +9,6 @@ CLUSTER_ID = "11112222-3333-4444-5555-666677778888" TEMPLATE_ID = "aaaabbbb-cccc-dddd-eeee-ffffffffffff" - def _cluster(uuid=CLUSTER_ID, name="my-k8s", status="CREATE_COMPLETE"): return { "uuid": uuid, "name": name, "status": status, @@ -23,7 +22,6 @@ def _cluster(uuid=CLUSTER_ID, name="my-k8s", status="CREATE_COMPLETE"): "created_at": "2025-01-01", "updated_at": None, } - def _template(uuid=TEMPLATE_ID, name="k8s-tmpl"): return { "uuid": uuid, "name": name, "coe": "kubernetes", @@ -39,7 +37,6 @@ def _template(uuid=TEMPLATE_ID, name="k8s-tmpl"): "created_at": "2025-01-01", "updated_at": None, } - def _setup_mock(mock_client): mock_client.container_infra_url = "https://magnum.example.com/v1" @@ -80,12 +77,10 @@ def _delete(url, **kwargs): return {"posted": posted, "patched": patched, "deleted": deleted} - # ══════════════════════════════════════════════════════════════════════════ # cluster list # ══════════════════════════════════════════════════════════════════════════ - class TestClusterList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -107,12 +102,10 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No clusters found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # cluster show # ══════════════════════════════════════════════════════════════════════════ - class TestClusterShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -124,12 +117,10 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "my-k8s" in result.output - # ══════════════════════════════════════════════════════════════════════════ # cluster create # ══════════════════════════════════════════════════════════════════════════ - class TestClusterCreate: def test_create(self, invoke, config_dir, mock_client, sample_profile): @@ -161,12 +152,10 @@ def test_create_with_options(self, invoke, config_dir, mock_client, sample_profi assert body["keypair"] == "my-key" assert body["flavor_id"] == "m1.large" - # ══════════════════════════════════════════════════════════════════════════ # cluster delete # ══════════════════════════════════════════════════════════════════════════ - class TestClusterDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -179,12 +168,10 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert "deletion started" in result.output assert len(state["deleted"]) == 1 - # ══════════════════════════════════════════════════════════════════════════ # cluster resize # ══════════════════════════════════════════════════════════════════════════ - class TestClusterResize: def test_resize(self, invoke, config_dir, mock_client, sample_profile): @@ -196,12 +183,10 @@ def test_resize(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "resize" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # cluster kubeconfig # ══════════════════════════════════════════════════════════════════════════ - class TestClusterKubeconfig: def test_kubeconfig(self, invoke, config_dir, mock_client, sample_profile): @@ -214,12 +199,10 @@ def test_kubeconfig(self, invoke, config_dir, mock_client, sample_profile): assert "API URL" in result.output assert "10.0.0.5" in result.output - # ══════════════════════════════════════════════════════════════════════════ # template-list / template-show # ══════════════════════════════════════════════════════════════════════════ - class TestClusterTemplates: def test_template_list(self, invoke, config_dir, mock_client, sample_profile): @@ -227,7 +210,7 @@ def test_template_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["cluster", "template-list"]) + result = invoke(["cluster", "template", "list"]) assert result.exit_code == 0 assert "k8s" in result.output @@ -237,7 +220,7 @@ def test_template_list_empty(self, invoke, config_dir, mock_client, sample_profi mock_client.container_infra_url = "https://magnum.example.com/v1" mock_client.get = lambda url, **kw: {"clustertemplates": []} - result = invoke(["cluster", "template-list"]) + result = invoke(["cluster", "template", "list"]) assert result.exit_code == 0 assert "No cluster templates found" in result.output @@ -246,7 +229,7 @@ def test_template_show(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["cluster", "template-show", TEMPLATE_ID]) + result = invoke(["cluster", "template", "show", TEMPLATE_ID]) assert result.exit_code == 0 assert "k8s-tmpl" in result.output @@ -255,7 +238,7 @@ def test_template_create(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["cluster", "template-create", "new-tmpl", + result = invoke(["cluster", "template", "create", "new-tmpl", "--image", "img-1", "--external-network", "ext-net", "--coe", "kubernetes"]) assert result.exit_code == 0 @@ -269,35 +252,30 @@ def test_template_delete(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["cluster", "template-delete", TEMPLATE_ID, "-y"]) + result = invoke(["cluster", "template", "delete", TEMPLATE_ID, "-y"]) assert result.exit_code == 0 assert "deleted" in result.output.lower() assert len(state["deleted"]) == 1 - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestClusterHelp: def test_cluster_help(self, invoke): result = invoke(["cluster", "--help"]) assert result.exit_code == 0 for cmd in ("list", "show", "create", "delete", "resize", - "kubeconfig", "template-list", "template-show", - "template-create", "template-delete"): + "kubeconfig"): assert cmd in result.output - # ══════════════════════════════════════════════════════════════════════════ # cluster upgrade / nodegroup CRUD # ══════════════════════════════════════════════════════════════════════════ NG_ID = "11111111-1111-1111-1111-111111111111" - class TestClusterUpgrade: def test_upgrade(self, invoke, mock_client): @@ -311,7 +289,6 @@ def test_upgrade(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["cluster", "upgrade", "--help"]).exit_code == 0 - class TestNodegroupList: def test_list(self, invoke, mock_client): @@ -320,19 +297,18 @@ def test_list(self, invoke, mock_client): {"uuid": NG_ID, "name": "default-worker", "role": "worker", "node_count": 3, "status": "CREATE_COMPLETE", "flavor_id": "m1.medium"}, ]} - result = invoke(["cluster", "nodegroup-list", CLUSTER_ID]) + result = invoke(["cluster", "nodegroup", "list", CLUSTER_ID]) assert result.exit_code == 0 assert "work" in result.output def test_list_empty(self, invoke, mock_client): mock_client.container_infra_url = "https://magnum.example.com/v1" mock_client.get.return_value = {"nodegroups": []} - result = invoke(["cluster", "nodegroup-list", CLUSTER_ID]) + result = invoke(["cluster", "nodegroup", "list", CLUSTER_ID]) assert "No node groups" in result.output def test_help(self, invoke): - assert invoke(["cluster", "nodegroup-list", "--help"]).exit_code == 0 - + assert invoke(["cluster", "nodegroup", "list", "--help"]).exit_code == 0 class TestNodegroupShow: @@ -345,20 +321,19 @@ def test_show(self, invoke, mock_client): "status": "CREATE_COMPLETE", "status_reason": "", "created_at": "", "updated_at": "", } - result = invoke(["cluster", "nodegroup-show", CLUSTER_ID, NG_ID]) + result = invoke(["cluster", "nodegroup", "show", CLUSTER_ID, NG_ID]) assert result.exit_code == 0 assert "work" in result.output def test_help(self, invoke): - assert invoke(["cluster", "nodegroup-show", "--help"]).exit_code == 0 - + assert invoke(["cluster", "nodegroup", "show", "--help"]).exit_code == 0 class TestNodegroupCreate: def test_create(self, invoke, mock_client): mock_client.container_infra_url = "https://magnum.example.com/v1" mock_client.post.return_value = {"uuid": NG_ID} - result = invoke(["cluster", "nodegroup-create", CLUSTER_ID, + result = invoke(["cluster", "nodegroup", "create", CLUSTER_ID, "--name", "gpu-pool", "--flavor-id", "gpu.medium", "--node-count", "2"]) @@ -368,14 +343,13 @@ def test_create(self, invoke, mock_client): assert body["node_count"] == 2 def test_help(self, invoke): - assert invoke(["cluster", "nodegroup-create", "--help"]).exit_code == 0 - + assert invoke(["cluster", "nodegroup", "create", "--help"]).exit_code == 0 class TestNodegroupUpdate: def test_update_node_count(self, invoke, mock_client): mock_client.container_infra_url = "https://magnum.example.com/v1" - result = invoke(["cluster", "nodegroup-update", CLUSTER_ID, NG_ID, + result = invoke(["cluster", "nodegroup", "update", CLUSTER_ID, NG_ID, "--node-count", "5"]) assert result.exit_code == 0 ops = mock_client.patch.call_args[1]["json"] @@ -383,27 +357,26 @@ def test_update_node_count(self, invoke, mock_client): def test_update_nothing(self, invoke, mock_client): mock_client.container_infra_url = "https://magnum.example.com/v1" - result = invoke(["cluster", "nodegroup-update", CLUSTER_ID, NG_ID]) + result = invoke(["cluster", "nodegroup", "update", CLUSTER_ID, NG_ID]) assert result.exit_code == 0 mock_client.patch.assert_not_called() def test_help(self, invoke): - assert invoke(["cluster", "nodegroup-update", "--help"]).exit_code == 0 - + assert invoke(["cluster", "nodegroup", "update", "--help"]).exit_code == 0 class TestNodegroupDelete: def test_delete_yes(self, invoke, mock_client): mock_client.container_infra_url = "https://magnum.example.com/v1" - result = invoke(["cluster", "nodegroup-delete", CLUSTER_ID, NG_ID, "--yes"]) + result = invoke(["cluster", "nodegroup", "delete", CLUSTER_ID, NG_ID, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): mock_client.container_infra_url = "https://magnum.example.com/v1" - result = invoke(["cluster", "nodegroup-delete", CLUSTER_ID, NG_ID], input="n\n") + result = invoke(["cluster", "nodegroup", "delete", CLUSTER_ID, NG_ID], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help(self, invoke): - assert invoke(["cluster", "nodegroup-delete", "--help"]).exit_code == 0 + assert invoke(["cluster", "nodegroup", "delete", "--help"]).exit_code == 0 diff --git a/tests/test_completion.py b/tests/test_completion.py index c21696e..3a22dcd 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -8,7 +8,6 @@ # completion — print instructions (legacy behavior) # ══════════════════════════════════════════════════════════════════════════ - class TestCompletion: def test_bash(self, invoke): @@ -46,12 +45,10 @@ def test_no_shell_no_env(self, invoke, monkeypatch): assert result.exit_code == 1 assert "auto-detect" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # completion install — auto-install # ══════════════════════════════════════════════════════════════════════════ - class TestCompletionInstall: def test_install_bash_explicit(self, invoke): @@ -80,12 +77,10 @@ def test_install_invalid_shell(self, invoke): result = invoke(["completion", "install", "powershell"]) assert result.exit_code != 0 - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestCompletionHelp: def test_completion_help(self, invoke): diff --git a/tests/test_completions.py b/tests/test_completions.py index bc8bb53..7a82ff0 100644 --- a/tests/test_completions.py +++ b/tests/test_completions.py @@ -18,7 +18,6 @@ def fake_ctx(): ctx.parent = None return ctx - @pytest.fixture def fake_ctx_with_profile(): ctx = MagicMock(spec=click.Context) @@ -26,7 +25,6 @@ def fake_ctx_with_profile(): ctx.parent = None return ctx - class TestBuildClient: def test_walks_parent_for_profile(self): @@ -67,7 +65,6 @@ def test_builds_client_when_config_ok(self, fake_ctx): client, profile = completions._build_client(fake_ctx) assert client is not None - class TestMatches: def test_match_on_id(self): @@ -82,7 +79,6 @@ def test_no_match(self): def test_case_insensitive(self): assert completions._matches({"id": "ABC", "name": ""}, "abc") is True - class TestCompleteFromCache: """When cache is fresh, no client is built.""" @@ -131,7 +127,6 @@ def test_fetch_failure_returns_empty(self, fake_ctx): out = completions.complete_servers(fake_ctx, None, "") assert out == [] - class TestAllCompletionFunctions: """Exercise every completion callback once with a cache hit.""" @@ -148,7 +143,6 @@ def test_cache_hit_path(self, fake_ctx, fn, items): out = fn(fake_ctx, None, "") assert len(out) >= 1 - class TestFetchPathsPerFunction: """Cache miss → fetch → different URLs/payloads per resource type.""" @@ -216,7 +210,6 @@ def test_security_groups_fetch(self, fake_ctx): out = completions.complete_security_groups(fake_ctx, None, "") assert out[0].value == "default" # name used, not id - class TestClientCloseFailureSwallowed: """client.close() exceptions in the finally branch must not propagate.""" diff --git a/tests/test_compute.py b/tests/test_compute.py index 1ced066..1b3aa9e 100644 --- a/tests/test_compute.py +++ b/tests/test_compute.py @@ -10,7 +10,6 @@ HV_ID = "21" SG_GROUP_ID = "gggg0000-1111-2222-3333-444455556666" - # ── helpers ──────────────────────────────────────────────────────────────── def _flavor(fid=FLAVOR_ID): @@ -21,7 +20,6 @@ def _flavor(fid=FLAVOR_ID): "extra_specs": {"hw:cpu_policy": "shared"}, } - def _hypervisor(hid=HV_ID): return { "id": hid, "hypervisor_hostname": "compute-01.example.com", @@ -32,7 +30,6 @@ def _hypervisor(hid=HV_ID): "running_vms": 4, "current_workload": 0, "host_ip": "10.0.0.1", } - def _aggregate(aid=AGG_ID): return { "id": aid, "name": "ssd-hosts", "availability_zone": "az1", @@ -41,7 +38,6 @@ def _aggregate(aid=AGG_ID): "created_at": "2025-01-01", "updated_at": None, } - def _server_group(gid=SG_GROUP_ID): return { "id": gid, "name": "no-same-host", @@ -49,7 +45,6 @@ def _server_group(gid=SG_GROUP_ID): "members": [], "project_id": "proj-1", "user_id": "user-1", } - def _server(sid=SERVER_ID): return { "id": sid, "name": "web-01", "status": "ACTIVE", @@ -63,7 +58,6 @@ def _server(sid=SERVER_ID): "created": "2025-01-01", "updated": "2025-01-01", } - def _setup_compute_mock(mock_client): mock_client.compute_url = "https://nova.example.com/v2.1" posted = {} @@ -133,12 +127,10 @@ def _delete(url, **kwargs): return {"posted": posted, "put_data": put_data, "deleted": deleted} - # ══════════════════════════════════════════════════════════════════════════ # Flavor (extended) # ══════════════════════════════════════════════════════════════════════════ - class TestFlavorExtended: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -197,12 +189,10 @@ def test_unset(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "removed" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Hypervisors # ══════════════════════════════════════════════════════════════════════════ - class TestHypervisor: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -232,12 +222,10 @@ def test_stats(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "vcpus" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # Aggregates # ══════════════════════════════════════════════════════════════════════════ - class TestAggregate: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -281,7 +269,7 @@ def test_add_host(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_compute_mock(mock_client) - result = invoke(["aggregate", "add-host", AGG_ID, "compute-03"]) + result = invoke(["aggregate", "host", "add", AGG_ID, "compute-03"]) assert result.exit_code == 0 assert "added" in result.output @@ -290,7 +278,7 @@ def test_remove_host(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_compute_mock(mock_client) - result = invoke(["aggregate", "remove-host", AGG_ID, "compute-01"]) + result = invoke(["aggregate", "host", "remove", AGG_ID, "compute-01"]) assert result.exit_code == 0 assert "removed" in result.output @@ -313,12 +301,10 @@ def test_set_nothing(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Nothing" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Availability Zones # ══════════════════════════════════════════════════════════════════════════ - class TestAvailabilityZones: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -331,12 +317,10 @@ def test_list(self, invoke, config_dir, mock_client, sample_profile): assert "az1" in result.output assert "az2" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Limits # ══════════════════════════════════════════════════════════════════════════ - class TestLimits: def test_limits(self, invoke, config_dir, mock_client, sample_profile): @@ -349,12 +333,10 @@ def test_limits(self, invoke, config_dir, mock_client, sample_profile): assert "Instances" in result.output assert "10" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Server Groups # ══════════════════════════════════════════════════════════════════════════ - class TestServerGroups: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -394,12 +376,10 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "deleted" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Server: migrate, live-migrate, sg add/remove, set, metadata, tags # ══════════════════════════════════════════════════════════════════════════ - class TestServerNewCommands: def test_migrate(self, invoke, config_dir, mock_client, sample_profile): @@ -425,7 +405,7 @@ def test_add_security_group(self, invoke, config_dir, mock_client, sample_profil set_active_profile("p") _setup_compute_mock(mock_client) - result = invoke(["server", "add-security-group", SERVER_ID, "allow-ssh"]) + result = invoke(["server", "add", "security-group", SERVER_ID, "allow-ssh"]) assert result.exit_code == 0 assert "added" in result.output @@ -434,7 +414,7 @@ def test_remove_security_group(self, invoke, config_dir, mock_client, sample_pro set_active_profile("p") _setup_compute_mock(mock_client) - result = invoke(["server", "remove-security-group", SERVER_ID, "allow-ssh"]) + result = invoke(["server", "remove", "security-group", SERVER_ID, "allow-ssh"]) assert result.exit_code == 0 assert "removed" in result.output @@ -479,7 +459,7 @@ def test_metadata_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_compute_mock(mock_client) - result = invoke(["server", "metadata-list", SERVER_ID]) + result = invoke(["server", "metadata", "list", SERVER_ID]) assert result.exit_code == 0 assert "env" in result.output @@ -488,16 +468,14 @@ def test_tag_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_compute_mock(mock_client) - result = invoke(["server", "tag-list", SERVER_ID]) + result = invoke(["server", "tag", "list", SERVER_ID]) assert result.exit_code == 0 assert "web" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestComputeHelp: def test_hypervisor_help(self, invoke): @@ -509,7 +487,7 @@ def test_hypervisor_help(self, invoke): def test_aggregate_help(self, invoke): result = invoke(["aggregate", "--help"]) assert result.exit_code == 0 - for cmd in ("list", "show", "create", "delete", "add-host", "remove-host", "set"): + for cmd in ("list", "show", "create", "delete", "set"): assert cmd in result.output def test_availability_zone_help(self, invoke): diff --git a/tests/test_config.py b/tests/test_config.py index 4d33ae6..5f41d5a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -85,7 +85,6 @@ def test_rename_to_existing_raises(self, config_dir, sample_profile): def test_get_nonexistent_profile_returns_empty(self, config_dir): assert get_profile("nope") == {} - # ── Legacy migration ──────────────────────────────────────────────────── class TestLegacyMigration: @@ -157,7 +156,6 @@ def test_normalise_legacy_keys_treats_non_uuid_as_legacy_name(self): assert cfg["project_name"] == "production" assert "project_id" not in cfg - # ── config_is_complete ────────────────────────────────────────────────── class TestConfigIsComplete: @@ -197,7 +195,6 @@ def test_incomplete_no_project(self): } assert config_is_complete(cfg) is False - # ── clouds.yaml ───────────────────────────────────────────────────────── class TestCloudsYaml: @@ -297,7 +294,6 @@ def test_load_config_via_os_cloud(self, monkeypatch, config_dir, tmp_path): assert cfg["username"] == "cloud-user" assert cfg["project_name"] == "cloud-proj" - # ── OS_* env vars ─────────────────────────────────────────────────────── class TestOSEnvVars: @@ -358,7 +354,6 @@ def test_orca_env_overlays_os_env(self, monkeypatch, config_dir): assert cfg["auth_url"] == "https://os:5000" assert cfg["username"] == "orca-override" # ORCA_* wins - # ── load_config priority chain ────────────────────────────────────────── class TestLoadConfigPriority: @@ -429,7 +424,6 @@ def test_os_env_beats_os_cloud(self, monkeypatch, config_dir, tmp_path): cfg = load_config() assert cfg["username"] == "env-user" # OS_* wins - # ── Edge cases & error paths ───────────────────────────────────────────── class TestConfigEdgeCases: @@ -479,7 +473,6 @@ def test_load_clouds_yaml_unknown_cloud_returns_empty(self, monkeypatch, tmp_pat from orca_cli.core.config import _load_clouds_yaml assert _load_clouds_yaml("nope") == {} - class TestCloudsYamlNormalisation: """Cover fallback branches in _normalise_clouds_yaml.""" diff --git a/tests/test_context.py b/tests/test_context.py index 8fc8371..829bf1e 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -112,7 +112,6 @@ def fake_client(cfg): assert c1 is c2 assert calls["n"] == 1 - # ── E2E: the global --region flag on the root CLI ─────────────────────────── class TestGlobalRegionFlag: diff --git a/tests/test_dns.py b/tests/test_dns.py index eee8036..57f72a6 100644 --- a/tests/test_dns.py +++ b/tests/test_dns.py @@ -8,7 +8,6 @@ DNS_URL = "https://dns.example.com" - def _zone(zone_id="zone-1111-2222-3333-444444444444", name="example.com.", status="ACTIVE", ztype="PRIMARY", email="admin@example.com", ttl=3600, serial=1234567890): @@ -20,7 +19,6 @@ def _zone(zone_id="zone-1111-2222-3333-444444444444", name="example.com.", "created_at": "2025-01-01T00:00:00Z", "updated_at": None, "version": 1, } - def _recordset(rs_id="rs-1111-2222-3333-444444444444", name="www.example.com.", rtype="A", records=None, status="ACTIVE", ttl=300, zone_id="zone-1111-2222-3333-444444444444", zone_name="example.com."): @@ -31,7 +29,6 @@ def _recordset(rs_id="rs-1111-2222-3333-444444444444", name="www.example.com.", "created_at": "2025-01-01T00:00:00Z", "updated_at": None, "version": 1, } - def _setup_mock(mock_client, zones=None, recordsets=None, zone_detail=None): zones = zones or [] recordsets = recordsets or [] @@ -76,12 +73,10 @@ def _delete(url, **kwargs): mock_client.delete = _delete mock_client.dns_url = DNS_URL - # ══════════════════════════════════════════════════════════════════════════ # zone list # ══════════════════════════════════════════════════════════════════════════ - class TestZoneList: def test_list_zones(self, invoke, config_dir, mock_client, sample_profile): @@ -114,12 +109,10 @@ def test_list_shows_columns(self, invoke, config_dir, mock_client, sample_profil # Rich may truncate ACTIVE→ACTI… and email; just verify the table rendered assert "DNS Zones" in result.output - # ══════════════════════════════════════════════════════════════════════════ # zone show # ══════════════════════════════════════════════════════════════════════════ - class TestZoneShow: def test_show_by_id(self, invoke, config_dir, mock_client, sample_profile): @@ -133,12 +126,10 @@ def test_show_by_id(self, invoke, config_dir, mock_client, sample_profile): assert "example.com." in result.output assert "ACTIVE" in result.output - # ══════════════════════════════════════════════════════════════════════════ # zone create # ══════════════════════════════════════════════════════════════════════════ - class TestZoneCreate: def test_create_zone(self, invoke, config_dir, mock_client, sample_profile): @@ -178,12 +169,10 @@ def test_create_zone_missing_dot(self, invoke, config_dir, mock_client, sample_p assert result.exit_code != 0 assert "dot" in result.output.lower() or "dot" in str(result.exception).lower() - # ══════════════════════════════════════════════════════════════════════════ # zone set (formerly zone-update) # ══════════════════════════════════════════════════════════════════════════ - class TestZoneUpdate: def test_update_zone(self, invoke, config_dir, mock_client, sample_profile): @@ -213,12 +202,10 @@ def test_update_nothing(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Nothing to update" in result.output - # ══════════════════════════════════════════════════════════════════════════ # zone delete # ══════════════════════════════════════════════════════════════════════════ - class TestZoneDelete: def test_delete_zone(self, invoke, config_dir, mock_client, sample_profile): @@ -230,12 +217,10 @@ def test_delete_zone(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "deleted" in result.output - # ══════════════════════════════════════════════════════════════════════════ # recordset list # ══════════════════════════════════════════════════════════════════════════ - class TestRecordList: def test_list_records(self, invoke, config_dir, mock_client, sample_profile): @@ -262,12 +247,10 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No recordsets found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # recordset show # ══════════════════════════════════════════════════════════════════════════ - class TestRecordShow: def test_show_record(self, invoke, config_dir, mock_client, sample_profile): @@ -287,12 +270,10 @@ def _get(url, **kwargs): assert result.exit_code == 0 assert "www.example.com." in result.output - # ══════════════════════════════════════════════════════════════════════════ # recordset create # ══════════════════════════════════════════════════════════════════════════ - class TestRecordCreate: def test_create_a_record(self, invoke, config_dir, mock_client, sample_profile): @@ -351,12 +332,10 @@ def capture_post(url, **kwargs): assert result.exit_code == 0 assert posted["ttl"] == 600 - # ══════════════════════════════════════════════════════════════════════════ # recordset set (formerly record-update) # ══════════════════════════════════════════════════════════════════════════ - class TestRecordUpdate: def test_update_record(self, invoke, config_dir, mock_client, sample_profile): @@ -388,12 +367,10 @@ def test_update_nothing(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Nothing to update" in result.output - # ══════════════════════════════════════════════════════════════════════════ # recordset delete # ══════════════════════════════════════════════════════════════════════════ - class TestRecordDelete: def test_delete_record(self, invoke, config_dir, mock_client, sample_profile): @@ -405,12 +382,10 @@ def test_delete_record(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "deleted" in result.output - # ══════════════════════════════════════════════════════════════════════════ # zone tree # ══════════════════════════════════════════════════════════════════════════ - class TestZoneTree: def test_tree_display(self, invoke, config_dir, mock_client, sample_profile): @@ -467,12 +442,10 @@ def _get(url, **kwargs): assert result.exit_code == 0 assert "example.com." in result.output - # ══════════════════════════════════════════════════════════════════════════ # zone reverse-lookup # ══════════════════════════════════════════════════════════════════════════ - class TestReverseLookup: def test_reverse_found_via_floatingips(self, invoke, config_dir, mock_client, sample_profile): @@ -532,12 +505,10 @@ def _get(url, **kwargs): assert result.exit_code == 0 assert "host.example.com." in result.output - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestDnsHelp: def test_zone_help(self, invoke): @@ -564,7 +535,6 @@ def test_recordset_create_help(self, invoke): assert "--type" in result.output assert "--record" in result.output - # ══════════════════════════════════════════════════════════════════════════ # zone transfer-request / transfer-accept / tld # ══════════════════════════════════════════════════════════════════════════ @@ -573,7 +543,6 @@ def test_recordset_create_help(self, invoke): _ZONE_ID2 = "dddddddd-dddd-dddd-dddd-dddddddddddd" _XFER_ID = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee" - class TestZoneTransferRequest: def _req(self, **kw): @@ -586,20 +555,20 @@ def test_create(self, invoke, mock_client): {"zones": [{"id": _ZONE_ID2, "name": _ZONE_NAME}]}, ] mock_client.post.return_value = {"id": _XFER_ID, "key": "secret-key"} - result = invoke(["zone", "transfer-request-create", _ZONE_NAME]) + result = invoke(["zone", "transfer", "request", "create", _ZONE_NAME]) assert result.exit_code == 0 assert "secret-key" in result.output def test_list(self, invoke, mock_client): mock_client.dns_url = DNS_URL mock_client.get.return_value = {"transfer_requests": [self._req()]} - result = invoke(["zone", "transfer-request-list"]) + result = invoke(["zone", "transfer", "request", "list"]) assert result.exit_code == 0 def test_list_empty(self, invoke, mock_client): mock_client.dns_url = DNS_URL mock_client.get.return_value = {"transfer_requests": []} - result = invoke(["zone", "transfer-request-list"]) + result = invoke(["zone", "transfer", "request", "list"]) assert "No transfer" in result.output def test_show(self, invoke, mock_client): @@ -609,39 +578,38 @@ def test_show(self, invoke, mock_client): "status": "ACTIVE", "target_project_id": "", "description": "", "created_at": "", "updated_at": "", } - result = invoke(["zone", "transfer-request-show", _XFER_ID]) + result = invoke(["zone", "transfer", "request", "show", _XFER_ID]) assert result.exit_code == 0 def test_delete_yes(self, invoke, mock_client): mock_client.dns_url = DNS_URL - result = invoke(["zone", "transfer-request-delete", _XFER_ID, "--yes"]) + result = invoke(["zone", "transfer", "request", "delete", _XFER_ID, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): mock_client.dns_url = DNS_URL - result = invoke(["zone", "transfer-request-delete", _XFER_ID], input="n\n") + result = invoke(["zone", "transfer", "request", "delete", _XFER_ID], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_accept(self, invoke, mock_client): mock_client.dns_url = DNS_URL mock_client.post.return_value = {"zone_id": _ZONE_ID2} - result = invoke(["zone", "transfer-accept", _XFER_ID, "mykey"]) + result = invoke(["zone", "transfer", "accept", _XFER_ID, "mykey"]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"] assert body["key"] == "mykey" assert body["zone_transfer_request_id"] == _XFER_ID def test_help_create(self, invoke): - assert invoke(["zone", "transfer-request-create", "--help"]).exit_code == 0 + assert invoke(["zone", "transfer", "request", "create", "--help"]).exit_code == 0 def test_help_list(self, invoke): - assert invoke(["zone", "transfer-request-list", "--help"]).exit_code == 0 + assert invoke(["zone", "transfer", "request", "list", "--help"]).exit_code == 0 def test_help_accept(self, invoke): - assert invoke(["zone", "transfer-accept", "--help"]).exit_code == 0 - + assert invoke(["zone", "transfer", "accept", "--help"]).exit_code == 0 class TestZoneTld: @@ -650,24 +618,24 @@ def test_list(self, invoke, mock_client): mock_client.get.return_value = {"tlds": [ {"id": "1", "name": "com", "description": ""}, ]} - result = invoke(["zone", "tld-list"]) + result = invoke(["zone", "tld", "list"]) assert result.exit_code == 0 assert "com" in result.output def test_create(self, invoke, mock_client): mock_client.dns_url = DNS_URL mock_client.post.return_value = {"id": "1", "name": "example"} - result = invoke(["zone", "tld-create", "example"]) + result = invoke(["zone", "tld", "create", "example"]) assert result.exit_code == 0 def test_delete_yes(self, invoke, mock_client): mock_client.dns_url = DNS_URL - result = invoke(["zone", "tld-delete", "1", "--yes"]) + result = invoke(["zone", "tld", "delete", "1", "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_help_list(self, invoke): - assert invoke(["zone", "tld-list", "--help"]).exit_code == 0 + assert invoke(["zone", "tld", "list", "--help"]).exit_code == 0 def test_help_create(self, invoke): - assert invoke(["zone", "tld-create", "--help"]).exit_code == 0 + assert invoke(["zone", "tld", "create", "--help"]).exit_code == 0 diff --git a/tests/test_doctor.py b/tests/test_doctor.py index 34b3315..f0d34e7 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -10,11 +10,9 @@ PRJ_ID = "proj-1111-1111-1111-111111111111" - def _base_token(user="admin", project="demo", project_id=PRJ_ID): return {"user": {"name": user}, "project": {"name": project, "id": project_id}} - def _limits(inst=2, inst_max=10, cores=4, cores_max=20, ram=4096, ram_max=51200): return {"limits": {"absolute": { "totalInstancesUsed": inst, "maxTotalInstances": inst_max, @@ -22,21 +20,18 @@ def _limits(inst=2, inst_max=10, cores=4, cores_max=20, ram=4096, ram_max=51200) "totalRAMUsed": ram, "maxTotalRAMSize": ram_max, }}} - def _vol_limits(vol=1, vol_max=10, gb=50, gb_max=1000): return {"limits": {"absolute": { "totalVolumesUsed": vol, "maxTotalVolumes": vol_max, "totalGigabytesUsed": gb, "maxTotalVolumeGigabytes": gb_max, }}} - def _net_quota(fip_used=2, fip_limit=10, sg_used=3, sg_limit=20): return {"quota": { "floatingip": {"used": fip_used, "limit": fip_limit}, "security_group": {"used": sg_used, "limit": sg_limit}, }} - def _sg(has_ssh=True, has_icmp=True): rules = [] if has_ssh: @@ -47,7 +42,6 @@ def _sg(has_ssh=True, has_icmp=True): return {"security_groups": [{"id": "sg-default", "name": "default", "security_group_rules": rules}]} - def _setup_happy_path(mock_client, sg_data=None): """Configure mock_client for a fully healthy environment.""" mock_client.compute_url = "https://nova.example.com/v2.1" @@ -73,7 +67,6 @@ def _get(url, **kw): mock_client.get.side_effect = _get - # ══════════════════════════════════════════════════════════════════════════ # Basic invocation # ══════════════════════════════════════════════════════════════════════════ @@ -102,7 +95,6 @@ def test_all_passed_message(self, invoke, mock_client): assert result.exit_code == 0 assert "passed" in result.output or "OK" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Authentication check & short-circuit # ══════════════════════════════════════════════════════════════════════════ @@ -141,7 +133,6 @@ def test_auth_failure_shows_skip_message(self, invoke, mock_client): assert result.exit_code == 0 assert "Skipped" in result.output or "fix authentication" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # Service-level isolation # ══════════════════════════════════════════════════════════════════════════ @@ -206,7 +197,6 @@ def _get(url, **kw): skipped_count = result.output.count("Skipped") assert skipped_count >= 2 - # ══════════════════════════════════════════════════════════════════════════ # 3-tier quota color thresholds # ══════════════════════════════════════════════════════════════════════════ @@ -261,7 +251,6 @@ def test_red_at_100(self, invoke, mock_client): assert result.exit_code == 0 assert "critical" in result.output.lower() or "10/10" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Security group checks # ══════════════════════════════════════════════════════════════════════════ @@ -288,7 +277,6 @@ def test_ok_when_both_present(self, invoke, mock_client): assert result.exit_code == 0 assert "SSH" in result.output - # ══════════════════════════════════════════════════════════════════════════ # --fix flag # ══════════════════════════════════════════════════════════════════════════ diff --git a/tests/test_event.py b/tests/test_event.py index c2189b5..a06775b 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -10,7 +10,6 @@ SRV_ID = "12345678-1234-1234-1234-123456789abc" REQ_ID = "req-aaaa-bbbb-cccc-dddd" - def _action(action="create", request_id=REQ_ID, start_time="2025-03-01T10:00:00Z", user_id="user-1", message="", events=None): return { @@ -23,7 +22,6 @@ def _action(action="create", request_id=REQ_ID, start_time="2025-03-01T10:00:00Z "events": events or [], } - def _sub_event(event_name="compute_run_instance", result="Success", start_time="2025-03-01T10:00:00Z", finish_time="2025-03-01T10:00:05Z"): return { @@ -33,14 +31,12 @@ def _sub_event(event_name="compute_run_instance", result="Success", "finish_time": finish_time, } - def _server(srv_id=SRV_ID, name="web-1", status="ACTIVE"): return { "id": srv_id, "name": name, "status": status, "security_groups": [{"name": "default"}], } - def _setup_mock(mock_client, actions=None, servers=None, action_detail=None): actions = actions or [] servers = servers or [] @@ -58,12 +54,10 @@ def _get(url, **kwargs): mock_client.get = _get mock_client.compute_url = "https://nova.example.com/v2.1" - # ══════════════════════════════════════════════════════════════════════════ # event list # ══════════════════════════════════════════════════════════════════════════ - class TestEventList: def test_list_actions(self, invoke, config_dir, mock_client, sample_profile): @@ -105,12 +99,10 @@ def test_list_shows_formatted_time(self, invoke, config_dir, mock_client, sample assert "2025-03-01" in result.output assert "10:00:00" in result.output - # ══════════════════════════════════════════════════════════════════════════ # event show # ══════════════════════════════════════════════════════════════════════════ - class TestEventShow: def test_show_action(self, invoke, config_dir, mock_client, sample_profile): @@ -174,12 +166,10 @@ def test_show_error_result(self, invoke, config_dir, mock_client, sample_profile result = invoke(["event", "show", SRV_ID, REQ_ID]) assert "Error" in result.output - # ══════════════════════════════════════════════════════════════════════════ # event all # ══════════════════════════════════════════════════════════════════════════ - class TestEventAll: def test_all_events(self, invoke, config_dir, mock_client, sample_profile): @@ -293,12 +283,10 @@ def _get(url, **kwargs): create_pos = result.output.index("create") assert stop_pos < create_pos - # ══════════════════════════════════════════════════════════════════════════ # event timeline # ══════════════════════════════════════════════════════════════════════════ - class TestEventTimeline: def test_timeline_display(self, invoke, config_dir, mock_client, sample_profile): @@ -359,12 +347,10 @@ def _get(url, **kwargs): assert result.exit_code == 0 assert "no sub-events" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestDropEmptyColumns: """Regression for the ``event list`` empty-columns rendering bug — when every row has an empty ``message`` / ``user_id`` those columns @@ -392,7 +378,6 @@ def test_never_returns_empty_list(self): defs = [("A", "a"), ("B", "b")] assert event_mod._drop_empty_columns(items, defs) == defs - class TestEventHelp: def test_event_help(self, invoke): diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index b976467..4019f4f 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -40,7 +40,6 @@ def test_request_id_omitted_when_blank(self): assert "request-id" not in err.message assert err.request_id == "" - class TestAuthenticationError: def test_default_message_suggests_setup(self): @@ -51,7 +50,6 @@ def test_custom_message(self): err = AuthenticationError("token expired") assert err.message == "token expired" - class TestConfigurationError: def test_default_message_suggests_setup(self): @@ -62,7 +60,6 @@ def test_custom_message(self): err = ConfigurationError("missing auth_url") assert err.message == "missing auth_url" - class TestHierarchy: def test_all_subclass_orca_cli_error(self): diff --git a/tests/test_export.py b/tests/test_export.py index 1ca79b4..24f99b1 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -18,7 +18,6 @@ SG_ID = "sg-8888" PORT_ID = "port-9999" - def _server(srv_id=SRV_ID, name="web-1", status="ACTIVE", image_id=IMG_ID): return { "id": srv_id, @@ -37,7 +36,6 @@ def _server(srv_id=SRV_ID, name="web-1", status="ACTIVE", image_id=IMG_ID): "created": "2025-01-01T00:00:00Z", } - def _volume(vol_id=VOL_ID, name="boot-vol", status="in-use", attached_srv=SRV_ID): attachments = [{"server_id": attached_srv}] if attached_srv else [] return { @@ -50,7 +48,6 @@ def _volume(vol_id=VOL_ID, name="boot-vol", status="in-use", attached_srv=SRV_ID "attachments": attachments, } - def _network(net_id=NET_ID, name="private-net", subnet_ids=None): return { "id": net_id, @@ -58,7 +55,6 @@ def _network(net_id=NET_ID, name="private-net", subnet_ids=None): "subnets": subnet_ids or [SUB_ID], } - def _subnet(sub_id=SUB_ID, name="private-sub", cidr="10.0.0.0/24"): return { "id": sub_id, @@ -69,7 +65,6 @@ def _subnet(sub_id=SUB_ID, name="private-sub", cidr="10.0.0.0/24"): "allocation_pools": [{"start": "10.0.0.10", "end": "10.0.0.200"}], } - def _router(rtr_id=RTR_ID, name="main-router", ext_net_id=NET_ID): return { "id": rtr_id, @@ -77,7 +72,6 @@ def _router(rtr_id=RTR_ID, name="main-router", ext_net_id=NET_ID): "external_gateway_info": {"network_id": ext_net_id}, } - def _floating_ip(fip_id=FIP_ID, ip="203.0.113.10", port_id=PORT_ID): return { "id": fip_id, @@ -86,7 +80,6 @@ def _floating_ip(fip_id=FIP_ID, ip="203.0.113.10", port_id=PORT_ID): "port_id": port_id, } - def _security_group(sg_id=SG_ID, name="default"): return { "id": sg_id, @@ -102,7 +95,6 @@ def _security_group(sg_id=SG_ID, name="default"): ], } - def _image(img_id=IMG_ID, name="Ubuntu 22.04", size=2147483648): return { "id": img_id, @@ -113,7 +105,6 @@ def _image(img_id=IMG_ID, name="Ubuntu 22.04", size=2147483648): "min_ram": 512, } - def _keypair(name="mykey"): return { "keypair": { @@ -123,7 +114,6 @@ def _keypair(name="mykey"): } } - def _setup_full_mock(mock_client): """Wire up mock_client.get to return full infrastructure data.""" mock_client.compute_url = "https://nova.example.com/v2.1" @@ -172,12 +162,10 @@ def _get(url, **kwargs): mock_client.get = _get - # ══════════════════════════════════════════════════════════════════════════ # Default export (YAML, all resources) # ══════════════════════════════════════════════════════════════════════════ - class TestExportDefault: def test_yaml_output(self, invoke, config_dir, mock_client, sample_profile): @@ -267,12 +255,10 @@ def test_yaml_header_has_profile(self, invoke, config_dir, mock_client, sample_p result = invoke(["export"]) assert "# Profile:" in result.output - # ══════════════════════════════════════════════════════════════════════════ # JSON format # ══════════════════════════════════════════════════════════════════════════ - class TestExportJSON: def test_json_output(self, invoke, config_dir, mock_client, sample_profile): @@ -300,12 +286,10 @@ def test_json_no_yaml_header(self, invoke, config_dir, mock_client, sample_profi result = invoke(["export", "-f", "json"]) assert "# orca infrastructure export" not in result.output - # ══════════════════════════════════════════════════════════════════════════ # Resource filtering (--resources) # ══════════════════════════════════════════════════════════════════════════ - class TestExportResourceFilter: def test_single_resource(self, invoke, config_dir, mock_client, sample_profile): @@ -351,12 +335,10 @@ def test_only_keypairs(self, invoke, config_dir, mock_client, sample_profile): assert "servers" not in data assert "volumes" not in data - # ══════════════════════════════════════════════════════════════════════════ # Output to file (--output) # ══════════════════════════════════════════════════════════════════════════ - class TestExportFile: def test_output_to_file(self, invoke, config_dir, mock_client, sample_profile, tmp_path): @@ -392,12 +374,10 @@ def test_output_creates_parent_dirs(self, invoke, config_dir, mock_client, sampl assert result.exit_code == 0 assert out_file.exists() - # ══════════════════════════════════════════════════════════════════════════ # Cross-resolution (names resolved from IDs) # ══════════════════════════════════════════════════════════════════════════ - class TestExportCrossResolution: def test_server_image_resolved(self, invoke, config_dir, mock_client, sample_profile): @@ -439,12 +419,10 @@ def test_floating_ip_attached_server_resolved(self, invoke, config_dir, mock_cli fip = data["floating_ips"][0] assert fip["attached_to"] == "web-1" - # ══════════════════════════════════════════════════════════════════════════ # Edge cases # ══════════════════════════════════════════════════════════════════════════ - class TestExportEdgeCases: def test_empty_infrastructure(self, invoke, config_dir, mock_client, sample_profile): @@ -585,12 +563,10 @@ def _get(url, **kwargs): assert rule["port_range"] == "all" assert rule["protocol"] == "any" - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestExportHelp: def test_export_help(self, invoke): diff --git a/tests/test_find.py b/tests/test_find.py index 0e3b3af..561f5b8 100644 --- a/tests/test_find.py +++ b/tests/test_find.py @@ -13,7 +13,6 @@ # Unit tests — per-resource searchers # ══════════════════════════════════════════════════════════════════════════ - def _client(**urls): c = MagicMock() c.compute_url = urls.get("compute", "https://nova") @@ -22,7 +21,6 @@ def _client(**urls): c.image_url = urls.get("image", "https://glance") return c - class TestSafe: def test_returns_result_from_callable(self): @@ -42,7 +40,6 @@ def test_empty_result_passes_through(self): fn = MagicMock(return_value=[]) assert find_mod._safe(fn) == [] - class TestFindServers: def test_match_on_name(self): @@ -74,7 +71,6 @@ def test_no_match(self): c.get.return_value = {"servers": [{"id": "s1", "name": "db"}]} assert find_mod._find_servers(c, "web") == [] - class TestFindPorts: def test_match_on_fixed_ip(self): @@ -105,7 +101,6 @@ def test_match_on_device_id(self): assert len(hits) == 1 assert "device_id=" in hits[0][1] - class TestFindFloatingIps: def test_match_on_floating_address(self): @@ -126,7 +121,6 @@ def test_match_on_fixed_address(self): hits = find_mod._find_floatingips(c, "10.0.0.42") assert "fixed=10.0.0.42" in hits[0][1] - class TestFindSubnets: def test_match_on_cidr(self): @@ -137,7 +131,6 @@ def test_match_on_cidr(self): hits = find_mod._find_subnets(c, "10.0.0.0/24") assert "cidr=10.0.0.0/24" in hits[0][1] - class TestFindKeypairs: def test_match_on_fingerprint(self): @@ -157,7 +150,6 @@ def test_match_on_name(self): hits = find_mod._find_keypairs(c, "admin") assert hits[0][1] == "name" - class TestFindRouters: def test_match_on_external_gateway_ip(self): @@ -171,7 +163,6 @@ def test_match_on_external_gateway_ip(self): hits = find_mod._find_routers(c, "203.0.113.1") assert "gw=" in hits[0][1] - class TestFindVolumesImagesNetworksSgs: """Plain name/id matches for the simpler resource types.""" @@ -195,12 +186,10 @@ def test_security_groups(self): c.get.return_value = {"security_groups": [{"id": "sg1", "name": "default"}]} assert find_mod._find_security_groups(c, "default")[0][1] == "name" - # ══════════════════════════════════════════════════════════════════════════ # Extra column # ══════════════════════════════════════════════════════════════════════════ - class TestExtraColumn: def test_server_extra(self): @@ -232,12 +221,10 @@ def test_keypair_type(self): def test_unknown_type(self): assert find_mod._extra("unknown", {}) == "" - # ══════════════════════════════════════════════════════════════════════════ # Integration — orca find # ══════════════════════════════════════════════════════════════════════════ - def _setup_profile(mock_client, sample_profile): save_profile("test", sample_profile) set_active_profile("test") @@ -246,7 +233,6 @@ def _setup_profile(mock_client, sample_profile): mock_client.volume_url = "https://cinder.example.com/v3" mock_client.image_url = "https://glance.example.com" - def _url_dispatcher(responses: dict): """Return a side_effect fn mapping URL substring → payload.""" def _get(url, **kw): @@ -256,7 +242,6 @@ def _get(url, **kw): return {} return _get - class TestFindHelp: def test_help(self, invoke): @@ -264,7 +249,6 @@ def test_help(self, invoke): assert result.exit_code == 0 assert "Universal search" in result.output - class TestFindIntegration: def test_ip_lookup_hits_server_and_port( diff --git a/tests/test_flavor.py b/tests/test_flavor.py index 3ff6680..64ddd57 100644 --- a/tests/test_flavor.py +++ b/tests/test_flavor.py @@ -6,22 +6,18 @@ # ── Helpers ──────────────────────────────────────────────────────────────── - def _flavor(fid="flv-1", name="m1.small", vcpus=2, ram=2048, disk=20): return {"id": fid, "name": name, "vcpus": vcpus, "ram": ram, "disk": disk} - def _setup_mock(mock_client, flavors=None): flavors = flavors if flavors is not None else [] mock_client.compute_url = "https://nova.example.com/v2.1" mock_client.get = lambda url, **kw: {"flavors": flavors} - # ══════════════════════════════════════════════════════════════════════════ # flavor list # ══════════════════════════════════════════════════════════════════════════ - class TestFlavorList: def test_list_flavors(self, invoke, config_dir, mock_client, sample_profile): @@ -84,12 +80,10 @@ def test_list_many_flavors(self, invoke, config_dir, mock_client, sample_profile assert "m1.x1" in result.output assert "m1.x10" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestFlavorHelp: def test_flavor_help(self, invoke): diff --git a/tests/test_floating_ip.py b/tests/test_floating_ip.py index c27c05f..a3153a5 100644 --- a/tests/test_floating_ip.py +++ b/tests/test_floating_ip.py @@ -12,7 +12,6 @@ NET_ID = "abcdabcd-1234-5678-9abc-def012345678" QOS_ID = "dd445566-7788-99aa-bbcc-ddeeff001122" - def _fip(fip_id=FIP_ID, ip="203.0.113.10", status="ACTIVE", port_id=PORT_ID, fixed_ip="10.0.0.5"): return { @@ -26,7 +25,6 @@ def _fip(fip_id=FIP_ID, ip="203.0.113.10", status="ACTIVE", "created_at": "2025-01-01T00:00:00Z", } - def _setup_mock(mock_client, fips=None, fip_detail=None): fips = fips if fips is not None else [] mock_client.network_url = "https://neutron.example.com" @@ -62,12 +60,10 @@ def _delete(url, **kwargs): return {"posted": posted, "put_data": put_data, "deleted": deleted} - # ══════════════════════════════════════════════════════════════════════════ # floating-ip list # ══════════════════════════════════════════════════════════════════════════ - class TestFipList: def test_list_fips(self, invoke, config_dir, mock_client, sample_profile): @@ -109,12 +105,10 @@ def test_list_unassociated_shows_dash(self, invoke, config_dir, mock_client, sam result = invoke(["floating-ip", "list"]) assert result.exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════ # floating-ip show # ══════════════════════════════════════════════════════════════════════════ - class TestFipShow: def test_show_fip(self, invoke, config_dir, mock_client, sample_profile): @@ -134,12 +128,10 @@ def test_show_displays_status(self, invoke, config_dir, mock_client, sample_prof result = invoke(["floating-ip", "show", FIP_ID]) assert "DOWN" in result.output - # ══════════════════════════════════════════════════════════════════════════ # floating-ip create # ══════════════════════════════════════════════════════════════════════════ - class TestFipCreate: def test_create_fip(self, invoke, config_dir, mock_client, sample_profile): @@ -160,12 +152,10 @@ def test_create_requires_network(self, invoke, config_dir, mock_client, sample_p result = invoke(["floating-ip", "create"]) assert result.exit_code != 0 - # ══════════════════════════════════════════════════════════════════════════ # floating-ip delete # ══════════════════════════════════════════════════════════════════════════ - class TestFipDelete: def test_delete_fip(self, invoke, config_dir, mock_client, sample_profile): @@ -186,12 +176,10 @@ def test_delete_aborts_without_confirm(self, invoke, config_dir, mock_client, sa _ = invoke(["floating-ip", "delete", FIP_ID], input="n\n") assert len(state["deleted"]) == 0 - # ══════════════════════════════════════════════════════════════════════════ # floating-ip associate / disassociate # ══════════════════════════════════════════════════════════════════════════ - class TestFipAssociate: def test_associate(self, invoke, config_dir, mock_client, sample_profile): @@ -232,12 +220,10 @@ def test_disassociate(self, invoke, config_dir, mock_client, sample_profile): assert "disassociated" in result.output.lower() assert state["put_data"]["floatingip"]["port_id"] is None - # ══════════════════════════════════════════════════════════════════════════ # floating-ip set # ══════════════════════════════════════════════════════════════════════════ - class TestFipSet: def test_set_port(self, invoke, config_dir, mock_client, sample_profile): @@ -295,12 +281,10 @@ def test_set_nothing(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Nothing" in result.output - # ══════════════════════════════════════════════════════════════════════════ # floating-ip unset # ══════════════════════════════════════════════════════════════════════════ - class TestFipUnset: def test_unset_port(self, invoke, config_dir, mock_client, sample_profile): @@ -330,12 +314,10 @@ def test_unset_nothing(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Nothing" in result.output - # ══════════════════════════════════════════════════════════════════════════ # floating-ip bulk-release # ══════════════════════════════════════════════════════════════════════════ - class TestFipBulkRelease: def test_bulk_release_down(self, invoke, config_dir, mock_client, sample_profile): @@ -406,12 +388,10 @@ def test_bulk_release_handles_failure(self, invoke, config_dir, mock_client, sam assert result.exit_code == 0 assert "1 failed" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestFipHelp: def test_floating_ip_help(self, invoke): diff --git a/tests/test_hypervisor.py b/tests/test_hypervisor.py index d4b2ce8..52da48c 100644 --- a/tests/test_hypervisor.py +++ b/tests/test_hypervisor.py @@ -51,7 +51,6 @@ ] } - class TestHypervisorList: def test_list(self, invoke, mock_client, write_config, sample_profile): @@ -62,12 +61,10 @@ def test_list(self, invoke, mock_client, write_config, sample_profile): assert "compute-01" in result.output assert "compute-02" in result.output - def _hostnames(invoke_result): """Extract ordered hostnames from `hypervisor usage -f json` output.""" return [r["Hostname"] for r in json.loads(invoke_result.output)] - class TestHypervisorUsage: """Verify the new ``usage`` command — fill rate + sorting.""" diff --git a/tests/test_identity.py b/tests/test_identity.py index 2c170e7..3f9044e 100644 --- a/tests/test_identity.py +++ b/tests/test_identity.py @@ -13,37 +13,30 @@ ROLE_ID = "rrrr0000-1111-2222-3333-444455556666" CRED_ID = "cccc0000-1111-2222-3333-444455556666" - def _user(uid=USER_ID): return {"id": uid, "name": "alice", "domain_id": DOMAIN_ID, "email": "alice@example.com", "enabled": True, "default_project_id": PROJECT_ID, "password_expires_at": None, "created_at": "2025-01-01", "updated_at": None} - def _project(pid=PROJECT_ID): return {"id": pid, "name": "my-project", "domain_id": DOMAIN_ID, "description": "A test project", "enabled": True, "parent_id": None, "tags": []} - def _domain(did=DOMAIN_ID): return {"id": did, "name": "my-domain", "description": "Test domain", "enabled": True} - def _group(gid=GROUP_ID): return {"id": gid, "name": "my-group", "domain_id": DOMAIN_ID, "description": "Test group"} - def _role(rid=ROLE_ID): return {"id": rid, "name": "member", "domain_id": None, "description": "Member role"} - def _appcred(cid=CRED_ID): return {"id": cid, "name": "my-cred", "description": "CI token", "project_id": PROJECT_ID, "expires_at": None, "unrestricted": False, "secret": "s3cr3t"} - def _setup_mock(mock_client): mock_client.identity_url = "https://keystone.example.com" # _token_data IS the inner token object (no wrapper) — matches OrcaClient @@ -122,12 +115,10 @@ def _delete(url, **kwargs): return {"posted": posted, "patched": patched, "deleted": deleted, "put_urls": put_urls} - # ══════════════════════════════════════════════════════════════════════════ # Users # ══════════════════════════════════════════════════════════════════════════ - class TestUserList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -149,7 +140,6 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No users found" in result.output - class TestUserShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -161,7 +151,6 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "alice" in result.output - class TestUserCreate: def test_create(self, invoke, config_dir, mock_client, sample_profile): @@ -174,7 +163,6 @@ def test_create(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "created" in result.output - class TestUserUpdate: def test_update(self, invoke, config_dir, mock_client, sample_profile): @@ -195,7 +183,6 @@ def test_update_nothing(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Nothing" in result.output - class TestUserDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -208,7 +195,6 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert "deleted" in result.output assert len(state["deleted"]) == 1 - class TestUserSetPassword: def test_set_password(self, invoke, config_dir, mock_client, sample_profile): @@ -216,16 +202,14 @@ def test_set_password(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["user", "set-password", USER_ID, "--password", "newpass"]) + result = invoke(["user", "password", "set", USER_ID, "--password", "newpass"]) assert result.exit_code == 0 assert "Password updated" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Projects # ══════════════════════════════════════════════════════════════════════════ - class TestProjectList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -247,7 +231,6 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No projects found" in result.output - class TestProjectShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -259,7 +242,6 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "my-project" in result.output - class TestProjectCreate: def test_create(self, invoke, config_dir, mock_client, sample_profile): @@ -272,7 +254,6 @@ def test_create(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "created" in result.output - class TestProjectUpdate: def test_update(self, invoke, config_dir, mock_client, sample_profile): @@ -293,7 +274,6 @@ def test_update_nothing(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Nothing" in result.output - class TestProjectDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -305,12 +285,10 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "deleted" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Domains # ══════════════════════════════════════════════════════════════════════════ - class TestDomainList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -322,7 +300,6 @@ def test_list(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "my-domain" in result.output - class TestDomainShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -334,7 +311,6 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "my-domain" in result.output - class TestDomainCreate: def test_create(self, invoke, config_dir, mock_client, sample_profile): @@ -346,7 +322,6 @@ def test_create(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "created" in result.output - class TestDomainUpdate: def test_update(self, invoke, config_dir, mock_client, sample_profile): @@ -367,7 +342,6 @@ def test_update_nothing(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Nothing" in result.output - class TestDomainDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -379,12 +353,10 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "deleted" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Roles # ══════════════════════════════════════════════════════════════════════════ - class TestRoleList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -396,7 +368,6 @@ def test_list(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "member" in result.output - class TestRoleShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -408,7 +379,6 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "member" in result.output - class TestRoleCreate: def test_create(self, invoke, config_dir, mock_client, sample_profile): @@ -420,7 +390,6 @@ def test_create(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "created" in result.output - class TestRoleDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -432,7 +401,6 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "deleted" in result.output - class TestRoleAdd: def test_add_to_project(self, invoke, config_dir, mock_client, sample_profile): @@ -472,7 +440,6 @@ def test_add_missing_scope(self, invoke, config_dir, mock_client, sample_profile result = invoke(["role", "add", "--user", USER_ID, ROLE_ID]) assert result.exit_code != 0 - class TestRoleAssignmentList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -480,16 +447,14 @@ def test_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["role", "assignment-list"]) + result = invoke(["role", "assignment", "list"]) assert result.exit_code == 0 assert ROLE_ID[:8] in result.output - # ══════════════════════════════════════════════════════════════════════════ # Groups # ══════════════════════════════════════════════════════════════════════════ - class TestGroupList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -501,7 +466,6 @@ def test_list(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "my-group" in result.output - class TestGroupShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -513,7 +477,6 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "my-group" in result.output - class TestGroupCreate: def test_create(self, invoke, config_dir, mock_client, sample_profile): @@ -525,7 +488,6 @@ def test_create(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "created" in result.output - class TestGroupUpdate: def test_update(self, invoke, config_dir, mock_client, sample_profile): @@ -546,7 +508,6 @@ def test_update_nothing(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Nothing" in result.output - class TestGroupDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -558,7 +519,6 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "deleted" in result.output - class TestGroupUsers: def test_add_user(self, invoke, config_dir, mock_client, sample_profile): @@ -566,7 +526,7 @@ def test_add_user(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["group", "add-user", GROUP_ID, USER_ID]) + result = invoke(["group", "user", "add", GROUP_ID, USER_ID]) assert result.exit_code == 0 assert "added" in result.output assert len(state["put_urls"]) == 1 @@ -576,7 +536,7 @@ def test_remove_user(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["group", "remove-user", GROUP_ID, USER_ID]) + result = invoke(["group", "user", "remove", GROUP_ID, USER_ID]) assert result.exit_code == 0 assert "removed" in result.output @@ -585,16 +545,14 @@ def test_user_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["group", "member-list", GROUP_ID]) + result = invoke(["group", "member", "list", GROUP_ID]) assert result.exit_code == 0 assert "alice" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Application Credentials # ══════════════════════════════════════════════════════════════════════════ - class TestApplicationCredentials: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -634,18 +592,16 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "deleted" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestIdentityHelp: def test_user_help(self, invoke): result = invoke(["user", "--help"]) assert result.exit_code == 0 - for cmd in ("list", "show", "create", "set", "delete", "set-password"): + for cmd in ("list", "show", "create", "set", "delete"): assert cmd in result.output def test_project_help(self, invoke): @@ -663,14 +619,13 @@ def test_domain_help(self, invoke): def test_role_help(self, invoke): result = invoke(["role", "--help"]) assert result.exit_code == 0 - for cmd in ("list", "show", "create", "delete", "add", "remove", "assignment-list"): + for cmd in ("list", "show", "create", "delete", "add", "remove"): assert cmd in result.output def test_group_help(self, invoke): result = invoke(["group", "--help"]) assert result.exit_code == 0 - for cmd in ("list", "show", "create", "set", "delete", - "add-user", "remove-user", "member-list"): + for cmd in ("list", "show", "create", "set", "delete"): assert cmd in result.output def test_application_credential_help(self, invoke): diff --git a/tests/test_image.py b/tests/test_image.py index 5bb65a3..66904b5 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -12,7 +12,6 @@ IMG_ID = "11112222-3333-4444-5555-666677778888" IMG_ID2 = "aaaabbbb-cccc-dddd-eeee-ffffffffffff" - def _image(img_id=IMG_ID, name="Ubuntu 22.04", status="active", size=2147483648, disk_format="qcow2", visibility="private", image_type=None, min_disk=20, min_ram=512): @@ -33,7 +32,6 @@ def _image(img_id=IMG_ID, name="Ubuntu 22.04", status="active", img["image_type"] = image_type return img - def _server(srv_id="srv-1", name="web-1", image_id=IMG_ID): return { "id": srv_id, @@ -41,7 +39,6 @@ def _server(srv_id="srv-1", name="web-1", image_id=IMG_ID): "image": {"id": image_id}, } - def _setup_mock(mock_client, images=None, image_detail=None, servers=None): images = images if images is not None else [] servers = servers if servers is not None else [] @@ -87,12 +84,10 @@ def _delete(url, **kwargs): return {"posted": posted, "patched": patched, "deleted": deleted, "put_urls": put_urls} - # ══════════════════════════════════════════════════════════════════════════ # image list # ══════════════════════════════════════════════════════════════════════════ - class TestImageList: def test_list_images(self, invoke, config_dir, mock_client, sample_profile): @@ -138,12 +133,10 @@ def test_list_shows_size_in_mb(self, invoke, config_dir, mock_client, sample_pro result = invoke(["image", "list"]) assert "100" in result.output - # ══════════════════════════════════════════════════════════════════════════ # image show # ══════════════════════════════════════════════════════════════════════════ - class TestImageShow: def test_show_image(self, invoke, config_dir, mock_client, sample_profile): @@ -163,12 +156,10 @@ def test_show_displays_size_in_mb(self, invoke, config_dir, mock_client, sample_ result = invoke(["image", "show", IMG_ID]) assert "1024 MB" in result.output - # ══════════════════════════════════════════════════════════════════════════ # image create # ══════════════════════════════════════════════════════════════════════════ - class TestImageCreate: def test_create_image(self, invoke, config_dir, mock_client, sample_profile): @@ -196,12 +187,10 @@ def test_create_with_options(self, invoke, config_dir, mock_client, sample_profi assert state["posted"]["min_ram"] == 512 assert state["posted"]["visibility"] == "shared" - # ══════════════════════════════════════════════════════════════════════════ # image update # ══════════════════════════════════════════════════════════════════════════ - class TestImageUpdate: def test_update_name(self, invoke, config_dir, mock_client, sample_profile): @@ -222,18 +211,15 @@ def test_update_nothing(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No changes requested" in result.output - # ══════════════════════════════════════════════════════════════════════════ # image create / update / show — custom property lifecycle # ══════════════════════════════════════════════════════════════════════════ - def _patch_ops(state) -> list[dict]: """Decode the JSON-Patch body captured by ``_setup_mock``.""" raw = state["patched"].get("content", b"") return _json.loads(raw.decode()) if raw else [] - class TestImageCreateProperty: def test_create_with_properties_sends_top_level_keys( @@ -301,7 +287,6 @@ def test_create_property_without_equals_is_rejected( assert result.exit_code != 0 assert state["posted"] == {} - class TestImageUpdateProperty: def test_update_replace_existing_property_emits_replace_op( @@ -409,7 +394,6 @@ def test_update_invalid_property_key_exits_before_http( assert "content" not in state["patched"] assert "invalid property key" in result.output.lower() - class TestImageShowProperty: def _image_with_props(self): @@ -509,12 +493,10 @@ def test_show_without_properties_omits_subtable( assert result.exit_code == 0, result.output assert "Properties" not in result.output - # ══════════════════════════════════════════════════════════════════════════ # image deactivate / reactivate # ══════════════════════════════════════════════════════════════════════════ - class TestImageActivation: def test_deactivate(self, invoke, config_dir, mock_client, sample_profile): @@ -535,12 +517,10 @@ def test_reactivate(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "reactivated" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # image tag-add / tag-delete # ══════════════════════════════════════════════════════════════════════════ - class TestImageTags: def test_tag_add(self, invoke, config_dir, mock_client, sample_profile): @@ -548,7 +528,7 @@ def test_tag_add(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["image", "tag-add", IMG_ID, "prod"]) + result = invoke(["image", "tag", "add", IMG_ID, "prod"]) assert result.exit_code == 0 assert "prod" in result.output assert any("tags/prod" in u for u in state["put_urls"]) @@ -558,17 +538,15 @@ def test_tag_delete(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["image", "tag-delete", IMG_ID, "old-tag"]) + result = invoke(["image", "tag", "delete", IMG_ID, "old-tag"]) assert result.exit_code == 0 assert "old-tag" in result.output assert any("tags/old-tag" in u for u in state["deleted"]) - # ══════════════════════════════════════════════════════════════════════════ # image delete # ══════════════════════════════════════════════════════════════════════════ - class TestImageDelete: def test_delete_image(self, invoke, config_dir, mock_client, sample_profile): @@ -589,12 +567,10 @@ def test_delete_aborts_without_confirm(self, invoke, config_dir, mock_client, sa _ = invoke(["image", "delete", IMG_ID], input="n\n") assert len(state["deleted"]) == 0 - # ══════════════════════════════════════════════════════════════════════════ # image unused # ══════════════════════════════════════════════════════════════════════════ - class TestImageUnused: def test_unused_finds_images(self, invoke, config_dir, mock_client, sample_profile): @@ -677,19 +653,17 @@ def test_unused_delete_aborts(self, invoke, config_dir, mock_client, sample_prof _ = invoke(["image", "unused", "--delete"], input="n\n") assert len(state["deleted"]) == 0 - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestImageHelp: def test_image_help(self, invoke): result = invoke(["image", "--help"]) assert result.exit_code == 0 for cmd in ("list", "show", "create", "update", "upload", "download", - "delete", "deactivate", "reactivate", "tag-add", "tag-delete", + "delete", "deactivate", "reactivate", "shrink", "unused"): assert cmd in result.output @@ -707,14 +681,12 @@ def test_image_shrink_help(self, invoke): result = invoke(["image", "shrink", "--help"]) assert result.exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════ # image upload — streaming chunked PUT to /v2/images/{id}/file # ══════════════════════════════════════════════════════════════════════════ GL = "https://glance.example.com" - class TestImageUpload: """`orca image upload` streams the file in 256 KB chunks via PUT.""" diff --git a/tests/test_image_import_cache.py b/tests/test_image_import_cache.py index 1202c27..bb27469 100644 --- a/tests/test_image_import_cache.py +++ b/tests/test_image_import_cache.py @@ -5,11 +5,9 @@ IMG = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" GL = "https://glance.example.com" - def _setup(mock_client): mock_client.image_url = GL - # ══════════════════════════════════════════════════════════════════════════ # image import # ══════════════════════════════════════════════════════════════════════════ @@ -78,7 +76,6 @@ def test_help(self, invoke): assert result.exit_code == 0 assert "web-download" in result.output - # ══════════════════════════════════════════════════════════════════════════ # image cache-list # ══════════════════════════════════════════════════════════════════════════ @@ -91,7 +88,7 @@ def test_list_cached_and_queued(self, invoke, mock_client): "cached_images": [IMG], "queued_images": ["bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"], } - result = invoke(["image", "cache-list"]) + result = invoke(["image", "cache", "list"]) assert result.exit_code == 0 assert "cached" in result.output assert "queued" in result.output @@ -99,20 +96,19 @@ def test_list_cached_and_queued(self, invoke, mock_client): def test_list_calls_correct_url(self, invoke, mock_client): _setup(mock_client) mock_client.get.return_value = {"cached_images": [], "queued_images": []} - invoke(["image", "cache-list"]) + invoke(["image", "cache", "list"]) url = mock_client.get.call_args[0][0] assert "/v2/cache" in url def test_list_empty(self, invoke, mock_client): _setup(mock_client) mock_client.get.return_value = {"cached_images": [], "queued_images": []} - result = invoke(["image", "cache-list"]) + result = invoke(["image", "cache", "list"]) assert result.exit_code == 0 assert "empty" in result.output.lower() or "Cache" in result.output def test_help(self, invoke): - assert invoke(["image", "cache-list", "--help"]).exit_code == 0 - + assert invoke(["image", "cache", "list", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # image cache-queue @@ -122,7 +118,7 @@ class TestImageCacheQueue: def test_queue(self, invoke, mock_client): _setup(mock_client) - result = invoke(["image", "cache-queue", IMG]) + result = invoke(["image", "cache", "queue", IMG]) assert result.exit_code == 0 assert mock_client.put.called url = mock_client.put.call_args[0][0] @@ -130,12 +126,11 @@ def test_queue(self, invoke, mock_client): def test_shows_confirmation(self, invoke, mock_client): _setup(mock_client) - result = invoke(["image", "cache-queue", IMG]) + result = invoke(["image", "cache", "queue", IMG]) assert "queue" in result.output.lower() or IMG[:8] in result.output def test_help(self, invoke): - assert invoke(["image", "cache-queue", "--help"]).exit_code == 0 - + assert invoke(["image", "cache", "queue", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # image cache-delete @@ -145,7 +140,7 @@ class TestImageCacheDelete: def test_delete_yes(self, invoke, mock_client): _setup(mock_client) - result = invoke(["image", "cache-delete", IMG, "--yes"]) + result = invoke(["image", "cache", "delete", IMG, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() url = mock_client.delete.call_args[0][0] @@ -153,13 +148,12 @@ def test_delete_yes(self, invoke, mock_client): def test_delete_requires_confirm(self, invoke, mock_client): _setup(mock_client) - result = invoke(["image", "cache-delete", IMG], input="n\n") + result = invoke(["image", "cache", "delete", IMG], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help(self, invoke): - assert invoke(["image", "cache-delete", "--help"]).exit_code == 0 - + assert invoke(["image", "cache", "delete", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # image cache-clear @@ -169,7 +163,7 @@ class TestImageCacheClear: def test_clear_yes(self, invoke, mock_client): _setup(mock_client) - result = invoke(["image", "cache-clear", "--yes"]) + result = invoke(["image", "cache", "clear", "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() url = mock_client.delete.call_args[0][0] @@ -177,15 +171,15 @@ def test_clear_yes(self, invoke, mock_client): def test_clear_requires_confirm(self, invoke, mock_client): _setup(mock_client) - result = invoke(["image", "cache-clear"], input="n\n") + result = invoke(["image", "cache", "clear"], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_clear_confirm_yes(self, invoke, mock_client): _setup(mock_client) - result = invoke(["image", "cache-clear"], input="y\n") + result = invoke(["image", "cache", "clear"], input="y\n") assert result.exit_code == 0 assert mock_client.delete.called def test_help(self, invoke): - assert invoke(["image", "cache-clear", "--help"]).exit_code == 0 + assert invoke(["image", "cache", "clear", "--help"]).exit_code == 0 diff --git a/tests/test_image_members.py b/tests/test_image_members.py index 9c0caa4..e8d99f6 100644 --- a/tests/test_image_members.py +++ b/tests/test_image_members.py @@ -7,7 +7,6 @@ IMG_ID = "img-1111-1111-1111-111111111111" PRJ_ID = "prj-2222-2222-2222-222222222222" - # ══════════════════════════════════════════════════════════════════════════ # member-list # ══════════════════════════════════════════════════════════════════════════ @@ -24,7 +23,7 @@ def test_list_members(self, invoke, mock_client): "schema": "/v2/schemas/member"}, ] } - result = invoke(["image", "member-list", IMG_ID]) + result = invoke(["image", "member", "list", IMG_ID]) assert result.exit_code == 0 assert "prj-2222" in result.output # Rich truncates long UUIDs assert "accepted" in result.output @@ -32,18 +31,17 @@ def test_list_members(self, invoke, mock_client): def test_list_empty(self, invoke, mock_client): mock_client.image_url = "https://glance.example.com" mock_client.get.return_value = {"members": []} - result = invoke(["image", "member-list", IMG_ID]) + result = invoke(["image", "member", "list", IMG_ID]) assert result.exit_code == 0 assert "No members" in result.output def test_list_calls_correct_url(self, invoke, mock_client): mock_client.image_url = "https://glance.example.com" mock_client.get.return_value = {"members": []} - invoke(["image", "member-list", IMG_ID]) + invoke(["image", "member", "list", IMG_ID]) url = mock_client.get.call_args[0][0] assert f"/v2/images/{IMG_ID}/members" in url - # ══════════════════════════════════════════════════════════════════════════ # member-create # ══════════════════════════════════════════════════════════════════════════ @@ -55,25 +53,24 @@ def test_create(self, invoke, mock_client): mock_client.post.return_value = { "member_id": PRJ_ID, "status": "pending" } - result = invoke(["image", "member-create", IMG_ID, PRJ_ID]) + result = invoke(["image", "member", "create", IMG_ID, PRJ_ID]) assert result.exit_code == 0 assert "pending" in result.output or "shared" in result.output def test_create_posts_correct_body(self, invoke, mock_client): mock_client.image_url = "https://glance.example.com" mock_client.post.return_value = {"member_id": PRJ_ID, "status": "pending"} - invoke(["image", "member-create", IMG_ID, PRJ_ID]) + invoke(["image", "member", "create", IMG_ID, PRJ_ID]) body = mock_client.post.call_args[1]["json"] assert body["member"] == PRJ_ID def test_create_shows_acceptance_hint(self, invoke, mock_client): mock_client.image_url = "https://glance.example.com" mock_client.post.return_value = {"member_id": PRJ_ID, "status": "pending"} - result = invoke(["image", "member-create", IMG_ID, PRJ_ID]) + result = invoke(["image", "member", "create", IMG_ID, PRJ_ID]) assert result.exit_code == 0 assert "member-set" in result.output or "accept" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # member-delete # ══════════════════════════════════════════════════════════════════════════ @@ -82,19 +79,18 @@ class TestImageMemberDelete: def test_delete(self, invoke, mock_client): mock_client.image_url = "https://glance.example.com" - result = invoke(["image", "member-delete", IMG_ID, PRJ_ID, "--yes"]) + result = invoke(["image", "member", "delete", IMG_ID, PRJ_ID, "--yes"]) assert result.exit_code == 0 assert "revoked" in result.output mock_client.delete.assert_called_once() def test_delete_calls_correct_url(self, invoke, mock_client): mock_client.image_url = "https://glance.example.com" - result = invoke(["image", "member-delete", IMG_ID, PRJ_ID, "--yes"]) + result = invoke(["image", "member", "delete", IMG_ID, PRJ_ID, "--yes"]) assert result.exit_code == 0 url = mock_client.delete.call_args[0][0] assert f"/v2/images/{IMG_ID}/members/{PRJ_ID}" in url - # ══════════════════════════════════════════════════════════════════════════ # member-set # ══════════════════════════════════════════════════════════════════════════ @@ -104,7 +100,7 @@ class TestImageMemberSet: @pytest.mark.parametrize("status", ["accepted", "rejected", "pending"]) def test_set_status(self, invoke, mock_client, status): mock_client.image_url = "https://glance.example.com" - result = invoke(["image", "member-set", IMG_ID, PRJ_ID, "--status", status]) + result = invoke(["image", "member", "set", IMG_ID, PRJ_ID, "--status", status]) assert result.exit_code == 0 assert status in result.output body = mock_client.put.call_args[1]["json"] @@ -112,15 +108,14 @@ def test_set_status(self, invoke, mock_client, status): def test_set_calls_correct_url(self, invoke, mock_client): mock_client.image_url = "https://glance.example.com" - invoke(["image", "member-set", IMG_ID, PRJ_ID, "--status", "accepted"]) + invoke(["image", "member", "set", IMG_ID, PRJ_ID, "--status", "accepted"]) url = mock_client.put.call_args[0][0] assert f"/v2/images/{IMG_ID}/members/{PRJ_ID}" in url def test_invalid_status_rejected(self, invoke, mock_client): - result = invoke(["image", "member-set", IMG_ID, PRJ_ID, "--status", "unknown"]) + result = invoke(["image", "member", "set", IMG_ID, PRJ_ID, "--status", "unknown"]) assert result.exit_code != 0 - # ══════════════════════════════════════════════════════════════════════════ # member-show # ══════════════════════════════════════════════════════════════════════════ @@ -137,7 +132,7 @@ def test_show_member(self, invoke, mock_client): "updated_at": "2024-01-02T00:00:00Z", "schema": "/v2/schemas/member", } - result = invoke(["image", "member-show", IMG_ID, PRJ_ID]) + result = invoke(["image", "member", "show", IMG_ID, PRJ_ID]) assert result.exit_code == 0 assert "accepted" in result.output assert "prj-2222" in result.output @@ -147,20 +142,17 @@ def test_show_calls_correct_url(self, invoke, mock_client): mock_client.get.return_value = { "image_id": IMG_ID, "member_id": PRJ_ID, "status": "pending", } - invoke(["image", "member-show", IMG_ID, PRJ_ID]) + invoke(["image", "member", "show", IMG_ID, PRJ_ID]) url = mock_client.get.call_args[0][0] assert f"/v2/images/{IMG_ID}/members/{PRJ_ID}" in url - # ══════════════════════════════════════════════════════════════════════════ # --help checks # ══════════════════════════════════════════════════════════════════════════ class TestImageMemberHelp: - @pytest.mark.parametrize("sub", [ - "member-list", "member-show", "member-create", "member-delete", "member-set" - ]) - def test_help(self, invoke, sub): - result = invoke(["image", sub, "--help"]) + def test_help(self, invoke): + # ``image member`` is a sub-group; --help on it lists the verbs. + result = invoke(["image", "member", "--help"]) assert result.exit_code == 0 diff --git a/tests/test_image_stage_tasks.py b/tests/test_image_stage_tasks.py index 3d6963c..86f40ee 100644 --- a/tests/test_image_stage_tasks.py +++ b/tests/test_image_stage_tasks.py @@ -8,16 +8,13 @@ TASK_ID = "tttttttt-tttt-tttt-tttt-tttttttttttt" GL = "https://glance.example.com" - def _setup(mock_client): mock_client.image_url = GL - # ══════════════════════════════════════════════════════════════════════════ # image stage # ══════════════════════════════════════════════════════════════════════════ - class TestImageStage: def test_stage_calls_correct_url(self, invoke, mock_client, tmp_path): @@ -65,12 +62,10 @@ def test_stage_help(self, invoke): assert result.exit_code == 0 assert "staging" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # image stores-info # ══════════════════════════════════════════════════════════════════════════ - class TestImageStoresInfo: def test_list_stores(self, invoke, mock_client): @@ -81,7 +76,7 @@ def test_list_stores(self, invoke, mock_client): {"id": "ceph2", "description": "Backup Ceph", "is_default": False}, ] } - result = invoke(["image", "stores-info"]) + result = invoke(["image", "stores", "info"]) assert result.exit_code == 0 assert "ceph1" in result.output assert "ceph2" in result.output @@ -93,35 +88,33 @@ def test_default_store_marked(self, invoke, mock_client): {"id": "fast", "description": "Fast SSD", "is_default": True}, ] } - result = invoke(["image", "stores-info"]) + result = invoke(["image", "stores", "info"]) assert result.exit_code == 0 assert "yes" in result.output def test_detail_flag_uses_detail_url(self, invoke, mock_client): _setup(mock_client) mock_client.get.return_value = {"stores": []} - invoke(["image", "stores-info", "--detail"]) + invoke(["image", "stores", "info", "--detail"]) url = mock_client.get.call_args[0][0] assert "/detail" in url def test_empty_stores(self, invoke, mock_client): _setup(mock_client) mock_client.get.return_value = {"stores": []} - result = invoke(["image", "stores-info"]) + result = invoke(["image", "stores", "info"]) assert result.exit_code == 0 assert "No stores found" in result.output def test_help(self, invoke): - result = invoke(["image", "stores-info", "--help"]) + result = invoke(["image", "stores", "info", "--help"]) assert result.exit_code == 0 assert "--detail" in result.output - # ══════════════════════════════════════════════════════════════════════════ # image task-list # ══════════════════════════════════════════════════════════════════════════ - class TestImageTaskList: def test_list_tasks(self, invoke, mock_client): @@ -138,7 +131,7 @@ def test_list_tasks(self, invoke, mock_client): } ] } - result = invoke(["image", "task-list"]) + result = invoke(["image", "task", "list"]) assert result.exit_code == 0 assert "impo" in result.output # "import" may be truncated to "impo…" assert "succ" in result.output # "success" may be truncated to "succ…" @@ -146,34 +139,32 @@ def test_list_tasks(self, invoke, mock_client): def test_filter_by_type(self, invoke, mock_client): _setup(mock_client) mock_client.get.return_value = {"tasks": []} - invoke(["image", "task-list", "--type", "import"]) + invoke(["image", "task", "list", "--type", "import"]) _, kwargs = mock_client.get.call_args assert kwargs.get("params", {}).get("type") == "import" def test_filter_by_status(self, invoke, mock_client): _setup(mock_client) mock_client.get.return_value = {"tasks": []} - invoke(["image", "task-list", "--status", "pending"]) + invoke(["image", "task", "list", "--status", "pending"]) _, kwargs = mock_client.get.call_args assert kwargs.get("params", {}).get("status") == "pending" def test_empty_tasks(self, invoke, mock_client): _setup(mock_client) mock_client.get.return_value = {"tasks": []} - result = invoke(["image", "task-list"]) + result = invoke(["image", "task", "list"]) assert result.exit_code == 0 assert "No tasks found" in result.output def test_invalid_type(self, invoke, mock_client): - result = invoke(["image", "task-list", "--type", "badtype"]) + result = invoke(["image", "task", "list", "--type", "badtype"]) assert result.exit_code != 0 - # ══════════════════════════════════════════════════════════════════════════ # image task-show # ══════════════════════════════════════════════════════════════════════════ - class TestImageTaskShow: def test_show_task(self, invoke, mock_client): @@ -190,7 +181,7 @@ def test_show_task(self, invoke, mock_client): "updated_at": "2025-01-01T01:00:00Z", "expires_at": "2025-01-08T00:00:00Z", } - result = invoke(["image", "task-show", TASK_ID]) + result = invoke(["image", "task", "show", TASK_ID]) assert result.exit_code == 0 assert "import" in result.output assert "processing" in result.output @@ -198,10 +189,10 @@ def test_show_task(self, invoke, mock_client): def test_show_calls_correct_url(self, invoke, mock_client): _setup(mock_client) mock_client.get.return_value = {"id": TASK_ID, "type": "import", "status": "success"} - invoke(["image", "task-show", TASK_ID]) + invoke(["image", "task", "show", TASK_ID]) url = mock_client.get.call_args[0][0] assert f"/v2/tasks/{TASK_ID}" in url def test_help(self, invoke): - result = invoke(["image", "task-show", "--help"]) + result = invoke(["image", "task", "show", "--help"]) assert result.exit_code == 0 diff --git a/tests/test_ip_whois.py b/tests/test_ip_whois.py index 8f77795..5e5d972 100644 --- a/tests/test_ip_whois.py +++ b/tests/test_ip_whois.py @@ -9,7 +9,6 @@ TARGET_IP = "10.0.0.15" FIP_IP = "203.0.113.42" - def _setup_mock(mock_client, fips=None, ports=None, servers=None, routers=None, subnets=None, lbs=None): fips = fips or [] @@ -40,12 +39,10 @@ def _get(url, **kwargs): mock_client.get = _get - # ══════════════════════════════════════════════════════════════════════════ # ip whois # ══════════════════════════════════════════════════════════════════════════ - class TestIpWhois: def test_find_floating_ip(self, invoke, config_dir, mock_client, sample_profile): @@ -211,12 +208,10 @@ def _get(url, **kwargs): assert result.exit_code == 0 assert "No resource found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # _ip_in_range # ══════════════════════════════════════════════════════════════════════════ - class TestIpInRange: def test_ip_in_range(self): @@ -239,12 +234,10 @@ def test_invalid_ip(self): from orca_cli.commands.ip_whois import _ip_in_range assert _ip_in_range("not-an-ip", "10.0.0.10", "10.0.0.200") is False - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestIpHelp: def test_ip_help(self, invoke): diff --git a/tests/test_keypair.py b/tests/test_keypair.py index 40033b4..fd661c2 100644 --- a/tests/test_keypair.py +++ b/tests/test_keypair.py @@ -6,7 +6,6 @@ # ── Helpers ──────────────────────────────────────────────────────────────── - def _keypair(name="mykey", ktype="ssh", fingerprint="aa:bb:cc:dd"): return { "keypair": { @@ -18,7 +17,6 @@ def _keypair(name="mykey", ktype="ssh", fingerprint="aa:bb:cc:dd"): } } - def _setup_mock(mock_client, keypairs=None, kp_detail=None): keypairs = keypairs if keypairs is not None else [] mock_client.compute_url = "https://nova.example.com/v2.1" @@ -57,12 +55,10 @@ def _delete(url, **kwargs): return {"posted": posted, "deleted": deleted} - # ══════════════════════════════════════════════════════════════════════════ # keypair list # ══════════════════════════════════════════════════════════════════════════ - class TestKeypairList: def test_list_keypairs(self, invoke, config_dir, mock_client, sample_profile): @@ -103,12 +99,10 @@ def test_list_shows_fingerprint(self, invoke, config_dir, mock_client, sample_pr result = invoke(["keypair", "list"]) assert "aa:bb" in result.output - # ══════════════════════════════════════════════════════════════════════════ # keypair show # ══════════════════════════════════════════════════════════════════════════ - class TestKeypairShow: def test_show_keypair(self, invoke, config_dir, mock_client, sample_profile): @@ -141,12 +135,10 @@ def test_show_displays_public_key(self, invoke, config_dir, mock_client, sample_ result = invoke(["keypair", "show", "mykey"]) assert "ssh-ed25519" in result.output - # ══════════════════════════════════════════════════════════════════════════ # keypair create # ══════════════════════════════════════════════════════════════════════════ - class TestKeypairCreate: def test_create_keypair(self, invoke, config_dir, mock_client, sample_profile, tmp_path): @@ -175,12 +167,10 @@ def test_create_default_save_path(self, invoke, config_dir, mock_client, sample_ pem = tmp_path / "mykey2.pem" assert pem.exists() - # ══════════════════════════════════════════════════════════════════════════ # keypair upload # ══════════════════════════════════════════════════════════════════════════ - class TestKeypairUpload: def test_upload_from_file(self, invoke, config_dir, mock_client, sample_profile, tmp_path): @@ -218,12 +208,10 @@ def test_upload_no_key_no_default(self, invoke, config_dir, mock_client, sample_ result = invoke(["keypair", "upload", "nokey"]) assert result.exit_code != 0 - # ══════════════════════════════════════════════════════════════════════════ # keypair delete # ══════════════════════════════════════════════════════════════════════════ - class TestKeypairDelete: def test_delete_keypair(self, invoke, config_dir, mock_client, sample_profile): @@ -244,12 +232,10 @@ def test_delete_aborts_without_confirm(self, invoke, config_dir, mock_client, sa _ = invoke(["keypair", "delete", "old-key"], input="n\n") assert len(state["deleted"]) == 0 - # ══════════════════════════════════════════════════════════════════════════ # Help # ═══════��══════════════════════════════════════════════════════════════════ - class TestKeypairHelp: def test_keypair_help(self, invoke): diff --git a/tests/test_keystone_admin.py b/tests/test_keystone_admin.py index 00febcb..81112a0 100644 --- a/tests/test_keystone_admin.py +++ b/tests/test_keystone_admin.py @@ -8,13 +8,11 @@ ID2 = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" KS = "https://keystone.example.com:5000/v3" - def _client(mock_client): mock_client.identity_url = KS mock_client.catalog = [] return mock_client - # ══════════════════════════════════════════════════════════════════════════ # endpoint # ══════════════════════════════════════════════════════════════════════════ @@ -56,7 +54,6 @@ def test_help(self, invoke): result = invoke(["endpoint", "list", "--help"]) assert result.exit_code == 0 - class TestEndpointShow: def test_show(self, invoke, mock_client): @@ -72,7 +69,6 @@ def test_show(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["endpoint", "show", "--help"]).exit_code == 0 - class TestEndpointCreate: def test_create(self, invoke, mock_client): @@ -99,7 +95,6 @@ def test_create_with_region(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["endpoint", "create", "--help"]).exit_code == 0 - class TestEndpointSet: def test_set_url(self, invoke, mock_client): @@ -117,7 +112,6 @@ def test_set_nothing(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["endpoint", "set", "--help"]).exit_code == 0 - class TestEndpointDelete: def test_delete_yes(self, invoke, mock_client): @@ -130,7 +124,6 @@ def test_delete_yes(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["endpoint", "delete", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════ # service # ══════════════════════════════════════════════════════════════════════════ @@ -164,7 +157,6 @@ def test_list_empty(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["service", "list", "--help"]).exit_code == 0 - class TestServiceCreate: def test_create(self, invoke, mock_client): @@ -187,7 +179,6 @@ def test_create_with_description(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["service", "create", "--help"]).exit_code == 0 - class TestServiceSet: def test_set_name(self, invoke, mock_client): @@ -202,7 +193,6 @@ def test_set_nothing(self, invoke, mock_client): assert result.exit_code == 0 mock_client.patch.assert_not_called() - class TestServiceDelete: def test_delete_yes(self, invoke, mock_client): @@ -214,7 +204,6 @@ def test_delete_yes(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["service", "delete", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════ # credential # ══════════════════════════════════════════════════════════════════════════ @@ -247,7 +236,6 @@ def test_list_filter_type(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["credential", "list", "--help"]).exit_code == 0 - class TestCredentialCreate: def test_create(self, invoke, mock_client): @@ -273,7 +261,6 @@ def test_create_with_project(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["credential", "create", "--help"]).exit_code == 0 - class TestCredentialDelete: def test_delete_yes(self, invoke, mock_client): @@ -285,7 +272,6 @@ def test_delete_yes(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["credential", "delete", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════ # region # ══════════════════════════════════════════════════════════════════════════ @@ -319,7 +305,6 @@ def test_list_empty(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["region", "list", "--help"]).exit_code == 0 - class TestRegionShow: def test_show(self, invoke, mock_client): @@ -334,7 +319,6 @@ def test_show(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["region", "show", "--help"]).exit_code == 0 - class TestRegionCreate: def test_create(self, invoke, mock_client): @@ -362,7 +346,6 @@ def test_create_with_parent(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["region", "create", "--help"]).exit_code == 0 - class TestRegionSet: def test_set_description(self, invoke, mock_client): @@ -378,7 +361,6 @@ def test_set_nothing(self, invoke, mock_client): assert result.exit_code == 0 mock_client.patch.assert_not_called() - class TestRegionDelete: def test_delete_yes(self, invoke, mock_client): @@ -391,7 +373,6 @@ def test_delete_yes(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["region", "delete", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════ # trust # ══════════════════════════════════════════════════════════════════════════ @@ -429,7 +410,6 @@ def test_list_empty(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["trust", "list", "--help"]).exit_code == 0 - class TestTrustShow: def test_show(self, invoke, mock_client): @@ -446,7 +426,6 @@ def test_show(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["trust", "show", "--help"]).exit_code == 0 - class TestTrustCreate: def test_create(self, invoke, mock_client): @@ -481,7 +460,6 @@ def test_create_expires_at(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["trust", "create", "--help"]).exit_code == 0 - class TestTrustDelete: def test_delete_yes(self, invoke, mock_client): @@ -493,7 +471,6 @@ def test_delete_yes(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["trust", "delete", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════ # auth token-revoke # ══════════════════════════════════════════════════════════════════════════ @@ -502,7 +479,7 @@ class TestTokenRevoke: def test_revoke(self, invoke, mock_client): mock_client.identity_url = KS - result = invoke(["auth", "token-revoke", "my-token-value"]) + result = invoke(["auth", "token", "revoke", "my-token-value"]) assert result.exit_code == 0 assert mock_client.delete.called url = mock_client.delete.call_args[0][0] @@ -510,15 +487,14 @@ def test_revoke(self, invoke, mock_client): def test_revoke_sends_subject_token_header(self, invoke, mock_client): mock_client.identity_url = KS - invoke(["auth", "token-revoke", "my-secret-token"]) + invoke(["auth", "token", "revoke", "my-secret-token"]) headers = mock_client.delete.call_args[1].get("headers", {}) assert headers.get("X-Subject-Token") == "my-secret-token" def test_help(self, invoke): - result = invoke(["auth", "token-revoke", "--help"]) + result = invoke(["auth", "token", "revoke", "--help"]) assert result.exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════ # CLI registration # ══════════════════════════════════════════════════════════════════════════ @@ -531,5 +507,5 @@ def test_command_registered(self, invoke, cmd): assert result.exit_code == 0 def test_token_revoke_registered(self, invoke): - result = invoke(["auth", "token-revoke", "--help"]) + result = invoke(["auth", "token", "revoke", "--help"]) assert result.exit_code == 0 diff --git a/tests/test_keystone_gaps.py b/tests/test_keystone_gaps.py index fbd6416..ac4e65d 100644 --- a/tests/test_keystone_gaps.py +++ b/tests/test_keystone_gaps.py @@ -23,12 +23,10 @@ USER_ID = "44444444-4444-4444-4444-444444444444" BASE = "https://keystone.example.com" - def _iam(mc): mc.identity_url = BASE return mc - # ══════════════════════════════════════════════════════════════════════════════ # Policy # ══════════════════════════════════════════════════════════════════════════════ @@ -100,7 +98,6 @@ def test_delete_requires_confirm(self, invoke, mock_client): def test_help(self, invoke, sub): assert invoke(["policy", sub, "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # Identity Provider # ══════════════════════════════════════════════════════════════════════════════ @@ -162,7 +159,6 @@ def test_delete_yes(self, invoke, mock_client): def test_help(self, invoke, sub): assert invoke(["identity-provider", sub, "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # Federation Protocol # ══════════════════════════════════════════════════════════════════════════════ @@ -217,7 +213,6 @@ def test_delete_yes(self, invoke, mock_client): def test_help(self, invoke, sub): assert invoke(["federation-protocol", sub, "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # Mapping # ══════════════════════════════════════════════════════════════════════════════ @@ -225,7 +220,6 @@ def test_help(self, invoke, sub): _RULES = [{"local": [{"user": {"name": "{0}"}}], "remote": [{"type": "REMOTE_USER"}]}] - class TestMapping: def test_list(self, invoke, mock_client): @@ -281,7 +275,6 @@ def test_delete_yes(self, invoke, mock_client): def test_help(self, invoke, sub): assert invoke(["mapping", sub, "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # Service Provider # ══════════════════════════════════════════════════════════════════════════════ @@ -346,7 +339,6 @@ def test_delete_yes(self, invoke, mock_client): def test_help(self, invoke, sub): assert invoke(["service-provider", sub, "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # Registered Limits # ══════════════════════════════════════════════════════════════════════════════ @@ -417,7 +409,6 @@ def test_delete_yes(self, invoke, mock_client): def test_help(self, invoke, sub): assert invoke(["registered-limit", sub, "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # Limits # ══════════════════════════════════════════════════════════════════════════════ @@ -489,7 +480,6 @@ def test_delete_yes(self, invoke, mock_client): def test_help(self, invoke, sub): assert invoke(["limit", sub, "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # Implied Roles # ══════════════════════════════════════════════════════════════════════════════ @@ -504,39 +494,37 @@ def test_list(self, invoke, mock_client): "implies": [{"id": ROLE_B, "name": "member"}]} ] } - result = invoke(["role", "implied-list"]) + result = invoke(["role", "implied", "list"]) assert result.exit_code == 0 assert "admin" in result.output def test_list_empty(self, invoke, mock_client): _iam(mock_client) mock_client.get.return_value = {"role_inferences": []} - result = invoke(["role", "implied-list"]) + result = invoke(["role", "implied", "list"]) assert "No implied roles" in result.output def test_create(self, invoke, mock_client): _iam(mock_client) - result = invoke(["role", "implied-create", ROLE_A, ROLE_B]) + result = invoke(["role", "implied", "create", ROLE_A, ROLE_B]) assert result.exit_code == 0 url = mock_client.put.call_args[0][0] assert f"roles/{ROLE_A}/implies/{ROLE_B}" in url def test_delete_yes(self, invoke, mock_client): _iam(mock_client) - result = invoke(["role", "implied-delete", ROLE_A, ROLE_B, "--yes"]) + result = invoke(["role", "implied", "delete", ROLE_A, ROLE_B, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): _iam(mock_client) - result = invoke(["role", "implied-delete", ROLE_A, ROLE_B], input="n\n") + result = invoke(["role", "implied", "delete", ROLE_A, ROLE_B], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() - @pytest.mark.parametrize("sub", ["implied-list", "implied-create", "implied-delete"]) - def test_help(self, invoke, sub): - assert invoke(["role", sub, "--help"]).exit_code == 0 - + def test_help(self, invoke): + assert invoke(["role", "implied", "create", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════════ # Endpoint Group @@ -610,25 +598,24 @@ def test_delete_requires_confirm(self, invoke, mock_client): def test_add_project(self, invoke, mock_client): _iam(mock_client) - result = invoke(["endpoint-group", "add-project", EG_ID, PROJECT_ID]) + result = invoke(["endpoint-group", "project", "add", EG_ID, PROJECT_ID]) assert result.exit_code == 0 url = mock_client.put.call_args[0][0] assert f"endpoint_groups/{EG_ID}/projects/{PROJECT_ID}" in url def test_remove_project_yes(self, invoke, mock_client): _iam(mock_client) - result = invoke(["endpoint-group", "remove-project", EG_ID, PROJECT_ID, "--yes"]) + result = invoke(["endpoint-group", "project", "remove", EG_ID, PROJECT_ID, "--yes"]) assert result.exit_code == 0 url = mock_client.delete.call_args[0][0] assert f"endpoint_groups/{EG_ID}/projects/{PROJECT_ID}" in url @pytest.mark.parametrize("sub", [ - "list", "show", "create", "set", "delete", "add-project", "remove-project", + "list", "show", "create", "set", "delete", ]) def test_help(self, invoke, sub): assert invoke(["endpoint-group", sub, "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # Access Rule # ══════════════════════════════════════════════════════════════════════════════ @@ -679,7 +666,6 @@ def test_delete_requires_confirm(self, invoke, mock_client): def test_help(self, invoke, sub): assert invoke(["access-rule", sub, "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # Token # ══════════════════════════════════════════════════════════════════════════════ diff --git a/tests/test_loadbalancer.py b/tests/test_loadbalancer.py index d3dfedb..66d14a3 100644 --- a/tests/test_loadbalancer.py +++ b/tests/test_loadbalancer.py @@ -13,7 +13,6 @@ HM_ID = "44445555-6666-7777-8888-999900001111" SUBNET_ID = "55556666-7777-8888-9999-000011112222" - def _lb(lb_id=LB_ID, name="my-lb", vip="10.0.0.100"): return { "id": lb_id, "name": name, "vip_address": vip, @@ -24,7 +23,6 @@ def _lb(lb_id=LB_ID, name="my-lb", vip="10.0.0.100"): "created_at": "2025-01-01", "updated_at": "2025-01-02", } - def _listener(lid=LISTENER_ID, name="http-listener", protocol="HTTP", port=80): return { "id": lid, "name": name, "protocol": protocol, "protocol_port": port, @@ -34,7 +32,6 @@ def _listener(lid=LISTENER_ID, name="http-listener", protocol="HTTP", port=80): "created_at": "2025-01-01", "updated_at": "2025-01-02", } - def _pool(pid=POOL_ID, name="web-pool"): return { "id": pid, "name": name, "protocol": "HTTP", @@ -44,7 +41,6 @@ def _pool(pid=POOL_ID, name="web-pool"): "admin_state_up": True, "created_at": "2025-01-01", "updated_at": "2025-01-02", } - def _member(mid=MEMBER_ID, addr="10.0.0.10", port=8080): return { "id": mid, "name": "backend-1", "address": addr, @@ -52,7 +48,6 @@ def _member(mid=MEMBER_ID, addr="10.0.0.10", port=8080): "operating_status": "ONLINE", } - def _hm(hid=HM_ID, name="http-check"): return { "id": hid, "name": name, "type": "HTTP", @@ -60,7 +55,6 @@ def _hm(hid=HM_ID, name="http-check"): "provisioning_status": "ACTIVE", } - def _setup_mock(mock_client): mock_client.load_balancer_url = "https://octavia.example.com" @@ -110,12 +104,10 @@ def _delete(url, **kwargs): return {"posted": posted, "deleted": deleted} - # ══════════════════════════════════════════════════════════════════════════ # loadbalancer list / show / create / delete # ══════════════════════════════════════════════════════════════════════════ - class TestLBCore: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -175,12 +167,10 @@ def test_delete_cascade(self, invoke, config_dir, mock_client, sample_profile): result = invoke(["loadbalancer", "delete", LB_ID, "--cascade", "-y"]) assert result.exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════ # Listeners # ══════════════════════════════════════════════════════════════════════════ - class TestListeners: def test_listener_list(self, invoke, config_dir, mock_client, sample_profile): @@ -188,7 +178,7 @@ def test_listener_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["loadbalancer", "listener-list"]) + result = invoke(["loadbalancer", "listener", "list"]) assert result.exit_code == 0 assert "HTTP" in result.output assert "80" in result.output @@ -198,7 +188,7 @@ def test_listener_show(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["loadbalancer", "listener-show", LISTENER_ID]) + result = invoke(["loadbalancer", "listener", "show", LISTENER_ID]) assert result.exit_code == 0 assert "HTTP" in result.output @@ -207,7 +197,7 @@ def test_listener_create(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["loadbalancer", "listener-create", "lis-1", + result = invoke(["loadbalancer", "listener", "create", "lis-1", "--lb-id", LB_ID, "--protocol", "HTTP", "--port", "80"]) assert result.exit_code == 0 assert "created" in result.output.lower() @@ -217,16 +207,14 @@ def test_listener_delete(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["loadbalancer", "listener-delete", LISTENER_ID, "-y"]) + result = invoke(["loadbalancer", "listener", "delete", LISTENER_ID, "-y"]) assert result.exit_code == 0 assert "deleted" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # Pools # ══════════════════════════════════════════════════════════════════════════ - class TestPools: def test_pool_list(self, invoke, config_dir, mock_client, sample_profile): @@ -234,7 +222,7 @@ def test_pool_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["loadbalancer", "pool-list"]) + result = invoke(["loadbalancer", "pool", "list"]) assert result.exit_code == 0 assert "web-" in result.output @@ -243,7 +231,7 @@ def test_pool_show(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["loadbalancer", "pool-show", POOL_ID]) + result = invoke(["loadbalancer", "pool", "show", POOL_ID]) assert result.exit_code == 0 assert "ROUND_ROBIN" in result.output @@ -252,7 +240,7 @@ def test_pool_create(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["loadbalancer", "pool-create", "my-pool", + result = invoke(["loadbalancer", "pool", "create", "my-pool", "--lb-id", LB_ID, "--protocol", "HTTP", "--algorithm", "ROUND_ROBIN"]) assert result.exit_code == 0 @@ -263,16 +251,14 @@ def test_pool_delete(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["loadbalancer", "pool-delete", POOL_ID, "-y"]) + result = invoke(["loadbalancer", "pool", "delete", POOL_ID, "-y"]) assert result.exit_code == 0 assert "deleted" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # Members # ══════════════════════════════════════════════════════════════════════════ - class TestMembers: def test_member_list(self, invoke, config_dir, mock_client, sample_profile): @@ -280,7 +266,7 @@ def test_member_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["loadbalancer", "member-list", POOL_ID]) + result = invoke(["loadbalancer", "member", "list", POOL_ID]) assert result.exit_code == 0 assert "10.0" in result.output assert "8080" in result.output @@ -290,7 +276,7 @@ def test_member_add(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["loadbalancer", "member-add", POOL_ID, + result = invoke(["loadbalancer", "member", "add", POOL_ID, "--address", "10.0.0.20", "--port", "8080"]) assert result.exit_code == 0 assert "added" in result.output.lower() @@ -300,16 +286,14 @@ def test_member_remove(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["loadbalancer", "member-remove", POOL_ID, MEMBER_ID, "-y"]) + result = invoke(["loadbalancer", "member", "remove", POOL_ID, MEMBER_ID, "-y"]) assert result.exit_code == 0 assert "removed" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # Health Monitors # ══════════════════════════════════════════════════════════════════════════ - class TestHealthMonitors: def test_hm_list(self, invoke, config_dir, mock_client, sample_profile): @@ -317,7 +301,7 @@ def test_hm_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["loadbalancer", "healthmonitor-list"]) + result = invoke(["loadbalancer", "healthmonitor", "list"]) assert result.exit_code == 0 assert "HTTP" in result.output @@ -326,7 +310,7 @@ def test_hm_create(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["loadbalancer", "healthmonitor-create", "hm-1", + result = invoke(["loadbalancer", "healthmonitor", "create", "hm-1", "--pool-id", POOL_ID, "--type", "HTTP", "--delay", "5", "--timeout", "3"]) assert result.exit_code == 0 @@ -337,30 +321,24 @@ def test_hm_delete(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["loadbalancer", "healthmonitor-delete", HM_ID, "-y"]) + result = invoke(["loadbalancer", "healthmonitor", "delete", HM_ID, "-y"]) assert result.exit_code == 0 assert "deleted" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestLBHelp: def test_lb_help(self, invoke): result = invoke(["loadbalancer", "--help"]) assert result.exit_code == 0 - for cmd in ("list", "show", "create", "delete", - "listener-list", "listener-create", - "pool-list", "pool-create", - "member-list", "member-add", - "healthmonitor-list", "healthmonitor-create"): + for cmd in ("list", "show", "create", "delete"): assert cmd in result.output def test_member_add_help(self, invoke): - result = invoke(["loadbalancer", "member-add", "--help"]) + result = invoke(["loadbalancer", "member", "add", "--help"]) assert result.exit_code == 0 assert "--address" in result.output assert "--port" in result.output diff --git a/tests/test_main.py b/tests/test_main.py index de9c0b6..8e61f1d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -48,7 +48,6 @@ def test_cli_is_group(self): for expected in ("server", "volume", "network", "profile", "doctor"): assert expected in names, f"missing {expected} in {sorted(names)}" - class TestLazyResolution: """The LazyOrcaGroup must resolve commands on demand without bulk-importing.""" @@ -75,7 +74,6 @@ def test_get_command_resolves_multi_command_module(self): def test_get_command_returns_none_for_unknown(self): assert main_module.cli.get_command(None, "this-command-does-not-exist") is None - class TestDebugFlag: """--debug attaches a stderr handler to the orca_cli logger.""" diff --git a/tests/test_metric.py b/tests/test_metric.py index 9233061..fc07fdc 100644 --- a/tests/test_metric.py +++ b/tests/test_metric.py @@ -9,7 +9,6 @@ METRIC_ID = "11112222-3333-4444-5555-666677778888" RESOURCE_ID = "aaaabbbb-cccc-dddd-eeee-ffffffffffff" - def _setup_mock(mock_client): mock_client.metric_url = "https://gnocchi.example.com" @@ -78,12 +77,10 @@ def _get(url, **kwargs): mock_client.get = _get - # ══════════════════════════════════════════════════════════════════════════ # resource-type-list # ══════════════════════════════════════════════════════════════════════════ - class TestResourceTypes: def test_resource_type_list(self, invoke, config_dir, mock_client, sample_profile): @@ -91,17 +88,15 @@ def test_resource_type_list(self, invoke, config_dir, mock_client, sample_profil set_active_profile("p") _setup_mock(mock_client) - result = invoke(["metric", "resource-type-list"]) + result = invoke(["metric", "resource-type", "list"]) assert result.exit_code == 0 assert "generic" in result.output assert "instance" in result.output - # ══════════════════════════════════════════════════════════════════════════ # resource-list / resource-show # ══════════════════════════════════════════════════════════════════════════ - class TestResources: def test_resource_list(self, invoke, config_dir, mock_client, sample_profile): @@ -109,7 +104,7 @@ def test_resource_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["metric", "resource-list"]) + result = invoke(["metric", "resource", "list"]) assert result.exit_code == 0 assert "generic" in result.output @@ -118,7 +113,7 @@ def test_resource_list_with_type(self, invoke, config_dir, mock_client, sample_p set_active_profile("p") _setup_mock(mock_client) - result = invoke(["metric", "resource-list", "--type", "instance"]) + result = invoke(["metric", "resource", "list", "--type", "instance"]) assert result.exit_code == 0 def test_resource_show(self, invoke, config_dir, mock_client, sample_profile): @@ -126,16 +121,14 @@ def test_resource_show(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["metric", "resource-show", RESOURCE_ID]) + result = invoke(["metric", "resource", "show", RESOURCE_ID]) assert result.exit_code == 0 assert "cpu_util" in result.output - # ══════════════════════════════════════════════════════════════════════════ # metric list / show # ══════════════════════════════════════════════════════════════════════════ - class TestMetrics: def test_metric_list(self, invoke, config_dir, mock_client, sample_profile): @@ -157,12 +150,10 @@ def test_metric_show(self, invoke, config_dir, mock_client, sample_profile): assert "cpu_util" in result.output assert "high" in result.output - # ══════════════════════════════════════════════════════════════════════════ # measures # ══════════════════════════════════════════════════════════════════════════ - class TestMeasures: def test_measures(self, invoke, config_dir, mock_client, sample_profile): @@ -185,12 +176,10 @@ def test_measures_empty(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No measures found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # archive-policy-list # ══════════════════════════════════════════════════════════════════════════ - class TestArchivePolicies: def test_archive_policy_list(self, invoke, config_dir, mock_client, sample_profile): @@ -198,16 +187,14 @@ def test_archive_policy_list(self, invoke, config_dir, mock_client, sample_profi set_active_profile("p") _setup_mock(mock_client) - result = invoke(["metric", "archive-policy-list"]) + result = invoke(["metric", "archive-policy", "list"]) assert result.exit_code == 0 assert "high" in result.output - # ══════════════════════════════════════════════════════════════════════════ # status # ══════════════════════════════════════════════════════════════════════════ - class TestMetricStatus: def test_status(self, invoke, config_dir, mock_client, sample_profile): @@ -219,20 +206,16 @@ def test_status(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "42" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestMetricHelp: def test_metric_help(self, invoke): result = invoke(["metric", "--help"]) assert result.exit_code == 0 - for cmd in ("list", "show", "measures", "resource-list", - "resource-show", "resource-type-list", - "archive-policy-list", "status"): + for cmd in ("list", "show", "measures", "status"): assert cmd in result.output def test_measures_help(self, invoke): @@ -241,14 +224,12 @@ def test_measures_help(self, invoke): assert "--start" in result.output assert "--granularity" in result.output - # ══════════════════════════════════════════════════════════════════════════ # metric create/delete / measures-add / archive-policy / resource-type # ══════════════════════════════════════════════════════════════════════════ _GNOCCHI = "https://gnocchi.example.com" - class TestMetricCreate: def test_create(self, invoke, mock_client): @@ -264,7 +245,6 @@ def test_create(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["metric", "create", "--help"]).exit_code == 0 - class TestMetricDelete: def test_delete_yes(self, invoke, mock_client): @@ -282,12 +262,11 @@ def test_delete_requires_confirm(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["metric", "delete", "--help"]).exit_code == 0 - class TestMeasuresAdd: def test_add(self, invoke, mock_client): mock_client.metric_url = _GNOCCHI - result = invoke(["metric", "measures-add", METRIC_ID, + result = invoke(["metric", "measures", "add", METRIC_ID, "--measure", "2026-01-01T00:00:00:42.5"]) assert result.exit_code == 0 payload = mock_client.post.call_args[1]["json"] @@ -296,13 +275,12 @@ def test_add(self, invoke, mock_client): def test_add_invalid_format(self, invoke, mock_client): mock_client.metric_url = _GNOCCHI - result = invoke(["metric", "measures-add", METRIC_ID, + result = invoke(["metric", "measures", "add", METRIC_ID, "--measure", "badformat"]) assert result.exit_code != 0 def test_help(self, invoke): - assert invoke(["metric", "measures-add", "--help"]).exit_code == 0 - + assert invoke(["metric", "measures", "add", "--help"]).exit_code == 0 class TestArchivePolicy: @@ -311,13 +289,13 @@ def test_show(self, invoke, mock_client): mock_client.get.return_value = { "name": "low", "definition": [], "aggregation_methods": ["mean"], "back_window": 0 } - result = invoke(["metric", "archive-policy-show", "low"]) + result = invoke(["metric", "archive-policy", "show", "low"]) assert result.exit_code == 0 assert "low" in result.output def test_create(self, invoke, mock_client): mock_client.metric_url = _GNOCCHI - result = invoke(["metric", "archive-policy-create", "my-policy", + result = invoke(["metric", "archive-policy", "create", "my-policy", "--definition", "1m:1440", "--definition", "1h:720"]) assert result.exit_code == 0 @@ -327,39 +305,38 @@ def test_create(self, invoke, mock_client): def test_create_invalid_definition(self, invoke, mock_client): mock_client.metric_url = _GNOCCHI - result = invoke(["metric", "archive-policy-create", "p", + result = invoke(["metric", "archive-policy", "create", "p", "--definition", "bad"]) assert result.exit_code != 0 def test_delete_yes(self, invoke, mock_client): mock_client.metric_url = _GNOCCHI - result = invoke(["metric", "archive-policy-delete", "my-policy", "--yes"]) + result = invoke(["metric", "archive-policy", "delete", "my-policy", "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): mock_client.metric_url = _GNOCCHI - result = invoke(["metric", "archive-policy-delete", "my-policy"], input="n\n") + result = invoke(["metric", "archive-policy", "delete", "my-policy"], input="n\n") assert result.exit_code != 0 def test_help_show(self, invoke): - assert invoke(["metric", "archive-policy-show", "--help"]).exit_code == 0 + assert invoke(["metric", "archive-policy", "show", "--help"]).exit_code == 0 def test_help_create(self, invoke): - assert invoke(["metric", "archive-policy-create", "--help"]).exit_code == 0 - + assert invoke(["metric", "archive-policy", "create", "--help"]).exit_code == 0 class TestResourceType: def test_show(self, invoke, mock_client): mock_client.metric_url = _GNOCCHI mock_client.get.return_value = {"name": "instance", "attributes": {}} - result = invoke(["metric", "resource-type-show", "instance"]) + result = invoke(["metric", "resource-type", "show", "instance"]) assert result.exit_code == 0 def test_create(self, invoke, mock_client): mock_client.metric_url = _GNOCCHI - result = invoke(["metric", "resource-type-create", "my-type", + result = invoke(["metric", "resource-type", "create", "my-type", "--attribute", "host:string"]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"] @@ -368,12 +345,12 @@ def test_create(self, invoke, mock_client): def test_delete_yes(self, invoke, mock_client): mock_client.metric_url = _GNOCCHI - result = invoke(["metric", "resource-type-delete", "my-type", "--yes"]) + result = invoke(["metric", "resource-type", "delete", "my-type", "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_help_show(self, invoke): - assert invoke(["metric", "resource-type-show", "--help"]).exit_code == 0 + assert invoke(["metric", "resource-type", "show", "--help"]).exit_code == 0 def test_help_create(self, invoke): - assert invoke(["metric", "resource-type-create", "--help"]).exit_code == 0 + assert invoke(["metric", "resource-type", "create", "--help"]).exit_code == 0 diff --git a/tests/test_naming_convention.py b/tests/test_naming_convention.py deleted file mode 100644 index c161f7a..0000000 --- a/tests/test_naming_convention.py +++ /dev/null @@ -1,179 +0,0 @@ -"""Ratchet test for ADR-0008 — command naming convention. - -Every hyphenated subcommand currently shipping is listed in the -``LEGACY_HYPHENATED_SUBCOMMANDS`` whitelist. The test fails if: - -- a *new* hyphenated subcommand appears anywhere in the live command - tree without being added to the whitelist (regression — the new - command should follow ``noun [subnoun] verb`` instead). -- a name in the whitelist no longer exists (it was removed or - renamed — clean it up so the whitelist keeps shrinking). - -To migrate a legacy command: - -1. Refactor the module to expose the new ``noun subnoun verb`` form. -2. Keep the old hyphenated name as an alias on the same callback. -3. Remove the old name from the whitelist below. - -This way the debt can only shrink — see ADR-0008. -""" - -from __future__ import annotations - -import click - -from orca_cli.main import cli - -LEGACY_HYPHENATED_SUBCOMMANDS: dict[str, set[str]] = { - # Modules migrated 2026-04 (lot 1) — old names live on as deprecated - # aliases, excluded from the live tree by the runtime: aggregate, - # alarm, auth, endpoint-group, flavor, group, role, secret (acl-*), - # security-group, trunk, user (set-password). - # backup — migrated 2026-04-28 (lot 10): action-*, client-*, job-*, - # session-* moved under sub-groups (action, client, job, session). - # ``session add-job`` and ``session remove-job`` keep the hyphen as - # compound-verb leaves under the session sub-group. - # cluster — migrated 2026-04-28 (lot 10): nodegroup-* and template-* - # moved under sub-groups (nodegroup, template). - "floating-ip": {"bulk-release"}, - "image": { - # ``share-and-accept`` is a compound verb (orca-specific UX - # combining member-create + member-set --status accepted). - "share-and-accept", - }, - # loadbalancer — migrated 2026-04-28: every leaf moved to a - # sub-group (amphora, healthmonitor, l7policy, l7rule, listener, - # member, pool, stats, status). l7policy/l7rule are compound nouns - # that keep their hyphen on the sub-group name. - # metric — migrated 2026-04-28: archive-policy-* under - # ``metric archive-policy``, resource-type-* under ``metric resource-type``, - # resource-list/show under ``metric resource``, measures-add under - # ``metric measures add``. - # "network" entry removed — every former hyphenated subcommand is now - # nested under a sub-group (agent, port, rbac, segment, subnet, - # auto-allocated-topology, router) or sub-sub-group (router add/remove - # for interface/route, router set/unset for gateway). - # object — migrated 2026-04-28: account-* moved under - # ``object account``, container-* moved under ``object container``. - # placement — migrated 2026-04-28: every leaf moved into a sub-group - # (resource-provider, resource-class, trait, allocation, usage). - # ``resource-provider`` and ``resource-class`` keep their hyphen as - # compound-noun sub-group names. Sub-leaves ``inventory-list/set/ - # show/delete-all``, ``trait-list/set/delete``, ``aggregate-list/ - # set/delete``, ``candidate-list`` keep their hyphen for the same - # reason (sub-resources of resource-provider / allocation). - # profile — migrated 2026-04-28 (lot 11): set-color → ``color set``, - # set-region → ``region set``, regions → ``region list``, - # to-/from-openrc → ``export/import openrc``, to-/from-clouds → - # ``export/import clouds``. Old hyphenated names live as deprecated - # aliases. Whitelist cleared. - # qos — migrated 2026-04-28: policy-* under ``qos policy``, - # rule-* under ``qos rule``. - # rating — migrated 2026-04-28: metric-* under ``rating metric``, - # module-* under ``rating module`` (incl. ``set-priority`` as a - # compound-verb leaf). - "secret": { - "container-create", "container-delete", "container-list", - "container-show", "get-payload", - "order-create", "order-delete", "order-list", "order-show", - }, - "server": { - # Decided exceptions (arbitrated, kept on purpose): - # - confirm-resize / revert-resize: `resize` is both an action - # (`server resize --flavor ...`) and would need to be a - # sub-group; statu quo, not worth the Click acrobatics. - # - port-forward: orca-exclusive (no openstack equivalent), - # compound verb, kept as-is. - # Note: attach-interface and live-migrate are now `deprecated=True` - # façades and excluded from this whitelist automatically by the - # test runtime. - "confirm-resize", - "port-forward", - "revert-resize", - }, - # stack — migrated 2026-04-28: every leaf moved into a sub-group - # (event/output/resource/resource-type/template); old hyphenated - # names live as deprecated aliases. - "volume": { - # Deliberate exception (compound verb, no openstack equivalent): - # ``upload-to-image`` mirrors the Cinder action name - # ``os-volume_upload_image`` and reads as a single verb phrase. - # Nesting it under a sub-group (e.g. ``volume image upload``) - # would suggest ``image`` is a sub-resource of ``volume``, which - # it isn't — Glance is just the target system. Same rationale - # as ``server port-forward``. - "upload-to-image", - }, - "zone": { - # ``reverse-lookup`` is a compound verb (orca-specific UX, no - # OSC equivalent) — kept on purpose. Other tld-* and - # transfer-* names are deprecated aliases excluded by the - # runtime. - "reverse-lookup", - }, -} - - -def _live_hyphenated_subcommands() -> dict[str, set[str]]: - """Walk the cli tree and return {top-group: {hyphenated-leaf-names}}. - - Two classes of hyphenated names are excluded by design: - - - Deprecated aliases (``deprecated=True``, registered via - ``add_command_with_alias``) — tracked separately as part of the - alias-and-deprecate plan, not as fresh debt. - - Sub-groups whose own name is a compound noun (e.g. - ``auto-allocated-topology``) — ADR-0008 explicitly allows - compound nouns to keep their hyphen because they read as one - word. The convention only forbids ``verb-noun`` glue on *leaf* - commands. - """ - out: dict[str, set[str]] = {} - for top_name in cli.list_commands(None): - top = cli.get_command(None, top_name) - if not isinstance(top, click.Group): - continue - hy = { - s for s, cmd in top.commands.items() - if "-" in s - and not getattr(cmd, "deprecated", False) - and not isinstance(cmd, click.Group) - } - if hy: - out[top_name] = hy - return out - - -def test_no_new_hyphenated_subcommand_outside_whitelist(): - """A new hyphenated subcommand must violate this test (see ADR-0008).""" - live = _live_hyphenated_subcommands() - new: dict[str, set[str]] = {} - for top, names in live.items(): - legacy = LEGACY_HYPHENATED_SUBCOMMANDS.get(top, set()) - extras = names - legacy - if extras: - new[top] = extras - - assert not new, ( - "New hyphenated subcommand(s) detected — name them with the " - "openstackclient convention (noun [subnoun] verb) per ADR-0008, " - "or add to the whitelist if it is a deliberate exception:\n" - + "\n".join(f" {top}: {sorted(extras)}" for top, extras in new.items()) - ) - - -def test_whitelist_does_not_carry_dead_entries(): - """Whitelist names that no longer exist in the cli must be removed.""" - live = _live_hyphenated_subcommands() - stale: dict[str, set[str]] = {} - for top, legacy in LEGACY_HYPHENATED_SUBCOMMANDS.items(): - live_names = live.get(top, set()) - gone = legacy - live_names - if gone: - stale[top] = gone - - assert not stale, ( - "Whitelist references commands that no longer exist — drop them " - "from tests/test_naming_convention.py:\n" - + "\n".join(f" {top}: {sorted(gone)}" for top, gone in stale.items()) - ) diff --git a/tests/test_network.py b/tests/test_network.py index b18aee0..46a6ed3 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -11,7 +11,6 @@ PORT_ID = "22223333-4444-5555-6666-777788889999" RTR_ID = "33334444-5555-6666-7777-888899990000" - def _net(net_id=NET_ID, name="my-net", status="ACTIVE"): return { "id": net_id, "name": name, "status": status, @@ -21,7 +20,6 @@ def _net(net_id=NET_ID, name="my-net", status="ACTIVE"): "created_at": "2025-01-01", "updated_at": "2025-01-02", } - def _subnet(sub_id=SUB_ID, name="my-sub", cidr="10.0.0.0/24"): return { "id": sub_id, "name": name, "cidr": cidr, @@ -31,7 +29,6 @@ def _subnet(sub_id=SUB_ID, name="my-sub", cidr="10.0.0.0/24"): "network_id": NET_ID, "created_at": "2025-01-01", } - def _port(port_id=PORT_ID, name="my-port", status="ACTIVE"): return { "id": port_id, "name": name, "status": status, @@ -42,7 +39,6 @@ def _port(port_id=PORT_ID, name="my-port", status="ACTIVE"): "security_groups": ["sg-1"], "created_at": "2025-01-01", } - def _router(rtr_id=RTR_ID, name="my-rtr", status="ACTIVE"): return { "id": rtr_id, "name": name, "status": status, @@ -51,7 +47,6 @@ def _router(rtr_id=RTR_ID, name="my-rtr", status="ACTIVE"): "routes": [], "created_at": "2025-01-01", } - def _setup_mock(mock_client): mock_client.network_url = "https://neutron.example.com" mock_client.compute_url = "https://nova.example.com/v2.1" @@ -114,12 +109,10 @@ def _delete(url, **kwargs): return {"posted": posted, "put_data": put_data, "deleted": deleted} - # ══════════════════════════════════════════════════════════════════════════ # Networks # ══════════════════════════════════════════════════════════════════════════ - class TestNetworks: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -187,12 +180,10 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "deleted" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # Subnets # ══════════════════════════════════════════════════════════════════════════ - class TestSubnets: def test_subnet_list(self, invoke, config_dir, mock_client, sample_profile): @@ -200,7 +191,7 @@ def test_subnet_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["network", "subnet-list"]) + result = invoke(["network", "subnet", "list"]) assert result.exit_code == 0 assert "10.0" in result.output @@ -209,7 +200,7 @@ def test_subnet_show(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["network", "subnet-show", SUB_ID]) + result = invoke(["network", "subnet", "show", SUB_ID]) assert result.exit_code == 0 assert "10.0.0.0/24" in result.output @@ -218,7 +209,7 @@ def test_subnet_create(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["network", "subnet-create", "new-sub", + result = invoke(["network", "subnet", "create", "new-sub", "--network-id", NET_ID, "--cidr", "10.0.1.0/24"]) assert result.exit_code == 0 assert "created" in result.output.lower() @@ -228,7 +219,7 @@ def test_subnet_create_with_options(self, invoke, config_dir, mock_client, sampl set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["network", "subnet-create", "v6-sub", + result = invoke(["network", "subnet", "create", "v6-sub", "--network-id", NET_ID, "--cidr", "fd00::/64", "--ip-version", "6", "--gateway", "fd00::1", "--dns", "8.8.8.8", "--dns", "8.8.4.4"]) @@ -242,16 +233,14 @@ def test_subnet_delete(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["network", "subnet-delete", SUB_ID, "-y"]) + result = invoke(["network", "subnet", "delete", SUB_ID, "-y"]) assert result.exit_code == 0 assert "deleted" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # Ports # ══════════════════════════════════════════════════════════════════════════ - class TestPorts: def test_port_list(self, invoke, config_dir, mock_client, sample_profile): @@ -259,7 +248,7 @@ def test_port_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["network", "port-list"]) + result = invoke(["network", "port", "list"]) assert result.exit_code == 0 assert "fa:1" in result.output @@ -268,7 +257,7 @@ def test_port_show(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["network", "port-show", PORT_ID]) + result = invoke(["network", "port", "show", PORT_ID]) assert result.exit_code == 0 def test_port_create(self, invoke, config_dir, mock_client, sample_profile): @@ -276,7 +265,7 @@ def test_port_create(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["network", "port-create", "--network-id", NET_ID]) + result = invoke(["network", "port", "create", "--network-id", NET_ID]) assert result.exit_code == 0 assert "created" in result.output.lower() @@ -285,7 +274,7 @@ def test_port_update(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["network", "port-update", PORT_ID, "--name", "renamed"]) + result = invoke(["network", "port", "update", PORT_ID, "--name", "renamed"]) assert result.exit_code == 0 assert "updated" in result.output.lower() @@ -294,16 +283,14 @@ def test_port_delete(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["network", "port-delete", PORT_ID, "-y"]) + result = invoke(["network", "port", "delete", PORT_ID, "-y"]) assert result.exit_code == 0 assert "deleted" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # Routers # ══════════════════════════════════════════════════════════════════════════ - class TestRouters: def test_router_list(self, invoke, config_dir, mock_client, sample_profile): @@ -311,7 +298,7 @@ def test_router_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["network", "router-list"]) + result = invoke(["network", "router", "list"]) assert result.exit_code == 0 assert "my-rtr" in result.output @@ -320,7 +307,7 @@ def test_router_show(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["network", "router-show", RTR_ID]) + result = invoke(["network", "router", "show", RTR_ID]) assert result.exit_code == 0 assert "my-rtr" in result.output @@ -329,7 +316,7 @@ def test_router_create(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["network", "router-create", "new-rtr"]) + result = invoke(["network", "router", "create", "new-rtr"]) assert result.exit_code == 0 assert "created" in result.output.lower() @@ -338,7 +325,7 @@ def test_router_create_with_ext_net(self, invoke, config_dir, mock_client, sampl set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["network", "router-create", "new-rtr", + result = invoke(["network", "router", "create", "new-rtr", "--external-network", NET_ID]) assert result.exit_code == 0 assert state["posted"]["router"]["external_gateway_info"]["network_id"] == NET_ID @@ -348,7 +335,7 @@ def test_router_delete(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["network", "router-delete", RTR_ID, "-y"]) + result = invoke(["network", "router", "delete", RTR_ID, "-y"]) assert result.exit_code == 0 assert "deleted" in result.output.lower() @@ -357,7 +344,7 @@ def test_router_add_interface(self, invoke, config_dir, mock_client, sample_prof set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["network", "router-add-interface", RTR_ID, + result = invoke(["network", "router", "add", "interface", RTR_ID, "--subnet-id", SUB_ID]) assert result.exit_code == 0 assert "added" in result.output.lower() @@ -367,17 +354,15 @@ def test_router_remove_interface(self, invoke, config_dir, mock_client, sample_p set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["network", "router-remove-interface", RTR_ID, + result = invoke(["network", "router", "remove", "interface", RTR_ID, "--subnet-id", SUB_ID]) assert result.exit_code == 0 assert "removed" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # Topology # ══════════════════════════════════════════════════════════════════════════ - class TestTopology: def test_topology(self, invoke, config_dir, mock_client, sample_profile): @@ -390,29 +375,25 @@ def test_topology(self, invoke, config_dir, mock_client, sample_profile): assert "my-net" in result.output assert "10.0.0" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - FIP_ID = "44445555-6666-7777-8888-999900001111" RBAC_ID = "55556666-7777-8888-9999-000011112222" SG_ID = "66667777-8888-9999-0000-111122223333" - # ══════════════════════════════════════════════════════════════════════════ # rbac-update # ══════════════════════════════════════════════════════════════════════════ - class TestRbacUpdate: def test_update_target_project(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["network", "rbac-update", RBAC_ID, "--target-project", "proj-2"]) + result = invoke(["network", "rbac", "update", RBAC_ID, "--target-project", "proj-2"]) assert result.exit_code == 0 assert "updated" in result.output assert state["put_data"]["rbac_policy"]["target_tenant"] == "proj-2" @@ -421,29 +402,27 @@ def test_update_wildcard(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["network", "rbac-update", RBAC_ID, "--target-project", "*"]) + result = invoke(["network", "rbac", "update", RBAC_ID, "--target-project", "*"]) assert result.exit_code == 0 assert state["put_data"]["rbac_policy"]["target_tenant"] == "*" def test_update_requires_target_project(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") - result = invoke(["network", "rbac-update", RBAC_ID]) + result = invoke(["network", "rbac", "update", RBAC_ID]) assert result.exit_code != 0 - # ══════════════════════════════════════════════════════════════════════════ # port-unset # ══════════════════════════════════════════════════════════════════════════ - class TestPortUnset: def test_unset_security_group(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["network", "port-unset", PORT_ID, "--security-group", "sg-1"]) + result = invoke(["network", "port", "unset", PORT_ID, "--security-group", "sg-1"]) assert result.exit_code == 0 assert "updated" in result.output # The put should have security_groups without "sg-1" @@ -453,7 +432,7 @@ def test_unset_qos_policy(self, invoke, config_dir, mock_client, sample_profile) save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["network", "port-unset", PORT_ID, "--qos-policy"]) + result = invoke(["network", "port", "unset", PORT_ID, "--qos-policy"]) assert result.exit_code == 0 assert state["put_data"]["port"]["qos_policy_id"] is None @@ -461,7 +440,7 @@ def test_unset_description(self, invoke, config_dir, mock_client, sample_profile save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["network", "port-unset", PORT_ID, "--description"]) + result = invoke(["network", "port", "unset", PORT_ID, "--description"]) assert result.exit_code == 0 assert state["put_data"]["port"]["description"] == "" @@ -469,23 +448,21 @@ def test_unset_nothing(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") _setup_mock(mock_client) - result = invoke(["network", "port-unset", PORT_ID]) + result = invoke(["network", "port", "unset", PORT_ID]) assert result.exit_code == 0 assert "Nothing to unset" in result.output - # ══════════════════════════════════════════════════════════════════════════ # router-set-gateway / router-unset-gateway # ══════════════════════════════════════════════════════════════════════════ - class TestRouterGateway: def test_set_gateway(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["network", "router-set-gateway", RTR_ID, + result = invoke(["network", "router", "set", "gateway", RTR_ID, "--external-network", NET_ID]) assert result.exit_code == 0 assert "Gateway set" in result.output @@ -496,7 +473,7 @@ def test_set_gateway_with_snat(self, invoke, config_dir, mock_client, sample_pro save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["network", "router-set-gateway", RTR_ID, + result = invoke(["network", "router", "set", "gateway", RTR_ID, "--external-network", NET_ID, "--enable-snat"]) assert result.exit_code == 0 gw = state["put_data"]["router"]["external_gateway_info"] @@ -506,24 +483,22 @@ def test_unset_gateway(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["network", "router-unset-gateway", RTR_ID]) + result = invoke(["network", "router", "unset", "gateway", RTR_ID]) assert result.exit_code == 0 assert "removed" in result.output assert state["put_data"]["router"]["external_gateway_info"] == {} - # ══════════════════════════════════════════════════════════════════════════ # router static routes # ══════════════════════════════════════════════════════════════════════════ - class TestRouterRoutes: def test_add_route(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["network", "router-add-route", RTR_ID, + result = invoke(["network", "router", "add", "route", RTR_ID, "--destination", "10.1.0.0/24", "--nexthop", "192.168.1.1"]) assert result.exit_code == 0 @@ -535,7 +510,7 @@ def test_add_route_url(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - invoke(["network", "router-add-route", RTR_ID, + invoke(["network", "router", "add", "route", RTR_ID, "--destination", "10.1.0.0/24", "--nexthop", "192.168.1.1"]) assert "add_extraroutes" in state["put_data"]["url"] @@ -543,7 +518,7 @@ def test_remove_route(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["network", "router-remove-route", RTR_ID, + result = invoke(["network", "router", "remove", "route", RTR_ID, "--destination", "10.1.0.0/24", "--nexthop", "192.168.1.1"]) assert result.exit_code == 0 @@ -555,31 +530,27 @@ def test_remove_route_url(self, invoke, config_dir, mock_client, sample_profile) save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - invoke(["network", "router-remove-route", RTR_ID, + invoke(["network", "router", "remove", "route", RTR_ID, "--destination", "10.1.0.0/24", "--nexthop", "192.168.1.1"]) assert "remove_extraroutes" in state["put_data"]["url"] def test_add_route_requires_both_options(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") - result = invoke(["network", "router-add-route", RTR_ID, "--destination", "10.0.0.0/24"]) + result = invoke(["network", "router", "add", "route", RTR_ID, "--destination", "10.0.0.0/24"]) assert result.exit_code != 0 - class TestNetworkHelp: def test_network_help(self, invoke): result = invoke(["network", "--help"]) assert result.exit_code == 0 for cmd in ("list", "show", "create", "delete", - "subnet-list", "subnet-create", - "port-list", "port-create", - "router-list", "router-create", "topology", "trace"): assert cmd in result.output def test_subnet_create_help(self, invoke): - result = invoke(["network", "subnet-create", "--help"]) + result = invoke(["network", "subnet", "create", "--help"]) assert result.exit_code == 0 assert "--cidr" in result.output assert "--dns" in result.output diff --git a/tests/test_neutron_admin.py b/tests/test_neutron_admin.py index 5db2586..1d736f7 100644 --- a/tests/test_neutron_admin.py +++ b/tests/test_neutron_admin.py @@ -15,12 +15,10 @@ PORT = "22222222-2222-2222-2222-222222222222" PORT2 = "33333333-3333-3333-3333-333333333333" - def _net(mock_client): mock_client.network_url = NET return mock_client - # ══════════════════════════════════════════════════════════════════════════ # network subnet-update # ══════════════════════════════════════════════════════════════════════════ @@ -29,14 +27,14 @@ class TestSubnetUpdate: def test_update_name(self, invoke, mock_client): _net(mock_client) - result = invoke(["network", "subnet-update", SUBNET, "--name", "new-name"]) + result = invoke(["network", "subnet", "update", SUBNET, "--name", "new-name"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"]["subnet"] assert body["name"] == "new-name" def test_update_dns(self, invoke, mock_client): _net(mock_client) - result = invoke(["network", "subnet-update", SUBNET, + result = invoke(["network", "subnet", "update", SUBNET, "--dns-nameserver", "8.8.8.8", "--dns-nameserver", "1.1.1.1"]) assert result.exit_code == 0 @@ -46,31 +44,30 @@ def test_update_dns(self, invoke, mock_client): def test_enable_dhcp(self, invoke, mock_client): _net(mock_client) - invoke(["network", "subnet-update", SUBNET, "--enable-dhcp"]) + invoke(["network", "subnet", "update", SUBNET, "--enable-dhcp"]) body = mock_client.put.call_args[1]["json"]["subnet"] assert body["enable_dhcp"] is True def test_disable_dhcp(self, invoke, mock_client): _net(mock_client) - invoke(["network", "subnet-update", SUBNET, "--disable-dhcp"]) + invoke(["network", "subnet", "update", SUBNET, "--disable-dhcp"]) body = mock_client.put.call_args[1]["json"]["subnet"] assert body["enable_dhcp"] is False def test_nothing_to_update(self, invoke, mock_client): _net(mock_client) - result = invoke(["network", "subnet-update", SUBNET]) + result = invoke(["network", "subnet", "update", SUBNET]) assert result.exit_code == 0 mock_client.put.assert_not_called() def test_calls_correct_url(self, invoke, mock_client): _net(mock_client) - invoke(["network", "subnet-update", SUBNET, "--name", "x"]) + invoke(["network", "subnet", "update", SUBNET, "--name", "x"]) url = mock_client.put.call_args[0][0] assert f"/v2.0/subnets/{SUBNET}" in url def test_help(self, invoke): - assert invoke(["network", "subnet-update", "--help"]).exit_code == 0 - + assert invoke(["network", "subnet", "update", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # network agent-list / show / set / delete @@ -85,19 +82,18 @@ def _agent(self, **kw): def test_list(self, invoke, mock_client): _net(mock_client) mock_client.get.return_value = {"agents": [self._agent()]} - result = invoke(["network", "agent-list"]) + result = invoke(["network", "agent", "list"]) assert result.exit_code == 0 assert "L3" in result.output def test_list_empty(self, invoke, mock_client): _net(mock_client) mock_client.get.return_value = {"agents": []} - result = invoke(["network", "agent-list"]) + result = invoke(["network", "agent", "list"]) assert "No agents" in result.output def test_help(self, invoke): - assert invoke(["network", "agent-list", "--help"]).exit_code == 0 - + assert invoke(["network", "agent", "list", "--help"]).exit_code == 0 class TestNetworkAgentShow: @@ -107,58 +103,55 @@ def test_show(self, invoke, mock_client): "id": AGENT, "agent_type": "L3 agent", "host": "ctrl1", "alive": True, "admin_state_up": True, "topic": "l3_agent", }} - result = invoke(["network", "agent-show", AGENT]) + result = invoke(["network", "agent", "show", AGENT]) assert result.exit_code == 0 assert "L3" in result.output def test_calls_correct_url(self, invoke, mock_client): _net(mock_client) mock_client.get.return_value = {"agent": {}} - invoke(["network", "agent-show", AGENT]) + invoke(["network", "agent", "show", AGENT]) url = mock_client.get.call_args[0][0] assert f"/v2.0/agents/{AGENT}" in url def test_help(self, invoke): - assert invoke(["network", "agent-show", "--help"]).exit_code == 0 - + assert invoke(["network", "agent", "show", "--help"]).exit_code == 0 class TestNetworkAgentSet: def test_disable(self, invoke, mock_client): _net(mock_client) - result = invoke(["network", "agent-set", AGENT, "--disable"]) + result = invoke(["network", "agent", "set", AGENT, "--disable"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"]["agent"] assert body["admin_state_up"] is False def test_enable(self, invoke, mock_client): _net(mock_client) - invoke(["network", "agent-set", AGENT, "--enable"]) + invoke(["network", "agent", "set", AGENT, "--enable"]) body = mock_client.put.call_args[1]["json"]["agent"] assert body["admin_state_up"] is True def test_help(self, invoke): - assert invoke(["network", "agent-set", "--help"]).exit_code == 0 - + assert invoke(["network", "agent", "set", "--help"]).exit_code == 0 class TestNetworkAgentDelete: def test_delete_yes(self, invoke, mock_client): _net(mock_client) - result = invoke(["network", "agent-delete", AGENT, "--yes"]) + result = invoke(["network", "agent", "delete", AGENT, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() assert f"/v2.0/agents/{AGENT}" in mock_client.delete.call_args[0][0] def test_delete_requires_confirm(self, invoke, mock_client): _net(mock_client) - result = invoke(["network", "agent-delete", AGENT], input="n\n") + result = invoke(["network", "agent", "delete", AGENT], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help(self, invoke): - assert invoke(["network", "agent-delete", "--help"]).exit_code == 0 - + assert invoke(["network", "agent", "delete", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # network rbac-list / show / create / delete @@ -173,19 +166,18 @@ def _rbac(self, **kw): def test_list(self, invoke, mock_client): _net(mock_client) mock_client.get.return_value = {"rbac_policies": [self._rbac()]} - result = invoke(["network", "rbac-list"]) + result = invoke(["network", "rbac", "list"]) assert result.exit_code == 0 assert "network" in result.output def test_list_empty(self, invoke, mock_client): _net(mock_client) mock_client.get.return_value = {"rbac_policies": []} - result = invoke(["network", "rbac-list"]) + result = invoke(["network", "rbac", "list"]) assert "No RBAC" in result.output def test_help(self, invoke): - assert invoke(["network", "rbac-list", "--help"]).exit_code == 0 - + assert invoke(["network", "rbac", "list", "--help"]).exit_code == 0 class TestNetworkRbacShow: @@ -195,20 +187,19 @@ def test_show(self, invoke, mock_client): "id": RBAC, "object_type": "network", "object_id": PORT, "action": "access_as_shared", "target_project_id": "*", }} - result = invoke(["network", "rbac-show", RBAC]) + result = invoke(["network", "rbac", "show", RBAC]) assert result.exit_code == 0 assert "network" in result.output def test_help(self, invoke): - assert invoke(["network", "rbac-show", "--help"]).exit_code == 0 - + assert invoke(["network", "rbac", "show", "--help"]).exit_code == 0 class TestNetworkRbacCreate: def test_create(self, invoke, mock_client): _net(mock_client) mock_client.post.return_value = {"rbac_policy": {"id": RBAC}} - result = invoke(["network", "rbac-create", + result = invoke(["network", "rbac", "create", "--object-type", "network", "--object", PORT, "--target-project", "*", @@ -221,7 +212,7 @@ def test_create(self, invoke, mock_client): def test_calls_correct_url(self, invoke, mock_client): _net(mock_client) mock_client.post.return_value = {"rbac_policy": {"id": RBAC}} - invoke(["network", "rbac-create", + invoke(["network", "rbac", "create", "--object-type", "network", "--object", PORT, "--target-project", "*", @@ -230,26 +221,24 @@ def test_calls_correct_url(self, invoke, mock_client): assert "/v2.0/rbac-policies" in url def test_help(self, invoke): - assert invoke(["network", "rbac-create", "--help"]).exit_code == 0 - + assert invoke(["network", "rbac", "create", "--help"]).exit_code == 0 class TestNetworkRbacDelete: def test_delete_yes(self, invoke, mock_client): _net(mock_client) - result = invoke(["network", "rbac-delete", RBAC, "--yes"]) + result = invoke(["network", "rbac", "delete", RBAC, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): _net(mock_client) - result = invoke(["network", "rbac-delete", RBAC], input="n\n") + result = invoke(["network", "rbac", "delete", RBAC], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help(self, invoke): - assert invoke(["network", "rbac-delete", "--help"]).exit_code == 0 - + assert invoke(["network", "rbac", "delete", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # subnet-pool @@ -290,7 +279,6 @@ def test_list_empty(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["subnet-pool", "list", "--help"]).exit_code == 0 - class TestSubnetPoolShow: def test_show(self, invoke, mock_client): @@ -307,7 +295,6 @@ def test_show(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["subnet-pool", "show", "--help"]).exit_code == 0 - class TestSubnetPoolCreate: def test_create(self, invoke, mock_client): @@ -335,7 +322,6 @@ def test_create_multiple_prefixes(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["subnet-pool", "create", "--help"]).exit_code == 0 - class TestSubnetPoolSet: def test_set_name(self, invoke, mock_client): @@ -354,7 +340,6 @@ def test_set_nothing(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["subnet-pool", "set", "--help"]).exit_code == 0 - class TestSubnetPoolDelete: def test_delete_yes(self, invoke, mock_client): @@ -372,7 +357,6 @@ def test_delete_requires_confirm(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["subnet-pool", "delete", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════ # qos policy CRUD # ══════════════════════════════════════════════════════════════════════════ @@ -386,25 +370,24 @@ def _policy(self, **kw): def test_list(self, invoke, mock_client): _net(mock_client) mock_client.get.return_value = {"policies": [self._policy()]} - result = invoke(["qos", "policy-list"]) + result = invoke(["qos", "policy", "list"]) assert result.exit_code == 0 assert "my-qos" in result.output def test_list_shared_filter(self, invoke, mock_client): _net(mock_client) mock_client.get.return_value = {"policies": []} - invoke(["qos", "policy-list", "--shared"]) + invoke(["qos", "policy", "list", "--shared"]) assert mock_client.get.call_args[1]["params"].get("shared") is True def test_list_empty(self, invoke, mock_client): _net(mock_client) mock_client.get.return_value = {"policies": []} - result = invoke(["qos", "policy-list"]) + result = invoke(["qos", "policy", "list"]) assert "No QoS" in result.output def test_help(self, invoke): - assert invoke(["qos", "policy-list", "--help"]).exit_code == 0 - + assert invoke(["qos", "policy", "list", "--help"]).exit_code == 0 class TestQosPolicyShow: @@ -414,20 +397,19 @@ def test_show(self, invoke, mock_client): "id": POLICY, "name": "my-qos", "shared": False, "is_default": False, "description": "", "project_id": "abc", }} - result = invoke(["qos", "policy-show", POLICY]) + result = invoke(["qos", "policy", "show", POLICY]) assert result.exit_code == 0 assert "my-qos" in result.output def test_help(self, invoke): - assert invoke(["qos", "policy-show", "--help"]).exit_code == 0 - + assert invoke(["qos", "policy", "show", "--help"]).exit_code == 0 class TestQosPolicyCreate: def test_create(self, invoke, mock_client): _net(mock_client) mock_client.post.return_value = {"policy": {"id": POLICY}} - result = invoke(["qos", "policy-create", "--name", "my-qos"]) + result = invoke(["qos", "policy", "create", "--name", "my-qos"]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"]["policy"] assert body["name"] == "my-qos" @@ -435,50 +417,47 @@ def test_create(self, invoke, mock_client): def test_create_shared(self, invoke, mock_client): _net(mock_client) mock_client.post.return_value = {"policy": {"id": POLICY}} - invoke(["qos", "policy-create", "--name", "q", "--shared"]) + invoke(["qos", "policy", "create", "--name", "q", "--shared"]) body = mock_client.post.call_args[1]["json"]["policy"] assert body["shared"] is True def test_help(self, invoke): - assert invoke(["qos", "policy-create", "--help"]).exit_code == 0 - + assert invoke(["qos", "policy", "create", "--help"]).exit_code == 0 class TestQosPolicySet: def test_set_name(self, invoke, mock_client): _net(mock_client) - result = invoke(["qos", "policy-set", POLICY, "--name", "renamed"]) + result = invoke(["qos", "policy", "set", POLICY, "--name", "renamed"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"]["policy"] assert body["name"] == "renamed" def test_set_nothing(self, invoke, mock_client): _net(mock_client) - result = invoke(["qos", "policy-set", POLICY]) + result = invoke(["qos", "policy", "set", POLICY]) assert result.exit_code == 0 mock_client.put.assert_not_called() def test_help(self, invoke): - assert invoke(["qos", "policy-set", "--help"]).exit_code == 0 - + assert invoke(["qos", "policy", "set", "--help"]).exit_code == 0 class TestQosPolicyDelete: def test_delete_yes(self, invoke, mock_client): _net(mock_client) - result = invoke(["qos", "policy-delete", POLICY, "--yes"]) + result = invoke(["qos", "policy", "delete", POLICY, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): _net(mock_client) - result = invoke(["qos", "policy-delete", POLICY], input="n\n") + result = invoke(["qos", "policy", "delete", POLICY], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help(self, invoke): - assert invoke(["qos", "policy-delete", "--help"]).exit_code == 0 - + assert invoke(["qos", "policy", "delete", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # qos rules @@ -491,14 +470,14 @@ def test_list_bandwidth(self, invoke, mock_client): mock_client.get.return_value = {"bandwidth_limit_rules": [ {"id": RULE, "max_kbps": 1000, "max_burst_kbps": 200, "direction": "egress"} ]} - result = invoke(["qos", "rule-list", POLICY]) + result = invoke(["qos", "rule", "list", POLICY]) assert result.exit_code == 0 assert "1000" in result.output def test_list_empty(self, invoke, mock_client): _net(mock_client) mock_client.get.return_value = {"bandwidth_limit_rules": []} - result = invoke(["qos", "rule-list", POLICY]) + result = invoke(["qos", "rule", "list", POLICY]) assert result.exit_code == 0 def test_list_dscp(self, invoke, mock_client): @@ -506,19 +485,18 @@ def test_list_dscp(self, invoke, mock_client): mock_client.get.return_value = {"dscp_marking_rules": [ {"id": RULE, "dscp_mark": 14} ]} - result = invoke(["qos", "rule-list", POLICY, "--type", "dscp-marking"]) + result = invoke(["qos", "rule", "list", POLICY, "--type", "dscp-marking"]) assert result.exit_code == 0 def test_help(self, invoke): - assert invoke(["qos", "rule-list", "--help"]).exit_code == 0 - + assert invoke(["qos", "rule", "list", "--help"]).exit_code == 0 class TestQosRuleCreate: def test_create_bandwidth_limit(self, invoke, mock_client): _net(mock_client) mock_client.post.return_value = {"bandwidth_limit_rule": {"id": RULE}} - result = invoke(["qos", "rule-create", POLICY, + result = invoke(["qos", "rule", "create", POLICY, "--type", "bandwidth-limit", "--max-kbps", "1000"]) assert result.exit_code == 0 @@ -527,13 +505,13 @@ def test_create_bandwidth_limit(self, invoke, mock_client): def test_create_bandwidth_requires_max_kbps(self, invoke, mock_client): _net(mock_client) - result = invoke(["qos", "rule-create", POLICY, "--type", "bandwidth-limit"]) + result = invoke(["qos", "rule", "create", POLICY, "--type", "bandwidth-limit"]) assert result.exit_code != 0 def test_create_dscp_marking(self, invoke, mock_client): _net(mock_client) mock_client.post.return_value = {"dscp_marking_rule": {"id": RULE}} - result = invoke(["qos", "rule-create", POLICY, + result = invoke(["qos", "rule", "create", POLICY, "--type", "dscp-marking", "--dscp-mark", "14"]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"]["dscp_marking_rule"] @@ -541,46 +519,44 @@ def test_create_dscp_marking(self, invoke, mock_client): def test_create_dscp_requires_mark(self, invoke, mock_client): _net(mock_client) - result = invoke(["qos", "rule-create", POLICY, "--type", "dscp-marking"]) + result = invoke(["qos", "rule", "create", POLICY, "--type", "dscp-marking"]) assert result.exit_code != 0 def test_create_minimum_bandwidth(self, invoke, mock_client): _net(mock_client) mock_client.post.return_value = {"minimum_bandwidth_rule": {"id": RULE}} - result = invoke(["qos", "rule-create", POLICY, + result = invoke(["qos", "rule", "create", POLICY, "--type", "minimum-bandwidth", "--min-kbps", "500"]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"]["minimum_bandwidth_rule"] assert body["min_kbps"] == 500 def test_help(self, invoke): - assert invoke(["qos", "rule-create", "--help"]).exit_code == 0 - + assert invoke(["qos", "rule", "create", "--help"]).exit_code == 0 class TestQosRuleDelete: def test_delete_yes(self, invoke, mock_client): _net(mock_client) - result = invoke(["qos", "rule-delete", POLICY, RULE, "--yes"]) + result = invoke(["qos", "rule", "delete", POLICY, RULE, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): _net(mock_client) - result = invoke(["qos", "rule-delete", POLICY, RULE], input="n\n") + result = invoke(["qos", "rule", "delete", POLICY, RULE], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_calls_correct_url(self, invoke, mock_client): _net(mock_client) - invoke(["qos", "rule-delete", POLICY, RULE, "--yes"]) + invoke(["qos", "rule", "delete", POLICY, RULE, "--yes"]) url = mock_client.delete.call_args[0][0] assert f"/qos/policies/{POLICY}" in url assert RULE in url def test_help(self, invoke): - assert invoke(["qos", "rule-delete", "--help"]).exit_code == 0 - + assert invoke(["qos", "rule", "delete", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # trunk @@ -608,7 +584,6 @@ def test_list_empty(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["trunk", "list", "--help"]).exit_code == 0 - class TestTrunkShow: def test_show(self, invoke, mock_client): @@ -625,7 +600,6 @@ def test_show(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["trunk", "show", "--help"]).exit_code == 0 - class TestTrunkCreate: def test_create(self, invoke, mock_client): @@ -647,7 +621,6 @@ def test_create_disabled(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["trunk", "create", "--help"]).exit_code == 0 - class TestTrunkSet: def test_set_name(self, invoke, mock_client): @@ -666,7 +639,6 @@ def test_set_nothing(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["trunk", "set", "--help"]).exit_code == 0 - class TestTrunkDelete: def test_delete_yes(self, invoke, mock_client): @@ -684,7 +656,6 @@ def test_delete_requires_confirm(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["trunk", "delete", "--help"]).exit_code == 0 - class TestTrunkSubportList: def test_list(self, invoke, mock_client): @@ -692,7 +663,7 @@ def test_list(self, invoke, mock_client): mock_client.get.return_value = {"sub_ports": [ {"port_id": PORT2, "segmentation_type": "vlan", "segmentation_id": 100} ]} - result = invoke(["trunk", "subport-list", TRUNK]) + result = invoke(["trunk", "subport", "list", TRUNK]) assert result.exit_code == 0 assert "vlan" in result.output assert "100" in result.output @@ -700,25 +671,24 @@ def test_list(self, invoke, mock_client): def test_list_empty(self, invoke, mock_client): _net(mock_client) mock_client.get.return_value = {"sub_ports": []} - result = invoke(["trunk", "subport-list", TRUNK]) + result = invoke(["trunk", "subport", "list", TRUNK]) assert "No sub-ports" in result.output def test_calls_correct_url(self, invoke, mock_client): _net(mock_client) mock_client.get.return_value = {"sub_ports": []} - invoke(["trunk", "subport-list", TRUNK]) + invoke(["trunk", "subport", "list", TRUNK]) url = mock_client.get.call_args[0][0] assert f"/v2.0/trunks/{TRUNK}/get_subports" in url def test_help(self, invoke): - assert invoke(["trunk", "subport-list", "--help"]).exit_code == 0 - + assert invoke(["trunk", "subport", "list", "--help"]).exit_code == 0 class TestTrunkAddSubport: def test_add(self, invoke, mock_client): _net(mock_client) - result = invoke(["trunk", "add-subport", TRUNK, + result = invoke(["trunk", "subport", "add", TRUNK, "--port", PORT2, "--segmentation-id", "100"]) assert result.exit_code == 0 @@ -728,39 +698,37 @@ def test_add(self, invoke, mock_client): def test_add_calls_correct_url(self, invoke, mock_client): _net(mock_client) - invoke(["trunk", "add-subport", TRUNK, + invoke(["trunk", "subport", "add", TRUNK, "--port", PORT2, "--segmentation-id", "100"]) url = mock_client.put.call_args[0][0] assert f"/v2.0/trunks/{TRUNK}/add_subports" in url def test_help(self, invoke): - assert invoke(["trunk", "add-subport", "--help"]).exit_code == 0 - + assert invoke(["trunk", "subport", "add", "--help"]).exit_code == 0 class TestTrunkRemoveSubport: def test_remove_yes(self, invoke, mock_client): _net(mock_client) - result = invoke(["trunk", "remove-subport", TRUNK, "--port", PORT2, "--yes"]) + result = invoke(["trunk", "subport", "remove", TRUNK, "--port", PORT2, "--yes"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"]["sub_ports"][0] assert body["port_id"] == PORT2 def test_remove_requires_confirm(self, invoke, mock_client): _net(mock_client) - result = invoke(["trunk", "remove-subport", TRUNK, "--port", PORT2], input="n\n") + result = invoke(["trunk", "subport", "remove", TRUNK, "--port", PORT2], input="n\n") assert result.exit_code != 0 mock_client.put.assert_not_called() def test_calls_correct_url(self, invoke, mock_client): _net(mock_client) - invoke(["trunk", "remove-subport", TRUNK, "--port", PORT2, "--yes"]) + invoke(["trunk", "subport", "remove", TRUNK, "--port", PORT2, "--yes"]) url = mock_client.put.call_args[0][0] assert f"/v2.0/trunks/{TRUNK}/remove_subports" in url def test_help(self, invoke): - assert invoke(["trunk", "remove-subport", "--help"]).exit_code == 0 - + assert invoke(["trunk", "subport", "remove", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # Registration @@ -778,29 +746,26 @@ def test_subnet_pool_subcommands(self, invoke, sub): def test_qos_registered(self, invoke): assert invoke(["qos", "--help"]).exit_code == 0 - @pytest.mark.parametrize("sub", ["policy-list", "policy-show", "policy-create", - "policy-set", "policy-delete", - "rule-list", "rule-create", "rule-delete"]) - def test_qos_subcommands(self, invoke, sub): - assert invoke(["qos", sub, "--help"]).exit_code == 0 + def test_qos_subcommands(self, invoke): + for cmd in (["qos", "policy", "--help"], ["qos", "rule", "--help"]): + assert invoke(cmd).exit_code == 0 def test_trunk_registered(self, invoke): assert invoke(["trunk", "--help"]).exit_code == 0 - @pytest.mark.parametrize("sub", ["list", "show", "create", "set", "delete", - "subport-list", "add-subport", "remove-subport"]) + @pytest.mark.parametrize("sub", ["list", "show", "create", "set", "delete"]) def test_trunk_subcommands(self, invoke, sub): assert invoke(["trunk", sub, "--help"]).exit_code == 0 def test_network_agent_subcommands(self, invoke): - for sub in ["agent-list", "agent-show", "agent-set", "agent-delete"]: - result = invoke(["network", sub, "--help"]) - assert result.exit_code == 0, f"network {sub} --help failed" + for sub in ["list", "show", "set", "delete"]: + result = invoke(["network", "agent", sub, "--help"]) + assert result.exit_code == 0, f"network agent {sub} --help failed" def test_network_rbac_subcommands(self, invoke): - for sub in ["rbac-list", "rbac-show", "rbac-create", "rbac-delete"]: - result = invoke(["network", sub, "--help"]) - assert result.exit_code == 0, f"network {sub} --help failed" + for sub in ["list", "show", "create", "delete"]: + result = invoke(["network", "rbac", sub, "--help"]) + assert result.exit_code == 0, f"network rbac {sub} --help failed" def test_network_subnet_update(self, invoke): - assert invoke(["network", "subnet-update", "--help"]).exit_code == 0 + assert invoke(["network", "subnet", "update", "--help"]).exit_code == 0 diff --git a/tests/test_no_click_exceptions_in_commands.py b/tests/test_no_click_exceptions_in_commands.py index e17d571..1112cb0 100644 --- a/tests/test_no_click_exceptions_in_commands.py +++ b/tests/test_no_click_exceptions_in_commands.py @@ -32,7 +32,6 @@ r"\braise\s+(?:click\.(?:ClickException|UsageError)|SystemExit)\b", ) - def test_no_click_exceptions_raised_in_commands(): """Walk every commands/core module and reject ``raise click.ClickException``, ``raise click.UsageError``, or diff --git a/tests/test_no_private_client_api_in_commands.py b/tests/test_no_private_client_api_in_commands.py index e50a6bd..d4b56bb 100644 --- a/tests/test_no_private_client_api_in_commands.py +++ b/tests/test_no_private_client_api_in_commands.py @@ -31,7 +31,6 @@ # Stripped: ``self._foo`` (services are allowed), ``cls._foo``, ``mock_client._foo``. _PRIVATE_ACCESS = re.compile(r"\bclient\._[A-Za-z]") - def test_no_private_client_attribute_access_in_commands() -> None: """Walk every command module and reject ``client._`` access. diff --git a/tests/test_nova_admin.py b/tests/test_nova_admin.py index 5b0faaa..8971e40 100644 --- a/tests/test_nova_admin.py +++ b/tests/test_nova_admin.py @@ -9,12 +9,10 @@ PRJ = "33333333-3333-3333-3333-333333333333" NOVA = "https://nova.example.com/v2.1" - def _nova(mock_client): mock_client.compute_url = NOVA return mock_client - # ══════════════════════════════════════════════════════════════════════════ # compute-service list # ══════════════════════════════════════════════════════════════════════════ @@ -64,7 +62,6 @@ def test_list_empty(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["compute-service", "list", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════ # compute-service set # ══════════════════════════════════════════════════════════════════════════ @@ -113,7 +110,6 @@ def test_calls_correct_url(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["compute-service", "set", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════ # compute-service delete # ══════════════════════════════════════════════════════════════════════════ @@ -136,7 +132,6 @@ def test_delete_requires_confirm(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["compute-service", "delete", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════ # server migration-list # ══════════════════════════════════════════════════════════════════════════ @@ -152,7 +147,7 @@ def _migration(self, **kw): def test_list(self, invoke, mock_client): _nova(mock_client) mock_client.get.return_value = {"migrations": [self._migration()]} - result = invoke(["server", "migration-list", SRV]) + result = invoke(["server", "migration", "list", SRV]) assert result.exit_code == 0 assert "host1" in result.output assert "host2" in result.output @@ -160,14 +155,14 @@ def test_list(self, invoke, mock_client): def test_list_calls_correct_url(self, invoke, mock_client): _nova(mock_client) mock_client.get.return_value = {"migrations": []} - invoke(["server", "migration-list", SRV]) + invoke(["server", "migration", "list", SRV]) url = mock_client.get.call_args[0][0] assert f"/servers/{SRV}/migrations" in url def test_list_empty(self, invoke, mock_client): _nova(mock_client) mock_client.get.return_value = {"migrations": []} - result = invoke(["server", "migration-list", SRV]) + result = invoke(["server", "migration", "list", SRV]) assert result.exit_code == 0 assert "No migrations" in result.output @@ -176,12 +171,11 @@ def test_shows_migration_type(self, invoke, mock_client): mock_client.get.return_value = {"migrations": [ self._migration(migration_type="cold-migration") ]} - result = invoke(["server", "migration-list", SRV]) + result = invoke(["server", "migration", "list", SRV]) assert "cold" in result.output.lower() def test_help(self, invoke): - assert invoke(["server", "migration-list", "--help"]).exit_code == 0 - + assert invoke(["server", "migration", "list", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # server migration-show @@ -198,20 +192,19 @@ def test_show(self, invoke, mock_client): "old_instance_type_id": FLV, "new_instance_type_id": FLV, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:01:00Z", }} - result = invoke(["server", "migration-show", SRV, "10"]) + result = invoke(["server", "migration", "show", SRV, "10"]) assert result.exit_code == 0 assert "completed" in result.output def test_show_calls_correct_url(self, invoke, mock_client): _nova(mock_client) mock_client.get.return_value = {"migration": {}} - invoke(["server", "migration-show", SRV, "10"]) + invoke(["server", "migration", "show", SRV, "10"]) url = mock_client.get.call_args[0][0] assert f"/servers/{SRV}/migrations/10" in url def test_help(self, invoke): - assert invoke(["server", "migration-show", "--help"]).exit_code == 0 - + assert invoke(["server", "migration", "show", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # flavor access-list @@ -224,27 +217,26 @@ def test_list(self, invoke, mock_client): mock_client.get.return_value = {"flavor_access": [ {"flavor_id": FLV, "tenant_id": PRJ}, ]} - result = invoke(["flavor", "access-list", FLV]) + result = invoke(["flavor", "access", "list", FLV]) assert result.exit_code == 0 assert PRJ[:8] in result.output def test_list_calls_correct_url(self, invoke, mock_client): _nova(mock_client) mock_client.get.return_value = {"flavor_access": []} - invoke(["flavor", "access-list", FLV]) + invoke(["flavor", "access", "list", FLV]) url = mock_client.get.call_args[0][0] assert f"/flavors/{FLV}/os-flavor-access" in url def test_list_empty(self, invoke, mock_client): _nova(mock_client) mock_client.get.return_value = {"flavor_access": []} - result = invoke(["flavor", "access-list", FLV]) + result = invoke(["flavor", "access", "list", FLV]) assert result.exit_code == 0 assert "No access" in result.output or "public" in result.output.lower() def test_help(self, invoke): - assert invoke(["flavor", "access-list", "--help"]).exit_code == 0 - + assert invoke(["flavor", "access", "list", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # flavor access-add @@ -254,7 +246,7 @@ class TestFlavorAccessAdd: def test_add(self, invoke, mock_client): _nova(mock_client) - result = invoke(["flavor", "access-add", FLV, PRJ]) + result = invoke(["flavor", "access", "add", FLV, PRJ]) assert result.exit_code == 0 assert mock_client.post.called body = mock_client.post.call_args[1]["json"] @@ -262,13 +254,12 @@ def test_add(self, invoke, mock_client): def test_add_calls_correct_url(self, invoke, mock_client): _nova(mock_client) - invoke(["flavor", "access-add", FLV, PRJ]) + invoke(["flavor", "access", "add", FLV, PRJ]) url = mock_client.post.call_args[0][0] assert f"/flavors/{FLV}/action" in url def test_help(self, invoke): - assert invoke(["flavor", "access-add", "--help"]).exit_code == 0 - + assert invoke(["flavor", "access", "add", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # flavor access-remove @@ -278,20 +269,19 @@ class TestFlavorAccessRemove: def test_remove_yes(self, invoke, mock_client): _nova(mock_client) - result = invoke(["flavor", "access-remove", FLV, PRJ, "--yes"]) + result = invoke(["flavor", "access", "remove", FLV, PRJ, "--yes"]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"] assert body["removeTenantAccess"]["tenant"] == PRJ def test_remove_requires_confirm(self, invoke, mock_client): _nova(mock_client) - result = invoke(["flavor", "access-remove", FLV, PRJ], input="n\n") + result = invoke(["flavor", "access", "remove", FLV, PRJ], input="n\n") assert result.exit_code != 0 mock_client.post.assert_not_called() def test_help(self, invoke): - assert invoke(["flavor", "access-remove", "--help"]).exit_code == 0 - + assert invoke(["flavor", "access", "remove", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # Registration @@ -306,10 +296,10 @@ def test_compute_service_registered(self, invoke): def test_compute_service_subcommands(self, invoke, sub): assert invoke(["compute-service", sub, "--help"]).exit_code == 0 - @pytest.mark.parametrize("sub", ["migration-list", "migration-show"]) - def test_server_migration_subcommands(self, invoke, sub): - assert invoke(["server", sub, "--help"]).exit_code == 0 + def test_server_migration_subcommands(self, invoke): + for sub in ("list", "show", "force-complete", "abort"): + assert invoke(["server", "migration", sub, "--help"]).exit_code == 0 - @pytest.mark.parametrize("sub", ["access-list", "access-add", "access-remove"]) - def test_flavor_access_subcommands(self, invoke, sub): - assert invoke(["flavor", sub, "--help"]).exit_code == 0 + def test_flavor_access_subcommands(self, invoke): + for sub in ("list", "add", "remove"): + assert invoke(["flavor", "access", sub, "--help"]).exit_code == 0 diff --git a/tests/test_object_store.py b/tests/test_object_store.py index dc4547e..66d39d0 100644 --- a/tests/test_object_store.py +++ b/tests/test_object_store.py @@ -8,7 +8,6 @@ # ── Helpers ──────────────────────────────────────────────────────────────── - def _fake_response(status_code=200, content=b"data", headers=None): resp = MagicMock() resp.status_code = status_code @@ -18,7 +17,6 @@ def _fake_response(status_code=200, content=b"data", headers=None): resp.headers = headers or {} return resp - def _setup_mock(mock_client): mock_client.object_store_url = "https://swift.example.com/v1/AUTH_proj" mock_client._headers = lambda: {"X-Auth-Token": "fake-token"} @@ -99,12 +97,10 @@ def _post(url, **kwargs): return {"deleted": deleted} - # ══════════════════════════════════════════════════════════════════════════ # stats # ══════════════════════════════════════════════════════════════════════════ - class TestObjectStats: def test_stats(self, invoke, config_dir, mock_client, sample_profile): @@ -117,12 +113,10 @@ def test_stats(self, invoke, config_dir, mock_client, sample_profile): assert "42" in result.output assert "3" in result.output - # ══════════════════════════════════════════════════════════════════════════ # container list # ══════════════════════════════════════════════════════════════════════════ - class TestContainerList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -145,12 +139,10 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No containers found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # container show # ══════════════════════════════════════════════════════════════════════════ - class TestContainerShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -163,12 +155,10 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): assert "docs" in result.output assert "5" in result.output - # ══════════════════════════════════════════════════════════════════════════ # container create # ══════════════════════════════════════════════════════════════════════════ - class TestContainerCreate: def test_create(self, invoke, config_dir, mock_client, sample_profile): @@ -180,12 +170,10 @@ def test_create(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "created" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # container delete # ══════════════════════════════════════════════════════════════════════════ - class TestContainerDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -225,12 +213,10 @@ def test_delete_aborts_without_yes(self, invoke, config_dir, mock_client, sample assert "deleted" not in result.output.lower() assert state["deleted"] == [] - # ══════════════════════════════════════════════════════════════════════════ # list (objects) # ══════════════════════════════════════════════════════════════════════════ - class TestObjectList: def test_list_objects(self, invoke, config_dir, mock_client, sample_profile): @@ -252,12 +238,10 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No objects found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # show (object) # ══════════════════════════════════════════════════════════════════════════ - class TestObjectShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -270,12 +254,10 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): assert "readme.txt" in result.output assert "1024" in result.output - # ══════════════════════════════════════════════════════════════════════════ # delete (objects) # ══════════════════════════════════════════════════════════════════════════ - class TestObjectDelete: def test_delete_objects(self, invoke, config_dir, mock_client, sample_profile): @@ -288,12 +270,10 @@ def test_delete_objects(self, invoke, config_dir, mock_client, sample_profile): assert "2 object(s)" in result.output assert len(state["deleted"]) == 2 - # ══════════════════════════════════════════════════════════════════════════ # container-set # ══════════════════════════════════════════════════════════════════════════ - class TestContainerSet: def test_set_metadata(self, invoke, config_dir, mock_client, sample_profile): @@ -313,12 +293,10 @@ def test_set_invalid_format(self, invoke, config_dir, mock_client, sample_profil result = invoke(["container", "set", "docs", "--property", "badformat"]) assert result.exit_code != 0 - # ══════════════════════════════════════════════════════════════════════════ # container unset # ══════════════════════════════════════════════════════════════════════════ - class TestContainerUnset: def test_unset_metadata(self, invoke, config_dir, mock_client, sample_profile): @@ -357,12 +335,10 @@ def _post(url, **kwargs): assert any("X-Remove-Container-Meta-env" in k for k in posted_headers) assert any("X-Remove-Container-Meta-owner" in k for k in posted_headers) - # ══════════════════════════════════════════════════════════════════════════ # object unset # ══════════════════════════════════════════════════════════════════════════ - class TestObjectUnset: def test_unset_metadata(self, invoke, config_dir, mock_client, sample_profile): @@ -402,12 +378,10 @@ def _post(url, **kwargs): assert any("X-Remove-Object-Meta-author" in k for k in posted_headers) assert any("X-Remove-Object-Meta-version" in k for k in posted_headers) - # ══════════════════════════════════════════════════════════════════════════ # account-set / account-unset # ══════════════════════════════════════════════════════════════════════════ - class TestAccountSet: def test_account_set(self, invoke, config_dir, mock_client, sample_profile): @@ -423,7 +397,7 @@ def _post(url, **kwargs): mock_client._http.post = _post - result = invoke(["object", "account-set", "--property", "quota=100GB"]) + result = invoke(["object", "account", "set", "--property", "quota=100GB"]) assert result.exit_code == 0 assert "set" in result.output.lower() assert any("X-Account-Meta-quota" in k for k in posted_headers) @@ -433,7 +407,7 @@ def test_account_set_invalid_format(self, invoke, config_dir, mock_client, sampl set_active_profile("p") _setup_mock(mock_client) - result = invoke(["object", "account-set", "--property", "badformat"]) + result = invoke(["object", "account", "set", "--property", "badformat"]) assert result.exit_code != 0 def test_account_unset(self, invoke, config_dir, mock_client, sample_profile): @@ -449,7 +423,7 @@ def _post(url, **kwargs): mock_client._http.post = _post - result = invoke(["object", "account-unset", "--property", "quota"]) + result = invoke(["object", "account", "unset", "--property", "quota"]) assert result.exit_code == 0 assert "removed" in result.output.lower() assert any("X-Remove-Account-Meta-quota" in k for k in posted_headers) @@ -467,17 +441,15 @@ def _post(url, **kwargs): mock_client._http.post = _post - result = invoke(["object", "account-unset", "--property", "quota", "--property", "tier"]) + result = invoke(["object", "account", "unset", "--property", "quota", "--property", "tier"]) assert result.exit_code == 0 assert any("X-Remove-Account-Meta-quota" in k for k in posted_headers) assert any("X-Remove-Account-Meta-tier" in k for k in posted_headers) - # ══════════════════════════════════════════════════════════════════════════ # tree # ══════════════════════════════════════════════════════════════════════════ - class TestObjectTree: def test_tree_account(self, invoke, config_dir, mock_client, sample_profile): @@ -499,12 +471,10 @@ def test_tree_container(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "readme" in result.output - # ══════════════════════════════════════════════════════════════════════════ # _human_size # ══════════════════════════════════════════════════════════════════════════ - class TestHumanSize: def test_bytes(self): @@ -527,19 +497,16 @@ def test_empty(self): from orca_cli.commands.object_store import _human_size assert _human_size("") == "—" - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestObjectHelp: def test_object_help(self, invoke): result = invoke(["object", "--help"]) assert result.exit_code == 0 - for cmd in ("list", "show", "upload", "download", "delete", "unset", "tree", - "account-set", "account-unset"): + for cmd in ("list", "show", "upload", "download", "delete", "unset", "tree"): assert cmd in result.output def test_container_help(self, invoke): diff --git a/tests/test_octavia_admin.py b/tests/test_octavia_admin.py index 2e2d3b0..e01663f 100644 --- a/tests/test_octavia_admin.py +++ b/tests/test_octavia_admin.py @@ -14,12 +14,10 @@ AMPH = "22222222-2222-2222-2222-222222222222" OCTAVIA = "https://octavia.example.com" - def _oct(mock_client): mock_client.load_balancer_url = OCTAVIA return mock_client - # ══════════════════════════════════════════════════════════════════════════ # loadbalancer set / stats-show / status-show # ══════════════════════════════════════════════════════════════════════════ @@ -54,7 +52,6 @@ def test_calls_correct_url(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["loadbalancer", "set", "--help"]).exit_code == 0 - class TestLoadBalancerStatsShow: def test_stats(self, invoke, mock_client): @@ -63,39 +60,37 @@ def test_stats(self, invoke, mock_client): "active_connections": 10, "bytes_in": 1000, "bytes_out": 2000, "request_errors": 0, "total_connections": 100, }} - result = invoke(["loadbalancer", "stats-show", LB]) + result = invoke(["loadbalancer", "stats", "show", LB]) assert result.exit_code == 0 assert "1000" in result.output def test_calls_correct_url(self, invoke, mock_client): _oct(mock_client) mock_client.get.return_value = {"stats": {}} - invoke(["loadbalancer", "stats-show", LB]) + invoke(["loadbalancer", "stats", "show", LB]) url = mock_client.get.call_args[0][0] assert f"/v2/lbaas/loadbalancers/{LB}/stats" in url def test_help(self, invoke): - assert invoke(["loadbalancer", "stats-show", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "stats", "show", "--help"]).exit_code == 0 class TestLoadBalancerStatusShow: def test_status(self, invoke, mock_client): _oct(mock_client) mock_client.get.return_value = {"statuses": {"loadbalancer": {"id": LB}}} - result = invoke(["loadbalancer", "status-show", LB]) + result = invoke(["loadbalancer", "status", "show", LB]) assert result.exit_code == 0 def test_calls_correct_url(self, invoke, mock_client): _oct(mock_client) mock_client.get.return_value = {"statuses": {}} - invoke(["loadbalancer", "status-show", LB]) + invoke(["loadbalancer", "status", "show", LB]) url = mock_client.get.call_args[0][0] assert f"/v2/lbaas/loadbalancers/{LB}/statuses" in url def test_help(self, invoke): - assert invoke(["loadbalancer", "status-show", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "status", "show", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # listener-set @@ -105,26 +100,25 @@ class TestListenerSet: def test_set_name(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "listener-set", LIST, "--name", "new-name"]) + result = invoke(["loadbalancer", "listener", "set", LIST, "--name", "new-name"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"]["listener"] assert body["name"] == "new-name" def test_set_connection_limit(self, invoke, mock_client): _oct(mock_client) - invoke(["loadbalancer", "listener-set", LIST, "--connection-limit", "1000"]) + invoke(["loadbalancer", "listener", "set", LIST, "--connection-limit", "1000"]) body = mock_client.put.call_args[1]["json"]["listener"] assert body["connection_limit"] == 1000 def test_set_nothing(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "listener-set", LIST]) + result = invoke(["loadbalancer", "listener", "set", LIST]) assert result.exit_code == 0 mock_client.put.assert_not_called() def test_help(self, invoke): - assert invoke(["loadbalancer", "listener-set", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "listener", "set", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # pool-set @@ -134,7 +128,7 @@ class TestPoolSet: def test_set_algorithm(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "pool-set", POOL, + result = invoke(["loadbalancer", "pool", "set", POOL, "--algorithm", "LEAST_CONNECTIONS"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"]["pool"] @@ -142,13 +136,12 @@ def test_set_algorithm(self, invoke, mock_client): def test_set_nothing(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "pool-set", POOL]) + result = invoke(["loadbalancer", "pool", "set", POOL]) assert result.exit_code == 0 mock_client.put.assert_not_called() def test_help(self, invoke): - assert invoke(["loadbalancer", "pool-set", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "pool", "set", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # member-show / member-set @@ -164,45 +157,43 @@ def test_show(self, invoke, mock_client): "provisioning_status": "ACTIVE", "admin_state_up": True, "subnet_id": "", "name": "", "created_at": "", "updated_at": "", }} - result = invoke(["loadbalancer", "member-show", POOL, MEMBER]) + result = invoke(["loadbalancer", "member", "show", POOL, MEMBER]) assert result.exit_code == 0 assert "ONLINE" in result.output def test_calls_correct_url(self, invoke, mock_client): _oct(mock_client) mock_client.get.return_value = {"member": {}} - invoke(["loadbalancer", "member-show", POOL, MEMBER]) + invoke(["loadbalancer", "member", "show", POOL, MEMBER]) url = mock_client.get.call_args[0][0] assert f"/v2/lbaas/pools/{POOL}/members/{MEMBER}" in url def test_help(self, invoke): - assert invoke(["loadbalancer", "member-show", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "member", "show", "--help"]).exit_code == 0 class TestMemberSet: def test_set_weight(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "member-set", POOL, MEMBER, "--weight", "5"]) + result = invoke(["loadbalancer", "member", "set", POOL, MEMBER, "--weight", "5"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"]["member"] assert body["weight"] == 5 def test_set_nothing(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "member-set", POOL, MEMBER]) + result = invoke(["loadbalancer", "member", "set", POOL, MEMBER]) assert result.exit_code == 0 mock_client.put.assert_not_called() def test_calls_correct_url(self, invoke, mock_client): _oct(mock_client) - invoke(["loadbalancer", "member-set", POOL, MEMBER, "--weight", "3"]) + invoke(["loadbalancer", "member", "set", POOL, MEMBER, "--weight", "3"]) url = mock_client.put.call_args[0][0] assert f"/v2/lbaas/pools/{POOL}/members/{MEMBER}" in url def test_help(self, invoke): - assert invoke(["loadbalancer", "member-set", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "member", "set", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # healthmonitor-show / healthmonitor-set @@ -219,39 +210,37 @@ def test_show(self, invoke, mock_client): "provisioning_status": "ACTIVE", "admin_state_up": True, "created_at": "", "updated_at": "", }} - result = invoke(["loadbalancer", "healthmonitor-show", HM]) + result = invoke(["loadbalancer", "healthmonitor", "show", HM]) assert result.exit_code == 0 assert "HTTP" in result.output def test_calls_correct_url(self, invoke, mock_client): _oct(mock_client) mock_client.get.return_value = {"healthmonitor": {}} - invoke(["loadbalancer", "healthmonitor-show", HM]) + invoke(["loadbalancer", "healthmonitor", "show", HM]) url = mock_client.get.call_args[0][0] assert f"/v2/lbaas/healthmonitors/{HM}" in url def test_help(self, invoke): - assert invoke(["loadbalancer", "healthmonitor-show", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "healthmonitor", "show", "--help"]).exit_code == 0 class TestHealthMonitorSet: def test_set_delay(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "healthmonitor-set", HM, "--delay", "10"]) + result = invoke(["loadbalancer", "healthmonitor", "set", HM, "--delay", "10"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"]["healthmonitor"] assert body["delay"] == 10 def test_set_nothing(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "healthmonitor-set", HM]) + result = invoke(["loadbalancer", "healthmonitor", "set", HM]) assert result.exit_code == 0 mock_client.put.assert_not_called() def test_help(self, invoke): - assert invoke(["loadbalancer", "healthmonitor-set", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "healthmonitor", "set", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # l7policy CRUD @@ -267,19 +256,18 @@ def _pol(self, **kw): def test_list(self, invoke, mock_client): _oct(mock_client) mock_client.get.return_value = {"l7policies": [self._pol()]} - result = invoke(["loadbalancer", "l7policy-list"]) + result = invoke(["loadbalancer", "l7policy", "list"]) assert result.exit_code == 0 assert "REJE" in result.output def test_list_empty(self, invoke, mock_client): _oct(mock_client) mock_client.get.return_value = {"l7policies": []} - result = invoke(["loadbalancer", "l7policy-list"]) + result = invoke(["loadbalancer", "l7policy", "list"]) assert "No L7" in result.output def test_help(self, invoke): - assert invoke(["loadbalancer", "l7policy-list", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "l7policy", "list", "--help"]).exit_code == 0 class TestL7PolicyShow: @@ -291,20 +279,19 @@ def test_show(self, invoke, mock_client): "redirect_prefix": "", "position": 1, "provisioning_status": "ACTIVE", "admin_state_up": True, "created_at": "", }} - result = invoke(["loadbalancer", "l7policy-show", L7POL]) + result = invoke(["loadbalancer", "l7policy", "show", L7POL]) assert result.exit_code == 0 assert "REJECT" in result.output def test_help(self, invoke): - assert invoke(["loadbalancer", "l7policy-show", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "l7policy", "show", "--help"]).exit_code == 0 class TestL7PolicyCreate: def test_create_reject(self, invoke, mock_client): _oct(mock_client) mock_client.post.return_value = {"l7policy": {"id": L7POL}} - result = invoke(["loadbalancer", "l7policy-create", + result = invoke(["loadbalancer", "l7policy", "create", "--listener-id", LIST, "--action", "REJECT"]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"]["l7policy"] @@ -314,7 +301,7 @@ def test_create_reject(self, invoke, mock_client): def test_create_redirect_pool(self, invoke, mock_client): _oct(mock_client) mock_client.post.return_value = {"l7policy": {"id": L7POL}} - invoke(["loadbalancer", "l7policy-create", + invoke(["loadbalancer", "l7policy", "create", "--listener-id", LIST, "--action", "REDIRECT_TO_POOL", "--redirect-pool-id", POOL]) @@ -324,51 +311,48 @@ def test_create_redirect_pool(self, invoke, mock_client): def test_calls_correct_url(self, invoke, mock_client): _oct(mock_client) mock_client.post.return_value = {"l7policy": {"id": L7POL}} - invoke(["loadbalancer", "l7policy-create", + invoke(["loadbalancer", "l7policy", "create", "--listener-id", LIST, "--action", "REJECT"]) url = mock_client.post.call_args[0][0] assert "/v2/lbaas/l7policies" in url def test_help(self, invoke): - assert invoke(["loadbalancer", "l7policy-create", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "l7policy", "create", "--help"]).exit_code == 0 class TestL7PolicySet: def test_set_action(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "l7policy-set", L7POL, "--action", "REJECT"]) + result = invoke(["loadbalancer", "l7policy", "set", L7POL, "--action", "REJECT"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"]["l7policy"] assert body["action"] == "REJECT" def test_set_nothing(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "l7policy-set", L7POL]) + result = invoke(["loadbalancer", "l7policy", "set", L7POL]) assert result.exit_code == 0 mock_client.put.assert_not_called() def test_help(self, invoke): - assert invoke(["loadbalancer", "l7policy-set", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "l7policy", "set", "--help"]).exit_code == 0 class TestL7PolicyDelete: def test_delete_yes(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "l7policy-delete", L7POL, "--yes"]) + result = invoke(["loadbalancer", "l7policy", "delete", L7POL, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "l7policy-delete", L7POL], input="n\n") + result = invoke(["loadbalancer", "l7policy", "delete", L7POL], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help(self, invoke): - assert invoke(["loadbalancer", "l7policy-delete", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "l7policy", "delete", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # l7rule CRUD @@ -382,20 +366,19 @@ def test_list(self, invoke, mock_client): {"id": L7RUL, "type": "PATH", "compare_type": "STARTS_WITH", "value": "/api", "invert": False, "provisioning_status": "ACTIVE"}, ]} - result = invoke(["loadbalancer", "l7rule-list", L7POL]) + result = invoke(["loadbalancer", "l7rule", "list", L7POL]) assert result.exit_code == 0 assert "PATH" in result.output def test_list_calls_correct_url(self, invoke, mock_client): _oct(mock_client) mock_client.get.return_value = {"rules": []} - invoke(["loadbalancer", "l7rule-list", L7POL]) + invoke(["loadbalancer", "l7rule", "list", L7POL]) url = mock_client.get.call_args[0][0] assert f"/v2/lbaas/l7policies/{L7POL}/rules" in url def test_help(self, invoke): - assert invoke(["loadbalancer", "l7rule-list", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "l7rule", "list", "--help"]).exit_code == 0 class TestL7RuleShow: @@ -406,20 +389,19 @@ def test_show(self, invoke, mock_client): "key": "", "value": "/api", "invert": False, "provisioning_status": "ACTIVE", "admin_state_up": True, "created_at": "", }} - result = invoke(["loadbalancer", "l7rule-show", L7POL, L7RUL]) + result = invoke(["loadbalancer", "l7rule", "show", L7POL, L7RUL]) assert result.exit_code == 0 assert "PATH" in result.output def test_help(self, invoke): - assert invoke(["loadbalancer", "l7rule-show", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "l7rule", "show", "--help"]).exit_code == 0 class TestL7RuleCreate: def test_create(self, invoke, mock_client): _oct(mock_client) mock_client.post.return_value = {"rule": {"id": L7RUL}} - result = invoke(["loadbalancer", "l7rule-create", L7POL, + result = invoke(["loadbalancer", "l7rule", "create", L7POL, "--type", "PATH", "--compare-type", "STARTS_WITH", "--value", "/api"]) @@ -431,7 +413,7 @@ def test_create(self, invoke, mock_client): def test_create_with_invert(self, invoke, mock_client): _oct(mock_client) mock_client.post.return_value = {"rule": {"id": L7RUL}} - invoke(["loadbalancer", "l7rule-create", L7POL, + invoke(["loadbalancer", "l7rule", "create", L7POL, "--type", "HOST_NAME", "--compare-type", "EQUAL_TO", "--value", "example.com", @@ -442,57 +424,54 @@ def test_create_with_invert(self, invoke, mock_client): def test_calls_correct_url(self, invoke, mock_client): _oct(mock_client) mock_client.post.return_value = {"rule": {"id": L7RUL}} - invoke(["loadbalancer", "l7rule-create", L7POL, + invoke(["loadbalancer", "l7rule", "create", L7POL, "--type", "PATH", "--compare-type", "REGEX", "--value", ".*"]) url = mock_client.post.call_args[0][0] assert f"/v2/lbaas/l7policies/{L7POL}/rules" in url def test_help(self, invoke): - assert invoke(["loadbalancer", "l7rule-create", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "l7rule", "create", "--help"]).exit_code == 0 class TestL7RuleSet: def test_set_value(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "l7rule-set", L7POL, L7RUL, "--value", "/new"]) + result = invoke(["loadbalancer", "l7rule", "set", L7POL, L7RUL, "--value", "/new"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"]["rule"] assert body["value"] == "/new" def test_set_nothing(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "l7rule-set", L7POL, L7RUL]) + result = invoke(["loadbalancer", "l7rule", "set", L7POL, L7RUL]) assert result.exit_code == 0 mock_client.put.assert_not_called() def test_calls_correct_url(self, invoke, mock_client): _oct(mock_client) - invoke(["loadbalancer", "l7rule-set", L7POL, L7RUL, "--value", "/x"]) + invoke(["loadbalancer", "l7rule", "set", L7POL, L7RUL, "--value", "/x"]) url = mock_client.put.call_args[0][0] assert f"/v2/lbaas/l7policies/{L7POL}/rules/{L7RUL}" in url def test_help(self, invoke): - assert invoke(["loadbalancer", "l7rule-set", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "l7rule", "set", "--help"]).exit_code == 0 class TestL7RuleDelete: def test_delete_yes(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "l7rule-delete", L7POL, L7RUL, "--yes"]) + result = invoke(["loadbalancer", "l7rule", "delete", L7POL, L7RUL, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "l7rule-delete", L7POL, L7RUL], input="n\n") + result = invoke(["loadbalancer", "l7rule", "delete", L7POL, L7RUL], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help(self, invoke): - assert invoke(["loadbalancer", "l7rule-delete", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "l7rule", "delete", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # amphora @@ -507,25 +486,24 @@ def _amph(self, **kw): def test_list(self, invoke, mock_client): _oct(mock_client) mock_client.get.return_value = {"amphorae": [self._amph()]} - result = invoke(["loadbalancer", "amphora-list"]) + result = invoke(["loadbalancer", "amphora", "list"]) assert result.exit_code == 0 assert "MAST" in result.output def test_list_filter_lb(self, invoke, mock_client): _oct(mock_client) mock_client.get.return_value = {"amphorae": []} - invoke(["loadbalancer", "amphora-list", "--lb-id", LB]) + invoke(["loadbalancer", "amphora", "list", "--lb-id", LB]) assert mock_client.get.call_args[1]["params"]["loadbalancer_id"] == LB def test_list_empty(self, invoke, mock_client): _oct(mock_client) mock_client.get.return_value = {"amphorae": []} - result = invoke(["loadbalancer", "amphora-list"]) + result = invoke(["loadbalancer", "amphora", "list"]) assert "No amphorae" in result.output def test_help(self, invoke): - assert invoke(["loadbalancer", "amphora-list", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "amphora", "list", "--help"]).exit_code == 0 class TestAmphoraShow: @@ -537,39 +515,37 @@ def test_show(self, invoke, mock_client): "ha_ip": "", "ha_port_id": "", "vrrp_ip": "", "vrrp_interface": "", "vrrp_priority": 100, "cert_expiration": "", "created_at": "", "updated_at": "", }} - result = invoke(["loadbalancer", "amphora-show", AMPH]) + result = invoke(["loadbalancer", "amphora", "show", AMPH]) assert result.exit_code == 0 assert "MASTER" in result.output def test_calls_correct_url(self, invoke, mock_client): _oct(mock_client) mock_client.get.return_value = {"amphora": {}} - invoke(["loadbalancer", "amphora-show", AMPH]) + invoke(["loadbalancer", "amphora", "show", AMPH]) url = mock_client.get.call_args[0][0] assert f"/v2/octavia/amphorae/{AMPH}" in url def test_help(self, invoke): - assert invoke(["loadbalancer", "amphora-show", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "amphora", "show", "--help"]).exit_code == 0 class TestAmphoraFailover: def test_failover_yes(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "amphora-failover", AMPH, "--yes"]) + result = invoke(["loadbalancer", "amphora", "failover", AMPH, "--yes"]) assert result.exit_code == 0 url = mock_client.put.call_args[0][0] assert f"/v2/octavia/amphorae/{AMPH}/failover" in url def test_failover_requires_confirm(self, invoke, mock_client): _oct(mock_client) - result = invoke(["loadbalancer", "amphora-failover", AMPH], input="n\n") + result = invoke(["loadbalancer", "amphora", "failover", AMPH], input="n\n") assert result.exit_code != 0 mock_client.put.assert_not_called() def test_help(self, invoke): - assert invoke(["loadbalancer", "amphora-failover", "--help"]).exit_code == 0 - + assert invoke(["loadbalancer", "amphora", "failover", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════ # Registration @@ -578,16 +554,7 @@ def test_help(self, invoke): class TestRegistration: @pytest.mark.parametrize("sub", [ - "set", "stats-show", "status-show", - "listener-set", - "pool-set", - "member-show", "member-set", - "healthmonitor-show", "healthmonitor-set", - "l7policy-list", "l7policy-show", "l7policy-create", - "l7policy-set", "l7policy-delete", - "l7rule-list", "l7rule-show", "l7rule-create", - "l7rule-set", "l7rule-delete", - "amphora-list", "amphora-show", "amphora-failover", + "set", ]) def test_subcommand_help(self, invoke, sub): result = invoke(["loadbalancer", sub, "--help"]) diff --git a/tests/test_output.py b/tests/test_output.py index c8743ae..04b51a4 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -21,7 +21,6 @@ def _wrap(): result = runner.invoke(_wrap, [], catch_exceptions=False) return result.output - class TestPrintListEmpty: def test_empty_list_table_mode_shows_yellow_msg(self): @@ -37,7 +36,6 @@ def test_empty_list_value_mode_prints_empty_msg(self): out = _capture(print_list, [], [("ID", "id")], output_format="value") assert "No results found." in out - class TestPrintListJson: def test_json_indent_by_default(self): @@ -53,7 +51,6 @@ def test_json_noindent_compact(self): assert "\n" not in out.strip() assert json.loads(out) == [{"ID": "1"}] - class TestPrintListValue: def test_value_mode_space_separated(self): @@ -64,7 +61,6 @@ def test_value_mode_space_separated(self): lines = out.strip().split("\n") assert lines == ["1 foo", "2 bar"] - class TestPrintListColumnFilter: def test_columns_filter_case_insensitive(self): @@ -79,7 +75,6 @@ def test_columns_filter_case_insensitive(self): data = json.loads(out) assert data == [{"Name": "foo", "Status": "ACTIVE"}] - class TestPrintListTable: def test_fit_width_renders(self): @@ -101,7 +96,6 @@ def test_max_width_zero_unlimited(self): out = _capture(print_list, items, [("ID", "id")], max_width=0) assert "1" in out - class TestPrintDetail: def test_json_mode(self): diff --git a/tests/test_overview.py b/tests/test_overview.py index f928b13..6fd11d5 100644 --- a/tests/test_overview.py +++ b/tests/test_overview.py @@ -6,7 +6,6 @@ # ── Helpers ──────────────────────────────────────────────────────────────── - def _setup_mock(mock_client, servers=None, volumes=None, fips=None, nets=None, subnets=None, routers=None, sgs=None, kps=None, images=None): @@ -48,12 +47,10 @@ def _get(url, **kwargs): mock_client.get = _get - # ══════════════════════════════════════════════════════════════════════════ # overview # ══════════════════════════════════════════════════════════════════════════ - class TestOverview: def test_overview_basic(self, invoke, config_dir, mock_client, sample_profile): @@ -134,12 +131,10 @@ def test_overview_volume_size(self, invoke, config_dir, mock_client, sample_prof result = invoke(["overview"]) assert "200 GB" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestOverviewHelp: def test_overview_help(self, invoke): diff --git a/tests/test_placement.py b/tests/test_placement.py index 67cf277..7a512ea 100644 --- a/tests/test_placement.py +++ b/tests/test_placement.py @@ -11,12 +11,10 @@ CONSUMER = "cccccccc-cccc-cccc-cccc-cccccccccccc" BASE = "https://placement.example.com" - def _placement(mc): mc.placement_url = BASE return mc - def _rp(**kw): return { "uuid": RP_UUID, "name": "compute-node-1", "generation": 0, @@ -24,7 +22,6 @@ def _rp(**kw): **kw, } - # ══════════════════════════════════════════════════════════════════════════════ # Resource Providers # ══════════════════════════════════════════════════════════════════════════════ @@ -34,51 +31,49 @@ class TestResourceProviderList: def test_list(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"resource_providers": [_rp()]} - result = invoke(["placement", "resource-provider-list"]) + result = invoke(["placement", "resource-provider", "list"]) assert result.exit_code == 0 assert "compute" in result.output def test_list_empty(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"resource_providers": []} - result = invoke(["placement", "resource-provider-list"]) + result = invoke(["placement", "resource-provider", "list"]) assert "No resource providers" in result.output def test_filter_name(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"resource_providers": []} - invoke(["placement", "resource-provider-list", "--name", "compute"]) + invoke(["placement", "resource-provider", "list", "--name", "compute"]) assert mock_client.get.call_args[1]["params"]["name"] == "compute" def test_filter_in_tree(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"resource_providers": []} - invoke(["placement", "resource-provider-list", "--in-tree", RP_UUID]) + invoke(["placement", "resource-provider", "list", "--in-tree", RP_UUID]) assert mock_client.get.call_args[1]["params"]["in_tree"] == RP_UUID def test_help(self, invoke): - assert invoke(["placement", "resource-provider-list", "--help"]).exit_code == 0 - + assert invoke(["placement", "resource-provider", "list", "--help"]).exit_code == 0 class TestResourceProviderShow: def test_show(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = _rp() - result = invoke(["placement", "resource-provider-show", RP_UUID]) + result = invoke(["placement", "resource-provider", "show", RP_UUID]) assert result.exit_code == 0 assert "compute" in result.output def test_help(self, invoke): - assert invoke(["placement", "resource-provider-show", "--help"]).exit_code == 0 - + assert invoke(["placement", "resource-provider", "show", "--help"]).exit_code == 0 class TestResourceProviderCreate: def test_create(self, invoke, mock_client): _placement(mock_client) mock_client.post.return_value = _rp(name="new-rp") - result = invoke(["placement", "resource-provider-create", "new-rp"]) + result = invoke(["placement", "resource-provider", "create", "new-rp"]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"] assert body["name"] == "new-rp" @@ -86,21 +81,20 @@ def test_create(self, invoke, mock_client): def test_create_with_parent(self, invoke, mock_client): _placement(mock_client) mock_client.post.return_value = _rp(parent_provider_uuid=RP2_UUID) - result = invoke(["placement", "resource-provider-create", "child-rp", + result = invoke(["placement", "resource-provider", "create", "child-rp", "--parent-uuid", RP2_UUID]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"] assert body["parent_provider_uuid"] == RP2_UUID def test_help(self, invoke): - assert invoke(["placement", "resource-provider-create", "--help"]).exit_code == 0 - + assert invoke(["placement", "resource-provider", "create", "--help"]).exit_code == 0 class TestResourceProviderSet: def test_set_name(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "resource-provider-set", RP_UUID, + result = invoke(["placement", "resource-provider", "set", RP_UUID, "--name", "renamed"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"] @@ -108,31 +102,29 @@ def test_set_name(self, invoke, mock_client): def test_set_nothing(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "resource-provider-set", RP_UUID]) + result = invoke(["placement", "resource-provider", "set", RP_UUID]) assert result.exit_code == 0 mock_client.put.assert_not_called() def test_help(self, invoke): - assert invoke(["placement", "resource-provider-set", "--help"]).exit_code == 0 - + assert invoke(["placement", "resource-provider", "set", "--help"]).exit_code == 0 class TestResourceProviderDelete: def test_delete_yes(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "resource-provider-delete", RP_UUID, "--yes"]) + result = invoke(["placement", "resource-provider", "delete", RP_UUID, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "resource-provider-delete", RP_UUID], input="n\n") + result = invoke(["placement", "resource-provider", "delete", RP_UUID], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help(self, invoke): - assert invoke(["placement", "resource-provider-delete", "--help"]).exit_code == 0 - + assert invoke(["placement", "resource-provider", "delete", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════════ # Inventories @@ -149,20 +141,20 @@ def test_list(self, invoke, mock_client): }, "resource_provider_generation": 1, } - result = invoke(["placement", "resource-provider-inventory-list", RP_UUID]) + result = invoke(["placement", "resource-provider", "inventory-list", RP_UUID]) assert result.exit_code == 0 assert "VCPU" in result.output def test_list_empty(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"inventories": {}, "resource_provider_generation": 0} - result = invoke(["placement", "resource-provider-inventory-list", RP_UUID]) + result = invoke(["placement", "resource-provider", "inventory-list", RP_UUID]) assert "No inventories" in result.output def test_set(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = _rp(generation=2) - result = invoke(["placement", "resource-provider-inventory-set", RP_UUID, "VCPU", + result = invoke(["placement", "resource-provider", "inventory-set", RP_UUID, "VCPU", "--total", "64"]) assert result.exit_code == 0 # New behaviour (post 1.20-mv fix): POST /inventories first to @@ -174,24 +166,23 @@ def test_set(self, invoke, mock_client): def test_delete_yes(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "resource-provider-inventory-delete", + result = invoke(["placement", "resource-provider", "inventory-delete", RP_UUID, "VCPU", "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "resource-provider-inventory-delete", + result = invoke(["placement", "resource-provider", "inventory-delete", RP_UUID, "VCPU"], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help_list(self, invoke): - assert invoke(["placement", "resource-provider-inventory-list", "--help"]).exit_code == 0 + assert invoke(["placement", "resource-provider", "inventory-list", "--help"]).exit_code == 0 def test_help_set(self, invoke): - assert invoke(["placement", "resource-provider-inventory-set", "--help"]).exit_code == 0 - + assert invoke(["placement", "resource-provider", "inventory-set", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════════ # Usages @@ -202,37 +193,36 @@ class TestUsage: def test_rp_usage(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"usages": {"VCPU": 8, "MEMORY_MB": 2048}} - result = invoke(["placement", "resource-provider-usage", RP_UUID]) + result = invoke(["placement", "resource-provider", "usage", RP_UUID]) assert result.exit_code == 0 assert "VCPU" in result.output def test_rp_usage_empty(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"usages": {}} - result = invoke(["placement", "resource-provider-usage", RP_UUID]) + result = invoke(["placement", "resource-provider", "usage", RP_UUID]) assert "No usages" in result.output def test_usage_list(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"usages": {"VCPU": 16}} - result = invoke(["placement", "usage-list", "--project-id", "proj-1"]) + result = invoke(["placement", "usage", "list", "--project-id", "proj-1"]) assert result.exit_code == 0 assert "VCPU" in result.output def test_usage_list_filters(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"usages": {}} - invoke(["placement", "usage-list", "--project-id", "p1", "--user-id", "u1"]) + invoke(["placement", "usage", "list", "--project-id", "p1", "--user-id", "u1"]) params = mock_client.get.call_args[1]["params"] assert params["project_id"] == "p1" assert params["user_id"] == "u1" def test_help_rp_usage(self, invoke): - assert invoke(["placement", "resource-provider-usage", "--help"]).exit_code == 0 + assert invoke(["placement", "resource-provider", "usage", "--help"]).exit_code == 0 def test_help_usage_list(self, invoke): - assert invoke(["placement", "usage-list", "--help"]).exit_code == 0 - + assert invoke(["placement", "usage", "list", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════════ # Resource Classes @@ -245,26 +235,26 @@ def test_list(self, invoke, mock_client): mock_client.get.return_value = { "resource_classes": [{"name": "VCPU"}, {"name": "MEMORY_MB"}, {"name": "DISK_GB"}] } - result = invoke(["placement", "resource-class-list"]) + result = invoke(["placement", "resource-class", "list"]) assert result.exit_code == 0 assert "VCPU" in result.output def test_list_empty(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"resource_classes": []} - result = invoke(["placement", "resource-class-list"]) + result = invoke(["placement", "resource-class", "list"]) assert "No resource classes" in result.output def test_show(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {} - result = invoke(["placement", "resource-class-show", "VCPU"]) + result = invoke(["placement", "resource-class", "show", "VCPU"]) assert result.exit_code == 0 assert "VCPU" in result.output def test_create(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "resource-class-create", "CUSTOM_GPU"]) + result = invoke(["placement", "resource-class", "create", "CUSTOM_GPU"]) assert result.exit_code == 0 # Fixed: POST /resource_classes with {"name": "..."} instead of # PUT /resource_classes/{name} with empty body. @@ -273,22 +263,21 @@ def test_create(self, invoke, mock_client): def test_delete_yes(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "resource-class-delete", "CUSTOM_GPU", "--yes"]) + result = invoke(["placement", "resource-class", "delete", "CUSTOM_GPU", "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "resource-class-delete", "CUSTOM_GPU"], input="n\n") + result = invoke(["placement", "resource-class", "delete", "CUSTOM_GPU"], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help_list(self, invoke): - assert invoke(["placement", "resource-class-list", "--help"]).exit_code == 0 + assert invoke(["placement", "resource-class", "list", "--help"]).exit_code == 0 def test_help_create(self, invoke): - assert invoke(["placement", "resource-class-create", "--help"]).exit_code == 0 - + assert invoke(["placement", "resource-class", "create", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════════ # Traits @@ -301,53 +290,52 @@ def test_list(self, invoke, mock_client): mock_client.get.return_value = { "traits": ["HW_CPU_X86_AVX2", "CUSTOM_HPC_ENABLED"] } - result = invoke(["placement", "trait-list"]) + result = invoke(["placement", "trait", "list"]) assert result.exit_code == 0 assert "HW_CPU" in result.output def test_list_empty(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"traits": []} - result = invoke(["placement", "trait-list"]) + result = invoke(["placement", "trait", "list"]) assert "No traits" in result.output def test_list_filter(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"traits": []} - invoke(["placement", "trait-list", "--name", "CUSTOM"]) + invoke(["placement", "trait", "list", "--name", "CUSTOM"]) assert mock_client.get.call_args[1]["params"]["name"] == "CUSTOM" def test_list_associated(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"traits": []} - invoke(["placement", "trait-list", "--associated"]) + invoke(["placement", "trait", "list", "--associated"]) assert mock_client.get.call_args[1]["params"]["associated"] == "true" def test_create(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "trait-create", "CUSTOM_HPC_ENABLED"]) + result = invoke(["placement", "trait", "create", "CUSTOM_HPC_ENABLED"]) assert result.exit_code == 0 url = mock_client.put.call_args[0][0] assert "CUSTOM_HPC_ENABLED" in url def test_delete_yes(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "trait-delete", "CUSTOM_HPC_ENABLED", "--yes"]) + result = invoke(["placement", "trait", "delete", "CUSTOM_HPC_ENABLED", "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "trait-delete", "CUSTOM_HPC_ENABLED"], input="n\n") + result = invoke(["placement", "trait", "delete", "CUSTOM_HPC_ENABLED"], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help_list(self, invoke): - assert invoke(["placement", "trait-list", "--help"]).exit_code == 0 + assert invoke(["placement", "trait", "list", "--help"]).exit_code == 0 def test_help_create(self, invoke): - assert invoke(["placement", "trait-create", "--help"]).exit_code == 0 - + assert invoke(["placement", "trait", "create", "--help"]).exit_code == 0 class TestResourceProviderTrait: @@ -357,20 +345,20 @@ def test_list(self, invoke, mock_client): "traits": ["CUSTOM_HPC_ENABLED"], "resource_provider_generation": 1, } - result = invoke(["placement", "resource-provider-trait-list", RP_UUID]) + result = invoke(["placement", "resource-provider", "trait-list", RP_UUID]) assert result.exit_code == 0 assert "CUSTOM" in result.output def test_list_empty(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"traits": [], "resource_provider_generation": 0} - result = invoke(["placement", "resource-provider-trait-list", RP_UUID]) + result = invoke(["placement", "resource-provider", "trait-list", RP_UUID]) assert "No traits" in result.output def test_set(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = _rp(generation=3) - result = invoke(["placement", "resource-provider-trait-set", RP_UUID, + result = invoke(["placement", "resource-provider", "trait-set", RP_UUID, "CUSTOM_HPC_ENABLED", "HW_CPU_X86_AVX2"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"] @@ -379,22 +367,21 @@ def test_set(self, invoke, mock_client): def test_delete_yes(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "resource-provider-trait-delete", RP_UUID, "--yes"]) + result = invoke(["placement", "resource-provider", "trait-delete", RP_UUID, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "resource-provider-trait-delete", RP_UUID], input="n\n") + result = invoke(["placement", "resource-provider", "trait-delete", RP_UUID], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help_list(self, invoke): - assert invoke(["placement", "resource-provider-trait-list", "--help"]).exit_code == 0 + assert invoke(["placement", "resource-provider", "trait-list", "--help"]).exit_code == 0 def test_help_set(self, invoke): - assert invoke(["placement", "resource-provider-trait-set", "--help"]).exit_code == 0 - + assert invoke(["placement", "resource-provider", "trait-set", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════════ # Allocations @@ -411,31 +398,31 @@ def test_show(self, invoke, mock_client): "project_id": "proj-1", "user_id": "user-1", } - result = invoke(["placement", "allocation-show", CONSUMER]) + result = invoke(["placement", "allocation", "show", CONSUMER]) assert result.exit_code == 0 assert "VCPU" in result.output def test_show_empty(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"allocations": {}} - result = invoke(["placement", "allocation-show", CONSUMER]) + result = invoke(["placement", "allocation", "show", CONSUMER]) assert "No allocations" in result.output def test_delete_yes(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "allocation-delete", CONSUMER, "--yes"]) + result = invoke(["placement", "allocation", "delete", CONSUMER, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_requires_confirm(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "allocation-delete", CONSUMER], input="n\n") + result = invoke(["placement", "allocation", "delete", CONSUMER], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_set(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "allocation-set", CONSUMER, + result = invoke(["placement", "allocation", "set", CONSUMER, "--resource-provider", RP_UUID, "--resource", "VCPU=4", "--resource", "MEMORY_MB=1024", @@ -448,7 +435,7 @@ def test_set(self, invoke, mock_client): def test_set_invalid_resource(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "allocation-set", CONSUMER, + result = invoke(["placement", "allocation", "set", CONSUMER, "--resource-provider", RP_UUID, "--resource", "BADFORMAT", "--project-id", "proj-1", @@ -456,14 +443,13 @@ def test_set_invalid_resource(self, invoke, mock_client): assert result.exit_code != 0 def test_help_show(self, invoke): - assert invoke(["placement", "allocation-show", "--help"]).exit_code == 0 + assert invoke(["placement", "allocation", "show", "--help"]).exit_code == 0 def test_help_delete(self, invoke): - assert invoke(["placement", "allocation-delete", "--help"]).exit_code == 0 + assert invoke(["placement", "allocation", "delete", "--help"]).exit_code == 0 def test_help_set(self, invoke): - assert invoke(["placement", "allocation-set", "--help"]).exit_code == 0 - + assert invoke(["placement", "allocation", "set", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════════ # Allocation Candidates @@ -484,7 +470,7 @@ def test_list(self, invoke, mock_client): "allocation_requests": [self._candidate()], "provider_summaries": {}, } - result = invoke(["placement", "allocation-candidate-list", + result = invoke(["placement", "allocation", "candidate-list", "--resource", "VCPU=4"]) assert result.exit_code == 0 assert "VCPU" in result.output @@ -492,14 +478,14 @@ def test_list(self, invoke, mock_client): def test_list_empty(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"allocation_requests": [], "provider_summaries": {}} - result = invoke(["placement", "allocation-candidate-list", + result = invoke(["placement", "allocation", "candidate-list", "--resource", "VCPU=4"]) assert "No allocation candidates" in result.output def test_list_with_traits(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"allocation_requests": [], "provider_summaries": {}} - invoke(["placement", "allocation-candidate-list", + invoke(["placement", "allocation", "candidate-list", "--resource", "VCPU=4", "--required", "HW_CPU_X86_AVX2", "--forbidden", "CUSTOM_NO_LIVE_MIGRATION"]) @@ -508,8 +494,7 @@ def test_list_with_traits(self, invoke, mock_client): assert "!CUSTOM_NO_LIVE_MIGRATION" in params["forbidden"] def test_help(self, invoke): - assert invoke(["placement", "allocation-candidate-list", "--help"]).exit_code == 0 - + assert invoke(["placement", "allocation", "candidate-list", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════════ # Resource Provider Aggregates @@ -517,7 +502,6 @@ def test_help(self, invoke): AGG_UUID = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee" - class TestResourceProviderAggregate: def test_list(self, invoke, mock_client): @@ -526,20 +510,20 @@ def test_list(self, invoke, mock_client): "aggregates": [AGG_UUID], "resource_provider_generation": 1, } - result = invoke(["placement", "resource-provider-aggregate-list", RP_UUID]) + result = invoke(["placement", "resource-provider", "aggregate-list", RP_UUID]) assert result.exit_code == 0 assert "eeee" in result.output def test_list_empty(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = {"aggregates": [], "resource_provider_generation": 0} - result = invoke(["placement", "resource-provider-aggregate-list", RP_UUID]) + result = invoke(["placement", "resource-provider", "aggregate-list", RP_UUID]) assert "No aggregates" in result.output def test_set(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = _rp(generation=1) - result = invoke(["placement", "resource-provider-aggregate-set", RP_UUID, AGG_UUID]) + result = invoke(["placement", "resource-provider", "aggregate-set", RP_UUID, AGG_UUID]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"] assert AGG_UUID in body["aggregates"] @@ -548,23 +532,22 @@ def test_set(self, invoke, mock_client): def test_delete_yes(self, invoke, mock_client): _placement(mock_client) mock_client.get.return_value = _rp(generation=2) - result = invoke(["placement", "resource-provider-aggregate-delete", RP_UUID, "--yes"]) + result = invoke(["placement", "resource-provider", "aggregate-delete", RP_UUID, "--yes"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"] assert body["aggregates"] == [] def test_delete_requires_confirm(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "resource-provider-aggregate-delete", RP_UUID], + result = invoke(["placement", "resource-provider", "aggregate-delete", RP_UUID], input="n\n") assert result.exit_code != 0 def test_help_list(self, invoke): - assert invoke(["placement", "resource-provider-aggregate-list", "--help"]).exit_code == 0 + assert invoke(["placement", "resource-provider", "aggregate-list", "--help"]).exit_code == 0 def test_help_set(self, invoke): - assert invoke(["placement", "resource-provider-aggregate-set", "--help"]).exit_code == 0 - + assert invoke(["placement", "resource-provider", "aggregate-set", "--help"]).exit_code == 0 # ══════════════════════════════════════════════════════════════════════════════ # Inventory — show single / delete all @@ -579,32 +562,31 @@ def test_show_single(self, invoke, mock_client): "max_unit": 64, "step_size": 1, "allocation_ratio": 16.0, "resource_provider_generation": 1, } - result = invoke(["placement", "resource-provider-inventory-show", RP_UUID, "VCPU"]) + result = invoke(["placement", "resource-provider", "inventory-show", RP_UUID, "VCPU"]) assert result.exit_code == 0 assert "64" in result.output def test_delete_all_yes(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "resource-provider-inventory-delete-all", + result = invoke(["placement", "resource-provider", "inventory-delete-all", RP_UUID, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_delete_all_requires_confirm(self, invoke, mock_client): _placement(mock_client) - result = invoke(["placement", "resource-provider-inventory-delete-all", RP_UUID], + result = invoke(["placement", "resource-provider", "inventory-delete-all", RP_UUID], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_help_show(self, invoke): - assert invoke(["placement", "resource-provider-inventory-show", "--help"]).exit_code == 0 + assert invoke(["placement", "resource-provider", "inventory-show", "--help"]).exit_code == 0 def test_help_delete_all(self, invoke): - assert invoke(["placement", "resource-provider-inventory-delete-all", + assert invoke(["placement", "resource-provider", "inventory-delete-all", "--help"]).exit_code == 0 - # ══════════════════════════════════════════════════════════════════════════════ # Registration # ══════════════════════════════════════════════════════════════════════════════ @@ -612,22 +594,6 @@ def test_help_delete_all(self, invoke): class TestRegistration: @pytest.mark.parametrize("sub", [ - "resource-provider-list", "resource-provider-show", - "resource-provider-create", "resource-provider-set", "resource-provider-delete", - "resource-provider-inventory-list", "resource-provider-inventory-set", - "resource-provider-inventory-delete", - "resource-provider-inventory-show", "resource-provider-inventory-delete-all", - "resource-provider-usage", - "resource-provider-trait-list", "resource-provider-trait-set", - "resource-provider-trait-delete", - "usage-list", - "resource-class-list", "resource-class-show", - "resource-class-create", "resource-class-delete", - "trait-list", "trait-create", "trait-delete", - "allocation-show", "allocation-delete", "allocation-set", - "allocation-candidate-list", - "resource-provider-aggregate-list", "resource-provider-aggregate-set", - "resource-provider-aggregate-delete", ]) def test_subcommand_help(self, invoke, sub): assert invoke(["placement", sub, "--help"]).exit_code == 0 diff --git a/tests/test_profile.py b/tests/test_profile.py index 6bf73e0..59670d5 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -10,7 +10,6 @@ # list # ══════════════════════════════════════════════════════════════════════════ - class TestProfileList: def test_list(self, invoke, config_dir, sample_profile): @@ -65,12 +64,10 @@ def test_list_mixed_password_and_ac(self, invoke, config_dir, sample_profile): assert "password" in result.output assert "app-cred" in result.output - # ══════════════════════════════════════════════════════════════════════════ # show # ══════════════════════════════════════════════════════════════════════════ - class TestProfileShow: def test_show(self, invoke, config_dir, sample_profile): @@ -98,12 +95,10 @@ def test_show_not_found(self, invoke, config_dir, sample_profile): assert result.exit_code != 0 assert "not found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # switch # ══════════════════════════════════════════════════════════════════════════ - class TestProfileSwitch: def test_switch(self, invoke, config_dir, sample_profile): @@ -189,12 +184,10 @@ def test_help_shows_interactive_hint(self, invoke): assert result.exit_code == 0 assert "interactive" in result.output.lower() or "menu" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # set-color # ══════════════════════════════════════════════════════════════════════════ - class TestProfileSetColor: def test_set_color(self, invoke, config_dir, sample_profile): @@ -218,12 +211,10 @@ def test_set_color_not_found(self, invoke, config_dir): result = invoke(["profile", "color", "set", "red", "nope"]) assert result.exit_code != 0 - # ══════════════════════════════════════════════════════════════════════════ # remove # ══════════════════════════════════════════════════════════════════════════ - class TestProfileRemove: def test_remove(self, invoke, config_dir, sample_profile): @@ -243,12 +234,10 @@ def test_remove_not_found(self, invoke, config_dir, sample_profile): assert result.exit_code != 0 assert "does not exist" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # rename # ══════════════════════════════════════════════════════════════════════════ - class TestProfileRename: def test_rename(self, invoke, config_dir, sample_profile): @@ -266,12 +255,10 @@ def test_rename_not_found(self, invoke, config_dir, sample_profile): result = invoke(["profile", "rename", "nope", "new"]) assert result.exit_code != 0 - # ══════════════════════════════════════════════════════════════════════════ # set-region # ══════════════════════════════════════════════════════════════════════════ - class TestProfileSetRegion: def test_set_region(self, invoke, config_dir, sample_profile): @@ -291,12 +278,10 @@ def test_clear_region(self, invoke, config_dir, sample_profile): assert result.exit_code == 0 assert "cleared" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # to-openrc # ══════════════════════════════════════════════════════════════════════════ - class TestProfileToOpenrc: def test_to_openrc_stdout(self, invoke, config_dir, sample_profile): @@ -318,12 +303,10 @@ def test_to_openrc_file(self, invoke, config_dir, sample_profile, tmp_path): content = Path(out).read_text() assert "OS_AUTH_URL" in content - # ══════════════════════════════════════════════════════════════════════════ # to-clouds # ══════════════════════════════════════════════════════════════════════════ - class TestProfileToClouds: def test_to_clouds_stdout(self, invoke, config_dir, sample_profile): @@ -345,12 +328,10 @@ def test_to_clouds_file(self, invoke, config_dir, sample_profile, tmp_path): content = Path(out).read_text() assert "clouds:" in content - # ══════════════════════════════════════════════════════════════════════════ # Helpers: _shell_quote, _parse_openrc, _os_env_to_cfg, _cfg_to_os_env # ══════════════════════════════════════════════════════════════════════════ - class TestHelpers: def test_shell_quote_simple(self): @@ -405,12 +386,10 @@ def test_cfg_to_os_env(self): assert env["OS_PROJECT_NAME"] == "myproj" assert env["OS_IDENTITY_API_VERSION"] == "3" - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestProfileHelp: def test_profile_help(self, invoke): @@ -421,6 +400,6 @@ def test_profile_help(self, invoke): "rename", "color", "region", "import", "export"): assert cmd in result.output # Deprecated hyphenated aliases must remain visible (with marker) - for cmd in ("set-color", "set-region", "regions", - "to-openrc", "to-clouds", "from-openrc", "from-clouds"): + for cmd in ( + "to-openrc"): assert cmd in result.output diff --git a/tests/test_profile_convert.py b/tests/test_profile_convert.py index a6976cb..fec6b4c 100644 --- a/tests/test_profile_convert.py +++ b/tests/test_profile_convert.py @@ -58,7 +58,6 @@ def test_export_nonexistent_profile_fails(self, invoke, config_dir, mock_client) result = invoke(["profile", "export", "openrc", "nope"]) assert result.exit_code != 0 - class TestToClouds: def test_export_as_clouds_yaml(self, invoke, config_dir, sample_profile, mock_client): @@ -109,7 +108,6 @@ def test_insecure_becomes_verify_false(self, invoke, config_dir, mock_client): data = yaml.safe_load(result.output) assert data["clouds"]["prod"].get("verify") is False - class TestFromOpenrc: def test_import_openrc_file(self, invoke, config_dir, mock_client, tmp_path): @@ -185,7 +183,6 @@ def test_import_handles_quoted_values(self, invoke, config_dir, mock_client, tmp assert cfg["auth_url"] == "https://ks:5000" assert cfg["password"] == "secret with spaces" - class TestFromClouds: def test_import_from_clouds_yaml(self, invoke, config_dir, mock_client, tmp_path, monkeypatch): @@ -253,7 +250,6 @@ def test_import_with_explicit_file(self, invoke, config_dir, mock_client, tmp_pa from orca_cli.core.config import get_profile assert get_profile("other")["auth_url"] == "https://other:5000" - class TestRoundTrip: """Export then import should preserve data.""" diff --git a/tests/test_project_cleanup.py b/tests/test_project_cleanup.py index a72943c..3c09bcd 100644 --- a/tests/test_project_cleanup.py +++ b/tests/test_project_cleanup.py @@ -18,7 +18,6 @@ def _make_net_svc(monkeypatch, **attrs): monkeypatch.setattr(proj_mod, "NetworkService", lambda _c: net_svc) return net_svc - class TestDeleteRouter: def test_clears_gateway_before_detaching_and_deleting(self, monkeypatch): net = _make_net_svc(monkeypatch) @@ -111,7 +110,6 @@ def test_continues_when_remove_interface_fails(self, monkeypatch): assert net.remove_router_interface.call_count == 2 net.delete_router.assert_called_once_with("r-1") - class TestDeleteNetwork: def test_deletes_orphan_and_stale_compute_ports(self, monkeypatch): net = _make_net_svc(monkeypatch) @@ -160,7 +158,6 @@ def test_continues_when_find_ports_fails(self, monkeypatch): net.delete_port.assert_not_called() net.delete.assert_called_once_with("n-1") - def _svc_that_raises(monkeypatch, service_attr: str, exc: Exception) -> MagicMock: """Replace one Service class on the project module so its delete raises exc.""" svc = MagicMock() @@ -179,7 +176,6 @@ def _svc_that_raises(monkeypatch, service_attr: str, exc: Exception) -> MagicMoc monkeypatch.setattr(proj_mod, service_attr, lambda _c: svc) return svc - class TestClassifyApiError: @pytest.mark.parametrize("status,expected", [ (404, Outcome.ALREADY_GONE), @@ -191,7 +187,6 @@ class TestClassifyApiError: def test_status_to_outcome(self, status, expected): assert proj_mod._classify_api_error(APIError(status, "detail")) is expected - class TestDeleteOneOutcomes: """_delete_one must map HTTP status to the right Outcome.""" @@ -239,7 +234,6 @@ def test_unexpected_exception_is_failed(self, monkeypatch): out = proj_mod._delete_one(object(), "volume", "v-1", "—") assert out is Outcome.FAILED - class TestRefreshAndWaitVolumes: """Volume settle window between server-delete and volume-delete phases. @@ -328,7 +322,6 @@ def test_cutoff_is_applied_on_refresh(self, monkeypatch): assert result == [("old", "o")] - class TestSummaryRendering: """End-to-end through the Click command: the summary line must aggregate the four outcomes independently — a mix of 404s, 409s and real failures diff --git a/tests/test_quota.py b/tests/test_quota.py index fafda97..dbeb706 100644 --- a/tests/test_quota.py +++ b/tests/test_quota.py @@ -6,7 +6,6 @@ # ── Helpers ──────────────────────────────────────────────────────────────── - def _setup_mock(mock_client, nova=None, cinder=None, neutron_quotas=None, nets=0, subnets=0, ports=0, routers=0, fips=0, sgs=0): mock_client.compute_url = "https://nova.example.com/v2.1" @@ -65,12 +64,10 @@ def _get(url, **kwargs): mock_client.get = _get - # ══════════════════════════════════════════════════════════════════════════ # quota # ══════════════════════════════════════════════════════════════════════════ - class TestQuota: def test_quota_basic(self, invoke, config_dir, mock_client, sample_profile): @@ -177,12 +174,10 @@ def _get(url, **kwargs): assert "unavailable" in result.output assert "Volume" in result.output # other sections still render - # ══════════════════════════════════════════════════════════════════════════ # _row helper # ══════════════════════════════════════════════════════════════════════════ - class TestRow: def test_row_percentage(self): @@ -229,12 +224,10 @@ def test_row_low_usage_green(self): assert "green" in r["usage"] assert "25%" in r["usage"] - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestQuotaHelp: def test_quota_help(self, invoke): diff --git a/tests/test_rating.py b/tests/test_rating.py index 6a8a7d2..3955931 100644 --- a/tests/test_rating.py +++ b/tests/test_rating.py @@ -9,12 +9,10 @@ GROUP_ID = "55555555-5555-5555-5555-555555555555" BASE = "https://cloudkitty.example.com" - def _rating(mc): mc.rating_url = BASE return mc - # ══════════════════════════════════════════════════════════════════════════════ # rating info / metric-list / metric-show # ══════════════════════════════════════════════════════════════════════════════ @@ -34,7 +32,7 @@ def test_metric_list(self, invoke, mock_client): {"metric_id": "instance_up", "unit": "instance", "metadata": ["flavor"]}, {"metric_id": "volume.size", "unit": "GiB", "metadata": []}, ]} - result = invoke(["rating", "metric-list"]) + result = invoke(["rating", "metric", "list"]) assert result.exit_code == 0 assert "instance_up" in result.output assert "volume.size" in result.output @@ -44,14 +42,13 @@ def test_metric_show_uses_plural_path(self, invoke, mock_client): mock_client.get.return_value = { "metric_id": "instance_up", "unit": "instance", "metadata": ["flavor"] } - result = invoke(["rating", "metric-show", "instance_up"]) + result = invoke(["rating", "metric", "show", "instance_up"]) assert result.exit_code == 0 # CloudKitty returns 405 on the singular /metric/{id} path. # The plural /metrics/{id} is the correct one. called_url = mock_client.get.call_args[0][0] assert called_url.endswith("/v1/info/metrics/instance_up") - # ══════════════════════════════════════════════════════════════════════════════ # rating summary / dataframes / quote # ══════════════════════════════════════════════════════════════════════════════ @@ -99,7 +96,6 @@ def test_summary_filters_missing_equals(self, invoke, mock_client): result = invoke(["rating", "summary", "--filters", "bad-filter"]) assert result.exit_code != 0 - class TestDataframes: def test_dataframes_v2(self, invoke, mock_client): @@ -132,7 +128,6 @@ def test_dataframes_empty(self, invoke, mock_client): assert result.exit_code == 0 assert "No dataframes" in result.output - class TestQuote: def test_quote_single_resource(self, invoke, mock_client): @@ -152,7 +147,6 @@ def test_quote_invalid_json(self, invoke, mock_client): result = invoke(["rating", "quote", "--resource", "not-json"]) assert result.exit_code != 0 - # ══════════════════════════════════════════════════════════════════════════════ # rating modules # ══════════════════════════════════════════════════════════════════════════════ @@ -165,7 +159,7 @@ def test_module_list(self, invoke, mock_client): {"module_id": "hashmap", "enabled": True, "priority": 1, "description": "HashMap"}, {"module_id": "pyscripts", "enabled": False, "priority": 0, "description": "PyScripts"}, ]} - result = invoke(["rating", "module-list"]) + result = invoke(["rating", "module", "list"]) assert result.exit_code == 0 assert "hashmap" in result.output assert "pyscripts" in result.output @@ -176,7 +170,7 @@ def test_module_enable_merges_current(self, invoke, mock_client): "module_id": "hashmap", "enabled": False, "priority": 1, "description": "HashMap", } - result = invoke(["rating", "module-enable", "hashmap"]) + result = invoke(["rating", "module", "enable", "hashmap"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"] # CloudKitty PUT wants the full module representation minus module_id @@ -190,7 +184,7 @@ def test_module_disable(self, invoke, mock_client): mock_client.get.return_value = { "module_id": "hashmap", "enabled": True, "priority": 1, "description": "" } - result = invoke(["rating", "module-disable", "hashmap"]) + result = invoke(["rating", "module", "disable", "hashmap"]) assert result.exit_code == 0 assert mock_client.put.call_args[1]["json"]["enabled"] is False @@ -199,11 +193,10 @@ def test_module_set_priority(self, invoke, mock_client): mock_client.get.return_value = { "module_id": "hashmap", "enabled": True, "priority": 1, "description": "" } - result = invoke(["rating", "module-set-priority", "hashmap", "10"]) + result = invoke(["rating", "module", "set-priority", "hashmap", "10"]) assert result.exit_code == 0 assert mock_client.put.call_args[1]["json"]["priority"] == 10 - # ══════════════════════════════════════════════════════════════════════════════ # rating hashmap # ══════════════════════════════════════════════════════════════════════════════ @@ -233,7 +226,6 @@ def test_service_delete(self, invoke, mock_client): mock_client.delete.assert_called_once() assert SERVICE_ID in mock_client.delete.call_args[0][0] - class TestHashmapMappings: def test_mapping_create_requires_field_or_service(self, invoke, mock_client): @@ -262,7 +254,6 @@ def test_mapping_delete(self, invoke, mock_client): result = invoke(["rating", "hashmap", "mapping-delete", MAPPING_ID, "-y"]) assert result.exit_code == 0 - class TestHashmapThresholds: def test_threshold_create_requires_field_or_service(self, invoke, mock_client): @@ -282,7 +273,6 @@ def test_threshold_create(self, invoke, mock_client): ]) assert result.exit_code == 0 - class TestHashmapGroups: def test_group_create(self, invoke, mock_client): @@ -304,7 +294,6 @@ def test_group_delete(self, invoke, mock_client): result = invoke(["rating", "hashmap", "group-delete", GROUP_ID, "-y"]) assert result.exit_code == 0 - class TestHashmapFieldsAndLists: def test_field_list_filtered(self, invoke, mock_client): @@ -354,7 +343,6 @@ def test_threshold_delete(self, invoke, mock_client): result = invoke(["rating", "hashmap", "threshold-delete", THRESHOLD_ID, "-y"]) assert result.exit_code == 0 - class TestModuleShow: def test_module_show(self, invoke, mock_client): @@ -363,6 +351,6 @@ def test_module_show(self, invoke, mock_client): "module_id": "hashmap", "enabled": True, "priority": 1, "description": "HashMap", "hot_config": True, } - result = invoke(["rating", "module-show", "hashmap"]) + result = invoke(["rating", "module", "show", "hashmap"]) assert result.exit_code == 0 assert "hashmap" in result.output.lower() diff --git a/tests/test_role.py b/tests/test_role.py index 9e395df..b460edf 100644 --- a/tests/test_role.py +++ b/tests/test_role.py @@ -12,12 +12,10 @@ PROJECT_ID = "abcdabcd-1234-5678-9abc-def012345678" DOMAIN_ID = "ddddeeee-ffff-0000-1111-222233334444" - def _role(role_id=ROLE_ID, name="my-role"): return {"id": role_id, "name": name, "description": "test role", "domain_id": None} - def _setup_mock(mock_client): mock_client.identity_url = "https://keystone.example.com:5000" @@ -60,12 +58,10 @@ def _delete(url, **kwargs): return {"patched": patched, "posted": posted, "deleted": deleted} - # ══════════════════════════════════════════════════════════════════════════ # role set # ══════════════════════════════════════════════════════════════════════════ - class TestRoleSet: def test_set_name(self, invoke, config_dir, mock_client, sample_profile): @@ -108,18 +104,14 @@ def test_set_nothing(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Nothing" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestRoleHelp: def test_role_help(self, invoke): result = invoke(["role", "--help"]) assert result.exit_code == 0 - for cmd in ("list", "show", "create", "delete", "add", "remove", - "assignment-list", "implied-list", "implied-create", - "implied-delete", "set"): + for cmd in ("list", "show", "create", "delete", "add", "remove", "set"): assert cmd in result.output diff --git a/tests/test_secret.py b/tests/test_secret.py index 780e692..9ed2748 100644 --- a/tests/test_secret.py +++ b/tests/test_secret.py @@ -11,7 +11,6 @@ SECRET_ID = "11112222-3333-4444-5555-666677778888" CONTAINER_ID = "aaaabbbb-cccc-dddd-eeee-ffffffffffff" - def _setup_mock(mock_client): mock_client.key_manager_url = "https://barbican.example.com" mock_client.token = "fake-token" @@ -87,12 +86,10 @@ def _http_stream(method, url, **kwargs): return {"posted": posted, "deleted": deleted} - # ══════════════════════════════════════════════════════════════════════════ # secret list # ══════════════════════════════════════════════════════════════════════════ - class TestSecretList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -114,12 +111,10 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No secrets found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # secret show # ══════════════════════════════════════════════════════════════════════════ - class TestSecretShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -132,12 +127,10 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): assert "my-secret" in result.output assert "opaque" in result.output - # ══════════════════════════════════════════════════════════════════════════ # secret create # ══════════════════════════════════════════════════════════════════════════ - class TestSecretCreate: def test_create(self, invoke, config_dir, mock_client, sample_profile): @@ -164,12 +157,10 @@ def test_create_with_options(self, invoke, config_dir, mock_client, sample_profi assert state["posted"]["bit_length"] == 256 assert state["posted"]["secret_type"] == "symmetric" - # ══════════════════════════════════════════════════════════════════════════ # secret delete # ══════════════════════════════════════════════════════════════════════════ - class TestSecretDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -182,12 +173,10 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert "deleted" in result.output.lower() assert len(state["deleted"]) == 1 - # ══════════════════════════════════════════════════════════════════════════ # secret get-payload # ══════════════════════════════════════════════════════════════════════════ - class TestSecretGetPayload: def test_get_payload(self, invoke, config_dir, mock_client, sample_profile): @@ -199,12 +188,10 @@ def test_get_payload(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "my-payload-value" in result.output - # ══════════════════════════════════════════════════════════════════════════ # container-list # ══════════════════════════════════════════════════════════════════════════ - class TestContainerList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -226,12 +213,10 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No containers found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # container-show # ══════════════════════════════════════════════════════════════════════════ - class TestContainerShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -244,12 +229,10 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): assert "my-container" in result.output assert "generic" in result.output - # ══════════════════════════════════════════════════════════════════════════ # container-delete # ══════════════════════════════════════════════════════════════════════════ - class TestContainerDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -261,12 +244,10 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "deleted" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestSecretHelp: def test_secret_help(self, invoke): @@ -276,7 +257,6 @@ def test_secret_help(self, invoke): "container-list", "container-show", "container-delete"): assert cmd in result.output - # ══════════════════════════════════════════════════════════════════════════ # container-create / acl-get / acl-set / acl-delete # order-list / order-show / order-create / order-delete @@ -285,7 +265,6 @@ def test_secret_help(self, invoke): _BARB = "https://barbican.example.com" _ORDER_ID = "cccccccc-cccc-cccc-cccc-cccccccccccc" - class TestSecretContainerCreate: def test_create(self, invoke, mock_client): @@ -309,7 +288,6 @@ def test_create_with_secrets(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["secret", "container-create", "--help"]).exit_code == 0 - class TestSecretAcl: def test_acl_get(self, invoke, mock_client): @@ -317,12 +295,12 @@ def test_acl_get(self, invoke, mock_client): mock_client.get.return_value = { "read": {"project-access": True, "users": [], "created": "", "updated": ""} } - result = invoke(["secret", "acl-get", SECRET_ID]) + result = invoke(["secret", "acl", "get", SECRET_ID]) assert result.exit_code == 0 def test_acl_set(self, invoke, mock_client): mock_client.key_manager_url = _BARB - result = invoke(["secret", "acl-set", SECRET_ID, + result = invoke(["secret", "acl", "set", SECRET_ID, "--user", "user-a", "--user", "user-b"]) assert result.exit_code == 0 body = mock_client.put.call_args[1]["json"] @@ -331,38 +309,37 @@ def test_acl_set(self, invoke, mock_client): def test_acl_set_no_project_access(self, invoke, mock_client): mock_client.key_manager_url = _BARB - invoke(["secret", "acl-set", SECRET_ID, "--no-project-access"]) + invoke(["secret", "acl", "set", SECRET_ID, "--no-project-access"]) body = mock_client.put.call_args[1]["json"] assert body["read"]["project-access"] is False def test_acl_delete_yes(self, invoke, mock_client): mock_client.key_manager_url = _BARB - result = invoke(["secret", "acl-delete", SECRET_ID, "--yes"]) + result = invoke(["secret", "acl", "delete", SECRET_ID, "--yes"]) assert result.exit_code == 0 mock_client.delete.assert_called_once() def test_acl_delete_requires_confirm(self, invoke, mock_client): mock_client.key_manager_url = _BARB - result = invoke(["secret", "acl-delete", SECRET_ID], input="n\n") + result = invoke(["secret", "acl", "delete", SECRET_ID], input="n\n") assert result.exit_code != 0 mock_client.delete.assert_not_called() def test_acl_calls_correct_url(self, invoke, mock_client): mock_client.key_manager_url = _BARB mock_client.get.return_value = {"read": {}} - invoke(["secret", "acl-get", SECRET_ID]) + invoke(["secret", "acl", "get", SECRET_ID]) url = mock_client.get.call_args[0][0] assert f"/v1/secrets/{SECRET_ID}/acl" in url def test_help_acl_get(self, invoke): - assert invoke(["secret", "acl-get", "--help"]).exit_code == 0 + assert invoke(["secret", "acl", "get", "--help"]).exit_code == 0 def test_help_acl_set(self, invoke): - assert invoke(["secret", "acl-set", "--help"]).exit_code == 0 + assert invoke(["secret", "acl", "set", "--help"]).exit_code == 0 def test_help_acl_delete(self, invoke): - assert invoke(["secret", "acl-delete", "--help"]).exit_code == 0 - + assert invoke(["secret", "acl", "delete", "--help"]).exit_code == 0 class TestSecretOrders: diff --git a/tests/test_security_group.py b/tests/test_security_group.py index 4e09c24..dc588f6 100644 --- a/tests/test_security_group.py +++ b/tests/test_security_group.py @@ -9,7 +9,6 @@ SG_ID = "11112222-3333-4444-5555-666677778888" RULE_ID = "aaaabbbb-cccc-dddd-eeee-ffffffffffff" - def _sg(sg_id=SG_ID, name="my-sg", rules=None): return { "id": sg_id, "name": name, @@ -21,7 +20,6 @@ def _sg(sg_id=SG_ID, name="my-sg", rules=None): ], } - def _setup_mock(mock_client): mock_client.network_url = "https://neutron.example.com" @@ -64,12 +62,10 @@ def _delete(url, **kwargs): return {"posted": posted, "put_data": put_data, "deleted": deleted} - # ══════════════════════════════════════════════════════════════════════════ # list # ══════════════════════════════════════════════════════════════════════════ - class TestSGList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -91,12 +87,10 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No security groups found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # show # ══════════════════════════════════════════════════════════════════════════ - class TestSGShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -118,12 +112,10 @@ def test_show_port_range(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "22" in result.output - # ══════════════════════════════════════════════════════════════════════════ # create # ══════════════════════════════════════════════════════════════════════════ - class TestSGCreate: def test_create(self, invoke, config_dir, mock_client, sample_profile): @@ -145,12 +137,10 @@ def test_create_with_description(self, invoke, config_dir, mock_client, sample_p assert result.exit_code == 0 assert state["posted"]["security_group"]["description"] == "Web SG" - # ══════════════════════════════════════════════════════════════════════════ # update # ══════════════════════════════════════════════════════════════════════════ - class TestSGUpdate: def test_update_name(self, invoke, config_dir, mock_client, sample_profile): @@ -171,12 +161,10 @@ def test_update_nothing(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Nothing" in result.output - # ══════════════════════════════════════════════════════════════════════════ # delete # ══════════════════════════════════════════════════════════════════════════ - class TestSGDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -189,12 +177,10 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert "deleted" in result.output.lower() assert len(state["deleted"]) == 1 - # ══════════════════════════════════════════════════════════════════════════ # rule-add # ══════════════════════════════════════════════════════════════════════════ - class TestSGRuleAdd: def test_rule_add(self, invoke, config_dir, mock_client, sample_profile): @@ -202,7 +188,7 @@ def test_rule_add(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["security-group", "rule-add", SG_ID, + result = invoke(["security-group", "rule", "add", SG_ID, "--direction", "ingress", "--protocol", "tcp", "--port-min", "80"]) assert result.exit_code == 0 @@ -216,19 +202,17 @@ def test_rule_add_with_remote_ip(self, invoke, config_dir, mock_client, sample_p set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["security-group", "rule-add", SG_ID, + result = invoke(["security-group", "rule", "add", SG_ID, "--direction", "ingress", "--protocol", "tcp", "--port-min", "443", "--remote-ip", "10.0.0.0/8"]) assert result.exit_code == 0 rule = state["posted"]["security_group_rule"] assert rule["remote_ip_prefix"] == "10.0.0.0/8" - # ══════════════════════════════════════════════════════════════════════════ # rule-delete # ══════════════════════════════════════════════════════════════════════════ - class TestSGRuleDelete: def test_rule_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -236,16 +220,14 @@ def test_rule_delete(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["security-group", "rule-delete", RULE_ID, "-y"]) + result = invoke(["security-group", "rule", "delete", RULE_ID, "-y"]) assert result.exit_code == 0 assert "deleted" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # clone # ══════════════════════════════════════════════════════════════════════════ - class TestSGClone: def test_clone(self, invoke, config_dir, mock_client, sample_profile): @@ -259,12 +241,10 @@ def test_clone(self, invoke, config_dir, mock_client, sample_profile): assert "cloned-sg" in result.output assert "1/1" in result.output - # ══════════════════════════════════════════════════════════════════════════ # cleanup # ══════════════════════════════════════════════════════════════════════════ - class TestSGCleanup: def test_cleanup_no_orphans(self, invoke, config_dir, mock_client, sample_profile): @@ -331,17 +311,14 @@ def _delete(url, **kwargs): assert len(deleted) == 1 assert "1 deleted" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestSGHelp: def test_sg_help(self, invoke): result = invoke(["security-group", "--help"]) assert result.exit_code == 0 - for cmd in ("list", "show", "create", "update", "delete", - "rule-add", "rule-delete", "clone", "cleanup"): + for cmd in ("list", "show", "create", "update", "delete", "clone", "cleanup"): assert cmd in result.output diff --git a/tests/test_server.py b/tests/test_server.py index f7b6eb7..bacdc93 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -13,7 +13,6 @@ NET_ID = "44445555-6666-7777-8888-999900001111" IMG_ID = "55556666-7777-8888-9999-000011112222" - def _srv(srv_id=SRV_ID, name="web-1", status="ACTIVE"): return { "id": srv_id, "name": name, "status": status, @@ -35,7 +34,6 @@ def _srv(srv_id=SRV_ID, name="web-1", status="ACTIVE"): "os-extended-volumes:volumes_attached": [{"id": VOL_ID}], } - def _setup_mock(mock_client): mock_client.compute_url = "https://nova.example.com/v2.1" mock_client.volume_url = "https://cinder.example.com/v3" @@ -109,12 +107,10 @@ def _delete(url, **kwargs): return {"posted": posted, "put_data": put_data, "deleted": deleted} - # ══════════════════════════════════════════════════════════════════════════ # list # ══════════════════════════════════════════════════════════════════════════ - class TestServerList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -136,12 +132,10 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No servers found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # show # ══════════════════════════════════════════════════════════════════════════ - class TestServerShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -154,12 +148,10 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): assert "web-1" in result.output assert "ACTIVE" in result.output - # ══════════════════════════════════════════════════════════════════════════ # create # ══════════════════════════════════════════════════════════════════════════ - class TestServerCreate: def test_create(self, invoke, config_dir, mock_client, sample_profile): @@ -195,12 +187,10 @@ def test_create_with_options(self, invoke, config_dir, mock_client, sample_profi assert body["networks"] == [{"uuid": NET_ID}] assert body["key_name"] == "my-key" - # ══════════════════════════════════════════════════════════════════════════ # delete # ══════════════════════════════════════════════════════════════════════════ - class TestServerDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -213,12 +203,10 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert "deleted" in result.output.lower() assert len(state["deleted"]) == 1 - # ══════════════════════════════════════════════════════════════════════════ # Actions: start, stop, reboot, pause, unpause, suspend, resume, etc. # ══════════════════════════════════════════════════════════════════════════ - class TestServerActions: def test_start(self, invoke, config_dir, mock_client, sample_profile): @@ -347,12 +335,10 @@ def test_unrescue(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Unrescue" in result.output - # ══════════════════════════════════════════════════════════════════════════ # resize / confirm / revert # ══════════════════════════════════════════════════════════════════════════ - class TestServerResize: def test_resize(self, invoke, config_dir, mock_client, sample_profile): @@ -382,12 +368,10 @@ def test_revert_resize(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Revert" in result.output - # ══════════════════════════════════════════════════════════════════════════ # rebuild # ══════════════════════════════════════════════════════════════════════════ - class TestServerRebuild: def test_rebuild(self, invoke, config_dir, mock_client, sample_profile): @@ -399,12 +383,10 @@ def test_rebuild(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Rebuild" in result.output - # ══════════════════════════════════════════════════════════════════════════ # rename # ══════════════════════════════════════════════════════════════════════════ - class TestServerRename: def test_rename(self, invoke, config_dir, mock_client, sample_profile): @@ -416,12 +398,10 @@ def test_rename(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "renamed" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # create-image # ══════════════════════════════════════════════════════════════════════════ - class TestServerCreateImage: def test_create_image(self, invoke, config_dir, mock_client, sample_profile): @@ -429,16 +409,14 @@ def test_create_image(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["server", "create-image", SRV_ID, "my-snapshot"]) + result = invoke(["server", "image", "create", SRV_ID, "my-snapshot"]) assert result.exit_code == 0 assert "my-snapshot" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Volume attachments # ══════════════════════════════════════════════════════════════════════════ - class TestServerVolumes: def test_attach_volume(self, invoke, config_dir, mock_client, sample_profile): @@ -446,7 +424,7 @@ def test_attach_volume(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["server", "attach-volume", SRV_ID, VOL_ID]) + result = invoke(["server", "add", "volume", SRV_ID, VOL_ID]) assert result.exit_code == 0 assert "attached" in result.output.lower() @@ -455,7 +433,7 @@ def test_detach_volume(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["server", "detach-volume", SRV_ID, VOL_ID]) + result = invoke(["server", "remove", "volume", SRV_ID, VOL_ID]) assert result.exit_code == 0 assert "detached" in result.output.lower() @@ -464,16 +442,14 @@ def test_list_volumes(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["server", "list-volumes", SRV_ID]) + result = invoke(["server", "volume", "list", SRV_ID]) assert result.exit_code == 0 assert "/dev/vda" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Interface attachments # ══════════════════════════════════════════════════════════════════════════ - class TestServerInterfaces: def test_attach_interface(self, invoke, config_dir, mock_client, sample_profile): @@ -498,7 +474,7 @@ def test_detach_interface(self, invoke, config_dir, mock_client, sample_profile) set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["server", "detach-interface", SRV_ID, PORT_ID]) + result = invoke(["server", "remove", "interface", SRV_ID, PORT_ID]) assert result.exit_code == 0 assert "detached" in result.output.lower() @@ -507,16 +483,14 @@ def test_list_interfaces(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["server", "list-interfaces", SRV_ID]) + result = invoke(["server", "interface", "list", SRV_ID]) assert result.exit_code == 0 assert "10.0" in result.output - # ══════════════════════════════════════════════════════════════════════════ # console-log # ══════════════════════════════════════════════════════════════════════════ - class TestServerConsoleLog: def test_console_log(self, invoke, config_dir, mock_client, sample_profile): @@ -524,7 +498,7 @@ def test_console_log(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["server", "console-log", SRV_ID]) + result = invoke(["server", "console", "log", SRV_ID]) assert result.exit_code == 0 assert "Boot log" in result.output @@ -534,16 +508,14 @@ def test_console_log_empty(self, invoke, config_dir, mock_client, sample_profile mock_client.compute_url = "https://nova.example.com/v2.1" mock_client.post = lambda url, **kw: {"output": ""} - result = invoke(["server", "console-log", SRV_ID]) + result = invoke(["server", "console", "log", SRV_ID]) assert result.exit_code == 0 assert "No console output" in result.output - # ══════════════════════════════════════════════════════════════════════════ # console-url # ══════════════════════════════════════════════════════════════════════════ - class TestServerConsoleUrl: def test_console_url(self, invoke, config_dir, mock_client, sample_profile): @@ -551,16 +523,14 @@ def test_console_url(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["server", "console-url", SRV_ID]) + result = invoke(["server", "console", "url", SRV_ID]) assert result.exit_code == 0 assert "vnc.example.com" in result.output - # ══════════════════════════════════════════════════════════════════════════ # snapshot (server + volumes) # ══════════════════════════════════════════════════════════════════════════ - class TestServerSnapshot: def test_snapshot(self, invoke, config_dir, mock_client, sample_profile): @@ -582,12 +552,10 @@ def test_snapshot_custom_name(self, invoke, config_dir, mock_client, sample_prof assert result.exit_code == 0 assert "backup-image" in result.output - # ══════════════════════════════════════════════════════════════════════════ # diff # ══════════════════════════════════════════════════════════════════════════ - class TestServerDiff: def test_diff(self, invoke, config_dir, mock_client, sample_profile): @@ -601,12 +569,10 @@ def test_diff(self, invoke, config_dir, mock_client, sample_profile): assert "web-2" in result.output assert "difference" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # bulk # ══════════════════════════════════════════════════════════════════════════ - class TestServerBulk: def test_bulk_stop(self, invoke, config_dir, mock_client, sample_profile): @@ -635,12 +601,10 @@ def test_bulk_no_match(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No servers match" in result.output - # ══════════════════════════════════════════════════════════════════════════ # clone # ══════════════════════════════════════════════════════════════════════════ - class TestServerClone: def test_clone(self, invoke, config_dir, mock_client, sample_profile): @@ -653,17 +617,14 @@ def test_clone(self, invoke, config_dir, mock_client, sample_profile): assert "web-clone" in result.output assert "creation started" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - # ══════════════════════════════════════════════════════════════════════════ # evacuate / dump-create / restore # ══════════════════════════════════════════════════════════════════════════ - class TestServerEvacuate: def test_evacuate_basic(self, invoke, mock_client): @@ -687,21 +648,19 @@ def test_evacuate_shared_storage(self, invoke, mock_client): body = state["posted"]["last_body"]["evacuate"] assert body["onSharedStorage"] is True - class TestServerDumpCreate: def test_dump_create(self, invoke, mock_client): state = _setup_mock(mock_client) - result = invoke(["server", "dump-create", SRV_ID]) + result = invoke(["server", "dump", "create", SRV_ID]) assert result.exit_code == 0 assert "trigger_crash_dump" in state["posted"]["last_body"] assert "triggered" in result.output def test_dump_create_help(self, invoke): - result = invoke(["server", "dump-create", "--help"]) + result = invoke(["server", "dump", "create", "--help"]) assert result.exit_code == 0 - class TestServerRestore: def test_restore(self, invoke, mock_client): @@ -711,17 +670,15 @@ def test_restore(self, invoke, mock_client): assert "restore" in state["posted"]["last_body"] assert "restored" in result.output - # ══════════════════════════════════════════════════════════════════════════ # fixed-ip add/remove # ══════════════════════════════════════════════════════════════════════════ - class TestServerFixedIP: def test_add_fixed_ip(self, invoke, mock_client): state = _setup_mock(mock_client) - result = invoke(["server", "add-fixed-ip", SRV_ID, NET_ID]) + result = invoke(["server", "add", "fixed-ip", SRV_ID, NET_ID]) assert result.exit_code == 0 body = state["posted"]["last_body"] assert "addFixedIp" in body @@ -729,23 +686,21 @@ def test_add_fixed_ip(self, invoke, mock_client): def test_remove_fixed_ip(self, invoke, mock_client): state = _setup_mock(mock_client) - result = invoke(["server", "remove-fixed-ip", SRV_ID, "10.0.0.5"]) + result = invoke(["server", "remove", "fixed-ip", SRV_ID, "10.0.0.5"]) assert result.exit_code == 0 body = state["posted"]["last_body"] assert "removeFixedIp" in body assert body["removeFixedIp"]["address"] == "10.0.0.5" - # ══════════════════════════════════════════════════════════════════════════ # port / network add/remove # ══════════════════════════════════════════════════════════════════════════ - class TestServerPortNetwork: def test_add_port(self, invoke, mock_client): state = _setup_mock(mock_client) - result = invoke(["server", "add-port", SRV_ID, PORT_ID]) + result = invoke(["server", "add", "port", SRV_ID, PORT_ID]) assert result.exit_code == 0 assert "attached" in result.output or "port" in result.output.lower() body = state["posted"]["last_body"] @@ -753,20 +708,20 @@ def test_add_port(self, invoke, mock_client): def test_remove_port(self, invoke, mock_client): state = _setup_mock(mock_client) - result = invoke(["server", "remove-port", SRV_ID, PORT_ID]) + result = invoke(["server", "remove", "port", SRV_ID, PORT_ID]) assert result.exit_code == 0 assert any(PORT_ID in u for u in state["deleted"]) def test_add_network(self, invoke, mock_client): state = _setup_mock(mock_client) - result = invoke(["server", "add-network", SRV_ID, NET_ID]) + result = invoke(["server", "add", "network", SRV_ID, NET_ID]) assert result.exit_code == 0 body = state["posted"]["last_body"] assert body["interfaceAttachment"]["net_id"] == NET_ID def test_remove_network_found(self, invoke, mock_client): state = _setup_mock(mock_client) - result = invoke(["server", "remove-network", SRV_ID, NET_ID]) + result = invoke(["server", "remove", "network", SRV_ID, NET_ID]) assert result.exit_code == 0 # Should delete the interface with matching net_id assert any("os-interface" in u for u in state["deleted"]) @@ -774,16 +729,14 @@ def test_remove_network_found(self, invoke, mock_client): def test_remove_network_not_found(self, invoke, mock_client): _ = _setup_mock(mock_client) - result = invoke(["server", "remove-network", SRV_ID, "no-such-net"]) + result = invoke(["server", "remove", "network", SRV_ID, "no-such-net"]) assert result.exit_code == 0 assert "No interfaces found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # unset # ══════════════════════════════════════════════════════════════════════════ - class TestServerUnset: def test_unset_property(self, invoke, mock_client): @@ -821,12 +774,10 @@ def test_unset_mixed(self, invoke, mock_client): assert any("metadata/env" in u for u in state["deleted"]) assert any("tags/frontend" in u for u in state["deleted"]) - # ══════════════════════════════════════════════════════════════════════════ # help # ══════════════════════════════════════════════════════════════════════════ - class TestServerHelp: def test_server_help(self, invoke): @@ -835,15 +786,9 @@ def test_server_help(self, invoke): for cmd in ("list", "show", "create", "delete", "start", "stop", "reboot", "pause", "unpause", "suspend", "resume", "lock", "unlock", "rescue", "unrescue", - "shelve", "unshelve", "resize", "rebuild", "rename", - "create-image", "attach-volume", "detach-volume", - "list-volumes", "attach-interface", "detach-interface", - "list-interfaces", "console-log", "console-url", + "shelve", "unshelve", "resize", "rebuild", "rename", "attach-interface", "snapshot", "wait", "bulk", "clone", "diff", "ssh", "port-forward", "password", - "evacuate", "dump-create", "restore", - "add-fixed-ip", "remove-fixed-ip", - "add-port", "remove-port", - "add-network", "remove-network", + "evacuate", "restore", "unset"): assert cmd in result.output diff --git a/tests/test_server_clone_boot_mode.py b/tests/test_server_clone_boot_mode.py index 97f8275..c998db3 100644 --- a/tests/test_server_clone_boot_mode.py +++ b/tests/test_server_clone_boot_mode.py @@ -11,7 +11,6 @@ FLAVOR_WITH_DISK = "flav-disk-20" FLAVOR_DISKLESS = "flav-disk-0" - def _mock_clone_environment(mock_client, flavor_id, flavor_disk): """Wire a mock_client that serves the GETs needed by `server clone`.""" mock_client.compute_url = "https://nova.example.com/v2.1" @@ -54,7 +53,6 @@ def _post(url, **kwargs): mock_client.post = _post return state - class TestCloneAutoDetect: def test_flavor_with_disk_defaults_to_boot_from_image( @@ -86,7 +84,6 @@ def test_diskless_flavor_falls_back_to_bfv( assert "block_device_mapping_v2" in body assert "imageRef" not in body - class TestCloneExplicitFlags: def test_boot_from_volume_override( diff --git a/tests/test_server_create_boot_mode.py b/tests/test_server_create_boot_mode.py index bfee692..ca716d9 100644 --- a/tests/test_server_create_boot_mode.py +++ b/tests/test_server_create_boot_mode.py @@ -8,7 +8,6 @@ FLAVOR_WITH_DISK = "flav-disk-20" FLAVOR_DISKLESS = "flav-disk-0" - def _mock_create_with_flavors(mock_client, flavors): """Wire a mock_client that answers GET /flavors/ and POST /servers.""" mock_client.compute_url = "https://nova.example.com/v2.1" @@ -29,7 +28,6 @@ def _post(url, **kwargs): mock_client.post = _post return state - # ── auto-detection ────────────────────────────────────────────────────────── class TestAutoDetect: @@ -76,7 +74,6 @@ def test_diskless_flavor_falls_back_to_bfv( assert bdm["volume_size"] == 30 assert "imageRef" not in body - # ── explicit flags ────────────────────────────────────────────────────────── class TestExplicitFlags: @@ -163,7 +160,6 @@ def test_mutual_exclusion( assert "mutually exclusive" in result.output assert "body" not in state["posted"] - # ── fallback when flavor lookup fails ─────────────────────────────────────── class TestFlavorLookupFailure: diff --git a/tests/test_server_ssh.py b/tests/test_server_ssh.py index 1ce7acc..6ad336d 100644 --- a/tests/test_server_ssh.py +++ b/tests/test_server_ssh.py @@ -14,7 +14,6 @@ SRV_ID = "11112222-3333-4444-5555-666677778888" IMG_ID = "aaaabbbb-cccc-dddd-eeee-ffffffffffff" - def _srv(**overrides): base = { "id": SRV_ID, @@ -30,7 +29,6 @@ def _srv(**overrides): base.update(overrides) return base - def _setup_profile(mock_client, sample_profile): """Wire mock_client into a saved+active profile so ensure_client works.""" save_profile("test", sample_profile) @@ -38,12 +36,10 @@ def _setup_profile(mock_client, sample_profile): mock_client.compute_url = "https://nova.example.com/v2.1" mock_client.image_url = "https://glance.example.com" - # ══════════════════════════════════════════════════════════════════════════ # _pick_ssh_ip # ══════════════════════════════════════════════════════════════════════════ - class TestPickSshIp: def test_prefers_floating_by_default(self): @@ -63,12 +59,10 @@ def test_floating_fallback_when_prefer_fixed_but_no_fixed(self): def test_none_when_empty(self): assert server_mod._pick_ssh_ip(_srv(addresses={})) is None - # ══════════════════════════════════════════════════════════════════════════ # _detect_ssh_user # ══════════════════════════════════════════════════════════════════════════ - class TestDetectSshUser: def test_ubuntu(self, mock_client): @@ -134,12 +128,10 @@ def test_boot_from_volume_no_metadata_returns_none(self, mock_client): }) assert server_mod._detect_ssh_user(mock_client, srv) is None - # ══════════════════════════════════════════════════════════════════════════ # _find_ssh_key # ══════════════════════════════════════════════════════════════════════════ - class TestFindSshKey: @pytest.fixture @@ -197,12 +189,10 @@ def test_no_ssh_dir_returns_none(self, tmp_path, monkeypatch): monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) assert server_mod._find_ssh_key("any") is None - # ══════════════════════════════════════════════════════════════════════════ # Integration — orca server ssh --dry-run # ══════════════════════════════════════════════════════════════════════════ - class TestServerSshIntegration: def test_dry_run_by_id_auto_user_and_key( diff --git a/tests/test_services_dns.py b/tests/test_services_dns.py index cf9e730..4e14953 100644 --- a/tests/test_services_dns.py +++ b/tests/test_services_dns.py @@ -17,19 +17,16 @@ DESIGNATE = "https://dns.example.com" BASE = f"{DESIGNATE}/v2" # ``with_version()`` appends v2 - @pytest.fixture def dns_client(): client = MagicMock() client.dns_url = DESIGNATE return client - @pytest.fixture def svc(dns_client): return DnsService(dns_client) - # ── zones ────────────────────────────────────────────────────────────── def test_find_zones(dns_client, svc): @@ -38,7 +35,6 @@ def test_find_zones(dns_client, svc): dns_client.get.assert_called_once_with(f"{BASE}/zones") assert out[0]["id"] == "z1" - def test_find_zones_passes_params_and_headers(dns_client, svc): dns_client.get.return_value = {"zones": []} svc.find_zones(params={"name": "x"}, headers={"X-Auth-All-Projects": "true"}) @@ -48,19 +44,16 @@ def test_find_zones_passes_params_and_headers(dns_client, svc): headers={"X-Auth-All-Projects": "true"}, ) - def test_get_zone_unwraps_envelope(dns_client, svc): dns_client.get.return_value = {"zone": {"id": "z1", "name": "ex.com."}} out = svc.get_zone("z1") dns_client.get.assert_called_once_with(f"{BASE}/zones/z1") assert out["name"] == "ex.com." - def test_get_zone_falls_back_when_no_envelope(dns_client, svc): dns_client.get.return_value = {"id": "raw"} assert svc.get_zone("z1") == {"id": "raw"} - def test_create_zone(dns_client, svc): dns_client.post.return_value = {"zone": {"id": "z-new"}} out = svc.create_zone({"name": "ex.com.", "email": "a@b"}) @@ -69,29 +62,24 @@ def test_create_zone(dns_client, svc): ) assert out["id"] == "z-new" - def test_create_zone_handles_none(dns_client, svc): dns_client.post.return_value = None assert svc.create_zone({"name": "ex.com."}) == {} - def test_update_zone_uses_patch(dns_client, svc): dns_client.patch.return_value = {"zone": {"id": "z1", "ttl": 600}} out = svc.update_zone("z1", {"ttl": 600}) dns_client.patch.assert_called_once_with(f"{BASE}/zones/z1", json={"ttl": 600}) assert out["ttl"] == 600 - def test_update_zone_handles_none(dns_client, svc): dns_client.patch.return_value = None assert svc.update_zone("z1", {}) == {} - def test_delete_zone(dns_client, svc): svc.delete_zone("z1") dns_client.delete.assert_called_once_with(f"{BASE}/zones/z1") - # ── recordsets ───────────────────────────────────────────────────────── def test_find_recordsets(dns_client, svc): @@ -101,7 +89,6 @@ def test_find_recordsets(dns_client, svc): f"{BASE}/zones/z1/recordsets", params={"type": "A"} ) - def test_find_all_recordsets(dns_client, svc): dns_client.get.return_value = {"recordsets": [{"id": "r"}]} out = svc.find_all_recordsets(params={"name": "x.ex.com."}) @@ -110,14 +97,12 @@ def test_find_all_recordsets(dns_client, svc): ) assert out[0]["id"] == "r" - def test_get_recordset(dns_client, svc): dns_client.get.return_value = {"recordset": {"id": "r1"}} out = svc.get_recordset("z1", "r1") dns_client.get.assert_called_once_with(f"{BASE}/zones/z1/recordsets/r1") assert out["id"] == "r1" - def test_create_recordset(dns_client, svc): dns_client.post.return_value = {"recordset": {"id": "r1"}} out = svc.create_recordset("z1", {"name": "x.ex.com.", "type": "A"}) @@ -127,12 +112,10 @@ def test_create_recordset(dns_client, svc): ) assert out["id"] == "r1" - def test_create_recordset_handles_none(dns_client, svc): dns_client.post.return_value = None assert svc.create_recordset("z1", {}) == {} - def test_update_recordset_uses_put(dns_client, svc): dns_client.put.return_value = {"recordset": {"id": "r1", "ttl": 60}} out = svc.update_recordset("z1", "r1", {"ttl": 60}) @@ -141,17 +124,14 @@ def test_update_recordset_uses_put(dns_client, svc): ) assert out["ttl"] == 60 - def test_update_recordset_handles_none(dns_client, svc): dns_client.put.return_value = None assert svc.update_recordset("z1", "r1", {}) == {} - def test_delete_recordset(dns_client, svc): svc.delete_recordset("z1", "r1") dns_client.delete.assert_called_once_with(f"{BASE}/zones/z1/recordsets/r1") - # ── export / import ─────────────────────────────────────────────────── def test_export_zone(dns_client, svc): @@ -160,23 +140,19 @@ def test_export_zone(dns_client, svc): dns_client.post.assert_called_once_with(f"{BASE}/zones/z1/tasks/export") assert out["status"] == "PENDING" - def test_export_zone_handles_none(dns_client, svc): dns_client.post.return_value = None assert svc.export_zone("z1") == {} - def test_get_export_task(dns_client, svc): dns_client.get.return_value = {"id": "exp1", "status": "COMPLETE"} svc.get_export_task("exp1") dns_client.get.assert_called_once_with(f"{BASE}/zones/tasks/exports/exp1") - def test_get_export_task_handles_none(dns_client, svc): dns_client.get.return_value = None assert svc.get_export_task("exp1") == {} - def test_fetch_export_text_returns_body(dns_client, svc): resp = MagicMock(status_code=200, text="$ORIGIN ex.com.\n") cm = MagicMock() @@ -191,7 +167,6 @@ def test_fetch_export_text_returns_body(dns_client, svc): ) assert "ex.com" in out - def test_fetch_export_text_raises_apierror_on_failure(dns_client, svc): resp = MagicMock(status_code=500, text="boom") cm = MagicMock() @@ -202,7 +177,6 @@ def test_fetch_export_text_raises_apierror_on_failure(dns_client, svc): with pytest.raises(APIError): svc.fetch_export_text("exp1") - def test_import_zone_text_posts_with_text_dns(dns_client, svc): resp = MagicMock(status_code=202) resp.json.return_value = {"id": "imp1", "status": "PENDING"} @@ -216,7 +190,6 @@ def test_import_zone_text_posts_with_text_dns(dns_client, svc): assert kwargs["content"].startswith(b"$ORIGIN") assert out["id"] == "imp1" - def test_import_zone_text_accepts_bytes(dns_client, svc): resp = MagicMock(status_code=200) resp.json.return_value = {"id": "imp1"} @@ -225,32 +198,27 @@ def test_import_zone_text_accepts_bytes(dns_client, svc): args, kwargs = dns_client.post_stream.call_args assert kwargs["content"] == b"already-bytes" - def test_import_zone_text_raises_on_bad_status(dns_client, svc): resp = MagicMock(status_code=400, text="bad zone") dns_client.post_stream = MagicMock(return_value=resp) with pytest.raises(APIError): svc.import_zone_text("garbage") - def test_import_zone_text_handles_invalid_json_body(dns_client, svc): resp = MagicMock(status_code=202) resp.json.side_effect = ValueError dns_client.post_stream = MagicMock(return_value=resp) assert svc.import_zone_text("...") == {} - def test_get_import_task(dns_client, svc): dns_client.get.return_value = {"id": "imp1", "status": "PENDING"} svc.get_import_task("imp1") dns_client.get.assert_called_once_with(f"{BASE}/zones/tasks/imports/imp1") - def test_get_import_task_handles_none(dns_client, svc): dns_client.get.return_value = None assert svc.get_import_task("imp1") == {} - # ── transfer requests ───────────────────────────────────────────────── def test_create_transfer_request(dns_client, svc): @@ -262,19 +230,16 @@ def test_create_transfer_request(dns_client, svc): ) assert out["key"] == "ABC" - def test_create_transfer_request_handles_none(dns_client, svc): dns_client.post.return_value = None assert svc.create_transfer_request("z1", {}) == {} - def test_find_transfer_requests(dns_client, svc): dns_client.get.return_value = {"transfer_requests": [{"id": "tr1"}]} out = svc.find_transfer_requests() dns_client.get.assert_called_once_with(f"{BASE}/zones/tasks/transfer_requests") assert out[0]["id"] == "tr1" - def test_get_transfer_request(dns_client, svc): dns_client.get.return_value = {"transfer_request": {"id": "tr1"}} svc.get_transfer_request("tr1") @@ -282,14 +247,12 @@ def test_get_transfer_request(dns_client, svc): f"{BASE}/zones/tasks/transfer_requests/tr1" ) - def test_delete_transfer_request(dns_client, svc): svc.delete_transfer_request("tr1") dns_client.delete.assert_called_once_with( f"{BASE}/zones/tasks/transfer_requests/tr1" ) - def test_accept_transfer(dns_client, svc): dns_client.post.return_value = {"id": "ta1"} svc.accept_transfer({"key": "ABC", "zone_transfer_request_id": "tr1"}) @@ -298,12 +261,10 @@ def test_accept_transfer(dns_client, svc): json={"key": "ABC", "zone_transfer_request_id": "tr1"}, ) - def test_accept_transfer_handles_none(dns_client, svc): dns_client.post.return_value = None assert svc.accept_transfer({}) == {} - # ── reverse / TLDs / abandon / axfr ─────────────────────────────────── def test_find_reverse_floatingips(dns_client, svc): @@ -312,40 +273,33 @@ def test_find_reverse_floatingips(dns_client, svc): dns_client.get.assert_called_once_with(f"{BASE}/reverse/floatingips") assert out[0]["id"] == "fip1" - def test_find_tlds(dns_client, svc): dns_client.get.return_value = {"tlds": [{"id": "t1", "name": "com"}]} svc.find_tlds() dns_client.get.assert_called_once_with(f"{BASE}/tlds") - def test_create_tld(dns_client, svc): dns_client.post.return_value = {"tld": {"id": "t1"}} out = svc.create_tld({"name": "test"}) dns_client.post.assert_called_once_with(f"{BASE}/tlds", json={"name": "test"}) assert out["id"] == "t1" - def test_create_tld_handles_none(dns_client, svc): dns_client.post.return_value = None assert svc.create_tld({}) == {} - def test_delete_tld(dns_client, svc): svc.delete_tld("t1") dns_client.delete.assert_called_once_with(f"{BASE}/tlds/t1") - def test_abandon_zone(dns_client, svc): svc.abandon_zone("z1") dns_client.post.assert_called_once_with(f"{BASE}/zones/z1/tasks/abandon") - def test_axfr_zone(dns_client, svc): svc.axfr_zone("z1") dns_client.post.assert_called_once_with(f"{BASE}/zones/z1/tasks/xfr") - # ── shares ──────────────────────────────────────────────────────────── def test_find_shares(dns_client, svc): @@ -354,19 +308,16 @@ def test_find_shares(dns_client, svc): dns_client.get.assert_called_once_with(f"{BASE}/zones/z1/shares") assert out[0]["id"] == "sh1" - def test_get_share(dns_client, svc): dns_client.get.return_value = {"shared_zone": {"id": "sh1"}} out = svc.get_share("z1", "sh1") dns_client.get.assert_called_once_with(f"{BASE}/zones/z1/shares/sh1") assert out["id"] == "sh1" - def test_get_share_handles_unexpected(dns_client, svc): dns_client.get.return_value = None assert svc.get_share("z1", "sh1") == {} - def test_create_share(dns_client, svc): dns_client.post.return_value = {"shared_zone": {"id": "sh1"}} out = svc.create_share("z1", "target-project") @@ -376,17 +327,14 @@ def test_create_share(dns_client, svc): ) assert out["id"] == "sh1" - def test_create_share_handles_none(dns_client, svc): dns_client.post.return_value = None assert svc.create_share("z1", "p2") == {} - def test_delete_share(dns_client, svc): svc.delete_share("z1", "sh1") dns_client.delete.assert_called_once_with(f"{BASE}/zones/z1/shares/sh1") - # ── blacklists ─────────────────────────────────────────────────────── def test_find_blacklists(dns_client, svc): @@ -394,19 +342,16 @@ def test_find_blacklists(dns_client, svc): svc.find_blacklists() dns_client.get.assert_called_once_with(f"{BASE}/blacklists") - def test_get_blacklist(dns_client, svc): dns_client.get.return_value = {"blacklist": {"id": "bl1"}} out = svc.get_blacklist("bl1") dns_client.get.assert_called_once_with(f"{BASE}/blacklists/bl1") assert out["id"] == "bl1" - def test_get_blacklist_handles_unexpected(dns_client, svc): dns_client.get.return_value = None assert svc.get_blacklist("bl1") == {} - def test_create_blacklist(dns_client, svc): dns_client.post.return_value = {"blacklist": {"id": "bl1"}} out = svc.create_blacklist({"pattern": ".*\\.bad\\.com\\."}) @@ -415,12 +360,10 @@ def test_create_blacklist(dns_client, svc): ) assert out["id"] == "bl1" - def test_create_blacklist_handles_none(dns_client, svc): dns_client.post.return_value = None assert svc.create_blacklist({}) == {} - def test_update_blacklist(dns_client, svc): dns_client.patch.return_value = {"blacklist": {"id": "bl1"}} out = svc.update_blacklist("bl1", {"description": "new"}) @@ -429,17 +372,14 @@ def test_update_blacklist(dns_client, svc): ) assert out["id"] == "bl1" - def test_update_blacklist_handles_none(dns_client, svc): dns_client.patch.return_value = None assert svc.update_blacklist("bl1", {}) == {} - def test_delete_blacklist(dns_client, svc): svc.delete_blacklist("bl1") dns_client.delete.assert_called_once_with(f"{BASE}/blacklists/bl1") - # ── exports / imports listings ──────────────────────────────────────── def test_find_exports(dns_client, svc): @@ -448,19 +388,16 @@ def test_find_exports(dns_client, svc): dns_client.get.assert_called_once_with(f"{BASE}/zones/tasks/exports") assert out[0]["id"] == "e1" - def test_delete_export(dns_client, svc): svc.delete_export("e1") dns_client.delete.assert_called_once_with(f"{BASE}/zones/tasks/exports/e1") - def test_find_imports(dns_client, svc): dns_client.get.return_value = {"imports": [{"id": "i1"}]} out = svc.find_imports() dns_client.get.assert_called_once_with(f"{BASE}/zones/tasks/imports") assert out[0]["id"] == "i1" - def test_delete_import(dns_client, svc): svc.delete_import("i1") dns_client.delete.assert_called_once_with(f"{BASE}/zones/tasks/imports/i1") diff --git a/tests/test_services_image.py b/tests/test_services_image.py index 930a786..b58e8c4 100644 --- a/tests/test_services_image.py +++ b/tests/test_services_image.py @@ -17,19 +17,16 @@ GLANCE = "https://glance.example.com/v2" - @pytest.fixture def glance_client(): client = MagicMock() client.image_url = GLANCE return client - @pytest.fixture def svc(glance_client): return ImageService(glance_client) - def _resp(status_code: int = 201, text: str = "") -> MagicMock: r = MagicMock() r.status_code = status_code @@ -37,7 +34,6 @@ def _resp(status_code: int = 201, text: str = "") -> MagicMock: r.text = text return r - # ── _stream_put status mapping ───────────────────────────────────────── def test_stream_put_success(glance_client, svc): @@ -46,39 +42,32 @@ def test_stream_put_success(glance_client, svc): svc._stream_put(f"{GLANCE}/images/i1/file", content=b"x", content_length=1) glance_client.put_stream.assert_called_once() - def test_stream_put_401_raises_authentication_error(glance_client, svc): glance_client.put_stream.return_value = _resp(401) with pytest.raises(AuthenticationError): svc._stream_put(f"{GLANCE}/images/i1/file", content=b"x", content_length=1) - def test_stream_put_403_raises_permission_denied(glance_client, svc): glance_client.put_stream.return_value = _resp(403) with pytest.raises(PermissionDeniedError): svc._stream_put(f"{GLANCE}/images/i1/file", content=b"x", content_length=1) - def test_stream_put_other_4xx_raises_api_error(glance_client, svc): glance_client.put_stream.return_value = _resp(413, text="too large") with pytest.raises(APIError): svc._stream_put(f"{GLANCE}/images/i1/file", content=b"x", content_length=1) - # ── URL helpers ──────────────────────────────────────────────────────── def test_download_url(svc): assert svc.download_url("img-1") == f"{GLANCE}/images/img-1/file" - def test_upload_url(svc): assert svc.upload_url("img-1") == f"{GLANCE}/images/img-1/file" - def test_stage_url(svc): assert svc.stage_url("img-1") == f"{GLANCE}/images/img-1/stage" - def test_stream_download_returns_get_stream_context(glance_client, svc): cm = MagicMock() glance_client.get_stream.return_value = cm @@ -86,24 +75,20 @@ def test_stream_download_returns_get_stream_context(glance_client, svc): glance_client.get_stream.assert_called_once_with(f"{GLANCE}/images/img-1/file") assert out is cm - # ── get_distro fallbacks ────────────────────────────────────────────── def test_get_distro_lowercases(glance_client, svc): glance_client.get.return_value = {"os_distro": "Ubuntu"} assert svc.get_distro("img-1") == "ubuntu" - def test_get_distro_returns_empty_when_attr_missing(glance_client, svc): glance_client.get.return_value = {"name": "no-distro"} assert svc.get_distro("img-1") == "" - def test_get_distro_swallows_exception(glance_client, svc): glance_client.get.side_effect = APIError(500, "server down") assert svc.get_distro("img-1") == "" - # ── metadef: namespaces ─────────────────────────────────────────────── def test_find_metadef_namespaces(glance_client, svc): @@ -112,7 +97,6 @@ def test_find_metadef_namespaces(glance_client, svc): glance_client.get.assert_called_once_with(f"{GLANCE}/metadefs/namespaces", params=None) assert out[0]["namespace"] == "OS::Compute::Hypervisor" - def test_find_metadef_namespaces_with_params(glance_client, svc): glance_client.get.return_value = {"namespaces": []} svc.find_metadef_namespaces(params={"resource_types": "OS::Glance::Image"}) @@ -121,7 +105,6 @@ def test_find_metadef_namespaces_with_params(glance_client, svc): params={"resource_types": "OS::Glance::Image"}, ) - def test_get_metadef_namespace(glance_client, svc): glance_client.get.return_value = {"namespace": "OS::Compute::Quota"} svc.get_metadef_namespace("OS::Compute::Quota") @@ -129,12 +112,10 @@ def test_get_metadef_namespace(glance_client, svc): f"{GLANCE}/metadefs/namespaces/OS::Compute::Quota" ) - def test_get_metadef_namespace_handles_none(glance_client, svc): glance_client.get.return_value = None assert svc.get_metadef_namespace("X") == {} - def test_create_metadef_namespace(glance_client, svc): glance_client.post.return_value = {"namespace": "ns1"} out = svc.create_metadef_namespace({"namespace": "ns1", "display_name": "NS"}) @@ -144,12 +125,10 @@ def test_create_metadef_namespace(glance_client, svc): ) assert out["namespace"] == "ns1" - def test_create_metadef_namespace_handles_none(glance_client, svc): glance_client.post.return_value = None assert svc.create_metadef_namespace({}) == {} - def test_update_metadef_namespace(glance_client, svc): glance_client.put.return_value = {"namespace": "ns1", "visibility": "public"} out = svc.update_metadef_namespace("ns1", {"visibility": "public"}) @@ -159,17 +138,14 @@ def test_update_metadef_namespace(glance_client, svc): ) assert out["visibility"] == "public" - def test_update_metadef_namespace_handles_none(glance_client, svc): glance_client.put.return_value = None assert svc.update_metadef_namespace("ns1", {}) == {} - def test_delete_metadef_namespace(glance_client, svc): svc.delete_metadef_namespace("ns1") glance_client.delete.assert_called_once_with(f"{GLANCE}/metadefs/namespaces/ns1") - # ── metadef: objects ────────────────────────────────────────────────── def test_find_metadef_objects(glance_client, svc): @@ -178,7 +154,6 @@ def test_find_metadef_objects(glance_client, svc): glance_client.get.assert_called_once_with(f"{GLANCE}/metadefs/namespaces/ns1/objects") assert out[0]["name"] == "obj1" - def test_get_metadef_object(glance_client, svc): glance_client.get.return_value = {"name": "obj1"} out = svc.get_metadef_object("ns1", "obj1") @@ -187,12 +162,10 @@ def test_get_metadef_object(glance_client, svc): ) assert out["name"] == "obj1" - def test_get_metadef_object_handles_none(glance_client, svc): glance_client.get.return_value = None assert svc.get_metadef_object("ns1", "obj1") == {} - def test_create_metadef_object(glance_client, svc): glance_client.post.return_value = {"name": "obj1"} out = svc.create_metadef_object("ns1", {"name": "obj1", "properties": {}}) @@ -202,12 +175,10 @@ def test_create_metadef_object(glance_client, svc): ) assert out["name"] == "obj1" - def test_create_metadef_object_handles_none(glance_client, svc): glance_client.post.return_value = None assert svc.create_metadef_object("ns1", {}) == {} - def test_update_metadef_object(glance_client, svc): glance_client.put.return_value = {"name": "obj1", "description": "new"} out = svc.update_metadef_object("ns1", "obj1", {"description": "new"}) @@ -217,19 +188,16 @@ def test_update_metadef_object(glance_client, svc): ) assert out["description"] == "new" - def test_update_metadef_object_handles_none(glance_client, svc): glance_client.put.return_value = None assert svc.update_metadef_object("ns1", "obj1", {}) == {} - def test_delete_metadef_object(glance_client, svc): svc.delete_metadef_object("ns1", "obj1") glance_client.delete.assert_called_once_with( f"{GLANCE}/metadefs/namespaces/ns1/objects/obj1" ) - # ── metadef: properties (special parsing — Glance returns a dict) ───── def test_find_metadef_properties_unwraps_dict_into_list(glance_client, svc): @@ -249,19 +217,16 @@ def test_find_metadef_properties_unwraps_dict_into_list(glance_client, svc): by_name = {p["name"]: p for p in out} assert by_name["hw_disk_bus"]["title"] == "Disk Bus" - def test_find_metadef_properties_accepts_list_shape(glance_client, svc): """Some servers / mocks return a list; the wrapper must tolerate it.""" glance_client.get.return_value = {"properties": [{"name": "x"}]} out = svc.find_metadef_properties("ns1") assert out == [{"name": "x"}] - def test_find_metadef_properties_returns_empty_on_unexpected(glance_client, svc): glance_client.get.return_value = {"properties": 123} assert svc.find_metadef_properties("ns1") == [] - def test_get_metadef_property(glance_client, svc): glance_client.get.return_value = {"name": "hw_disk_bus", "type": "string"} out = svc.get_metadef_property("ns1", "hw_disk_bus") @@ -270,12 +235,10 @@ def test_get_metadef_property(glance_client, svc): ) assert out["type"] == "string" - def test_get_metadef_property_handles_none(glance_client, svc): glance_client.get.return_value = None assert svc.get_metadef_property("ns1", "x") == {} - def test_create_metadef_property(glance_client, svc): glance_client.post.return_value = {"name": "p1"} out = svc.create_metadef_property("ns1", {"name": "p1", "type": "integer"}) @@ -285,12 +248,10 @@ def test_create_metadef_property(glance_client, svc): ) assert out["name"] == "p1" - def test_create_metadef_property_handles_none(glance_client, svc): glance_client.post.return_value = None assert svc.create_metadef_property("ns1", {}) == {} - def test_update_metadef_property(glance_client, svc): glance_client.put.return_value = {"name": "p1", "title": "Updated"} out = svc.update_metadef_property("ns1", "p1", {"title": "Updated"}) @@ -300,19 +261,16 @@ def test_update_metadef_property(glance_client, svc): ) assert out["title"] == "Updated" - def test_update_metadef_property_handles_none(glance_client, svc): glance_client.put.return_value = None assert svc.update_metadef_property("ns1", "p1", {}) == {} - def test_delete_metadef_property(glance_client, svc): svc.delete_metadef_property("ns1", "p1") glance_client.delete.assert_called_once_with( f"{GLANCE}/metadefs/namespaces/ns1/properties/p1" ) - # ── metadef: resource-type associations ────────────────────────────── def test_find_metadef_resource_types(glance_client, svc): @@ -321,7 +279,6 @@ def test_find_metadef_resource_types(glance_client, svc): glance_client.get.assert_called_once_with(f"{GLANCE}/metadefs/resource_types") assert out[0]["name"] == "OS::Glance::Image" - def test_find_metadef_resource_type_associations(glance_client, svc): glance_client.get.return_value = { "resource_type_associations": [{"name": "OS::Glance::Image"}] @@ -332,7 +289,6 @@ def test_find_metadef_resource_type_associations(glance_client, svc): ) assert out[0]["name"] == "OS::Glance::Image" - def test_create_metadef_resource_type_association(glance_client, svc): glance_client.post.return_value = {"name": "OS::Glance::Image"} out = svc.create_metadef_resource_type_association( @@ -344,12 +300,10 @@ def test_create_metadef_resource_type_association(glance_client, svc): ) assert out["name"] == "OS::Glance::Image" - def test_create_metadef_resource_type_association_handles_none(glance_client, svc): glance_client.post.return_value = None assert svc.create_metadef_resource_type_association("ns1", {}) == {} - def test_delete_metadef_resource_type_association(glance_client, svc): svc.delete_metadef_resource_type_association("ns1", "OS::Glance::Image") glance_client.delete.assert_called_once_with( diff --git a/tests/test_services_object_store.py b/tests/test_services_object_store.py index 72d7f9b..aa84411 100644 --- a/tests/test_services_object_store.py +++ b/tests/test_services_object_store.py @@ -16,7 +16,6 @@ SWIFT = "https://swift.example.com/v1/AUTH_5678" - def _ok_resp(status_code: int = 200, headers: dict | None = None) -> MagicMock: resp = MagicMock() resp.status_code = status_code @@ -25,19 +24,16 @@ def _ok_resp(status_code: int = 200, headers: dict | None = None) -> MagicMock: resp.text = "" return resp - @pytest.fixture def swift_client(): client = MagicMock() client.object_store_url = SWIFT return client - @pytest.fixture def svc(swift_client): return ObjectStoreService(swift_client) - # ── status mapping ───────────────────────────────────────────────────── def test_head_account_maps_401_to_authentication_error(swift_client, svc): @@ -45,13 +41,11 @@ def test_head_account_maps_401_to_authentication_error(swift_client, svc): with pytest.raises(AuthenticationError): svc.head_account() - def test_head_account_maps_403_to_permission_denied(swift_client, svc): swift_client.head_request.return_value = _ok_resp(403) with pytest.raises(PermissionDeniedError): svc.head_account() - def test_head_account_maps_other_4xx_to_api_error(swift_client, svc): resp = _ok_resp(404) resp.text = "Account not found" @@ -60,7 +54,6 @@ def test_head_account_maps_other_4xx_to_api_error(swift_client, svc): svc.head_account() assert "404" in str(exc.value) or "not found" in str(exc.value).lower() - # ── account ─────────────────────────────────────────────────────────── def test_head_account_returns_headers_as_dict(swift_client, svc): @@ -71,7 +64,6 @@ def test_head_account_returns_headers_as_dict(swift_client, svc): swift_client.head_request.assert_called_once_with(SWIFT) assert out["X-Account-Container-Count"] == "3" - def test_post_account_metadata_uses_post_no_body(swift_client, svc): swift_client.post_no_body.return_value = _ok_resp(204) svc.post_account_metadata({"X-Account-Meta-Owner": "alice"}) @@ -79,7 +71,6 @@ def test_post_account_metadata_uses_post_no_body(swift_client, svc): SWIFT, extra_headers={"X-Account-Meta-Owner": "alice"} ) - # ── containers ──────────────────────────────────────────────────────── def test_find_containers(swift_client, svc): @@ -88,12 +79,10 @@ def test_find_containers(swift_client, svc): swift_client.get.assert_called_once_with(f"{SWIFT}?format=json") assert [c["name"] for c in out] == ["c1", "c2"] - def test_find_containers_handles_unexpected_shape(swift_client, svc): swift_client.get.return_value = {"unexpected": "dict"} assert svc.find_containers() == [] - def test_head_container(swift_client, svc): swift_client.head_request.return_value = _ok_resp( 200, headers={"X-Container-Object-Count": "5"} @@ -102,7 +91,6 @@ def test_head_container(swift_client, svc): swift_client.head_request.assert_called_once_with(f"{SWIFT}/c1") assert out["X-Container-Object-Count"] == "5" - def test_create_container_puts_empty_stream(swift_client, svc): swift_client.put_stream.return_value = _ok_resp(201) svc.create_container("c1") @@ -111,14 +99,12 @@ def test_create_container_puts_empty_stream(swift_client, svc): assert kwargs["content"] == b"" assert kwargs["content_length"] == 0 - def test_create_container_passes_extra_headers(swift_client, svc): swift_client.put_stream.return_value = _ok_resp(201) svc.create_container("c1", headers={"X-Container-Read": ".r:*"}) args, kwargs = swift_client.put_stream.call_args assert kwargs["extra_headers"] == {"X-Container-Read": ".r:*"} - def test_create_container_raises_on_failure(swift_client, svc): resp = _ok_resp(409) resp.text = "Conflict" @@ -126,12 +112,10 @@ def test_create_container_raises_on_failure(swift_client, svc): with pytest.raises(APIError): svc.create_container("c1") - def test_delete_container(swift_client, svc): svc.delete_container("c1") swift_client.delete.assert_called_once_with(f"{SWIFT}/c1") - def test_post_container_metadata(swift_client, svc): swift_client.post_no_body.return_value = _ok_resp(204) svc.post_container_metadata("c1", {"X-Container-Meta-Owner": "bob"}) @@ -139,7 +123,6 @@ def test_post_container_metadata(swift_client, svc): f"{SWIFT}/c1", extra_headers={"X-Container-Meta-Owner": "bob"} ) - # ── objects ─────────────────────────────────────────────────────────── def test_find_objects_basic(swift_client, svc): @@ -147,7 +130,6 @@ def test_find_objects_basic(swift_client, svc): svc.find_objects("c1") swift_client.get.assert_called_once_with(f"{SWIFT}/c1?format=json") - def test_find_objects_with_prefix_and_delimiter(swift_client, svc): swift_client.get.return_value = [] svc.find_objects("c1", prefix="dir/", delimiter="/") @@ -155,12 +137,10 @@ def test_find_objects_with_prefix_and_delimiter(swift_client, svc): f"{SWIFT}/c1?format=json&prefix=dir/&delimiter=/" ) - def test_find_objects_handles_dict_response(swift_client, svc): swift_client.get.return_value = {"unexpected": "dict"} assert svc.find_objects("c1") == [] - def test_head_object(swift_client, svc): swift_client.head_request.return_value = _ok_resp( 200, headers={"Content-Length": "42"} @@ -169,12 +149,10 @@ def test_head_object(swift_client, svc): swift_client.head_request.assert_called_once_with(f"{SWIFT}/c1/o1") assert out["Content-Length"] == "42" - def test_delete_object(swift_client, svc): svc.delete_object("c1", "o1") swift_client.delete.assert_called_once_with(f"{SWIFT}/c1/o1") - def test_post_object_metadata(swift_client, svc): swift_client.post_no_body.return_value = _ok_resp(202) svc.post_object_metadata("c1", "o1", {"X-Object-Meta-Tag": "draft"}) @@ -182,7 +160,6 @@ def test_post_object_metadata(swift_client, svc): f"{SWIFT}/c1/o1", extra_headers={"X-Object-Meta-Tag": "draft"} ) - # ── streaming I/O ────────────────────────────────────────────────────── def test_upload_object_default_args(swift_client, svc): @@ -194,7 +171,6 @@ def test_upload_object_default_args(swift_client, svc): assert kwargs["content_type"] == "application/octet-stream" assert kwargs["content_length"] is None - def test_upload_object_with_query(swift_client, svc): """SLO manifest uploads pass ?multipart-manifest=put.""" swift_client.put_stream.return_value = _ok_resp(201) @@ -208,13 +184,11 @@ def test_upload_object_with_query(swift_client, svc): assert kwargs["content_type"] == "application/json" assert kwargs["content_length"] == 2 - def test_upload_object_raises_on_failure(swift_client, svc): swift_client.put_stream.return_value = _ok_resp(507) with pytest.raises(APIError): svc.upload_object("c1", "o1", content=b"") - def test_download_object_returns_stream_context(swift_client, svc): cm = MagicMock() swift_client.get_stream.return_value = cm @@ -222,7 +196,6 @@ def test_download_object_returns_stream_context(swift_client, svc): swift_client.get_stream.assert_called_once_with(f"{SWIFT}/c1/o1") assert out is cm - def test_fetch_object_bytes_returns_content(swift_client, svc): resp = _ok_resp(200) resp.read = MagicMock() @@ -233,7 +206,6 @@ def test_fetch_object_bytes_returns_content(swift_client, svc): swift_client.get_stream.return_value = cm assert svc.fetch_object_bytes("c1", "o1") == b"hello world" - def test_fetch_object_bytes_raises_on_failure(swift_client, svc): resp = _ok_resp(403) resp.read = MagicMock() @@ -244,6 +216,5 @@ def test_fetch_object_bytes_raises_on_failure(swift_client, svc): with pytest.raises(PermissionDeniedError): svc.fetch_object_bytes("c1", "o1") - def test_object_url_returns_canonical_path(svc): assert svc.object_url("c1", "path/to/o1") == f"{SWIFT}/c1/path/to/o1" diff --git a/tests/test_services_orchestration.py b/tests/test_services_orchestration.py index 9b6eb79..9162d31 100644 --- a/tests/test_services_orchestration.py +++ b/tests/test_services_orchestration.py @@ -16,19 +16,16 @@ HEAT = "https://heat.example.com/v1/project-uuid-5678" - @pytest.fixture def heat_client(): client = MagicMock() client.orchestration_url = HEAT return client - @pytest.fixture def svc(heat_client): return OrchestrationService(heat_client) - # ── stacks (CRUD) ──────────────────────────────────────────────────────── def test_find_returns_stacks_list(heat_client, svc): @@ -37,7 +34,6 @@ def test_find_returns_stacks_list(heat_client, svc): heat_client.get.assert_called_once_with(f"{HEAT}/stacks", params=None, headers=None) assert [s["id"] for s in out] == ["s1", "s2"] - def test_find_passes_params_and_headers(heat_client, svc): heat_client.get.return_value = {"stacks": []} svc.find(params={"status": "COMPLETE"}, headers={"X": "1"}) @@ -45,7 +41,6 @@ def test_find_passes_params_and_headers(heat_client, svc): f"{HEAT}/stacks", params={"status": "COMPLETE"}, headers={"X": "1"} ) - def test_find_all_paginates(heat_client, svc): heat_client.paginate.return_value = [{"id": "a"}, {"id": "b"}] out = svc.find_all(page_size=50, params={"q": "1"}) @@ -54,31 +49,26 @@ def test_find_all_paginates(heat_client, svc): ) assert len(out) == 2 - def test_get_by_name_only_uses_canonical_url(heat_client, svc): heat_client.get.return_value = {"stack": {"id": "abc"}} out = svc.get("my-stack") heat_client.get.assert_called_once_with(f"{HEAT}/stacks/my-stack") assert out == {"id": "abc"} - def test_get_with_stack_id_uses_canonical_path(heat_client, svc): heat_client.get.return_value = {"stack": {"id": "abc"}} svc.get("my-stack", "abc") heat_client.get.assert_called_once_with(f"{HEAT}/stacks/my-stack/abc") - def test_get_unwraps_stack_envelope(heat_client, svc): heat_client.get.return_value = {"stack": {"id": "abc", "name": "my-stack"}} assert svc.get("my-stack")["name"] == "my-stack" - def test_get_falls_back_when_no_envelope(heat_client, svc): # Some Heat error shapes return a flat dict with no ``stack`` key. heat_client.get.return_value = {"id": "raw"} assert svc.get("x") == {"id": "raw"} - def test_create_posts_body_and_returns_stack(heat_client, svc): heat_client.post.return_value = {"stack": {"id": "new"}} out = svc.create({"stack_name": "n", "template": "t"}) @@ -87,24 +77,20 @@ def test_create_posts_body_and_returns_stack(heat_client, svc): ) assert out["id"] == "new" - def test_create_handles_empty_response(heat_client, svc): heat_client.post.return_value = None assert svc.create({"stack_name": "n"}) == {} - def test_update_calls_put(heat_client, svc): svc.update("n", "id-1", {"template": "t2"}) heat_client.put.assert_called_once_with( f"{HEAT}/stacks/n/id-1", json={"template": "t2"} ) - def test_delete_calls_delete(heat_client, svc): svc.delete("n", "id-1") heat_client.delete.assert_called_once_with(f"{HEAT}/stacks/n/id-1") - def test_action_posts_to_actions_endpoint(heat_client, svc): heat_client.post.return_value = {} svc.action("n", "id-1", {"suspend": None}) @@ -112,14 +98,12 @@ def test_action_posts_to_actions_endpoint(heat_client, svc): f"{HEAT}/stacks/n/id-1/actions", json={"suspend": None} ) - def test_abandon_calls_delete_on_abandon(heat_client, svc): heat_client.delete.return_value = {"stack_data": "..."} out = svc.abandon("n", "id-1") heat_client.delete.assert_called_once_with(f"{HEAT}/stacks/n/id-1/abandon") assert "stack_data" in out - # ── resources & events ────────────────────────────────────────────────── def test_find_resources_unwraps_envelope(heat_client, svc): @@ -128,14 +112,12 @@ def test_find_resources_unwraps_envelope(heat_client, svc): heat_client.get.assert_called_once_with(f"{HEAT}/stacks/n/id-1/resources") assert out[0]["name"] == "r1" - def test_get_resource_unwraps_envelope(heat_client, svc): heat_client.get.return_value = {"resource": {"name": "r1", "type": "T"}} out = svc.get_resource("n", "id-1", "r1") heat_client.get.assert_called_once_with(f"{HEAT}/stacks/n/id-1/resources/r1") assert out["type"] == "T" - def test_find_events_passes_params(heat_client, svc): heat_client.get.return_value = {"events": [{"id": "e1"}]} svc.find_events("n", "id-1", params={"limit": 50}) @@ -143,7 +125,6 @@ def test_find_events_passes_params(heat_client, svc): f"{HEAT}/stacks/n/id-1/events", params={"limit": 50} ) - def test_get_event_uses_long_path(heat_client, svc): heat_client.get.return_value = {"event": {"id": "e1"}} out = svc.get_event("n", "id-1", "r1", "e1") @@ -152,7 +133,6 @@ def test_get_event_uses_long_path(heat_client, svc): ) assert out["id"] == "e1" - # ── outputs ──────────────────────────────────────────────────────────── def test_find_outputs(heat_client, svc): @@ -161,14 +141,12 @@ def test_find_outputs(heat_client, svc): heat_client.get.assert_called_once_with(f"{HEAT}/stacks/n/id-1/outputs") assert out[0]["output_key"] == "k1" - def test_get_output(heat_client, svc): heat_client.get.return_value = {"output": {"output_key": "k1", "output_value": "v"}} out = svc.get_output("n", "id-1", "k1") heat_client.get.assert_called_once_with(f"{HEAT}/stacks/n/id-1/outputs/k1") assert out["output_value"] == "v" - # ── templates / resource types ────────────────────────────────────────── def test_get_template(heat_client, svc): @@ -177,7 +155,6 @@ def test_get_template(heat_client, svc): heat_client.get.assert_called_once_with(f"{HEAT}/stacks/n/id-1/template") assert "heat_template_version" in out - def test_validate_template_returns_dict(heat_client, svc): heat_client.post.return_value = {"Description": "ok"} out = svc.validate_template({"template": "..."}) @@ -186,12 +163,10 @@ def test_validate_template_returns_dict(heat_client, svc): ) assert out == {"Description": "ok"} - def test_validate_template_handles_none(heat_client, svc): heat_client.post.return_value = None assert svc.validate_template({}) == {} - def test_find_resource_types(heat_client, svc): heat_client.get.return_value = {"resource_types": ["OS::Nova::Server"]} out = svc.find_resource_types(params={"name": "Server"}) @@ -200,7 +175,6 @@ def test_find_resource_types(heat_client, svc): ) assert out == ["OS::Nova::Server"] - def test_get_resource_type_template(heat_client, svc): heat_client.get.return_value = {"properties": {}} out = svc.get_resource_type_template("OS::Nova::Server", @@ -211,12 +185,10 @@ def test_get_resource_type_template(heat_client, svc): ) assert out == {"properties": {}} - def test_get_resource_type_template_handles_none(heat_client, svc): heat_client.get.return_value = None assert svc.get_resource_type_template("X") == {} - # ── snapshots ────────────────────────────────────────────────────────── def test_create_snapshot_without_name_sends_empty_body(heat_client, svc): @@ -226,7 +198,6 @@ def test_create_snapshot_without_name_sends_empty_body(heat_client, svc): f"{HEAT}/stacks/n/id-1/snapshots", json={} ) - def test_create_snapshot_with_name_sets_name_in_body(heat_client, svc): heat_client.post.return_value = {"id": "snap1"} svc.create_snapshot("n", "id-1", "daily") @@ -234,48 +205,40 @@ def test_create_snapshot_with_name_sets_name_in_body(heat_client, svc): f"{HEAT}/stacks/n/id-1/snapshots", json={"name": "daily"} ) - def test_create_snapshot_returns_empty_on_none_response(heat_client, svc): heat_client.post.return_value = None assert svc.create_snapshot("n", "id-1") == {} - def test_find_snapshots(heat_client, svc): heat_client.get.return_value = {"snapshots": [{"id": "s1"}, {"id": "s2"}]} out = svc.find_snapshots("n", "id-1") heat_client.get.assert_called_once_with(f"{HEAT}/stacks/n/id-1/snapshots") assert [s["id"] for s in out] == ["s1", "s2"] - def test_find_snapshots_handles_unexpected_shape(heat_client, svc): heat_client.get.return_value = "not a dict" assert svc.find_snapshots("n", "id-1") == [] - def test_get_snapshot_unwraps_envelope(heat_client, svc): heat_client.get.return_value = {"snapshot": {"id": "s1", "status": "COMPLETE"}} out = svc.get_snapshot("n", "id-1", "s1") heat_client.get.assert_called_once_with(f"{HEAT}/stacks/n/id-1/snapshots/s1") assert out["status"] == "COMPLETE" - def test_get_snapshot_returns_empty_on_unexpected_shape(heat_client, svc): heat_client.get.return_value = None assert svc.get_snapshot("n", "id-1", "s1") == {} - def test_delete_snapshot(heat_client, svc): svc.delete_snapshot("n", "id-1", "s1") heat_client.delete.assert_called_once_with(f"{HEAT}/stacks/n/id-1/snapshots/s1") - def test_restore_snapshot_posts_to_restore_endpoint(heat_client, svc): svc.restore_snapshot("n", "id-1", "s1") heat_client.post.assert_called_once_with( f"{HEAT}/stacks/n/id-1/snapshots/s1/restore" ) - # ── adopt ────────────────────────────────────────────────────────────── def test_adopt_posts_to_stacks(heat_client, svc): @@ -286,12 +249,10 @@ def test_adopt_posts_to_stacks(heat_client, svc): ) assert out["id"] == "new" - def test_adopt_handles_none(heat_client, svc): heat_client.post.return_value = None assert svc.adopt({}) == {} - # ── files / environment / breakpoints ───────────────────────────────── def test_get_files(heat_client, svc): @@ -300,24 +261,20 @@ def test_get_files(heat_client, svc): heat_client.get.assert_called_once_with(f"{HEAT}/stacks/n/id-1/files") assert out == {"file1.yaml": "contents"} - def test_get_files_handles_none(heat_client, svc): heat_client.get.return_value = None assert svc.get_files("n", "id-1") == {} - def test_get_environment(heat_client, svc): heat_client.get.return_value = {"resource_registry": {}} out = svc.get_environment("n", "id-1") heat_client.get.assert_called_once_with(f"{HEAT}/stacks/n/id-1/environment") assert "resource_registry" in out - def test_get_environment_handles_none(heat_client, svc): heat_client.get.return_value = None assert svc.get_environment("n", "id-1") == {} - # ── resource actions: signal / mark unhealthy / get metadata ─────────── def test_signal_resource_with_body(heat_client, svc): @@ -326,14 +283,12 @@ def test_signal_resource_with_body(heat_client, svc): f"{HEAT}/stacks/n/id-1/resources/r1/signal", json={"data": 42} ) - def test_signal_resource_without_body_sends_empty(heat_client, svc): svc.signal_resource("n", "id-1", "r1") heat_client.post.assert_called_once_with( f"{HEAT}/stacks/n/id-1/resources/r1/signal", json={} ) - def test_mark_resource_unhealthy_default_reason(heat_client, svc): svc.mark_resource_unhealthy("n", "id-1", "r1") heat_client.patch.assert_called_once() @@ -342,13 +297,11 @@ def test_mark_resource_unhealthy_default_reason(heat_client, svc): assert kwargs["json"]["mark_unhealthy"] is True assert "marked unhealthy via orca" in kwargs["json"]["resource_status_reason"] - def test_mark_resource_unhealthy_custom_reason(heat_client, svc): svc.mark_resource_unhealthy("n", "id-1", "r1", status_reason="manual override") args, kwargs = heat_client.patch.call_args assert kwargs["json"]["resource_status_reason"] == "manual override" - def test_get_resource_metadata_unwraps_envelope(heat_client, svc): heat_client.get.return_value = {"metadata": {"k": "v"}} out = svc.get_resource_metadata("n", "id-1", "r1") @@ -357,7 +310,6 @@ def test_get_resource_metadata_unwraps_envelope(heat_client, svc): ) assert out == {"k": "v"} - def test_get_resource_metadata_handles_unexpected(heat_client, svc): heat_client.get.return_value = None assert svc.get_resource_metadata("n", "id-1", "r1") == {} diff --git a/tests/test_setup.py b/tests/test_setup.py index 09be79f..83445bb 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -17,7 +17,6 @@ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestSetupHelp: def test_setup_help(self, invoke): @@ -26,12 +25,10 @@ def test_setup_help(self, invoke): assert "--profile" in result.output assert "credentials" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # New profile — no other profiles exist → auto-activates # ══════════════════════════════════════════════════════════════════════════ - def _full_answers( auth_url="https://keystone.foo:5000", username="alice", @@ -55,7 +52,6 @@ def _full_answers( insecure, ]) + "\n" - class TestSetupNewProfileNoExisting: """First-time setup: no profiles on disk → new profile becomes active.""" @@ -78,12 +74,10 @@ def test_creates_and_auto_activates(self, invoke, config_dir): # region_name was empty → omitted assert "region_name" not in data["profiles"]["first"] - # ══════════════════════════════════════════════════════════════════════════ # New profile — other profiles exist → asks "Switch to X now?" # ══════════════════════════════════════════════════════════════════════════ - class TestSetupNewProfileWithOthers: def test_switch_yes(self, invoke, write_config, sample_profile): @@ -111,12 +105,10 @@ def test_switch_no(self, invoke, write_config, sample_profile, config_dir): assert "newprof" in data["profiles"] assert data["active_profile"] == "existing" - # ══════════════════════════════════════════════════════════════════════════ # Edit existing profile — no switch prompt, pre-fills defaults # ══════════════════════════════════════════════════════════════════════════ - class TestSetupEditExisting: def test_edit_keeps_defaults(self, invoke, write_config, sample_profile, config_dir): @@ -170,12 +162,10 @@ def test_edit_legacy_profile_falls_back_to_domain_id( assert saved.get("user_domain_name") == legacy_profile["domain_id"] assert saved.get("project_name") == legacy_profile["project_id"] - # ══════════════════════════════════════════════════════════════════════════ # Default profile resolution — when --profile is omitted, uses active # ══════════════════════════════════════════════════════════════════════════ - class TestSetupAppCredential: """New profile via the application-credential branch.""" @@ -201,7 +191,6 @@ def test_creates_app_cred_profile(self, invoke, config_dir): assert "password" not in saved assert "project_name" not in saved - class TestSetupDefaultProfileResolution: def test_no_profile_flag_uses_active(self, invoke, write_config, sample_profile): @@ -218,12 +207,10 @@ def test_no_profile_flag_uses_active(self, invoke, write_config, sample_profile) assert result.exit_code == 0, result.output assert "editing profile: active-one" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Shell completion auto-install # ══════════════════════════════════════════════════════════════════════════ - class TestDetectShell: def test_detects_zsh(self, monkeypatch): @@ -246,7 +233,6 @@ def test_empty_shell_returns_none(self, monkeypatch): monkeypatch.delenv("SHELL", raising=False) assert detect_shell() is None - class TestInstallCompletionBashZsh: """Static-script install (ADR 0010): generate the script once, then source. @@ -363,7 +349,6 @@ def test_install_fails_gracefully_when_orca_missing(self, tmp_path, monkeypatch) else: raise AssertionError("expected OrcaCLIError when orca is not on PATH") - class TestInstallCompletionFish: def test_fish_no_orca_on_path(self, tmp_path, monkeypatch): @@ -413,7 +398,6 @@ class _Result: assert "failed" in msg assert not target.exists() - class TestMaybeInstallCompletion: def test_noop_when_stdin_not_tty(self, monkeypatch): diff --git a/tests/test_stack.py b/tests/test_stack.py index 440d0d6..2cad46b 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -8,7 +8,6 @@ STACK_ID = "11112222-3333-4444-5555-666677778888" - def _stack(stack_id=STACK_ID, name="my-stack", status="CREATE_COMPLETE"): return { "id": stack_id, "stack_name": name, "stack_status": status, @@ -21,7 +20,6 @@ def _stack(stack_id=STACK_ID, name="my-stack", status="CREATE_COMPLETE"): "parameters": {"param1": "value1"}, } - def _setup_mock(mock_client): mock_client.orchestration_url = "https://heat.example.com/v1/proj" mock_client.compute_url = "https://nova.example.com/v2.1" @@ -108,12 +106,10 @@ def _delete(url, **kwargs): return {"posted": posted, "put_data": put_data, "deleted": deleted} - # ═══════════════════════════════════════════════════════════════════════��══ # list # ══════════════════════════════════════════════════════════════════════════ - class TestStackList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -135,12 +131,10 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No stacks found" in result.output - # ══════════════════════════════════════════════════════════════════════════ # show # ══════════════════════════════════════════════════════════════════════════ - class TestStackShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -152,12 +146,10 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "my-stack" in result.output - # ══════════════════════════════════════════════════════════════════════════ # delete # ══════════════════════════════════════════════════════════════════════════ - class TestStackDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -170,12 +162,10 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert "deletion" in result.output.lower() assert len(state["deleted"]) == 1 - # ══════════════════════════════════════════════════════════════════════════ # actions: check, suspend, resume, cancel # ══════════════════════════════════════════════════════════════════════════ - class TestStackActions: def test_check(self, invoke, config_dir, mock_client, sample_profile): @@ -214,12 +204,10 @@ def test_cancel(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Cancel" in result.output - # ══════════════════════════════════════════════════════════════════════════ # resource-list / resource-show # ═════════════════════════════════════════════════════════════════════════�� - class TestStackResources: def test_resource_list(self, invoke, config_dir, mock_client, sample_profile): @@ -227,16 +215,14 @@ def test_resource_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["stack", "resource-list", "my-stack"]) + result = invoke(["stack", "resource", "list", "my-stack"]) assert result.exit_code == 0 assert "my-ser" in result.output - # ══════════════════════════════════════════════════════════════════════════ # event-list # ══════════════════════════════════════════════════════════════════════════ - class TestStackEvents: def test_event_list(self, invoke, config_dir, mock_client, sample_profile): @@ -244,16 +230,14 @@ def test_event_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["stack", "event-list", "my-stack"]) + result = invoke(["stack", "event", "list", "my-stack"]) assert result.exit_code == 0 assert "my-ser" in result.output - # ══════════════════════════════════════════════════════════════════════════ # output-list / output-show # ══════════════════════════════════════════════════════════════════════════ - class TestStackOutputs: def test_output_list(self, invoke, config_dir, mock_client, sample_profile): @@ -261,7 +245,7 @@ def test_output_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["stack", "output-list", "my-stack"]) + result = invoke(["stack", "output", "list", "my-stack"]) assert result.exit_code == 0 assert "url" in result.output @@ -270,16 +254,14 @@ def test_output_show(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["stack", "output-show", "my-stack", "url"]) + result = invoke(["stack", "output", "show", "my-stack", "url"]) assert result.exit_code == 0 assert "example.com" in result.output - # ══════════════════════════════════════════════════════════════════════════ # template-show # ══════════════════════════════════════════════════════════════════════════ - class TestStackTemplate: def test_template_show(self, invoke, config_dir, mock_client, sample_profile): @@ -287,17 +269,15 @@ def test_template_show(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["stack", "template-show", "my-stack"]) + result = invoke(["stack", "template", "show", "my-stack"]) assert result.exit_code == 0 # Rich Syntax output — check for key terms assert "heat_template" in result.output or "Server" in result.output or "server" in result.output - # ══════════════════════════════════════════════════════════════════════════ # topology # ══════════════════════════════════════════════════════════════════════════ - class TestStackTopology: def test_topology(self, invoke, config_dir, mock_client, sample_profile): @@ -310,12 +290,10 @@ def test_topology(self, invoke, config_dir, mock_client, sample_profile): assert "my-stack" in result.output assert "my-ser" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Helpers # ══════════════════════════════════════════════════════════════════════════ - class TestHelpers: def test_status_style_complete(self): @@ -342,12 +320,10 @@ def test_parse_params_invalid(self): with pytest.raises(Exception): _parse_params(("badformat",)) - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestStackHelp: def test_stack_help(self, invoke): @@ -355,14 +331,9 @@ def test_stack_help(self, invoke): assert result.exit_code == 0 for cmd in ("list", "show", "create", "update", "delete", "check", "suspend", "resume", "cancel", - "resource-list", "resource-show", - "event-list", "event-show", - "output-list", "output-show", - "template-show", "template-validate", "topology", "diff"): assert cmd in result.output - # ══════════════════════════════════════════════════════════════════════════ # stack abandon / resource-type-list / resource-type-show # ══════════════════════════════════════════════════════════════════════════ @@ -386,33 +357,31 @@ def test_abandon_requires_confirm(self, invoke, mock_client): def test_help(self, invoke): assert invoke(["stack", "abandon", "--help"]).exit_code == 0 - class TestStackResourceTypeList: def test_list(self, invoke, mock_client): mock_client.orchestration_url = "https://heat.example.com/v1/proj" mock_client.get.return_value = {"resource_types": ["OS::Nova::Server", "OS::Neutron::Net"]} - result = invoke(["stack", "resource-type-list"]) + result = invoke(["stack", "resource-type", "list"]) assert result.exit_code == 0 assert "Nova" in result.output def test_filter(self, invoke, mock_client): mock_client.orchestration_url = "https://heat.example.com/v1/proj" mock_client.get.return_value = {"resource_types": []} - invoke(["stack", "resource-type-list", "--filter", "Nova"]) + invoke(["stack", "resource-type", "list", "--filter", "Nova"]) assert mock_client.get.call_args[1]["params"]["name"] == "Nova" def test_help(self, invoke): - assert invoke(["stack", "resource-type-list", "--help"]).exit_code == 0 - + assert invoke(["stack", "resource-type", "list", "--help"]).exit_code == 0 class TestStackResourceTypeShow: def test_show(self, invoke, mock_client): mock_client.orchestration_url = "https://heat.example.com/v1/proj" mock_client.get.return_value = {"heat_template_version": "2021-04-06"} - result = invoke(["stack", "resource-type-show", "OS::Nova::Server"]) + result = invoke(["stack", "resource-type", "show", "OS::Nova::Server"]) assert result.exit_code == 0 def test_help(self, invoke): - assert invoke(["stack", "resource-type-show", "--help"]).exit_code == 0 + assert invoke(["stack", "resource-type", "show", "--help"]).exit_code == 0 diff --git a/tests/test_usage.py b/tests/test_usage.py index f89c6ee..c35d5e8 100644 --- a/tests/test_usage.py +++ b/tests/test_usage.py @@ -6,7 +6,6 @@ # ── Helpers ──────────────────────────────────────────────────────────────── - def _setup_mock(mock_client, server_usages=None, empty=False): mock_client.compute_url = "https://nova.example.com/v2.1" @@ -38,12 +37,10 @@ def _get(url, **kwargs): mock_client.get = _get - # ══════════════════════════════════════════════════════════════════════════ # usage # ══════════���═══════════════════════════════════════════════════════════════ - class TestUsage: def test_usage_basic(self, invoke, config_dir, mock_client, sample_profile): @@ -85,12 +82,10 @@ def test_usage_summary(self, invoke, config_dir, mock_client, sample_profile): assert "vCPU" in result.output assert "1441" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestUsageHelp: def test_usage_help(self, invoke): diff --git a/tests/test_validators.py b/tests/test_validators.py index a21db82..e1449d5 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -51,7 +51,6 @@ def test_rejects_partial_uuid(self): with pytest.raises(click.BadParameter): validate_id(None, None, "1234-5678") - class TestValidateIp: def test_accepts_valid_ipv4(self): @@ -87,7 +86,6 @@ def test_rejects_empty(self): with pytest.raises(click.BadParameter): validate_ip(None, None, "") - class TestSafeOutputPath: def test_accepts_regular_path(self, tmp_path): @@ -113,7 +111,6 @@ def test_rejects_existing_symlink(self, tmp_path): with pytest.raises(click.BadParameter, match="symlink"): safe_output_path(link) - class TestSafeChildPath: def test_accepts_child_within_base(self, tmp_path): diff --git a/tests/test_volume.py b/tests/test_volume.py index 54eaa8e..c9ab348 100644 --- a/tests/test_volume.py +++ b/tests/test_volume.py @@ -9,7 +9,6 @@ VOL_ID = "11112222-3333-4444-5555-666677778888" SNAP_ID = "aaaabbbb-cccc-dddd-eeee-ffffffffffff" - def _vol(vol_id=VOL_ID, name="my-vol", status="available", size=50): return { "id": vol_id, "name": name, "status": status, "size": size, @@ -22,7 +21,6 @@ def _vol(vol_id=VOL_ID, name="my-vol", status="available", size=50): "attachments": [], } - def _snap(snap_id=SNAP_ID, name="my-snap", vol_id=VOL_ID): return { "id": snap_id, "name": name, "volume_id": vol_id, @@ -30,7 +28,6 @@ def _snap(snap_id=SNAP_ID, name="my-snap", vol_id=VOL_ID): "description": "test snap", "created_at": "2025-01-01", } - def _setup_mock(mock_client): mock_client.volume_url = "https://cinder.example.com/v3" mock_client.compute_url = "https://nova.example.com/v2.1" @@ -82,12 +79,10 @@ def _delete(url, **kwargs): return {"posted": posted, "put_data": put_data, "deleted": deleted} - # ═════════════��══════════════════════════════���═════════════════════════════ # list # ══════════════════════════════════════════════════════════════════════════ - class TestVolumeList: def test_list(self, invoke, config_dir, mock_client, sample_profile): @@ -109,12 +104,10 @@ def test_list_empty(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "No volumes found" in result.output - # ══════��══════════════════════════��════════════════════════════���═══════════ # show # ════════════════════��═════════════════════════════════════════════════════ - class TestVolumeShow: def test_show(self, invoke, config_dir, mock_client, sample_profile): @@ -127,12 +120,10 @@ def test_show(self, invoke, config_dir, mock_client, sample_profile): assert "my-" in result.output assert "50 GB" in result.output - # ═══���══════════════════════════��═══════════════════════════════��═══════════ # create # ════���════════════���═════════════════════════════���══════════════════════════ - class TestVolumeCreate: def test_create(self, invoke, config_dir, mock_client, sample_profile): @@ -159,12 +150,10 @@ def test_create_with_options(self, invoke, config_dir, mock_client, sample_profi body = state["posted"]["last_body"]["volume"] assert body["volume_type"] == "SSD" - # ═════���═══════════════════════════���══════════════════════════════���═════════ # update # ══════════════════════════════════════════════════════════════════════════ - class TestVolumeUpdate: def test_update(self, invoke, config_dir, mock_client, sample_profile): @@ -185,12 +174,10 @@ def test_update_nothing(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "Nothing" in result.output - # ═════════════���══════════════════════════���═════════════════════════════════ # extend / retype / set-bootable / set-readonly # ═════════════════════���═══════════════════════════��════════════════════════ - class TestVolumeActions: def test_extend(self, invoke, config_dir, mock_client, sample_profile): @@ -229,12 +216,10 @@ def test_set_readonly(self, invoke, config_dir, mock_client, sample_profile): assert result.exit_code == 0 assert "readonly" in result.output.lower() - # ═════════════════════════════════════════════════════���════════════════════ # delete # ═══════════════════════════���═══════════════════════════��══════════════════ - class TestVolumeDelete: def test_delete(self, invoke, config_dir, mock_client, sample_profile): @@ -247,12 +232,10 @@ def test_delete(self, invoke, config_dir, mock_client, sample_profile): assert "deleted" in result.output.lower() assert len(state["deleted"]) == 1 - # ═══���══════════════════════════════════════════════════════════════════════ # snapshot-list / show / create / delete # ══════���══════════════════════��═════════════════════════��══════════════════ - class TestSnapshots: def test_snapshot_list(self, invoke, config_dir, mock_client, sample_profile): @@ -260,7 +243,7 @@ def test_snapshot_list(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["volume", "snapshot-list"]) + result = invoke(["volume", "snapshot", "list"]) assert result.exit_code == 0 assert "my-s" in result.output @@ -270,7 +253,7 @@ def test_snapshot_list_empty(self, invoke, config_dir, mock_client, sample_profi mock_client.volume_url = "https://cinder.example.com/v3" mock_client.get = lambda url, **kw: {"snapshots": []} - result = invoke(["volume", "snapshot-list"]) + result = invoke(["volume", "snapshot", "list"]) assert result.exit_code == 0 assert "No snapshots found" in result.output @@ -279,7 +262,7 @@ def test_snapshot_show(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _setup_mock(mock_client) - result = invoke(["volume", "snapshot-show", SNAP_ID]) + result = invoke(["volume", "snapshot", "show", SNAP_ID]) assert result.exit_code == 0 assert "my-snap" in result.output @@ -288,7 +271,7 @@ def test_snapshot_create(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["volume", "snapshot-create", VOL_ID, "--name", "backup"]) + result = invoke(["volume", "snapshot", "create", VOL_ID, "--name", "backup"]) assert result.exit_code == 0 assert "created" in result.output.lower() @@ -297,16 +280,14 @@ def test_snapshot_delete(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["volume", "snapshot-delete", SNAP_ID, "-y"]) + result = invoke(["volume", "snapshot", "delete", SNAP_ID, "-y"]) assert result.exit_code == 0 assert "deleted" in result.output.lower() - # ═════════════════════════════════════════════���════════════════════════════ # tree # ════════���═════════════════════════════════════════════════════════════════ - class TestVolumeTree: def test_tree(self, invoke, config_dir, mock_client, sample_profile): @@ -319,12 +300,10 @@ def test_tree(self, invoke, config_dir, mock_client, sample_profile): assert "my-" in result.output assert "1 volumes" in result.output or "50 GB" in result.output - # ═════��══════════════════════════���═════════════════════════════════════════ # Help # ═══��══════════════════════════════════════════════════════════════════════ - # ══════════════════════════════════════════════════════════════════════════ # migrate / revert-to-snapshot / summary # ══════════════════════════════════════════════════════════════════════════ @@ -335,7 +314,6 @@ def test_tree(self, invoke, config_dir, mock_client, sample_profile): GT_ID = "dd444444-4444-4444-4444-444444444444" VT_ID = "ee555555-5555-5555-5555-555555555555" - def _setup_extended(mock_client): """Extended mock adding messages, attachments, groups and summary.""" state = _setup_mock(mock_client) @@ -392,7 +370,6 @@ def _post(url, **kwargs): mock_client.post = _post return state - class TestVolumeMigrate: def test_migrate(self, invoke, config_dir, mock_client, sample_profile): @@ -419,18 +396,16 @@ def test_migrate_requires_host(self, invoke, config_dir, mock_client, sample_pro result = invoke(["volume", "migrate", VOL_ID]) assert result.exit_code != 0 - class TestVolumeRevertToSnapshot: def test_revert(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["volume", "revert-to-snapshot", VOL_ID, SNAP_ID]) + result = invoke(["volume", "snapshot", "revert", VOL_ID, SNAP_ID]) assert result.exit_code == 0 assert state["posted"]["last_body"]["revert"]["snapshot_id"] == SNAP_ID - class TestVolumeSummary: def test_summary(self, invoke, config_dir, mock_client, sample_profile): @@ -442,19 +417,17 @@ def test_summary(self, invoke, config_dir, mock_client, sample_profile): assert "5" in result.output assert "250" in result.output - # ══════════════════════════════════════════════════════════════════════════ # messages # ══════════════════════════════════════════════════════════════════════════ - class TestVolumeMessages: def test_message_list(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") _setup_extended(mock_client) - result = invoke(["volume", "message-list"]) + result = invoke(["volume", "message", "list"]) assert result.exit_code == 0 assert "VOLU" in result.output or "No space" in result.output @@ -462,14 +435,14 @@ def test_message_list_filter_type(self, invoke, config_dir, mock_client, sample_ save_profile("p", sample_profile) set_active_profile("p") _setup_extended(mock_client) - result = invoke(["volume", "message-list", "--resource-type", "VOLUME"]) + result = invoke(["volume", "message", "list", "--resource-type", "VOLUME"]) assert result.exit_code == 0 def test_message_show(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") _setup_extended(mock_client) - result = invoke(["volume", "message-show", MSG_ID]) + result = invoke(["volume", "message", "show", MSG_ID]) assert result.exit_code == 0 assert "No space left" in result.output @@ -477,29 +450,27 @@ def test_message_delete(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") state = _setup_extended(mock_client) - result = invoke(["volume", "message-delete", MSG_ID, "--yes"]) + result = invoke(["volume", "message", "delete", MSG_ID, "--yes"]) assert result.exit_code == 0 assert any(MSG_ID in u for u in state["deleted"]) def test_invalid_resource_type(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") - result = invoke(["volume", "message-list", "--resource-type", "INVALID"]) + result = invoke(["volume", "message", "list", "--resource-type", "INVALID"]) assert result.exit_code != 0 - # ══════════════════════════════════════════════════════════════════════════ # attachments # ══════════════════════════════════════════════════════════════════════════ - class TestVolumeAttachments: def test_attachment_list(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") _setup_extended(mock_client) - result = invoke(["volume", "attachment-list"]) + result = invoke(["volume", "attachment", "list"]) assert result.exit_code == 0 assert "att" in result.output.lower() or "attached" in result.output @@ -507,14 +478,14 @@ def test_attachment_list_filter(self, invoke, config_dir, mock_client, sample_pr save_profile("p", sample_profile) set_active_profile("p") _setup_extended(mock_client) - result = invoke(["volume", "attachment-list", "--volume-id", VOL_ID]) + result = invoke(["volume", "attachment", "list", "--volume-id", VOL_ID]) assert result.exit_code == 0 def test_attachment_show(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") _setup_extended(mock_client) - result = invoke(["volume", "attachment-show", ATT_ID]) + result = invoke(["volume", "attachment", "show", ATT_ID]) assert result.exit_code == 0 assert "attached" in result.output @@ -522,23 +493,21 @@ def test_attachment_delete(self, invoke, config_dir, mock_client, sample_profile save_profile("p", sample_profile) set_active_profile("p") state = _setup_extended(mock_client) - result = invoke(["volume", "attachment-delete", ATT_ID, "--yes"]) + result = invoke(["volume", "attachment", "delete", ATT_ID, "--yes"]) assert result.exit_code == 0 assert any(ATT_ID in u for u in state["deleted"]) - # ══════════════════════════════════════════════════════════════════════════ # groups # ══════════════════════════════════════════════════════════════════════════ - class TestVolumeGroups: def test_group_list(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") _setup_extended(mock_client) - result = invoke(["volume", "group-list"]) + result = invoke(["volume", "group", "list"]) assert result.exit_code == 0 assert "my-" in result.output @@ -546,7 +515,7 @@ def test_group_show(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") _setup_extended(mock_client) - result = invoke(["volume", "group-show", GRP_ID]) + result = invoke(["volume", "group", "show", GRP_ID]) assert result.exit_code == 0 assert "my-group" in result.output @@ -554,7 +523,7 @@ def test_group_create(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") state = _setup_extended(mock_client) - result = invoke(["volume", "group-create", "my-group", + result = invoke(["volume", "group", "create", "my-group", "--group-type", GT_ID, "--volume-type", VT_ID]) assert result.exit_code == 0 body = state["posted"]["last_body"]["group"] @@ -564,14 +533,14 @@ def test_group_create(self, invoke, config_dir, mock_client, sample_profile): def test_group_create_requires_group_type(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") - result = invoke(["volume", "group-create", "grp", "--volume-type", VT_ID]) + result = invoke(["volume", "group", "create", "grp", "--volume-type", VT_ID]) assert result.exit_code != 0 def test_group_update_name(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["volume", "group-update", GRP_ID, "--name", "new-name"]) + result = invoke(["volume", "group", "update", GRP_ID, "--name", "new-name"]) assert result.exit_code == 0 assert state["put_data"]["last_body"]["group"]["name"] == "new-name" @@ -579,7 +548,7 @@ def test_group_update_add_remove_volumes(self, invoke, config_dir, mock_client, save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["volume", "group-update", GRP_ID, + result = invoke(["volume", "group", "update", GRP_ID, "--add-volume", VOL_ID, "--remove-volume", SNAP_ID]) assert result.exit_code == 0 body = state["put_data"]["last_body"]["group"] @@ -590,7 +559,7 @@ def test_group_update_nothing(self, invoke, config_dir, mock_client, sample_prof save_profile("p", sample_profile) set_active_profile("p") _setup_mock(mock_client) - result = invoke(["volume", "group-update", GRP_ID]) + result = invoke(["volume", "group", "update", GRP_ID]) assert result.exit_code == 0 assert "Nothing to update" in result.output @@ -598,7 +567,7 @@ def test_group_delete(self, invoke, config_dir, mock_client, sample_profile): save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["volume", "group-delete", GRP_ID, "--yes"]) + result = invoke(["volume", "group", "delete", GRP_ID, "--yes"]) assert result.exit_code == 0 body = state["posted"]["last_body"]["delete"] assert body["delete-volumes"] is False @@ -607,18 +576,16 @@ def test_group_delete_with_volumes(self, invoke, config_dir, mock_client, sample save_profile("p", sample_profile) set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["volume", "group-delete", GRP_ID, "--delete-volumes", "--yes"]) + result = invoke(["volume", "group", "delete", GRP_ID, "--delete-volumes", "--yes"]) assert result.exit_code == 0 assert state["posted"]["last_body"]["delete"]["delete-volumes"] is True - # ══════════════════════════════════════════════════════════════════════════ # backup-create / backup-restore # ══════════════════════════════════════════════════════════════════════════ BKP_ID = "ff001122-3344-5566-7788-99aabbccddee" - class TestVolumeBackupCreate: def test_create(self, invoke, config_dir, mock_client, sample_profile): @@ -626,7 +593,7 @@ def test_create(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["volume", "backup-create", VOL_ID, "--name", "my-backup"]) + result = invoke(["volume", "backup", "create", VOL_ID, "--name", "my-backup"]) assert result.exit_code == 0 body = state["posted"]["last_body"]["backup"] assert body["volume_id"] == VOL_ID @@ -637,7 +604,7 @@ def test_create_incremental_force(self, invoke, config_dir, mock_client, sample_ set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["volume", "backup-create", VOL_ID, + result = invoke(["volume", "backup", "create", VOL_ID, "--incremental", "--force"]) assert result.exit_code == 0 body = state["posted"]["last_body"]["backup"] @@ -649,11 +616,10 @@ def test_create_with_snapshot(self, invoke, config_dir, mock_client, sample_prof set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["volume", "backup-create", VOL_ID, "--snapshot-id", SNAP_ID]) + result = invoke(["volume", "backup", "create", VOL_ID, "--snapshot-id", SNAP_ID]) assert result.exit_code == 0 assert state["posted"]["last_body"]["backup"]["snapshot_id"] == SNAP_ID - class TestVolumeBackupRestore: def test_restore(self, invoke, config_dir, mock_client, sample_profile): @@ -661,7 +627,7 @@ def test_restore(self, invoke, config_dir, mock_client, sample_profile): set_active_profile("p") _ = _setup_mock(mock_client) - result = invoke(["volume", "backup-restore", BKP_ID]) + result = invoke(["volume", "backup", "restore", BKP_ID]) assert result.exit_code == 0 assert "restore" in result.output.lower() or "backup" in result.output.lower() @@ -670,7 +636,7 @@ def test_restore_to_volume(self, invoke, config_dir, mock_client, sample_profile set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["volume", "backup-restore", BKP_ID, "--volume-id", VOL_ID]) + result = invoke(["volume", "backup", "restore", BKP_ID, "--volume-id", VOL_ID]) assert result.exit_code == 0 body = state["posted"]["last_body"]["restore"] assert body["volume_id"] == VOL_ID @@ -680,25 +646,16 @@ def test_restore_with_name(self, invoke, config_dir, mock_client, sample_profile set_active_profile("p") state = _setup_mock(mock_client) - result = invoke(["volume", "backup-restore", BKP_ID, "--name", "restored-vol"]) + result = invoke(["volume", "backup", "restore", BKP_ID, "--name", "restored-vol"]) assert result.exit_code == 0 assert state["posted"]["last_body"]["restore"]["name"] == "restored-vol" - class TestVolumeHelp: def test_volume_help(self, invoke): result = invoke(["volume", "--help"]) assert result.exit_code == 0 for cmd in ("list", "show", "create", "update", "delete", - "extend", "retype", "set-bootable", "set-readonly", - "snapshot-list", "snapshot-show", "snapshot-create", - "snapshot-delete", "tree", - "migrate", "revert-to-snapshot", "summary", - "message-list", "message-show", "message-delete", - "attachment-list", "attachment-show", "attachment-delete", - "group-list", "group-show", "group-create", - "group-update", "group-delete", - "backup-list", "backup-show", "backup-create", - "backup-delete", "backup-restore"): + "extend", "retype", "set-bootable", "set-readonly", "tree", + "migrate", "summary"): assert cmd in result.output diff --git a/tests/test_volume_backup.py b/tests/test_volume_backup.py index ccce9b9..944c12a 100644 --- a/tests/test_volume_backup.py +++ b/tests/test_volume_backup.py @@ -4,14 +4,11 @@ from unittest.mock import patch -import pytest - # Valid UUIDs for validate_id VOL_ID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" BKP_ID = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" RST_ID = "cccccccc-cccc-cccc-cccc-cccccccccccc" - # ══════════════════════════════════════════════════════════════════════════ # backup-list # ══════════════════════════════════════════════════════════════════════════ @@ -27,26 +24,25 @@ def test_list(self, invoke, mock_client): "created_at": "2024-01-01T00:00:00Z"}, ] } - result = invoke(["volume", "backup-list"]) + result = invoke(["volume", "backup", "list"]) assert result.exit_code == 0 assert "my-" in result.output # Rich truncates in narrow CliRunner terminal def test_list_empty(self, invoke, mock_client): mock_client.volume_url = "https://cinder.example.com/v3" mock_client.get.return_value = {"backups": []} - result = invoke(["volume", "backup-list"]) + result = invoke(["volume", "backup", "list"]) assert result.exit_code == 0 assert "No backups" in result.output def test_list_all_projects(self, invoke, mock_client): mock_client.volume_url = "https://cinder.example.com/v3" mock_client.get.return_value = {"backups": []} - result = invoke(["volume", "backup-list", "--all-projects"]) + result = invoke(["volume", "backup", "list", "--all-projects"]) assert result.exit_code == 0 call_kwargs = mock_client.get.call_args[1] assert call_kwargs.get("params", {}).get("all_tenants") == 1 - # ══════════════════════════════════════════════════════════════════════════ # backup-show # ══════════════════════════════════════════════════════════════════════════ @@ -66,13 +62,12 @@ def test_show(self, invoke, mock_client): "description": "daily backup", } } - result = invoke(["volume", "backup-show", BKP_ID]) + result = invoke(["volume", "backup", "show", BKP_ID]) assert result.exit_code == 0 assert "my-backup" in result.output assert "available" in result.output assert "yes" in result.output # is_incremental - # ══════════════════════════════════════════════════════════════════════════ # backup-create # ══════════════════════════════════════════════════════════════════════════ @@ -91,7 +86,7 @@ def test_create_basic(self, invoke, mock_client): mock_client.volume_url = "https://cinder.example.com/v3" mock_client.post.return_value = {"backup": {"id": BKP_ID, "name": "my-backup"}} mock_client.get.return_value = self._FULL_BKP - result = invoke(["volume", "backup-create", VOL_ID, "--name", "my-backup"]) + result = invoke(["volume", "backup", "create", VOL_ID, "--name", "my-backup"]) assert result.exit_code == 0 assert BKP_ID in result.output body = mock_client.post.call_args[1]["json"]["backup"] @@ -102,7 +97,7 @@ def test_create_incremental(self, invoke, mock_client): mock_client.volume_url = "https://cinder.example.com/v3" mock_client.post.return_value = {"backup": {"id": BKP_ID, "name": None}} mock_client.get.return_value = self._FULL_BKP - result = invoke(["volume", "backup-create", VOL_ID, "--incremental"]) + result = invoke(["volume", "backup", "create", VOL_ID, "--incremental"]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"]["backup"] assert body.get("incremental") is True @@ -111,7 +106,7 @@ def test_create_force(self, invoke, mock_client): mock_client.volume_url = "https://cinder.example.com/v3" mock_client.post.return_value = {"backup": {"id": BKP_ID, "name": None}} mock_client.get.return_value = self._FULL_BKP - result = invoke(["volume", "backup-create", VOL_ID, "--force"]) + result = invoke(["volume", "backup", "create", VOL_ID, "--force"]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"]["backup"] assert body.get("force") is True @@ -121,13 +116,12 @@ def test_create_with_wait(self, invoke, mock_client): mock_client.post.return_value = {"backup": {"id": BKP_ID, "name": "bk"}} mock_client.get.return_value = self._FULL_BKP with patch("orca_cli.commands.volume.wait_for_resource") as mock_wait: - result = invoke(["volume", "backup-create", VOL_ID, "--wait"]) + result = invoke(["volume", "backup", "create", VOL_ID, "--wait"]) assert result.exit_code == 0 mock_wait.assert_called_once() _, kwargs = mock_wait.call_args assert kwargs.get("target_status") == "available" - # ══════════════════════════════════════════════════════════════════════════ # backup-delete # ══════════════════════════════════════════════════════════════════════════ @@ -136,19 +130,18 @@ class TestVolumeBackupDelete: def test_delete(self, invoke, mock_client): mock_client.volume_url = "https://cinder.example.com/v3" - result = invoke(["volume", "backup-delete", BKP_ID, "--yes"]) + result = invoke(["volume", "backup", "delete", BKP_ID, "--yes"]) assert result.exit_code == 0 assert "deleted" in result.output mock_client.delete.assert_called_once() def test_delete_force(self, invoke, mock_client): mock_client.volume_url = "https://cinder.example.com/v3" - result = invoke(["volume", "backup-delete", BKP_ID, "--yes", "--force"]) + result = invoke(["volume", "backup", "delete", BKP_ID, "--yes", "--force"]) assert result.exit_code == 0 _, kwargs = mock_client.delete.call_args assert kwargs.get("params", {}).get("force") is True - # ══════════════════════════════════════════════════════════════════════════ # backup-restore # ══════════════════════════════════════════════════════════════════════════ @@ -158,14 +151,14 @@ class TestVolumeBackupRestore: def test_restore_new_volume(self, invoke, mock_client): mock_client.volume_url = "https://cinder.example.com/v3" mock_client.post.return_value = {"restore": {"volume_id": RST_ID}} - result = invoke(["volume", "backup-restore", BKP_ID]) + result = invoke(["volume", "backup", "restore", BKP_ID]) assert result.exit_code == 0 assert RST_ID in result.output def test_restore_to_existing_volume(self, invoke, mock_client): mock_client.volume_url = "https://cinder.example.com/v3" mock_client.post.return_value = {"restore": {"volume_id": VOL_ID}} - result = invoke(["volume", "backup-restore", BKP_ID, "--volume-id", VOL_ID]) + result = invoke(["volume", "backup", "restore", BKP_ID, "--volume-id", VOL_ID]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"]["restore"] assert body["volume_id"] == VOL_ID @@ -174,7 +167,7 @@ def test_restore_with_wait(self, invoke, mock_client): mock_client.volume_url = "https://cinder.example.com/v3" mock_client.post.return_value = {"restore": {"volume_id": RST_ID}} with patch("orca_cli.commands.volume.wait_for_resource") as mock_wait: - result = invoke(["volume", "backup-restore", BKP_ID, "--wait"]) + result = invoke(["volume", "backup", "restore", BKP_ID, "--wait"]) assert result.exit_code == 0 mock_wait.assert_called_once() assert "available" in str(mock_wait.call_args) @@ -182,21 +175,18 @@ def test_restore_with_wait(self, invoke, mock_client): def test_restore_with_name(self, invoke, mock_client): mock_client.volume_url = "https://cinder.example.com/v3" mock_client.post.return_value = {"restore": {"volume_id": RST_ID}} - result = invoke(["volume", "backup-restore", BKP_ID, "--name", "restored"]) + result = invoke(["volume", "backup", "restore", BKP_ID, "--name", "restored"]) assert result.exit_code == 0 body = mock_client.post.call_args[1]["json"]["restore"] assert body["name"] == "restored" - # ══════════════════════════════════════════════════════════════════════════ # --help checks # ══════════════════════════════════════════════════════════════════════════ class TestVolumeBackupHelp: - @pytest.mark.parametrize("sub", [ - "backup-list", "backup-show", "backup-create", "backup-delete", "backup-restore" - ]) - def test_help(self, invoke, sub): - result = invoke(["volume", sub, "--help"]) + def test_help(self, invoke): + # ``volume backup`` is now a sub-group; --help on it lists the verbs. + result = invoke(["volume", "backup", "--help"]) assert result.exit_code == 0 diff --git a/tests/test_volume_upload_to_image.py b/tests/test_volume_upload_to_image.py index c74d071..9a11425 100644 --- a/tests/test_volume_upload_to_image.py +++ b/tests/test_volume_upload_to_image.py @@ -11,14 +11,12 @@ VOL_ID = "11112222-3333-4444-5555-666677778888" IMG_ID = "aaaa1111-bbbb-2222-cccc-333333333333" - def _vol(status="available", name="src-vol"): return { "id": VOL_ID, "name": name, "status": status, "size": 10, "bootable": "true", "attachments": [], } - def _img(status="queued", size=None, **extra): img = {"id": IMG_ID, "name": "my-image", "status": status, "disk_format": "qcow2", "container_format": "bare", @@ -28,7 +26,6 @@ def _img(status="queued", size=None, **extra): img.update(extra) return img - class _Recorder: """Capture HTTP-shaped calls made through the mock OrcaClient.""" @@ -99,17 +96,14 @@ def _patch(url, content=None, content_type=None, **kwargs): mock_client.patch = _patch return self - @pytest.fixture def setup(mock_client, sample_profile, config_dir): save_profile("p", sample_profile) set_active_profile("p") return mock_client - # ── happy path ──────────────────────────────────────────────────────────── - def test_happy_path_with_property_and_wait(monkeypatch, invoke, setup): """Available volume → POST action, PATCH property, poll to active.""" rec = _Recorder().install( @@ -161,10 +155,8 @@ def test_happy_path_with_property_and_wait(monkeypatch, invoke, setup): assert IMG_ID in result.output assert "active" in result.output.lower() - # ── in-use volume guards ────────────────────────────────────────────────── - def test_in_use_without_force_aborts_before_post(invoke, setup): rec = _Recorder().install(setup, vol_status="in-use") @@ -177,7 +169,6 @@ def test_in_use_without_force_aborts_before_post(invoke, setup): assert "in-use" in out assert "--force" in out - def test_in_use_with_force_sends_force_true(invoke, setup): rec = _Recorder().install(setup, vol_status="in-use") @@ -189,10 +180,8 @@ def test_in_use_with_force_sends_force_true(invoke, setup): body = action_posts[0][1] assert body["os-volume_upload_image"]["force"] is True - # ── failure during --wait ──────────────────────────────────────────────── - def test_killed_during_wait_exits_nonzero(monkeypatch, invoke, setup): rec = _Recorder().install( setup, @@ -214,10 +203,8 @@ def test_killed_during_wait_exits_nonzero(monkeypatch, invoke, setup): # No PATCH because no --property. assert rec.patches == [] - # ── property validation ────────────────────────────────────────────────── - def test_invalid_property_key_aborts_before_any_call(invoke, setup): rec = _Recorder().install(setup) @@ -230,7 +217,6 @@ def test_invalid_property_key_aborts_before_any_call(invoke, setup): assert rec.posts == [] assert rec.patches == [] - def test_property_value_with_special_chars_forwarded_intact(invoke, setup): rec = _Recorder().install(setup) url_value = "https://x?a=1&b=2" @@ -245,10 +231,8 @@ def test_property_value_with_special_chars_forwarded_intact(invoke, setup): ops = _json.loads(rec.patches[0][1]) assert ops == [{"op": "add", "path": "/url", "value": url_value}] - # ── defensive: missing image_id in Cinder response ─────────────────────── - def test_missing_image_id_in_response_aborts(invoke, setup): """If Cinder accepts the action but doesn't return an image_id, fail loudly.""" rec = _Recorder().install(setup, upload_response_image_id=None) @@ -261,10 +245,8 @@ def test_missing_image_id_in_response_aborts(invoke, setup): # No PATCH should be attempted without an image to target. assert rec.patches == [] - # ── older-microversion compatibility ──────────────────────────────────── - def test_default_omits_visibility_and_protected(invoke, setup): """Without explicit flags, the body must not carry visibility/protected. @@ -280,7 +262,6 @@ def test_default_omits_visibility_and_protected(invoke, setup): assert "visibility" not in payload assert "protected" not in payload - def test_explicit_visibility_is_forwarded(invoke, setup): rec = _Recorder().install(setup) @@ -294,7 +275,6 @@ def test_explicit_visibility_is_forwarded(invoke, setup): assert payload["visibility"] == "shared" assert "protected" not in payload - def test_explicit_protected_is_forwarded(invoke, setup): rec = _Recorder().install(setup) @@ -308,7 +288,6 @@ def test_explicit_protected_is_forwarded(invoke, setup): assert payload["protected"] is True assert "visibility" not in payload - def test_strict_cinder_rejects_when_visibility_present(invoke, setup): """Simulate the Infomaniak-class 400 to prove the default path passes on a strict back-end and that explicit flags surface the error. @@ -356,10 +335,8 @@ def _strict_post(url, **kwargs): out = result_strict.output + (result_strict.stderr_bytes or b"").decode(errors="replace") assert "400" in out - # ── name resolution ────────────────────────────────────────────────────── - def test_resolve_volume_by_name(invoke, setup): rec = _Recorder().install(setup) diff --git a/tests/test_wait_and_dryrun.py b/tests/test_wait_and_dryrun.py index d3590d1..3756f2c 100644 --- a/tests/test_wait_and_dryrun.py +++ b/tests/test_wait_and_dryrun.py @@ -15,7 +15,6 @@ SRV = "11111111-1111-1111-1111-111111111111" VOL = "22222222-2222-2222-2222-222222222222" - # ══════════════════════════════════════════════════════════════════════════ # Cache module # ══════════════════════════════════════════════════════════════════════════ @@ -59,7 +58,6 @@ def test_profile_namespacing(self, tmp_path, monkeypatch): assert cache.load("prod", "servers") == [{"id": "prod-vm"}] assert cache.load("dev", "servers") == [{"id": "dev-vm"}] - # ══════════════════════════════════════════════════════════════════════════ # Waiter module # ══════════════════════════════════════════════════════════════════════════ @@ -177,7 +175,6 @@ def test_custom_interval_passed_to_sleep(self): for call in sleep_mock.call_args_list: assert call.args[0] == 7 - # ══════════════════════════════════════════════════════════════════════════ # server delete --dry-run # ══════════════════════════════════════════════════════════════════════════ @@ -203,7 +200,6 @@ def test_dry_run_skips_confirmation(self, invoke, mock_client): assert result.exit_code == 0 mock_client.delete.assert_not_called() - # ══════════════════════════════════════════════════════════════════════════ # server delete --wait # ══════════════════════════════════════════════════════════════════════════ @@ -218,7 +214,6 @@ def test_wait_calls_waiter(self, invoke, mock_client): _, kwargs = mock_wait.call_args assert kwargs.get("delete_mode") is True - # ══════════════════════════════════════════════════════════════════════════ # server create --wait # ══════════════════════════════════════════════════════════════════════════ @@ -252,7 +247,6 @@ def test_wait_invokes_waiter(self, invoke, mock_client): _, kwargs = mock_wait.call_args assert kwargs.get("target_status") == "ACTIVE" or "ACTIVE" in str(mock_wait.call_args) - # ══════════════════════════════════════════════════════════════════════════ # server start/stop/reboot --wait # ══════════════════════════════════════════════════════════════════════════ @@ -289,7 +283,6 @@ def test_reboot_with_wait(self, invoke, mock_client): mock_wait.assert_called_once() assert "ACTIVE" in str(mock_wait.call_args) - # ══════════════════════════════════════════════════════════════════════════ # volume create --wait / volume delete --dry-run / --wait # ══════════════════════════════════════════════════════════════════════════ @@ -310,7 +303,6 @@ def test_wait_invokes_waiter(self, invoke, mock_client): mock_wait.assert_called_once() assert "available" in str(mock_wait.call_args) - class TestVolumeDeleteDryRun: def test_dry_run_shows_info(self, invoke, mock_client): diff --git a/tests/test_watch.py b/tests/test_watch.py index a5d24bc..88af498 100644 --- a/tests/test_watch.py +++ b/tests/test_watch.py @@ -6,7 +6,6 @@ # _styled_status # ══════════════════════════════════════════════════════════════════════════ - class TestStyledStatus: def test_active(self): @@ -26,12 +25,10 @@ def test_unknown(self): t = _styled_status("WEIRD") assert t.plain == "WEIRD" - # ══════════════════════════════════════════════════════════════════════════ # _extract_ip # ══════════════════════════════════════════════════════════════════════════ - class TestExtractIp: def test_prefer_floating(self): @@ -57,12 +54,10 @@ def test_empty(self): from orca_cli.commands.watch import _extract_ip assert _extract_ip({}) == "—" - # ══════════════════════════════════════════════════════════════════════════ # Help # ══════════════════════════════════════════════════════════════════════════ - class TestWatchHelp: def test_watch_help(self, invoke): @@ -71,7 +66,6 @@ def test_watch_help(self, invoke): assert "--interval" in result.output assert "dashboard" in result.output.lower() - # ══════════════════════════════════════════════════════════════════════════ # Safe fetchers — each swallows exceptions to an empty list # ══════════════════════════════════════════════════════════════════════════ @@ -160,7 +154,6 @@ def test_fetch_recent_events_per_server_exception_swallows(self): assert len(out) == 1 assert out[0]["_server_name"] == "db" - class TestBuildDashboard: def test_with_data(self): diff --git a/tests/test_wizard.py b/tests/test_wizard.py index 09981b6..8feca0b 100644 --- a/tests/test_wizard.py +++ b/tests/test_wizard.py @@ -24,28 +24,22 @@ def _images(): return [{"id": IMG, "name": "Ubuntu 22.04", "status": "active", "os_distro": "ubuntu", "size": 2_000_000_000}] - def _flavors(): return [{"id": FLV, "name": "m1.small", "vcpus": 2, "ram": 2048, "disk": 20}] - def _networks(): return [{"id": NET, "name": "private", "status": "ACTIVE", "shared": False}] - def _keypairs(): return [{"keypair": {"name": "my-key", "type": "ssh", "fingerprint": "aa:bb:cc:dd:ee:ff:00:11:22:33"}}] - def _sgs(): return [{"id": "sg-1", "name": "default", "description": "Default SG"}] - def _vtypes(): return [{"id": "vt-1", "name": "ceph-ssd", "description": "Fast SSD"}] - def _limits(): return {"limits": {"absolute": { "totalInstancesUsed": 2, "maxTotalInstances": 10, @@ -53,7 +47,6 @@ def _limits(): "totalRAMUsed": 4096, "maxTotalRAMSize": 51200, }}} - def _mock_client_for_server(): mc = MagicMock() mc.compute_url = "https://nova.example.com/v2.1" @@ -80,7 +73,6 @@ def _get(url, **kw): mc.post.return_value = {"server": {"id": SRV, "adminPass": ""}} return mc - def _mock_client_for_volume(): mc = MagicMock() mc.compute_url = "https://nova.example.com/v2.1" @@ -97,7 +89,6 @@ def _get(url, **kw): mc.post.return_value = {"volume": {"id": VOL, "name": "my-vol", "size": 50}} return mc - # ══════════════════════════════════════════════════════════════════════════ # Unit tests — wizard helpers # ══════════════════════════════════════════════════════════════════════════ @@ -157,7 +148,6 @@ def cmd(): # Should abort (Abort exception or non-zero exit) assert r.exit_code != 0 or "No item" in r.output - class TestWizardFormatHelpers: def test_fmt_bytes_gb(self): @@ -175,7 +165,6 @@ def test_fmt_ram_gb(self): def test_fmt_ram_mb(self): assert "MB" in wiz._fmt_ram(512) - class TestBuildServerCommand: def test_basic(self): @@ -201,7 +190,6 @@ def test_multiple_sgs(self): assert "sg1" in cmd assert "sg2" in cmd - class TestSelectSecurityGroups: """Multi-select parser for SGs: comma-separated indices, skip blank, skip bad ints.""" @@ -252,7 +240,6 @@ def cmd(): r = CliRunner().invoke(cmd, input="1,foo,99\n") assert "SGS:['only']" in r.output - class TestSelectCidr: def test_option_1_returns_open(self): @@ -273,7 +260,6 @@ def cmd(): r = runner.invoke(cmd, input="2\n192.168.1.0/24\n") assert "CIDR:192.168.1.0/24" in r.output - # ══════════════════════════════════════════════════════════════════════════ # Integration tests — server create -i # ══════════════════════════════════════════════════════════════════════════ @@ -393,7 +379,6 @@ def test_missing_required_without_interactive(self, invoke, mock_client): assert result.exit_code != 0 assert "--name" in result.output or "Missing" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Integration tests — volume create -i # ══════════════════════════════════════════════════════════════════════════ @@ -445,14 +430,12 @@ def test_wizard_help(self, invoke): assert result.exit_code == 0 assert "interactive" in result.output.lower() or "-i" in result.output - # ══════════════════════════════════════════════════════════════════════════ # Integration tests — doctor --fix --cidr # ══════════════════════════════════════════════════════════════════════════ PRJ_ID = "proj-1111-1111-1111-111111111111" - def _sg(has_ssh=True, has_icmp=True): rules = [] if has_ssh: @@ -463,7 +446,6 @@ def _sg(has_ssh=True, has_icmp=True): return {"security_groups": [{"id": "sg-default", "name": "default", "security_group_rules": rules}]} - def _setup_doctor(mock_client, sg_data=None): mock_client.compute_url = "https://nova.example.com/v2.1" mock_client.network_url = "https://neutron.example.com" @@ -502,7 +484,6 @@ def _get(url, **kw): mock_client.get.side_effect = _get - class TestDoctorFixCidr: def test_fix_uses_cidr_option(self, invoke, mock_client): @@ -540,7 +521,6 @@ def test_no_post_when_rules_present_with_cidr(self, invoke, mock_client): invoke(["doctor", "--fix", "--cidr", "10.0.0.0/8"]) mock_client.post.assert_not_called() - # ══════════════════════════════════════════════════════════════════════════ # Helpers # ══════════════════════════════════════════════════════════════════════════ @@ -570,7 +550,6 @@ def _get(url, **kw): mock_client.get.side_effect = _get mock_client.post.return_value = {"server": {"id": SRV, "adminPass": ""}} - def _setup_vol(mock_client): """Configure mock_client for volume create -i tests.""" mock_client.compute_url = "https://nova.example.com/v2.1"