You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
recyclarr/src/Recyclarr.TrashLib.Tests/CustomFormat/Processors/PersistenceSteps/JsonTransactionStepTest.cs

360 lines
8.9 KiB

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<List<JObject>>(@"
[{
'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<List<JObject>>(@"
[{
'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<ProcessedCustomFormatData>
{
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<TrashIdMapping>
{
new("", "", 1) {CustomFormatId = 2}
};
var guideCfs = new List<ProcessedCustomFormatData>
{
NewCf.Processed("updated", "", 1, guideCfData)
};
var radarrCfs = JsonConvert.DeserializeObject<List<JObject>>(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<TrashIdMapping>
{
new("testtrashid", "", 2),
new("", "", 3)
};
var radarrCfs = JsonConvert.DeserializeObject<List<JObject>>(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<List<JObject>>(serviceCfData)!;
var guideCfs = new List<ProcessedCustomFormatData>
{
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<List<JObject>>(serviceCfData)!;
var guideCfs = new List<ProcessedCustomFormatData>
{
NewCf.Processed("first", "")
};
processor.Process(guideCfs, serviceCfs);
processor.Transactions.UpdatedCustomFormats.Should().BeEquivalentTo(
new[] {NewCf.Processed("first", "", 1)},
o => o.Including(x => x.FormatId));
}
}