From 9b9ec1bbad412da01476a1de0e945e02068d3350 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:25:35 +0000 Subject: [PATCH 1/5] Initial plan From 7827aafd48422261b0b12d31749fed91ca675705 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:37:43 +0000 Subject: [PATCH 2/5] Fix resource-collection.js fingerprint computation for non-fingerprinted assets - Modified ComputeFingerprintSuffix to include integrity hash for non-fingerprinted assets - Added comprehensive unit tests to verify the fix - Tests verify different scenarios: fingerprinted, non-fingerprinted, mixed, and null properties Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../Builder/ResourceCollectionUrlEndpoint.cs | 27 +++ .../ResourceCollectionUrlEndpointTest.cs | 176 ++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs diff --git a/src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs b/src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs index b09e7d7be431..6251f932ee9e 100644 --- a/src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs +++ b/src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs @@ -118,6 +118,33 @@ 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; + break; + } + else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase)) + { + integrity = property.Value; + } + } + + // 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), diff --git a/src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs b/src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs new file mode 100644 index 000000000000..3fcb36ae4094 --- /dev/null +++ b/src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs @@ -0,0 +1,176 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +public class ResourceCollectionUrlEndpointTest +{ + [Fact] + public void ComputeFingerprintSuffix_IncludesIntegrityForNonFingerprintedAssets() + { + // Arrange - Create a collection with non-fingerprinted assets that have integrity hashes + var resources = new List + { + // 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 = InvokeComputeFingerprintSuffix(collection); + + // Arrange - Change the integrity of one asset (simulates content change between builds) + resources = new List + { + 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 = InvokeComputeFingerprintSuffix(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 + { + // 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 = InvokeComputeFingerprintSuffix(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 + { + 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 = InvokeComputeFingerprintSuffix(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 + { + // 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 = InvokeComputeFingerprintSuffix(collection); + + // Arrange - Change only the non-fingerprinted asset's integrity + resources = new List + { + // 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 = InvokeComputeFingerprintSuffix(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 + { + 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 = InvokeComputeFingerprintSuffix(collection); + Assert.NotNull(fingerprint); + Assert.StartsWith(".", fingerprint); + } + + private static string InvokeComputeFingerprintSuffix(ResourceAssetCollection collection) + { + // Use reflection to invoke the private static method + var method = typeof(ResourceCollectionUrlEndpoint).GetMethod( + "ComputeFingerprintSuffix", + BindingFlags.NonPublic | BindingFlags.Static); + + Assert.NotNull(method); + + var result = method.Invoke(null, new object[] { collection }); + return result as string; + } +} From 00e5cf26a06817d9b287f48d1aa512e15814e1a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:41:48 +0000 Subject: [PATCH 3/5] Address code review feedback: optimize loop and use InternalsVisibleTo - Changed ComputeFingerprintSuffix from private to internal for better testability - Added early exit when label is found to avoid unnecessary iterations - Updated tests to use internal method directly instead of reflection Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../Builder/ResourceCollectionUrlEndpoint.cs | 4 ++- .../ResourceCollectionUrlEndpointTest.cs | 29 +++++-------------- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs b/src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs index 6251f932ee9e..7bb18908c1c3 100644 --- a/src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs +++ b/src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs @@ -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)resourceCollection; var incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); @@ -131,11 +131,13 @@ private static string ComputeFingerprintSuffix(ResourceAssetCollection resourceC 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 } } diff --git a/src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs b/src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs index 3fcb36ae4094..9b1d0b2187cf 100644 --- a/src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs +++ b/src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Reflection; - namespace Microsoft.AspNetCore.Components.Endpoints; public class ResourceCollectionUrlEndpointTest @@ -26,7 +24,7 @@ public void ComputeFingerprintSuffix_IncludesIntegrityForNonFingerprintedAssets( var collection = new ResourceAssetCollection(resources); // Act - var fingerprint1 = InvokeComputeFingerprintSuffix(collection); + var fingerprint1 = ResourceCollectionUrlEndpoint.ComputeFingerprintSuffix(collection); // Arrange - Change the integrity of one asset (simulates content change between builds) resources = new List @@ -43,7 +41,7 @@ public void ComputeFingerprintSuffix_IncludesIntegrityForNonFingerprintedAssets( collection = new ResourceAssetCollection(resources); // Act - var fingerprint2 = InvokeComputeFingerprintSuffix(collection); + var fingerprint2 = ResourceCollectionUrlEndpoint.ComputeFingerprintSuffix(collection); // Assert - Fingerprints should be different because integrity changed Assert.NotEqual(fingerprint1, fingerprint2); @@ -70,7 +68,7 @@ public void ComputeFingerprintSuffix_DoesNotIncludeIntegrityForFingerprintedAsse var collection = new ResourceAssetCollection(resources); // Act - var fingerprint1 = InvokeComputeFingerprintSuffix(collection); + 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 @@ -90,7 +88,7 @@ public void ComputeFingerprintSuffix_DoesNotIncludeIntegrityForFingerprintedAsse collection = new ResourceAssetCollection(resources); // Act - var fingerprint2 = InvokeComputeFingerprintSuffix(collection); + 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 @@ -118,7 +116,7 @@ public void ComputeFingerprintSuffix_HandlesMixedAssets() var collection = new ResourceAssetCollection(resources); // Act - var fingerprint1 = InvokeComputeFingerprintSuffix(collection); + var fingerprint1 = ResourceCollectionUrlEndpoint.ComputeFingerprintSuffix(collection); // Arrange - Change only the non-fingerprinted asset's integrity resources = new List @@ -138,7 +136,7 @@ public void ComputeFingerprintSuffix_HandlesMixedAssets() collection = new ResourceAssetCollection(resources); // Act - var fingerprint2 = InvokeComputeFingerprintSuffix(collection); + var fingerprint2 = ResourceCollectionUrlEndpoint.ComputeFingerprintSuffix(collection); // Assert - Fingerprints should be different because non-fingerprinted asset's integrity changed Assert.NotEqual(fingerprint1, fingerprint2); @@ -156,21 +154,8 @@ public void ComputeFingerprintSuffix_HandlesAssetsWithNoProperties() var collection = new ResourceAssetCollection(resources); // Act & Assert - Should not throw - var fingerprint = InvokeComputeFingerprintSuffix(collection); + var fingerprint = ResourceCollectionUrlEndpoint.ComputeFingerprintSuffix(collection); Assert.NotNull(fingerprint); Assert.StartsWith(".", fingerprint); } - - private static string InvokeComputeFingerprintSuffix(ResourceAssetCollection collection) - { - // Use reflection to invoke the private static method - var method = typeof(ResourceCollectionUrlEndpoint).GetMethod( - "ComputeFingerprintSuffix", - BindingFlags.NonPublic | BindingFlags.Static); - - Assert.NotNull(method); - - var result = method.Invoke(null, new object[] { collection }); - return result as string; - } } From 9c8b9c17f35ca5bcd8fb3854b2c70ee3058d723e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:06:15 +0000 Subject: [PATCH 4/5] Address PR feedback: extract helper method and use modern C# syntax - Extracted GetAdditionalDataForFingerprint helper method that returns string? for cleaner separation - Updated tests to use collection expressions and target-typed new() for modern C# style - All tests passing Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../Builder/ResourceCollectionUrlEndpoint.cs | 60 ++++--- .../ResourceCollectionUrlEndpointTest.cs | 154 +++++++++--------- 2 files changed, 111 insertions(+), 103 deletions(-) diff --git a/src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs b/src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs index 7bb18908c1c3..e29c685c0bc3 100644 --- a/src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs +++ b/src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs @@ -119,33 +119,10 @@ internal static string ComputeFingerprintSuffix(ResourceAssetCollection resource 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 additionalData = GetAdditionalDataForFingerprint(resource); + if (additionalData != 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 - } - } - - // 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); - } + AppendToHash(incrementalHash, buffer, ref rented, additionalData); } } incrementalHash.GetCurrentHash(result); @@ -157,6 +134,37 @@ internal static string ComputeFingerprintSuffix(ResourceAssetCollection resource return fingerprintSpan[..(length + 1)].ToString(); } + private static string? GetAdditionalDataForFingerprint(ResourceAsset resource) + { + // 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) + { + return 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 + } + } + + // If there's no label (non-fingerprinted) but there is an integrity value, return it + return !hasLabel ? integrity : null; + } + private static void AppendToHash(IncrementalHash incrementalHash, Span buffer, ref byte[]? rented, string value) { if (Encoding.UTF8.TryGetBytes(value, buffer, out var written)) diff --git a/src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs b/src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs index 9b1d0b2187cf..952aca1fcef1 100644 --- a/src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs +++ b/src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs @@ -9,35 +9,35 @@ public class ResourceCollectionUrlEndpointTest public void ComputeFingerprintSuffix_IncludesIntegrityForNonFingerprintedAssets() { // Arrange - Create a collection with non-fingerprinted assets that have integrity hashes - var resources = new List - { + ResourceAsset[] resources = + [ // 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") - }) - }; + new("/_framework/MyApp.dll", + [ + new("integrity", "sha256-ABC123") + ]), + new("/_framework/System.dll", + [ + new("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 - { - new ResourceAsset("/_framework/MyApp.dll", new[] - { - new ResourceAssetProperty("integrity", "sha256-CHANGED") - }), - new ResourceAsset("/_framework/System.dll", new[] - { - new ResourceAssetProperty("integrity", "sha256-XYZ789") - }) - }; + resources = + [ + new("/_framework/MyApp.dll", + [ + new("integrity", "sha256-CHANGED") + ]), + new("/_framework/System.dll", + [ + new("integrity", "sha256-XYZ789") + ]) + ]; collection = new ResourceAssetCollection(resources); // Act @@ -51,20 +51,20 @@ public void ComputeFingerprintSuffix_IncludesIntegrityForNonFingerprintedAssets( public void ComputeFingerprintSuffix_DoesNotIncludeIntegrityForFingerprintedAssets() { // Arrange - Create a collection with fingerprinted assets (have label property) - var resources = new List - { + ResourceAsset[] resources = + [ // 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") - }) - }; + new("/_framework/MyApp.ABC123.dll", + [ + new("label", "MyApp.dll"), + new("integrity", "sha256-ABC123") + ]), + new("/_framework/System.XYZ789.dll", + [ + new("label", "System.dll"), + new("integrity", "sha256-XYZ789") + ]) + ]; var collection = new ResourceAssetCollection(resources); // Act @@ -72,19 +72,19 @@ public void ComputeFingerprintSuffix_DoesNotIncludeIntegrityForFingerprintedAsse // 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 - { - 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") - }) - }; + resources = + [ + new("/_framework/MyApp.ABC123.dll", + [ + new("label", "MyApp.dll"), + new("integrity", "sha256-CHANGED") + ]), + new("/_framework/System.XYZ789.dll", + [ + new("label", "System.dll"), + new("integrity", "sha256-XYZ789") + ]) + ]; collection = new ResourceAssetCollection(resources); // Act @@ -99,40 +99,40 @@ public void ComputeFingerprintSuffix_DoesNotIncludeIntegrityForFingerprintedAsse public void ComputeFingerprintSuffix_HandlesMixedAssets() { // Arrange - Mix of fingerprinted and non-fingerprinted assets - var resources = new List - { + ResourceAsset[] resources = + [ // Fingerprinted asset - new ResourceAsset("/_framework/MyApp.ABC123.dll", new[] - { - new ResourceAssetProperty("label", "MyApp.dll"), - new ResourceAssetProperty("integrity", "sha256-ABC123") - }), + new("/_framework/MyApp.ABC123.dll", + [ + new("label", "MyApp.dll"), + new("integrity", "sha256-ABC123") + ]), // Non-fingerprinted asset - new ResourceAsset("/_framework/custom.js", new[] - { - new ResourceAssetProperty("integrity", "sha256-CUSTOM") - }) - }; + new("/_framework/custom.js", + [ + new("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 - { + resources = + [ // Fingerprinted asset (same as before) - new ResourceAsset("/_framework/MyApp.ABC123.dll", new[] - { - new ResourceAssetProperty("label", "MyApp.dll"), - new ResourceAssetProperty("integrity", "sha256-ABC123") - }), + new("/_framework/MyApp.ABC123.dll", + [ + new("label", "MyApp.dll"), + new("integrity", "sha256-ABC123") + ]), // Non-fingerprinted asset with changed integrity - new ResourceAsset("/_framework/custom.js", new[] - { - new ResourceAssetProperty("integrity", "sha256-MODIFIED") - }) - }; + new("/_framework/custom.js", + [ + new("integrity", "sha256-MODIFIED") + ]) + ]; collection = new ResourceAssetCollection(resources); // Act @@ -146,11 +146,11 @@ public void ComputeFingerprintSuffix_HandlesMixedAssets() public void ComputeFingerprintSuffix_HandlesAssetsWithNoProperties() { // Arrange - var resources = new List - { - new ResourceAsset("/_framework/file1.dll", null), - new ResourceAsset("/_framework/file2.dll", new ResourceAssetProperty[] { }) - }; + ResourceAsset[] resources = + [ + new("/_framework/file1.dll", null), + new("/_framework/file2.dll", []) + ]; var collection = new ResourceAssetCollection(resources); // Act & Assert - Should not throw From 0a8a76e1b38b1e91f751088fd084505cb6283ef6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:18:05 +0000 Subject: [PATCH 5/5] Add test for original bug scenario: non-fingerprinted asset integrity change Added ComputeFingerprintSuffix_ChangesWhenNonFingerprintedAssetIntegrityChanges test that directly simulates the original issue scenario where a file change (e.g., Counter.razor or styles.css) updates integrity but not URL with WasmFingerprintAssets=false Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../ResourceCollectionUrlEndpointTest.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs b/src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs index 952aca1fcef1..ae8266dcc61e 100644 --- a/src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs +++ b/src/Components/Endpoints/test/Builder/ResourceCollectionUrlEndpointTest.cs @@ -158,4 +158,40 @@ public void ComputeFingerprintSuffix_HandlesAssetsWithNoProperties() Assert.NotNull(fingerprint); Assert.StartsWith(".", fingerprint); } + + [Fact] + public void ComputeFingerprintSuffix_ChangesWhenNonFingerprintedAssetIntegrityChanges() + { + // This test simulates the original bug scenario: + // When WasmFingerprintAssets=false, changing a file (e.g., Counter.razor or wwwroot/styles.css) + // updates its integrity hash but not its URL. The resource-collection fingerprint must change + // to prevent serving stale cached resource-collection.js with outdated integrity values. + + // Arrange - Initial state: non-fingerprinted asset with original integrity + var collection1 = new ResourceAssetCollection( + [ + new("/_framework/app.styles.css", + [ + new("integrity", "sha256-OriginalHash123456") + ]) + ]); + + // Act - Compute initial fingerprint + var fingerprintSuffix1 = ResourceCollectionUrlEndpoint.ComputeFingerprintSuffix(collection1); + + // Arrange - Simulate content change: same URL, different integrity (e.g., after modifying styles.css) + var collection2 = new ResourceAssetCollection( + [ + new("/_framework/app.styles.css", + [ + new("integrity", "sha256-ModifiedHash789012") + ]) + ]); + + // Act - Compute fingerprint after content change + var fingerprintSuffix2 = ResourceCollectionUrlEndpoint.ComputeFingerprintSuffix(collection2); + + // Assert - Fingerprint must be different to ensure browser fetches updated resource-collection.js + Assert.NotEqual(fingerprintSuffix1, fingerprintSuffix2); + } }