Files
Cleanuparr/code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/General/OidcConfigTests.cs
2026-03-12 22:12:20 +02:00

358 lines
9.5 KiB
C#

using Cleanuparr.Persistence.Models.Auth;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Auth;
public sealed class OidcConfigTests
{
#region Validate - Disabled Config
[Fact]
public void Validate_Disabled_WithEmptyFields_DoesNotThrow()
{
var config = new OidcConfig
{
Enabled = false,
IssuerUrl = string.Empty,
ClientId = string.Empty
};
Should.NotThrow(() => config.Validate());
}
[Fact]
public void Validate_Disabled_WithPopulatedFields_DoesNotThrow()
{
var config = new OidcConfig
{
Enabled = false,
IssuerUrl = "https://auth.example.com",
ClientId = "my-client"
};
Should.NotThrow(() => config.Validate());
}
#endregion
#region Validate - Issuer URL
[Fact]
public void Validate_Enabled_ValidHttpsIssuerUrl_DoesNotThrow()
{
var config = new OidcConfig
{
Enabled = true,
IssuerUrl = "https://auth.example.com/application/o/cleanuparr/",
ClientId = "my-client",
ProviderName = "Authentik"
};
Should.NotThrow(() => config.Validate());
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void Validate_Enabled_EmptyIssuerUrl_ThrowsValidationException(string issuerUrl)
{
var config = new OidcConfig
{
Enabled = true,
IssuerUrl = issuerUrl,
ClientId = "my-client"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
exception.Message.ShouldBe("OIDC Issuer URL is required when OIDC is enabled");
}
[Fact]
public void Validate_Enabled_InvalidIssuerUrl_ThrowsValidationException()
{
var config = new OidcConfig
{
Enabled = true,
IssuerUrl = "not-a-valid-url",
ClientId = "my-client"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
exception.Message.ShouldBe("OIDC Issuer URL must be a valid absolute URL");
}
[Fact]
public void Validate_Enabled_HttpIssuerUrl_ThrowsValidationException()
{
var config = new OidcConfig
{
Enabled = true,
IssuerUrl = "http://auth.example.com",
ClientId = "my-client"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
exception.Message.ShouldBe("OIDC Issuer URL must use HTTPS");
}
[Theory]
[InlineData("http://localhost:8080/auth")]
[InlineData("http://127.0.0.1:9000/auth")]
[InlineData("http://[::1]:9000/auth")]
public void Validate_Enabled_HttpLocalhostIssuerUrl_DoesNotThrow(string issuerUrl)
{
var config = new OidcConfig
{
Enabled = true,
IssuerUrl = issuerUrl,
ClientId = "my-client",
ProviderName = "Dev"
};
Should.NotThrow(() => config.Validate());
}
[Fact]
public void Validate_Enabled_IssuerUrlWithTrailingSlash_DoesNotThrow()
{
var config = new OidcConfig
{
Enabled = true,
IssuerUrl = "https://auth.example.com/",
ClientId = "my-client",
ProviderName = "Authentik"
};
Should.NotThrow(() => config.Validate());
}
#endregion
#region Validate - Client ID
[Theory]
[InlineData("")]
[InlineData(" ")]
public void Validate_Enabled_EmptyClientId_ThrowsValidationException(string clientId)
{
var config = new OidcConfig
{
Enabled = true,
IssuerUrl = "https://auth.example.com",
ClientId = clientId
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
exception.Message.ShouldBe("OIDC Client ID is required when OIDC is enabled");
}
#endregion
#region Validate - Provider Name
[Theory]
[InlineData("")]
[InlineData(" ")]
public void Validate_Enabled_EmptyProviderName_ThrowsValidationException(string providerName)
{
var config = new OidcConfig
{
Enabled = true,
IssuerUrl = "https://auth.example.com",
ClientId = "my-client",
ProviderName = providerName
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
exception.Message.ShouldBe("OIDC Provider Name is required when OIDC is enabled");
}
#endregion
#region Validate - Full Valid Configs
[Fact]
public void Validate_Enabled_ValidFullConfig_DoesNotThrow()
{
var config = new OidcConfig
{
Enabled = true,
IssuerUrl = "https://authentik.example.com/application/o/cleanuparr/",
ClientId = "cleanuparr-client-id",
ClientSecret = "my-secret",
Scopes = "openid profile email",
AuthorizedSubject = "user-123-abc",
ProviderName = "Authentik"
};
Should.NotThrow(() => config.Validate());
}
[Fact]
public void Validate_Enabled_WithoutClientSecret_DoesNotThrow()
{
var config = new OidcConfig
{
Enabled = true,
IssuerUrl = "https://auth.example.com",
ClientId = "my-client",
ClientSecret = string.Empty,
ProviderName = "Keycloak"
};
Should.NotThrow(() => config.Validate());
}
#endregion
#region Default Values
[Fact]
public void DefaultValues_AreCorrect()
{
var config = new OidcConfig();
config.Enabled.ShouldBeFalse();
config.IssuerUrl.ShouldBe(string.Empty);
config.ClientId.ShouldBe(string.Empty);
config.ClientSecret.ShouldBe(string.Empty);
config.Scopes.ShouldBe("openid profile email");
config.AuthorizedSubject.ShouldBe(string.Empty);
config.ProviderName.ShouldBe("OIDC");
config.ExclusiveMode.ShouldBeFalse();
}
#endregion
#region Validate - Exclusive Mode
[Fact]
public void Validate_ExclusiveMode_WhenOidcDisabled_Throws()
{
var config = new OidcConfig
{
Enabled = false,
ExclusiveMode = true,
AuthorizedSubject = "some-subject"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
exception.Message.ShouldBe("OIDC must be enabled to use exclusive mode");
}
[Fact]
public void Validate_ExclusiveMode_WithoutAuthorizedSubject_DoesNotThrow()
{
var config = new OidcConfig
{
Enabled = true,
ExclusiveMode = true,
IssuerUrl = "https://auth.example.com",
ClientId = "my-client",
ProviderName = "Test",
AuthorizedSubject = string.Empty
};
Should.NotThrow(() => config.Validate());
}
[Fact]
public void Validate_ExclusiveMode_FullyConfigured_DoesNotThrow()
{
var config = new OidcConfig
{
Enabled = true,
ExclusiveMode = true,
IssuerUrl = "https://auth.example.com",
ClientId = "my-client",
ProviderName = "Test",
AuthorizedSubject = "user-123"
};
Should.NotThrow(() => config.Validate());
}
[Fact]
public void Validate_ExclusiveModeFalse_OidcDisabled_DoesNotThrow()
{
var config = new OidcConfig
{
Enabled = false,
ExclusiveMode = false
};
Should.NotThrow(() => config.Validate());
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void Validate_ExclusiveMode_WhitespaceAuthorizedSubject_DoesNotThrow(string subject)
{
var config = new OidcConfig
{
Enabled = true,
ExclusiveMode = true,
IssuerUrl = "https://auth.example.com",
ClientId = "my-client",
ProviderName = "Test",
AuthorizedSubject = subject
};
Should.NotThrow(() => config.Validate());
}
#endregion
#region Additional Edge Cases
[Fact]
public void Validate_Enabled_HttpLocalhostWithoutPort_DoesNotThrow()
{
var config = new OidcConfig
{
Enabled = true,
IssuerUrl = "http://localhost/auth",
ClientId = "my-client",
ProviderName = "Dev"
};
Should.NotThrow(() => config.Validate());
}
[Fact]
public void Validate_Enabled_ScopesWithoutOpenid_StillPasses()
{
// Documenting current behavior: the Validate method does not enforce "openid" in scopes.
// This is intentional — the IdP will reject if openid is missing, giving a clear error.
var config = new OidcConfig
{
Enabled = true,
IssuerUrl = "https://auth.example.com",
ClientId = "my-client",
Scopes = "profile email",
ProviderName = "Test"
};
Should.NotThrow(() => config.Validate());
}
[Fact]
public void Validate_Enabled_FtpScheme_ThrowsValidationException()
{
var config = new OidcConfig
{
Enabled = true,
IssuerUrl = "ftp://auth.example.com",
ClientId = "my-client"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
exception.Message.ShouldBe("OIDC Issuer URL must use HTTPS");
}
#endregion
}