From f35c60def74e0253f7dd0cb8283774d8cde0287c Mon Sep 17 00:00:00 2001 From: byCrookie Date: Sat, 28 Oct 2023 19:29:32 +0200 Subject: [PATCH] add pipeline tests --- .../Pipeline/DeviceFlowAuthPipelineTests.cs | 144 ++++++++++++++++++ .../Auth/Pipeline/PersistedPipelineTests.cs | 125 +++++++++++++++ .../TokenFromConfigurationPipelineTests.cs | 126 +++++++++++++++ .../Auth/Pipeline/DeviceFlowAuthPipeline.cs | 23 ++- .../Auth/Pipeline/LoginPipelineBuilder.cs | 4 +- .../Github/Auth/Pipeline/PersistedPipeline.cs | 6 +- .../TokenFromConfigurationPipeline.cs | 7 +- 7 files changed, 422 insertions(+), 13 deletions(-) create mode 100644 GithubBackup/GithubBackup.Cli.Tests/Commands/Github/Auth/Pipeline/DeviceFlowAuthPipelineTests.cs create mode 100644 GithubBackup/GithubBackup.Cli.Tests/Commands/Github/Auth/Pipeline/PersistedPipelineTests.cs create mode 100644 GithubBackup/GithubBackup.Cli.Tests/Commands/Github/Auth/Pipeline/TokenFromConfigurationPipelineTests.cs diff --git a/GithubBackup/GithubBackup.Cli.Tests/Commands/Github/Auth/Pipeline/DeviceFlowAuthPipelineTests.cs b/GithubBackup/GithubBackup.Cli.Tests/Commands/Github/Auth/Pipeline/DeviceFlowAuthPipelineTests.cs new file mode 100644 index 0000000..5e9e016 --- /dev/null +++ b/GithubBackup/GithubBackup.Cli.Tests/Commands/Github/Auth/Pipeline/DeviceFlowAuthPipelineTests.cs @@ -0,0 +1,144 @@ +using FluentAssertions; +using GithubBackup.Cli.Commands.Github.Auth; +using GithubBackup.Cli.Commands.Github.Auth.Pipeline; +using GithubBackup.Cli.Commands.Github.Login; +using GithubBackup.Cli.Commands.Global; +using GithubBackup.Core.Github.Authentication; +using GithubBackup.Core.Github.Credentials; +using GithubBackup.Core.Github.Users; +using GithubBackup.TestUtils.Logging; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Spectre.Console.Testing; + +namespace GithubBackup.Cli.Tests.Commands.Github.Auth.Pipeline; + +public class DeviceFlowAuthPipelineTests +{ + private readonly DeviceFlowAuthPipeline _sut; + + private readonly ILoginPipeline _next; + + private readonly ILogger _logger; + private readonly IGithubTokenStore _githubTokenStore; + private readonly IPersistentCredentialStore _persistentCredentialStore; + private readonly IUserService _userService; + private readonly IAuthenticationService _authenticationService; + private readonly TestConsole _ansiConsole; + + public DeviceFlowAuthPipelineTests() + { + _logger = Substitute.For>(); + _githubTokenStore = Substitute.For(); + _persistentCredentialStore = Substitute.For(); + _userService = Substitute.For(); + _authenticationService = Substitute.For(); + _ansiConsole = new TestConsole(); + + _sut = new DeviceFlowAuthPipeline( + _logger, + _githubTokenStore, + _persistentCredentialStore, + _userService, + _authenticationService, + _ansiConsole + ); + + _next = Substitute.For(); + _sut.Next = _next; + } + + [Fact] + public async Task LoginAsync_NotResponsible_CallNext() + { + var ct = CancellationToken.None; + var globalArgs = new GlobalArgs(LogLevel.Debug, false, new FileInfo("Test")); + var loginArgs = new LoginArgs(null, false); + + await _sut.LoginAsync(globalArgs, loginArgs, true, ct); + + await _next.Received(1).LoginAsync(globalArgs, loginArgs, true, ct); + + _logger.VerifyLogs(); + } + + [Fact] + public async Task LoginAsync_ValidAndPersist_PersistTokenAndReturnUser() + { + const string token = "token"; + var ct = CancellationToken.None; + var globalArgs = new GlobalArgs(LogLevel.Debug, false, new FileInfo("Test")); + var loginArgs = new LoginArgs(null, true); + var user = new User("test", "test"); + + var deviceAndUserCodes = new DeviceAndUserCodes("device", "user", "url", 1, 1); + _authenticationService.RequestDeviceAndUserCodesAsync(ct).Returns(deviceAndUserCodes); + var accessToken = new AccessToken(token, "type", "scope"); + _authenticationService.PollForAccessTokenAsync(deviceAndUserCodes.DeviceCode, deviceAndUserCodes.Interval, ct) + .Returns(accessToken); + _userService.WhoAmIAsync(ct).Returns(user); + + await _sut.LoginAsync(globalArgs, loginArgs, true, ct); + + await _githubTokenStore.Received(1).SetAsync(token); + await _persistentCredentialStore.Received(1).StoreTokenAsync(token, ct); + await _next.Received(0).LoginAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + + _logger.VerifyLogs(new LogEntry(LogLevel.Information, "Using device flow authentication")); + } + + [Fact] + public async Task LoginAsync_ValidAndNotPersist_DoNotPersistTokenAndReturnUser() + { + const string token = "token"; + var ct = CancellationToken.None; + var globalArgs = new GlobalArgs(LogLevel.Debug, false, new FileInfo("Test")); + var loginArgs = new LoginArgs(null, true); + var user = new User("test", "test"); + + var deviceAndUserCodes = new DeviceAndUserCodes("device", "user", "url", 1, 1); + _authenticationService.RequestDeviceAndUserCodesAsync(ct).Returns(deviceAndUserCodes); + var accessToken = new AccessToken(token, "type", "scope"); + _authenticationService.PollForAccessTokenAsync(deviceAndUserCodes.DeviceCode, deviceAndUserCodes.Interval, ct) + .Returns(accessToken); + _userService.WhoAmIAsync(ct).Returns(user); + + await _sut.LoginAsync(globalArgs, loginArgs, false, ct); + + await _githubTokenStore.Received(1).SetAsync(token); + await _persistentCredentialStore.Received(0).StoreTokenAsync(Arg.Any(), Arg.Any()); + await _next.Received(0).LoginAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + + _logger.VerifyLogs(new LogEntry(LogLevel.Information, "Using device flow authentication")); + } + + + [Fact] + public async Task LoginAsync_Invalid_ThrowException() + { + const string token = "token"; + var ct = CancellationToken.None; + var globalArgs = new GlobalArgs(LogLevel.Debug, false, new FileInfo("Test")); + var loginArgs = new LoginArgs(null, true); + + var deviceAndUserCodes = new DeviceAndUserCodes("device", "user", "url", 1, 1); + _authenticationService.RequestDeviceAndUserCodesAsync(ct).Returns(deviceAndUserCodes); + var accessToken = new AccessToken(token, "type", "scope"); + _authenticationService.PollForAccessTokenAsync(deviceAndUserCodes.DeviceCode, deviceAndUserCodes.Interval, ct) + .Returns(accessToken); + _userService.WhoAmIAsync(ct).ThrowsAsync(); + + var action = () => _sut.LoginAsync(globalArgs, loginArgs, true, ct); + + await action.Should().ThrowAsync(); + + await _githubTokenStore.Received(0).SetAsync(token); + await _persistentCredentialStore.Received(0).StoreTokenAsync(Arg.Any(), Arg.Any()); + + _logger.VerifyLogs( + new LogEntry(LogLevel.Information, "Using device flow authentication"), + new LogEntry(LogLevel.Error, """Token \(null\) is invalid""") + ); + } +} \ No newline at end of file diff --git a/GithubBackup/GithubBackup.Cli.Tests/Commands/Github/Auth/Pipeline/PersistedPipelineTests.cs b/GithubBackup/GithubBackup.Cli.Tests/Commands/Github/Auth/Pipeline/PersistedPipelineTests.cs new file mode 100644 index 0000000..f13edf3 --- /dev/null +++ b/GithubBackup/GithubBackup.Cli.Tests/Commands/Github/Auth/Pipeline/PersistedPipelineTests.cs @@ -0,0 +1,125 @@ +using FluentAssertions; +using GithubBackup.Cli.Commands.Github.Auth; +using GithubBackup.Cli.Commands.Github.Auth.Pipeline; +using GithubBackup.Cli.Commands.Github.Login; +using GithubBackup.Cli.Commands.Global; +using GithubBackup.Core.Github.Credentials; +using GithubBackup.Core.Github.Users; +using GithubBackup.TestUtils.Logging; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; + +namespace GithubBackup.Cli.Tests.Commands.Github.Auth.Pipeline; + +public class PersistedPipelineTests +{ + private readonly PersistedPipeline _sut; + + private readonly ILogger _logger; + private readonly IGithubTokenStore _githubTokenStore; + private readonly IPersistentCredentialStore _persistentCredentialStore; + private readonly IUserService _userService; + + private readonly ILoginPipeline _next; + + public PersistedPipelineTests() + { + _logger = Substitute.For>(); + _githubTokenStore = Substitute.For(); + _persistentCredentialStore = Substitute.For(); + _userService = Substitute.For(); + + _sut = new PersistedPipeline( + _logger, + _persistentCredentialStore, + _githubTokenStore, + _userService + ); + + _next = Substitute.For(); + _sut.Next = _next; + } + + [Fact] + public async Task LoginAsync_NotResponsible_CallNext() + { + var ct = CancellationToken.None; + var globalArgs = new GlobalArgs(LogLevel.Debug, false, new FileInfo("Test")); + var loginArgs = new LoginArgs("token", true); + + await _sut.LoginAsync(globalArgs, loginArgs, true, ct); + + await _next.Received(1).LoginAsync(globalArgs, loginArgs, true, ct); + + _logger.VerifyLogs(); + } + + [Fact] + public async Task LoginAsync_NoToken_CallNext() + { + const string? token = null; + var ct = CancellationToken.None; + var globalArgs = new GlobalArgs(LogLevel.Debug, false, new FileInfo("Test")); + var loginArgs = new LoginArgs(null, false); + + _persistentCredentialStore.LoadTokenAsync(ct).Returns(token); + + await _sut.LoginAsync(globalArgs, loginArgs, true, ct); + + await _githubTokenStore.Received(0).SetAsync(Arg.Any()); + await _persistentCredentialStore.Received(0).StoreTokenAsync(Arg.Any(), Arg.Any()); + + await _next.Received(1).LoginAsync(globalArgs, loginArgs, true, ct); + + _logger.VerifyLogs( + new LogEntry(LogLevel.Information, "Using token from persistent store"), + new LogEntry(LogLevel.Information, "Persistent token not found") + ); + } + + [Fact] + public async Task LoginAsync_TokenNotValid_CallNext() + { + const string token = "token"; + var ct = CancellationToken.None; + var globalArgs = new GlobalArgs(LogLevel.Debug, false, new FileInfo("Test")); + var loginArgs = new LoginArgs(null, false); + + _persistentCredentialStore.LoadTokenAsync(ct).Returns(token); + _userService.WhoAmIAsync(ct).ThrowsAsync(new Exception("test")); + + await _sut.LoginAsync(globalArgs, loginArgs, true, ct); + + await _githubTokenStore.Received(0).SetAsync(Arg.Any()); + await _persistentCredentialStore.Received(0).StoreTokenAsync(Arg.Any(), Arg.Any()); + + await _next.Received(1).LoginAsync(globalArgs, loginArgs, true, ct); + + _logger.VerifyLogs( + new LogEntry(LogLevel.Information, "Using token from persistent store"), + new LogEntry(LogLevel.Information, "Persistent token is invalid: test") + ); + } + + [Fact] + public async Task LoginAsync_ValidToken_ReturnUser() + { + const string token = "token"; + var ct = CancellationToken.None; + var globalArgs = new GlobalArgs(LogLevel.Debug, false, new FileInfo("Test")); + var loginArgs = new LoginArgs(null, false); + var user = new User("test", "test"); + + _userService.WhoAmIAsync(ct).Returns(user); + _persistentCredentialStore.LoadTokenAsync(ct).Returns(token); + + await _sut.LoginAsync(globalArgs, loginArgs, true, ct); + + await _githubTokenStore.Received(1).SetAsync(token); + await _persistentCredentialStore.Received(0).StoreTokenAsync(Arg.Any(), Arg.Any()); + await _next.Received(0).LoginAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + + _logger.VerifyLogs(new LogEntry(LogLevel.Information, "Using token from persistent store")); + } +} \ No newline at end of file diff --git a/GithubBackup/GithubBackup.Cli.Tests/Commands/Github/Auth/Pipeline/TokenFromConfigurationPipelineTests.cs b/GithubBackup/GithubBackup.Cli.Tests/Commands/Github/Auth/Pipeline/TokenFromConfigurationPipelineTests.cs new file mode 100644 index 0000000..2087300 --- /dev/null +++ b/GithubBackup/GithubBackup.Cli.Tests/Commands/Github/Auth/Pipeline/TokenFromConfigurationPipelineTests.cs @@ -0,0 +1,126 @@ +using FluentAssertions; +using GithubBackup.Cli.Commands.Github.Auth.Pipeline; +using GithubBackup.Cli.Commands.Github.Login; +using GithubBackup.Cli.Commands.Global; +using GithubBackup.Core.Github.Credentials; +using GithubBackup.Core.Github.Users; +using GithubBackup.TestUtils.Configuration; +using GithubBackup.TestUtils.Logging; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; + +namespace GithubBackup.Cli.Tests.Commands.Github.Auth.Pipeline; + +public class TokenFromConfigurationPipelineTests +{ + private readonly ILogger _logger = Substitute.For>(); + private readonly IGithubTokenStore _githubTokenStore = Substitute.For(); + private readonly IUserService _userService = Substitute.For(); + private readonly ILoginPipeline _next = Substitute.For(); + + [Fact] + public async Task LoginAsync_NotResponsible_CallNext() + { + var ct = CancellationToken.None; + var globalArgs = new GlobalArgs(LogLevel.Debug, false, new FileInfo("Test")); + var loginArgs = new LoginArgs(null, false); + + await CreatePipeline(null).LoginAsync(globalArgs, loginArgs, true, ct); + + await _next.Received(1).LoginAsync(globalArgs, loginArgs, true, ct); + + _logger.VerifyLogs(); + } + + [Fact] + public async Task LoginAsync_ValidAndPersist_PersistTokenAndReturnUser() + { + const string token = "token"; + var ct = CancellationToken.None; + var globalArgs = new GlobalArgs(LogLevel.Debug, false, new FileInfo("Test")); + var loginArgs = new LoginArgs(token, false); + var user = new User("test", "test"); + + _userService.WhoAmIAsync(ct).Returns(user); + + await CreatePipeline(token).LoginAsync(globalArgs, loginArgs, true, ct); + + await _githubTokenStore.Received(1).SetAsync(token); + await _next.Received(0).LoginAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + + _logger.VerifyLogs(new LogEntry(LogLevel.Information, "Using token from environment variable")); + } + + [Fact] + public async Task LoginAsync_ValidAndNotPersist_DoNotPersistTokenAndReturnUser() + { + const string token = "token"; + var ct = CancellationToken.None; + var globalArgs = new GlobalArgs(LogLevel.Debug, false, new FileInfo("Test")); + var loginArgs = new LoginArgs(token, false); + var user = new User("test", "test"); + + _userService.WhoAmIAsync(ct).Returns(user); + + await CreatePipeline(token).LoginAsync(globalArgs, loginArgs, false, ct); + + await _githubTokenStore.Received(1).SetAsync(token); + await _next.Received(0).LoginAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + + _logger.VerifyLogs(new LogEntry(LogLevel.Information, "Using token from environment variable")); + } + + [Fact] + public async Task LoginAsync_Invalid_ThrowException() + { + const string token = "token"; + var ct = CancellationToken.None; + var globalArgs = new GlobalArgs(LogLevel.Debug, false, new FileInfo("Test")); + var loginArgs = new LoginArgs(token, false); + + _userService.WhoAmIAsync(ct).ThrowsAsync(); + + var action = () => CreatePipeline(token).LoginAsync(globalArgs, loginArgs, true, ct); + + await action.Should().ThrowAsync(); + + await _githubTokenStore.Received(0).SetAsync(token); + + _logger.VerifyLogs( + new LogEntry(LogLevel.Information, "Using token from environment variable"), + new LogEntry(LogLevel.Error, "Token token is invalid") + ); + } + + private ILoginPipeline CreatePipeline(string? token) + { + var configuration = CreateConfiguration(token); + + return new TokenFromConfigurationPipeline( + _logger, + configuration, + _githubTokenStore, + _userService + ) + { + Next = _next + }; + } + + private static IConfigurationRoot CreateConfiguration(string? envToken) + { + if (string.IsNullOrWhiteSpace(envToken)) + { + return new ConfigurationBuilder().Build(); + } + + return new ConfigurationBuilder() + .Add(new KeyValueConfigurationProvider(new Dictionary + { + { "TOKEN", envToken } + })) + .Build(); + } +} \ No newline at end of file diff --git a/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/DeviceFlowAuthPipeline.cs b/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/DeviceFlowAuthPipeline.cs index f2185aa..cd6e506 100644 --- a/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/DeviceFlowAuthPipeline.cs +++ b/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/DeviceFlowAuthPipeline.cs @@ -45,19 +45,30 @@ private static bool IsReponsible(LoginArgs args) { if (!IsReponsible(args)) { - throw new InvalidOperationException("This pipeline is not responsible for this login request"); + return await Next!.LoginAsync(globalArgs, args, persist, ct); } _logger.LogInformation("Using device flow authentication"); var oauthToken = await GetOAuthTokenAsync(globalArgs, ct); - await _githubTokenStore.SetAsync(oauthToken); - if (persist) + try { - await _persistentCredentialStore.StoreTokenAsync(oauthToken, ct); - } + var user = await _userService.WhoAmIAsync(ct); + + await _githubTokenStore.SetAsync(oauthToken); + + if (persist) + { + await _persistentCredentialStore.StoreTokenAsync(oauthToken, ct); + } - return await _userService.WhoAmIAsync(ct); + return user; + } + catch (Exception e) + { + _logger.LogError(e, "Token {Token} is invalid", args.Token); + throw new Exception($"Token {args.Token} is invalid"); + } } private async Task GetOAuthTokenAsync(GlobalArgs globalArgs, CancellationToken ct) diff --git a/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/LoginPipelineBuilder.cs b/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/LoginPipelineBuilder.cs index 18380c5..f17b513 100644 --- a/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/LoginPipelineBuilder.cs +++ b/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/LoginPipelineBuilder.cs @@ -42,10 +42,10 @@ public ILoginPipeline WithPersistent() var tokenArgPipeline = _tokenArgPipelineFactory.Create(); var deviceFlowAuthPipeline = _deviceFlowAuthPipelineFactory.Create(); - persistedPipeline.Next = tokenArgPipeline; tokenArgPipeline.Next = tokenFromConfigurationPipeline; tokenFromConfigurationPipeline.Next = deviceFlowAuthPipeline; - deviceFlowAuthPipeline.Next = defaultPipeline; + deviceFlowAuthPipeline.Next = persistedPipeline; + persistedPipeline.Next = defaultPipeline; return persistedPipeline; } diff --git a/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/PersistedPipeline.cs b/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/PersistedPipeline.cs index f158eed..a2e6da7 100644 --- a/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/PersistedPipeline.cs +++ b/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/PersistedPipeline.cs @@ -54,6 +54,7 @@ private static bool IsReponsible(LoginArgs args) { try { + _logger.LogInformation("Using token from persistent store"); var t = await _persistentCredentialStore.LoadTokenAsync(ct); if (string.IsNullOrWhiteSpace(t)) @@ -61,9 +62,10 @@ private static bool IsReponsible(LoginArgs args) _logger.LogInformation("Persistent token not found"); return null; } - + + var user = await _userService.WhoAmIAsync(ct); await _githubTokenStore.SetAsync(t); - return await _userService.WhoAmIAsync(ct); + return user; } catch (Exception e) { diff --git a/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/TokenFromConfigurationPipeline.cs b/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/TokenFromConfigurationPipeline.cs index f3a81a9..77b35ec 100644 --- a/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/TokenFromConfigurationPipeline.cs +++ b/GithubBackup/GithubBackup.Cli/Commands/Github/Auth/Pipeline/TokenFromConfigurationPipeline.cs @@ -41,14 +41,15 @@ private bool IsReponsible() { return await Next!.LoginAsync(globalArgs, args, persist, ct); } - + _logger.LogInformation("Using token from environment variable"); var token = _configuration.GetValue("TOKEN"); - await _githubTokenStore.SetAsync(token); try { - return await _userService.WhoAmIAsync(ct); + var user = await _userService.WhoAmIAsync(ct); + await _githubTokenStore.SetAsync(token); + return user; } catch (Exception e) {