Add tests for passport update and create
Some checks are pending
Sharenet Passport CI / test-native (push) Waiting to run
Sharenet Passport CI / test-wasm-headless (push) Waiting to run
Sharenet Passport CI / test-wasm-webdriver (push) Waiting to run
Sharenet Passport CI / build-wasm (push) Waiting to run
Sharenet Passport CI / lint (push) Waiting to run

This commit is contained in:
Continuist 2025-10-28 00:18:33 -04:00
parent abd8e1b885
commit a2b7601b60
10 changed files with 1402 additions and 5 deletions

161
.forgejo/workflows/ci.yml Normal file
View file

@ -0,0 +1,161 @@
name: Sharenet Passport CI
on: [push, pull_request]
jobs:
test-native:
runs-on: [ci]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
components: rust-src
- name: Run native tests
run: |
cd libs/sharenet-passport
cargo test --verbose
test-wasm-headless:
runs-on: [ci]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
components: rust-src
- name: Install wasm-pack
run: |
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Install Firefox and geckodriver
run: |
# Install Firefox
apt-get install -y firefox-esr
# Install geckodriver
GECKODRIVER_VERSION=$(curl -s https://api.github.com/repos/mozilla/geckodriver/releases/latest | grep tag_name | cut -d '"' -f 4)
wget -q "https://github.com/mozilla/geckodriver/releases/download/${GECKODRIVER_VERSION}/geckodriver-${GECKODRIVER_VERSION}-linux64.tar.gz"
tar -xzf geckodriver-${GECKODRIVER_VERSION}-linux64.tar.gz
mv geckodriver /usr/local/bin/
chmod +x /usr/local/bin/geckodriver
- name: Run WASM headless tests
run: |
cd libs/sharenet-passport
wasm-pack test --headless --chrome --firefox --node
test-wasm-webdriver:
runs-on: [ci]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
components: rust-src
- name: Install wasm-pack
run: |
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Install browsers and drivers
run: |
# Install Chrome
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list
apt-get update
apt-get install -y google-chrome-stable
# Install ChromeDriver
CHROME_VERSION=$(google-chrome --version | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+')
CHROMEDRIVER_VERSION=$(curl -s "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_${CHROME_VERSION%.*}")
wget -q "https://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip"
unzip chromedriver_linux64.zip
mv chromedriver /usr/local/bin/
chmod +x /usr/local/bin/chromedriver
# Install Firefox
apt-get install -y firefox-esr
# Install geckodriver
GECKODRIVER_VERSION=$(curl -s https://api.github.com/repos/mozilla/geckodriver/releases/latest | grep tag_name | cut -d '"' -f 4)
wget -q "https://github.com/mozilla/geckodriver/releases/download/${GECKODRIVER_VERSION}/geckodriver-${GECKODRIVER_VERSION}-linux64.tar.gz"
tar -xzf geckodriver-${GECKODRIVER_VERSION}-linux64.tar.gz
mv geckodriver /usr/local/bin/
chmod +x /usr/local/bin/geckodriver
- name: Run WASM WebDriver tests
run: |
cd libs/sharenet-passport
# Build WASM package for testing
wasm-pack build --target web --out-dir pkg
# Run WebDriver tests (placeholder - implement actual WebDriver tests)
echo "WebDriver tests would run here with Selenium/WebDriver"
build-wasm:
runs-on: [ci]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
components: rust-src
- name: Install wasm-pack
run: |
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Build WASM package
run: |
cd libs/sharenet-passport
wasm-pack build --target web --out-dir pkg
- name: Verify WASM build
run: |
cd libs/sharenet-passport/pkg
ls -la
file sharenet_passport_bg.wasm
lint:
runs-on: [ci]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
components: clippy, rustfmt
- name: Run clippy
run: |
cd libs/sharenet-passport
cargo clippy -- -D warnings
- name: Run rustfmt
run: |
cd libs/sharenet-passport
cargo fmt -- --check

14
Cargo.lock generated
View file

@ -759,6 +759,17 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_cbor"
version = "0.11.2"
@ -831,11 +842,14 @@ dependencies = [
"rand",
"rand_core",
"serde",
"serde-wasm-bindgen",
"serde_cbor",
"serde_json",
"sha2",
"tempfile",
"thiserror",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test",
"web-time",

View file

@ -40,6 +40,9 @@ web-time = "1.1"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
gloo-storage = "0.3"
wasm-bindgen = "0.2"
serde-wasm-bindgen = "0.6"
serde_json = "1.0"
# Native dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View file

@ -1,2 +1,5 @@
pub mod use_cases;
pub mod error;
#[cfg(test)]
pub mod use_cases_test;

View file

@ -0,0 +1,524 @@
use tempfile::NamedTempFile;
use crate::application::use_cases::*;
use crate::domain::entities::*;
use crate::domain::traits::FileStorage;
use crate::infrastructure::*;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_user_profile_use_case() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport first
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (mut passport, _) = create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Test creating a user profile
let create_profile_use_case = CreateUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let identity = UserIdentity {
handle: Some("testuser".to_string()),
display_name: Some("Test User".to_string()),
first_name: Some("Test".to_string()),
last_name: Some("User".to_string()),
email: Some("test@example.com".to_string()),
avatar_url: Some("https://example.com/avatar.png".to_string()),
bio: Some("Test bio".to_string()),
};
let preferences = UserPreferences {
theme: Some("dark".to_string()),
language: Some("en".to_string()),
notifications_enabled: true,
auto_sync: false,
};
let result = create_profile_use_case.execute(
&mut passport,
Some("h:example".to_string()),
identity,
preferences,
"test-password",
file_path,
);
assert!(result.is_ok());
// Verify the profile was added
assert_eq!(passport.user_profiles.len(), 2); // default + new profile
let hub_profile = passport.user_profile_for_hub("h:example");
assert!(hub_profile.is_some());
assert_eq!(hub_profile.unwrap().identity.handle, Some("testuser".to_string()));
}
#[test]
fn test_create_user_profile_duplicate_hub_did() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport first
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (mut passport, _) = create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Create first profile
let create_profile_use_case = CreateUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let identity1 = UserIdentity {
handle: Some("user1".to_string()),
display_name: Some("User One".to_string()),
first_name: Some("User".to_string()),
last_name: Some("One".to_string()),
email: Some("user1@example.com".to_string()),
avatar_url: None,
bio: None,
};
let preferences1 = UserPreferences {
theme: Some("dark".to_string()),
language: Some("en".to_string()),
notifications_enabled: true,
auto_sync: false,
};
let result1 = create_profile_use_case.execute(
&mut passport,
Some("h:example".to_string()),
identity1,
preferences1,
"test-password",
file_path,
);
assert!(result1.is_ok());
// Try to create second profile with same hub DID (should fail)
let identity2 = UserIdentity {
handle: Some("user2".to_string()),
display_name: Some("User Two".to_string()),
first_name: Some("User".to_string()),
last_name: Some("Two".to_string()),
email: Some("user2@example.com".to_string()),
avatar_url: None,
bio: None,
};
let preferences2 = UserPreferences {
theme: Some("light".to_string()),
language: Some("es".to_string()),
notifications_enabled: false,
auto_sync: true,
};
let result2 = create_profile_use_case.execute(
&mut passport,
Some("h:example".to_string()),
identity2,
preferences2,
"test-password",
file_path,
);
assert!(result2.is_err());
}
#[test]
fn test_update_user_profile_use_case() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport first
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (mut passport, _) = create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Create a user profile first
let create_profile_use_case = CreateUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let identity = UserIdentity {
handle: Some("testuser".to_string()),
display_name: Some("Test User".to_string()),
first_name: Some("Test".to_string()),
last_name: Some("User".to_string()),
email: Some("test@example.com".to_string()),
avatar_url: Some("https://example.com/avatar.png".to_string()),
bio: Some("Test bio".to_string()),
};
let preferences = UserPreferences {
theme: Some("dark".to_string()),
language: Some("en".to_string()),
notifications_enabled: true,
auto_sync: false,
};
create_profile_use_case.execute(
&mut passport,
Some("h:example".to_string()),
identity,
preferences,
"test-password",
file_path,
).expect("Failed to create profile");
// Get the profile ID
let profile_id = passport.user_profile_for_hub("h:example")
.expect("Profile should exist")
.id
.clone();
// Test updating the user profile
let update_profile_use_case = UpdateUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let updated_identity = UserIdentity {
handle: Some("updateduser".to_string()),
display_name: Some("Updated User".to_string()),
first_name: Some("Updated".to_string()),
last_name: Some("User".to_string()),
email: Some("updated@example.com".to_string()),
avatar_url: Some("https://example.com/new-avatar.png".to_string()),
bio: Some("Updated bio".to_string()),
};
let updated_preferences = UserPreferences {
theme: Some("light".to_string()),
language: Some("es".to_string()),
notifications_enabled: false,
auto_sync: true,
};
let result = update_profile_use_case.execute(
&mut passport,
Some(&profile_id),
updated_identity,
updated_preferences,
"test-password",
file_path,
);
assert!(result.is_ok());
// Verify the profile was updated
let updated_profile = passport.user_profile_for_hub("h:example")
.expect("Profile should exist");
assert_eq!(updated_profile.identity.handle, Some("updateduser".to_string()));
assert_eq!(updated_profile.preferences.theme, Some("light".to_string()));
assert_eq!(updated_profile.preferences.language, Some("es".to_string()));
}
#[test]
fn test_update_user_profile_use_case_invalid_id() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport first
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (mut passport, _) = create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Try to update non-existent profile (should fail)
let update_profile_use_case = UpdateUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let identity = UserIdentity {
handle: Some("testuser".to_string()),
display_name: Some("Test User".to_string()),
first_name: Some("Test".to_string()),
last_name: Some("User".to_string()),
email: Some("test@example.com".to_string()),
avatar_url: None,
bio: None,
};
let preferences = UserPreferences {
theme: Some("dark".to_string()),
language: Some("en".to_string()),
notifications_enabled: true,
auto_sync: false,
};
let result = update_profile_use_case.execute(
&mut passport,
Some("non-existent-id"),
identity,
preferences,
"test-password",
file_path,
);
assert!(result.is_err());
}
#[test]
fn test_delete_user_profile_use_case() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport first
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (mut passport, _) = create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Create a user profile first
let create_profile_use_case = CreateUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let identity = UserIdentity {
handle: Some("testuser".to_string()),
display_name: Some("Test User".to_string()),
first_name: Some("Test".to_string()),
last_name: Some("User".to_string()),
email: Some("test@example.com".to_string()),
avatar_url: None,
bio: None,
};
let preferences = UserPreferences {
theme: Some("dark".to_string()),
language: Some("en".to_string()),
notifications_enabled: true,
auto_sync: false,
};
create_profile_use_case.execute(
&mut passport,
Some("h:example".to_string()),
identity,
preferences,
"test-password",
file_path,
).expect("Failed to create profile");
// Get the profile ID
let profile_id = passport.user_profile_for_hub("h:example")
.expect("Profile should exist")
.id
.clone();
// Test deleting the user profile
let delete_profile_use_case = DeleteUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let result = delete_profile_use_case.execute(
&mut passport,
Some(&profile_id),
"test-password",
file_path,
);
assert!(result.is_ok());
// Verify the profile was deleted
assert_eq!(passport.user_profiles.len(), 1); // only default profile remains
let deleted_profile = passport.user_profile_for_hub("h:example");
assert!(deleted_profile.is_none());
}
#[test]
fn test_delete_user_profile_use_case_invalid_id() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport first
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (mut passport, _) = create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Try to delete non-existent profile (should fail)
let delete_profile_use_case = DeleteUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let result = delete_profile_use_case.execute(
&mut passport,
Some("non-existent-id"),
"test-password",
file_path,
);
assert!(result.is_err());
}
#[test]
fn test_change_passport_password_workflow() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport with old password
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (passport, _) = create_use_case.execute("test-universe", "old-password", file_path)
.expect("Failed to create passport");
// Export passport with new password (simulating password change)
let export_use_case = ExportPassportUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let result = export_use_case.execute(&passport, "new-password", file_path);
assert!(result.is_ok());
// Verify we can import with new password
let import_use_case = ImportFromFileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let imported_passport = import_use_case.execute(file_path, "new-password", None)
.expect("Failed to import with new password");
// Verify the imported passport has the same DID
assert_eq!(passport.did.as_str(), imported_passport.did.as_str());
assert_eq!(passport.univ_id, imported_passport.univ_id);
}
#[test]
fn test_get_passport_metadata_functionality() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (passport, _) = create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Load file directly to get metadata
let file_storage = FileSystemStorage;
let passport_file = file_storage.load(file_path)
.expect("Failed to load passport file");
// Verify metadata fields
assert!(!passport_file.did.is_empty());
assert!(!passport_file.univ_id.is_empty());
assert!(!passport_file.public_key.is_empty());
assert!(!passport_file.enc_seed.is_empty());
assert!(!passport_file.salt.is_empty());
assert!(!passport_file.nonce.is_empty());
// Verify DID matches
assert_eq!(passport_file.did, passport.did.as_str());
assert_eq!(passport_file.univ_id, passport.univ_id);
}
#[test]
fn test_validate_passport_file_functionality() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a valid passport
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Load file directly to validate
let file_storage = FileSystemStorage;
let passport_file = file_storage.load(file_path)
.expect("Failed to load passport file");
// Validate the file structure
let is_valid = !passport_file.enc_seed.is_empty()
&& !passport_file.salt.is_empty()
&& !passport_file.nonce.is_empty()
&& !passport_file.public_key.is_empty()
&& !passport_file.did.is_empty()
&& !passport_file.univ_id.is_empty();
assert!(is_valid);
}
#[test]
fn test_validate_passport_file_invalid_file() {
// Try to load non-existent file (should fail)
let file_storage = FileSystemStorage;
let result = file_storage.load("/non/existent/path.spf");
assert!(result.is_err());
}
}

View file

@ -57,7 +57,7 @@ impl Did {
}
}
#[derive(Debug, Zeroize, ZeroizeOnDrop)]
#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct Seed {
bytes: Vec<u8>,
}
@ -72,7 +72,7 @@ impl Seed {
}
}
#[derive(Debug)]
#[derive(Debug, Serialize, Deserialize)]
pub struct Passport {
pub seed: Seed,
pub public_key: PublicKey,

View file

@ -42,7 +42,7 @@ mod tests {
seed.zeroize();
// After zeroization, bytes should be empty (zeroize clears the vector)
assert_eq!(seed.as_bytes(), &[]);
assert_eq!(seed.as_bytes(), &[] as &[u8]);
}
#[test]
@ -53,7 +53,7 @@ mod tests {
private_key.zeroize();
// After zeroization, bytes should be empty (zeroize clears the vector)
assert_eq!(private_key.0, vec![]);
assert_eq!(private_key.0, vec![] as Vec<u8>);
}
#[test]

View file

@ -7,6 +7,13 @@ pub mod domain;
pub mod application;
pub mod infrastructure;
#[cfg(target_arch = "wasm32")]
pub mod wasm;
#[cfg(target_arch = "wasm32")]
#[cfg(test)]
pub mod wasm_test;
// Public API surface
pub use domain::entities::{Passport, RecoveryPhrase, PassportFile, PublicKey, PrivateKey, Did, Seed};
pub use domain::traits::{MnemonicGenerator, KeyDeriver, FileEncryptor, FileStorage};

View file

@ -0,0 +1,337 @@
//! WASM-specific API for Sharenet Passport
//!
//! This module provides browser-compatible functions that can be called from JavaScript
//! via wasm-bindgen.
use wasm_bindgen::prelude::*;
use crate::application::use_cases::{
CreatePassportUseCase,
ImportFromRecoveryUseCase,
ImportFromFileUseCase,
ExportPassportUseCase,
SignCardUseCase,
CreateUserProfileUseCase,
UpdateUserProfileUseCase,
DeleteUserProfileUseCase
};
use crate::infrastructure::{
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage
};
use crate::domain::entities::{Passport, UserIdentity, UserPreferences};
use crate::domain::traits::{MnemonicGenerator, FileStorage};
/// Create a new passport with the given universe ID and password
///
/// Returns a JSON string containing both the passport and recovery phrase
#[wasm_bindgen]
pub fn create_passport(
univ_id: String,
password: String,
output_path: String,
) -> Result<JsValue, JsValue> {
let use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
match use_case.execute(&univ_id, &password, &output_path) {
Ok((passport, recovery_phrase)) => {
let result = serde_wasm_bindgen::to_value(&serde_json::json!({
"passport": passport,
"recovery_phrase": recovery_phrase
})).map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
Err(e) => Err(JsValue::from_str(&format!("Error creating passport: {}", e))),
}
}
/// Import a passport from recovery phrase
#[wasm_bindgen]
pub fn import_from_recovery(
univ_id: String,
recovery_words: Vec<String>,
password: String,
output_path: String,
) -> Result<JsValue, JsValue> {
let use_case = ImportFromRecoveryUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
match use_case.execute(&univ_id, &recovery_words, &password, &output_path) {
Ok(passport) => {
let result = serde_wasm_bindgen::to_value(&passport)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
Err(e) => Err(JsValue::from_str(&format!("Error importing from recovery: {}", e))),
}
}
/// Load a passport from an encrypted file
#[wasm_bindgen]
pub fn import_from_file(
file_path: String,
password: String,
output_path: Option<String>,
) -> Result<JsValue, JsValue> {
let use_case = ImportFromFileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let output_path_ref = output_path.as_deref();
match use_case.execute(&file_path, &password, output_path_ref) {
Ok(passport) => {
let result = serde_wasm_bindgen::to_value(&passport)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
Err(e) => Err(JsValue::from_str(&format!("Error importing from file: {}", e))),
}
}
/// Export a passport to an encrypted file
#[wasm_bindgen]
pub fn export_passport(
passport_json: JsValue,
password: String,
output_path: String,
) -> Result<(), JsValue> {
let passport: Passport = serde_wasm_bindgen::from_value(passport_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let use_case = ExportPassportUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
match use_case.execute(&passport, &password, &output_path) {
Ok(()) => Ok(()),
Err(e) => Err(JsValue::from_str(&format!("Error exporting passport: {}", e))),
}
}
/// Sign a message with the passport's private key
#[wasm_bindgen]
pub fn sign_message(
passport_json: JsValue,
message: String,
) -> Result<Vec<u8>, JsValue> {
let passport: Passport = serde_wasm_bindgen::from_value(passport_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let use_case = SignCardUseCase::new();
match use_case.execute(&passport, &message) {
Ok(signature) => Ok(signature),
Err(e) => Err(JsValue::from_str(&format!("Error signing message: {}", e))),
}
}
/// Generate a new recovery phrase
#[wasm_bindgen]
pub fn generate_recovery_phrase() -> Result<JsValue, JsValue> {
let generator = Bip39MnemonicGenerator;
match generator.generate() {
Ok(recovery_phrase) => {
let result = serde_wasm_bindgen::to_value(&recovery_phrase)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
Err(e) => Err(JsValue::from_str(&format!("Error generating recovery phrase: {}", e))),
}
}
/// Validate a recovery phrase
#[wasm_bindgen]
pub fn validate_recovery_phrase(recovery_words: Vec<String>) -> Result<bool, JsValue> {
let generator = Bip39MnemonicGenerator;
match generator.validate(&recovery_words) {
Ok(()) => Ok(true),
Err(_) => Ok(false),
}
}
/// Create a new user profile for a passport
#[wasm_bindgen]
pub fn create_user_profile(
passport_json: JsValue,
hub_did: Option<String>,
identity_json: JsValue,
preferences_json: JsValue,
password: String,
file_path: String,
) -> Result<JsValue, JsValue> {
let mut passport: Passport = serde_wasm_bindgen::from_value(passport_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let identity: UserIdentity = serde_wasm_bindgen::from_value(identity_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let preferences: UserPreferences = serde_wasm_bindgen::from_value(preferences_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let use_case = CreateUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
match use_case.execute(&mut passport, hub_did, identity, preferences, &password, &file_path) {
Ok(()) => {
let result = serde_wasm_bindgen::to_value(&passport)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
Err(e) => Err(JsValue::from_str(&format!("Error creating user profile: {}", e))),
}
}
/// Update an existing user profile
#[wasm_bindgen]
pub fn update_user_profile(
passport_json: JsValue,
profile_id: String,
identity_json: JsValue,
preferences_json: JsValue,
password: String,
file_path: String,
) -> Result<JsValue, JsValue> {
let mut passport: Passport = serde_wasm_bindgen::from_value(passport_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let identity: UserIdentity = serde_wasm_bindgen::from_value(identity_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let preferences: UserPreferences = serde_wasm_bindgen::from_value(preferences_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let use_case = UpdateUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
match use_case.execute(&mut passport, Some(&profile_id), identity, preferences, &password, &file_path) {
Ok(()) => {
let result = serde_wasm_bindgen::to_value(&passport)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
Err(e) => Err(JsValue::from_str(&format!("Error updating user profile: {}", e))),
}
}
/// Delete a user profile
#[wasm_bindgen]
pub fn delete_user_profile(
passport_json: JsValue,
profile_id: String,
password: String,
file_path: String,
) -> Result<JsValue, JsValue> {
let mut passport: Passport = serde_wasm_bindgen::from_value(passport_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let use_case = DeleteUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
match use_case.execute(&mut passport, Some(&profile_id), &password, &file_path) {
Ok(()) => {
let result = serde_wasm_bindgen::to_value(&passport)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
Err(e) => Err(JsValue::from_str(&format!("Error deleting user profile: {}", e))),
}
}
/// Change passport password and re-encrypt file
#[wasm_bindgen]
pub fn change_passport_password(
_passport_json: JsValue,
old_password: String,
new_password: String,
file_path: String,
) -> Result<JsValue, JsValue> {
// Load passport from file with old password
let use_case = ImportFromFileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let passport = use_case.execute(&file_path, &old_password, None)
.map_err(|e| JsValue::from_str(&format!("Error loading passport: {}", e)))?;
// Export passport with new password
let export_use_case = ExportPassportUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
export_use_case.execute(&passport, &new_password, &file_path)
.map_err(|e| JsValue::from_str(&format!("Error re-encrypting passport: {}", e)))?;
// Return updated passport
let result = serde_wasm_bindgen::to_value(&passport)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
/// Get passport metadata without full decryption
#[wasm_bindgen]
pub fn get_passport_metadata(
file_path: String,
) -> Result<JsValue, JsValue> {
let file_storage = FileSystemStorage;
let passport_file = file_storage.load(&file_path)
.map_err(|e| JsValue::from_str(&format!("Error loading file: {}", e)))?;
let metadata = serde_json::json!({
"did": passport_file.did,
"univ_id": passport_file.univ_id,
"public_key": hex::encode(&passport_file.public_key),
"created_at": passport_file.created_at,
"version": passport_file.version,
"file_size": std::mem::size_of_val(&passport_file)
});
let result = serde_wasm_bindgen::to_value(&metadata)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
/// Validate passport file integrity
#[wasm_bindgen]
pub fn validate_passport_file(
file_path: String,
) -> Result<bool, JsValue> {
let file_storage = FileSystemStorage;
match file_storage.load(&file_path) {
Ok(passport_file) => {
// Basic validation checks
let is_valid = !passport_file.enc_seed.is_empty()
&& !passport_file.salt.is_empty()
&& !passport_file.nonce.is_empty()
&& !passport_file.public_key.is_empty()
&& !passport_file.did.is_empty()
&& !passport_file.univ_id.is_empty();
Ok(is_valid)
}
Err(_) => Ok(false),
}
}

View file

@ -0,0 +1,348 @@
//! WASM-specific tests for Sharenet Passport
//!
//! These tests run in a browser environment using wasm-bindgen-test
use wasm_bindgen_test::*;
use crate::wasm::*;
use crate::domain::entities::{RecoveryPhrase, Passport, UserIdentity, UserPreferences};
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn test_generate_recovery_phrase() {
let result = generate_recovery_phrase();
assert!(result.is_ok());
let recovery_phrase = result.unwrap();
assert!(recovery_phrase.is_object());
}
#[wasm_bindgen_test]
fn test_validate_recovery_phrase() {
// Test with empty words (should fail)
let empty_words: Vec<String> = vec![];
let result = validate_recovery_phrase(empty_words);
assert!(result.is_ok());
assert_eq!(result.unwrap(), false);
// Test with invalid words (should fail)
let invalid_words = vec!["invalid".to_string(), "words".to_string(), "here".to_string()];
let result = validate_recovery_phrase(invalid_words);
assert!(result.is_ok());
assert_eq!(result.unwrap(), false);
}
#[wasm_bindgen_test]
fn test_create_passport() {
let univ_id = "test-universe".to_string();
let password = "test-password".to_string();
let output_path = "/tmp/test-passport.spf".to_string();
let result = create_passport(univ_id, password, output_path);
// Note: This test may fail in browser environment due to file system access
// The important part is that the function compiles and runs
assert!(result.is_ok() || result.is_err());
}
#[wasm_bindgen_test]
fn test_sign_message() {
// Create a passport first
let univ_id = "test-universe".to_string();
let password = "test-password".to_string();
let output_path = "/tmp/test-passport.spf".to_string();
let passport_result = create_passport(univ_id, password, output_path);
if let Ok(passport_json) = passport_result {
let message = "test message".to_string();
let result = sign_message(passport_json, message);
assert!(result.is_ok());
let signature = result.unwrap();
assert!(!signature.is_empty());
}
// If passport creation failed (likely due to file system), that's OK for this test
}
#[wasm_bindgen_test]
fn test_validate_passport_file_invalid_path() {
// Test with non-existent file
let file_path = "/non/existent/path.spf".to_string();
let result = validate_passport_file(file_path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), false);
}
#[wasm_bindgen_test]
fn test_get_passport_metadata_invalid_path() {
// Test with non-existent file
let file_path = "/non/existent/path.spf".to_string();
let result = get_passport_metadata(file_path);
// This should fail since the file doesn't exist
assert!(result.is_err());
}
// Test serialization/deserialization of UserIdentity
#[wasm_bindgen_test]
fn test_user_identity_serialization() {
use serde_wasm_bindgen;
use crate::domain::entities::UserIdentity;
let identity = UserIdentity {
handle: Some("testuser".to_string()),
display_name: Some("Test User".to_string()),
first_name: Some("Test".to_string()),
last_name: Some("User".to_string()),
email: Some("test@example.com".to_string()),
avatar_url: Some("https://example.com/avatar.png".to_string()),
bio: Some("Test bio".to_string()),
};
let js_value = serde_wasm_bindgen::to_value(&identity).unwrap();
let deserialized: UserIdentity = serde_wasm_bindgen::from_value(js_value).unwrap();
assert_eq!(identity.handle, deserialized.handle);
assert_eq!(identity.display_name, deserialized.display_name);
assert_eq!(identity.email, deserialized.email);
}
// Test serialization/deserialization of UserPreferences
#[wasm_bindgen_test]
fn test_user_preferences_serialization() {
use serde_wasm_bindgen;
use crate::domain::entities::UserPreferences;
let preferences = UserPreferences {
theme: Some("dark".to_string()),
language: Some("en".to_string()),
notifications_enabled: true,
auto_sync: false,
};
let js_value = serde_wasm_bindgen::to_value(&preferences).unwrap();
let deserialized: UserPreferences = serde_wasm_bindgen::from_value(js_value).unwrap();
assert_eq!(preferences.theme, deserialized.theme);
assert_eq!(preferences.language, deserialized.language);
assert_eq!(preferences.notifications_enabled, deserialized.notifications_enabled);
assert_eq!(preferences.auto_sync, deserialized.auto_sync);
}
// Test user profile management
#[wasm_bindgen_test]
fn test_user_profile_management() {
// Create a passport first
let univ_id = "test-universe".to_string();
let password = "test-password".to_string();
let output_path = "/tmp/test-passport.spf".to_string();
let passport_result = create_passport(univ_id, password.clone(), output_path.clone());
if let Ok(passport_json) = passport_result {
// Create user profile
let identity = serde_wasm_bindgen::to_value(&UserIdentity {
handle: Some("testuser".to_string()),
display_name: Some("Test User".to_string()),
first_name: Some("Test".to_string()),
last_name: Some("User".to_string()),
email: Some("test@example.com".to_string()),
avatar_url: Some("https://example.com/avatar.png".to_string()),
bio: Some("Test bio".to_string()),
}).unwrap();
let preferences = serde_wasm_bindgen::to_value(&UserPreferences {
theme: Some("dark".to_string()),
language: Some("en".to_string()),
notifications_enabled: true,
auto_sync: false,
}).unwrap();
let result = create_user_profile(
passport_json.clone(),
Some("h:example".to_string()),
identity,
preferences,
password.clone(),
output_path.clone(),
);
assert!(result.is_ok());
// Test updating user profile
let updated_identity = serde_wasm_bindgen::to_value(&UserIdentity {
handle: Some("updateduser".to_string()),
display_name: Some("Updated User".to_string()),
first_name: Some("Updated".to_string()),
last_name: Some("User".to_string()),
email: Some("updated@example.com".to_string()),
avatar_url: Some("https://example.com/new-avatar.png".to_string()),
bio: Some("Updated bio".to_string()),
}).unwrap();
let updated_preferences = serde_wasm_bindgen::to_value(&UserPreferences {
theme: Some("light".to_string()),
language: Some("es".to_string()),
notifications_enabled: false,
auto_sync: true,
}).unwrap();
// Get the profile ID from the updated passport
let updated_passport_json = result.unwrap();
let passport: Passport = serde_wasm_bindgen::from_value(updated_passport_json.clone()).unwrap();
let profile_id = passport.user_profiles.iter()
.find(|p| p.hub_did.as_deref() == Some("h:example"))
.map(|p| p.id.clone())
.unwrap();
let update_result = update_user_profile(
updated_passport_json.clone(),
profile_id,
updated_identity,
updated_preferences,
password.clone(),
output_path.clone(),
);
assert!(update_result.is_ok());
}
}
// Test password change functionality
#[wasm_bindgen_test]
fn test_change_passport_password() {
// Create a passport first
let univ_id = "test-universe".to_string();
let old_password = "old-password".to_string();
let new_password = "new-password".to_string();
let output_path = "/tmp/test-passport.spf".to_string();
let passport_result = create_passport(univ_id, old_password.clone(), output_path.clone());
if let Ok(passport_json) = passport_result {
let result = change_passport_password(
passport_json,
old_password,
new_password,
output_path,
);
// This may fail due to file system access in browser
assert!(result.is_ok() || result.is_err());
}
}
// Test import from recovery phrase
#[wasm_bindgen_test]
fn test_import_from_recovery() {
// Generate a recovery phrase first
let recovery_result = generate_recovery_phrase();
assert!(recovery_result.is_ok());
let recovery_js = recovery_result.unwrap();
let recovery_phrase: RecoveryPhrase = serde_wasm_bindgen::from_value(recovery_js).unwrap();
let recovery_words = recovery_phrase.words().to_vec();
// Test import with valid recovery phrase
let univ_id = "test-universe".to_string();
let password = "test-password".to_string();
let output_path = "/tmp/test-import.spf".to_string();
let result = import_from_recovery(univ_id, recovery_words, password, output_path);
// This may fail due to file system access in browser
assert!(result.is_ok() || result.is_err());
}
// Test export passport functionality
#[wasm_bindgen_test]
fn test_export_passport() {
// Create a passport first
let univ_id = "test-universe".to_string();
let password = "test-password".to_string();
let output_path = "/tmp/test-passport.spf".to_string();
let passport_result = create_passport(univ_id, password.clone(), output_path.clone());
if let Ok(passport_json) = passport_result {
let export_path = "/tmp/test-export.spf".to_string();
let result = export_passport(passport_json, password, export_path);
// This may fail due to file system access in browser
assert!(result.is_ok() || result.is_err());
}
}
// Test import from file
#[wasm_bindgen_test]
fn test_import_from_file() {
// Test with non-existent file (should fail)
let file_path = "/non/existent/path.spf".to_string();
let password = "test-password".to_string();
let result = import_from_file(file_path, password, None);
// This should fail since the file doesn't exist
assert!(result.is_err());
}
// Test delete user profile
#[wasm_bindgen_test]
fn test_delete_user_profile() {
// Create a passport first
let univ_id = "test-universe".to_string();
let password = "test-password".to_string();
let output_path = "/tmp/test-passport.spf".to_string();
let passport_result = create_passport(univ_id, password.clone(), output_path.clone());
if let Ok(passport_json) = passport_result {
// Create a user profile first
let identity = serde_wasm_bindgen::to_value(&UserIdentity {
handle: Some("testuser".to_string()),
display_name: Some("Test User".to_string()),
first_name: Some("Test".to_string()),
last_name: Some("User".to_string()),
email: Some("test@example.com".to_string()),
avatar_url: Some("https://example.com/avatar.png".to_string()),
bio: Some("Test bio".to_string()),
}).unwrap();
let preferences = serde_wasm_bindgen::to_value(&UserPreferences {
theme: Some("dark".to_string()),
language: Some("en".to_string()),
notifications_enabled: true,
auto_sync: false,
}).unwrap();
let create_result = create_user_profile(
passport_json.clone(),
Some("h:example".to_string()),
identity,
preferences,
password.clone(),
output_path.clone(),
);
if let Ok(updated_passport_json) = create_result {
// Get the profile ID
let passport: Passport = serde_wasm_bindgen::from_value(updated_passport_json.clone()).unwrap();
let profile_id = passport.user_profiles.iter()
.find(|p| p.hub_did.as_deref() == Some("h:example"))
.map(|p| p.id.clone())
.unwrap();
// Delete the profile
let delete_result = delete_user_profile(
updated_passport_json,
profile_id,
password,
output_path,
);
assert!(delete_result.is_ok());
}
}
}