test: add ProposalNumberGenerator, LineItemService state guard, and API client interceptor tests
- ProposalNumberGenerator tests (8 tests): format validation (SHI-YYYY-NNNN),
sequence incrementing, revision skipping, year boundary isolation, uniqueness,
zero-padding, high sequence rollover. Uses SQLite in-memory with Postgres
function stubs to support ExecuteSqlRawAsync.
- LineItemService state guard tests (18 tests): verifies line items cannot be
created/bulk-updated/deleted on Approved or Sent proposals (QA-C2), confirms
operations succeed on InReview and Revised statuses, validates
KeyNotFoundException on missing proposals, verifies audit logging.
- API client interceptor tests (14 tests): request interceptor attaches Bearer
token from sessionStorage (WEB-C1), handles missing/malformed token data,
response interceptor dispatches Redux logout on 401 (WEB-M2), returns friendly
messages for 403/404, extracts server error details, handles network errors.
- DbContextFactory updated to suppress InMemoryEventId.TransactionIgnoredWarning
so BulkUpdateAsync tests work with in-memory provider.
- Added SqliteDbContextFactory for tests requiring relational features.
- Added Microsoft.EntityFrameworkCore.Sqlite to test project dependencies.
Total: 104 .NET tests (was 77), 26 web tests (was 12). CI already wired.
2026-05-27 18:10:55 -04:00
|
|
|
using Microsoft.Data.Sqlite;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using ProposalSystem.Infrastructure.Data;
|
|
|
|
|
|
|
|
|
|
namespace ProposalSystem.Tests.Helpers;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates SQLite-backed in-memory DbContext instances for tests that require relational
|
|
|
|
|
/// features (e.g., ExecuteSqlRawAsync, transactions). The SQLite connection is kept open
|
|
|
|
|
/// for the lifetime of the context; disposing the context closes the connection and
|
|
|
|
|
/// discards the in-memory database.
|
|
|
|
|
///
|
|
|
|
|
/// Registers stub Postgres functions (hashtext, pg_advisory_xact_lock) so that
|
|
|
|
|
/// production SQL using these functions does not throw in the test environment.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static class SqliteDbContextFactory
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a new ProposalDbContext backed by a unique SQLite in-memory database.
|
|
|
|
|
/// The returned context owns the connection; dispose the context to clean up.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static ProposalDbContext Create()
|
|
|
|
|
{
|
|
|
|
|
var connection = new SqliteConnection("DataSource=:memory:");
|
|
|
|
|
connection.Open();
|
|
|
|
|
|
|
|
|
|
// Register Postgres-specific function stubs so ExecuteSqlRawAsync calls
|
|
|
|
|
// like "SELECT pg_advisory_xact_lock(hashtext('...'))" succeed in SQLite.
|
|
|
|
|
RegisterPostgresStubs(connection);
|
|
|
|
|
|
|
|
|
|
var options = new DbContextOptionsBuilder<ProposalDbContext>()
|
|
|
|
|
.UseSqlite(connection)
|
|
|
|
|
.Options;
|
|
|
|
|
|
|
|
|
|
var context = new ProposalDbContext(options);
|
|
|
|
|
context.Database.EnsureCreated();
|
|
|
|
|
return context;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 01:18:30 -04:00
|
|
|
/// <summary>
|
|
|
|
|
/// Opens a shared in-memory database and returns the connection plus a factory
|
|
|
|
|
/// for additional contexts over the SAME database — needed by optimistic-
|
|
|
|
|
/// concurrency tests that simulate two competing writers. Callers dispose the
|
|
|
|
|
/// connection after the contexts.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static (SqliteConnection Connection, Func<ProposalDbContext> ContextFactory) CreateShared()
|
|
|
|
|
{
|
|
|
|
|
var connection = new SqliteConnection("DataSource=:memory:");
|
|
|
|
|
connection.Open();
|
|
|
|
|
RegisterPostgresStubs(connection);
|
|
|
|
|
|
|
|
|
|
var options = new DbContextOptionsBuilder<ProposalDbContext>()
|
|
|
|
|
.UseSqlite(connection)
|
|
|
|
|
.Options;
|
|
|
|
|
|
|
|
|
|
var first = new ProposalDbContext(options);
|
|
|
|
|
first.Database.EnsureCreated();
|
|
|
|
|
first.Dispose();
|
|
|
|
|
|
|
|
|
|
return (connection, () => new ProposalDbContext(options));
|
|
|
|
|
}
|
|
|
|
|
|
test: add ProposalNumberGenerator, LineItemService state guard, and API client interceptor tests
- ProposalNumberGenerator tests (8 tests): format validation (SHI-YYYY-NNNN),
sequence incrementing, revision skipping, year boundary isolation, uniqueness,
zero-padding, high sequence rollover. Uses SQLite in-memory with Postgres
function stubs to support ExecuteSqlRawAsync.
- LineItemService state guard tests (18 tests): verifies line items cannot be
created/bulk-updated/deleted on Approved or Sent proposals (QA-C2), confirms
operations succeed on InReview and Revised statuses, validates
KeyNotFoundException on missing proposals, verifies audit logging.
- API client interceptor tests (14 tests): request interceptor attaches Bearer
token from sessionStorage (WEB-C1), handles missing/malformed token data,
response interceptor dispatches Redux logout on 401 (WEB-M2), returns friendly
messages for 403/404, extracts server error details, handles network errors.
- DbContextFactory updated to suppress InMemoryEventId.TransactionIgnoredWarning
so BulkUpdateAsync tests work with in-memory provider.
- Added SqliteDbContextFactory for tests requiring relational features.
- Added Microsoft.EntityFrameworkCore.Sqlite to test project dependencies.
Total: 104 .NET tests (was 77), 26 web tests (was 12). CI already wired.
2026-05-27 18:10:55 -04:00
|
|
|
private static void RegisterPostgresStubs(SqliteConnection connection)
|
|
|
|
|
{
|
|
|
|
|
// hashtext(text) -> integer — returns a constant; only needed so SQL parses
|
|
|
|
|
connection.CreateFunction("hashtext", (string _) => 0);
|
|
|
|
|
|
|
|
|
|
// pg_advisory_xact_lock(bigint) -> void — no-op in tests
|
|
|
|
|
connection.CreateFunction("pg_advisory_xact_lock", (long _) => 0);
|
|
|
|
|
}
|
|
|
|
|
}
|