diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e819ecc763496f81ce8511e028ca3c47078704ca..9973627f3925e55a43e17a852fd066ec6ef0b750 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -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
   
diff --git a/src/KpiGenerator.Tests/KpiGenerator.Tests.csproj b/src/KpiGenerator.Tests/KpiGenerator.Tests.csproj
new file mode 100644
index 0000000000000000000000000000000000000000..f0551f1dfd963abf40463911f4ab67aecf8e0d52
--- /dev/null
+++ b/src/KpiGenerator.Tests/KpiGenerator.Tests.csproj
@@ -0,0 +1,30 @@
+<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>
diff --git a/src/KpiGenerator.Tests/ProjectReportingTests.cs b/src/KpiGenerator.Tests/ProjectReportingTests.cs
new file mode 100644
index 0000000000000000000000000000000000000000..5cd8eb95c982434bb0d5ad81c627b0fc0461d222
--- /dev/null
+++ b/src/KpiGenerator.Tests/ProjectReportingTests.cs
@@ -0,0 +1,294 @@
+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
+}
diff --git a/src/KpiGenerator.Tests/ResourceReportingTests.cs b/src/KpiGenerator.Tests/ResourceReportingTests.cs
new file mode 100644
index 0000000000000000000000000000000000000000..ebd6030c7a728c925b138f7a6d380d89ffd8022f
--- /dev/null
+++ b/src/KpiGenerator.Tests/ResourceReportingTests.cs
@@ -0,0 +1,312 @@
+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
+}
diff --git a/src/KpiGenerator.Tests/TestData.cs b/src/KpiGenerator.Tests/TestData.cs
new file mode 100644
index 0000000000000000000000000000000000000000..3566ead311a48847cc0ea34dc466b3ee3805a3b3
--- /dev/null
+++ b/src/KpiGenerator.Tests/TestData.cs
@@ -0,0 +1,150 @@
+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
diff --git a/src/KpiGenerator.Tests/UserReportingTests.cs b/src/KpiGenerator.Tests/UserReportingTests.cs
new file mode 100644
index 0000000000000000000000000000000000000000..5386dbd1b25154449f4d568d8308f4e094bb2fef
--- /dev/null
+++ b/src/KpiGenerator.Tests/UserReportingTests.cs
@@ -0,0 +1,333 @@
+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
+}
diff --git a/src/KpiGenerator.sln b/src/KpiGenerator.sln
index 43ff3942319cc241b3601c9cdfa131cf11266b2a..b970c6c0b9445c82b2333319c28b21d7a4b4705f 100644
--- a/src/KpiGenerator.sln
+++ b/src/KpiGenerator.sln
@@ -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
diff --git a/src/KpiGenerator/KpiGenerator.csproj b/src/KpiGenerator/KpiGenerator.csproj
index 31c784e9ac847b336e51904544675f97012f90a9..d43fceec1d6c47b0f16d54c7b696449360868801 100644
--- a/src/KpiGenerator/KpiGenerator.csproj
+++ b/src/KpiGenerator/KpiGenerator.csproj
@@ -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" />
diff --git a/src/KpiGenerator/MappingProfiles/MappingProfiles.cs b/src/KpiGenerator/MappingProfiles/MappingProfiles.cs
index ca15d23ecc330164c5e96a37dedbb779a04441bd..1b5e1b3a23b7e8ef664bf29dc2eb7514be117820 100644
--- a/src/KpiGenerator/MappingProfiles/MappingProfiles.cs
+++ b/src/KpiGenerator/MappingProfiles/MappingProfiles.cs
@@ -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));
@@ -63,6 +57,11 @@ public class MappingProfiles : Profile
         CreateMap<OrganizationDto, Organization>()
             .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))
diff --git a/src/KpiGenerator/Models/Organization.cs b/src/KpiGenerator/Models/Organization.cs
index 9b42922a25329b215beec2bf74f8b3bd627b1509..47be044c3545ed13f1556ea95503b8546d2b6b7f 100644
--- a/src/KpiGenerator/Models/Organization.cs
+++ b/src/KpiGenerator/Models/Organization.cs
@@ -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
diff --git a/src/KpiGenerator/Models/ProjectOrganization.cs b/src/KpiGenerator/Models/ProjectOrganization.cs
new file mode 100644
index 0000000000000000000000000000000000000000..cd84ccd3ea99c6de4346ece61397f37b94ea2a59
--- /dev/null
+++ b/src/KpiGenerator/Models/ProjectOrganization.cs
@@ -0,0 +1,9 @@
+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
diff --git a/src/KpiGenerator/Models/ProjectReport.cs b/src/KpiGenerator/Models/ProjectReport.cs
index fcd871b85fb60dad6c55d5d5a02cbeae64165c75..f410b5d95b39986ccdbcbb7552705eb1aacd011f 100644
--- a/src/KpiGenerator/Models/ProjectReport.cs
+++ b/src/KpiGenerator/Models/ProjectReport.cs
@@ -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!;
diff --git a/src/KpiGenerator/Models/ResourceReport.cs b/src/KpiGenerator/Models/ResourceReport.cs
index 8c1081218238d1c5b4e65a5ff4b70cba7ab2f0c4..ec79d732105fc3e9633cf198e52b100cbe7d597b 100644
--- a/src/KpiGenerator/Models/ResourceReport.cs
+++ b/src/KpiGenerator/Models/ResourceReport.cs
@@ -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!;
diff --git a/src/KpiGenerator/Models/UserOrganization.cs b/src/KpiGenerator/Models/UserOrganization.cs
new file mode 100644
index 0000000000000000000000000000000000000000..a22fadc85406b77bf603236afc4173a993cd66d3
--- /dev/null
+++ b/src/KpiGenerator/Models/UserOrganization.cs
@@ -0,0 +1,17 @@
+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
diff --git a/src/KpiGenerator/Models/UserReport.cs b/src/KpiGenerator/Models/UserReport.cs
index e21ff764cc94dc2b6abded1eceaee39255b98763..0e780dd1f45f253197071119bdc735ea7bdd220f 100644
--- a/src/KpiGenerator/Models/UserReport.cs
+++ b/src/KpiGenerator/Models/UserReport.cs
@@ -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!;
 
diff --git a/src/KpiGenerator/Program.cs b/src/KpiGenerator/Program.cs
index c1f05f50b2e040150549ff2ffa2b04665c1f2ad0..c33fd06b0b850383196ee54bd199f360180a0845 100644
--- a/src/KpiGenerator/Program.cs
+++ b/src/KpiGenerator/Program.cs
@@ -1,9 +1,14 @@
 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>();
diff --git a/src/KpiGenerator/Reportings/ApplicationProfile/ApplicationProfileReporting.cs b/src/KpiGenerator/Reportings/ApplicationProfile/ApplicationProfileReporting.cs
index 6170eada32f3e7d8f0a45122bcdee30b79daa493..c6818795c24fccac82c32e8dba5907b5079506df 100644
--- a/src/KpiGenerator/Reportings/ApplicationProfile/ApplicationProfileReporting.cs
+++ b/src/KpiGenerator/Reportings/ApplicationProfile/ApplicationProfileReporting.cs
@@ -1,7 +1,6 @@
 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.");
diff --git a/src/KpiGenerator/Reportings/Complete/CompleteReporting.cs b/src/KpiGenerator/Reportings/Complete/CompleteReporting.cs
index 05e489e36d3618be1c349a677107046d6ad355dc..1ca432d7c95db304df374a975cb449ee0dceafb0 100644
--- a/src/KpiGenerator/Reportings/Complete/CompleteReporting.cs
+++ b/src/KpiGenerator/Reportings/Complete/CompleteReporting.cs
@@ -1,6 +1,9 @@
 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,55 +11,55 @@ using static KPIGenerator.Utils.CommandLineOptions;
 
 namespace Coscine.KpiGenerator.Reportings.Complete;
 
-public class CompleteReporting
+public class CompleteReporting(
+    ProjectReporting projectReporting,
+    ResourceReporting resourceReporting,
+    UserReporting userReporting,
+    ApplicationProfileReporting applicationProfileReporting,
+    SystemReporting systemReporting,
+    ILogger<CompleteReporting> logger,
+    [FromKeyedServices("gitlab")] IStorageService gitlabStorageService,
+    [FromKeyedServices("local")] IStorageService localStorageService
+    )
 {
-    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;
+    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 CompleteReporting(
-        ProjectReporting projectReporting,
-        ResourceReporting resourceReporting,
-        UserReporting userReporting,
-        ApplicationProfileReporting applicationProfileReporting,
-        SystemReporting systemReporting,
-        ILogger<CompleteReporting> logger,
-        [FromKeyedServices("gitlab")] IStorageService gitlabStorageService,
-        [FromKeyedServices("local")] IStorageService localStorageService
-    )
-    {
-        _projectReporting = projectReporting;
-        _resourceReporting = resourceReporting;
-        _userReporting = userReporting;
-        _applicationProfileReporting = applicationProfileReporting;
-        _systemReporting = systemReporting;
-        _logger = logger;
-        _gitlabStorageService = gitlabStorageService;
-        _localStorageService = localStorageService;
-    }
-
     public async Task<bool> RunAsync(CompleteReportingOptions reportingOptions)
     {
         _logger.LogInformation("Generating Complete Reporting...");
         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;
     }
 
diff --git a/src/KpiGenerator/Reportings/Project/ProjectReporting.cs b/src/KpiGenerator/Reportings/Project/ProjectReporting.cs
index ddcd82274aa291d5cf1a2d665e7c9674bf6a9096..ba8d846a7cc1c037b190fc2a8c87bcc9d70777cf 100644
--- a/src/KpiGenerator/Reportings/Project/ProjectReporting.cs
+++ b/src/KpiGenerator/Reportings/Project/ProjectReporting.cs
@@ -1,7 +1,6 @@
 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>();
diff --git a/src/KpiGenerator/Reportings/Resource/ResourceReporting.cs b/src/KpiGenerator/Reportings/Resource/ResourceReporting.cs
index 942b49eb0cde99a19dbc47aea0feae3fffdb31d1..3857acb9fce15b9c7ad2c20902bc629b330642d0 100644
--- a/src/KpiGenerator/Reportings/Resource/ResourceReporting.cs
+++ b/src/KpiGenerator/Reportings/Resource/ResourceReporting.cs
@@ -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...");
diff --git a/src/KpiGenerator/Reportings/System/SystemReporting.cs b/src/KpiGenerator/Reportings/System/SystemReporting.cs
index c47dd82e32198048196caa5292f9cff4746dff58..b19a23c6b3aa32e755ad082174c58040e05ead71 100644
--- a/src/KpiGenerator/Reportings/System/SystemReporting.cs
+++ b/src/KpiGenerator/Reportings/System/SystemReporting.cs
@@ -1,8 +1,4 @@
-using AutoMapper;
-using Coscine.ApiClient;
-using Coscine.ApiClient.Core.Api;
-using Coscine.ApiClient.Core.Client;
-using Coscine.KpiGenerator.Models;
+using Coscine.KpiGenerator.Models;
 using Coscine.KpiGenerator.Models.ConfigurationModels;
 using Coscine.KpiGenerator.Utils;
 using Microsoft.Extensions.DependencyInjection;
@@ -12,12 +8,12 @@ using Newtonsoft.Json;
 using System.Net.Http.Headers;
 using System.Text;
 using static KPIGenerator.Utils.CommandLineOptions;
+using HttpMethod = System.Net.Http.HttpMethod;
 
-namespace Coscine.KpiGenerator.Reportings.Resource;
+namespace Coscine.KpiGenerator.Reportings.System;
 
 public class SystemReporting
 {
-    private readonly IMapper _mapper;
     private readonly ILogger<SystemReporting> _logger;
     private readonly IStorageService _gitlabStorageService;
     private readonly IStorageService _localStorageService;
@@ -25,15 +21,10 @@ public class SystemReporting
     private readonly ReportingConfiguration _reportingConfiguration;
     private readonly HttpClient _httpClient;
 
-    public static string AdminToken { get; set; } = null!;
-    public AdminApi AdminApi { get; init; }
-    public ProjectApi ProjectApi { get; init; }
-
     public SystemReportingOptions Options { get; private set; } = null!;
     public string ReportingFileName { get; }
 
     public SystemReporting(
-        IMapper mapper,
         IHttpClientFactory httpClientFactory,
         ILogger<SystemReporting> logger,
         [FromKeyedServices("gitlab")] IStorageService gitlabStorageService,
@@ -42,7 +33,6 @@ public class SystemReporting
         IOptionsMonitor<ReportingConfiguration> reportingConfiguration
     )
     {
-        _mapper = mapper;
         _logger = logger;
         _gitlabStorageService = gitlabStorageService;
         _localStorageService = localStorageService;
@@ -50,17 +40,6 @@ public class SystemReporting
         _httpClient = httpClientFactory.CreateClient("MaintenanceClient");
         _reportingConfiguration = reportingConfiguration.CurrentValue;
         ReportingFileName = _kpiConfiguration.SystemKpi.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);
     }
 
     public async Task<bool> RunAsync(SystemReportingOptions reportingOptions)
@@ -69,14 +48,27 @@ public class SystemReporting
         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("System Reporting", reportingFiles);
+        try
+        {
+            if (!Options.DummyMode)
+            {
+                success = await _gitlabStorageService.PublishAsync("System 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("System Reporting", reportingFiles);
         }
+
         return success;
     }
 
@@ -107,7 +99,7 @@ public class SystemReporting
         var requestMessage = new HttpRequestMessage()
         {
             RequestUri = new Uri(_kpiConfiguration.SystemKpi.Maintenance.Url),
-            Method = System.Net.Http.HttpMethod.Get
+            Method = HttpMethod.Get, // Use global alias
         };
 
         // Add Basic Authentication Headers
diff --git a/src/KpiGenerator/Reportings/User/UserReporting.cs b/src/KpiGenerator/Reportings/User/UserReporting.cs
index 7165448c716fe50dd2959191551dad9dd202b686..8db2ad4d1e6c4720e521f501d80a401e2d6bbda6 100644
--- a/src/KpiGenerator/Reportings/User/UserReporting.cs
+++ b/src/KpiGenerator/Reportings/User/UserReporting.cs
@@ -1,7 +1,6 @@
 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;
@@ -12,7 +11,7 @@ using Microsoft.Extensions.Options;
 using Newtonsoft.Json;
 using static KPIGenerator.Utils.CommandLineOptions;
 
-namespace Coscine.KpiGenerator.Reportings.Resource;
+namespace Coscine.KpiGenerator.Reportings.User;
 
 public class UserReporting
 {
@@ -22,11 +21,8 @@ public class UserReporting
     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 RoleApi RoleApi { get; init; }
+    private readonly IAdminApi _adminApi;
+    private readonly IRoleApi _roleApi;
 
     public UserReportingOptions Options { get; private set; } = null!;
     public string ReportingFileName { get; }
@@ -37,7 +33,9 @@ public class UserReporting
         [FromKeyedServices("gitlab")] IStorageService gitlabStorageService,
         [FromKeyedServices("local")] IStorageService localStorageService,
         IOptionsMonitor<KpiConfiguration> kpiConfiguration,
-        IOptionsMonitor<ReportingConfiguration> reportingConfiguration
+        IOptionsMonitor<ReportingConfiguration> reportingConfiguration,
+        IAdminApi adminApi,
+        IRoleApi roleApi
     )
     {
         _mapper = mapper;
@@ -48,17 +46,8 @@ public class UserReporting
         _reportingConfiguration = reportingConfiguration.CurrentValue;
         ReportingFileName = _kpiConfiguration.UserKpi.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);
-        RoleApi = new RoleApi(configuration);
+        _adminApi = adminApi;
+        _roleApi = roleApi;
     }
 
     public async Task<bool> RunAsync(UserReportingOptions reportingOptions)
@@ -67,32 +56,45 @@ public class UserReporting
         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("User Reporting", reportingFiles);
+        try
+        {
+            if (!Options.DummyMode)
+            {
+                success = await _gitlabStorageService.PublishAsync("User 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("User 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: false, pageNumber: currentPage, pageSize: 50)).ToListAsync();
+                (currentPage) => _adminApi.GetAllProjectsAsync(includeDeleted: false, pageNumber: currentPage, pageSize: 50)).ToListAsync();
         _logger.LogDebug("Got all projects.");
 
         _logger.LogDebug("Getting all roles...");
         var roles = await PaginationHelper.GetAllAsync<RoleDtoPagedResponse, RoleDto>(
-            (currentPage) => RoleApi.GetRolesAsync(pageNumber: currentPage, pageSize: 50)).ToListAsync();
+            (currentPage) => _roleApi.GetRolesAsync(pageNumber: currentPage, pageSize: 50)).ToListAsync();
         _logger.LogDebug("Got all roles.");
 
         _logger.LogDebug("Working on users asynchronously...");
         var users = PaginationHelper.GetAllAsync<UserDtoPagedResponse, UserDto>(
-            (currentPage) => AdminApi.GetAllUsersAsync(tosAccepted: true, pageNumber: currentPage, pageSize: 50));
+            (currentPage) => _adminApi.GetAllUsersAsync(tosAccepted: true, pageNumber: currentPage, pageSize: 50));
 
         var reportingFiles = new List<ReportingFileObject>();
         var returnObjects = new List<UserReport>();
diff --git a/src/KpiGenerator/Utils/Helpers.cs b/src/KpiGenerator/Utils/Helpers.cs
index 4ce269aba4aaa8b5da199a70e00a033cffef373f..e0e9cdf4a321939df13b01d7d0642dd164d6884d 100644
--- a/src/KpiGenerator/Utils/Helpers.cs
+++ b/src/KpiGenerator/Utils/Helpers.cs
@@ -11,7 +11,11 @@ public static class Helpers
         var result = new List<Organization>();
         foreach (var org in organizations.Where(o => o is not null && o.RorUrl is not null))
         {
-            var ror = org.RorUrl;
+            var ror = org?.RorUrl;
+            if (string.IsNullOrWhiteSpace(ror) || org is null)
+            {
+                continue;
+            }
             result.Add(new Organization
             {
                 RorUrl = ror.Contains('#') ? ror[..ror.IndexOf('#')] : ror, // e.g. <https://ror.org/04xfq0f34#ORG-42NHW> turns into <https://ror.org/04xfq0f34>