XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Net;
using System.Text;
using System.Xml;
using XFEEDToolBox.Core.News;
using XFEExtension.NetCore.XUnit.Attributes;

namespace XFEEDToolBox.Test;

[TestFixture]
public class GalnetNewsTests
{
    private static string Feed(string id = "one", string date = "Sat, 22 Oct 3312 00:00:00 GMT", string title = "Galnet headline") => $"""
        <?xml version="1.0" encoding="utf-8"?>
        <rss version="2.0"><channel><title>Elite Dangerous Galnet News</title><item>
        <guid>{id}</guid><title><![CDATA[{title}]]></title>
        <link>https://www.elitedangerous.com/news/galnet/example</link><pubDate>{date}</pubDate>
        <description><![CDATA[First &amp; second.<br/>Next paragraph.<script>hidden()</script>]]></description>
        </item></channel></rss>
        """;

    private sealed class Handler : HttpMessageHandler
    {
        public string Xml = Feed();
        public HttpStatusCode Status = HttpStatusCode.OK;
        public bool Offline, WaitForCancellation;
        public readonly TaskCompletionSource Entered = new(TaskCreationOptions.RunContinuationsAsynchronously);
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
        {
            Check.Equal(GalnetRss.FeedUrl, request.RequestUri!.AbsoluteUri);
            Entered.TrySetResult();
            if (WaitForCancellation) await Task.Delay(Timeout.Infinite, token);
            if (Offline) throw new HttpRequestException("offline");
            return new(Status) { Content = new StringContent(Xml, Encoding.UTF8, "application/rss+xml") };
        }
    }

    [Test] public void ParsesRealGameYearAndReadablePlainText()
    {
        var article = GalnetRss.Parse(Feed(title: "Anaconda Mk II &amp; field trials")).Single();
        Check.Equal("Anaconda Mk II & field trials", article.Title);
        Check.Equal("First & second.\nNext paragraph.", article.Body);
        Check.Equal(3312, article.PublishedAt!.Value.Year);
        Check.Equal("22 OCT 3312", article.GameDate);
        Check.That(article.SourceUri?.Host == "www.elitedangerous.com");
    }

    [Test] public void RejectsExternalEntitiesAndNonOfficialLinks()
    {
        Check.Throws<XmlException>(() => GalnetRss.Parse("<!DOCTYPE rss [<!ENTITY x SYSTEM 'file:///C:/Windows/win.ini'>]><rss><channel><item>&x;</item></channel></rss>"));
        var article = GalnetRss.Parse(Feed().Replace("https://www.elitedangerous.com/news/galnet/example", "https://invalid.example/news")).Single();
        Check.Equal("", article.Link);
        Check.That(article.SourceUri is null);
        Check.That(GalnetRss.OfficialUri("https://user@www.elitedangerous.com/news") is null);
    }

    [Test] public async Task PersistsNewsAcrossRestartAndRetainsItWhenOffline()
    {
        using var folder = new TemporaryDirectory();
        var path = folder.File("news.json");
        using var handler = new Handler(); using var http = new HttpClient(handler);
        var first = await new GalnetNewsService(path, http).RefreshAsync();
        Check.That(first.Updated && File.Exists(path));
        handler.Offline = true;
        var restarted = new GalnetNewsService(path, http);
        var saved = await restarted.LoadCachedAsync();
        Check.Equal("one", saved!.Articles.Single().Id);
        var failed = await restarted.RefreshAsync();
        Check.That(ReferenceEquals(saved, failed.Snapshot) && !failed.Updated);
        Check.Equal(first.Snapshot!.RetrievedAt, failed.Snapshot!.RetrievedAt);
    }

    [Test] public async Task EmptyMalformedAndOlderResponsesCannotEraseTheSavedEdition()
    {
        using var folder = new TemporaryDirectory();
        var path = folder.File("news.json");
        using var handler = new Handler(); using var http = new HttpClient(handler);
        var service = new GalnetNewsService(path, http);
        var original = (await service.RefreshAsync()).Snapshot;
        var saved = await File.ReadAllTextAsync(path);
        foreach (var response in new[] { "<rss><channel/></rss>", "<html>not news</html>", "bad xml", Feed("old", "21 Oct 3312 00:00:00 GMT"), new string('x', GalnetRss.MaximumBytes + 1) })
        {
            handler.Xml = response;
            var result = await service.RefreshAsync();
            Check.That(ReferenceEquals(original, result.Snapshot) && !result.Updated);
            Check.Equal(saved, await File.ReadAllTextAsync(path));
        }
    }

    [Test] public async Task OnlyNewOrChangedContentReplacesTheEdition()
    {
        using var folder = new TemporaryDirectory();
        using var handler = new Handler(); using var http = new HttpClient(handler);
        var service = new GalnetNewsService(folder.File("news.json"), http);
        var first = await service.RefreshAsync();
        var same = await service.RefreshAsync();
        Check.That(!same.Updated && ReferenceEquals(first.Snapshot, same.Snapshot));
        handler.Xml = Feed("two", "23 Oct 3312 00:00:00 GMT");
        var next = await service.RefreshAsync();
        Check.That(next.Updated);
        Check.Equal("two", next.Snapshot!.Articles.Single().Id);
        Check.Equal("two", (await new GalnetNewsService(folder.File("news.json"), http).LoadCachedAsync())!.Articles.Single().Id);
    }

    [Test] public async Task CancellingRefreshLeavesTheSavedNewsIntact()
    {
        using var folder = new TemporaryDirectory();
        var path = folder.File("news.json");
        using var handler = new Handler(); using var http = new HttpClient(handler);
        var service = new GalnetNewsService(path, http);
        var original = (await service.RefreshAsync()).Snapshot;
        var saved = await File.ReadAllTextAsync(path);
        handler.WaitForCancellation = true;
        using var stop = new CancellationTokenSource();
        var pending = service.RefreshAsync(stop.Token);
        stop.Cancel();
        try { await pending; throw new InvalidOperationException("Expected cancellation"); }
        catch (OperationCanceledException) { }
        Check.That(ReferenceEquals(original, service.Current));
        Check.Equal(saved, await File.ReadAllTextAsync(path));
    }

    [Test] public async Task InvalidCacheCanBeReplacedByValidNews()
    {
        using var folder = new TemporaryDirectory();
        var path = folder.File("news.json");
        await File.WriteAllTextAsync(path, """{"articles":[null]}""");
        using var handler = new Handler(); using var http = new HttpClient(handler);
        var result = await new GalnetNewsService(path, http).RefreshAsync();
        Check.That(result.Updated && result.Snapshot!.Articles.Count == 1);
    }
}

[TestFixture, Explicit("Reads Frontier's public Galnet feed; opt in manually.")]
public class GalnetNewsLiveTests
{
    [Test] public async Task ReadsOfficialFeedAndRestoresTheDownloadedEdition()
    {
        using var folder = new TemporaryDirectory();
        var path = folder.File("news.json");
        var result = await new GalnetNewsService(path).RefreshAsync();
        Check.That(result.Snapshot is { Articles.Count: > 0 }, result.Message);
        var snapshot = result.Snapshot!;
        var restored = await new GalnetNewsService(path).LoadCachedAsync();
        Check.That(restored is not null && restored.Articles.SequenceEqual(snapshot.Articles));
        var report = System.IO.Path.GetFullPath("artifacts/local-news-0416/live-summary.json");
        Directory.CreateDirectory(System.IO.Path.GetDirectoryName(report)!);
        await File.WriteAllTextAsync(report, System.Text.Json.JsonSerializer.Serialize(new
        {
            source = GalnetRss.FeedUrl, count = snapshot.Articles.Count,
            latestTitle = snapshot.Articles[0].Title, gameDate = snapshot.Articles[0].GameDate,
            snapshot.RetrievedAt, restoredAfterRestart = true
        }, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }));
    }
}