mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 17:41:09 +00:00
5ca9ccab2c
AZ-513: POST/PATCH/DELETE /classes for detection-class CRUD; new DetectionClass entity, schema, DTOs, IDetectionClassService. Unblocks ui/AZ-512. AZ-196: POST /devices auto-assigns sequential azj-NNNN serial+email +password and inserts a CompanionPC user. Returns plaintext credentials for the provisioning script. AZ-183: Resources table + POST /get-update + POST /resources/publish for fleet OTA. Per-resource encryption_key column AES-256-CBC encrypted at rest with ResourcesConfig.EncryptionMasterKey; ICache wraps the per-(arch,stage) latest-versions lookup and is invalidated on publish. Adds IDbFactory.RunAdmin<T> overload for write-and-return. Backfills _docs/02_document/module-layout.md to satisfy the implement skill's File Ownership prerequisite (the _docs/ artifact set predates the Step 1.5 module-layout addition). Code review: PASS_WITH_WARNINGS — see _docs/03_implementation/reviews/batch_05_review.md. Co-authored-by: Cursor <cursoragent@cursor.com>
152 lines
5.2 KiB
C#
152 lines
5.2 KiB
C#
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<RegisterDeviceResponseDto>(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<string>();
|
|
|
|
try
|
|
{
|
|
// Act
|
|
using var first = await client.PostAsync("/devices", new { });
|
|
first.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
var firstDto = await first.Content.ReadFromJsonAsync<RegisterDeviceResponseDto>(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<RegisterDeviceResponseDto>(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<RegisterDeviceResponseDto>(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);
|
|
}
|
|
}
|