//----------------------------------------------------------------------- // // Copyright (c) aliasvault. All rights reserved. // Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. // //----------------------------------------------------------------------- /* * Note: this file is used for E2E testing purposes only. It contains test endpoints that are used by * E2E tests (browser extension Playwright tests, mobile app UI tests) to manipulate server state. * * These endpoints are only available in DEBUG builds. */ namespace AliasVault.Api.Controllers.Tests; using AliasServerDb; using AliasVault.Api.Controllers.Abstracts; using Asp.Versioning; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; #if DEBUG /// /// Test controller that contains test endpoints for E2E testing purposes. /// All endpoints are hidden from Swagger and only work in Development environment. /// /// UserManager instance. /// IWebHostEnvironment instance. /// DbContext factory instance. [ApiVersion("1")] public class TestController( UserManager userManager, IWebHostEnvironment environment, IAliasServerDbContextFactory dbContextFactory) : AuthenticatedRequestController(userManager) { /// /// Authenticated test request. Used to verify authentication is working. /// /// Static OK. [HttpGet("")] public IActionResult TestCall() { if (!environment.IsDevelopment()) { return NotFound(); } return Ok(); } /// /// Test request that throws an exception. Used for testing error handling. /// /// Never returns - always throws. [AllowAnonymous] [HttpGet("Error")] public IActionResult TestCallError() { if (!environment.IsDevelopment()) { return NotFound(); } // Throw an exception here to test error handling. throw new ArgumentException("Test error"); } /// /// Delete the newest vault revisions for the current user. /// Used for testing RPO (Recovery Point Objective) recovery scenarios. /// /// Number of newest revisions to delete. /// OK with the number of deleted revisions, or NotFound in production. [HttpDelete("vault-revisions/{count:int}")] public async Task DeleteVaultRevisions(int count) { if (!environment.IsDevelopment()) { return NotFound(); } if (count <= 0) { return BadRequest("Count must be greater than 0"); } var user = await GetCurrentUserAsync(); if (user == null) { return Unauthorized(); } await using var context = await dbContextFactory.CreateDbContextAsync(); // Get the newest revisions to delete var revisionsToDelete = await context.Vaults .Where(v => v.UserId == user.Id) .OrderByDescending(v => v.RevisionNumber) .Take(count) .ToListAsync(); if (revisionsToDelete.Count == 0) { return Ok(new { deleted = 0, message = "No revisions found to delete" }); } // Delete the revisions context.Vaults.RemoveRange(revisionsToDelete); var deletedCount = await context.SaveChangesAsync(); return Ok(new { deleted = revisionsToDelete.Count, deletedRevisions = revisionsToDelete.Select(r => r.RevisionNumber).ToList(), message = $"Deleted {revisionsToDelete.Count} vault revision(s)", }); } /// /// Get vault revision information for the current user. /// Used for E2E tests to verify vault state. /// /// Vault revision information. [HttpGet("vault-revisions")] public async Task GetVaultRevisions() { if (!environment.IsDevelopment()) { return NotFound(); } var user = await GetCurrentUserAsync(); if (user == null) { return Unauthorized(); } await using var context = await dbContextFactory.CreateDbContextAsync(); var revisions = await context.Vaults .Where(v => v.UserId == user.Id) .OrderByDescending(v => v.RevisionNumber) .Select(v => new { v.RevisionNumber, v.CreatedAt, v.UpdatedAt, }) .ToListAsync(); return Ok(new { count = revisions.Count, currentRevision = revisions.FirstOrDefault()?.RevisionNumber ?? 0, revisions, }); } /// /// Block the current user's account. /// Used for testing forced logout scenarios. /// After calling this, any subsequent API calls to /status will return 401. /// /// OK with the blocked status. [HttpPost("block-user")] public async Task BlockUser() { if (!environment.IsDevelopment()) { return NotFound(); } var user = await GetCurrentUserAsync(); if (user == null) { return Unauthorized(); } await using var context = await dbContextFactory.CreateDbContextAsync(); // Find the user in the new context and block them var dbUser = await context.AliasVaultUsers.FindAsync(user.Id); if (dbUser == null) { return NotFound("User not found"); } dbUser.Blocked = true; await context.SaveChangesAsync(); return Ok(new { blocked = true, message = $"User {user.UserName} has been blocked", }); } /// /// Unblock the current user's account. /// Used for testing - allows re-enabling the account after forced logout test. /// Note: This uses the JWT token which is still valid even for blocked users, /// so the user can unblock themselves for testing purposes. /// /// OK with the blocked status. [HttpPost("unblock-user")] public async Task UnblockUser() { if (!environment.IsDevelopment()) { return NotFound(); } var user = await GetCurrentUserAsync(); if (user == null) { return Unauthorized(); } await using var context = await dbContextFactory.CreateDbContextAsync(); // Find the user in the new context and unblock them var dbUser = await context.AliasVaultUsers.FindAsync(user.Id); if (dbUser == null) { return NotFound("User not found"); } dbUser.Blocked = false; await context.SaveChangesAsync(); return Ok(new { blocked = false, message = $"User {user.UserName} has been unblocked", }); } /// /// Get vault revision information for a user by username. /// Anonymous endpoint for E2E tests that cannot access auth tokens. /// Only available in DEBUG builds. /// /// The username to look up. /// Vault revision information. [AllowAnonymous] [HttpGet("vault-revisions/by-username/{username}")] public async Task GetVaultRevisionsByUsername(string username) { if (!environment.IsDevelopment()) { return NotFound(); } await using var context = await dbContextFactory.CreateDbContextAsync(); var user = await context.AliasVaultUsers .FirstOrDefaultAsync(u => u.NormalizedUserName == username.ToUpperInvariant()); if (user == null) { return NotFound($"User '{username}' not found"); } var revisions = await context.Vaults .Where(v => v.UserId == user.Id) .OrderByDescending(v => v.RevisionNumber) .Select(v => new { v.RevisionNumber, v.CreatedAt, v.UpdatedAt, }) .ToListAsync(); return Ok(new { count = revisions.Count, currentRevision = revisions.FirstOrDefault()?.RevisionNumber ?? 0, revisions, }); } /// /// Delete the newest vault revisions for a user by username. /// Anonymous endpoint for E2E tests that cannot access auth tokens. /// Only available in DEBUG builds. /// /// The username to look up. /// Number of newest revisions to delete. /// OK with the number of deleted revisions. [AllowAnonymous] [HttpDelete("vault-revisions/by-username/{username}/{count:int}")] public async Task DeleteVaultRevisionsByUsername(string username, int count) { if (!environment.IsDevelopment()) { return NotFound(); } if (count <= 0) { return BadRequest("Count must be greater than 0"); } await using var context = await dbContextFactory.CreateDbContextAsync(); var user = await context.AliasVaultUsers .FirstOrDefaultAsync(u => u.NormalizedUserName == username.ToUpperInvariant()); if (user == null) { return NotFound($"User '{username}' not found"); } // Get the newest revisions to delete var revisionsToDelete = await context.Vaults .Where(v => v.UserId == user.Id) .OrderByDescending(v => v.RevisionNumber) .Take(count) .ToListAsync(); if (revisionsToDelete.Count == 0) { return Ok(new { deleted = 0, message = "No revisions found to delete" }); } // Delete the revisions context.Vaults.RemoveRange(revisionsToDelete); await context.SaveChangesAsync(); return Ok(new { deleted = revisionsToDelete.Count, deletedRevisions = revisionsToDelete.Select(r => r.RevisionNumber).ToList(), message = $"Deleted {revisionsToDelete.Count} vault revision(s)", }); } /// /// Block a user's account by username. /// Anonymous endpoint for E2E tests that cannot access auth tokens. /// Only available in DEBUG builds. /// /// The username to block. /// OK with the blocked status. [AllowAnonymous] [HttpPost("block-user/by-username/{username}")] public async Task BlockUserByUsername(string username) { if (!environment.IsDevelopment()) { return NotFound(); } await using var context = await dbContextFactory.CreateDbContextAsync(); var user = await context.AliasVaultUsers .FirstOrDefaultAsync(u => u.NormalizedUserName == username.ToUpperInvariant()); if (user == null) { return NotFound($"User '{username}' not found"); } user.Blocked = true; await context.SaveChangesAsync(); return Ok(new { blocked = true, message = $"User {user.UserName} has been blocked", }); } /// /// Unblock a user's account by username. /// Anonymous endpoint for E2E tests that cannot access auth tokens. /// Only available in DEBUG builds. /// /// The username to unblock. /// OK with the blocked status. [AllowAnonymous] [HttpPost("unblock-user/by-username/{username}")] public async Task UnblockUserByUsername(string username) { if (!environment.IsDevelopment()) { return NotFound(); } await using var context = await dbContextFactory.CreateDbContextAsync(); var user = await context.AliasVaultUsers .FirstOrDefaultAsync(u => u.NormalizedUserName == username.ToUpperInvariant()); if (user == null) { return NotFound($"User '{username}' not found"); } user.Blocked = false; await context.SaveChangesAsync(); return Ok(new { blocked = false, message = $"User {user.UserName} has been unblocked", }); } } #endif