Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • Fix/xxxx-indexOutOfRange
  • Fix/xxxx-minorFixes
  • Fix/xxxx-organization
  • Fix/xxxx-wrap
  • Hotfix/2332-userInstitutesInReporting
  • Hotfix/2388-sensitive
  • Hotfix/3115-userReportingEmpty
  • Hotfix/3115-userReportingEmpty2
  • Hotfix/xxxx-rors
  • Issue/2181-kpiGeneratorBase
  • Issue/2182-kpiGeneratorUser
  • Issue/2183-kpiGeneratorResource
  • Issue/2184-kpiGeneratorProject
  • Issue/2185-kpiGeneratorAP
  • Issue/2186-systemStatusReporting
  • Issue/2283-activityFix
  • Issue/2304-virtuosoRoars
  • Issue/2330-fixNaNQuotainAdmin
  • Issue/2432-publicationKpi
  • Issue/2492-respOrg
  • Issue/2518-docs
  • Issue/2568-betterLogging
  • Issue/2666-adminCronjobs
  • Issue/2666-adminCronjobs-theSequal
  • Issue/2847-reporting
  • Issue/2850-removeGrantId
  • Issue/2982-kpiDataPub
  • Issue/3005-kpiReportingBroken
  • Issue/3073-kpi
  • Issue/3142-kpiGenerator
  • dev
  • gitkeep
  • main
  • v0.1.0
  • v0.1.1
  • v0.1.10
  • v0.1.11
  • v0.1.12
  • v0.1.13
  • v0.1.14
  • v0.1.15
  • v0.1.16
  • v0.1.17
  • v0.1.18
  • v0.1.19
  • v0.1.2
  • v0.1.20
  • v0.1.21
  • v0.1.22
  • v0.1.23
  • v0.1.3
  • v0.1.4
  • v0.1.5
  • v0.1.6
  • v0.1.7
  • v0.1.8
  • v0.1.9
  • v1.0.1
  • v1.0.2
  • v1.0.3
  • v1.0.4
  • v1.0.5
  • v1.0.6
  • v1.0.7
  • v1.0.8
  • v1.0.9
  • v1.1.0
  • v1.1.1
  • v1.2.0
  • v1.2.1
  • v1.2.10
  • v1.2.2
  • v1.2.3
  • v1.2.4
  • v1.2.5
  • v1.2.6
  • v1.2.7
  • v1.2.8
  • v1.2.9
79 results

Target

Select target project
  • coscine/backend/scripts/kpi-generator
1 result
Select Git revision
  • Fix/xxxx-indexOutOfRange
  • Fix/xxxx-minorFixes
  • Fix/xxxx-organization
  • Fix/xxxx-wrap
  • Hotfix/2332-userInstitutesInReporting
  • Hotfix/2388-sensitive
  • Hotfix/3115-userReportingEmpty
  • Hotfix/3115-userReportingEmpty2
  • Hotfix/xxxx-rors
  • Issue/2181-kpiGeneratorBase
  • Issue/2182-kpiGeneratorUser
  • Issue/2183-kpiGeneratorResource
  • Issue/2184-kpiGeneratorProject
  • Issue/2185-kpiGeneratorAP
  • Issue/2186-systemStatusReporting
  • Issue/2283-activityFix
  • Issue/2304-virtuosoRoars
  • Issue/2330-fixNaNQuotainAdmin
  • Issue/2432-publicationKpi
  • Issue/2492-respOrg
  • Issue/2518-docs
  • Issue/2568-betterLogging
  • Issue/2666-adminCronjobs
  • Issue/2666-adminCronjobs-theSequal
  • Issue/2847-reporting
  • Issue/2850-removeGrantId
  • Issue/2982-kpiDataPub
  • Issue/3005-kpiReportingBroken
  • Issue/3073-kpi
  • Issue/3142-kpiGenerator
  • dev
  • gitkeep
  • main
  • v0.1.0
  • v0.1.1
  • v0.1.10
  • v0.1.11
  • v0.1.12
  • v0.1.13
  • v0.1.14
  • v0.1.15
  • v0.1.16
  • v0.1.17
  • v0.1.18
  • v0.1.19
  • v0.1.2
  • v0.1.20
  • v0.1.21
  • v0.1.22
  • v0.1.23
  • v0.1.3
  • v0.1.4
  • v0.1.5
  • v0.1.6
  • v0.1.7
  • v0.1.8
  • v0.1.9
  • v1.0.1
  • v1.0.2
  • v1.0.3
  • v1.0.4
  • v1.0.5
  • v1.0.6
  • v1.0.7
  • v1.0.8
  • v1.0.9
  • v1.1.0
  • v1.1.1
  • v1.2.0
  • v1.2.1
  • v1.2.10
  • v1.2.2
  • v1.2.3
  • v1.2.4
  • v1.2.5
  • v1.2.6
  • v1.2.7
  • v1.2.8
  • v1.2.9
79 results
Show changes
Commits on Source (17)
Showing
with 1272 additions and 105 deletions
......@@ -6,6 +6,7 @@ include:
stages:
- build
- test
- publish
variables:
......@@ -17,6 +18,9 @@ build-branch:
build-nuget-release:
extends: .build-nuget-release
test:
extends: .test
publish-gitlab-release:
extends: .publish-gitlab-release
......
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="NSubstitute.Analyzers.CSharp" Version="1.0.17" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\KpiGenerator\KpiGenerator.csproj" />
</ItemGroup>
</Project>
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 IProjectQuotaApi _projectQuotaApi = 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>();
_projectQuotaApi = Substitute.For<ProjectQuotaApi>();
}
#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, _projectQuotaApi);
// 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, _projectQuotaApi);
// 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, _projectQuotaApi);
_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, _projectQuotaApi);
_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, _projectQuotaApi);
_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, _projectQuotaApi);
_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
}
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.Resource;
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 ResourceReportingTests
{
private IMapper _mapper = null!;
private ILogger<ResourceReporting> _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 ResourceReporting _resourceReporting = null!; // System Under Test
[SetUp]
public void SetUp()
{
// NSubstitute Mocks
_logger = Substitute.For<ILogger<ResourceReporting>>();
_gitlabStorageService = Substitute.For<IStorageService>();
_localStorageService = Substitute.For<IStorageService>();
// Mock IOptionsMonitor
var kpiConfig = new KpiConfiguration
{
ResourceKpi = new("resource_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;
var resources = TestData.ResourceAdminDtos;
_adminApi
.GetAllProjectsAsync(
includeDeleted: 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"));
});
_adminApi
.GetAllResourcesAsync(
includeDeleted: Arg.Any<bool>(),
includeQuotas: Arg.Any<bool>(),
pageNumber: Arg.Any<int>(),
pageSize: Arg.Any<int>()
)
.Returns(ci =>
{
// Return the test resources data, single page
var pagination = new Pagination(currentPage: 1, pageSize: 2, totalCount: 2, totalPages: 1);
return Task.FromResult(new ResourceAdminDtoPagedResponse(data: resources, pagination: pagination, statusCode: 200, traceId: "dummy-trace-id"));
});
_resourceReporting = new ResourceReporting(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi);
// Act
var result = await _resourceReporting.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("resource_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"));
});
_adminApi
.GetAllResourcesAsync()
.Returns(ci =>
{
// No resources, empty data
var pagination = new Pagination(currentPage: 1, pageSize: 0, totalCount: 0, totalPages: 1);
return Task.FromResult(new ResourceAdminDtoPagedResponse(data: [], pagination: pagination, statusCode: 200, traceId: "dummy-trace-id"));
});
_resourceReporting = new ResourceReporting(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi);
// Act
var result = await _resourceReporting.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("resource_reporting.json").Or.Contain("general"));
}
#endregion
#region RunAsync Tests
[Test]
public async Task RunAsync_WhenGitlabPublishSucceeds_ShouldReturnTrue()
{
// Arrange
var options = new ResourceReportingOptions { DummyMode = false };
var reportingFiles = new List<ReportingFileObject>
{
new() { Path = "foo", Content = new MemoryStream() }
};
// We want to ensure that GenerateReportingAsync returns some test objects
_resourceReporting = Substitute.ForPartsOf<ResourceReporting>(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi);
_resourceReporting
.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 _resourceReporting.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("Resource 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 ResourceReportingOptions { DummyMode = false };
var reportingFiles = new List<ReportingFileObject>
{
new() { Path = "bar", Content = new MemoryStream() }
};
// Partial mock to override GenerateReportingAsync
_resourceReporting = Substitute.ForPartsOf<ResourceReporting>(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi);
_resourceReporting
.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 _resourceReporting.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("Resource Reporting", reportingFiles);
// Verify fallback to local was called
await _localStorageService.Received(1)
.PublishAsync("Resource Reporting", reportingFiles);
}
[Test]
public async Task RunAsync_WhenInDummyMode_ShouldSkipGitlabAndPublishToLocal()
{
// Arrange
var options = new ResourceReportingOptions { DummyMode = true };
var reportingFiles = new List<ReportingFileObject>
{
new() { Path = "dummy", Content = new MemoryStream() }
};
// Partial mock to override GenerateReportingAsync
_resourceReporting = Substitute.ForPartsOf<ResourceReporting>(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi);
_resourceReporting
.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 _resourceReporting.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("Resource Reporting", reportingFiles);
}
[Test]
public async Task RunAsync_WhenBothGitlabAndLocalFail_ShouldReturnFalse()
{
// Arrange
var options = new ResourceReportingOptions { DummyMode = false };
var reportingFiles = new List<ReportingFileObject>
{
new() { Path = "fail", Content = new MemoryStream() }
};
// Partial mock
_resourceReporting = Substitute.ForPartsOf<ResourceReporting>(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi);
_resourceReporting
.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 _resourceReporting.RunAsync(options);
// Assert
Assert.That(result, Is.False, "Expected RunAsync to return false if both GitLab and local publish fail.");
}
#endregion
}
using Coscine.ApiClient.Core.Model;
static class TestData
{
public static List<ProjectAdminDto> ProjectAdminDtos => ReturnProjectAdminDtos();
public static List<ResourceAdminDto> ResourceAdminDtos => ReturnResourceAdminDtos();
public static List<UserDto> UserDtos => ReturnUserDtos();
public static List<RoleDto> RoleDtos => ReturnRoleDtos();
private static List<RoleDto> ReturnRoleDtos()
{
return
[
new(id: Guid.Parse("778f7bd2-128b-45aa-a093-807da3b9aecc"),
displayName: "Role One",
description: "Role One Description"
),
new(id: Guid.Parse("6e5aadc3-72ea-4f96-827d-90c5c15d34af"),
displayName: "Role Two",
description: "Role Two Description"
)
];
}
private static List<UserDto> ReturnUserDtos()
{
return
[
new(id: Guid.Parse("fd971cef-7c02-45df-97c4-ef757c05bc19"),
displayName: "User One",
givenName: "User",
familyName: "One",
emails: [ new(email: "one@user.com", isConfirmed: true, isPrimary: true) ],
language: new(id: Guid.NewGuid(), displayName: "English", abbreviation: "en"),
areToSAccepted: true,
latestActivity: DateTime.UtcNow.AddHours(-1),
disciplines: [new(id: Guid.NewGuid(), uri: "https://one.discipline.org", displayNameEn: "Discipline One", displayNameDe: "Disziplin Eins")],
organizations: [new(uri: "https://ror.org/12345", displayName: "Organization One", readOnly: true)],
identities: [new(id: Guid.NewGuid(), displayName: "Provider One")]
),
new(id: Guid.Parse("21c95165-8b44-46a5-b711-4d4e0c54eda2"),
displayName: "User Two",
givenName: "User",
familyName: "Two",
emails: [ new(email: "two@user.com", isConfirmed: true, isPrimary: true) ],
language: new(id: Guid.NewGuid(), displayName: "English", abbreviation: "en"),
areToSAccepted: true,
latestActivity: DateTime.UtcNow.AddHours(-3),
disciplines: [new(id: Guid.NewGuid(), uri: "https://two.discipline.org", displayNameEn: "Discipline Two", displayNameDe: "Disziplin Zwei")],
organizations: [new(uri: "https://ror.org/54321", displayName: "Organization Two", readOnly: true)],
identities: [new(id: Guid.NewGuid(), displayName: "Provider Two")]
)
];
}
private static List<ResourceAdminDto> ReturnResourceAdminDtos()
{
return
[
new(id: Guid.Parse("a5092027-0d0b-4be6-9025-5a2397f34fce"),
name: "Resource One",
displayName: "Resource One",
description: "Resource One Description",
pid: "resource-one",
type: new(generalType: "ds", specificType: "dsweb"),
dateCreated: DateTime.UtcNow,
creator: new() { Id = Guid.NewGuid() },
resourceQuota: new(resource: new(id: Guid.Parse("a5092027-0d0b-4be6-9025-5a2397f34fce")), usedPercentage: 0.5f, used: new(), reserved: new()),
visibility: new(),
keywords: ["keyword1", "keyword2"],
disciplines: [new(id: Guid.NewGuid(), uri: "https://one.discipline.org", displayNameEn: "Discipline One", displayNameDe: "Disziplin Eins")],
license: new(),
usageRights: "usage rights",
metadataLocalCopy: false,
metadataExtraction: false,
archived: false,
projects: [],
applicationProfile: new(uri: "https://example.org"),
fixedValues: [],
projectResources: [new(projectId: Guid.Parse("0fd58e44-f3cc-4aec-a865-b8f37aa17466"), resourceId: Guid.Parse("a5092027-0d0b-4be6-9025-5a2397f34fce"))],
deleted: false
),
new(id: Guid.Parse("cc5d1783-8ff1-4401-a2ce-d1300bd1b2a5"),
name: "Resource Two",
displayName: "Resource Two",
description: "Resource Two Description",
pid: "resource-two",
type: new(generalType: "ds", specificType: "dss3"),
dateCreated: DateTime.UtcNow,
creator: new() { Id = Guid.NewGuid() },
resourceQuota: new(resource: new(id: Guid.Parse("cc5d1783-8ff1-4401-a2ce-d1300bd1b2a5")), usedPercentage: 0.5f, used: new(), reserved: new()),
visibility: new(),
keywords: ["keyword3"],
disciplines: [new(id: Guid.NewGuid(), uri: "https://one.discipline.org", displayNameEn: "Discipline One", displayNameDe: "Disziplin Eins")],
license: new(),
usageRights: "usage rights",
metadataLocalCopy: false,
metadataExtraction: false,
archived: false,
projects: [],
applicationProfile: new(uri: "https://example.org"),
fixedValues: [],
projectResources: [new(projectId: Guid.Parse("0fd58e44-f3cc-4aec-a865-b8f37aa17466"), resourceId: Guid.Parse("a5092027-0d0b-4be6-9025-5a2397f34fce"))],
deleted: false
)
];
}
private static List<ProjectAdminDto> ReturnProjectAdminDtos()
{
return
[
new(id: Guid.Parse("ce1af24e-2719-425c-947d-d7538f06fed0"),
name: "Project One",
displayName: "Project One",
description: "Project One Description",
pid: "project-one",
creationDate: DateTime.UtcNow,
slug: "project-one",
creator: new() { Id = Guid.NewGuid() },
projectQuota: [],
visibility: new(),
disciplines: [new(id: Guid.NewGuid(), uri: "https://one.discipline.org", displayNameEn: "Discipline One", displayNameDe: "Disziplin Eins")],
organizations: [
new(uri: "https://ror.org/12345", displayName: "Organization One", responsible: true),
new(uri: "https://ror.org/54321", displayName: "Organization Two", responsible: false)
],
projectRoles: [ new(projectId: Guid.Parse("ce1af24e-2719-425c-947d-d7538f06fed0"), userId: Guid.Parse("fd971cef-7c02-45df-97c4-ef757c05bc19"), roleId: Guid.Parse("778f7bd2-128b-45aa-a093-807da3b9aecc")) ]
),
new(id: Guid.Parse("0fd58e44-f3cc-4aec-a865-b8f37aa17466"),
name: "Project Two",
displayName: "Project Two",
description: "Project Two Description",
pid: "project-two",
creationDate: DateTime.UtcNow,
slug: "project-two",
creator: new() { Id = Guid.NewGuid() },
projectQuota: [],
visibility: new(),
disciplines: [new(id: Guid.NewGuid(), uri: "https://one.discipline.org", displayNameEn: "Discipline One", displayNameDe: "Disziplin Eins")],
organizations: [
new(uri: "https://ror.org/12345", displayName: "Organization One", responsible: false),
new(uri: "https://ror.org/54321", displayName: "Organization Two", responsible: true)
],
projectRoles: [ new(projectId: Guid.Parse("0fd58e44-f3cc-4aec-a865-b8f37aa17466"), userId: Guid.Parse("21c95165-8b44-46a5-b711-4d4e0c54eda2"), roleId: Guid.Parse("6e5aadc3-72ea-4f96-827d-90c5c15d34af")) ]
)
];
}
}
\ No newline at end of file
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.User;
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 UserReportingTests
{
private IMapper _mapper = null!;
private ILogger<UserReporting> _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 IRoleApi _roleApi = null!;
private UserReporting _userReporting = null!; // System Under Test
[SetUp]
public void SetUp()
{
// NSubstitute Mocks
_logger = Substitute.For<ILogger<UserReporting>>();
_gitlabStorageService = Substitute.For<IStorageService>();
_localStorageService = Substitute.For<IStorageService>();
// Mock IOptionsMonitor
var kpiConfig = new KpiConfiguration
{
UserKpi = new("user_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>();
_roleApi = Substitute.For<IRoleApi>();
}
#region GenerateReportingAsync Tests
[Test]
public async Task GenerateReportingAsync_ReturnsGeneralAndPerOrgFiles_WhenProjectsExist()
{
// Arrange
var projects = TestData.ProjectAdminDtos;
var users = TestData.UserDtos;
var roles = TestData.RoleDtos;
_adminApi
.GetAllProjectsAsync(
includeDeleted: 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"));
});
_adminApi
.GetAllUsersAsync(
tosAccepted: Arg.Any<bool>(),
pageNumber: Arg.Any<int>(),
pageSize: Arg.Any<int>()
)
.Returns(ci =>
{
// Return the test users data, single page
var pagination = new Pagination(currentPage: 1, pageSize: 2, totalCount: 2, totalPages: 1);
return Task.FromResult(new UserDtoPagedResponse(data: users, pagination: pagination, statusCode: 200, traceId: "dummy-trace-id"));
});
_roleApi
.GetRolesAsync(
pageNumber: Arg.Any<int>(),
pageSize: Arg.Any<int>()
)
.Returns(ci =>
{
// Return the test roles data, single page
var pagination = new Pagination(currentPage: 1, pageSize: 2, totalCount: 2, totalPages: 1);
return Task.FromResult(new RoleDtoPagedResponse(data: roles, pagination: pagination, statusCode: 200, traceId: "dummy-trace-id"));
});
_userReporting = new UserReporting(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi, _roleApi);
// Act
var result = await _userReporting.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("user_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"));
});
_adminApi
.GetAllUsersAsync()
.Returns(ci =>
{
// No users, empty data
var pagination = new Pagination(currentPage: 1, pageSize: 0, totalCount: 0, totalPages: 1);
return Task.FromResult(new UserDtoPagedResponse(data: [], pagination: pagination, statusCode: 200, traceId: "dummy-trace-id"));
});
_roleApi
.GetRolesAsync()
.Returns(ci =>
{
// No roles, empty data
var pagination = new Pagination(currentPage: 1, pageSize: 0, totalCount: 0, totalPages: 1);
return Task.FromResult(new RoleDtoPagedResponse(data: [], pagination: pagination, statusCode: 200, traceId: "dummy-trace-id"));
});
_userReporting = new UserReporting(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi, _roleApi);
// Act
var result = await _userReporting.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("user_reporting.json").Or.Contain("general"));
}
#endregion
#region RunAsync Tests
[Test]
public async Task RunAsync_WhenGitlabPublishSucceeds_ShouldReturnTrue()
{
// Arrange
var options = new UserReportingOptions { DummyMode = false };
var reportingFiles = new List<ReportingFileObject>
{
new() { Path = "foo", Content = new MemoryStream() }
};
// We want to ensure that GenerateReportingAsync returns some test objects
_userReporting = Substitute.ForPartsOf<UserReporting>(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi, _roleApi);
_userReporting
.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 _userReporting.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("User 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 UserReportingOptions { DummyMode = false };
var reportingFiles = new List<ReportingFileObject>
{
new() { Path = "bar", Content = new MemoryStream() }
};
// Partial mock to override GenerateReportingAsync
_userReporting = Substitute.ForPartsOf<UserReporting>(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi, _roleApi);
_userReporting
.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 _userReporting.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("User Reporting", reportingFiles);
// Verify fallback to local was called
await _localStorageService.Received(1)
.PublishAsync("User Reporting", reportingFiles);
}
[Test]
public async Task RunAsync_WhenInDummyMode_ShouldSkipGitlabAndPublishToLocal()
{
// Arrange
var options = new UserReportingOptions { DummyMode = true };
var reportingFiles = new List<ReportingFileObject>
{
new() { Path = "dummy", Content = new MemoryStream() }
};
// Partial mock to override GenerateReportingAsync
_userReporting = Substitute.ForPartsOf<UserReporting>(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi, _roleApi);
_userReporting
.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 _userReporting.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("User Reporting", reportingFiles);
}
[Test]
public async Task RunAsync_WhenBothGitlabAndLocalFail_ShouldReturnFalse()
{
// Arrange
var options = new UserReportingOptions { DummyMode = false };
var reportingFiles = new List<ReportingFileObject>
{
new() { Path = "fail", Content = new MemoryStream() }
};
// Partial mock
_userReporting = Substitute.ForPartsOf<UserReporting>(_mapper, _logger, _gitlabStorageService, _localStorageService, _kpiConfiguration, _reportingConfiguration, _adminApi, _roleApi);
_userReporting
.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 _userReporting.RunAsync(options);
// Assert
Assert.That(result, Is.False, "Expected RunAsync to return false if both GitLab and local publish fail.");
}
#endregion
}
......@@ -5,6 +5,8 @@ VisualStudioVersion = 17.2.32526.322
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KpiGenerator", "KpiGenerator\KpiGenerator.csproj", "{2B402E93-467B-49C1-8350-9277BDEDA9C3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KpiGenerator.Tests", "KpiGenerator.Tests\KpiGenerator.Tests.csproj", "{6A8D781C-E8B5-428F-8DDB-84F8A88BB832}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
......@@ -15,6 +17,10 @@ Global
{2B402E93-467B-49C1-8350-9277BDEDA9C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B402E93-467B-49C1-8350-9277BDEDA9C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B402E93-467B-49C1-8350-9277BDEDA9C3}.Release|Any CPU.Build.0 = Release|Any CPU
{6A8D781C-E8B5-428F-8DDB-84F8A88BB832}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A8D781C-E8B5-428F-8DDB-84F8A88BB832}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A8D781C-E8B5-428F-8DDB-84F8A88BB832}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A8D781C-E8B5-428F-8DDB-84F8A88BB832}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
......
......@@ -7,13 +7,13 @@
<AssemblyName>Coscine.KpiGenerator</AssemblyName>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>1.2.5</Version>
<Version>1.2.6</Version>
</PropertyGroup>
<PropertyGroup>
<Authors>RWTH Aachen University</Authors>
<Company>IT Center, RWTH Aachen University</Company>
<Copyright>©2024 IT Center, RWTH Aachen University</Copyright>
<Copyright>©2025 IT Center, RWTH Aachen University</Copyright>
<Description>KPI Generator is a part of the Coscine group.</Description>
</PropertyGroup>
......@@ -21,7 +21,7 @@
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Coscine.ApiClient" Version="1.9.3" />
<PackageReference Include="Coscine.ApiClient" Version="1.9.4" />
<PackageReference Include="dotNetRdf.Core" Version="3.1.1" />
<PackageReference Include="GitLabApiClient" Version="1.8.1-beta.5" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
......
......@@ -36,7 +36,6 @@ public class MappingProfiles : Profile
CreateMap<UserDto, UserReport>()
.ForMember(ur => ur.Disciplines, opt => opt.MapFrom(dto => dto.Disciplines))
.ForMember(ur => ur.Institutes, opt => opt.MapFrom(dto => dto.Institutes))
.ForMember(ur => ur.LatestActivity, opt => opt.MapFrom(dto => dto.LatestActivity))
.ForMember(ur => ur.LoginProviders, opt => opt.MapFrom(dto => dto.Identities))
.ForMember(ur => ur.Organizations, opt => opt.MapFrom(dto => dto.Organizations))
......@@ -50,12 +49,7 @@ public class MappingProfiles : Profile
.ForMember(lp => lp.Id, opt => opt.MapFrom(dto => dto.Id))
.ForMember(lp => lp.DisplayName, opt => opt.MapFrom(dto => dto.DisplayName));
CreateMap<UserInstituteDto, Organization>()
.ForMember(o => o.ReadOnly, opt => opt.MapFrom(dto => dto.ReadOnly))
.ForMember(o => o.Name, opt => opt.MapFrom(dto => dto.DisplayName))
.ForMember(o => o.RorUrl, opt => opt.MapFrom(dto => dto.Uri));
CreateMap<UserOrganizationDto, Organization>()
CreateMap<UserOrganizationDto, UserOrganization>()
.ForMember(o => o.ReadOnly, opt => opt.MapFrom(dto => dto.ReadOnly))
.ForMember(o => o.Name, opt => opt.MapFrom(dto => dto.DisplayName))
.ForMember(o => o.RorUrl, opt => opt.MapFrom(dto => dto.Uri));
......@@ -64,6 +58,11 @@ public class MappingProfiles : Profile
.ForMember(o => o.Name, opt => opt.MapFrom(dto => dto.DisplayName))
.ForMember(o => o.RorUrl, opt => opt.MapFrom(dto => dto.Uri));
CreateMap<ProjectOrganizationDto, ProjectOrganization>()
.ForMember(po => po.Name, opt => opt.MapFrom(dto => dto.DisplayName))
.ForMember(po => po.RorUrl, opt => opt.MapFrom(dto => dto.Uri))
.ForMember(po => po.Responsible, opt => opt.MapFrom(dto => dto.Responsible));
CreateMap<VisibilityDto, ProjectVisibility>()
.ForMember(pv => pv.Id, opt => opt.MapFrom(dto => dto.Id))
.ForMember(pv => pv.DisplayName, opt => opt.MapFrom(dto => dto.DisplayName));
......
......@@ -17,16 +17,4 @@ public record Organization
/// <example>https://ror.org/04xfq0f34</example>
[JsonPropertyName("RorUrl")]
public string RorUrl { get; init; } = null!;
/// <summary>
/// Determines if the organization's details can be modified.
/// </summary>
/// <value><c>true</c> if the organization is read-only; otherwise, <c>false</c>.</value>
/// <remarks>
/// This property defaults to <c>true</c> and can be manually set to <c>false</c>.
/// For entries set by Shibboleth, this property is automatically set to <c>true</c>.
/// </remarks>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("ReadOnly")]
public bool? ReadOnly { get; init; }
}
\ No newline at end of file
namespace Coscine.KpiGenerator.Models;
public record ProjectOrganization : Organization
{
/// <summary>
/// Determines if the organization is Responsible for a given project.
/// </summary>
public bool Responsible { get; set; }
}
\ No newline at end of file
......@@ -14,7 +14,7 @@ public record ProjectReport
public IReadOnlyList<PublicationRequestReport> PublicationRequests { get; set; } = null!;
[JsonPropertyName("Organizations")]
public IReadOnlyList<Organization> Organizations { get; init; } = null!;
public IReadOnlyList<ProjectOrganization> Organizations { get; init; } = null!;
[JsonPropertyName("Disciplines")]
public IReadOnlyList<Discipline> Disciplines { get; init; } = null!;
......
......@@ -26,7 +26,7 @@ public record ResourceReport
public string RelatedProjectId { get; init; } = null!;
[JsonPropertyName("Organizations")]
public IReadOnlyList<Organization> Organizations { get; set; } = [];
public IReadOnlyList<ProjectOrganization> Organizations { get; set; } = [];
[JsonPropertyName("Disciplines")]
public IReadOnlyList<Discipline> Disciplines { get; init; } = null!;
......
using System.Text.Json.Serialization;
namespace Coscine.KpiGenerator.Models;
public record UserOrganization : Organization {
/// <summary>
/// Determines if the organization's details can be modified.
/// </summary>
/// <value><c>true</c> if the organization is read-only; otherwise, <c>false</c>.</value>
/// <remarks>
/// This property defaults to <c>true</c> and can be manually set to <c>false</c>.
/// For entries set by Shibboleth, this property is automatically set to <c>true</c>.
/// </remarks>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("ReadOnly")]
public bool? ReadOnly { get; init; }
}
\ No newline at end of file
......@@ -4,9 +4,7 @@ public record UserReport
{
public IReadOnlyList<RelatedProject> RelatedProjects { get; set; } = null!;
public IReadOnlyList<Organization> Organizations { get; init; } = null!;
public IReadOnlyList<Organization> Institutes { get; init; } = null!;
public IReadOnlyList<UserOrganization> Organizations { get; init; } = null!;
public IReadOnlyList<Discipline> Disciplines { get; init; } = null!;
......
using CommandLine;
using Coscine.ApiClient.Core.Api;
using Coscine.ApiClient.Core.Client;
using Coscine.KpiGenerator.Logging;
using Coscine.KpiGenerator.Models.ConfigurationModels;
using Coscine.KpiGenerator.Reportings.ApplicationProfile;
using Coscine.KpiGenerator.Reportings.Complete;
using Coscine.KpiGenerator.Reportings.Project;
using Coscine.KpiGenerator.Reportings.Resource;
using Coscine.KpiGenerator.Reportings.System;
using Coscine.KpiGenerator.Reportings.User;
using Coscine.KpiGenerator.Utils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
......@@ -165,6 +170,22 @@ public class Program
// Register the HTTP client
services.AddHttpClient();
// Add the API clients
var apiConfiguration = new Configuration()
{
BasePath = $"{reportingConfiguration.Endpoint.TrimEnd('/')}/coscine",
ApiKeyPrefix = { { "Authorization", "Bearer" } },
ApiKey = { { "Authorization", reportingConfiguration.ApiKey } },
Timeout = 300000 // 5 minutes
};
services.AddSingleton<IAdminApi>(new AdminApi(apiConfiguration));
services.AddSingleton<IApplicationProfileApi>(new ApplicationProfileApi(apiConfiguration));
services.AddSingleton<IProjectApi>(new ProjectApi(apiConfiguration));
services.AddSingleton<IProjectQuotaApi>(new ProjectQuotaApi(apiConfiguration));
services.AddSingleton<IProjectResourceQuotaApi>(new ProjectResourceQuotaApi(apiConfiguration));
services.AddSingleton<IRoleApi>(new RoleApi(apiConfiguration));
services.AddSingleton<IUserApi>(new UserApi(apiConfiguration));
// Add services for reporting
services.AddTransient<CompleteReporting>();
services.AddTransient<ProjectReporting>();
......
using AutoMapper;
using Coscine.ApiClient;
using Coscine.ApiClient.Core.Api;
using Coscine.ApiClient.Core.Client;
using Coscine.ApiClient.Core.Model;
using Coscine.KpiGenerator.Models;
using Coscine.KpiGenerator.Models.ConfigurationModels;
......@@ -15,7 +14,7 @@ using VDS.RDF.Nodes;
using VDS.RDF.Parsing;
using static KPIGenerator.Utils.CommandLineOptions;
namespace Coscine.KpiGenerator.Reportings.Resource;
namespace Coscine.KpiGenerator.Reportings.ApplicationProfile;
public class ApplicationProfileReporting
{
......@@ -25,11 +24,7 @@ public class ApplicationProfileReporting
private readonly IStorageService _localStorageService;
private readonly KpiConfiguration _kpiConfiguration;
private readonly ReportingConfiguration _reportingConfiguration;
public static string AdminToken { get; set; } = null!;
public AdminApi AdminApi { get; init; }
public ProjectApi ProjectApi { get; init; }
public ApplicationProfileApi ApplicationProfileApi { get; }
private readonly IApplicationProfileApi _applicationProfileApi;
public ApplicationProfileReportingOptions Options { get; private set; } = null!;
public string ReportingFileName { get; }
......@@ -40,7 +35,8 @@ public class ApplicationProfileReporting
[FromKeyedServices("gitlab")] IStorageService gitlabStorageService,
[FromKeyedServices("local")] IStorageService localStorageService,
IOptionsMonitor<KpiConfiguration> kpiConfiguration,
IOptionsMonitor<ReportingConfiguration> reportingConfiguration
IOptionsMonitor<ReportingConfiguration> reportingConfiguration,
IApplicationProfileApi applicationProfileApi
)
{
_mapper = mapper;
......@@ -51,17 +47,7 @@ public class ApplicationProfileReporting
_reportingConfiguration = reportingConfiguration.CurrentValue;
ReportingFileName = _kpiConfiguration.ApplicationProfileKpi.FileName;
var configuration = new Configuration()
{
BasePath = $"{_reportingConfiguration.Endpoint.TrimEnd('/')}/coscine",
ApiKeyPrefix = { { "Authorization", "Bearer" } },
ApiKey = { { "Authorization", _reportingConfiguration.ApiKey } },
Timeout = 300000 // 5 minutes
};
AdminApi = new AdminApi(configuration);
ProjectApi = new ProjectApi(configuration);
ApplicationProfileApi = new ApplicationProfileApi(configuration);
_applicationProfileApi = applicationProfileApi;
}
public async Task<bool> RunAsync(ApplicationProfileReportingOptions reportingOptions)
......@@ -70,14 +56,27 @@ public class ApplicationProfileReporting
Options = reportingOptions;
var reportingFiles = await GenerateReportingAsync();
bool success = false;
_logger.LogInformation("Publishing to GitLab...");
// Publish to GitLab first, if that fails, publish to local storage
var success = Options.DummyMode || await _gitlabStorageService.PublishAsync("Application Profile Reporting", reportingFiles);
try
{
if (!Options.DummyMode)
{
success = await _gitlabStorageService.PublishAsync("Application Profile Reporting", reportingFiles);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "GitLab publish failed. Falling back to local storage.");
}
if (!success || Options.DummyMode)
{
_logger.LogInformation("Failed to publish to GitLab. Publishing to local storage instead.");
_logger.LogInformation("Publishing to local storage instead.");
success = await _localStorageService.PublishAsync("Application Profile Reporting", reportingFiles);
}
return success;
}
......@@ -88,7 +87,7 @@ public class ApplicationProfileReporting
_logger.LogDebug("Working on application profiles asynchronously...");
var applicationProfiles = PaginationHelper.GetAllAsync<ApplicationProfileDtoPagedResponse, ApplicationProfileDto>(
(pageNumber) => ApplicationProfileApi.GetApplicationProfilesAsync(pageNumber: pageNumber, pageSize: 500));
(pageNumber) => _applicationProfileApi.GetApplicationProfilesAsync(pageNumber: pageNumber, pageSize: 500));
_logger.LogInformation("Relevant application profiles found.");
await foreach (var ap in applicationProfiles)
......@@ -97,7 +96,7 @@ public class ApplicationProfileReporting
var g = new Graph();
try
{
var applicationProfile = await ApplicationProfileApi.GetApplicationProfileAsync(ap.Uri, RdfFormat.TextTurtle);
var applicationProfile = await _applicationProfileApi.GetApplicationProfileAsync(ap.Uri, RdfFormat.TextTurtle);
_logger.LogDebug("Application profile retrieved. Parsing...");
StringParser.Parse(g, applicationProfile.Data.Definition.Content);
_logger.LogDebug("Application profile parsed.");
......
using Coscine.KpiGenerator.Models;
using Coscine.KpiGenerator.Reportings.ApplicationProfile;
using Coscine.KpiGenerator.Reportings.Project;
using Coscine.KpiGenerator.Reportings.Resource;
using Coscine.KpiGenerator.Reportings.System;
using Coscine.KpiGenerator.Reportings.User;
using Coscine.KpiGenerator.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
......@@ -8,21 +11,7 @@ using static KPIGenerator.Utils.CommandLineOptions;
namespace Coscine.KpiGenerator.Reportings.Complete;
public class CompleteReporting
{
private readonly ProjectReporting _projectReporting;
private readonly ResourceReporting _resourceReporting;
private readonly UserReporting _userReporting;
private readonly ApplicationProfileReporting _applicationProfileReporting;
private readonly SystemReporting _systemReporting;
private readonly ILogger<CompleteReporting> _logger;
private readonly IStorageService _gitlabStorageService;
private readonly IStorageService _localStorageService;
public CompleteReportingOptions Options { get; private set; } = null!;
public CompleteReporting(
public class CompleteReporting(
ProjectReporting projectReporting,
ResourceReporting resourceReporting,
UserReporting userReporting,
......@@ -33,15 +22,16 @@ public class CompleteReporting
[FromKeyedServices("local")] IStorageService localStorageService
)
{
_projectReporting = projectReporting;
_resourceReporting = resourceReporting;
_userReporting = userReporting;
_applicationProfileReporting = applicationProfileReporting;
_systemReporting = systemReporting;
_logger = logger;
_gitlabStorageService = gitlabStorageService;
_localStorageService = localStorageService;
}
private readonly ProjectReporting _projectReporting = projectReporting;
private readonly ResourceReporting _resourceReporting = resourceReporting;
private readonly UserReporting _userReporting = userReporting;
private readonly ApplicationProfileReporting _applicationProfileReporting = applicationProfileReporting;
private readonly SystemReporting _systemReporting = systemReporting;
private readonly ILogger<CompleteReporting> _logger = logger;
private readonly IStorageService _gitlabStorageService = gitlabStorageService;
private readonly IStorageService _localStorageService = localStorageService;
public CompleteReportingOptions Options { get; private set; } = null!;
public async Task<bool> RunAsync(CompleteReportingOptions reportingOptions)
{
......@@ -49,14 +39,27 @@ public class CompleteReporting
Options = reportingOptions;
var reportingFiles = await GenerateReportingAsync();
// Publish to GitLab first, if that fails, publish to local storage
bool success = false;
_logger.LogInformation("Publishing to GitLab...");
var success = Options.DummyMode || await _gitlabStorageService.PublishAsync("Complete Reporting", reportingFiles);
// Publish to GitLab first, if that fails, publish to local storage
try
{
if (!Options.DummyMode)
{
success = await _gitlabStorageService.PublishAsync("Complete Reporting", reportingFiles);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "GitLab publish failed. Falling back to local storage.");
}
if (!success || Options.DummyMode)
{
_logger.LogInformation("Failed to publish to GitLab. Publishing to local storage instead.");
_logger.LogInformation("Publishing to local storage instead.");
success = await _localStorageService.PublishAsync("Complete Reporting", reportingFiles);
}
return success;
}
......
using AutoMapper;
using Coscine.ApiClient;
using Coscine.ApiClient.Core.Api;
using Coscine.ApiClient.Core.Client;
using Coscine.ApiClient.Core.Model;
using Coscine.KpiGenerator.Models;
using Coscine.KpiGenerator.Models.ConfigurationModels;
......@@ -22,10 +21,8 @@ public class ProjectReporting
private readonly IStorageService _localStorageService;
private readonly KpiConfiguration _kpiConfiguration;
private readonly ReportingConfiguration _reportingConfiguration;
public static string AdminToken { get; set; } = null!;
public AdminApi AdminApi { get; init; }
public ProjectQuotaApi ProjectQuotaApi { get; init; }
private readonly IAdminApi _adminApi;
private readonly IProjectQuotaApi _projectQuotaApi;
public ProjectReportingOptions Options { get; private set; } = null!;
public string ReportingFileName { get; }
......@@ -36,7 +33,9 @@ public class ProjectReporting
[FromKeyedServices("gitlab")] IStorageService gitlabStorageService,
[FromKeyedServices("local")] IStorageService localStorageService,
IOptionsMonitor<KpiConfiguration> kpiConfiguration,
IOptionsMonitor<ReportingConfiguration> reportingConfiguration
IOptionsMonitor<ReportingConfiguration> reportingConfiguration,
IAdminApi adminApi,
IProjectQuotaApi projectQuotaApi
)
{
_mapper = mapper;
......@@ -47,16 +46,8 @@ public class ProjectReporting
_reportingConfiguration = reportingConfiguration.CurrentValue;
ReportingFileName = _kpiConfiguration.ProjectKpi.FileName;
var configuration = new Configuration()
{
BasePath = $"{_reportingConfiguration.Endpoint.TrimEnd('/')}/coscine",
ApiKeyPrefix = { { "Authorization", "Bearer" } },
ApiKey = { { "Authorization", _reportingConfiguration.ApiKey } },
Timeout = 300000 // 5 minutes
};
AdminApi = new AdminApi(configuration);
ProjectQuotaApi = new ProjectQuotaApi(configuration);
_adminApi = adminApi;
_projectQuotaApi = projectQuotaApi;
}
public async Task<bool> RunAsync(ProjectReportingOptions reportingOptions)
......@@ -66,22 +57,35 @@ public class ProjectReporting
var reportingFiles = await GenerateReportingAsync();
bool success = false;
_logger.LogInformation("Publishing to GitLab...");
// Publish to GitLab first, if that fails, publish to local storage
var success = Options.DummyMode || await _gitlabStorageService.PublishAsync("Project Reporting", reportingFiles);
try
{
if (!Options.DummyMode)
{
success = await _gitlabStorageService.PublishAsync("Project Reporting", reportingFiles);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "GitLab publish failed. Falling back to local storage.");
}
if (!success || Options.DummyMode)
{
_logger.LogInformation("Failed to publish to GitLab. Publishing to local storage instead.");
_logger.LogInformation("Publishing to local storage instead.");
success = await _localStorageService.PublishAsync("Project Reporting", reportingFiles);
}
return success;
}
public async Task<IEnumerable<ReportingFileObject>> GenerateReportingAsync()
public virtual async Task<IEnumerable<ReportingFileObject>> GenerateReportingAsync()
{
_logger.LogDebug("Working on projects asynchronously...");
var projects = PaginationHelper.GetAllAsync<ProjectAdminDtoPagedResponse, ProjectAdminDto>(
(currentPage) => AdminApi.GetAllProjectsAsync(includeDeleted: false, includeQuotas: true, includePublicationRequests: true, pageNumber: currentPage, pageSize: 50));
(currentPage) => _adminApi.GetAllProjectsAsync(includeDeleted: false, includeQuotas: true, includePublicationRequests: true, pageNumber: currentPage, pageSize: 50));
var reportingFiles = new List<ReportingFileObject>();
var returnObjects = new List<ProjectReport>();
......