mirror of
https://github.com/XFox111/SimpleOTP.git
synced 2026-04-22 08:00:45 +03:00
Initial commit
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* This file is used by Code Analysis to maintain SuppressMessage
|
||||
* attributes that are applied to this project.
|
||||
* Project-level suppressions either have no target or are given
|
||||
* a specific target and scoped to a namespace, type, member, etc.
|
||||
*/
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1625:Element documentation should not be copied and pasted", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1519:Braces should not be omitted from multi-line child statement", Justification = "Reviewd by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "Reviewd by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
|
||||
@@ -0,0 +1,37 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SimpleOTP.Helpers;
|
||||
|
||||
namespace SimpleOTP.Test.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit-tests for Base32 encoder.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class Base32UnitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Test overall work of the encoder.
|
||||
/// </summary>
|
||||
[TestMethod("Overall Base32 encoder test")]
|
||||
public void EncoderTest()
|
||||
{
|
||||
// byte[] bytes = new byte[new Random().Next(128, 161)]; // FIXME: See SimpleOTP.Helpers.Base32Encoder.Encode()
|
||||
byte[] bytes = new byte[160];
|
||||
new Random().NextBytes(bytes);
|
||||
string str = Base32Encoder.Encode(bytes);
|
||||
|
||||
bytes = Base32Encoder.Decode(str);
|
||||
string result = Base32Encoder.Encode(bytes);
|
||||
Assert.AreEqual(str, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SimpleOTP.Helpers;
|
||||
|
||||
namespace SimpleOTP.Test.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit-tests for key generator.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SecretGeneratorUnitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Overall test of key generator.
|
||||
/// </summary>
|
||||
[TestMethod("Overall generator tests")]
|
||||
public void Test_Generator()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => SecretGenerator.GenerateSecret(64));
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => SecretGenerator.GenerateSecret(256));
|
||||
|
||||
string key = SecretGenerator.GenerateSecret();
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(key));
|
||||
|
||||
key = SecretGenerator.GenerateSecret(128);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SimpleOTP.Models;
|
||||
|
||||
namespace SimpleOTP.Test.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit-tests for OTP URI parser.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class UriParserUnitTest
|
||||
{
|
||||
private static readonly Guid TestGuid = Guid.NewGuid();
|
||||
|
||||
private readonly string[] uriParts =
|
||||
{
|
||||
"otpauth",
|
||||
"totp|hotp",
|
||||
"FoxDev%20Studio",
|
||||
"eugene@xfox111.net",
|
||||
"secret=ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH",
|
||||
"issuer=FoxDev%20Studio%20Issuer",
|
||||
"algorithm=SHA1|algorithm=SHA512",
|
||||
"digits=8",
|
||||
"period=10",
|
||||
"counter=10000",
|
||||
};
|
||||
|
||||
private readonly OTPConfiguration[] configs =
|
||||
{
|
||||
new ()
|
||||
{
|
||||
AccountName = "eugene@xfox111.net",
|
||||
Algorithm = Enums.Algorithm.SHA512,
|
||||
Digits = 8,
|
||||
Type = Enums.OTPType.TOTP,
|
||||
Secret = "ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH",
|
||||
Issuer = "FoxDev Studio Issuer",
|
||||
IssuerLabel = "FoxDev Studio",
|
||||
Id = TestGuid,
|
||||
Period = TimeSpan.FromSeconds(10),
|
||||
},
|
||||
new ()
|
||||
{
|
||||
AccountName = "eugene@xfox111.net",
|
||||
Algorithm = Enums.Algorithm.SHA512,
|
||||
Digits = 8,
|
||||
Type = Enums.OTPType.HOTP,
|
||||
Secret = "ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH",
|
||||
Issuer = "FoxDev Studio Issuer",
|
||||
IssuerLabel = "FoxDev Studio",
|
||||
Id = TestGuid,
|
||||
Counter = 10000,
|
||||
},
|
||||
new ()
|
||||
{
|
||||
AccountName = "eugene@xfox111.net",
|
||||
Secret = "ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH",
|
||||
Issuer = "FoxDev Studio",
|
||||
IssuerLabel = "FoxDev Studio",
|
||||
Id = TestGuid,
|
||||
},
|
||||
new ()
|
||||
{
|
||||
AccountName = "eugene@xfox111.net",
|
||||
Type = Enums.OTPType.HOTP,
|
||||
Secret = "ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH",
|
||||
Issuer = "FoxDev Studio",
|
||||
IssuerLabel = "FoxDev Studio",
|
||||
Id = TestGuid,
|
||||
Counter = 10000,
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Test parser with full TOTP URI.
|
||||
/// </summary>
|
||||
[TestMethod("Valid full URI (TOTP)")]
|
||||
public void ParseValidUri_FullFormed_Totp()
|
||||
{
|
||||
string uri = $"{uriParts[0]}://{uriParts[1].Split('|')[0]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[5]}&{uriParts[6].Split('|')[1]}&{uriParts[7]}&{uriParts[8]}";
|
||||
var config = SimpleOTP.Helpers.UriParser.ParseUri(new Uri(uri));
|
||||
Assert.IsNotNull(config);
|
||||
config.Id = TestGuid;
|
||||
Assert.AreEqual(configs[0], config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test parser with full HOTP URI.
|
||||
/// </summary>
|
||||
[TestMethod("Valid full URI (HOTP)")]
|
||||
public void ParseValidUri_FullFormed_Hotp()
|
||||
{
|
||||
string uri = $"{uriParts[0]}://{uriParts[1].Split('|')[1]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[5]}&{uriParts[6].Split('|')[1]}&{uriParts[7]}&{uriParts[9]}";
|
||||
var config = SimpleOTP.Helpers.UriParser.ParseUri(new Uri(uri));
|
||||
Assert.IsNotNull(config);
|
||||
config.Id = TestGuid;
|
||||
Assert.AreEqual(configs[1], config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test parser with TOTP URI. Minimal parameter set.
|
||||
/// </summary>
|
||||
[TestMethod("Valid minimal URI (TOTP)")]
|
||||
public void ParseValidUri_Minimal_Totp()
|
||||
{
|
||||
string uri = $"{uriParts[0]}://{uriParts[1].Split('|')[0]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}";
|
||||
var config = SimpleOTP.Helpers.UriParser.ParseUri(new Uri(uri));
|
||||
Assert.IsNotNull(config);
|
||||
config.Id = TestGuid;
|
||||
Assert.AreEqual(configs[2], config);
|
||||
|
||||
config = SimpleOTP.Helpers.UriParser.ParseUri(new Uri($"otpauth://totp/eugene@xfox111.net?{uriParts[4]}&{uriParts[5]}&algorithm=SHA256"));
|
||||
Assert.IsNotNull(config);
|
||||
config.Id = TestGuid;
|
||||
Assert.AreEqual(configs[2] with { IssuerLabel = "FoxDev Studio Issuer", Issuer = "FoxDev Studio Issuer", Algorithm = Enums.Algorithm.SHA256 }, config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test parser with HOTP URI. Minimal parameter set.
|
||||
/// </summary>
|
||||
[TestMethod("Valid minimal URI (HOTP)")]
|
||||
public void ParseValidUri_Minimal_Hotp()
|
||||
{
|
||||
string uri = $"{uriParts[0]}://{uriParts[1].Split('|')[1]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[9]}";
|
||||
var config = SimpleOTP.Helpers.UriParser.ParseUri(new Uri(uri));
|
||||
Assert.IsNotNull(config);
|
||||
config.Id = TestGuid;
|
||||
Assert.AreEqual(configs[3], config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test parser with invalid OTP URIs.
|
||||
/// </summary>
|
||||
[TestMethod("Invalid URI")]
|
||||
public void ParseInvalidUris()
|
||||
{
|
||||
string[] uris =
|
||||
{
|
||||
$"https://{uriParts[1].Split('|')[1]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[9]}",
|
||||
$"{uriParts[0]}://otp/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[9]}",
|
||||
$"{uriParts[0]}://{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[9]}",
|
||||
$"{uriParts[0]}://{uriParts[1].Split('|')[0]}/{uriParts[3]}?{uriParts[4]}&{uriParts[9]}",
|
||||
$"{uriParts[0]}://{uriParts[1].Split('|')[0]}/?{uriParts[4]}&{uriParts[9]}",
|
||||
$"{uriParts[0]}://{uriParts[1].Split('|')[0]}/{uriParts[2]}:{uriParts[3]}",
|
||||
$"{uriParts[0]}://{uriParts[1].Split('|')[1]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}",
|
||||
};
|
||||
foreach (string uri in uris)
|
||||
Assert.ThrowsException<ArgumentException>(() => SimpleOTP.Helpers.UriParser.ParseUri(new Uri(uri)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SimpleOTP.Models;
|
||||
|
||||
namespace SimpleOTP.Test.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit-tests for OTP code model.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class OTPCodeUnitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Get formatted OTP code.
|
||||
/// </summary>
|
||||
[TestMethod("Code formatting")]
|
||||
public void Test_GetFullCode()
|
||||
{
|
||||
OTPCode code = new ()
|
||||
{
|
||||
Code = 123,
|
||||
Expiring = DateTime.UtcNow.AddSeconds(30),
|
||||
};
|
||||
|
||||
Assert.AreEqual("000123", code.GetCode());
|
||||
Assert.AreEqual("000 123", code.GetCode("000 000"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SimpleOTP.Models;
|
||||
|
||||
namespace SimpleOTP.Test.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit-tests for OTP generator configuration model.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class OTPConfigurationUnitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Get otpauth link from minimal configuration.
|
||||
/// </summary>
|
||||
[TestMethod("Link generator (short)")]
|
||||
public void TestShortLinkGenerator()
|
||||
{
|
||||
OTPConfiguration config = OTPConfiguration.GenerateConfiguration("FoxDev Studio", "eugene@xfox111.net");
|
||||
System.Diagnostics.Debug.WriteLine(config.Id);
|
||||
Uri uri = config.GetUri();
|
||||
Assert.AreEqual($"otpauth://totp/FoxDev+Studio:eugene@xfox111.net?secret={config.Secret}&issuer=FoxDev+Studio", uri.AbsoluteUri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get otpauth link from complete configurations (TOTP + HOTP).
|
||||
/// </summary>
|
||||
[TestMethod("Link generator (full)")]
|
||||
public void TestFullLinkGenerator()
|
||||
{
|
||||
OTPConfiguration config = OTPConfiguration.GetConfiguration("otpauth://totp/FoxDev%20Studio:eugene@xfox111.net?secret=ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH&issuer=FoxDev%20Studio%20Issuer&algorithm=SHA512&digits=8&period=10");
|
||||
Assert.AreEqual($"otpauth://totp/FoxDev+Studio:eugene@xfox111.net?secret=ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH&issuer=FoxDev+Studio+Issuer&algorithm=SHA512&digits=8&period=10", config.GetUri().AbsoluteUri);
|
||||
config.Type = Enums.OTPType.HOTP;
|
||||
Assert.AreEqual($"otpauth://hotp/FoxDev+Studio:eugene@xfox111.net?secret=ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH&issuer=FoxDev+Studio+Issuer&algorithm=SHA512&digits=8&counter=0", config.GetUri().AbsoluteUri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test user-friendly secret formatting.
|
||||
/// </summary>
|
||||
[TestMethod("Fancy secret")]
|
||||
public void GetFormattedSecret()
|
||||
{
|
||||
OTPConfiguration config = OTPConfiguration.GetConfiguration("ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH", "FoxDev Studio", "eugene@xfox111.net");
|
||||
Assert.AreEqual($"ESQV TYRM 2CWZ C3NX 24GR RWIA UUWV HWQH", config.GetFancySecret());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test QR code generation for OTP config.
|
||||
/// </summary>
|
||||
/// <returns><see cref="Task"/>.</returns>
|
||||
[TestMethod("QR code image")]
|
||||
public async Task GetQrImage()
|
||||
{
|
||||
OTPConfiguration config = OTPConfiguration.GetConfiguration("ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH", "FoxDev Studio", "eugene@xfox111.net");
|
||||
string imageStr = await config.GetQrImage();
|
||||
Assert.AreEqual(
|
||||
"data:image/png;base64,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", imageStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SimpleOTP.Models;
|
||||
|
||||
namespace SimpleOTP.Test
|
||||
{
|
||||
/// <summary>
|
||||
/// OTP factory class unit-tests.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class OTPFactoryUnitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Complex test of OTP factory.
|
||||
/// </summary>
|
||||
/// <returns><see cref="Task"/>.</returns>
|
||||
[TestMethod("Overall factory test")]
|
||||
public async Task TestFactory()
|
||||
{
|
||||
OTPConfiguration config = OTPConfiguration.GetConfiguration("ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH", "FoxDev Studio", "eugene@xfox111.net");
|
||||
config.Period = TimeSpan.FromSeconds(3);
|
||||
using OTPFactory factory = new (config, 1500);
|
||||
System.Diagnostics.Debug.WriteLine(factory.Configuration);
|
||||
var code = factory.CurrentCode;
|
||||
|
||||
factory.Configuration = config;
|
||||
factory.CurrentCode = code;
|
||||
|
||||
await Task.Delay(3500);
|
||||
Assert.AreNotEqual(code.Code, factory.CurrentCode.Code);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SimpleOTP.Models;
|
||||
|
||||
namespace SimpleOTP.Test
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit-tests for OTP generator.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class OTPServiceUnitTest
|
||||
{
|
||||
private readonly DateTime time = new (2021, 5, 28, 10, 47, 50, DateTimeKind.Utc);
|
||||
private OTPConfiguration totpConfig = OTPConfiguration.GetConfiguration(new Uri("otpauth://totp/FoxDev%20Studio:eugene@xfox111.net?secret=JBSWY3DPEHPK3PXP&issuer=FoxDev%20Studio"));
|
||||
private OTPConfiguration hotpConfig = OTPConfiguration.GetConfiguration(new Uri("otpauth://hotp/FoxDev%20Studio:eugene@xfox111.net?secret=JBSWY3DPEHPK3PXP&issuer=FoxDev%20Studio&counter=10000"));
|
||||
|
||||
/// <summary>
|
||||
/// Test time-based OTP generator with pre-calculated code.
|
||||
/// </summary>
|
||||
[TestMethod("TOTP code generation")]
|
||||
public void GenerateCode_Totp()
|
||||
{
|
||||
var config = totpConfig with { };
|
||||
var code = OTPService.GenerateCode(ref config, time);
|
||||
Assert.AreEqual(160102, code.Code);
|
||||
|
||||
config.Algorithm = Enums.Algorithm.SHA256;
|
||||
code = OTPService.GenerateCode(ref config, time);
|
||||
Assert.AreEqual(195671, code.Code);
|
||||
|
||||
config.Algorithm = Enums.Algorithm.SHA512;
|
||||
code = OTPService.GenerateCode(ref config, time);
|
||||
Assert.AreEqual(293657, code.Code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test HOTP generator with pre-calculated code.
|
||||
/// </summary>
|
||||
[TestMethod("HOTP code generation")]
|
||||
public void GenerateCode_Hotp()
|
||||
{
|
||||
var code = OTPService.GenerateCode(ref hotpConfig);
|
||||
Assert.AreEqual(457608, code.Code);
|
||||
Assert.AreEqual(10001, hotpConfig.Counter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test time-based OTP validator with series of codes.
|
||||
/// </summary>
|
||||
[TestMethod("TOTP code validation")]
|
||||
public void ValidateCode_Totp()
|
||||
{
|
||||
int[] codes =
|
||||
{
|
||||
OTPService.GenerateCode(ref totpConfig, DateTime.UtcNow.AddSeconds(-45)).Code,
|
||||
OTPService.GenerateCode(ref totpConfig, DateTime.UtcNow.AddSeconds(-15)).Code,
|
||||
OTPService.GenerateCode(ref totpConfig, DateTime.UtcNow.AddSeconds(0)).Code,
|
||||
OTPService.GenerateCode(ref totpConfig, DateTime.UtcNow.AddSeconds(15)).Code,
|
||||
OTPService.GenerateCode(ref totpConfig, DateTime.UtcNow.AddSeconds(45)).Code,
|
||||
};
|
||||
Assert.IsFalse(OTPService.ValidateCode(codes[0], totpConfig));
|
||||
Assert.IsTrue(OTPService.ValidateCode(codes[1], totpConfig));
|
||||
Assert.IsTrue(OTPService.ValidateCode(codes[2], totpConfig));
|
||||
Assert.IsTrue(OTPService.ValidateCode(codes[3], totpConfig));
|
||||
Assert.IsFalse(OTPService.ValidateCode(codes[4], totpConfig));
|
||||
|
||||
Assert.IsTrue(OTPService.ValidateCode(codes[0], totpConfig, TimeSpan.FromSeconds(60)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test HOTP validator with series of codes.
|
||||
/// </summary>
|
||||
[TestMethod("HOTP code validation")]
|
||||
public void ValidateCode_Hotp()
|
||||
{
|
||||
hotpConfig.Counter = 10000;
|
||||
int[] codes =
|
||||
{
|
||||
OTPService.GenerateCode(ref hotpConfig).Code,
|
||||
OTPService.GenerateCode(ref hotpConfig).Code,
|
||||
OTPService.GenerateCode(ref hotpConfig).Code,
|
||||
OTPService.GenerateCode(ref hotpConfig).Code,
|
||||
OTPService.GenerateCode(ref hotpConfig).Code,
|
||||
};
|
||||
|
||||
hotpConfig.Counter = 10002;
|
||||
Assert.IsFalse(OTPService.ValidateCode(codes[0], ref hotpConfig, 1, true));
|
||||
Assert.AreEqual(10002, hotpConfig.Counter);
|
||||
Assert.IsTrue(OTPService.ValidateCode(codes[1], ref hotpConfig, 1, true));
|
||||
Assert.AreEqual(10001, hotpConfig.Counter);
|
||||
hotpConfig.Counter = 10002;
|
||||
Assert.IsTrue(OTPService.ValidateCode(codes[2], ref hotpConfig, 1, true));
|
||||
Assert.AreEqual(10002, hotpConfig.Counter);
|
||||
hotpConfig.Counter = 10002;
|
||||
Assert.IsTrue(OTPService.ValidateCode(codes[3], ref hotpConfig, 1, true));
|
||||
Assert.AreEqual(10003, hotpConfig.Counter);
|
||||
hotpConfig.Counter = 10002;
|
||||
Assert.IsFalse(OTPService.ValidateCode(codes[4], ref hotpConfig, 1, true));
|
||||
Assert.AreEqual(10002, hotpConfig.Counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<AssemblyName>SimpleOTP.Tests</AssemblyName>
|
||||
<PackageId>SimpleOTP.Tests</PackageId>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
|
||||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
|
||||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0-release-20210429-01" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.4-preview-20210513-02" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.4-preview-20210513-02" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.66">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SimpleOTP\SimpleOTP.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
|
||||
"settings":
|
||||
{
|
||||
"documentationRules":
|
||||
{
|
||||
"companyName": "Eugene Fox",
|
||||
"copyrightText": "------------------------------------------------------------\nCopyright ©{year} {companyName}. All rights reserved.\nCode by {author}\n\nLicensed under MIT license (https://opensource.org/licenses/MIT)\n------------------------------------------------------------",
|
||||
"xmlHeader": false,
|
||||
"variables":
|
||||
{
|
||||
"year": "2021",
|
||||
"author": "Eugene Fox (aka XFox)"
|
||||
},
|
||||
"headerDecoration": "------------------------------------------------------------",
|
||||
"fileNamingConvention": "stylecop",
|
||||
"documentationCulture": "en-US"
|
||||
},
|
||||
"indentation":
|
||||
{
|
||||
"useTabs": true
|
||||
},
|
||||
"layoutRules":
|
||||
{
|
||||
"newlineAtEndOfFile": "omit"
|
||||
},
|
||||
"maintainabilityRules":
|
||||
{
|
||||
"topLevelTypes": [ "class", "interface", "struct" ]
|
||||
},
|
||||
"namingRules":
|
||||
{
|
||||
"allowCommonHungarianPrefixes": false,
|
||||
"allowedNamespaceComponents": [],
|
||||
"tupleElementNameCasing": "PascalCase"
|
||||
},
|
||||
"orderingRules":
|
||||
{
|
||||
"blankLinesBetweenUsingGroups": "require",
|
||||
"systemUsingDirectivesFirst": true,
|
||||
"usingDirectivesPlacement": "outsideNamespace"
|
||||
},
|
||||
"readabilityRules":
|
||||
{
|
||||
"allowBuiltInTypeAliases": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user