diff --git a/CHANGELOG.md b/CHANGELOG.md index 04812b7..d0771ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,9 +47,19 @@ and this project adheres to the versioning of Testing a lookup credential, or a lookup credential type, performs the lookup and reports what came back, with `--inputs` and `--metadata` to try values that are not saved yet. Both take JSON or YAML, or `@` a file holding either. +- Every connection setting now answers to an `ASCENDER_` environment variable: + `ASCENDER_HOST`, `ASCENDER_OAUTH_TOKEN`, `ASCENDER_TOKEN`, `ASCENDER_USERNAME`, + `ASCENDER_PASSWORD`, `ASCENDER_VERIFY_SSL`, `ASCENDER_VERBOSE`, + `ASCENDER_FORMAT` and `ASCENDER_COLOR`. The `CONTROLLER_` and `TOWER_` prefixes + still name the same settings, so nothing written against them breaks. Where more + than one is set, `ASCENDER_` wins, then `CONTROLLER_`, then `TOWER_`. ### Changed +- `ascender login -f human` prints `export ASCENDER_OAUTH_TOKEN=` rather than + `export CONTROLLER_OAUTH_TOKEN=`. Both variables are read, so a shell that + already sourced the old line keeps working. + - TLS certificates are now verified by default. Set `ASCENDERKIT_ASSUME_UNTRUSTED` to restore the previous behaviour when talking to an Ascender that presents a self-signed certificate. The CLI is unaffected: it already verified unless given diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7046665..0b03cb2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,9 +49,9 @@ imported back, that monitored jobs exit 0, 1 and 2 for success, failure and cancellation, and that token authentication works. ```bash -export CONTROLLER_HOST=https://ascender.example.org -export CONTROLLER_USERNAME=admin CONTROLLER_PASSWORD=... -export CONTROLLER_VERIFY_SSL=false # self-signed development servers +export ASCENDER_HOST=https://ascender.example.org +export ASCENDER_USERNAME=admin ASCENDER_PASSWORD=... +export ASCENDER_VERIFY_SSL=false # self-signed development servers pytest tests/integration -v ``` diff --git a/README.md b/README.md index eef6d17..5e5105b 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ pip install -e ascender-kit Point the client at your server and confirm it can authenticate: ```bash -export CONTROLLER_HOST=https://ascender.example.org -export CONTROLLER_USERNAME=alice -export CONTROLLER_PASSWORD=secret +export ASCENDER_HOST=https://ascender.example.org +export ASCENDER_USERNAME=alice +export ASCENDER_PASSWORD=secret ascender config ``` @@ -69,9 +69,11 @@ Connection settings resolve from highest to lowest precedence: | Precedence | Source | | ---------- | ------ | | 1 | Command line flags, such as `--conf.host` and `--conf.token` | -| 2 | Environment variables: `CONTROLLER_HOST`, `CONTROLLER_USERNAME`, `CONTROLLER_PASSWORD` | +| 2 | Environment variables: `ASCENDER_HOST`, `ASCENDER_USERNAME`, `ASCENDER_PASSWORD` | | 3 | The config file written by `ascender login` and `ascender config` | +The `CONTROLLER_` and `TOWER_` prefixes still name the same variables, so a script written against either keeps working. + For repeated use, generate a token instead of passing credentials each time: ```bash diff --git a/ascenderkit/cli/docs/README.md b/ascenderkit/cli/docs/README.md index a397abd..869d84c 100644 --- a/ascenderkit/cli/docs/README.md +++ b/ascenderkit/cli/docs/README.md @@ -14,8 +14,8 @@ wrong. To build the reference guide as well, point the same command at a real Ascender: - ~ CONTROLLER_HOST=https://ascender.example.org CONTROLLER_USERNAME=example \ - CONTROLLER_PASSWORD=secret sphinx-build -b html -W source build/html + ~ ASCENDER_HOST=https://ascender.example.org ASCENDER_USERNAME=example \ + ASCENDER_PASSWORD=secret sphinx-build -b html -W source build/html What needs a server, and why ---------------------------- @@ -28,5 +28,5 @@ version, settings and user access level. The extension itself imports without a server. `render()` runs only when the `autoprogram` directive asks for the parser, so that one page is what needs -`CONTROLLER_HOST`, not the Sphinx run. `conf.py` drops the page when no +`ASCENDER_HOST`, not the Sphinx run. `conf.py` drops the page when no credentials are present, which is what makes the offline build work. diff --git a/ascenderkit/cli/docs/source/authentication.rst b/ascenderkit/cli/docs/source/authentication.rst index 70d448c..ca6a09e 100644 --- a/ascenderkit/cli/docs/source/authentication.rst +++ b/ascenderkit/cli/docs/source/authentication.rst @@ -12,9 +12,9 @@ The preferred mechanism for authenticating with Ascender is by generating and st .. code:: bash - CONTROLLER_HOST=https://ascender.example.org \ - CONTROLLER_USERNAME=alice \ - CONTROLLER_PASSWORD=secret \ + ASCENDER_HOST=https://ascender.example.org \ + ASCENDER_USERNAME=alice \ + ASCENDER_PASSWORD=secret \ ascender login As a convenience, the ``ascender login -f human`` command prints a shell-formatted token @@ -22,15 +22,15 @@ value: .. code:: bash - export CONTROLLER_OAUTH_TOKEN=6E5SXhld7AMOhpRveZsLJQsfs9VS8U + export ASCENDER_OAUTH_TOKEN=6E5SXhld7AMOhpRveZsLJQsfs9VS8U By ingesting this token, you can run subsequent CLI commands without having to specify your username and password each time: .. code:: bash - export CONTROLLER_HOST=https://ascender.example.org - $(CONTROLLER_USERNAME=alice CONTROLLER_PASSWORD=secret ascender login -f human) + export ASCENDER_HOST=https://ascender.example.org + $(ASCENDER_USERNAME=alice ASCENDER_PASSWORD=secret ascender login -f human) ascender config Working with OAuth2.0 Applications @@ -43,7 +43,7 @@ application was created. .. code:: bash - CONTROLLER_USERNAME=alice CONTROLLER_PASSWORD=secret ascender login \ + ASCENDER_USERNAME=alice ASCENDER_PASSWORD=secret ascender login \ --conf.client_id --conf.client_secret @@ -55,7 +55,7 @@ a read-only token, specify ``--scope read``: .. code:: bash - CONTROLLER_USERNAME=alice CONTROLLER_PASSWORD=secret \ + ASCENDER_USERNAME=alice ASCENDER_PASSWORD=secret \ ascender login --conf.scope read Session Authentication @@ -65,5 +65,5 @@ specify your username and password on every invocation: .. code:: bash - CONTROLLER_USERNAME=alice CONTROLLER_PASSWORD=secret ascender jobs list + ASCENDER_USERNAME=alice ASCENDER_PASSWORD=secret ascender jobs list ascender --conf.username alice --conf.password secret jobs list diff --git a/ascenderkit/cli/docs/source/conf.py b/ascenderkit/cli/docs/source/conf.py index 515a97c..3de1087 100644 --- a/ascenderkit/cli/docs/source/conf.py +++ b/ascenderkit/cli/docs/source/conf.py @@ -63,16 +63,16 @@ # resource against a running Ascender. There is no static command table to fall # back on: the CLI discovers its own commands the same way. So that one page is # the only part of these docs that needs a server, and asking for it without one -# exits with a message naming CONTROLLER_HOST. +# exits with a message naming ASCENDER_HOST. # # Rather than fail the whole build, drop the page when there are no credentials. # The six hand-written pages then build offline, which is what CI does, and a -# build with CONTROLLER_HOST set still produces the complete documentation. +# build with ASCENDER_HOST set still produces the complete documentation. # # The toctree in index.rst names reference unconditionally. Moving that entry # behind an `only` directive does not help, because toctree entries resolve # while the source is read and `only` is evaluated later, so the warning is # suppressed by name instead. -if not (os.environ.get('CONTROLLER_HOST') or os.environ.get('TOWER_HOST')): +if not any(os.environ.get(prefix + 'HOST') for prefix in ('ASCENDER_', 'CONTROLLER_', 'TOWER_')): exclude_patterns = ['reference.rst'] suppress_warnings = ['toc.excluded'] diff --git a/ascenderkit/cli/docs/source/output.rst b/ascenderkit/cli/docs/source/output.rst index 6463c22..a1ffb39 100644 --- a/ascenderkit/cli/docs/source/output.rst +++ b/ascenderkit/cli/docs/source/output.rst @@ -54,4 +54,4 @@ Colorized Output By default, |prog| prints colorized output using ANSI color codes. To disable this functionality, specify ``--conf.color f`` or set the environment variable -``CONTROLLER_COLOR=f``. +``ASCENDER_COLOR=f``. diff --git a/ascenderkit/cli/docs/source/usage.rst b/ascenderkit/cli/docs/source/usage.rst index 843e936..73f0c5f 100644 --- a/ascenderkit/cli/docs/source/usage.rst +++ b/ascenderkit/cli/docs/source/usage.rst @@ -77,17 +77,22 @@ A few of the most important ones are: ``-f, --conf.format`` used to specify a custom output format (the default is json) -``--conf.host, CONTROLLER_HOST`` +``--conf.host, ASCENDER_HOST`` the full URL of the Ascender host (i.e., https://my.ascender.example.org) -``-k, --conf.insecure, CONTROLLER_VERIFY_SSL`` +``-k, --conf.insecure, ASCENDER_VERIFY_SSL`` allows insecure server connections when using SSL -``--conf.username, CONTROLLER_USERNAME`` +``--conf.username, ASCENDER_USERNAME`` the Ascender username to use for authentication -``--conf.password, CONTROLLER_PASSWORD`` +``--conf.password, ASCENDER_PASSWORD`` the Ascender password to use for authentication -``--conf.token, CONTROLLER_OAUTH_TOKEN`` +``--conf.token, ASCENDER_OAUTH_TOKEN`` an OAuth2.0 token to use for authentication + +Each of those variables answers to two older prefixes as well, ``CONTROLLER_`` +and ``TOWER_``, so a script written against either keeps working. Where more +than one is set the ``ASCENDER_`` name wins, then ``CONTROLLER_``, then +``TOWER_``. diff --git a/ascenderkit/cli/format.py b/ascenderkit/cli/format.py index e5c08c8..7caecfc 100644 --- a/ascenderkit/cli/format.py +++ b/ascenderkit/cli/format.py @@ -22,6 +22,31 @@ def strtobool(val): raise ValueError(f"invalid truth value {val!r}") +def env_default(env, suffix, default, extra=()): + """The first of the accepted names that `env` sets, else `default`. + + Three prefixes name the same setting, and all three stay readable so a + script written against any of them keeps working: `ASCENDER_` is what the + client documents, `CONTROLLER_` is what it read before the rebrand, and + `TOWER_` is what awxkit read before that. Precedence runs newest first. + + `extra` names further suffixes to try under the same prefix before moving + on to the next one, which is how `ASCENDER_OAUTH_TOKEN` and `ASCENDER_TOKEN` + both reach the token argument. The prefix is the outer loop because it says + which release the script was written against, where the suffix is only a + spelling of the same thing. + + A name that is set but empty wins over a later one, which is what the + nested `env.get()` calls this replaces did. + """ + for prefix in ('ASCENDER_', 'CONTROLLER_', 'TOWER_'): + for this_suffix in (suffix,) + tuple(extra): + name = prefix + this_suffix + if name in env: + return env[name] + return default + + def get_config_credentials(): """Load username and password from config.credentials.default. @@ -41,12 +66,12 @@ def add_authentication_arguments(parser, env): auth = parser.add_argument_group('authentication') auth.add_argument( '--conf.host', - default=env.get('CONTROLLER_HOST', env.get('TOWER_HOST', 'https://127.0.0.1:443')), + default=env_default(env, 'HOST', 'https://127.0.0.1:443'), metavar='https://example.ascender.org', ) auth.add_argument( '--conf.token', - default=env.get('CONTROLLER_OAUTH_TOKEN', env.get('CONTROLLER_TOKEN', env.get('TOWER_OAUTH_TOKEN', env.get('TOWER_TOKEN', '')))), + default=env_default(env, 'OAUTH_TOKEN', '', extra=('TOKEN',)), help='an OAuth2.0 token (get one by using `ascender login`)', metavar='TEXT', ) @@ -55,12 +80,12 @@ def add_authentication_arguments(parser, env): # options configured via cli args take higher precedence than those from the config auth.add_argument( '--conf.username', - default=env.get('CONTROLLER_USERNAME', env.get('TOWER_USERNAME', config_username)), + default=env_default(env, 'USERNAME', config_username), metavar='TEXT', ) auth.add_argument( '--conf.password', - default=env.get('CONTROLLER_PASSWORD', env.get('TOWER_PASSWORD', config_password)), + default=env_default(env, 'PASSWORD', config_password), metavar='TEXT', ) @@ -68,7 +93,7 @@ def add_authentication_arguments(parser, env): '-k', '--conf.insecure', help='Allow insecure server connections when using SSL', - default=not strtobool(env.get('CONTROLLER_VERIFY_SSL', env.get('TOWER_VERIFY_SSL', 'True'))), + default=not strtobool(env_default(env, 'VERIFY_SSL', 'True')), action='store_true', ) @@ -79,7 +104,7 @@ def add_verbose(formatting, env): '--verbose', dest='conf.verbose', help='print debug-level logs, including requests made', - default=strtobool(env.get('CONTROLLER_VERBOSE', env.get('TOWER_VERBOSE', 'f'))), + default=strtobool(env_default(env, 'VERBOSE', 'f')), action="store_true", ) @@ -91,7 +116,7 @@ def add_formatting_import_export(parser, env): '--conf.format', dest='conf.format', choices=['json', 'yaml'], - default=env.get('CONTROLLER_FORMAT', env.get('TOWER_FORMAT', 'json')), + default=env_default(env, 'FORMAT', 'json'), help=('specify a format for the input and output'), ) add_verbose(formatting, env) @@ -105,7 +130,7 @@ def add_output_formatting_arguments(parser, env): '--conf.format', dest='conf.format', choices=FORMATTERS.keys(), - default=env.get('CONTROLLER_FORMAT', env.get('TOWER_FORMAT', 'json')), + default=env_default(env, 'FORMAT', 'json'), help=('specify a format for the input and output'), ) formatting.add_argument( @@ -119,7 +144,7 @@ def add_output_formatting_arguments(parser, env): '--conf.color', metavar='BOOLEAN', help='Display colorized output. Defaults to True', - default=env.get('CONTROLLER_COLOR', env.get('TOWER_COLOR', 't')), + default=env_default(env, 'COLOR', 't'), type=strtobool, ) add_verbose(formatting, env) diff --git a/ascenderkit/cli/resource.py b/ascenderkit/cli/resource.py index bc95e04..c87b829 100644 --- a/ascenderkit/cli/resource.py +++ b/ascenderkit/cli/resource.py @@ -99,7 +99,7 @@ def handle(self, client, parser): else: fmt = client.get_config('format') if fmt == 'human': - print(f'export CONTROLLER_OAUTH_TOKEN={token}') + print(f'export ASCENDER_OAUTH_TOKEN={token}') else: print(to_str(FORMATTERS[fmt]({'token': token}, '.')).strip()) diff --git a/ascenderkit/cli/sphinx.py b/ascenderkit/cli/sphinx.py index 0cdd19e..5a9ea44 100644 --- a/ascenderkit/cli/sphinx.py +++ b/ascenderkit/cli/sphinx.py @@ -49,13 +49,9 @@ def render(): # The return value of this function is an argparse.ArgumentParser, which # the sphinxcontrib.autoprogram plugin crawls and generates an indexed # Sphinx document from. - for e in ( - ('CONTROLLER_HOST', 'TOWER_HOST'), - ('CONTROLLER_USERNAME', 'TOWER_USERNAME'), - ('CONTROLLER_PASSWORD', 'TOWER_PASSWORD'), - ): - if not os.environ.get(e[0]) and not os.environ.get(e[1]): - raise SystemExit('Please specify a valid {} for a real (running) installation.'.format(e[0])) # noqa + for suffix in ('HOST', 'USERNAME', 'PASSWORD'): + if not any(os.environ.get(prefix + suffix) for prefix in ('ASCENDER_', 'CONTROLLER_', 'TOWER_')): + raise SystemExit('Please specify a valid ASCENDER_{} for a real (running) installation.'.format(suffix)) # noqa cli = CLI() cli.parse_args(['ascender', '--help']) cli.connect() diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index c3476d9..e5a4e3d 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,12 +1,12 @@ """Tests that need a running Ascender. -The whole directory is skipped unless CONTROLLER_HOST is set, so `pytest +The whole directory is skipped unless a host is set, so `pytest tests/` stays offline by default and CI is unaffected until it is pointed at a server. To run them: - export CONTROLLER_HOST=https://ascender.example.org - export CONTROLLER_USERNAME=admin CONTROLLER_PASSWORD=... - export CONTROLLER_VERIFY_SSL=false # self-signed development servers + export ASCENDER_HOST=https://ascender.example.org + export ASCENDER_USERNAME=admin ASCENDER_PASSWORD=... + export ASCENDER_VERIFY_SSL=false # self-signed development servers pytest tests/integration -v """ @@ -15,7 +15,9 @@ import pytest -HOST = os.environ.get('CONTROLLER_HOST') +from ascenderkit.cli.format import env_default + +HOST = env_default(os.environ, 'HOST', None) # Without a server there is nothing here to run, so do not collect it at all. # A skip marker would still build the fixtures and report a wall of errors. @@ -31,7 +33,7 @@ def pytest_collection_modifyitems(items): @pytest.fixture(scope='session') def insecure(): """`-k` when the server presents a certificate we should not verify.""" - verify = os.environ.get('CONTROLLER_VERIFY_SSL', 'true').lower() + verify = env_default(os.environ, 'VERIFY_SSL', 'true').lower() return ['-k'] if verify in ('false', 'f', 'no', 'n', '0', 'off') else [] @@ -59,10 +61,10 @@ def api(): from ascenderkit.utils import PseudoNamespace config.base_url = HOST - config.assume_untrusted = os.environ.get('CONTROLLER_VERIFY_SSL', 'true').lower() in ('false', 'f', 'no', 'n', '0', 'off') + config.assume_untrusted = env_default(os.environ, 'VERIFY_SSL', 'true').lower() in ('false', 'f', 'no', 'n', '0', 'off') config.use_sessions = True config.credentials = PseudoNamespace( - {'default': {'username': os.environ.get('CONTROLLER_USERNAME', 'admin'), 'password': os.environ.get('CONTROLLER_PASSWORD', '')}} + {'default': {'username': env_default(os.environ, 'USERNAME', 'admin'), 'password': env_default(os.environ, 'PASSWORD', '')}} ) root = _api.Api() root.load_session().get() diff --git a/tests/integration/test_auth.py b/tests/integration/test_auth.py index 32427cc..1ba830a 100644 --- a/tests/integration/test_auth.py +++ b/tests/integration/test_auth.py @@ -9,6 +9,13 @@ import pytest +# Every prefix that reaches the same argument. A developer who exports one of +# the older spellings would otherwise still be authenticated by password, and +# these tests would pass without the token path being exercised at all. +_PREFIXES = ('ASCENDER_', 'CONTROLLER_', 'TOWER_') +_CREDENTIAL_NAMES = tuple(p + s for p in _PREFIXES for s in ('USERNAME', 'PASSWORD')) +_VERIFY_NAMES = tuple(p + 'VERIFY_SSL' for p in _PREFIXES) + @pytest.fixture def token(ascender): @@ -25,13 +32,13 @@ def test_login_human_format_is_shell_ready(ascender): rc, out, err = ascender('login', '-f', 'human') assert rc == 0, err - assert out.strip().startswith('export CONTROLLER_OAUTH_TOKEN=') + assert out.strip().startswith('export ASCENDER_OAUTH_TOKEN=') def test_a_token_authenticates(ascender, token, insecure): """No username or password, only the token.""" - env = {k: v for k, v in os.environ.items() if k not in ('CONTROLLER_USERNAME', 'CONTROLLER_PASSWORD')} - env['CONTROLLER_OAUTH_TOKEN'] = token + env = {k: v for k, v in os.environ.items() if k not in _CREDENTIAL_NAMES} + env['ASCENDER_OAUTH_TOKEN'] = token proc = subprocess.run(['ascender', *insecure, 'me', '-f', 'human'], capture_output=True, text=True, env=env, timeout=300) @@ -40,8 +47,8 @@ def test_a_token_authenticates(ascender, token, insecure): def test_a_bad_token_is_refused(ascender, insecure): - env = {k: v for k, v in os.environ.items() if k not in ('CONTROLLER_USERNAME', 'CONTROLLER_PASSWORD')} - env['CONTROLLER_OAUTH_TOKEN'] = 'not-a-real-token' + env = {k: v for k, v in os.environ.items() if k not in _CREDENTIAL_NAMES} + env['ASCENDER_OAUTH_TOKEN'] = 'not-a-real-token' proc = subprocess.run(['ascender', *insecure, 'me'], capture_output=True, text=True, env=env, timeout=300) @@ -55,7 +62,7 @@ def test_tls_is_verified_without_the_insecure_flag(insecure): # Drop the environment's opt-out as well as the flag; either one alone # still turns verification off, which is what this is checking for. - env = {k: v for k, v in os.environ.items() if k not in ('CONTROLLER_VERIFY_SSL', 'TOWER_VERIFY_SSL')} + env = {k: v for k, v in os.environ.items() if k not in _VERIFY_NAMES} proc = subprocess.run(['ascender', 'me'], capture_output=True, text=True, env=env, timeout=300) diff --git a/tests/unit/cli/test_config.py b/tests/unit/cli/test_config.py index 5c0e711..49f8d36 100644 --- a/tests/unit/cli/test_config.py +++ b/tests/unit/cli/test_config.py @@ -15,6 +15,22 @@ def test_host_from_environment(): assert config.base_url == 'https://xyz.local' +def test_host_from_the_ascender_environment_name(): + cli = CLI() + cli.parse_args(['ascender'], env={'ASCENDER_HOST': 'https://xyz.local'}) + with pytest.raises(ConnectionError): + cli.connect() + assert config.base_url == 'https://xyz.local' + + +def test_the_ascender_host_name_wins_over_the_older_ones(): + cli = CLI() + cli.parse_args(['ascender'], env={'ASCENDER_HOST': 'https://xyz.local', 'CONTROLLER_HOST': 'https://IGNORE', 'TOWER_HOST': 'https://IGNORE'}) + with pytest.raises(ConnectionError): + cli.connect() + assert config.base_url == 'https://xyz.local' + + def test_host_from_argv(): cli = CLI() cli.parse_args(['ascender', '--conf.host', 'https://xyz.local']) diff --git a/tests/unit/cli/test_environment_prefixes.py b/tests/unit/cli/test_environment_prefixes.py new file mode 100644 index 0000000..5b60920 --- /dev/null +++ b/tests/unit/cli/test_environment_prefixes.py @@ -0,0 +1,64 @@ +"""Which environment variable wins when more than one names the same setting. + +Three prefixes reach the same arguments. `ASCENDER_` is what the client +documents, `CONTROLLER_` is what it read before the rebrand, and `TOWER_` is +what awxkit read before that. A script written against any of them has to keep +working, so none of the three can simply be dropped, and the only question a +reader needs answered is which one wins when two are set at once. +""" + +import pytest + +from ascenderkit.cli.format import env_default + + +def test_the_ascender_name_wins(): + env = {'ASCENDER_HOST': 'new', 'CONTROLLER_HOST': 'middle', 'TOWER_HOST': 'old'} + assert env_default(env, 'HOST', 'fallback') == 'new' + + +def test_the_controller_name_wins_over_the_tower_one(): + env = {'CONTROLLER_HOST': 'middle', 'TOWER_HOST': 'old'} + assert env_default(env, 'HOST', 'fallback') == 'middle' + + +@pytest.mark.parametrize('prefix', ['ASCENDER_', 'CONTROLLER_', 'TOWER_']) +def test_any_one_of_the_three_on_its_own_is_read(prefix): + assert env_default({prefix + 'USERNAME': 'mary'}, 'USERNAME', 'admin') == 'mary' + + +def test_nothing_set_falls_back(): + assert env_default({}, 'USERNAME', 'admin') == 'admin' + + +def test_a_name_that_is_set_but_empty_still_wins(): + """`env.get(name, other)` returned an empty value rather than looking on, + and a deployment that exports an empty token to mean "no token" relies on + it, so the ordered lookup has to do the same. + """ + assert env_default({'ASCENDER_PASSWORD': ''}, 'PASSWORD', 'password') == '' + assert env_default({'TOWER_PASSWORD': '', 'ASCENDER_PASSWORD': 'set'}, 'PASSWORD', 'password') == 'set' + + +def test_the_oauth_token_name_is_tried_before_the_short_one(): + """Both spellings exist for the token, across all three prefixes, and the + full name is the one the login command prints. + """ + env = {'ASCENDER_OAUTH_TOKEN': 'full', 'ASCENDER_TOKEN': 'short'} + assert env_default(env, 'OAUTH_TOKEN', '', extra=('TOKEN',)) == 'full' + + +def test_a_newer_prefix_beats_a_longer_name(): + """`ASCENDER_TOKEN` is preferred over `CONTROLLER_OAUTH_TOKEN`: the prefix + says which release wrote the script, and the suffix is only a spelling. + """ + env = {'ASCENDER_TOKEN': 'short', 'CONTROLLER_OAUTH_TOKEN': 'full'} + assert env_default(env, 'OAUTH_TOKEN', '', extra=('TOKEN',)) == 'short' + + +def test_the_old_chain_keeps_its_own_order(): + """`CONTROLLER_TOKEN` came before `TOWER_OAUTH_TOKEN` before this change, + and still does, so adding a prefix on top reorders nothing below it. + """ + env = {'CONTROLLER_TOKEN': 'middle', 'TOWER_OAUTH_TOKEN': 'old'} + assert env_default(env, 'OAUTH_TOKEN', '', extra=('TOKEN',)) == 'middle' diff --git a/tests/unit/cli/test_sphinx.py b/tests/unit/cli/test_sphinx.py index 86e437f..42cec4e 100644 --- a/tests/unit/cli/test_sphinx.py +++ b/tests/unit/cli/test_sphinx.py @@ -7,11 +7,12 @@ def test_extension_imports_without_a_running_ascender(monkeypatch): """The module used to end in `parser = render()`, so importing it raised - SystemExit unless CONTROLLER_HOST pointed at a live server. Sphinx could + SystemExit unless ASCENDER_HOST pointed at a live server. Sphinx could not load the extension at all, which is why no workflow builds the docs. """ - for var in ('CONTROLLER_HOST', 'TOWER_HOST', 'CONTROLLER_USERNAME', 'TOWER_USERNAME', 'CONTROLLER_PASSWORD', 'TOWER_PASSWORD'): - monkeypatch.delenv(var, raising=False) + for prefix in ('ASCENDER_', 'CONTROLLER_', 'TOWER_'): + for suffix in ('HOST', 'USERNAME', 'PASSWORD'): + monkeypatch.delenv(prefix + suffix, raising=False) module = importlib.import_module('ascenderkit.cli.sphinx') importlib.reload(module) @@ -21,12 +22,12 @@ def test_extension_imports_without_a_running_ascender(monkeypatch): def test_asking_for_the_parser_is_what_needs_the_server(monkeypatch): - for var in ('CONTROLLER_HOST', 'TOWER_HOST'): - monkeypatch.delenv(var, raising=False) + for prefix in ('ASCENDER_', 'CONTROLLER_', 'TOWER_'): + monkeypatch.delenv(prefix + 'HOST', raising=False) module = importlib.reload(importlib.import_module('ascenderkit.cli.sphinx')) - with pytest.raises(SystemExit, match='CONTROLLER_HOST'): + with pytest.raises(SystemExit, match='ASCENDER_HOST'): module.parser