diff --git a/tests/TarninaHub.Tests/Controllers/ApplicationsControllerTests.cs b/tests/TarninaHub.Tests/Controllers/ApplicationsControllerTests.cs new file mode 100644 index 0000000..021ff9b --- /dev/null +++ b/tests/TarninaHub.Tests/Controllers/ApplicationsControllerTests.cs @@ -0,0 +1,87 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TarninaHub.Application.Abstractions; +using TarninaHub.Application.Dtos; +using TarninaHub.Controllers; +using TarninaHub.Tests.Stubs; + +namespace TarninaHub.Tests.Controllers; + +public class ApplicationsControllerTests +{ + private static ApplicationsController CreateController( + IApplicationService applicationService, + IIdeaService? ideaService = null) + { + var controller = new ApplicationsController(applicationService, ideaService ?? new StubIdeaService()); + var identity = new ClaimsIdentity( + [new Claim("sub", "user:tester"), new Claim("role", "user")], + authenticationType: "test"); + + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }, + }; + + return controller; + } + + [Fact] + public async Task Create_ReturnsConflictWhenCallNotOpen() + { + var service = new StubApplicationService( + new CreateApplicationResult(CreateApplicationOutcome.CallNotOpen, null)); + var controller = CreateController(service); + + var result = await controller.Create( + new CreateApplicationRequest("grant_call:x", "idea:y", null), + CancellationToken.None); + + var conflict = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status409Conflict, conflict.StatusCode); + } + + [Fact] + public async Task Create_ReturnsCreatedWhenSuccessful() + { + var application = new ApplicationResponse( + "application:new", + "grant_call:x", + "idea:y", + new Dictionary(), + "draft", + DateTimeOffset.UtcNow); + var service = new StubApplicationService( + new CreateApplicationResult(CreateApplicationOutcome.Created, application)); + var controller = CreateController(service); + + var result = await controller.Create( + new CreateApplicationRequest("grant_call:x", "idea:y", null), + CancellationToken.None); + + Assert.IsType(result.Result); + } + + private sealed class StubApplicationService : IApplicationService + { + private readonly CreateApplicationResult _createResult; + + public StubApplicationService(CreateApplicationResult createResult) + { + _createResult = createResult; + } + + public Task CreateAsync( + string userId, + CreateApplicationRequest request, + CancellationToken cancellationToken) => Task.FromResult(_createResult); + + public Task GetAsync(string applicationId, CancellationToken cancellationToken) => + Task.FromResult(_createResult.Application); + + public Task> ListForAuthorAsync( + string authorId, + CancellationToken cancellationToken) => Task.FromResult>([]); + } +} diff --git a/tests/TarninaHub.Tests/Domain/IdeaAndApplicationRulesTests.cs b/tests/TarninaHub.Tests/Domain/IdeaAndApplicationRulesTests.cs new file mode 100644 index 0000000..4053837 --- /dev/null +++ b/tests/TarninaHub.Tests/Domain/IdeaAndApplicationRulesTests.cs @@ -0,0 +1,40 @@ +using TarninaHub.Domain.Enums; +using TarninaHub.Domain.Rules; + +namespace TarninaHub.Tests.Domain; + +public class IdeaAndApplicationRulesTests +{ + [Theory] + [InlineData(IdeaStatus.Draft, IdeaStatus.Submitted, true)] + [InlineData(IdeaStatus.Submitted, IdeaStatus.InReview, true)] + [InlineData(IdeaStatus.InReview, IdeaStatus.Accepted, true)] + [InlineData(IdeaStatus.InReview, IdeaStatus.Rejected, true)] + [InlineData(IdeaStatus.Draft, IdeaStatus.Accepted, false)] + [InlineData(IdeaStatus.Accepted, IdeaStatus.Draft, false)] + public void IdeaWorkflow_EnforcesTransitions(IdeaStatus from, IdeaStatus to, bool expected) + { + Assert.Equal(expected, IdeaWorkflow.CanTransition(from, to)); + } + + [Fact] + public void IdeaWorkflow_RecognizesTerminalStates() + { + Assert.True(IdeaWorkflow.IsTerminal(IdeaStatus.Accepted)); + Assert.True(IdeaWorkflow.IsTerminal(IdeaStatus.Rejected)); + Assert.False(IdeaWorkflow.IsTerminal(IdeaStatus.Draft)); + } + + [Fact] + public void ApplicationWindow_IsOpenOnlyWithinOpenCallWindow() + { + var opensAt = new DateTimeOffset(2026, 9, 1, 0, 0, 0, TimeSpan.Zero); + var closesAt = new DateTimeOffset(2026, 12, 31, 23, 59, 59, TimeSpan.Zero); + var now = new DateTimeOffset(2026, 10, 4, 12, 0, 0, TimeSpan.Zero); + + Assert.True(ApplicationWindow.IsOpen("open", opensAt, closesAt, now)); + Assert.False(ApplicationWindow.IsOpen("closed", opensAt, closesAt, now)); + Assert.False(ApplicationWindow.IsOpen("open", opensAt, closesAt, opensAt.AddDays(-1))); + Assert.False(ApplicationWindow.IsOpen("open", opensAt, closesAt, closesAt.AddSeconds(1))); + } +} diff --git a/tests/TarninaHub.Tests/Stubs/StubIdeaService.cs b/tests/TarninaHub.Tests/Stubs/StubIdeaService.cs new file mode 100644 index 0000000..f20dc62 --- /dev/null +++ b/tests/TarninaHub.Tests/Stubs/StubIdeaService.cs @@ -0,0 +1,28 @@ +using TarninaHub.Application.Abstractions; +using TarninaHub.Application.Dtos; + +namespace TarninaHub.Tests.Stubs; + +/// Minimal IIdeaService stub for controller tests. +public sealed class StubIdeaService : IIdeaService +{ + public IdeaResponse? Idea { get; set; } + + public Task CreateAsync(string authorId, CreateIdeaRequest request, CancellationToken cancellationToken) => + Task.FromResult(Idea ?? throw new InvalidOperationException()); + + public Task GetAsync(string ideaId, CancellationToken cancellationToken) => + Task.FromResult(Idea); + + public Task> ListForAuthorAsync(string authorId, CancellationToken cancellationToken) => + Task.FromResult>(Idea is null ? [] : [Idea]); + + public Task UpdateAsync(string ideaId, UpdateIdeaRequest request, CancellationToken cancellationToken) => + Task.FromResult(Idea); + + public Task GetCardAsync(string ideaId, CancellationToken cancellationToken) => + Task.FromResult(null); + + public Task UpdateCardAsync(string ideaId, IdeaCardRequest request, CancellationToken cancellationToken) => + Task.FromResult(null); +}