Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3.32.0 #612

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public async Task GetExpiredChallengesForSync_WithPlayerWithNullishEndDate_DoesN
var store = BuildTestableStore(challenge);
var sut = new ChallengeSyncService
(
A.Fake<IActingUserService>(),
A.Fake<ConsoleActorMap>(),
A.Fake<IGameEngineService>(),
A.Fake<ILogger<IChallengeSyncService>>(),
Expand Down Expand Up @@ -59,6 +60,7 @@ public async Task GetExpiredChallengesForSync_WithPlayerSessionEndInFuture_DoesN
var store = BuildTestableStore(challenge);
var sut = new ChallengeSyncService
(
A.Fake<IActingUserService>(),
A.Fake<ConsoleActorMap>(),
A.Fake<IGameEngineService>(),
A.Fake<ILogger<IChallengeSyncService>>(),
Expand Down Expand Up @@ -91,6 +93,7 @@ public async Task GetExpiredChallengesForSync_WithChallengeAlreadySynced_DoesNot
var store = BuildTestableStore(challenge);
var sut = new ChallengeSyncService
(
A.Fake<IActingUserService>(),
A.Fake<ConsoleActorMap>(),
A.Fake<IGameEngineService>(),
A.Fake<ILogger<IChallengeSyncService>>(),
Expand Down Expand Up @@ -123,6 +126,7 @@ public async Task GetExpiredChallengesForSync_WithNonNullishEndDate_DoesNotSync(
var store = BuildTestableStore(challenge);
var sut = new ChallengeSyncService
(
A.Fake<IActingUserService>(),
A.Fake<ConsoleActorMap>(),
A.Fake<IGameEngineService>(),
A.Fake<ILogger<IChallengeSyncService>>(),
Expand Down Expand Up @@ -155,6 +159,7 @@ public async Task GetExpiredChallengesForSync_WithAllRequiredCriteriaAndNullishE
var store = BuildTestableStore(challenge);
var sut = new ChallengeSyncService
(
A.Fake<IActingUserService>(),
A.Fake<ConsoleActorMap>(),
A.Fake<IGameEngineService>(),
A.Fake<ILogger<IChallengeSyncService>>(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
using Gameboard.Api.Common;
using AutoMapper;
using Gameboard.Api.Common.Services;
using Gameboard.Api.Data;
using Gameboard.Api.Features.Challenges;
using Gameboard.Api.Features.Practice;
using Gameboard.Api.Features.Users;
using Microsoft.EntityFrameworkCore;

namespace Gameboard.Api.Tests.Unit;

public class SearchPracticeChallengesTests
public class PracticeService_BuildPracticeChallengesQueryBase_Tests
{
[Theory, GameboardAutoData]
public async Task SearchPracticeChallenges_WithDisabled_ReturnsEmpty(IFixture fixture)
Expand All @@ -32,7 +31,7 @@ public async Task SearchPracticeChallenges_WithDisabled_ReturnsEmpty(IFixture fi
var sut = GetSutWithResults(fixture, disabledSpec);

// when a query for all challenges is issued
var query = await sut.BuildQuery(string.Empty, []);
var query = await sut.GetPracticeChallengesQueryBase(string.Empty);
var result = await query.ToArrayAsync(CancellationToken.None);

// then we expect no results
Expand Down Expand Up @@ -61,14 +60,14 @@ public async Task SearchPracticeChallenges_WithEnabled_Returns(IFixture fixture)
var sut = GetSutWithResults(fixture, enabledSpec);

// when a query for all challenges is issued
var query = await sut.BuildQuery(string.Empty, []);
var query = await sut.GetPracticeChallengesQueryBase(string.Empty);
var result = await query.ToArrayAsync(CancellationToken.None);

// then we expect one result
result.Length.ShouldBe(1);
}

private SearchPracticeChallengesHandler GetSutWithResults(IFixture fixture, params Data.ChallengeSpec[] specs)
private PracticeService GetSutWithResults(IFixture fixture, params Data.ChallengeSpec[] specs)
{
var queryResults = specs.BuildMock();

Expand All @@ -77,16 +76,14 @@ private SearchPracticeChallengesHandler GetSutWithResults(IFixture fixture, para
.WithAnyArguments()
.Returns(queryResults);

var sut = new SearchPracticeChallengesHandler
return new PracticeService
(
A.Fake<IChallengeDocsService>(),
A.Fake<IPagingService>(),
A.Fake<IGuidService>(),
A.Fake<IMapper>(),
A.Fake<INowService>(),
A.Fake<IUserRolePermissionsService>(),
A.Fake<IPracticeService>(),
A.Fake<ISlugService>(),
store
);

return sut;
}
}
2 changes: 1 addition & 1 deletion src/Gameboard.Api/Common/Services/PagingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public interface IPagingService

internal class PagingService : IPagingService
{
private static readonly int DEFAULT_PAGE_SIZE = 20;
private static readonly int DEFAULT_PAGE_SIZE = 25;

public PagedEnumerable<T> Page<T>(IEnumerable<T> items, PagingArgs pagingArgs = null)
{
Expand Down
92 changes: 52 additions & 40 deletions src/Gameboard.Api/Extensions/WebApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,46 +68,6 @@ public static void ConfigureServices(this WebApplicationBuilder builder, AppSett
{
var services = builder.Services;

// serilog config
builder.Host.UseSerilog();

Serilog.Debugging.SelfLog.Enable(Console.Error);
var loggerConfiguration = new LoggerConfiguration()
.MinimumLevel.Is(settings.Logging.MinimumLogLevel)
.WriteTo.Console(theme: AnsiConsoleTheme.Code);

// normally, you'd do this in an appsettings.json and just rely on built-in config
// assembly stuff, but we distribute with a helm chart and a weird conf format, so
// we need to manually set up the log levels
foreach (var logNamespace in settings.Logging.NamespacesErrorLevel)
{
loggerConfiguration = loggerConfiguration.MinimumLevel.Override(logNamespace, LogEventLevel.Error);
}

foreach (var logNamespace in settings.Logging.NamespacesFatalLevel)
{
loggerConfiguration = loggerConfiguration.MinimumLevel.Override(logNamespace, LogEventLevel.Fatal);
}

foreach (var logNamespace in settings.Logging.NamespacesInfoLevel)
{
loggerConfiguration = loggerConfiguration.MinimumLevel.Override(logNamespace, LogEventLevel.Information);
}

foreach (var logNamespace in settings.Logging.NamespacesWarningLevel)
{
loggerConfiguration = loggerConfiguration.MinimumLevel.Override(logNamespace, LogEventLevel.Warning);
}

// set up sinks on demand
if (settings.Logging.SeqInstanceUrl.IsNotEmpty())
{
loggerConfiguration = loggerConfiguration.WriteTo.Seq(settings.Logging.SeqInstanceUrl, apiKey: settings.Logging.SeqInstanceApiKey);
}

// weirdly, this really does appear to be the way to replace the default logger with Serilog 🤷
Log.Logger = loggerConfiguration.CreateLogger();

services
.AddMvc()
.AddGameboardJsonOptions();
Expand All @@ -124,6 +84,8 @@ public static void ConfigureServices(this WebApplicationBuilder builder, AppSett
.SetApplicationName(AppConstants.DataProtectionPurpose)
.PersistKeys(() => settings.Cache);

builder.AddGameboardSerilog(settings);

services
.AddSingleton(_ => settings.Core)
.AddSingleton(_ => settings.Crucible)
Expand All @@ -149,4 +111,54 @@ public static void ConfigureServices(this WebApplicationBuilder builder, AppSett
services.AddConfiguredAuthentication(settings.Oidc, settings.ApiKey, builder.Environment);
services.AddConfiguredAuthorization();
}

private static WebApplicationBuilder AddGameboardSerilog(this WebApplicationBuilder builder, AppSettings settings)
{
// SERILOG CONFIG
// Gameboard uses Serilog, which is awesome, because Serilog is awesome. By default, it just
// writes to the console sink, which you can ingest with any log ingester (and is useful if you
// choose to monitor the output of its pod in a K8s-style scenario). But if you want richer logging,
// you can add a Seq instance using its configuration so you get nice metadata like the userID
// and name for API requests. Want to use a non-Seq sink? We get it. PR us and let's talk about it.
builder.Host.UseSerilog();

Serilog.Debugging.SelfLog.Enable(Console.Error);
var loggerConfiguration = new LoggerConfiguration()
.MinimumLevel.Is(settings.Logging.MinimumLogLevel)
.WriteTo.Console(theme: AnsiConsoleTheme.Code);

// normally, you'd do this in an appsettings.json and just rely on built-in config
// assembly stuff, but we distribute with a helm chart and a weird conf format, so
// we need to manually set up the log levels
foreach (var logNamespace in settings.Logging.NamespacesErrorLevel)
{
loggerConfiguration = loggerConfiguration.MinimumLevel.Override(logNamespace, LogEventLevel.Error);
}

foreach (var logNamespace in settings.Logging.NamespacesFatalLevel)
{
loggerConfiguration = loggerConfiguration.MinimumLevel.Override(logNamespace, LogEventLevel.Fatal);
}

foreach (var logNamespace in settings.Logging.NamespacesInfoLevel)
{
loggerConfiguration = loggerConfiguration.MinimumLevel.Override(logNamespace, LogEventLevel.Information);
}

foreach (var logNamespace in settings.Logging.NamespacesWarningLevel)
{
loggerConfiguration = loggerConfiguration.MinimumLevel.Override(logNamespace, LogEventLevel.Warning);
}

// set up sinks on demand
if (settings.Logging.SeqInstanceUrl.IsNotEmpty())
{
loggerConfiguration = loggerConfiguration.WriteTo.Seq(settings.Logging.SeqInstanceUrl, apiKey: settings.Logging.SeqInstanceApiKey);
}

// weirdly, this really does appear to be the way to replace the default logger with Serilog 🤷
Log.Logger = loggerConfiguration.CreateLogger();

return builder;
}
}
4 changes: 4 additions & 0 deletions src/Gameboard.Api/Features/Challenge/ChallengeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ await Hub.Clients.Group(result.TeamId).ChallengeEvent
return result;
}

[HttpPut("/api/challenge/{challengeId}/sync")]
public Task<GameEngineGameState> Sync([FromRoute] string challengeId, CancellationToken cancellationToken)
=> _mediator.Send(new SyncChallengeCommand(challengeId), cancellationToken);

/// <summary>
/// Grade a challenge
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Threading;
using System.Threading.Tasks;
using Gameboard.Api.Features.GameEngine;
using Gameboard.Api.Features.Users;
using Gameboard.Api.Structure.MediatR;
using MediatR;

namespace Gameboard.Api.Features.Challenges;

public record SyncChallengeCommand(string ChallengeId) : IRequest<GameEngineGameState>;

internal sealed class SyncChallengeHandler
(
IChallengeSyncService challengeSyncService,
IValidatorService validator
) : IRequestHandler<SyncChallengeCommand, GameEngineGameState>
{
public async Task<GameEngineGameState> Handle(SyncChallengeCommand request, CancellationToken cancellationToken)
{
await validator
.Auth(c => c.Require(PermissionKey.Admin_View))
.AddEntityExistsValidator<Data.Challenge>(request.ChallengeId)
.Validate(cancellationToken);

return await challengeSyncService.Sync(request.ChallengeId, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,18 @@ public int GetDeployingChallengeCount(string teamId)
return entry.Specs.Count;
}

public IEnumerable<string> GetTags(Data.ChallengeSpec spec)
public string[] GetTags(string tagString)
{
if (spec.Tags.IsEmpty())
if (tagString.IsEmpty())
return [];

return CommonRegexes
return [..
CommonRegexes
.WhitespaceGreedy
.Split(spec.Tags)
.Split(tagString)
.Select(m => m.Trim().ToLower())
.Where(m => m.IsNotEmpty())
.ToArray();
];
}

public async Task<Challenge> Create(NewChallenge model, string actorId, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace Gameboard.Api.Features.Challenges;

public interface IChallengeSyncService
{
Task<GameEngineGameState> Sync(string challengeId, CancellationToken cancellationToken);
Task Sync(Data.Challenge challenge, GameEngineGameState challengeState, string actingUserId, CancellationToken cancellationToken);
Task SyncExpired(CancellationToken cancellationToken);
}
Expand All @@ -25,6 +26,7 @@ public interface IChallengeSyncService
/// </summary>
internal class ChallengeSyncService
(
IActingUserService actingUser,
ConsoleActorMap consoleActorMap,
IGameEngineService gameEngine,
ILogger<IChallengeSyncService> logger,
Expand All @@ -33,13 +35,31 @@ internal class ChallengeSyncService
IStore store
) : IChallengeSyncService
{
private readonly IActingUserService _actingUser = actingUser;
private readonly ConsoleActorMap _consoleActorMap = consoleActorMap;
private readonly IGameEngineService _gameEngine = gameEngine;
private readonly ILogger<IChallengeSyncService> _logger = logger;
private readonly IMapper _mapper = mapper;
private readonly INowService _now = now;
private readonly IStore _store = store;

public async Task<GameEngineGameState> Sync(string challengeId, CancellationToken cancellationToken)
{
var challenge = await _store
.WithNoTracking<Data.Challenge>()
.SingleOrDefaultAsync(c => c.Id == challengeId, cancellationToken);

if (challenge is null)
{
return null;
}

var state = await _gameEngine.GetChallengeState(GameEngineType.TopoMojo, challenge.State);
await Sync(challenge, state, _actingUser.Get()?.Id, cancellationToken);

return await _gameEngine.GetChallengeState(GameEngineType.TopoMojo, challenge.State);
}

public Task Sync(Data.Challenge challenge, GameEngineGameState state, string actingUserId, CancellationToken cancellationToken)
=> Sync(cancellationToken, new SyncEntry(actingUserId, challenge, state));

Expand Down
19 changes: 7 additions & 12 deletions src/Gameboard.Api/Features/Game/GameService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ public interface IGameService
Task<SessionForecast[]> SessionForecast(string id);
Task<Data.Game> Update(ChangedGame account);
Task UpdateImage(string id, string type, string filename);
Task<bool> UserIsTeamPlayer(string uid, string tid);
}

public class GameService
Expand All @@ -38,11 +37,13 @@ public class GameService
IMapper mapper,
CoreOptions options,
Defaults defaults,
IGuidService guids,
INowService nowService,
IStore store
) : _Service(logger, mapper, options), IGameService
{
private readonly Defaults _defaults = defaults;
private readonly IGuidService _guids = guids;
private readonly INowService _now = nowService;
private readonly IStore _store = store;

Expand Down Expand Up @@ -217,16 +218,6 @@ public async Task UpdateImage(string id, string type, string filename)
public Task<bool> IsUserPlaying(string gameId, string userId)
=> _store.AnyAsync<Data.Player>(p => p.GameId == gameId && p.UserId == userId, CancellationToken.None);

public async Task<bool> UserIsTeamPlayer(string uid, string tid)
{
bool authd = await _store.AnyAsync<Data.User>(u =>
u.Id == uid &&
u.Enrollments.Any(e => e.TeamId == tid)
, CancellationToken.None);

return authd;
}

public async Task DeleteGameCardImage(string gameId)
{
if (!await _store.WithNoTracking<Data.Game>().AnyAsync(g => g.Id == gameId))
Expand All @@ -248,6 +239,10 @@ public async Task<UploadedFile> SaveGameCardImage(string gameId, IFormFile file)
if (!await _store.WithNoTracking<Data.Game>().AnyAsync(g => g.Id == gameId))
throw new ResourceNotFound<Data.Game>(gameId);

// we currently intentionally leave the old image around for quasi-logging purposes,
// but we could delete if desired here by getting the path from the Game entity.
// We generate a semi-random name for the new file in GetGameCardFileNameBase to bypass
// network-level caching.
var fileName = $"{GetGameCardFileNameBase(gameId)}{Path.GetExtension(file.FileName.ToLower())}";
var path = Path.Combine(Options.ImageFolder, fileName);

Expand All @@ -259,7 +254,7 @@ public async Task<UploadedFile> SaveGameCardImage(string gameId, IFormFile file)
}

private string GetGameCardFileNameBase(string gameId)
=> $"{gameId.ToLower()}_card";
=> $"{gameId.ToLower()}_card_{_guids.Generate()[..6]}";

private IQueryable<Data.Game> BuildSearchQuery(GameSearchFilter model, bool canViewUnpublished = false)
{
Expand Down
Loading
Loading