Skip to content
Snippets Groups Projects
Commit e03ffc4f authored by Petar Hristov's avatar Petar Hristov :speech_balloon:
Browse files

Merge branch 'dev' into 'main'

Dev

See merge request !43
parents a616611e 8f5248ed
Branches
Tags
1 merge request!43Dev
Pipeline #1608213 passed
Showing
with 1294 additions and 119 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
......
......@@ -13,7 +13,7 @@
<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>();
......
......@@ -22,11 +22,7 @@ public class ResourceReporting
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 ProjectResourceQuotaApi ProjectResourceQuotaApi { get; init; }
public ProjectApi ProjectApi { get; init; }
private readonly IAdminApi _adminApi;
public ResourceReportingOptions Options { get; private set; } = null!;
public string ReportingFileName { get; }
......@@ -37,7 +33,8 @@ public class ResourceReporting
[FromKeyedServices("gitlab")] IStorageService gitlabStorageService,
[FromKeyedServices("local")] IStorageService localStorageService,
IOptionsMonitor<KpiConfiguration> kpiConfiguration,
IOptionsMonitor<ReportingConfiguration> reportingConfiguration
IOptionsMonitor<ReportingConfiguration> reportingConfiguration,
IAdminApi adminApi
)
{
_mapper = mapper;
......@@ -56,9 +53,7 @@ public class ResourceReporting
Timeout = 300000 // 5 minutes
};
AdminApi = new AdminApi(configuration);
ProjectApi = new ProjectApi(configuration);
ProjectResourceQuotaApi = new ProjectResourceQuotaApi(configuration);
_adminApi = adminApi;
}
public async Task<bool> RunAsync(ResourceReportingOptions reportingOptions)
......@@ -67,27 +62,40 @@ public class ResourceReporting
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("Resources Reporting", reportingFiles);
try
{
if (!Options.DummyMode)
{
success = await _gitlabStorageService.PublishAsync("Resource 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.");
success = await _localStorageService.PublishAsync("Resources Reporting", reportingFiles);
_logger.LogInformation("Publishing to local storage instead.");
success = await _localStorageService.PublishAsync("Resource Reporting", reportingFiles);
}
return success;
}
public async Task<IEnumerable<ReportingFileObject>> GenerateReportingAsync()
public virtual async Task<IEnumerable<ReportingFileObject>> GenerateReportingAsync()
{
_logger.LogDebug("Getting all projects...");
var projects = await PaginationHelper.GetAllAsync<ProjectAdminDtoPagedResponse, ProjectAdminDto>(
(currentPage) => AdminApi.GetAllProjectsAsync(includeDeleted: true, pageNumber: currentPage, pageSize: 50)).ToListAsync();
(currentPage) => _adminApi.GetAllProjectsAsync(includeDeleted: true, pageNumber: currentPage, pageSize: 50)).ToListAsync();
_logger.LogDebug("Got all projects.");
_logger.LogDebug("Working on resources asynchronously...");
var resources = PaginationHelper.GetAllAsync<ResourceAdminDtoPagedResponse, ResourceAdminDto>(
(currentPage) => AdminApi.GetAllResourcesAsync(includeDeleted: false, includeQuotas: true, pageNumber: currentPage, pageSize: 50));
(currentPage) => _adminApi.GetAllResourcesAsync(includeDeleted: false, includeQuotas: true, pageNumber: currentPage, pageSize: 50));
var reportingFiles = new List<ReportingFileObject>();
var returnObjects = new List<ResourceReport>();
......@@ -102,7 +110,7 @@ public class ResourceReporting
_logger.LogDebug("Processed related project {projectId}...", project?.Id);
if (project != null)
{
var o = _mapper.Map<List<Organization>>(project?.Organizations);
var o = _mapper.Map<List<ProjectOrganization>>(project?.Organizations);
returnObject.Organizations = o ?? [];
}
_logger.LogDebug("Processed organizations...");
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment