Files
trakqr/src/api.Tests/WorkspaceEndpointTests.cs
Jonathan Bourdon e7d96f5508 feat: comprehensive app improvements with Pinia state management
Backend:
- Add API keys management (create, list, delete endpoints)
- Add email verification flow (verify, resend verification)
- Add account management (profile, change password, delete account)
- Add billing/Stripe integration (checkout, portal, webhooks)
- Add GeoIP service for analytics
- Add bulk link creation and link restore endpoints
- Add QR code analytics endpoint
- Add project description field with migration
- Add QR code name and logo support with migration
- Improve QR code generator with logo overlay support
- Add rate limiting middleware
- Update tests for new functionality

Frontend:
- Refactor entire app to use Pinia for state management
- Add auth store with initialization, login, register, logout
- Add workspace store with CRUD for workspaces, projects, links,
  QR codes, domains, assets, and analytics
- Add localStorage persistence for workspace selection
- Update App.vue with proper store initialization
- Update AppLayout.vue to use store methods instead of direct API
- Refactor Projects.vue and Domains.vue to use store state/actions
- Add VerifyEmail.vue for email verification flow
- Add ForgotPassword.vue and ResetPassword.vue
- Add Settings.vue with profile, password, API keys, danger zone
- Add QRCodeDetail.vue for QR code analytics
- Add Billing.vue for subscription management
- Expand api/client.js with all new API methods
- Add workspace change watchers for automatic data refresh

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 18:53:03 -05:00

198 lines
7.8 KiB
C#

using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using api.Features.Auth.Common;
using api.Features.Workspaces.Common;
using FluentAssertions;
namespace Api.Tests;
public class WorkspaceEndpointTests(ApiWebApplicationFactory factory)
: IClassFixture<ApiWebApplicationFactory>
{
private readonly HttpClient _client = factory.CreateClient();
private async Task<(string Token, Guid WorkspaceId)> GetAuthAndWorkspaceAsync(string email, bool upgradeToPro = false)
{
var response = await _client.PostAsJsonAsync("/auth/register", new { Email = email, Password = "password123" });
if (response.StatusCode == HttpStatusCode.Conflict)
{
response = await _client.PostAsJsonAsync("/auth/login", new { Email = email, Password = "password123" });
}
var result = await response.Content.ReadFromJsonAsync<AuthResponse>();
var token = result!.Token;
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var workspacesResponse = await _client.GetAsync("/workspaces");
var workspaces = await workspacesResponse.Content.ReadFromJsonAsync<WorkspaceListResponse>();
var workspaceId = workspaces!.Workspaces.First().Id;
if (upgradeToPro)
{
await factory.UpgradeWorkspaceToPro(workspaceId);
}
return (token, workspaceId);
}
private async Task<string> GetAuthTokenAsync(string email = "workspace-test@example.com")
{
var (token, _) = await GetAuthAndWorkspaceAsync(email);
return token;
}
[Fact]
public async Task ListWorkspaces_WithValidToken_ReturnsWorkspaces()
{
// Arrange
var token = await GetAuthTokenAsync("list-ws@example.com");
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
var response = await _client.GetAsync("/workspaces");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var result = await response.Content.ReadFromJsonAsync<WorkspaceListResponse>();
result.Should().NotBeNull();
result!.Workspaces.Should().HaveCountGreaterThanOrEqualTo(1); // Default workspace created on registration
}
[Fact]
public async Task ListWorkspaces_WithoutToken_ReturnsUnauthorized()
{
// Act
var response = await _client.GetAsync("/workspaces");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
[Fact]
public async Task CreateWorkspace_WithValidData_ReturnsCreated()
{
// Arrange - upgrade to Pro to allow creating additional workspaces
var (token, _) = await GetAuthAndWorkspaceAsync("create-ws@example.com", upgradeToPro: true);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
var response = await _client.PostAsJsonAsync("/workspaces", new { Name = "Test Workspace" });
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var result = await response.Content.ReadFromJsonAsync<WorkspaceResponse>();
result.Should().NotBeNull();
result!.Name.Should().Be("Test Workspace");
result.Plan.Should().Be("Free");
result.Id.Should().NotBeEmpty();
}
[Fact]
public async Task CreateWorkspace_WithEmptyName_ReturnsBadRequest()
{
// Arrange
var token = await GetAuthTokenAsync("create-ws-invalid@example.com");
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
var response = await _client.PostAsJsonAsync("/workspaces", new { Name = "" });
// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
[Fact]
public async Task GetWorkspace_WithValidId_ReturnsWorkspace()
{
// Arrange - use the default workspace (created on registration)
var (token, workspaceId) = await GetAuthAndWorkspaceAsync("get-ws@example.com");
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
var response = await _client.GetAsync($"/workspaces/{workspaceId}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var result = await response.Content.ReadFromJsonAsync<WorkspaceResponse>();
result!.Id.Should().Be(workspaceId);
result.Name.Should().NotBeNullOrEmpty();
}
[Fact]
public async Task GetWorkspace_WithInvalidId_ReturnsNotFound()
{
// Arrange
var token = await GetAuthTokenAsync("get-ws-invalid@example.com");
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
var response = await _client.GetAsync($"/workspaces/{Guid.NewGuid()}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
[Fact]
public async Task UpdateWorkspace_WithValidData_ReturnsUpdated()
{
// Arrange - use the default workspace (created on registration)
var (token, workspaceId) = await GetAuthAndWorkspaceAsync("update-ws@example.com");
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
var response = await _client.PutAsJsonAsync($"/workspaces/{workspaceId}", new { Name = "Updated Name" });
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var result = await response.Content.ReadFromJsonAsync<WorkspaceResponse>();
result!.Name.Should().Be("Updated Name");
}
[Fact]
public async Task DeleteWorkspace_WithValidId_ReturnsSuccess()
{
// Arrange - upgrade to Pro to allow creating additional workspaces
var (token, _) = await GetAuthAndWorkspaceAsync("delete-ws@example.com", upgradeToPro: true);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var createResponse = await _client.PostAsJsonAsync("/workspaces", new { Name = "To Delete" });
var created = await createResponse.Content.ReadFromJsonAsync<WorkspaceResponse>();
// Act
var response = await _client.DeleteAsync($"/workspaces/{created!.Id}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
// Verify it's deleted
var getResponse = await _client.GetAsync($"/workspaces/{created.Id}");
getResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
[Fact]
public async Task Workspace_CannotAccessOtherUsersWorkspace()
{
// Arrange - Create two users
var token1 = await GetAuthTokenAsync("user1-ws@example.com");
var token2 = await GetAuthTokenAsync("user2-ws@example.com");
// Create workspace as user1
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token1);
var createResponse = await _client.PostAsJsonAsync("/workspaces", new { Name = "User1 Workspace" });
var created = await createResponse.Content.ReadFromJsonAsync<WorkspaceResponse>();
// Try to access as user2
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token2);
// Act
var getResponse = await _client.GetAsync($"/workspaces/{created!.Id}");
var updateResponse = await _client.PutAsJsonAsync($"/workspaces/{created.Id}", new { Name = "Hacked" });
var deleteResponse = await _client.DeleteAsync($"/workspaces/{created.Id}");
// Assert - All should return NotFound (not exposing existence)
getResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
updateResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
deleteResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
}