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>
198 lines
7.8 KiB
C#
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);
|
|
}
|
|
}
|