Skip to content

Commit f13807d

Browse files
authored
Merge pull request #126 from Sakeeb91/issue-125-latest-plugin-version
Allow plugin install to default to latest (#125)
2 parents 6563f0a + 37438da commit f13807d

7 files changed

Lines changed: 131 additions & 10 deletions

File tree

src/FlowCtl/Commands/Plugins/Install/InstallPluginCommand.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ public InstallPluginCommand() : base("install", Resources.Commands_Plugins_Insta
99
var typeOption = new Option<string>(new[] { "-t", "--type" },
1010
description: Resources.Commands_Plugins_Install_TypeOption) { IsRequired = true };
1111

12-
var versionOption = new Option<string>(new[] { "-v", "--version" },
13-
description: Resources.Commands_Plugins_VersionOption) { IsRequired = true };
12+
var versionOption = new Option<string?>(new[] { "-v", "--version" },
13+
description: Resources.Commands_Plugins_VersionOption);
1414

1515
var addressOption = new Option<string?>(new[] { "-a", "--address" },
1616
description: Resources.Commands_FlowSynxAddress);
@@ -19,4 +19,4 @@ public InstallPluginCommand() : base("install", Resources.Commands_Plugins_Insta
1919
AddOption(versionOption);
2020
AddOption(addressOption);
2121
}
22-
}
22+
}

src/FlowCtl/Commands/Plugins/Install/InstallPluginCommandOptions.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22

33
internal class InstallPluginCommandOptions : ICommandOptions
44
{
5+
/// <summary>
6+
/// FlowSynx plugin identifier (e.g. email-sender).
7+
/// </summary>
58
public required string Type { get; set; }
6-
public required string Version { get; set; }
9+
10+
/// <summary>
11+
/// Optional plugin version; defaults to the FlowSynx "latest" tag.
12+
/// </summary>
13+
public string? Version { get; set; }
14+
715
public string? Address { get; set; } = string.Empty;
8-
}
16+
}

src/FlowCtl/Commands/Plugins/Install/InstallPluginCommandOptionsHandler.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using FlowCtl.Core.Services.Authentication;
1+
using System.Globalization;
2+
using FlowCtl.Core.Services.Authentication;
23
using FlowCtl.Core.Services.Logger;
34
using FlowCtl.Extensions;
45
using FlowSynx.Client;
@@ -8,6 +9,8 @@ namespace FlowCtl.Commands.Plugins.Install;
89

910
internal class InstallPluginCommandOptionsHandler : ICommandOptionsHandler<InstallPluginCommandOptions>
1011
{
12+
private const string LatestPluginVersion = "latest"; // FlowSynx resolves this marker to the newest plugin release.
13+
1114
private readonly IFlowCtlLogger _flowCtlLogger;
1215
private readonly IFlowSynxClient _flowSynxClient;
1316
private readonly IAuthenticationManager _authenticationManager;
@@ -41,7 +44,14 @@ private async Task Execute(InstallPluginCommandOptions options, CancellationToke
4144
_flowSynxClient.SetConnection(connection);
4245
}
4346

44-
var request = new InstallPluginRequest { Type = options.Type, Version = options.Version };
47+
var resolvedVersion = ResolvePluginVersion(options.Version);
48+
if (string.IsNullOrWhiteSpace(options.Version))
49+
{
50+
_flowCtlLogger.Write(string.Format(CultureInfo.InvariantCulture,
51+
Resources.Commands_Plugins_Install_DefaultVersionInfo, resolvedVersion));
52+
}
53+
54+
var request = new InstallPluginRequest { Type = options.Type, Version = resolvedVersion };
4555
var result = await _flowSynxClient.Plugins.InstallAsync(request, cancellationToken);
4656

4757
if (result.StatusCode != 200)
@@ -58,4 +68,10 @@ private async Task Execute(InstallPluginCommandOptions options, CancellationToke
5868
_flowCtlLogger.WriteError(ex.Message);
5969
}
6070
}
61-
}
71+
72+
/// <summary>
73+
/// Normalizes the requested version by falling back to the FlowSynx "latest" tag.
74+
/// </summary>
75+
private static string ResolvePluginVersion(string? requestedVersion) =>
76+
string.IsNullOrWhiteSpace(requestedVersion) ? LatestPluginVersion : requestedVersion;
77+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("FlowCtl.UnitTests")]

src/FlowCtl/Resources.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/FlowCtl/Resources.resx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,9 @@
282282
<data name="Commands_Plugins_InstallDescription" xml:space="preserve">
283283
<value>Install plugin</value>
284284
</data>
285+
<data name="Commands_Plugins_Install_DefaultVersionInfo" xml:space="preserve">
286+
<value>No plugin version was specified; using '{0}'.</value>
287+
</data>
285288
<data name="Commands_Plugins_Install_TypeOption" xml:space="preserve">
286289
<value>Specifies the plugin type should be installed.</value>
287290
</data>
@@ -301,7 +304,7 @@
301304
<value>Update plugin</value>
302305
</data>
303306
<data name="Commands_Plugins_VersionOption" xml:space="preserve">
304-
<value>Specifies the plugin version should be installed.</value>
307+
<value>Specifies the plugin version to install. Defaults to 'latest' when omitted.</value>
305308
</data>
306309
<data name="Commands_RootDescription" xml:space="preserve">
307310
<value>flowctl controls the FlowSynx engine</value>
@@ -522,4 +525,4 @@
522525
<data name="LocationService_ErrorDuringGettingRootLocation" xml:space="preserve">
523526
<value>The base location could not be found.</value>
524527
</data>
525-
</root>
528+
</root>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using FlowCtl.Commands.Plugins.Install;
6+
using FlowCtl.Core.Services.Authentication;
7+
using FlowCtl.Core.Services.Logger;
8+
using FlowSynx.Client;
9+
using FlowSynx.Client.Authentication;
10+
using FlowSynx.Client.Messages.Requests.Plugins;
11+
using FlowSynx.Client.Messages.Responses;
12+
using FlowSynx.Client.Services;
13+
using Moq;
14+
15+
namespace FlowCtl.UnitTests.Commands.Plugins;
16+
17+
public class InstallPluginCommandOptionsHandlerTests
18+
{
19+
[Fact]
20+
public async Task HandleAsync_DefaultsToLatestVersion_WhenVersionIsMissing()
21+
{
22+
// Arrange
23+
var loggerMock = new Mock<IFlowCtlLogger>();
24+
var pluginsServiceMock = new Mock<IPluginsService>();
25+
var flowSynxClientMock = new Mock<IFlowSynxClient>();
26+
var authenticationManagerMock = CreateAuthenticationManagerMock();
27+
28+
flowSynxClientMock.Setup(client => client.Plugins)
29+
.Returns(pluginsServiceMock.Object);
30+
flowSynxClientMock.Setup(client => client.SetAuthenticationStrategy(It.IsAny<IAuthenticationStrategy>()));
31+
32+
pluginsServiceMock.Setup(service => service.InstallAsync(
33+
It.Is<InstallPluginRequest>(request => request.Version == "latest"),
34+
It.IsAny<CancellationToken>()))
35+
.ReturnsAsync(CreateSuccessfulInstallResult());
36+
37+
var handler = new InstallPluginCommandOptionsHandler(
38+
loggerMock.Object,
39+
flowSynxClientMock.Object,
40+
authenticationManagerMock.Object);
41+
42+
var options = new InstallPluginCommandOptions
43+
{
44+
Type = "email-sender",
45+
Version = null
46+
};
47+
48+
// Act
49+
await handler.HandleAsync(options, CancellationToken.None);
50+
51+
// Assert
52+
pluginsServiceMock.Verify(service => service.InstallAsync(
53+
It.Is<InstallPluginRequest>(request => request.Version == "latest"),
54+
It.IsAny<CancellationToken>()), Times.Once);
55+
56+
loggerMock.Verify(logger => logger.Write(
57+
It.Is<string>(message => message.Contains("latest", StringComparison.OrdinalIgnoreCase))),
58+
Times.Once);
59+
}
60+
61+
private static Mock<IAuthenticationManager> CreateAuthenticationManagerMock()
62+
{
63+
var mock = new Mock<IAuthenticationManager>();
64+
mock.SetupGet(manager => manager.IsLoggedIn).Returns(true);
65+
mock.SetupGet(manager => manager.IsBasicAuthenticationUsed).Returns(false);
66+
mock.Setup(manager => manager.GetData())
67+
.Returns(new AuthenticationData { AccessToken = "token" });
68+
return mock;
69+
}
70+
71+
private static HttpResult<Result<Unit>> CreateSuccessfulInstallResult() =>
72+
new()
73+
{
74+
StatusCode = 200,
75+
Payload = new Result<Unit>
76+
{
77+
Succeeded = true,
78+
Data = new Unit(),
79+
Messages = new List<string>()
80+
}
81+
};
82+
}

0 commit comments

Comments
 (0)