Added unit tests for Technitiumlibrary.Security.Otp#55
Added unit tests for Technitiumlibrary.Security.Otp#55zbalkan wants to merge 4 commits intoTechnitiumSoftware:masterfrom
Conversation
Signed-off-by: Zafer Balkan <zafer@zaferbalkan.com>
There was a problem hiding this comment.
Pull request overview
Adds an MSTest-based unit test suite for TechnitiumLibrary.Security.OTP and wires it into the repository via solution/project additions plus a GitHub Actions workflow + README badge.
Changes:
- Add
TechnitiumLibrary.UnitTestsproject to the solution and referenceTechnitiumLibrary.Security.OTP. - Add unit tests covering
Authenticator(TOTP generation/validation and algorithm variants) andAuthenticatorKeyUri(construction, parsing/round-tripping, QR code generation). - Add a Windows/MSBuild GitHub Actions workflow to build and run the unit tests, and a README badge for visibility.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| TechnitiumLibrary.sln | Adds the unit test project to the solution configurations. |
| TechnitiumLibrary.UnitTests/TechnitiumLibrary.UnitTests.csproj | Introduces the unit test project definition and OTP project reference. |
| TechnitiumLibrary.UnitTests/TechnitiumLibrary.Security.OTP/AuthenticatorTests.cs | Adds tests for TOTP generation and validation behavior. |
| TechnitiumLibrary.UnitTests/TechnitiumLibrary.Security.OTP/AuthenticatorKeyUriTests.cs | Adds tests for key URI creation/parsing/serialization and QR code output. |
| TechnitiumLibrary.UnitTests/MSTestSettings.cs | Enables parallel test execution at method level. |
| README.md | Adds a CI badge for the unit testing workflow. |
| .github/workflows/unit-testing.yml | Adds a Windows CI job to restore/build/test using MSBuild. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Assert.Contains("otpauth://", uriString); | ||
| Assert.Contains("issuer=ACME", uriString); | ||
| Assert.Contains("alice%40example.com", uriString); // corrected expectation |
There was a problem hiding this comment.
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 |
| byte[] result = uri.GetQRCodePngImage(); | ||
|
|
||
| Assert.IsNotNull(result); | ||
| Assert.IsGreaterThan(32, result.Length, "QR PNG must contain image bytes"); |
There was a problem hiding this comment.
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"); |
| # TechnitiumLibrary | ||
| A library for .net based applications. | ||
|
|
||
| ## Quality Assurance | ||
|
|
||
| [](https://github.com/TechnitiumSoftware/TechnitiumLibrary/actions/workflows/unit-testing.yml) No newline at end of file |
There was a problem hiding this comment.
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.
| public void Constructor_ShouldRejectUnsupportedType() | ||
| { | ||
| AuthenticatorKeyUri uri = new AuthenticatorKeyUri("hotp", "Issuer", "acc", "ABCD"); | ||
| Assert.ThrowsExactly<NotSupportedException>(() => _ = new Authenticator(uri)); |
There was a problem hiding this comment.
Constructor_ShouldRejectUnsupportedType expects NotSupportedException, but Authenticator currently throws due to referencing _keyUri.Type before _keyUri is assigned (null dereference) when keyUri.Type != "totp". Either fix Authenticator to throw NotSupportedException using keyUri.Type, or adjust this test to match the current behavior (prefer fixing Authenticator).
| Assert.ThrowsExactly<NotSupportedException>(() => _ = new Authenticator(uri)); | |
| Assert.ThrowsExactly<NullReferenceException>(() => _ = new Authenticator(uri)); |
| 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)); |
There was a problem hiding this comment.
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)); |
| // 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 |
There was a problem hiding this comment.
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 |
| 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"); |
There was a problem hiding this comment.
Generate_ShouldProduceValidInstance uses Assert.IsGreaterThanOrEqualTo which is not available in many MSTest versions, and the check/message don’t align with the inputs (keySize: 10 but comparing length against 8). Prefer an assertion that works across MSTest versions and validate against the expected Base32 length/relationship to keySize so the test is meaningful and stable.
| 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)."); |
Relies on #29 being accepted first