From 7d803ac65c5f71e2df166201d65397aac96434a9 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:58:02 -0600 Subject: [PATCH 01/14] feat: add update bulk year command --- .editorconfig | 12 +++- .../Records/Update/Bulk/YearCommand.cs | 58 +++++++++++++++++++ .../Commands/Records/Update/BulkCommand.cs | 9 +++ .../Commands/Records/UpdateCommand.cs | 9 +++ src/OnspringCLI/Commands/RecordsCommand.cs | 1 + src/OnspringCLI/Usings.cs | 2 + .../Commands/Records/FindCommandTests.cs | 2 +- .../Records/Update/Bulk/YearCommandTests.cs | 14 +++++ .../Records/Update/BulkCommandTests.cs | 25 ++++++++ .../Commands/Records/UpdateCommandTests.cs | 25 ++++++++ .../UnitTests/Commands/RecordsCommandTests.cs | 13 ++++- tests/OnspringCLI.Tests/Usings.cs | 2 + 12 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs create mode 100644 src/OnspringCLI/Commands/Records/Update/BulkCommand.cs create mode 100644 src/OnspringCLI/Commands/Records/UpdateCommand.cs create mode 100644 tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs create mode 100644 tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/BulkCommandTests.cs create mode 100644 tests/OnspringCLI.Tests/UnitTests/Commands/Records/UpdateCommandTests.cs diff --git a/.editorconfig b/.editorconfig index 1a80ed1..abc8ce7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -28,6 +28,10 @@ insert_final_newline = false #### .NET Coding Conventions #### [*.{cs,vb}] +dotnet_diagnostic.IDE0100.severity = none +dotnet_diagnostic.IDE0058.severity = none +dotnet_diagnostic.CA1707.severity = none + # Organize usings dotnet_separate_import_directive_groups = true dotnet_sort_system_directives_first = true @@ -82,9 +86,9 @@ dotnet_remove_unnecessary_suppression_exclusions = none [*.cs] # var preferences -csharp_style_var_elsewhere = false:silent -csharp_style_var_for_built_in_types = false:silent -csharp_style_var_when_type_is_apparent = false:silent +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion # Expression-bodied members csharp_style_expression_bodied_accessors = true:silent @@ -130,6 +134,8 @@ csharp_using_directive_placement = outside_namespace:silent #### C# Formatting Rules #### +csharp_style_namespace_declarations = file_scoped:suggestion + # New line preferences csharp_new_line_before_catch = true csharp_new_line_before_else = true diff --git a/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs new file mode 100644 index 0000000..7b00f39 --- /dev/null +++ b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs @@ -0,0 +1,58 @@ + +namespace OnspringCLI.Commands.Records.Update.Bulk; + +public class YearCommand : Command +{ + public YearCommand() : base("year", "Adjusts the value of a list and/or date field by given years") + { + var settingsFileOption = new Option( + aliases: ["--file", "-f"], + description: "The path to the .csv file that contains the list of apps and fields to update." + ) + { + IsRequired = true + }; + + settingsFileOption.AddValidator(FileInfoOptionValidator.Validate); + + AddOption(settingsFileOption); + + AddOption( + new Option( + aliases: ["--years", "-y"], + description: "The number of years to adjust the field by." + ) + { + IsRequired = true + } + ); + } + + public new class Handler : ICommandHandler + { + public Task InvokeAsync(InvocationContext context) + { + // TODO: Implement this method + // 1. Parse the csv file to get a list of apps and their fields + // 2. Validate the apps and fields exist and can be accessed by this app + // 3. For each app page through the records and update the field value of fields + // that match a field in the list + // a. If the field is a list field, get the correct GUID for the target year. Add the value if necessary. + // b. If the field is a date field, adjust the date by the number of years + // c. Update the record with the new field values + // d. Log the record id and the field that was updated + // e. If the record could not be updated, log the record id and the error message, and continue to the next record + // 4. Log the number of records updated and the number of records that could not be updated + // 5. Return 0 if all records were updated successfully, 1 if any records could not be updated + // 6. Write out errors to a file if any records could not be updated + throw new NotImplementedException(); + } + + [ExcludeFromCodeCoverage] + public int Invoke(InvocationContext context) + { + throw new NotImplementedException(); + } + + } +} \ No newline at end of file diff --git a/src/OnspringCLI/Commands/Records/Update/BulkCommand.cs b/src/OnspringCLI/Commands/Records/Update/BulkCommand.cs new file mode 100644 index 0000000..3a23ac6 --- /dev/null +++ b/src/OnspringCLI/Commands/Records/Update/BulkCommand.cs @@ -0,0 +1,9 @@ +namespace OnspringCLI.Commands.Records.Update; + +public class BulkCommand : Command +{ + public BulkCommand() : base("bulk", "Update records in bulk") + { + AddCommand(new YearCommand()); + } +} \ No newline at end of file diff --git a/src/OnspringCLI/Commands/Records/UpdateCommand.cs b/src/OnspringCLI/Commands/Records/UpdateCommand.cs new file mode 100644 index 0000000..326f90e --- /dev/null +++ b/src/OnspringCLI/Commands/Records/UpdateCommand.cs @@ -0,0 +1,9 @@ +namespace OnspringCLI.Commands.Records; + +public class UpdateCommand : Command +{ + public UpdateCommand() : base("update", "Update records") + { + AddCommand(new BulkCommand()); + } +} \ No newline at end of file diff --git a/src/OnspringCLI/Commands/RecordsCommand.cs b/src/OnspringCLI/Commands/RecordsCommand.cs index 74dd9b1..4dba990 100644 --- a/src/OnspringCLI/Commands/RecordsCommand.cs +++ b/src/OnspringCLI/Commands/RecordsCommand.cs @@ -5,5 +5,6 @@ public class RecordsCommand : Command public RecordsCommand() : base("records", "Manage records") { AddCommand(new FindCommand()); + AddCommand(new UpdateCommand()); } } \ No newline at end of file diff --git a/src/OnspringCLI/Usings.cs b/src/OnspringCLI/Usings.cs index dbce84d..0ad59b2 100644 --- a/src/OnspringCLI/Usings.cs +++ b/src/OnspringCLI/Usings.cs @@ -24,6 +24,8 @@ global using OnspringCLI.Commands.Attachments; global using OnspringCLI.Commands.Records; global using OnspringCLI.Commands.Records.Find; +global using OnspringCLI.Commands.Records.Update; +global using OnspringCLI.Commands.Records.Update.Bulk; global using OnspringCLI.Extensions; global using OnspringCLI.Factories; global using OnspringCLI.Interfaces; diff --git a/tests/OnspringCLI.Tests/UnitTests/Commands/Records/FindCommandTests.cs b/tests/OnspringCLI.Tests/UnitTests/Commands/Records/FindCommandTests.cs index 4716571..658b2b5 100644 --- a/tests/OnspringCLI.Tests/UnitTests/Commands/Records/FindCommandTests.cs +++ b/tests/OnspringCLI.Tests/UnitTests/Commands/Records/FindCommandTests.cs @@ -18,7 +18,7 @@ public void FindCommand_WhenCalled_ItShouldHaveAReferencesCommand() var findCommand = new FindCommand(); findCommand.Subcommands - .FirstOrDefault(x => x.Name == "references") + .FirstOrDefault(static x => x.Name == "references") .Should() .NotBeNull().And.BeOfType(); } diff --git a/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs b/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs new file mode 100644 index 0000000..bde7593 --- /dev/null +++ b/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs @@ -0,0 +1,14 @@ +namespace OnspringCLI.Tests.UnitTests.Commands.Records.Update.Bulk; + +public class YearCommandTests +{ + [Fact] + public void YearCommand_WhenCalled_ReturnsNewInstance() + { + var yearCommand = new YearCommand(); + + yearCommand.Should().NotBeNull(); + yearCommand.Name.Should().Be("year"); + yearCommand.Description.Should().Be("Adjusts the value of a list and/or date field by given years"); + } +} \ No newline at end of file diff --git a/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/BulkCommandTests.cs b/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/BulkCommandTests.cs new file mode 100644 index 0000000..f1973a0 --- /dev/null +++ b/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/BulkCommandTests.cs @@ -0,0 +1,25 @@ +namespace OnspringCLI.Tests.UnitTests.Commands.Records.Update; + +public class BulkCommandTests +{ + [Fact] + public void BulkCommand_WhenCalled_ReturnsNewInstance() + { + var bulkCommand = new BulkCommand(); + + bulkCommand.Should().NotBeNull(); + bulkCommand.Name.Should().Be("bulk"); + bulkCommand.Description.Should().Be("Update records in bulk"); + } + + [Fact] + public void BulkCommand_WhenCalled_ItShouldHaveAYearCommand() + { + var bulkCommand = new BulkCommand(); + + bulkCommand.Subcommands + .FirstOrDefault(static x => x.Name == "year") + .Should() + .NotBeNull().And.BeOfType(); + } +} \ No newline at end of file diff --git a/tests/OnspringCLI.Tests/UnitTests/Commands/Records/UpdateCommandTests.cs b/tests/OnspringCLI.Tests/UnitTests/Commands/Records/UpdateCommandTests.cs new file mode 100644 index 0000000..f5760c2 --- /dev/null +++ b/tests/OnspringCLI.Tests/UnitTests/Commands/Records/UpdateCommandTests.cs @@ -0,0 +1,25 @@ +namespace OnspringCLI.Tests.UnitTests.Commands.Records; + +public class UpdateCommandTests +{ + [Fact] + public void UpdateCommand_WhenCalled_ReturnsNewInstance() + { + var updateCommand = new UpdateCommand(); + + updateCommand.Should().NotBeNull(); + updateCommand.Name.Should().Be("update"); + updateCommand.Description.Should().Be("Update records"); + } + + [Fact] + public void UpdateCommand_WhenCalled_ItShouldHaveABulkCommand() + { + var updateCommand = new UpdateCommand(); + + updateCommand.Subcommands + .FirstOrDefault(static x => x.Name == "bulk") + .Should() + .NotBeNull().And.BeOfType(); + } +} \ No newline at end of file diff --git a/tests/OnspringCLI.Tests/UnitTests/Commands/RecordsCommandTests.cs b/tests/OnspringCLI.Tests/UnitTests/Commands/RecordsCommandTests.cs index fec655b..59841da 100644 --- a/tests/OnspringCLI.Tests/UnitTests/Commands/RecordsCommandTests.cs +++ b/tests/OnspringCLI.Tests/UnitTests/Commands/RecordsCommandTests.cs @@ -18,8 +18,19 @@ public void RecordsCommand_WhenCalled_ItShouldHaveAFindCommand() var recordsCommand = new RecordsCommand(); recordsCommand.Subcommands - .FirstOrDefault(x => x.Name == "find") + .FirstOrDefault(static x => x.Name == "find") .Should() .NotBeNull().And.BeOfType(); } + + [Fact] + public void RecordsCommand_WhenCalled_ItShouldHaveAnUpdateCommand() + { + var recordsCommand = new RecordsCommand(); + + recordsCommand.Subcommands + .FirstOrDefault(static x => x.Name == "update") + .Should() + .NotBeNull().And.BeOfType(); + } } \ No newline at end of file diff --git a/tests/OnspringCLI.Tests/Usings.cs b/tests/OnspringCLI.Tests/Usings.cs index 00d3104..2ab963f 100644 --- a/tests/OnspringCLI.Tests/Usings.cs +++ b/tests/OnspringCLI.Tests/Usings.cs @@ -20,6 +20,8 @@ global using OnspringCLI.Commands.Attachments; global using OnspringCLI.Commands.Records; global using OnspringCLI.Commands.Records.Find; +global using OnspringCLI.Commands.Records.Update; +global using OnspringCLI.Commands.Records.Update.Bulk; global using OnspringCLI.Factories; global using OnspringCLI.Interfaces; global using OnspringCLI.Maps; From dae94d6784477019e143500e76778f01795e4e25 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:58:09 -0600 Subject: [PATCH 02/14] feat: begin work on implementing update bulk year command --- .editorconfig | 2 +- .../Records/Update/Bulk/YearCommand.cs | 108 +++++++++++++++++- .../Interfaces/IOnspringService.cs | 29 +++-- .../Interfaces/IRecordsProcessor.cs | 3 +- .../Processors/RecordsProcessor.cs | 40 +++---- 5 files changed, 138 insertions(+), 44 deletions(-) diff --git a/.editorconfig b/.editorconfig index abc8ce7..6288000 100644 --- a/.editorconfig +++ b/.editorconfig @@ -54,7 +54,7 @@ dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent # Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion # Expression-level preferences dotnet_style_coalesce_expression = true:suggestion diff --git a/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs index 7b00f39..613bc9b 100644 --- a/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs +++ b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs @@ -1,4 +1,3 @@ - namespace OnspringCLI.Commands.Records.Update.Bulk; public class YearCommand : Command @@ -30,7 +29,18 @@ public YearCommand() : base("year", "Adjusts the value of a list and/or date fie public new class Handler : ICommandHandler { - public Task InvokeAsync(InvocationContext context) + private readonly ILogger _logger; + private readonly IRecordsProcessor _processor; + public FileInfo File { get; set; } + public int Years { get; set; } + + public Handler(ILogger logger, IRecordsProcessor processor) + { + _logger = logger.ForContext(); + _processor = processor; + } + + public async Task InvokeAsync(InvocationContext context) { // TODO: Implement this method // 1. Parse the csv file to get a list of apps and their fields @@ -45,7 +55,74 @@ public Task InvokeAsync(InvocationContext context) // 4. Log the number of records updated and the number of records that could not be updated // 5. Return 0 if all records were updated successfully, 1 if any records could not be updated // 6. Write out errors to a file if any records could not be updated - throw new NotImplementedException(); + + _logger.Information("Starting bulk year update"); + + _logger.Information("Loading settings from {File}.", File.FullName); + var fieldsToUpdate = await GetFieldsToUpdateAsync(); + + _logger.Information("Validating apps."); + var allApps = await _processor.GetApps(); + var appsFound = allApps.Where(a => fieldsToUpdate.ContainsKey(a.Name)).ToList(); + + if (appsFound.Count != fieldsToUpdate.Count) + { + var appsNotFound = fieldsToUpdate.Keys.Except(appsFound.Select(a => a.Name)); + _logger.Warning("The following apps in the file could not be found: {Apps}.", appsNotFound); + return 1; + } + + _logger.Information("Validating fields."); + var mapping = new Dictionary>(); + var fieldsNotFound = new Dictionary>(); + var invalidFieldsFound = new Dictionary>(); + + foreach (var app in appsFound) + { + var fields = await _processor.GetFieldsForApp(app.Id); + var fieldsLookingFor = fieldsToUpdate[app.Name]; + var foundFields = fields.Where(f => fieldsLookingFor + .Contains(f.Name, StringComparer.OrdinalIgnoreCase)) + .ToList(); + + // TODO: Extract to method + if (foundFields.Count != fieldsLookingFor.Count) + { + var fieldsNotFoundForApp = fieldsLookingFor.Except(foundFields.Select(f => f.Name)); + fieldsNotFound.Add(app.Name, [.. fieldsNotFoundForApp]); + } + + // TODO: Extract to method + if (foundFields.Any(f => f.Type is not FieldType.List and not FieldType.Date)) + { + var invalidFields = foundFields + .Where(f => f.Type is not FieldType.List and not FieldType.Date) + .Select(f => f.Name); + + invalidFieldsFound.Add(app.Name, [.. invalidFields]); + } + + if (fieldsNotFound.ContainsKey(app.Name) || invalidFieldsFound.ContainsKey(app.Name)) + { + continue; + } + + mapping.Add(app, foundFields); + } + + if (fieldsNotFound.Count > 0) + { + foreach (var (app, fields) in fieldsNotFound) + { + _logger.Warning("The following fields in app {App} could not be found: {Fields}.", app, fields); + } + + return 2; + } + + _logger.Information("Updating records."); + + return 0; } [ExcludeFromCodeCoverage] @@ -54,5 +131,30 @@ public int Invoke(InvocationContext context) throw new NotImplementedException(); } + private async Task>> GetFieldsToUpdateAsync() + { + var fieldsToUpdate = new Dictionary>(); + + using var reader = new StreamReader(File.FullName); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + + await foreach (var record in csv.GetRecordsAsync()) + { + if (fieldsToUpdate.TryGetValue(record.App, out var value)) + { + value.Add(record.Field); + continue; + } + + fieldsToUpdate.Add(record.App, [record.Field]); + } + + return fieldsToUpdate; + } + + private record FieldToUpdate( + string App, + string Field + ); } } \ No newline at end of file diff --git a/src/OnspringCLI/Interfaces/IOnspringService.cs b/src/OnspringCLI/Interfaces/IOnspringService.cs index 8f86b25..6603579 100644 --- a/src/OnspringCLI/Interfaces/IOnspringService.cs +++ b/src/OnspringCLI/Interfaces/IOnspringService.cs @@ -1,17 +1,16 @@ -namespace OnspringCLI.Interfaces +namespace OnspringCLI.Interfaces; + +public interface IOnspringService { - public interface IOnspringService - { - Task TryDeleteFile(string apiKey, OnspringFileRequest fileRequest); - Task> GetAllFields(string apiKey, int appId); - Task> GetApps(string apiKey); - Task GetAPageOfRecords(string apiKey, int appId, List fieldIds, PagingRequest pagingRequest); - Task GetFile(string apiKey, OnspringFileRequest fileRequest); - Task GetReport(string apiKey, int reportId); - Task GetField(string apiKey, int fieldId); - Task GetAPageOfRecordsByQuery(string apiKey, int appId, List fieldIds, string queryFilter, PagingRequest? pagingRequest = null); - Task GetFileInfo(string apiKey, OnspringFileRequest fileRequest); - Task?> SaveFile(string apiKey, SaveFileRequest request); - Task?> UpdateRecord(string apiKey, ResultRecord recordUpdates); - } + Task TryDeleteFile(string apiKey, OnspringFileRequest fileRequest); + Task> GetAllFields(string apiKey, int appId); + Task> GetApps(string apiKey); + Task GetAPageOfRecords(string apiKey, int appId, List fieldIds, PagingRequest pagingRequest); + Task GetFile(string apiKey, OnspringFileRequest fileRequest); + Task GetReport(string apiKey, int reportId); + Task GetField(string apiKey, int fieldId); + Task GetAPageOfRecordsByQuery(string apiKey, int appId, List fieldIds, string queryFilter, PagingRequest? pagingRequest = null); + Task GetFileInfo(string apiKey, OnspringFileRequest fileRequest); + Task?> SaveFile(string apiKey, SaveFileRequest request); + Task?> UpdateRecord(string apiKey, ResultRecord recordUpdates); } \ No newline at end of file diff --git a/src/OnspringCLI/Interfaces/IRecordsProcessor.cs b/src/OnspringCLI/Interfaces/IRecordsProcessor.cs index 0eefc90..2de77c4 100644 --- a/src/OnspringCLI/Interfaces/IRecordsProcessor.cs +++ b/src/OnspringCLI/Interfaces/IRecordsProcessor.cs @@ -1,10 +1,9 @@ - - namespace OnspringCLI.Interfaces; public interface IRecordsProcessor { Task> GetApps(); + Task> GetFieldsForApp(int appId); Task> GetReferenceFields(int sourceAppId, int targetAppId); Task> GetReferences(App sourceApp, List referenceFields, List recordIds); void WriteReferencesReport(List references, string outputDirectory); diff --git a/src/OnspringCLI/Processors/RecordsProcessor.cs b/src/OnspringCLI/Processors/RecordsProcessor.cs index 775c023..113ced3 100644 --- a/src/OnspringCLI/Processors/RecordsProcessor.cs +++ b/src/OnspringCLI/Processors/RecordsProcessor.cs @@ -1,41 +1,35 @@ namespace OnspringCLI.Processors; -class RecordsProcessor : IRecordsProcessor +internal class RecordsProcessor( + ILogger logger, + IReportService reportService, + IOnspringService onspringService, + IOptions globalOptions +) : IRecordsProcessor { - private readonly ILogger _logger; - private readonly IReportService _reportService; - private readonly IOnspringService _onspringService; - private readonly GlobalOptions _globalOptions; - - public RecordsProcessor( - ILogger logger, - IReportService reportService, - IOnspringService onspringService, - IOptions globalOptions - ) + private readonly ILogger _logger = logger.ForContext(); + private readonly IReportService _reportService = reportService; + private readonly IOnspringService _onspringService = onspringService; + private readonly GlobalOptions _globalOptions = globalOptions.Value; + + public Task> GetApps() { - _logger = logger.ForContext(); - _reportService = reportService; - _onspringService = onspringService; - _globalOptions = globalOptions.Value; + return _onspringService.GetApps(_globalOptions.SourceApiKey); } - public Task> GetApps() => _onspringService.GetApps(_globalOptions.SourceApiKey); - public async Task> GetReferenceFields(int sourceAppId, int targetAppId) { var fields = await _onspringService.GetAllFields(_globalOptions.SourceApiKey, sourceAppId); - return fields + return [.. fields .Where(f => f.Type is FieldType.Reference) .Cast() - .Where(f => f.ReferencedAppId == targetAppId) - .ToList(); + .Where(f => f.ReferencedAppId == targetAppId)]; } public async Task> GetReferences(App sourceApp, List referenceFields, List recordIds) { - var referenceFieldIds = referenceFields.Select(f => f.Id).ToList(); + var referenceFieldIds = referenceFields.Select(static f => f.Id).ToList(); var pagingRequest = new PagingRequest { PageNumber = 1 }; var totalPages = 1; @@ -81,7 +75,7 @@ public async Task> GetReferences(App sourceApp, List GetReferencesFromRecords( + private static List GetReferencesFromRecords( App sourceApp, List records, List referenceFields, From a30ef4075c7be0ed4d91c951a59beff9dfb8ee01 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 25 Mar 2025 00:23:48 -0500 Subject: [PATCH 03/14] feat: more work on update year command --- lib/onspring-api-sdk | 2 +- .../Records/Update/Bulk/YearCommand.cs | 80 +++++++++---------- .../Extensions/HostBuilderExtensions.cs | 11 ++- .../Factories/UpdateYearSettingsFactory.cs | 31 +++++++ .../Interfaces/IUpdateYearSettingsFactory.cs | 6 ++ src/OnspringCLI/Models/FieldToUpdate.cs | 3 + src/OnspringCLI/Models/UpdateYearSettings.cs | 3 + .../Processors/RecordsProcessor.cs | 5 ++ .../TestData/OptionsFactory.cs | 6 ++ .../Records/Update/Bulk/YearCommandTests.cs | 38 +++++++++ .../Processors/RecordsProcessorTests.cs | 34 +++++--- 11 files changed, 164 insertions(+), 55 deletions(-) create mode 100644 src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs create mode 100644 src/OnspringCLI/Interfaces/IUpdateYearSettingsFactory.cs create mode 100644 src/OnspringCLI/Models/FieldToUpdate.cs create mode 100644 src/OnspringCLI/Models/UpdateYearSettings.cs diff --git a/lib/onspring-api-sdk b/lib/onspring-api-sdk index 3fc3c0a..cef402f 160000 --- a/lib/onspring-api-sdk +++ b/lib/onspring-api-sdk @@ -1 +1 @@ -Subproject commit 3fc3c0a9a986af8bf7bf4f2ab79f411363bb3243 +Subproject commit cef402ffdf06a23bd8b512c91de9c09b07cb4618 diff --git a/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs index 613bc9b..4168b0e 100644 --- a/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs +++ b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs @@ -31,13 +31,15 @@ public YearCommand() : base("year", "Adjusts the value of a list and/or date fie { private readonly ILogger _logger; private readonly IRecordsProcessor _processor; - public FileInfo File { get; set; } + private readonly IUpdateYearSettingsFactory _settingsFactory; + public FileInfo? File { get; set; } public int Years { get; set; } - public Handler(ILogger logger, IRecordsProcessor processor) + public Handler(ILogger logger, IRecordsProcessor processor, IUpdateYearSettingsFactory settingsFactory) { _logger = logger.ForContext(); _processor = processor; + _settingsFactory = settingsFactory; } public async Task InvokeAsync(InvocationContext context) @@ -59,15 +61,15 @@ public async Task InvokeAsync(InvocationContext context) _logger.Information("Starting bulk year update"); _logger.Information("Loading settings from {File}.", File.FullName); - var fieldsToUpdate = await GetFieldsToUpdateAsync(); + var settings = await _settingsFactory.CreateAsync(File); _logger.Information("Validating apps."); var allApps = await _processor.GetApps(); - var appsFound = allApps.Where(a => fieldsToUpdate.ContainsKey(a.Name)).ToList(); + var appsFound = allApps.Where(a => settings.AppFieldsMap.ContainsKey(a.Name)).ToList(); - if (appsFound.Count != fieldsToUpdate.Count) + if (appsFound.Count != settings.AppFieldsMap.Count) { - var appsNotFound = fieldsToUpdate.Keys.Except(appsFound.Select(a => a.Name)); + var appsNotFound = settings.AppFieldsMap.Keys.Except(appsFound.Select(a => a.Name)); _logger.Warning("The following apps in the file could not be found: {Apps}.", appsNotFound); return 1; } @@ -80,26 +82,21 @@ public async Task InvokeAsync(InvocationContext context) foreach (var app in appsFound) { var fields = await _processor.GetFieldsForApp(app.Id); - var fieldsLookingFor = fieldsToUpdate[app.Name]; + var fieldsLookingFor = settings.AppFieldsMap[app.Name]; var foundFields = fields.Where(f => fieldsLookingFor - .Contains(f.Name, StringComparer.OrdinalIgnoreCase)) - .ToList(); + .Contains(f.Name, StringComparer.OrdinalIgnoreCase)) + .ToList(); - // TODO: Extract to method - if (foundFields.Count != fieldsLookingFor.Count) + var (notFoundFields, invalidFields) = ValidateFields(fields, fieldsLookingFor); + + if (notFoundFields.Count > 0) { - var fieldsNotFoundForApp = fieldsLookingFor.Except(foundFields.Select(f => f.Name)); - fieldsNotFound.Add(app.Name, [.. fieldsNotFoundForApp]); + fieldsNotFound.Add(app.Name, notFoundFields); } - // TODO: Extract to method - if (foundFields.Any(f => f.Type is not FieldType.List and not FieldType.Date)) + if (invalidFields.Count > 0) { - var invalidFields = foundFields - .Where(f => f.Type is not FieldType.List and not FieldType.Date) - .Select(f => f.Name); - - invalidFieldsFound.Add(app.Name, [.. invalidFields]); + invalidFieldsFound.Add(app.Name, invalidFields); } if (fieldsNotFound.ContainsKey(app.Name) || invalidFieldsFound.ContainsKey(app.Name)) @@ -114,9 +111,20 @@ public async Task InvokeAsync(InvocationContext context) { foreach (var (app, fields) in fieldsNotFound) { - _logger.Warning("The following fields in app {App} could not be found: {Fields}.", app, fields); + _logger.Warning("The following fields in app {App} could not be found: {Fields}.", app, string.Join(", ", fields)); + } + } + + if (invalidFieldsFound.Count > 0) + { + foreach (var (app, fields) in invalidFieldsFound) + { + _logger.Warning("The following fields in app {App} are not valid for updating: {Fields}.", app, string.Join(", ", fields)); } + } + if (fieldsNotFound.Count > 0 || invalidFieldsFound.Count > 0) + { return 2; } @@ -131,30 +139,22 @@ public int Invoke(InvocationContext context) throw new NotImplementedException(); } - private async Task>> GetFieldsToUpdateAsync() + private static ValidationResult ValidateFields(List fields, List fieldsLookingFor) { - var fieldsToUpdate = new Dictionary>(); - - using var reader = new StreamReader(File.FullName); - using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + var foundFields = fields.Where(f => fieldsLookingFor + .Contains(f.Name, StringComparer.OrdinalIgnoreCase)) + .ToList(); - await foreach (var record in csv.GetRecordsAsync()) - { - if (fieldsToUpdate.TryGetValue(record.App, out var value)) - { - value.Add(record.Field); - continue; - } + var notFoundFields = fieldsLookingFor.Except(foundFields.Select(f => f.Name)).ToList(); - fieldsToUpdate.Add(record.App, [record.Field]); - } + var invalidFields = foundFields + .Where(f => f.Type is not FieldType.List and not FieldType.Date) + .Select(f => f.Name) + .ToList(); - return fieldsToUpdate; + return new(notFoundFields, invalidFields); } - private record FieldToUpdate( - string App, - string Field - ); + private record ValidationResult(List NotFoundFields, List InvalidFields); } } \ No newline at end of file diff --git a/src/OnspringCLI/Extensions/HostBuilderExtensions.cs b/src/OnspringCLI/Extensions/HostBuilderExtensions.cs index 19fd6a3..c8369ba 100644 --- a/src/OnspringCLI/Extensions/HostBuilderExtensions.cs +++ b/src/OnspringCLI/Extensions/HostBuilderExtensions.cs @@ -1,7 +1,7 @@ namespace OnspringCLI.Extensions; [ExcludeFromCodeCoverage] -static class HostBuilderExtensions +internal static class HostBuilderExtensions { public static IHostBuilder AddSerilog(this IHostBuilder hostBuilder) { @@ -40,7 +40,8 @@ public static IHostBuilder AddSerilog(this IHostBuilder hostBuilder) lc .MinimumLevel.ControlledBy(logLevelSwitch) .WriteTo.Console( - theme: AnsiConsoleTheme.Code + theme: AnsiConsoleTheme.Code, + formatProvider: CultureInfo.InvariantCulture ) ); } @@ -51,7 +52,7 @@ public static IHostBuilder AddServices(this IHostBuilder hostBuilder) { return hostBuilder .ConfigureServices( - (hostingContext, services) => + static (hostingContext, services) => { var logLevelSwitch = new LoggingLevelSwitch( LogEventLevel.Information @@ -66,6 +67,7 @@ public static IHostBuilder AddServices(this IHostBuilder hostBuilder) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } ); } @@ -85,6 +87,7 @@ public static IHostBuilder AddCommandHandlers(this IHostBuilder hostBuilder) Commands.Attachments.Delete.BulkCommand.Handler >() .UseCommandHandler() - .UseCommandHandler(); + .UseCommandHandler() + .UseCommandHandler(); } } \ No newline at end of file diff --git a/src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs b/src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs new file mode 100644 index 0000000..e934826 --- /dev/null +++ b/src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs @@ -0,0 +1,31 @@ + +namespace OnspringCLI.Factories; + +internal class UpdateYearSettingsFactory : IUpdateYearSettingsFactory +{ + public async Task CreateAsync(FileInfo? file) + { + var fieldsToUpdate = new Dictionary>(); + + if (file is null) + { + throw new InvalidOperationException("The file path is not set."); + } + + using var reader = new StreamReader(file.FullName); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + + await foreach (var record in csv.GetRecordsAsync()) + { + if (fieldsToUpdate.TryGetValue(record.AppName, out var value)) + { + value.Add(record.FieldName); + continue; + } + + fieldsToUpdate.Add(record.AppName, [record.FieldName]); + } + + return new(fieldsToUpdate); + } +} \ No newline at end of file diff --git a/src/OnspringCLI/Interfaces/IUpdateYearSettingsFactory.cs b/src/OnspringCLI/Interfaces/IUpdateYearSettingsFactory.cs new file mode 100644 index 0000000..491dbcb --- /dev/null +++ b/src/OnspringCLI/Interfaces/IUpdateYearSettingsFactory.cs @@ -0,0 +1,6 @@ +namespace OnspringCLI.Interfaces; + +public interface IUpdateYearSettingsFactory +{ + Task CreateAsync(FileInfo? file); +} \ No newline at end of file diff --git a/src/OnspringCLI/Models/FieldToUpdate.cs b/src/OnspringCLI/Models/FieldToUpdate.cs new file mode 100644 index 0000000..411768d --- /dev/null +++ b/src/OnspringCLI/Models/FieldToUpdate.cs @@ -0,0 +1,3 @@ +namespace OnspringCLI.Models; + +internal record FieldToUpdate(string AppName, string FieldName); \ No newline at end of file diff --git a/src/OnspringCLI/Models/UpdateYearSettings.cs b/src/OnspringCLI/Models/UpdateYearSettings.cs new file mode 100644 index 0000000..1a007c3 --- /dev/null +++ b/src/OnspringCLI/Models/UpdateYearSettings.cs @@ -0,0 +1,3 @@ +namespace OnspringCLI.Models; + +public record UpdateYearSettings(Dictionary> AppFieldsMap); \ No newline at end of file diff --git a/src/OnspringCLI/Processors/RecordsProcessor.cs b/src/OnspringCLI/Processors/RecordsProcessor.cs index 113ced3..90a9e42 100644 --- a/src/OnspringCLI/Processors/RecordsProcessor.cs +++ b/src/OnspringCLI/Processors/RecordsProcessor.cs @@ -154,4 +154,9 @@ public void WriteReferencesReport(List references, string outpu "references-report.csv" ); } + + public async Task> GetFieldsForApp(int appId) + { + return await _onspringService.GetAllFields(_globalOptions.SourceApiKey, appId); + } } \ No newline at end of file diff --git a/tests/OnspringCLI.Tests/TestData/OptionsFactory.cs b/tests/OnspringCLI.Tests/TestData/OptionsFactory.cs index 0ea5996..ffeddf9 100644 --- a/tests/OnspringCLI.Tests/TestData/OptionsFactory.cs +++ b/tests/OnspringCLI.Tests/TestData/OptionsFactory.cs @@ -87,4 +87,10 @@ public static class OptionsFactory "--record-ids", "1,2" ]; + + public static string[] RequiredYearOptions => + [ + "--file", + "TestData/Files/fields.csv" + ]; } \ No newline at end of file diff --git a/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs b/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs index bde7593..11e6c5e 100644 --- a/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs +++ b/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs @@ -11,4 +11,42 @@ public void YearCommand_WhenCalled_ReturnsNewInstance() yearCommand.Name.Should().Be("year"); yearCommand.Description.Should().Be("Adjusts the value of a list and/or date field by given years"); } + + public class HandlerTests + { + private readonly Mock _loggerMock; + private readonly Mock _processorMock; + private readonly YearCommand.Handler _handler; + private readonly YearCommand _command; + + public HandlerTests() + { + _loggerMock = new Mock(); + _processorMock = new Mock(); + + _loggerMock + .Setup(static x => x.ForContext()) + .Returns(_loggerMock.Object); + + _handler = new YearCommand.Handler( + _loggerMock.Object, + _processorMock.Object + ); + + _command = []; + _command.SetHandler(_handler.InvokeAsync); + } + + [Fact] + public async Task InvokeAsync_WhenCalledAndNoAppsAreFound_ItShouldReturnNonZeroValue() + { + _processorMock + .Setup(static x => x.GetApps()) + .ReturnsAsync([]); + + var result = await _handler.InvokeAsync(context); + + result.Should().Be(1); + } + } } \ No newline at end of file diff --git a/tests/OnspringCLI.Tests/UnitTests/Processors/RecordsProcessorTests.cs b/tests/OnspringCLI.Tests/UnitTests/Processors/RecordsProcessorTests.cs index 77729c1..1862962 100644 --- a/tests/OnspringCLI.Tests/UnitTests/Processors/RecordsProcessorTests.cs +++ b/tests/OnspringCLI.Tests/UnitTests/Processors/RecordsProcessorTests.cs @@ -11,7 +11,7 @@ public class RecordsProcessorTests public RecordsProcessorTests() { _globalOptionsMock - .SetupGet(m => m.Value) + .SetupGet(static m => m.Value) .Returns(new GlobalOptions { SourceApiKey = "sourceApiKey", @@ -19,7 +19,7 @@ public RecordsProcessorTests() }); _loggerMock - .Setup(m => m.ForContext()) + .Setup(static m => m.ForContext()) .Returns(_loggerMock.Object); _processor = new RecordsProcessor( @@ -36,7 +36,7 @@ public async Task GetApps_WhenCalled_ItShouldReturnApps() var apps = new List { new() }; _onspringServiceMock - .Setup(x => x.GetApps(It.IsAny())) + .Setup(static x => x.GetApps(It.IsAny())) .ReturnsAsync(apps); var result = await _processor.GetApps(); @@ -48,7 +48,7 @@ public async Task GetApps_WhenCalled_ItShouldReturnApps() public async Task GetReferenceFields_WhenCalledAndNoReferenceFields_ItShouldReturnEmptyList() { _onspringServiceMock - .Setup(x => x.GetAllFields(It.IsAny(), It.IsAny())) + .Setup(static x => x.GetAllFields(It.IsAny(), It.IsAny())) .ReturnsAsync([]); var result = await _processor.GetReferenceFields(It.IsAny(), It.IsAny()); @@ -67,7 +67,7 @@ public async Task GetReferenceFields_WhenCalledAndReferenceFields_ItShouldReturn }; _onspringServiceMock - .Setup(x => x.GetAllFields(It.IsAny(), It.IsAny())) + .Setup(static x => x.GetAllFields(It.IsAny(), It.IsAny())) .ReturnsAsync(fields); var result = await _processor.GetReferenceFields(It.IsAny(), 2); @@ -84,7 +84,7 @@ public async Task GetReferences_WhenCalledAndSourceAppHasNoRecords_ItShouldRetur var recordIds = new List { 1 }; _onspringServiceMock - .Setup(x => x.GetAPageOfRecords( + .Setup(static x => x.GetAPageOfRecords( It.IsAny(), It.IsAny(), It.IsAny>(), @@ -113,7 +113,7 @@ public async Task GetReferences_WhenCalledAndSourceAppHasOnePageOfRecordsWithout }; _onspringServiceMock - .Setup(x => x.GetAPageOfRecords( + .Setup(static x => x.GetAPageOfRecords( It.IsAny(), It.IsAny(), It.IsAny>(), @@ -190,7 +190,7 @@ public async Task GetReferences_WhenCalledAndSourceAppHasOnePageOfRecordsWithRef }; _onspringServiceMock - .Setup(x => x.GetAPageOfRecords( + .Setup(static x => x.GetAPageOfRecords( It.IsAny(), It.IsAny(), It.IsAny>(), @@ -291,7 +291,7 @@ public async Task GetReferences_WhenCalledAndSourceAppHasMultiplePagesOfRecordsW }; _onspringServiceMock - .SetupSequence(x => x.GetAPageOfRecords( + .SetupSequence(static x => x.GetAPageOfRecords( It.IsAny(), It.IsAny(), It.IsAny>(), @@ -332,7 +332,7 @@ public void WriteReferencesReport_WhenCalled_ItShouldWriteReferencesToCsv() _processor.WriteReferencesReport([], "output"); _reportServiceMock.Verify( - m => m.WriteCsvReport( + static m => m.WriteCsvReport( It.IsAny>(), typeof(RecordReferenceMap), It.IsAny(), @@ -341,4 +341,18 @@ public void WriteReferencesReport_WhenCalled_ItShouldWriteReferencesToCsv() Times.Once ); } + + [Fact] + public async Task GetFieldsForApp_WhenCalled_ItShouldReturnFields() + { + var fields = new List { new() }; + + _onspringServiceMock + .Setup(static x => x.GetAllFields(It.IsAny(), It.IsAny())) + .ReturnsAsync(fields); + + var result = await _processor.GetFieldsForApp(It.IsAny()); + + result.Should().BeEquivalentTo(fields); + } } \ No newline at end of file From 8bbae7f1306a2827c9842aff44dd8722301405d7 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 25 Mar 2025 17:32:34 -0500 Subject: [PATCH 04/14] feat: continue to work on implementing update year command --- lib/onspring-api-sdk | 2 +- .../Records/Update/Bulk/YearCommand.cs | 15 +++--- src/OnspringCLI/Models/UpdateYearSettings.cs | 12 ++++- .../Records/Update/Bulk/YearCommandTests.cs | 13 +++-- .../Factories/UpdateYearSettingsFactory.cs | 48 +++++++++++++++++++ 5 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 tests/OnspringCLI.Tests/UnitTests/Factories/UpdateYearSettingsFactory.cs diff --git a/lib/onspring-api-sdk b/lib/onspring-api-sdk index cef402f..3fc3c0a 160000 --- a/lib/onspring-api-sdk +++ b/lib/onspring-api-sdk @@ -1 +1 @@ -Subproject commit cef402ffdf06a23bd8b512c91de9c09b07cb4618 +Subproject commit 3fc3c0a9a986af8bf7bf4f2ab79f411363bb3243 diff --git a/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs index 4168b0e..4060cf2 100644 --- a/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs +++ b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs @@ -60,17 +60,20 @@ public async Task InvokeAsync(InvocationContext context) _logger.Information("Starting bulk year update"); - _logger.Information("Loading settings from {File}.", File.FullName); + _logger.Information("Loading settings from {File}.", File?.FullName); var settings = await _settingsFactory.CreateAsync(File); _logger.Information("Validating apps."); var allApps = await _processor.GetApps(); - var appsFound = allApps.Where(a => settings.AppFieldsMap.ContainsKey(a.Name)).ToList(); + var appsValidationResult = settings.ValidateApps(allApps); - if (appsFound.Count != settings.AppFieldsMap.Count) + if (appsValidationResult.IsValid is false) { - var appsNotFound = settings.AppFieldsMap.Keys.Except(appsFound.Select(a => a.Name)); - _logger.Warning("The following apps in the file could not be found: {Apps}.", appsNotFound); + _logger.Warning( + "The following apps in the file could not be found: {Apps}.", + string.Join(", ", appsValidationResult.AppsNotFound) + ); + return 1; } @@ -79,7 +82,7 @@ public async Task InvokeAsync(InvocationContext context) var fieldsNotFound = new Dictionary>(); var invalidFieldsFound = new Dictionary>(); - foreach (var app in appsFound) + foreach (var app in appsValidationResult.AppsFound) { var fields = await _processor.GetFieldsForApp(app.Id); var fieldsLookingFor = settings.AppFieldsMap[app.Name]; diff --git a/src/OnspringCLI/Models/UpdateYearSettings.cs b/src/OnspringCLI/Models/UpdateYearSettings.cs index 1a007c3..a897c40 100644 --- a/src/OnspringCLI/Models/UpdateYearSettings.cs +++ b/src/OnspringCLI/Models/UpdateYearSettings.cs @@ -1,3 +1,13 @@ namespace OnspringCLI.Models; -public record UpdateYearSettings(Dictionary> AppFieldsMap); \ No newline at end of file +public record UpdateYearSettings(Dictionary> AppFieldsMap) +{ + public AppsValidationResult ValidateApps(List apps) + { + var appsFound = apps.Where(a => AppFieldsMap.ContainsKey(a.Name)).ToList(); + var appsNotFound = AppFieldsMap.Keys.Except(apps.Select(a => a.Name)).ToList(); + return new(appsNotFound.Count == 0, appsNotFound, appsFound); + } +} + +public record AppsValidationResult(bool IsValid, List AppsNotFound, List AppsFound); \ No newline at end of file diff --git a/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs b/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs index 11e6c5e..fda1ca6 100644 --- a/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs +++ b/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs @@ -14,23 +14,22 @@ public void YearCommand_WhenCalled_ReturnsNewInstance() public class HandlerTests { - private readonly Mock _loggerMock; - private readonly Mock _processorMock; + private readonly Mock _loggerMock = new(); + private readonly Mock _processorMock = new(); + private readonly Mock _settingsFactoryMock = new(); private readonly YearCommand.Handler _handler; private readonly YearCommand _command; public HandlerTests() { - _loggerMock = new Mock(); - _processorMock = new Mock(); - _loggerMock .Setup(static x => x.ForContext()) .Returns(_loggerMock.Object); _handler = new YearCommand.Handler( _loggerMock.Object, - _processorMock.Object + _processorMock.Object, + _settingsFactoryMock.Object ); _command = []; @@ -44,7 +43,7 @@ public async Task InvokeAsync_WhenCalledAndNoAppsAreFound_ItShouldReturnNonZeroV .Setup(static x => x.GetApps()) .ReturnsAsync([]); - var result = await _handler.InvokeAsync(context); + var result = await _command.InvokeAsync(OptionsFactory.RequiredYearOptions); result.Should().Be(1); } diff --git a/tests/OnspringCLI.Tests/UnitTests/Factories/UpdateYearSettingsFactory.cs b/tests/OnspringCLI.Tests/UnitTests/Factories/UpdateYearSettingsFactory.cs new file mode 100644 index 0000000..14f6c34 --- /dev/null +++ b/tests/OnspringCLI.Tests/UnitTests/Factories/UpdateYearSettingsFactory.cs @@ -0,0 +1,48 @@ +namespace OnspringCLI.Tests.UnitTests.Factories; + +public class UpdateYearSettingsFactoryTests : IDisposable +{ + private readonly UpdateYearSettingsFactory _sut = new(); + private const string TestFilePath = "year-settings.csv"; + + public UpdateYearSettingsFactoryTests() + { + CreateTestFile("AppName,FieldName\nApp1,Field1\nApp1,Field2\nApp2,Field1\nApp2,Field2"); + } + + [Fact] + public async Task CreateAsync_WhenCalledWithNullFile_ItShouldThrowInvalidOperationException() + { + var action = () => _sut.CreateAsync(null); + + await action.Should().ThrowAsync(); + } + + [Fact] + public async Task CreateAsync_WhenCalled_ItShouldReturnUpdateYearSettings() + { + var file = new FileInfo(TestFilePath); + + var result = await _sut.CreateAsync(file); + + result.Should().NotBeNull(); + result.AppFieldsMap["App1"].Should().BeEquivalentTo(new List { "Field1", "Field2" }); + result.AppFieldsMap["App2"].Should().BeEquivalentTo(new List { "Field1", "Field2" }); + } + + public void Dispose() + { + DeleteTestFile(); + GC.SuppressFinalize(this); + } + + private static void CreateTestFile(string content) + { + File.WriteAllText(TestFilePath, content); + } + + private static void DeleteTestFile() + { + File.Delete(TestFilePath); + } +} From 823983b0ce68eb1634dba14d304d15c2e6f21c2f Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 9 Apr 2025 20:34:29 -0500 Subject: [PATCH 05/14] feat: working through update logic --- lib/onspring-api-sdk | 2 +- .../Records/Update/Bulk/YearCommand.cs | 121 ++++++++++++------ .../Interfaces/IRecordsProcessor.cs | 1 + src/OnspringCLI/Models/UpdateYearSettings.cs | 35 ++++- .../Models/UpdateYearSettingsTests.cs | 97 ++++++++++++++ 5 files changed, 215 insertions(+), 41 deletions(-) create mode 100644 tests/OnspringCLI.Tests/UnitTests/Models/UpdateYearSettingsTests.cs diff --git a/lib/onspring-api-sdk b/lib/onspring-api-sdk index 3fc3c0a..cef402f 160000 --- a/lib/onspring-api-sdk +++ b/lib/onspring-api-sdk @@ -1 +1 @@ -Subproject commit 3fc3c0a9a986af8bf7bf4f2ab79f411363bb3243 +Subproject commit cef402ffdf06a23bd8b512c91de9c09b07cb4618 diff --git a/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs index 4060cf2..213da05 100644 --- a/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs +++ b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs @@ -78,28 +78,23 @@ public async Task InvokeAsync(InvocationContext context) } _logger.Information("Validating fields."); - var mapping = new Dictionary>(); + var mappings = new Dictionary>(); var fieldsNotFound = new Dictionary>(); var invalidFieldsFound = new Dictionary>(); foreach (var app in appsValidationResult.AppsFound) { var fields = await _processor.GetFieldsForApp(app.Id); - var fieldsLookingFor = settings.AppFieldsMap[app.Name]; - var foundFields = fields.Where(f => fieldsLookingFor - .Contains(f.Name, StringComparer.OrdinalIgnoreCase)) - .ToList(); + var fieldsValidationResult = settings.ValidateFields(app, fields); - var (notFoundFields, invalidFields) = ValidateFields(fields, fieldsLookingFor); - - if (notFoundFields.Count > 0) + if (fieldsValidationResult.FieldsNotFound.Count > 0) { - fieldsNotFound.Add(app.Name, notFoundFields); + fieldsNotFound.Add(app.Name, fieldsValidationResult.FieldsNotFound); } - if (invalidFields.Count > 0) + if (fieldsValidationResult.InvalidFields.Count > 0) { - invalidFieldsFound.Add(app.Name, invalidFields); + invalidFieldsFound.Add(app.Name, fieldsValidationResult.InvalidFields); } if (fieldsNotFound.ContainsKey(app.Name) || invalidFieldsFound.ContainsKey(app.Name)) @@ -107,32 +102,98 @@ public async Task InvokeAsync(InvocationContext context) continue; } - mapping.Add(app, foundFields); + mappings.Add(app, fieldsValidationResult.FieldsFound); } - if (fieldsNotFound.Count > 0) + if (fieldsNotFound.Count > 0 || invalidFieldsFound.Count > 0) { foreach (var (app, fields) in fieldsNotFound) { _logger.Warning("The following fields in app {App} could not be found: {Fields}.", app, string.Join(", ", fields)); } - } - if (invalidFieldsFound.Count > 0) - { - foreach (var (app, fields) in invalidFieldsFound) + foreach (var (app, fields) in fieldsNotFound) { - _logger.Warning("The following fields in app {App} are not valid for updating: {Fields}.", app, string.Join(", ", fields)); + _logger.Warning("The following fields in app {App} could not be found: {Fields}.", app, string.Join(", ", fields)); } - } - if (fieldsNotFound.Count > 0 || invalidFieldsFound.Count > 0) - { return 2; } _logger.Information("Updating records."); + foreach (var mapping in mappings) + { + await foreach (var record in _processor.GetRecords(mapping.Key, mapping.Value)) + { + var updatedRecord = new ResultRecord() + { + AppId = record.AppId, + RecordId = record.RecordId + }; + + // TODO: Finish building updated record + foreach (var field in mapping.Value) + { + var fieldValue = record.FieldData.FirstOrDefault(fv => fv.FieldId == field.Id); + + if (fieldValue is null) + { + _logger.Debug("No value found for {Field} on {Record} in {App}", field.Name, record.RecordId, record.AppId); + continue; + } + + if (field is ListField listField) + { + if (listField.Multiplicity is Multiplicity.SingleSelect) + { + var singleSelectListFieldValue = fieldValue.AsNullableGuid(); + + if (singleSelectListFieldValue is null) + { + _logger.Debug("Unable to get value for {Field} on {Record} in {App}", field.Name, record.RecordId, record.AppId); + } + + continue; + } + + var multiSelectListFieldValue = fieldValue.AsGuidList(); + + if (multiSelectListFieldValue is null) + { + _logger.Debug("Unable to get value for {Field} on {Record} in {App}", field.Name, record.RecordId, mapping.Key); + } + + continue; + } + + if (field.Type is FieldType.Date) + { + var dateFieldValue = fieldValue.AsNullableDateTime(); + + if (dateFieldValue is null || dateFieldValue.HasValue is false) + { + _logger.Debug("Unable to get value for {Field} on {Record} in {App}", field.Name, record.RecordId, mapping.Key); + continue; + } + + var newDateFieldValue = dateFieldValue.Value.AddYears(Years); + _logger.Debug( + "Updating value for {Field} on {Record} in {App} from {OldValue} to {NewValue}", + field.Name, + record.RecordId, + mapping.Key, + dateFieldValue.Value, + newDateFieldValue + ); + updatedRecord.FieldData.Add(new DateFieldValue(fieldValue.FieldId, newDateFieldValue)); + + continue; + } + } + } + } + return 0; } @@ -141,23 +202,5 @@ public int Invoke(InvocationContext context) { throw new NotImplementedException(); } - - private static ValidationResult ValidateFields(List fields, List fieldsLookingFor) - { - var foundFields = fields.Where(f => fieldsLookingFor - .Contains(f.Name, StringComparer.OrdinalIgnoreCase)) - .ToList(); - - var notFoundFields = fieldsLookingFor.Except(foundFields.Select(f => f.Name)).ToList(); - - var invalidFields = foundFields - .Where(f => f.Type is not FieldType.List and not FieldType.Date) - .Select(f => f.Name) - .ToList(); - - return new(notFoundFields, invalidFields); - } - - private record ValidationResult(List NotFoundFields, List InvalidFields); } } \ No newline at end of file diff --git a/src/OnspringCLI/Interfaces/IRecordsProcessor.cs b/src/OnspringCLI/Interfaces/IRecordsProcessor.cs index 2de77c4..5357353 100644 --- a/src/OnspringCLI/Interfaces/IRecordsProcessor.cs +++ b/src/OnspringCLI/Interfaces/IRecordsProcessor.cs @@ -3,6 +3,7 @@ namespace OnspringCLI.Interfaces; public interface IRecordsProcessor { Task> GetApps(); + IAsyncEnumerable GetRecords(App app, List fields); Task> GetFieldsForApp(int appId); Task> GetReferenceFields(int sourceAppId, int targetAppId); Task> GetReferences(App sourceApp, List referenceFields, List recordIds); diff --git a/src/OnspringCLI/Models/UpdateYearSettings.cs b/src/OnspringCLI/Models/UpdateYearSettings.cs index a897c40..ae0acce 100644 --- a/src/OnspringCLI/Models/UpdateYearSettings.cs +++ b/src/OnspringCLI/Models/UpdateYearSettings.cs @@ -8,6 +8,39 @@ public AppsValidationResult ValidateApps(List apps) var appsNotFound = AppFieldsMap.Keys.Except(apps.Select(a => a.Name)).ToList(); return new(appsNotFound.Count == 0, appsNotFound, appsFound); } + + public FieldsValidationResult ValidateFields(App app, List fields) + { + var fieldsLookingFor = AppFieldsMap[app.Name]; + var foundFields = fields.Where(f => fieldsLookingFor + .Contains(f.Name, StringComparer.OrdinalIgnoreCase)) + .ToList(); + + var notFoundFields = fieldsLookingFor.Except(foundFields.Select(f => f.Name)).ToList(); + + var invalidFields = foundFields + .Where(f => f.Type is not FieldType.List and not FieldType.Date) + .Select(f => f.Name) + .ToList(); + + return new( + notFoundFields.Count + invalidFields.Count == 0, + notFoundFields, + invalidFields, + foundFields + ); + } } -public record AppsValidationResult(bool IsValid, List AppsNotFound, List AppsFound); \ No newline at end of file +public record AppsValidationResult( + bool IsValid, + List AppsNotFound, + List AppsFound +); + +public record FieldsValidationResult( + bool IsValid, + List FieldsNotFound, + List InvalidFields, + List FieldsFound +); \ No newline at end of file diff --git a/tests/OnspringCLI.Tests/UnitTests/Models/UpdateYearSettingsTests.cs b/tests/OnspringCLI.Tests/UnitTests/Models/UpdateYearSettingsTests.cs new file mode 100644 index 0000000..4036c2f --- /dev/null +++ b/tests/OnspringCLI.Tests/UnitTests/Models/UpdateYearSettingsTests.cs @@ -0,0 +1,97 @@ +namespace OnspringCLI.Tests.UnitTests.Models; + +public class UpdateYearSettingsTests +{ + [Fact] + public void ValidateApps_WhenGivenValidListOfApps_ItShouldReturnResult() + { + var appFieldsMap = new Dictionary>() + { + { "App 1" , []}, + }; + + var settings = new UpdateYearSettings(appFieldsMap); + var testApp = new App() { Name = "App 1" }; + + var result = settings.ValidateApps([testApp]); + + result.IsValid.Should().BeTrue(); + result.AppsNotFound.Should().BeEmpty(); + result.AppsFound.Should().BeEquivalentTo([testApp]); + } + + [Fact] + public void ValidateApps_WhenGivenInvalidListOfApps_ItShouldReturnResult() + { + var appFieldsMap = new Dictionary>() + { + { "App 1" , []}, + }; + + var settings = new UpdateYearSettings(appFieldsMap); + + var result = settings.ValidateApps([new App() { Name = "App 2" }]); + + result.IsValid.Should().BeFalse(); + result.AppsNotFound.Should().BeEquivalentTo(["App 1"]); + result.AppsFound.Should().BeEmpty(); + } + + [Fact] + public void ValidateFields_WhenGivenFieldsThatCanNotBeFound_ItShouldReturnResult() + { + var testApp = new App() { Name = "App 1" }; + var testField = new Field() { Name = "Field 1" }; + var appFieldsMap = new Dictionary>() + { + { testApp.Name, [testField.Name]}, + }; + + var settings = new UpdateYearSettings(appFieldsMap); + var result = settings.ValidateFields(testApp, [new Field() { Name = "Field 2" }]); + + result.IsValid.Should().BeFalse(); + result.FieldsNotFound.Should().BeEquivalentTo([testField.Name]); + result.FieldsFound.Should().BeEmpty(); + result.InvalidFields.Should().BeEmpty(); + } + + [Fact] + public void ValidateFields_WhenGivenFieldsThatAreInvalid_ItShouldReturnResult() + { + var testApp = new App() { Name = "App 1" }; + var testField = new ReferenceField() { Name = "Field 1", }; + var appFieldsMap = new Dictionary>() + { + { testApp.Name, [testField.Name] } + }; + + var settings = new UpdateYearSettings(appFieldsMap); + var result = settings.ValidateFields(testApp, [testField]); + + result.IsValid.Should().BeFalse(); + result.FieldsNotFound.Should().BeEmpty(); + result.FieldsFound.Should().BeEquivalentTo([testField]); + result.InvalidFields.Should().NotBeEquivalentTo([testField]); + } + + [Fact] + public void ValidateFields_WhenGivenValidListOfFields_ItShouldReturnResult() + { + var testApp = new App() { Name = "App 1" }; + var testDateField = new Field() { Name = "Field 1", Type = FieldType.Date }; + var testListField = new Field() { Name = "Field 2", Type = FieldType.List }; + var appFieldsMap = new Dictionary>() + { + { testApp.Name, [testDateField.Name, testListField.Name] } + }; + + var settings = new UpdateYearSettings(appFieldsMap); + var result = settings.ValidateFields(testApp, [testDateField, testListField]); + + result.IsValid.Should().BeTrue(); + result.FieldsNotFound.Should().BeEmpty(); + result.FieldsFound.Should().BeEquivalentTo([testDateField, testListField]); + result.InvalidFields.Should().BeEmpty(); + } +} \ No newline at end of file From 19a30e1a4aacd963c5aaa6b21572a0018a38a2b9 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Thu, 10 Apr 2025 10:33:10 -0500 Subject: [PATCH 06/14] feat: add logic to handling list fields --- .../Records/Update/Bulk/YearCommand.cs | 46 +++++-- .../Interfaces/IOnspringService.cs | 2 + .../Processors/RecordsProcessor.cs | 124 ++++++++++++++++++ src/OnspringCLI/Services/OnspringService.cs | 68 +++++++++- src/OnspringCLI/Usings.cs | 1 + 5 files changed, 227 insertions(+), 14 deletions(-) diff --git a/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs index 213da05..8bc067d 100644 --- a/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs +++ b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs @@ -132,38 +132,60 @@ public async Task InvokeAsync(InvocationContext context) RecordId = record.RecordId }; - // TODO: Finish building updated record foreach (var field in mapping.Value) { var fieldValue = record.FieldData.FirstOrDefault(fv => fv.FieldId == field.Id); if (fieldValue is null) { - _logger.Debug("No value found for {Field} on {Record} in {App}", field.Name, record.RecordId, record.AppId); + _logger.Debug("No value found for {Field} on {Record} in {App}", field.Name, record.RecordId, mapping.Key); continue; } - if (field is ListField listField) + if (field is ListField listField && listField.Multiplicity is Multiplicity.SingleSelect) { - if (listField.Multiplicity is Multiplicity.SingleSelect) + var singleSelectListFieldValue = fieldValue.AsNullableGuid(); + + if (singleSelectListFieldValue is null) { - var singleSelectListFieldValue = fieldValue.AsNullableGuid(); + _logger.Debug("Unable to get value for {Field} on {Record} in {App}", field.Name, record.RecordId, mapping.Key); + continue; + } + + var value = listField.Values.FirstOrDefault(v => v.Id == singleSelectListFieldValue); - if (singleSelectListFieldValue is null) - { - _logger.Debug("Unable to get value for {Field} on {Record} in {App}", field.Name, record.RecordId, record.AppId); - } + if (value is null) + { + _logger.Debug( + "Unable to find list value for {Value} for {Field} on {Record} in {App}", + singleSelectListFieldValue, + field.Name, + record.RecordId, + mapping.Key + ); continue; } - var multiSelectListFieldValue = fieldValue.AsGuidList(); + var isYear = int.TryParse(value.Name, out var valueAsYear); - if (multiSelectListFieldValue is null) + if (isYear is false) { - _logger.Debug("Unable to get value for {Field} on {Record} in {App}", field.Name, record.RecordId, mapping.Key); + _logger.Debug( + "Unable to parse a year value from {Value} for {Field} on {Record} in {App}", + value.Name, + field.Name, + record.RecordId, + mapping.Key + ); + + continue; } + var newYearValue = valueAsYear + Years; + + + continue; } diff --git a/src/OnspringCLI/Interfaces/IOnspringService.cs b/src/OnspringCLI/Interfaces/IOnspringService.cs index 6603579..fadcb20 100644 --- a/src/OnspringCLI/Interfaces/IOnspringService.cs +++ b/src/OnspringCLI/Interfaces/IOnspringService.cs @@ -1,3 +1,4 @@ + namespace OnspringCLI.Interfaces; public interface IOnspringService @@ -13,4 +14,5 @@ public interface IOnspringService Task GetFileInfo(string apiKey, OnspringFileRequest fileRequest); Task?> SaveFile(string apiKey, SaveFileRequest request); Task?> UpdateRecord(string apiKey, ResultRecord recordUpdates); + Task GetOrAddListValueByName(string apiKey, int fieldId, ListValue value); } \ No newline at end of file diff --git a/src/OnspringCLI/Processors/RecordsProcessor.cs b/src/OnspringCLI/Processors/RecordsProcessor.cs index 90a9e42..0110695 100644 --- a/src/OnspringCLI/Processors/RecordsProcessor.cs +++ b/src/OnspringCLI/Processors/RecordsProcessor.cs @@ -1,3 +1,4 @@ + namespace OnspringCLI.Processors; internal class RecordsProcessor( @@ -159,4 +160,127 @@ public async Task> GetFieldsForApp(int appId) { return await _onspringService.GetAllFields(_globalOptions.SourceApiKey, appId); } + + public IAsyncEnumerable GetRecords(App app, List fields) + { + throw new NotImplementedException(); + } + + public async Task UpdateRecordYearValuesAsync(string appName, ResultRecord record, List fields, int years) + { + var updatedRecord = new ResultRecord() + { + AppId = record.AppId, + RecordId = record.RecordId + }; + + foreach (var field in fields) + { + var fieldValue = record.FieldData.FirstOrDefault(fv => fv.FieldId == field.Id); + + if (fieldValue is null) + { + _logger.Debug("No value found for {Field} on {Record} in {App}", field.Name, record.RecordId, appName); + continue; + } + + if (field is ListField listField && listField.Multiplicity is Multiplicity.SingleSelect) + { + var singleSelectListFieldValue = fieldValue.AsNullableGuid(); + + if (singleSelectListFieldValue is null) + { + _logger.Debug("Unable to get value for {Field} on {Record} in {App}", field.Name, record.RecordId, appName); + continue; + } + + var value = listField.Values.FirstOrDefault(v => v.Id == singleSelectListFieldValue); + + if (value is null) + { + _logger.Debug( + "Unable to find list value for {Value} for {Field} on {Record} in {App}", + singleSelectListFieldValue, + field.Name, + record.RecordId, + appName + ); + + continue; + } + + var isYear = int.TryParse(value.Name, out var valueAsYear); + + if (isYear is false) + { + _logger.Debug( + "Unable to parse a year value from {Value} for {Field} on {Record} in {App}", + value.Name, + field.Name, + record.RecordId, + appName + ); + + continue; + } + + var newYearValue = valueAsYear + years; + var listValue = new ListValue() + { + Name = newYearValue.ToString(CultureInfo.InvariantCulture), + NumericValue = newYearValue, + }; + + var newYearValueId = await _onspringService.GetOrAddListValueByName(_globalOptions.SourceApiKey, listField.Id, listValue); + + if (newYearValueId is null) + { + _logger.Debug( + "Unable to get new year value {NewYearValue} for {Field} on {Record} in {App}", + newYearValue, + field.Name, + record.RecordId, + appName + ); + + continue; + } + + _logger.Debug( + "Updating value for {Field} on {Record} in {App} from {OldValue} to {NewValue}", + field.Name, + record.RecordId, + appName, + valueAsYear, + newYearValue + ); + updatedRecord.FieldData.Add(new GuidFieldValue(listField.Id, newYearValueId)); + continue; + } + + if (field.Type is FieldType.Date) + { + var dateFieldValue = fieldValue.AsNullableDateTime(); + + if (dateFieldValue is null || dateFieldValue.HasValue is false) + { + _logger.Debug("Unable to get value for {Field} on {Record} in {App}", field.Name, record.RecordId, appName); + continue; + } + + var newDateFieldValue = dateFieldValue.Value.AddYears(years); + _logger.Debug( + "Updating value for {Field} on {Record} in {App} from {OldValue} to {NewValue}", + field.Name, + record.RecordId, + appName, + dateFieldValue.Value, + newDateFieldValue + ); + updatedRecord.FieldData.Add(new DateFieldValue(fieldValue.FieldId, newDateFieldValue)); + + continue; + } + } + } } \ No newline at end of file diff --git a/src/OnspringCLI/Services/OnspringService.cs b/src/OnspringCLI/Services/OnspringService.cs index f179bd2..d6c411b 100644 --- a/src/OnspringCLI/Services/OnspringService.cs +++ b/src/OnspringCLI/Services/OnspringService.cs @@ -1,5 +1,3 @@ -using System.Net; - namespace OnspringCLI.Services; public class OnspringService : IOnspringService @@ -424,6 +422,72 @@ public async Task TryDeleteFile(string apiKey, OnspringFileRequest fileReq } } + public async Task GetOrAddListValueByName(string apiKey, int fieldId, ListValue value) + { + try + { + var client = _clientFactory.Create(apiKey); + var listFieldResponse = await ExecuteRequest(async () => await client.GetFieldAsync(fieldId)); + + if (listFieldResponse.IsSuccessful is false) + { + _logger.Debug( + "Unable to get list field with id {Id}: {StatusCode} - {Message}", + fieldId, + listFieldResponse.StatusCode, + listFieldResponse.Message + ); + + return null; + } + + if (listFieldResponse.Value is not ListField listField) + { + _logger.Debug( + "Field retrieved with id {Id} is not a list field", + listFieldResponse.Value.Id + ); + + return null; + } + + var existingValue = listField.Values.FirstOrDefault(lv => lv.Name == value.Name); + + if (existingValue is not null) + { + return existingValue.Id; + } + + var saveListValueResponse = await ExecuteRequest(async () => await client.SaveListItemAsync(new() + { + ListId = listField.ListId, + Name = value.Name, + NumericValue = value.NumericValue, + Color = value.Color, + Weight = value.SortOrder, + })); + + if (saveListValueResponse.IsSuccessful is false) + { + _logger.Debug( + "Unable to add list value with name {Name}: {StatusCode} - {Message}", + value.Name, + saveListValueResponse.StatusCode, + saveListValueResponse.Message + ); + + return null; + } + + return saveListValueResponse.Value.Id; + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to get or add list value with name {Name}.", value.Name); + return null; + } + } + [ExcludeFromCodeCoverage] private async Task> ExecuteRequest(Func>> func, int retry = 1) { diff --git a/src/OnspringCLI/Usings.cs b/src/OnspringCLI/Usings.cs index 0ad59b2..ad3b66f 100644 --- a/src/OnspringCLI/Usings.cs +++ b/src/OnspringCLI/Usings.cs @@ -7,6 +7,7 @@ global using System.CommandLine.Parsing; global using System.Diagnostics.CodeAnalysis; global using System.Globalization; +global using System.Net; global using CsvHelper; global using CsvHelper.Configuration; From b2fae26a4c469e2e91595011396d4e1c23dcf229 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Thu, 10 Apr 2025 13:52:26 -0500 Subject: [PATCH 07/14] feat: return updated record and log counts --- .../Records/Update/Bulk/YearCommand.cs | 124 ++++-------------- .../Factories/UpdateYearSettingsFactory.cs | 17 +++ .../Interfaces/IRecordsProcessor.cs | 1 + src/OnspringCLI/Models/FieldToUpdate.cs | 16 ++- .../Processors/RecordsProcessor.cs | 66 +++++++++- 5 files changed, 120 insertions(+), 104 deletions(-) diff --git a/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs index 8bc067d..53c2d8b 100644 --- a/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs +++ b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs @@ -44,20 +44,6 @@ public Handler(ILogger logger, IRecordsProcessor processor, IUpdateYearSettingsF public async Task InvokeAsync(InvocationContext context) { - // TODO: Implement this method - // 1. Parse the csv file to get a list of apps and their fields - // 2. Validate the apps and fields exist and can be accessed by this app - // 3. For each app page through the records and update the field value of fields - // that match a field in the list - // a. If the field is a list field, get the correct GUID for the target year. Add the value if necessary. - // b. If the field is a date field, adjust the date by the number of years - // c. Update the record with the new field values - // d. Log the record id and the field that was updated - // e. If the record could not be updated, log the record id and the error message, and continue to the next record - // 4. Log the number of records updated and the number of records that could not be updated - // 5. Return 0 if all records were updated successfully, 1 if any records could not be updated - // 6. Write out errors to a file if any records could not be updated - _logger.Information("Starting bulk year update"); _logger.Information("Loading settings from {File}.", File?.FullName); @@ -120,102 +106,44 @@ public async Task InvokeAsync(InvocationContext context) return 2; } - _logger.Information("Updating records."); + _logger.Information("Updating records..."); + + var counts = new Dictionary(); foreach (var mapping in mappings) { await foreach (var record in _processor.GetRecords(mapping.Key, mapping.Value)) { - var updatedRecord = new ResultRecord() + var updatedRecord = await _processor.UpdateRecordYearValues( + mapping.Key.Name, + record, + mapping.Value, + Years + ); + + if (updatedRecord is null) { - AppId = record.AppId, - RecordId = record.RecordId - }; + continue; + } - foreach (var field in mapping.Value) + if (counts.TryGetValue(mapping.Key.Name, out var value)) + { + counts[mapping.Key.Name] = ++value; + } + else { - var fieldValue = record.FieldData.FirstOrDefault(fv => fv.FieldId == field.Id); - - if (fieldValue is null) - { - _logger.Debug("No value found for {Field} on {Record} in {App}", field.Name, record.RecordId, mapping.Key); - continue; - } - - if (field is ListField listField && listField.Multiplicity is Multiplicity.SingleSelect) - { - var singleSelectListFieldValue = fieldValue.AsNullableGuid(); - - if (singleSelectListFieldValue is null) - { - _logger.Debug("Unable to get value for {Field} on {Record} in {App}", field.Name, record.RecordId, mapping.Key); - continue; - } - - var value = listField.Values.FirstOrDefault(v => v.Id == singleSelectListFieldValue); - - if (value is null) - { - _logger.Debug( - "Unable to find list value for {Value} for {Field} on {Record} in {App}", - singleSelectListFieldValue, - field.Name, - record.RecordId, - mapping.Key - ); - - continue; - } - - var isYear = int.TryParse(value.Name, out var valueAsYear); - - if (isYear is false) - { - _logger.Debug( - "Unable to parse a year value from {Value} for {Field} on {Record} in {App}", - value.Name, - field.Name, - record.RecordId, - mapping.Key - ); - - continue; - } - - var newYearValue = valueAsYear + Years; - - - - continue; - } - - if (field.Type is FieldType.Date) - { - var dateFieldValue = fieldValue.AsNullableDateTime(); - - if (dateFieldValue is null || dateFieldValue.HasValue is false) - { - _logger.Debug("Unable to get value for {Field} on {Record} in {App}", field.Name, record.RecordId, mapping.Key); - continue; - } - - var newDateFieldValue = dateFieldValue.Value.AddYears(Years); - _logger.Debug( - "Updating value for {Field} on {Record} in {App} from {OldValue} to {NewValue}", - field.Name, - record.RecordId, - mapping.Key, - dateFieldValue.Value, - newDateFieldValue - ); - updatedRecord.FieldData.Add(new DateFieldValue(fieldValue.FieldId, newDateFieldValue)); - - continue; - } + counts.Add(mapping.Key.Name, 1); } } } + foreach (var (app, count) in counts) + { + _logger.Information("Updated {Count} record(s) in app {App}.", count, app); + } + + _logger.Information("Finished bulk year update"); + return 0; } diff --git a/src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs b/src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs index e934826..94eac0b 100644 --- a/src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs +++ b/src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs @@ -14,6 +14,7 @@ public async Task CreateAsync(FileInfo? file) using var reader = new StreamReader(file.FullName); using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + csv.Context.RegisterClassMap(); await foreach (var record in csv.GetRecordsAsync()) { @@ -28,4 +29,20 @@ public async Task CreateAsync(FileInfo? file) return new(fieldsToUpdate); } + + private class FieldToUpdateMap : ClassMap + { + public FieldToUpdateMap() + { + Map(static m => m.AppName) + .Name("AppName") + .Name("App Name") + .Name("App"); + + Map(static m => m.FieldName) + .Name("FieldName") + .Name("Field Name") + .Name("Field"); + } + } } \ No newline at end of file diff --git a/src/OnspringCLI/Interfaces/IRecordsProcessor.cs b/src/OnspringCLI/Interfaces/IRecordsProcessor.cs index 5357353..fd59ef4 100644 --- a/src/OnspringCLI/Interfaces/IRecordsProcessor.cs +++ b/src/OnspringCLI/Interfaces/IRecordsProcessor.cs @@ -8,4 +8,5 @@ public interface IRecordsProcessor Task> GetReferenceFields(int sourceAppId, int targetAppId); Task> GetReferences(App sourceApp, List referenceFields, List recordIds); void WriteReferencesReport(List references, string outputDirectory); + Task UpdateRecordYearValues(string appName, ResultRecord record, List fields, int years); } \ No newline at end of file diff --git a/src/OnspringCLI/Models/FieldToUpdate.cs b/src/OnspringCLI/Models/FieldToUpdate.cs index 411768d..453ae46 100644 --- a/src/OnspringCLI/Models/FieldToUpdate.cs +++ b/src/OnspringCLI/Models/FieldToUpdate.cs @@ -1,3 +1,17 @@ namespace OnspringCLI.Models; -internal record FieldToUpdate(string AppName, string FieldName); \ No newline at end of file +internal record FieldToUpdate +{ + public string AppName { get; init; } = string.Empty; + public string FieldName { get; init; } = string.Empty; + + internal FieldToUpdate() + { + } + + internal FieldToUpdate(string appName, string fieldName) + { + AppName = appName; + FieldName = fieldName; + } +} \ No newline at end of file diff --git a/src/OnspringCLI/Processors/RecordsProcessor.cs b/src/OnspringCLI/Processors/RecordsProcessor.cs index 0110695..59842c7 100644 --- a/src/OnspringCLI/Processors/RecordsProcessor.cs +++ b/src/OnspringCLI/Processors/RecordsProcessor.cs @@ -1,4 +1,3 @@ - namespace OnspringCLI.Processors; internal class RecordsProcessor( @@ -142,7 +141,6 @@ List recordIds } } } - return references; } @@ -161,12 +159,45 @@ public async Task> GetFieldsForApp(int appId) return await _onspringService.GetAllFields(_globalOptions.SourceApiKey, appId); } - public IAsyncEnumerable GetRecords(App app, List fields) + public async IAsyncEnumerable GetRecords(App app, List fields) { - throw new NotImplementedException(); + var pagingRequest = new PagingRequest { PageNumber = 1 }; + var totalPages = 1; + + do + { + var page = await _onspringService.GetAPageOfRecords( + _globalOptions.SourceApiKey, + app.Id, + [.. fields.Select(static f => f.Id)], + pagingRequest + ); + + if (page is null || page.Items.Count is 0) + { + _logger.Warning("No records found in app {AppId} for page {PageNumber}.", app.Id, pagingRequest.PageNumber); + yield break; + } + + totalPages = page.TotalPages; + + _logger.Information( + "Records retrieved from app {AppId} for page {PageNumber} of {TotalPages}.", + app.Id, + pagingRequest.PageNumber, + totalPages + ); + + foreach (var record in page.Items) + { + yield return record; + } + + pagingRequest.PageNumber++; + } while (pagingRequest.PageNumber <= totalPages); } - public async Task UpdateRecordYearValuesAsync(string appName, ResultRecord record, List fields, int years) + public async Task UpdateRecordYearValues(string appName, ResultRecord record, List fields, int years) { var updatedRecord = new ResultRecord() { @@ -282,5 +313,30 @@ public async Task UpdateRecordYearValuesAsync(string appName, ResultRecord recor continue; } } + + if (updatedRecord.FieldData.Count is 0) + { + _logger.Debug("No fields to update for {Record} in {App}", record.RecordId, appName); + return null; + } + + var updateRecordResponse = await _onspringService.UpdateRecord( + _globalOptions.SourceApiKey, + updatedRecord + ); + + if (updateRecordResponse is null) + { + _logger.Warning("Unable to update record {Record} in {App}", record.RecordId, appName); + return null; + } + + _logger.Information( + "Record {Record} in {App} updated successfully", + record.RecordId, + appName + ); + + return updatedRecord; } } \ No newline at end of file From 5e05ad56ce4de419beed7acf22bf1b4415a943a2 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:52:08 -0500 Subject: [PATCH 08/14] fix: correct name mapping --- .../Factories/UpdateYearSettingsFactory.cs | 11 ++--------- .../UnitTests/Factories/UpdateYearSettingsFactory.cs | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs b/src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs index 94eac0b..18e1a3f 100644 --- a/src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs +++ b/src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs @@ -34,15 +34,8 @@ private class FieldToUpdateMap : ClassMap { public FieldToUpdateMap() { - Map(static m => m.AppName) - .Name("AppName") - .Name("App Name") - .Name("App"); - - Map(static m => m.FieldName) - .Name("FieldName") - .Name("Field Name") - .Name("Field"); + Map(static m => m.AppName).Name("AppName", "App Name", "App"); + Map(static m => m.FieldName).Name("FieldName", "Field Name", "Field"); } } } \ No newline at end of file diff --git a/tests/OnspringCLI.Tests/UnitTests/Factories/UpdateYearSettingsFactory.cs b/tests/OnspringCLI.Tests/UnitTests/Factories/UpdateYearSettingsFactory.cs index 14f6c34..9d90d95 100644 --- a/tests/OnspringCLI.Tests/UnitTests/Factories/UpdateYearSettingsFactory.cs +++ b/tests/OnspringCLI.Tests/UnitTests/Factories/UpdateYearSettingsFactory.cs @@ -45,4 +45,4 @@ private static void DeleteTestFile() { File.Delete(TestFilePath); } -} +} \ No newline at end of file From 70b2c6de177a9dbeed9c61d5e6cab9c40a1b4a89 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:39:00 -0500 Subject: [PATCH 09/14] tests: add tests for year command handler --- src/OnspringCLI/Models/FieldToUpdate.cs | 6 - .../TestData/Files/fields.csv | 5 + .../TestData/OptionsFactory.cs | 4 +- .../Records/Update/Bulk/YearCommandTests.cs | 158 +++++++++++++++++- 4 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 tests/OnspringCLI.Tests/TestData/Files/fields.csv diff --git a/src/OnspringCLI/Models/FieldToUpdate.cs b/src/OnspringCLI/Models/FieldToUpdate.cs index 453ae46..42f1617 100644 --- a/src/OnspringCLI/Models/FieldToUpdate.cs +++ b/src/OnspringCLI/Models/FieldToUpdate.cs @@ -8,10 +8,4 @@ internal record FieldToUpdate internal FieldToUpdate() { } - - internal FieldToUpdate(string appName, string fieldName) - { - AppName = appName; - FieldName = fieldName; - } } \ No newline at end of file diff --git a/tests/OnspringCLI.Tests/TestData/Files/fields.csv b/tests/OnspringCLI.Tests/TestData/Files/fields.csv new file mode 100644 index 0000000..05071ab --- /dev/null +++ b/tests/OnspringCLI.Tests/TestData/Files/fields.csv @@ -0,0 +1,5 @@ +AppName,FieldName +App1,Field1 +App1,Field2 +App2,Field1 +App2,Field2 \ No newline at end of file diff --git a/tests/OnspringCLI.Tests/TestData/OptionsFactory.cs b/tests/OnspringCLI.Tests/TestData/OptionsFactory.cs index ffeddf9..fa42ded 100644 --- a/tests/OnspringCLI.Tests/TestData/OptionsFactory.cs +++ b/tests/OnspringCLI.Tests/TestData/OptionsFactory.cs @@ -91,6 +91,8 @@ public static class OptionsFactory public static string[] RequiredYearOptions => [ "--file", - "TestData/Files/fields.csv" + "TestData/Files/fields.csv", + "--years", + "1", ]; } \ No newline at end of file diff --git a/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs b/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs index fda1ca6..7ce4968 100644 --- a/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs +++ b/tests/OnspringCLI.Tests/UnitTests/Commands/Records/Update/Bulk/YearCommandTests.cs @@ -39,13 +39,169 @@ public HandlerTests() [Fact] public async Task InvokeAsync_WhenCalledAndNoAppsAreFound_ItShouldReturnNonZeroValue() { + var map = new Dictionary> + { + { "App1", new List { "Field1", "Field2" } }, + { "App2", new List { "Field3" } } + }; + + _settingsFactoryMock + .Setup(static x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(new UpdateYearSettings(map)); + + _processorMock + .Setup(static x => x.GetApps()) + .ReturnsAsync([]); + + var result = await _command.InvokeAsync(OptionsFactory.RequiredYearOptions); + + result.Should().NotBe(0); + } + + [Fact] + public async Task InvokeAsync_WhenCalledAndFieldsAreNotFound_ItShouldReturnNonZeroValue() + { + var apps = new List + { + new() { Name = "App1" }, + new() { Name = "App2" } + }; + + var map = apps.ToDictionary( + static app => app.Name, + static app => new List { "Field1", "Field2" } + ); + + _settingsFactoryMock + .Setup(static x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(new UpdateYearSettings(map)); + _processorMock .Setup(static x => x.GetApps()) + .ReturnsAsync(apps); + + _processorMock + .Setup(static x => x.GetFieldsForApp(It.IsAny())) .ReturnsAsync([]); var result = await _command.InvokeAsync(OptionsFactory.RequiredYearOptions); - result.Should().Be(1); + result.Should().NotBe(0); + + _processorMock + .Verify( + static x => x.GetFieldsForApp(It.IsAny()), + Times.Exactly(apps.Count) + ); + } + + [Fact] + public async Task InvokeAsync_WhenCalledAndInvalidFieldsAreFound_ItShouldReturnNonZeroValue() + { + var apps = new List + { + new() { Name = "App1" }, + new() { Name = "App2" } + }; + + var fields = new List + { + new() { Name = "Field1", Type = FieldType.Text }, + new() { Name = "Field2", Type = FieldType.List } + }; + + var map = apps.ToDictionary( + static app => app.Name, + app => fields.Select(static f => f.Name).ToList() + ); + + _settingsFactoryMock + .Setup(static x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(new UpdateYearSettings(map)); + + _processorMock + .Setup(static x => x.GetApps()) + .ReturnsAsync(apps); + + _processorMock + .Setup(static x => x.GetFieldsForApp(It.IsAny())) + .ReturnsAsync(fields); + + var result = await _command.InvokeAsync(OptionsFactory.RequiredYearOptions); + + result.Should().NotBe(0); + + _processorMock + .Verify( + static x => x.GetFieldsForApp(It.IsAny()), + Times.Exactly(apps.Count) + ); + } + + [Fact] + public async Task InvokeAsync_WhenCalledAndAllAppsAndFieldsAreValid_ItShouldReturnZero() + { + var apps = new List + { + new() { Id = 1, Name = "App1" }, + new() { Id = 2, Name = "App2" } + }; + + var fields = new List + { + new() { Id = 1, Name = "Field1", Type = FieldType.List }, + new() { Id = 2, Name = "Field2", Type = FieldType.Date } + }; + + var map = apps.ToDictionary( + static app => app.Name, + app => fields.Select(static f => f.Name).ToList() + ); + + _settingsFactoryMock + .Setup(static x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(new UpdateYearSettings(map)); + + _processorMock + .Setup(static x => x.GetApps()) + .ReturnsAsync(apps); + + _processorMock + .Setup(static x => x.GetFieldsForApp(It.IsAny())) + .ReturnsAsync(fields); + + static async IAsyncEnumerable GetRecords() + { + var records = new List() + { + new(), + new(), + new() + }; + + foreach (var record in records) + { + await Task.Delay(10); + yield return record; + } + } + + _processorMock + .Setup(static x => x.UpdateRecordYearValues( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny() + )) + .ReturnsAsync(new ResultRecord()); + + _processorMock + .Setup(static x => x.GetRecords(It.IsAny(), It.IsAny>())) + .Returns(GetRecords); + + var result = await _command.InvokeAsync(OptionsFactory.RequiredYearOptions); + + result.Should().Be(0); } } } \ No newline at end of file From cc794ff586ebd124b10a008fde0cf56db58fad1a Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 10 Jun 2025 14:34:05 -0500 Subject: [PATCH 10/14] tests: add tests for get or add list value by name --- tests/.editorconfig | 5 + .../Services/OnspringServiceTests.cs | 395 +++++++++++------- 2 files changed, 239 insertions(+), 161 deletions(-) create mode 100644 tests/.editorconfig diff --git a/tests/.editorconfig b/tests/.editorconfig new file mode 100644 index 0000000..bb93f93 --- /dev/null +++ b/tests/.editorconfig @@ -0,0 +1,5 @@ +[*.cs] + +# diagnostics + +dotnet_diagnostic.CA2201.severity = none diff --git a/tests/OnspringCLI.Tests/UnitTests/Services/OnspringServiceTests.cs b/tests/OnspringCLI.Tests/UnitTests/Services/OnspringServiceTests.cs index 90802e6..0333de3 100644 --- a/tests/OnspringCLI.Tests/UnitTests/Services/OnspringServiceTests.cs +++ b/tests/OnspringCLI.Tests/UnitTests/Services/OnspringServiceTests.cs @@ -1,5 +1,3 @@ -using Xunit.Sdk; - namespace OnspringCLI.Tests.UnitTests.Services; public class OnspringServiceTests @@ -15,22 +13,12 @@ public OnspringServiceTests() _mockClient = new Mock(); _loggerMock - .Setup( - x => x.ForContext() - ) - .Returns( - _loggerMock.Object - ); + .Setup(static x => x.ForContext()) + .Returns(_loggerMock.Object); _clientFactoryMock - .Setup( - m => m.Create( - It.IsAny() - ) - ) - .Returns( - _mockClient.Object - ); + .Setup(static m => m.Create(It.IsAny())) + .Returns(_mockClient.Object); _onspringService = new OnspringService( _loggerMock.Object, @@ -52,7 +40,7 @@ public async Task GetAllFields_WhenCalledAndNoFieldsAreFound_ItShouldReturnAnEmp _mockClient .Setup( - m => m.GetFieldsForAppAsync( + static m => m.GetFieldsForAppAsync( It.IsAny(), It.IsAny() ) @@ -67,7 +55,7 @@ public async Task GetAllFields_WhenCalledAndNoFieldsAreFound_ItShouldReturnAnEmp result.Should().BeEmpty(); _mockClient.Verify( - m => m.GetFieldsForAppAsync( + static m => m.GetFieldsForAppAsync( It.IsAny(), It.IsAny() ), @@ -89,7 +77,7 @@ GetPagedFieldsResponse pagedFieldsResponse _mockClient .Setup( - m => m.GetFieldsForAppAsync( + static m => m.GetFieldsForAppAsync( It.IsAny(), It.IsAny() ) @@ -106,7 +94,7 @@ GetPagedFieldsResponse pagedFieldsResponse result.Should().BeEquivalentTo(pagedFieldsResponse.Items); _mockClient.Verify( - m => m.GetFieldsForAppAsync( + static m => m.GetFieldsForAppAsync( It.IsAny(), It.IsAny() ), @@ -135,7 +123,7 @@ GetPagedFieldsResponse pageOne _mockClient .SetupSequence( - m => m.GetFieldsForAppAsync( + static m => m.GetFieldsForAppAsync( It.IsAny(), It.IsAny() ) @@ -153,7 +141,7 @@ GetPagedFieldsResponse pageOne result.Should().BeEquivalentTo(pageOne.Items.Concat(pageTwo.Items)); _mockClient.Verify( - m => m.GetFieldsForAppAsync( + static m => m.GetFieldsForAppAsync( It.IsAny(), It.IsAny() ), @@ -180,7 +168,7 @@ GetPagedFieldsResponse pageOne _mockClient .SetupSequence( - m => m.GetFieldsForAppAsync( + static m => m.GetFieldsForAppAsync( It.IsAny(), It.IsAny() ) @@ -200,7 +188,7 @@ GetPagedFieldsResponse pageOne result.Should().BeEquivalentTo(pageOne.Items); _mockClient.Verify( - m => m.GetFieldsForAppAsync( + static m => m.GetFieldsForAppAsync( It.IsAny(), It.IsAny() ), @@ -213,14 +201,12 @@ public async Task GetAllFields_WhenCalledAndExceptionIsThrown_ItShouldReturnAnEm { _mockClient .Setup( - m => m.GetFieldsForAppAsync( + static m => m.GetFieldsForAppAsync( It.IsAny(), It.IsAny() ) ) - .Throws( - new Exception() - ); + .Throws(new Exception()); var result = await _onspringService.GetAllFields( It.IsAny(), @@ -231,7 +217,7 @@ public async Task GetAllFields_WhenCalledAndExceptionIsThrown_ItShouldReturnAnEm result.Should().BeOfType>(); _mockClient.Verify( - m => m.GetFieldsForAppAsync( + static m => m.GetFieldsForAppAsync( It.IsAny(), It.IsAny() ), @@ -244,7 +230,7 @@ public async Task GetAllFields_WhenCalledAndHttpRequestExceptionOrTaskCanceledEx { _mockClient .SetupSequence( - m => m.GetFieldsForAppAsync( + static m => m.GetFieldsForAppAsync( It.IsAny(), It.IsAny() ) @@ -262,7 +248,7 @@ public async Task GetAllFields_WhenCalledAndHttpRequestExceptionOrTaskCanceledEx result.Should().BeOfType>(); _mockClient.Verify( - m => m.GetFieldsForAppAsync( + static m => m.GetFieldsForAppAsync( It.IsAny(), It.IsAny() ), @@ -283,11 +269,9 @@ GetPagedRecordsResponse recordsResponse ); _mockClient - .Setup( - m => m.GetRecordsForAppAsync( - It.IsAny() - ) - ) + .Setup(static m => m.GetRecordsForAppAsync( + It.IsAny() + )) .ReturnsAsync(apiResponse); var result = await _onspringService.GetAPageOfRecords( @@ -313,7 +297,7 @@ GetPagedRecordsResponse recordsResponse result.Items.Should().BeEquivalentTo(recordsResponse.Items); _mockClient.Verify( - m => m.GetRecordsForAppAsync( + static m => m.GetRecordsForAppAsync( It.IsAny() ), Times.Exactly(1) @@ -330,7 +314,7 @@ public async Task GetAPageOfRecords_WhenCalledAndRequestIsUnsuccessful_ItShouldR _mockClient .SetupSequence( - m => m.GetRecordsForAppAsync( + static m => m.GetRecordsForAppAsync( It.IsAny() ) ) @@ -348,7 +332,7 @@ public async Task GetAPageOfRecords_WhenCalledAndRequestIsUnsuccessful_ItShouldR result.Should().BeNull(); _mockClient.Verify( - m => m.GetRecordsForAppAsync( + static m => m.GetRecordsForAppAsync( It.IsAny() ), Times.Exactly(3) @@ -372,11 +356,9 @@ GetPagedRecordsResponse recordsResponse ); _mockClient - .Setup( - m => m.GetRecordsForAppAsync( - It.IsAny() - ) - ) + .Setup(static m => m.GetRecordsForAppAsync( + It.IsAny() + )) .ReturnsAsync(apiResponse); var result = await _onspringService.GetAPageOfRecords( @@ -401,7 +383,7 @@ GetPagedRecordsResponse recordsResponse result.Items.Should().BeOfType>(); _mockClient.Verify( - m => m.GetRecordsForAppAsync( + static m => m.GetRecordsForAppAsync( It.IsAny() ), Times.Exactly(1) @@ -412,14 +394,10 @@ GetPagedRecordsResponse recordsResponse public async Task GetAPageOfRecords_WhenCalledAndExceptionIsThrown_ItShouldReturnNull() { _mockClient - .Setup( - m => m.GetRecordsForAppAsync( - It.IsAny() - ) - ) - .Throws( - new Exception() - ); + .Setup(static m => m.GetRecordsForAppAsync( + It.IsAny() + )) + .Throws(new Exception()); var result = await _onspringService.GetAPageOfRecords( It.IsAny(), @@ -431,7 +409,7 @@ public async Task GetAPageOfRecords_WhenCalledAndExceptionIsThrown_ItShouldRetur result.Should().BeNull(); _mockClient.Verify( - m => m.GetRecordsForAppAsync( + static m => m.GetRecordsForAppAsync( It.IsAny() ), Times.Exactly(1) @@ -443,7 +421,7 @@ public async Task GetAPageOfRecords_WhenCalledAndHttpRequestOrTaskExceptionIsThr { _mockClient .SetupSequence( - m => m.GetRecordsForAppAsync( + static m => m.GetRecordsForAppAsync( It.IsAny() ) ) @@ -461,7 +439,7 @@ public async Task GetAPageOfRecords_WhenCalledAndHttpRequestOrTaskExceptionIsThr result.Should().BeNull(); _mockClient.Verify( - m => m.GetRecordsForAppAsync( + static m => m.GetRecordsForAppAsync( It.IsAny() ), Times.Exactly(3) @@ -481,13 +459,11 @@ GetFileResponse fileResponse ); _mockClient - .Setup( - m => m.GetFileAsync( - It.IsAny(), - It.IsAny(), - It.IsAny() - ) - ) + .Setup(static m => m.GetFileAsync( + It.IsAny(), + It.IsAny(), + It.IsAny() + )) .ReturnsAsync(apiResponse); var fileRequest = new OnspringFileRequest( @@ -504,12 +480,7 @@ GetFileResponse fileResponse result.Should().NotBeNull(); result.Should().BeOfType(); - if (result is null) - { - return; - } - - result.FileName.Should().Be(fileResponse.FileName); + result!.FileName.Should().Be(fileResponse.FileName); result.ContentLength.Should().Be(fileResponse.ContentLength); result.ContentType.Should().Be(fileResponse.ContentType); result.Stream.Should().NotBeNull(); @@ -525,7 +496,7 @@ public async Task GetFile_WhenCalledAndFileIsNotFound_ItShouldReturnNullAfterRet _mockClient .Setup( - m => m.GetFileAsync( + static m => m.GetFileAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -547,7 +518,7 @@ public async Task GetFile_WhenCalledAndFileIsNotFound_ItShouldReturnNullAfterRet result.Should().BeNull(); _mockClient.Verify( - m => m.GetFileAsync( + static m => m.GetFileAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -561,7 +532,7 @@ public async Task GetFile_WhenCalledAndExceptionIsThrown_ItShouldReturnNull() { _mockClient .Setup( - m => m.GetFileAsync( + static m => m.GetFileAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -583,7 +554,7 @@ public async Task GetFile_WhenCalledAndExceptionIsThrown_ItShouldReturnNull() result.Should().BeNull(); _mockClient.Verify( - m => m.GetFileAsync( + static m => m.GetFileAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -597,7 +568,7 @@ public async Task GetFile_WhenCalledAndHttpRequestOrTaskCanceledExceptionIsThrow { _mockClient .SetupSequence( - m => m.GetFileAsync( + static m => m.GetFileAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -621,7 +592,7 @@ public async Task GetFile_WhenCalledAndHttpRequestOrTaskCanceledExceptionIsThrow result.Should().BeNull(); _mockClient.Verify( - m => m.GetFileAsync( + static m => m.GetFileAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -643,7 +614,7 @@ Field field ); _mockClient - .Setup(m => m.GetFieldAsync(It.IsAny())) + .Setup(static m => m.GetFieldAsync(It.IsAny())) .ReturnsAsync(apiResponse); var result = await _onspringService.GetField( @@ -654,17 +625,12 @@ Field field result.Should().NotBeNull(); result.Should().BeOfType(); - if (result is null) - { - return; - } - - result.Id.Should().Be(field.Id); + result!.Id.Should().Be(field.Id); result.Name.Should().Be(field.Name); result.Type.Should().Be(field.Type); _mockClient.Verify( - m => m.GetFieldAsync( + static m => m.GetFieldAsync( It.IsAny() ), Times.Exactly(1) @@ -680,7 +646,7 @@ public async Task GetField_WhenCalledAndFieldIsNotFound_ItShouldReturnNull() ); _mockClient - .Setup(m => m.GetFieldAsync(It.IsAny())) + .Setup(static m => m.GetFieldAsync(It.IsAny())) .ReturnsAsync(apiResponse); var result = await _onspringService.GetField( @@ -691,7 +657,7 @@ public async Task GetField_WhenCalledAndFieldIsNotFound_ItShouldReturnNull() result.Should().BeNull(); _mockClient.Verify( - m => m.GetFieldAsync( + static m => m.GetFieldAsync( It.IsAny() ), Times.Exactly(1) @@ -702,7 +668,7 @@ public async Task GetField_WhenCalledAndFieldIsNotFound_ItShouldReturnNull() public async Task GetField_WhenCalledAndExceptionIsThrown_ItShouldReturnNull() { _mockClient - .Setup(m => m.GetFieldAsync(It.IsAny())) + .Setup(static m => m.GetFieldAsync(It.IsAny())) .Throws(new Exception()); var result = await _onspringService.GetField( @@ -713,7 +679,7 @@ public async Task GetField_WhenCalledAndExceptionIsThrown_ItShouldReturnNull() result.Should().BeNull(); _mockClient.Verify( - m => m.GetFieldAsync( + static m => m.GetFieldAsync( It.IsAny() ), Times.Exactly(1) @@ -724,7 +690,7 @@ public async Task GetField_WhenCalledAndExceptionIsThrown_ItShouldReturnNull() public async Task GetField_WhenCalledAndHttpRequestOrTaskCanceledExceptionIsThrown_ItShouldReturnNullAfterAttemptingThreeTimes() { _mockClient - .SetupSequence(m => m.GetFieldAsync(It.IsAny())) + .SetupSequence(static m => m.GetFieldAsync(It.IsAny())) .Throws(new HttpRequestException()) .Throws(new TaskCanceledException()) .Throws(new TaskCanceledException()); @@ -737,7 +703,7 @@ public async Task GetField_WhenCalledAndHttpRequestOrTaskCanceledExceptionIsThro result.Should().BeNull(); _mockClient.Verify( - m => m.GetFieldAsync( + static m => m.GetFieldAsync( It.IsAny() ), Times.Exactly(3) @@ -753,7 +719,7 @@ public async Task GetField_WhenCalledAndRequestIsUnsuccessful_ItShouldReturnNull ); _mockClient - .Setup(m => m.GetFieldAsync(It.IsAny())) + .Setup(static m => m.GetFieldAsync(It.IsAny())) .ReturnsAsync(apiResponse); var result = await _onspringService.GetField( @@ -764,7 +730,7 @@ public async Task GetField_WhenCalledAndRequestIsUnsuccessful_ItShouldReturnNull result.Should().BeNull(); _mockClient.Verify( - m => m.GetFieldAsync( + static m => m.GetFieldAsync( It.IsAny() ), Times.Exactly(3) @@ -785,7 +751,7 @@ GetFileInfoResponse fileInfo _mockClient .Setup( - m => m.GetFileInfoAsync( + static m => m.GetFileInfoAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -809,7 +775,7 @@ GetFileInfoResponse fileInfo result.Should().BeEquivalentTo(fileInfo); _mockClient.Verify( - m => m.GetFileInfoAsync( + static m => m.GetFileInfoAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -828,7 +794,7 @@ public async Task GetFileInfo_WhenCalledAndFileInfoIsNotFound_ItShouldReturnNull _mockClient .Setup( - m => m.GetFileInfoAsync( + static m => m.GetFileInfoAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -850,7 +816,7 @@ public async Task GetFileInfo_WhenCalledAndFileInfoIsNotFound_ItShouldReturnNull result.Should().BeNull(); _mockClient.Verify( - m => m.GetFileInfoAsync( + static m => m.GetFileInfoAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -864,7 +830,7 @@ public async Task GetFileInfo_WhenCalledAndExceptionIsThrown_ItShouldReturnNull( { _mockClient .Setup( - m => m.GetFileInfoAsync( + static m => m.GetFileInfoAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -886,7 +852,7 @@ public async Task GetFileInfo_WhenCalledAndExceptionIsThrown_ItShouldReturnNull( result.Should().BeNull(); _mockClient.Verify( - m => m.GetFileInfoAsync( + static m => m.GetFileInfoAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -900,7 +866,7 @@ public async Task GetFileInfo_WhenCalledAndHttpRequestOrTaskCanceledExceptionIsT { _mockClient .SetupSequence( - m => m.GetFileInfoAsync( + static m => m.GetFileInfoAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -924,7 +890,7 @@ public async Task GetFileInfo_WhenCalledAndHttpRequestOrTaskCanceledExceptionIsT result.Should().BeNull(); _mockClient.Verify( - m => m.GetFileInfoAsync( + static m => m.GetFileInfoAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -943,7 +909,7 @@ public async Task GetFileInfo_WhenCalledAndRequestIsUnsuccessful_ItShouldReturnN _mockClient .Setup( - m => m.GetFileInfoAsync( + static m => m.GetFileInfoAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -965,7 +931,7 @@ public async Task GetFileInfo_WhenCalledAndRequestIsUnsuccessful_ItShouldReturnN result.Should().BeNull(); _mockClient.Verify( - m => m.GetFileInfoAsync( + static m => m.GetFileInfoAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -991,7 +957,7 @@ GetPagedRecordsResponse recordsResponse _mockClient .Setup( - m => m.QueryRecordsAsync( + static m => m.QueryRecordsAsync( It.IsAny(), It.IsAny() ) @@ -1009,12 +975,7 @@ GetPagedRecordsResponse recordsResponse result.Should().NotBeNull(); result.Should().BeOfType(); - if (result is null) - { - return; - } - - result.TotalPages.Should().Be(recordsResponse.TotalPages); + result!.TotalPages.Should().Be(recordsResponse.TotalPages); result.TotalRecords.Should().Be(recordsResponse.TotalRecords); result.PageNumber.Should().Be(recordsResponse.PageNumber); result.Items.Should().HaveCount(recordsResponse.Items.Count); @@ -1022,7 +983,7 @@ GetPagedRecordsResponse recordsResponse result.Items.Should().BeEquivalentTo(recordsResponse.Items); _mockClient.Verify( - m => m.QueryRecordsAsync( + static m => m.QueryRecordsAsync( It.IsAny(), It.IsAny() ), @@ -1040,7 +1001,7 @@ public async Task GetAPageOfRecordsByQuery_WhenCalledAndRequestIsUnsuccessful_It _mockClient .SetupSequence( - m => m.QueryRecordsAsync( + static m => m.QueryRecordsAsync( It.IsAny(), It.IsAny() ) @@ -1060,7 +1021,7 @@ public async Task GetAPageOfRecordsByQuery_WhenCalledAndRequestIsUnsuccessful_It result.Should().BeNull(); _mockClient.Verify( - m => m.QueryRecordsAsync( + static m => m.QueryRecordsAsync( It.IsAny(), It.IsAny() ), @@ -1086,7 +1047,7 @@ GetPagedRecordsResponse recordsResponse _mockClient .Setup( - m => m.QueryRecordsAsync( + static m => m.QueryRecordsAsync( It.IsAny(), It.IsAny() ) @@ -1104,19 +1065,14 @@ GetPagedRecordsResponse recordsResponse result.Should().NotBeNull(); result.Should().BeOfType(); - if (result is null) - { - return; - } - - result.TotalPages.Should().Be(totalPages); + result!.TotalPages.Should().Be(totalPages); result.TotalRecords.Should().Be(totalRecords); result.PageNumber.Should().Be(pageNumber); result.Items.Should().BeEmpty(); result.Items.Should().BeOfType>(); _mockClient.Verify( - m => m.QueryRecordsAsync( + static m => m.QueryRecordsAsync( It.IsAny(), It.IsAny() ), @@ -1129,7 +1085,7 @@ public async Task GetAPageOfRecordsByQuery_WhenCalledAndExceptionIsThrown_ItShou { _mockClient .Setup( - m => m.QueryRecordsAsync( + static m => m.QueryRecordsAsync( It.IsAny(), It.IsAny() ) @@ -1147,7 +1103,7 @@ public async Task GetAPageOfRecordsByQuery_WhenCalledAndExceptionIsThrown_ItShou result.Should().BeNull(); _mockClient.Verify( - m => m.QueryRecordsAsync( + static m => m.QueryRecordsAsync( It.IsAny(), It.IsAny() ), @@ -1160,7 +1116,7 @@ public async Task GetAPageOfRecordsByQuery_WhenCalledAndHttpRequestOrTaskExcepti { _mockClient .SetupSequence( - m => m.QueryRecordsAsync( + static m => m.QueryRecordsAsync( It.IsAny(), It.IsAny() ) @@ -1179,7 +1135,7 @@ public async Task GetAPageOfRecordsByQuery_WhenCalledAndHttpRequestOrTaskExcepti result.Should().BeNull(); _mockClient.Verify( - m => m.QueryRecordsAsync( + static m => m.QueryRecordsAsync( It.IsAny(), It.IsAny() ), @@ -1199,7 +1155,7 @@ public async Task GetReport_WhenCalledAndReportIsFound_ItShouldReturnReportData( _mockClient .Setup( - m => m.GetReportAsync( + static m => m.GetReportAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1217,7 +1173,7 @@ public async Task GetReport_WhenCalledAndReportIsFound_ItShouldReturnReportData( result.Should().BeEquivalentTo(reportData); _mockClient.Verify( - m => m.GetReportAsync( + static m => m.GetReportAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1236,7 +1192,7 @@ public async Task GetReport_WhenCalledAndRequestIsUnsuccessful_ItShouldReturnNul _mockClient .Setup( - m => m.GetReportAsync( + static m => m.GetReportAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1252,7 +1208,7 @@ public async Task GetReport_WhenCalledAndRequestIsUnsuccessful_ItShouldReturnNul result.Should().BeNull(); _mockClient.Verify( - m => m.GetReportAsync( + static m => m.GetReportAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1266,7 +1222,7 @@ public async Task GetReport_WhenCalledAndExceptionIsThrown_ItShouldReturnNull() { _mockClient .Setup( - m => m.GetReportAsync( + static m => m.GetReportAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1282,7 +1238,7 @@ public async Task GetReport_WhenCalledAndExceptionIsThrown_ItShouldReturnNull() result.Should().BeNull(); _mockClient.Verify( - m => m.GetReportAsync( + static m => m.GetReportAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1296,7 +1252,7 @@ public async Task GetReport_WhenCalledAndHttpRequestOrTaskExceptionIsThrown_ItSh { _mockClient .SetupSequence( - m => m.GetReportAsync( + static m => m.GetReportAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1313,7 +1269,7 @@ public async Task GetReport_WhenCalledAndHttpRequestOrTaskExceptionIsThrown_ItSh result.Should().BeNull(); _mockClient.Verify( - m => m.GetReportAsync( + static m => m.GetReportAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1334,7 +1290,7 @@ public async Task SaveFile_WhenCalledAndFileIsSaved_ItShouldReturnACreatedWithId _mockClient .Setup( - m => m.SaveFileAsync( + static m => m.SaveFileAsync( It.IsAny() ) ) @@ -1361,7 +1317,7 @@ public async Task SaveFile_WhenCalledAndFileIsSaved_ItShouldReturnACreatedWithId result.Should().BeEquivalentTo(createdWithIdResponse); _mockClient.Verify( - m => m.SaveFileAsync( + static m => m.SaveFileAsync( It.IsAny() ), Times.Exactly(1) @@ -1378,7 +1334,7 @@ public async Task SaveFile_WhenCalledAndRequestIsUnsuccessful_ItShouldReturnNull _mockClient .Setup( - m => m.SaveFileAsync( + static m => m.SaveFileAsync( It.IsAny() ) ) @@ -1403,7 +1359,7 @@ public async Task SaveFile_WhenCalledAndRequestIsUnsuccessful_ItShouldReturnNull result.Should().BeNull(); _mockClient.Verify( - m => m.SaveFileAsync( + static m => m.SaveFileAsync( It.IsAny() ), Times.Exactly(3) @@ -1415,7 +1371,7 @@ public async Task SaveFile_WhenCalledAndExceptionIsThrown_ItShouldReturnNull() { _mockClient .Setup( - m => m.SaveFileAsync( + static m => m.SaveFileAsync( It.IsAny() ) ) @@ -1440,7 +1396,7 @@ public async Task SaveFile_WhenCalledAndExceptionIsThrown_ItShouldReturnNull() result.Should().BeNull(); _mockClient.Verify( - m => m.SaveFileAsync( + static m => m.SaveFileAsync( It.IsAny() ), Times.Exactly(1) @@ -1452,7 +1408,7 @@ public async Task SaveFile_WhenCalledAndHttpRequestOrTaskExceptionIsThrown_ItSho { _mockClient .SetupSequence( - m => m.SaveFileAsync( + static m => m.SaveFileAsync( It.IsAny() ) ) @@ -1478,7 +1434,7 @@ public async Task SaveFile_WhenCalledAndHttpRequestOrTaskExceptionIsThrown_ItSho result.Should().BeNull(); _mockClient.Verify( - m => m.SaveFileAsync( + static m => m.SaveFileAsync( It.IsAny() ), Times.Exactly(3) @@ -1495,7 +1451,7 @@ public async Task TryDeleteFile_WhenCalledAndFileIsDeleted_ItShouldReturnTrue() _mockClient .Setup( - m => m.DeleteFileAsync( + static m => m.DeleteFileAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1517,7 +1473,7 @@ public async Task TryDeleteFile_WhenCalledAndFileIsDeleted_ItShouldReturnTrue() result.Should().BeTrue(); _mockClient.Verify( - m => m.DeleteFileAsync( + static m => m.DeleteFileAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1536,7 +1492,7 @@ public async Task TryDeleteFile_WhenCalledAndRequestIsUnsuccessful_ItShouldRetur _mockClient .Setup( - m => m.DeleteFileAsync( + static m => m.DeleteFileAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1558,7 +1514,7 @@ public async Task TryDeleteFile_WhenCalledAndRequestIsUnsuccessful_ItShouldRetur result.Should().BeFalse(); _mockClient.Verify( - m => m.DeleteFileAsync( + static m => m.DeleteFileAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1572,7 +1528,7 @@ public async Task TryDeleteFile_WhenCalledAndExceptionIsThrown_ItShouldReturnFal { _mockClient .Setup( - m => m.DeleteFileAsync( + static m => m.DeleteFileAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1594,7 +1550,7 @@ public async Task TryDeleteFile_WhenCalledAndExceptionIsThrown_ItShouldReturnFal result.Should().BeFalse(); _mockClient.Verify( - m => m.DeleteFileAsync( + static m => m.DeleteFileAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1608,7 +1564,7 @@ public async Task TryDeleteFile_WhenCalledAndHttpRequestOrTaskExceptionIsThrown_ { _mockClient .SetupSequence( - m => m.DeleteFileAsync( + static m => m.DeleteFileAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1632,7 +1588,7 @@ public async Task TryDeleteFile_WhenCalledAndHttpRequestOrTaskExceptionIsThrown_ result.Should().BeFalse(); _mockClient.Verify( - m => m.DeleteFileAsync( + static m => m.DeleteFileAsync( It.IsAny(), It.IsAny(), It.IsAny() @@ -1653,7 +1609,7 @@ public async Task UpdateRecord_WhenCalledAndRecordIsUpdated_ItShouldReturnACreat _mockClient .Setup( - m => m.SaveRecordAsync( + static m => m.SaveRecordAsync( It.IsAny() ) ) @@ -1674,7 +1630,7 @@ public async Task UpdateRecord_WhenCalledAndRecordIsUpdated_ItShouldReturnACreat result.Should().BeEquivalentTo(saveRecordResponse); _mockClient.Verify( - m => m.SaveRecordAsync( + static m => m.SaveRecordAsync( It.IsAny() ), Times.Exactly(1) @@ -1691,7 +1647,7 @@ public async Task UpdateRecord_WhenCalledAndRequestIsUnsuccessful_ItShouldReturn _mockClient .Setup( - m => m.SaveRecordAsync( + static m => m.SaveRecordAsync( It.IsAny() ) ) @@ -1712,7 +1668,7 @@ public async Task UpdateRecord_WhenCalledAndRequestIsUnsuccessful_ItShouldReturn result.Should().BeNull(); _mockClient.Verify( - m => m.SaveRecordAsync( + static m => m.SaveRecordAsync( It.IsAny() ), Times.Exactly(3) @@ -1724,7 +1680,7 @@ public async Task UpdateRecord_WhenCalledAndExceptionIsThrown_ItShouldReturnNull { _mockClient .Setup( - m => m.SaveRecordAsync( + static m => m.SaveRecordAsync( It.IsAny() ) ) @@ -1745,7 +1701,7 @@ public async Task UpdateRecord_WhenCalledAndExceptionIsThrown_ItShouldReturnNull result.Should().BeNull(); _mockClient.Verify( - m => m.SaveRecordAsync( + static m => m.SaveRecordAsync( It.IsAny() ), Times.Exactly(1) @@ -1757,7 +1713,7 @@ public async Task UpdateRecord_WhenCalledAndHttpRequestOrTaskExceptionIsThrown_I { _mockClient .SetupSequence( - m => m.SaveRecordAsync( + static m => m.SaveRecordAsync( It.IsAny() ) ) @@ -1780,7 +1736,7 @@ public async Task UpdateRecord_WhenCalledAndHttpRequestOrTaskExceptionIsThrown_I result.Should().BeNull(); _mockClient.Verify( - m => m.SaveRecordAsync( + static m => m.SaveRecordAsync( It.IsAny() ), Times.Exactly(3) @@ -1833,7 +1789,7 @@ public async Task GetApps_WhenCalledAndInitialRequestFails_ItShouldReturnEmptyLi var apiResponse = ApiResponseFactory.GetApiResponse(HttpStatusCode.BadRequest, "Bad Request"); _mockClient - .Setup(m => m.GetAppsAsync(It.IsAny())) + .Setup(static m => m.GetAppsAsync(It.IsAny())) .ReturnsAsync(apiResponse); var result = await _onspringService.GetApps(It.IsAny()); @@ -1874,7 +1830,7 @@ public async Task GetApps_WhenCalledAndInitialRequestAndRemainingRequestSucceeds }; _mockClient - .SetupSequence(m => m.GetAppsAsync(It.IsAny())) + .SetupSequence(static m => m.GetAppsAsync(It.IsAny())) .ReturnsAsync(firstPage) .ReturnsAsync(secondPage); @@ -1882,7 +1838,7 @@ public async Task GetApps_WhenCalledAndInitialRequestAndRemainingRequestSucceeds result.Should().HaveCount(2); - _mockClient.Verify(m => m.GetAppsAsync( + _mockClient.Verify(static m => m.GetAppsAsync( It.IsAny()), Times.Exactly(2) ); @@ -1911,7 +1867,7 @@ public async Task GetApps_WhenCalledAndInitialRequestSucceedsAndRemainingRequest var secondPage = ApiResponseFactory.GetApiResponse(HttpStatusCode.BadRequest, "Bad Request"); _mockClient - .SetupSequence(m => m.GetAppsAsync(It.IsAny())) + .SetupSequence(static m => m.GetAppsAsync(It.IsAny())) .ReturnsAsync(firstPage) .ReturnsAsync(secondPage!); @@ -1919,7 +1875,7 @@ public async Task GetApps_WhenCalledAndInitialRequestSucceedsAndRemainingRequest result.Should().HaveCount(1); - _mockClient.Verify(m => m.GetAppsAsync( + _mockClient.Verify(static m => m.GetAppsAsync( It.IsAny()), Times.Exactly(2) ); @@ -1929,11 +1885,128 @@ public async Task GetApps_WhenCalledAndInitialRequestSucceedsAndRemainingRequest public async Task GetApps_WhenCalledAndExceptionIsThrown_ItShouldReturnEmptyList() { _mockClient - .Setup(m => m.GetAppsAsync(It.IsAny())) + .Setup(static m => m.GetAppsAsync(It.IsAny())) .ThrowsAsync(new Exception()); var result = await _onspringService.GetApps(It.IsAny()); result.Should().BeEmpty(); } + + [Fact] + public async Task GetOrAddListValueByName_WhenCalledAndFieldRequestFails_ItShouldReturnNull() + { + _mockClient + .Setup(static m => m.GetFieldAsync(It.IsAny())) + .ReturnsAsync(ApiResponseFactory.GetApiResponse(HttpStatusCode.BadRequest, "Bad Request")); + + var result = await _onspringService.GetOrAddListValueByName("apikey", 1, new()); + + result.Should().BeNull(); + } + + [Fact] + public async Task GetOrAddListValueByName_WhenExceptionIsThrown_ItShouldReturnNull() + { + _mockClient + .Setup(static m => m.GetFieldAsync(It.IsAny())) + .ThrowsAsync(new Exception()); + + var result = await _onspringService.GetOrAddListValueByName("apikey", 1, new()); + + result.Should().BeNull(); + } + + [Fact] + public async Task GetOrAddListValueByName_WhenCalledAndFieldIsNotListField_ItShouldReturnNull() + { + _mockClient + .Setup(static m => m.GetFieldAsync(It.IsAny())) + .ReturnsAsync(ApiResponseFactory.GetApiResponse(HttpStatusCode.OK, "OK", new Field + { + Type = FieldType.Text + })); + + var result = await _onspringService.GetOrAddListValueByName("apikey", 1, new()); + + result.Should().BeNull(); + } + + [Fact] + public async Task GetOrAddListValueByName_WhenCalledAndValueExists_ItShouldReturnExistingValueId() + { + var listValueId = Guid.NewGuid(); + var listValueName = "existing_value"; + + _mockClient + .Setup(static m => m.GetFieldAsync(It.IsAny())) + .ReturnsAsync(ApiResponseFactory.GetApiResponse(HttpStatusCode.OK, "OK", new ListField + { + Type = FieldType.List, + Values = [ + new() + { + Id = listValueId, + Name = listValueName, + } + ] + })); + + var result = await _onspringService.GetOrAddListValueByName("apikey", 1, new() { Name = listValueName }); + + result.Should().Be(listValueId); + } + + [Fact] + public async Task GetOrAddListValueByName_WhenCalledAndValueDoesNotExistButSavingNewValueFails_ItShouldReturnNull() + { + _mockClient + .Setup(static m => m.GetFieldAsync(It.IsAny())) + .ReturnsAsync(ApiResponseFactory.GetApiResponse(HttpStatusCode.OK, "OK", new ListField + { + Type = FieldType.List, + Values = [] + })); + + _mockClient + .Setup(static m => m.SaveListItemAsync(It.IsAny())) + .ReturnsAsync(ApiResponseFactory.GetApiResponse(HttpStatusCode.BadRequest, "Bad Request")); + + var result = await _onspringService.GetOrAddListValueByName("apikey", 1, new() { Name = "new_value" }); + + result.Should().BeNull(); + + _mockClient.Verify( + static m => m.SaveListItemAsync(It.IsAny()), + Times.Exactly(1) + ); + } + + [Fact] + public async Task GetOrAddListValueByName_WhenCalledAndValueDoesNotExistAndSavingNewValueSucceeds_ItShouldReturnNewValueId() + { + var newValueId = Guid.NewGuid(); + var newValueName = "new_value"; + + _mockClient + .Setup(static m => m.GetFieldAsync(It.IsAny())) + .ReturnsAsync(ApiResponseFactory.GetApiResponse(HttpStatusCode.OK, "OK", new ListField + { + Type = FieldType.List, + Values = [] + })); + + _mockClient + .Setup(static m => m.SaveListItemAsync(It.IsAny())) + .ReturnsAsync(ApiResponseFactory.GetApiResponse(HttpStatusCode.OK, "OK", new SaveListItemResponse(newValueId))); + + var result = await _onspringService.GetOrAddListValueByName("apikey", 1, new() { Name = newValueName }); + + result.Should().Be(newValueId); + + _mockClient.Verify( + static m => m.SaveListItemAsync(It.IsAny()), + Times.Exactly(1) + ); + } } \ No newline at end of file From f6cb72d6993ccdcfebde64d4ea5030020b1e29e1 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 10 Jun 2025 15:22:24 -0500 Subject: [PATCH 11/14] tests: stub out tests for update year values method --- .../Processors/RecordsProcessorTests.cs | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/tests/OnspringCLI.Tests/UnitTests/Processors/RecordsProcessorTests.cs b/tests/OnspringCLI.Tests/UnitTests/Processors/RecordsProcessorTests.cs index 1862962..a84c627 100644 --- a/tests/OnspringCLI.Tests/UnitTests/Processors/RecordsProcessorTests.cs +++ b/tests/OnspringCLI.Tests/UnitTests/Processors/RecordsProcessorTests.cs @@ -355,4 +355,135 @@ public async Task GetFieldsForApp_WhenCalled_ItShouldReturnFields() result.Should().BeEquivalentTo(fields); } + + [Fact] + public async Task GetRecords_WhenCalledAndPageIsNull_ItShouldReturnEmptyList() + { + _onspringServiceMock + .Setup(static x => x.GetAPageOfRecords( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny() + )) + .ReturnsAsync(null as GetPagedRecordsResponse); + + var result = new List(); + + await foreach (var record in _processor.GetRecords(new(), [])) + { + result.Add(record); + } + + result.Should().BeEmpty(); + } + + [Fact] + public async Task GetRecords_WhenCalledAndPageHasNoRecords_ItShouldReturnEmptyList() + { + var page = new GetPagedRecordsResponse + { + TotalRecords = 0, + PageNumber = 1, + TotalPages = 1, + Items = [] + }; + + _onspringServiceMock + .Setup(static x => x.GetAPageOfRecords( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny() + )) + .ReturnsAsync(page); + + var result = new List(); + + await foreach (var record in _processor.GetRecords(new(), [])) + { + result.Add(record); + } + + result.Should().BeEmpty(); + } + + [Fact] + public async Task GetRecords_WhenCalledAndPageHasRecords_ItShouldReturnRecords() + { + var page = new GetPagedRecordsResponse + { + TotalRecords = 1, + PageNumber = 1, + TotalPages = 1, + Items = [new() { RecordId = 1, FieldData = [] }] + }; + + _onspringServiceMock + .Setup(static x => x.GetAPageOfRecords( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny() + )) + .ReturnsAsync(page); + + var result = new List(); + + await foreach (var record in _processor.GetRecords(new(), [new()])) + { + result.Add(record); + } + + result.Should().HaveCount(1); + result.First().RecordId.Should().Be(1); + } + + [Fact] + public Task UpdateRecordYearValues_WhenCalledAndFieldHasNoValue_ItShouldNotUpdateRecord() + { + throw new NotImplementedException(); + } + + [Fact] + public Task UpdateRecordYearValues_WhenCalledAndFieldIsNotListOrDate_ItShouldNotUpdateRecord() + { + throw new NotImplementedException(); + } + + [Fact] + public Task UpdateRecordYearValues_WhenCalledAndFieldIsMultiSelectList_ItShouldNotUpdateRecord() + { + throw new NotImplementedException(); + } + + [Fact] + public Task UpdateRecordYearValues_WhenCalledAndFieldIsSingleSelectListButUnableToFindListValue_ItShouldNotUpdateRecord() + { + throw new NotImplementedException(); + } + + [Fact] + public Task UpdateRecordYearValues_WhenCalledAndFieldIsSingleSelectListButValueIsNotAYear_ItShouldNotUpdateRecord() + { + throw new NotImplementedException(); + } + + [Fact] + public Task UpdateRecordYearValues_WhenCalledAndFieldIsSingleSelectListButUnableToAddNewValue_ItShouldNotUpdateRecord() + { + throw new NotImplementedException(); + } + + [Fact] + public Task UpdateRecordYearValues_WhenCalledAndFieldIsSingleSelectListAndValueIsAYear_ItShouldUpdateRecord() + { + throw new NotImplementedException(); + } + + [Fact] + public Task UpdateRecordYearValues_WhenCalledAndFieldIsDate_ItShouldUpdateRecord() + { + throw new NotImplementedException(); + } } \ No newline at end of file From f5df22eebba2e52d28094fd6aaabbd2ecf7e9b18 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:49:14 -0500 Subject: [PATCH 12/14] tests: add tests for update record year values method --- .../Processors/RecordsProcessorTests.cs | 423 +++++++++++++++++- 1 file changed, 407 insertions(+), 16 deletions(-) diff --git a/tests/OnspringCLI.Tests/UnitTests/Processors/RecordsProcessorTests.cs b/tests/OnspringCLI.Tests/UnitTests/Processors/RecordsProcessorTests.cs index a84c627..f58c5b1 100644 --- a/tests/OnspringCLI.Tests/UnitTests/Processors/RecordsProcessorTests.cs +++ b/tests/OnspringCLI.Tests/UnitTests/Processors/RecordsProcessorTests.cs @@ -440,50 +440,441 @@ public async Task GetRecords_WhenCalledAndPageHasRecords_ItShouldReturnRecords() } [Fact] - public Task UpdateRecordYearValues_WhenCalledAndFieldHasNoValue_ItShouldNotUpdateRecord() + public async Task UpdateRecordYearValues_WhenCalledAndFieldHasNoValue_ItShouldNotUpdateRecord() { - throw new NotImplementedException(); + var fields = new List + { + new() + }; + + var record = new ResultRecord + { + RecordId = 1, + FieldData = [], + }; + + var result = await _processor.UpdateRecordYearValues("appName", record, fields, 1); + + result.Should().BeNull(); + + _onspringServiceMock.Verify( + static m => m.UpdateRecord(It.IsAny(), It.IsAny()), + Times.Never + ); } [Fact] - public Task UpdateRecordYearValues_WhenCalledAndFieldIsNotListOrDate_ItShouldNotUpdateRecord() + public async Task UpdateRecordYearValues_WhenCalledAndFieldIsNotListOrDate_ItShouldNotUpdateRecord() { - throw new NotImplementedException(); + var textField = new Field + { + Id = 1, + Type = FieldType.Text + }; + + var fields = new List + { + textField, + }; + + var record = new ResultRecord + { + RecordId = 1, + FieldData = [ + new StringFieldValue { FieldId = textField.Id, Value = "Not a year" } + ], + }; + + var result = await _processor.UpdateRecordYearValues("appName", record, fields, 1); + + result.Should().BeNull(); + + _onspringServiceMock.Verify( + static m => m.UpdateRecord(It.IsAny(), It.IsAny()), + Times.Never + ); } [Fact] - public Task UpdateRecordYearValues_WhenCalledAndFieldIsMultiSelectList_ItShouldNotUpdateRecord() + public async Task UpdateRecordYearValues_WhenCalledAndFieldIsMultiSelectList_ItShouldNotUpdateRecord() { - throw new NotImplementedException(); + var multiSelectField = new ListField + { + Id = 1, + Type = FieldType.List, + Multiplicity = Multiplicity.MultiSelect + }; + + var fields = new List + { + multiSelectField, + }; + + var record = new ResultRecord + { + RecordId = 1, + FieldData = [ + new GuidListFieldValue { FieldId = multiSelectField.Id, Value = [] } + ], + }; + + var result = await _processor.UpdateRecordYearValues("appName", record, fields, 1); + + result.Should().BeNull(); + + _onspringServiceMock.Verify( + static m => m.UpdateRecord(It.IsAny(), It.IsAny()), + Times.Never + ); } [Fact] - public Task UpdateRecordYearValues_WhenCalledAndFieldIsSingleSelectListButUnableToFindListValue_ItShouldNotUpdateRecord() + public async Task UpdateRecordYearValues_WhenCalledAndFieldIsSingleSelectListButValueIsNull_ItShouldNotUpdateRecord() { - throw new NotImplementedException(); + var singleSelectField = new ListField + { + Id = 1, + Type = FieldType.List, + Multiplicity = Multiplicity.SingleSelect + }; + + var fields = new List + { + singleSelectField, + }; + + var record = new ResultRecord + { + RecordId = 1, + FieldData = [ + new GuidFieldValue { FieldId = singleSelectField.Id, Value = null } + ], + }; + + var result = await _processor.UpdateRecordYearValues("appName", record, fields, 1); + + result.Should().BeNull(); + + _onspringServiceMock.Verify( + static m => m.UpdateRecord(It.IsAny(), It.IsAny()), + Times.Never + ); } [Fact] - public Task UpdateRecordYearValues_WhenCalledAndFieldIsSingleSelectListButValueIsNotAYear_ItShouldNotUpdateRecord() + public async Task UpdateRecordYearValues_WhenCalledAndFieldIsSingleSelectListButUnableToFindListValue_ItShouldNotUpdateRecord() { - throw new NotImplementedException(); + var singleSelectField = new ListField + { + Id = 1, + Type = FieldType.List, + Multiplicity = Multiplicity.SingleSelect, + Values = [] + }; + + var fields = new List + { + singleSelectField, + }; + + var record = new ResultRecord + { + RecordId = 1, + FieldData = [ + new GuidFieldValue { FieldId = singleSelectField.Id, Value = Guid.NewGuid() } + ], + }; + + var result = await _processor.UpdateRecordYearValues("appName", record, fields, 1); + + result.Should().BeNull(); + + _onspringServiceMock.Verify( + static m => m.UpdateRecord(It.IsAny(), It.IsAny()), + Times.Never + ); } [Fact] - public Task UpdateRecordYearValues_WhenCalledAndFieldIsSingleSelectListButUnableToAddNewValue_ItShouldNotUpdateRecord() + public async Task UpdateRecordYearValues_WhenCalledAndFieldIsSingleSelectListButValueIsNotAYear_ItShouldNotUpdateRecord() { - throw new NotImplementedException(); + var listValue = new ListValue + { + Id = Guid.NewGuid(), + Name = "Not a year", + }; + + var singleSelectField = new ListField + { + Id = 1, + Type = FieldType.List, + Multiplicity = Multiplicity.SingleSelect, + Values = [listValue] + }; + + var fields = new List + { + singleSelectField, + }; + + var record = new ResultRecord + { + RecordId = 1, + FieldData = [ + new GuidFieldValue { FieldId = singleSelectField.Id, Value = listValue.Id } + ], + }; + + var result = await _processor.UpdateRecordYearValues("appName", record, fields, 1); + + result.Should().BeNull(); + + _onspringServiceMock.Verify( + static m => m.UpdateRecord(It.IsAny(), It.IsAny()), + Times.Never + ); } [Fact] - public Task UpdateRecordYearValues_WhenCalledAndFieldIsSingleSelectListAndValueIsAYear_ItShouldUpdateRecord() + public async Task UpdateRecordYearValues_WhenCalledAndFieldIsSingleSelectListButUnableToAddNewValue_ItShouldNotUpdateRecord() { - throw new NotImplementedException(); + var listValue = new ListValue + { + Id = Guid.NewGuid(), + Name = "2023", + }; + + var singleSelectField = new ListField + { + Id = 1, + Type = FieldType.List, + Multiplicity = Multiplicity.SingleSelect, + Values = [listValue] + }; + + var fields = new List + { + singleSelectField, + }; + + var record = new ResultRecord + { + RecordId = 1, + FieldData = [ + new GuidFieldValue { FieldId = singleSelectField.Id, Value = listValue.Id } + ], + }; + + _onspringServiceMock + .Setup(static x => x.GetOrAddListValueByName(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(null as Guid?); + + var result = await _processor.UpdateRecordYearValues("appName", record, fields, 1); + + result.Should().BeNull(); + + _onspringServiceMock.Verify( + static m => m.UpdateRecord(It.IsAny(), It.IsAny()), + Times.Never + ); } [Fact] - public Task UpdateRecordYearValues_WhenCalledAndFieldIsDate_ItShouldUpdateRecord() + public async Task UpdateRecordYearValues_WhenCalledAndFieldIsSingleSelectListAndValueIsAYearButUpdatingRecordFailsItShouldReturnNull() { - throw new NotImplementedException(); + var year = 2023; + var numberOfYears = 1; + var expectedYear = year + numberOfYears; + + var listValue = new ListValue + { + Id = Guid.NewGuid(), + Name = year.ToString(CultureInfo.InvariantCulture), + }; + + var singleSelectField = new ListField + { + Id = 1, + Type = FieldType.List, + Multiplicity = Multiplicity.SingleSelect, + Values = [listValue] + }; + + var fields = new List + { + singleSelectField, + }; + + var record = new ResultRecord + { + RecordId = 1, + FieldData = [ + new GuidFieldValue { FieldId = singleSelectField.Id, Value = listValue.Id } + ], + }; + + var newListValueGuid = Guid.NewGuid(); + + _onspringServiceMock + .Setup(x => x.GetOrAddListValueByName( + It.IsAny(), + It.IsAny(), + It.Is( + x => x.Name == expectedYear.ToString(CultureInfo.InvariantCulture) && + x.NumericValue == expectedYear + ) + )) + .ReturnsAsync(newListValueGuid); + + _onspringServiceMock + .Setup(static x => x.UpdateRecord(It.IsAny(), It.IsAny())) + .ReturnsAsync(null as CreatedWithIdResponse); + + var result = await _processor.UpdateRecordYearValues("appName", record, fields, numberOfYears); + + result.Should().BeNull(); + + _onspringServiceMock.Verify( + static m => m.UpdateRecord(It.IsAny(), It.IsAny()), + Times.Once + ); + } + + [Fact] + public async Task UpdateRecordYearValues_WhenCalledAndFieldIsSingleSelectListAndValueIsAYear_ItShouldUpdateRecord() + { + var year = 2023; + var numberOfYears = 1; + var expectedYear = year + numberOfYears; + + var listValue = new ListValue + { + Id = Guid.NewGuid(), + Name = year.ToString(CultureInfo.InvariantCulture), + }; + + var singleSelectField = new ListField + { + Id = 1, + Type = FieldType.List, + Multiplicity = Multiplicity.SingleSelect, + Values = [listValue] + }; + + var fields = new List + { + singleSelectField, + }; + + var record = new ResultRecord + { + RecordId = 1, + FieldData = [ + new GuidFieldValue { FieldId = singleSelectField.Id, Value = listValue.Id } + ], + }; + + var newListValueGuid = Guid.NewGuid(); + + _onspringServiceMock + .Setup(x => x.GetOrAddListValueByName( + It.IsAny(), + It.IsAny(), + It.Is(x => x.Name == expectedYear.ToString(CultureInfo.InvariantCulture) && + x.NumericValue == expectedYear + ) + )) + .ReturnsAsync(newListValueGuid); + + _onspringServiceMock + .Setup(static x => x.UpdateRecord(It.IsAny(), It.IsAny())) + .ReturnsAsync(new CreatedWithIdResponse(1)); + + var result = await _processor.UpdateRecordYearValues("appName", record, fields, 1); + + result!.FieldData.Should().HaveCount(1); + result.FieldData.First().As().Value.Should().Be(newListValueGuid); + + _onspringServiceMock.Verify( + m => m.UpdateRecord(It.IsAny(), result), + Times.Once + ); + } + + [Fact] + public async Task UpdateRecordYearValues_WhenCalledAndFieldIsDateButValueIsNull_ItShouldNotUpdateRecord() + { + var dateField = new Field + { + Id = 1, + Type = FieldType.Date, + }; + + var fields = new List + { + dateField, + }; + + var record = new ResultRecord + { + RecordId = 1, + FieldData = [ + new DateFieldValue { FieldId = dateField.Id, Value = null } + ], + }; + + var result = await _processor.UpdateRecordYearValues("appName", record, fields, 1); + + result.Should().BeNull(); + + _onspringServiceMock.Verify( + static m => m.UpdateRecord(It.IsAny(), It.IsAny()), + Times.Never + ); + } + + [Fact] + public async Task UpdateRecordYearValues_WhenCalledAndFieldIsDate_ItShouldUpdateRecord() + { + var year = 2023; + var numberOfYears = 1; + var expectedYear = year + numberOfYears; + + var dateField = new Field + { + Id = 1, + Type = FieldType.Date, + }; + + var fields = new List + { + dateField, + }; + + var record = new ResultRecord + { + RecordId = 1, + FieldData = [ + new DateFieldValue + { + FieldId = dateField.Id, + Value = new DateTime(year, 1, 1) + } + ], + }; + + _onspringServiceMock + .Setup(static x => x.UpdateRecord(It.IsAny(), It.IsAny())) + .ReturnsAsync(new CreatedWithIdResponse(1)); + + var result = await _processor.UpdateRecordYearValues("appName", record, fields, numberOfYears); + + result!.FieldData.Should().HaveCount(1); + result.FieldData.First().As().Value.Should().Be(new(expectedYear, 1, 1)); + + _onspringServiceMock.Verify( + m => m.UpdateRecord(It.IsAny(), result), + Times.Once + ); } } \ No newline at end of file From 7682461d31972d7473c52f9e54a27a282dbc15d4 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:59:37 -0500 Subject: [PATCH 13/14] docs: update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index a07f44e..b9c73a7 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,9 @@ You are also welcome to clone this repository and run the app using the [.NET 9] - **Records** - Commands for working with records in an Onspring instance. - **Find** - **References** - Locate all references to set of content records in one app from all other apps. See [References Find Records](https://github.com/StevanFreeborn/OnspringCLI/wiki/Records-Find-References) + - **Update** + - **Bulk** + - **Year** - Update the values of a list of date and/or list fields by a specified number of years. See [Records Update Bulk Year](https://github.com/StevanFreeborn/OnspringCLI/wiki/Records-Update-Bulk-Year) for more information. **Note:** The app will prompt you for any required information that is not provided via the command line. From 5865bbeb9dcb4b8d1adb88c3f4d56a9969f7deaf Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 11 Jun 2025 18:12:56 -0500 Subject: [PATCH 14/14] fix: remove duplicate logging and sanitize non-width zero space characters from field names --- src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs | 4 ++-- src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs index 53c2d8b..f19e9b2 100644 --- a/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs +++ b/src/OnspringCLI/Commands/Records/Update/Bulk/YearCommand.cs @@ -98,9 +98,9 @@ public async Task InvokeAsync(InvocationContext context) _logger.Warning("The following fields in app {App} could not be found: {Fields}.", app, string.Join(", ", fields)); } - foreach (var (app, fields) in fieldsNotFound) + foreach (var (app, fields) in invalidFieldsFound) { - _logger.Warning("The following fields in app {App} could not be found: {Fields}.", app, string.Join(", ", fields)); + _logger.Warning("The following fields in app {App} are not list or date fields: {Fields}.", app, string.Join(", ", fields)); } return 2; diff --git a/src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs b/src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs index 18e1a3f..83594db 100644 --- a/src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs +++ b/src/OnspringCLI/Factories/UpdateYearSettingsFactory.cs @@ -18,13 +18,15 @@ public async Task CreateAsync(FileInfo? file) await foreach (var record in csv.GetRecordsAsync()) { + var fieldName = record.FieldName.Replace("\u200b", string.Empty); + if (fieldsToUpdate.TryGetValue(record.AppName, out var value)) { - value.Add(record.FieldName); + value.Add(fieldName); continue; } - fieldsToUpdate.Add(record.AppName, [record.FieldName]); + fieldsToUpdate.Add(record.AppName, [fieldName]); } return new(fieldsToUpdate);