Files
git.stella-ops.org/src/AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.Analyzers.Tests/HttpClientUsageAnalyzerTests.cs

190 lines
6.9 KiB
C#

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 CodeFix_RewritesToFactoryCall()
{
const string source = """
using System.Net.Http;
namespace Sample.Service;
public sealed class Demo
{
public void Run()
{
var client = new HttpClient();
}
}
""";
const string expected = """
using System.Net.Http;
namespace Sample.Service;
public sealed class Demo
{
public void Run()
{
var client = global::StellaOps.AirGap.Policy.EgressHttpClientFactory.Create(egressPolicy: /* TODO: provide IEgressPolicy instance */, request: new global::StellaOps.AirGap.Policy.EgressRequest(component: "REPLACE_COMPONENT", destination: new global::System.Uri("https://replace-with-endpoint"), intent: "REPLACE_INTENT"));
}
}
""";
var updated = await ApplyCodeFixAsync(source, assemblyName: "Sample.Service");
Assert.Equal(expected.ReplaceLineEndings(), updated.ReplaceLineEndings());
}
private static async Task<ImmutableArray<Diagnostic>> 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<DiagnosticAnalyzer>(analyzer));
return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
}
private static async Task<string> ApplyCodeFixAsync(string source, string assemblyName)
{
using var workspace = new AdhocWorkspace();
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);
var stubDocumentId = DocumentId.CreateNewId(projectId);
var solution = workspace.CurrentSolution
.AddProject(projectId, "TestProject", "TestProject", LanguageNames.CSharp)
.WithProjectCompilationOptions(projectId, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.WithProjectAssemblyName(projectId, assemblyName)
.AddMetadataReferences(projectId, CreateMetadataReferences())
.AddDocument(documentId, "Test.cs", SourceText.From(source))
.AddDocument(stubDocumentId, "PolicyStubs.cs", SourceText.From(PolicyStubSource));
var project = solution.GetProject(projectId)!;
var document = solution.GetDocument(documentId)!;
var compilation = await project.GetCompilationAsync();
var analyzer = new HttpClientUsageAnalyzer();
var diagnostics = await compilation!.WithAnalyzers(ImmutableArray.Create<DiagnosticAnalyzer>(analyzer))
.GetAnalyzerDiagnosticsAsync();
var diagnostic = Assert.Single(diagnostics);
var codeFixProvider = new HttpClientUsageCodeFixProvider();
var actions = new List<CodeAction>();
var context = new CodeFixContext(
document,
diagnostic,
(action, _) => actions.Add(action),
CancellationToken.None);
await codeFixProvider.RegisterCodeFixesAsync(context);
var action = Assert.Single(actions);
var operations = await action.GetOperationsAsync(CancellationToken.None);
foreach (var operation in operations)
{
operation.Apply(workspace, CancellationToken.None);
}
var updatedDocument = workspace.CurrentSolution.GetDocument(documentId)!;
var updatedText = await updatedDocument.GetTextAsync();
return updatedText.ToString();
}
private static IEnumerable<MetadataReference> 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();
}
}
""";
}