Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/AutoMapper/AutoMapper.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@
<Exec Condition=" '$(OS)' == 'Windows_NT' " Command="powershell -ExecutionPolicy Unrestricted -File &quot;$(ProjectDir)ApiCompat\PreBuild.ps1&quot; -version $(Version)" />
<Exec Condition=" '$(OS)' != 'Windows_NT' " Command="bash &quot;$(ProjectDir)ApiCompat/PreBuild.sh&quot; $(Version)" />
</Target> -->

<Target Name="EmbedBuildDate" BeforeTargets="CoreCompile">
<Exec Command="git log -1 --format=%25cI" ConsoleToMSBuild="true" IgnoreExitCode="true" StandardOutputImportance="Low">
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitDate" />
</Exec>
<PropertyGroup>
<BuildDateUtc Condition="'$(GitCommitDate)' != ''">$(GitCommitDate)</BuildDateUtc>
</PropertyGroup>
<WriteLinesToFile Condition="'$(BuildDateUtc)' != ''" File="$(IntermediateOutputPath)BuildDateGenerated.cs" Lines="[assembly: System.Reflection.AssemblyMetadata(&quot;BuildDateUtc&quot;, &quot;$(BuildDateUtc)&quot;)]" Overwrite="true" />
<ItemGroup>
<Compile Include="$(IntermediateOutputPath)BuildDateGenerated.cs" Condition="'$(BuildDateUtc)' != ''" />
</ItemGroup>
</Target>
Comment on lines 64 to 75
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The EmbedBuildDate target uses DateTime.UtcNow which makes every build produce a different assembly, conflicting with the Deterministic build setting on line 26. This will break deterministic builds and may cause issues with build caching, reproducible builds, and package signing.

Consider using a source control-based approach (like git commit timestamp) or an environment variable that can be set to a fixed value in CI/CD pipelines while allowing dynamic values for local development builds. For example, you could check for an environment variable first: BuildDateUtc>$([System.Environment]::GetEnvironmentVariable('BUILD_DATE_UTC', 'EnvironmentVariableTarget.Process'))</BuildDateUtc> and only fall back to UtcNow if not set, or use SOURCE_DATE_EPOCH for reproducible builds.

Copilot uses AI. Check for mistakes.

<ItemGroup>
<Using Include="System.Linq.Expressions.Expression" Static="true"/>
<Using Include="AutoMapper.Execution.ExpressionBuilder" Static="true"/>
Expand Down
26 changes: 26 additions & 0 deletions src/AutoMapper/Licensing/BuildInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Reflection;

namespace AutoMapper.Licensing;

internal static class BuildInfo
{
public static DateTimeOffset? BuildDate { get; } = GetBuildDate();

private static DateTimeOffset? GetBuildDate()
{
var assembly = typeof(BuildInfo).Assembly;

var buildDateAttribute = assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.FirstOrDefault(a => a.Key == "BuildDateUtc");

if (buildDateAttribute?.Value != null &&
DateTimeOffset.TryParse(buildDateAttribute.Value, out var buildDate))
{
return buildDate;
}

return null;
}
}

4 changes: 4 additions & 0 deletions src/AutoMapper/Licensing/License.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public License(ClaimsPrincipal claims)
ProductType = productType;
}

var perpetualValue = claims.FindFirst("perpetual")?.Value;
IsPerpetual = perpetualValue?.ToLowerInvariant() is "true" or "1";

IsConfigured = AccountId != null
&& CustomerId != null
&& SubscriptionId != null
Expand All @@ -57,6 +60,7 @@ public License(ClaimsPrincipal claims)
public DateTimeOffset? ExpirationDate { get; }
public Edition? Edition { get; }
public ProductType? ProductType { get; }
public bool IsPerpetual { get; }

public bool IsConfigured { get; }
}
33 changes: 32 additions & 1 deletion src/AutoMapper/Licensing/LicenseValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ namespace AutoMapper.Licensing;
internal class LicenseValidator
{
private readonly ILogger _logger;
private readonly DateTimeOffset? _buildDate;

public LicenseValidator(ILoggerFactory loggerFactory)
: this(loggerFactory, BuildInfo.BuildDate)
{
}

public LicenseValidator(ILoggerFactory loggerFactory, DateTimeOffset? buildDate)
{
_logger = loggerFactory.CreateLogger("LuckyPennySoftware.AutoMapper.License");
_buildDate = buildDate;
}

public void Validate(License license)
Expand All @@ -31,7 +38,31 @@ public void Validate(License license)
var diff = DateTime.UtcNow.Date.Subtract(license.ExpirationDate.Value.Date).TotalDays;
if (diff > 0)
{
errors.Add($"Your license for the Lucky Penny software AutoMapper expired {diff} days ago.");
// If perpetual, check if build date is before expiration
if (license.IsPerpetual && _buildDate.HasValue)
{
var buildDateDiff = _buildDate.Value.Date.Subtract(license.ExpirationDate.Value.Date).TotalDays;
if (buildDateDiff <= 0)
{
_logger.LogInformation(
"Your license for the Lucky Penny software AutoMapper expired {expiredDaysAgo} days ago, but perpetual licensing is active because the build date ({buildDate:O}) is before the license expiration date ({licenseExpiration:O}).",
diff, _buildDate, license.ExpirationDate);
// Don't add to errors - perpetual fallback applies
}
else
{
errors.Add($"Your license for the Lucky Penny software AutoMapper expired {diff} days ago.");
}
}
else
{
if (license.IsPerpetual)
{
_logger.LogWarning(
"Your license for the Lucky Penny software AutoMapper has the perpetual flag set, but the build date could not be determined. Perpetual licensing is unavailable.");
}
errors.Add($"Your license for the Lucky Penny software AutoMapper expired {diff} days ago.");
}
}

if (license.ProductType.Value != ProductType.AutoMapper
Expand Down
115 changes: 115 additions & 0 deletions src/UnitTests/Licensing/LicenseValidatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,119 @@ public void Should_return_invalid_when_expired()
logMessages
.ShouldContain(log => log.Level == LogLevel.Error);
}

[Fact]
public void Should_allow_perpetual_license_when_build_date_before_expiration()
{
var factory = new LoggerFactory();
var provider = new FakeLoggerProvider();
factory.AddProvider(provider);

var buildDate = DateTimeOffset.UtcNow.AddDays(-30);
var licenseValidator = new LicenseValidator(factory, buildDate);
var license = new License(
new Claim("account_id", Guid.NewGuid().ToString()),
new Claim("customer_id", Guid.NewGuid().ToString()),
new Claim("sub_id", Guid.NewGuid().ToString()),
new Claim("iat", DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds().ToString()),
new Claim("exp", DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds().ToString()),
new Claim("edition", nameof(Edition.Professional)),
new Claim("type", nameof(AutoMapper.Licensing.ProductType.AutoMapper)),
new Claim("perpetual", "true"));

license.IsConfigured.ShouldBeTrue();
license.IsPerpetual.ShouldBeTrue();

licenseValidator.Validate(license);

var logMessages = provider.Collector.GetSnapshot();
logMessages.ShouldNotContain(log => log.Level == LogLevel.Error);
logMessages.ShouldContain(log => log.Level == LogLevel.Information &&
log.Message.Contains("perpetual"));
}

[Fact]
public void Should_reject_perpetual_license_when_build_date_after_expiration()
{
var factory = new LoggerFactory();
var provider = new FakeLoggerProvider();
factory.AddProvider(provider);

var buildDate = DateTimeOffset.UtcNow.AddDays(-1); // Build date in past, after expiration
var licenseValidator = new LicenseValidator(factory, buildDate);
var license = new License(
new Claim("account_id", Guid.NewGuid().ToString()),
new Claim("customer_id", Guid.NewGuid().ToString()),
new Claim("sub_id", Guid.NewGuid().ToString()),
new Claim("iat", DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds().ToString()),
new Claim("exp", DateTimeOffset.UtcNow.AddDays(-30).ToUnixTimeSeconds().ToString()),
new Claim("edition", nameof(Edition.Professional)),
new Claim("type", nameof(AutoMapper.Licensing.ProductType.AutoMapper)),
new Claim("perpetual", "true"));

license.IsConfigured.ShouldBeTrue();
license.IsPerpetual.ShouldBeTrue();

licenseValidator.Validate(license);

var logMessages = provider.Collector.GetSnapshot();
logMessages.ShouldContain(log => log.Level == LogLevel.Error &&
log.Message.Contains("expired"));
}

[Fact]
public void Should_warn_and_error_when_perpetual_but_build_date_is_null()
{
var factory = new LoggerFactory();
var provider = new FakeLoggerProvider();
factory.AddProvider(provider);

var licenseValidator = new LicenseValidator(factory, buildDate: null);
var license = new License(
new Claim("account_id", Guid.NewGuid().ToString()),
new Claim("customer_id", Guid.NewGuid().ToString()),
new Claim("sub_id", Guid.NewGuid().ToString()),
new Claim("iat", DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds().ToString()),
new Claim("exp", DateTimeOffset.UtcNow.AddDays(-10).ToUnixTimeSeconds().ToString()),
new Claim("edition", nameof(Edition.Professional)),
new Claim("type", nameof(AutoMapper.Licensing.ProductType.AutoMapper)),
new Claim("perpetual", "true"));

license.IsConfigured.ShouldBeTrue();
license.IsPerpetual.ShouldBeTrue();

licenseValidator.Validate(license);

var logMessages = provider.Collector.GetSnapshot();
logMessages.ShouldContain(log => log.Level == LogLevel.Warning && log.Message.Contains("perpetual"));
logMessages.ShouldContain(log => log.Level == LogLevel.Error && log.Message.Contains("expired"));
}

[Fact]
public void Should_handle_missing_perpetual_claim()
{
var factory = new LoggerFactory();
var provider = new FakeLoggerProvider();
factory.AddProvider(provider);

var licenseValidator = new LicenseValidator(factory);
var license = new License(
new Claim("account_id", Guid.NewGuid().ToString()),
new Claim("customer_id", Guid.NewGuid().ToString()),
new Claim("sub_id", Guid.NewGuid().ToString()),
new Claim("iat", DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds().ToString()),
new Claim("exp", DateTimeOffset.UtcNow.AddDays(1).ToUnixTimeSeconds().ToString()),
new Claim("edition", nameof(Edition.Community)),
new Claim("type", nameof(AutoMapper.Licensing.ProductType.Bundle)));

license.IsConfigured.ShouldBeTrue();
license.IsPerpetual.ShouldBeFalse();

licenseValidator.Validate(license);

var logMessages = provider.Collector.GetSnapshot();
logMessages.ShouldNotContain(log => log.Level == LogLevel.Error
|| log.Level == LogLevel.Warning
|| log.Level == LogLevel.Critical);
}
Comment on lines +201 to +227
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests don't cover the edge case where a perpetual license is used but the build date is unavailable (null). According to the validation logic on line 37 of LicenseValidator.cs, if _buildDate is null, the perpetual license feature won't activate and the expired license will be rejected.

Add a test case that validates an expired perpetual license when no build date is provided (call new LicenseValidator(factory) without passing a buildDate, and ensure BuildInfo.BuildDate returns null in the test context). This should verify that the system gracefully handles this scenario and logs an appropriate error.

Copilot uses AI. Check for mistakes.
}