Skip to content

Commit 20b991e

Browse files
RpcClient: Various improvements for safety + ux (#19203)
* Add file system locking for installing Bicep CLI (based on module registry restore logic) * Provide simplified interface for calling BicepClientFactory * Support configurable timeout for named pipe connection * Support stdout as an alternative transport to named pipes
1 parent 0b71d7a commit 20b991e

File tree

9 files changed

+660
-73
lines changed

9 files changed

+660
-73
lines changed

src/Bicep.RpcClient.Tests/BicepClientTests.cs

Lines changed: 132 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.IO.Pipes;
45
using System.Runtime.InteropServices;
56
using System.Threading.Tasks;
67
using Bicep.Core.FileSystem;
@@ -22,12 +23,12 @@ public class BicepClientTests
2223
[TestInitialize]
2324
public async Task TestInitialize()
2425
{
25-
var clientFactory = new BicepClientFactory(new());
26+
var clientFactory = new BicepClientFactory();
2627
var cliName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "bicep.exe" : "bicep";
2728
var cliPath = Path.GetFullPath(Path.Combine(typeof(BicepClientTests).Assembly.Location, $"../{cliName}"));
2829

29-
Bicep = await clientFactory.InitializeFromPath(
30-
cliPath,
30+
Bicep = await clientFactory.Initialize(
31+
new() { ExistingCliPath = cliPath },
3132
TestContext.CancellationTokenSource.Token);
3233
}
3334

@@ -82,7 +83,7 @@ public async Task Download_fetches_and_installs_bicep_cli(string name, Architect
8283

8384
var bicepCliPath = await clientFactory.Download(new()
8485
{
85-
InstallPath = outputDir,
86+
InstallBasePath = outputDir,
8687
OsPlatform = osPlatform,
8788
Architecture = architecture,
8889
}, TestContext.CancellationTokenSource.Token);
@@ -93,22 +94,121 @@ public async Task Download_fetches_and_installs_bicep_cli(string name, Architect
9394
}
9495

9596
[TestMethod]
96-
public async Task DownloadAndInitialize_validates_version_number_format()
97+
public async Task Initialize_validates_version_number_format()
9798
{
98-
var clientFactory = new BicepClientFactory(new());
99-
await FluentActions.Invoking(() => clientFactory.DownloadAndInitialize(new() { BicepVersion = "v0.1.1" }, default))
99+
var clientFactory = new BicepClientFactory();
100+
await FluentActions.Invoking(() => clientFactory.Initialize(new() { BicepVersion = "v0.1.1" }, default))
100101
.Should().ThrowAsync<ArgumentException>().WithMessage("Invalid Bicep version format 'v0.1.1'. Expected format: 'x.y.z' where x, y, and z are integers.");
101102
}
102103

104+
[DataTestMethod]
105+
[DataRow("1.2", "Invalid Bicep version format '1.2'. Expected format: 'x.y.z' where x, y, and z are integers.")]
106+
[DataRow("v1.2.3", "Invalid Bicep version format 'v1.2.3'. Expected format: 'x.y.z' where x, y, and z are integers.")]
107+
[DataRow("1.2.3.4", "Invalid Bicep version format '1.2.3.4'. Expected format: 'x.y.z' where x, y, and z are integers.")]
108+
[DataRow("latest", "Invalid Bicep version format 'latest'. Expected format: 'x.y.z' where x, y, and z are integers.")]
109+
public void Validate_throws_for_invalid_BicepVersion(string version, string expectedMessage)
110+
{
111+
FluentActions.Invoking(() => BicepClientConfiguration.Validate(new() { BicepVersion = version }))
112+
.Should().Throw<ArgumentException>().WithMessage(expectedMessage);
113+
}
114+
115+
[DataTestMethod]
116+
[DataRow("1.2.3")]
117+
[DataRow("0.0.0")]
118+
[DataRow("100.200.300")]
119+
public void Validate_accepts_valid_BicepVersion(string version)
120+
{
121+
FluentActions.Invoking(() => BicepClientConfiguration.Validate(new() { BicepVersion = version }))
122+
.Should().NotThrow();
123+
}
124+
125+
[TestMethod]
126+
public void Validate_throws_when_ExistingCliPath_combined_with_InstallBasePath()
127+
{
128+
FluentActions.Invoking(() => BicepClientConfiguration.Validate(new() { ExistingCliPath = "/some/path", InstallBasePath = "/some/base" }))
129+
.Should().Throw<ArgumentException>().WithMessage("*ExistingCliPath*InstallBasePath*");
130+
}
131+
132+
[TestMethod]
133+
public void Validate_throws_when_ExistingCliPath_combined_with_BicepVersion()
134+
{
135+
FluentActions.Invoking(() => BicepClientConfiguration.Validate(new() { ExistingCliPath = "/some/path", BicepVersion = "1.0.0" }))
136+
.Should().Throw<ArgumentException>().WithMessage("*ExistingCliPath*BicepVersion*");
137+
}
138+
139+
[TestMethod]
140+
public void Validate_throws_when_ExistingCliPath_combined_with_OsPlatform()
141+
{
142+
FluentActions.Invoking(() => BicepClientConfiguration.Validate(new() { ExistingCliPath = "/some/path", OsPlatform = OSPlatform.Linux }))
143+
.Should().Throw<ArgumentException>().WithMessage("*ExistingCliPath*OsPlatform*");
144+
}
145+
146+
[TestMethod]
147+
public void Validate_throws_when_ExistingCliPath_combined_with_Architecture()
148+
{
149+
FluentActions.Invoking(() => BicepClientConfiguration.Validate(new() { ExistingCliPath = "/some/path", Architecture = Architecture.X64 }))
150+
.Should().Throw<ArgumentException>().WithMessage("*ExistingCliPath*Architecture*");
151+
}
152+
153+
[TestMethod]
154+
public void Validate_throws_when_Stdio_combined_with_ConnectionTimeout()
155+
{
156+
FluentActions.Invoking(() => BicepClientConfiguration.Validate(new() { ConnectionMode = BicepConnectionMode.Stdio, ConnectionTimeout = TimeSpan.FromSeconds(10) }))
157+
.Should().Throw<ArgumentException>().WithMessage("*ConnectionTimeout*Stdio*");
158+
}
159+
160+
[TestMethod]
161+
public void Validate_accepts_Stdio_without_ConnectionTimeout()
162+
{
163+
FluentActions.Invoking(() => BicepClientConfiguration.Validate(new() { ConnectionMode = BicepConnectionMode.Stdio }))
164+
.Should().NotThrow();
165+
}
166+
167+
[TestMethod]
168+
public void Validate_accepts_Stdio_with_ExistingCliPath()
169+
{
170+
var existingPath = typeof(BicepClientTests).Assembly.Location;
171+
FluentActions.Invoking(() => BicepClientConfiguration.Validate(new() { ConnectionMode = BicepConnectionMode.Stdio, ExistingCliPath = existingPath }))
172+
.Should().NotThrow();
173+
}
174+
103175
[TestMethod]
104-
public async Task DownloadAndInitialize_validates_path_existence()
176+
public async Task Initialize_validates_path_existence()
105177
{
106178
var nonExistentPath = FileHelper.GetUniqueTestOutputPath(TestContext);
107-
var clientFactory = new BicepClientFactory(new());
108-
await FluentActions.Invoking(() => clientFactory.InitializeFromPath(nonExistentPath, default))
179+
var clientFactory = new BicepClientFactory();
180+
await FluentActions.Invoking(() => clientFactory.Initialize(new() { ExistingCliPath = nonExistentPath }, default))
109181
.Should().ThrowAsync<FileNotFoundException>().WithMessage($"The specified Bicep CLI path does not exist: '{nonExistentPath}'.");
110182
}
111183

184+
[TestMethod]
185+
public void Validate_throws_when_ExistingCliPath_does_not_exist()
186+
{
187+
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
188+
FluentActions.Invoking(() => BicepClientConfiguration.Validate(new() { ExistingCliPath = nonExistentPath }))
189+
.Should().Throw<FileNotFoundException>().WithMessage($"The specified Bicep CLI path does not exist: '{nonExistentPath}'.");
190+
}
191+
192+
[TestMethod]
193+
public async Task Initialize_throws_NotSupportedException_for_unsupported_ConnectionMode()
194+
{
195+
var existingPath = typeof(BicepClientTests).Assembly.Location;
196+
var clientFactory = new BicepClientFactory();
197+
// Cast an unknown value to exercise the default branch in the factory's switch expression.
198+
var unknownMode = (BicepConnectionMode)99;
199+
await FluentActions.Invoking(() => clientFactory.Initialize(new() { ExistingCliPath = existingPath, ConnectionMode = unknownMode }, default))
200+
.Should().ThrowAsync<NotSupportedException>();
201+
}
202+
203+
[TestMethod]
204+
public async Task WaitForPipeConnection_throws_timeout_exception_when_connection_times_out()
205+
{
206+
using var pipeStream = new NamedPipeServerStream(Guid.NewGuid().ToString(), PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
207+
208+
await FluentActions.Invoking(() => BicepClient.WaitForPipeConnection(pipeStream, TimeSpan.FromMilliseconds(500), CancellationToken.None))
209+
.Should().ThrowAsync<TimeoutException>().WithMessage("Timed out waiting for the Bicep CLI process to connect after * seconds.");
210+
}
211+
112212
[TestMethod]
113213
public void BuildDownloadUrlForTag_returns_correct_url()
114214
{
@@ -138,6 +238,28 @@ param location string
138238
result.Diagnostics.Should().Contain(x => x.Code == "no-unused-params");
139239
}
140240

241+
[TestMethod]
242+
public async Task Compile_runs_successfully_with_stdio()
243+
{
244+
var clientFactory = new BicepClientFactory();
245+
var cliName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "bicep.exe" : "bicep";
246+
var cliPath = Path.GetFullPath(Path.Combine(typeof(BicepClientTests).Assembly.Location, $"../{cliName}"));
247+
248+
using var bicep = await clientFactory.Initialize(
249+
new() { ExistingCliPath = cliPath, ConnectionMode = BicepConnectionMode.Stdio },
250+
TestContext.CancellationTokenSource.Token);
251+
252+
var bicepFile = FileHelper.SaveResultFile(TestContext, "main.bicep", """
253+
param location string
254+
""");
255+
256+
var result = await bicep.Compile(new(bicepFile));
257+
258+
result.Success.Should().BeTrue();
259+
result.Contents.Should().NotBeNullOrEmpty();
260+
result.Diagnostics.Should().Contain(x => x.Code == "no-unused-params");
261+
}
262+
141263
[TestMethod]
142264
public async Task CompileParams_runs_successfully()
143265
{

0 commit comments

Comments
 (0)