Newer
Older
Sunfish / tests / Sunfish.Tests / WebUnitTests.cs
@Ivan Dominguez Ivan Dominguez 21 hours ago 5 KB 3.0
using System.Text;
using Sunfish.Http;
using Sunfish.Web;

namespace Sunfish.Tests;

public class HtmlBuilderTests
{
    [Fact]
    public void ClassicModeIsPureAscii()
    {
        string escaped = HtmlBuilder.Escape("Año <ñ> & \"€\" ő 日", classic: true);
        Assert.Equal("A&#241;o &lt;&#241;&gt; &amp; &quot;?&quot; o ?", escaped);
        Assert.All(escaped, c => Assert.True(c < 0x80));
    }

    [Fact]
    public void ModernModeKeepsUnicode() =>
        Assert.Equal("Año &lt;日&gt;", HtmlBuilder.Escape("Año <日>", classic: false));

    [Fact]
    public void UrlSegmentsAreUtf8Percent() =>
        Assert.Equal("Fotos%20del%20a%C3%B1o.jpg", HtmlBuilder.UrlSegment("Fotos del año.jpg"));
}

public class ClientProfileTests
{
    [Theory]
    [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/130 Safari/537.36", ClientKind.Modern)]
    [InlineData("Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0", ClientKind.Modern)]
    [InlineData("Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 Safari/605.1.15", ClientKind.Modern)]
    [InlineData("Mozilla/4.0 (compatible; MSIE 5.5; Windows 98)", ClientKind.Classic)]
    [InlineData("Mozilla/3.01 (Win95; I)", ClientKind.Classic)]
    [InlineData("Mozilla/3.0 (Win16; I)", ClientKind.Win16)]
    [InlineData("Mozilla/2.0 (compatible; MSIE 3.0; Windows 3.1)", ClientKind.Win16)]
    [InlineData("Mozilla/3.0 (Macintosh; I; PPC)", ClientKind.Mac)]
    [InlineData("Mozilla/4.0 (compatible; MSIE 5.0; Mac_PowerPC)", ClientKind.Mac)]
    [InlineData("Lynx/2.8.9rel.1 libwww-FM/2.14", ClientKind.Text)]
    [InlineData("xChaos_Arachne/5.1.89;GPL,386+", ClientKind.Dos)]
    [InlineData(null, ClientKind.Classic)]
    public void DetectsFromUserAgent(string? userAgent, ClientKind expected)
    {
        var profile = ClientProfile.Detect(userAgent, null);
        Assert.Equal(expected, profile.Kind);
        Assert.False(profile.Forced);
    }

    [Fact]
    public void QueryForcesTheProfile()
    {
        var profile = ClientProfile.Detect("Mozilla/5.0 (X11) Gecko/20100101 Firefox/130.0", "dos");
        Assert.Equal(ClientKind.Dos, profile.Kind);
        Assert.True(profile.Forced);
        Assert.Equal(ClientKind.Modern, ClientProfile.Detect("Mozilla/5.0 Gecko/1", "nonsense").Kind);
    }
}

public class ByteRangeTests
{
    [Theory]
    [InlineData("bytes=0-99", 1000, 0, 99)]
    [InlineData("bytes=500-", 1000, 500, 999)]
    [InlineData("bytes=-100", 1000, 900, 999)]
    [InlineData("bytes=900-5000", 1000, 900, 999)]
    [InlineData("bytes=-5000", 1000, 0, 999)]
    public void ValidRanges(string header, long size, long start, long end)
    {
        Assert.Equal(ByteRange.ParseResult.Valid, ByteRange.TryParse(header, size, out var range));
        Assert.Equal(new ByteRange(start, end), range);
    }

    [Theory]
    [InlineData(null)]
    [InlineData("items=0-1")]
    [InlineData("bytes=0-1,5-6")]
    [InlineData("bytes=9-3")]
    [InlineData("bytes=a-b")]
    public void IgnoredRanges(string? header) =>
        Assert.Equal(ByteRange.ParseResult.None, ByteRange.TryParse(header, 1000, out _));

    [Fact]
    public void RangeBeyondTheEnd() =>
        Assert.Equal(ByteRange.ParseResult.Unsatisfiable, ByteRange.TryParse("bytes=1000-", 1000, out _));
}

public class HttpRequestReaderTests
{
    private static async Task<HttpRequest?> Read(string raw)
    {
        var reader = new HttpRequestReader(new MemoryStream(Encoding.Latin1.GetBytes(raw)));
        return await reader.ReadRequestAsync(() => Task.CompletedTask, CancellationToken.None);
    }

    [Fact]
    public async Task ParsesHttp11()
    {
        var r = await Read("GET /a%20b?x=1&y HTTP/1.1\r\nHost: h\r\nUser-Agent: test\r\n\r\n");
        Assert.NotNull(r);
        Assert.Equal("GET", r.Method);
        Assert.Equal("/a%20b", r.Path);
        Assert.Equal("1", r.GetQuery("x"));
        Assert.True(r.HasQuery("y"));
        Assert.True(r.IsHttp11OrLater);
        Assert.True(r.WantsKeepAlive);
        Assert.Equal("test", r.Headers["user-agent"]);
    }

    [Fact]
    public async Task AcceptsBareLineFeedsAndNoHost()
    {
        var r = await Read("\n\nGET / HTTP/1.0\nAccept: */*\n\n");
        Assert.NotNull(r);
        Assert.Equal("/", r.Path);
        Assert.False(r.WantsKeepAlive);
        Assert.Null(r.Headers["Host"]);
    }

    [Fact]
    public async Task ParsesHttp09()
    {
        var r = await Read("GET /index.html\r\n");
        Assert.NotNull(r);
        Assert.True(r.IsHttp09);
        Assert.Equal("/index.html", r.Path);
    }

    [Fact]
    public async Task FoldedHeadersAndJunkLines()
    {
        var r = await Read("GET / HTTP/1.0\r\nX-Long: a\r\n  b\r\nbasura sin dos puntos\r\n\r\n");
        Assert.Equal("a b", r!.Headers["X-Long"]);
    }

    [Fact]
    public async Task AbsoluteTargetAndRawHighBytes()
    {
        var r = await Read("GET http://host:8080/año/?q HTTP/1.0\r\n\r\n");
        Assert.Equal("/a%F1o/", r!.Path);
        Assert.Equal("q", r.QueryString);
    }

    [Theory]
    [InlineData("POST /x\r\n", 400)]
    [InlineData("GET / HTTP/2.0\r\n\r\n", 505)]
    [InlineData("GET / HTTP/x\r\n\r\n", 400)]
    public async Task RejectsMalformedRequests(string raw, int status)
    {
        var e = await Assert.ThrowsAsync<HttpParseException>(() => Read(raw));
        Assert.Equal(status, e.Status);
    }

    [Fact]
    public async Task ReadsChunkedBodies()
    {
        var r = await Read("PUT /x HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n6;ext=1\r\n world\r\n0\r\n\r\n");
        var ms = new MemoryStream();
        await r!.Body.CopyToAsync(ms);
        Assert.Equal("hello world", Encoding.ASCII.GetString(ms.ToArray()));
    }

    [Fact]
    public async Task ReturnsNullOnCleanClose() => Assert.Null(await Read(""));
}