12 Commits

Author SHA1 Message Date
de5e051ec1 Update build package path to use Release folder
The `<DesktopBuildPackageLocation>` property in `IISProfile.pubxml` was updated to change the build package location from the `PreRelease` folder to the `Release` folder. This ensures that the build output is now directed to the appropriate release directory.
2026-08-04 15:56:51 +02:00
7bdd3906bd Update project versions to stable 1.0.0 release
Downgraded versioning information in `ReC.API.csproj` and
`ReC.Client.csproj` from beta versions to a stable `1.0.0`
release. Updated `<Version>`, `<AssemblyVersion>`,
`<FileVersion>`, and `<InformationalVersion>` fields
accordingly:

- `ReC.API.csproj`: Changed from `2.4.0-beta` to `1.0.0`.
- `ReC.Client.csproj`: Changed from `2.0.0-beta` to `1.0.0`.

These changes mark the transition from beta to stable
release versions for both projects.
2026-08-04 15:56:40 +02:00
49e7f7a45b Refactor RecActionApiTests for alias consistency
Updated `using` directives in `RecActionApiTests.cs` to alias `BatchRecActionViewResponse` as `ClientBatchResponse` for consistent usage. Replaced variable type `BatchRecActionViewResponse?` with `ClientBatchResponse?` in two instances. No functional changes were made to the `Assert.Pass` statements or the `InvokeAsync` method call.
2026-08-04 15:29:01 +02:00
713e4f2a02 Add tests for RecActions.InvokeAsync method
Enhanced test coverage for the `InvokeAsync` method in the
`RecActions` client by adding three new test cases:

1. `InvokeAsync_returns_BatchRecActionViewResponse_on_success`:
   - Verifies successful invocation with valid data.
   - Includes checks for `TotalActionCount` and `ActionExceptionCount`.

2. `InvokeAsync_with_batchId_string_returns_BatchRecActionViewResponse`:
   - Tests invocation with a `batchId` string argument.
   - Handles scenarios where test data is unavailable.

3. `InvokeAsync_with_batchId_string_and_unknown_profile_throws_ReCApiException`:
   - Confirms that an unknown profile ID throws a `ReCApiException`.
   - Validates exception details like HTTP method and request URI.

These changes improve test robustness by covering success, edge,
and error scenarios for the `InvokeAsync` method.
2026-08-04 14:12:01 +02:00
99bdf77ead Add BatchRecActionViewResponse and update InvokeAsync
Introduce the `BatchRecActionViewResponse` class to represent the
result of batch RecAction invocations, including total actions
processed and exceptions encountered.

Update `InvokeAsync` methods in `RecActionApi` to return a
`BatchRecActionViewResponse` instead of `void`. Modify the
implementation to deserialize API responses into this new class.

Add XML documentation for the updated return type and use
conditional compilation to handle nullable reference types
for .NET Framework and other target frameworks.
2026-08-04 14:05:50 +02:00
c729cb33e3 Refactor BatchRecActionViewResponse to a record type
Refactored `BatchRecActionViewResponse` from a mutable class to an immutable record type with a constructor for `TotalActionCount` and a new `ActionExceptionCount` property. Moved the record to its own file under the `ReC.Application.Common.Dto` namespace.

Updated `InvokeBatchRecActionViewsCommand` and `InvokeRecActionViewsCommandHandler` to use the new record type. Removed `SuccessCount` and `FailureCount` properties, replacing them with `ActionExceptionCount`.

Performed minor cleanup, including removing the old class definition and updating namespaces.
2026-08-04 13:55:08 +02:00
cad7ca16cb Certainly! Please provide the list of code changes so I can help craft a concise and comprehensive commit message for you. 2026-08-03 17:12:49 +02:00
39d1292cc9 refactor: simplify test code in ResultQueryTests
- Remove unnecessary try-catch block in ShouldRead_Invalid_RecActionView test
- Code now directly awaits the query execution without exception handling
- Improves test readability and clarity
2026-08-03 17:11:36 +02:00
8e0ee6fbfa feat: add batch action execution response tracking
- Introduce BatchRecActionViewResponse to track execution metrics
- Add SuccessCount and FailureCount properties to monitor execution results
- Change InvokeBatchRecActionViewsCommand to return response data
- Update RecActionController to return Ok with response instead of Accepted
- Improve error handling to distinguish RecActionException from unexpected errors
- Consolidate exception handling logic for ErrorAction.Continue scenarios
2026-08-03 17:11:25 +02:00
003f4c60f6 refactor: migrate logging infrastructure from NLog to Serilog
- Replace NLog packages with Serilog.AspNetCore and related dependencies
- Add Serilog.UI with SQLite provider for log visualization
- Implement environment-based logging configuration:
  * Development: Console output with simplified template
  * Production: File-based logging with separate files per level
- Add SQLite sink for persistent log storage and web UI access
- Configure rolling file policies with configurable retention
- Update appsettings.Logging.json to use Serilog configuration format
- Enable Serilog self-diagnostics for troubleshooting
2026-08-03 17:11:13 +02:00
cb12cfcbc2 Refactor query handlers to remove NotFoundException
Refactored query handlers (`ReadProfileViewQueryHandler`,
`ReadRecActionViewQueryHandler`, and `ReadResultViewQueryHandler`)
to return empty results instead of throwing `NotFoundException`
when no data is found. Simplified the logic by removing null
or empty result checks and directly returning mapped results.

Updated corresponding test cases to align with the new behavior:
- Removed `try-catch` blocks for `NotFoundException`.
- Adjusted assertions to handle empty results.
- Removed the test case for `ReadRecActionViewQuery` that
  expected `NotFoundException`.

This change reflects a design shift to let the calling code
handle empty results instead of relying on exceptions.
2026-08-03 16:29:35 +02:00
4380f17bdb Update build package location for pre-release builds
The `<DesktopBuildPackageLocation>` property in the `IISProfile.pubxml` file was updated to use the `PreRelease` folder instead of the `API` folder. This change reflects a new deployment strategy for pre-release builds.
2026-08-03 16:21:21 +02:00
18 changed files with 251 additions and 164 deletions

View File

@@ -21,12 +21,11 @@ public class RecActionController(IMediator mediator) : ControllerBase
[ProducesResponseType(StatusCodes.Status202Accepted)]
public async Task<IActionResult> Invoke([FromRoute] long profileId, [FromBody] InvokeReferences references, CancellationToken cancel = default)
{
await mediator.Send(new InvokeBatchRecActionViewsCommand
return Ok(await mediator.Send(new InvokeBatchRecActionViewsCommand
{
ProfileId = profileId,
References = references
}, cancel);
return Accepted();
}, cancel));
}
#region CRUD

View File

@@ -1,27 +1,96 @@
using Microsoft.AspNetCore.Rewrite;
using Microsoft.EntityFrameworkCore;
using NLog;
using NLog.Web;
using ReC.API.Middleware;
using ReC.Application;
using ReC.Infrastructure;
using Serilog;
using Serilog.Ui.Core.Extensions;
using Serilog.Ui.SqliteDataProvider.Extensions;
using Serilog.Ui.Web.Extensions;
using System.Reflection;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized!");
// Enable Serilog self-diagnostics
Serilog.Debugging.SelfLog.Enable(msg => Console.WriteLine($"[SERILOG] {msg}"));
// Build temporary configuration to read log directory from appsettings.Logging.json
var tempConfig = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile("appsettings.Logging.json", optional: false)
.AddEnvironmentVariables()
.Build();
var isDevelopment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development";
var logDirectory = tempConfig.GetValue<string>("Serilog:LogDirectory")
?? Path.Combine(AppContext.BaseDirectory, "logs");
var retainedFileCountLimit = tempConfig.GetValue("Serilog:RetainedFileCountLimit", 30);
Directory.CreateDirectory(logDirectory);
var sqliteDbPath = Path.Combine(logDirectory, "logs.db");
var logFilePathTemplate = Path.Combine(logDirectory, ".Rec.API-.log");
Console.WriteLine($"[INFO] Log Directory: {logDirectory}");
// Configure Serilog based on environment:
// Development : Console + SQLite (+ Web UI)
// Production : File (per-level) + SQLite (+ Web UI)
var loggerConfig = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft.AspNetCore", Serilog.Events.LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", Serilog.Events.LogEventLevel.Warning)
.Enrich.FromLogContext();
if (isDevelopment)
{
loggerConfig.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}");
}
else
{
loggerConfig
.WriteTo.File(
Path.Combine(logDirectory, ".Rec.API-Info.log"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: retainedFileCountLimit,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information,
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.WriteTo.File(
Path.Combine(logDirectory, ".Rec.API-Warning.log"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: retainedFileCountLimit,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Warning,
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.WriteTo.File(
Path.Combine(logDirectory, ".Rec.API-Error.log"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: retainedFileCountLimit,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Error,
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.WriteTo.File(
Path.Combine(logDirectory, ".Rec.API-Critical.log"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: retainedFileCountLimit,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Fatal,
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}");
}
// SQLite sink is always active (used by Serilog Web UI)
loggerConfig.WriteTo.SQLite(sqliteDbPath, storeTimestampInUtc: true);
Log.Logger = loggerConfig.CreateLogger();
Log.Information("Logging initialized!");
try
{
Log.Information("Starting ReC.API application");
var builder = WebApplication.CreateBuilder(args);
builder.Logging.SetMinimumLevel(LogLevel.Trace);
if (!builder.Environment.IsDevelopment())
{
builder.Logging.ClearProviders();
builder.Host.UseNLog();
}
// Use Serilog for logging
builder.Host.UseSerilog();
var config = builder.Configuration;
@@ -71,6 +140,16 @@ try
c.IncludeXmlComments(xmlPath);
});
// Serilog Web UI — SQLite provider, same path used by the SQLite sink above
builder.Services.AddSerilogUi(logUIOpt =>
{
logUIOpt.UseSqliteServer(dbOpt =>
{
dbOpt.WithConnectionString($"Data Source={sqliteDbPath}");
dbOpt.WithTable("Logs");
});
});
var app = builder.Build();
app.UseMiddleware<ExceptionHandlingMiddleware>();
@@ -89,14 +168,21 @@ try
app.UseAuthorization();
// Serilog Web UI is always active (both Development and Production)
app.UseSerilogUi();
app.MapControllers();
app.Run();
}
catch(Exception ex)
catch (Exception ex)
{
logger.Error(ex, "Stopped program because of exception");
Log.Fatal(ex, "Stopped program because of exception");
throw;
}
finally
{
Log.CloseAndFlush();
}
public partial class Program;

View File

@@ -9,7 +9,7 @@
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<ExcludeApp_Data>false</ExcludeApp_Data>
<ProjectGuid>420218ad-3c27-4003-9a84-36c92352f175</ProjectGuid>
<DesktopBuildPackageLocation>M:\App&amp;Service\0 DD - Smart UP\ReC\API\$(Version)\Rec.API.zip</DesktopBuildPackageLocation>
<DesktopBuildPackageLocation>M:\App&amp;Service\0 DD - Smart UP\ReC\Release\API\$(Version)\Rec.API.zip</DesktopBuildPackageLocation>
<PackageAsSingleFile>true</PackageAsSingleFile>
<DeployIisAppPath>Rec.API</DeployIisAppPath>
<_TargetId>IISWebDeployPackage</_TargetId>

View File

@@ -10,10 +10,10 @@
<Product>ReC.API</Product>
<PackageIcon>Assets\icon.ico</PackageIcon>
<PackageTags>digital data rest-caller rec api</PackageTags>
<Version>2.4.0-beta</Version>
<AssemblyVersion>2.4.0.0</AssemblyVersion>
<FileVersion>2.4.0.0</FileVersion>
<InformationalVersion>2.4.0-beta</InformationalVersion>
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<InformationalVersion>1.0.0</InformationalVersion>
<Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
@@ -23,8 +23,13 @@
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.11" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="NLog" Version="5.2.5" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="10.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.SQLite" Version="7.0.0" />
<PackageReference Include="Serilog.UI" Version="3.2.0" />
<PackageReference Include="Serilog.UI.SqliteProvider" Version="1.1.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -5,55 +5,8 @@
"Microsoft.AspNetCore": "Warning"
}
},
"NLog": {
"throwConfigExceptions": true,
"variables": {
"logDirectory": "E:\\LogFiles\\Digital Data\\Rec.API",
"logFileNamePrefix": "${shortdate}.Rec.API"
},
"targets": {
"infoLogs": {
"type": "File",
"fileName": "${logDirectory}\\${logFileNamePrefix}-Info.log",
"maxArchiveDays": 30
},
"warningLogs": {
"type": "File",
"fileName": "${logDirectory}\\${logFileNamePrefix}-Warning.log",
"maxArchiveDays": 30
},
"errorLogs": {
"type": "File",
"fileName": "${logDirectory}\\${logFileNamePrefix}-Error.log",
"maxArchiveDays": 30
},
"criticalLogs": {
"type": "File",
"fileName": "${logDirectory}\\${logFileNamePrefix}-Critical.log",
"maxArchiveDays": 30
}
},
"rules": [
{
"logger": "*",
"level": "Info",
"writeTo": "infoLogs"
},
{
"logger": "*",
"level": "Warn",
"writeTo": "warningLogs"
},
{
"logger": "*",
"level": "Error",
"writeTo": "errorLogs"
},
{
"logger": "*",
"level": "Fatal",
"writeTo": "criticalLogs"
}
]
"Serilog": {
"LogDirectory": "E:\\LogFiles\\Digital Data\\Rec.API",
"RetainedFileCountLimit": 30
}
}

View File

@@ -0,0 +1,6 @@
namespace ReC.Application.Common.Dto;
public record BatchRecActionViewResponse(int TotalActionCount)
{
public int ActionExceptionCount { get; internal set; } = 0;
}

View File

@@ -29,8 +29,6 @@ public class ReadProfileViewQueryHandler(IRepository<ProfileView> repo, IMapper
var profiles = await query.ToListAsync(cancel);
return profiles is null || profiles.Count == 0
? throw new NotFoundException($"Profile {request.Id} not found.")
: mapper.Map<IEnumerable<ProfileViewDto>>(profiles);
return mapper.Map<IEnumerable<ProfileViewDto>>(profiles);
}
}

View File

@@ -1,23 +1,26 @@
using MediatR;
using Microsoft.Extensions.Logging;
using ReC.Application.Common.Dto;
using ReC.Application.Common.Exceptions;
using ReC.Application.RecActions.Queries;
using ReC.Domain.Constants;
namespace ReC.Application.RecActions.Commands;
public record InvokeBatchRecActionViewsCommand : IRequest
public record InvokeBatchRecActionViewsCommand : IRequest<BatchRecActionViewResponse>
{
public long ProfileId { get; init; }
public required InvokeReferences References { get; init; }
}
public class InvokeRecActionViewsCommandHandler(ISender sender, ILogger<InvokeRecActionViewsCommandHandler>? logger = null) : IRequestHandler<InvokeBatchRecActionViewsCommand>
public class InvokeRecActionViewsCommandHandler(ISender sender, ILogger<InvokeRecActionViewsCommandHandler>? logger = null) : IRequestHandler<InvokeBatchRecActionViewsCommand, BatchRecActionViewResponse>
{
public async Task Handle(InvokeBatchRecActionViewsCommand request, CancellationToken cancel)
public async Task<BatchRecActionViewResponse> Handle(InvokeBatchRecActionViewsCommand request, CancellationToken cancel)
{
var actions = await sender.Send(new ReadRecActionViewQuery() { ProfileId = request.ProfileId }, cancel);
var res = new BatchRecActionViewResponse(TotalActionCount: actions.Count());
foreach (var action in actions)
{
try
@@ -28,30 +31,26 @@ public class InvokeRecActionViewsCommandHandler(ISender sender, ILogger<InvokeRe
References = request.References
}, cancel);
}
catch (RecActionException ex)
{
switch (action.ErrorAction)
{
case ErrorAction.Continue:
logger?.LogWarning(ex, "Rec action failed but continuing. ActionId: {ActionId}, ProfileId: {ProfileId}", ex.ActionId, ex.ProfileId);
break;
default:
// Rethrow the exception to stop processing further actions
throw;
}
}
catch (Exception ex)
{
switch (action.ErrorAction)
{
case ErrorAction.Continue:
logger?.LogError(ex, "Unexpected error during rec action. ActionId: {ActionId}, ProfileId: {ProfileId}", action.Id, action.ProfileId);
if (ex is RecActionException recEx)
logger?.LogWarning(ex, "Rec action failed but continuing. ActionId: {ActionId}, ProfileId: {ProfileId}", recEx.ActionId, recEx.ProfileId);
else
logger?.LogError(ex, "Unexpected error during rec action. ActionId: {ActionId}, ProfileId: {ProfileId}", action.Id, action.ProfileId);
break;
default:
// Rethrow the exception to stop processing further actions
throw;
}
res.ActionExceptionCount += 1;
}
}
return res;
}
}

View File

@@ -31,9 +31,6 @@ public class ReadRecActionViewQueryHandler(IRepository<RecActionView> repo, IMap
var actions = await query.ToListAsync(cancel);
if (actions.Count == 0)
throw new NotFoundException($"No actions found for the profile {request.ProfileId}.");
return mapper.Map<IEnumerable<RecActionViewDto>>(actions);
}
}

View File

@@ -54,15 +54,6 @@ public class ReadResultViewQueryHandler(IRepository<ResultView> repo, IMapper ma
? await GetLastBatchEntitiesAsync(q, cancel)
: await q.ToListAsync(cancel);
if (entities.Count == 0)
throw new NotFoundException($"No result views found for the given criteria. Criteria: {
JsonSerializer.Serialize(request, options: new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
})}"
);
return mapper.Map<IEnumerable<ResultViewDto>>(entities);
}

View File

@@ -0,0 +1,14 @@
namespace ReC.Client.Api
{
/// <summary>
/// Represents the result of a batch RecAction invocation.
/// </summary>
public class BatchRecActionViewResponse
{
/// <summary>Total number of actions that were processed.</summary>
public int TotalActionCount { get; set; }
/// <summary>Number of actions that resulted in an exception.</summary>
public int ActionExceptionCount { get; set; }
}
}

View File

@@ -30,18 +30,20 @@ namespace ReC.Client.Api
/// <param name="profileId">The profile identifier.</param>
/// <param name="references">Optional reference values to pass through to all result records.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A <see cref="BatchRecActionViewResponse"/> describing the outcome of the batch invocation.</returns>
/// <exception cref="ReCApiException">Thrown when the API responds with a non-success status code.</exception>
#if NETFRAMEWORK
public async Task InvokeAsync(long profileId, InvokeReferences references = null, CancellationToken cancellationToken = default)
public async Task<BatchRecActionViewResponse> InvokeAsync(long profileId, InvokeReferences references = null, CancellationToken cancellationToken = default)
#else
public async Task InvokeAsync(long profileId, InvokeReferences? references = null, CancellationToken cancellationToken = default)
public async Task<BatchRecActionViewResponse?> InvokeAsync(long profileId, InvokeReferences? references = null, CancellationToken cancellationToken = default)
#endif
{
var content = references != null ? ReCClientHelpers.ToJsonContent(references) : null;
using (content)
using (var resp = await Http.PostAsync($"{ResourcePath}/invoke/{profileId}", content, cancellationToken))
{
await ReCClientHelpers.HandleResponseAsync(resp, Logger, Options.LogSuccessfulRequests, cancellationToken).ConfigureAwait(false);
var body = await ReCClientHelpers.HandleResponseAsync(resp, Logger, Options.LogSuccessfulRequests, cancellationToken).ConfigureAwait(false);
return ReCClientHelpers.Deserialize<BatchRecActionViewResponse>(body);
}
}
@@ -51,8 +53,13 @@ namespace ReC.Client.Api
/// <param name="profileId">The profile identifier.</param>
/// <param name="batchId">Batch identifier.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A <see cref="BatchRecActionViewResponse"/> describing the outcome of the batch invocation.</returns>
/// <exception cref="ReCApiException">Thrown when the API responds with a non-success status code.</exception>
public Task InvokeAsync(long profileId, string batchId, CancellationToken cancellationToken = default)
#if NETFRAMEWORK
public Task<BatchRecActionViewResponse> InvokeAsync(long profileId, string batchId, CancellationToken cancellationToken = default)
#else
public Task<BatchRecActionViewResponse?> InvokeAsync(long profileId, string batchId, CancellationToken cancellationToken = default)
#endif
{
return InvokeAsync(profileId, new InvokeReferences() { BatchId = batchId }, cancellationToken);
}

View File

@@ -11,9 +11,9 @@
<PackageIcon>icon.png</PackageIcon>
<RepositoryUrl>http://git.dd:3000/AppStd/Rec.git</RepositoryUrl>
<PackageTags>digital data rec api client</PackageTags>
<Version>2.0.0-beta</Version>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<FileVersion>2.0.0.0</FileVersion>
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<Description>Client-Bibliothek für die Interaktion mit der ReC.API, die typisierten HTTP-Zugriff und DI-Integration bietet.</Description>
</PropertyGroup>

View File

@@ -30,23 +30,18 @@ public class ProfileQueryTests : RecApplicationTestBase
var (sender, scope) = CreateScopedSender();
using var _ = scope;
try
var profiles = await sender.Send(new ReadProfileViewQuery
{
var profiles = await sender.Send(new ReadProfileViewQuery
{
Id = profileId,
IncludeActions = false
});
Id = profileId,
IncludeActions = false
});
var profile = profiles.Single();
Assert.That(profile.Id, Is.EqualTo(profileId));
Assert.That(profile.ProfileName, Is.Not.Null.And.Not.Empty);
Assert.That(profile.Active, Is.True);
}
catch (NotFoundException)
var profile = profiles.SingleOrDefault();
Assert.Multiple(() =>
{
Assert.Pass("NotFound is acceptable when profile does not exist");
}
Assert.That(profile?.Id, Is.EqualTo(profileId));
Assert.That(profile?.ProfileName, Is.Not.Null.And.Not.Empty);
Assert.That(profile?.Active, Is.True);
});
}
}

View File

@@ -30,34 +30,11 @@ public class RecActionQueryTests : RecApplicationTestBase
var (sender, scope) = CreateScopedSender();
using var _ = scope;
try
var actions = await sender.Send(new ReadRecActionViewQuery
{
var actions = await sender.Send(new ReadRecActionViewQuery
{
ProfileId = profileId
});
ProfileId = profileId
});
Assert.That(actions, Is.Not.Empty);
Assert.That(actions.All(a => a.ProfileId == profileId));
}
catch (NotFoundException)
{
Assert.Pass("NotFound is acceptable when test data is unavailable");
}
}
[Test]
public void ReadRecActionViewQuery_with_unknown_profile_throws_not_found()
{
var (sender, scope) = CreateScopedSender();
using var _ = scope;
var invalidProfileId = long.MaxValue;
Assert.ThrowsAsync<NotFoundException>(async () =>
await sender.Send(new ReadRecActionViewQuery
{
ProfileId = invalidProfileId
}));
Assert.That(actions.All(a => a.ProfileId == profileId));
}
}

View File

@@ -25,18 +25,11 @@ public class ResultQueryTests : RecApplicationTestBase
var invalidActionId = long.MaxValue;
try
await sender.Send(new ReadResultViewQuery
{
await sender.Send(new ReadResultViewQuery
{
ActionId = invalidActionId
});
ActionId = invalidActionId
});
Assert.Pass("Read completed for unknown action id.");
}
catch (NotFoundException)
{
Assert.Pass("NotFound is acceptable for unknown action");
}
Assert.Pass("Read completed for unknown action id.");
}
}

View File

@@ -7,6 +7,7 @@ using ReC.Application.RecActions.Commands;
using ReC.Client;
using ReC.Client.Api;
using ClientInvokeReferences = ReC.Client.Api.InvokeReferences;
using ClientBatchResponse = ReC.Client.Api.BatchRecActionViewResponse;
namespace ReC.Tests.Client;
@@ -201,4 +202,70 @@ public class RecActionApiTests : RecClientTestBase
Assert.That(ex, Is.Not.Null);
Assert.That(ex!.Method, Is.EqualTo("POST"));
}
[Test]
public async Task InvokeAsync_returns_BatchRecActionViewResponse_on_success()
{
var profileId = await TryResolveProfileIdAsync();
if (profileId is null or <= 0)
Assert.Ignore("No profile available in the database for this test (set FakeProfileId or insert a profile).");
var (client, scope) = CreateScopedClient();
using var _ = scope;
ClientBatchResponse? result;
try
{
result = await client.RecActions.InvokeAsync(profileId!.Value, new ClientInvokeReferences { BatchId = "test-batch" });
}
catch (ReCApiException ex)
{
Assert.Pass($"API returned {ex.StatusCode} <20> acceptable when test data is unavailable.");
return;
}
Assert.That(result, Is.Not.Null);
Assert.That(result!.TotalActionCount, Is.GreaterThanOrEqualTo(0));
Assert.That(result.ActionExceptionCount, Is.GreaterThanOrEqualTo(0));
Assert.That(result.ActionExceptionCount, Is.LessThanOrEqualTo(result.TotalActionCount));
}
[Test]
public async Task InvokeAsync_with_batchId_string_returns_BatchRecActionViewResponse()
{
var profileId = await TryResolveProfileIdAsync();
if (profileId is null or <= 0)
Assert.Ignore("No profile available in the database for this test (set FakeProfileId or insert a profile).");
var (client, scope) = CreateScopedClient();
using var _ = scope;
ClientBatchResponse? result;
try
{
result = await client.RecActions.InvokeAsync(profileId!.Value, "test-batch");
}
catch (ReCApiException ex)
{
Assert.Pass($"API returned {ex.StatusCode} <20> acceptable when test data is unavailable.");
return;
}
Assert.That(result, Is.Not.Null);
Assert.That(result!.TotalActionCount, Is.GreaterThanOrEqualTo(0));
}
[Test]
public void InvokeAsync_with_batchId_string_and_unknown_profile_throws_ReCApiException()
{
var (client, scope) = CreateScopedClient();
using var _ = scope;
var ex = Assert.ThrowsAsync<ReCApiException>(async () =>
await client.RecActions.InvokeAsync(long.MaxValue, "test-batch"));
Assert.That(ex, Is.Not.Null);
Assert.That(ex!.Method, Is.EqualTo("POST"));
Assert.That(ex.RequestUri!.AbsolutePath, Does.Contain("invoke"));
}
}