|
16 | 16 | from linodecli import configuration |
17 | 17 | from linodecli.configuration import ( |
18 | 18 | _bool_input, |
19 | | - _check_full_access, |
20 | 19 | _default_text_input, |
21 | 20 | _default_thing_input, |
22 | 21 | ) |
@@ -299,7 +298,7 @@ def mock_input(prompt): |
299 | 298 | requests_mock.Mocker() as m, |
300 | 299 | ): |
301 | 300 | m.get(f"{self.base_url}/profile", json={"username": "cli-dev"}) |
302 | | - m.get(f"{self.base_url}/profile/grants", status_code=204) |
| 301 | + m.get(f"{self.base_url}/profile/sshkeys", json={"data": []}) |
303 | 302 | m.get( |
304 | 303 | f"{self.base_url}/regions", |
305 | 304 | json={"data": [{"id": "test-region"}]}, |
@@ -350,7 +349,7 @@ def mock_input(prompt): |
350 | 349 | requests_mock.Mocker() as m, |
351 | 350 | ): |
352 | 351 | m.get(f"{self.base_url}/profile", json={"username": "cli-dev"}) |
353 | | - m.get(f"{self.base_url}/profile/grants", status_code=204) |
| 352 | + m.get(f"{self.base_url}/profile/sshkeys", json={"data": []}) |
354 | 353 | m.get( |
355 | 354 | f"{self.base_url}/regions", |
356 | 355 | json={"data": [{"id": "test-region"}]}, |
@@ -679,39 +678,103 @@ def test_custom_config_path(self, monkeypatch, tmp_path): |
679 | 678 | assert expected_configs[i] == configs[i] |
680 | 679 |
|
681 | 680 |
|
682 | | -class TestCheckFullAccess: |
| 681 | +class TestConfigureAuthorizedUsers: |
683 | 682 | """ |
684 | | - Unit tests for _check_full_access |
| 683 | + Unit tests for the authorized_users gate in CLIConfig.configure(), |
| 684 | + which uses GET /profile/sshkeys as a capability check instead of |
| 685 | + GET /profile/grants (which is inaccessible to IAM-enrolled users). |
685 | 686 | """ |
686 | 687 |
|
687 | 688 | base_url = "https://linode-test.com" |
688 | 689 | test_token = "cli-dev-token" |
689 | 690 |
|
690 | | - def test_full_access_returns_true(self): |
| 691 | + def _base_mocks(self, m, *, sshkeys_status=200, users_json=None): |
| 692 | + """Register the common mocks needed for a configure() call.""" |
| 693 | + m.get(f"{self.base_url}/profile", json={"username": "cli-dev"}) |
| 694 | + m.get( |
| 695 | + f"{self.base_url}/profile/sshkeys", |
| 696 | + status_code=sshkeys_status, |
| 697 | + json={"data": []}, |
| 698 | + ) |
| 699 | + m.get(f"{self.base_url}/regions", json={"data": [{"id": "us-east"}]}) |
| 700 | + m.get( |
| 701 | + f"{self.base_url}/linode/types", |
| 702 | + json={"data": [{"id": "g6-nanode-1"}]}, |
| 703 | + ) |
| 704 | + m.get( |
| 705 | + f"{self.base_url}/images", |
| 706 | + json={"data": [{"id": "linode/debian12"}]}, |
| 707 | + ) |
| 708 | + if users_json is not None: |
| 709 | + m.get(f"{self.base_url}/account/users", json=users_json) |
| 710 | + |
| 711 | + def test_sshkeys_success_populates_authorized_users(self): |
691 | 712 | """ |
692 | | - 204 No Content means the token has full (unrestricted) access. |
| 713 | + When /profile/sshkeys succeeds, /account/users is fetched and |
| 714 | + authorized_users is offered. |
693 | 715 | """ |
694 | | - with requests_mock.Mocker() as m: |
695 | | - m.get(f"{self.base_url}/profile/grants", status_code=204) |
696 | | - assert _check_full_access(self.base_url, self.test_token) is True |
| 716 | + conf = configuration.CLIConfig(self.base_url, skip_config=True) |
| 717 | + answers = iter(["1", "1", "1", "1", "n", "n"]) |
697 | 718 |
|
698 | | - def test_restricted_access_returns_false(self): |
| 719 | + with ( |
| 720 | + patch("linodecli.configuration.open", mock_open()), |
| 721 | + patch("os.chmod", lambda a, b: None), |
| 722 | + patch("builtins.input", lambda _: next(answers)), |
| 723 | + contextlib.redirect_stdout(io.StringIO()), |
| 724 | + patch("linodecli.configuration._check_browsers", lambda: False), |
| 725 | + patch.dict(os.environ, {"LINODE_CLI_TOKEN": self.test_token}), |
| 726 | + requests_mock.Mocker() as m, |
| 727 | + ): |
| 728 | + self._base_mocks( |
| 729 | + m, |
| 730 | + users_json={ |
| 731 | + "data": [{"username": "cli-dev", "ssh_keys": ["key1"]}] |
| 732 | + }, |
| 733 | + ) |
| 734 | + conf.configure() |
| 735 | + |
| 736 | + assert conf.get_value("authorized_users") == "cli-dev" |
| 737 | + |
| 738 | + def test_sshkeys_401_skips_authorized_users(self): |
699 | 739 | """ |
700 | | - 200 with a grants body means the token has restricted access. |
| 740 | + When /profile/sshkeys returns 401 (restricted token), the |
| 741 | + authorized_users prompt is skipped. |
701 | 742 | """ |
702 | | - with requests_mock.Mocker() as m: |
703 | | - m.get( |
704 | | - f"{self.base_url}/profile/grants", |
705 | | - status_code=200, |
706 | | - json={"linode": []}, |
707 | | - ) |
708 | | - assert _check_full_access(self.base_url, self.test_token) is False |
| 743 | + conf = configuration.CLIConfig(self.base_url, skip_config=True) |
| 744 | + answers = iter(["1", "1", "1", "n", "n"]) |
709 | 745 |
|
710 | | - def test_iam_user_403_returns_false(self): |
| 746 | + with ( |
| 747 | + patch("linodecli.configuration.open", mock_open()), |
| 748 | + patch("os.chmod", lambda a, b: None), |
| 749 | + patch("builtins.input", lambda _: next(answers)), |
| 750 | + contextlib.redirect_stdout(io.StringIO()), |
| 751 | + patch("linodecli.configuration._check_browsers", lambda: False), |
| 752 | + patch.dict(os.environ, {"LINODE_CLI_TOKEN": self.test_token}), |
| 753 | + requests_mock.Mocker() as m, |
| 754 | + ): |
| 755 | + self._base_mocks(m, sshkeys_status=401) |
| 756 | + conf.configure() |
| 757 | + |
| 758 | + assert conf.get_value("authorized_users") is None |
| 759 | + |
| 760 | + def test_sshkeys_403_iam_skips_authorized_users(self): |
711 | 761 | """ |
712 | | - IAM-enrolled users receive a 403 from /profile/grants. |
713 | | - This should be treated as "not full access" rather than a fatal error. |
| 762 | + When /profile/sshkeys returns 403 (IAM-enrolled user), the |
| 763 | + authorized_users prompt is skipped. |
714 | 764 | """ |
715 | | - with requests_mock.Mocker() as m: |
716 | | - m.get(f"{self.base_url}/profile/grants", status_code=403) |
717 | | - assert _check_full_access(self.base_url, self.test_token) is False |
| 765 | + conf = configuration.CLIConfig(self.base_url, skip_config=True) |
| 766 | + answers = iter(["1", "1", "1", "n", "n"]) |
| 767 | + |
| 768 | + with ( |
| 769 | + patch("linodecli.configuration.open", mock_open()), |
| 770 | + patch("os.chmod", lambda a, b: None), |
| 771 | + patch("builtins.input", lambda _: next(answers)), |
| 772 | + contextlib.redirect_stdout(io.StringIO()), |
| 773 | + patch("linodecli.configuration._check_browsers", lambda: False), |
| 774 | + patch.dict(os.environ, {"LINODE_CLI_TOKEN": self.test_token}), |
| 775 | + requests_mock.Mocker() as m, |
| 776 | + ): |
| 777 | + self._base_mocks(m, sshkeys_status=403) |
| 778 | + conf.configure() |
| 779 | + |
| 780 | + assert conf.get_value("authorized_users") is None |
0 commit comments