Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions src/Bicep.RpcClient.Tests/BicepClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,87 @@ public async Task Download_fetches_and_installs_bicep_cli(string name, Architect
installedContent.Should().BeEquivalentTo(randomBytes);
}

[TestMethod]
public async Task Download_uses_specified_BicepVersion_without_querying_latest()
{
var outputDir = FileHelper.GetUniqueTestOutputPath(TestContext);

MockHttpMessageHandler mockHandler = new();
// Any request other than the pinned-version artifact (e.g. releases/latest) should fail the test.
mockHandler.Fallback.Throw(new InvalidOperationException("Unexpected request - the latest version should not be queried when BicepVersion is set."));

var randomBytes = Guid.NewGuid().ToByteArray();
mockHandler.When(HttpMethod.Get, "https://downloads.bicep.azure.com/v9.8.7/bicep-linux-x64")
.Respond(_ => new(System.Net.HttpStatusCode.OK) { Content = new ByteArrayContent(randomBytes) });

var clientFactory = new BicepClientFactory(new(mockHandler));

var bicepCliPath = await clientFactory.Download(new()
{
InstallBasePath = outputDir,
OsPlatform = OSPlatform.Linux,
Architecture = Architecture.X64,
BicepVersion = "9.8.7",
}, TestContext.CancellationTokenSource.Token);

bicepCliPath.Should().Be(Path.Combine(outputDir, "v9.8.7", "bicep"));
(await File.ReadAllBytesAsync(bicepCliPath)).Should().BeEquivalentTo(randomBytes);
}

[TestMethod]
public async Task Download_skips_download_when_cli_is_already_installed()
{
var outputDir = FileHelper.GetUniqueTestOutputPath(TestContext);
var existingPath = Path.Combine(outputDir, "v9.8.7", "bicep");
Directory.CreateDirectory(Path.GetDirectoryName(existingPath)!);
await File.WriteAllTextAsync(existingPath, "already-installed");

MockHttpMessageHandler mockHandler = new();
// No download should occur, so any HTTP request fails the test.
mockHandler.Fallback.Throw(new InvalidOperationException("Unexpected request - the CLI is already installed."));

var clientFactory = new BicepClientFactory(new(mockHandler));

var bicepCliPath = await clientFactory.Download(new()
{
InstallBasePath = outputDir,
OsPlatform = OSPlatform.Linux,
Architecture = Architecture.X64,
BicepVersion = "9.8.7",
}, TestContext.CancellationTokenSource.Token);

bicepCliPath.Should().Be(existingPath);
(await File.ReadAllTextAsync(bicepCliPath)).Should().Be("already-installed");
}

[TestMethod]
public async Task Download_falls_back_to_obsolete_InstallPath_when_InstallBasePath_is_not_set()
{
var outputDir = FileHelper.GetUniqueTestOutputPath(TestContext);

MockHttpMessageHandler mockHandler = new();
var randomBytes = Guid.NewGuid().ToByteArray();
mockHandler.When(HttpMethod.Get, "https://downloads.bicep.azure.com/v9.8.7/bicep-linux-x64")
.Respond(_ => new(System.Net.HttpStatusCode.OK) { Content = new ByteArrayContent(randomBytes) });

var clientFactory = new BicepClientFactory(new(mockHandler));

#pragma warning disable CS0618 // Type or member is obsolete
var configuration = new BicepClientConfiguration
{
InstallPath = outputDir,
OsPlatform = OSPlatform.Linux,
Architecture = Architecture.X64,
BicepVersion = "9.8.7",
};
#pragma warning restore CS0618 // Type or member is obsolete

var bicepCliPath = await clientFactory.Download(configuration, TestContext.CancellationTokenSource.Token);

bicepCliPath.Should().Be(Path.Combine(outputDir, "v9.8.7", "bicep"));
(await File.ReadAllBytesAsync(bicepCliPath)).Should().BeEquivalentTo(randomBytes);
}

[TestMethod]
public async Task Initialize_validates_version_number_format()
{
Expand Down Expand Up @@ -345,4 +426,50 @@ public async Task GetMetadataResponse_runs_successfully()

result.Exports[0].Description.Should().Be("A foo object");
}

[TestMethod]
public async Task GetDeploymentGraph_runs_successfully()
{
var bicepFile = FileHelper.SaveResultFile(TestContext, "main.bicep", """
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-02-01' = {
name: 'myStgAct'
location: 'westus'
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}

resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2021-02-01' = {
parent: storageAccount
name: 'default'
}
""");

var result = await Bicep.GetDeploymentGraph(new(bicepFile));

result.Nodes.Should().Contain(node => node.Name == "storageAccount");
result.Nodes.Should().Contain(node => node.Name == "blobService");
result.Edges.Should().Contain(edge => edge.Source == "blobService" && edge.Target == "storageAccount");
}

[TestMethod]
public async Task GetFileReferences_runs_successfully()
{
var outputPath = FileHelper.SaveResultFiles(TestContext, [
new("main.bicep", """
module mod 'mod.bicep' = {
name: 'mod'
}
"""),
new("mod.bicep", """
param unused string
"""),
]);

var result = await Bicep.GetFileReferences(new(Path.Combine(outputPath, "main.bicep")));

result.FilePaths.Should().Contain(path => path.EndsWith("main.bicep"));
result.FilePaths.Should().Contain(path => path.EndsWith("mod.bicep"));
}
}
167 changes: 167 additions & 0 deletions src/Bicep.RpcClient.Tests/BicepClientUnitTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Concurrent;
using Bicep.RpcClient.JsonRpc;
using Bicep.RpcClient.Models;
using FluentAssertions;

namespace Bicep.RpcClient.Tests;

[TestClass]
public class BicepClientUnitTests
{
public TestContext TestContext { get; set; } = null!;

private CancellationToken Token => TestContext.CancellationTokenSource.Token;

[TestMethod]
public async Task GetVersion_caches_result_and_does_not_re_issue_request()
{
var rpc = new FakeJsonRpcClient();
rpc.SetResponse("bicep/version", new VersionResponse("1.2.3"));
using var client = new BicepClient(rpc);

(await client.GetVersion(Token)).Should().Be("1.2.3");
(await client.GetVersion(Token)).Should().Be("1.2.3");

rpc.CallCount("bicep/version").Should().Be(1);
}

[TestMethod]
public async Task Format_throws_when_cli_version_is_below_minimum()
{
var rpc = new FakeJsonRpcClient();
rpc.SetResponse("bicep/version", new VersionResponse("0.37.0"));
using var client = new BicepClient(rpc);

await FluentActions.Invoking(() => client.Format(new("main.bicep"), Token))
.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*requires Bicep CLI version '0.37.1' or later*0.37.0*");

rpc.CallCount("bicep/format").Should().Be(0);
}

[TestMethod]
public async Task Format_succeeds_when_cli_version_meets_minimum()
{
var rpc = new FakeJsonRpcClient();
rpc.SetResponse("bicep/version", new VersionResponse("0.37.1"));
rpc.SetResponse("bicep/format", new FormatResponse("formatted"));
using var client = new BicepClient(rpc);

var result = await client.Format(new("main.bicep"), Token);

result.Contents.Should().Be("formatted");
rpc.CallCount("bicep/format").Should().Be(1);
}

[TestMethod]
public async Task GetSnapshot_throws_when_cli_version_is_below_minimum()
{
var rpc = new FakeJsonRpcClient();
rpc.SetResponse("bicep/version", new VersionResponse("0.36.0"));
using var client = new BicepClient(rpc);

await FluentActions.Invoking(() => client.GetSnapshot(
new("main.bicepparam", new(null, null, null, null, null), null), Token))
.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*requires Bicep CLI version '0.36.1' or later*0.36.0*");

rpc.CallCount("bicep/getSnapshot").Should().Be(0);
}

[TestMethod]
public async Task GetSnapshot_succeeds_when_cli_version_meets_minimum()
{
var rpc = new FakeJsonRpcClient();
rpc.SetResponse("bicep/version", new VersionResponse("0.36.1"));
rpc.SetResponse("bicep/getSnapshot", new GetSnapshotResponse("snapshot-contents"));
using var client = new BicepClient(rpc);

var result = await client.GetSnapshot(
new("main.bicepparam", new(null, null, null, null, null), null), Token);

result.Snapshot.Should().Be("snapshot-contents");
rpc.CallCount("bicep/getSnapshot").Should().Be(1);
}

[TestMethod]
public async Task Compile_forwards_request_to_the_expected_method()
{
var rpc = new FakeJsonRpcClient();
rpc.SetResponse("bicep/compile", new CompileResponse(true, [], "{}"));
using var client = new BicepClient(rpc);

var result = await client.Compile(new("main.bicep"), Token);

result.Success.Should().BeTrue();
rpc.CallCount("bicep/compile").Should().Be(1);
}

[TestMethod]
public async Task GetDeploymentGraph_forwards_request_to_the_expected_method()
{
var rpc = new FakeJsonRpcClient();
rpc.SetResponse("bicep/getDeploymentGraph", new GetDeploymentGraphResponse([], []));
using var client = new BicepClient(rpc);

var result = await client.GetDeploymentGraph(new("main.bicep"), Token);

result.Nodes.Should().BeEmpty();
rpc.CallCount("bicep/getDeploymentGraph").Should().Be(1);
}

[TestMethod]
public async Task GetFileReferences_forwards_request_to_the_expected_method()
{
var rpc = new FakeJsonRpcClient();
rpc.SetResponse("bicep/getFileReferences", new GetFileReferencesResponse(["main.bicep"]));
using var client = new BicepClient(rpc);

var result = await client.GetFileReferences(new("main.bicep"), Token);

result.FilePaths.Should().Contain("main.bicep");
rpc.CallCount("bicep/getFileReferences").Should().Be(1);
}

[TestMethod]
public void Dispose_disposes_the_underlying_rpc_client()
{
var rpc = new FakeJsonRpcClient();
var client = new BicepClient(rpc);

client.Dispose();

rpc.IsDisposed.Should().BeTrue();
}

private sealed class FakeJsonRpcClient : IJsonRpcClient
{
private readonly ConcurrentDictionary<string, object> responsesByMethod = new();
private readonly ConcurrentDictionary<string, int> callCountsByMethod = new();

public bool IsDisposed { get; private set; }

public void SetResponse<TResponse>(string method, TResponse response)
=> responsesByMethod[method] = response!;

public int CallCount(string method) => callCountsByMethod.TryGetValue(method, out var count) ? count : 0;

public Task<TResponse> SendRequest<TRequest, TResponse>(string method, TRequest request, CancellationToken cancellationToken)
{
callCountsByMethod.AddOrUpdate(method, 1, (_, count) => count + 1);

if (!responsesByMethod.TryGetValue(method, out var response))
{
throw new InvalidOperationException($"No response configured for method '{method}'.");
}

return Task.FromResult((TResponse)response);
}

public Task Listen(Action onComplete, CancellationToken cancellationToken) => Task.CompletedTask;

public void Dispose() => IsDisposed = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ namespace Bicep.RpcClient
"ad.")]
System.Threading.Tasks.Task<Bicep.RpcClient.IBicepClient> InitializeFromPath(string bicepCliPath, System.Threading.CancellationToken cancellationToken = default);
}
public class PooledBicepClientFactory : Bicep.RpcClient.IBicepClientFactory, System.IDisposable
{
public PooledBicepClientFactory(System.Net.Http.HttpClient? httpClient = null, System.TimeSpan? inactivityInterval = default) { }
public void Dispose() { }
[System.Obsolete("Use Initialize instead.")]
public System.Threading.Tasks.Task<Bicep.RpcClient.IBicepClient> DownloadAndInitialize(Bicep.RpcClient.BicepClientConfiguration configuration, System.Threading.CancellationToken cancellationToken) { }
public System.Threading.Tasks.Task<Bicep.RpcClient.IBicepClient> Initialize(Bicep.RpcClient.BicepClientConfiguration configuration, System.Threading.CancellationToken cancellationToken) { }
[System.Obsolete("Use Initialize with a BicepClientConfiguration that has ExistingCliPath set inste" +
"ad.")]
public System.Threading.Tasks.Task<Bicep.RpcClient.IBicepClient> InitializeFromPath(string bicepCliPath, System.Threading.CancellationToken cancellationToken) { }
}
}
namespace Bicep.RpcClient.Models
{
Expand Down
Loading
Loading