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
14 changes: 14 additions & 0 deletions src/SIL.Harmony.Tests/ModelVersionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,20 @@ public async Task AuthoredChangesGetTheLatestModelVersion()
.ChangeEntities.Select(ce => ce.AuthoredModelVersion).Should().Equal(2, 2, 2);
}

[Fact]
public async Task AuthoringAnUnregisteredTypeThrows()
{
await using var oldApp = ForkDatabase(OldAppWithoutNotes);
var wordId = Guid.NewGuid();
await oldApp.WriteNextChange(new NewWordChange(wordId, "hello"));
var commitsBefore = oldApp.DbContext.Commits.Count();

var act = async () => await oldApp.WriteNextChange(new SetWordNoteChange(wordId, "note"));

(await act.Should().ThrowAsync<ArgumentException>()).WithMessage($"*{nameof(SetWordNoteChange)}*");
oldApp.DbContext.Commits.Count().Should().Be(commitsBefore);
}

[Fact]
public async Task UnknownTypeIsAppliedAfterUpgrade()
{
Expand Down
9 changes: 5 additions & 4 deletions src/SIL.Harmony/Changes/PeekThenConcreteChangeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
namespace SIL.Harmony.Changes;

/// <summary>
/// Owns <see cref="IChange"/> discrimination. Requires <c>$type</c> as the first JSON property
/// Owns <see cref="IChange"/> discrimination. Every stored change has <c>$type</c> as its first JSON property
/// (matching synthetic write order from <see cref="Config.HarmonyConfig"/>).
/// Known discriminators deserialize via cached concrete <see cref="JsonTypeInfo"/>;
/// an unknown discriminator either throws or falls back to <see cref="OpaqueChange"/>
/// (preserving the raw payload), depending on <see cref="UnknownChangeHandling"/>.
/// Registered types are written with their discriminator and deserialize via cached concrete <see cref="JsonTypeInfo"/>.
/// An unregistered discriminator (from a newer client) throws or, per <see cref="UnknownChangeHandling"/>, is read as an
/// <see cref="OpaqueChange"/> that is written back unchanged and never applied.
/// The app itself can't author an unregistered type: <see cref="DataModel"/> rejects it.
/// </summary>
internal sealed class PeekThenConcreteChangeConverter : JsonConverter<IChange>
{
Expand Down
2 changes: 2 additions & 0 deletions src/SIL.Harmony/Config/HarmonyConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ public void ConfigureJsonOptions(Action<JsonSerializerOptions> configure)
_jsonOptionsBuilder.Configure(configure);
}

internal bool IsRegisteredChangeType(Type type) => _lazyChangeDiscriminatorMaps.Value.ByType.ContainsKey(type);

private ChangeDiscriminatorMaps BuildChangeDiscriminatorMaps()
{
ModelVersionBuilder.Freeze();
Expand Down
4 changes: 4 additions & 0 deletions src/SIL.Harmony/DataModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ private async Task Add(Commit commit)

internal static ChangeEntity<IChange> ToChangeEntity(IChange change, int index, Guid commitId, HarmonyConfig config)
{
if (!config.IsRegisteredChangeType(change.GetType()))
throw new ArgumentException(
$"Change type {change.GetType().FullName} is not registered, list it with {nameof(ModelVersionChanges)}.{nameof(ModelVersionChanges.Change)}<T>() in a model version",
nameof(change));
return new ChangeEntity<IChange>()
{
Change = change,
Expand Down
Loading