Public Access
Merge branch 'claude/main-page-group-hierarchy-3a3210'
This commit is contained in:
@@ -394,6 +394,49 @@ public sealed class CacheStoreTests : IAsyncLifetime
|
||||
all.Detail.ShouldBe(new byte[] { 1, 2, 3 });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A conflict can be recorded against any kind of item, not only the kinds that were here first.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Every other test in this section uses <see cref="SyncEntityType.Host"/>, and that is how three item
|
||||
/// kinds shipped with no way to record a conflict at all: the two logs and the buckets were added to both
|
||||
/// enums, to the reconciler registry and to the cipher pinning, while <c>AadResourceTypes.For</c> — which
|
||||
/// <c>ConflictStore.RecordAsync</c> calls unconditionally — kept throwing for them. The two other callers
|
||||
/// of that mapping only reach it when an item carries plaintext fields, which none of the three does, so
|
||||
/// nothing else so much as touched the gap.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A theory over the types rather than one more <c>Host</c> case, because the failure was never about
|
||||
/// conflicts and always about which types the layer below had been taught. Recording is asserted through
|
||||
/// a read-back rather than by "it did not throw": an arm returning the wrong resource type would seal
|
||||
/// under one AAD and open under another, which is a null detail rather than an exception.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Theory]
|
||||
[InlineData(SyncEntityType.Host)]
|
||||
[InlineData(SyncEntityType.HostGroup)]
|
||||
[InlineData(SyncEntityType.Snippet)]
|
||||
[InlineData(SyncEntityType.ConnectionLogEntry)]
|
||||
[InlineData(SyncEntityType.ActivityLogEntry)]
|
||||
[InlineData(SyncEntityType.ObjectStore)]
|
||||
public async Task AConflict_CanBeRecordedForEveryKindOfItem(SyncEntityType entityType)
|
||||
{
|
||||
var detail = System.Text.Encoding.UTF8.GetBytes($$"""{"kind":"{{entityType}}"}""");
|
||||
|
||||
var id = await harness.Conflicts.RecordAsync(
|
||||
VaultId, entityType, Guid.CreateVersion7(), ConflictKind.FieldOverridden, detail, Token);
|
||||
|
||||
var listed = (await harness.Conflicts.ListAsync(VaultId, false, Token)).ShouldHaveSingleItem();
|
||||
|
||||
listed.Id.ShouldBe(id);
|
||||
listed.EntityType.ShouldBe(entityType);
|
||||
listed.Detail.ShouldBe(
|
||||
detail,
|
||||
"an empty detail here means the record was sealed under one resource type and opened under "
|
||||
+ "another, which ListAsync reports as nothing rather than as a failure");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnUnacknowledgedConflict_CannotBeDiscarded()
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Security.Cryptography;
|
||||
using DodoSSH.Client.Domain;
|
||||
using DodoSSH.Client.Storage;
|
||||
using DodoSSH.Client.Sync;
|
||||
using DodoSSH.Contracts;
|
||||
using DodoSSH.Crypto;
|
||||
@@ -151,6 +152,63 @@ public sealed class AadResourceTypeTests
|
||||
.ShouldBe(ItemKinds.SyncedTypes, ignoreOrder: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The same pairing, made a second time in the storage layer, and every type must be in it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <c>AadResourceTypes.For</c> is the cache's copy of the table above: the ciphers seal an item for the
|
||||
/// <em>server</em>, and this seals the two things the local cache holds in the clear — a relay host's
|
||||
/// address, and the values a merge overrode. A type missing from it throws rather than mis-seals, which
|
||||
/// sounds like the safe failure and is not: <c>ConflictStore.RecordAsync</c> calls it unconditionally, so
|
||||
/// the exception lands on the path that records what a merge discarded.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This is written after finding three types missing from it — <c>ConnectionLogEntry</c>,
|
||||
/// <c>ActivityLogEntry</c> and <c>ObjectStore</c> went two shipping phases without an arm, because the
|
||||
/// only unconditional caller is one a test suite reaches solely by causing a real merge conflict. Asserted
|
||||
/// over the whole wire enum rather than over <c>ItemKinds.SyncedTypes</c>, which is the stronger claim and
|
||||
/// the one the switch actually makes: the two reserved association types have arms too.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void EverySyncableType_HasAnArmInTheStorageMapping()
|
||||
{
|
||||
var missing = new List<SyncEntityType>();
|
||||
|
||||
foreach (var wire in Enum.GetValues<SyncEntityType>())
|
||||
{
|
||||
if (wire == SyncEntityType.Unspecified)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
CryptoSpec.AadResourceType resource;
|
||||
|
||||
try
|
||||
{
|
||||
resource = AadResourceTypes.For(wire);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
missing.Add(wire);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Same name, as the cipher table demands — a wrong-but-present arm is the failure this half
|
||||
// would otherwise wave through.
|
||||
Enum.GetName(resource).ShouldBe(
|
||||
Enum.GetName(wire),
|
||||
$"AadResourceTypes.For({wire}) returns {resource}, which binds this type's cache records "
|
||||
+ "to another type's resource.");
|
||||
}
|
||||
|
||||
missing.ShouldBeEmpty(
|
||||
"every syncable type needs an arm in AadResourceTypes.For, or a conflict recorded against one "
|
||||
+ "of these throws instead of being written — and the conflict log is what justifies the merge "
|
||||
+ "picking a winner.");
|
||||
}
|
||||
|
||||
private static EncryptedPayload SealSample(
|
||||
SyncEntityType wire,
|
||||
byte[] vaultKey,
|
||||
|
||||
Reference in New Issue
Block a user