Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export function get() {
return content.ToArray();
}

private static string ComputeFingerprintSuffix(ResourceAssetCollection resourceCollection)
internal static string ComputeFingerprintSuffix(ResourceAssetCollection resourceCollection)
{
var resources = (IReadOnlyList<ResourceAsset>)resourceCollection;
var incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
Expand All @@ -118,6 +118,35 @@ private static string ComputeFingerprintSuffix(ResourceAssetCollection resourceC
{
var url = resource.Url;
AppendToHash(incrementalHash, buffer, ref rented, url);

// For non-fingerprinted assets (those without a 'label' property), we need to include
// the integrity hash in the fingerprint calculation. This ensures that if the content
// changes between builds, the resource-collection fingerprint also changes.
if (resource.Properties != null)
{
var hasLabel = false;
string? integrity = null;
foreach (var property in resource.Properties)
{
if (property.Name.Equals("label", StringComparison.OrdinalIgnoreCase))
{
hasLabel = true;
// No need to continue if we found a label - we won't use integrity
break;
}
else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase))
{
integrity = property.Value;
// Continue searching in case there's also a label property
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Move this into a helper method that returns a string and then have the check be against the returned string. If there is more data we need to hash we will return a non-null string. Otherwise, we return null and skip.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Extracted GetAdditionalDataForFingerprint helper method that returns string? - returns the integrity value for non-fingerprinted assets or null otherwise. Commit: 9c8b9c1


// If there's no label (non-fingerprinted) but there is an integrity value, include it
if (!hasLabel && integrity != null)
{
AppendToHash(incrementalHash, buffer, ref rented, integrity);
}
}
}
incrementalHash.GetCurrentHash(result);
// Base64 encoding at most increases size by (4 * byteSize / 3 + 2),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components.Endpoints;

public class ResourceCollectionUrlEndpointTest
{
[Fact]
public void ComputeFingerprintSuffix_IncludesIntegrityForNonFingerprintedAssets()
Copy link
Member

Choose a reason for hiding this comment

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

Look at the microsoftdocs and use modern C# (collection initializers, target type, etc.) where possible.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to use collection expressions and target-typed new() throughout the tests for modern C# style. Commit: 9c8b9c1

{
// Arrange - Create a collection with non-fingerprinted assets that have integrity hashes
var resources = new List<ResourceAsset>
{
// Non-fingerprinted asset with integrity (simulates WasmFingerprintAssets=false scenario)
new ResourceAsset("/_framework/MyApp.dll", new[]
{
new ResourceAssetProperty("integrity", "sha256-ABC123")
}),
new ResourceAsset("/_framework/System.dll", new[]
{
new ResourceAssetProperty("integrity", "sha256-XYZ789")
})
};
var collection = new ResourceAssetCollection(resources);

// Act
var fingerprint1 = ResourceCollectionUrlEndpoint.ComputeFingerprintSuffix(collection);

// Arrange - Change the integrity of one asset (simulates content change between builds)
resources = new List<ResourceAsset>
{
new ResourceAsset("/_framework/MyApp.dll", new[]
{
new ResourceAssetProperty("integrity", "sha256-CHANGED")
}),
new ResourceAsset("/_framework/System.dll", new[]
{
new ResourceAssetProperty("integrity", "sha256-XYZ789")
})
};
collection = new ResourceAssetCollection(resources);

// Act
var fingerprint2 = ResourceCollectionUrlEndpoint.ComputeFingerprintSuffix(collection);

// Assert - Fingerprints should be different because integrity changed
Assert.NotEqual(fingerprint1, fingerprint2);
}

[Fact]
public void ComputeFingerprintSuffix_DoesNotIncludeIntegrityForFingerprintedAssets()
{
// Arrange - Create a collection with fingerprinted assets (have label property)
var resources = new List<ResourceAsset>
{
// Fingerprinted asset with label (simulates WasmFingerprintAssets=true scenario)
new ResourceAsset("/_framework/MyApp.ABC123.dll", new[]
{
new ResourceAssetProperty("label", "MyApp.dll"),
new ResourceAssetProperty("integrity", "sha256-ABC123")
}),
new ResourceAsset("/_framework/System.XYZ789.dll", new[]
{
new ResourceAssetProperty("label", "System.dll"),
new ResourceAssetProperty("integrity", "sha256-XYZ789")
})
};
var collection = new ResourceAssetCollection(resources);

// Act
var fingerprint1 = ResourceCollectionUrlEndpoint.ComputeFingerprintSuffix(collection);

// Arrange - Change the integrity (but not the URL) of one asset
// For fingerprinted assets, the URL already contains the hash, so we don't need to include integrity
resources = new List<ResourceAsset>
{
new ResourceAsset("/_framework/MyApp.ABC123.dll", new[]
{
new ResourceAssetProperty("label", "MyApp.dll"),
new ResourceAssetProperty("integrity", "sha256-CHANGED")
}),
new ResourceAsset("/_framework/System.XYZ789.dll", new[]
{
new ResourceAssetProperty("label", "System.dll"),
new ResourceAssetProperty("integrity", "sha256-XYZ789")
})
};
collection = new ResourceAssetCollection(resources);

// Act
var fingerprint2 = ResourceCollectionUrlEndpoint.ComputeFingerprintSuffix(collection);

// Assert - Fingerprints should be the same because for fingerprinted assets,
// the URL (not integrity) is what matters, and the URL didn't change
Assert.Equal(fingerprint1, fingerprint2);
}

[Fact]
public void ComputeFingerprintSuffix_HandlesMixedAssets()
{
// Arrange - Mix of fingerprinted and non-fingerprinted assets
var resources = new List<ResourceAsset>
{
// Fingerprinted asset
new ResourceAsset("/_framework/MyApp.ABC123.dll", new[]
{
new ResourceAssetProperty("label", "MyApp.dll"),
new ResourceAssetProperty("integrity", "sha256-ABC123")
}),
// Non-fingerprinted asset
new ResourceAsset("/_framework/custom.js", new[]
{
new ResourceAssetProperty("integrity", "sha256-CUSTOM")
})
};
var collection = new ResourceAssetCollection(resources);

// Act
var fingerprint1 = ResourceCollectionUrlEndpoint.ComputeFingerprintSuffix(collection);

// Arrange - Change only the non-fingerprinted asset's integrity
resources = new List<ResourceAsset>
{
// Fingerprinted asset (same as before)
new ResourceAsset("/_framework/MyApp.ABC123.dll", new[]
{
new ResourceAssetProperty("label", "MyApp.dll"),
new ResourceAssetProperty("integrity", "sha256-ABC123")
}),
// Non-fingerprinted asset with changed integrity
new ResourceAsset("/_framework/custom.js", new[]
{
new ResourceAssetProperty("integrity", "sha256-MODIFIED")
})
};
collection = new ResourceAssetCollection(resources);

// Act
var fingerprint2 = ResourceCollectionUrlEndpoint.ComputeFingerprintSuffix(collection);

// Assert - Fingerprints should be different because non-fingerprinted asset's integrity changed
Assert.NotEqual(fingerprint1, fingerprint2);
}

[Fact]
public void ComputeFingerprintSuffix_HandlesAssetsWithNoProperties()
{
// Arrange
var resources = new List<ResourceAsset>
{
new ResourceAsset("/_framework/file1.dll", null),
new ResourceAsset("/_framework/file2.dll", new ResourceAssetProperty[] { })
};
var collection = new ResourceAssetCollection(resources);

// Act & Assert - Should not throw
var fingerprint = ResourceCollectionUrlEndpoint.ComputeFingerprintSuffix(collection);
Assert.NotNull(fingerprint);
Assert.StartsWith(".", fingerprint);
}
Copy link
Member

Choose a reason for hiding this comment

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

@copilot Add a unit test that simulates the original scenario, which is the integrity of a non-fingerprinted asset changes.

First we compute the fingerprint suffix, then we change the integrity on a non-fingerprinted asset and verify the suffix is different.

Copy link
Member

Choose a reason for hiding this comment

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

This is the only test we need for this. All other behavior will be well covered by existing tests

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added ComputeFingerprintSuffix_ChangesWhenNonFingerprintedAssetIntegrityChanges test that simulates the original scenario - computes fingerprint with initial integrity, changes the integrity on a non-fingerprinted asset (same URL), and verifies the fingerprint is different. Commit: 0a8a76e

}
Loading