Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions API.IntegrationTests/API.IntegrationTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<!-- NuGet packages -->
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" />
<PackageReference Include="Testcontainers.PostgreSql" />
<PackageReference Include="Testcontainers.Redis" />
<PackageReference Include="TUnit" />
Expand Down
166 changes: 166 additions & 0 deletions API.IntegrationTests/Helpers/TestHelper.cs
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);
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,17 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage

private class CloudflareTurnstileVerifyResponseDto
{
[System.Text.Json.Serialization.JsonPropertyName("success")]
public bool Success { get; init; }
[System.Text.Json.Serialization.JsonPropertyName("error-codes")]
public required string[] ErrorCodes { get; init; }
[System.Text.Json.Serialization.JsonPropertyName("challenge_ts")]
public DateTime ChallengeTs { get; init; }
[System.Text.Json.Serialization.JsonPropertyName("hostname")]
public required string Hostname { get; init; }
[System.Text.Json.Serialization.JsonPropertyName("action")]
public required string Action { get; init; }
[System.Text.Json.Serialization.JsonPropertyName("cdata")]
public required string Cdata { get; init; }
}
}
116 changes: 116 additions & 0 deletions API.IntegrationTests/Tests/AccountAuthenticatedTests.cs
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()
Comment thread
hhvrc marked this conversation as resolved.
Outdated
{
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#");
Comment thread
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);
}
}
Loading
Loading