pinakes-server: expand test coverage for server features
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ia09d2d3ad7f6613e21d20321e0877bc16a6a6964
This commit is contained in:
parent
60b6aa1fe8
commit
ee5df288bc
8 changed files with 853 additions and 10 deletions
|
|
@ -3,7 +3,19 @@ use axum::{
|
|||
body::Body,
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use common::*;
|
||||
use common::{
|
||||
delete_authed,
|
||||
get,
|
||||
get_authed,
|
||||
patch_json_authed,
|
||||
post_json,
|
||||
post_json_authed,
|
||||
put_json_authed,
|
||||
response_body,
|
||||
setup_app,
|
||||
setup_app_with_auth,
|
||||
test_addr,
|
||||
};
|
||||
use http_body_util::BodyExt;
|
||||
use tower::ServiceExt;
|
||||
|
||||
|
|
@ -708,3 +720,19 @@ async fn test_share_link_expired() {
|
|||
|| response.status() == StatusCode::INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_sync_device_requires_editor() {
|
||||
let (app, _, _, viewer_token) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(put_json_authed(
|
||||
&format!("/api/v1/sync/devices/{fake_id}"),
|
||||
r#"{"name":"renamed"}"#,
|
||||
&viewer_token,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
|
|
|||
158
crates/pinakes-server/tests/books.rs
Normal file
158
crates/pinakes-server/tests/books.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
mod common;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use common::{
|
||||
delete_authed,
|
||||
get,
|
||||
get_authed,
|
||||
patch_json_authed,
|
||||
post_json_authed,
|
||||
put_json_authed,
|
||||
response_body,
|
||||
setup_app,
|
||||
setup_app_with_auth,
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_books_empty() {
|
||||
let app = setup_app().await;
|
||||
let resp = app.oneshot(get("/api/v1/books")).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = response_body(resp).await;
|
||||
let items = body.as_array().expect("array response");
|
||||
assert!(items.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_book_metadata_not_found() {
|
||||
let app = setup_app().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.oneshot(get(&format!("/api/v1/books/{fake_id}/metadata")))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
resp.status() == StatusCode::NOT_FOUND
|
||||
|| resp.status() == StatusCode::INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_books_with_filters() {
|
||||
let app = setup_app().await;
|
||||
let resp = app
|
||||
.oneshot(get("/api/v1/books?author=Tolkien&limit=10"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_series_empty() {
|
||||
let app = setup_app().await;
|
||||
let resp = app.oneshot(get("/api/v1/books/series")).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_authors_empty() {
|
||||
let app = setup_app().await;
|
||||
let resp = app
|
||||
.oneshot(get("/api/v1/books/authors?offset=0&limit=50"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reading_progress_nonexistent_book() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(get_authed(
|
||||
&format!("/api/v1/books/{fake_id}/progress"),
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
// Nonexistent book; expect NOT_FOUND or empty response
|
||||
assert!(
|
||||
resp.status() == StatusCode::NOT_FOUND || resp.status() == StatusCode::OK
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_reading_progress_nonexistent_book() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(put_json_authed(
|
||||
&format!("/api/v1/books/{fake_id}/progress"),
|
||||
r#"{"current_page":42}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
// Nonexistent book; expect NOT_FOUND or error
|
||||
assert!(
|
||||
resp.status() == StatusCode::NOT_FOUND
|
||||
|| resp.status() == StatusCode::INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reading_list_empty() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(get_authed("/api/v1/books/reading-list", &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn import_media_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(post_json_authed(
|
||||
"/api/v1/media/import",
|
||||
r#"{"path":"/tmp/test.txt"}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_media_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(patch_json_authed(
|
||||
&format!("/api/v1/media/{fake_id}"),
|
||||
r#"{"title":"new"}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_media_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(delete_authed(&format!("/api/v1/media/{fake_id}"), &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
145
crates/pinakes-server/tests/media_ops.rs
Normal file
145
crates/pinakes-server/tests/media_ops.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
mod common;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use common::{
|
||||
delete_authed,
|
||||
get,
|
||||
get_authed,
|
||||
patch_json_authed,
|
||||
post_json_authed,
|
||||
put_json_authed,
|
||||
response_body,
|
||||
setup_app,
|
||||
setup_app_with_auth,
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn media_count_empty() {
|
||||
let app = setup_app().await;
|
||||
let resp = app.oneshot(get("/api/v1/media/count")).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = response_body(resp).await;
|
||||
assert_eq!(body["count"], 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn batch_delete_empty_ids() {
|
||||
let (app, admin, ..) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(post_json_authed(
|
||||
"/api/v1/media/batch/delete",
|
||||
r#"{"ids":[]}"#,
|
||||
&admin,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
// Empty ids should be rejected (validation requires 1+ items)
|
||||
assert!(
|
||||
resp.status() == StatusCode::BAD_REQUEST
|
||||
|| resp.status() == StatusCode::UNPROCESSABLE_ENTITY
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn batch_delete_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let body = format!(r#"{{"ids":["{fake_id}"]}}"#);
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(post_json_authed(
|
||||
"/api/v1/media/batch/delete",
|
||||
&body,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_trash_empty() {
|
||||
let (app, _, editor, _) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(get_authed("/api/v1/trash?offset=0&limit=50", &editor))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = response_body(resp).await;
|
||||
assert_eq!(body["total_count"], 0);
|
||||
let items = body["items"].as_array().expect("items array");
|
||||
assert!(items.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn batch_tag_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(post_json_authed(
|
||||
"/api/v1/media/batch/tag",
|
||||
r#"{"media_ids":[],"tag_ids":[]}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_trash_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(get_authed("/api/v1/trash", &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rename_media_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(patch_json_authed(
|
||||
&format!("/api/v1/media/{fake_id}/rename"),
|
||||
r#"{"new_name":"renamed.txt"}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permanent_delete_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(delete_authed(&format!("/api/v1/media/{fake_id}"), &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_sync_device_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(put_json_authed(
|
||||
&format!("/api/v1/sync/devices/{fake_id}"),
|
||||
r#"{"name":"renamed"}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
125
crates/pinakes-server/tests/notes.rs
Normal file
125
crates/pinakes-server/tests/notes.rs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
mod common;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use common::{
|
||||
delete_authed,
|
||||
get,
|
||||
get_authed,
|
||||
patch_json_authed,
|
||||
post_json_authed,
|
||||
put_json_authed,
|
||||
response_body,
|
||||
setup_app,
|
||||
setup_app_with_auth,
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn backlinks_for_nonexistent_media() {
|
||||
let app = setup_app().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.oneshot(get(&format!("/api/v1/media/{fake_id}/backlinks")))
|
||||
.await
|
||||
.unwrap();
|
||||
// Should return OK with empty list, or NOT_FOUND
|
||||
assert!(
|
||||
resp.status() == StatusCode::OK || resp.status() == StatusCode::NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn outgoing_links_for_nonexistent_media() {
|
||||
let app = setup_app().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.oneshot(get(&format!("/api/v1/media/{fake_id}/outgoing-links")))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
resp.status() == StatusCode::OK || resp.status() == StatusCode::NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notes_graph_empty() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(get_authed("/api/v1/notes/graph", &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = response_body(resp).await;
|
||||
assert!(body.is_object() || body.is_array());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unresolved_count_zero() {
|
||||
let app = setup_app().await;
|
||||
let resp = app
|
||||
.oneshot(get("/api/v1/notes/unresolved-count"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reindex_links_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(post_json_authed(
|
||||
&format!("/api/v1/media/{fake_id}/reindex-links"),
|
||||
"{}",
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_media_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(patch_json_authed(
|
||||
&format!("/api/v1/media/{fake_id}"),
|
||||
r#"{"title":"new title"}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_media_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(delete_authed(&format!("/api/v1/media/{fake_id}"), &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_sync_device_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(put_json_authed(
|
||||
&format!("/api/v1/sync/devices/{fake_id}"),
|
||||
r#"{"name":"renamed"}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
|
@ -2,7 +2,20 @@ mod common;
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::{body::Body, http::StatusCode};
|
||||
use common::*;
|
||||
use common::{
|
||||
default_config,
|
||||
delete_authed,
|
||||
get,
|
||||
get_authed,
|
||||
patch_json_authed,
|
||||
post_json,
|
||||
post_json_authed,
|
||||
put_json_authed,
|
||||
response_body,
|
||||
setup_app,
|
||||
setup_app_with_auth,
|
||||
test_addr,
|
||||
};
|
||||
use http_body_util::BodyExt;
|
||||
use pinakes_core::{config::PluginsConfig, plugin::PluginManager};
|
||||
use tower::ServiceExt;
|
||||
|
|
@ -164,3 +177,98 @@ async fn test_plugin_uninstall_not_found() {
|
|||
|| response.status() == StatusCode::NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
// RBAC tests using common helpers with auth setup
|
||||
|
||||
#[tokio::test]
|
||||
async fn media_list_unauthenticated() {
|
||||
let app = setup_app().await;
|
||||
let resp = app.oneshot(get("/api/v1/media")).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = response_body(resp).await;
|
||||
assert!(body.is_array());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn media_list_authenticated() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(get_authed("/api/v1/media", &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn import_unauthenticated_rejected() {
|
||||
let (app, ..) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(post_json(
|
||||
"/api/v1/media/import",
|
||||
r#"{"path":"/tmp/test.txt"}"#,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn import_viewer_forbidden() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(post_json_authed(
|
||||
"/api/v1/media/import",
|
||||
r#"{"path":"/tmp/test.txt"}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_media_viewer_forbidden() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(patch_json_authed(
|
||||
&format!("/api/v1/media/{fake_id}"),
|
||||
r#"{"title":"new"}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_media_viewer_forbidden() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(delete_authed(&format!("/api/v1/media/{fake_id}"), &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_sync_device_viewer_forbidden() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(put_json_authed(
|
||||
&format!("/api/v1/sync/devices/{fake_id}"),
|
||||
r#"{"name":"renamed"}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
|
|
|||
142
crates/pinakes-server/tests/shares.rs
Normal file
142
crates/pinakes-server/tests/shares.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
mod common;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use common::{
|
||||
delete_authed,
|
||||
get,
|
||||
get_authed,
|
||||
patch_json_authed,
|
||||
post_json,
|
||||
post_json_authed,
|
||||
put_json_authed,
|
||||
response_body,
|
||||
setup_app,
|
||||
setup_app_with_auth,
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_outgoing_shares_empty() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(get_authed("/api/v1/shares/outgoing", &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = response_body(resp).await;
|
||||
let shares = body.as_array().expect("array response");
|
||||
assert!(shares.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_incoming_shares_empty() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(get_authed("/api/v1/shares/incoming", &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn share_notifications_empty() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(get_authed("/api/v1/notifications/shares", &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn batch_delete_shares_requires_auth() {
|
||||
let (app, ..) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(post_json("/api/v1/shares/batch/delete", r#"{"ids":[]}"#))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn batch_delete_shares_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(post_json_authed(
|
||||
"/api/v1/shares/batch/delete",
|
||||
r#"{"ids":[]}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_share_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let body = format!(r#"{{"media_id":"{fake_id}","share_type":"link"}}"#);
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(post_json_authed("/api/v1/shares", &body, &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_share_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(patch_json_authed(
|
||||
&format!("/api/v1/shares/{fake_id}"),
|
||||
r#"{"permissions":["read"]}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_share_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(delete_authed(&format!("/api/v1/shares/{fake_id}"), &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_sync_device_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(put_json_authed(
|
||||
&format!("/api/v1/sync/devices/{fake_id}"),
|
||||
r#"{"name":"renamed"}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn media_list_no_auth() {
|
||||
let app = setup_app().await;
|
||||
let resp = app.oneshot(get("/api/v1/media")).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
137
crates/pinakes-server/tests/sync.rs
Normal file
137
crates/pinakes-server/tests/sync.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
mod common;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use common::{
|
||||
delete_authed,
|
||||
get,
|
||||
get_authed,
|
||||
patch_json_authed,
|
||||
post_json,
|
||||
post_json_authed,
|
||||
put_json_authed,
|
||||
response_body,
|
||||
setup_app,
|
||||
setup_app_with_auth,
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_sync_devices_empty() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(get_authed("/api/v1/sync/devices", &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = response_body(resp).await;
|
||||
let devices = body.as_array().expect("array response");
|
||||
assert!(devices.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_changes_sync_disabled() {
|
||||
// Default config has sync.enabled = false; endpoint should reject
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(get_authed("/api/v1/sync/changes", &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_conflicts_requires_device_token() {
|
||||
// list_conflicts requires X-Device-Token header; omitting it returns 400
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(get_authed("/api/v1/sync/conflicts", &viewer))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_device_requires_auth() {
|
||||
let (app, ..) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(post_json(
|
||||
"/api/v1/sync/devices",
|
||||
r#"{"name":"test","device_type":"desktop","client_version":"0.3.0"}"#,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_device_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(post_json_authed(
|
||||
"/api/v1/sync/devices",
|
||||
r#"{"name":"test","device_type":"desktop","client_version":"0.3.0"}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_device_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(put_json_authed(
|
||||
&format!("/api/v1/sync/devices/{fake_id}"),
|
||||
r#"{"name":"renamed"}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_device_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(delete_authed(
|
||||
&format!("/api/v1/sync/devices/{fake_id}"),
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_media_requires_editor() {
|
||||
let (app, _, _, viewer) = setup_app_with_auth().await;
|
||||
let fake_id = uuid::Uuid::now_v7();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(patch_json_authed(
|
||||
&format!("/api/v1/media/{fake_id}"),
|
||||
r#"{"title":"new"}"#,
|
||||
&viewer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn media_list_no_auth() {
|
||||
let app = setup_app().await;
|
||||
let resp = app.oneshot(get("/api/v1/media")).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
|
@ -146,6 +146,14 @@ impl AppState {
|
|||
import_input: String::new(),
|
||||
status_message: None,
|
||||
should_quit: false,
|
||||
page_offset: 0,
|
||||
page_size: 50,
|
||||
total_media_count: 0,
|
||||
server_url: server_url.to_string(),
|
||||
// Multi-select
|
||||
selected_items: FxHashSet::default(),
|
||||
selection_mode: false,
|
||||
pending_batch_delete: false,
|
||||
duplicate_groups: Vec::new(),
|
||||
duplicates_selected: None,
|
||||
database_stats: None,
|
||||
|
|
@ -174,14 +182,6 @@ impl AppState {
|
|||
reading_progress: None,
|
||||
page_input: String::new(),
|
||||
entering_page: false,
|
||||
page_offset: 0,
|
||||
page_size: 50,
|
||||
total_media_count: 0,
|
||||
server_url: server_url.to_string(),
|
||||
// Multi-select
|
||||
selected_items: FxHashSet::default(),
|
||||
selection_mode: false,
|
||||
pending_batch_delete: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue