From fd57cb86251851e04da316fbc6c997a1d5400d5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:06:53 -0500 Subject: [PATCH 01/11] build(deps): bump softprops/action-gh-release from 2.3.3 to 2.4.1 (#830) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/remote-release-trigger.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/remote-release-trigger.yml b/.github/workflows/remote-release-trigger.yml index 5ab27ce42..6d472f860 100644 --- a/.github/workflows/remote-release-trigger.yml +++ b/.github/workflows/remote-release-trigger.yml @@ -66,7 +66,7 @@ jobs: commit_sha: ${{ steps.calculate_head_sha.outputs.commit_sha }} - name: Release - uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # pin@v2.3.3 + uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # pin@v2.4.1 with: target_commitish: 'main' token: ${{ steps.generate_token.outputs.token }} From 4d10dd7783ad40583742c2ce00ea69d70e92cd82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:07:40 -0500 Subject: [PATCH 02/11] build(deps): bump Andrew-Chen-Wang/github-wiki-action from 5.0.1 to 5.0.3 (#826) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish-wiki.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-wiki.yml b/.github/workflows/publish-wiki.yml index 666f78840..e24607285 100644 --- a/.github/workflows/publish-wiki.yml +++ b/.github/workflows/publish-wiki.yml @@ -15,4 +15,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: Andrew-Chen-Wang/github-wiki-action@2c80c13ee98aa43683bd77973ef4916e2eedf817 # pin@v5.0.1 + - uses: Andrew-Chen-Wang/github-wiki-action@6448478bd55f1f3f752c93af8ac03207eccc3213 # pin@v5.0.3 From c2d5637dcc3ca9a354ba954edfa56e89475e4710 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:08:02 -0500 Subject: [PATCH 03/11] build(deps): bump actions/upload-artifact from 4 to 5 (#829) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/e2e-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-suite.yml b/.github/workflows/e2e-suite.yml index f0dc76d14..0f2662e3d 100644 --- a/.github/workflows/e2e-suite.yml +++ b/.github/workflows/e2e-suite.yml @@ -127,7 +127,7 @@ jobs: - name: Upload Test Report as Artifact if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: test-report-file if-no-files-found: ignore From 5dadc242a2991b298487e86396feeb51dda64d06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:08:17 -0500 Subject: [PATCH 04/11] build(deps): bump actions/download-artifact from 5 to 6 (#827) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/e2e-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-suite.yml b/.github/workflows/e2e-suite.yml index 0f2662e3d..2998c906c 100644 --- a/.github/workflows/e2e-suite.yml +++ b/.github/workflows/e2e-suite.yml @@ -243,7 +243,7 @@ jobs: submodules: 'recursive' - name: Download test report - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: test-report-file From a246f534ceb7299d9a4cd14f313d35134cc700db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:08:37 -0500 Subject: [PATCH 05/11] build(deps): bump github/codeql-action from 3 to 4 (#828) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 330afa4bb..256aafe61 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -26,12 +26,12 @@ jobs: uses: actions/checkout@v5 - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" From 1ab118dcf5aeb71ba55bd7e689a66203e060f4b7 Mon Sep 17 00:00:00 2001 From: merll Date: Fri, 7 Nov 2025 22:25:39 +0100 Subject: [PATCH 06/11] feat: use generic cli args for get-kubeconfig plugin (#787) Co-authored-by: Erik Zilber Co-authored-by: Zhiwei Liang --- linodecli/plugins/get-kubeconfig.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/linodecli/plugins/get-kubeconfig.py b/linodecli/plugins/get-kubeconfig.py index d7af0249a..a839416eb 100644 --- a/linodecli/plugins/get-kubeconfig.py +++ b/linodecli/plugins/get-kubeconfig.py @@ -15,6 +15,7 @@ from linodecli.exit_codes import ExitCodes from linodecli.help_formatter import SortingHelpFormatter +from linodecli.plugins import inherit_plugin_args PLUGIN_BASE = "linode-cli get-kubeconfig" @@ -23,8 +24,10 @@ def call(args, context): """ The entrypoint for this plugin """ - parser = argparse.ArgumentParser( - PLUGIN_BASE, add_help=True, formatter_class=SortingHelpFormatter + parser = inherit_plugin_args( + argparse.ArgumentParser( + PLUGIN_BASE, add_help=True, formatter_class=SortingHelpFormatter + ) ) group = parser.add_mutually_exclusive_group() From 0b5b6736cd3e88ffa267d37fa5663052060b4622 Mon Sep 17 00:00:00 2001 From: Pawel <100145168+PawelSnoch@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:32:10 +0100 Subject: [PATCH 07/11] Add tests for cli commands: monitor token-get, alerts help, alerts definition-view --- tests/integration/helpers.py | 7 +++++ tests/integration/monitor/test_alerts.py | 38 +++++++++++++++++++++++ tests/integration/monitor/test_metrics.py | 32 +++++++++++++++---- 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index c58aad09f..35a8a22fe 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -1,5 +1,6 @@ import json import random +import re import subprocess import time from string import ascii_lowercase @@ -204,3 +205,9 @@ def get_random_region_with_caps( matching_region_ids = [region["id"] for region in matching_regions] return random.choice(matching_region_ids) if matching_region_ids else None + + +def assert_help_actions_list(expected_actions, help_output): + output_actions = re.findall("\│\s(\S+)\s*\│", help_output) + for expected_action in expected_actions: + assert expected_action in output_actions diff --git a/tests/integration/monitor/test_alerts.py b/tests/integration/monitor/test_alerts.py index d03471bde..d844f698f 100644 --- a/tests/integration/monitor/test_alerts.py +++ b/tests/integration/monitor/test_alerts.py @@ -3,6 +3,7 @@ from tests.integration.helpers import ( BASE_CMDS, assert_headers_in_lines, + assert_help_actions_list, delete_target_id, exec_test_command, get_random_text, @@ -10,6 +11,27 @@ ) +def test_help_alerts(): + output = exec_test_command( + BASE_CMDS["alerts"] + + [ + "--help", + "--text", + "--delimiter=,", + ] + ) + + actions = [ + "channels-list", + "definition-create", + "definition-delete", + "definition-update", + "definition-view", + "definitions-list-all", + ] + assert_help_actions_list(actions, output) + + def test_channels_list(): res = exec_test_command( BASE_CMDS["alerts"] + ["channels-list", "--text", "--delimiter=,"] @@ -77,6 +99,22 @@ def test_alerts_definition_create(get_channel_id, get_service_type): ) +def test_list_alert_definitions_for_service_type(get_service_type): + service_type = get_service_type + output = exec_test_command( + BASE_CMDS["alerts"] + + [ + "service-definition-view", + service_type, + "--text", + "--delimiter=,", + ] + ) + + headers = ["class", "created", "label", "severity", "service_type"] + assert_headers_in_lines(headers, output.splitlines()) + + def test_alerts_list(): res = exec_test_command( BASE_CMDS["alerts"] diff --git a/tests/integration/monitor/test_metrics.py b/tests/integration/monitor/test_metrics.py index 38742e18e..ae19732da 100644 --- a/tests/integration/monitor/test_metrics.py +++ b/tests/integration/monitor/test_metrics.py @@ -1,8 +1,10 @@ import pytest +from linodecli.exit_codes import ExitCodes from tests.integration.helpers import ( BASE_CMDS, assert_headers_in_lines, + exec_failing_test_command, exec_test_command, ) @@ -62,12 +64,12 @@ def test_service_list(): def test_service_view(get_service_type): - dashboard_id = get_service_type + service_type = get_service_type res = exec_test_command( BASE_CMDS["monitor"] + [ "service-view", - dashboard_id, + service_type, "--text", "--delimiter=,", ] @@ -79,12 +81,12 @@ def test_service_view(get_service_type): def test_dashboard_service_type_list(get_service_type): - dashboard_id = get_service_type + service_type = get_service_type res = exec_test_command( BASE_CMDS["monitor"] + [ "dashboards-list", - dashboard_id, + service_type, "--text", "--delimiter=,", ] @@ -96,12 +98,12 @@ def test_dashboard_service_type_list(get_service_type): def test_metrics_list(get_service_type): - dashboard_id = get_service_type + service_type = get_service_type res = exec_test_command( BASE_CMDS["monitor"] + [ "metrics-list", - dashboard_id, + service_type, "--text", "--delimiter=,", ] @@ -117,3 +119,21 @@ def test_metrics_list(get_service_type): "scrape_interval", ] assert_headers_in_lines(headers, lines) + + +def test_try_create_token_with_not_existing_entity(get_service_type): + service_type = get_service_type + output = exec_failing_test_command( + BASE_CMDS["monitor"] + + [ + "token-get", + service_type, + "--entity_ids", + "99999999999", + "--text", + "--delimiter=,", + ], + expected_code=ExitCodes.REQUEST_FAILED, + ) + assert "Request failed: 403" in output + assert "The following entity_ids are not valid - [99999999999]" in output From 76f1a7aebc418c8639ffaeeed54f9c53f57a1157 Mon Sep 17 00:00:00 2001 From: Vinay <143587840+vshanthe@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:10:19 +0530 Subject: [PATCH 08/11] VPC test related to Dual stack (#831) --- tests/integration/conftest.py | 2 + tests/integration/vpc/conftest.py | 2 + tests/integration/vpc/test_vpc.py | 143 ++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 389007f41..f79439942 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -215,6 +215,8 @@ def create_vpc_w_subnet(): vpc_label, "--region", region, + "--ipv6.range", + "auto", "--subnets.ipv4", "10.0.0.0/24", "--subnets.label", diff --git a/tests/integration/vpc/conftest.py b/tests/integration/vpc/conftest.py index 9f231377c..c5751cb9f 100644 --- a/tests/integration/vpc/conftest.py +++ b/tests/integration/vpc/conftest.py @@ -34,6 +34,8 @@ def test_vpc_wo_subnet(): label, "--region", region, + "--ipv6.range", + "auto", "--no-headers", "--text", "--format=id", diff --git a/tests/integration/vpc/test_vpc.py b/tests/integration/vpc/test_vpc.py index a5f9d1192..4d2300327 100644 --- a/tests/integration/vpc/test_vpc.py +++ b/tests/integration/vpc/test_vpc.py @@ -1,3 +1,4 @@ +import json import re import pytest @@ -221,3 +222,145 @@ def test_fails_to_update_vpc_subenet_w_invalid_label(test_vpc_w_subnet): assert "Request failed: 400" in res assert "Label must include only ASCII" in res + + +def test_create_vpc_with_ipv6_auto(): + region = get_random_region_with_caps(required_capabilities=["VPCs"]) + label = get_random_text(5) + "-vpc" + + res = exec_test_command( + BASE_CMD + + [ + "create", + "--label", + label, + "--region", + region, + "--ipv6.range", + "auto", + "--json", + ] + ) + + vpc_data = json.loads(res)[0] + + assert "id" in vpc_data + assert "ipv6" in vpc_data + assert isinstance(vpc_data["ipv6"], list) + assert len(vpc_data["ipv6"]) > 0 + + ipv6_entry = vpc_data["ipv6"][0] + assert "range" in ipv6_entry + + +@pytest.mark.parametrize("prefix_len", ["52"]) +def test_create_vpc_with_custom_ipv6_prefix_length(prefix_len): + region = get_random_region_with_caps(required_capabilities=["VPCs"]) + label = get_random_text(5) + f"-vpc{prefix_len}" + + res = exec_test_command( + BASE_CMD + + [ + "create", + "--label", + label, + "--region", + region, + "--ipv6.range", + f"/{prefix_len}", + "--json", + ] + ) + + vpc_data = json.loads(res)[0] + + assert "ipv6" in vpc_data + ipv6_entry = vpc_data["ipv6"][0] + ipv6_range = ipv6_entry.get("range", "") + assert isinstance(ipv6_range, str) + assert ipv6_range.endswith(f"/{prefix_len}") + + +def test_create_subnet_with_ipv6_auto(test_vpc_wo_subnet): + vpc_id = test_vpc_wo_subnet + subnet_label = get_random_text(5) + "-ipv6subnet" + + res = exec_test_command( + BASE_CMD + + [ + "subnet-create", + "--label", + subnet_label, + "--ipv4", + "10.0.10.0/24", + "--ipv6.range", + "auto", + vpc_id, + "--json", + ] + ) + + subnet_data = json.loads(res)[0] + + assert "id" in subnet_data + assert ( + "ipv6" in subnet_data + ), f"No IPv6 info found in response: {subnet_data}" + + ipv6_entries = subnet_data["ipv6"] + assert ( + isinstance(ipv6_entries, list) and len(ipv6_entries) > 0 + ), "Expected non-empty IPv6 list" + + ipv6_range = ipv6_entries[0].get("range", "") + assert isinstance(ipv6_range, str) + assert "/" in ipv6_range, f"Unexpected IPv6 CIDR format: {ipv6_range}" + + +def test_fails_to_create_vpc_with_invalid_ipv6_range(): + region = get_random_region_with_caps(required_capabilities=["VPCs"]) + label = get_random_text(5) + "-invalidvpc" + + res = exec_failing_test_command( + BASE_CMD + + [ + "create", + "--label", + label, + "--region", + region, + "--ipv6.range", + "10.0.0.0/64", + ], + ExitCodes.REQUEST_FAILED, + ) + + assert "Request failed: 400" in res + + +def test_list_vpc_ip_address(): + + res = exec_test_command( + BASE_CMD + ["ips-all-list", "--text", "--delimiter=,"] + ) + + lines = res.splitlines() + + headers = ["address", "region", "subnet_id"] + + for header in headers: + assert header in lines[0] + + +def test_list_vpc_ipv6s_address(): + + res = exec_test_command( + BASE_CMD + ["ipv6s-all-list", "--text", "--delimiter=,"] + ) + + lines = res.splitlines() + + headers = ["address", "region", "subnet_id"] + + for header in headers: + assert header in lines[0] From 15177171c29b8c5fe7e2aa6c0081da09c74ecb23 Mon Sep 17 00:00:00 2001 From: Pawel <100145168+PawelSnoch@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:11:44 +0100 Subject: [PATCH 09/11] Adds integration tests for private image sharing (#833) --- tests/integration/helpers.py | 4 +- tests/integration/sharegroups/fixtures.py | 139 ++++++ .../sharegroups/test_images_sharegroups.py | 426 ++++++++++++++++++ 3 files changed, 568 insertions(+), 1 deletion(-) create mode 100644 tests/integration/sharegroups/fixtures.py create mode 100644 tests/integration/sharegroups/test_images_sharegroups.py diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 35a8a22fe..f74d81957 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -28,6 +28,8 @@ "domains", "events", "image", + "images", + "image-sharegroups", "image-upload", "firewalls", "kernels", @@ -208,6 +210,6 @@ def get_random_region_with_caps( def assert_help_actions_list(expected_actions, help_output): - output_actions = re.findall("\│\s(\S+)\s*\│", help_output) + output_actions = re.findall(r"│\s(\S+(?:,\s)?\S+)\s*│", help_output) for expected_action in expected_actions: assert expected_action in output_actions diff --git a/tests/integration/sharegroups/fixtures.py b/tests/integration/sharegroups/fixtures.py new file mode 100644 index 000000000..488b1a023 --- /dev/null +++ b/tests/integration/sharegroups/fixtures.py @@ -0,0 +1,139 @@ +import pytest + +from tests.integration.helpers import ( + BASE_CMDS, + exec_test_command, + get_random_text, +) + + +@pytest.fixture +def get_region(): + regions = exec_test_command( + BASE_CMDS["regions"] + + [ + "list", + "--text", + "--no-headers", + "--delimiter", + ",", + "--format", + "id", + ] + ).splitlines() + first_id = regions[0] + yield first_id + + +def wait_for_image_status(id, expected_status, timeout=180, interval=5): + import time + + current_status = exec_test_command( + BASE_CMDS["images"] + + [ + "view", + id, + "--text", + "--no-headers", + "--delimiter", + ",", + "--format", + "status", + ] + ).splitlines() + timer = 0 + while current_status[0] != expected_status and timer < timeout: + time.sleep(interval) + timer += interval + current_status = exec_test_command( + BASE_CMDS["images"] + + [ + "view", + id, + "--text", + "--no-headers", + "--delimiter", + ",", + "--format", + "status", + ] + ).splitlines() + if timer >= timeout: + raise TimeoutError( + f"Created image did not reach status '{expected_status}' within {timeout} seconds." + ) + + +@pytest.fixture(scope="function") +def create_image_id(get_region): + linode_id = exec_test_command( + BASE_CMDS["linodes"] + + [ + "create", + "--image", + "linode/alpine3.22", + "--region", + get_region, + "--type", + "g6-nanode-1", + "--root_pass", + "aComplex@Password", + "--text", + "--no-headers", + "--delimiter", + ",", + "--format", + "id", + ] + ) + disks = exec_test_command( + BASE_CMDS["linodes"] + + [ + "disks-list", + linode_id, + "--text", + "--no-headers", + "--delimiter", + ",", + "--format", + "id", + ] + ).splitlines() + image_id = exec_test_command( + BASE_CMDS["images"] + + [ + "create", + "--label", + "linode-cli-test-image-sharing-image", + "--disk_id", + disks[0], + "--text", + "--no-headers", + "--delimiter", + ",", + "--format", + "id", + ] + ) + wait_for_image_status(image_id, "available") + yield linode_id, image_id + + +@pytest.fixture(scope="function") +def create_share_group(): + label = get_random_text(8) + "_sharegroup_cli_test" + share_group = exec_test_command( + BASE_CMDS["image-sharegroups"] + + [ + "create", + "--label", + label, + "--text", + "--no-headers", + "--delimiter", + ",", + "--format", + "id,uuid", + ] + ).split(",") + yield share_group[0], share_group[1] diff --git a/tests/integration/sharegroups/test_images_sharegroups.py b/tests/integration/sharegroups/test_images_sharegroups.py new file mode 100644 index 000000000..1ab8bf340 --- /dev/null +++ b/tests/integration/sharegroups/test_images_sharegroups.py @@ -0,0 +1,426 @@ +from linodecli.exit_codes import ExitCodes +from tests.integration.helpers import ( + BASE_CMDS, + assert_headers_in_lines, + assert_help_actions_list, + delete_target_id, + exec_failing_test_command, + exec_test_command, + get_random_text, +) +from tests.integration.sharegroups.fixtures import ( # noqa: F401 + create_image_id, + create_share_group, + get_region, +) + + +def test_help_image_sharegroups(): + output = exec_test_command( + BASE_CMDS["image-sharegroups"] + ["--help", "--text", "--delimiter=,"] + ) + actions = [ + "create", + "delete, rm", + "image-add", + "image-remove", + "image-update", + "images-list", + "images-list-by-token", + "list, ls", + "member-add", + "member-delete", + "member-update", + "member-view", + "members-list", + "token-create", + "token-delete", + "token-update", + "token-view", + "tokens-list", + "update", + "view", + "view-by-token", + ] + assert_help_actions_list(actions, output) + + +def test_list_all_share_groups(): + result = exec_test_command( + BASE_CMDS["image-sharegroups"] + ["list", "--delimiter", ",", "--text"] + ) + lines = result.splitlines() + headers = [ + "id", + "label", + "uuid", + "description", + "is_suspended", + "images_count", + "members_count", + ] + assert_headers_in_lines(headers, lines) + + +def test_add_list_update_remove_image_to_share_group( + create_share_group, create_image_id +): + result_add_image = exec_test_command( + BASE_CMDS["image-sharegroups"] + + [ + "image-add", + "--images.id", + create_image_id[1], + create_share_group[0], + "--delimiter", + ",", + "--text", + ] + ).splitlines() + headers = [ + "id", + "label", + "description", + "size", + "total_size", + "capabilities", + "is_public", + "is_shared", + "tags", + ] + assert_headers_in_lines(headers, result_add_image) + assert "linode-cli-test-image-sharing-image" in result_add_image[1] + + result_list = exec_test_command( + BASE_CMDS["image-sharegroups"] + + ["images-list", create_share_group[0], "--delimiter", ",", "--text"] + ).splitlines() + headers = [ + "id", + "label", + "description", + "size", + "total_size", + "capabilities", + "is_public", + "is_shared", + "tags", + ] + assert_headers_in_lines(headers, result_list) + assert "linode-cli-test-image-sharing-image" in result_list[1] + share_image_id = result_list[1].split(",")[0] + + result_update_image = exec_test_command( + BASE_CMDS["image-sharegroups"] + + [ + "image-update", + create_share_group[0], + share_image_id, + "--label", + "updated_label", + "--description", + "Updated description.", + "--delimiter", + ",", + "--text", + ] + ).splitlines() + assert_headers_in_lines(headers, result_update_image) + assert "updated_label" in result_update_image[1] + assert "Updated description." in result_update_image[1] + + exec_test_command( + BASE_CMDS["image-sharegroups"] + + [ + "image-remove", + create_share_group[0], + share_image_id, + "--delimiter", + ",", + "--text", + ] + ).splitlines() + + result_list = exec_test_command( + BASE_CMDS["image-sharegroups"] + + ["images-list", create_share_group[0], "--delimiter", ",", "--text"] + ) + assert "linode-cli-test-image-sharing-image" not in result_list + assert "updated_label" not in result_list + + delete_target_id(target="image-sharegroups", id=create_share_group[0]) + delete_target_id(target="images", id=create_image_id[1]) + delete_target_id(target="linodes", id=create_image_id[0]) + + +def test_try_add_member_use_invalid_token(create_share_group): + result = exec_failing_test_command( + BASE_CMDS["image-sharegroups"] + + [ + "member-add", + "--token", + "notExistingToken", + "--label", + "test add member", + create_share_group[0], + "--delimiter", + ",", + "--text", + ], + expected_code=ExitCodes.REQUEST_FAILED, + ) + assert "Request failed: 500" in result + assert "Invalid token format" in result + + delete_target_id(target="image-sharegroups", id=create_share_group[0]) + + +def test_list_members_for_invalid_token(create_share_group): + result = exec_test_command( + BASE_CMDS["image-sharegroups"] + + [ + "members-list", + "--token", + "notExistingToken", + create_share_group[0], + "--delimiter", + ",", + "--text", + ] + ).splitlines() + headers = ["token_uuid", "label", "status"] + assert_headers_in_lines(headers, result) + + delete_target_id(target="image-sharegroups", id=create_share_group[0]) + + +def test_try_revoke_membership_for_invalid_token(): + result = exec_failing_test_command( + BASE_CMDS["image-sharegroups"] + + [ + "member-delete", + "9876543", + "notExistingToken", + "--delimiter", + ",", + "--text", + "--no-headers", + ], + expected_code=ExitCodes.REQUEST_FAILED, + ) + assert "Request failed: 404" in result + assert "Not found" in result + + +def test_try_update_membership_for_invalid_token(): + result = exec_failing_test_command( + BASE_CMDS["image-sharegroups"] + + [ + "member-update", + "9876543", + "notExistingToken", + "--delimiter", + ",", + "--text", + "--no-headers", + "--label", + "update", + ], + expected_code=ExitCodes.REQUEST_FAILED, + ) + assert "Request failed: 404" in result + assert "Not found" in result + + +def test_try_view_membership_for_invalid_token(): + result = exec_failing_test_command( + BASE_CMDS["image-sharegroups"] + + [ + "member-view", + "9876543", + "notExistingToken", + "--delimiter", + ",", + "--text", + "--no-headers", + ], + expected_code=ExitCodes.REQUEST_FAILED, + ) + assert "Request failed: 404" in result + assert "Not found" in result + + +def test_create_read_update_delete_share_group(): + group_label = get_random_text(8) + "_sharegroup_cli_test" + create_result = exec_test_command( + BASE_CMDS["image-sharegroups"] + + [ + "create", + "--label", + group_label, + "--description", + "Test create", + "--delimiter", + ",", + "--text", + ] + ).splitlines() + headers = [ + "id", + "label", + "uuid", + "description", + "is_suspended", + "images_count", + "members_count", + ] + assert_headers_in_lines(headers, create_result) + assert group_label in create_result[1] + assert "Test create" in create_result[1] + share_group_id = create_result[1].split(",")[0] + + get_result = exec_test_command( + BASE_CMDS["image-sharegroups"] + + ["view", share_group_id, "--delimiter", ",", "--text"] + ).splitlines() + assert_headers_in_lines(headers, get_result) + assert group_label in get_result[1] + + update_result = exec_test_command( + BASE_CMDS["image-sharegroups"] + + [ + "update", + "--description", + "Description update", + "--label", + group_label + "_updated", + share_group_id, + "--delimiter", + ",", + "--text", + ] + ).splitlines() + assert_headers_in_lines(headers, update_result) + assert group_label + "_updated" in update_result[1] + assert "Description update" in update_result[1] + + exec_test_command( + BASE_CMDS["image-sharegroups"] + ["delete", share_group_id] + ) + result_after_delete = exec_failing_test_command( + BASE_CMDS["image-sharegroups"] + + ["view", share_group_id, "--delimiter", ",", "--text"], + expected_code=ExitCodes.REQUEST_FAILED, + ) + assert "Request failed: 404" in result_after_delete + assert "Not found" in result_after_delete + + +def test_try_to_create_token(create_share_group): + share_group_uuid = create_share_group[1] + result = exec_failing_test_command( + BASE_CMDS["image-sharegroups"] + + [ + "token-create", + "--label", + "cli_test", + "--valid_for_sharegroup_uuid", + share_group_uuid, + "--delimiter", + ",", + "--text", + ], + expected_code=ExitCodes.REQUEST_FAILED, + ) + assert "Request failed: 400" in result + assert "You may not create a token for your own sharegroup" in result + + delete_target_id(target="image-sharegroups", id=create_share_group[0]) + + +def test_try_read_invalid_token(): + result = exec_failing_test_command( + BASE_CMDS["image-sharegroups"] + + ["token-view", "36b0-4d52_invalid", "--delimiter", ",", "--text"], + expected_code=ExitCodes.REQUEST_FAILED, + ) + assert "Request failed: 404" in result + assert "Not found" in result + + +def test_try_to_update_invalid_token(): + result = exec_failing_test_command( + BASE_CMDS["image-sharegroups"] + + [ + "token-update", + "--label", + "cli_test_update", + "36b0-4d52_invalid", + "--delimiter", + ",", + "--text", + ], + expected_code=ExitCodes.REQUEST_FAILED, + ) + assert "Request failed: 404" in result + assert "Not found" in result + + +def test_try_to_delete_token(): + result = exec_failing_test_command( + BASE_CMDS["image-sharegroups"] + + ["token-delete", "36b0-4d52_invalid", "--delimiter", ",", "--text"], + expected_code=ExitCodes.REQUEST_FAILED, + ) + assert "Request failed: 404" in result + assert "Not found" in result + + +def test_get_details_about_all_the_users_tokens(): + result = exec_test_command( + BASE_CMDS["image-sharegroups"] + + ["tokens-list", "--delimiter", ",", "--text"] + ) + lines = result.splitlines() + headers = [ + "token_uuid", + "label", + "status", + "valid_for_sharegroup_uuid", + "sharegroup_uuid", + "sharegroup_label", + ] + assert_headers_in_lines(headers, lines) + + +def test_try_to_list_all_shared_images_for_invalid_token(): + result = exec_failing_test_command( + BASE_CMDS["image-sharegroups"] + + [ + "images-list-by-token", + "notExistingToken", + "--delimiter", + ",", + "--text", + ], + expected_code=ExitCodes.REQUEST_FAILED, + ) + assert "Request failed: 404" in result + assert "Not found" in result + + +def test_try_gets_details_about_your_share_group_for_invalid_token(): + result = exec_failing_test_command( + BASE_CMDS["image-sharegroups"] + + [ + "view-by-token", + "notExistingToken", + "--delimiter", + ",", + "--text", + "--no-headers", + ], + expected_code=ExitCodes.REQUEST_FAILED, + ) + assert "Request failed: 404" in result + assert "Not found" in result From c57396187ac63c802243ef2fcd5e8b940bd31dfd Mon Sep 17 00:00:00 2001 From: Erik Zilber Date: Wed, 19 Nov 2025 12:09:33 -0500 Subject: [PATCH 10/11] Point to v4beta for image sharing tests (#834) --- tests/integration/sharegroups/fixtures.py | 4 +- .../sharegroups/test_images_sharegroups.py | 58 ++++++++++++++----- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/tests/integration/sharegroups/fixtures.py b/tests/integration/sharegroups/fixtures.py index 488b1a023..82461d54c 100644 --- a/tests/integration/sharegroups/fixtures.py +++ b/tests/integration/sharegroups/fixtures.py @@ -1,4 +1,5 @@ import pytest +from pytest import MonkeyPatch from tests.integration.helpers import ( BASE_CMDS, @@ -120,7 +121,8 @@ def create_image_id(get_region): @pytest.fixture(scope="function") -def create_share_group(): +def create_share_group(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") label = get_random_text(8) + "_sharegroup_cli_test" share_group = exec_test_command( BASE_CMDS["image-sharegroups"] diff --git a/tests/integration/sharegroups/test_images_sharegroups.py b/tests/integration/sharegroups/test_images_sharegroups.py index 1ab8bf340..c8fdc3cd7 100644 --- a/tests/integration/sharegroups/test_images_sharegroups.py +++ b/tests/integration/sharegroups/test_images_sharegroups.py @@ -1,3 +1,5 @@ +from pytest import MonkeyPatch + from linodecli.exit_codes import ExitCodes from tests.integration.helpers import ( BASE_CMDS, @@ -15,7 +17,8 @@ ) -def test_help_image_sharegroups(): +def test_help_image_sharegroups(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") output = exec_test_command( BASE_CMDS["image-sharegroups"] + ["--help", "--text", "--delimiter=,"] ) @@ -45,7 +48,8 @@ def test_help_image_sharegroups(): assert_help_actions_list(actions, output) -def test_list_all_share_groups(): +def test_list_all_share_groups(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") result = exec_test_command( BASE_CMDS["image-sharegroups"] + ["list", "--delimiter", ",", "--text"] ) @@ -63,8 +67,9 @@ def test_list_all_share_groups(): def test_add_list_update_remove_image_to_share_group( - create_share_group, create_image_id + create_share_group, create_image_id, monkeypatch: MonkeyPatch ): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") result_add_image = exec_test_command( BASE_CMDS["image-sharegroups"] + [ @@ -153,7 +158,10 @@ def test_add_list_update_remove_image_to_share_group( delete_target_id(target="linodes", id=create_image_id[0]) -def test_try_add_member_use_invalid_token(create_share_group): +def test_try_add_member_use_invalid_token( + create_share_group, monkeypatch: MonkeyPatch +): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") result = exec_failing_test_command( BASE_CMDS["image-sharegroups"] + [ @@ -175,7 +183,10 @@ def test_try_add_member_use_invalid_token(create_share_group): delete_target_id(target="image-sharegroups", id=create_share_group[0]) -def test_list_members_for_invalid_token(create_share_group): +def test_list_members_for_invalid_token( + create_share_group, monkeypatch: MonkeyPatch +): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") result = exec_test_command( BASE_CMDS["image-sharegroups"] + [ @@ -194,7 +205,8 @@ def test_list_members_for_invalid_token(create_share_group): delete_target_id(target="image-sharegroups", id=create_share_group[0]) -def test_try_revoke_membership_for_invalid_token(): +def test_try_revoke_membership_for_invalid_token(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") result = exec_failing_test_command( BASE_CMDS["image-sharegroups"] + [ @@ -212,7 +224,8 @@ def test_try_revoke_membership_for_invalid_token(): assert "Not found" in result -def test_try_update_membership_for_invalid_token(): +def test_try_update_membership_for_invalid_token(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") result = exec_failing_test_command( BASE_CMDS["image-sharegroups"] + [ @@ -232,7 +245,8 @@ def test_try_update_membership_for_invalid_token(): assert "Not found" in result -def test_try_view_membership_for_invalid_token(): +def test_try_view_membership_for_invalid_token(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") result = exec_failing_test_command( BASE_CMDS["image-sharegroups"] + [ @@ -250,7 +264,8 @@ def test_try_view_membership_for_invalid_token(): assert "Not found" in result -def test_create_read_update_delete_share_group(): +def test_create_read_update_delete_share_group(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") group_label = get_random_text(8) + "_sharegroup_cli_test" create_result = exec_test_command( BASE_CMDS["image-sharegroups"] @@ -316,7 +331,8 @@ def test_create_read_update_delete_share_group(): assert "Not found" in result_after_delete -def test_try_to_create_token(create_share_group): +def test_try_to_create_token(create_share_group, monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") share_group_uuid = create_share_group[1] result = exec_failing_test_command( BASE_CMDS["image-sharegroups"] @@ -338,7 +354,8 @@ def test_try_to_create_token(create_share_group): delete_target_id(target="image-sharegroups", id=create_share_group[0]) -def test_try_read_invalid_token(): +def test_try_read_invalid_token(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") result = exec_failing_test_command( BASE_CMDS["image-sharegroups"] + ["token-view", "36b0-4d52_invalid", "--delimiter", ",", "--text"], @@ -348,7 +365,8 @@ def test_try_read_invalid_token(): assert "Not found" in result -def test_try_to_update_invalid_token(): +def test_try_to_update_invalid_token(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") result = exec_failing_test_command( BASE_CMDS["image-sharegroups"] + [ @@ -366,7 +384,8 @@ def test_try_to_update_invalid_token(): assert "Not found" in result -def test_try_to_delete_token(): +def test_try_to_delete_token(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") result = exec_failing_test_command( BASE_CMDS["image-sharegroups"] + ["token-delete", "36b0-4d52_invalid", "--delimiter", ",", "--text"], @@ -376,7 +395,8 @@ def test_try_to_delete_token(): assert "Not found" in result -def test_get_details_about_all_the_users_tokens(): +def test_get_details_about_all_the_users_tokens(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") result = exec_test_command( BASE_CMDS["image-sharegroups"] + ["tokens-list", "--delimiter", ",", "--text"] @@ -393,7 +413,10 @@ def test_get_details_about_all_the_users_tokens(): assert_headers_in_lines(headers, lines) -def test_try_to_list_all_shared_images_for_invalid_token(): +def test_try_to_list_all_shared_images_for_invalid_token( + monkeypatch: MonkeyPatch, +): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") result = exec_failing_test_command( BASE_CMDS["image-sharegroups"] + [ @@ -409,7 +432,10 @@ def test_try_to_list_all_shared_images_for_invalid_token(): assert "Not found" in result -def test_try_gets_details_about_your_share_group_for_invalid_token(): +def test_try_gets_details_about_your_share_group_for_invalid_token( + monkeypatch: MonkeyPatch, +): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") result = exec_failing_test_command( BASE_CMDS["image-sharegroups"] + [ From b6a1b6a7b8c8a503d0dace2538ef346056da52c1 Mon Sep 17 00:00:00 2001 From: Erik Zilber Date: Wed, 19 Nov 2025 14:30:12 -0500 Subject: [PATCH 11/11] Fix various integration test issues (#836) --- tests/integration/conftest.py | 4 ++-- .../database/test_database_engine_config.py | 2 +- tests/integration/monitor/test_alerts.py | 2 +- tests/integration/vpc/conftest.py | 4 ++-- tests/integration/vpc/test_vpc.py | 19 +++++++++++++++++++ 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index f79439942..253f1498e 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -215,8 +215,8 @@ def create_vpc_w_subnet(): vpc_label, "--region", region, - "--ipv6.range", - "auto", + # "--ipv6.range", TODO: Uncomment after VPC Dual Stack is ready to ship + # "auto", "--subnets.ipv4", "10.0.0.0/24", "--subnets.label", diff --git a/tests/integration/database/test_database_engine_config.py b/tests/integration/database/test_database_engine_config.py index 92fc6b7e5..55c7fa8a9 100644 --- a/tests/integration/database/test_database_engine_config.py +++ b/tests/integration/database/test_database_engine_config.py @@ -497,7 +497,7 @@ def test_mysql_engine_config_view(): binlog_retention = mysql_config[0]["binlog_retention_period"] assert binlog_retention["type"] == "integer" assert binlog_retention["minimum"] == 600 - assert binlog_retention["maximum"] == 604800 + assert binlog_retention["maximum"] == 9007199254740991 assert binlog_retention["requires_restart"] is False mysql_settings = mysql_config[0]["mysql"] diff --git a/tests/integration/monitor/test_alerts.py b/tests/integration/monitor/test_alerts.py index d844f698f..15ff6449f 100644 --- a/tests/integration/monitor/test_alerts.py +++ b/tests/integration/monitor/test_alerts.py @@ -104,7 +104,7 @@ def test_list_alert_definitions_for_service_type(get_service_type): output = exec_test_command( BASE_CMDS["alerts"] + [ - "service-definition-view", + "service-definitions-list", service_type, "--text", "--delimiter=,", diff --git a/tests/integration/vpc/conftest.py b/tests/integration/vpc/conftest.py index c5751cb9f..9b1abace0 100644 --- a/tests/integration/vpc/conftest.py +++ b/tests/integration/vpc/conftest.py @@ -34,8 +34,8 @@ def test_vpc_wo_subnet(): label, "--region", region, - "--ipv6.range", - "auto", + # "--ipv6.range", TODO: Uncomment after VPC Dual Stack is ready to ship + # "auto", "--no-headers", "--text", "--format=id", diff --git a/tests/integration/vpc/test_vpc.py b/tests/integration/vpc/test_vpc.py index 4d2300327..0117374db 100644 --- a/tests/integration/vpc/test_vpc.py +++ b/tests/integration/vpc/test_vpc.py @@ -15,6 +15,10 @@ BASE_CMD = ["linode-cli", "vpcs"] +# TODO: Remove this variable and @pytest.mark.skipif once VPC Dual Stack is ready to ship +disable_vpc_dual_stack_tests = True + + def test_list_vpcs(test_vpc_wo_subnet): vpc_id = test_vpc_wo_subnet res = exec_test_command(BASE_CMDS["vpcs"] + ["ls", "--text"]) @@ -224,6 +228,9 @@ def test_fails_to_update_vpc_subenet_w_invalid_label(test_vpc_w_subnet): assert "Label must include only ASCII" in res +@pytest.mark.skipif( + disable_vpc_dual_stack_tests, reason="Dual-stack tests disabled" +) def test_create_vpc_with_ipv6_auto(): region = get_random_region_with_caps(required_capabilities=["VPCs"]) label = get_random_text(5) + "-vpc" @@ -254,6 +261,9 @@ def test_create_vpc_with_ipv6_auto(): @pytest.mark.parametrize("prefix_len", ["52"]) +@pytest.mark.skipif( + disable_vpc_dual_stack_tests, reason="Dual-stack tests disabled" +) def test_create_vpc_with_custom_ipv6_prefix_length(prefix_len): region = get_random_region_with_caps(required_capabilities=["VPCs"]) label = get_random_text(5) + f"-vpc{prefix_len}" @@ -281,6 +291,9 @@ def test_create_vpc_with_custom_ipv6_prefix_length(prefix_len): assert ipv6_range.endswith(f"/{prefix_len}") +@pytest.mark.skipif( + disable_vpc_dual_stack_tests, reason="Dual-stack tests disabled" +) def test_create_subnet_with_ipv6_auto(test_vpc_wo_subnet): vpc_id = test_vpc_wo_subnet subnet_label = get_random_text(5) + "-ipv6subnet" @@ -317,6 +330,9 @@ def test_create_subnet_with_ipv6_auto(test_vpc_wo_subnet): assert "/" in ipv6_range, f"Unexpected IPv6 CIDR format: {ipv6_range}" +@pytest.mark.skipif( + disable_vpc_dual_stack_tests, reason="Dual-stack tests disabled" +) def test_fails_to_create_vpc_with_invalid_ipv6_range(): region = get_random_region_with_caps(required_capabilities=["VPCs"]) label = get_random_text(5) + "-invalidvpc" @@ -352,6 +368,9 @@ def test_list_vpc_ip_address(): assert header in lines[0] +@pytest.mark.skipif( + disable_vpc_dual_stack_tests, reason="Dual-stack tests disabled" +) def test_list_vpc_ipv6s_address(): res = exec_test_command(