diff --git a/src/Standard.Licensing.Tests/LicenseSignatureTests.cs b/src/Standard.Licensing.Tests/LicenseSignatureTests.cs index c3537be..cbd1bb1 100644 --- a/src/Standard.Licensing.Tests/LicenseSignatureTests.cs +++ b/src/Standard.Licensing.Tests/LicenseSignatureTests.cs @@ -52,7 +52,7 @@ private static DateTime ConvertToRfc1123(DateTime dateTime) { return DateTime.ParseExact( dateTime.ToUniversalTime().ToString("r", CultureInfo.InvariantCulture) - , "r", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); + , "r", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); } [Test] @@ -80,13 +80,23 @@ public void Can_Generate_And_Validate_Signature_With_Empty_License() Assert.That(license.VerifySignature(publicKey), Is.True); } - [Test] - public void Can_Generate_And_Validate_Signature_With_Standard_License() + public static IEnumerable LocalAndUtc + { + get + { + yield return new TestCaseData(DateTime.Now.AddMinutes(1)); + yield return new TestCaseData(DateTime.Now.AddYears(1)); + yield return new TestCaseData(DateTime.UtcNow.AddMinutes(1)); + yield return new TestCaseData(DateTime.UtcNow.AddYears(1)); + } + } + + [Test, TestCaseSource(nameof(LocalAndUtc))] + public void Can_Generate_And_Validate_Signature_With_Standard_License(DateTime expirationDate) { var licenseId = Guid.NewGuid(); var customerName = "Max Mustermann"; var customerEmail = "max@mustermann.tld"; - var expirationDate = DateTime.Now.AddYears(1); var productFeatures = new Dictionary { {"Sales Module", "yes"}, diff --git a/src/Standard.Licensing.Tests/LicenseValidationTests.cs b/src/Standard.Licensing.Tests/LicenseValidationTests.cs index 9a0b601..deb7fad 100644 --- a/src/Standard.Licensing.Tests/LicenseValidationTests.cs +++ b/src/Standard.Licensing.Tests/LicenseValidationTests.cs @@ -101,7 +101,6 @@ public void Can_Validate_Invalid_Signature() [Test] public void Can_Validate_Expired_ExpirationDate() { - var publicKey = ""; var licenseData = @" 77d4c193-6088-4c64-9663-ed7398ae8c1a Trial @@ -118,6 +117,8 @@ public void Can_Validate_Expired_ExpirationDate() var license = License.Load(licenseData); + Assert.That(license.Expiration.Kind, Is.EqualTo(DateTimeKind.Utc)); + var validationResults = license .Validate() .ExpirationDate() @@ -126,13 +127,52 @@ public void Can_Validate_Expired_ExpirationDate() Assert.That(validationResults, Is.Not.Null); Assert.That(validationResults.Count(), Is.EqualTo(1)); Assert.That(validationResults.FirstOrDefault(), Is.TypeOf()); - } [Test] - public void Can_Validate_Expired_ExpirationDate_CustomDateTime() + public void Can_Validate_NotExpired_ExpirationDate() + { + var licenseData = @" + 77d4c193-6088-4c64-9663-ed7398ae8c1a + Trial + Fri, 31 Dec 2100 23:00:00 GMT + 1 + + John Doe + john@doe.tld + + + + MEUCIQCCEDAldOZHHIKvYZRDdzUP4V51y23d6deeK5jIFy27GQIgDz2CndjBh4Vb8tiC3FGQ6fn3GKt8d/P5+luJH0cWv+I= + "; + + var license = License.Load(licenseData); + + Assert.That(license.Expiration.Kind, Is.EqualTo(DateTimeKind.Utc)); + + var validationResults = license + .Validate() + .ExpirationDate() + .AssertValidLicense().ToList(); + + Assert.That(validationResults, Is.Not.Null); + Assert.That(validationResults.Count(), Is.EqualTo(0)); + } + + public static IEnumerable LocalAndUtcExpired + { + get + { + yield return new TestCaseData(new DateTime(1899, 12, 31, 23, 01, 0, DateTimeKind.Utc)); + yield return new TestCaseData(new DateTime(1899, 12, 31, 23, 01, 0, DateTimeKind.Utc).ToLocalTime()); + yield return new TestCaseData(new DateTime(1899, 12, 31, 23, 30, 0, DateTimeKind.Utc)); + yield return new TestCaseData(new DateTime(1899, 12, 31, 23, 30, 0, DateTimeKind.Utc).ToLocalTime()); + } + } + + [Test, TestCaseSource(nameof(LocalAndUtcExpired))] + public void Can_Validate_Expired_ExpirationDate_CustomDateTime(DateTime expirationDate) { - var publicKey = ""; var licenseData = @" 77d4c193-6088-4c64-9663-ed7398ae8c1a Trial @@ -149,15 +189,57 @@ public void Can_Validate_Expired_ExpirationDate_CustomDateTime() var license = License.Load(licenseData); + Assert.That(license.Expiration.Kind, Is.EqualTo(DateTimeKind.Utc)); + var validationResults = license .Validate() - .ExpirationDate(systemDateTime: new DateTime(1900, 1, 2, 0, 0, 0, DateTimeKind.Utc)) + .ExpirationDate(systemDateTime: expirationDate) .AssertValidLicense().ToList(); Assert.That(validationResults, Is.Not.Null); Assert.That(validationResults.Count(), Is.EqualTo(1)); Assert.That(validationResults.FirstOrDefault(), Is.TypeOf()); + } + + public static IEnumerable LocalAndUtcNotExpired + { + get + { + yield return new TestCaseData(new DateTime(1899, 12, 31, 22, 59, 0, DateTimeKind.Utc)); + yield return new TestCaseData(new DateTime(1899, 12, 31, 22, 59, 0, DateTimeKind.Utc).ToLocalTime()); + yield return new TestCaseData(new DateTime(1899, 12, 31, 22, 30, 0, DateTimeKind.Utc)); + yield return new TestCaseData(new DateTime(1899, 12, 31, 22, 30, 0, DateTimeKind.Utc).ToLocalTime()); + } + } + + [Test, TestCaseSource(nameof(LocalAndUtcNotExpired))] + public void Can_Validate_NotExpired_ExpirationDate_CustomDateTime(DateTime expirationDate) + { + var licenseData = @" + 77d4c193-6088-4c64-9663-ed7398ae8c1a + Trial + Sun, 31 Dec 1899 23:00:00 GMT + 1 + + John Doe + john@doe.tld + + + + MEUCIQCCEDAldOZHHIKvYZRDdzUP4V51y23d6deeK5jIFy27GQIgDz2CndjBh4Vb8tiC3FGQ6fn3GKt8d/P5+luJH0cWv+I= + "; + + var license = License.Load(licenseData); + Assert.That(license.Expiration.Kind, Is.EqualTo(DateTimeKind.Utc)); + + var validationResults = license + .Validate() + .ExpirationDate(systemDateTime: expirationDate) + .AssertValidLicense().ToList(); + + Assert.That(validationResults, Is.Not.Null); + Assert.That(validationResults.Count(), Is.EqualTo(0)); } [Test] diff --git a/src/Standard.Licensing/License.cs b/src/Standard.Licensing/License.cs index 397e10e..ba450e2 100644 --- a/src/Standard.Licensing/License.cs +++ b/src/Standard.Licensing/License.cs @@ -165,10 +165,16 @@ public LicenseAttributes AdditionalAttributes } /// - /// Gets or sets the expiration date of this . + /// Gets or sets the expiration date in UTC of this . /// Use this property to set the expiration date for a trial license /// or the expiration of support & subscription updates for a standard license. /// + /// + /// The parameter can be in UTC because + /// does not modify a UTC value. + /// + /// The expiration date of the in local time or UTC. + /// The expiration date of the in UTC. public DateTime Expiration { get @@ -176,10 +182,21 @@ public DateTime Expiration return DateTime.ParseExact( GetTag("Expiration") ?? - DateTime.MaxValue.ToUniversalTime().ToString("r", CultureInfo.InvariantCulture) - , "r", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); + DateTime.MaxValue.ToString("r", CultureInfo.InvariantCulture) + , "r", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); + } + set + { + if (value.Kind == DateTimeKind.Unspecified) + { + throw new ArgumentException($"The {nameof(value)} must be in local time or UTC.", nameof(value)); + } + + if (!IsSigned) + { + SetTag("Expiration", value.ToUniversalTime().ToString("r", CultureInfo.InvariantCulture)); + } } - set { if (!IsSigned) SetTag("Expiration", value.ToUniversalTime().ToString("r", CultureInfo.InvariantCulture)); } } /// diff --git a/src/Standard.Licensing/LicenseBuilder.cs b/src/Standard.Licensing/LicenseBuilder.cs index 0a2c2d8..c4f3330 100644 --- a/src/Standard.Licensing/LicenseBuilder.cs +++ b/src/Standard.Licensing/LicenseBuilder.cs @@ -69,10 +69,19 @@ public ILicenseBuilder As(LicenseType type) /// /// Sets the expiration date of the . /// - /// The expiration date of the . + /// + /// The parameter can be in UTC because ToUniversalTime() + /// does not modify a UTC value. + /// + /// The expiration date of the in local time or UTC. /// The . public ILicenseBuilder ExpiresAt(DateTime date) { + if (date.Kind == DateTimeKind.Unspecified) + { + throw new ArgumentException($"The {nameof(date)} must be in local time or UTC.", nameof(date)); + } + license.Expiration = date.ToUniversalTime(); return this; } diff --git a/src/Standard.Licensing/Validation/LicenseValidationExtensions.cs b/src/Standard.Licensing/Validation/LicenseValidationExtensions.cs index f65cdbe..2a5b03a 100644 --- a/src/Standard.Licensing/Validation/LicenseValidationExtensions.cs +++ b/src/Standard.Licensing/Validation/LicenseValidationExtensions.cs @@ -59,14 +59,18 @@ public static IValidationChain ExpirationDate(this IStartValidationChain validat /// /// Validates if the license has been expired. /// + /// + /// The parameter can be in UTC because + /// does not modify a UTC value. + /// /// The current . - /// The System DateTime to compare to, default is DateTime.Now. Can be changed to NTP / other internet API times. + /// The system DateTime to compare to in local time or UTC, default is DateTime.Now. Can be changed to NTP / other internet API times. /// An instance of . public static IValidationChain ExpirationDate(this IStartValidationChain validationChain, DateTime systemDateTime) { var validationChainBuilder = (validationChain as ValidationChainBuilder); var validator = validationChainBuilder.StartValidatorChain(); - validator.Validate = license => license.Expiration > systemDateTime; + validator.Validate = license => license.Expiration > systemDateTime.ToUniversalTime(); validator.FailureResult = new LicenseExpiredValidationFailure() {