Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.2.6",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
.DS_Store
.DS_Store
# Build results
[Bb]in/
[Oo]bj/

# DLLs and Executables
*.dll
*.exe
*.pdb
*.user
128 changes: 128 additions & 0 deletions api.tests/Controllers/ReservationContollerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using Controllers;
using Microsoft.AspNetCore.Mvc;
using Models;
using Models.Errors;
using Moq;
using Repositories.Interfaces;
using Validators.Interfaces;

namespace api.tests.Controllers
{
public class ReservationControllerTests
{
private readonly Mock<IReservationRepository> _mockRepo;
private readonly Mock<IRoomRepository> _mockRoomRepo;
private readonly Mock<IGuestRepository> _mockGuestRepo;
private readonly Mock<IReservationValidator> _mockValidator;
private readonly ReservationController _controller;

public ReservationControllerTests()
{
_mockRepo = new Mock<IReservationRepository>();
_mockRoomRepo = new Mock<IRoomRepository>();
_mockGuestRepo = new Mock<IGuestRepository>();
_mockValidator = new Mock<IReservationValidator>();

_controller = new ReservationController(
_mockRepo.Object,
_mockRoomRepo.Object,
_mockGuestRepo.Object,
_mockValidator.Object
);
}

[Fact]
public async Task BookReservation_ReturnsBadRequest_WhenValidationFails()
{
// Arrange
var booking = new Reservation { RoomNumber = "101", GuestEmail = "test@test.com" };
_mockValidator
.Setup(v => v.Validate(booking))
.Returns(new List<string> { "Invalid date range" });

// Act
var result = await _controller.BookReservation(booking);

// Assert
var badRequest = Assert.IsType<BadRequestObjectResult>(result.Result);
Assert.NotNull(badRequest.Value);
}

[Fact]
public async Task BookReservation_ReturnsBadRequest_WhenRoomDoesNotExist()
{
// Arrange
var booking = new Reservation { RoomNumber = "999", GuestEmail = "test@test.com" };
_mockValidator.Setup(v => v.Validate(booking)).Returns(new List<string>());
_mockRoomRepo
.Setup(r => r.GetRoom(booking.RoomNumber))
.ThrowsAsync(new NotFoundException("Room not found"));

// Act
var result = await _controller.BookReservation(booking);

// Assert
Assert.IsType<BadRequestObjectResult>(result.Result);
}

[Fact]
public async Task BookReservation_CreatesGuest_WhenGuestIsNew()
{
// Arrange
var booking = new Reservation
{
RoomNumber = "101",
GuestEmail = "new@guest.com",
Start = DateTime.Now,
End = DateTime.Now.AddDays(1),
};

_mockValidator.Setup(v => v.Validate(booking)).Returns(new List<string>());
_mockRoomRepo
.Setup(r => r.GetRoom(booking.RoomNumber))
.ReturnsAsync(new Room { Number = "101" });

// Simulate Guest NOT found
_mockGuestRepo
.Setup(g => g.GetGuestByEmail(booking.GuestEmail))
.ThrowsAsync(new NotFoundException("Guest not found"));

_mockRepo
.Setup(r => r.CreateReservation(It.IsAny<Reservation>()))
.ReturnsAsync(booking);

// Act
await _controller.BookReservation(booking);

// Assert
_mockGuestRepo.Verify(
g => g.CreateGuest(It.Is<Guest>(gt => gt.Email == booking.GuestEmail)),
Times.Once
);
}

[Fact]
public async Task BookReservation_ReturnsConflict_WhenOverlapExists()
{
// Arrange
var booking = new Reservation { RoomNumber = "101", GuestEmail = "test@test.com" };
_mockValidator.Setup(v => v.Validate(booking)).Returns(new List<string>());
_mockRoomRepo
.Setup(r => r.GetRoom(booking.RoomNumber))
.ReturnsAsync(new Room { Number = "101" });
_mockGuestRepo
.Setup(g => g.GetGuestByEmail(booking.GuestEmail))
.ReturnsAsync(new Guest { Email = "test@test.com", Name = "Test" });

_mockRepo
.Setup(r => r.CreateReservation(It.IsAny<Reservation>()))
.ThrowsAsync(new InvalidOperationException("Room is already booked"));

// Act
var result = await _controller.BookReservation(booking);

// Assert
Assert.IsType<ConflictObjectResult>(result.Result);
}
}
}
117 changes: 117 additions & 0 deletions api.tests/Controllers/StaffControllerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System.Security.Claims;
using Controllers;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Models;
using Moq;
using Repositories.Interfaces;
using Services;
using Xunit;

namespace api.tests.Controllers
{
public class StaffControllerTests
{
private readonly Mock<IConfiguration> _configMock;
private readonly Mock<IReservationRepository> _repoMock;
private readonly Mock<ICheckInService> _checkInServiceMock;
private readonly StaffController _controller;

public StaffControllerTests()
{
_configMock = new Mock<IConfiguration>();
_repoMock = new Mock<IReservationRepository>();
_checkInServiceMock = new Mock<ICheckInService>();

_controller = new StaffController(
_configMock.Object, _repoMock.Object, checkInService: _checkInServiceMock.Object);

// Mocking HttpContext for Authentication methods
var httpContext = new DefaultHttpContext();
_controller.ControllerContext = new ControllerContext { HttpContext = httpContext };
}

[Fact]
public async Task Login_WithCorrectCode_ReturnsOkAndSignsIn()
{
// Arrange
_configMock.Setup(c => c.GetSection("staffAccessCode").Value).Returns("pass");

var authServiceMock = new Mock<IAuthenticationService>();
var serviceProviderMock = new Mock<IServiceProvider>();
serviceProviderMock
.Setup(s => s.GetService(typeof(IAuthenticationService)))
.Returns(authServiceMock.Object);
_controller.ControllerContext.HttpContext.RequestServices = serviceProviderMock.Object;

// Act
var result = await _controller.CheckCode("pass");

Comment on lines +42 to +51

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CheckCode awaits HttpContext.SignOutAsync/SignInAsync. In this test, IAuthenticationService.SignOutAsync/SignInAsync are not set up on the mock, so Moq will return null tasks and the controller will throw when awaiting. Configure those methods to return Task.CompletedTask (and optionally verify they were called).

Copilot uses AI. Check for mistakes.
// Assert
Assert.IsType<OkObjectResult>(result);
}

[Fact]
public async Task GetStaffReservations_ReturnsUpcomingReservations()
{
// Arrange
var expectedReservations = new List<Reservation>
{
new Reservation
{
Id = Guid.NewGuid(),
GuestEmail = "staff_view@test.com",
RoomNumber = "101",
Start = DateTime.Now.AddDays(1),
End = DateTime.Now.AddDays(2),
},
};
_repoMock.Setup(r => r.GetUpcomingReservations()).ReturnsAsync(expectedReservations);

// Act
var result = await _controller.GetStaffReservations();

// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnedReservations = Assert.IsAssignableFrom<IEnumerable<Reservation>>(
okResult.Value
);
Assert.Single(returnedReservations);
Assert.Contains(returnedReservations, r => r.GuestEmail == "staff_view@test.com");
}

[Fact]
public async Task CheckIn_ServiceReturnsSuccess_ReturnsOk()
{
// Arrange
var resId = Guid.NewGuid();
_checkInServiceMock
.Setup(s => s.ProcessCheckIn(resId, "test@test.com"))
.ReturnsAsync((true, string.Empty));

// Act
var result = await _controller.CheckIn(resId, "test@test.com");

// Assert
Assert.IsType<OkResult>(result);
}

[Fact]
public async Task CheckIn_ServiceReturnsNotFoundError_ReturnsNotFound()
{
// Arrange
var resId = Guid.NewGuid();
_checkInServiceMock
.Setup(s => s.ProcessCheckIn(resId, "test@test.com"))
.ReturnsAsync((false, "Reservation not found."));

// Act
var result = await _controller.CheckIn(resId, "test@test.com");

// Assert
Assert.IsType<NotFoundResult>(result);
}
}
}
108 changes: 108 additions & 0 deletions api.tests/Repositories/ReservationRepositoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System.Data;
using Microsoft.Data.Sqlite;
using Models;
using Repositories;
using Xunit;

namespace api.tests.Repositories
{
public class ReservationRepositoryTests : IDisposable
{
private readonly IDbConnection _db;
private readonly ReservationRepository _repo;

public ReservationRepositoryTests()
{
// Set up a fresh in-memory SQLite database for every test
_db = new SqliteConnection("Data Source=:memory:");
_db.Open();

// Initialize the schema (Tables: Guests, Rooms, Reservations)
InitializeSchema();

_repo = new ReservationRepository(_db);
}

private void InitializeSchema()
{
using var command = _db.CreateCommand();
command.CommandText =
@"
CREATE TABLE Guests (Email TEXT PRIMARY KEY, Name TEXT);
CREATE TABLE Rooms (Number TEXT PRIMARY KEY, State INTEGER);
CREATE TABLE Reservations (
Id GUID PRIMARY KEY,
RoomNumber TEXT,
GuestEmail TEXT,
Start DATETIME,
End DATETIME,
CheckedIn INTEGER DEFAULT 0,
CheckedOut INTEGER DEFAULT 0
);
INSERT INTO Rooms (Number, State) VALUES ('101', 1);
INSERT INTO Guests (Email, Name) VALUES ('test@test.com', 'Test Guest');
";
command.ExecuteNonQuery();
}

[Fact]
public async Task CreateReservation_SavesToDatabase()
{
// Arrange
var reservation = new Reservation
{
Id = Guid.NewGuid(),
RoomNumber = "101",
GuestEmail = "test@test.com",
Start = DateTime.Now.AddDays(1),
End = DateTime.Now.AddDays(2),
};

// Act
var result = await _repo.CreateReservation(reservation);

// Assert
Assert.NotNull(result);
var all = await _repo.GetReservations();
Assert.Single(all);
}

[Fact]
public async Task HasConflict_ReturnsTrue_WhenDatesOverlap()
{
// Arrange: Existing booking for tomorrow
var start = DateTime.Today.AddDays(1);
var end = DateTime.Today.AddDays(3);

await _repo.CreateReservation(
new Reservation
{
Id = Guid.NewGuid(),
RoomNumber = "101",
GuestEmail = "a@a.com",
Start = start,
End = end,
}
);

// Act: Try to book an overlapping slot
var overlap = new Reservation
{
RoomNumber = "101",
Start = start.AddDays(1),
End = end.AddDays(1),
GuestEmail = "",
};
var hasConflict = await _repo.HasConflict(overlap);

// Assert
Assert.True(hasConflict);
}

public void Dispose()
{
_db.Close();
_db.Dispose();
}
}
}
Loading
Loading