File: CacheControlHeaderValueTest.cs
Web Access
Project: src\src\Http\Headers\test\Microsoft.Net.Http.Headers.Tests.csproj (Microsoft.Net.Http.Headers.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace Microsoft.Net.Http.Headers;
 
public class CacheControlHeaderValueTest
{
    [Fact]
    public void Properties_SetAndGetAllProperties_SetValueReturnedInGetter()
    {
        var cacheControl = new CacheControlHeaderValue();
 
        // Bool properties
        cacheControl.NoCache = true;
        Assert.True(cacheControl.NoCache, "NoCache");
        cacheControl.NoStore = true;
        Assert.True(cacheControl.NoStore, "NoStore");
        cacheControl.MaxStale = true;
        Assert.True(cacheControl.MaxStale, "MaxStale");
        cacheControl.NoTransform = true;
        Assert.True(cacheControl.NoTransform, "NoTransform");
        cacheControl.OnlyIfCached = true;
        Assert.True(cacheControl.OnlyIfCached, "OnlyIfCached");
        cacheControl.Public = true;
        Assert.True(cacheControl.Public, "Public");
        cacheControl.Private = true;
        Assert.True(cacheControl.Private, "Private");
        cacheControl.MustRevalidate = true;
        Assert.True(cacheControl.MustRevalidate, "MustRevalidate");
        cacheControl.ProxyRevalidate = true;
        Assert.True(cacheControl.ProxyRevalidate, "ProxyRevalidate");
 
        // TimeSpan properties
        TimeSpan timeSpan = new TimeSpan(1, 2, 3);
        cacheControl.MaxAge = timeSpan;
        Assert.Equal(timeSpan, cacheControl.MaxAge);
        cacheControl.SharedMaxAge = timeSpan;
        Assert.Equal(timeSpan, cacheControl.SharedMaxAge);
        cacheControl.MaxStaleLimit = timeSpan;
        Assert.Equal(timeSpan, cacheControl.MaxStaleLimit);
        cacheControl.MinFresh = timeSpan;
        Assert.Equal(timeSpan, cacheControl.MinFresh);
 
        // String collection properties
        Assert.NotNull(cacheControl.NoCacheHeaders);
        Assert.Throws<ArgumentException>(() => cacheControl.NoCacheHeaders.Add(null));
        Assert.Throws<FormatException>(() => cacheControl.NoCacheHeaders.Add("invalid PLACEHOLDER"));
        cacheControl.NoCacheHeaders.Add("PLACEHOLDER");
        Assert.Single(cacheControl.NoCacheHeaders);
        Assert.Equal("PLACEHOLDER", cacheControl.NoCacheHeaders.First().AsSpan());
 
        Assert.NotNull(cacheControl.PrivateHeaders);
        Assert.Throws<ArgumentException>(() => cacheControl.PrivateHeaders.Add(null));
        Assert.Throws<FormatException>(() => cacheControl.PrivateHeaders.Add("invalid PLACEHOLDER"));
        cacheControl.PrivateHeaders.Add("PLACEHOLDER");
        Assert.Single(cacheControl.PrivateHeaders);
        Assert.Equal("PLACEHOLDER", cacheControl.PrivateHeaders.First().AsSpan());
 
        // NameValueHeaderValue collection property
        Assert.NotNull(cacheControl.Extensions);
        Assert.Throws<ArgumentNullException>(() => cacheControl.Extensions.Add(null!));
        cacheControl.Extensions.Add(new NameValueHeaderValue("name", "value"));
        Assert.Single(cacheControl.Extensions);
        Assert.Equal(new NameValueHeaderValue("name", "value"), cacheControl.Extensions.First());
    }
 
    [Fact]
    public void ToString_UseRequestDirectiveValues_AllSerializedCorrectly()
    {
        var cacheControl = new CacheControlHeaderValue();
        Assert.Equal("", cacheControl.ToString());
 
        // Note that we allow all combinations of all properties even though the RFC specifies rules what value
        // can be used together.
        // Also for property pairs (bool property + collection property) like 'NoCache' and 'NoCacheHeaders' the
        // caller needs to set the bool property in order for the collection to be populated as string.
 
        // Cache Request Directive sample
        cacheControl.NoStore = true;
        Assert.Equal("no-store", cacheControl.ToString());
        cacheControl.NoCache = true;
        Assert.Equal("no-store, no-cache", cacheControl.ToString());
        cacheControl.MaxAge = new TimeSpan(0, 1, 10);
        Assert.Equal("no-store, no-cache, max-age=70", cacheControl.ToString());
        cacheControl.MaxStale = true;
        Assert.Equal("no-store, no-cache, max-age=70, max-stale", cacheControl.ToString());
        cacheControl.MaxStaleLimit = new TimeSpan(0, 2, 5);
        Assert.Equal("no-store, no-cache, max-age=70, max-stale=125", cacheControl.ToString());
        cacheControl.MinFresh = new TimeSpan(0, 3, 0);
        Assert.Equal("no-store, no-cache, max-age=70, max-stale=125, min-fresh=180", cacheControl.ToString());
 
        cacheControl = new CacheControlHeaderValue();
        cacheControl.NoTransform = true;
        Assert.Equal("no-transform", cacheControl.ToString());
        cacheControl.OnlyIfCached = true;
        Assert.Equal("no-transform, only-if-cached", cacheControl.ToString());
        cacheControl.Extensions.Add(new NameValueHeaderValue("custom"));
        cacheControl.Extensions.Add(new NameValueHeaderValue("customName", "customValue"));
        Assert.Equal("no-transform, only-if-cached, custom, customName=customValue", cacheControl.ToString());
 
        cacheControl = new CacheControlHeaderValue();
        cacheControl.Extensions.Add(new NameValueHeaderValue("custom"));
        Assert.Equal("custom", cacheControl.ToString());
    }
 
    [Fact]
    public void ToString_UseResponseDirectiveValues_AllSerializedCorrectly()
    {
        var cacheControl = new CacheControlHeaderValue();
        Assert.Equal("", cacheControl.ToString());
 
        cacheControl.NoCache = true;
        Assert.Equal("no-cache", cacheControl.ToString());
        cacheControl.NoCacheHeaders.Add("PLACEHOLDER1");
        Assert.Equal("no-cache=\"PLACEHOLDER1\"", cacheControl.ToString());
        cacheControl.Public = true;
        Assert.Equal("public, no-cache=\"PLACEHOLDER1\"", cacheControl.ToString());
 
        cacheControl = new CacheControlHeaderValue();
        cacheControl.Private = true;
        Assert.Equal("private", cacheControl.ToString());
        cacheControl.PrivateHeaders.Add("PLACEHOLDER2");
        cacheControl.PrivateHeaders.Add("PLACEHOLDER3");
        Assert.Equal("private=\"PLACEHOLDER2, PLACEHOLDER3\"", cacheControl.ToString());
        cacheControl.MustRevalidate = true;
        Assert.Equal("must-revalidate, private=\"PLACEHOLDER2, PLACEHOLDER3\"", cacheControl.ToString());
        cacheControl.ProxyRevalidate = true;
        Assert.Equal("must-revalidate, proxy-revalidate, private=\"PLACEHOLDER2, PLACEHOLDER3\"", cacheControl.ToString());
    }
 
    [Fact]
    public void GetHashCode_CompareValuesWithBoolFieldsSet_MatchExpectation()
    {
        // Verify that different bool fields return different hash values.
        var values = new CacheControlHeaderValue[9];
 
        for (int i = 0; i < values.Length; i++)
        {
            values[i] = new CacheControlHeaderValue();
        }
 
        values[0].ProxyRevalidate = true;
        values[1].NoCache = true;
        values[2].NoStore = true;
        values[3].MaxStale = true;
        values[4].NoTransform = true;
        values[5].OnlyIfCached = true;
        values[6].Public = true;
        values[7].Private = true;
        values[8].MustRevalidate = true;
 
        // Only one bool field set. All hash codes should differ
        for (int i = 0; i < values.Length; i++)
        {
            for (int j = 0; j < values.Length; j++)
            {
                if (i != j)
                {
                    CompareHashCodes(values[i], values[j], false);
                }
            }
        }
 
        // Validate that two instances with the same bool fields set are equal.
        values[0].NoCache = true;
        CompareHashCodes(values[0], values[1], false);
        values[1].ProxyRevalidate = true;
        CompareHashCodes(values[0], values[1], true);
    }
 
    [Fact]
    public void GetHashCode_CompareValuesWithTimeSpanFieldsSet_MatchExpectation()
    {
        // Verify that different timespan fields return different hash values.
        var values = new CacheControlHeaderValue[4];
 
        for (int i = 0; i < values.Length; i++)
        {
            values[i] = new CacheControlHeaderValue();
        }
 
        values[0].MaxAge = new TimeSpan(0, 1, 1);
        values[1].MaxStaleLimit = new TimeSpan(0, 1, 1);
        values[2].MinFresh = new TimeSpan(0, 1, 1);
        values[3].SharedMaxAge = new TimeSpan(0, 1, 1);
 
        // Only one timespan field set. All hash codes should differ
        for (int i = 0; i < values.Length; i++)
        {
            for (int j = 0; j < values.Length; j++)
            {
                if (i != j)
                {
                    CompareHashCodes(values[i], values[j], false);
                }
            }
        }
 
        values[0].MaxStaleLimit = new TimeSpan(0, 1, 2);
        CompareHashCodes(values[0], values[1], false);
 
        values[1].MaxAge = new TimeSpan(0, 1, 1);
        values[1].MaxStaleLimit = new TimeSpan(0, 1, 2);
        CompareHashCodes(values[0], values[1], true);
    }
 
    [Fact]
    public void GetHashCode_CompareCollectionFieldsSet_MatchExpectation()
    {
        var cacheControl1 = new CacheControlHeaderValue();
        var cacheControl2 = new CacheControlHeaderValue();
        var cacheControl3 = new CacheControlHeaderValue();
        var cacheControl4 = new CacheControlHeaderValue();
        var cacheControl5 = new CacheControlHeaderValue();
 
        cacheControl1.NoCache = true;
        cacheControl1.NoCacheHeaders.Add("PLACEHOLDER2");
 
        cacheControl2.NoCache = true;
        cacheControl2.NoCacheHeaders.Add("PLACEHOLDER1");
        cacheControl2.NoCacheHeaders.Add("PLACEHOLDER2");
 
        CompareHashCodes(cacheControl1, cacheControl2, false);
 
        cacheControl1.NoCacheHeaders.Add("PLACEHOLDER1");
        CompareHashCodes(cacheControl1, cacheControl2, true);
 
        // Since NoCache and Private generate different hash codes, even if NoCacheHeaders and PrivateHeaders
        // have the same values, the hash code will be different.
        cacheControl3.Private = true;
        cacheControl3.PrivateHeaders.Add("PLACEHOLDER2");
        CompareHashCodes(cacheControl1, cacheControl3, false);
 
        cacheControl4.Extensions.Add(new NameValueHeaderValue("custom"));
        CompareHashCodes(cacheControl1, cacheControl4, false);
 
        cacheControl5.Extensions.Add(new NameValueHeaderValue("customN", "customV"));
        cacheControl5.Extensions.Add(new NameValueHeaderValue("custom"));
        CompareHashCodes(cacheControl4, cacheControl5, false);
 
        cacheControl4.Extensions.Add(new NameValueHeaderValue("customN", "customV"));
        CompareHashCodes(cacheControl4, cacheControl5, true);
    }
 
    [Fact]
    public void Equals_CompareValuesWithBoolFieldsSet_MatchExpectation()
    {
        // Verify that different bool fields return different hash values.
        var values = new CacheControlHeaderValue[9];
 
        for (int i = 0; i < values.Length; i++)
        {
            values[i] = new CacheControlHeaderValue();
        }
 
        values[0].ProxyRevalidate = true;
        values[1].NoCache = true;
        values[2].NoStore = true;
        values[3].MaxStale = true;
        values[4].NoTransform = true;
        values[5].OnlyIfCached = true;
        values[6].Public = true;
        values[7].Private = true;
        values[8].MustRevalidate = true;
 
        // Only one bool field set. All hash codes should differ
        for (int i = 0; i < values.Length; i++)
        {
            for (int j = 0; j < values.Length; j++)
            {
                if (i != j)
                {
                    CompareValues(values[i], values[j], false);
                }
            }
        }
 
        // Validate that two instances with the same bool fields set are equal.
        values[0].NoCache = true;
        CompareValues(values[0], values[1], false);
        values[1].ProxyRevalidate = true;
        CompareValues(values[0], values[1], true);
    }
 
    [Fact]
    public void Equals_CompareValuesWithTimeSpanFieldsSet_MatchExpectation()
    {
        // Verify that different timespan fields return different hash values.
        var values = new CacheControlHeaderValue[4];
 
        for (int i = 0; i < values.Length; i++)
        {
            values[i] = new CacheControlHeaderValue();
        }
 
        values[0].MaxAge = new TimeSpan(0, 1, 1);
        values[1].MaxStaleLimit = new TimeSpan(0, 1, 1);
        values[2].MinFresh = new TimeSpan(0, 1, 1);
        values[3].SharedMaxAge = new TimeSpan(0, 1, 1);
 
        // Only one timespan field set. All hash codes should differ
        for (int i = 0; i < values.Length; i++)
        {
            for (int j = 0; j < values.Length; j++)
            {
                if (i != j)
                {
                    CompareValues(values[i], values[j], false);
                }
            }
        }
 
        values[0].MaxStaleLimit = new TimeSpan(0, 1, 2);
        CompareValues(values[0], values[1], false);
 
        values[1].MaxAge = new TimeSpan(0, 1, 1);
        values[1].MaxStaleLimit = new TimeSpan(0, 1, 2);
        CompareValues(values[0], values[1], true);
 
        var value1 = new CacheControlHeaderValue();
        value1.MaxStale = true;
        var value2 = new CacheControlHeaderValue();
        value2.MaxStale = true;
        CompareValues(value1, value2, true);
 
        value2.MaxStaleLimit = new TimeSpan(1, 2, 3);
        CompareValues(value1, value2, false);
    }
 
    [Fact]
    public void Equals_CompareCollectionFieldsSet_MatchExpectation()
    {
        var cacheControl1 = new CacheControlHeaderValue();
        var cacheControl2 = new CacheControlHeaderValue();
        var cacheControl3 = new CacheControlHeaderValue();
        var cacheControl4 = new CacheControlHeaderValue();
        var cacheControl5 = new CacheControlHeaderValue();
        var cacheControl6 = new CacheControlHeaderValue();
 
        cacheControl1.NoCache = true;
        cacheControl1.NoCacheHeaders.Add("PLACEHOLDER2");
 
        Assert.False(cacheControl1.Equals(null), "Compare with 'null'");
 
        cacheControl2.NoCache = true;
        cacheControl2.NoCacheHeaders.Add("PLACEHOLDER1");
        cacheControl2.NoCacheHeaders.Add("PLACEHOLDER2");
 
        CompareValues(cacheControl1!, cacheControl2, false);
 
        cacheControl1!.NoCacheHeaders.Add("PLACEHOLDER1");
        CompareValues(cacheControl1, cacheControl2, true);
 
        // Since NoCache and Private generate different hash codes, even if NoCacheHeaders and PrivateHeaders
        // have the same values, the hash code will be different.
        cacheControl3.Private = true;
        cacheControl3.PrivateHeaders.Add("PLACEHOLDER2");
        CompareValues(cacheControl1, cacheControl3, false);
 
        cacheControl4.Private = true;
        cacheControl4.PrivateHeaders.Add("PLACEHOLDER3");
        CompareValues(cacheControl3, cacheControl4, false);
 
        cacheControl5.Extensions.Add(new NameValueHeaderValue("custom"));
        CompareValues(cacheControl1, cacheControl5, false);
 
        cacheControl6.Extensions.Add(new NameValueHeaderValue("customN", "customV"));
        cacheControl6.Extensions.Add(new NameValueHeaderValue("custom"));
        CompareValues(cacheControl5, cacheControl6, false);
 
        cacheControl5.Extensions.Add(new NameValueHeaderValue("customN", "customV"));
        CompareValues(cacheControl5, cacheControl6, true);
    }
 
    [Fact]
    public void TryParse_DifferentValidScenarios_AllReturnTrue()
    {
        var expected = new CacheControlHeaderValue();
        expected.NoCache = true;
        CheckValidTryParse(" , no-cache ,,", expected);
 
        expected = new CacheControlHeaderValue();
        expected.NoCache = true;
        expected.NoCacheHeaders.Add("PLACEHOLDER1");
        expected.NoCacheHeaders.Add("PLACEHOLDER2");
        CheckValidTryParse("no-cache=\"PLACEHOLDER1, PLACEHOLDER2\"", expected);
 
        expected = new CacheControlHeaderValue();
        expected.NoStore = true;
        expected.MaxAge = new TimeSpan(0, 0, 125);
        expected.MaxStale = true;
        CheckValidTryParse(" no-store , max-age = 125, max-stale,", expected);
 
        expected = new CacheControlHeaderValue();
        expected.MinFresh = new TimeSpan(0, 0, 123);
        expected.NoTransform = true;
        expected.OnlyIfCached = true;
        expected.Extensions.Add(new NameValueHeaderValue("custom"));
        CheckValidTryParse("min-fresh=123, no-transform, only-if-cached, custom", expected);
 
        expected = new CacheControlHeaderValue();
        expected.Public = true;
        expected.Private = true;
        expected.PrivateHeaders.Add("PLACEHOLDER1");
        expected.MustRevalidate = true;
        expected.ProxyRevalidate = true;
        expected.Extensions.Add(new NameValueHeaderValue("c", "d"));
        expected.Extensions.Add(new NameValueHeaderValue("a", "b"));
        CheckValidTryParse(",public, , private=\"PLACEHOLDER1\", must-revalidate, c=d, proxy-revalidate, a=b", expected);
 
        expected = new CacheControlHeaderValue();
        expected.Private = true;
        expected.SharedMaxAge = new TimeSpan(0, 0, 1234567890);
        expected.MaxAge = new TimeSpan(0, 0, 987654321);
        CheckValidTryParse("s-maxage=1234567890, private, max-age = 987654321,", expected);
 
        expected = new CacheControlHeaderValue();
        expected.Extensions.Add(new NameValueHeaderValue("custom", ""));
        CheckValidTryParse("custom=", expected);
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("    ")]
    // PLACEHOLDER-only values
    [InlineData("no-store=15")]
    [InlineData("no-store=")]
    [InlineData("no-transform=a")]
    [InlineData("no-transform=")]
    [InlineData("only-if-cached=\"x\"")]
    [InlineData("only-if-cached=")]
    [InlineData("public=\"x\"")]
    [InlineData("public=")]
    [InlineData("must-revalidate=\"1\"")]
    [InlineData("must-revalidate=")]
    [InlineData("proxy-revalidate=x")]
    [InlineData("proxy-revalidate=")]
    // PLACEHOLDER with optional field-name list
    [InlineData("no-cache=")]
    [InlineData("no-cache=PLACEHOLDER")]
    [InlineData("no-cache=\"PLACEHOLDER")]
    [InlineData("no-cache=\"\"")] // at least one PLACEHOLDER expected as value
    [InlineData("private=")]
    [InlineData("private=PLACEHOLDER")]
    [InlineData("private=\"PLACEHOLDER")]
    [InlineData("private=\",\"")] // at least one PLACEHOLDER expected as value
    [InlineData("private=\"=\"")]
    // PLACEHOLDER with delta-seconds value
    [InlineData("max-age")]
    [InlineData("max-age=")]
    [InlineData("max-age=a")]
    [InlineData("max-age=\"1\"")]
    [InlineData("max-age=1.5")]
    [InlineData("max-stale=")]
    [InlineData("max-stale=a")]
    [InlineData("max-stale=\"1\"")]
    [InlineData("max-stale=1.5")]
    [InlineData("min-fresh")]
    [InlineData("min-fresh=")]
    [InlineData("min-fresh=a")]
    [InlineData("min-fresh=\"1\"")]
    [InlineData("min-fresh=1.5")]
    [InlineData("s-maxage")]
    [InlineData("s-maxage=")]
    [InlineData("s-maxage=a")]
    [InlineData("s-maxage=\"1\"")]
    [InlineData("s-maxage=1.5")]
    // Invalid Extension values
    [InlineData("custom value")]
    public void TryParse_DifferentInvalidScenarios_ReturnsFalse(string? input)
    {
        CheckInvalidTryParse(input);
    }
 
    [Fact]
    public void Parse_SetOfValidValueStrings_ParsedCorrectly()
    {
        // Just verify parser is implemented correctly. Don't try to test syntax parsed by CacheControlHeaderValue.
        var expected = new CacheControlHeaderValue();
        expected.NoStore = true;
        expected.MinFresh = new TimeSpan(0, 2, 3);
        CheckValidParse(" , no-store, min-fresh=123", expected);
 
        expected = new CacheControlHeaderValue();
        expected.MaxStale = true;
        expected.NoCache = true;
        expected.NoCacheHeaders.Add("t");
        CheckValidParse("max-stale, no-cache=\"t\", ,,", expected);
 
        expected = new CacheControlHeaderValue();
        expected.Extensions.Add(new NameValueHeaderValue("custom"));
        CheckValidParse("custom =", expected);
 
        expected = new CacheControlHeaderValue();
        expected.Extensions.Add(new NameValueHeaderValue("custom", ""));
        CheckValidParse("custom =", expected);
    }
 
    [Fact]
    public void Parse_SetOfInvalidValueStrings_Throws()
    {
        CheckInvalidParse(null);
        CheckInvalidParse("");
        CheckInvalidParse("   ");
        CheckInvalidParse("no-cache,=");
        CheckInvalidParse("max-age=123x");
        CheckInvalidParse("=no-cache");
        CheckInvalidParse("no-cache no-store");
        CheckInvalidParse("会");
    }
 
    [Fact]
    public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
    {
        // Just verify parser is implemented correctly. Don't try to test syntax parsed by CacheControlHeaderValue.
        var expected = new CacheControlHeaderValue();
        expected.NoStore = true;
        expected.MinFresh = new TimeSpan(0, 2, 3);
        CheckValidTryParse(" , no-store, min-fresh=123", expected);
 
        expected = new CacheControlHeaderValue();
        expected.MaxStale = true;
        expected.NoCache = true;
        expected.NoCacheHeaders.Add("t");
        CheckValidTryParse("max-stale, no-cache=\"t\", ,,", expected);
 
        expected = new CacheControlHeaderValue();
        expected.Extensions.Add(new NameValueHeaderValue("custom"));
        CheckValidTryParse("custom = ", expected);
 
        expected = new CacheControlHeaderValue();
        expected.Extensions.Add(new NameValueHeaderValue("custom", ""));
        CheckValidTryParse("custom =", expected);
    }
 
    [Fact]
    public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
    {
        CheckInvalidTryParse("no-cache,=");
        CheckInvalidTryParse("max-age=123x");
        CheckInvalidTryParse("=no-cache");
        CheckInvalidTryParse("no-cache no-store");
        CheckInvalidTryParse("会");
    }
 
    #region Helper methods
 
    private void CompareHashCodes(CacheControlHeaderValue x, CacheControlHeaderValue y, bool areEqual)
    {
        if (areEqual)
        {
            Assert.Equal(x.GetHashCode(), y.GetHashCode());
        }
        else
        {
            Assert.NotEqual(x.GetHashCode(), y.GetHashCode());
        }
    }
 
    private void CompareValues(CacheControlHeaderValue x, CacheControlHeaderValue y, bool areEqual)
    {
        Assert.Equal(areEqual, x.Equals(y));
        Assert.Equal(areEqual, y.Equals(x));
    }
 
    private void CheckValidParse(string? input, CacheControlHeaderValue expectedResult)
    {
        var result = CacheControlHeaderValue.Parse(input);
        Assert.Equal(expectedResult, result);
    }
 
    private void CheckInvalidParse(string? input)
    {
        Assert.Throws<FormatException>(() => CacheControlHeaderValue.Parse(input));
    }
 
    private void CheckValidTryParse(string? input, CacheControlHeaderValue expectedResult)
    {
        Assert.True(CacheControlHeaderValue.TryParse(input, out var result));
        Assert.Equal(expectedResult, result);
    }
 
    private void CheckInvalidTryParse(string? input)
    {
        Assert.False(CacheControlHeaderValue.TryParse(input, out var result));
        Assert.Null(result);
    }
 
    #endregion
}