using System.IO; using JobTrackerApi.Services; using JobTrackerApi.Tests.TestSupport; using Microsoft.AspNetCore.DataProtection; using Xunit; namespace JobTrackerApi.Tests; // Regression coverage for the SSRF guard in ImapService: an authenticated user's IMAP "connect" // target must not be usable to probe loopback/RFC1918/link-local/cloud-metadata addresses. public sealed class ImapServiceSsrfGuardTests { [Theory] [InlineData("127.0.0.1")] [InlineData("localhost")] [InlineData("10.0.0.5")] [InlineData("172.16.0.5")] [InlineData("192.168.1.5")] [InlineData("169.254.169.254")] // cloud metadata endpoint public async Task ConnectAsync_rejects_internal_and_metadata_hosts(string host) { var service = CreateService(); var ex = await Assert.ThrowsAsync(() => service.ConnectAsync("user-1", host, 993, true, "user", "password", CancellationToken.None)); // Message must not leak connect-vs-auth distinction (that's the oracle this guard closes). Assert.DoesNotContain("resolve", ex.Message, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("reachable", ex.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail() { var service = CreateService(); var ex = await Assert.ThrowsAsync(() => service.ConnectAsync("user-1", "this-host-does-not-exist.invalid", 993, true, "user", "password", CancellationToken.None)); Assert.Equal("Could not connect to that IMAP server with the given credentials. Check host, port, and password.", ex.Message); } private static ImapService CreateService() { var db = TestHostFactory.CreateInMemoryDb(); var protectionProvider = DataProtectionProvider.Create(new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"jobtracker-tests-{Guid.NewGuid():N}"))); return new ImapService(db, protectionProvider); } }