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}}" diff --git a/.github/workflows/e2e-suite.yml b/.github/workflows/e2e-suite.yml index f0dc76d14..2998c906c 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 @@ -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 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 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 }} 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() diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 389007f41..253f1498e 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", 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/helpers.py b/tests/integration/helpers.py index c58aad09f..f74d81957 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 @@ -27,6 +28,8 @@ "domains", "events", "image", + "images", + "image-sharegroups", "image-upload", "firewalls", "kernels", @@ -204,3 +207,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(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/monitor/test_alerts.py b/tests/integration/monitor/test_alerts.py index d03471bde..15ff6449f 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-definitions-list", + 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 diff --git a/tests/integration/sharegroups/fixtures.py b/tests/integration/sharegroups/fixtures.py new file mode 100644 index 000000000..82461d54c --- /dev/null +++ b/tests/integration/sharegroups/fixtures.py @@ -0,0 +1,141 @@ +import pytest +from pytest import MonkeyPatch + +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(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"] + + [ + "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..c8fdc3cd7 --- /dev/null +++ b/tests/integration/sharegroups/test_images_sharegroups.py @@ -0,0 +1,452 @@ +from pytest import MonkeyPatch + +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(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") + 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(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") + 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, monkeypatch: MonkeyPatch +): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") + 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, monkeypatch: MonkeyPatch +): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") + 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, monkeypatch: MonkeyPatch +): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") + 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(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") + 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(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") + 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(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") + 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(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"] + + [ + "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, 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"] + + [ + "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(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"], + expected_code=ExitCodes.REQUEST_FAILED, + ) + assert "Request failed: 404" in result + assert "Not found" in result + + +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"] + + [ + "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(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"], + 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(monkeypatch: MonkeyPatch): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") + 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( + monkeypatch: MonkeyPatch, +): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") + 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( + monkeypatch: MonkeyPatch, +): + monkeypatch.setenv("LINODE_CLI_API_VERSION", "v4beta") + 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 diff --git a/tests/integration/vpc/conftest.py b/tests/integration/vpc/conftest.py index 9f231377c..9b1abace0 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", 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 a5f9d1192..0117374db 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 @@ -14,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"]) @@ -221,3 +226,160 @@ 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 + + +@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" + + 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"]) +@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}" + + 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}") + + +@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" + + 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}" + + +@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" + + 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] + + +@pytest.mark.skipif( + disable_vpc_dual_stack_tests, reason="Dual-stack tests disabled" +) +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]