Add integration tests

This commit is contained in:
Robert van Diest
2026-03-25 19:44:53 +01:00
parent ac37afdcee
commit 4fcd0d60f4
10 changed files with 705 additions and 2 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -66,3 +66,5 @@ app.UseAuthorization();
app.MapControllers();
app.Run();
public partial class Program { }

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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));
}
}

View File

@@ -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);
}
}