Skip to content

Commit b4a9a65

Browse files
authored
Merge pull request #624 from cmu-sei/next
v3.33.0
2 parents 1c39640 + 4a9fcf8 commit b4a9a65

31 files changed

+682
-136
lines changed

src/Gameboard.Api.Tests.Integration/Fixtures/Services/TestGameEngineService.cs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Gameboard.Api.Common;
33
using Gameboard.Api.Common.Services;
44
using Gameboard.Api.Data;
5+
using Gameboard.Api.Features.Consoles;
56
using Gameboard.Api.Features.GameEngine;
67

78
namespace Gameboard.Api.Tests.Integration.Fixtures;
@@ -68,12 +69,37 @@ public Task<GameEngineGameState> GetChallengeState(GameEngineType gameEngineType
6869
return Task.FromResult(new GameEngineGameState());
6970
}
7071

71-
public Task<ConsoleSummary> GetConsole(Data.Challenge entity, ConsoleRequest model, bool observer)
72+
public Task<ConsoleState> GetConsole(Data.Challenge entity, ConsoleRequest model, bool observer)
7273
{
73-
return Task.FromResult(new ConsoleSummary { });
74+
return Task.FromResult(new ConsoleState
75+
{
76+
Id = new ConsoleId { ChallengeId = entity.Id, Name = model.Name },
77+
AccessTicket = string.Empty,
78+
IsRunning = false,
79+
Url = "https://sei.cmu.edu"
80+
});
7481
}
7582

76-
public IEnumerable<GameEngineGamespaceVm> GetGamespaceVms(GameEngineGameState state)
83+
public Task<ConsoleState> GetConsole(GameEngineType gameEngine, ConsoleId consoleId, CancellationToken cancellationToken)
84+
=> Task.FromResult(new ConsoleState
85+
{
86+
Id = consoleId,
87+
AccessTicket = string.Empty,
88+
IsRunning = false,
89+
Url = "https://sei.cmu.edu"
90+
});
91+
92+
public Task<ConsoleState[]> GetConsoles(GameEngineType gameEngine, ConsoleId[] consoleIds, CancellationToken cancellationToken)
93+
{
94+
return Task.FromResult<ConsoleState[]>([.. consoleIds.Select(c => new ConsoleState
95+
{
96+
Id = c,
97+
AccessTicket = string.Empty,
98+
IsRunning = false,
99+
Url = "https://sei.cmu.edu"
100+
})]);
101+
}
102+
public IEnumerable<GameEngineGamespaceVm> GetVmsFromState(GameEngineGameState state)
77103
=> [];
78104

79105
public Task<GameEngineGameState> GetPreview(Data.ChallengeSpec spec)

src/Gameboard.Api.Tests.Integration/Tests/Features/Consoles/RecordUserConsoleActiveTests.cs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using Gameboard.Api.Common;
2+
using Gameboard.Api.Common.Services;
23
using Gameboard.Api.Data;
34
using Gameboard.Api.Features.Consoles;
5+
using Gameboard.Api.Features.GameEngine;
46

57
namespace Gameboard.Api.Tests.Integration;
68

@@ -9,7 +11,7 @@ public class RecordUserConsoleActiveTests(GameboardTestContext testContext) : IC
911
private readonly GameboardTestContext _testContext = testContext;
1012

1113
[Theory, GbIntegrationAutoData]
12-
public async Task ActionRecorded_WithPracticeSessionNearEnd_Extends(string userId, IFixture fixture)
14+
public async Task ActionRecorded_WithPracticeSessionNearEnd_Extends(string challengeId, string userId, IFixture fixture)
1315
{
1416
// given
1517
await _testContext.WithDataState(state =>
@@ -23,15 +25,17 @@ await _testContext.WithDataState(state =>
2325
p.User = state.Build<Data.User>(fixture, u => u.Id = userId);
2426
p.Challenges = state.Build<Data.Challenge>(fixture, c =>
2527
{
28+
c.Id = challengeId;
2629
c.PlayerMode = PlayerMode.Practice;
30+
c.State = GetChallengeState(challengeId);
2731
}).ToCollection();
2832
});
2933
});
3034

3135
// when
3236
var result = await _testContext
3337
.CreateHttpClientWithActingUser(u => u.Id = userId)
34-
.PostAsync("api/consoles/active", null)
38+
.PostAsync("api/consoles/active", new ConsoleId { ChallengeId = challengeId, Name = "my-vm" }.ToJsonBody())
3539
.DeserializeResponseAs<ConsoleActionResponse>();
3640

3741
// then
@@ -40,7 +44,7 @@ await _testContext.WithDataState(state =>
4044
}
4145

4246
[Theory, GbIntegrationAutoData]
43-
public async Task ActionRecorded_WithPracticeSessionNotNearEnd_DoesNotExtend(string userId, IFixture fixture)
47+
public async Task ActionRecorded_WithPracticeSessionNotNearEnd_DoesNotExtend(string challengeId, string userId, IFixture fixture)
4448
{
4549
// given
4650
await _testContext.WithDataState(state =>
@@ -55,15 +59,17 @@ await _testContext.WithDataState(state =>
5559
p.User = state.Build<Data.User>(fixture, u => u.Id = userId);
5660
p.Challenges = state.Build<Data.Challenge>(fixture, c =>
5761
{
62+
c.Id = challengeId;
5863
c.PlayerMode = PlayerMode.Practice;
64+
c.State = GetChallengeState(challengeId);
5965
}).ToCollection();
6066
});
6167
});
6268

6369
// when
6470
var result = await _testContext
6571
.CreateHttpClientWithActingUser(u => u.Id = userId)
66-
.PostAsync("api/consoles/active", null)
72+
.PostAsync("api/consoles/active", new ConsoleId { ChallengeId = challengeId, Name = "my-vm" }.ToJsonBody())
6773
.DeserializeResponseAs<ConsoleActionResponse>();
6874

6975
// then
@@ -72,7 +78,7 @@ await _testContext.WithDataState(state =>
7278
}
7379

7480
[Theory, GbIntegrationAutoData]
75-
public async Task ActionRecorded_WithCompetitiveSessionNearEnd_Extends(string userId, IFixture fixture)
81+
public async Task ActionRecorded_WithCompetitiveSessionNearEnd_Extends(string challengeId, string userId, IFixture fixture)
7682
{
7783
// given
7884
await _testContext.WithDataState(state =>
@@ -87,18 +93,38 @@ await _testContext.WithDataState(state =>
8793
p.User = state.Build<Data.User>(fixture, u => u.Id = userId);
8894
p.Challenges = state.Build<Data.Challenge>(fixture, c =>
8995
{
96+
c.Id = challengeId;
9097
c.PlayerMode = PlayerMode.Practice;
98+
c.State = GetChallengeState(challengeId);
9199
}).ToCollection();
92100
});
93101
});
94102

95103
// when
96104
var result = await _testContext
97105
.CreateHttpClientWithActingUser(u => u.Id = userId)
98-
.PostAsync("api/consoles/active", null)
106+
.PostAsync("api/consoles/active", new ConsoleId { ChallengeId = challengeId, Name = "my-vm" }.ToJsonBody())
99107
.DeserializeResponseAs<ConsoleActionResponse>();
100108

101109
// then
102110
result.Message.ShouldBeNull();
103111
}
112+
113+
private string GetChallengeState(string challengeId)
114+
{
115+
var state = new GameEngineGameState
116+
{
117+
Vms = [new GameEngineVmState
118+
{
119+
Id = "123",
120+
Name = "my-vm",
121+
IsolationId = challengeId,
122+
IsRunning = true,
123+
IsVisible = true
124+
}]
125+
};
126+
127+
var jsonService = _testContext.Services.GetRequiredService<IJsonService>();
128+
return jsonService.Serialize(state);
129+
}
104130
}

src/Gameboard.Api/Features/Challenge/Challenge.cs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ public class UserActiveChallengeVm
8383
{
8484
public required string Id { get; set; }
8585
public required string Name { get; set; }
86+
public required string AccessTicket { get; set; }
87+
public required string Url { get; set; }
8688
}
8789

8890
public class UserActiveChallengeScoreAndAttemptsState
@@ -174,23 +176,18 @@ public class ObserveVM
174176
public bool IsVisible { get; set; }
175177
}
176178

177-
public class ConsoleRequest
179+
public class GetConsoleStateRequest
178180
{
181+
public string ChallengeId { get; set; }
179182
public string Name { get; set; }
180-
public string SessionId { get; set; }
181-
public ConsoleAction Action { get; set; }
182-
public string Id => $"{Name}#{SessionId}";
183183
}
184184

185-
public class ConsoleSummary
185+
public class ConsoleRequest
186186
{
187-
public string Id { get; set; }
188187
public string Name { get; set; }
189188
public string SessionId { get; set; }
190-
public string Url { get; set; }
191-
public bool IsRunning { get; set; }
192-
public bool IsObserver { get; set; }
193-
public string Ticket { get; set; }
189+
public ConsoleAction Action { get; set; }
190+
public string Id { get => $"{Name}#{SessionId}"; }
194191
}
195192

196193
public enum ConsoleAction

src/Gameboard.Api/Features/Challenge/ChallengeController.cs

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Gameboard.Api.Common.Services;
88
using Gameboard.Api.Data;
99
using Gameboard.Api.Features.Challenges;
10+
using Gameboard.Api.Features.Consoles;
1011
using Gameboard.Api.Features.GameEngine;
1112
using Gameboard.Api.Features.Users;
1213
using Gameboard.Api.Hubs;
@@ -29,11 +30,12 @@ public class ChallengeController
2930
IDistributedCache cache,
3031
ChallengeValidator validator,
3132
ChallengeService challengeService,
33+
IGameEngineService gameEngineService,
3234
IMediator mediator,
3335
IUserRolePermissionsService permissionsService,
3436
PlayerService playerService,
3537
IHubContext<AppHub, IAppHubEvent> hub,
36-
ConsoleActorMap actormap
38+
ConsoleActorMap actorMap
3739
) : GameboardLegacyController(actingUserService, logger, cache, validator)
3840
{
3941
private readonly IMediator _mediator = mediator;
@@ -42,7 +44,7 @@ ConsoleActorMap actormap
4244
ChallengeService ChallengeService { get; } = challengeService;
4345
PlayerService PlayerService { get; } = playerService;
4446
IHubContext<AppHub, IAppHubEvent> Hub { get; } = hub;
45-
ConsoleActorMap ActorMap { get; } = actormap;
47+
ConsoleActorMap ActorMap { get; } = actorMap;
4648

4749
/// <summary>
4850
/// Purge a challenge. This deletes the challenge instance, and all progress on it, and can't be undone.
@@ -113,7 +115,6 @@ await AuthorizeAny
113115
);
114116

115117
await Validate(new Entity { Id = id });
116-
117118
return await ChallengeService.Get(id);
118119
}
119120

@@ -275,10 +276,11 @@ public async Task<IEnumerable<GameEngineSectionSubmission>> Audit([FromRoute] st
275276
/// Console action (ticket, reset)
276277
/// </summary>
277278
/// <param name="model"></param>
279+
/// <param name="cancellationToken"></param>
278280
/// <returns></returns>
279281
[HttpPost("/api/challenge/console")]
280282
[Authorize(AppConstants.ConsolePolicy)]
281-
public async Task<ConsoleSummary> GetConsole([FromBody] ConsoleRequest model)
283+
public async Task<ConsoleState> SendConsoleCommand([FromBody] ConsoleRequest model, CancellationToken cancellationToken)
282284
{
283285
await Validate(new Entity { Id = model.SessionId });
284286
var isTeamMember = await ChallengeService.UserIsPlayingChallenge(model.SessionId, Actor.Id);
@@ -288,30 +290,20 @@ public async Task<ConsoleSummary> GetConsole([FromBody] ConsoleRequest model)
288290
await Authorize(_permissionsService.Can(PermissionKey.Teams_Observe));
289291

290292
Logger.LogInformation($"""Console access attempt ({model.Id} / {Actor.Id}): Allowed.""");
291-
var result = await ChallengeService.GetConsole(model, isTeamMember.Equals(false));
293+
var result = await gameEngineService.GetConsole(GameEngineType.TopoMojo, new ConsoleId { ChallengeId = model.SessionId, Name = model.Name }, cancellationToken);
292294

293295
if (isTeamMember)
294-
ActorMap.Update(await ChallengeService.SetConsoleActor(model, Actor.Id, Actor.ApprovedName));
296+
{
297+
ActorMap.Update(await ChallengeService.SetConsoleActor(new ConsoleId
298+
{
299+
ChallengeId = model.SessionId,
300+
Name = model.Name
301+
}, Actor.Id, Actor.ApprovedName));
302+
}
295303

296304
return result;
297305
}
298306

299-
/// <summary>
300-
/// Console action (ticket, reset)
301-
/// </summary>
302-
/// <param name="model"></param>
303-
/// <returns></returns>
304-
[HttpPut("/api/challenge/console")]
305-
[Authorize(AppConstants.ConsolePolicy)]
306-
public async Task SetConsoleActor([FromBody] ConsoleRequest model)
307-
{
308-
await Validate(new Entity { Id = model.SessionId });
309-
310-
var isTeamMember = await ChallengeService.UserIsPlayingChallenge(model.SessionId, Actor.Id);
311-
if (isTeamMember)
312-
ActorMap.Update(await ChallengeService.SetConsoleActor(model, Actor.Id, Actor.ApprovedName));
313-
}
314-
315307
[HttpGet("/api/challenge/consoles")]
316308
public async Task<List<ObserveChallenge>> FindConsoles([FromQuery] string gid)
317309
{

src/Gameboard.Api/Features/Challenge/ChallengeMapper.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,6 @@ public ChallengeMapper()
127127
CreateMap<TopoMojo.Api.Client.VmState, ObserveVM>()
128128
.ForMember(d => d.ChallengeId, opt => opt.MapFrom(s => s.IsolationId))
129129
;
130-
131-
CreateMap<TopoMojo.Api.Client.VmConsole, ConsoleSummary>()
132-
.ForMember(d => d.SessionId, opt => opt.MapFrom(s => s.IsolationId))
133-
;
134-
135130
CreateMap<Data.ChallengeEvent, ChallengeEventSummary>();
136131
}
137132
}

src/Gameboard.Api/Features/Challenge/ConsoleActorMap.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public ConsoleActor[] Find(string gid = "")
5151
: _cache.Values
5252
;
5353

54-
return q.ToArray();
54+
return [.. q];
5555
}
5656

5757
public ConsoleActor FindActor(string uid)

src/Gameboard.Api/Features/Challenge/Services/ChallengeService.cs

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using Microsoft.EntityFrameworkCore;
2121
using Microsoft.Extensions.Logging;
2222
using ServiceStack;
23+
using Gameboard.Api.Features.Consoles;
2324

2425
namespace Gameboard.Api.Services;
2526

@@ -632,21 +633,6 @@ await _store.DoTransaction(async dbContext =>
632633
}, CancellationToken.None);
633634
}
634635

635-
public async Task<ConsoleSummary> GetConsole(ConsoleRequest model, bool observer)
636-
{
637-
var entity = await _challengeStore.Retrieve(model.SessionId);
638-
var challenge = Mapper.Map<Challenge>(entity);
639-
640-
if (!challenge.State.Vms.Any(v => v.Name == model.Name))
641-
{
642-
var vmNames = string.Join(", ", challenge.State.Vms.Select(vm => vm.Name));
643-
throw new ResourceNotFound<GameEngineVmState>("n/a", $"VMS for challenge {model.Name} - searching for {model.Name}, found these names: {vmNames}");
644-
}
645-
646-
var console = await _gameEngine.GetConsole(entity, model, observer);
647-
return console ?? throw new InvalidConsoleAction();
648-
}
649-
650636
public async Task<List<ObserveChallenge>> GetChallengeConsoles(string gameId)
651637
{
652638
// retrieve challenges to list
@@ -730,22 +716,22 @@ public GameEngineGameState TransformStateRelativeUrls(GameEngineGameState state)
730716
return state;
731717
}
732718

733-
internal async Task<ConsoleActor> SetConsoleActor(ConsoleRequest model, string id, string name)
719+
internal async Task<ConsoleActor> SetConsoleActor(ConsoleId console, string id, string name)
734720
{
735721
var entity = await _challengeStore.DbSet
736722
.Include(c => c.Player)
737-
.FirstOrDefaultAsync(c => c.Id == model.SessionId);
723+
.FirstOrDefaultAsync(c => c.Id == console.ChallengeId);
738724

739725
return new ConsoleActor
740726
{
741727
UserId = id,
742728
UserName = name,
743729
PlayerName = entity.Player.Name,
744730
ChallengeName = entity.Name,
745-
ChallengeId = model.SessionId,
731+
ChallengeId = console.ChallengeId,
746732
GameId = entity.GameId,
747733
TeamId = entity.TeamId,
748-
VmName = model.Name,
734+
VmName = console.Name,
749735
Timestamp = DateTimeOffset.UtcNow
750736
};
751737
}

src/Gameboard.Api/Features/ChallengeSpec/ChallengeSpecController.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ public async Task<ExternalSpec[]> List([FromQuery] SearchFilter model)
9393
}
9494

9595
[HttpGet("/api/challengespecs/by-game")]
96-
public Task<IEnumerable<GameChallengeSpecs>> ListByGame()
97-
=> _challengeSpecService.ListByGame();
96+
public Task<GameChallengeSpecs[]> ListByGame([FromQuery] string gameId)
97+
=> _challengeSpecService.ListByGame(gameId.IsEmpty() ? null : gameId);
9898

9999
/// <summary>
100100
/// Load solve performance for the challenge spec

0 commit comments

Comments
 (0)