feat: migrate file storage to postgres with sqlx
This commit is contained in:
1098
backend/Cargo.lock
generated
1098
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,10 @@ tower-cookies = "0.11.0"
|
|||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||||
lazy-regex = "3.6.0"
|
lazy-regex = "3.6.0"
|
||||||
|
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono"] }
|
||||||
|
chrono = { version = "0.4.44", features = ["serde"] }
|
||||||
|
dotenvy = "0.15.7"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
axum-test = "20.0.0"
|
axum-test = "20.0.0"
|
||||||
|
serial_test = "3.4.0"
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE file_records (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
file_type TEXT NOT NULL,
|
||||||
|
size BIGINT NOT NULL,
|
||||||
|
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
@@ -11,7 +11,7 @@ use tracing::info;
|
|||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
model::FileController,
|
model::FileRepository,
|
||||||
web::{
|
web::{
|
||||||
mw_auth::{mw_ctx_resolver, mw_require_auth},
|
mw_auth::{mw_ctx_resolver, mw_require_auth},
|
||||||
routes_file::routes_file,
|
routes_file::routes_file,
|
||||||
@@ -31,10 +31,10 @@ async fn main() -> Result<()> {
|
|||||||
.with(tracing_subscriber::fmt::layer())
|
.with(tracing_subscriber::fmt::layer())
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let file_controller = FileController::new().await?;
|
let file_repository = FileRepository::new().await?;
|
||||||
|
|
||||||
let routes_file =
|
let routes_file =
|
||||||
routes_file(file_controller.clone()).route_layer(middleware::from_fn(mw_require_auth));
|
routes_file(file_repository.clone()).route_layer(middleware::from_fn(mw_require_auth));
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.nest("/api", routes_file)
|
.nest("/api", routes_file)
|
||||||
@@ -43,7 +43,7 @@ async fn main() -> Result<()> {
|
|||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.layer(middleware::map_response(main_response_mapper))
|
.layer(middleware::map_response(main_response_mapper))
|
||||||
.layer(middleware::from_fn_with_state(
|
.layer(middleware::from_fn_with_state(
|
||||||
file_controller,
|
file_repository,
|
||||||
mw_ctx_resolver,
|
mw_ctx_resolver,
|
||||||
))
|
))
|
||||||
.layer(CookieManagerLayer::new())
|
.layer(CookieManagerLayer::new())
|
||||||
|
|||||||
@@ -1,67 +1,120 @@
|
|||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::{PgPool, prelude::FromRow};
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use crate::error::LoftError;
|
use crate::error::LoftError;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize, FromRow)]
|
||||||
pub struct File {
|
pub struct FileRecord {
|
||||||
pub id: u64, // make uuid
|
pub id: i64,
|
||||||
|
// pub user_id: i64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub file_type: String, // make enum
|
pub file_type: String,
|
||||||
|
pub size: i64,
|
||||||
|
pub uploaded_at: chrono::DateTime<chrono::Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct FileToCreate {
|
pub struct FileToCreate {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub file_type: String, // make enum
|
pub file_type: FileType,
|
||||||
|
pub size: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum FileType {
|
||||||
|
Image,
|
||||||
|
Video,
|
||||||
|
Document,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FileType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
FileType::Image => write!(f, "Image"),
|
||||||
|
FileType::Video => write!(f, "Video"),
|
||||||
|
FileType::Document => write!(f, "Document"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FileController {
|
pub struct FileRepository {
|
||||||
file_storage: Arc<Mutex<Vec<Option<File>>>>,
|
pub pool: PgPool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileController {
|
impl FileRepository {
|
||||||
pub async fn new() -> Result<Self, LoftError> {
|
pub async fn new() -> Result<Self, LoftError> {
|
||||||
Ok(Self {
|
dotenvy::dotenv().ok();
|
||||||
file_storage: Arc::default(),
|
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||||
})
|
let pool = PgPool::connect(&database_url).await.unwrap();
|
||||||
|
|
||||||
|
Ok(Self { pool })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn upload_file(&self, file_to_create: FileToCreate) -> Result<File, LoftError> {
|
pub async fn upload_file(&self, file_to_create: FileToCreate) -> Result<FileRecord, LoftError> {
|
||||||
let mut file_storage = self.file_storage.lock().unwrap();
|
let file = sqlx::query_as!(
|
||||||
let id = file_storage.len() as u64;
|
FileRecord,
|
||||||
let file = File {
|
r#"
|
||||||
id,
|
INSERT INTO file_records (name, file_type, size)
|
||||||
name: file_to_create.name,
|
VALUES ($1, $2, $3)
|
||||||
file_type: file_to_create.file_type,
|
RETURNING *
|
||||||
};
|
"#,
|
||||||
file_storage.push(Some(file.clone()));
|
file_to_create.name,
|
||||||
|
file_to_create.file_type.to_string(),
|
||||||
|
file_to_create.size as i64
|
||||||
|
)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
Ok(file)
|
Ok(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn download_file(&self, file_id: u64) -> Result<File, LoftError> {
|
pub async fn download_file(&self, file_id: i64) -> Result<FileRecord, LoftError> {
|
||||||
let file_storage = self.file_storage.lock().unwrap();
|
sqlx::query_as!(
|
||||||
file_storage
|
FileRecord,
|
||||||
.iter()
|
r#"
|
||||||
.flatten()
|
SELECT *
|
||||||
.find(|f| f.id == file_id)
|
FROM file_records fr
|
||||||
.ok_or(LoftError::FileIdNotFound)
|
WHERE fr.id = $1
|
||||||
.cloned()
|
"#,
|
||||||
|
file_id
|
||||||
|
)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.ok_or(LoftError::FileIdNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_file(&self, file_id: u64) -> Result<File, LoftError> {
|
pub async fn delete_file(&self, file_id: i64) -> Result<FileRecord, LoftError> {
|
||||||
let mut file_storage = self.file_storage.lock().unwrap();
|
sqlx::query_as!(
|
||||||
let file = file_storage
|
FileRecord,
|
||||||
.get_mut(file_id as usize)
|
r#"
|
||||||
.and_then(|f| f.take());
|
DELETE FROM file_records
|
||||||
file.ok_or(LoftError::FileIdNotFound)
|
WHERE id = $1
|
||||||
|
RETURNING *
|
||||||
|
"#,
|
||||||
|
file_id
|
||||||
|
)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.ok_or(LoftError::FileIdNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_files(&self) -> Result<Vec<File>, LoftError> {
|
pub async fn list_files(&self) -> Result<Vec<FileRecord>, LoftError> {
|
||||||
let file_storage = self.file_storage.lock().unwrap();
|
let files = sqlx::query_as!(
|
||||||
let files = file_storage.iter().flatten().cloned().collect();
|
FileRecord,
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM file_records fr
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
Ok(files)
|
Ok(files)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,59 +123,86 @@ impl FileController {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
async fn fc() -> Result<FileController, LoftError> {
|
async fn file_repository() -> Result<FileRepository, LoftError> {
|
||||||
Ok(FileController::new().await.unwrap())
|
Ok(FileRepository::new().await.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_file(name: &str) -> FileToCreate {
|
fn new_file_record(name: &str, file_type: FileType, size: i64) -> FileToCreate {
|
||||||
FileToCreate {
|
FileToCreate {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
file_type: "text".to_string(),
|
file_type,
|
||||||
|
size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
async fn test_upload_and_list() {
|
async fn test_upload_and_list() {
|
||||||
let fc = fc().await.unwrap();
|
let file_repository = file_repository().await.unwrap();
|
||||||
fc.upload_file(new_file("a.txt")).await.unwrap();
|
sqlx::query!("TRUNCATE TABLE file_records")
|
||||||
fc.upload_file(new_file("b.txt")).await.unwrap();
|
.execute(&file_repository.pool)
|
||||||
let files = fc.list_files().await.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let file_a = new_file_record("a", FileType::Document, 10);
|
||||||
|
let file_b = new_file_record("b", FileType::Document, 10);
|
||||||
|
file_repository.upload_file(file_a).await.unwrap();
|
||||||
|
file_repository.upload_file(file_b).await.unwrap();
|
||||||
|
let files = file_repository.list_files().await.unwrap();
|
||||||
assert_eq!(files.len(), 2);
|
assert_eq!(files.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
async fn test_download() {
|
async fn test_download() {
|
||||||
let fc = fc().await.unwrap();
|
let file_repository = file_repository().await.unwrap();
|
||||||
let uploaded = fc.upload_file(new_file("a.txt")).await.unwrap();
|
sqlx::query!("TRUNCATE TABLE file_records")
|
||||||
let downloaded = fc.download_file(uploaded.id).await.unwrap();
|
.execute(&file_repository.pool)
|
||||||
assert_eq!(downloaded.name, "a.txt");
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let file_a = new_file_record("a", FileType::Document, 10);
|
||||||
|
let uploaded = file_repository.upload_file(file_a).await.unwrap();
|
||||||
|
let downloaded = file_repository.download_file(uploaded.id).await.unwrap();
|
||||||
|
assert_eq!(downloaded.name, "a");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
async fn test_download_not_found() {
|
async fn test_download_not_found() {
|
||||||
let fc = fc().await.unwrap();
|
let file_repository = file_repository().await.unwrap();
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
fc.download_file(99).await,
|
file_repository.download_file(99).await,
|
||||||
Err(LoftError::FileIdNotFound)
|
Err(LoftError::FileIdNotFound)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
async fn test_delete() {
|
async fn test_delete() {
|
||||||
let fc = fc().await.unwrap();
|
let file_repository = file_repository().await.unwrap();
|
||||||
let uploaded = fc.upload_file(new_file("a.txt")).await.unwrap();
|
sqlx::query!("TRUNCATE TABLE file_records")
|
||||||
fc.delete_file(uploaded.id).await.unwrap();
|
.execute(&file_repository.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let file_a = new_file_record("a", FileType::Document, 10);
|
||||||
|
let uploaded = file_repository.upload_file(file_a).await.unwrap();
|
||||||
|
file_repository.delete_file(uploaded.id).await.unwrap();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
fc.download_file(uploaded.id).await,
|
file_repository.download_file(uploaded.id).await,
|
||||||
Err(LoftError::FileIdNotFound)
|
Err(LoftError::FileIdNotFound)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
async fn test_delete_not_found() {
|
async fn test_delete_not_found() {
|
||||||
let fc = fc().await.unwrap();
|
let file_repository = file_repository().await.unwrap();
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
fc.delete_file(99).await,
|
file_repository.delete_file(99).await,
|
||||||
Err(LoftError::FileIdNotFound)
|
Err(LoftError::FileIdNotFound)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,53 +7,53 @@ use tracing::info;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::LoftError,
|
error::LoftError,
|
||||||
model::{File, FileController, FileToCreate},
|
model::{FileRecord, FileRepository, FileToCreate},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn routes_file(file_controller: FileController) -> Router {
|
pub fn routes_file(file_repository: FileRepository) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/files", get(list_files).post(upload_file))
|
.route("/files", get(list_files).post(upload_file))
|
||||||
.route("/files/{id}", get(download_file).delete(delete_file))
|
.route("/files/{id}", get(download_file).delete(delete_file))
|
||||||
.with_state(file_controller)
|
.with_state(file_repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn upload_file(
|
async fn upload_file(
|
||||||
State(file_controller): State<FileController>,
|
State(file_repository): State<FileRepository>,
|
||||||
Json(file_to_create): Json<FileToCreate>,
|
Json(file_to_create): Json<FileToCreate>,
|
||||||
) -> Result<Json<File>, LoftError> {
|
) -> Result<Json<FileRecord>, LoftError> {
|
||||||
info!("handler: upload_file");
|
info!("handler: upload_file");
|
||||||
|
|
||||||
let file = file_controller.upload_file(file_to_create).await?;
|
let file = file_repository.upload_file(file_to_create).await?;
|
||||||
Ok(Json(file))
|
Ok(Json(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download_file(
|
async fn download_file(
|
||||||
State(file_controller): State<FileController>,
|
State(file_repository): State<FileRepository>,
|
||||||
Path(file_id): Path<u64>,
|
Path(file_id): Path<u64>,
|
||||||
) -> Result<Json<File>, LoftError> {
|
) -> Result<Json<FileRecord>, LoftError> {
|
||||||
info!("handler: download_file");
|
info!("handler: download_file");
|
||||||
|
|
||||||
let file = file_controller.download_file(file_id).await?;
|
let file = file_repository.download_file(file_id as i64).await?;
|
||||||
Ok(Json(file))
|
Ok(Json(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_file(
|
async fn delete_file(
|
||||||
State(file_controller): State<FileController>,
|
State(file_repository): State<FileRepository>,
|
||||||
Path(file_id): Path<u64>,
|
Path(file_id): Path<u64>,
|
||||||
) -> Result<Json<File>, LoftError> {
|
) -> Result<Json<FileRecord>, LoftError> {
|
||||||
info!("handler: delete_file");
|
info!("handler: delete_file");
|
||||||
|
|
||||||
let file = file_controller.delete_file(file_id).await?;
|
let file = file_repository.delete_file(file_id as i64).await?;
|
||||||
Ok(Json(file))
|
Ok(Json(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_files(
|
async fn list_files(
|
||||||
State(file_controller): State<FileController>,
|
State(file_repository): State<FileRepository>,
|
||||||
// can add a filters param here
|
// can add a filters param here
|
||||||
) -> Result<Json<Vec<File>>, LoftError> {
|
) -> Result<Json<Vec<FileRecord>>, LoftError> {
|
||||||
info!("handler: list_files");
|
info!("handler: list_files");
|
||||||
|
|
||||||
let files = file_controller.list_files().await?;
|
let files = file_repository.list_files().await?;
|
||||||
Ok(Json(files))
|
Ok(Json(files))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ mod tests {
|
|||||||
use tower_cookies::CookieManagerLayer;
|
use tower_cookies::CookieManagerLayer;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
model::FileController,
|
model::FileRepository,
|
||||||
web::{
|
web::{
|
||||||
mw_auth::{mw_ctx_resolver, mw_require_auth},
|
mw_auth::{mw_ctx_resolver, mw_require_auth},
|
||||||
routes_file::routes_file,
|
routes_file::routes_file,
|
||||||
@@ -77,13 +77,13 @@ mod tests {
|
|||||||
const BAD_AUTH_COOKIE: &str = "auth-token=user-1.0123456789";
|
const BAD_AUTH_COOKIE: &str = "auth-token=user-1.0123456789";
|
||||||
|
|
||||||
async fn test_server() -> TestServer {
|
async fn test_server() -> TestServer {
|
||||||
let file_controller = FileController::new().await.unwrap();
|
let file_repository = FileRepository::new().await.unwrap();
|
||||||
let routes_file =
|
let routes_file =
|
||||||
routes_file(file_controller.clone()).route_layer(middleware::from_fn(mw_require_auth));
|
routes_file(file_repository.clone()).route_layer(middleware::from_fn(mw_require_auth));
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.nest("/api", routes_file)
|
.nest("/api", routes_file)
|
||||||
.layer(middleware::from_fn_with_state(
|
.layer(middleware::from_fn_with_state(
|
||||||
file_controller,
|
file_repository,
|
||||||
mw_ctx_resolver,
|
mw_ctx_resolver,
|
||||||
))
|
))
|
||||||
.layer(CookieManagerLayer::new());
|
.layer(CookieManagerLayer::new());
|
||||||
@@ -111,13 +111,20 @@ mod tests {
|
|||||||
let server = test_server().await;
|
let server = test_server().await;
|
||||||
server
|
server
|
||||||
.post("/api/files")
|
.post("/api/files")
|
||||||
.json(&json!({"name": "a.txt", "file_type": "text"}))
|
.json(&json!({"name": "a", "file_type": "Document"}))
|
||||||
.await
|
.await
|
||||||
.assert_status_unauthorized();
|
.assert_status_unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
async fn test_list_files_empty() {
|
async fn test_list_files_empty() {
|
||||||
|
let file_repository = FileRepository::new().await.unwrap();
|
||||||
|
sqlx::query!("TRUNCATE TABLE file_records")
|
||||||
|
.execute(&file_repository.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let server = test_server().await;
|
let server = test_server().await;
|
||||||
server
|
server
|
||||||
.get("/api/files")
|
.get("/api/files")
|
||||||
@@ -128,17 +135,25 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
async fn test_upload_and_list_files() {
|
async fn test_upload_and_list_files() {
|
||||||
|
let file_repository = FileRepository::new().await.unwrap();
|
||||||
|
sqlx::query!("TRUNCATE TABLE file_records")
|
||||||
|
.execute(&file_repository.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let server = test_server().await;
|
let server = test_server().await;
|
||||||
let res = server
|
let res = server
|
||||||
.post("/api/files")
|
.post("/api/files")
|
||||||
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
||||||
.json(&json!({"name": "a.txt", "file_type": "text"}))
|
.json(&json!({"name": "a", "file_type": "Document", "size": 10}))
|
||||||
.await;
|
.await;
|
||||||
res.assert_status_ok();
|
res.assert_status_ok();
|
||||||
let file = res.json::<serde_json::Value>();
|
let file = res.json::<serde_json::Value>();
|
||||||
assert_eq!(file["name"], "a.txt");
|
assert_eq!(file["name"], "a");
|
||||||
assert_eq!(file["id"], 0);
|
assert_eq!(file["file_type"], "Document");
|
||||||
|
assert_eq!(file["size"], 10);
|
||||||
|
|
||||||
let list = server
|
let list = server
|
||||||
.get("/api/files")
|
.get("/api/files")
|
||||||
@@ -149,22 +164,25 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
async fn test_download_file() {
|
async fn test_download_file() {
|
||||||
let server = test_server().await;
|
let server = test_server().await;
|
||||||
server
|
let post_res = server
|
||||||
.post("/api/files")
|
.post("/api/files")
|
||||||
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
||||||
.json(&json!({"name": "b.txt", "file_type": "text"}))
|
.json(&json!({"name": "b", "file_type": "Document", "size": 10}))
|
||||||
.await;
|
.await;
|
||||||
|
let id = post_res.json::<serde_json::Value>()["id"].as_i64().unwrap();
|
||||||
let res = server
|
let res = server
|
||||||
.get("/api/files/0")
|
.get(&format!("/api/files/{id}"))
|
||||||
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
||||||
.await;
|
.await;
|
||||||
res.assert_status_ok();
|
res.assert_status_ok();
|
||||||
assert_eq!(res.json::<serde_json::Value>()["name"], "b.txt");
|
assert_eq!(res.json::<serde_json::Value>()["name"], "b");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
async fn test_download_file_not_found() {
|
async fn test_download_file_not_found() {
|
||||||
let server = test_server().await;
|
let server = test_server().await;
|
||||||
server
|
server
|
||||||
@@ -175,26 +193,31 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
async fn test_delete_file() {
|
async fn test_delete_file() {
|
||||||
let server = test_server().await;
|
let server = test_server().await;
|
||||||
server
|
let post_res = server
|
||||||
.post("/api/files")
|
.post("/api/files")
|
||||||
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
||||||
.json(&json!({"name": "c.txt", "file_type": "text"}))
|
.json(&json!({"name": "c", "file_type": "Document", "size": 10}))
|
||||||
.await;
|
.await;
|
||||||
|
let id = post_res.json::<serde_json::Value>()["id"].as_i64().unwrap();
|
||||||
|
|
||||||
server
|
server
|
||||||
.delete("/api/files/0")
|
.delete(&format!("/api/files/{id}"))
|
||||||
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
||||||
.await
|
.await
|
||||||
.assert_status_ok();
|
.assert_status_ok();
|
||||||
|
|
||||||
server
|
server
|
||||||
.get("/api/files/0")
|
.get(&format!("/api/files/{id}"))
|
||||||
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
||||||
.await
|
.await
|
||||||
.assert_status_not_found();
|
.assert_status_not_found();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
async fn test_delete_file_not_found() {
|
async fn test_delete_file_not_found() {
|
||||||
let server = test_server().await;
|
let server = test_server().await;
|
||||||
server
|
server
|
||||||
|
|||||||
Reference in New Issue
Block a user