using System.Net; using System.Net.Http.Json; using System.Text.Json; using System.Text.RegularExpressions; using Azaion.E2E.Helpers; using FluentAssertions; using Xunit; namespace Azaion.E2E.Tests; [Collection("E2E")] public sealed class DeviceRegistrationTests { private static readonly JsonSerializerOptions ResponseJsonOptions = new() { PropertyNameCaseInsensitive = true }; private static readonly Regex SerialPattern = new(@"^azj-\d{4}$", RegexOptions.Compiled); private static readonly Regex EmailPattern = new(@"^azj-\d{4}@azaion\.com$", RegexOptions.Compiled); private sealed record RegisterDeviceResponseDto(string Serial, string Email, string Password); private readonly TestFixture _fixture; public DeviceRegistrationTests(TestFixture fixture) => _fixture = fixture; private static string EmailPath(string email) => $"/users/{Uri.EscapeDataString(email)}"; [Fact] public async Task AC1_Post_devices_returns_serial_email_and_password() { // Arrange using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); string? createdEmail = null; try { // Act using var response = await client.PostAsync("/devices", new { }); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var dto = await response.Content.ReadFromJsonAsync(ResponseJsonOptions); dto.Should().NotBeNull(); SerialPattern.IsMatch(dto!.Serial).Should().BeTrue($"serial '{dto.Serial}' should match azj-NNNN"); EmailPattern.IsMatch(dto.Email).Should().BeTrue($"email '{dto.Email}' should match azj-NNNN@azaion.com"); dto.Password.Should().HaveLength(32); dto.Email.Should().StartWith(dto.Serial); createdEmail = dto.Email; } finally { if (createdEmail is not null) using (await client.DeleteAsync(EmailPath(createdEmail))) { } } } [Fact] public async Task AC2_Sequential_device_serials_are_strictly_increasing() { // Arrange using var client = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); var emails = new List(); try { // Act using var first = await client.PostAsync("/devices", new { }); first.StatusCode.Should().Be(HttpStatusCode.OK); var firstDto = await first.Content.ReadFromJsonAsync(ResponseJsonOptions); firstDto.Should().NotBeNull(); emails.Add(firstDto!.Email); using var second = await client.PostAsync("/devices", new { }); second.StatusCode.Should().Be(HttpStatusCode.OK); var secondDto = await second.Content.ReadFromJsonAsync(ResponseJsonOptions); secondDto.Should().NotBeNull(); emails.Add(secondDto!.Email); // Assert var firstNumber = int.Parse(firstDto.Serial[4..]); var secondNumber = int.Parse(secondDto.Serial[4..]); secondNumber.Should().Be(firstNumber + 1); } finally { foreach (var email in emails) using (await client.DeleteAsync(EmailPath(email))) { } } } [Fact] public async Task AC3_Returned_credentials_can_login() { // Arrange using var adminClient = _fixture.CreateAuthenticatedClient(_fixture.AdminToken); string? createdEmail = null; try { using var response = await adminClient.PostAsync("/devices", new { }); response.StatusCode.Should().Be(HttpStatusCode.OK); var dto = await response.Content.ReadFromJsonAsync(ResponseJsonOptions); dto.Should().NotBeNull(); createdEmail = dto!.Email; // Act using var loginClient = _fixture.CreateApiClient(); var token = await loginClient.LoginAsync(dto.Email, dto.Password); // Assert token.Should().NotBeNullOrWhiteSpace(); } finally { if (createdEmail is not null) using (await adminClient.DeleteAsync(EmailPath(createdEmail))) { } } } [Fact] public async Task AC4_Post_devices_without_jwt_returns_401() { // Arrange using var client = _fixture.CreateApiClient(); // Act using var response = await client.PostAsync("/devices", new { }); // Assert response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } [Fact] public async Task AC4_Post_devices_with_non_admin_jwt_returns_403() { // Arrange var loginClient = _fixture.CreateApiClient(); var uploaderToken = await loginClient.LoginAsync(_fixture.UploaderEmail, _fixture.UploaderPassword); loginClient.Dispose(); using var client = _fixture.CreateAuthenticatedClient(uploaderToken); // Act using var response = await client.PostAsync("/devices", new { }); // Assert response.StatusCode.Should().Be(HttpStatusCode.Forbidden); } }