Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions CredentialCache.Test/CustomCredentialTypeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,40 @@ public void AddOrReplaceRefusesAnUnregisteredSubclassAndLeavesTheCacheUnchanged(
Assert.AreSame(original, stored);
}

[TestMethod]
public void EveryStoreRefusesAnUnregisteredSubclassWithTheSameException()
{
NeverRegisteredCredential credential = new() { Value = "x" };
using CredentialCache cache = new(new InMemoryCredentialStore());
string expected = Assert.ThrowsExactly<CredentialStoreException>(
() => cache.AddOrReplace(NewPersona(), credential)).Message;

InMemoryCredentialStore inMemory = new();
PersonaGUID persona = NewPersona();
CredentialStoreException fromInMemory = Assert.ThrowsExactly<CredentialStoreException>(
() => inMemory.Save(persona, credential));
Assert.AreEqual(expected, fromInMemory.Message);
Assert.IsFalse(inMemory.TryLoad(persona, out _), "The refused credential should not have been kept.");

// The platform store serializes before any native call, so this needs no keyring. Comparing
// the message tells the type refusal apart from a store that is merely unavailable.
ICredentialStore platform = CredentialStoreFactory.CreateDefault("ktsu.CredentialCache.Test.Unregistered");
CredentialStoreException fromPlatform = Assert.ThrowsExactly<CredentialStoreException>(
() => platform.Save(NewPersona(), credential));
Assert.AreEqual(expected, fromPlatform.Message);
}

[TestMethod]
public void TheSerializationHelpersRefuseAnUnregisteredSubclass()
{
NeverRegisteredCredential credential = new() { Value = "x" };

Assert.Contains(nameof(NeverRegisteredCredential), Assert.ThrowsExactly<CredentialStoreException>(
() => CredentialSerialization.Serialize(credential)).Message);
Assert.Contains(nameof(NeverRegisteredCredential), Assert.ThrowsExactly<CredentialStoreException>(
() => CredentialSerialization.SerializeToString(credential)).Message);
}

[TestMethod]
public void RegisteringTheSameTypeAndDiscriminatorAgainDoesNothing()
{
Expand Down
22 changes: 18 additions & 4 deletions CredentialCache/Storage/CredentialSerialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,28 @@ internal static void ThrowIfUnknownType(Credential credential)
/// <summary>
/// Serializes the credential to a UTF-8 JSON byte array.
/// </summary>
public static byte[] Serialize(Credential credential) =>
JsonSerializer.SerializeToUtf8Bytes(credential, options);
/// <exception cref="CredentialStoreException">
/// The credential's type is neither one of the library's own nor registered with
/// <see cref="RegisterDerivedType{T}(string)"/>.
/// </exception>
public static byte[] Serialize(Credential credential)
{
ThrowIfUnknownType(credential);
return JsonSerializer.SerializeToUtf8Bytes(credential, options);
}

/// <summary>
/// Serializes the credential to a UTF-8 JSON string.
/// </summary>
public static string SerializeToString(Credential credential) =>
JsonSerializer.Serialize(credential, options);
/// <exception cref="CredentialStoreException">
/// The credential's type is neither one of the library's own nor registered with
/// <see cref="RegisterDerivedType{T}(string)"/>.
/// </exception>
public static string SerializeToString(Credential credential)
{
ThrowIfUnknownType(credential);
return JsonSerializer.Serialize(credential, options);
}

/// <summary>
/// Deserializes a credential from a UTF-8 JSON byte array. Returns <c>null</c> if the bytes
Expand Down
4 changes: 4 additions & 0 deletions CredentialCache/Storage/InMemoryCredentialStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public void Save(PersonaGUID persona, Credential credential)
{
ArgumentNullException.ThrowIfNull(persona);
ArgumentNullException.ThrowIfNull(credential);

// Refuse what a platform store could not serialize, so code tested against this store
// sees the same failure it would in production.
CredentialSerialization.ThrowIfUnknownType(credential);
_items[persona] = credential;
}

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ cache.RegisterCredentialFactory(new CertificateFactory());

If a subclass uses `SemanticString<T>` properties, they round-trip through `ktsu.RoundTripStringJsonConverter` automatically.

`AddOrReplace` refuses a subclass that was never registered with a `CredentialStoreException` naming the type, and leaves the cache and the store unchanged. It does this for every store, `InMemoryCredentialStore` included, so a test that uses the in-memory store fails the same way production would.
`AddOrReplace` refuses a subclass that was never registered with a `CredentialStoreException` naming the type, and leaves the cache and the store unchanged. It does this for every store, `InMemoryCredentialStore` included, so a test that uses the in-memory store fails the same way production would. Calling `Save` on a store directly refuses it the same way, with the same exception and message, from the in-memory store and each platform store. So do `CredentialSerialization.Serialize` and `SerializeToString`, which means a custom store built on them refuses it too.

## Customising the backing store

Expand Down
Loading