Improve SGU logon resilience and client UX

This commit is contained in:
2026-09-01 10:37:40 -06:00
parent 3d0897316d
commit da01343985
27 changed files with 869 additions and 37 deletions
@@ -0,0 +1,44 @@
using System.Text;
using SGU.AuthBroker.Core.Profiles;
using Xunit;
namespace SGU.AuthBroker.Core.Tests;
public sealed class SguHtmlDecoderTests
{
[Fact]
public void DecodesWindows1252WhenSguOmitsACharset()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
byte[] bytes = Encoding.GetEncoding(1252).GetBytes(
"<span>ANALISTA DE INMERSIÓN — FACULTAD DE INGENIERÍA</span>");
string decoded = SguHtmlDecoder.Decode(bytes);
Assert.Contains("INMERSIÓN", decoded, StringComparison.Ordinal);
Assert.Contains("INGENIERÍA", decoded, StringComparison.Ordinal);
Assert.Contains('—', decoded);
Assert.DoesNotContain('\uFFFD', decoded);
}
[Fact]
public void FallsBackWhenTheDeclaredUtf8CharsetIsIncorrect()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
byte[] bytes = Encoding.GetEncoding(1252).GetBytes("JESÚS GONZÁLEZ");
string decoded = SguHtmlDecoder.Decode(bytes, "utf-8");
Assert.Equal("JESÚS GONZÁLEZ", decoded);
}
[Fact]
public void HonorsValidUtf8WithoutADeclaration()
{
byte[] bytes = Encoding.UTF8.GetBytes("María del Carmen 🔐");
string decoded = SguHtmlDecoder.Decode(bytes);
Assert.Equal("María del Carmen 🔐", decoded);
}
}
@@ -38,11 +38,11 @@ public sealed class SguProfileParserTests
Assert.NotNull(profile);
Assert.Equal("017045", profile.EmployeeNumber);
Assert.Equal("JESÚS ALEJANDRO ROSALES GONZÁLEZ", profile.DisplayName);
Assert.Equal("Jesús Alejandro Rosales González", profile.DisplayName);
Assert.Equal("persona@lasalle.mx", profile.Email);
Assert.Equal("SINDICALIZADO QUINCENAL (ACTIVO)", profile.EmployeeType);
Assert.Equal("ANALISTA DE PROYECTOS", profile.JobTitle);
Assert.Equal("FACULTAD DE INGENIERÍA", profile.Department);
Assert.Equal("Sindicalizado quincenal (activo)", profile.EmployeeType);
Assert.Equal("Analista de Proyectos", profile.JobTitle);
Assert.Equal("Facultad de Ingeniería", profile.Department);
}
[Fact]
@@ -68,12 +68,44 @@ public sealed class SguProfileParserTests
InstitutionalProfile? profile = SguProfileParser.ParseMenu(html);
Assert.NotNull(profile);
Assert.Equal("MARÍA & JOSÉ", profile.DisplayName);
Assert.Equal("María & José", profile.DisplayName);
Assert.Null(profile.Email);
Assert.Null(profile.JobTitle);
Assert.Null(profile.Department);
}
[Theory]
[InlineData("MARÍA DEL CARMEN", "María del Carmen")]
[InlineData("MIGUEL DE CERVANTES", "Miguel de Cervantes")]
[InlineData("CARLOS DE LA FUENTE", "Carlos de la Fuente")]
[InlineData("MARÍA-JOSÉ O'CONNOR", "María-José O'Connor")]
public void PreservesSpanishNameParticlesAndAccents(string source, string expected)
{
const string marker = "ctl00_lblNombreUsuario";
InstitutionalProfile? profile = SguProfileParser.ParseMenu(
$"<span id=\"{marker}\">{source}</span>");
Assert.NotNull(profile);
Assert.Equal(expected, profile.DisplayName);
}
[Fact]
public void RejectsReplacementCharactersInsteadOfWritingCorruptMetadata()
{
const string html = """
<span id="ctl00_contenedor_decEncabezado_lblNombre">017045 - JESS ROSALES</span>
<span id="ctl00_contenedor_decEncabezado_lblPuesto">ANALISTA DE INMERSIN</span>
""";
InstitutionalProfile? profile = SguProfileParser.ParseAdministrative(html, "017045");
Assert.NotNull(profile);
Assert.Null(profile.DisplayName);
Assert.Null(profile.JobTitle);
Assert.Equal("017045", profile.EmployeeNumber);
}
[Fact]
public void MissingKnownFieldsProducesNoProfile()
{
@@ -10,9 +10,11 @@ namespace SGU.CredentialProvider.SmokeProbe;
internal static class Program
{
private static readonly Guid ProviderClassId = new("D789CFD8-5AD4-489F-9B83-7EB5D9D09335");
private static readonly Guid ProviderLogoFieldType = new("2D837775-F6CD-464E-A745-482FD0B47493");
private static readonly string[] ExpectedLabels =
[
"Acceso institucional SGU",
"Acceso institucional SGU",
"Usa tu clave institucional (DO, AL o AD + 6 dígitos) y contraseña.",
"Clave institucional",
@@ -52,6 +54,7 @@ internal static class Program
ThrowIfFailed(provider.GetFieldDescriptorCount(out uint fieldCount), "GetFieldDescriptorCount");
List<string> labels = [];
bool providerLogoPresent = false;
for (uint index = 0; index < fieldCount; index++)
{
ThrowIfFailed(provider.GetFieldDescriptorAt(index, out IntPtr descriptorPointer), "GetFieldDescriptorAt");
@@ -64,6 +67,8 @@ internal static class Program
{
FieldDescriptor descriptor = Marshal.PtrToStructure<FieldDescriptor>(descriptorPointer);
labels.Add(descriptor.Label ?? string.Empty);
providerLogoPresent |= descriptor.FieldType == FieldType.TileImage &&
descriptor.FieldTypeGuid == ProviderLogoFieldType;
}
finally
{
@@ -81,6 +86,7 @@ internal static class Program
bool passed = fieldCount == ExpectedLabels.Length &&
credentialCount == 1 &&
credential != IntPtr.Zero &&
providerLogoPresent &&
labels.SequenceEqual(ExpectedLabels, StringComparer.Ordinal);
if (mode != "enumeration" && passed)
@@ -96,6 +102,7 @@ internal static class Program
usageScenario = "Logon",
fieldCount,
labels,
providerLogoPresent,
credentialCount,
defaultIndex,
autoLogon = autoLogon != 0
@@ -7,6 +7,12 @@ namespace SGU.CredentialProvider.Tests;
public sealed class BrokerClientTests
{
[Fact]
public void DefaultClientTimeoutLeavesMarginForThePortalAndBroker()
{
Assert.Equal(20, new ProviderSettings().TimeoutSeconds);
}
[Fact]
public async Task SendsTheOriginalPasswordWithoutDerivation()
{
@@ -0,0 +1,34 @@
using System.Drawing;
using Lithnet.CredentialProvider;
using Xunit;
namespace SGU.CredentialProvider.Tests;
public sealed class ProviderTileIconTests
{
[Fact]
public void ProviderPublishesASeventyTwoPixelLogoForSignInOptions()
{
SguCredentialProvider provider = new();
CredentialProviderLogoControl logo = Assert.Single(
provider.GetControls(UsageScenario.Logon).OfType<CredentialProviderLogoControl>());
Assert.Equal(ProviderTileIcon.Size, logo.Bitmap.Width);
Assert.Equal(ProviderTileIcon.Size, logo.Bitmap.Height);
Assert.Equal(Color.FromArgb(0, 83, 155).ToArgb(), logo.Bitmap.GetPixel(0, 0).ToArgb());
int lightPixels = 0;
for (int x = 0; x < logo.Bitmap.Width; x++)
{
for (int y = 0; y < logo.Bitmap.Height; y++)
{
if (logo.Bitmap.GetPixel(x, y).GetBrightness() > 0.7f)
{
lightPixels++;
}
}
}
Assert.InRange(lightPixels, 200, 2_000);
}
}