File: OutputCacheEntryFormatterTests.cs
Web Access
Project: src\src\Middleware\OutputCaching\test\Microsoft.AspNetCore.OutputCaching.Tests.csproj (Microsoft.AspNetCore.OutputCaching.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Net.Http.Headers;
 
namespace Microsoft.AspNetCore.OutputCaching.Tests;
 
public class OutputCacheEntryFormatterTests_SimpleStore : OutputCacheEntryFormatterTests
{
    public override ITestOutputCacheStore GetStore() => new SimpleTestOutputCache();
}
 
public class OutputCacheEntryFormatterTests_BufferStore : OutputCacheEntryFormatterTests
{
    public override ITestOutputCacheStore GetStore() => new BufferTestOutputCache();
}
 
public abstract class OutputCacheEntryFormatterTests
{
    public abstract ITestOutputCacheStore GetStore();
 
    // arbitrarily some time 17 May 2023 - so we can predict payloads
    static readonly DateTimeOffset KnownTime = DateTimeOffset.FromUnixTimeMilliseconds(1684322693875);
 
    [Fact]
    public async Task StoreAndGet_StoresEmptyValues()
    {
        var store = GetStore();
        var key = "abc";
        using var entry = new OutputCacheEntry(KnownTime, StatusCodes.Status200OK);
 
        await OutputCacheEntryFormatter.StoreAsync(key, entry, null, TimeSpan.Zero, store, NullLogger.Instance, default);
 
        var result = await OutputCacheEntryFormatter.GetAsync(key, store, default);
 
        AssertEntriesAreSame(entry, result);
    }
 
    [Fact]
    public async Task StoreAndGet_StoresAllValues()
    {
        var bodySegment1 = "lorem"u8.ToArray();
        var bodySegment2 = "こんにちは"u8.ToArray();
 
        var store = GetStore();
        var key = "abc";
        using (var entry = new OutputCacheEntry(KnownTime, StatusCodes.Status201Created)
            .CopyHeadersFrom(new HeaderDictionary { [HeaderNames.Accept] = new[] { "text/plain", "text/html" }, [HeaderNames.AcceptCharset] = "utf8" })
            .CreateBodyFrom(new[] { bodySegment1, bodySegment1 }))
        {
            await OutputCacheEntryFormatter.StoreAsync(key, entry, new HashSet<string>() { "tag", "タグ" }, TimeSpan.Zero, store, NullLogger.Instance, default);
            var result = await OutputCacheEntryFormatter.GetAsync(key, store, default);
 
            AssertEntriesAreSame(entry, result);
        }
    }
 
    [Fact]
 
    public async Task StoreAndGet_StoresNullHeaders()
    {
        var store = GetStore();
        var key = "abc";
 
        using (var entry = new OutputCacheEntry(KnownTime, StatusCodes.Status201Created))
        {
            entry.CopyHeadersFrom(new HeaderDictionary { [""] = "", [HeaderNames.Accept] = new[] { null, null, "", "text/html" }, [HeaderNames.AcceptCharset] = new string[] { null } });
 
            await OutputCacheEntryFormatter.StoreAsync(key, entry, null, TimeSpan.Zero, store, NullLogger.Instance, default);
        }
        var payload = await store.GetAsync(key, CancellationToken.None);
        Assert.NotNull(payload);
        var hex = BitConverter.ToString(payload);
        Assert.Equal(KnownV2Payload, hex);
 
        var result = await OutputCacheEntryFormatter.GetAsync(key, store, default);
 
        Assert.Equal(3, result.Headers.Length);
        Assert.True(result.TryFindHeader("", out var values), "Find ''");
        Assert.Equal("", values);
        Assert.True(result.TryFindHeader(HeaderNames.Accept, out values));
        Assert.Equal(4, values.Count);
        Assert.Equal("", values[0]);
        Assert.Equal("", values[1]);
        Assert.Equal("", values[2]);
        Assert.Equal("text/html", values[3]);
        Assert.True(result.TryFindHeader(HeaderNames.AcceptCharset, out values), "Find 'AcceptCharset'");
        Assert.Equal("", values[0]);
    }
 
    [Fact]
    public void KnownV1AndV2AreCompatible()
    {
        AssertEntriesAreSame(
            OutputCacheEntryFormatter.Deserialize(FromHex(KnownV1Payload)),
            OutputCacheEntryFormatter.Deserialize(FromHex(KnownV2Payload))
        );
    }
    static byte[] FromHex(string hex)
    {
        // inefficient; for testing only
        hex = hex.Replace("-", "");
        var arr = new byte[hex.Length / 2];
        int index = 0;
        for (int i = 0; i < arr.Length; i++)
        {
            arr[i] = (byte)((Nibble(hex[index++]) << 4) | Nibble(hex[index++]));
        }
        return arr;
 
        static int Nibble(char value)
        {
            return value switch
            {
                >= '0' and <= '9' => value - '0',
                >= 'a' and <= 'f' => value - 'a' + 10,
                >= 'A' and <= 'F' => value - 'A' + 10,
                _ => throw new ArgumentOutOfRangeException(nameof(value), "token is not hex: " + value.ToString())
            };
        }
    }
 
    const string KnownV1Payload = "01-B0-E8-8E-B2-95-D9-D5-ED-08-00-C9-01-03-00-01-00-06-41-63-63-65-70-74-04-00-00-00-09-74-65-78-74-2F-68-74-6D-6C-0E-41-63-63-65-70-74-2D-43-68-61-72-73-65-74-01-00-00-00";
    // 01                                              version 1
    // B0-E8-8E-B2-95-D9-D5-ED-08                      ticks 1684322693875
    // 00                                              offset 0
    // C9-01                                           status 201
    // 03                                              headers 3
    // 00                                              [0] header name ""
    // 01                                              [0] header value count 1
    // 00                                              [0.0] header value ""
    // 06-41-63-63-65-70-74                            [1] header name "Accept"
    // 04                                              [1] header value count 4
    // 00-00-00                                        [1.0, 1.1, 1.2] header value ""
    // 09-74-65-78-74-2F-68-74-6D-6C                   [1.3] header value "text/html"
    // 0E-41-63-63-65-70-74-2D-43-68-61-72-73-65-74    [2] header name "Accept-Charset"
    // 01                                              [2] header value count 1
    // 00                                              [2.0] header value ""
    // 00                                              segment count 0
    // 00                                              tag count 0
 
    const string KnownV2Payload = "02-B0-E8-8E-B2-95-D9-D5-ED-08-00-C9-01-03-00-01-00-01-04-00-00-00-9B-01-03-01-00-00";
    // 02                                              version 2
    // B0-E8-8E-B2-95-D9-D5-ED-08                      ticks 1684322693875
    // 00                                              offset 0
    // C9-01                                           status 201
    // 03                                              headers 3
    // 00                                              [0] header name ""
    // 01                                              [0] header value count 1
    // 00                                              [0.0] header value ""
    // 01                                              [1] header name "Accept"
    // 04                                              [1] header value count 4
    // 00-00-00                                        [1.0, 1.1, 1.2] header value ""
    // 9B-01                                           [1.3] header value "text/html"
    // 03                                              [2] header name "Accept-Charset"
    // 01                                              [2] header value count 1
    // 00                                              [2.0] header value ""
    // 00                                              segment count 0
 
    private static void AssertEntriesAreSame(OutputCacheEntry expected, OutputCacheEntry actual)
    {
        Assert.NotNull(expected);
        Assert.NotNull(actual);
        Assert.Equal(expected.Created, actual.Created);
        Assert.Equal(expected.StatusCode, actual.StatusCode);
        Assert.True(expected.Headers.Span.SequenceEqual(actual.Headers.Span), "Headers");
        Assert.Equal(expected.Body.Length, actual.Body.Length);
        Assert.True(SequenceEqual(expected.Body, actual.Body));
    }
 
    static bool SequenceEqual(ReadOnlySequence<byte> x, ReadOnlySequence<byte> y)
    {
        var xLinear = Linearize(x, out var xBuffer);
        var yLinear = Linearize(x, out var yBuffer);
 
        var result = xLinear.Span.SequenceEqual(yLinear.Span);
 
        if (xBuffer is not null)
        {
            ArrayPool<byte>.Shared.Return(xBuffer);
        }
        if (yBuffer is not null)
        {
            ArrayPool<byte>.Shared.Return(yBuffer);
        }
 
        return result;
 
        static ReadOnlyMemory<byte> Linearize(in ReadOnlySequence<byte> value, out byte[] lease)
        {
            lease = null;
            if (value.IsEmpty) { return default; }
            if (value.IsSingleSegment) { return value.First; }
 
            var len = checked((int)value.Length);
            lease = ArrayPool<byte>.Shared.Rent(len);
            value.CopyTo(lease);
            return new ReadOnlyMemory<byte>(lease, 0, len);
        }
    }
}