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");
}