Repository navigation
Throw CredentialStoreException, not TypeInitializationException, when libsecret is missing [patch] #184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Throw CredentialStoreException, not TypeInitializationException, when libsecret is missing [patch] #184
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| // Copyright (c) 2023-2026 ktsu-dev contributors | ||
|
|
||
| namespace ktsu.CredentialCache.Test; | ||
|
|
||
| using System.Runtime.InteropServices; | ||
| using System.Runtime.Versioning; | ||
| using ktsu.CredentialCache.Storage; | ||
|
|
||
| /// <summary> | ||
| /// Covers the Linux store on a host without libsecret (a headless server, SSH session or | ||
| /// container). Callers are told to catch <see cref="CredentialStoreException"/> and fall back, | ||
| /// so that is what every operation must throw, never a <see cref="TypeInitializationException"/>. | ||
| /// </summary> | ||
| [TestClass] | ||
| public class LibsecretMissingTests | ||
| { | ||
| [TestMethod] | ||
| public void TranslateMissingLibraryWrapsDllNotFoundException() | ||
| { | ||
| if (!OperatingSystem.IsLinux()) | ||
| { | ||
| Assert.Inconclusive("The libsecret store is only built on Linux."); | ||
| return; | ||
| } | ||
|
Check warning on line 24 in CredentialCache.Test/LibsecretMissingTests.cs
|
||
|
|
||
| AssertTranslated(new DllNotFoundException("Unable to load shared library 'libsecret-1.so.0'")); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void TranslateMissingLibraryWrapsEntryPointNotFoundException() | ||
| { | ||
| if (!OperatingSystem.IsLinux()) | ||
| { | ||
| Assert.Inconclusive("The libsecret store is only built on Linux."); | ||
| return; | ||
| } | ||
|
Check warning on line 36 in CredentialCache.Test/LibsecretMissingTests.cs
|
||
|
|
||
| AssertTranslated(new EntryPointNotFoundException("secret_schema_new")); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void TranslateMissingLibraryLeavesOtherFailuresAlone() | ||
| { | ||
| if (!OperatingSystem.IsLinux()) | ||
| { | ||
| Assert.Inconclusive("The libsecret store is only built on Linux."); | ||
| return; | ||
| } | ||
|
Check warning on line 48 in CredentialCache.Test/LibsecretMissingTests.cs
|
||
|
|
||
| AssertNotTranslated(); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void SchemaCacheCreatesTheHandleOnceAndRetriesAfterAFailure() | ||
| { | ||
| if (!OperatingSystem.IsLinux()) | ||
| { | ||
| Assert.Inconclusive("The libsecret store is only built on Linux."); | ||
| return; | ||
| } | ||
|
Check warning on line 60 in CredentialCache.Test/LibsecretMissingTests.cs
|
||
|
|
||
| AssertSchemaCacheBehaviour(); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void EveryOperationThrowsCredentialStoreExceptionWhenLibsecretIsMissing() | ||
| { | ||
| if (!OperatingSystem.IsLinux()) | ||
| { | ||
| Assert.Inconclusive("The libsecret store is only built on Linux."); | ||
| return; | ||
| } | ||
|
Check warning on line 72 in CredentialCache.Test/LibsecretMissingTests.cs
|
||
|
|
||
| if (NativeLibrary.TryLoad("libsecret-1.so.0", out IntPtr handle)) | ||
| { | ||
| NativeLibrary.Free(handle); | ||
| Assert.Inconclusive("libsecret is installed here, so the missing-library path cannot be reached."); | ||
| return; | ||
| } | ||
|
|
||
| AssertEveryOperationThrows(); | ||
| } | ||
|
|
||
| [SupportedOSPlatform("linux")] | ||
| private static void AssertTranslated(Exception nativeFailure) | ||
| { | ||
| CredentialStoreException exception = Assert.ThrowsExactly<CredentialStoreException>( | ||
| () => LinuxSecretServiceCredentialStore.TranslateMissingLibrary<IntPtr>(() => throw nativeFailure)); | ||
|
|
||
| Assert.AreSame(nativeFailure, exception.InnerException); | ||
| StringAssert.Contains(exception.Message, "libsecret"); | ||
|
Check warning on line 91 in CredentialCache.Test/LibsecretMissingTests.cs
|
||
| } | ||
|
|
||
| [SupportedOSPlatform("linux")] | ||
| private static void AssertNotTranslated() | ||
| { | ||
| Assert.AreEqual(42, LinuxSecretServiceCredentialStore.TranslateMissingLibrary(() => 42)); | ||
| Assert.ThrowsExactly<InvalidOperationException>( | ||
| () => LinuxSecretServiceCredentialStore.TranslateMissingLibrary<int>(() => throw new InvalidOperationException())); | ||
| } | ||
|
|
||
| [SupportedOSPlatform("linux")] | ||
| private static void AssertSchemaCacheBehaviour() | ||
| { | ||
| LinuxSecretServiceCredentialStore.NativeHandleCache cache = new(); | ||
| int calls = 0; | ||
|
|
||
| // A failure is not cached: the next call tries again rather than rethrowing. | ||
| Assert.ThrowsExactly<CredentialStoreException>(() => cache.GetOrCreate(() => | ||
| { | ||
| calls++; | ||
| throw new CredentialStoreException("libsecret-1.so.0 could not be loaded."); | ||
| })); | ||
|
|
||
| IntPtr created = cache.GetOrCreate(() => | ||
| { | ||
| calls++; | ||
| return new IntPtr(42); | ||
| }); | ||
| IntPtr cached = cache.GetOrCreate(() => | ||
| { | ||
| calls++; | ||
| return new IntPtr(7); | ||
| }); | ||
|
|
||
| Assert.AreEqual(new IntPtr(42), created); | ||
| Assert.AreEqual(created, cached); | ||
| Assert.AreEqual(2, calls); | ||
| } | ||
|
|
||
| [SupportedOSPlatform("linux")] | ||
| private static void AssertEveryOperationThrows() | ||
| { | ||
| LinuxSecretServiceCredentialStore store = new($"ktsu.CredentialCache.MissingLibsecretTest.{Guid.NewGuid():N}"); | ||
| PersonaGUID persona = CredentialCache.CreatePersonaGUID(); | ||
|
|
||
| // Twice each: a type initializer would fail once and then rethrow its cached | ||
| // TypeInitializationException on every later call. | ||
| for (int attempt = 0; attempt < 2; attempt++) | ||
| { | ||
| Assert.ThrowsExactly<CredentialStoreException>(() => store.TryLoad(persona, out _)); | ||
| Assert.ThrowsExactly<CredentialStoreException>(() => store.Remove(persona)); | ||
| Assert.ThrowsExactly<CredentialStoreException>(() => store.Save(persona, new CredentialWithNothing())); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the same finding as the earlier thread on this call. It was re-posted because the call moved into
GetSchemaHandle. The answer is the same:secret_schema_newis libsecret, which has no managed equivalent, so the code stays as is.Generated by Claude Code