-
Notifications
You must be signed in to change notification settings - Fork 10
Add comprehensive test suite (374 tests) #276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
hhvrc
wants to merge
9
commits into
develop
Choose a base branch
from
feature/integration-tests
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
21a5048
Add comprehensive integration test suite (~111 tests)
hhvrc d7c84f7
Add unit tests and SignalR/shares integration tests
hhvrc bf17988
Fix integration test flakiness from BCrypt thread pool starvation
hhvrc cc5a84a
Fix integration test flakiness with parallelism limiter
hhvrc 6fa001a
Update API.IntegrationTests/Tests/TokensTests.cs
hhvrc eb5c282
Update API.IntegrationTests/Tests/DeviceEndpointTests.cs
hhvrc d04df20
Update API.IntegrationTests/Tests/AccountAuthenticatedTests.cs
hhvrc 4593c79
Apply suggestions from code review
hhvrc 37aca8b
Add code coverage extension
hhvrc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| using System.Net; | ||
| using System.Text; | ||
| using System.Text.Json; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using OpenShock.Common.Constants; | ||
| using OpenShock.Common.OpenShockDb; | ||
| using OpenShock.Common.Services.Session; | ||
| using OpenShock.Common.Utils; | ||
|
|
||
| namespace OpenShock.API.IntegrationTests.Helpers; | ||
|
|
||
| public static class TestHelper | ||
| { | ||
| private static readonly JsonSerializerOptions JsonOptions = new() | ||
| { | ||
| PropertyNamingPolicy = JsonNamingPolicy.CamelCase | ||
| }; | ||
|
|
||
| /// <summary> | ||
| /// Creates a user directly in DB, creates a session via ISessionService, returns auth info. | ||
| /// This bypasses signup/login endpoints entirely to avoid rate limiting. | ||
| /// </summary> | ||
| public static async Task<AuthenticatedUser> CreateAndLoginUser( | ||
| WebApplicationFactory factory, | ||
| string username, | ||
| string email, | ||
| string password) | ||
| { | ||
| // 1. Create user directly in DB | ||
| var userId = await CreateUserInDb(factory, username, email, password); | ||
|
|
||
| // 2. Create session via ISessionService (stored in Redis) | ||
| await using var scope = factory.Services.CreateAsyncScope(); | ||
| var sessionService = scope.ServiceProvider.GetRequiredService<ISessionService>(); | ||
| var session = await sessionService.CreateSessionAsync(userId, "IntegrationTest", "127.0.0.1"); | ||
|
|
||
| return new AuthenticatedUser(userId, username, email, session.Token); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates an HttpClient that sends the session cookie for authentication. | ||
| /// </summary> | ||
| public static HttpClient CreateAuthenticatedClient(WebApplicationFactory factory, string sessionToken) | ||
| { | ||
| var client = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions | ||
| { | ||
| AllowAutoRedirect = false, | ||
| HandleCookies = false | ||
| }); | ||
| client.DefaultRequestHeaders.Add("Cookie", $"{AuthConstants.UserSessionCookieName}={sessionToken}"); | ||
| return client; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates an HttpClient that sends an API token header for authentication. | ||
| /// </summary> | ||
| public static HttpClient CreateApiTokenClient(WebApplicationFactory factory, string apiToken) | ||
| { | ||
| var client = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions | ||
| { | ||
| AllowAutoRedirect = false, | ||
| HandleCookies = false | ||
| }); | ||
| client.DefaultRequestHeaders.Add(AuthConstants.ApiTokenHeaderName, apiToken); | ||
| return client; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates an HttpClient that sends a hub/device token header for authentication. | ||
| /// </summary> | ||
| public static HttpClient CreateHubTokenClient(WebApplicationFactory factory, string hubToken) | ||
| { | ||
| var client = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions | ||
| { | ||
| AllowAutoRedirect = false, | ||
| HandleCookies = false | ||
| }); | ||
| client.DefaultRequestHeaders.Add(AuthConstants.HubTokenHeaderName, hubToken); | ||
| return client; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates a user directly in the DB (bypasses signup endpoint). | ||
| /// </summary> | ||
| public static async Task<Guid> CreateUserInDb( | ||
| WebApplicationFactory factory, | ||
| string username, | ||
| string email, | ||
| string password, | ||
| bool activated = true) | ||
| { | ||
| await using var scope = factory.Services.CreateAsyncScope(); | ||
| var db = scope.ServiceProvider.GetRequiredService<OpenShockContext>(); | ||
|
|
||
| var userId = Guid.CreateVersion7(); | ||
| db.Users.Add(new User | ||
| { | ||
| Id = userId, | ||
| Name = username, | ||
| Email = email, | ||
| PasswordHash = HashingUtils.HashPassword(password), | ||
| ActivatedAt = activated ? DateTime.UtcNow : null | ||
| }); | ||
| await db.SaveChangesAsync(); | ||
| return userId; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates a device in the DB for a given user. Returns (deviceId, deviceToken). | ||
| /// </summary> | ||
| public static async Task<(Guid DeviceId, string Token)> CreateDeviceInDb( | ||
| WebApplicationFactory factory, | ||
| Guid ownerId, | ||
| string name = "TestDevice") | ||
| { | ||
| await using var scope = factory.Services.CreateAsyncScope(); | ||
| var db = scope.ServiceProvider.GetRequiredService<OpenShockContext>(); | ||
|
|
||
| var deviceId = Guid.CreateVersion7(); | ||
| var token = CryptoUtils.RandomAlphaNumericString(256); | ||
| db.Devices.Add(new Device | ||
| { | ||
| Id = deviceId, | ||
| Name = name, | ||
| OwnerId = ownerId, | ||
| Token = token, | ||
| CreatedAt = DateTime.UtcNow | ||
| }); | ||
| await db.SaveChangesAsync(); | ||
| return (deviceId, token); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates an API token in the DB for a given user. Returns the raw token string. | ||
| /// </summary> | ||
| public static async Task<(Guid TokenId, string RawToken)> CreateApiTokenInDb( | ||
| WebApplicationFactory factory, | ||
| Guid userId, | ||
| string name = "TestToken", | ||
| List<Common.Models.PermissionType>? permissions = null) | ||
| { | ||
| await using var scope = factory.Services.CreateAsyncScope(); | ||
| var db = scope.ServiceProvider.GetRequiredService<OpenShockContext>(); | ||
|
|
||
| var rawToken = CryptoUtils.RandomAlphaNumericString(AuthConstants.ApiTokenLength); | ||
| var tokenId = Guid.CreateVersion7(); | ||
| db.ApiTokens.Add(new ApiToken | ||
| { | ||
| Id = tokenId, | ||
| UserId = userId, | ||
| Name = name, | ||
| TokenHash = HashingUtils.HashToken(rawToken), | ||
| CreatedByIp = IPAddress.Loopback, | ||
| Permissions = permissions ?? [Common.Models.PermissionType.Shockers_Use] | ||
| }); | ||
| await db.SaveChangesAsync(); | ||
| return (tokenId, rawToken); | ||
| } | ||
|
|
||
| public static StringContent JsonContent(object obj) | ||
| { | ||
| return new StringContent(JsonSerializer.Serialize(obj, JsonOptions), Encoding.UTF8, "application/json"); | ||
| } | ||
| } | ||
|
|
||
| public sealed record AuthenticatedUser(Guid Id, string Username, string Email, string SessionToken); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
116 changes: 116 additions & 0 deletions
116
API.IntegrationTests/Tests/AccountAuthenticatedTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| using System.Net; | ||
| using OpenShock.API.IntegrationTests.Helpers; | ||
|
|
||
| namespace OpenShock.API.IntegrationTests.Tests; | ||
|
|
||
| public sealed class AccountAuthenticatedTests | ||
| { | ||
| [ClassDataSource<WebApplicationFactory>(Shared = SharedType.PerTestSession)] | ||
| public required WebApplicationFactory WebApplicationFactory { get; init; } | ||
|
|
||
| // --- Change Password --- | ||
|
|
||
| [Test] | ||
| public async Task ChangePassword_Success() | ||
| { | ||
| var user = await TestHelper.CreateAndLoginUser(WebApplicationFactory, "chgpwd", "chgpwd@test.org", "OldPassword123#"); | ||
| using var client = TestHelper.CreateAuthenticatedClient(WebApplicationFactory, user.SessionToken); | ||
|
|
||
| var response = await client.PostAsync("/1/account/password", TestHelper.JsonContent(new | ||
| { | ||
| currentPassword = "OldPassword123#", | ||
| newPassword = "NewPassword456#" | ||
| })); | ||
|
|
||
| await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK); | ||
|
|
||
| // Verify can login with new password | ||
| using var loginClient = WebApplicationFactory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions | ||
| { | ||
| AllowAutoRedirect = false, | ||
| HandleCookies = false | ||
| }); | ||
| var loginResponse = await loginClient.PostAsync("/2/account/login", TestHelper.JsonContent(new | ||
| { | ||
| usernameOrEmail = "chgpwd@test.org", | ||
| password = "NewPassword456#", | ||
| turnstileResponse = "valid-token" | ||
| })); | ||
| await Assert.That(loginResponse.StatusCode).IsEqualTo(HttpStatusCode.OK); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task ChangePassword_WrongCurrentPassword_Returns401() | ||
| { | ||
| var user = await TestHelper.CreateAndLoginUser(WebApplicationFactory, "chgpwdbad", "chgpwdbad@test.org", "CorrectPassword123#"); | ||
| using var client = TestHelper.CreateAuthenticatedClient(WebApplicationFactory, user.SessionToken); | ||
|
|
||
| var response = await client.PostAsync("/1/account/password", TestHelper.JsonContent(new | ||
| { | ||
| currentPassword = "WrongPassword!", | ||
| newPassword = "NewPassword456#" | ||
| })); | ||
|
|
||
| await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Forbidden); | ||
| } | ||
|
|
||
| // --- Change Username --- | ||
|
|
||
| [Test] | ||
| public async Task ChangeUsername_Success() | ||
| { | ||
| var user = await TestHelper.CreateAndLoginUser(WebApplicationFactory, "oldname", "chguname@test.org", "SecurePassword123#"); | ||
| using var client = TestHelper.CreateAuthenticatedClient(WebApplicationFactory, user.SessionToken); | ||
|
|
||
| var response = await client.PostAsync("/1/account/username", TestHelper.JsonContent(new | ||
| { | ||
| username = "newname" | ||
| })); | ||
|
|
||
| await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task ChangeUsername_Taken_Returns409() | ||
| { | ||
| var user1 = await TestHelper.CreateAndLoginUser(WebApplicationFactory, "takenname", "takenname@test.org", "SecurePassword123#"); | ||
|
hhvrc marked this conversation as resolved.
Outdated
|
||
| var user2 = await TestHelper.CreateAndLoginUser(WebApplicationFactory, "wantsname", "wantsname@test.org", "SecurePassword123#"); | ||
| using var client = TestHelper.CreateAuthenticatedClient(WebApplicationFactory, user2.SessionToken); | ||
|
|
||
| var response = await client.PostAsync("/1/account/username", TestHelper.JsonContent(new | ||
| { | ||
| username = "takenname" | ||
| })); | ||
|
|
||
| await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Conflict); | ||
| } | ||
|
|
||
| // --- Unauthenticated access --- | ||
|
|
||
| [Test] | ||
| public async Task ChangePassword_Unauthenticated_Returns401() | ||
| { | ||
| using var client = WebApplicationFactory.CreateClient(); | ||
|
|
||
| var response = await client.PostAsync("/1/account/password", TestHelper.JsonContent(new | ||
| { | ||
| currentPassword = "anything", | ||
| newPassword = "anything" | ||
| })); | ||
|
|
||
| await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Unauthorized); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task ChangeUsername_Unauthenticated_Returns401() | ||
| { | ||
| using var client = WebApplicationFactory.CreateClient(); | ||
|
|
||
| var response = await client.PostAsync("/1/account/username", TestHelper.JsonContent(new | ||
| { | ||
| username = "anything" | ||
| })); | ||
|
|
||
| await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Unauthorized); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.