Skip to content
Draft
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
6 changes: 3 additions & 3 deletions src/SIL.Harmony.Core/IRemoteResourceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/// the remote Id is opaque to the CRDT lib and could be a URL or some other identifier provided by the backend
/// the local path returned for the application code to use as required, it could be a URL if needed also.
/// </summary>
public interface IRemoteResourceService
public interface IRemoteResourceService<TMetadata> where TMetadata : class
{
/// <summary>
/// instructs application code to download a resource from the remote server
Expand All @@ -22,8 +22,8 @@ public interface IRemoteResourceService
/// <param name="resourceId">id of the resource in the CRDT</param>
/// <param name="localPath">full path to the resource on the local machine</param>
/// <returns>an upload result with the remote id, the id will be stored and transmitted to other clients so they can also download the resource</returns>
Task<UploadResult> UploadResource(Guid resourceId, string localPath);
Task<UploadResult<TMetadata>> UploadResource(Guid resourceId, string localPath);
}

public record DownloadResult(string LocalPath);
public record UploadResult(string RemoteId);
public record UploadResult<TMetadata>(string RemoteId, TMetadata? Metadata = null) where TMetadata : class;
2 changes: 1 addition & 1 deletion src/SIL.Harmony.Sample/CrdtSampleKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ public static IServiceCollection AddCrdtDataSample(this IServiceCollection servi
services.AddCrdtData<SampleDbContext>(config =>
{
config.EnableProjectedTables = true;
config.AddRemoteResourceEntity();
config.ChangeTypeListBuilder
.Add<NewWordChange>()
.Add<NewDefinitionChange>()
Expand Down Expand Up @@ -84,6 +83,7 @@ public static IServiceCollection AddCrdtDataSample(this IServiceCollection servi
builder.HasIndex(wt => new { wt.WordId, wt.TagId }).IsUnique();
});
});
services.AddCrdtRemoteResources<MediaMetadata>();
return services;
}
}
3 changes: 3 additions & 0 deletions src/SIL.Harmony.Sample/MediaMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace SIL.Harmony.Sample;

public record MediaMetadata(string FileName, string MimeType, long SizeBytes);
7 changes: 5 additions & 2 deletions src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,19 @@
Relational:TableName: LocalResource
Relational:ViewName:
Relational:ViewSchema:
EntityType: RemoteResource
EntityType: RemoteResource<MediaMetadata>
Properties:
Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd
DeletedAt (DateTimeOffset?)
Metadata (MediaMetadata)
Annotations:
Relational:ColumnType: jsonb
RemoteId (string)
SnapshotId (no field, Guid?) Shadow FK Index
Keys:
Id PK
Foreign keys:
RemoteResource {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull
RemoteResource<MediaMetadata> {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull
Indexes:
SnapshotId Unique
Annotations:
Expand Down
103 changes: 103 additions & 0 deletions src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesMetadataTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;
using SIL.Harmony.Resource;
using SIL.Harmony.Sample;

namespace SIL.Harmony.Tests.ResourceTests;

public class RemoteResourcesMetadataTests : DataModelTestBase
{
private RemoteServiceMock _remoteServiceMock = new();
private ResourceService<MediaMetadata> _resourceService =>
_services.GetRequiredService<ResourceService<MediaMetadata>>();

private string CreateFile(string contents, [CallerMemberName] string fileName = "")
{
var filePath = Path.GetFullPath(fileName + ".txt");
File.WriteAllText(filePath, contents);
return filePath;
}

private static MediaMetadata SampleMetadata(string fileName = "photo.jpg") =>
new(fileName, "image/jpeg", 102400);

[Fact]
public async Task CreateWithUpload_UsesPassedMetadataWhenUploadReturnsNone()
{
var metadata = SampleMetadata();
var localFile = CreateFile("image data");
var resource = await _resourceService.AddLocalResource(localFile, _localClientId, metadata,
resourceService: _remoteServiceMock);
resource.Metadata.Should().BeEquivalentTo(metadata);
var stored = await DataModel.GetLatest<RemoteResource<MediaMetadata>>(resource.Id);
stored!.Metadata.Should().BeEquivalentTo(metadata);
(await _resourceService.GetResource(resource.Id))!.Metadata.Should().BeEquivalentTo(metadata);
}

[Fact]
public async Task CreateWithUpload_UploadMetadataOverridesPassedMetadata()
{
var passedMetadata = SampleMetadata("passed.jpg");
var uploadMetadata = new MediaMetadata("from-upload.jpg", "image/png", 204800);
var localFile = CreateFile("image data");
_remoteServiceMock.SetUploadMetadata(localFile, uploadMetadata);
var resource = await _resourceService.AddLocalResource(localFile, _localClientId, passedMetadata,
resourceService: _remoteServiceMock);
resource.Metadata.Should().BeEquivalentTo(uploadMetadata);
resource.Metadata.Should().NotBeEquivalentTo(passedMetadata);
var stored = await DataModel.GetLatest<RemoteResource<MediaMetadata>>(resource.Id);
stored!.Metadata.Should().BeEquivalentTo(uploadMetadata);
(await _resourceService.GetResource(resource.Id))!.Metadata.Should().BeEquivalentTo(uploadMetadata);
}

[Fact]
public async Task CreatePendingUpload_IncludesMetadata()
{
var metadata = SampleMetadata("pending.mp4");
var localFile = CreateFile("video data");
var resource = await _resourceService.AddLocalResource(localFile, _localClientId, metadata,
resourceService: null);
resource.Metadata.Should().BeEquivalentTo(metadata);
var stored = await DataModel.GetLatest<RemoteResource<MediaMetadata>>(resource.Id);
stored!.Metadata.Should().BeEquivalentTo(metadata);
}

[Fact]
public async Task AllResources_IncludesMetadata()
{
var metadata = SampleMetadata();
var localFile = CreateFile("list test");
await _resourceService.AddLocalResource(localFile, _localClientId, metadata, resourceService: _remoteServiceMock);
var all = await _resourceService.AllResources();
all.Should().ContainSingle().Which.Metadata.Should().BeEquivalentTo(metadata);
}

[Fact]
public async Task SetResourceMetadata_UpdatesAndSyncs()
{
var metadata = SampleMetadata();
var localFile = CreateFile("sync test");
var resource = await _resourceService.AddLocalResource(localFile, _localClientId, metadata,
resourceService: _remoteServiceMock);
var remoteClient = ForkDatabase();
var updated = metadata with { FileName = "renamed.jpg" };
await _resourceService.SetResourceMetadata(resource.Id, _localClientId, updated);
await DataModel.SyncWith(remoteClient.DataModel);
(await _resourceService.GetResource(resource.Id))!.Metadata.Should().BeEquivalentTo(updated);
(await remoteClient.DataModel.GetLatest<RemoteResource<MediaMetadata>>(resource.Id))!.Metadata
.Should().BeEquivalentTo(updated);
}

[Fact]
public async Task CreateWithoutMetadata_DeserializesWithNullMetadata()
{
var resourceId = Guid.NewGuid();
var remoteId = _remoteServiceMock.CreateRemoteResource("legacy");
await DataModel.AddChange(_localClientId,
new CreateRemoteResourceChange<MediaMetadata>(resourceId, remoteId));
var stored = await DataModel.GetLatest<RemoteResource<MediaMetadata>>(resourceId);
stored!.Metadata.Should().BeNull();
stored.RemoteId.Should().Be(remoteId);
}
}

Loading
Loading