Select Git revision
ProjectReportingTests.cs

Petar Hristov authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
ProjectReportingTests.cs 11.78 KiB
using AutoMapper;
using Coscine.ApiClient.Core.Api;
using Coscine.ApiClient.Core.Model;
using Coscine.KpiGenerator.MappingProfiles;
using Coscine.KpiGenerator.Models;
using Coscine.KpiGenerator.Models.ConfigurationModels;
using Coscine.KpiGenerator.Reportings.Project;
using Coscine.KpiGenerator.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using NSubstitute.Extensions;
using static Coscine.KpiGenerator.Models.ConfigurationModels.ReportingConfiguration;
using static KPIGenerator.Utils.CommandLineOptions;
namespace KpiGenerator.Tests;
[TestFixture]
public class ProjectReportingTests
{
private IMapper _mapper = null!;
private ILogger<ProjectReporting> _logger = null!;
private IStorageService _gitlabStorageService = null!;
private IStorageService _localStorageService = null!;
private IOptionsMonitor<KpiConfiguration> _kpiConfiguration = null!;
private IOptionsMonitor<ReportingConfiguration> _reportingConfiguration = null!;
private IAdminApi _adminApi = null!;
private ProjectReporting _projectReporting = null!; // System Under Test
[SetUp]
public void SetUp()
{
// NSubstitute Mocks
_logger = Substitute.For<ILogger<ProjectReporting>>();
_gitlabStorageService = Substitute.For<IStorageService>();
_localStorageService = Substitute.For<IStorageService>();
// Mock IOptionsMonitor
var kpiConfig = new KpiConfiguration
{
ProjectKpi = new("project_reporting.json")
};
_kpiConfiguration = Substitute.For<IOptionsMonitor<KpiConfiguration>>();
_kpiConfiguration.CurrentValue.Returns(kpiConfig);
// Create a real mapper
_mapper = new MapperConfiguration(cfg =>
{
cfg.AddProfile<MappingProfiles>();
}).CreateMapper();
// Mock ReportingConfiguration
var reportingConfig = new ReportingConfiguration
{
Endpoint = "https://some-endpoint/api",
ApiKey = "dummy-api-key",
// Possibly fill Organization as needed
Organization = new OrganizationConfiguration
{
OtherOrganization = new()
{
Name = "Other",
RorUrl = "https://ror.org/_other",
}
}
};
_reportingConfiguration = Substitute.For<IOptionsMonitor<ReportingConfiguration>>();
_reportingConfiguration.CurrentValue.Returns(reportingConfig);
_adminApi = Substitute.For<IAdminApi>();
}
#region GenerateReportingAsync Tests
[Test]
public async Task GenerateReportingAsync_ReturnsGeneralAndPerOrgFiles_WhenProjectsExist()
{
// Arrange
var projects = TestData.ProjectAdminDtos;
_adminApi
.GetAllProjectsAsync(
includeDeleted: Arg.Any<bool>(),
includeQuotas: Arg.Any<bool>(),
includePublicationRequests: Arg.Any<bool>(),
pageNumber: Arg.Any<int>(),
pageSize: Arg.Any<int>()
)
.Returns(ci =>
{
// Return the test projects data, single page
var pagination = new Pagination(currentPage: 1, pageSize: 2, totalCount: 2, totalPages: 1);
return Task.FromResult(new ProjectAdminDtoPagedResponse(data: projects, pagination: pagination, statusCode: 200, traceId: "dummy-trace-id"));
});
_projectReporting = new ProjectReporting(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi);
// Act
var result = await _projectReporting.GenerateReportingAsync();
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Count(), Is.EqualTo(3), "Expected 3 reporting files.");
var generalFile = result.FirstOrDefault(r => r.Path.Contains("general", StringComparison.OrdinalIgnoreCase)
|| r.Path.EndsWith("project_reporting.json"));
Assert.That(generalFile, Is.Not.Null, "General file should exist.");
var orgFile1 = result.FirstOrDefault(r => r.Path.Contains("12345"));
Assert.That(orgFile1, Is.Not.Null, "Per-organization file for ror.org/12345 should exist.");
var orgFile2 = result.FirstOrDefault(r => r.Path.Contains("54321"));
Assert.That(orgFile2, Is.Not.Null, "Per-organization file for ror.org/54321 should exist.");
}
[Test]
public async Task GenerateReportingAsync_ReturnsOnlyGeneralFile_WhenNoProjects()
{
// Arrange
_adminApi
.GetAllProjectsAsync()
.Returns(ci =>
{
// No projects, empty data
var pagination = new Pagination(currentPage: 1, pageSize: 0, totalCount: 0, totalPages: 1);
return Task.FromResult(new ProjectAdminDtoPagedResponse(data: [], pagination: pagination, statusCode: 200, traceId: "dummy-trace-id"));
});
_projectReporting = new ProjectReporting(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi);
// Act
var result = await _projectReporting.GenerateReportingAsync();
// Assert
// We expect 1 file: the "general" file (with an empty list of projects).
Assert.That(result.Count(), Is.EqualTo(1));
var file = result.First();
Assert.That(file.Path, Does.Contain("project_reporting.json").Or.Contain("general"));
}
#endregion
#region RunAsync Tests
[Test]
public async Task RunAsync_WhenGitlabPublishSucceeds_ShouldReturnTrue()
{
// Arrange
var options = new ProjectReportingOptions { DummyMode = false };
var reportingFiles = new List<ReportingFileObject>
{
new() { Path = "foo", Content = new MemoryStream() }
};
// We want to ensure that GenerateReportingAsync returns some test objects
_projectReporting = Substitute.ForPartsOf<ProjectReporting>(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi);
_projectReporting
.Configure()
.GenerateReportingAsync()
.Returns(Task.FromResult((IEnumerable<ReportingFileObject>)reportingFiles));
// GitLab publish => success
_gitlabStorageService.PublishAsync(Arg.Any<string>(), Arg.Any<IEnumerable<ReportingFileObject>>())
.Returns(Task.FromResult(true));
// Local storage shouldn't be called if GitLab is successful
_localStorageService.PublishAsync(Arg.Any<string>(), Arg.Any<IEnumerable<ReportingFileObject>>())
.Returns(Task.FromResult(true)); // default
// Act
var result = await _projectReporting.RunAsync(options);
// Assert
Assert.That(result, Is.True, "Expected RunAsync to return true if GitLab publish succeeds.");
// Verify GitLab was called
await _gitlabStorageService.Received(1)
.PublishAsync("Project Reporting", reportingFiles);
// Verify Local storage was never called
await _localStorageService.DidNotReceiveWithAnyArgs()
.PublishAsync(Arg.Any<string>(), Arg.Any<IEnumerable<ReportingFileObject>>());
}
[Test]
public async Task RunAsync_WhenGitlabPublishFails_ShouldFallbackToLocalStorage()
{
// Arrange
var options = new ProjectReportingOptions { DummyMode = false };
var reportingFiles = new List<ReportingFileObject>
{
new() { Path = "bar", Content = new MemoryStream() }
};
// Partial mock to override GenerateReportingAsync
_projectReporting = Substitute.ForPartsOf<ProjectReporting>(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi);
_projectReporting
.Configure()
.GenerateReportingAsync()
.Returns(Task.FromResult((IEnumerable<ReportingFileObject>)reportingFiles));
// GitLab publish => fails
_gitlabStorageService.PublishAsync(Arg.Any<string>(), Arg.Any<IEnumerable<ReportingFileObject>>())
.Returns(Task.FromResult(false));
// Local publish => success
_localStorageService.PublishAsync(Arg.Any<string>(), Arg.Any<IEnumerable<ReportingFileObject>>())
.Returns(Task.FromResult(true));
// Act
var result = await _projectReporting.RunAsync(options);
// Assert
Assert.That(result, Is.True, "Expected RunAsync to return true if local storage publish succeeds after GitLab fails.");
// Verify GitLab was called
await _gitlabStorageService.Received(1)
.PublishAsync("Project Reporting", reportingFiles);
// Verify fallback to local was called
await _localStorageService.Received(1)
.PublishAsync("Project Reporting", reportingFiles);
}
[Test]
public async Task RunAsync_WhenInDummyMode_ShouldSkipGitlabAndPublishToLocal()
{
// Arrange
var options = new ProjectReportingOptions { DummyMode = true };
var reportingFiles = new List<ReportingFileObject>
{
new() { Path = "dummy", Content = new MemoryStream() }
};
// Partial mock to override GenerateReportingAsync
_projectReporting = Substitute.ForPartsOf<ProjectReporting>(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi);
_projectReporting
.Configure()
.GenerateReportingAsync()
.Returns(Task.FromResult((IEnumerable<ReportingFileObject>)reportingFiles));
// GitLab publish => should not be called
// Local publish => success
_localStorageService.PublishAsync(Arg.Any<string>(), Arg.Any<IEnumerable<ReportingFileObject>>())
.Returns(Task.FromResult(true));
// Act
var result = await _projectReporting.RunAsync(options);
// Assert
Assert.That(result, Is.True, "Expected RunAsync to return true if local storage publish succeeds in DummyMode.");
// Verify GitLab wasn't called
await _gitlabStorageService.DidNotReceiveWithAnyArgs()
.PublishAsync(Arg.Any<string>(), Arg.Any<IEnumerable<ReportingFileObject>>());
// Verify local storage was called
await _localStorageService.Received(1)
.PublishAsync("Project Reporting", reportingFiles);
}
[Test]
public async Task RunAsync_WhenBothGitlabAndLocalFail_ShouldReturnFalse()
{
// Arrange
var options = new ProjectReportingOptions { DummyMode = false };
var reportingFiles = new List<ReportingFileObject>
{
new() { Path = "fail", Content = new MemoryStream() }
};
// Partial mock
_projectReporting = Substitute.ForPartsOf<ProjectReporting>(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi);
_projectReporting
.Configure()
.GenerateReportingAsync()
.Returns(Task.FromResult((IEnumerable<ReportingFileObject>)reportingFiles));
_gitlabStorageService.PublishAsync(Arg.Any<string>(), Arg.Any<IEnumerable<ReportingFileObject>>())
.Returns(Task.FromResult(false)); // GitLab fails
_localStorageService.PublishAsync(Arg.Any<string>(), Arg.Any<IEnumerable<ReportingFileObject>>())
.Returns(Task.FromResult(false)); // Local also fails
// Act
var result = await _projectReporting.RunAsync(options);
// Assert
Assert.That(result, Is.False, "Expected RunAsync to return false if both GitLab and local publish fail.");
}
#endregion
}