diff --git a/src/Api/Dirt/Controllers/ReportsController.cs b/src/Api/Dirt/Controllers/ReportsController.cs index 8bb8b5e487..e7c7e4a9bf 100644 --- a/src/Api/Dirt/Controllers/ReportsController.cs +++ b/src/Api/Dirt/Controllers/ReportsController.cs @@ -281,4 +281,127 @@ public class ReportsController : Controller } return await _getOrganizationReportQuery.GetLatestOrganizationReportAsync(orgId); } + + /// + /// Gets the Organization Report Summary for an organization. + /// This includes the latest report's encrypted data, encryption key, and date. + /// This is a mock implementation and should be replaced with actual data retrieval logic. + /// + /// + /// Min date (example: 2023-01-01) + /// Max date (example: 2023-12-31) + /// + /// + [HttpGet("organization-report-summary/{orgId}")] + public IEnumerable GetOrganizationReportSummary( + [FromRoute] Guid orgId, + [FromQuery] DateOnly from, + [FromQuery] DateOnly to) + { + if (!ModelState.IsValid) + { + throw new BadRequestException(ModelState); + } + + GuardOrganizationAccess(orgId); + + // FIXME: remove this mock class when actual data retrieval is implemented + return MockOrganizationReportSummary.GetMockData() + .Where(_ => _.OrganizationId == orgId + && _.Date >= from.ToDateTime(TimeOnly.MinValue) + && _.Date <= to.ToDateTime(TimeOnly.MaxValue)); + } + + /// + /// Creates a new Organization Report Summary for an organization. + /// This is a mock implementation and should be replaced with actual creation logic. + /// + /// + /// Returns 204 Created with the created OrganizationReportSummaryModel + /// + [HttpPost("organization-report-summary")] + public IActionResult CreateOrganizationReportSummary([FromBody] OrganizationReportSummaryModel model) + { + if (!ModelState.IsValid) + { + throw new BadRequestException(ModelState); + } + + GuardOrganizationAccess(model.OrganizationId); + + // TODO: Implement actual creation logic + + // Returns 204 No Content as a placeholder + return NoContent(); + } + + [HttpPut("organization-report-summary")] + public IActionResult UpdateOrganizationReportSummary([FromBody] OrganizationReportSummaryModel model) + { + if (!ModelState.IsValid) + { + throw new BadRequestException(ModelState); + } + + GuardOrganizationAccess(model.OrganizationId); + + // TODO: Implement actual update logic + + // Returns 204 No Content as a placeholder + return NoContent(); + } + + private void GuardOrganizationAccess(Guid organizationId) + { + if (!_currentContext.AccessReports(organizationId).Result) + { + throw new NotFoundException(); + } + } + + // FIXME: remove this mock class when actual data retrieval is implemented + private class MockOrganizationReportSummary + { + public static List GetMockData() + { + return new List + { + new OrganizationReportSummaryModel + { + OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), + EncryptedData = "2.EtCcxDEBoF1MYChYHC4Q1w==|RyZ07R7qEFBbc/ICLFpEMockL9K+PD6rOod6DGHHrkaRLHUDqDwmxbu3jnD0cg8s7GIYmp0jApHXC+82QdApk87pA0Kr8fN2Rj0+8bDQCjhKfoRTipAB25S/n2E+ttjvlFfag92S66XqUH9S/eZw/Q==|0bPfykHk3SqS/biLNcNoYtH6YTstBEKu3AhvdZZLxhU=", + EncryptionKey = "2.Dd/TtdNwxWdYg9+fRkxh6w==|8KAiK9SoadgFRmyVOchd4tNh2vErD1Rv9x1gqtsE5tzxKE/V/5kkr1WuVG+QpEj//YaQt221UEMESRSXicZ7a9cB6xXLBkbbFwmecQRJVBs=|902em44n9cwciZzYrYuX6MRzRa+4hh1HHfNAxyJx/IM=", + Date = DateTime.UtcNow + }, + new OrganizationReportSummaryModel + { + OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), + EncryptedData = "2.HvY4fAvbzYV1hqa3255m5Q==|WcKga2Wka5i8fVso8MgjzfBAwxaqdhZDL3bnvhDsisZ0r9lNKQcG3YUQSFpJxr74cgg5QRQaFieCUe2YppciHDT6bsaE2VzFce3cNNB821uTFqnlJClkGJpG1nGvPupdErrg4Ik57WenEzYesmR4pw==|F0aJfF+1MlPm+eAlQnDgFnwfv198N9VtPqFJa4+UFqk=", + EncryptionKey = "2.ctMgLN4ycPusbQArG/uiag==|NtqiQsAoUxMSTBQsxAMyVLWdt5lVEUGZQNxZSBU4l76ywH2f6dx5FWFrcF3t3GBqy5yDoc5eBg0VlJDW9coqzp8j9n8h1iMrtmXPyBMAhbc=|pbH+w68BUdUKYCfNRpjd8NENw2lZ0vfxgMuTrsrRCTQ=", + Date = DateTime.UtcNow.AddMonths(-1) + }, + new OrganizationReportSummaryModel + { + OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), + EncryptedData = "2.NH4qLZYUkz/+qpB/mRsLTA==|LEFt05jJz0ngh+Hl5lqk6kebj7lZMefA3eFdL1kLJSGdD3uTOngRwH7GXLQNFeQOxutnLX9YUILbUEPwaM8gCwNQ1KWYdB1Z+Ky4nzKRb60N7L5aTA2za6zXTIdjv7Zwhg0jPZ6sPevTuvSyqjMCuA==|Uuu6gZaF0wvB2mHFwtvHegMxfe8DgsYWTRfGiVn4lkM=", + EncryptionKey = "2.3YwG78ykSxAn44NcymdG4w==|4jfn0nLoFielicAFbmq27DNUUjV4SwGePnjYRmOa7hk4pEPnQRS3MsTJFbutVyXOgKFY9Yn2yGFZownY9EmXOMM+gHPD0t6TfzUKqQcRyuI=|wasP9zZEL9mFH5HzJYrMxnKUr/XlFKXCxG9uW66uaPU=", + Date = DateTime.UtcNow.AddMonths(-1) + }, + new OrganizationReportSummaryModel + { + OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), + EncryptedData = "2.YmKWj/707wDPONh+JXPBOw==|Fx4jcUHmnUnSMCU8vdThMSYpDyKPnC09TxpSbNxia0M6MFbd5WHElcVribrYgTENyU0HlqPW43hThJ6xXCM0EjEWP7/jb/0l07vMNkA7sDYq+czf0XnYZgZSGKh06wFVz8xkhaPTdsiO4CXuMsoH+w==|DDVwVFHzdfbPQe3ycCx82eYVHDW97V/eWTPsNpHX/+U=", + EncryptionKey = "2.f/U45I7KF+JKfnvOArUyaw==|zNhhS2q2WwBl6SqLWMkxrXC8EX91Ra9LJExywkJhsRbxubRLt7fK+YWc8T1LUaDmMwJ3G8buSPGzyacKX0lnUR33dW6DIaLNgRZ/ekb/zkg=|qFoIZWwS0foiiIOyikFRwQKmmmI2HeyHcOVklJnIILI=", + Date = DateTime.UtcNow.AddMonths(-1) + }, + new OrganizationReportSummaryModel + { + OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), + EncryptedData = "2.WYauwooJUEY3kZsDPphmrA==|oguYW6h10A4GxK4KkRS0X32qSTekU2CkGqNDNGfisUgvJzsyoVTafO9sVcdPdg4BUM7YNkPMjYiKEc5jMHkIgLzbnM27jcGvMJrrccSrLHiWL6/mEiqQkV3TlfiZF9i3wqj1ITsYRzM454uNle6Wrg==|uR67aFYb1i5LSidWib0iTf8091l8GY5olHkVXse3CAw=", + EncryptionKey = "2.ZyV9+9A2cxNaf8dfzfbnlA==|hhorBpVkcrrhTtNmd6SNHYI8gPNokGLOC22Vx8Qa/AotDAcyuYWw56zsawMnzpAdJGEJFtszKM2+VUVOcroCTMWHpy8yNf/kZA6uPk3Lz3s=|ASzVeJf+K1ZB8NXuypamRBGRuRq0GUHZBEy5r/O7ORY=", + Date = DateTime.UtcNow.AddMonths(-1) + }, + }; + } + } } diff --git a/src/Api/Dirt/Models/Response/OrganizationReportSummaryModel.cs b/src/Api/Dirt/Models/Response/OrganizationReportSummaryModel.cs new file mode 100644 index 0000000000..d912fb699e --- /dev/null +++ b/src/Api/Dirt/Models/Response/OrganizationReportSummaryModel.cs @@ -0,0 +1,9 @@ +namespace Bit.Api.Dirt.Models.Response; + +public class OrganizationReportSummaryModel +{ + public Guid OrganizationId { get; set; } + public required string EncryptedData { get; set; } + public required string EncryptionKey { get; set; } + public DateTime Date { get; set; } +} diff --git a/test/Api.Test/Dirt/ReportsControllerTests.cs b/test/Api.Test/Dirt/ReportsControllerTests.cs index af285d8b85..4636406df5 100644 --- a/test/Api.Test/Dirt/ReportsControllerTests.cs +++ b/test/Api.Test/Dirt/ReportsControllerTests.cs @@ -1,12 +1,14 @@ using AutoFixture; using Bit.Api.Dirt.Controllers; using Bit.Api.Dirt.Models; +using Bit.Api.Dirt.Models.Response; using Bit.Core.Context; using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; using Bit.Core.Dirt.Reports.ReportFeatures.Requests; using Bit.Core.Exceptions; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Mvc; using NSubstitute; using Xunit; @@ -280,4 +282,185 @@ public class ReportsControllerTests _ = sutProvider.GetDependency() .Received(0); } + + [Theory, BitAutoData] + public void CreateOrganizationReportSummary_ReturnsNoContent_WhenAccessGranted(SutProvider sutProvider) + { + // Arrange + var orgId = Guid.NewGuid(); + var model = new OrganizationReportSummaryModel + { + OrganizationId = orgId, + EncryptedData = "mock-data", + EncryptionKey = "mock-key", + Date = DateTime.UtcNow + }; + sutProvider.GetDependency().AccessReports(orgId).Returns(true); + + // Act + var result = sutProvider.Sut.CreateOrganizationReportSummary(model); + + // Assert + Assert.IsType(result); + } + + [Theory, BitAutoData] + public void CreateOrganizationReportSummary_ThrowsNotFoundException_WhenAccessDenied(SutProvider sutProvider) + { + // Arrange + var orgId = Guid.NewGuid(); + var model = new OrganizationReportSummaryModel + { + OrganizationId = orgId, + EncryptedData = "mock-data", + EncryptionKey = "mock-key", + Date = DateTime.UtcNow + }; + sutProvider.GetDependency().AccessReports(orgId).Returns(false); + + // Act & Assert + Assert.Throws( + () => sutProvider.Sut.CreateOrganizationReportSummary(model)); + } + + [Theory, BitAutoData] + public void GetOrganizationReportSummary_ThrowsNotFoundException_WhenAccessDenied( + SutProvider sutProvider + ) + { + // Arrange + var orgId = Guid.NewGuid(); + + sutProvider.GetDependency().AccessReports(orgId).Returns(false); + + // Act & Assert + Assert.Throws( + () => sutProvider.Sut.GetOrganizationReportSummary(orgId, DateOnly.FromDateTime(DateTime.UtcNow), DateOnly.FromDateTime(DateTime.UtcNow))); + } + + [Theory, BitAutoData] + public void GetOrganizationReportSummary_returnsExpectedResult( + SutProvider sutProvider + ) + { + // Arrange + var orgId = Guid.NewGuid(); + var dates = new[] + { + DateOnly.FromDateTime(DateTime.UtcNow), + DateOnly.FromDateTime(DateTime.UtcNow.AddMonths(-1)) + }; + + sutProvider.GetDependency().AccessReports(orgId).Returns(true); + + // Act + var result = sutProvider.Sut.GetOrganizationReportSummary(orgId, dates[0], dates[1]); + + // Assert + Assert.NotNull(result); + } + + [Theory, BitAutoData] + public void CreateOrganizationReportSummary_ReturnsNoContent_WhenModelIsValidAndAccessGranted( + SutProvider sutProvider + ) + { + // Arrange + var orgId = Guid.NewGuid(); + var model = new OrganizationReportSummaryModel + { + OrganizationId = orgId, + EncryptedData = "mock-data", + EncryptionKey = "mock-key" + }; + sutProvider.Sut.ModelState.Clear(); + sutProvider.GetDependency().AccessReports(orgId).Returns(true); + + // Act + var result = sutProvider.Sut.CreateOrganizationReportSummary(model); + + // Assert + Assert.IsType(result); + } + + [Theory, BitAutoData] + public void CreateOrganizationReportSummary_ThrowsBadRequestException_WhenModelStateIsInvalid( + SutProvider sutProvider + ) + { + // Arrange + var orgId = Guid.NewGuid(); + var model = new OrganizationReportSummaryModel + { + OrganizationId = orgId, + EncryptedData = "mock-data", + EncryptionKey = "mock-key" + }; + sutProvider.Sut.ModelState.AddModelError("key", "error"); + + // Act & Assert + Assert.Throws(() => sutProvider.Sut.CreateOrganizationReportSummary(model)); + } + + [Theory, BitAutoData] + public void UpdateOrganizationReportSummary_ReturnsNoContent_WhenModelIsValidAndAccessGranted( + SutProvider sutProvider + ) + { + // Arrange + var orgId = Guid.NewGuid(); + var model = new OrganizationReportSummaryModel + { + OrganizationId = orgId, + EncryptedData = "mock-data", + EncryptionKey = "mock-key" + }; + sutProvider.Sut.ModelState.Clear(); + sutProvider.GetDependency().AccessReports(orgId).Returns(true); + + // Act + var result = sutProvider.Sut.UpdateOrganizationReportSummary(model); + + // Assert + Assert.IsType(result); + } + + [Theory, BitAutoData] + public void UpdateOrganizationReportSummary_ThrowsBadRequestException_WhenModelStateIsInvalid( + SutProvider sutProvider + ) + { + // Arrange + var orgId = Guid.NewGuid(); + var model = new OrganizationReportSummaryModel + { + OrganizationId = orgId, + EncryptedData = "mock-data", + EncryptionKey = "mock-key" + }; + sutProvider.Sut.ModelState.AddModelError("key", "error"); + + // Act & Assert + Assert.Throws(() => sutProvider.Sut.UpdateOrganizationReportSummary(model)); + } + + [Theory, BitAutoData] + public void UpdateOrganizationReportSummary_ThrowsNotFoundException_WhenAccessDenied( + SutProvider sutProvider + ) + { + // Arrange + var orgId = Guid.NewGuid(); + var model = new OrganizationReportSummaryModel + { + OrganizationId = orgId, + EncryptedData = "mock-data", + EncryptionKey = "mock-key" + }; + sutProvider.Sut.ModelState.Clear(); + sutProvider.GetDependency().AccessReports(orgId).Returns(false); + + // Act & Assert + Assert.Throws(() => sutProvider.Sut.UpdateOrganizationReportSummary(model)); + } }