diff --git a/tests/Promote.NuGet.Tests/Promote.NuGet.Tests.csproj b/tests/Promote.NuGet.Tests/Promote.NuGet.Tests.csproj
index aa3e26f..6698a6a 100644
--- a/tests/Promote.NuGet.Tests/Promote.NuGet.Tests.csproj
+++ b/tests/Promote.NuGet.Tests/Promote.NuGet.Tests.csproj
@@ -1,5 +1,6 @@
+
\ No newline at end of file
diff --git a/tests/Promote.NuGet.Tests/PromoteNugetProcessRunner.cs b/tests/Promote.NuGet.Tests/PromoteNugetProcessRunner.cs
new file mode 100644
index 0000000..ea1255f
--- /dev/null
+++ b/tests/Promote.NuGet.Tests/PromoteNugetProcessRunner.cs
@@ -0,0 +1,125 @@
+using System.Diagnostics;
+using System.IO;
+
+namespace Promote.NuGet.Tests;
+
+public static class PromoteNugetProcessRunner
+{
+ public sealed class ProcessWrapper : IAsyncDisposable
+ {
+ public Process Process { get; }
+
+ public ProcessWrapper(Process process)
+ {
+ Process = process;
+ }
+
+ public void WaitForExit()
+ {
+ Process.WaitForExit();
+ }
+
+ public bool WaitForExit(int milliseconds)
+ {
+ return Process.WaitForExit(milliseconds);
+ }
+
+ public Task WaitForExitAsync(CancellationToken cancellationToken = default)
+ {
+ return Process.WaitForExitAsync(cancellationToken);
+ }
+
+ public int ExitCode => Process.ExitCode;
+
+ public StreamReader StandardError => Process.StandardError;
+
+ public StreamReader StandardOutput => Process.StandardOutput;
+
+ public async ValueTask DisposeAsync()
+ {
+ if (Process.HasExited == false)
+ {
+ TestContext.WriteLine("The process is still running. Dumping its output and killing the process.");
+ TestContext.WriteLine("Error output:");
+ TestContext.WriteLine(await Process.StandardError.ReadToEndAsync());
+ TestContext.WriteLine("Standard output:");
+ TestContext.WriteLine(await Process.StandardOutput.ReadToEndAsync());
+
+ TestContext.WriteLine("Killing...");
+ Process.Kill();
+ await Process.WaitForExitAsync();
+
+ TestContext.WriteLine("The process is stopped.");
+ }
+
+ Process.Dispose();
+ }
+ }
+
+ public record ProcessRunResult(int ExitCode, IReadOnlyCollection StdOutput, IReadOnlyCollection StdError);
+
+ public static async Task RunForResultAsync(params string[] arguments)
+ {
+ await using var process = await RunToExitAsync(arguments);
+
+ var stdOutput = new List();
+ while (await process.StandardOutput.ReadLineAsync() is { } line)
+ {
+ stdOutput.Add(line);
+ }
+
+ var stdError = new List();
+ while (await process.StandardError.ReadLineAsync() is { } line)
+ {
+ stdError.Add(line);
+ }
+
+ return new ProcessRunResult(process.ExitCode, stdOutput, stdError);
+ }
+
+ public static async Task RunToExitAsync(params string[] arguments)
+ {
+ var cancellationToken = TestContext.CurrentContext.CancellationToken;
+
+ var process = Run(arguments);
+
+ await process.WaitForExitAsync(cancellationToken);
+
+ return process;
+ }
+
+ public static ProcessWrapper Run(params string[] arguments)
+ {
+ var processStartInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ ArgumentList = { "Promote.NuGet.dll" },
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ };
+
+ foreach (var argument in arguments)
+ {
+ processStartInfo.ArgumentList.Add(argument);
+ }
+
+ Process? process = null;
+ try
+ {
+ process = Process.Start(processStartInfo);
+
+ if (process == null)
+ {
+ Assert.Fail("Failed to run the process");
+ Environment.FailFast("UNREACHABLE");
+ }
+
+ return new ProcessWrapper(process);
+ }
+ catch
+ {
+ process?.Dispose();
+ throw;
+ }
+ }
+}
diff --git a/tests/Promote.NuGet.Tests/PromoteSinglePackageTests.cs b/tests/Promote.NuGet.Tests/PromoteSinglePackageTests.cs
new file mode 100644
index 0000000..e386245
--- /dev/null
+++ b/tests/Promote.NuGet.Tests/PromoteSinglePackageTests.cs
@@ -0,0 +1,66 @@
+using NuGet.Protocol.Core.Types;
+using NuGet.Versioning;
+using Promote.NuGet.Feeds;
+using Promote.NuGet.TestInfrastructure;
+
+namespace Promote.NuGet.Tests;
+
+[TestFixture]
+public class PromoteSinglePackageTests
+{
+ [Test, CancelAfter(30_000)]
+ public async Task Promotes_a_package_with_its_dependencies_to_destination_feed()
+ {
+ await using var destinationFeed = await LocalNugetFeed.Create();
+
+ // Act
+ var result = await PromoteNugetProcessRunner.RunForResultAsync(
+ "promote",
+ "package",
+ "System.Runtime",
+ "--version", "4.3.0",
+ "--destination", destinationFeed.FeedUrl,
+ "--destination-api-key", destinationFeed.ApiKey
+ );
+
+ var destinationFeedDescriptor = new NuGetRepositoryDescriptor(destinationFeed.FeedUrl, destinationFeed.ApiKey);
+ var destinationRepo = new NuGetRepository(destinationFeedDescriptor, NullSourceCacheContext.Instance, TestNuGetLogger.Instance);
+
+ // Assert
+ result.StdOutput.Should().StartWith(
+ new[]
+ {
+ "Resolving packages to promote:",
+ "└── System.Runtime 4.3.0"
+ }
+ );
+
+ result.StdOutput.Should().ContainInConsecutiveOrder(
+ "Found 3 package(s) to promote:",
+ "├── Microsoft.NETCore.Platforms 1.1.0",
+ "├── Microsoft.NETCore.Targets 1.1.0",
+ "└── System.Runtime 4.3.0"
+ );
+
+ result.StdOutput.Should().ContainInOrder(
+ "(1/3) Promote Microsoft.NETCore.Platforms 1.1.0",
+ "(2/3) Promote Microsoft.NETCore.Targets 1.1.0",
+ "(3/3) Promote System.Runtime 4.3.0",
+ "3 package(s) promoted."
+ );
+
+ result.StdError.Should().BeEmpty();
+ result.ExitCode.Should().Be(0);
+
+ await AssertContainsVersions(destinationRepo, "System.Runtime", new[] { new NuGetVersion(4, 3, 0) });
+ await AssertContainsVersions(destinationRepo, "Microsoft.NETCore.Platforms", new[] { new NuGetVersion(1, 1, 0) });
+ await AssertContainsVersions(destinationRepo, "Microsoft.NETCore.Targets", new[] { new NuGetVersion(1, 1, 0) });
+ }
+
+ private static async Task AssertContainsVersions(INuGetRepository repo, string packageId, params NuGetVersion[] expectedVersions)
+ {
+ var netCorePlatformsPackages = await repo.Packages.GetAllVersions(packageId);
+ netCorePlatformsPackages.IsSuccess.Should().BeTrue();
+ netCorePlatformsPackages.Value.Should().Contain(expectedVersions);
+ }
+}
diff --git a/tests/Promote.NuGet.Tests/ToolVersionTests.cs b/tests/Promote.NuGet.Tests/ToolVersionTests.cs
new file mode 100644
index 0000000..edc462e
--- /dev/null
+++ b/tests/Promote.NuGet.Tests/ToolVersionTests.cs
@@ -0,0 +1,22 @@
+using System.Diagnostics;
+
+namespace Promote.NuGet.Tests;
+
+[TestFixture]
+public class ToolVersionTests
+{
+ [Test, CancelAfter(10_000)]
+ public async Task Returns_version_of_the_tool()
+ {
+ var expectedVersion = FileVersionInfo.GetVersionInfo(typeof(Program).Assembly.Location).ProductVersion ?? string.Empty;
+ var expectedVersionLines = expectedVersion.Chunk(80).Select(x => new string(x)).ToList();
+
+ // Act
+ var result = await PromoteNugetProcessRunner.RunForResultAsync("--version");
+
+ // Assert
+ result.StdOutput.Should().BeEquivalentTo(expectedVersionLines);
+ result.StdError.Should().BeEmpty();
+ result.ExitCode.Should().Be(0);
+ }
+}