13976dea3b
Replace hardcoded route strings with ApiRoutes.* constants: - ApiRoutes.Auth.PublicKey, Login, Logout, Me - ApiRoutes.Search.Base - ApiRoutes.Lookup.Items, ProfitCenters, WorkCenters, Operators
177 lines
7.3 KiB
C#
177 lines
7.3 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using JdeScoping.Core.ApiContracts;
|
|
using JdeScoping.Core.Models;
|
|
using JdeScoping.Core.Models.Auth;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Shouldly;
|
|
|
|
namespace JdeScoping.Api.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Integration tests for authentication flow with encrypted login.
|
|
/// Note: These tests require a running test server with UseFakeAuth=true
|
|
/// </summary>
|
|
public class AuthenticationTests : IClassFixture<TestWebApplicationFactory>
|
|
{
|
|
private readonly TestWebApplicationFactory _factory;
|
|
private readonly HttpClient _client;
|
|
|
|
public AuthenticationTests(TestWebApplicationFactory factory)
|
|
{
|
|
_factory = factory;
|
|
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
|
|
{
|
|
HandleCookies = true,
|
|
AllowAutoRedirect = false
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches the server's public key and encrypts login credentials.
|
|
/// </summary>
|
|
private async Task<EncryptedLoginRequest> EncryptLoginAsync(HttpClient client, string username, string password)
|
|
{
|
|
// Step 1: Fetch the public key from the server
|
|
var publicKeyResponse = await client.GetFromJsonAsync<PublicKeyResponse>($"/{ApiRoutes.Auth.PublicKey}");
|
|
publicKeyResponse.ShouldNotBeNull();
|
|
publicKeyResponse.PublicKeyPem.ShouldStartWith("-----BEGIN PUBLIC KEY-----");
|
|
|
|
// Step 2: Create login model and serialize to JSON
|
|
var loginModel = new LoginModel { Username = username, Password = password };
|
|
var json = JsonSerializer.Serialize(loginModel);
|
|
var plaintext = Encoding.UTF8.GetBytes(json);
|
|
|
|
// Step 3: Import the public key and encrypt
|
|
using var rsa = RSA.Create();
|
|
rsa.ImportFromPem(publicKeyResponse.PublicKeyPem);
|
|
var ciphertext = rsa.Encrypt(plaintext, RSAEncryptionPadding.OaepSHA256);
|
|
|
|
return new EncryptedLoginRequest(Convert.ToBase64String(ciphertext));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetPublicKey_ReturnsValidPemKey()
|
|
{
|
|
// Act
|
|
var response = await _client.GetAsync($"/{ApiRoutes.Auth.PublicKey}");
|
|
|
|
// Assert
|
|
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
var publicKeyResponse = await response.Content.ReadFromJsonAsync<PublicKeyResponse>();
|
|
publicKeyResponse.ShouldNotBeNull();
|
|
publicKeyResponse.PublicKeyPem.ShouldStartWith("-----BEGIN PUBLIC KEY-----");
|
|
publicKeyResponse.PublicKeyPem.ShouldContain("-----END PUBLIC KEY-----");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FullLoginLogoutFlow_WithCookies()
|
|
{
|
|
// Step 1: Login with encrypted credentials
|
|
var encryptedRequest = await EncryptLoginAsync(_client, "testuser", "testpass");
|
|
var loginResponse = await _client.PostAsJsonAsync($"/{ApiRoutes.Auth.Login}", encryptedRequest);
|
|
|
|
loginResponse.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
var loginResult = await loginResponse.Content.ReadFromJsonAsync<LoginResultModel>();
|
|
loginResult.ShouldNotBeNull();
|
|
loginResult.Success.ShouldBeTrue();
|
|
loginResult.User.ShouldNotBeNull();
|
|
loginResult.User.Username.ShouldBe("testuser");
|
|
|
|
// Step 2: Verify we can access protected endpoint
|
|
var meResponse = await _client.GetAsync($"/{ApiRoutes.Auth.Me}");
|
|
meResponse.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
var meUser = await meResponse.Content.ReadFromJsonAsync<UserInfo>();
|
|
meUser.ShouldNotBeNull();
|
|
meUser.Username.ShouldBe("testuser");
|
|
|
|
// Step 3: Logout
|
|
var logoutResponse = await _client.PostAsync($"/{ApiRoutes.Auth.Logout}", null);
|
|
logoutResponse.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
|
|
// Step 4: Verify protected endpoint returns 401 after logout
|
|
var afterLogoutResponse = await _client.GetAsync($"/{ApiRoutes.Auth.Me}");
|
|
afterLogoutResponse.StatusCode.ShouldBe(HttpStatusCode.Unauthorized);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProtectedEndpoints_Return401_WithoutAuth()
|
|
{
|
|
// Use a fresh client without cookies (using factory to connect to test server)
|
|
var freshClient = _factory.CreateClient(new WebApplicationFactoryClientOptions
|
|
{
|
|
HandleCookies = false,
|
|
AllowAutoRedirect = false
|
|
});
|
|
|
|
// Search endpoints require auth
|
|
var searchResponse = await freshClient.GetAsync($"/{ApiRoutes.Search.Base}");
|
|
searchResponse.StatusCode.ShouldBe(HttpStatusCode.Unauthorized);
|
|
|
|
// Auth me endpoint requires auth
|
|
var meResponse = await freshClient.GetAsync($"/{ApiRoutes.Auth.Me}");
|
|
meResponse.StatusCode.ShouldBe(HttpStatusCode.Unauthorized);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProtectedEndpoints_Work_WithAuthCookie()
|
|
{
|
|
// Login first with encrypted credentials
|
|
var encryptedRequest = await EncryptLoginAsync(_client, "testuser", "testpass");
|
|
var loginResponse = await _client.PostAsJsonAsync($"/{ApiRoutes.Auth.Login}", encryptedRequest);
|
|
loginResponse.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
|
|
var loginResult = await loginResponse.Content.ReadFromJsonAsync<LoginResultModel>();
|
|
loginResult.ShouldNotBeNull();
|
|
loginResult.Success.ShouldBeTrue();
|
|
|
|
// Now search endpoint should work
|
|
var searchResponse = await _client.GetAsync($"/{ApiRoutes.Search.Base}");
|
|
searchResponse.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LookupEndpoints_DoNotRequireAuth()
|
|
{
|
|
// Use a fresh client without cookies (using factory to connect to test server)
|
|
var freshClient = _factory.CreateClient(new WebApplicationFactoryClientOptions
|
|
{
|
|
HandleCookies = false,
|
|
AllowAutoRedirect = false
|
|
});
|
|
|
|
// Lookup endpoints should work without auth
|
|
var itemsResponse = await freshClient.GetAsync($"/{ApiRoutes.Lookup.Items}?q=test");
|
|
itemsResponse.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
|
|
var profitCentersResponse = await freshClient.GetAsync($"/{ApiRoutes.Lookup.ProfitCenters}?q=test");
|
|
profitCentersResponse.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
|
|
var workCentersResponse = await freshClient.GetAsync($"/{ApiRoutes.Lookup.WorkCenters}?q=test");
|
|
workCentersResponse.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
|
|
var operatorsResponse = await freshClient.GetAsync($"/{ApiRoutes.Lookup.Operators}?q=test");
|
|
operatorsResponse.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Login_WithInvalidEncryptedData_ReturnsBadRequest()
|
|
{
|
|
// Arrange - send invalid encrypted data
|
|
var invalidRequest = new EncryptedLoginRequest("not-valid-base64!!!");
|
|
|
|
// Act
|
|
var response = await _client.PostAsJsonAsync($"/{ApiRoutes.Auth.Login}", invalidRequest);
|
|
|
|
// Assert
|
|
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
|
var result = await response.Content.ReadFromJsonAsync<LoginResultModel>();
|
|
result.ShouldNotBeNull();
|
|
result.Success.ShouldBeFalse();
|
|
result.ErrorMessage.ShouldBe("Invalid encrypted payload");
|
|
}
|
|
}
|