using AIStudio.Tools.Web; namespace AIStudio.Tests.Tools.Web; /// /// Checks how web addresses are found in a chat and when two of them count as the same. /// /// /// A chat restricted by a mailbox may read a web page only when its address stands in the chat. /// Two mistakes are possible, and they cost differently. An address found too short or too long /// keeps the user from a page they gave the AI, which is annoying. Two addresses counted as the /// same although they ask the server for different things would let the model put mail content /// into a query, which is what the restriction is there to prevent. /// [TestFixture] public sealed class WebAddressesTests { [TestCase("See https://example.org/report.", "https://example.org/report")] [TestCase("Is it https://example.org/report?", "https://example.org/report")] [TestCase("[The report](https://example.org/report)", "https://example.org/report")] [TestCase("", "https://example.org/report")] [TestCase("\"https://example.org/report\"", "https://example.org/report")] [TestCase("'https://example.org/report'", "https://example.org/report")] [TestCase("(see https://example.org/report)", "https://example.org/report")] [TestCase("https://en.wikipedia.org/wiki/Mercury_(planet)", "https://en.wikipedia.org/wiki/Mercury_(planet)")] [TestCase("[Mercury](https://en.wikipedia.org/wiki/Mercury_(planet))", "https://en.wikipedia.org/wiki/Mercury_(planet)")] [TestCase("HTTPS://EXAMPLE.ORG/Report, and more", "HTTPS://EXAMPLE.ORG/Report")] // Internal services often run without TLS, e.g., a simple Python web server which publishes the // results of a data science run: [TestCase("The results are on http://10.20.30.40:8000/run-17/.", "http://10.20.30.40:8000/run-17/")] [TestCase("Server runs at http://localhost:8000/", "http://localhost:8000/")] [TestCase("[Plot](http://ds-results.intern:8080/plots/loss.html)", "http://ds-results.intern:8080/plots/loss.html")] [TestCase("HTTP://DS-RESULTS.INTERN/report.html; see there", "HTTP://DS-RESULTS.INTERN/report.html")] [TestCase("Try http://[fd00::5]:8000/run-17/ as well", "http://[fd00::5]:8000/run-17/")] public void AnAddressIsFoundAsItStands(string text, string expected) => Assert.That(WebAddresses.Find(text), Is.EqualTo(new[] { expected })); [Test] public void AnAddressWithHtmlEntitiesIsAlsoFoundDecoded() => Assert.That(WebAddresses.Find("https://example.org/search?q=budget&year=2026"), Is.EqualTo(new[] { "https://example.org/search?q=budget&year=2026", "https://example.org/search?q=budget&year=2026" })); [Test] public void TextWithoutAddressesHasNone() { Assert.Multiple(() => { Assert.That(WebAddresses.Find("No address here, not even example.org."), Is.Empty, "Without a scheme, nobody can tell an address from a word."); Assert.That(WebAddresses.Find("ftp://example.org/report"), Is.Empty); Assert.That(WebAddresses.Find(null), Is.Empty); }); } [TestCase("https://example.org/report", "https://EXAMPLE.org/report")] [TestCase("https://example.org/report", "HTTPS://example.org/report")] [TestCase("https://example.org/report", "https://example.org/report#summary")] [TestCase("https://example.org/report", "https://example.org:443/report")] [TestCase("https://example.org/report", "https://example.org./report")] [TestCase("https://bücher.example/", "https://xn--bcher-kva.example/")] [TestCase("https://example.org/a/../report", "https://example.org/report")] [TestCase("http://ds-results.intern/report.html", "http://DS-RESULTS.intern:80/report.html")] [TestCase("http://ds-results.intern/report.html", "HTTP://ds-results.intern/report.html#loss")] [TestCase("http://10.20.30.40:8000/run-17/", "http://10.20.30.40:8000/run-17/#plot")] [TestCase("http://localhost:8000/", "http://LOCALHOST:8000/")] [TestCase("http://[fd00::5]:8000/run-17/", "http://[FD00:0:0:0:0:0:0:5]:8000/run-17/")] public void AddressesAskingForTheSameCountAsTheSame(string inChat, string fromModel) { Assert.Multiple(() => { Assert.That(WebAddresses.TryCreateRequestKey(inChat, out var expected), Is.True); Assert.That(WebAddresses.TryCreateRequestKey(fromModel, out var actual), Is.True); Assert.That(actual, Is.EqualTo(expected), "The server gets the same request, so nothing more of the chat leaves AI Studio."); }); } [TestCase("https://example.org/report?year=2026", "https://example.org/report?year=2026¬e=budget")] [TestCase("https://example.org/report?year=2026", "https://example.org/report?year=2025")] [TestCase("https://example.org/report", "https://example.org/report?budget")] [TestCase("https://example.org/report", "https://example.org/Report")] [TestCase("https://example.org/report", "https://example.org/report/budget")] [TestCase("https://example.org/report", "https://budget@example.org/report")] [TestCase("https://example.org/report", "http://example.org/report")] [TestCase("https://example.org/report", "https://example.org:8443/report")] [TestCase("https://example.org/report", "https://budget.example.org/report")] [TestCase("http://ds-results.intern:8000/report.html", "http://ds-results.intern/report.html")] [TestCase("http://ds-results.intern/report.html", "https://ds-results.intern/report.html")] [TestCase("http://10.20.30.40:8000/run-17/", "http://10.20.30.40:8001/run-17/")] [TestCase("http://10.20.30.40:8000/run-17/", "http://10.20.30.41:8000/run-17/")] [TestCase("http://10.20.30.40:8000/run-17/", "http://10.20.30.40:8000/run-17/?budget=2026")] [TestCase("http://[fd00::5]:8000/run-17/", "http://[fd00::6]:8000/run-17/")] [TestCase("http://[fd00::5]:8000/run-17/", "http://[fd00::5]:8001/run-17/")] public void AddressesAskingForSomethingElseDoNotCount(string inChat, string fromModel) { Assert.Multiple(() => { Assert.That(WebAddresses.TryCreateRequestKey(inChat, out var expected), Is.True); Assert.That(WebAddresses.TryCreateRequestKey(fromModel, out var actual), Is.True); Assert.That(actual, Is.Not.EqualTo(expected), "The difference reaches the server, and it may be a piece of a mail."); }); } [TestCase("mailto:someone@example.org")] [TestCase("file:///etc/passwd")] [TestCase("not an address")] [TestCase(null)] public void OnlyWebAddressesHaveARequestKey(string? address) => Assert.That(WebAddresses.TryCreateRequestKey(address, out _), Is.False); }