diff --git a/.vscode/settings.json b/.vscode/settings.json
index 0df252c66..49d117177 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -27,6 +27,7 @@
"spki",
"totp",
"uniffi",
+ "wiremock",
"wordlist",
"XCHACHA",
"Zeroize",
diff --git a/Cargo.lock b/Cargo.lock
index 4e3412708..89b6743bb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -609,10 +609,21 @@ name = "bitwarden-state"
version = "1.0.0"
dependencies = [
"async-trait",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
"tokio",
]
+[[package]]
+name = "bitwarden-test"
+version = "1.0.0"
+dependencies = [
+ "async-trait",
+ "bitwarden-api-api",
+ "bitwarden-state",
+ "reqwest",
+ "wiremock",
+]
+
[[package]]
name = "bitwarden-threading"
version = "1.0.0"
@@ -691,6 +702,7 @@ dependencies = [
"bitwarden-crypto",
"bitwarden-error",
"bitwarden-state",
+ "bitwarden-test",
"chrono",
"data-encoding",
"hmac",
@@ -707,6 +719,8 @@ dependencies = [
"uniffi",
"uuid",
"wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wiremock",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index c6bd4f340..3d43a62bc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,10 +31,11 @@ bitwarden-fido = { path = "crates/bitwarden-fido", version = "=1.0.0" }
bitwarden-generators = { path = "crates/bitwarden-generators", version = "=1.0.0" }
bitwarden-ipc = { path = "crates/bitwarden-ipc", version = "=1.0.0" }
bitwarden-send = { path = "crates/bitwarden-send", version = "=1.0.0" }
-bitwarden-state = { path = "crates/bitwarden-state", version = "=1.0.0" }
-bitwarden-threading = { path = "crates/bitwarden-threading", version = "=1.0.0" }
bitwarden-sm = { path = "bitwarden_license/bitwarden-sm", version = "=1.0.0" }
bitwarden-ssh = { path = "crates/bitwarden-ssh", version = "=1.0.0" }
+bitwarden-state = { path = "crates/bitwarden-state", version = "=1.0.0" }
+bitwarden-test = { path = "crates/bitwarden-test", version = "=1.0.0" }
+bitwarden-threading = { path = "crates/bitwarden-threading", version = "=1.0.0" }
bitwarden-uuid = { path = "crates/bitwarden-uuid", version = "=1.0.0" }
bitwarden-uuid-macro = { path = "crates/bitwarden-uuid-macro", version = "=1.0.0" }
bitwarden-vault = { path = "crates/bitwarden-vault", version = "=1.0.0" }
@@ -60,6 +61,7 @@ serde = { version = ">=1.0, <2.0", features = ["derive"] }
serde_json = ">=1.0.96, <2.0"
serde_qs = ">=0.12.0, <0.16"
serde_repr = ">=0.1.12, <0.2"
+serde-wasm-bindgen = ">=0.6.0, <0.7"
syn = ">=2.0.87, <3"
thiserror = ">=1.0.40, <3"
tokio = { version = "1.36.0", features = ["macros"] }
@@ -72,7 +74,7 @@ validator = { version = ">=0.18.1, <0.21", features = ["derive"] }
wasm-bindgen = { version = ">=0.2.91, <0.3", features = ["serde-serialize"] }
wasm-bindgen-futures = "0.4.41"
wasm-bindgen-test = "0.3.45"
-serde-wasm-bindgen = ">=0.6.0, <0.7"
+wiremock = ">=0.6.0, <0.7"
# There is an incompatibility when using pkcs5 and chacha20 on wasm builds. This can be removed once a new
# rustcrypto-formats crate version is released since the fix has been upstreamed.
diff --git a/bitwarden_license/bitwarden-sm/Cargo.toml b/bitwarden_license/bitwarden-sm/Cargo.toml
index ce2521938..0eed312d1 100644
--- a/bitwarden_license/bitwarden-sm/Cargo.toml
+++ b/bitwarden_license/bitwarden-sm/Cargo.toml
@@ -28,7 +28,7 @@ validator = { workspace = true }
[dev-dependencies]
tokio = { workspace = true, features = ["rt"] }
-wiremock = "0.6.0"
+wiremock = { workspace = true }
[lints]
workspace = true
diff --git a/crates/bitwarden-generators/Cargo.toml b/crates/bitwarden-generators/Cargo.toml
index ccee89a3b..93f601cee 100644
--- a/crates/bitwarden-generators/Cargo.toml
+++ b/crates/bitwarden-generators/Cargo.toml
@@ -39,7 +39,7 @@ wasm-bindgen = { workspace = true, optional = true }
[dev-dependencies]
rand_chacha = "0.3.1"
tokio = { workspace = true, features = ["rt"] }
-wiremock = "0.6.0"
+wiremock = { workspace = true }
[lints]
workspace = true
diff --git a/crates/bitwarden-test/Cargo.toml b/crates/bitwarden-test/Cargo.toml
new file mode 100644
index 000000000..0b7f8f481
--- /dev/null
+++ b/crates/bitwarden-test/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "bitwarden-test"
+description = """
+Internal crate for the bitwarden crate. Do not use.
+"""
+
+version.workspace = true
+authors.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+readme.workspace = true
+homepage.workspace = true
+repository.workspace = true
+license-file.workspace = true
+keywords.workspace = true
+
+[dependencies]
+async-trait = { workspace = true }
+bitwarden-api-api.workspace = true
+bitwarden-state = { workspace = true }
+reqwest = { workspace = true }
+wiremock = { workspace = true }
+
+[lints]
+workspace = true
diff --git a/crates/bitwarden-test/README.md b/crates/bitwarden-test/README.md
new file mode 100644
index 000000000..00f329a85
--- /dev/null
+++ b/crates/bitwarden-test/README.md
@@ -0,0 +1,7 @@
+# Bitwarden Test
+
+
+This crate should only be used in tests and should not be included in production builds.
+
+
+Contains test utilities for Bitwarden.
diff --git a/crates/bitwarden-test/src/api.rs b/crates/bitwarden-test/src/api.rs
new file mode 100644
index 000000000..265dee15b
--- /dev/null
+++ b/crates/bitwarden-test/src/api.rs
@@ -0,0 +1,24 @@
+use bitwarden_api_api::apis::configuration::Configuration;
+
+/// Helper for testing the Bitwarden API using wiremock.
+///
+/// Warning: when using `Mock::expected` ensure `server` is not dropped before the test completes,
+pub async fn start_api_mock(mocks: Vec) -> (wiremock::MockServer, Configuration) {
+ let server = wiremock::MockServer::start().await;
+
+ for mock in mocks {
+ server.register(mock).await;
+ }
+
+ let config = Configuration {
+ base_path: server.uri(),
+ user_agent: Some("test-agent".to_string()),
+ client: reqwest::Client::new(),
+ basic_auth: None,
+ oauth_access_token: None,
+ bearer_access_token: None,
+ api_key: None,
+ };
+
+ (server, config)
+}
diff --git a/crates/bitwarden-test/src/lib.rs b/crates/bitwarden-test/src/lib.rs
new file mode 100644
index 000000000..fcc1930a0
--- /dev/null
+++ b/crates/bitwarden-test/src/lib.rs
@@ -0,0 +1,7 @@
+#![doc = include_str!("../README.md")]
+
+mod api;
+pub use api::*;
+
+mod repository;
+pub use repository::*;
diff --git a/crates/bitwarden-test/src/repository.rs b/crates/bitwarden-test/src/repository.rs
new file mode 100644
index 000000000..31d4cb3ce
--- /dev/null
+++ b/crates/bitwarden-test/src/repository.rs
@@ -0,0 +1,54 @@
+use bitwarden_state::repository::{Repository, RepositoryError, RepositoryItem};
+
+/// A simple in-memory repository implementation. The data is only stored in memory and will not
+/// persist beyond the lifetime of the repository instance.
+///
+/// Primary use case is for unit and integration tests.
+pub struct MemoryRepository {
+ store: std::sync::Mutex>,
+}
+
+impl Default for MemoryRepository {
+ fn default() -> Self {
+ Self {
+ store: std::sync::Mutex::new(std::collections::HashMap::new()),
+ }
+ }
+}
+
+#[async_trait::async_trait]
+impl Repository for MemoryRepository {
+ async fn get(&self, key: String) -> Result