Skip to content
9 changes: 8 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ dotnet_style_qualification_for_property = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_event = false:silent

# EF Core scaffold-generated DbContext files — partial methods are intentional extension points
# EF Core scaffold-generated DbContext files — partial methods are intentional extension points,
# and the maintainability index is dominated by the generated OnModelCreating (not hand-maintained code)
[**/SQLContext/*Context.cs]
dotnet_diagnostic.S3251.severity = none
dotnet_diagnostic.CA1505.severity = none

# S6964: flags value-type props as under-postable, but ApiPagination is never bound from a request:
# ApiPaginationAttribute.OnActionExecuting always constructs the object and overwrites the action argument
[web/Models/ApiPagination.cs]
dotnet_diagnostic.S6964.severity = none
40 changes: 24 additions & 16 deletions test/ClinicalScheduler/Integration/ServiceLayerIntegrationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,25 @@ public class ServiceLayerIntegrationTest : IntegrationTestBase

public ServiceLayerIntegrationTest()
{

// Create mock services since actual services require VIPERContext
// Note: studentLogger not used since we're mocking the service
_studentScheduleService = CreateMockStudentScheduleService();
_instructorScheduleService = CreateMockInstructorScheduleService();

_clinicalScheduleService = new ClinicalScheduleService(
_studentScheduleService,
_instructorScheduleService
);

var personLogger = Substitute.For<ILogger<PersonService>>();
_personService = new PersonService(personLogger, Context, AaudContext);

// EvaluationPolicyService is now static-like with no constructor parameters needed
_evaluationPolicyService = new EvaluationPolicyService();
}

// Note: studentLogger not used since we're mocking the service
private static IStudentScheduleService CreateMockStudentScheduleService()
{
var mockStudentService = Substitute.For<IStudentScheduleService>();
mockStudentService.GetStudentScheduleAsync(
Arg.Any<int?>(), Arg.Any<string>(), Arg.Any<int?>(), Arg.Any<int?>(),
Expand Down Expand Up @@ -84,9 +100,12 @@ public ServiceLayerIntegrationTest()

return Task.FromResult(mockData);
});
_studentScheduleService = mockStudentService;
return mockStudentService;
}

// Note: instructorLogger not used since we're mocking the service
// Note: instructorLogger not used since we're mocking the service
private static IInstructorScheduleService CreateMockInstructorScheduleService()
{
var mockInstructorService = Substitute.For<IInstructorScheduleService>();
mockInstructorService.GetInstructorScheduleAsync(
Arg.Any<int?>(), Arg.Any<string>(), Arg.Any<int?>(), Arg.Any<int?>(),
Expand Down Expand Up @@ -159,18 +178,7 @@ public ServiceLayerIntegrationTest()

return Task.FromResult(mockData);
});
_instructorScheduleService = mockInstructorService;

_clinicalScheduleService = new ClinicalScheduleService(
_studentScheduleService,
_instructorScheduleService
);

var personLogger = Substitute.For<ILogger<PersonService>>();
_personService = new PersonService(personLogger, Context, AaudContext);

// EvaluationPolicyService is now static-like with no constructor parameters needed
_evaluationPolicyService = new EvaluationPolicyService();
return mockInstructorService;
}

[Fact]
Expand Down
141 changes: 141 additions & 0 deletions test/Directory/DirectoryControllerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Viper.Areas.Directory.Controllers;
using Viper.Classes.SQLContext;
using Viper.Models.AAUD;

// Not Viper.test.Directory: a namespace segment named "Directory" shadows System.IO.Directory
// for every file under Viper.test.*
// ReSharper disable once CheckNamespace
namespace Viper.test.DirectoryArea;

/// <summary>
/// Tests for the shared directory search query, run against the relational SQLite
/// provider so the predicate goes through EF's SQL translator like it does on SQL
/// Server (the InMemory provider executes LINQ in memory and skips translation).
/// SQLite string matching is case-sensitive where SQL Server's default collation is
/// not, so seed data matches the search term's exact case.
/// </summary>
public sealed class DirectoryControllerTests : IDisposable
{
/// <summary>
/// A trimmed AAUDContext mapping only AaudUser: the full AAUD warehouse model has
/// computed columns and views SQLite EnsureCreated cannot build (same pattern as
/// StudentGroupServiceQueryTests). Current is a plain column here, so tests set it
/// directly instead of via the production computed column.
/// </summary>
private sealed class TestAaudContext(DbContextOptions<AAUDContext> options) : AAUDContext(options)
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var clrType in modelBuilder.Model.GetEntityTypes()
.Select(e => e.ClrType)
.Where(t => t != typeof(AaudUser))
.Distinct()
.ToList())
{
modelBuilder.Ignore(clrType);
}
modelBuilder.Entity<AaudUser>().HasKey(e => e.AaudUserId);
}
}

private readonly SqliteConnection _connection;
private readonly AAUDContext _context;

public DirectoryControllerTests()
{
_connection = new SqliteConnection("DataSource=:memory:");
_connection.Open();
_context = new TestAaudContext(
new DbContextOptionsBuilder<AAUDContext>().UseSqlite(_connection).Options);
_context.Database.EnsureCreated();
}

public void Dispose()
{
_context.Dispose();
_connection.Dispose();
}

[Fact]
public async Task SearchCurrentAaudUsers_MatchesNameAndEveryIdentifierField()
{
SeedUser(1, lastName: "Delfigo", firstName: "Ann");
SeedUser(2, lastName: "Berry", firstName: "Bo", mailId: "fig@ucdavis.edu");
SeedUser(3, lastName: "Cherry", firstName: "Cy", loginId: "figuser");
SeedUser(4, lastName: "Damson", firstName: "Di", spridenId: "fig123");
SeedUser(5, lastName: "Elder", firstName: "Ed", pidm: "00fig");
SeedUser(6, lastName: "Fennel", firstName: "Flo", mothraId: "fig999");
SeedUser(7, lastName: "Guava", firstName: "Gil", employeeId: "emp-fig");
SeedUser(8, lastName: "Haw", firstName: "Hal", iamId: "iam-fig");
SeedUser(9, lastName: "Ivy", firstName: "Ira");
await _context.SaveChangesAsync(TestContext.Current.CancellationToken);

var results = await DirectoryController.SearchCurrentAaudUsers(_context, "fig");

// One match per field, none for user 9, ordered by last name
Assert.Equal([2, 3, 4, 1, 5, 6, 7, 8], results.Select(u => u.AaudUserId));
}

[Fact]
public async Task SearchCurrentAaudUsers_MatchesTermSpanningFirstAndLastName()
{
SeedUser(1, lastName: "Graham", firstName: "Anna");
SeedUser(2, lastName: "Graham", firstName: "Steve");
await _context.SaveChangesAsync(TestContext.Current.CancellationToken);

var results = await DirectoryController.SearchCurrentAaudUsers(_context, "na Gra");

Assert.Equal([1], results.Select(u => u.AaudUserId));
}

[Fact]
public async Task SearchCurrentAaudUsers_ExcludesUsersNoLongerCurrent()
{
SeedUser(1, lastName: "Figworth", firstName: "Cur");
SeedUser(2, lastName: "Figworth", firstName: "Old", current: 0);
await _context.SaveChangesAsync(TestContext.Current.CancellationToken);

var results = await DirectoryController.SearchCurrentAaudUsers(_context, "Figworth");

Assert.Equal([1], results.Select(u => u.AaudUserId));
}

[Fact]
public async Task SearchCurrentAaudUsers_OrdersByLastNameThenFirstName()
{
SeedUser(1, lastName: "Fig", firstName: "Zoe");
SeedUser(2, lastName: "Fig", firstName: "Al");
SeedUser(3, lastName: "Elm", firstName: "Bea", mailId: "Fig@ucdavis.edu");
await _context.SaveChangesAsync(TestContext.Current.CancellationToken);

var results = await DirectoryController.SearchCurrentAaudUsers(_context, "Fig");

Assert.Equal([3, 2, 1], results.Select(u => u.AaudUserId));
}

private void SeedUser(int id, string lastName, string firstName, string? mailId = null,
string? loginId = null, string? spridenId = null, string? pidm = null,
string? mothraId = null, string? employeeId = null, string? iamId = null, int current = 1)
{
_context.AaudUsers.Add(new AaudUser
{
AaudUserId = id,
ClientId = "test",
MothraId = mothraId ?? $"m-{id}",
LoginId = loginId,
MailId = mailId,
SpridenId = spridenId,
Pidm = pidm,
EmployeeId = employeeId,
IamId = iamId,
LastName = lastName,
FirstName = firstName,
DisplayLastName = lastName,
DisplayFirstName = firstName,
DisplayFullName = firstName + " " + lastName,
Current = current
});
}
}
50 changes: 50 additions & 0 deletions test/RAPS/AdGroupsControllerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Runtime.Versioning;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Viper.Areas.RAPS.Controllers;
using Viper.Areas.RAPS.Models;
using Viper.Classes.SQLContext;
using Viper.Models.RAPS;

namespace Viper.test.RAPS
{
[SupportedOSPlatform("windows")]
public class AdGroupsControllerTests
{
[Fact]
public async Task UpdateGroup_RejectsMismatchedGroupId_AndLeavesGroupUnchanged()
{
using var sqlLiteConnection = new SqliteConnection("Filename=:memory:");
await sqlLiteConnection.OpenAsync(TestContext.Current.CancellationToken);
var sqlLiteContextOptions = new DbContextOptionsBuilder<RAPSContext>()
.UseSqlite(sqlLiteConnection)
.Options;
using var context = new RAPSContext(sqlLiteContextOptions);
await context.Database.EnsureCreatedAsync(TestContext.Current.CancellationToken);

// arrange
var ouGroup = new OuGroup { Name = "OriginalName", Description = "Original description" };
context.OuGroups.Add(ouGroup);
await context.SaveChangesAsync(TestContext.Current.CancellationToken);

var adGroupsController = new AdGroupsController(context);
var mismatchedEdit = new GroupAddEdit
{
GroupId = ouGroup.OugroupId + 1,
Name = "ChangedName",
Description = "Changed description"
};

// act
var result = await adGroupsController.UpdateGroup(ouGroup.OugroupId, mismatchedEdit);

// assert
Assert.IsType<BadRequestResult>(result);
var unchangedGroup = await context.OuGroups.FindAsync(new object?[] { ouGroup.OugroupId }, TestContext.Current.CancellationToken);
Assert.NotNull(unchangedGroup);
Assert.Equal("OriginalName", unchangedGroup.Name);
Assert.Equal("Original description", unchangedGroup.Description);
}
}
}
Loading
Loading