|
| 1 | +using Microsoft.AspNetCore.Http; |
| 2 | +using Microsoft.AspNetCore.Mvc; |
| 3 | +using Microsoft.Extensions.Logging; |
| 4 | +using NSubstitute; |
| 5 | +using NSubstitute.ExceptionExtensions; |
| 6 | +using Viper.Areas.Scheduler.Controllers; |
| 7 | +using Viper.Areas.Scheduler.Models.DTOs.Responses; |
| 8 | +using Viper.Areas.Scheduler.Services; |
| 9 | +using Viper.Models.AAUD; |
| 10 | + |
| 11 | +namespace Viper.test.Scheduler |
| 12 | +{ |
| 13 | + public sealed class JobsControllerTests |
| 14 | + { |
| 15 | + private readonly ISchedulerJobsService _service; |
| 16 | + private readonly IUserHelper _userHelper; |
| 17 | + private readonly ILogger<JobsController> _logger; |
| 18 | + private readonly JobsController _sut; |
| 19 | + |
| 20 | + public JobsControllerTests() |
| 21 | + { |
| 22 | + _service = Substitute.For<ISchedulerJobsService>(); |
| 23 | + _userHelper = Substitute.For<IUserHelper>(); |
| 24 | + _logger = Substitute.For<ILogger<JobsController>>(); |
| 25 | + _sut = new JobsController(_service, _userHelper, _logger); |
| 26 | + } |
| 27 | + |
| 28 | + // ──────────── ListJobs ──────────── |
| 29 | + |
| 30 | + [Fact] |
| 31 | + public async Task ListJobs_ReturnsOkWithServiceResult() |
| 32 | + { |
| 33 | + var dtos = new List<SchedulerJobDto> { new() { Id = "raps:role-refresh" } }; |
| 34 | + _service.ListJobsAsync(Arg.Any<CancellationToken>()).Returns(dtos); |
| 35 | + |
| 36 | + var result = await _sut.ListJobs(CancellationToken.None); |
| 37 | + |
| 38 | + var ok = Assert.IsType<OkObjectResult>(result.Result); |
| 39 | + Assert.Same(dtos, ok.Value); |
| 40 | + } |
| 41 | + |
| 42 | + // ──────────── GetJob ──────────── |
| 43 | + |
| 44 | + [Fact] |
| 45 | + public async Task GetJob_ReturnsOkWhenServiceFindsJob() |
| 46 | + { |
| 47 | + var dto = new SchedulerJobDto { Id = "raps:role-refresh" }; |
| 48 | + _service.GetJobAsync("raps:role-refresh", Arg.Any<CancellationToken>()).Returns(dto); |
| 49 | + |
| 50 | + var result = await _sut.GetJob("raps:role-refresh", CancellationToken.None); |
| 51 | + |
| 52 | + var ok = Assert.IsType<OkObjectResult>(result.Result); |
| 53 | + Assert.Same(dto, ok.Value); |
| 54 | + } |
| 55 | + |
| 56 | + [Fact] |
| 57 | + public async Task GetJob_ReturnsNotFoundWhenServiceReturnsNull() |
| 58 | + { |
| 59 | + _service.GetJobAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()).Returns((SchedulerJobDto?)null); |
| 60 | + |
| 61 | + var result = await _sut.GetJob("missing", CancellationToken.None); |
| 62 | + |
| 63 | + Assert.IsType<NotFoundResult>(result.Result); |
| 64 | + } |
| 65 | + |
| 66 | + // ──────────── PauseJob ──────────── |
| 67 | + |
| 68 | + [Fact] |
| 69 | + public async Task PauseJob_ReturnsOkWhenServiceCompletes() |
| 70 | + { |
| 71 | + _userHelper.GetCurrentUser().Returns(new AaudUser { LoginId = "alice" }); |
| 72 | + var dto = new PauseResumeResultDto { Id = "raps:role-refresh", IsPaused = true }; |
| 73 | + _service.PauseJobAsync("raps:role-refresh", "alice", Arg.Any<byte[]?>(), Arg.Any<CancellationToken>()) |
| 74 | + .Returns(dto); |
| 75 | + |
| 76 | + var result = await _sut.PauseJob("raps:role-refresh", new JobsController.PauseRequest(), CancellationToken.None); |
| 77 | + |
| 78 | + var ok = Assert.IsType<OkObjectResult>(result.Result); |
| 79 | + Assert.Same(dto, ok.Value); |
| 80 | + } |
| 81 | + |
| 82 | + [Fact] |
| 83 | + public async Task PauseJob_Returns202WhenDeregistrationPending() |
| 84 | + { |
| 85 | + _userHelper.GetCurrentUser().Returns(new AaudUser { LoginId = "alice" }); |
| 86 | + var dto = new PauseResumeResultDto { Id = "raps:role-refresh", IsPaused = true, DeregistrationPending = true }; |
| 87 | + _service.PauseJobAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<byte[]?>(), Arg.Any<CancellationToken>()) |
| 88 | + .Returns(dto); |
| 89 | + |
| 90 | + var result = await _sut.PauseJob("raps:role-refresh", null, CancellationToken.None); |
| 91 | + |
| 92 | + var status = Assert.IsType<ObjectResult>(result.Result); |
| 93 | + Assert.Equal(StatusCodes.Status202Accepted, status.StatusCode); |
| 94 | + Assert.Same(dto, status.Value); |
| 95 | + } |
| 96 | + |
| 97 | + [Fact] |
| 98 | + public async Task PauseJob_Returns403OnSystemJobProtection() |
| 99 | + { |
| 100 | + _userHelper.GetCurrentUser().Returns(new AaudUser { LoginId = "alice" }); |
| 101 | + _service.PauseJobAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<byte[]?>(), Arg.Any<CancellationToken>()) |
| 102 | + .ThrowsAsyncForAnyArgs(new SchedulerSystemJobProtectedException("__scheduler:reconcile")); |
| 103 | + |
| 104 | + var result = await _sut.PauseJob("__scheduler:reconcile", null, CancellationToken.None); |
| 105 | + |
| 106 | + var status = Assert.IsType<ObjectResult>(result.Result); |
| 107 | + Assert.Equal(StatusCodes.Status403Forbidden, status.StatusCode); |
| 108 | + } |
| 109 | + |
| 110 | + [Fact] |
| 111 | + public async Task PauseJob_Returns404WhenJobMissing() |
| 112 | + { |
| 113 | + _userHelper.GetCurrentUser().Returns(new AaudUser { LoginId = "alice" }); |
| 114 | + _service.PauseJobAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<byte[]?>(), Arg.Any<CancellationToken>()) |
| 115 | + .ThrowsAsyncForAnyArgs(new SchedulerJobNotFoundException("nope")); |
| 116 | + |
| 117 | + var result = await _sut.PauseJob("nope", null, CancellationToken.None); |
| 118 | + |
| 119 | + Assert.IsType<NotFoundResult>(result.Result); |
| 120 | + } |
| 121 | + |
| 122 | + [Fact] |
| 123 | + public async Task PauseJob_Returns409OnConcurrencyConflict() |
| 124 | + { |
| 125 | + _userHelper.GetCurrentUser().Returns(new AaudUser { LoginId = "alice" }); |
| 126 | + _service.PauseJobAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<byte[]?>(), Arg.Any<CancellationToken>()) |
| 127 | + .ThrowsAsyncForAnyArgs(new SchedulerConcurrencyException("raps:role-refresh")); |
| 128 | + |
| 129 | + var result = await _sut.PauseJob("raps:role-refresh", null, CancellationToken.None); |
| 130 | + |
| 131 | + Assert.IsType<ConflictObjectResult>(result.Result); |
| 132 | + } |
| 133 | + |
| 134 | + [Fact] |
| 135 | + public async Task PauseJob_Returns400OnInvalidBase64RowVersion() |
| 136 | + { |
| 137 | + _userHelper.GetCurrentUser().Returns(new AaudUser { LoginId = "alice" }); |
| 138 | + |
| 139 | + var result = await _sut.PauseJob( |
| 140 | + "raps:role-refresh", |
| 141 | + new JobsController.PauseRequest { RowVersion = "not-base-64!!!" }, |
| 142 | + CancellationToken.None); |
| 143 | + |
| 144 | + Assert.IsType<BadRequestObjectResult>(result.Result); |
| 145 | + await _service.DidNotReceiveWithAnyArgs().PauseJobAsync(default!, default!, default, default); |
| 146 | + } |
| 147 | + |
| 148 | + [Fact] |
| 149 | + public async Task PauseJob_FallsBackToSchedulerActorWhenNoUser() |
| 150 | + { |
| 151 | + _userHelper.GetCurrentUser().Returns((AaudUser?)null); |
| 152 | + _service.PauseJobAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<byte[]?>(), Arg.Any<CancellationToken>()) |
| 153 | + .Returns(new PauseResumeResultDto { Id = "raps:role-refresh", IsPaused = true }); |
| 154 | + |
| 155 | + await _sut.PauseJob("raps:role-refresh", null, CancellationToken.None); |
| 156 | + |
| 157 | + await _service.Received(1).PauseJobAsync( |
| 158 | + "raps:role-refresh", |
| 159 | + ISchedulerJobsService.SchedulerActor, |
| 160 | + Arg.Any<byte[]?>(), |
| 161 | + Arg.Any<CancellationToken>()); |
| 162 | + } |
| 163 | + |
| 164 | + // ──────────── ResumeJob ──────────── |
| 165 | + |
| 166 | + [Fact] |
| 167 | + public async Task ResumeJob_ReturnsOkWhenServiceCompletes() |
| 168 | + { |
| 169 | + var dto = new PauseResumeResultDto { Id = "raps:role-refresh", IsPaused = false }; |
| 170 | + _service.ResumeJobAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<CancellationToken>()).Returns(dto); |
| 171 | + |
| 172 | + var result = await _sut.ResumeJob( |
| 173 | + "raps:role-refresh", |
| 174 | + new JobsController.ResumeRequest { RowVersion = Convert.ToBase64String([1, 2, 3, 4]) }, |
| 175 | + CancellationToken.None); |
| 176 | + |
| 177 | + var ok = Assert.IsType<OkObjectResult>(result.Result); |
| 178 | + Assert.Same(dto, ok.Value); |
| 179 | + } |
| 180 | + |
| 181 | + [Fact] |
| 182 | + public async Task ResumeJob_Returns400WhenRowVersionMissing() |
| 183 | + { |
| 184 | + var result = await _sut.ResumeJob("raps:role-refresh", new JobsController.ResumeRequest(), CancellationToken.None); |
| 185 | + |
| 186 | + Assert.IsType<BadRequestObjectResult>(result.Result); |
| 187 | + await _service.DidNotReceiveWithAnyArgs().ResumeJobAsync(default!, default!, default); |
| 188 | + } |
| 189 | + |
| 190 | + [Fact] |
| 191 | + public async Task ResumeJob_Returns400WhenBodyMissing() |
| 192 | + { |
| 193 | + var result = await _sut.ResumeJob("raps:role-refresh", null, CancellationToken.None); |
| 194 | + |
| 195 | + Assert.IsType<BadRequestObjectResult>(result.Result); |
| 196 | + } |
| 197 | + |
| 198 | + [Fact] |
| 199 | + public async Task ResumeJob_Returns400OnInvalidBase64() |
| 200 | + { |
| 201 | + var result = await _sut.ResumeJob( |
| 202 | + "raps:role-refresh", |
| 203 | + new JobsController.ResumeRequest { RowVersion = "***bad***" }, |
| 204 | + CancellationToken.None); |
| 205 | + |
| 206 | + Assert.IsType<BadRequestObjectResult>(result.Result); |
| 207 | + } |
| 208 | + |
| 209 | + [Fact] |
| 210 | + public async Task ResumeJob_Returns403OnSystemJobProtection() |
| 211 | + { |
| 212 | + _service.ResumeJobAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<CancellationToken>()) |
| 213 | + .ThrowsAsyncForAnyArgs(new SchedulerSystemJobProtectedException("__scheduler:reconcile")); |
| 214 | + |
| 215 | + var result = await _sut.ResumeJob( |
| 216 | + "__scheduler:reconcile", |
| 217 | + new JobsController.ResumeRequest { RowVersion = Convert.ToBase64String([1]) }, |
| 218 | + CancellationToken.None); |
| 219 | + |
| 220 | + var status = Assert.IsType<ObjectResult>(result.Result); |
| 221 | + Assert.Equal(StatusCodes.Status403Forbidden, status.StatusCode); |
| 222 | + } |
| 223 | + |
| 224 | + [Fact] |
| 225 | + public async Task ResumeJob_Returns404WithMarkerNotFoundCodeWhenMissing() |
| 226 | + { |
| 227 | + _service.ResumeJobAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<CancellationToken>()) |
| 228 | + .ThrowsAsyncForAnyArgs(new SchedulerJobNotFoundException("nope")); |
| 229 | + |
| 230 | + var result = await _sut.ResumeJob( |
| 231 | + "nope", |
| 232 | + new JobsController.ResumeRequest { RowVersion = Convert.ToBase64String([1]) }, |
| 233 | + CancellationToken.None); |
| 234 | + |
| 235 | + var notFound = Assert.IsType<NotFoundObjectResult>(result.Result); |
| 236 | + Assert.Equal( |
| 237 | + "marker_not_found", |
| 238 | + notFound.Value?.GetType().GetProperty("error")?.GetValue(notFound.Value)); |
| 239 | + } |
| 240 | + |
| 241 | + [Fact] |
| 242 | + public async Task ResumeJob_Returns409OnConcurrencyConflict() |
| 243 | + { |
| 244 | + _service.ResumeJobAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<CancellationToken>()) |
| 245 | + .ThrowsAsyncForAnyArgs(new SchedulerConcurrencyException("raps:role-refresh")); |
| 246 | + |
| 247 | + var result = await _sut.ResumeJob( |
| 248 | + "raps:role-refresh", |
| 249 | + new JobsController.ResumeRequest { RowVersion = Convert.ToBase64String([1]) }, |
| 250 | + CancellationToken.None); |
| 251 | + |
| 252 | + Assert.IsType<ConflictObjectResult>(result.Result); |
| 253 | + } |
| 254 | + } |
| 255 | +} |
0 commit comments