using FluentAssertions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NUnit.Framework; using Recyclarr.TestLibrary.AutoFixture; using Recyclarr.TestLibrary.FluentAssertions; using Recyclarr.TrashLib.Services.CustomFormat.Models; using Recyclarr.TrashLib.Services.CustomFormat.Models.Cache; using Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps; using Recyclarr.TrashLib.TestLibrary; /* Sample Custom Format response from Radarr API { "id": 1, "name": "test", "includeCustomFormatWhenRenaming": false, "specifications": [ { "name": "asdf", "implementation": "ReleaseTitleSpecification", "implementationName": "Release Title", "infoLink": "https://wiki.servarr.com/Radarr_Settings#Custom_Formats_2", "negate": false, "required": false, "fields": [ { "order": 0, "name": "value", "label": "Regular Expression", "value": "asdf", "type": "textbox", "advanced": false } ] } ] } */ namespace Recyclarr.TrashLib.Tests.CustomFormat.Processors.PersistenceSteps; [TestFixture] [Parallelizable(ParallelScope.All)] public class JsonTransactionStepTest { [Test, AutoMockData] public void Combination_of_create_update_and_unchanged_and_verify_proper_json_merging( JsonTransactionStep processor) { var radarrCfs = JsonConvert.DeserializeObject>(@" [{ 'id': 1, 'name': 'user_defined', 'specifications': [{ 'name': 'spec1', 'negate': false, 'fields': [{ 'name': 'value', 'value': 'value1' }] }] }, { 'id': 2, 'name': 'updated', 'specifications': [{ 'name': 'spec2', 'negate': false, 'fields': [{ 'name': 'value', 'untouchable': 'field', 'value': 'value1' }] }] }, { 'id': 3, 'name': 'no_change', 'specifications': [{ 'name': 'spec4', 'negate': false, 'fields': [{ 'name': 'value', 'value': 'value1' }] }] }]")!; var guideCfData = JsonConvert.DeserializeObject>(@" [{ 'name': 'created', 'specifications': [{ 'name': 'spec5', 'fields': { 'value': 'value2' } }] }, { 'name': 'updated_different_name', 'specifications': [{ 'name': 'spec2', 'negate': true, 'new_spec_field': 'new_spec_value', 'fields': { 'value': 'value2', 'new_field': 'new_value' } }, { 'name': 'new_spec', 'fields': { 'value': 'value3' } }] }, { 'name': 'no_change', 'specifications': [{ 'name': 'spec4', 'negate': false, 'fields': { 'value': 'value1' } }] }]")!; var guideCfs = new List { NewCf.Processed("created", "id1", guideCfData[0]), NewCf.Processed("updated_different_name", "id2", 2, guideCfData[1]), NewCf.Processed("no_change", "id3", 3, guideCfData[2]) }; processor.Process(guideCfs, radarrCfs); var expectedJson = new[] { @"{ 'name': 'created', 'specifications': [{ 'name': 'spec5', 'fields': [{ 'name': 'value', 'value': 'value2' }] }] }", @"{ 'id': 2, 'name': 'updated_different_name', 'specifications': [{ 'name': 'spec2', 'negate': true, 'new_spec_field': 'new_spec_value', 'fields': [{ 'name': 'value', 'untouchable': 'field', 'value': 'value2', 'new_field': 'new_value' }] }, { 'name': 'new_spec', 'fields': [{ 'name': 'value', 'value': 'value3' }] }] }", @"{ 'id': 3, 'name': 'no_change', 'specifications': [{ 'name': 'spec4', 'negate': false, 'fields': [{ 'name': 'value', 'value': 'value1' }] }] }" }; var expectedTransactions = new CustomFormatTransactionData(); expectedTransactions.NewCustomFormats.Add(guideCfs[0]); expectedTransactions.UpdatedCustomFormats.Add(guideCfs[1]); expectedTransactions.UnchangedCustomFormats.Add(guideCfs[2]); processor.Transactions.Should().BeEquivalentTo(expectedTransactions); processor.Transactions.NewCustomFormats.First().Json.Should() .BeEquivalentTo(JObject.Parse(expectedJson[0]), op => op.Using(new JsonEquivalencyStep())); processor.Transactions.UpdatedCustomFormats.First().Json.Should() .BeEquivalentTo(JObject.Parse(expectedJson[1]), op => op.Using(new JsonEquivalencyStep())); processor.Transactions.UnchangedCustomFormats.First().Json.Should() .BeEquivalentTo(JObject.Parse(expectedJson[2]), op => op.Using(new JsonEquivalencyStep())); processor.Transactions.ConflictingCustomFormats.Should().BeEmpty(); } [Test, AutoMockData] public void Deletes_happen_before_updates( JsonTransactionStep processor) { const string radarrCfData = @"[{ 'id': 1, 'name': 'updated', 'specifications': [{ 'name': 'spec1', 'fields': [{ 'name': 'value', 'value': 'value1' }] }] }, { 'id': 2, 'name': 'deleted', 'specifications': [{ 'name': 'spec2', 'negate': false, 'fields': [{ 'name': 'value', 'untouchable': 'field', 'value': 'value1' }] }] }]"; var guideCfData = JObject.Parse(@"{ 'name': 'updated', 'specifications': [{ 'name': 'spec2', 'fields': { 'value': 'value2' } }] }"); var deletedCfsInCache = new List { new("", "", 1) {CustomFormatId = 2} }; var guideCfs = new List { NewCf.Processed("updated", "", 1, guideCfData) }; var radarrCfs = JsonConvert.DeserializeObject>(radarrCfData); processor.Process(guideCfs, radarrCfs!); processor.RecordDeletions(deletedCfsInCache, radarrCfs!); const string expectedJson = @"{ 'id': 1, 'name': 'updated', 'specifications': [{ 'name': 'spec2', 'fields': [{ 'name': 'value', 'value': 'value2' }] }] }"; var expectedTransactions = new CustomFormatTransactionData(); expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("", "", 2)); expectedTransactions.UpdatedCustomFormats.Add(guideCfs[0]); processor.Transactions.Should().BeEquivalentTo(expectedTransactions); processor.Transactions.UpdatedCustomFormats.First().Json.Should() .BeEquivalentTo(JObject.Parse(expectedJson), op => op.Using(new JsonEquivalencyStep())); } [Test, AutoMockData] public void Only_delete_correct_cfs( JsonTransactionStep processor) { const string radarrCfData = @"[{ 'id': 1, 'name': 'not_deleted', 'specifications': [{ 'name': 'spec1', 'negate': false, 'fields': [{ 'name': 'value', 'value': 'value1' }] }] }, { 'id': 2, 'name': 'deleted', 'specifications': [{ 'name': 'spec2', 'negate': false, 'fields': [{ 'name': 'value', 'untouchable': 'field', 'value': 'value1' }] }] }]"; var deletedCfsInCache = new List { new("testtrashid", "", 2), new("", "", 3) }; var radarrCfs = JsonConvert.DeserializeObject>(radarrCfData); processor.RecordDeletions(deletedCfsInCache, radarrCfs!); var expectedTransactions = new CustomFormatTransactionData(); expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("testtrashid", "", 2)); processor.Transactions.Should().BeEquivalentTo(expectedTransactions); } [Test, AutoMockData] public void Conflicting_ids_detected( JsonTransactionStep processor) { const string serviceCfData = @" [{ 'id': 1, 'name': 'first' }, { 'id': 2, 'name': 'second' }]"; var serviceCfs = JsonConvert.DeserializeObject>(serviceCfData)!; var guideCfs = new List { NewCf.Processed("first", "", 2) }; processor.Process(guideCfs, serviceCfs); var expectedTransactions = new CustomFormatTransactionData(); expectedTransactions.ConflictingCustomFormats.Add(new ConflictingCustomFormat(guideCfs[0], 1)); processor.Transactions.Should().BeEquivalentTo(expectedTransactions); } [Test, AutoMockData] public void Service_cf_id_set_when_no_cache_entry(JsonTransactionStep processor) { const string serviceCfData = @" [{ 'id': 1, 'name': 'first' }]"; var serviceCfs = JsonConvert.DeserializeObject>(serviceCfData)!; var guideCfs = new List { NewCf.Processed("first", "") }; processor.Process(guideCfs, serviceCfs); processor.Transactions.UpdatedCustomFormats.Should().BeEquivalentTo( new[] {NewCf.Processed("first", "", 1)}, o => o.Including(x => x.FormatId)); } }