mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-19 05:47:43 -04:00
Add server gap RPO test iOS (#1404)
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 70;
|
||||
objectVersion = 60;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -272,7 +272,7 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
CEE9098F2DA548C7008D568F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
CEE9098F2DA548C7008D568F /* Exceptions for "Autofill" folder in "Autofill" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
@@ -282,13 +282,84 @@
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
CE59C7602E4F47FD0024A246 /* AliasVaultUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = AliasVaultUITests; sourceTree = "<group>"; };
|
||||
CE77825E2EA1822400A75E6F /* VaultUtils */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultUtils; sourceTree = "<group>"; };
|
||||
CEE480882DBE86DC00F4A367 /* VaultStoreKit */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultStoreKit; sourceTree = "<group>"; };
|
||||
CEE480972DBE86DD00F4A367 /* VaultStoreKitTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultStoreKitTests; sourceTree = "<group>"; };
|
||||
CEE4816B2DBE8AC800F4A367 /* VaultUI */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultUI; sourceTree = "<group>"; };
|
||||
CEE482AB2DBE8EFE00F4A367 /* VaultModels */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultModels; sourceTree = "<group>"; };
|
||||
CEE909812DA548C7008D568F /* Autofill */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (CEE9098F2DA548C7008D568F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Autofill; sourceTree = "<group>"; };
|
||||
CE59C7602E4F47FD0024A246 /* AliasVaultUITests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = AliasVaultUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE77825E2EA1822400A75E6F /* VaultUtils */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultUtils;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE480882DBE86DC00F4A367 /* VaultStoreKit */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultStoreKit;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE480972DBE86DD00F4A367 /* VaultStoreKitTests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultStoreKitTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE4816B2DBE8AC800F4A367 /* VaultUI */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultUI;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE482AB2DBE8EFE00F4A367 /* VaultModels */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultModels;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE909812DA548C7008D568F /* Autofill */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
CEE9098F2DA548C7008D568F /* Exceptions for "Autofill" folder in "Autofill" target */,
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = Autofill;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -448,7 +519,7 @@
|
||||
path = Generated;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6114AD0A31612EA499210371 = {
|
||||
6114AD0A31612EA499210371 /* */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8F8258CE46969A87661E0302 /* RustCore.swift */,
|
||||
@@ -484,7 +555,7 @@
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
D65327D7A22EEC0BE12398D9 /* Pods */,
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
6114AD0A31612EA499210371,
|
||||
6114AD0A31612EA499210371 /* */,
|
||||
CE5212B62F0C061800F4C835 /* Recovered References */,
|
||||
3AD00C41DD88E219A26C2E41 /* RustCoreFramework */,
|
||||
);
|
||||
@@ -1642,7 +1713,10 @@
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
@@ -1696,7 +1770,10 @@
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
USE_HERMES = true;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -400,4 +400,90 @@ enum TestUserRegistration {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Helpers (DEV API Endpoints)
|
||||
|
||||
/// Delete the newest vault revisions for the authenticated user.
|
||||
/// This endpoint only works in development mode.
|
||||
/// Used for testing RPO (Recovery Point Objective) recovery scenarios.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - count: Number of newest revisions to delete
|
||||
/// - token: Authentication token
|
||||
/// - apiBaseUrl: Optional API base URL (defaults to apiUrl)
|
||||
/// - Returns: Number of deleted revisions
|
||||
static func deleteVaultRevisions(
|
||||
count: Int,
|
||||
token: String,
|
||||
apiBaseUrl: String? = nil
|
||||
) async throws -> Int {
|
||||
let url = (apiBaseUrl ?? apiUrl).trimmingCharacters(in: CharacterSet(charactersIn: "/")) + "/v1/"
|
||||
|
||||
var request = URLRequest(url: URL(string: "\(url)Test/vault-revisions/\(count)")!)
|
||||
request.httpMethod = "DELETE"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw NSError(domain: "TestUserRegistration", code: 10,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Invalid response"])
|
||||
}
|
||||
|
||||
guard httpResponse.statusCode == 200 else {
|
||||
let errorText = String(data: data, encoding: .utf8) ?? "Unknown error"
|
||||
throw NSError(domain: "TestUserRegistration", code: httpResponse.statusCode,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Failed to delete vault revisions: \(errorText)"])
|
||||
}
|
||||
|
||||
// Parse response to get deleted count
|
||||
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let deleted = json["deleted"] as? Int {
|
||||
return deleted
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
/// Get vault revision information for the authenticated user.
|
||||
/// This endpoint only works in development mode.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - token: Authentication token
|
||||
/// - apiBaseUrl: Optional API base URL (defaults to apiUrl)
|
||||
/// - Returns: Tuple of (count, currentRevision)
|
||||
static func getVaultRevisions(
|
||||
token: String,
|
||||
apiBaseUrl: String? = nil
|
||||
) async throws -> (count: Int, currentRevision: Int) {
|
||||
let url = (apiBaseUrl ?? apiUrl).trimmingCharacters(in: CharacterSet(charactersIn: "/")) + "/v1/"
|
||||
|
||||
var request = URLRequest(url: URL(string: "\(url)Test/vault-revisions")!)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw NSError(domain: "TestUserRegistration", code: 10,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Invalid response"])
|
||||
}
|
||||
|
||||
guard httpResponse.statusCode == 200 else {
|
||||
let errorText = String(data: data, encoding: .utf8) ?? "Unknown error"
|
||||
throw NSError(domain: "TestUserRegistration", code: httpResponse.statusCode,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Failed to get vault revisions: \(errorText)"])
|
||||
}
|
||||
|
||||
// Parse response
|
||||
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let count = json["count"] as? Int,
|
||||
let currentRevision = json["currentRevision"] as? Int {
|
||||
return (count, currentRevision)
|
||||
}
|
||||
|
||||
return (0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* Note: this file is used for E2E testing purposes only. It contains test endpoints that are called by pages on
|
||||
* the client for testing purposes. Because certain endpoints that simulate exceptions are prone to Denial-Of-Service
|
||||
* attack surfaces we don't include this file in the production build.
|
||||
* 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.
|
||||
*
|
||||
* Security measures:
|
||||
* 1. All endpoints check IsDevelopment() and return 404 in production
|
||||
* 2. All endpoints are hidden from Swagger documentation via ApiExplorerSettings
|
||||
*/
|
||||
|
||||
namespace AliasVault.Api.Controllers.Tests;
|
||||
@@ -19,33 +22,142 @@ using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Test controller that contains test endpoints called by pages on the client for E2E testing purposes.
|
||||
/// Test controller that contains test endpoints for E2E testing purposes.
|
||||
/// All endpoints are hidden from Swagger and only work in Development environment.
|
||||
/// </summary>
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
/// <param name="environment">IWebHostEnvironment instance.</param>
|
||||
/// <param name="dbContextFactory">DbContext factory instance.</param>
|
||||
[ApiVersion("1")]
|
||||
public class TestController(UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class TestController(
|
||||
UserManager<AliasVaultUser> userManager,
|
||||
IWebHostEnvironment environment,
|
||||
IAliasServerDbContextFactory dbContextFactory) : AuthenticatedRequestController(userManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// Authenticated test request.
|
||||
/// Authenticated test request. Used to verify authentication is working.
|
||||
/// </summary>
|
||||
/// <returns>Static OK.</returns>
|
||||
[HttpGet("")]
|
||||
public IActionResult TestCall()
|
||||
{
|
||||
if (!environment.IsDevelopment())
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test request that throws an exception. Used for testing error handling.
|
||||
/// </summary>
|
||||
/// <returns>Static OK.</returns>
|
||||
/// <returns>Never returns - always throws.</returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("Error")]
|
||||
public IActionResult TestCallError()
|
||||
{
|
||||
if (!environment.IsDevelopment())
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
// Throw an exception here to test error handling.
|
||||
throw new ArgumentException("Test error");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete the newest vault revisions for the current user.
|
||||
/// Used for testing RPO (Recovery Point Objective) recovery scenarios.
|
||||
/// </summary>
|
||||
/// <param name="count">Number of newest revisions to delete.</param>
|
||||
/// <returns>OK with the number of deleted revisions, or NotFound in production.</returns>
|
||||
[HttpDelete("vault-revisions/{count:int}")]
|
||||
public async Task<IActionResult> 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)",
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get vault revision information for the current user.
|
||||
/// Used for E2E tests to verify vault state.
|
||||
/// </summary>
|
||||
/// <returns>Vault revision information.</returns>
|
||||
[HttpGet("vault-revisions")]
|
||||
public async Task<IActionResult> 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user