From 5a90d6c261f5803ea382dd12d660528aa5327903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=E2=96=88=E2=96=88=E2=96=88=E2=96=88=E2=96=88?= Date: Wed, 23 Oct 2024 21:51:47 -0400 Subject: [PATCH 1/2] Unit testing coverage for API project nullinside-development-group/nullinside-api-twitch-bot#39 --- .../Shared/UserHelpersTests.cs | 27 +- .../Nullinside.Api.Tests.csproj | 1 - .../Controllers/DockerControllerTests.cs | 170 +++++++++ .../FeatureToggleControllerTests.cs | 40 +++ .../Controllers/UserControllerTests.cs | 337 ++++++++++++++++++ src/Nullinside.Api.Tests/UnitTestBase.cs | 33 ++ .../Controllers/DatabaseController.cs | 5 +- .../Controllers/DockerController.cs | 6 +- .../Controllers/FeatureToggleController.cs | 7 +- .../Controllers/UserController.cs | 25 +- src/Nullinside.Api/Shared/Json/AuthToken.cs | 3 + .../Shared/Json/BasicServerFailure.cs | 3 + .../Shared/Json/GoogleOpenIdToken.cs | 3 + .../Json/TurnOnOrOffDockerResourcesRequest.cs | 3 + 14 files changed, 622 insertions(+), 41 deletions(-) create mode 100644 src/Nullinside.Api.Tests/Nullinside.Api/Controllers/DockerControllerTests.cs create mode 100644 src/Nullinside.Api.Tests/Nullinside.Api/Controllers/FeatureToggleControllerTests.cs create mode 100644 src/Nullinside.Api.Tests/Nullinside.Api/Controllers/UserControllerTests.cs create mode 100644 src/Nullinside.Api.Tests/UnitTestBase.cs diff --git a/src/Nullinside.Api.Tests/Nullinside.Api.Model/Shared/UserHelpersTests.cs b/src/Nullinside.Api.Tests/Nullinside.Api.Model/Shared/UserHelpersTests.cs index 7af3690..b464526 100644 --- a/src/Nullinside.Api.Tests/Nullinside.Api.Model/Shared/UserHelpersTests.cs +++ b/src/Nullinside.Api.Tests/Nullinside.Api.Model/Shared/UserHelpersTests.cs @@ -1,31 +1,12 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; - -using Nullinside.Api.Model; using Nullinside.Api.Model.Ddl; using Nullinside.Api.Model.Shared; namespace Nullinside.Api.Tests.Nullinside.Api.Model.Shared; -public class UserHelpersTests { - private INullinsideContext _db; - - [SetUp] - public void Setup() { - // Create an in-memory database to fake the SQL queries. Note that we generate a random GUID for the name - // here. If you use the same name more than once you'll get collisions between tests. - DbContextOptions contextOptions = new DbContextOptionsBuilder() - .UseInMemoryDatabase(Guid.NewGuid().ToString()) - .ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning)) - .Options; - _db = new NullinsideContext(contextOptions); - } - - [TearDown] - public async Task TearDown() { - await _db.DisposeAsync(); - } - +/// +/// Tests for the class. +/// +public class UserHelpersTests : UnitTestBase { /// /// The case where a user is generating a new token to replace their existing one. /// diff --git a/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj b/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj index f03613d..7e1deec 100644 --- a/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj +++ b/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj @@ -32,7 +32,6 @@ - diff --git a/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/DockerControllerTests.cs b/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/DockerControllerTests.cs new file mode 100644 index 0000000..8be90ee --- /dev/null +++ b/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/DockerControllerTests.cs @@ -0,0 +1,170 @@ +using Microsoft.AspNetCore.Mvc; + +using Moq; + +using Nullinside.Api.Common.Docker; +using Nullinside.Api.Common.Docker.Support; +using Nullinside.Api.Controllers; +using Nullinside.Api.Model.Ddl; +using Nullinside.Api.Shared.Json; + +namespace Nullinside.Api.Tests.Nullinside.Api.Controllers; + +/// +/// Tests for the class +/// +public class DockerControllerTests : UnitTestBase { + /// + /// The docker proxy. + /// + private Mock _docker; + + /// + /// Add the docker proxy. + /// + public override void Setup() { + base.Setup(); + _docker = new Mock(); + } + + /// + /// Tests that given a list of docker projects from the database we can match it against the actively running + /// projects on the server. + /// + [Test] + public async Task DatabaseMatchesCommandOutputSuccessfully() { + // Create three entries in the database for what we allow people to see. All of these should be in the output. + _db.DockerDeployments.AddRange( + new DockerDeployments { + Id = 1, DisplayName = "Good", IsDockerComposeProject = true, Name = "Good", Notes = "Should be in output" + }, + new DockerDeployments { + Id = 2, DisplayName = "Good without matching name", IsDockerComposeProject = true, Name = "NonMatchingName", + Notes = "Should be in output" + }, + new DockerDeployments { + Id = 3, DisplayName = "Good non-compose", IsDockerComposeProject = false, Name = "Stuff", + Notes = "Should be in output" + } + ); + await _db.SaveChangesAsync(); + + // Create two entries "in the server" for what is actually running. We should only match on the "good" one. The bad + // one is different enough that it shouldn't match. + var compose = new List { + new() { Id = 1, IsOnline = true, Name = "Good", Notes = "Should be in output's IsOnline field" }, + new() { Id = 2, IsOnline = false, Name = "Bad", Notes = "Should not be in output" } + }; + _docker.Setup(d => d.GetDockerComposeProjects(It.IsAny())) + .Returns(() => Task.FromResult(compose.AsEnumerable())); + + var containers = new List { + new() { Id = 3, IsOnline = true, Name = "doesn't match", Notes = "Should not be in output" } + }; + _docker.Setup(d => d.GetContainers(It.IsAny())) + .Returns(() => Task.FromResult(containers.AsEnumerable())); + + // Make the call and ensure it's successful. + var controller = new DockerController(_db, _docker.Object); + ObjectResult obj = await controller.GetDockerResources(); + Assert.That(obj.StatusCode, Is.EqualTo(200)); + + // There should be three results. One that was actively running "Good" and the others weren't actively running. + var deployments = obj.Value as List; + Assert.That(deployments, Is.Not.Null); + Assert.That(deployments.Count, Is.EqualTo(3)); + Assert.That(deployments.FirstOrDefault(d => d.Name == "Good")?.IsOnline, Is.True); + Assert.That(deployments.FirstOrDefault(d => d.Name == "Good without matching name")?.IsOnline, Is.False); + Assert.That(deployments.FirstOrDefault(d => d.Name == "Good non-compose")?.IsOnline, Is.False); + } + + /// + /// Tests that turning on/off a compose project calls the correct thing. + /// + [Test] + public async Task OnOffComposeProjectsWork() { + // Create two entries in the database for what we allow people to adjust. + _db.DockerDeployments.AddRange( + new DockerDeployments { + Id = 1, DisplayName = "Good", IsDockerComposeProject = true, Name = "Good", Notes = "Should be in output" + }, + new DockerDeployments { + Id = 2, DisplayName = "Bad", IsDockerComposeProject = true, Name = "Bad", Notes = "Should not be in output" + } + ); + await _db.SaveChangesAsync(); + + // Only a call with "Good" will work. + _docker.Setup(d => + d.TurnOnOffDockerCompose("Good", It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(() => Task.FromResult(true)); + + // Make the call and ensure it's successful. + var controller = new DockerController(_db, _docker.Object); + ObjectResult obj = + await controller.TurnOnOrOffDockerResources(1, new TurnOnOrOffDockerResourcesRequest { TurnOn = true }); + Assert.That(obj.StatusCode, Is.EqualTo(200)); + + // Ensure we called the 3rd party API with a value of "Good" to turn on a compose. + bool deployments = (bool)obj.Value; + Assert.That(deployments, Is.True); + } + + /// + /// Tests that turning on/off a container project calls the correct thing. + /// + [Test] + public async Task OnOffContainerWork() { + // Create two entries in the database for what we allow people to adjust. + _db.DockerDeployments.AddRange( + new DockerDeployments { + Id = 1, DisplayName = "Good", IsDockerComposeProject = false, Name = "Good", Notes = "Should be in output" + }, + new DockerDeployments { + Id = 2, DisplayName = "Bad", IsDockerComposeProject = false, Name = "Bad", Notes = "Should not be in output" + } + ); + await _db.SaveChangesAsync(); + + // Only a call with "Good" will work. + _docker.Setup(d => d.TurnOnOffDockerContainer("Good", It.IsAny(), It.IsAny())) + .Returns(() => Task.FromResult(true)); + + // Make the call and ensure it's successful. + var controller = new DockerController(_db, _docker.Object); + ObjectResult obj = + await controller.TurnOnOrOffDockerResources(1, new TurnOnOrOffDockerResourcesRequest { TurnOn = true }); + Assert.That(obj.StatusCode, Is.EqualTo(200)); + + // Ensure we called the 3rd party API with a value of "Good" to turn on a container. + bool deployments = (bool)obj.Value; + Assert.That(deployments, Is.True); + } + + /// + /// Tests providing an invalid id will result in a HTTP bad request. + /// + [Test] + public async Task InvalidIdIsBadRequest() { + // Create two entries in the database for what we allow people to adjust. + _db.DockerDeployments.Add( + new DockerDeployments { + Id = 2, DisplayName = "Bad", IsDockerComposeProject = false, Name = "Bad", Notes = "Should not be in output" + } + ); + await _db.SaveChangesAsync(); + + // Only a call with "Good" will get true. + _docker.Setup(d => d.TurnOnOffDockerContainer("Good", It.IsAny(), It.IsAny())) + .Returns(() => Task.FromResult(true)); + + // Make the call and ensure it's successful. + var controller = new DockerController(_db, _docker.Object); + ObjectResult obj = + await controller.TurnOnOrOffDockerResources(1, new TurnOnOrOffDockerResourcesRequest { TurnOn = true }); + Assert.That(obj.StatusCode, Is.EqualTo(400)); + + // Bad request is returned to user with a generic error message. + Assert.That(obj.Value, Is.TypeOf()); + } +} \ No newline at end of file diff --git a/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/FeatureToggleControllerTests.cs b/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/FeatureToggleControllerTests.cs new file mode 100644 index 0000000..0347e60 --- /dev/null +++ b/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/FeatureToggleControllerTests.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Mvc; + +using Nullinside.Api.Controllers; +using Nullinside.Api.Model.Ddl; + +namespace Nullinside.Api.Tests.Nullinside.Api.Controllers; + +/// +/// Tests for the class +/// +public class FeatureToggleControllerTests : UnitTestBase { + /// + /// Tests that we can pull the feature toggles. It's basically a straight through. + /// + [Test] + public async Task GetAllFeatureToggles() { + // Creates two feature toggles. + _db.FeatureToggle.AddRange( + new FeatureToggle { + Id = 1, Feature = "hi", IsEnabled = true + }, + new FeatureToggle { + Id = 2, Feature = "bye", IsEnabled = false + } + ); + await _db.SaveChangesAsync(); + + // Make the call and ensure it's successful. + var controller = new FeatureToggleController(_db); + ObjectResult obj = await controller.GetAll(); + Assert.That(obj.StatusCode, Is.EqualTo(200)); + + // Ensure they passed through cleanly. + var featureToggles = obj.Value as IEnumerable; + Assert.That(featureToggles, Is.Not.Null); + Assert.That(featureToggles.Count, Is.EqualTo(2)); + Assert.That(featureToggles.FirstOrDefault(f => f.Feature == "hi")?.IsEnabled, Is.True); + Assert.That(featureToggles.FirstOrDefault(f => f.Feature == "bye")?.IsEnabled, Is.False); + } +} \ No newline at end of file diff --git a/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/UserControllerTests.cs b/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/UserControllerTests.cs new file mode 100644 index 0000000..56c591b --- /dev/null +++ b/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/UserControllerTests.cs @@ -0,0 +1,337 @@ +using System.Security.Claims; + +using Google.Apis.Auth; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Configuration; + +using Moq; + +using Nullinside.Api.Common.Twitch; +using Nullinside.Api.Controllers; +using Nullinside.Api.Model; +using Nullinside.Api.Model.Ddl; +using Nullinside.Api.Shared.Json; + +namespace Nullinside.Api.Tests.Nullinside.Api.Controllers; + +/// +/// Tests for the class +/// +public class UserControllerTests : UnitTestBase { + /// + /// The mock configuration. + /// + private IConfiguration _configuration; + + /// + /// The twitch api. + /// + private Mock _twitchApi; + + /// + public override void Setup() { + base.Setup(); + + // Setup the config + var config = new Dictionary() { + { "Api:SiteUrl", string.Empty } + }; + + _configuration = new ConfigurationBuilder() + .AddInMemoryCollection(config) + .Build(); + + _twitchApi = new Mock(); + } + + /// + /// Tests that we can login with google for a user that already exists. + /// + [Test] + public async Task PerformGoogleLoginExisting() { + // Create an existing user. + this._db.Users.Add(new User { + Id = 1, + Email = "hi" + }); + + await this._db.SaveChangesAsync(); + + // Make the call and ensure it's successful. + var controller = new TestableUserController(_configuration, _db); + controller.Email = "hi"; + RedirectResult obj = await controller.Login(new GoogleOpenIdToken { credential = "stuff" }); + + // We should have been redirected to the successful route. + Assert.That(obj.Url.StartsWith("/user/login?token="), Is.True); + + // No additional users should have been created. + Assert.That(this._db.Users.Count(), Is.EqualTo(1)); + + // We should have saved the token in the existing user's database. + Assert.That(obj.Url.EndsWith(this._db.Users.First().Token), Is.True); + } + + /// + /// Tests that we can login with google for a new user. + /// + [Test] + public async Task PerformGoogleLoginNewUser() { + // Make the call and ensure it's successful. + var controller = new TestableUserController(_configuration, _db); + controller.Email = "hi"; + RedirectResult obj = await controller.Login(new GoogleOpenIdToken { credential = "stuff" }); + + // We should have been redirected to the successful route. + Assert.That(obj.Url.StartsWith("/user/login?token="), Is.True); + + // No additional users should have been created. + Assert.That(this._db.Users.Count(), Is.EqualTo(1)); + + // We should have saved the token in the existing user's database. + Assert.That(obj.Url.EndsWith(this._db.Users.First().Token), Is.True); + } + + /// + /// Tests that we handle DB errors correctly in the google login. + /// + [Test] + public async Task GoToErrorOnDbException() { + this._db.Users = null; + + // Make the call and ensure it's successful. + var controller = new TestableUserController(_configuration, _db); + controller.Email = "hi"; + RedirectResult obj = await controller.Login(new GoogleOpenIdToken { credential = "stuff" }); + + // We should have been redirected to the error. + Assert.That(obj.Url.StartsWith("/user/login?error="), Is.True); + } + + /// + /// Tests that we handle bad gmail responses. + /// + [Test] + public async Task GoToErrorOnBadGmailResponse() { + // Make the call and ensure it's successful. + var controller = new TestableUserController(_configuration, _db); + controller.Email = null; + RedirectResult obj = await controller.Login(new GoogleOpenIdToken { credential = "stuff" }); + + // We should have been redirected to the error. + Assert.That(obj.Url.StartsWith("/user/login?error="), Is.True); + } + + /// + /// Tests that we can login with twitch for a user that already exists. + /// + [Test] + public async Task PerformTwitchLoginExisting() { + // Tells us twitch parsed the code successfully. + _twitchApi.Setup(a => a.CreateAccessToken(It.IsAny(), It.IsAny())) + .Returns(() => Task.FromResult(new TwitchAccessToken())); + + // Gets a matching email address from our database + _twitchApi.Setup(a => a.GetUserEmail(It.IsAny())) + .Returns(() => Task.FromResult("hi")); + + // Create an existing user. + this._db.Users.Add(new User { + Id = 1, + Email = "hi" + }); + + await this._db.SaveChangesAsync(); + + // Make the call and ensure it's successful. + var controller = new TestableUserController(_configuration, _db); + var obj = await controller.TwitchLogin("things", _twitchApi.Object); + + // We should have been redirected to the successful route. + Assert.That(obj.Url.StartsWith("/user/login?token="), Is.True); + + // No additional users should have been created. + Assert.That(this._db.Users.Count(), Is.EqualTo(1)); + + // We should have saved the token in the existing user's database. + Assert.That(obj.Url.EndsWith(this._db.Users.First().Token), Is.True); + } + + /// + /// Tests that we can login with twitch for a new user. + /// + [Test] + public async Task PerformTwitchLoginNewUser() { + // Tells us twitch parsed the code successfully. + _twitchApi.Setup(a => a.CreateAccessToken(It.IsAny(), It.IsAny())) + .Returns(() => Task.FromResult(new TwitchAccessToken())); + + // Gets a matching email address from our database + _twitchApi.Setup(a => a.GetUserEmail(It.IsAny())) + .Returns(() => Task.FromResult("hi")); + + // Make the call and ensure it's successful. + var controller = new TestableUserController(_configuration, _db); + var obj = await controller.TwitchLogin("things", _twitchApi.Object); + + // We should have been redirected to the successful route. + Assert.That(obj.Url.StartsWith("/user/login?token="), Is.True); + + // No additional users should have been created. + Assert.That(this._db.Users.Count(), Is.EqualTo(1)); + + // We should have saved the token in the existing user's database. + Assert.That(obj.Url.EndsWith(this._db.Users.First().Token), Is.True); + } + + /// + /// Tests that we handle a bad response from twitch. + /// + [Test] + public async Task PerformTwitchLoginBadTwitchResponse() { + // Tells us twitch thinks it was a bad code. + _twitchApi.Setup(a => a.CreateAccessToken(It.IsAny(), It.IsAny())) + .Returns(() => Task.FromResult(null)); + + // Make the call and ensure it's successful. + var controller = new TestableUserController(_configuration, _db); + var obj = await controller.TwitchLogin("things", _twitchApi.Object); + + // We should have gone down the bad route + Assert.That(obj.Url.StartsWith("/user/login?error="), Is.True); + } + + /// + /// Tests that we can login with twitch but it has no email associated with the account. + /// + [Test] + public async Task PerformTwitchLoginWithNoEmailAccount() { + // Tells us twitch parsed the code successfully. + _twitchApi.Setup(a => a.CreateAccessToken(It.IsAny(), It.IsAny())) + .Returns(() => Task.FromResult(new TwitchAccessToken())); + + // Make the call and ensure it's successful. + var controller = new TestableUserController(_configuration, _db); + var obj = await controller.TwitchLogin("things", _twitchApi.Object); + + // We should have gone down the bad route because no email was associated with the twitch account. + Assert.That(obj.Url.StartsWith("/user/login?error="), Is.True); + } + + /// + /// Tests that we handle database failures correctly in the twitch login process. + /// + [Test] + public async Task PerformTwitchLoginDbFailure() { + _db.Users = null; + + // Tells us twitch parsed the code successfully. + _twitchApi.Setup(a => a.CreateAccessToken(It.IsAny(), It.IsAny())) + .Returns(() => Task.FromResult(new TwitchAccessToken())); + + // Gets an email address from twitch + _twitchApi.Setup(a => a.GetUserEmail(It.IsAny())) + .Returns(() => Task.FromResult("hi")); + + // Make the call and ensure it's successful. + var controller = new TestableUserController(_configuration, _db); + var obj = await controller.TwitchLogin("things", _twitchApi.Object); + + // We should have been redirected to the error route because of an exception in DB processing. + Assert.That(obj.Url.StartsWith("/user/login?error="), Is.True); + } + + /// + /// Tests that we get the roles assigned to the user. + /// + [Test] + public void GetRoles() { + // Setup the logged in user + var claims = new List { + new(ClaimTypes.Role, "candy") + }; + var identity = new ClaimsIdentity(claims, "icecream"); + + // Make the call and ensure it's successful. + var controller = new TestableUserController(_configuration, _db); + controller.ControllerContext = new ControllerContext(); + controller.ControllerContext.HttpContext = new DefaultHttpContext(); + controller.ControllerContext.HttpContext.User = new ClaimsPrincipal(identity); + + var obj = controller.GetRoles(); + Assert.That(obj.StatusCode, Is.EqualTo(200)); + + // Ensure we got the role we put in. + var roles = obj.Value.GetType().GetProperty("roles").GetValue(obj.Value) as IEnumerable; + Assert.That(roles.Count(), Is.EqualTo(1)); + Assert.That(roles.First(), Is.EqualTo("candy")); + } + + /// + /// Tests that we can validate a token that exists. + /// + [Test] + public async Task ValidateTokenExists() { + this._db.Users.Add(new User { Token = "123" }); + await this._db.SaveChangesAsync(); + + // Make the call and ensure it's successful. + var controller = new TestableUserController(_configuration, _db); + var obj = await controller.Validate(new AuthToken("123")); + Assert.That((obj as IStatusCodeActionResult)?.StatusCode, Is.EqualTo(200)); + + // Ensure we returned that the token was correct. + Assert.That((obj as ObjectResult)?.Value, Is.True); + } + + /// + /// Tests that we do not validate tokens that do not exist. + /// + [Test] + public async Task ValidateFailWithoutToken() { + // Make the call and ensure it fails. + var controller = new TestableUserController(_configuration, _db); + var obj = await controller.Validate(new AuthToken("123")); + Assert.That((obj as IStatusCodeActionResult)?.StatusCode, Is.EqualTo(401)); + } + + /// + /// Tests that unhandled exceptions are performed appropriately. + /// + [Test] + public async Task ValidateFailOnDbFailure() { + this._db.Users = null; + + // Make the call and ensure it fails. + var controller = new TestableUserController(_configuration, _db); + var obj = await controller.Validate(new AuthToken("123")); + Assert.That((obj as IStatusCodeActionResult)?.StatusCode, Is.EqualTo(500)); + } +} + +/// +/// A testable version of the user controller that removes 3rd party dependencies. +/// +public class TestableUserController : UserController { + /// + /// Gets or sets the email to include in the google payload. + /// + public string? Email { get; set; } + + /// + public TestableUserController(IConfiguration configuration, INullinsideContext dbContext) : base(configuration, dbContext) + { + } + + /// + protected override Task GenerateUserObject(GoogleOpenIdToken creds) { + if (null != Email) { + return Task.FromResult(new GoogleJsonWebSignature.Payload() { Email = Email }); + } + + return Task.FromResult(null); + } +} \ No newline at end of file diff --git a/src/Nullinside.Api.Tests/UnitTestBase.cs b/src/Nullinside.Api.Tests/UnitTestBase.cs new file mode 100644 index 0000000..ebc3f16 --- /dev/null +++ b/src/Nullinside.Api.Tests/UnitTestBase.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; + +using Nullinside.Api.Model; + +namespace Nullinside.Api.Tests; + +/// +/// A base class for all unit tests. +/// +public abstract class UnitTestBase { + /// + /// A fake database. + /// + protected INullinsideContext _db; + + [SetUp] + public virtual void Setup() { + // Create an in-memory database to fake the SQL queries. Note that we generate a random GUID for the name + // here. If you use the same name more than once you'll get collisions between tests. + DbContextOptions contextOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning)) + .Options; + _db = new NullinsideContext(contextOptions); + } + + [TearDown] + public virtual async Task TearDown() { + // Dispose since it has one. + await _db.DisposeAsync(); + } +} \ No newline at end of file diff --git a/src/Nullinside.Api/Controllers/DatabaseController.cs b/src/Nullinside.Api/Controllers/DatabaseController.cs index fade3f1..65a2847 100644 --- a/src/Nullinside.Api/Controllers/DatabaseController.cs +++ b/src/Nullinside.Api/Controllers/DatabaseController.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + using log4net; using Microsoft.AspNetCore.Authorization; @@ -13,6 +15,7 @@ namespace Nullinside.Api.Controllers; /// [ApiController] [Route("[controller]")] +[ExcludeFromCodeCoverage(Justification = "This is only to do database migrations, nothing to test.")] public class DatabaseController : ControllerBase { /// /// The nullinside database. @@ -39,7 +42,7 @@ public DatabaseController(INullinsideContext dbContext) { [AllowAnonymous] [HttpGet] [Route("migration")] - public async Task Migrate() { + public async Task Migrate() { await _dbContext.Database.MigrateAsync(); return Ok(); } diff --git a/src/Nullinside.Api/Controllers/DockerController.cs b/src/Nullinside.Api/Controllers/DockerController.cs index 3b9b380..0393ccb 100644 --- a/src/Nullinside.Api/Controllers/DockerController.cs +++ b/src/Nullinside.Api/Controllers/DockerController.cs @@ -52,7 +52,7 @@ public DockerController(INullinsideContext dbContext, IDockerProxy dockerProxy) [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task GetDockerResources(CancellationToken token) { + public async Task GetDockerResources(CancellationToken token = new()) { // Get all existing docker containers and docker projects. Task> containers = _docker.GetContainers(token); Task> projects = _docker.GetDockerComposeProjects(token); @@ -93,8 +93,8 @@ public async Task GetDockerResources(CancellationToken token) { [HttpPost("{id:int}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task TurnOnOrOffDockerResources(int id, TurnOnOrOffDockerResourcesRequest request, - CancellationToken token) { + public async Task TurnOnOrOffDockerResources(int id, TurnOnOrOffDockerResourcesRequest request, + CancellationToken token = new()) { DockerDeployments? recognizedProjects = await _dbContext.DockerDeployments .FirstOrDefaultAsync(d => d.Id == id, token); if (null == recognizedProjects) { diff --git a/src/Nullinside.Api/Controllers/FeatureToggleController.cs b/src/Nullinside.Api/Controllers/FeatureToggleController.cs index b181bcb..a1732b7 100644 --- a/src/Nullinside.Api/Controllers/FeatureToggleController.cs +++ b/src/Nullinside.Api/Controllers/FeatureToggleController.cs @@ -39,10 +39,7 @@ public FeatureToggleController(INullinsideContext dbContext) { /// The collection of feature toggles. [AllowAnonymous] [HttpGet] - public async Task GetAll(CancellationToken token) { - return Ok((await _dbContext.FeatureToggle - .ToListAsync(token)) - .Select(r => new { r.Feature, r.IsEnabled }) - ); + public async Task GetAll(CancellationToken token = new()) { + return Ok(await _dbContext.FeatureToggle.ToListAsync(token)); } } \ No newline at end of file diff --git a/src/Nullinside.Api/Controllers/UserController.cs b/src/Nullinside.Api/Controllers/UserController.cs index 2c08876..a89a2a2 100644 --- a/src/Nullinside.Api/Controllers/UserController.cs +++ b/src/Nullinside.Api/Controllers/UserController.cs @@ -57,10 +57,10 @@ public UserController(IConfiguration configuration, INullinsideContext dbContext [AllowAnonymous] [HttpPost] [Route("login")] - public async Task Login([FromForm] GoogleOpenIdToken creds, CancellationToken token) { + public async Task Login([FromForm] GoogleOpenIdToken creds, CancellationToken token = new()) { string? siteUrl = _configuration.GetValue("Api:SiteUrl"); try { - GoogleJsonWebSignature.Payload? credentials = await GoogleJsonWebSignature.ValidateAsync(creds.credential); + GoogleJsonWebSignature.Payload? credentials = await this.GenerateUserObject(creds); if (string.IsNullOrWhiteSpace(credentials?.Email)) { return Redirect($"{siteUrl}/user/login?error=1"); } @@ -77,7 +77,15 @@ public async Task Login([FromForm] GoogleOpenIdToken creds, Cance } } - + /// + /// Converts the credential string we get from google to a representation we read information from. + /// + /// The credentials from Google. + /// The user information object. + protected async virtual Task GenerateUserObject(GoogleOpenIdToken creds) { + return await GoogleJsonWebSignature.ValidateAsync(creds.credential); + } + /// /// **NOT CALLED BY SITE OR USERS** This endpoint is called by twitch as part of their oauth workflow. It /// redirects users back to the nullinside website. @@ -95,7 +103,7 @@ public async Task Login([FromForm] GoogleOpenIdToken creds, Cance [AllowAnonymous] [HttpGet] [Route("twitch-login")] - public async Task TwitchLogin([FromQuery] string code, [FromServices] ITwitchApiProxy api, + public async Task TwitchLogin([FromQuery] string code, [FromServices] ITwitchApiProxy api, CancellationToken token = new()) { string? siteUrl = _configuration.GetValue("Api:SiteUrl"); if (null == await api.CreateAccessToken(code, token)) { @@ -123,13 +131,13 @@ public async Task TwitchLogin([FromQuery] string code, [FromServi [Route("roles")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public IActionResult GetRoles() { + public ObjectResult GetRoles() { return Ok(new { roles = (from identify in User.Identities from claim in identify.Claims where claim.Type == ClaimTypes.Role - select claim.Value).Distinct() + select claim.Value).Distinct().ToList() }); } @@ -137,15 +145,16 @@ from claim in identify.Claims /// Validates that the provided token is valid. /// /// The token to validate. + /// The cancellation token. /// 200 if successful, 401 otherwise. [AllowAnonymous] [HttpPost] [Route("token/validate")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task Validate(AuthToken token) { + public async Task Validate(AuthToken token, CancellationToken cancellationToken = new()) { try { - User? existing = await _dbContext.Users.FirstOrDefaultAsync(u => u.Token == token.Token && !u.IsBanned); + User? existing = await _dbContext.Users.FirstOrDefaultAsync(u => u.Token == token.Token && !u.IsBanned, cancellationToken); if (null == existing) { return Unauthorized(); } diff --git a/src/Nullinside.Api/Shared/Json/AuthToken.cs b/src/Nullinside.Api/Shared/Json/AuthToken.cs index bf5acbd..c1f7775 100644 --- a/src/Nullinside.Api/Shared/Json/AuthToken.cs +++ b/src/Nullinside.Api/Shared/Json/AuthToken.cs @@ -1,8 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + namespace Nullinside.Api.Shared.Json; /// /// Represents an authentication token provided to the site via a "Bearer" token header. /// +[ExcludeFromCodeCoverage(Justification = "JSON")] public class AuthToken { /// /// Initializes a new instance of the class. diff --git a/src/Nullinside.Api/Shared/Json/BasicServerFailure.cs b/src/Nullinside.Api/Shared/Json/BasicServerFailure.cs index 7735dda..046e432 100644 --- a/src/Nullinside.Api/Shared/Json/BasicServerFailure.cs +++ b/src/Nullinside.Api/Shared/Json/BasicServerFailure.cs @@ -1,8 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + namespace Nullinside.Api.Shared.Json; /// /// Represents a basic error where you just want to give the caller an error message and nothing more. /// +[ExcludeFromCodeCoverage(Justification = "JSON")] public class BasicServerFailure { /// /// Initializes a new instance of the class. diff --git a/src/Nullinside.Api/Shared/Json/GoogleOpenIdToken.cs b/src/Nullinside.Api/Shared/Json/GoogleOpenIdToken.cs index 1eb2c8a..a329e82 100644 --- a/src/Nullinside.Api/Shared/Json/GoogleOpenIdToken.cs +++ b/src/Nullinside.Api/Shared/Json/GoogleOpenIdToken.cs @@ -1,8 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + namespace Nullinside.Api.Shared.Json; /// /// Represents the response from google for an OpenId token. /// +[ExcludeFromCodeCoverage(Justification = "JSON")] public class GoogleOpenIdToken { /// /// The cross site scripting check. diff --git a/src/Nullinside.Api/Shared/Json/TurnOnOrOffDockerResourcesRequest.cs b/src/Nullinside.Api/Shared/Json/TurnOnOrOffDockerResourcesRequest.cs index b966180..e9b7c3b 100644 --- a/src/Nullinside.Api/Shared/Json/TurnOnOrOffDockerResourcesRequest.cs +++ b/src/Nullinside.Api/Shared/Json/TurnOnOrOffDockerResourcesRequest.cs @@ -1,8 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + namespace Nullinside.Api.Shared.Json; /// /// A request to turn on or off a docker resource. /// +[ExcludeFromCodeCoverage(Justification = "JSON")] public class TurnOnOrOffDockerResourcesRequest { /// /// True to turn the resource on, false to turn it off. From 05e6dfcaa498fb7f90b72d08e9059674434bf926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=E2=96=88=E2=96=88=E2=96=88=E2=96=88=E2=96=88?= Date: Wed, 23 Oct 2024 22:04:19 -0400 Subject: [PATCH 2/2] Removing JSON serialization files from coverage --- .../BasicAuthorizationRequirement.cs | 3 +++ .../Desktop/GithubLatestReleaseJson.cs | 5 ++++- .../Docker/Support/DockerComposeLsOutput.cs | 3 +++ .../Docker/Support/DockerResource.cs | 3 +++ .../Exceptions/RetryException.cs | 5 ++++- .../Json/TwitchModeratedChannelsResponse.cs | 7 +++++- .../Controllers/DockerControllerTests.cs | 4 ++-- .../Controllers/UserControllerTests.cs | 22 +++++++++---------- 8 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/Nullinside.Api.Common.AspNetCore/Middleware/BasicAuthorizationRequirement.cs b/src/Nullinside.Api.Common.AspNetCore/Middleware/BasicAuthorizationRequirement.cs index 0988173..6ad1b74 100644 --- a/src/Nullinside.Api.Common.AspNetCore/Middleware/BasicAuthorizationRequirement.cs +++ b/src/Nullinside.Api.Common.AspNetCore/Middleware/BasicAuthorizationRequirement.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + using Microsoft.AspNetCore.Authorization; namespace Nullinside.Api.Common.AspNetCore.Middleware; @@ -5,6 +7,7 @@ namespace Nullinside.Api.Common.AspNetCore.Middleware; /// /// Represents a requirement where a user is expected to have one role. /// +[ExcludeFromCodeCoverage] public class BasicAuthorizationRequirement : IAuthorizationRequirement { /// /// Initializes a new instance of the class. diff --git a/src/Nullinside.Api.Common/Desktop/GithubLatestReleaseJson.cs b/src/Nullinside.Api.Common/Desktop/GithubLatestReleaseJson.cs index c4df99c..671c4c0 100644 --- a/src/Nullinside.Api.Common/Desktop/GithubLatestReleaseJson.cs +++ b/src/Nullinside.Api.Common/Desktop/GithubLatestReleaseJson.cs @@ -1,8 +1,11 @@ -namespace Nullinside.Api.Common.Desktop; +using System.Diagnostics.CodeAnalysis; + +namespace Nullinside.Api.Common.Desktop; /// /// The response information from GitHub's API. /// +[ExcludeFromCodeCoverage] public class GithubLatestReleaseJson { /// /// The url of the resource. diff --git a/src/Nullinside.Api.Common/Docker/Support/DockerComposeLsOutput.cs b/src/Nullinside.Api.Common/Docker/Support/DockerComposeLsOutput.cs index bef764f..cc17893 100644 --- a/src/Nullinside.Api.Common/Docker/Support/DockerComposeLsOutput.cs +++ b/src/Nullinside.Api.Common/Docker/Support/DockerComposeLsOutput.cs @@ -1,8 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + namespace Nullinside.Api.Common.Docker.Support; /// /// The `docker compose ls --format 'json'` output. /// +[ExcludeFromCodeCoverage] public class DockerComposeLsOutput { /// /// The name of the docker compose project. diff --git a/src/Nullinside.Api.Common/Docker/Support/DockerResource.cs b/src/Nullinside.Api.Common/Docker/Support/DockerResource.cs index bcfa5b4..7c042c0 100644 --- a/src/Nullinside.Api.Common/Docker/Support/DockerResource.cs +++ b/src/Nullinside.Api.Common/Docker/Support/DockerResource.cs @@ -1,9 +1,12 @@ +using System.Diagnostics.CodeAnalysis; + namespace Nullinside.Api.Common.Docker.Support; /// /// A docker resource representing either a docker compose project /// or a single docker container. /// +[ExcludeFromCodeCoverage] public class DockerResource { /// /// Initializes a new instance of the class. diff --git a/src/Nullinside.Api.Common/Exceptions/RetryException.cs b/src/Nullinside.Api.Common/Exceptions/RetryException.cs index 4bfff08..3478e45 100644 --- a/src/Nullinside.Api.Common/Exceptions/RetryException.cs +++ b/src/Nullinside.Api.Common/Exceptions/RetryException.cs @@ -1,8 +1,11 @@ -namespace Nullinside.Api.Common.Exceptions; +using System.Diagnostics.CodeAnalysis; + +namespace Nullinside.Api.Common.Exceptions; /// /// An exception thrown if an action continues to fail after retrying. /// +[ExcludeFromCodeCoverage] public class RetryException : Exception { /// /// Initializes a new instance of the class. diff --git a/src/Nullinside.Api.Common/Twitch/Json/TwitchModeratedChannelsResponse.cs b/src/Nullinside.Api.Common/Twitch/Json/TwitchModeratedChannelsResponse.cs index c28d7d4..94c958e 100644 --- a/src/Nullinside.Api.Common/Twitch/Json/TwitchModeratedChannelsResponse.cs +++ b/src/Nullinside.Api.Common/Twitch/Json/TwitchModeratedChannelsResponse.cs @@ -1,8 +1,11 @@ -namespace Nullinside.Api.Common.Twitch.Json; +using System.Diagnostics.CodeAnalysis; + +namespace Nullinside.Api.Common.Twitch.Json; /// /// The response to a query for what channels a user is moderator for. /// +[ExcludeFromCodeCoverage] public class TwitchModeratedChannelsResponse { /// /// The list of channels the user moderates for. @@ -18,6 +21,7 @@ public class TwitchModeratedChannelsResponse { /// /// A channel the user moderates. /// +[ExcludeFromCodeCoverage] public class TwitchModeratedChannel { /// /// The twitch id. @@ -38,6 +42,7 @@ public class TwitchModeratedChannel { /// /// Pagination information. /// +[ExcludeFromCodeCoverage] public class Pagination { /// /// The cursor to pass to "after" for pagination. diff --git a/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/DockerControllerTests.cs b/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/DockerControllerTests.cs index 8be90ee..aa475da 100644 --- a/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/DockerControllerTests.cs +++ b/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/DockerControllerTests.cs @@ -106,7 +106,7 @@ public async Task OnOffComposeProjectsWork() { Assert.That(obj.StatusCode, Is.EqualTo(200)); // Ensure we called the 3rd party API with a value of "Good" to turn on a compose. - bool deployments = (bool)obj.Value; + bool deployments = (bool)obj.Value!; Assert.That(deployments, Is.True); } @@ -137,7 +137,7 @@ public async Task OnOffContainerWork() { Assert.That(obj.StatusCode, Is.EqualTo(200)); // Ensure we called the 3rd party API with a value of "Good" to turn on a container. - bool deployments = (bool)obj.Value; + bool deployments = (bool)obj.Value!; Assert.That(deployments, Is.True); } diff --git a/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/UserControllerTests.cs b/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/UserControllerTests.cs index 56c591b..7ac5a7d 100644 --- a/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/UserControllerTests.cs +++ b/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/UserControllerTests.cs @@ -72,7 +72,7 @@ public async Task PerformGoogleLoginExisting() { Assert.That(this._db.Users.Count(), Is.EqualTo(1)); // We should have saved the token in the existing user's database. - Assert.That(obj.Url.EndsWith(this._db.Users.First().Token), Is.True); + Assert.That(obj.Url.EndsWith(this._db.Users.First().Token!), Is.True); } /// @@ -92,7 +92,7 @@ public async Task PerformGoogleLoginNewUser() { Assert.That(this._db.Users.Count(), Is.EqualTo(1)); // We should have saved the token in the existing user's database. - Assert.That(obj.Url.EndsWith(this._db.Users.First().Token), Is.True); + Assert.That(obj.Url.EndsWith(this._db.Users.First().Token!), Is.True); } /// @@ -100,7 +100,7 @@ public async Task PerformGoogleLoginNewUser() { /// [Test] public async Task GoToErrorOnDbException() { - this._db.Users = null; + this._db.Users = null!; // Make the call and ensure it's successful. var controller = new TestableUserController(_configuration, _db); @@ -157,7 +157,7 @@ public async Task PerformTwitchLoginExisting() { Assert.That(this._db.Users.Count(), Is.EqualTo(1)); // We should have saved the token in the existing user's database. - Assert.That(obj.Url.EndsWith(this._db.Users.First().Token), Is.True); + Assert.That(obj.Url.EndsWith(this._db.Users.First().Token!), Is.True); } /// @@ -184,7 +184,7 @@ public async Task PerformTwitchLoginNewUser() { Assert.That(this._db.Users.Count(), Is.EqualTo(1)); // We should have saved the token in the existing user's database. - Assert.That(obj.Url.EndsWith(this._db.Users.First().Token), Is.True); + Assert.That(obj.Url.EndsWith(this._db.Users.First().Token!), Is.True); } /// @@ -226,7 +226,7 @@ public async Task PerformTwitchLoginWithNoEmailAccount() { /// [Test] public async Task PerformTwitchLoginDbFailure() { - _db.Users = null; + _db.Users = null!; // Tells us twitch parsed the code successfully. _twitchApi.Setup(a => a.CreateAccessToken(It.IsAny(), It.IsAny())) @@ -265,9 +265,9 @@ public void GetRoles() { Assert.That(obj.StatusCode, Is.EqualTo(200)); // Ensure we got the role we put in. - var roles = obj.Value.GetType().GetProperty("roles").GetValue(obj.Value) as IEnumerable; - Assert.That(roles.Count(), Is.EqualTo(1)); - Assert.That(roles.First(), Is.EqualTo("candy")); + var roles = obj.Value!.GetType().GetProperty("roles")!.GetValue(obj.Value) as IEnumerable; + Assert.That(roles!.Count(), Is.EqualTo(1)); + Assert.That(roles!.First(), Is.EqualTo("candy")); } /// @@ -303,7 +303,7 @@ public async Task ValidateFailWithoutToken() { /// [Test] public async Task ValidateFailOnDbFailure() { - this._db.Users = null; + this._db.Users = null!; // Make the call and ensure it fails. var controller = new TestableUserController(_configuration, _db); @@ -329,7 +329,7 @@ public TestableUserController(IConfiguration configuration, INullinsideContext d /// protected override Task GenerateUserObject(GoogleOpenIdToken creds) { if (null != Email) { - return Task.FromResult(new GoogleJsonWebSignature.Payload() { Email = Email }); + return Task.FromResult(new GoogleJsonWebSignature.Payload() { Email = Email }); } return Task.FromResult(null);