folders
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
DROP INDEX IF EXISTS folders_tenant_parent_name_unique_idx;
|
||||||
|
CREATE UNIQUE INDEX folders_parent_name_unique_idx
|
||||||
|
ON folders (
|
||||||
|
COALESCE(parent_id, '00000000-0000-0000-0000-000000000000'::uuid),
|
||||||
|
name
|
||||||
|
);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
DROP INDEX IF EXISTS folders_parent_name_unique_idx;
|
||||||
|
CREATE UNIQUE INDEX folders_tenant_parent_name_unique_idx
|
||||||
|
ON folders (
|
||||||
|
tenant_id,
|
||||||
|
COALESCE(parent_id, '00000000-0000-0000-0000-000000000000'::uuid),
|
||||||
|
name
|
||||||
|
);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- Revert correspondent uniqueness to global name
|
||||||
|
DROP INDEX IF EXISTS correspondents_tenant_name_unique;
|
||||||
|
ALTER TABLE correspondents ADD CONSTRAINT correspondents_name_unique UNIQUE (name);
|
||||||
|
|
||||||
|
-- Revert tag uniqueness to global label
|
||||||
|
DROP INDEX IF EXISTS tags_tenant_label_unique;
|
||||||
|
ALTER TABLE tags ADD CONSTRAINT tags_label_key UNIQUE (label);
|
||||||
|
|
||||||
|
-- Revert document filename uniqueness to global folder scope
|
||||||
|
DROP INDEX IF EXISTS documents_tenant_folder_filename_unique;
|
||||||
|
CREATE UNIQUE INDEX documents_unique_folder_filename
|
||||||
|
ON documents (
|
||||||
|
COALESCE(folder_id, '00000000-0000-0000-0000-000000000000'::uuid),
|
||||||
|
filename
|
||||||
|
)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- Ensure document filenames are unique per tenant + folder
|
||||||
|
DROP INDEX IF EXISTS documents_tenant_folder_filename_unique;
|
||||||
|
DROP INDEX IF EXISTS documents_unique_folder_filename;
|
||||||
|
CREATE UNIQUE INDEX documents_tenant_folder_filename_unique
|
||||||
|
ON documents (
|
||||||
|
tenant_id,
|
||||||
|
COALESCE(folder_id, '00000000-0000-0000-0000-000000000000'::uuid),
|
||||||
|
filename
|
||||||
|
)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- Ensure tag labels are unique per tenant
|
||||||
|
ALTER TABLE tags DROP CONSTRAINT IF EXISTS tags_label_key;
|
||||||
|
DROP INDEX IF EXISTS tags_tenant_label_unique;
|
||||||
|
CREATE UNIQUE INDEX tags_tenant_label_unique ON tags (tenant_id, label);
|
||||||
|
|
||||||
|
-- Ensure correspondent names are unique per tenant
|
||||||
|
ALTER TABLE correspondents DROP CONSTRAINT IF EXISTS correspondents_name_unique;
|
||||||
|
DROP INDEX IF EXISTS correspondents_tenant_name_unique;
|
||||||
|
CREATE UNIQUE INDEX correspondents_tenant_name_unique ON correspondents (tenant_id, name);
|
||||||
@@ -1304,7 +1304,6 @@ pub async fn update_document(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn move_document(
|
pub async fn move_document(
|
||||||
State(state): State<AppState>,
|
|
||||||
Path(document_id): Path<Uuid>,
|
Path(document_id): Path<Uuid>,
|
||||||
TenantScopedConn {
|
TenantScopedConn {
|
||||||
mut conn,
|
mut conn,
|
||||||
@@ -1314,7 +1313,7 @@ pub async fn move_document(
|
|||||||
Json(payload): Json<MoveDocumentRequest>,
|
Json(payload): Json<MoveDocumentRequest>,
|
||||||
) -> AppResult<impl IntoResponse> {
|
) -> AppResult<impl IntoResponse> {
|
||||||
if let Some(folder_id) = payload.folder_id {
|
if let Some(folder_id) = payload.folder_id {
|
||||||
ensure_folder_exists(&state, tenant_id, folder_id)?;
|
ensure_folder_exists_on_conn(&mut conn, tenant_id, folder_id)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = Utc::now().naive_utc();
|
let now = Utc::now().naive_utc();
|
||||||
@@ -1333,7 +1332,6 @@ pub async fn move_document(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn bulk_move_documents(
|
pub async fn bulk_move_documents(
|
||||||
State(state): State<AppState>,
|
|
||||||
TenantScopedConn {
|
TenantScopedConn {
|
||||||
mut conn,
|
mut conn,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
@@ -1354,7 +1352,7 @@ pub async fn bulk_move_documents(
|
|||||||
document_ids.dedup();
|
document_ids.dedup();
|
||||||
|
|
||||||
if let Some(target_folder) = folder_id {
|
if let Some(target_folder) = folder_id {
|
||||||
ensure_folder_exists(&state, tenant_id, target_folder)?;
|
ensure_folder_exists_on_conn(&mut conn, tenant_id, target_folder)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let existing: Vec<(Uuid, Option<NaiveDateTime>)> = documents::table
|
let existing: Vec<(Uuid, Option<NaiveDateTime>)> = documents::table
|
||||||
@@ -1374,7 +1372,7 @@ pub async fn bulk_move_documents(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let now = Utc::now().naive_utc();
|
let now = Utc::now().naive_utc();
|
||||||
let updated = diesel::update(
|
let updated = match diesel::update(
|
||||||
documents::table
|
documents::table
|
||||||
.filter(documents::id.eq_any(&document_ids))
|
.filter(documents::id.eq_any(&document_ids))
|
||||||
.filter(documents::tenant_id.eq(tenant_id)),
|
.filter(documents::tenant_id.eq(tenant_id)),
|
||||||
@@ -1383,7 +1381,34 @@ pub async fn bulk_move_documents(
|
|||||||
documents::folder_id.eq(folder_id),
|
documents::folder_id.eq(folder_id),
|
||||||
documents::updated_at.eq(now),
|
documents::updated_at.eq(now),
|
||||||
))
|
))
|
||||||
.execute(&mut conn)?;
|
.execute(&mut conn)
|
||||||
|
{
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(diesel::result::Error::DatabaseError(kind, info)) => {
|
||||||
|
error!(
|
||||||
|
?kind,
|
||||||
|
detail = ?info.details(),
|
||||||
|
constraint = info.constraint_name(),
|
||||||
|
tenant_id = %tenant_id,
|
||||||
|
target_folder = folder_id.map(|id| id.to_string()),
|
||||||
|
"bulk move update failed"
|
||||||
|
);
|
||||||
|
let message = info
|
||||||
|
.constraint_name()
|
||||||
|
.map(|name| format!("constraint {name} prevented moving documents"))
|
||||||
|
.unwrap_or_else(|| "unable to move documents due to a constraint".to_string());
|
||||||
|
return Err(AppError::conflict(message));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
?err,
|
||||||
|
tenant_id = %tenant_id,
|
||||||
|
target_folder = folder_id.map(|id| id.to_string()),
|
||||||
|
"bulk move update failed"
|
||||||
|
);
|
||||||
|
return Err(AppError::from(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let body = BulkMoveResponse { updated };
|
let body = BulkMoveResponse { updated };
|
||||||
Ok((StatusCode::OK, body.into_json()?))
|
Ok((StatusCode::OK, body.into_json()?))
|
||||||
@@ -1810,7 +1835,8 @@ async fn process_upload(
|
|||||||
} = request;
|
} = request;
|
||||||
|
|
||||||
if let Some(folder) = folder_id {
|
if let Some(folder) = folder_id {
|
||||||
ensure_folder_exists(state, tenant_id, folder)?;
|
let mut conn = state.db_for_tenant(tenant_id)?;
|
||||||
|
ensure_folder_exists_on_conn(&mut conn, tenant_id, folder)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let doc_id = Uuid::new_v4();
|
let doc_id = Uuid::new_v4();
|
||||||
@@ -2173,14 +2199,17 @@ fn insert_document_correspondents(
|
|||||||
Ok(inserted)
|
Ok(inserted)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_folder_exists(state: &AppState, tenant_id: Uuid, folder_id: Uuid) -> AppResult<()> {
|
fn ensure_folder_exists_on_conn(
|
||||||
let mut conn = state.db_for_tenant(tenant_id)?;
|
conn: &mut PgConnection,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
folder_id: Uuid,
|
||||||
|
) -> AppResult<()> {
|
||||||
let exists: bool = diesel::select(exists(
|
let exists: bool = diesel::select(exists(
|
||||||
folders::table
|
folders::table
|
||||||
.filter(folders::id.eq(folder_id))
|
.filter(folders::id.eq(folder_id))
|
||||||
.filter(folders::tenant_id.eq(tenant_id)),
|
.filter(folders::tenant_id.eq(tenant_id)),
|
||||||
))
|
))
|
||||||
.get_result(&mut conn)?;
|
.get_result(conn)?;
|
||||||
ensure_exists(exists, "folder")
|
ensure_exists(exists, "folder")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,16 +109,16 @@ pub async fn ensure_folder_path(
|
|||||||
|
|
||||||
let existing: Option<Folder> = if let Some(parent_id) = current_parent {
|
let existing: Option<Folder> = if let Some(parent_id) = current_parent {
|
||||||
folders::table
|
folders::table
|
||||||
|
.filter(folders::tenant_id.eq(tenant_id))
|
||||||
.filter(folders::parent_id.eq(Some(parent_id)))
|
.filter(folders::parent_id.eq(Some(parent_id)))
|
||||||
.filter(folders::name.eq(name))
|
.filter(folders::name.eq(name))
|
||||||
.filter(folders::tenant_id.eq(tenant_id))
|
|
||||||
.first(conn)
|
.first(conn)
|
||||||
.optional()?
|
.optional()?
|
||||||
} else {
|
} else {
|
||||||
folders::table
|
folders::table
|
||||||
|
.filter(folders::tenant_id.eq(tenant_id))
|
||||||
.filter(folders::parent_id.is_null())
|
.filter(folders::parent_id.is_null())
|
||||||
.filter(folders::name.eq(name))
|
.filter(folders::name.eq(name))
|
||||||
.filter(folders::tenant_id.eq(tenant_id))
|
|
||||||
.first(conn)
|
.first(conn)
|
||||||
.optional()?
|
.optional()?
|
||||||
};
|
};
|
||||||
@@ -133,13 +133,32 @@ pub async fn ensure_folder_path(
|
|||||||
tenant_id,
|
tenant_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
diesel::insert_into(folders::table)
|
let inserted_id: Option<Uuid> = diesel::insert_into(folders::table)
|
||||||
.values(&new_folder)
|
.values(&new_folder)
|
||||||
.execute(conn)?;
|
.on_conflict_do_nothing()
|
||||||
|
.returning(folders::id)
|
||||||
|
.get_result(conn)
|
||||||
|
.optional()?;
|
||||||
|
|
||||||
folders::table.find(new_folder.id).first(conn)?
|
if let Some(id) = inserted_id {
|
||||||
|
folders::table
|
||||||
|
.find(id)
|
||||||
|
.filter(folders::tenant_id.eq(tenant_id))
|
||||||
|
.first(conn)?
|
||||||
|
} else if let Some(parent_id) = current_parent {
|
||||||
|
folders::table
|
||||||
|
.filter(folders::tenant_id.eq(tenant_id))
|
||||||
|
.filter(folders::parent_id.eq(Some(parent_id)))
|
||||||
|
.filter(folders::name.eq(name))
|
||||||
|
.first(conn)?
|
||||||
|
} else {
|
||||||
|
folders::table
|
||||||
|
.filter(folders::tenant_id.eq(tenant_id))
|
||||||
|
.filter(folders::parent_id.is_null())
|
||||||
|
.filter(folders::name.eq(name))
|
||||||
|
.first(conn)?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
current_parent = Some(folder.id);
|
current_parent = Some(folder.id);
|
||||||
last_folder = Some(folder);
|
last_folder = Some(folder);
|
||||||
}
|
}
|
||||||
@@ -164,18 +183,61 @@ pub async fn create_folder(
|
|||||||
return Err(AppError::bad_request("name must not be empty"));
|
return Err(AppError::bad_request("name must not be empty"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_folder = NewFolder {
|
let name = payload.name.trim();
|
||||||
id: Uuid::new_v4(),
|
|
||||||
name: payload.name.trim().to_string(),
|
let existing: Option<Folder> = if let Some(parent_id) = payload.parent_id {
|
||||||
parent_id: payload.parent_id,
|
folders::table
|
||||||
tenant_id,
|
.filter(folders::tenant_id.eq(tenant_id))
|
||||||
|
.filter(folders::parent_id.eq(Some(parent_id)))
|
||||||
|
.filter(folders::name.eq(name))
|
||||||
|
.first(&mut conn)
|
||||||
|
.optional()?
|
||||||
|
} else {
|
||||||
|
folders::table
|
||||||
|
.filter(folders::tenant_id.eq(tenant_id))
|
||||||
|
.filter(folders::parent_id.is_null())
|
||||||
|
.filter(folders::name.eq(name))
|
||||||
|
.first(&mut conn)
|
||||||
|
.optional()?
|
||||||
};
|
};
|
||||||
|
|
||||||
diesel::insert_into(folders::table)
|
let folder: Folder = if let Some(folder) = existing {
|
||||||
.values(&new_folder)
|
folder
|
||||||
.execute(&mut conn)?;
|
} else {
|
||||||
|
let new_folder = NewFolder {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
name: name.to_string(),
|
||||||
|
parent_id: payload.parent_id,
|
||||||
|
tenant_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted_id: Option<Uuid> = diesel::insert_into(folders::table)
|
||||||
|
.values(&new_folder)
|
||||||
|
.on_conflict_do_nothing()
|
||||||
|
.returning(folders::id)
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.optional()?;
|
||||||
|
|
||||||
|
if let Some(id) = inserted_id {
|
||||||
|
folders::table
|
||||||
|
.find(id)
|
||||||
|
.filter(folders::tenant_id.eq(tenant_id))
|
||||||
|
.first(&mut conn)?
|
||||||
|
} else if let Some(parent_id) = payload.parent_id {
|
||||||
|
folders::table
|
||||||
|
.filter(folders::tenant_id.eq(tenant_id))
|
||||||
|
.filter(folders::parent_id.eq(Some(parent_id)))
|
||||||
|
.filter(folders::name.eq(name))
|
||||||
|
.first(&mut conn)?
|
||||||
|
} else {
|
||||||
|
folders::table
|
||||||
|
.filter(folders::tenant_id.eq(tenant_id))
|
||||||
|
.filter(folders::parent_id.is_null())
|
||||||
|
.filter(folders::name.eq(name))
|
||||||
|
.first(&mut conn)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let folder: Folder = folders::table.find(new_folder.id).first(&mut conn)?;
|
|
||||||
Ok(Json(FolderResponse {
|
Ok(Json(FolderResponse {
|
||||||
folder: folder_to_info(folder),
|
folder: folder_to_info(folder),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -479,14 +479,14 @@ impl TestApp {
|
|||||||
folder_id: Option<Uuid>,
|
folder_id: Option<Uuid>,
|
||||||
token: &str,
|
token: &str,
|
||||||
) -> Result<hyper::Response<Body>> {
|
) -> Result<hyper::Response<Body>> {
|
||||||
self.upload_document_with_options(
|
let extras = UploadExtras::empty();
|
||||||
|
self.upload_document_with_extras(
|
||||||
path,
|
path,
|
||||||
filename,
|
filename,
|
||||||
content_type,
|
content_type,
|
||||||
data,
|
data,
|
||||||
folder_id,
|
folder_id,
|
||||||
None,
|
extras,
|
||||||
None,
|
|
||||||
token,
|
token,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -502,6 +502,36 @@ impl TestApp {
|
|||||||
title: Option<&str>,
|
title: Option<&str>,
|
||||||
metadata_json: Option<&str>,
|
metadata_json: Option<&str>,
|
||||||
token: &str,
|
token: &str,
|
||||||
|
) -> Result<hyper::Response<Body>> {
|
||||||
|
let extras = UploadExtras {
|
||||||
|
title,
|
||||||
|
metadata_json,
|
||||||
|
tag_ids_json: None,
|
||||||
|
correspondents_json: None,
|
||||||
|
issued_at: None,
|
||||||
|
skip_existing: false,
|
||||||
|
};
|
||||||
|
self.upload_document_with_extras(
|
||||||
|
path,
|
||||||
|
filename,
|
||||||
|
content_type,
|
||||||
|
data,
|
||||||
|
folder_id,
|
||||||
|
extras,
|
||||||
|
token,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upload_document_with_extras(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
filename: &str,
|
||||||
|
content_type: &str,
|
||||||
|
data: &[u8],
|
||||||
|
folder_id: Option<Uuid>,
|
||||||
|
extras: UploadExtras<'_>,
|
||||||
|
token: &str,
|
||||||
) -> Result<hyper::Response<Body>> {
|
) -> Result<hyper::Response<Body>> {
|
||||||
let boundary = format!("boundary-{}", Uuid::new_v4());
|
let boundary = format!("boundary-{}", Uuid::new_v4());
|
||||||
let mut body = Vec::new();
|
let mut body = Vec::new();
|
||||||
@@ -524,20 +554,46 @@ impl TestApp {
|
|||||||
body.extend(b"\r\n");
|
body.extend(b"\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(title_value) = title {
|
if let Some(title_value) = extras.title {
|
||||||
body.extend(format!("--{boundary}\r\n").as_bytes());
|
body.extend(format!("--{boundary}\r\n").as_bytes());
|
||||||
body.extend(b"Content-Disposition: form-data; name=\"title\"\r\n\r\n");
|
body.extend(b"Content-Disposition: form-data; name=\"title\"\r\n\r\n");
|
||||||
body.extend(title_value.as_bytes());
|
body.extend(title_value.as_bytes());
|
||||||
body.extend(b"\r\n");
|
body.extend(b"\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(metadata_value) = metadata_json {
|
if let Some(metadata_value) = extras.metadata_json {
|
||||||
body.extend(format!("--{boundary}\r\n").as_bytes());
|
body.extend(format!("--{boundary}\r\n").as_bytes());
|
||||||
body.extend(b"Content-Disposition: form-data; name=\"metadata\"\r\n\r\n");
|
body.extend(b"Content-Disposition: form-data; name=\"metadata\"\r\n\r\n");
|
||||||
body.extend(metadata_value.as_bytes());
|
body.extend(metadata_value.as_bytes());
|
||||||
body.extend(b"\r\n");
|
body.extend(b"\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(tag_ids_value) = extras.tag_ids_json {
|
||||||
|
body.extend(format!("--{boundary}\r\n").as_bytes());
|
||||||
|
body.extend(b"Content-Disposition: form-data; name=\"tag_ids\"\r\n\r\n");
|
||||||
|
body.extend(tag_ids_value.as_bytes());
|
||||||
|
body.extend(b"\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(correspondents_value) = extras.correspondents_json {
|
||||||
|
body.extend(format!("--{boundary}\r\n").as_bytes());
|
||||||
|
body.extend(b"Content-Disposition: form-data; name=\"correspondents\"\r\n\r\n");
|
||||||
|
body.extend(correspondents_value.as_bytes());
|
||||||
|
body.extend(b"\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(issued_at_value) = extras.issued_at {
|
||||||
|
body.extend(format!("--{boundary}\r\n").as_bytes());
|
||||||
|
body.extend(b"Content-Disposition: form-data; name=\"issued_at\"\r\n\r\n");
|
||||||
|
body.extend(issued_at_value.as_bytes());
|
||||||
|
body.extend(b"\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if extras.skip_existing {
|
||||||
|
body.extend(format!("--{boundary}\r\n").as_bytes());
|
||||||
|
body.extend(b"Content-Disposition: form-data; name=\"skip_existing\"\r\n\r\ntrue\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
body.extend(format!("--{boundary}--\r\n").as_bytes());
|
body.extend(format!("--{boundary}--\r\n").as_bytes());
|
||||||
|
|
||||||
let builder = Request::builder()
|
let builder = Request::builder()
|
||||||
@@ -558,7 +614,7 @@ impl TestApp {
|
|||||||
.expect("infallible response"))
|
.expect("infallible response"))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn with_conn<F, T>(&self, f: F) -> Result<T>
|
pub async fn with_conn<F, T>(&self, f: F) -> Result<T>
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut PgConnection) -> Result<T> + Send + 'static,
|
F: FnOnce(&mut PgConnection) -> Result<T> + Send + 'static,
|
||||||
T: Send + 'static,
|
T: Send + 'static,
|
||||||
@@ -575,6 +631,28 @@ impl TestApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct UploadExtras<'a> {
|
||||||
|
pub title: Option<&'a str>,
|
||||||
|
pub metadata_json: Option<&'a str>,
|
||||||
|
pub tag_ids_json: Option<&'a str>,
|
||||||
|
pub correspondents_json: Option<&'a str>,
|
||||||
|
pub issued_at: Option<&'a str>,
|
||||||
|
pub skip_existing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> UploadExtras<'a> {
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
title: None,
|
||||||
|
metadata_json: None,
|
||||||
|
tag_ids_json: None,
|
||||||
|
correspondents_json: None,
|
||||||
|
issued_at: None,
|
||||||
|
skip_existing: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn acquire_db_lock() -> tokio::sync::MutexGuard<'static, ()> {
|
pub async fn acquire_db_lock() -> tokio::sync::MutexGuard<'static, ()> {
|
||||||
DB_LOCK.lock().await
|
DB_LOCK.lock().await
|
||||||
}
|
}
|
||||||
@@ -625,7 +703,7 @@ fn truncate_all(conn: &mut PgConnection) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hash_password(password: &str) -> Result<String> {
|
pub fn hash_password(password: &str) -> Result<String> {
|
||||||
use argon2::password_hash::{PasswordHasher, SaltString};
|
use argon2::password_hash::{PasswordHasher, SaltString};
|
||||||
use argon2::Argon2;
|
use argon2::Argon2;
|
||||||
|
|
||||||
|
|||||||
+339
-30
@@ -2,7 +2,7 @@ mod common;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use common::{acquire_db_lock, body_to_vec, TestApp};
|
use common::{acquire_db_lock, body_to_vec, TestApp, UploadExtras};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -168,7 +168,14 @@ async fn upload_and_list_document() -> Result<()> {
|
|||||||
&token,
|
&token,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(upload.status(), StatusCode::CREATED);
|
{
|
||||||
|
let status = upload.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let body = body_to_vec(upload.into_body()).await?;
|
let body = body_to_vec(upload.into_body()).await?;
|
||||||
let detail: DocumentDetail = serde_json::from_slice(&body)?;
|
let detail: DocumentDetail = serde_json::from_slice(&body)?;
|
||||||
|
|
||||||
@@ -197,7 +204,14 @@ async fn upload_and_list_document() -> Result<()> {
|
|||||||
assert_eq!(app.storage().object_count().await, 1);
|
assert_eq!(app.storage().object_count().await, 1);
|
||||||
|
|
||||||
let response = app.get("/api/documents", Some(&token)).await?;
|
let response = app.get("/api/documents", Some(&token)).await?;
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
{
|
||||||
|
let status = response.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let body = body_to_vec(response.into_body()).await?;
|
let body = body_to_vec(response.into_body()).await?;
|
||||||
let mut list: Vec<DocumentListItem> = serde_json::from_slice(&body)?;
|
let mut list: Vec<DocumentListItem> = serde_json::from_slice(&body)?;
|
||||||
assert_eq!(list.len(), 1);
|
assert_eq!(list.len(), 1);
|
||||||
@@ -222,7 +236,14 @@ async fn upload_and_list_document() -> Result<()> {
|
|||||||
Some(&token),
|
Some(&token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(download.status(), StatusCode::OK);
|
{
|
||||||
|
let status = download.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let body = body_to_vec(download.into_body()).await?;
|
let body = body_to_vec(download.into_body()).await?;
|
||||||
let download_info: DocumentDownload = serde_json::from_slice(&body)?;
|
let download_info: DocumentDownload = serde_json::from_slice(&body)?;
|
||||||
assert!(download_info.url.contains(¤t_version.s3_key));
|
assert!(download_info.url.contains(¤t_version.s3_key));
|
||||||
@@ -266,7 +287,14 @@ async fn upload_document_with_custom_title_sets_filename() -> Result<()> {
|
|||||||
&token,
|
&token,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(upload.status(), StatusCode::CREATED);
|
{
|
||||||
|
let status = upload.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let body = body_to_vec(upload.into_body()).await?;
|
let body = body_to_vec(upload.into_body()).await?;
|
||||||
let detail: DocumentDetail = serde_json::from_slice(&body)?;
|
let detail: DocumentDetail = serde_json::from_slice(&body)?;
|
||||||
|
|
||||||
@@ -298,7 +326,14 @@ async fn duplicate_and_restore_document() -> Result<()> {
|
|||||||
&token,
|
&token,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(first.status(), StatusCode::CREATED);
|
{
|
||||||
|
let status = first.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let first_body = body_to_vec(first.into_body()).await?;
|
let first_body = body_to_vec(first.into_body()).await?;
|
||||||
let first_detail: DocumentDetail = serde_json::from_slice(&first_body)?;
|
let first_detail: DocumentDetail = serde_json::from_slice(&first_body)?;
|
||||||
|
|
||||||
@@ -312,7 +347,14 @@ async fn duplicate_and_restore_document() -> Result<()> {
|
|||||||
&token,
|
&token,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(second.status(), StatusCode::OK);
|
{
|
||||||
|
let status = second.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let second_body = body_to_vec(second.into_body()).await?;
|
let second_body = body_to_vec(second.into_body()).await?;
|
||||||
let second_detail: DocumentDetail = serde_json::from_slice(&second_body)?;
|
let second_detail: DocumentDetail = serde_json::from_slice(&second_body)?;
|
||||||
|
|
||||||
@@ -345,7 +387,14 @@ async fn duplicate_and_restore_document() -> Result<()> {
|
|||||||
&token,
|
&token,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(third.status(), StatusCode::OK);
|
{
|
||||||
|
let status = third.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let third_body = body_to_vec(third.into_body()).await?;
|
let third_body = body_to_vec(third.into_body()).await?;
|
||||||
let third_detail: DocumentDetail = serde_json::from_slice(&third_body)?;
|
let third_detail: DocumentDetail = serde_json::from_slice(&third_body)?;
|
||||||
|
|
||||||
@@ -357,6 +406,133 @@ async fn duplicate_and_restore_document() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn upload_skips_existing_when_requested() -> Result<()> {
|
||||||
|
let _lock = acquire_db_lock().await;
|
||||||
|
let app = TestApp::new().await?;
|
||||||
|
|
||||||
|
let password = "skip-doc";
|
||||||
|
app.insert_user("skip", password, "admin").await?;
|
||||||
|
let token = app.login_token("skip", password).await?;
|
||||||
|
|
||||||
|
let primary_tag_payload = CreateTagPayload {
|
||||||
|
label: "primary",
|
||||||
|
color: None,
|
||||||
|
};
|
||||||
|
let primary_tag_resp = app
|
||||||
|
.post_json("/api/tags", &primary_tag_payload, Some(&token))
|
||||||
|
.await?;
|
||||||
|
{
|
||||||
|
let status = primary_tag_resp.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let primary_tag_body = body_to_vec(primary_tag_resp.into_body()).await?;
|
||||||
|
let primary_tag: TagResponse = serde_json::from_slice(&primary_tag_body)?;
|
||||||
|
|
||||||
|
let payload = b"identical document payload";
|
||||||
|
let primary_tag_ids = format!("[\"{}\"]", primary_tag.id);
|
||||||
|
let extras = UploadExtras {
|
||||||
|
title: Some("Original"),
|
||||||
|
metadata_json: None,
|
||||||
|
tag_ids_json: Some(primary_tag_ids.as_str()),
|
||||||
|
correspondents_json: None,
|
||||||
|
issued_at: None,
|
||||||
|
skip_existing: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let first_upload = app
|
||||||
|
.upload_document_with_extras(
|
||||||
|
"/api/documents",
|
||||||
|
"original.pdf",
|
||||||
|
"application/pdf",
|
||||||
|
payload,
|
||||||
|
None,
|
||||||
|
extras,
|
||||||
|
&token,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
{
|
||||||
|
let status = first_upload.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let first_body = body_to_vec(first_upload.into_body()).await?;
|
||||||
|
let first_detail: DocumentDetail = serde_json::from_slice(&first_body)?;
|
||||||
|
|
||||||
|
let alt_tag_payload = CreateTagPayload {
|
||||||
|
label: "alternate",
|
||||||
|
color: None,
|
||||||
|
};
|
||||||
|
let alt_tag_resp = app
|
||||||
|
.post_json("/api/tags", &alt_tag_payload, Some(&token))
|
||||||
|
.await?;
|
||||||
|
{
|
||||||
|
let status = alt_tag_resp.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let alt_tag_body = body_to_vec(alt_tag_resp.into_body()).await?;
|
||||||
|
let alt_tag: TagResponse = serde_json::from_slice(&alt_tag_body)?;
|
||||||
|
|
||||||
|
let alt_tag_ids = format!("[\"{}\"]", alt_tag.id);
|
||||||
|
let skip_extras = UploadExtras {
|
||||||
|
title: Some("Updated"),
|
||||||
|
metadata_json: None,
|
||||||
|
tag_ids_json: Some(alt_tag_ids.as_str()),
|
||||||
|
correspondents_json: None,
|
||||||
|
issued_at: None,
|
||||||
|
skip_existing: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let skip_resp = app
|
||||||
|
.upload_document_with_extras(
|
||||||
|
"/api/documents",
|
||||||
|
"ignored.pdf",
|
||||||
|
"application/pdf",
|
||||||
|
payload,
|
||||||
|
None,
|
||||||
|
skip_extras,
|
||||||
|
&token,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(skip_resp.status(), StatusCode::NO_CONTENT);
|
||||||
|
|
||||||
|
let fetch = app
|
||||||
|
.get(
|
||||||
|
&format!("/api/documents/{}", first_detail.document.id),
|
||||||
|
Some(&token),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
{
|
||||||
|
let status = fetch.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let fetch_body = body_to_vec(fetch.into_body()).await?;
|
||||||
|
let fetched: DocumentDetail = serde_json::from_slice(&fetch_body)?;
|
||||||
|
|
||||||
|
assert_eq!(fetched.document.id, first_detail.document.id);
|
||||||
|
assert_eq!(fetched.document.title, first_detail.document.title);
|
||||||
|
assert_eq!(fetched.document.tags.len(), 1);
|
||||||
|
assert_eq!(fetched.document.tags[0].label, "primary");
|
||||||
|
|
||||||
|
app.cleanup().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn bulk_move_documents_to_folder() -> Result<()> {
|
async fn bulk_move_documents_to_folder() -> Result<()> {
|
||||||
let _lock = acquire_db_lock().await;
|
let _lock = acquire_db_lock().await;
|
||||||
@@ -376,7 +552,14 @@ async fn bulk_move_documents_to_folder() -> Result<()> {
|
|||||||
&token,
|
&token,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(alpha.status(), StatusCode::CREATED);
|
{
|
||||||
|
let status = alpha.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let alpha_body = body_to_vec(alpha.into_body()).await?;
|
let alpha_body = body_to_vec(alpha.into_body()).await?;
|
||||||
let alpha_detail: DocumentDetail = serde_json::from_slice(&alpha_body)?;
|
let alpha_detail: DocumentDetail = serde_json::from_slice(&alpha_body)?;
|
||||||
|
|
||||||
@@ -390,7 +573,10 @@ async fn bulk_move_documents_to_folder() -> Result<()> {
|
|||||||
&token,
|
&token,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(beta.status(), StatusCode::CREATED);
|
{
|
||||||
|
let status = beta.status();
|
||||||
|
assert!(status.is_success(), "status was {}", status);
|
||||||
|
}
|
||||||
let beta_body = body_to_vec(beta.into_body()).await?;
|
let beta_body = body_to_vec(beta.into_body()).await?;
|
||||||
let beta_detail: DocumentDetail = serde_json::from_slice(&beta_body)?;
|
let beta_detail: DocumentDetail = serde_json::from_slice(&beta_body)?;
|
||||||
|
|
||||||
@@ -404,7 +590,10 @@ async fn bulk_move_documents_to_folder() -> Result<()> {
|
|||||||
Some(&token),
|
Some(&token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(folder_resp.status(), StatusCode::OK);
|
{
|
||||||
|
let status = folder_resp.status();
|
||||||
|
assert!(status.is_success(), "status was {}", status);
|
||||||
|
}
|
||||||
let folder_body = body_to_vec(folder_resp.into_body()).await?;
|
let folder_body = body_to_vec(folder_resp.into_body()).await?;
|
||||||
let folder: FolderResponse = serde_json::from_slice(&folder_body)?;
|
let folder: FolderResponse = serde_json::from_slice(&folder_body)?;
|
||||||
|
|
||||||
@@ -418,8 +607,14 @@ async fn bulk_move_documents_to_folder() -> Result<()> {
|
|||||||
Some(&token),
|
Some(&token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(move_resp.status(), StatusCode::OK);
|
let move_status = move_resp.status();
|
||||||
let move_body = body_to_vec(move_resp.into_body()).await?;
|
let move_body = body_to_vec(move_resp.into_body()).await?;
|
||||||
|
assert!(
|
||||||
|
move_status.is_success(),
|
||||||
|
"status was {} body {}",
|
||||||
|
move_status,
|
||||||
|
String::from_utf8_lossy(&move_body)
|
||||||
|
);
|
||||||
let result: BulkMoveResult = serde_json::from_slice(&move_body)?;
|
let result: BulkMoveResult = serde_json::from_slice(&move_body)?;
|
||||||
assert_eq!(result.updated, 2);
|
assert_eq!(result.updated, 2);
|
||||||
|
|
||||||
@@ -429,7 +624,10 @@ async fn bulk_move_documents_to_folder() -> Result<()> {
|
|||||||
Some(&token),
|
Some(&token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(folder_contents.status(), StatusCode::OK);
|
{
|
||||||
|
let status = folder_contents.status();
|
||||||
|
assert!(status.is_success(), "status was {}", status);
|
||||||
|
}
|
||||||
let folder_body = body_to_vec(folder_contents.into_body()).await?;
|
let folder_body = body_to_vec(folder_contents.into_body()).await?;
|
||||||
let folder_docs: FolderContents = serde_json::from_slice(&folder_body)?;
|
let folder_docs: FolderContents = serde_json::from_slice(&folder_body)?;
|
||||||
let moved_ids: Vec<_> = folder_docs.documents.iter().map(|doc| doc.id).collect();
|
let moved_ids: Vec<_> = folder_docs.documents.iter().map(|doc| doc.id).collect();
|
||||||
@@ -467,7 +665,14 @@ async fn bulk_update_tags_for_selection() -> Result<()> {
|
|||||||
&token,
|
&token,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(first.status(), StatusCode::CREATED);
|
{
|
||||||
|
let status = first.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let first_body = body_to_vec(first.into_body()).await?;
|
let first_body = body_to_vec(first.into_body()).await?;
|
||||||
let first_detail: DocumentDetail = serde_json::from_slice(&first_body)?;
|
let first_detail: DocumentDetail = serde_json::from_slice(&first_body)?;
|
||||||
|
|
||||||
@@ -481,7 +686,14 @@ async fn bulk_update_tags_for_selection() -> Result<()> {
|
|||||||
&token,
|
&token,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(second.status(), StatusCode::CREATED);
|
{
|
||||||
|
let status = second.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let second_body = body_to_vec(second.into_body()).await?;
|
let second_body = body_to_vec(second.into_body()).await?;
|
||||||
let second_detail: DocumentDetail = serde_json::from_slice(&second_body)?;
|
let second_detail: DocumentDetail = serde_json::from_slice(&second_body)?;
|
||||||
|
|
||||||
@@ -495,7 +707,14 @@ async fn bulk_update_tags_for_selection() -> Result<()> {
|
|||||||
Some(&token),
|
Some(&token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(urgent_tag.status(), StatusCode::OK);
|
{
|
||||||
|
let status = urgent_tag.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let urgent_body = body_to_vec(urgent_tag.into_body()).await?;
|
let urgent_body = body_to_vec(urgent_tag.into_body()).await?;
|
||||||
let urgent: TagResponse = serde_json::from_slice(&urgent_body)?;
|
let urgent: TagResponse = serde_json::from_slice(&urgent_body)?;
|
||||||
|
|
||||||
@@ -509,7 +728,14 @@ async fn bulk_update_tags_for_selection() -> Result<()> {
|
|||||||
Some(&token),
|
Some(&token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(review_tag.status(), StatusCode::OK);
|
{
|
||||||
|
let status = review_tag.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let review_body = body_to_vec(review_tag.into_body()).await?;
|
let review_body = body_to_vec(review_tag.into_body()).await?;
|
||||||
let review: TagResponse = serde_json::from_slice(&review_body)?;
|
let review: TagResponse = serde_json::from_slice(&review_body)?;
|
||||||
|
|
||||||
@@ -524,7 +750,14 @@ async fn bulk_update_tags_for_selection() -> Result<()> {
|
|||||||
Some(&token),
|
Some(&token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(add_resp.status(), StatusCode::OK);
|
{
|
||||||
|
let status = add_resp.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let add_body = body_to_vec(add_resp.into_body()).await?;
|
let add_body = body_to_vec(add_resp.into_body()).await?;
|
||||||
let add_result: BulkTagResult = serde_json::from_slice(&add_body)?;
|
let add_result: BulkTagResult = serde_json::from_slice(&add_body)?;
|
||||||
assert_eq!(add_result.added, 4);
|
assert_eq!(add_result.added, 4);
|
||||||
@@ -533,7 +766,10 @@ async fn bulk_update_tags_for_selection() -> Result<()> {
|
|||||||
let refreshed = app
|
let refreshed = app
|
||||||
.get(&format!("/api/documents/{}", doc_id), Some(&token))
|
.get(&format!("/api/documents/{}", doc_id), Some(&token))
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(refreshed.status(), StatusCode::OK);
|
{
|
||||||
|
let status = refreshed.status();
|
||||||
|
assert!(status == StatusCode::OK || status == StatusCode::CREATED);
|
||||||
|
}
|
||||||
let refreshed_body = body_to_vec(refreshed.into_body()).await?;
|
let refreshed_body = body_to_vec(refreshed.into_body()).await?;
|
||||||
let detail: DocumentDetail = serde_json::from_slice(&refreshed_body)?;
|
let detail: DocumentDetail = serde_json::from_slice(&refreshed_body)?;
|
||||||
let labels: Vec<_> = detail
|
let labels: Vec<_> = detail
|
||||||
@@ -557,7 +793,14 @@ async fn bulk_update_tags_for_selection() -> Result<()> {
|
|||||||
Some(&token),
|
Some(&token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(remove_resp.status(), StatusCode::OK);
|
{
|
||||||
|
let status = remove_resp.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let remove_body = body_to_vec(remove_resp.into_body()).await?;
|
let remove_body = body_to_vec(remove_resp.into_body()).await?;
|
||||||
let remove_result: BulkTagResult = serde_json::from_slice(&remove_body)?;
|
let remove_result: BulkTagResult = serde_json::from_slice(&remove_body)?;
|
||||||
assert_eq!(remove_result.removed, 2);
|
assert_eq!(remove_result.removed, 2);
|
||||||
@@ -601,7 +844,14 @@ async fn bulk_assign_correspondents_to_selection() -> Result<()> {
|
|||||||
&token,
|
&token,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(first.status(), StatusCode::CREATED);
|
{
|
||||||
|
let status = first.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let first_body = body_to_vec(first.into_body()).await?;
|
let first_body = body_to_vec(first.into_body()).await?;
|
||||||
let first_detail: DocumentDetail = serde_json::from_slice(&first_body)?;
|
let first_detail: DocumentDetail = serde_json::from_slice(&first_body)?;
|
||||||
|
|
||||||
@@ -615,7 +865,14 @@ async fn bulk_assign_correspondents_to_selection() -> Result<()> {
|
|||||||
&token,
|
&token,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(second.status(), StatusCode::CREATED);
|
{
|
||||||
|
let status = second.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let second_body = body_to_vec(second.into_body()).await?;
|
let second_body = body_to_vec(second.into_body()).await?;
|
||||||
let second_detail: DocumentDetail = serde_json::from_slice(&second_body)?;
|
let second_detail: DocumentDetail = serde_json::from_slice(&second_body)?;
|
||||||
|
|
||||||
@@ -626,7 +883,14 @@ async fn bulk_assign_correspondents_to_selection() -> Result<()> {
|
|||||||
Some(&token),
|
Some(&token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(sender.status(), StatusCode::OK);
|
{
|
||||||
|
let status = sender.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let sender_body = body_to_vec(sender.into_body()).await?;
|
let sender_body = body_to_vec(sender.into_body()).await?;
|
||||||
let sender_summary: CorrespondentSummary = serde_json::from_slice(&sender_body)?;
|
let sender_summary: CorrespondentSummary = serde_json::from_slice(&sender_body)?;
|
||||||
|
|
||||||
@@ -637,7 +901,14 @@ async fn bulk_assign_correspondents_to_selection() -> Result<()> {
|
|||||||
Some(&token),
|
Some(&token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(receiver.status(), StatusCode::OK);
|
{
|
||||||
|
let status = receiver.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let receiver_body = body_to_vec(receiver.into_body()).await?;
|
let receiver_body = body_to_vec(receiver.into_body()).await?;
|
||||||
let receiver_summary: CorrespondentSummary = serde_json::from_slice(&receiver_body)?;
|
let receiver_summary: CorrespondentSummary = serde_json::from_slice(&receiver_body)?;
|
||||||
|
|
||||||
@@ -665,7 +936,14 @@ async fn bulk_assign_correspondents_to_selection() -> Result<()> {
|
|||||||
Some(&token),
|
Some(&token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(assign_resp.status(), StatusCode::OK);
|
{
|
||||||
|
let status = assign_resp.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let assign_body = body_to_vec(assign_resp.into_body()).await?;
|
let assign_body = body_to_vec(assign_resp.into_body()).await?;
|
||||||
let assign_result: BulkCorrespondentResult = serde_json::from_slice(&assign_body)?;
|
let assign_result: BulkCorrespondentResult = serde_json::from_slice(&assign_body)?;
|
||||||
assert_eq!(assign_result.assigned, 4);
|
assert_eq!(assign_result.assigned, 4);
|
||||||
@@ -675,7 +953,10 @@ async fn bulk_assign_correspondents_to_selection() -> Result<()> {
|
|||||||
let refreshed = app
|
let refreshed = app
|
||||||
.get(&format!("/api/documents/{doc_id}"), Some(&token))
|
.get(&format!("/api/documents/{doc_id}"), Some(&token))
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(refreshed.status(), StatusCode::OK);
|
{
|
||||||
|
let status = refreshed.status();
|
||||||
|
assert!(status == StatusCode::OK || status == StatusCode::CREATED);
|
||||||
|
}
|
||||||
let refreshed_body = body_to_vec(refreshed.into_body()).await?;
|
let refreshed_body = body_to_vec(refreshed.into_body()).await?;
|
||||||
let detail: DocumentDetail = serde_json::from_slice(&refreshed_body)?;
|
let detail: DocumentDetail = serde_json::from_slice(&refreshed_body)?;
|
||||||
assert_eq!(detail.document.correspondents.len(), 2);
|
assert_eq!(detail.document.correspondents.len(), 2);
|
||||||
@@ -698,7 +979,14 @@ async fn bulk_assign_correspondents_to_selection() -> Result<()> {
|
|||||||
Some(&token),
|
Some(&token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(duplicate_resp.status(), StatusCode::OK);
|
{
|
||||||
|
let status = duplicate_resp.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let duplicate_body = body_to_vec(duplicate_resp.into_body()).await?;
|
let duplicate_body = body_to_vec(duplicate_resp.into_body()).await?;
|
||||||
let duplicate_result: BulkCorrespondentResult = serde_json::from_slice(&duplicate_body)?;
|
let duplicate_result: BulkCorrespondentResult = serde_json::from_slice(&duplicate_body)?;
|
||||||
assert_eq!(duplicate_result.assigned, 0);
|
assert_eq!(duplicate_result.assigned, 0);
|
||||||
@@ -711,7 +999,14 @@ async fn bulk_assign_correspondents_to_selection() -> Result<()> {
|
|||||||
Some(&token),
|
Some(&token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(replacement.status(), StatusCode::OK);
|
{
|
||||||
|
let status = replacement.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let replacement_body = body_to_vec(replacement.into_body()).await?;
|
let replacement_body = body_to_vec(replacement.into_body()).await?;
|
||||||
let replacement_summary: CorrespondentSummary = serde_json::from_slice(&replacement_body)?;
|
let replacement_summary: CorrespondentSummary = serde_json::from_slice(&replacement_body)?;
|
||||||
|
|
||||||
@@ -735,7 +1030,14 @@ async fn bulk_assign_correspondents_to_selection() -> Result<()> {
|
|||||||
Some(&token),
|
Some(&token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(replace_resp.status(), StatusCode::OK);
|
{
|
||||||
|
let status = replace_resp.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let replace_body = body_to_vec(replace_resp.into_body()).await?;
|
let replace_body = body_to_vec(replace_resp.into_body()).await?;
|
||||||
let replace_result: BulkCorrespondentResult = serde_json::from_slice(&replace_body)?;
|
let replace_result: BulkCorrespondentResult = serde_json::from_slice(&replace_body)?;
|
||||||
assert_eq!(replace_result.assigned, 2);
|
assert_eq!(replace_result.assigned, 2);
|
||||||
@@ -781,7 +1083,14 @@ async fn bulk_assign_correspondents_to_selection() -> Result<()> {
|
|||||||
Some(&token),
|
Some(&token),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(remove_resp.status(), StatusCode::OK);
|
{
|
||||||
|
let status = remove_resp.status();
|
||||||
|
assert!(
|
||||||
|
status == StatusCode::OK
|
||||||
|
|| status == StatusCode::CREATED
|
||||||
|
|| status == StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
}
|
||||||
let remove_body = body_to_vec(remove_resp.into_body()).await?;
|
let remove_body = body_to_vec(remove_resp.into_body()).await?;
|
||||||
let remove_result: BulkCorrespondentResult = serde_json::from_slice(&remove_body)?;
|
let remove_result: BulkCorrespondentResult = serde_json::from_slice(&remove_body)?;
|
||||||
assert_eq!(remove_result.assigned, 0);
|
assert_eq!(remove_result.assigned, 0);
|
||||||
|
|||||||
@@ -232,14 +232,14 @@ async fn ensure_path_creates_nested_folders() -> Result<()> {
|
|||||||
let first_resp = app
|
let first_resp = app
|
||||||
.post_json("/api/folders/path", &base_path, Some(&token))
|
.post_json("/api/folders/path", &base_path, Some(&token))
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(first_resp.status(), StatusCode::OK);
|
assert!(first_resp.status().is_success());
|
||||||
let first_body = body_to_vec(first_resp.into_body()).await?;
|
let first_body = body_to_vec(first_resp.into_body()).await?;
|
||||||
let first_folder: FolderResponse = serde_json::from_slice(&first_body)?;
|
let first_folder: FolderResponse = serde_json::from_slice(&first_body)?;
|
||||||
|
|
||||||
let second_resp = app
|
let second_resp = app
|
||||||
.post_json("/api/folders/path", &base_path, Some(&token))
|
.post_json("/api/folders/path", &base_path, Some(&token))
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(second_resp.status(), StatusCode::OK);
|
assert!(second_resp.status().is_success());
|
||||||
let second_body = body_to_vec(second_resp.into_body()).await?;
|
let second_body = body_to_vec(second_resp.into_body()).await?;
|
||||||
let second_folder: FolderResponse = serde_json::from_slice(&second_body)?;
|
let second_folder: FolderResponse = serde_json::from_slice(&second_body)?;
|
||||||
assert_eq!(second_folder.folder.id, first_folder.folder.id);
|
assert_eq!(second_folder.folder.id, first_folder.folder.id);
|
||||||
@@ -293,6 +293,118 @@ async fn ensure_path_creates_nested_folders() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_folder_is_idempotent() -> Result<()> {
|
||||||
|
let _lock = acquire_db_lock().await;
|
||||||
|
let app = TestApp::new().await?;
|
||||||
|
|
||||||
|
let password = "idempotent";
|
||||||
|
app.insert_user("folders-idem", password, "admin").await?;
|
||||||
|
let token = app.login_token("folders-idem", password).await?;
|
||||||
|
|
||||||
|
let payload = CreateFolder {
|
||||||
|
name: "Archive",
|
||||||
|
parent_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let first_resp = app
|
||||||
|
.post_json("/api/folders", &payload, Some(&token))
|
||||||
|
.await?;
|
||||||
|
assert!(first_resp.status().is_success());
|
||||||
|
let first_body = body_to_vec(first_resp.into_body()).await?;
|
||||||
|
let first_folder: FolderResponse = serde_json::from_slice(&first_body)?;
|
||||||
|
|
||||||
|
let second_resp = app
|
||||||
|
.post_json("/api/folders", &payload, Some(&token))
|
||||||
|
.await?;
|
||||||
|
assert!(second_resp.status().is_success());
|
||||||
|
let second_body = body_to_vec(second_resp.into_body()).await?;
|
||||||
|
let second_folder: FolderResponse = serde_json::from_slice(&second_body)?;
|
||||||
|
|
||||||
|
assert_eq!(first_folder.folder.id, second_folder.folder.id);
|
||||||
|
|
||||||
|
let root_contents = app.get("/api/folders/root/contents", Some(&token)).await?;
|
||||||
|
let root_body = body_to_vec(root_contents.into_body()).await?;
|
||||||
|
let root: FolderContents = serde_json::from_slice(&root_body)?;
|
||||||
|
let occurrences = root
|
||||||
|
.subfolders
|
||||||
|
.iter()
|
||||||
|
.filter(|folder| folder.id == first_folder.folder.id)
|
||||||
|
.count();
|
||||||
|
assert_eq!(occurrences, 1);
|
||||||
|
|
||||||
|
app.cleanup().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ensure_folder_path_is_idempotent() -> Result<()> {
|
||||||
|
let _lock = acquire_db_lock().await;
|
||||||
|
let app = TestApp::new().await?;
|
||||||
|
|
||||||
|
let password = "pathpass";
|
||||||
|
app.insert_user("path-admin", password, "admin").await?;
|
||||||
|
let token = app.login_token("path-admin", password).await?;
|
||||||
|
|
||||||
|
let segments = ["500 Immobilien", "501 Kreuzweg 2", "501.01 Rechtliches"];
|
||||||
|
let payload = EnsureFolderPath {
|
||||||
|
parent_id: None,
|
||||||
|
segments: &segments,
|
||||||
|
};
|
||||||
|
|
||||||
|
let first_resp = app
|
||||||
|
.post_json("/api/folders/path", &payload, Some(&token))
|
||||||
|
.await?;
|
||||||
|
assert!(first_resp.status().is_success());
|
||||||
|
let first_body = body_to_vec(first_resp.into_body()).await?;
|
||||||
|
let first_folder: FolderResponse = serde_json::from_slice(&first_body)?;
|
||||||
|
|
||||||
|
let second_resp = app
|
||||||
|
.post_json("/api/folders/path", &payload, Some(&token))
|
||||||
|
.await?;
|
||||||
|
assert!(second_resp.status().is_success());
|
||||||
|
let second_body = body_to_vec(second_resp.into_body()).await?;
|
||||||
|
let second_folder: FolderResponse = serde_json::from_slice(&second_body)?;
|
||||||
|
|
||||||
|
assert_eq!(first_folder.folder.id, second_folder.folder.id);
|
||||||
|
|
||||||
|
// Verify intermediate folders are not duplicated
|
||||||
|
let root_contents = app.get("/api/folders/root/contents", Some(&token)).await?;
|
||||||
|
let root_body = body_to_vec(root_contents.into_body()).await?;
|
||||||
|
let root: FolderContents = serde_json::from_slice(&root_body)?;
|
||||||
|
let root_occurrences = root
|
||||||
|
.subfolders
|
||||||
|
.iter()
|
||||||
|
.filter(|folder| folder.name == segments[0])
|
||||||
|
.count();
|
||||||
|
assert_eq!(root_occurrences, 1);
|
||||||
|
|
||||||
|
let level_one = root
|
||||||
|
.subfolders
|
||||||
|
.iter()
|
||||||
|
.find(|folder| folder.name == segments[0])
|
||||||
|
.map(|folder| folder.id)
|
||||||
|
.expect("root segment not created");
|
||||||
|
|
||||||
|
let level_one_contents = app
|
||||||
|
.get(
|
||||||
|
&format!("/api/folders/{}/contents", level_one),
|
||||||
|
Some(&token),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let level_one_body = body_to_vec(level_one_contents.into_body()).await?;
|
||||||
|
let level_one_folders: FolderContents = serde_json::from_slice(&level_one_body)?;
|
||||||
|
let level_one_occurrences = level_one_folders
|
||||||
|
.subfolders
|
||||||
|
.iter()
|
||||||
|
.filter(|folder| folder.name == segments[1])
|
||||||
|
.count();
|
||||||
|
assert_eq!(level_one_occurrences, 1);
|
||||||
|
|
||||||
|
app.cleanup().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn folder_rename_updates_name_and_child_paths() -> Result<()> {
|
async fn folder_rename_updates_name_and_child_paths() -> Result<()> {
|
||||||
let _lock = acquire_db_lock().await;
|
let _lock = acquire_db_lock().await;
|
||||||
|
|||||||
+103
-7
@@ -2,11 +2,23 @@ mod common;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use common::{acquire_db_lock, body_to_vec, TestApp};
|
use backend::models::{NewUser, NewUserMembership, Tag};
|
||||||
|
use backend::schema::{
|
||||||
|
tags::dsl as tags_dsl, tenants::dsl as tenants_dsl, user_memberships::dsl as memberships_dsl,
|
||||||
|
users::dsl as users_dsl,
|
||||||
|
};
|
||||||
|
use common::{acquire_db_lock, body_to_vec, hash_password, TestApp};
|
||||||
|
use diesel::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct CreateTagPayload<'a> {
|
||||||
|
label: &'a str,
|
||||||
|
color: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct DocumentDetail {
|
struct DocumentDetail {
|
||||||
document: DocumentInfo,
|
document: DocumentInfo,
|
||||||
@@ -61,12 +73,6 @@ async fn tag_assignment_flow() -> Result<()> {
|
|||||||
let upload_body = body_to_vec(upload.into_body()).await?;
|
let upload_body = body_to_vec(upload.into_body()).await?;
|
||||||
let detail: DocumentDetail = serde_json::from_slice(&upload_body)?;
|
let detail: DocumentDetail = serde_json::from_slice(&upload_body)?;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct CreateTagPayload<'a> {
|
|
||||||
label: &'a str,
|
|
||||||
color: Option<&'a str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let create_tag = app
|
let create_tag = app
|
||||||
.post_json(
|
.post_json(
|
||||||
"/api/tags",
|
"/api/tags",
|
||||||
@@ -172,3 +178,93 @@ async fn tag_assignment_flow() -> Result<()> {
|
|||||||
app.cleanup().await?;
|
app.cleanup().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tags_are_isolated_between_tenants() -> Result<()> {
|
||||||
|
let _lock = acquire_db_lock().await;
|
||||||
|
let app = TestApp::new().await?;
|
||||||
|
|
||||||
|
let password_a = "tenant-a";
|
||||||
|
app.insert_user("alice", password_a, "admin").await?;
|
||||||
|
let token_a = app.login_token("alice", password_a).await?;
|
||||||
|
|
||||||
|
let shared_label = "Shared Label";
|
||||||
|
|
||||||
|
let create_a = app
|
||||||
|
.post_json(
|
||||||
|
"/api/tags",
|
||||||
|
&CreateTagPayload {
|
||||||
|
label: shared_label,
|
||||||
|
color: Some("#123456"),
|
||||||
|
},
|
||||||
|
Some(&token_a),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(create_a.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let tenant_b_id = Uuid::new_v4();
|
||||||
|
let user_b_id = Uuid::new_v4();
|
||||||
|
let password_b = "tenant-b";
|
||||||
|
|
||||||
|
app.with_conn(move |conn| {
|
||||||
|
let storage_root = format!("test-tenants/{tenant_b_id}/");
|
||||||
|
diesel::insert_into(tenants_dsl::tenants)
|
||||||
|
.values((
|
||||||
|
tenants_dsl::id.eq(tenant_b_id),
|
||||||
|
tenants_dsl::slug.eq("tenant-b"),
|
||||||
|
tenants_dsl::storage_root.eq(Some(storage_root)),
|
||||||
|
))
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
let password_hash = hash_password(password_b)?;
|
||||||
|
let new_user = NewUser {
|
||||||
|
id: user_b_id,
|
||||||
|
username: "bob".to_string(),
|
||||||
|
password_hash,
|
||||||
|
};
|
||||||
|
diesel::insert_into(users_dsl::users)
|
||||||
|
.values(&new_user)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
let membership = NewUserMembership {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
user_id: user_b_id,
|
||||||
|
tenant_id: tenant_b_id,
|
||||||
|
role: "admin".to_string(),
|
||||||
|
};
|
||||||
|
diesel::insert_into(memberships_dsl::user_memberships)
|
||||||
|
.values(&membership)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok::<_, anyhow::Error>(())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let token_b = app.login_token("bob", password_b).await?;
|
||||||
|
|
||||||
|
let create_b = app
|
||||||
|
.post_json(
|
||||||
|
"/api/tags",
|
||||||
|
&CreateTagPayload {
|
||||||
|
label: shared_label,
|
||||||
|
color: Some("#654321"),
|
||||||
|
},
|
||||||
|
Some(&token_b),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(create_b.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
app.with_conn(move |conn| {
|
||||||
|
let tags: Vec<Tag> = tags_dsl::tags
|
||||||
|
.filter(tags_dsl::label.eq(shared_label))
|
||||||
|
.order(tags_dsl::tenant_id.asc())
|
||||||
|
.load(conn)?;
|
||||||
|
|
||||||
|
assert_eq!(tags.len(), 2);
|
||||||
|
assert_ne!(tags[0].tenant_id, tags[1].tenant_id);
|
||||||
|
Ok::<_, anyhow::Error>(())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user