using AIStudio.Assistants.BeyondFactsLab; using Microsoft.Extensions.Logging.Abstractions; namespace AIStudio.Tests.BeyondFactsLab; /// /// Checks how the BeyondFacts Lab stores the materials of a preparation. /// [TestFixture] public sealed class BeyondFactsLabStoreMaterialTests { private const string HASH = "0f343b0931126a20f133d67c2b018a3b5f1d1f5c1f6a8e0a3d5a5a3e9c6a1b2c"; private const string OTHER_HASH = "1f343b0931126a20f133d67c2b018a3b5f1d1f5c1f6a8e0a3d5a5a3e9c6a1b2c"; private string dataDirectory = string.Empty; private string RootDirectory => Path.Combine(this.dataDirectory, BeyondFactsLabStore.ROOT_DIRECTORY_NAME); [SetUp] public void CreateDataDirectory() { this.dataDirectory = Path.Combine(Path.GetTempPath(), $"ai-studio-beyondfacts-lab-materials-{Guid.NewGuid():N}"); Directory.CreateDirectory(this.dataDirectory); } [TearDown] public void DeleteDataDirectory() { if (Directory.Exists(this.dataDirectory)) Directory.Delete(this.dataDirectory, true); } [Test] public async Task AMaterialIsStoredWithItsText() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var addition = await store.AddMaterialAsync(created.ProjectId, Material("Einladung.docx", HASH), "Podium am 10. Dezember"); var changed = await store.LoadAsync(created.ProjectId); var materials = await this.CreateStore().ListMaterialsAsync(created.ProjectId); var text = await store.ReadMaterialTextAsync(created.ProjectId, addition.Material.MaterialId, 1_000); using (Assert.EnterMultipleScope()) { Assert.That(addition.IsDuplicate, Is.False); Assert.That(materials, Has.Count.EqualTo(1)); Assert.That(materials[0].Material!.Name, Is.EqualTo("Einladung.docx")); Assert.That(materials[0].Material!.TextLength, Is.EqualTo("Podium am 10. Dezember".Length)); Assert.That(materials[0].Material!.SourceBytes, Is.EqualTo(2_048)); Assert.That(text, Is.EqualTo("Podium am 10. Dezember")); Assert.That(changed!.ModifiedAtUtc, Is.GreaterThan(created.ModifiedAtUtc), "A material is work on the preparation."); Assert.That(File.Exists(this.MaterialFile(created.ProjectId, addition.Material.MaterialId, ".json")), Is.True); Assert.That(File.Exists(this.MaterialFile(created.ProjectId, addition.Material.MaterialId, ".md")), Is.True); } } [Test] public async Task AnImageIsStoredWithItsCopy() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); byte[] copy = [0x89, 0x50, 0x4E, 0x47]; var addition = await store.AddImageMaterialAsync(created.ProjectId, ImageMaterial("Poster.tiff", HASH), copy); var materials = await this.CreateStore().ListMaterialsAsync(created.ProjectId); var path = store.MaterialImagePath(created.ProjectId, materials[0].Material!); using (Assert.EnterMultipleScope()) { Assert.That(addition.IsDuplicate, Is.False); Assert.That(materials[0].Material!.Kind, Is.EqualTo(BflMaterialKind.IMAGE)); Assert.That(materials[0].Material!.Image!.Width, Is.EqualTo(2_560)); Assert.That(materials[0].Material!.TextLength, Is.Zero); Assert.That(path, Is.EqualTo(this.MaterialFile(created.ProjectId, addition.Material.MaterialId, ".png"))); Assert.That(await File.ReadAllBytesAsync(path), Is.EqualTo(copy)); Assert.That(File.Exists(this.MaterialFile(created.ProjectId, addition.Material.MaterialId, ".md")), Is.False, "An image has no text."); } } [Test] public async Task AnImageWithoutItsCopyCannotBeOpened() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var addition = await store.AddImageMaterialAsync(created.ProjectId, ImageMaterial("Poster.png", HASH), new byte[] { 1 }); File.Delete(this.MaterialFile(created.ProjectId, addition.Material.MaterialId, ".png")); var materials = await store.ListMaterialsAsync(created.ProjectId); Assert.That(materials.Single().IsAvailable, Is.False); } [Test] public async Task DeletingAnImageDeletesItsCopy() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var addition = await store.AddImageMaterialAsync(created.ProjectId, ImageMaterial("Poster.png", HASH), new byte[] { 1 }); await store.DeleteMaterialAsync(created.ProjectId, addition.Material.MaterialId); using (Assert.EnterMultipleScope()) { Assert.That(await store.ListMaterialsAsync(created.ProjectId), Is.Empty); Assert.That(File.Exists(this.MaterialFile(created.ProjectId, addition.Material.MaterialId, ".png")), Is.False); } } [Test] public async Task ARecordingIsStoredWithItsTranscript() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var material = Material("Interview.mp3", HASH); material.Kind = BflMaterialKind.RECORDING; var addition = await store.AddMaterialAsync(created.ProjectId, material, "Guten Tag."); var materials = await this.CreateStore().ListMaterialsAsync(created.ProjectId); using (Assert.EnterMultipleScope()) { Assert.That(materials[0].Material!.Kind, Is.EqualTo(BflMaterialKind.RECORDING)); Assert.That(await store.ReadMaterialTextAsync(created.ProjectId, addition.Material.MaterialId, 100), Is.EqualTo("Guten Tag.")); } } [Test] public async Task AMaterialStoredBeforeTheKindsIsADocument() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var addition = await store.AddMaterialAsync(created.ProjectId, Material("Paper.pdf", HASH), "Text"); var json = this.MaterialFile(created.ProjectId, addition.Material.MaterialId, ".json"); var lines = (await File.ReadAllLinesAsync(json)).Where(line => !line.Contains("\"kind\"", StringComparison.Ordinal) && !line.Contains("\"image\"", StringComparison.Ordinal)); await File.WriteAllLinesAsync(json, lines); var materials = await this.CreateStore().ListMaterialsAsync(created.ProjectId); Assert.That(materials.Single().Material!.Kind, Is.EqualTo(BflMaterialKind.DOCUMENT)); } [Test] public async Task AnImageWithoutTheDescriptionOfItsCopyIsDamaged() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var addition = await store.AddImageMaterialAsync(created.ProjectId, ImageMaterial("Poster.png", HASH), new byte[] { 1 }); var json = this.MaterialFile(created.ProjectId, addition.Material.MaterialId, ".json"); await File.WriteAllTextAsync(json, (await File.ReadAllTextAsync(json)).Replace("\"png\"", "\"../../x\"", StringComparison.Ordinal)); var materials = await this.CreateStore().ListMaterialsAsync(created.ProjectId); Assert.That(materials.Single().IsAvailable, Is.False, "An unknown extension could point anywhere."); } [Test] public async Task AnImageCannotBeAddedAsText() { var store = this.CreateStore(); await Assert.ThrowsAsync(async () => await store.AddMaterialAsync(Guid.NewGuid(), ImageMaterial("Poster.png", HASH), "Text")); } [Test] public async Task ADocumentCannotBeAddedAsAnImage() { var store = this.CreateStore(); await Assert.ThrowsAsync(async () => await store.AddImageMaterialAsync(Guid.NewGuid(), Material("Paper.pdf", HASH), new byte[] { 1 })); } [TestCase(true)] [TestCase(false)] public async Task AMaterialKeepsWhetherItsTextIsOnlyTheBeginning(bool isShortened) { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var material = Material("Buch.pdf", HASH); material.IsShortened = isShortened; await store.AddMaterialAsync(created.ProjectId, material, "Kapitel 1"); var materials = await this.CreateStore().ListMaterialsAsync(created.ProjectId); Assert.That(materials[0].Material!.IsShortened, Is.EqualTo(isShortened)); } [TestCase("/Users/person/Documents/Talk.pdf")] [TestCase(@"C:\Users\person\Documents\Talk.pdf")] [TestCase("Talk.pdf")] public async Task TheStoreKeepsNoPath(string name) { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var addition = await store.AddMaterialAsync(created.ProjectId, Material(name, HASH), "Text"); var json = await File.ReadAllTextAsync(this.MaterialFile(created.ProjectId, addition.Material.MaterialId, ".json")); using (Assert.EnterMultipleScope()) { Assert.That(addition.Material.Name, Is.EqualTo("Talk.pdf")); Assert.That(json, Does.Not.Contain("Users")); Assert.That(json, Does.Not.Contain("Documents")); } } [Test] public async Task AFolderAloneIsNoName() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); await Assert.ThrowsAsync(async () => await store.AddMaterialAsync(created.ProjectId, Material("/Users/person/", HASH), "Text")); } [Test] public async Task ADocumentWithTheSameContentIsStoredOnce() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var first = await store.AddMaterialAsync(created.ProjectId, Material("Paper.pdf", HASH), "Text"); var second = await store.AddMaterialAsync(created.ProjectId, Material("Paper (Kopie).pdf", HASH.ToUpperInvariant()), "Text"); var materials = await store.ListMaterialsAsync(created.ProjectId); using (Assert.EnterMultipleScope()) { Assert.That(second.IsDuplicate, Is.True); Assert.That(second.Material.MaterialId, Is.EqualTo(first.Material.MaterialId)); Assert.That(second.Material.Name, Is.EqualTo("Paper.pdf")); Assert.That(materials, Has.Count.EqualTo(1)); } } [Test] public async Task MaterialsAreListedInTheOrderTheyWereAdded() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); await store.AddMaterialAsync(created.ProjectId, Material("Erstes.pdf", HASH), "Eins"); await Task.Delay(20); await store.AddMaterialAsync(created.ProjectId, Material("Zweites.pdf", OTHER_HASH), "Zwei"); var materials = await store.ListMaterialsAsync(created.ProjectId); Assert.That(materials.Select(entry => entry.Material!.Name), Is.EqualTo(new[] { "Erstes.pdf", "Zweites.pdf" })); } [Test] public async Task APreparationWithoutMaterialsHasNone() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); Assert.That(await store.ListMaterialsAsync(created.ProjectId), Is.Empty); } [Test] public async Task AMaterialWithoutTextCannotBeOpened() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var addition = await store.AddMaterialAsync(created.ProjectId, Material("Paper.pdf", HASH), "Text"); File.Delete(this.MaterialFile(created.ProjectId, addition.Material.MaterialId, ".md")); var materials = await store.ListMaterialsAsync(created.ProjectId); using (Assert.EnterMultipleScope()) { Assert.That(materials, Has.Count.EqualTo(1)); Assert.That(materials[0].IsAvailable, Is.False); Assert.That(materials[0].MaterialId, Is.EqualTo(addition.Material.MaterialId)); } } [Test] public async Task ADamagedMaterialIsListedAndCanBeDeleted() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var addition = await store.AddMaterialAsync(created.ProjectId, Material("Paper.pdf", HASH), "Text"); var json = this.MaterialFile(created.ProjectId, addition.Material.MaterialId, ".json"); await File.WriteAllTextAsync(json, """{ "schemaVersion": 1, "fieldOfANewerVersion": true }"""); var damaged = await store.ListMaterialsAsync(created.ProjectId); await store.DeleteMaterialAsync(created.ProjectId, addition.Material.MaterialId); using (Assert.EnterMultipleScope()) { Assert.That(damaged.Single().IsAvailable, Is.False); Assert.That(await store.ListMaterialsAsync(created.ProjectId), Is.Empty); Assert.That(File.Exists(json), Is.False); Assert.That(File.Exists(this.MaterialFile(created.ProjectId, addition.Material.MaterialId, ".md")), Is.False); } } [Test] public async Task ADamagedMaterialIsNoDuplicate() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var addition = await store.AddMaterialAsync(created.ProjectId, Material("Paper.pdf", HASH), "Text"); await File.WriteAllTextAsync(this.MaterialFile(created.ProjectId, addition.Material.MaterialId, ".json"), "{"); var again = await store.AddMaterialAsync(created.ProjectId, Material("Paper.pdf", HASH), "Text"); Assert.That(again.IsDuplicate, Is.False, "The person cannot see the damaged one, so the document has to come back."); } [Test] public async Task DeletingAMaterialCountsAsAChange() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var addition = await store.AddMaterialAsync(created.ProjectId, Material("Paper.pdf", HASH), "Text"); var afterAdding = await store.LoadAsync(created.ProjectId); await Task.Delay(20); var afterDeleting = await store.DeleteMaterialAsync(created.ProjectId, addition.Material.MaterialId); using (Assert.EnterMultipleScope()) { Assert.That(afterDeleting.ModifiedAtUtc, Is.GreaterThan(afterAdding!.ModifiedAtUtc)); Assert.That(await store.ListMaterialsAsync(created.ProjectId), Is.Empty); } } [Test] public async Task ReadingStopsAtTheLimit() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var text = string.Concat(Enumerable.Repeat("Ein Satz über Wissenschaft. ", 10_000)); var addition = await store.AddMaterialAsync(created.ProjectId, Material("Paper.pdf", HASH), text); using (Assert.EnterMultipleScope()) { Assert.That(await store.ReadMaterialTextAsync(created.ProjectId, addition.Material.MaterialId, 100_000), Is.EqualTo(text[..100_000])); Assert.That(await store.ReadMaterialTextAsync(created.ProjectId, addition.Material.MaterialId, text.Length + 10), Is.EqualTo(text)); Assert.That(await store.ReadMaterialTextAsync(created.ProjectId, addition.Material.MaterialId, 0), Is.Empty); } } [Test] public async Task ReadingADeletedMaterialFails() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var addition = await store.AddMaterialAsync(created.ProjectId, Material("Paper.pdf", HASH), "Text"); await store.DeleteMaterialAsync(created.ProjectId, addition.Material.MaterialId); await Assert.ThrowsAsync(async () => await store.ReadMaterialTextAsync(created.ProjectId, addition.Material.MaterialId, 100)); } [Test] public async Task AMaterialOfAMissingPreparationIsNotStored() { var store = this.CreateStore(); await Assert.ThrowsAsync(async () => await store.AddMaterialAsync(Guid.NewGuid(), Material("Paper.pdf", HASH), "Text")); } [TestCase("")] [TestCase(" \n")] public async Task AMaterialWithoutTextIsRefused(string text) { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); await Assert.ThrowsAsync(async () => await store.AddMaterialAsync(created.ProjectId, Material("Paper.pdf", HASH), text)); } [Test] public async Task DeletingAPreparationDeletesItsMaterials() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); await store.AddMaterialAsync(created.ProjectId, Material("Paper.pdf", HASH), "Text"); await store.DeleteAsync(created.ProjectId); Assert.That(Directory.Exists(Path.Combine(this.RootDirectory, created.ProjectId.ToString("D"))), Is.False); } private static BflMaterial Material(string name, string hash) => new() { Name = name, SourceHash = hash, SourceBytes = 2_048, }; private static BflMaterial ImageMaterial(string name, string hash) => new() { Name = name, Kind = BflMaterialKind.IMAGE, Image = new() { Extension = "png", Width = 2_560, Height = 1_440 }, SourceHash = hash, SourceBytes = 4_096, }; private string MaterialFile(Guid projectId, Guid materialId, string extension) => Path.Combine(this.RootDirectory, projectId.ToString("D"), "materials", $"{materialId:D}{extension}"); private BeyondFactsLabStore CreateStore() => new(NullLogger.Instance, new BflStorageOptions { DataDirectory = this.dataDirectory }); }