diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs index a3a97b5eb..0b1a6e9fe 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs @@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Apps public async Task RestoreAsync(RestoreContext context) { - var json = await context.Reader.ReadJsonAttachmentAsync(SettingsFile); + var json = await context.Reader.ReadJsonAsync(SettingsFile); await appUISettings.SetAsync(context.AppId, null, json); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs index a6616e85f..5204d2844 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs @@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Assets private async Task RestoreTagsAsync(RestoreContext context) { - var tags = await context.Reader.ReadJsonAttachmentAsync(TagsFile); + var tags = await context.Reader.ReadJsonAsync(TagsFile); await tagService.RebuildTagsAsync(context.AppId, TagGroups.Assets, tags); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs index b063caba0..f3498393d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs @@ -136,12 +136,12 @@ namespace Squidex.Domain.Apps.Entities.Backup { using (var writer = await backupArchiveLocation.OpenWriterAsync(stream)) { + await writer.WriteVersionAsync(); + var userMapping = new UserMapping(actor); var context = new BackupContext(Key, userMapping, writer); - var filter = $"^[^\\-]*-{Regex.Escape(Key)}"; - await eventStore.QueryAsync(async storedEvent => { var @event = eventDataFormatter.Parse(storedEvent.Data); @@ -162,7 +162,7 @@ namespace Squidex.Domain.Apps.Entities.Backup job.HandledAssets = writer.WrittenAttachments; lastTimestamp = await WritePeriodically(lastTimestamp); - }, filter, null, ct); + }, GetFilter(), null, ct); foreach (var handler in handlers) { @@ -215,6 +215,11 @@ namespace Squidex.Domain.Apps.Entities.Backup } } + private string GetFilter() + { + return $"^[^\\-]*-{Regex.Escape(Key)}"; + } + private async Task WritePeriodically(Instant lastTimestamp) { var now = clock.GetCurrentInstant(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs index 40ecc9e00..8b71f5b6e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs @@ -54,27 +54,16 @@ namespace Squidex.Domain.Apps.Entities.Backup } } - public Task ReadJsonAttachmentAsync(string name) + public Task ReadJsonAsync(string name) { Guard.NotNullOrEmpty(name, nameof(name)); - var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); + var entry = GetEntry(name); - if (attachmentEntry == null) + using (var stream = entry.Open()) { - throw new FileNotFoundException("Cannot find attachment.", name); + return Task.FromResult(serializer.Deserialize(stream, null)); } - - T result; - - using (var stream = attachmentEntry.Open()) - { - result = serializer.Deserialize(stream, null); - } - - readAttachments++; - - return Task.FromResult(result); } public async Task ReadBlobAsync(string name, Func handler) @@ -82,19 +71,26 @@ namespace Squidex.Domain.Apps.Entities.Backup Guard.NotNullOrEmpty(name, nameof(name)); Guard.NotNull(handler, nameof(handler)); - var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); + var entry = GetEntry(name); - if (attachmentEntry == null) + using (var stream = entry.Open()) { - throw new FileNotFoundException("Cannot find attachment.", name); + await handler(stream); } + } - using (var stream = attachmentEntry.Open()) + private ZipArchiveEntry GetEntry(string name) + { + var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); + + if (attachmentEntry == null) { - await handler(stream); + throw new FileNotFoundException("Cannot find attachment.", name); } readAttachments++; + + return attachmentEntry; } public async Task ReadEventsAsync(IStreamNameResolver streamNameResolver, IEventDataFormatter formatter, Func<(string Stream, Envelope Event), Task> handler) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/CompatibilityExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/CompatibilityExtensions.cs new file mode 100644 index 000000000..d3ad293fe --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/CompatibilityExtensions.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public static class CompatibilityExtensions + { + private const string VersionFile = "Version.json"; + private static readonly FileVersion None = new FileVersion(); + private static readonly FileVersion Expected = new FileVersion { Major = 5 }; + + public sealed class FileVersion + { + public int Major { get; set; } + + public bool Equals(FileVersion other) + { + return Major == other.Major; + } + } + + public static Task WriteVersionAsync(this IBackupWriter writer) + { + return writer.WriteJsonAsync(VersionFile, Expected); + } + + public static async Task CheckCompatibilityAsync(this IBackupReader reader) + { + var current = await reader.ReadVersionAsync(); + + if (!Expected.Equals(current)) + { + throw new BackupRestoreException("Backup file is not compatible with this version."); + } + } + + private static async Task ReadVersionAsync(this IBackupReader reader) + { + try + { + return await reader.ReadJsonAsync(VersionFile); + } + catch + { + return None; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs index f584e162e..fe252db22 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs @@ -23,6 +23,6 @@ namespace Squidex.Domain.Apps.Entities.Backup Task ReadEventsAsync(IStreamNameResolver streamNameResolver, IEventDataFormatter formatter, Func<(string Stream, Envelope Event), Task> handler); - Task ReadJsonAttachmentAsync(string name); + Task ReadJsonAsync(string name); } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs index fbd4fd29a..08849bd08 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs @@ -161,6 +161,8 @@ namespace Squidex.Domain.Apps.Entities.Backup using (var reader = await DownloadAsync()) { + await reader.CheckCompatibilityAsync(); + using (Profiler.Trace("ReadEvents")) { await ReadEventsAsync(reader, handlers); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs index 38bef8228..b89dd8bd4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs @@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Backup Guard.NotNull(reader, nameof(reader)); Guard.NotNull(userResolver, nameof(userResolver)); - var json = await reader.ReadJsonAttachmentAsync>(UsersFile); + var json = await reader.ReadJsonAsync>(UsersFile); foreach (var (userId, email) in json) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs index 8cd27d32b..ae4d05ce1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs @@ -224,7 +224,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { try { - return await reader.ReadJsonAttachmentAsync(UrlsFile); + return await reader.ReadJsonAsync(UrlsFile); } catch { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchTextIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchTextIndex.cs index 8e239f710..006218884 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchTextIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchTextIndex.cs @@ -69,10 +69,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.Elastic { var data = new { - appId = upsert.AppId.Id, + appId = upsert.AppId.Id.ToString(), appName = upsert.AppId.Name, - contentId = upsert.ContentId, - schemaId = upsert.SchemaId.Id, + contentId = upsert.ContentId.ToString(), + schemaId = upsert.SchemaId.Id.ToString(), schemaName = upsert.SchemaId.Name, serveAll = upsert.ServeAll, servePublished = upsert.ServePublished, diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs index 23dfd7ffe..e641db500 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs @@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var context = CreateRestoreContext(); - A.CallTo(() => context.Reader.ReadJsonAttachmentAsync(A._)) + A.CallTo(() => context.Reader.ReadJsonAsync(A._)) .Returns(settings); await sut.RestoreAsync(context); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs index a36003c30..c9cdeca1e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs @@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var context = CreateRestoreContext(); - A.CallTo(() => context.Reader.ReadJsonAttachmentAsync(A._)) + A.CallTo(() => context.Reader.ReadJsonAsync(A._)) .Returns(tags); await sut.RestoreAsync(context); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupCompatibilityTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupCompatibilityTests.cs new file mode 100644 index 000000000..fbb728a75 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupCompatibilityTests.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using System.Threading.Tasks; +using FakeItEasy; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public class BackupCompatibilityTests + { + [Fact] + public async Task Should_writer_version() + { + var writer = A.Fake(); + + await writer.WriteVersionAsync(); + + A.CallTo(() => writer.WriteJsonAsync(A._, + A.That.Matches(x => x.Major == 5))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_throw_exception_if_backup_has_correct_version() + { + var reader = A.Fake(); + + A.CallTo(() => reader.ReadJsonAsync(A._)) + .Returns(new CompatibilityExtensions.FileVersion { Major = 5 }); + + await reader.CheckCompatibilityAsync(); + } + + [Fact] + public async Task Should_throw_exception_if_backup_has_wrong_version() + { + var reader = A.Fake(); + + A.CallTo(() => reader.ReadJsonAsync(A._)) + .Returns(new CompatibilityExtensions.FileVersion { Major = 3 }); + + await Assert.ThrowsAsync(() => reader.CheckCompatibilityAsync()); + } + + [Fact] + public async Task Should_throw_exception_if_backup_has_no_version() + { + var reader = A.Fake(); + + A.CallTo(() => reader.ReadJsonAsync(A._)) + .Throws(new FileNotFoundException()); + + await Assert.ThrowsAsync(() => reader.CheckCompatibilityAsync()); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs index 4ec4cbf67..f44acd2c7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.TestHelpers; @@ -31,13 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Backup [TypeName(nameof(MyEvent))] public sealed class MyEvent : IEvent { - public DomainId DomainIdRaw { get; set; } - - public DomainId DomainIdEmpty { get; set; } - - public NamedId DomainIdNamed { get; set; } - - public Dictionary Values { get; set; } + public Guid Id { get; set; } = Guid.NewGuid(); } public BackupReaderWriterTests() @@ -47,14 +40,72 @@ namespace Squidex.Domain.Apps.Entities.Backup formatter = new DefaultEventDataFormatter(typeNameRegistry, serializer); } + [Fact] + public async Task Should_read_and_write_json_async() + { + var file = "File.json"; + + var value = Guid.NewGuid(); + + await TestReaderWriterAsync(BackupVersion.V1, async writer => + { + await WriteJsonGuidAsync(writer, file, value); + }, async reader => + { + var read = await ReadJsonGuidAsync(reader, file); + + Assert.Equal(value, read); + }); + } + + [Fact] + public async Task Should_read_and_write_blob_async() + { + var file = "File.json"; + + var value = Guid.NewGuid(); + + await TestReaderWriterAsync(BackupVersion.V1, async writer => + { + await WriteGuidAsync(writer, file, value); + }, async reader => + { + var read = await ReadGuidAsync(reader, file); + + Assert.Equal(value, read); + }); + } + + [Fact] + public async Task Should_throw_exception_if_json_not_found() + { + await TestReaderWriterAsync(BackupVersion.V1, writer => + { + return Task.CompletedTask; + }, async reader => + { + await Assert.ThrowsAsync(() => reader.ReadJsonAsync("404")); + }); + } + + [Fact] + public async Task Should_throw_exception_if_blob_not_found() + { + await TestReaderWriterAsync(BackupVersion.V1, writer => + { + return Task.CompletedTask; + }, async reader => + { + await Assert.ThrowsAsync(() => reader.ReadBlobAsync("404", s => Task.CompletedTask)); + }); + } + [Theory] [InlineData(BackupVersion.V1)] [InlineData(BackupVersion.V2)] public async Task Should_write_and_read_events_to_backup(BackupVersion version) { - var stream = new MemoryStream(); - - var random = new Random(); + var randomGenerator = new Random(); var randomDomainIds = new List(); for (var i = 0; i < 100; i++) @@ -64,85 +115,64 @@ namespace Squidex.Domain.Apps.Entities.Backup DomainId RandomDomainId() { - return randomDomainIds[random.Next(randomDomainIds.Count)]; + return randomDomainIds[randomGenerator.Next(randomDomainIds.Count)]; } - var sourceEvents = new List<(string Stream, Envelope Event)>(); + var sourceEvents = new List<(string Stream, Envelope Event)>(); for (var i = 0; i < 200; i++) { - var @event = new MyEvent - { - DomainIdNamed = NamedId.Of(RandomDomainId(), $"name{i}"), - DomainIdRaw = RandomDomainId(), - Values = new Dictionary - { - [RandomDomainId()] = "Key" - } - }; + var @event = new MyEvent(); - var envelope = Envelope.Create(@event); + var envelope = Envelope.Create(@event); - envelope.Headers.Add(RandomDomainId().ToString(), i); - envelope.Headers.Add("Id", RandomDomainId().ToString()); + envelope.Headers.Add("Id", @event.Id.ToString()); envelope.Headers.Add("Index", i); sourceEvents.Add(($"My-{RandomDomainId()}", envelope)); } - using (var writer = new BackupWriter(serializer, stream, true, version)) + await TestReaderWriterAsync(version, async writer => { - foreach (var (_, envelope) in sourceEvents) + foreach (var (stream, envelope) in sourceEvents) { var eventData = formatter.ToEventData(envelope, Guid.NewGuid(), true); - var eventStored = new StoredEvent("S", "1", 2, eventData); + var eventStored = new StoredEvent(stream, "1", 2, eventData); var index = int.Parse(envelope.Headers["Index"].ToString()); if (index % 17 == 0) { - await writer.WriteBlobAsync(index.ToString(), innerStream => - { - innerStream.WriteByte((byte)index); - - return Task.CompletedTask; - }); + await WriteGuidAsync(writer, index.ToString(), envelope.Payload.Id); } else if (index % 37 == 0) { - await writer.WriteJsonAsync(index.ToString(), $"JSON_{index}"); + await WriteJsonGuidAsync(writer, index.ToString(), envelope.Payload.Id); } writer.WriteEvent(eventStored); } - } - - stream.Position = 0; - - var targetEvents = new List<(string Stream, Envelope Event)>(); - - using (var reader = new BackupReader(serializer, stream)) + }, async reader => { + var targetEvents = new List<(string Stream, Envelope Event)>(); + await reader.ReadEventsAsync(streamNameResolver, formatter, async @event => { var index = int.Parse(@event.Event.Headers["Index"].ToString()); + var id = Guid.Parse(@event.Event.Headers["Id"].ToString()); + if (index % 17 == 0) { - await reader.ReadBlobAsync(index.ToString(), innerStream => - { - var byteRead = innerStream.ReadByte(); - - Assert.Equal((byte)index, byteRead); + var guid = await ReadGuidAsync(reader, index.ToString()); - return Task.CompletedTask; - }); + Assert.Equal(id, guid); } else if (index % 37 == 0) { - var json = await reader.ReadJsonAttachmentAsync(index.ToString()); + var guid = await ReadJsonGuidAsync(reader, index.ToString()); - Assert.Equal($"JSON_{index}", json); + Assert.Equal(id, guid); } targetEvents.Add(@event); @@ -150,15 +180,66 @@ namespace Squidex.Domain.Apps.Entities.Backup for (var i = 0; i < targetEvents.Count; i++) { - var target = targetEvents[i].Event.To(); + var targetEvent = targetEvents[i].Event.To(); + var targetStream = targetEvents[i].Stream; + + var sourceEvent = sourceEvents[i].Event.To(); + var sourceStream = sourceEvents[i].Stream; + + Assert.Equal(sourceEvent.Payload.Id, targetEvent.Payload.Id); + Assert.Equal(sourceStream, targetStream); + } + }); + } + + private static Task ReadJsonGuidAsync(IBackupReader reader, string file) + { + return reader.ReadJsonAsync(file); + } + + private static Task WriteJsonGuidAsync(IBackupWriter writer, string file, Guid value) + { + return writer.WriteJsonAsync(file, value); + } - var source = sourceEvents[i].Event.To(); + private static Task WriteGuidAsync(IBackupWriter writer, string file, Guid value) + { + return writer.WriteBlobAsync(file, async stream => + { + await stream.WriteAsync(value.ToByteArray()); + }); + } - Assert.Equal(source.Payload.Values.First().Key, target.Payload.Values.First().Key); - Assert.Equal(source.Payload.DomainIdRaw, target.Payload.DomainIdRaw); - Assert.Equal(source.Payload.DomainIdNamed.Id, target.Payload.DomainIdNamed.Id); + private static async Task ReadGuidAsync(IBackupReader reader, string file) + { + var read = Guid.Empty; - Assert.Equal(DomainId.Empty, target.Payload.DomainIdEmpty); + await reader.ReadBlobAsync(file, async stream => + { + var buffer = new byte[16]; + + await stream.ReadAsync(buffer); + + read = new Guid(buffer); + }); + + return read; + } + + private async Task TestReaderWriterAsync(BackupVersion version, Func write, Func read) + { + using (var stream = new MemoryStream()) + { + using (var writer = new BackupWriter(serializer, stream, true, version)) + { + await write(writer); + } + + stream.Position = 0; + + using (var reader = new BackupReader(serializer, stream)) + { + await read(reader); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/UserMappingTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/UserMappingTests.cs index 9239c3b32..da27d4d3f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/UserMappingTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/UserMappingTests.cs @@ -123,7 +123,7 @@ namespace Squidex.Domain.Apps.Entities.Backup var reader = A.Fake(); - A.CallTo(() => reader.ReadJsonAttachmentAsync>(A._)) + A.CallTo(() => reader.ReadJsonAsync>(A._)) .Returns(storedUsers); return reader; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs index f1d7c76b8..5e76dfa59 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs @@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => urlGenerator.AssetContentBase(appId.Name)) .Returns(newAssetsUrlApp); - A.CallTo(() => reader.ReadJsonAttachmentAsync(A._)) + A.CallTo(() => reader.ReadJsonAsync(A._)) .Returns(new BackupContents.Urls { Assets = oldAssetsUrl,