collab: Extract tests into integration tests crate (#47668)

Reduces time needed to build collab tests from 48s to 38s.

Release Notes:

- N/A
This commit is contained in:
Piotr Osiewicz authored and GitHub committed 2026-01-26 20:28:09 +01:00
1 parent 5a234bec6e
commit 8c8a0d9c4d
35 files changed
+552 -2998

No files matched your search

Generated
+1
View File
@@ -3293,6 +3293,7 @@ dependencies = [
"chrono",
"client",
"clock",
"collab",
"collab_ui",
"collections",
"command_palette_hooks",
+1
View File
@@ -278,6 +278,7 @@ clock = { path = "crates/clock" }
cloud_api_client = { path = "crates/cloud_api_client" }
cloud_api_types = { path = "crates/cloud_api_types" }
cloud_llm_client = { path = "crates/cloud_llm_client" }
collab = { path = "crates/collab" }
collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections", version = "0.1.0" }
command_palette = { path = "crates/command_palette" }
+9
View File
@@ -17,6 +17,14 @@ name = "collab"
sqlite = ["sea-orm/sqlx-sqlite", "sqlx/sqlite"]
test-support = ["sqlite"]
[lib]
test = false
[[test]]
name = "collab_tests"
required-features = ["test-support"]
path = "tests/integration/collab_tests.rs"
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
@@ -81,6 +89,7 @@ buffer_diff.workspace = true
call = { workspace = true, features = ["test-support"] }
channel.workspace = true
client = { workspace = true, features = ["test-support"] }
collab = { workspace = true, features = ["test-support"] }
collab_ui = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
command_palette_hooks.workspace = true
+5 -178
View File
@@ -108,13 +108,13 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
))
}
const MAX_ACCESS_TOKENS_TO_STORE: usize = 8;
pub const MAX_ACCESS_TOKENS_TO_STORE: usize = 8;
#[derive(Serialize, Deserialize)]
struct AccessTokenJson {
version: usize,
id: AccessTokenId,
token: String,
pub struct AccessTokenJson {
pub version: usize,
pub id: AccessTokenId,
pub token: String,
}
/// Creates a new access token to identify the given user. before returning it, you should
@@ -224,176 +224,3 @@ pub async fn verify_access_token(
},
})
}
#[cfg(test)]
mod test {
use rand::prelude::*;
use scrypt::password_hash::{PasswordHasher, SaltString};
use sea_orm::EntityTrait;
use super::*;
use crate::db::{NewUserParams, access_token};
#[gpui::test]
async fn test_verify_access_token(cx: &mut gpui::TestAppContext) {
let test_db = crate::db::TestDb::sqlite(cx.executor());
let db = test_db.db();
let user = db
.create_user(
"example@example.com",
None,
false,
NewUserParams {
github_login: "example".into(),
github_user_id: 1,
},
)
.await
.unwrap();
let token = create_access_token(db, user.user_id, None).await.unwrap();
assert!(matches!(
verify_access_token(&token, user.user_id, db).await.unwrap(),
VerifyAccessTokenResult {
is_valid: true,
impersonator_id: None,
}
));
let old_token = create_previous_access_token(user.user_id, None, db)
.await
.unwrap();
let old_token_id = serde_json::from_str::<AccessTokenJson>(&old_token)
.unwrap()
.id;
let hash = db
.transaction(|tx| async move {
Ok(access_token::Entity::find_by_id(old_token_id)
.one(&*tx)
.await?)
})
.await
.unwrap()
.unwrap()
.hash;
assert!(hash.starts_with("$scrypt$"));
assert!(matches!(
verify_access_token(&old_token, user.user_id, db)
.await
.unwrap(),
VerifyAccessTokenResult {
is_valid: true,
impersonator_id: None,
}
));
let hash = db
.transaction(|tx| async move {
Ok(access_token::Entity::find_by_id(old_token_id)
.one(&*tx)
.await?)
})
.await
.unwrap()
.unwrap()
.hash;
assert!(hash.starts_with("$sha256$"));
assert!(matches!(
verify_access_token(&old_token, user.user_id, db)
.await
.unwrap(),
VerifyAccessTokenResult {
is_valid: true,
impersonator_id: None,
}
));
assert!(matches!(
verify_access_token(&token, user.user_id, db).await.unwrap(),
VerifyAccessTokenResult {
is_valid: true,
impersonator_id: None,
}
));
}
async fn create_previous_access_token(
user_id: UserId,
impersonated_user_id: Option<UserId>,
db: &Database,
) -> Result<String> {
let access_token = rpc::auth::random_token();
let access_token_hash = previous_hash_access_token(&access_token)?;
let id = db
.create_access_token(
user_id,
impersonated_user_id,
&access_token_hash,
MAX_ACCESS_TOKENS_TO_STORE,
)
.await?;
Ok(serde_json::to_string(&AccessTokenJson {
version: 1,
id,
token: access_token,
})?)
}
fn previous_hash_access_token(token: &str) -> Result<String> {
// Avoid slow hashing in debug mode.
let params = if cfg!(debug_assertions) {
scrypt::Params::new(1, 1, 1, scrypt::Params::RECOMMENDED_LEN).unwrap()
} else {
scrypt::Params::new(14, 8, 1, scrypt::Params::RECOMMENDED_LEN).unwrap()
};
Ok(Scrypt
.hash_password_customized(
token.as_bytes(),
None,
None,
params,
&SaltString::generate(PasswordHashRngCompat::new()),
)
.map_err(anyhow::Error::new)?
.to_string())
}
// TODO: remove once we password_hash v0.6 is released.
struct PasswordHashRngCompat(rand::rngs::ThreadRng);
impl PasswordHashRngCompat {
fn new() -> Self {
Self(rand::rng())
}
}
impl scrypt::password_hash::rand_core::RngCore for PasswordHashRngCompat {
fn next_u32(&mut self) -> u32 {
self.0.next_u32()
}
fn next_u64(&mut self) -> u64 {
self.0.next_u64()
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0.fill_bytes(dest);
}
fn try_fill_bytes(
&mut self,
dest: &mut [u8],
) -> Result<(), scrypt::password_hash::rand_core::Error> {
self.fill_bytes(dest);
Ok(())
}
}
impl scrypt::password_hash::rand_core::CryptoRng for PasswordHashRngCompat {}
}
+10 -15
View File
@@ -1,8 +1,6 @@
mod ids;
mod queries;
pub mod queries;
mod tables;
#[cfg(test)]
pub mod tests;
use crate::{Error, Result};
use anyhow::{Context as _, anyhow};
@@ -37,15 +35,12 @@ use tokio::sync::{Mutex, OwnedMutexGuard};
use util::paths::PathStyle;
use worktree_settings_file::LocalSettingsKind;
#[cfg(test)]
pub use tests::TestDb;
pub use ids::*;
pub use sea_orm::ConnectOptions;
pub use tables::user::Model as User;
pub use tables::*;
#[cfg(test)]
#[cfg(feature = "test-support")]
pub struct DatabaseTestOptions {
pub executor: gpui::BackgroundExecutor,
pub runtime: tokio::runtime::Runtime,
@@ -55,14 +50,14 @@ pub struct DatabaseTestOptions {
/// Database gives you a handle that lets you access the database.
/// It handles pooling internally.
pub struct Database {
options: ConnectOptions,
pool: DatabaseConnection,
pub options: ConnectOptions,
pub pool: DatabaseConnection,
rooms: DashMap<RoomId, Arc<Mutex<()>>>,
projects: DashMap<ProjectId, Arc<Mutex<()>>>,
notification_kinds_by_id: HashMap<NotificationKindId, &'static str>,
notification_kinds_by_name: HashMap<String, NotificationKindId>,
#[cfg(test)]
test_options: Option<DatabaseTestOptions>,
#[cfg(feature = "test-support")]
pub test_options: Option<DatabaseTestOptions>,
}
// The `Database` type has so many methods that its impl blocks are split into
@@ -78,7 +73,7 @@ impl Database {
projects: DashMap::with_capacity(16384),
notification_kinds_by_id: HashMap::default(),
notification_kinds_by_name: HashMap::default(),
#[cfg(test)]
#[cfg(feature = "test-support")]
test_options: None,
})
}
@@ -87,7 +82,7 @@ impl Database {
&self.options
}
#[cfg(test)]
#[cfg(feature = "test-support")]
pub fn reset(&self) {
self.rooms.clear();
self.projects.clear();
@@ -248,7 +243,7 @@ impl Database {
where
F: Future<Output = Result<T>>,
{
#[cfg(test)]
#[cfg(feature = "test-support")]
{
let test_options = self.test_options.as_ref().unwrap();
test_options.executor.simulate_random_delay().await;
@@ -260,7 +255,7 @@ impl Database {
test_options.runtime.block_on(future)
}
#[cfg(not(test))]
#[cfg(not(feature = "test-support"))]
{
future.await
}
+5 -5
View File
@@ -7,7 +7,7 @@ use rpc::{
use sea_orm::{ActiveValue, DbBackend, TryGetableMany};
impl Database {
#[cfg(test)]
#[cfg(feature = "test-support")]
pub async fn all_channels(&self) -> Result<Vec<(ChannelId, String)>> {
self.transaction(move |tx| async move {
let mut channels = Vec::new();
@@ -21,12 +21,12 @@ impl Database {
.await
}
#[cfg(test)]
#[cfg(feature = "test-support")]
pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result<ChannelId> {
Ok(self.create_channel(name, None, creator_id).await?.0.id)
}
#[cfg(test)]
#[cfg(feature = "test-support")]
pub async fn create_sub_channel(
&self,
name: &str,
@@ -226,7 +226,7 @@ impl Database {
.await
}
#[cfg(test)]
#[cfg(feature = "test-support")]
pub async fn set_channel_requires_zed_cla(
&self,
channel_id: ChannelId,
@@ -885,7 +885,7 @@ impl Database {
.await
}
pub(crate) async fn get_channel_internal(
pub async fn get_channel_internal(
&self,
channel_id: ChannelId,
tx: &DatabaseTransaction,
+1 -1
View File
@@ -2,7 +2,7 @@ use super::*;
impl Database {
/// Records that a given user has signed the CLA.
#[cfg(test)]
#[cfg(feature = "test-support")]
pub async fn add_contributor(
&self,
github_login: &str,
-217
View File
@@ -1,218 +1 @@
mod buffer_tests;
mod channel_tests;
mod db_tests;
mod extension_tests;
mod migrations;
use std::sync::Arc;
use std::sync::atomic::{AtomicI32, Ordering::SeqCst};
use std::time::Duration;
use gpui::BackgroundExecutor;
use parking_lot::Mutex;
use rand::prelude::*;
use sea_orm::ConnectionTrait;
use sqlx::migrate::MigrateDatabase;
use self::migrations::run_database_migrations;
use super::*;
pub struct TestDb {
pub db: Option<Arc<Database>>,
pub connection: Option<sqlx::AnyConnection>,
}
impl TestDb {
pub fn sqlite(executor: BackgroundExecutor) -> Self {
let url = "sqlite::memory:";
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.unwrap();
let mut db = runtime.block_on(async {
let mut options = ConnectOptions::new(url);
options.max_connections(5);
let mut db = Database::new(options).await.unwrap();
let sql = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/migrations.sqlite/20221109000000_test_schema.sql"
));
db.pool
.execute(sea_orm::Statement::from_string(
db.pool.get_database_backend(),
sql,
))
.await
.unwrap();
db.initialize_notification_kinds().await.unwrap();
db
});
db.test_options = Some(DatabaseTestOptions {
executor,
runtime,
query_failure_probability: parking_lot::Mutex::new(0.0),
});
Self {
db: Some(Arc::new(db)),
connection: None,
}
}
pub fn postgres(executor: BackgroundExecutor) -> Self {
static LOCK: Mutex<()> = Mutex::new(());
let _guard = LOCK.lock();
let mut rng = StdRng::from_os_rng();
let url = format!(
"postgres://postgres@localhost/zed-test-{}",
rng.random::<u128>()
);
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.unwrap();
let mut db = runtime.block_on(async {
sqlx::Postgres::create_database(&url)
.await
.expect("failed to create test db");
let mut options = ConnectOptions::new(url);
options
.max_connections(5)
.idle_timeout(Duration::from_secs(0));
let mut db = Database::new(options).await.unwrap();
let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations");
run_database_migrations(db.options(), migrations_path)
.await
.unwrap();
db.initialize_notification_kinds().await.unwrap();
db
});
db.test_options = Some(DatabaseTestOptions {
executor,
runtime,
query_failure_probability: parking_lot::Mutex::new(0.0),
});
Self {
db: Some(Arc::new(db)),
connection: None,
}
}
pub fn db(&self) -> &Arc<Database> {
self.db.as_ref().unwrap()
}
pub fn set_query_failure_probability(&self, probability: f64) {
let database = self.db.as_ref().unwrap();
let test_options = database.test_options.as_ref().unwrap();
*test_options.query_failure_probability.lock() = probability;
}
}
#[macro_export]
macro_rules! test_both_dbs {
($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => {
#[cfg(target_os = "macos")]
#[gpui::test]
async fn $postgres_test_name(cx: &mut gpui::TestAppContext) {
let test_db = $crate::db::TestDb::postgres(cx.executor().clone());
$test_name(test_db.db()).await;
}
#[gpui::test]
async fn $sqlite_test_name(cx: &mut gpui::TestAppContext) {
let test_db = $crate::db::TestDb::sqlite(cx.executor().clone());
$test_name(test_db.db()).await;
}
};
}
impl Drop for TestDb {
fn drop(&mut self) {
let db = self.db.take().unwrap();
if let sea_orm::DatabaseBackend::Postgres = db.pool.get_database_backend() {
db.test_options.as_ref().unwrap().runtime.block_on(async {
use util::ResultExt;
let query = "
SELECT pg_terminate_backend(pg_stat_activity.pid)
FROM pg_stat_activity
WHERE
pg_stat_activity.datname = current_database() AND
pid <> pg_backend_pid();
";
db.pool
.execute(sea_orm::Statement::from_string(
db.pool.get_database_backend(),
query,
))
.await
.log_err();
sqlx::Postgres::drop_database(db.options.get_url())
.await
.log_err();
})
}
}
}
#[track_caller]
fn assert_channel_tree_matches(actual: Vec<Channel>, expected: Vec<Channel>) {
let expected_channels = expected.into_iter().collect::<HashSet<_>>();
let actual_channels = actual.into_iter().collect::<HashSet<_>>();
pretty_assertions::assert_eq!(expected_channels, actual_channels);
}
fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str)]) -> Vec<Channel> {
use std::collections::HashMap;
let mut result = Vec::new();
let mut order_by_parent: HashMap<Vec<ChannelId>, i32> = HashMap::new();
for (id, parent_path, name) in channels {
let parent_key = parent_path.to_vec();
let order = if parent_key.is_empty() {
1
} else {
*order_by_parent
.entry(parent_key.clone())
.and_modify(|e| *e += 1)
.or_insert(1)
};
result.push(Channel {
id: *id,
name: (*name).to_owned(),
visibility: ChannelVisibility::Members,
parent_path: parent_key,
channel_order: order,
});
}
result
}
static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
async fn new_test_user(db: &Arc<Database>, email: &str) -> UserId {
db.create_user(
email,
None,
false,
NewUserParams {
github_login: email[0..email.find('@').unwrap()].to_string(),
github_user_id: GITHUB_USER_ID.fetch_add(1, SeqCst),
},
)
.await
.unwrap()
.user_id
}
+4 -4
View File
@@ -1,12 +1,12 @@
use std::{future::Future, time::Duration};
#[cfg(test)]
#[cfg(feature = "test-support")]
use gpui::BackgroundExecutor;
#[derive(Clone)]
pub enum Executor {
Production,
#[cfg(test)]
#[cfg(feature = "test-support")]
Deterministic(BackgroundExecutor),
}
@@ -19,7 +19,7 @@ impl Executor {
Executor::Production => {
tokio::spawn(future);
}
#[cfg(test)]
#[cfg(feature = "test-support")]
Executor::Deterministic(background) => {
background.spawn(future).detach();
}
@@ -31,7 +31,7 @@ impl Executor {
async move {
match this {
Executor::Production => tokio::time::sleep(duration).await,
#[cfg(test)]
#[cfg(feature = "test-support")]
Executor::Deterministic(background) => background.timer(duration).await,
}
}
+1 -4
View File
@@ -6,9 +6,6 @@ pub mod executor;
pub mod rpc;
pub mod seed;
#[cfg(test)]
mod tests;
use anyhow::Context as _;
use aws_config::{BehaviorVersion, Region};
use axum::{
@@ -169,7 +166,7 @@ impl Config {
}
}
#[cfg(test)]
#[cfg(feature = "test-support")]
pub fn test() -> Self {
Self {
http_port: 0,
+8 -8
View File
@@ -204,16 +204,16 @@ struct Session {
impl Session {
async fn db(&self) -> tokio::sync::MutexGuard<'_, DbHandle> {
#[cfg(test)]
#[cfg(feature = "test-support")]
tokio::task::yield_now().await;
let guard = self.db.lock().await;
#[cfg(test)]
#[cfg(feature = "test-support")]
tokio::task::yield_now().await;
guard
}
async fn connection_pool(&self) -> ConnectionPoolGuard<'_> {
#[cfg(test)]
#[cfg(feature = "test-support")]
tokio::task::yield_now().await;
let guard = self.connection_pool.lock();
ConnectionPoolGuard {
@@ -267,13 +267,13 @@ impl Deref for DbHandle {
pub struct Server {
id: parking_lot::Mutex<ServerId>,
peer: Arc<Peer>,
pub(crate) connection_pool: Arc<parking_lot::Mutex<ConnectionPool>>,
pub connection_pool: Arc<parking_lot::Mutex<ConnectionPool>>,
app_state: Arc<AppState>,
handlers: HashMap<TypeId, MessageHandler>,
teardown: watch::Sender<bool>,
}
pub(crate) struct ConnectionPoolGuard<'a> {
struct ConnectionPoolGuard<'a> {
guard: parking_lot::MutexGuard<'a, ConnectionPool>,
_not_send: PhantomData<Rc<()>>,
}
@@ -651,7 +651,7 @@ impl Server {
let _ = self.teardown.send(true);
}
#[cfg(test)]
#[cfg(feature = "test-support")]
pub fn reset(&self, id: ServerId) {
self.teardown();
*self.id.lock() = id;
@@ -659,7 +659,7 @@ impl Server {
let _ = self.teardown.send(false);
}
#[cfg(test)]
#[cfg(feature = "test-support")]
pub fn id(&self) -> ServerId {
*self.id.lock()
}
@@ -1013,7 +1013,7 @@ impl DerefMut for ConnectionPoolGuard<'_> {
impl Drop for ConnectionPoolGuard<'_> {
fn drop(&mut self) {
#[cfg(test)]
#[cfg(feature = "test-support")]
self.check_invariants();
}
}
+1 -1
View File
@@ -168,7 +168,7 @@ impl ConnectionPool {
.is_empty()
}
#[cfg(test)]
#[cfg(feature = "test-support")]
pub fn check_invariants(&self) {
for (connection_id, connection) in &self.connections {
assert!(
-51
View File
@@ -1,51 +0,0 @@
use call::Room;
use client::ChannelId;
use gpui::{Entity, TestAppContext};
mod agent_sharing_tests;
mod channel_buffer_tests;
mod channel_guest_tests;
mod channel_tests;
mod editor_tests;
mod following_tests;
mod git_tests;
mod integration_tests;
mod notification_tests;
mod random_channel_buffer_tests;
mod random_project_collaboration_tests;
mod randomized_test_helpers;
mod remote_editing_collaboration_tests;
mod test_server;
pub use randomized_test_helpers::{
RandomizedTest, TestError, UserTestPlan, run_randomized_test, save_randomized_test_plan,
};
pub use test_server::{TestClient, TestServer};
#[derive(Debug, Eq, PartialEq)]
struct RoomParticipants {
remote: Vec<String>,
pending: Vec<String>,
}
fn room_participants(room: &Entity<Room>, cx: &mut TestAppContext) -> RoomParticipants {
room.read_with(cx, |room, _| {
let mut remote = room
.remote_participants()
.values()
.map(|participant| participant.user.github_login.clone().to_string())
.collect::<Vec<_>>();
let mut pending = room
.pending_participants()
.iter()
.map(|user| user.github_login.clone().to_string())
.collect::<Vec<_>>();
remote.sort();
pending.sort();
RoomParticipants { remote, pending }
})
}
fn channel_id(room: &Entity<Room>, cx: &mut TestAppContext) -> Option<ChannelId> {
cx.read(|cx| room.read(cx).channel_id())
}
File diff suppressed because it is too large. Load diff
@@ -3,7 +3,7 @@ use gpui::{BackgroundExecutor, TestAppContext};
use rpc::proto;
use uuid::Uuid;
use crate::tests::TestServer;
use crate::TestServer;
#[gpui::test]
async fn test_share_and_retrieve_thread(
@@ -1,10 +1,8 @@
use crate::{
rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
tests::{TestServer, test_server::open_channel_notes},
};
use crate::{TestServer, test_server::open_channel_notes};
use call::ActiveCall;
use channel::ACKNOWLEDGE_DEBOUNCE_INTERVAL;
use client::{Collaborator, ParticipantIndex, UserId};
use collab::rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT};
use collab_ui::channel_view::ChannelView;
use collections::HashMap;
use editor::{Anchor, Editor, MultiBufferOffset, ToOffset};
@@ -1,11 +1,11 @@
use crate::{db::ChannelId, tests::TestServer};
use crate::TestServer;
use call::ActiveCall;
use chrono::Utc;
use collab::db::ChannelId;
use editor::Editor;
use gpui::{BackgroundExecutor, TestAppContext};
use rpc::proto;
use util::rel_path::rel_path;
#[gpui::test]
async fn test_channel_guests(
executor: BackgroundExecutor,
@@ -1,11 +1,11 @@
use crate::{
db::{self, UserId},
rpc::RECONNECT_TIMEOUT,
tests::{RoomParticipants, TestServer, room_participants},
};
use crate::{RoomParticipants, TestServer, room_participants};
use call::ActiveCall;
use channel::{ChannelMembership, ChannelStore};
use client::{ChannelId, User};
use collab::{
db::{self, UserId},
rpc::RECONNECT_TIMEOUT,
};
use futures::future::try_join_all;
use gpui::{BackgroundExecutor, Entity, SharedString, TestAppContext};
use rpc::{
@@ -0,0 +1,230 @@
use call::Room;
use client::ChannelId;
use gpui::{Entity, TestAppContext};
mod agent_sharing_tests;
mod channel_buffer_tests;
mod channel_guest_tests;
mod channel_tests;
mod db_tests;
mod editor_tests;
mod following_tests;
mod git_tests;
mod integration_tests;
mod notification_tests;
mod random_channel_buffer_tests;
mod random_project_collaboration_tests;
mod randomized_test_helpers;
mod remote_editing_collaboration_tests;
mod test_server;
pub use randomized_test_helpers::{
RandomizedTest, TestError, UserTestPlan, run_randomized_test, save_randomized_test_plan,
};
pub use test_server::{TestClient, TestServer};
#[derive(Debug, Eq, PartialEq)]
struct RoomParticipants {
remote: Vec<String>,
pending: Vec<String>,
}
fn room_participants(room: &Entity<Room>, cx: &mut TestAppContext) -> RoomParticipants {
room.read_with(cx, |room, _| {
let mut remote = room
.remote_participants()
.values()
.map(|participant| participant.user.github_login.clone().to_string())
.collect::<Vec<_>>();
let mut pending = room
.pending_participants()
.iter()
.map(|user| user.github_login.clone().to_string())
.collect::<Vec<_>>();
remote.sort();
pending.sort();
RoomParticipants { remote, pending }
})
}
fn channel_id(room: &Entity<Room>, cx: &mut TestAppContext) -> Option<ChannelId> {
cx.read(|cx| room.read(cx).channel_id())
}
mod auth_token_tests {
use collab::auth::{
AccessTokenJson, MAX_ACCESS_TOKENS_TO_STORE, VerifyAccessTokenResult, create_access_token,
verify_access_token,
};
use rand::prelude::*;
use scrypt::Scrypt;
use scrypt::password_hash::{PasswordHasher, SaltString};
use sea_orm::EntityTrait;
use collab::db::{Database, NewUserParams, UserId, access_token};
use collab::*;
#[gpui::test]
async fn test_verify_access_token(cx: &mut gpui::TestAppContext) {
let test_db = crate::db_tests::TestDb::sqlite(cx.executor());
let db = test_db.db();
let user = db
.create_user(
"example@example.com",
None,
false,
NewUserParams {
github_login: "example".into(),
github_user_id: 1,
},
)
.await
.unwrap();
let token = create_access_token(db, user.user_id, None).await.unwrap();
assert!(matches!(
verify_access_token(&token, user.user_id, db).await.unwrap(),
VerifyAccessTokenResult {
is_valid: true,
impersonator_id: None,
}
));
let old_token = create_previous_access_token(user.user_id, None, db)
.await
.unwrap();
let old_token_id = serde_json::from_str::<AccessTokenJson>(&old_token)
.unwrap()
.id;
let hash = db
.transaction(|tx| async move {
Ok(access_token::Entity::find_by_id(old_token_id)
.one(&*tx)
.await?)
})
.await
.unwrap()
.unwrap()
.hash;
assert!(hash.starts_with("$scrypt$"));
assert!(matches!(
verify_access_token(&old_token, user.user_id, db)
.await
.unwrap(),
VerifyAccessTokenResult {
is_valid: true,
impersonator_id: None,
}
));
let hash = db
.transaction(|tx| async move {
Ok(access_token::Entity::find_by_id(old_token_id)
.one(&*tx)
.await?)
})
.await
.unwrap()
.unwrap()
.hash;
assert!(hash.starts_with("$sha256$"));
assert!(matches!(
verify_access_token(&old_token, user.user_id, db)
.await
.unwrap(),
VerifyAccessTokenResult {
is_valid: true,
impersonator_id: None,
}
));
assert!(matches!(
verify_access_token(&token, user.user_id, db).await.unwrap(),
VerifyAccessTokenResult {
is_valid: true,
impersonator_id: None,
}
));
}
async fn create_previous_access_token(
user_id: UserId,
impersonated_user_id: Option<UserId>,
db: &Database,
) -> Result<String> {
let access_token = collab::auth::random_token();
let access_token_hash = previous_hash_access_token(&access_token)?;
let id = db
.create_access_token(
user_id,
impersonated_user_id,
&access_token_hash,
MAX_ACCESS_TOKENS_TO_STORE,
)
.await?;
Ok(serde_json::to_string(&AccessTokenJson {
version: 1,
id,
token: access_token,
})?)
}
#[expect(clippy::result_large_err)]
fn previous_hash_access_token(token: &str) -> Result<String> {
// Avoid slow hashing in debug mode.
let params = if cfg!(debug_assertions) {
scrypt::Params::new(1, 1, 1, scrypt::Params::RECOMMENDED_LEN).unwrap()
} else {
scrypt::Params::new(14, 8, 1, scrypt::Params::RECOMMENDED_LEN).unwrap()
};
Ok(Scrypt
.hash_password_customized(
token.as_bytes(),
None,
None,
params,
&SaltString::generate(PasswordHashRngCompat::new()),
)
.map_err(anyhow::Error::new)?
.to_string())
}
// TODO: remove once we password_hash v0.6 is released.
struct PasswordHashRngCompat(rand::rngs::ThreadRng);
impl PasswordHashRngCompat {
fn new() -> Self {
Self(rand::rng())
}
}
impl scrypt::password_hash::rand_core::RngCore for PasswordHashRngCompat {
fn next_u32(&mut self) -> u32 {
self.0.next_u32()
}
fn next_u64(&mut self) -> u64 {
self.0.next_u64()
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0.fill_bytes(dest);
}
fn try_fill_bytes(
&mut self,
dest: &mut [u8],
) -> Result<(), scrypt::password_hash::rand_core::Error> {
self.fill_bytes(dest);
Ok(())
}
}
impl scrypt::password_hash::rand_core::CryptoRng for PasswordHashRngCompat {}
}
+219
View File
@@ -0,0 +1,219 @@
mod buffer_tests;
mod channel_tests;
mod db_tests;
mod extension_tests;
mod migrations;
use std::sync::Arc;
use std::sync::atomic::{AtomicI32, Ordering::SeqCst};
use std::time::Duration;
use collections::HashSet;
use gpui::BackgroundExecutor;
use parking_lot::Mutex;
use rand::prelude::*;
use sea_orm::ConnectionTrait;
use sqlx::migrate::MigrateDatabase;
use self::migrations::run_database_migrations;
use collab::db::*;
pub struct TestDb {
pub db: Option<Arc<Database>>,
pub connection: Option<sqlx::AnyConnection>,
}
impl TestDb {
pub fn sqlite(executor: BackgroundExecutor) -> Self {
let url = "sqlite::memory:";
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.unwrap();
let mut db = runtime.block_on(async {
let mut options = ConnectOptions::new(url);
options.max_connections(5);
let mut db = Database::new(options).await.unwrap();
let sql = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/migrations.sqlite/20221109000000_test_schema.sql"
));
db.pool
.execute(sea_orm::Statement::from_string(
db.pool.get_database_backend(),
sql,
))
.await
.unwrap();
db.initialize_notification_kinds().await.unwrap();
db
});
db.test_options = Some(DatabaseTestOptions {
executor,
runtime,
query_failure_probability: parking_lot::Mutex::new(0.0),
});
Self {
db: Some(Arc::new(db)),
connection: None,
}
}
pub fn postgres(executor: BackgroundExecutor) -> Self {
static LOCK: Mutex<()> = Mutex::new(());
let _guard = LOCK.lock();
let mut rng = StdRng::from_os_rng();
let url = format!(
"postgres://postgres@localhost/zed-test-{}",
rng.random::<u128>()
);
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.unwrap();
let mut db = runtime.block_on(async {
sqlx::Postgres::create_database(&url)
.await
.expect("failed to create test db");
let mut options = ConnectOptions::new(url);
options
.max_connections(5)
.idle_timeout(Duration::from_secs(0));
let mut db = Database::new(options).await.unwrap();
let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations");
run_database_migrations(db.options(), migrations_path)
.await
.unwrap();
db.initialize_notification_kinds().await.unwrap();
db
});
db.test_options = Some(DatabaseTestOptions {
executor,
runtime,
query_failure_probability: parking_lot::Mutex::new(0.0),
});
Self {
db: Some(Arc::new(db)),
connection: None,
}
}
pub fn db(&self) -> &Arc<Database> {
self.db.as_ref().unwrap()
}
pub fn set_query_failure_probability(&self, probability: f64) {
let database = self.db.as_ref().unwrap();
let test_options = database.test_options.as_ref().unwrap();
*test_options.query_failure_probability.lock() = probability;
}
}
#[macro_export]
macro_rules! test_both_dbs {
($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => {
#[cfg(target_os = "macos")]
#[gpui::test]
async fn $postgres_test_name(cx: &mut gpui::TestAppContext) {
let test_db = $crate::db_tests::TestDb::postgres(cx.executor().clone());
$test_name(test_db.db()).await;
}
#[gpui::test]
async fn $sqlite_test_name(cx: &mut gpui::TestAppContext) {
let test_db = $crate::db_tests::TestDb::sqlite(cx.executor().clone());
$test_name(test_db.db()).await;
}
};
}
impl Drop for TestDb {
fn drop(&mut self) {
let db = self.db.take().unwrap();
if let sea_orm::DatabaseBackend::Postgres = db.pool.get_database_backend() {
db.test_options.as_ref().unwrap().runtime.block_on(async {
use util::ResultExt;
let query = "
SELECT pg_terminate_backend(pg_stat_activity.pid)
FROM pg_stat_activity
WHERE
pg_stat_activity.datname = current_database() AND
pid <> pg_backend_pid();
";
db.pool
.execute(sea_orm::Statement::from_string(
db.pool.get_database_backend(),
query,
))
.await
.log_err();
sqlx::Postgres::drop_database(db.options.get_url())
.await
.log_err();
})
}
}
}
#[track_caller]
fn assert_channel_tree_matches(actual: Vec<Channel>, expected: Vec<Channel>) {
let expected_channels = expected.into_iter().collect::<HashSet<_>>();
let actual_channels = actual.into_iter().collect::<HashSet<_>>();
pretty_assertions::assert_eq!(expected_channels, actual_channels);
}
fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str)]) -> Vec<Channel> {
use std::collections::HashMap;
let mut result = Vec::new();
let mut order_by_parent: HashMap<Vec<ChannelId>, i32> = HashMap::new();
for (id, parent_path, name) in channels {
let parent_key = parent_path.to_vec();
let order = if parent_key.is_empty() {
1
} else {
*order_by_parent
.entry(parent_key.clone())
.and_modify(|e| *e += 1)
.or_insert(1)
};
result.push(Channel {
id: *id,
name: (*name).to_owned(),
visibility: ChannelVisibility::Members,
parent_path: parent_key,
channel_order: order,
});
}
result
}
static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
async fn new_test_user(db: &Arc<Database>, email: &str) -> UserId {
db.create_user(
email,
None,
false,
NewUserParams {
github_login: email[0..email.find('@').unwrap()].to_string(),
github_user_id: GITHUB_USER_ID.fetch_add(1, SeqCst),
},
)
.await
.unwrap()
.user_id
}
@@ -1,6 +1,7 @@
use super::*;
use crate::test_both_dbs;
use language::proto::{self, serialize_version};
use rpc::ConnectionId;
use text::{Buffer, ReplicaId};
test_both_dbs!(
@@ -1,10 +1,6 @@
use crate::{
db::{
Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId,
tests::{assert_channel_tree_matches, channel_tree, new_test_user},
},
test_both_dbs,
};
use super::{assert_channel_tree_matches, channel_tree, new_test_user};
use crate::test_both_dbs;
use collab::db::{Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId};
use rpc::{
ConnectionId,
proto::{self, reorder_channel},
@@ -689,12 +685,12 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.await
.unwrap();
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
db.set_channel_visibility(zed_channel, collab::db::ChannelVisibility::Public, admin)
.await
.unwrap();
db.set_channel_visibility(
public_channel_id,
crate::db::ChannelVisibility::Public,
collab::db::ChannelVisibility::Public,
admin,
)
.await
@@ -1,7 +1,11 @@
use super::*;
use crate::test_both_dbs;
use super::*;
use chrono::Utc;
use collab::db::RoomId;
use collab::db::*;
use pretty_assertions::assert_eq;
use rpc::ConnectionId;
use std::sync::Arc;
test_both_dbs!(
@@ -541,7 +545,7 @@ fn test_fuzzy_like_string() {
#[cfg(target_os = "macos")]
#[gpui::test]
async fn test_fuzzy_search_users(cx: &mut gpui::TestAppContext) {
let test_db = tests::TestDb::postgres(cx.executor());
let test_db = TestDb::postgres(cx.executor());
let db = test_db.db();
for (i, github_login) in [
"California",
@@ -594,7 +598,7 @@ test_both_dbs!(
);
async fn test_upsert_shared_thread(db: &Arc<Database>) {
use crate::db::SharedThreadId;
use collab::db::SharedThreadId;
use uuid::Uuid;
let user_id = new_test_user(db, "user1@example.com").await;
@@ -624,7 +628,7 @@ test_both_dbs!(
);
async fn test_upsert_shared_thread_updates_existing(db: &Arc<Database>) {
use crate::db::SharedThreadId;
use collab::db::SharedThreadId;
use uuid::Uuid;
let user_id = new_test_user(db, "user1@example.com").await;
@@ -665,7 +669,7 @@ test_both_dbs!(
);
async fn test_cannot_update_another_users_shared_thread(db: &Arc<Database>) {
use crate::db::SharedThreadId;
use collab::db::SharedThreadId;
use uuid::Uuid;
let user1_id = new_test_user(db, "user1@example.com").await;
@@ -694,7 +698,7 @@ test_both_dbs!(
);
async fn test_get_nonexistent_shared_thread(db: &Arc<Database>) {
use crate::db::SharedThreadId;
use collab::db::SharedThreadId;
use uuid::Uuid;
let result = db
@@ -3,13 +3,11 @@ use std::sync::Arc;
use rpc::ExtensionProvides;
use super::Database;
use crate::db::ExtensionVersionConstraints;
use crate::{
db::{ExtensionMetadata, NewExtensionVersion, queries::extensions::convert_time_to_chrono},
test_both_dbs,
};
use crate::test_both_dbs;
use collab::db::Database;
use collab::db::ExtensionVersionConstraints;
use collab::db::{NewExtensionVersion, queries::extensions::convert_time_to_chrono};
use rpc::ExtensionMetadata;
test_both_dbs!(
test_extensions,
test_extensions_postgres,
@@ -1,5 +1,6 @@
use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
use crate::TestServer;
use call::ActiveCall;
use collab::rpc::RECONNECT_TIMEOUT;
use collections::{HashMap, HashSet};
use editor::{
DocumentColorsRenderMode, Editor, FETCH_COLORS_DEBOUNCE_TIMEOUT, MultiBufferOffset, RowInfo,
@@ -1,5 +1,5 @@
#![allow(clippy::reversed_empty_ranges)]
use crate::tests::TestServer;
use crate::TestServer;
use call::{ActiveCall, ParticipantLocation};
use client::ChannelId;
use collab_ui::{
@@ -462,7 +462,7 @@ async fn test_basic_following(
// #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
{
use crate::rpc::RECONNECT_TIMEOUT;
use collab::rpc::RECONNECT_TIMEOUT;
use gpui::TestScreenCaptureSource;
use workspace::{
dock::{DockPosition, test::TestPanel},
@@ -10,7 +10,7 @@ use util::{path, rel_path::rel_path};
use workspace::Workspace;
//
use crate::tests::TestServer;
use crate::TestServer;
#[gpui::test]
async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
@@ -1,9 +1,6 @@
use crate::{
rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
tests::{
RoomParticipants, TestClient, TestServer, channel_id, following_tests::join_channel,
room_participants,
},
RoomParticipants, TestClient, TestServer, channel_id, following_tests::join_channel,
room_participants,
};
use anyhow::{Result, anyhow};
use assistant_slash_command::SlashCommandWorkingSet;
@@ -11,6 +8,7 @@ use assistant_text_thread::TextThreadStore;
use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus, assert_hunks};
use call::{ActiveCall, ParticipantLocation, Room, room};
use client::{RECEIVE_TIMEOUT, User};
use collab::rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT};
use collections::{BTreeMap, HashMap, HashSet};
use fs::{FakeFs, Fs as _, RemoveOptions};
use futures::{StreamExt as _, channel::mpsc};
@@ -6,7 +6,7 @@ use parking_lot::Mutex;
use pretty_assertions::assert_eq;
use rpc::{Notification, proto};
use crate::tests::TestServer;
use crate::TestServer;
#[gpui::test]
async fn test_notifications(
@@ -1,4 +1,4 @@
use crate::db::ChannelRole;
use collab::db::*;
use super::{RandomizedTest, TestClient, TestError, TestServer, UserTestPlan, run_randomized_test};
use anyhow::Result;
@@ -13,10 +13,7 @@ use std::{
};
use text::Bias;
#[gpui::test(
iterations = 100,
on_failure = "crate::tests::save_randomized_test_plan"
)]
#[gpui::test(iterations = 100, on_failure = "crate::save_randomized_test_plan")]
async fn test_random_channel_buffers(
cx: &mut TestAppContext,
executor: BackgroundExecutor,
@@ -1,8 +1,8 @@
use super::{RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
use crate::{db::UserId, tests::run_randomized_test};
use crate::{RandomizedTest, TestClient, TestError, TestServer, UserTestPlan, run_randomized_test};
use anyhow::{Context as _, Result};
use async_trait::async_trait;
use call::ActiveCall;
use collab::db::UserId;
use collections::{BTreeMap, HashMap};
use editor::Bias;
use fs::{FakeFs, Fs as _};
@@ -33,10 +33,7 @@ use util::{
rel_path::{RelPath, RelPathBuf, rel_path},
};
#[gpui::test(
iterations = 100,
on_failure = "crate::tests::save_randomized_test_plan"
)]
#[gpui::test(iterations = 100, on_failure = "crate::save_randomized_test_plan")]
async fn test_random_project_collaboration(
cx: &mut TestAppContext,
executor: BackgroundExecutor,
@@ -1,9 +1,9 @@
use crate::{
use crate::{TestClient, TestServer};
use async_trait::async_trait;
use collab::{
db::{self, NewUserParams, UserId},
rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
tests::{TestClient, TestServer},
};
use async_trait::async_trait;
use futures::StreamExt;
use gpui::{BackgroundExecutor, Task, TestAppContext};
use parking_lot::Mutex;
@@ -1,4 +1,4 @@
use crate::tests::TestServer;
use crate::TestServer;
use call::ActiveCall;
use collections::{HashMap, HashSet};
@@ -1,9 +1,3 @@
use crate::{
AppState, Config,
db::{NewUserParams, UserId, tests::TestDb},
executor::Executor,
rpc::{CLEANUP_TIMEOUT, Principal, RECONNECT_TIMEOUT, Server, ZedVersion},
};
use anyhow::anyhow;
use call::ActiveCall;
use channel::{ChannelBuffer, ChannelStore};
@@ -13,6 +7,12 @@ use client::{
proto::PeerId,
};
use clock::FakeSystemClock;
use collab::{
AppState, Config,
db::{NewUserParams, UserId},
executor::Executor,
rpc::{CLEANUP_TIMEOUT, Principal, RECONNECT_TIMEOUT, Server, ZedVersion},
};
use collab_ui::channel_view::ChannelView;
use collections::{HashMap, HashSet};
@@ -49,6 +49,8 @@ use workspace::{Workspace, WorkspaceStore};
use livekit_client::test::TestServer as LivekitTestServer;
use crate::db_tests::TestDb;
pub struct TestServer {
pub app_state: Arc<AppState>,
pub test_livekit_server: Arc<LivekitTestServer>,