Skip to content
Open
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
35 changes: 31 additions & 4 deletions CredentialCache.Test/NullPropertyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,13 @@ public void SerializeRejectsANullNonNullableProperty()
}

[TestMethod]
public void AddOrReplaceRejectsANullNonNullablePropertyAndLeavesTheStoreAndCacheUnchanged()
[DataRow(false)]
[DataRow(true)]
public void AddOrReplaceRejectsANullNonNullablePropertyAndLeavesTheStoreAndCacheUnchanged(bool inMemory)
{
RawBlobCredentialStore store = new();
using CredentialCache cache = new(store);
RawBlobCredentialStore rawStore = new();
InMemoryCredentialStore memoryStore = new();
using CredentialCache cache = inMemory ? new(memoryStore) : new(rawStore);
PersonaGUID persona = CredentialCache.CreatePersonaGUID();
CredentialWithUsernamePassword credential = new()
{
Expand All @@ -78,7 +81,31 @@ public void AddOrReplaceRejectsANullNonNullablePropertyAndLeavesTheStoreAndCache

Assert.ThrowsExactly<ArgumentException>(() => cache.AddOrReplace(persona, credential));

Assert.IsFalse(store.Blobs.ContainsKey(persona));
Assert.IsFalse(rawStore.Blobs.ContainsKey(persona));
Assert.IsFalse(memoryStore.TryLoad(persona, out _));
Assert.IsFalse(cache.TryGet(persona, out _));
}

[TestMethod]
public void InMemoryStoreSaveRejectsANullNonNullablePropertyAsAPlatformStoreWould()
{
InMemoryCredentialStore store = new();
PersonaGUID persona = CredentialCache.CreatePersonaGUID();

Assert.ThrowsExactly<ArgumentException>(() => store.Save(persona, new CredentialWithToken { Token = null! }));

Assert.IsFalse(store.TryLoad(persona, out _));
}

[TestMethod]
public void InMemoryStoreStillSavesACompleteCredential()
{
using CredentialCache cache = new(new InMemoryCredentialStore());
PersonaGUID persona = CredentialCache.CreatePersonaGUID();

cache.AddOrReplace(persona, new CredentialWithToken { Token = SemanticString<CredentialToken>.Create("abc") });

Assert.IsTrue(cache.TryGet(persona, out Credential? credential));
Assert.AreEqual("abc", ((CredentialWithToken)credential!).Token.ToString());
}
}
12 changes: 7 additions & 5 deletions CredentialCache/CredentialCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/// <summary>
/// Represents a globally unique identifier for a persona.
/// </summary>
public sealed record class PersonaGUID : SemanticString<PersonaGUID> { }

Check warning on line 12 in CredentialCache/CredentialCache.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Rename record 'PersonaGUID' to match pascal case naming rules, consider using 'PersonaGuid'.

Check warning on line 12 in CredentialCache/CredentialCache.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Rename record 'PersonaGUID' to match pascal case naming rules, consider using 'PersonaGuid'.

Check warning on line 12 in CredentialCache/CredentialCache.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Rename record 'PersonaGUID' to match pascal case naming rules, consider using 'PersonaGuid'.

Check warning on line 12 in CredentialCache/CredentialCache.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Rename record 'PersonaGUID' to match pascal case naming rules, consider using 'PersonaGuid'.

/// <summary>
/// Caches <see cref="Credential"/> instances in memory and persists each one through
Expand Down Expand Up @@ -212,18 +212,20 @@
/// cache and the store are left unchanged.
/// </exception>
/// <exception cref="ArgumentException">
/// The store serializes credentials and <paramref name="credential"/> has a non-nullable
/// property set to <c>null</c>. The cache and the store are left unchanged.
/// <paramref name="credential"/> has a non-nullable property set to <c>null</c>, so no
/// serializing store could read it back. The cache and the store are left unchanged, whichever
/// store is configured.
/// </exception>
public void AddOrReplace(PersonaGUID persona, Credential credential)
{
ArgumentNullException.ThrowIfNull(persona);
ArgumentNullException.ThrowIfNull(credential);
ThrowIfDisposed();

// Checked here rather than left to the store, so every store refuses the same types. The
// in-memory store never serializes, and it accepted types every platform store rejects.
CredentialSerialization.ThrowIfUnknownType(credential);
// Checked here rather than left to the store, so every store refuses the same credentials.
// The in-memory store never serializes, and it accepted unregistered types and null
// non-nullable properties that every platform store rejects.
CredentialSerialization.ThrowIfNotSerializable(credential);

lock (LockFor(persona))
{
Expand Down
15 changes: 15 additions & 0 deletions CredentialCache/Storage/CredentialSerialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,21 @@ internal static void ThrowIfUnknownType(Credential credential)
}
}

/// <summary>
/// Throws what <see cref="Serialize(Credential)"/> would throw for <paramref name="credential"/>,
/// without keeping its serialized form, so a store that never serializes refuses the same
/// credentials as one that does.
/// </summary>
/// <exception cref="CredentialStoreException">
/// The credential's type is neither one of the library's own nor registered with
/// <see cref="RegisterDerivedType{T}(string)"/>.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="credential"/> has a non-nullable property set to <c>null</c>, so it could
/// not be read back.
/// </exception>
internal static void ThrowIfNotSerializable(Credential credential) => Zero(Serialize(credential));

/// <summary>
/// Serializes the credential to a UTF-8 JSON byte array.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion CredentialCache/Storage/InMemoryCredentialStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public void Save(PersonaGUID persona, Credential 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);
CredentialSerialization.ThrowIfNotSerializable(credential);
_items[persona] = credential;
}

Expand Down
Loading