Add integration tests
This commit is contained in:
21
.github/workflows/ci.yml
vendored
21
.github/workflows/ci.yml
vendored
@@ -22,13 +22,30 @@ jobs:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Run unit tests
|
||||
run: dotnet test src/backend/tests/unit/Randall.Domain.UnitTests.csproj --logger "console;verbosity=normal"
|
||||
run: dotnet test src/backend/tests/unit/Randall.Domain.UnitTests/Randall.Domain.UnitTests.csproj --logger "console;verbosity=normal"
|
||||
|
||||
# ── Integration Tests ──────────────────────────────────────────────────────
|
||||
integration-tests:
|
||||
name: Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Run integration tests
|
||||
run: dotnet test src/backend/tests/integration/Randall.Api.IntegrationTests/Randall.Api.IntegrationTests.csproj --logger "console;verbosity=normal"
|
||||
|
||||
# ── E2E Tests ──────────────────────────────────────────────────────────────
|
||||
e2e:
|
||||
name: E2E Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: unit-tests
|
||||
needs: [unit-tests, integration-tests]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -9,4 +9,7 @@
|
||||
<Folder Name="/tests/unit/">
|
||||
<Project Path="tests/unit/Randall.Domain.UnitTests/Randall.Domain.UnitTests.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/integration/">
|
||||
<Project Path="tests/integration/Randall.Api.IntegrationTests/Randall.Api.IntegrationTests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
@@ -66,3 +66,5 @@ app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
|
||||
public partial class Program { }
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Randall.Api.IntegrationTests.Helpers;
|
||||
|
||||
namespace Randall.Api.IntegrationTests.Admin;
|
||||
|
||||
public class AdminTests(CustomWebApplicationFactory factory) : IClassFixture<CustomWebApplicationFactory>
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetAllUsers_WithoutAuth_Returns401()
|
||||
{
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/admin/users");
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAllUsers_WithNonAdmin_Returns403()
|
||||
{
|
||||
var email = $"{Guid.NewGuid():N}@test.com";
|
||||
var token = await AuthHelper.CreateApprovedUserAndLoginAsync(factory, email);
|
||||
|
||||
var client = factory.CreateClient();
|
||||
AuthHelper.SetBearerToken(client, token);
|
||||
|
||||
var response = await client.GetAsync("/api/admin/users");
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAllUsers_WithAdmin_ReturnsUserList()
|
||||
{
|
||||
var client = factory.CreateClient();
|
||||
var adminLogin = await AuthHelper.LoginAsAdminAsync(client);
|
||||
AuthHelper.SetBearerToken(client, adminLogin.Token);
|
||||
|
||||
var users = await client.GetFromJsonAsync<List<AdminUserResponse>>(
|
||||
"/api/admin/users", AuthHelper.JsonOptions);
|
||||
|
||||
Assert.NotNull(users);
|
||||
Assert.Contains(users, u => u.Email == AuthHelper.AdminEmail && u.IsAdmin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPendingUsers_ShowsNewlyRegisteredUser()
|
||||
{
|
||||
var email = $"{Guid.NewGuid():N}@test.com";
|
||||
var registerClient = factory.CreateClient();
|
||||
await registerClient.PostAsJsonAsync("/api/auth/register",
|
||||
new { Email = email, Password = "Test@1234", Name = "Pending User" });
|
||||
|
||||
var client = factory.CreateClient();
|
||||
var adminLogin = await AuthHelper.LoginAsAdminAsync(client);
|
||||
AuthHelper.SetBearerToken(client, adminLogin.Token);
|
||||
|
||||
var pending = await client.GetFromJsonAsync<List<PendingUserResponse>>(
|
||||
"/api/admin/users/pending", AuthHelper.JsonOptions);
|
||||
|
||||
Assert.NotNull(pending);
|
||||
Assert.Contains(pending, u => u.Email.Equals(email, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApproveUser_AllowsSubsequentLogin()
|
||||
{
|
||||
var email = $"{Guid.NewGuid():N}@test.com";
|
||||
var registerClient = factory.CreateClient();
|
||||
await registerClient.PostAsJsonAsync("/api/auth/register",
|
||||
new { Email = email, Password = "Test@1234", Name = "Pending User" });
|
||||
|
||||
var client = factory.CreateClient();
|
||||
var adminLogin = await AuthHelper.LoginAsAdminAsync(client);
|
||||
AuthHelper.SetBearerToken(client, adminLogin.Token);
|
||||
|
||||
var pending = await client.GetFromJsonAsync<List<PendingUserResponse>>(
|
||||
"/api/admin/users/pending", AuthHelper.JsonOptions);
|
||||
var userId = pending!.First(u => u.Email.Equals(email, StringComparison.OrdinalIgnoreCase)).Id;
|
||||
|
||||
var approveResponse = await client.PostAsync($"/api/admin/users/{userId}/approve", null);
|
||||
Assert.Equal(HttpStatusCode.NoContent, approveResponse.StatusCode);
|
||||
|
||||
// User can now login
|
||||
var loginClient = factory.CreateClient();
|
||||
var login = await AuthHelper.LoginAsync(loginClient, email, "Test@1234");
|
||||
Assert.NotEmpty(login.Token);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MakeAdmin_ElevatesUserToAdmin()
|
||||
{
|
||||
var email = $"{Guid.NewGuid():N}@test.com";
|
||||
await AuthHelper.CreateApprovedUserAndLoginAsync(factory, email);
|
||||
|
||||
var client = factory.CreateClient();
|
||||
var adminLogin = await AuthHelper.LoginAsAdminAsync(client);
|
||||
AuthHelper.SetBearerToken(client, adminLogin.Token);
|
||||
|
||||
var users = await client.GetFromJsonAsync<List<AdminUserResponse>>(
|
||||
"/api/admin/users", AuthHelper.JsonOptions);
|
||||
var userId = users!.First(u => u.Email.Equals(email, StringComparison.OrdinalIgnoreCase)).Id;
|
||||
|
||||
var response = await client.PostAsync($"/api/admin/users/{userId}/make-admin", null);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
|
||||
// Verify the user is now admin
|
||||
var updated = await client.GetFromJsonAsync<List<AdminUserResponse>>(
|
||||
"/api/admin/users", AuthHelper.JsonOptions);
|
||||
Assert.True(updated!.First(u => u.Id == userId).IsAdmin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteUser_NonAdminUser_Returns204()
|
||||
{
|
||||
var email = $"{Guid.NewGuid():N}@test.com";
|
||||
await AuthHelper.CreateApprovedUserAndLoginAsync(factory, email);
|
||||
|
||||
var client = factory.CreateClient();
|
||||
var adminLogin = await AuthHelper.LoginAsAdminAsync(client);
|
||||
AuthHelper.SetBearerToken(client, adminLogin.Token);
|
||||
|
||||
var users = await client.GetFromJsonAsync<List<AdminUserResponse>>(
|
||||
"/api/admin/users", AuthHelper.JsonOptions);
|
||||
var userId = users!.First(u => u.Email.Equals(email, StringComparison.OrdinalIgnoreCase)).Id;
|
||||
|
||||
var response = await client.DeleteAsync($"/api/admin/users/{userId}");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteUser_Self_ReturnsBadRequest()
|
||||
{
|
||||
var client = factory.CreateClient();
|
||||
var adminLogin = await AuthHelper.LoginAsAdminAsync(client);
|
||||
AuthHelper.SetBearerToken(client, adminLogin.Token);
|
||||
|
||||
var users = await client.GetFromJsonAsync<List<AdminUserResponse>>(
|
||||
"/api/admin/users", AuthHelper.JsonOptions);
|
||||
var adminId = users!.First(u => u.Email == AuthHelper.AdminEmail).Id;
|
||||
|
||||
var response = await client.DeleteAsync($"/api/admin/users/{adminId}");
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Randall.Api.IntegrationTests.Helpers;
|
||||
|
||||
namespace Randall.Api.IntegrationTests.Auth;
|
||||
|
||||
public class AuthTests(CustomWebApplicationFactory factory) : IClassFixture<CustomWebApplicationFactory>
|
||||
{
|
||||
private readonly HttpClient _client = factory.CreateClient();
|
||||
|
||||
[Fact]
|
||||
public async Task Register_WithValidData_ReturnsOk()
|
||||
{
|
||||
var response = await _client.PostAsJsonAsync("/api/auth/register",
|
||||
new { Email = $"{Guid.NewGuid():N}@test.com", Password = "Test@1234", Name = "New User" });
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<RegisterResponse>(AuthHelper.JsonOptions);
|
||||
Assert.NotNull(body?.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Register_WithDuplicateEmail_ReturnsBadRequest()
|
||||
{
|
||||
var email = $"{Guid.NewGuid():N}@test.com";
|
||||
await _client.PostAsJsonAsync("/api/auth/register",
|
||||
new { Email = email, Password = "Test@1234", Name = "First" });
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/api/auth/register",
|
||||
new { Email = email, Password = "Test@1234", Name = "Second" });
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Login_WithNonExistentUser_ReturnsBadRequest()
|
||||
{
|
||||
var response = await _client.PostAsJsonAsync("/api/auth/login",
|
||||
new { Email = "nobody@test.com", Password = "Test@1234" });
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Login_WithWrongPassword_ReturnsBadRequest()
|
||||
{
|
||||
var email = $"{Guid.NewGuid():N}@test.com";
|
||||
await _client.PostAsJsonAsync("/api/auth/register",
|
||||
new { Email = email, Password = "Test@1234", Name = "Test User" });
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/api/auth/login",
|
||||
new { Email = email, Password = "WrongPassword" });
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Login_WithUnapprovedUser_ReturnsBadRequest()
|
||||
{
|
||||
var email = $"{Guid.NewGuid():N}@test.com";
|
||||
await _client.PostAsJsonAsync("/api/auth/register",
|
||||
new { Email = email, Password = "Test@1234", Name = "Test User" });
|
||||
|
||||
// Attempt login without approval
|
||||
var response = await _client.PostAsJsonAsync("/api/auth/login",
|
||||
new { Email = email, Password = "Test@1234" });
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Login_AsAdmin_ReturnsTokenWithAdminFlag()
|
||||
{
|
||||
var response = await AuthHelper.LoginAsAdminAsync(_client);
|
||||
|
||||
Assert.NotEmpty(response.Token);
|
||||
Assert.True(response.IsAdmin);
|
||||
Assert.Equal(AuthHelper.AdminEmail, response.Email);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Login_AsApprovedNonAdminUser_ReturnsTokenWithoutAdminFlag()
|
||||
{
|
||||
var email = $"{Guid.NewGuid():N}@test.com";
|
||||
await AuthHelper.CreateApprovedUserAndLoginAsync(factory, email);
|
||||
|
||||
var login = await AuthHelper.LoginAsync(_client, email, "Test@1234");
|
||||
|
||||
Assert.NotEmpty(login.Token);
|
||||
Assert.False(login.IsAdmin);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Randall.Api.IntegrationTests;
|
||||
|
||||
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
private readonly string _dbPath = Path.Combine(
|
||||
Path.GetTempPath(), $"randall_test_{Guid.NewGuid():N}.db");
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.UseEnvironment("Testing");
|
||||
builder.ConfigureAppConfiguration((_, config) =>
|
||||
{
|
||||
config.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ConnectionStrings:DefaultConnection"] = $"Data Source={_dbPath}",
|
||||
["Jwt:Key"] = "test-jwt-secret-key-that-is-at-least-32-chars-long!",
|
||||
["Jwt:Issuer"] = "randall-api",
|
||||
["Jwt:Audience"] = "randall-app",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (disposing && File.Exists(_dbPath))
|
||||
File.Delete(_dbPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Randall.Api.IntegrationTests.Helpers;
|
||||
|
||||
// Local response DTOs — mirror the API's shape without coupling to production types
|
||||
public record LoginResponse(string Token, string Name, string Email, bool IsAdmin);
|
||||
public record RegisterResponse(string Message);
|
||||
public record PendingUserResponse(Guid Id, string Name, string Email);
|
||||
public record AdminUserResponse(Guid Id, string Name, string Email, bool IsApproved, bool IsAdmin);
|
||||
public record WorkplaceResponse(Guid Id, string Name, string Location);
|
||||
public record WorkplaceScheduleResponse(Guid Id, string Name, string Location, bool IsAvailable, string? ReservedBy);
|
||||
public record CreatedReservationResponse(Guid Id, Guid WorkplaceId, string EmployeeName, string Date);
|
||||
public record ReservationResponse(Guid Id, Guid WorkplaceId, string WorkplaceName, string WorkplaceLocation, string Date, string Status, DateTime CreatedAt);
|
||||
public record ReservationDetailResponse(Guid Id, Guid WorkplaceId, string WorkplaceName, string WorkplaceLocation, string EmployeeName, string EmployeeEmail, string Date, string Status, DateTime CreatedAt);
|
||||
|
||||
public static class AuthHelper
|
||||
{
|
||||
public const string AdminEmail = "admin@randall.local";
|
||||
public const string AdminPassword = "Admin@123";
|
||||
|
||||
public static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
public static async Task<LoginResponse> LoginAsync(HttpClient client, string email, string password)
|
||||
{
|
||||
var response = await client.PostAsJsonAsync("/api/auth/login",
|
||||
new { Email = email, Password = password });
|
||||
response.EnsureSuccessStatusCode();
|
||||
return (await response.Content.ReadFromJsonAsync<LoginResponse>(JsonOptions))!;
|
||||
}
|
||||
|
||||
public static Task<LoginResponse> LoginAsAdminAsync(HttpClient client) =>
|
||||
LoginAsync(client, AdminEmail, AdminPassword);
|
||||
|
||||
public static void SetBearerToken(HttpClient client, string token) =>
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new user, approves them via the admin account, and returns their JWT token.
|
||||
/// </summary>
|
||||
public static async Task<string> CreateApprovedUserAndLoginAsync(
|
||||
CustomWebApplicationFactory factory,
|
||||
string email,
|
||||
string password = "Test@1234",
|
||||
string name = "Test User")
|
||||
{
|
||||
var client = factory.CreateClient();
|
||||
await client.PostAsJsonAsync("/api/auth/register", new { Email = email, Password = password, Name = name });
|
||||
|
||||
var adminClient = factory.CreateClient();
|
||||
var adminLogin = await LoginAsAdminAsync(adminClient);
|
||||
SetBearerToken(adminClient, adminLogin.Token);
|
||||
|
||||
var pending = await adminClient.GetFromJsonAsync<List<PendingUserResponse>>(
|
||||
"/api/admin/users/pending", JsonOptions);
|
||||
var user = pending!.First(u => u.Email.Equals(email, StringComparison.OrdinalIgnoreCase));
|
||||
await adminClient.PostAsync($"/api/admin/users/{user.Id}/approve", null);
|
||||
|
||||
var loginClient = factory.CreateClient();
|
||||
var login = await LoginAsync(loginClient, email, password);
|
||||
return login.Token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Randall.Api\Randall.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,186 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Randall.Api.IntegrationTests.Helpers;
|
||||
|
||||
namespace Randall.Api.IntegrationTests.Reservations;
|
||||
|
||||
public class ReservationTests(CustomWebApplicationFactory factory) : IClassFixture<CustomWebApplicationFactory>
|
||||
{
|
||||
[Fact]
|
||||
public async Task Create_WithoutAuth_Returns401()
|
||||
{
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/reservations",
|
||||
new { WorkplaceId = Guid.NewGuid(), Date = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(1) });
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_ValidReservation_Returns201WithDetails()
|
||||
{
|
||||
var (client, workplaceId, date) = await SetupUserWithWorkplaceAsync(daysOffset: 1);
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/reservations",
|
||||
new { WorkplaceId = workplaceId, Date = date });
|
||||
|
||||
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||
var created = await response.Content.ReadFromJsonAsync<CreatedReservationResponse>(AuthHelper.JsonOptions);
|
||||
Assert.NotEqual(Guid.Empty, created!.Id);
|
||||
Assert.Equal(workplaceId, created.WorkplaceId);
|
||||
Assert.Equal(date.ToString("yyyy-MM-dd"), created.Date);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetById_ForExistingReservation_ReturnsDetails()
|
||||
{
|
||||
var (client, workplaceId, date) = await SetupUserWithWorkplaceAsync(daysOffset: 2);
|
||||
|
||||
var createResponse = await client.PostAsJsonAsync("/api/reservations",
|
||||
new { WorkplaceId = workplaceId, Date = date });
|
||||
var created = await createResponse.Content.ReadFromJsonAsync<CreatedReservationResponse>(AuthHelper.JsonOptions);
|
||||
|
||||
var response = await client.GetAsync($"/api/reservations/{created!.Id}");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var detail = await response.Content.ReadFromJsonAsync<ReservationDetailResponse>(AuthHelper.JsonOptions);
|
||||
Assert.Equal(created.Id, detail!.Id);
|
||||
Assert.Equal(workplaceId, detail.WorkplaceId);
|
||||
Assert.Equal("Active", detail.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetById_ForNonExistentReservation_Returns404()
|
||||
{
|
||||
var client = factory.CreateClient();
|
||||
var adminLogin = await AuthHelper.LoginAsAdminAsync(client);
|
||||
AuthHelper.SetBearerToken(client, adminLogin.Token);
|
||||
|
||||
var response = await client.GetAsync($"/api/reservations/{Guid.NewGuid()}");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMy_ReturnsOnlyOwnReservations()
|
||||
{
|
||||
var email = $"{Guid.NewGuid():N}@test.com";
|
||||
var token = await AuthHelper.CreateApprovedUserAndLoginAsync(factory, email);
|
||||
var client = factory.CreateClient();
|
||||
AuthHelper.SetBearerToken(client, token);
|
||||
|
||||
var workplaces = await client.GetFromJsonAsync<List<WorkplaceResponse>>(
|
||||
"/api/workplaces", AuthHelper.JsonOptions);
|
||||
var date = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(3);
|
||||
await client.PostAsJsonAsync("/api/reservations",
|
||||
new { WorkplaceId = workplaces![1].Id, Date = date });
|
||||
|
||||
var response = await client.GetAsync("/api/reservations/my");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var reservations = await response.Content.ReadFromJsonAsync<List<ReservationResponse>>(AuthHelper.JsonOptions);
|
||||
Assert.NotNull(reservations);
|
||||
Assert.NotEmpty(reservations);
|
||||
Assert.All(reservations, r => Assert.Equal("Active", r.Status));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_SameWorkplaceSameDay_ReturnsBadRequest()
|
||||
{
|
||||
// Two different users try to reserve the same workplace on the same day
|
||||
var email1 = $"{Guid.NewGuid():N}@test.com";
|
||||
var email2 = $"{Guid.NewGuid():N}@test.com";
|
||||
var token1 = await AuthHelper.CreateApprovedUserAndLoginAsync(factory, email1);
|
||||
var token2 = await AuthHelper.CreateApprovedUserAndLoginAsync(factory, email2);
|
||||
|
||||
var client = factory.CreateClient();
|
||||
AuthHelper.SetBearerToken(client, token1);
|
||||
var workplaces = await client.GetFromJsonAsync<List<WorkplaceResponse>>(
|
||||
"/api/workplaces", AuthHelper.JsonOptions);
|
||||
var workplaceId = workplaces![2].Id;
|
||||
var date = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(4);
|
||||
|
||||
await client.PostAsJsonAsync("/api/reservations", new { WorkplaceId = workplaceId, Date = date });
|
||||
|
||||
var client2 = factory.CreateClient();
|
||||
AuthHelper.SetBearerToken(client2, token2);
|
||||
var response = await client2.PostAsJsonAsync("/api/reservations",
|
||||
new { WorkplaceId = workplaceId, Date = date });
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_SameEmployeeSameDay_ReturnsBadRequest()
|
||||
{
|
||||
var email = $"{Guid.NewGuid():N}@test.com";
|
||||
var token = await AuthHelper.CreateApprovedUserAndLoginAsync(factory, email);
|
||||
var client = factory.CreateClient();
|
||||
AuthHelper.SetBearerToken(client, token);
|
||||
|
||||
var workplaces = await client.GetFromJsonAsync<List<WorkplaceResponse>>(
|
||||
"/api/workplaces", AuthHelper.JsonOptions);
|
||||
var date = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(5);
|
||||
|
||||
// First reservation succeeds
|
||||
await client.PostAsJsonAsync("/api/reservations",
|
||||
new { WorkplaceId = workplaces![3].Id, Date = date });
|
||||
|
||||
// Same employee, different workplace, same day
|
||||
var response = await client.PostAsJsonAsync("/api/reservations",
|
||||
new { WorkplaceId = workplaces[4].Id, Date = date });
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cancel_OwnReservation_Returns204()
|
||||
{
|
||||
var (client, workplaceId, date) = await SetupUserWithWorkplaceAsync(daysOffset: 6);
|
||||
|
||||
var createResponse = await client.PostAsJsonAsync("/api/reservations",
|
||||
new { WorkplaceId = workplaceId, Date = date });
|
||||
var created = await createResponse.Content.ReadFromJsonAsync<CreatedReservationResponse>(AuthHelper.JsonOptions);
|
||||
|
||||
var response = await client.DeleteAsync($"/api/reservations/{created!.Id}");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cancel_AnotherUsersReservation_ReturnsBadRequest()
|
||||
{
|
||||
// User 1 creates a reservation
|
||||
var (client1, workplaceId, date) = await SetupUserWithWorkplaceAsync(daysOffset: 8);
|
||||
var createResponse = await client1.PostAsJsonAsync("/api/reservations",
|
||||
new { WorkplaceId = workplaceId, Date = date });
|
||||
var created = await createResponse.Content.ReadFromJsonAsync<CreatedReservationResponse>(AuthHelper.JsonOptions);
|
||||
|
||||
// User 2 tries to cancel it
|
||||
var email2 = $"{Guid.NewGuid():N}@test.com";
|
||||
var token2 = await AuthHelper.CreateApprovedUserAndLoginAsync(factory, email2);
|
||||
var client2 = factory.CreateClient();
|
||||
AuthHelper.SetBearerToken(client2, token2);
|
||||
|
||||
var response = await client2.DeleteAsync($"/api/reservations/{created!.Id}");
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
// Helper: creates a fresh approved user and returns their authenticated client,
|
||||
// the first available workplace ID, and a future date unique to the test.
|
||||
private async Task<(HttpClient client, Guid workplaceId, DateOnly date)> SetupUserWithWorkplaceAsync(int daysOffset)
|
||||
{
|
||||
var email = $"{Guid.NewGuid():N}@test.com";
|
||||
var token = await AuthHelper.CreateApprovedUserAndLoginAsync(factory, email);
|
||||
|
||||
var client = factory.CreateClient();
|
||||
AuthHelper.SetBearerToken(client, token);
|
||||
|
||||
var workplaces = await client.GetFromJsonAsync<List<WorkplaceResponse>>(
|
||||
"/api/workplaces", AuthHelper.JsonOptions);
|
||||
|
||||
return (client, workplaces![0].Id, DateOnly.FromDateTime(DateTime.UtcNow).AddDays(daysOffset));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Randall.Api.IntegrationTests.Helpers;
|
||||
|
||||
namespace Randall.Api.IntegrationTests.Workplaces;
|
||||
|
||||
public class WorkplaceTests(CustomWebApplicationFactory factory) : IClassFixture<CustomWebApplicationFactory>
|
||||
{
|
||||
private readonly string _today = DateOnly.FromDateTime(DateTime.UtcNow).ToString("yyyy-MM-dd");
|
||||
private readonly string _yesterday = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(-1).ToString("yyyy-MM-dd");
|
||||
|
||||
[Fact]
|
||||
public async Task GetAll_WithoutAuth_Returns401()
|
||||
{
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/workplaces");
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAll_WithAuth_ReturnsAllSeededWorkplaces()
|
||||
{
|
||||
var client = factory.CreateClient();
|
||||
var adminLogin = await AuthHelper.LoginAsAdminAsync(client);
|
||||
AuthHelper.SetBearerToken(client, adminLogin.Token);
|
||||
|
||||
var workplaces = await client.GetFromJsonAsync<List<WorkplaceResponse>>(
|
||||
"/api/workplaces", AuthHelper.JsonOptions);
|
||||
|
||||
Assert.NotNull(workplaces);
|
||||
Assert.Equal(16, workplaces.Count);
|
||||
Assert.All(workplaces, w =>
|
||||
{
|
||||
Assert.NotEqual(Guid.Empty, w.Id);
|
||||
Assert.NotEmpty(w.Name);
|
||||
Assert.NotEmpty(w.Location);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAvailable_WithPastDate_ReturnsBadRequest()
|
||||
{
|
||||
var client = factory.CreateClient();
|
||||
var adminLogin = await AuthHelper.LoginAsAdminAsync(client);
|
||||
AuthHelper.SetBearerToken(client, adminLogin.Token);
|
||||
|
||||
var response = await client.GetAsync($"/api/workplaces/available?date={_yesterday}");
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAvailable_ForTodayWithNoReservations_ReturnsAllWorkplaces()
|
||||
{
|
||||
var client = factory.CreateClient();
|
||||
var adminLogin = await AuthHelper.LoginAsAdminAsync(client);
|
||||
AuthHelper.SetBearerToken(client, adminLogin.Token);
|
||||
|
||||
var workplaces = await client.GetFromJsonAsync<List<WorkplaceResponse>>(
|
||||
$"/api/workplaces/available?date={_today}", AuthHelper.JsonOptions);
|
||||
|
||||
Assert.NotNull(workplaces);
|
||||
Assert.Equal(16, workplaces.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSchedule_WithoutAuth_Returns401()
|
||||
{
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync($"/api/workplaces/schedule?date={_today}");
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSchedule_ForTodayWithNoReservations_ReturnsAllAsAvailable()
|
||||
{
|
||||
var client = factory.CreateClient();
|
||||
var adminLogin = await AuthHelper.LoginAsAdminAsync(client);
|
||||
AuthHelper.SetBearerToken(client, adminLogin.Token);
|
||||
|
||||
var schedule = await client.GetFromJsonAsync<List<WorkplaceScheduleResponse>>(
|
||||
$"/api/workplaces/schedule?date={_today}", AuthHelper.JsonOptions);
|
||||
|
||||
Assert.NotNull(schedule);
|
||||
Assert.Equal(16, schedule.Count);
|
||||
Assert.All(schedule, s =>
|
||||
{
|
||||
Assert.True(s.IsAvailable);
|
||||
Assert.Null(s.ReservedBy);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSchedule_AfterReservation_ShowsWorkplaceAsUnavailable()
|
||||
{
|
||||
var email = $"{Guid.NewGuid():N}@test.com";
|
||||
var userToken = await AuthHelper.CreateApprovedUserAndLoginAsync(factory, email, name: "Schedule Tester");
|
||||
|
||||
var client = factory.CreateClient();
|
||||
AuthHelper.SetBearerToken(client, userToken);
|
||||
|
||||
var workplaces = await client.GetFromJsonAsync<List<WorkplaceResponse>>(
|
||||
"/api/workplaces", AuthHelper.JsonOptions);
|
||||
var workplaceId = workplaces![0].Id;
|
||||
|
||||
// Reserve for a date unique to this test to avoid conflicts
|
||||
var date = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(7);
|
||||
var dateStr = date.ToString("yyyy-MM-dd");
|
||||
|
||||
await client.PostAsJsonAsync("/api/reservations",
|
||||
new { WorkplaceId = workplaceId, Date = date });
|
||||
|
||||
var schedule = await client.GetFromJsonAsync<List<WorkplaceScheduleResponse>>(
|
||||
$"/api/workplaces/schedule?date={dateStr}", AuthHelper.JsonOptions);
|
||||
|
||||
var entry = schedule!.First(s => s.Id == workplaceId);
|
||||
Assert.False(entry.IsAvailable);
|
||||
Assert.Equal("Schedule Tester", entry.ReservedBy);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user