-
Notifications
You must be signed in to change notification settings - Fork 59
Added unit tests for Technitiumlibrary.Security.Otp #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| name: Unit testing (Windows / MSBuild) | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| push: | ||
| branches: ["master"] | ||
| pull_request: | ||
| branches: ["master"] | ||
| schedule: | ||
| - cron: "0 0 * * 0" # weekly, Sunday 00:00 UTC | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: windows-latest | ||
|
|
||
| env: | ||
| SOLUTION_NAME: TechnitiumLibrary.sln | ||
| BUILD_CONFIGURATION: Debug | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Install .NET 9 SDK | ||
| uses: actions/setup-dotnet@v4 | ||
| with: | ||
| dotnet-version: 9.0.x | ||
|
|
||
| - name: Add MSBuild to PATH | ||
| uses: microsoft/setup-msbuild@v2 | ||
|
|
||
| - name: Restore | ||
| run: msbuild ${{ env.SOLUTION_NAME }} /t:Restore | ||
|
|
||
| - name: Build | ||
| run: msbuild ${{ env.SOLUTION_NAME }} /m /p:Configuration=${{ env.BUILD_CONFIGURATION }} | ||
|
|
||
| - name: Test (msbuild) | ||
| run: msbuild TechnitiumLibrary.UnitTests\TechnitiumLibrary.UnitTests.csproj /t:Test /p:Configuration=${{ env.BUILD_CONFIGURATION }} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,6 @@ | ||
| # TechnitiumLibrary | ||
| A library for .net based applications. | ||
|
|
||
| ## Quality Assurance | ||
|
|
||
| [](https://github.com/TechnitiumSoftware/TechnitiumLibrary/actions/workflows/unit-testing.yml) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
|
||
| [assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,123 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using TechnitiumLibrary.Security.OTP; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace TechnitiumLibrary.UnitTests.TechnitiumLibrary.Security.OTP | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [TestClass] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public sealed class AuthenticatorKeyUriTests | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [TestMethod] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void Constructor_ShouldAssignFieldsProperly() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AuthenticatorKeyUri uri = new AuthenticatorKeyUri( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "totp", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "ExampleCorp", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "user@example.com", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "SECRET123", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| algorithm: "SHA256", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| digits: 8, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| period: 45); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.AreEqual("totp", uri.Type); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.AreEqual("ExampleCorp", uri.Issuer); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.AreEqual("user@example.com", uri.AccountName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.AreEqual("SECRET123", uri.Secret); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.AreEqual("SHA256", uri.Algorithm); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.AreEqual(8, uri.Digits); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.AreEqual(45, uri.Period); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [TestMethod] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void Constructor_ShouldRejectInvalidDigitRange() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ = new AuthenticatorKeyUri("totp", "X", "Y", "ABC", digits: 5)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [TestMethod] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void Constructor_ShouldRejectNegativePeriod() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ = new AuthenticatorKeyUri("totp", "X", "Y", "ABC", period: -1)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [TestMethod] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void Generate_ShouldProduceValidInstance() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AuthenticatorKeyUri uri = AuthenticatorKeyUri.Generate( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| issuer: "Corp", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| accountName: "user@example.com", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| keySize: 10); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.AreEqual("totp", uri.Type); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.AreEqual("Corp", uri.Issuer); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.AreEqual("user@example.com", uri.AccountName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.IsNotNull(uri.Secret); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.IsGreaterThanOrEqualTo(8, uri.Secret.Length, "Base32 length must be greater than raw bytes"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+48
to
+57
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AuthenticatorKeyUri uri = AuthenticatorKeyUri.Generate( | |
| issuer: "Corp", | |
| accountName: "user@example.com", | |
| keySize: 10); | |
| Assert.AreEqual("totp", uri.Type); | |
| Assert.AreEqual("Corp", uri.Issuer); | |
| Assert.AreEqual("user@example.com", uri.AccountName); | |
| Assert.IsNotNull(uri.Secret); | |
| Assert.IsGreaterThanOrEqualTo(8, uri.Secret.Length, "Base32 length must be greater than raw bytes"); | |
| const int keySize = 10; | |
| AuthenticatorKeyUri uri = AuthenticatorKeyUri.Generate( | |
| issuer: "Corp", | |
| accountName: "user@example.com", | |
| keySize: keySize); | |
| Assert.AreEqual("totp", uri.Type); | |
| Assert.AreEqual("Corp", uri.Issuer); | |
| Assert.AreEqual("user@example.com", uri.AccountName); | |
| Assert.IsNotNull(uri.Secret); | |
| int expectedMinBase32Length = (int)Math.Ceiling(keySize * 8 / 5.0); | |
| Assert.IsTrue( | |
| uri.Secret.Length >= expectedMinBase32Length, | |
| "Base32-encoded secret length must be at least ceil(keySize * 8 / 5)."); |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert.Contains(...) is not available in many MSTest versions (commonly StringAssert.Contains is used instead). As written, this may fail to compile depending on the MSTest packages/SDK actually used by the project.
| Assert.Contains("otpauth://", uriString); | |
| Assert.Contains("issuer=ACME", uriString); | |
| Assert.Contains("alice%40example.com", uriString); // corrected expectation | |
| StringAssert.Contains(uriString, "otpauth://"); | |
| StringAssert.Contains(uriString, "issuer=ACME"); | |
| StringAssert.Contains(uriString, "alice%40example.com"); // corrected expectation |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GetQRCodePngImage_ShouldReturnNonEmptyByteArray uses Assert.IsGreaterThan, which is not available in many MSTest versions. Use an assertion that is supported broadly (e.g., checking result.Length with Assert.IsTrue) to avoid build breaks.
| Assert.IsGreaterThan(32, result.Length, "QR PNG must contain image bytes"); | |
| Assert.IsTrue(result.Length > 32, "QR PNG must contain image bytes"); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,154 @@ | ||||||||||||||||||||||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||||||||||||||||||||||
| using System; | ||||||||||||||||||||||
| using TechnitiumLibrary.Security.OTP; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| namespace TechnitiumLibrary.UnitTests.TechnitiumLibrary.Security.OTP | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| [TestClass] | ||||||||||||||||||||||
| public sealed class AuthenticatorTests | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // | ||||||||||||||||||||||
| // RFC 4226 Appendix D test vector | ||||||||||||||||||||||
| // Secret = "12345678901234567890" in ASCII | ||||||||||||||||||||||
| // which Base32 encodes to: | ||||||||||||||||||||||
| // "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ" | ||||||||||||||||||||||
| // | ||||||||||||||||||||||
| private const string RfcBase32Secret = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| [TestMethod] | ||||||||||||||||||||||
| public void Constructor_ShouldRejectUnsupportedType() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| AuthenticatorKeyUri uri = new AuthenticatorKeyUri("hotp", "Issuer", "acc", "ABCD"); | ||||||||||||||||||||||
| Assert.ThrowsExactly<NotSupportedException>(() => _ = new Authenticator(uri)); | ||||||||||||||||||||||
|
||||||||||||||||||||||
| Assert.ThrowsExactly<NotSupportedException>(() => _ = new Authenticator(uri)); | |
| Assert.ThrowsExactly<NullReferenceException>(() => _ = new Authenticator(uri)); |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test comment mentions a default "windowSteps = 1" skew allowance, but Authenticator.IsTOTPValid defaults to fudge = 10 periods. Update the comment (or pass an explicit fudge value in the assertion) so the test documents the actual behavior being exercised.
| // Generate a code for the NEXT step (+30s) so it is within +1 window | |
| string codeNextWindow = auth.GetTOTP(utcNow.AddSeconds(30)); | |
| // Default windowSteps = 1 accepts ±1 step | |
| // Generate a code for the NEXT step (+30s), which is well within the default skew window | |
| string codeNextWindow = auth.GetTOTP(utcNow.AddSeconds(30)); | |
| // Default fudge = 10 accepts ±10 steps |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IsTOTPValid_ShouldReturnFalseOutsideSkewWindow generates a TOTP using a fixed 2020 timestamp, but IsTOTPValid() validates against DateTime.UtcNow. This makes the test trivially pass (the code will never match) and it does not actually verify the skew/fudge window behavior. Generate the candidate code relative to the same captured utcNow that IsTOTPValid will use (or add an overload that accepts a timestamp for validation).
| DateTime now = new DateTime(2020, 10, 10, 12, 00, 00, DateTimeKind.Local); | |
| // Generate 6 periods ahead (6 * 30s = 180s) | |
| // Default fudge = 10 periods → OK until 10. | |
| string farFutureCode = auth.GetTOTP(now.AddSeconds(11 * 30)); | |
| DateTime utcNow = DateTime.UtcNow; | |
| // Generate a code 11 periods ahead (11 * 30s = 330s) | |
| // Assuming a default skew of ±10 periods, this should be rejected. | |
| string farFutureCode = auth.GetTOTP(utcNow.AddSeconds(11 * 30)); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| <Project Sdk="MSTest.Sdk/4.0.1"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net9.0</TargetFramework> | ||
| <LangVersion>latest</LangVersion> | ||
| <ImplicitUsings>disable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| <UseVSTest>true</UseVSTest> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <Folder Include="TechnitiumLibrary.Security.OTP\" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\TechnitiumLibrary.Security.OTP\TechnitiumLibrary.Security.OTP.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR title/description suggests it only adds OTP unit tests, but it also introduces the entire unit test project, a new CI workflow, and a README badge. If the intent is to depend on PR #29 for test infrastructure, consider retargeting this PR on top of #29 or updating the title/description to reflect the additional infra changes to avoid confusion during review/merge.