diff --git a/src/Nullinside.Api.Common/Twitch/Support/TwitchBotLoginErrors.cs b/src/Nullinside.Api.Common/Twitch/Support/TwitchBotLoginErrors.cs index 1f03f70..5822b9b 100644 --- a/src/Nullinside.Api.Common/Twitch/Support/TwitchBotLoginErrors.cs +++ b/src/Nullinside.Api.Common/Twitch/Support/TwitchBotLoginErrors.cs @@ -1,4 +1,4 @@ -namespace Nullinside.Api.Shared.Support; +namespace Nullinside.Api.Common.Twitch.Support; /// /// Enumerates the types of errors when authenticating with twitch. diff --git a/src/Nullinside.Api.Model/Migrations/NullinsideContextModelSnapshot.cs b/src/Nullinside.Api.Model/Migrations/NullinsideContextModelSnapshot.cs index d33e5f8..c739d53 100644 --- a/src/Nullinside.Api.Model/Migrations/NullinsideContextModelSnapshot.cs +++ b/src/Nullinside.Api.Model/Migrations/NullinsideContextModelSnapshot.cs @@ -16,7 +16,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("ProductVersion", "8.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 64); modelBuilder.Entity("Nullinside.Api.Model.Ddl.DockerDeployments", b => diff --git a/src/Nullinside.Api.Model/NullinsideContext.cs b/src/Nullinside.Api.Model/NullinsideContext.cs index d582574..3f520dc 100644 --- a/src/Nullinside.Api.Model/NullinsideContext.cs +++ b/src/Nullinside.Api.Model/NullinsideContext.cs @@ -66,22 +66,6 @@ public NullinsideContext(DbContextOptions options) : base(opt /// public DbSet TwitchUserChatLogs { get; set; } = null!; - /// - /// Configures the default database connection. - /// - /// The database configuration options. - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - string? server = Environment.GetEnvironmentVariable("MYSQL_SERVER"); - string? username = Environment.GetEnvironmentVariable("MYSQL_USERNAME"); - string? password = Environment.GetEnvironmentVariable("MYSQL_PASSWORD"); - optionsBuilder.UseMySQL( - $"server={server};database=nullinside;user={username};password={password};AllowUserVariables=true;", - builder => { - builder.CommandTimeout(60 * 5); - builder.EnableRetryOnFailure(3); - }); - } - /// /// Dynamically finds all classes and generates tables from their definitions. /// diff --git a/src/Nullinside.Api.Model/NullinsideContextFactory.cs b/src/Nullinside.Api.Model/NullinsideDesignTimeDbContextFactory.cs similarity index 93% rename from src/Nullinside.Api.Model/NullinsideContextFactory.cs rename to src/Nullinside.Api.Model/NullinsideDesignTimeDbContextFactory.cs index 243db0e..9a13176 100644 --- a/src/Nullinside.Api.Model/NullinsideContextFactory.cs +++ b/src/Nullinside.Api.Model/NullinsideDesignTimeDbContextFactory.cs @@ -9,7 +9,7 @@ namespace Nullinside.Api.Model; /// references more than once solution with a DbContext in it. This factory is lazy loaded by the CLI automatically /// simply by implementing the IDesignTimeDbContextFactory interface. /// -public class NullinsideContextFactory : IDesignTimeDbContextFactory { +public class NullinsideDesignTimeDbContextFactory : IDesignTimeDbContextFactory { /// /// Creates a new database context. /// diff --git a/src/Nullinside.Api.Model/Shared/UserHelpers.cs b/src/Nullinside.Api.Model/Shared/UserHelpers.cs index 9a6eb70..09ae7c2 100644 --- a/src/Nullinside.Api.Model/Shared/UserHelpers.cs +++ b/src/Nullinside.Api.Model/Shared/UserHelpers.cs @@ -22,7 +22,7 @@ public static class UserHelpers { /// The username of the user on twitch. /// The id of the user on twitch. /// The bearer token if successful, null otherwise. - public static async Task GetTokenAndSaveToDatabase(INullinsideContext dbContext, string email, + public static async Task GenerateTokenAndSaveToDatabase(INullinsideContext dbContext, string email, CancellationToken token = new(), string? authToken = null, string? refreshToken = null, DateTime? expires = null, string? twitchUsername = null, string? twitchId = null) { string bearerToken = AuthUtils.GenerateBearerToken(); diff --git a/src/Nullinside.Api.Tests/Nullinside.Api.Model/Shared/UserHelpersTests.cs b/src/Nullinside.Api.Tests/Nullinside.Api.Model/Shared/UserHelpersTests.cs new file mode 100644 index 0000000..7af3690 --- /dev/null +++ b/src/Nullinside.Api.Tests/Nullinside.Api.Model/Shared/UserHelpersTests.cs @@ -0,0 +1,90 @@ +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(); + } + + /// + /// The case where a user is generating a new token to replace their existing one. + /// + [Test] + public async Task GenerateTokenForExistingUser() { + _db.Users.Add( + new User { + Email = "email" + } + ); + await _db.SaveChangesAsync(); + + // Verify there is only one user + Assert.That(_db.Users.Count(), Is.EqualTo(1)); + + // Generate a new token + string? token = await UserHelpers.GenerateTokenAndSaveToDatabase(_db, "email"); + Assert.That(token, Is.Not.Null); + + // Verify we still only have one user + Assert.That(_db.Users.Count(), Is.EqualTo(1)); + Assert.That(_db.Users.First().Token, Is.EqualTo(token)); + } + + /// + /// The case where a user is getting a token for the first time. A new user should be created. The existing user + /// should be untouched. + /// + [Test] + public async Task GenerateTokenForNewUser() { + _db.Users.Add( + new User { + Email = "email2" + } + ); + await _db.SaveChangesAsync(); + + // Verify there is only one user + Assert.That(_db.Users.Count(), Is.EqualTo(1)); + + // Generate a new token + string? token = await UserHelpers.GenerateTokenAndSaveToDatabase(_db, "email"); + Assert.That(token, Is.Not.Null); + + // Verify we have a new user + Assert.That(_db.Users.Count(), Is.EqualTo(2)); + Assert.That(_db.Users.FirstOrDefault(u => u.Email == "email")?.Token, Is.EqualTo(token)); + + // Verfy the old user is untouched + Assert.That(_db.Users.FirstOrDefault(u => u.Email == "email2")?.Token, Is.Null); + } + + /// + /// Unexpected database errors should result in a null being returned. + /// + [Test] + public async Task HandleUnexpectedErrors() { + // Force an error to occur. + string? token = await UserHelpers.GenerateTokenAndSaveToDatabase(null!, "email"); + Assert.That(token, Is.Null); + } +} \ No newline at end of file diff --git a/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj b/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj index c4ef76a..f03613d 100644 --- a/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj +++ b/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj @@ -14,9 +14,11 @@ - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -27,4 +29,17 @@ + + + + + + + + + + + + + diff --git a/src/Nullinside.Api.Tests/UnitTest1.cs b/src/Nullinside.Api.Tests/UnitTest1.cs deleted file mode 100644 index b3f493c..0000000 --- a/src/Nullinside.Api.Tests/UnitTest1.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Nullinside.Api.Tests; - -public class Tests { - [SetUp] - public void Setup() { - } - - [Test] - public void Test1() { - Assert.Pass(); - } -} \ No newline at end of file diff --git a/src/Nullinside.Api/Controllers/UserController.cs b/src/Nullinside.Api/Controllers/UserController.cs index 7d30944..2c08876 100644 --- a/src/Nullinside.Api/Controllers/UserController.cs +++ b/src/Nullinside.Api/Controllers/UserController.cs @@ -65,7 +65,7 @@ public async Task Login([FromForm] GoogleOpenIdToken creds, Cance return Redirect($"{siteUrl}/user/login?error=1"); } - string? bearerToken = await UserHelpers.GetTokenAndSaveToDatabase(_dbContext, credentials.Email, token); + string? bearerToken = await UserHelpers.GenerateTokenAndSaveToDatabase(_dbContext, credentials.Email, token); if (string.IsNullOrWhiteSpace(bearerToken)) { return Redirect($"{siteUrl}/user/login?error=2"); } @@ -107,7 +107,7 @@ public async Task TwitchLogin([FromQuery] string code, [FromServi return Redirect($"{siteUrl}/user/login?error=4"); } - string? bearerToken = await UserHelpers.GetTokenAndSaveToDatabase(_dbContext, email, token); + string? bearerToken = await UserHelpers.GenerateTokenAndSaveToDatabase(_dbContext, email, token); if (string.IsNullOrWhiteSpace(bearerToken)) { return Redirect($"{siteUrl}/user/login?error=2"); }