Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -503,4 +503,4 @@ web/Areas/Effort/Scripts/Effort_Database_Schema_And_Data_LEGACY.txt
.fallow/
VueApp/.fallow/
jscpd-report/
inspect-report/
inspect-report/
162 changes: 162 additions & 0 deletions test/Services/UserInfoServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.DirectoryServices.Protocols;
using Viper.Areas.Directory.Services;
using Viper.Areas.RAPS.Services;
using Viper.Areas.RAPS.Models.Uinform;
using Viper.Classes.SQLContext;
using Viper.Classes.Utilities;
using Xunit;
using Amazon;
using Amazon.Extensions.NETCore.Setup;

namespace Viper.test.Services
{
public class UserInfoServiceTests
{
private readonly ITestOutputHelper _output;

public UserInfoServiceTests(ITestOutputHelper output)
{
_output = output;
Console.SetOut(new ConsoleRedirector(output));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Console.SetOut is process-global and never restored, so after this test ends the redirector still points at its now-inactive ITestOutputHelper. Later tests that call Console.WriteLine then throw InvalidOperationException: There is no currently active test (seen while merging to Development: EmailNotificationTest.RemoveInstructorScheduleAsync_* failed this way, stack-tracing back into ConsoleRedirector.WriteLine). It's order-dependent, so it can present as flaky.

Minimal fix, make the class IDisposable (xUnit calls Dispose per test):

private readonly TextWriter _originalOut = Console.Out;
public void Dispose() => Console.SetOut(_originalOut);

Console.Out is shared and classes run in parallel, so a [Collection] to serialize these (or dropping the global redirect) fully closes it.

}

[Fact]
public async Task TestGetUserInfo()
{
IConfigurationRoot config;
try
{
// Setup configuration using environment, appsettings, and SSM Parameter Store
var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile("appsettings.Development.json", optional: true)
.AddEnvironmentVariables()
.Build();

var awsOptions = new AWSOptions
{
Region = RegionEndpoint.USWest1
};
var configBuilder = new ConfigurationBuilder()
.AddConfiguration(configuration)
.AddSystemsManager("/Development", awsOptions)
.AddSystemsManager("/Shared", awsOptions);
config = configBuilder.Build();
}
catch (Exception ex) when (ex.ToString().Contains("Amazon") || ex.ToString().Contains("EC2") || ex.ToString().Contains("Metadata") || ex.ToString().Contains("credential"))
{
_output.WriteLine($"[SKIPPED] AWS SSM Parameter Store is not available: {ex.Message}");
return; // Gracefully pass/skip the test in CI/CD pipeline
}

try
{
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(config);
services.AddMemoryCache();
services.AddHttpClient();

// Register database contexts using connection strings from SSM config
void RegisterContext<TContext>(string key) where TContext : DbContext
{
var connStr = config.GetConnectionString(key);
if (string.IsNullOrEmpty(connStr))
{
throw new Exception($"ConnectionString for '{key}' is empty or missing!");
}
services.AddDbContext<TContext>(options => options.UseSqlServer(connStr));
}

RegisterContext<AAUDContext>("AAUD");
RegisterContext<RAPSContext>("RAPS");
RegisterContext<CoursesContext>("Courses");
services.AddDbContext<EquipmentLoanContext>(options => options.UseSqlServer(config.GetConnectionString("VIPER")));
services.AddDbContext<PPSContext>(options => options.UseSqlServer(config.GetConnectionString("VIPER")));
services.AddDbContext<IDCardsContext>(options => options.UseSqlServer(config.GetConnectionString("VIPER")));
services.AddDbContext<KeysContext>(options => options.UseSqlServer(config.GetConnectionString("VIPER")));

services.AddScoped<UserInfoService>();

var serviceProvider = services.BuildServiceProvider();
HttpHelper.Configure(serviceProvider.GetRequiredService<IMemoryCache>(), config, null!, null!, null!, null!);

// Test logic will call GetUserInfoAsync and populate AD/Instinct details

var userInfoService = serviceProvider.GetRequiredService<UserInfoService>();

// Query Mothra ID 00065542 (Brandon Edwards - 'be5')
var result = await userInfoService.GetUserInfoAsync(null, "00065542");
Comment thread
rlorenzo marked this conversation as resolved.
Outdated

if (result == null)
{
_output.WriteLine("[DEBUG] GetUserInfoAsync returned null!");
throw new Xunit.Sdk.XunitException("GetUserInfoAsync returned null");
}

_output.WriteLine($"[DEBUG] IamId: '{result.IamId}'");
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
_output.WriteLine($"[DEBUG] DisplayName: '{result.DisplayFullName}'");
_output.WriteLine($"[DEBUG] InstinctId: '{result.InstinctId}'");
_output.WriteLine($"[DEBUG] InstinctUsername: '{result.InstinctUsername}'");
_output.WriteLine($"[DEBUG] InstinctStatus: '{result.InstinctStatus}'");
_output.WriteLine($"[DEBUG] InstinctIsActive: {result.InstinctIsActive}");

_output.WriteLine($"[DEBUG] ADDisplayName: '{result.ADDisplayName}'");
_output.WriteLine($"[DEBUG] ADMail: '{result.ADMail}'");
_output.WriteLine($"[DEBUG] ADSamAccountName: '{result.ADSamAccountName}'");
_output.WriteLine($"[DEBUG] ADUserPrincipalName: '{result.ADUserPrincipalName}'");
_output.WriteLine($"[DEBUG] ADDistinguishedName: '{result.ADDistinguishedName}'");
_output.WriteLine($"[DEBUG] ADMemberOf count: {result.ADMemberOf?.Count ?? 0}");
if (result.ADMemberOf != null)
{
foreach (var group in result.ADMemberOf)
{
_output.WriteLine($" Group: '{group}'");
}
}

if (result.InstinctRoles != null)
{
_output.WriteLine($"[DEBUG] InstinctRoles: {string.Join(", ", result.InstinctRoles)}");
}

if (result.InstinctInfo != null && !string.IsNullOrEmpty(result.InstinctInfo.ErrorMessage))
{
_output.WriteLine($"[DEBUG] Instinct API Error: {result.InstinctInfo.ErrorMessage}");
}

Assert.NotNull(result.InstinctId);
Assert.Equal("be5", result.InstinctUsername);
}
catch (Exception ex) when (ex.ToString().Contains("SqlException") || ex.ToString().Contains("network-related") || ex.ToString().Contains("login failed") || ex.ToString().Contains("LdapException") || ex.ToString().Contains("Active Directory"))
{
_output.WriteLine($"[SKIPPED] Database or network resources not accessible in this environment: {ex.Message}");
return; // Gracefully pass/skip the test in CI/CD pipeline
}
catch (Exception ex)
{
_output.WriteLine($"[DEBUG] Test execution failed with exception: {ex}");
throw;
}
}

private class ConsoleRedirector : TextWriter
{
private readonly ITestOutputHelper _output;
public ConsoleRedirector(ITestOutputHelper output) => _output = output;
public override System.Text.Encoding Encoding => System.Text.Encoding.UTF8;
public override void WriteLine(string? value) => _output.WriteLine(value ?? "");
public override void Write(string? value) => _output.WriteLine(value ?? "");
}
}
}
Loading
Loading