Skip to content

Commit

Permalink
Add event metric backend query interface
Browse files Browse the repository at this point in the history
  • Loading branch information
pmachapman committed Dec 15, 2024
1 parent ce77f33 commit 13226aa
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 4 deletions.
33 changes: 33 additions & 0 deletions src/SIL.XForge.Scripture/Controllers/SFProjectsRpcController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,39 @@ public async Task<IRpcMethodResult> DeleteTrainingData(string projectId, string
}
}

public async Task<IRpcMethodResult> EventMetrics(string projectId, int pageIndex, int pageSize)
{
try
{
return Ok(await projectService.GetEventMetricsAsync(UserId, SystemRoles, projectId, pageIndex, pageSize));
}
catch (ForbiddenException)
{
return ForbiddenError();
}
catch (DataNotFoundException dnfe)
{
return NotFoundError(dnfe.Message);
}
catch (FormatException fe)
{
return InvalidParamsError(fe.Message);
}
catch (Exception)
{
_exceptionHandler.RecordEndpointInfoForException(
new Dictionary<string, string>
{
{ "method", "EventMetrics" },
{ "projectId", projectId },
{ "pageIndex", pageIndex.ToString() },
{ "pageSize", pageSize.ToString() },
}
);
throw;
}
}

public IRpcMethodResult RetrievePreTranslationStatus(string projectId)
{
try
Expand Down
7 changes: 7 additions & 0 deletions src/SIL.XForge.Scripture/Services/ISFProjectService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ Task<string> GetLinkSharingKeyAsync(
int daysBeforeExpiration
);
Task<ValidShareKey> CheckShareKeyValidity(string shareKey);
Task<IEnumerable<EventMetric>> GetEventMetricsAsync(
string curUserId,
string[] systemRoles,
string projectId,
int pageIndex,
int pageSize
);
Task<SFProject> GetProjectAsync(string projectId);
SFProjectSecret GetProjectSecretByShareKey(string shareKey);
Task ReserveLinkSharingKeyAsync(string curUserId, string shareKey, int daysBeforeExpiration);
Expand Down
49 changes: 49 additions & 0 deletions src/SIL.XForge.Scripture/Services/SFProjectService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Newtonsoft.Json.Linq;
using SIL.XForge.Configuration;
using SIL.XForge.DataAccess;
using SIL.XForge.EventMetrics;
using SIL.XForge.Models;
using SIL.XForge.Realtime;
using SIL.XForge.Realtime.Json0;
Expand Down Expand Up @@ -43,6 +44,7 @@ public class SFProjectService : ProjectService<SFProject, SFProjectSecret>, ISFP
private readonly ISecurityService _securityService;
private readonly IStringLocalizer<SharedResource> _localizer;
private readonly ITransceleratorService _transceleratorService;
private readonly IEventMetricService _eventMetricService;
private readonly ISFProjectRights _projectRights;

public SFProjectService(
Expand All @@ -62,6 +64,7 @@ public SFProjectService(
IStringLocalizer<SharedResource> localizer,
ITransceleratorService transceleratorService,
IBackgroundJobClient backgroundJobClient,
IEventMetricService eventMetricService,
ISFProjectRights projectRights
)
: base(realtimeService, siteOptions, audioService, projectSecrets, fileSystemService)
Expand All @@ -76,6 +79,7 @@ ISFProjectRights projectRights
_securityService = securityService;
_localizer = localizer;
_transceleratorService = transceleratorService;
_eventMetricService = eventMetricService;
_backgroundJobClient = backgroundJobClient;
_projectRights = projectRights;
}
Expand Down Expand Up @@ -958,6 +962,51 @@ public async Task<ValidShareKey> CheckShareKeyValidity(string shareKey)
};
}

public async Task<IEnumerable<EventMetric>> GetEventMetricsAsync(
string curUserId,
string[] systemRoles,
string projectId,
int pageIndex,
int pageSize
)
{
// Ensure that the page index is valid
if (pageIndex < 0)
{
throw new FormatException($"{nameof(pageIndex)} is not a valid page index.");
}

// Ensure that the page size is valid
if (pageSize <= 0)
{
throw new FormatException($"{nameof(pageSize)} is not a valid page size.");
}

await using IConnection conn = await RealtimeService.ConnectAsync(curUserId);
IDocument<SFProject> projectDoc = await conn.FetchAsync<SFProject>(projectId);

// Ensure that the project exists
if (!projectDoc.IsLoaded)
{
throw new DataNotFoundException("The project does not exist.");
}

// The user must be an admin on the project, or have system admin or serval admin permissions
if (
!(
IsProjectAdmin(projectDoc.Data, curUserId)
|| systemRoles.Contains(SystemRole.SystemAdmin)
|| systemRoles.Contains(SystemRole.ServalAdmin)
)
)
{
throw new ForbiddenException();
}

// Return the event metrics
return _eventMetricService.GetEventMetrics(projectId, pageIndex, pageSize);
}

public SFProjectSecret GetProjectSecretByShareKey(string shareKey)
{
SFProjectSecret projectSecret =
Expand Down
3 changes: 2 additions & 1 deletion src/SIL.XForge/Services/EventMetricService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ namespace SIL.XForge.Services;

public class EventMetricService(IRepository<EventMetric> eventMetrics) : IEventMetricService
{
public IEnumerable<EventMetric> GetEventMetrics(string? projectId, int pageIndex, int pageSize = 10)
public IEnumerable<EventMetric> GetEventMetrics(string? projectId, int pageIndex, int pageSize)
{
// Do not allow querying of event metrics without a project identifier
if (projectId is null)
{
return [];
Expand Down
2 changes: 1 addition & 1 deletion src/SIL.XForge/Services/IEventMetricService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace SIL.XForge.Services;

public interface IEventMetricService
{
IEnumerable<EventMetric> GetEventMetrics(string? projectId, int pageIndex, int pageSize = 10);
IEnumerable<EventMetric> GetEventMetrics(string? projectId, int pageIndex, int pageSize);
Task SaveEventMetricAsync(
string? projectId,
string? userId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,81 @@ public void DeleteTrainingData_UnknownError()
env.ExceptionHandler.Received().RecordEndpointInfoForException(Arg.Any<Dictionary<string, string>>());
}

[Test]
public async Task EventMetrics_Success()
{
var env = new TestEnvironment();
const int pageIndex = 0;
const int pageSize = 10;

// SUT
var result = await env.Controller.EventMetrics(Project01, pageIndex, pageSize);
Assert.IsInstanceOf<RpcMethodSuccessResult>(result);
await env.SFProjectService.Received().GetEventMetricsAsync(User01, Roles, Project01, pageIndex, pageSize);
}

[Test]
public async Task EventMetrics_Forbidden()
{
var env = new TestEnvironment();
const int pageIndex = 0;
const int pageSize = 10;
env.SFProjectService.GetEventMetricsAsync(User01, Roles, Project01, pageIndex, pageSize)
.Throws(new ForbiddenException());

// SUT
var result = await env.Controller.EventMetrics(Project01, pageIndex, pageSize);
Assert.IsInstanceOf<RpcMethodErrorResult>(result);
Assert.AreEqual(RpcControllerBase.ForbiddenErrorCode, (result as RpcMethodErrorResult)!.ErrorCode);
}

[Test]
public async Task EventMetrics_InvalidParams()
{
var env = new TestEnvironment();
const int pageIndex = 0;
const int pageSize = 10;
const string errorMessage = "Invalid Format";
env.SFProjectService.GetEventMetricsAsync(User01, Roles, Project01, pageIndex, pageSize)
.Throws(new FormatException(errorMessage));

// SUT
var result = await env.Controller.EventMetrics(Project01, pageIndex, pageSize);
Assert.IsInstanceOf<RpcMethodErrorResult>(result);
Assert.AreEqual(errorMessage, (result as RpcMethodErrorResult)!.Message);
}

[Test]
public async Task EventMetrics_NotFound()
{
var env = new TestEnvironment();
const int pageIndex = 0;
const int pageSize = 10;
const string errorMessage = "Not Found";
env.SFProjectService.GetEventMetricsAsync(User01, Roles, Project01, pageIndex, pageSize)
.Throws(new DataNotFoundException(errorMessage));

// SUT
var result = await env.Controller.EventMetrics(Project01, pageIndex, pageSize);
Assert.IsInstanceOf<RpcMethodErrorResult>(result);
Assert.AreEqual(errorMessage, (result as RpcMethodErrorResult)!.Message);
Assert.AreEqual(RpcControllerBase.NotFoundErrorCode, (result as RpcMethodErrorResult)!.ErrorCode);
}

[Test]
public void EventMetrics_UnknownError()
{
var env = new TestEnvironment();
const int pageIndex = 0;
const int pageSize = 10;
env.SFProjectService.GetEventMetricsAsync(User01, Roles, Project01, pageIndex, pageSize)
.Throws(new ArgumentNullException());

// SUT
Assert.ThrowsAsync<ArgumentNullException>(() => env.Controller.EventMetrics(Project01, pageIndex, pageSize));
env.ExceptionHandler.Received().RecordEndpointInfoForException(Arg.Any<Dictionary<string, string>>());
}

[Test]
public async Task Invite_Success()
{
Expand Down
124 changes: 124 additions & 0 deletions test/SIL.XForge.Scripture.Tests/Services/SFProjectServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using NUnit.Framework;
using SIL.XForge.Configuration;
using SIL.XForge.DataAccess;
using SIL.XForge.EventMetrics;
using SIL.XForge.Models;
using SIL.XForge.Realtime;
using SIL.XForge.Scripture.Models;
Expand Down Expand Up @@ -4074,6 +4075,126 @@ public void SetUserProjectPermissionsAsync_NonAdminDoesNotHavePermission()
Assert.AreEqual(0, project.UserPermissions.Count);
}

[Test]
public void GetEventMetrics_InvalidPageIndex()
{
var env = new TestEnvironment();

// SUT
Assert.ThrowsAsync<FormatException>(
() =>
env.Service.GetEventMetricsAsync(
User01,
systemRoles: [SystemRole.User],
Project01,
pageIndex: -1,
pageSize: 0
)
);
}

[Test]
public void GetEventMetrics_InvalidPageSize()
{
var env = new TestEnvironment();

// SUT
Assert.ThrowsAsync<FormatException>(
() =>
env.Service.GetEventMetricsAsync(
User01,
systemRoles: [SystemRole.User],
Project01,
pageIndex: 0,
pageSize: 0
)
);
}

[Test]
public void GetEventMetrics_InvalidProject()
{
var env = new TestEnvironment();

// SUT
Assert.ThrowsAsync<DataNotFoundException>(
() =>
env.Service.GetEventMetricsAsync(
User01,
systemRoles: [SystemRole.User],
projectId: "invalid_project",
pageIndex: 0,
pageSize: 10
)
);
}

[Test]
public async Task GetEventMetrics_ProjectAdmin()
{
var env = new TestEnvironment();

// SUT
IEnumerable<EventMetric> _ = await env.Service.GetEventMetricsAsync(
User01,
systemRoles: [SystemRole.User],
Project01,
pageIndex: 0,
pageSize: 10
);
env.EventMetricService.Received().GetEventMetrics(Project01, pageIndex: 0, pageSize: 10);
}

[Test]
public async Task GetEventMetrics_ServalAdmin()
{
var env = new TestEnvironment();

// SUT
IEnumerable<EventMetric> _ = await env.Service.GetEventMetricsAsync(
User06,
systemRoles: [SystemRole.ServalAdmin],
Project01,
pageIndex: 0,
pageSize: 10
);
env.EventMetricService.Received().GetEventMetrics(Project01, pageIndex: 0, pageSize: 10);
}

[Test]
public async Task GetEventMetrics_SystemAdmin()
{
var env = new TestEnvironment();

// SUT
IEnumerable<EventMetric> _ = await env.Service.GetEventMetricsAsync(
User06,
systemRoles: [SystemRole.SystemAdmin],
Project01,
pageIndex: 0,
pageSize: 10
);
env.EventMetricService.Received().GetEventMetrics(Project01, pageIndex: 0, pageSize: 10);
}

[Test]
public void GetEventMetrics_UserForbidden()
{
var env = new TestEnvironment();

// SUT
Assert.ThrowsAsync<ForbiddenException>(
() =>
env.Service.GetEventMetricsAsync(
User05,
systemRoles: [SystemRole.User],
Project01,
pageIndex: 0,
pageSize: 10
)
);
}

private class TestEnvironment
{
public static readonly Uri WebsiteUrl = new Uri("http://localhost/", UriKind.Absolute);
Expand Down Expand Up @@ -4827,6 +4948,7 @@ public TestEnvironment()
SecurityService = Substitute.For<ISecurityService>();
SecurityService.GenerateKey().Returns("1234abc");
TransceleratorService = Substitute.For<ITransceleratorService>();
EventMetricService = Substitute.For<IEventMetricService>();
BackgroundJobClient = Substitute.For<IBackgroundJobClient>();

// These project rights correspond to the permissions in the projects above
Expand Down Expand Up @@ -4949,6 +5071,7 @@ public TestEnvironment()
Localizer,
TransceleratorService,
BackgroundJobClient,
EventMetricService,
ProjectRights
);
}
Expand All @@ -4961,6 +5084,7 @@ public TestEnvironment()
public IFileSystemService FileSystemService { get; }
public MemoryRepository<SFProjectSecret> ProjectSecrets { get; }
public IEmailService EmailService { get; }
public IEventMetricService EventMetricService { get; }
public ISecurityService SecurityService { get; }
public IParatextService ParatextService { get; }
public IStringLocalizer<SharedResource> Localizer { get; }
Expand Down
Loading

0 comments on commit 13226aa

Please sign in to comment.