using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Net.Http; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; using Xunit; using StellaOps.TestKit; namespace StellaOps.AirGap.Policy.Analyzers.Tests; public sealed class HttpClientUsageAnalyzerTests { [Trait("Category", TestCategories.Unit)] [Fact] public async Task ReportsDiagnostic_ForNewHttpClient() { const string source = """ using System.Net.Http; namespace Sample.App; public sealed class Demo { public void Run() { var client = new HttpClient(); } } """; var diagnostics = await AnalyzeAsync(source, assemblyName: "Sample.App"); Assert.Contains(diagnostics, d => d.Id == HttpClientUsageAnalyzer.DiagnosticId); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task DoesNotReportDiagnostic_InsidePolicyAssembly() { const string source = """ using System.Net.Http; namespace StellaOps.AirGap.Policy.Internal; internal static class Loopback { public static HttpClient Create() => new HttpClient(); } """; var diagnostics = await AnalyzeAsync(source, assemblyName: "StellaOps.AirGap.Policy"); Assert.DoesNotContain(diagnostics, d => d.Id == HttpClientUsageAnalyzer.DiagnosticId); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task DoesNotReportDiagnostic_ForTestingAssemblyNames() { const string source = """ using System.Net.Http; namespace Sample.App; public sealed class Demo { public void Run() { var client = new HttpClient(); } } """; var diagnostics = await AnalyzeAsync(source, assemblyName: "Sample.App.Testing"); Assert.DoesNotContain(diagnostics, d => d.Id == HttpClientUsageAnalyzer.DiagnosticId); } private static async Task> AnalyzeAsync(string source, string assemblyName) { var compilation = CSharpCompilation.Create( assemblyName, new[] { CSharpSyntaxTree.ParseText(source), CSharpSyntaxTree.ParseText(PolicyStubSource), }, CreateMetadataReferences(), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); var analyzer = new HttpClientUsageAnalyzer(); var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer)); return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(); } private static IEnumerable CreateMetadataReferences() { yield return MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location); yield return MetadataReference.CreateFromFile(typeof(Uri).GetTypeInfo().Assembly.Location); yield return MetadataReference.CreateFromFile(typeof(HttpClient).GetTypeInfo().Assembly.Location); yield return MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location); } private const string PolicyStubSource = """ namespace StellaOps.AirGap.Policy { public interface IEgressPolicy { void EnsureAllowed(EgressRequest request); } public readonly record struct EgressRequest(string Component, System.Uri Destination, string Intent); public static class EgressHttpClientFactory { public static System.Net.Http.HttpClient Create(IEgressPolicy egressPolicy, EgressRequest request) => throw new System.NotImplementedException(); public static System.Net.Http.HttpClient Create(IEgressPolicy egressPolicy, EgressRequest request, System.Func clientFactory) => throw new System.NotImplementedException(); } } """; }