test_credentials
Tests for :mod:pyrevit.coreutils.credentials, the extension credential store.
Covers the config contract and the crypto contract separately; the module docstring explains why that split matters.
Two things are under test and they are deliberately kept apart:
-
The config contract - which key a credential lives under, that setting one removes the legacy plaintext keys, that clearing one leaves nothing behind, and above all that a failure never deletes a working token. A dict-backed fake
IConfigurationmodels the C# side exactly astest_config_roundtripdoes, so these run identically under IronPython 2/3 and CPython and never touch the user's real config. -
The crypto - that a sealed value really round-trips, and that every way a stored value can be unusable is reported as unreadable rather than as absent. These need Windows DPAPI, so they skip where it is unavailable instead of passing vacuously.
The distinction the second group protects is the one that turns a stale token into a confusing libgit2 error about a missing authentication callback, so a silent-skip here would hide a real regression.
Run from Revit via the pyRevit DevTools "Credentials Module Tests" button.
Classes
AbsentCredentialTests
Bases: _CredentialsTestCase
No stored credential is a normal state, not a failure.
Methods:
test_missing_section_reports_no_credential()
test_section_without_credential_key_reports_no_credential()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_has_credential_is_false_when_absent()
test_builtin_private_repo_flag_is_not_a_credential()
A shipped extension gets private_repo=True with no credential at all.
Reading the flag as "needs auth" is what used to send public repos down an authenticated path, so it has to stay independent of the store.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_migration_ignores_non_extension_sections()
A non-extension section is not an extension credential.
Deliberately not in MigrationTests: that class skips wholesale without DPAPI, and this case needs none.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_migration_ignores_a_library_section_with_no_secret()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
setUp()
tearDown()
seed_plaintext(section, **keys)
Write keys the way a pre-sealing pyRevit would have.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
SealingTests
Bases: _CredentialsTestCase
The crypto contract, against real Windows DPAPI where available.
Methods:
setUp()
test_round_trip_restores_the_credential()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_round_trip_restores_a_password_credential()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_stored_value_does_not_contain_the_secret()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_repr_does_not_leak_the_secret()
test_two_seals_of_the_same_credential_differ()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_blank_username_is_rejected()
test_blank_secret_is_rejected()
test_rejected_write_leaves_nothing_behind()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
tearDown()
seed_plaintext(section, **keys)
Write keys the way a pre-sealing pyRevit would have.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
UnreadableCredentialTests
Bases: _CredentialsTestCase
A stored value that will not decrypt must not look like no credential.
Methods:
setUp()
test_undecryptable_value_raises_unavailable()
test_undecryptable_value_still_counts_as_configured()
A stored value counts as configured even when it cannot be read.
That is the difference between "re-enter your token" and "you never had one", and only the first is actionable.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_tampered_value_raises_unavailable()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
tearDown()
seed_plaintext(section, **keys)
Write keys the way a pre-sealing pyRevit would have.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
DeleteCredentialTests
Bases: _CredentialsTestCase
Clearing a credential must leave nothing that looks like one.
Methods:
setUp()
test_delete_removes_the_credential_key()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_delete_clears_the_private_repo_flag()
A cleared credential must not leave the flag claiming one.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_delete_on_a_section_without_a_credential_reports_nothing_removed()
test_delete_on_a_missing_section_reports_nothing_removed()
tearDown()
seed_plaintext(section, **keys)
Write keys the way a pre-sealing pyRevit would have.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
ReadOnlyConfigTests
Bases: _CredentialsTestCase
An admin-locked config must refuse loudly, not accept and drop.
Methods:
setUp()
test_set_refuses_on_a_read_only_config()
test_delete_refuses_on_a_read_only_config()
test_is_available_is_false_on_a_read_only_config()
tearDown()
seed_plaintext(section, **keys)
Write keys the way a pre-sealing pyRevit would have.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
MigrationTests
Bases: _CredentialsTestCase
One-time sealing of the plaintext credentials older pyRevit wrote.
Methods:
setUp()
test_token_only_section_is_migrated()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_password_section_keeps_its_username()
A real username/password pair must survive the migration.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_cli_style_token_section_is_migrated()
--persist-credentials wrote token plus an oauth2/password mirror.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_builtin_section_with_no_credential_is_left_alone()
private_repo=True on a shipped extension is not a credential.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_username_without_a_secret_is_not_migrated()
Sealing a username with no secret is refused.
A usable-looking entry that can never authenticate is worse than no entry.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_already_migrated_section_is_idempotent()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_migration_covers_a_section_for_an_uninstalled_extension()
The migration walks the config, not the installed packages.
A token for something temporarily uninstalled would otherwise stay in the clear indefinitely, because the extension manager never sees it.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_multiple_sections_are_all_migrated()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
tearDown()
seed_plaintext(section, **keys)
Write keys the way a pre-sealing pyRevit would have.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
MigrationNoDataLossTests
Bases: _CredentialsTestCase
A migration that cannot seal must keep the plaintext it could not replace.
Methods:
setUp()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
tearDown()
test_plaintext_survives_a_failed_seal()
A failed seal must leave the plaintext credential in place.
Clearing the legacy keys before the sealed value is verified destroys the only usable copy of a working token.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_migration_reports_nothing_migrated_when_sealing_fails()
test_a_failing_section_does_not_stop_the_others()
One bad section must not strand every other extension's token.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
seed_plaintext(section, **keys)
Write keys the way a pre-sealing pyRevit would have.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
DroppedFlushTests
Bases: _CredentialsTestCase
A write that is accepted into memory but never reaches the file.
save_changes swallows the failure, so the only way to notice is to read
the file. The legacy keys must survive that case, which is the entire point
of verifying before removing them.
Methods:
setUp()
test_dropped_flush_keeps_the_legacy_token()
A flush that never lands must not cost the user their only token.
migrate_legacy_credentials catches per-section failures so one bad extension cannot strand the rest, so it reports zero migrated rather than propagating; the guarantee that matters is that the plaintext survives.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_dropped_flush_raises_rather_than_reporting_success()
set_credential must not claim success for a value that is not on disk.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_dropped_flush_never_removes_a_working_credential()
A dropped flush must not cost the user a working credential.
This is the whole point of verifying before clearing anything.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_a_landing_flush_stores_the_credential()
The control case: with the file actually written, the store succeeds.
Without this, the three tests above would also pass against a verification that rejected every write.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_dropped_cleanup_save_reports_the_plaintext_left_behind()
A sealed value that lands while the cleanup does not is not success.
The plaintext is the thing this change exists to remove, so reporting the write as done while the token is still readable in the file is the one outcome that must not pass silently.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_landing_cleanup_save_removes_the_plaintext()
The control case for the check above: nothing is left behind.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_delete_reports_a_credential_it_could_not_persist_as_removed()
delete_credential must not claim success for a dropped flush.
The Extensions dialog says the settings were saved on the strength of this return value, and a credential still in the file comes back on the next start.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_delete_that_lands_reports_removed()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_unverifiable_config_does_not_leave_a_credential_in_memory()
A verification that cannot run must not leave the write in place.
get_credential reads the in-memory store, so a value left there after an unverifiable write is one a restart will not have.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
tearDown()
seed_plaintext(section, **keys)
Write keys the way a pre-sealing pyRevit would have.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
CredentialKindTests
Bases: _CredentialsTestCase
A password must never be recorded as a token, and vice versa.
Methods:
setUp()
test_password_kind_survives_the_round_trip()
test_token_kind_survives_the_round_trip()
test_unknown_kind_is_rejected_rather_than_coerced()
Defaulting to a token would store a password mislabelled, silently.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_cli_style_migration_records_a_token_not_a_password()
A mirrored token is still a token, not a password.
Every legacy writer mirrored the token into password "for backwards compat", so preferring password labels every migrated token as one.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_username_password_migration_records_a_password()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
tearDown()
seed_plaintext(section, **keys)
Write keys the way a pre-sealing pyRevit would have.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
StoredFormatContractTests
Bases: _CredentialsTestCase
Pins the stored format, which is a C#/Python contract nothing else guards.
Both sides' docstrings say the format has to change together. The point of these is that the Python side is compared against the C# values rather than against a second copy of the same string: a Python literal equalling a Python literal still passes after the C# side changed, which is exactly the orphaning scenario this exists to catch.
Methods:
setUp()
test_config_key_matches_the_csharp_constant()
test_legacy_keys_match_the_csharp_constants()
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_credential_key_set_matches_the_csharp_list()
The Python key list must match the C# one.
The C# side keeps a single list for the admin-config merge and the CLI, so a key added there and not here would be cleared in only one of them.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_accepted_kinds_match_the_csharp_enum()
The accepted kind names must be the C# enum's, as a set.
Order is not part of the contract - nothing looks a kind up by position -
and it is not even stable across hosts: dir() sorts alphabetically
under IronPython 2 but follows declaration order under CPython 3, so an
ordered comparison passes on one engine and fails on the other.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_separator_cannot_corrupt_a_secret()
A secret full of separator characters must not shift the fields.
The '.' separator is only unambiguous because base64 cannot contain it.
Source code in pyrevitlib/pyrevit/unittests/test_credentials.py
test_username_containing_the_separator_does_not_shift_fields()
test_non_ascii_secret_survives()
tearDown()
seed_plaintext(section, **keys)
Write keys the way a pre-sealing pyRevit would have.