Skip to content

Commit

Permalink
add tests for media list request
Browse files Browse the repository at this point in the history
  • Loading branch information
thebino committed Aug 17, 2023
1 parent 62b124c commit 48c0132
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 89 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ anyhow = { version = "1.0.72" }
chrono = { version = "0.4.26", features = ["serde"] }

hyper = { version = "0.14", features = ["full"] }

mime = { version = "0.3" }
mockall = { version = "0.11.4" }

rstest = { version = "0.18.1" }
Expand Down
3 changes: 2 additions & 1 deletion crates/media/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

# Router
axum = { workspace = true }
axum = { workspace = true, features = ["multipart"] }
tower-http = { workspace = true }
mime = { workspace = true }

# persistency
sea-orm = { workspace = true }
Expand Down
109 changes: 77 additions & 32 deletions crates/media/src/api/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

use axum::routing::{get, patch, post, delete};
use axum::routing::{delete, get, patch, post};
use axum::Router;

use super::routes::delete_media_id::delete_media_id;
Expand Down Expand Up @@ -45,32 +45,26 @@ impl MediaApi {
// 403 Forbidden
// 500 Internal Server Error
.route("/media", get(get_media))

// Creates a new media item to aggregate related files for current user
// 201 - Created
// 400 Bad Request - The request body was malformed or a field violated its constraints.
// 400 Bad Request - The request body was malformed or a field violated its constraints.
// 401 Unauthorized - You are unauthenticated
// 403 Forbidden - You are authenticated but have no permission to manage the target user.
// 500 Internal Server Error
.route("/media", post(post_media))

// Returns a specific owned or shared media item for current user
// 200 - Ok
// 400 Bad Request - The request body was malformed or a field violated its constraints.
// 400 Bad Request - The request body was malformed or a field violated its constraints.
// 401 Unauthorized - You are unauthenticated
// 403 Forbidden - You are authenticated but have no permission to manage the target user.
// 500 Internal Server Error
.route("/media/:media_id", get(get_media_id))

// Add files for a specific media item
.route("/media/:media_id", post(post_media_id))

// Updates fields from a specific media item for current user
.route("/media/:media_id", patch(patch_media_id))

// Deletes the given item owned by the user
.route("/media/:media_id", delete(delete_media_id))

// list owned and shared albums
.route("/albums", get(get_albums))
// create new album
Expand All @@ -83,44 +77,95 @@ impl MediaApi {
.route("/albums/:entity_id/share", patch(patch_albums_id_share))
// unshares the given album
.route("/albums/:entity_id/unshare", patch(patch_albums_id_unshare))

.layer(tower_http::trace::TraceLayer::new_for_http())
}
}

#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use axum::{
body::Body,
http::{self, Request, StatusCode},
};
use serde_json::json;
use tower::ServiceExt;

use crate::repository::{MediaRepositoryState, MediaRepository};
#[tokio::test]
async fn get_media_with_query_success() {
// given
let app = Router::new().nest("/", MediaApi::routes());

use super::*;
use axum::{body::Body, http::{Request, StatusCode}};
use rstest::rstest;
// when
let response = app
.oneshot(
Request::builder()
.uri("/media?limit=100000&offset=1")
.method("GET")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

// then
assert_eq!(response.status(), StatusCode::OK);

let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
let body: String = serde_json::from_slice(&body).unwrap();

assert_eq!(body, "list media items. limit=100000, offset=1");
}

#[rstest]
#[case("/?name=Wonder")]
#[tokio::test]
async fn get_media_success(#[case] uri: &'static str) {
async fn get_media_without_query_success() {
// given
let repo: MediaRepositoryState = Arc::new(MediaRepository::new().await);
let api: Router<MediaRepositoryState> = MediaApi::routes().with_state(repo);
let app = Router::new().nest("/", MediaApi::routes());

// when
/*
TODO: find replacement for `oneshot`
let response = api::oneshot(
Request::builder()
.uri("/media")
.method("GET")
.body(Body::empty())
.unwrap()
).await.unwrap;
let response = app
.oneshot(
Request::builder()
.uri("/media")
.method("GET")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

// then
assert_eq!(response.status(), StatusCode::OK);

let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
let body = serde_json::from_slice(&body).unwrap();
let body: String = serde_json::from_slice(&body).unwrap();

assert_eq!(body, "list media items. limit=1000, offset=0");
}

// TODO: re-enable test
// #[tokio::test]
async fn post_media_success() {
// given
let app = Router::new().nest("/", MediaApi::routes());

// when
let response = app
.oneshot(
Request::builder()
.uri("/media")
.method("POST")
.header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
// add multipart file to body
.body(Body::from(
serde_json::to_vec(&json!([1, 2, 3, 4])).unwrap(),
))
.unwrap(),
)
.await
.unwrap();

// then
assert_eq!(response.status(), StatusCode::NOT_FOUND);
*/
assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED);
}
}
21 changes: 16 additions & 5 deletions crates/media/src/api/routes/get_media.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Returns a list of owned media items for current user
//!
use axum::{http::StatusCode, extract::Query, Json};
//!
use axum::{extract::Query, http::StatusCode, Json};
use serde::{Deserialize, Serialize};
use std::result::Result;

Expand All @@ -10,7 +10,18 @@ pub(crate) struct MediaListQuery {
limit: Option<i32>,
}

pub(crate) async fn get_media(Query(query): Query<MediaListQuery>) -> Result<Json<String>, StatusCode> {
// Err(StatusCode::UNAUTHORIZED)
Ok(Json(format!("list media items. limit={}, offset={}", query.limit.unwrap(), query.offset.unwrap()).to_owned()))
pub(crate) async fn get_media(
Query(query): Query<MediaListQuery>,
) -> Result<Json<String>, StatusCode> {
// TODO: check auth header
// TODO: read list from persistency
// TODO: return list
Ok(Json(
format!(
"list media items. limit={}, offset={}",
query.limit.unwrap_or_else(|| 1000),
query.offset.unwrap_or_else(|| 0)
)
.to_owned(),
))
}
17 changes: 16 additions & 1 deletion crates/media/src/api/routes/post_media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,23 @@
//!

use axum::http::StatusCode;
use axum::extract::Multipart;

pub(crate) async fn post_media() -> std::result::Result<String, StatusCode> {
pub(crate) async fn post_media(mut multipart: Multipart) -> std::result::Result<String, StatusCode> {
while let Some(field) = multipart.next_field().await.unwrap() {
let name = field.name().unwrap().to_string();
let file_name = field.file_name().unwrap().to_string();
let content_type = field.content_type().unwrap().to_string();
let data = field.bytes().await.unwrap();

println!(
"Length of `{}` (`{}`: `{}`) is {} bytes",
name,
file_name,
content_type,
data.len()
);
}
// TODO: read authentication header
Err(StatusCode::NOT_IMPLEMENTED)
}
12 changes: 6 additions & 6 deletions crates/media/src/data/media_item.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
/* Photos.network · A privacy first photo storage and sharing service for fediverse.
* Copyright (C) 2020 Photos network developers
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Expand All @@ -29,11 +29,11 @@ pub struct MediaItem {
pub details: Option<ExifInformation>,
pub tags: Option<Vec<String>>,
pub location: Option<Location>,
pub references: Option<Vec<File>>
pub references: Option<Vec<File>>,
}

impl MediaItem {
#[warn(dead_code)]
#[allow(dead_code)]
fn new(name: &'static str) -> Self {
MediaItem {
uuid: "",
Expand All @@ -43,7 +43,7 @@ impl MediaItem {
location: None,
details: None,
tags: None,
references: None
references: None,
}
}
}
48 changes: 5 additions & 43 deletions crates/media/src/repository/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,28 @@ use crate::data::media_item::MediaItem;
use crate::data::open_db_conn;

pub struct MediaRepository {
#[allow(dead_code)]
db_url: &'static str,
#[allow(dead_code)]
db: DatabaseConnection,
}

#[allow(dead_code)]
pub(crate) type MediaRepositoryState = Arc<MediaRepository>;

/// MockPhotosRepositoryTrait is created by automock macro
#[cfg_attr(test, mockall::automock)]
#[async_trait]
trait MediaRepositoryTrait {
#[allow(dead_code)]
async fn new(db_url: &'static str) -> Self;

// Gets a list of media items from the DB filted by user_id
async fn get_media_items_for_user(&self, user_id: &str) -> Result<Vec<MediaItem>, DataAccessError>;
}

impl MediaRepository {
#[allow(dead_code)]
pub(crate) async fn new() -> Self {
Self {
db_url: "",
Expand All @@ -69,46 +74,3 @@ impl MediaRepositoryTrait for MediaRepository {
Err(DataAccessError::OtherError)
}
}


#[cfg(test)]
mod tests {
use sea_orm::{DbConn, Schema, DbBackend, sea_query::TableCreateStatement};

use super::*;

async fn setup_schema(db: &DbConn) {
let schema = Schema::new(DbBackend::Sqlite);

// Derive from Entity
// let stmt: TableCreateStatement = schema.create_table_from_entity(MyEntity);

// Or setup manually
// assert_eq!(
// stmt.build(SqliteQueryBuilder),
// Table::create()
// .table(MyEntity)
// .col(
// ColumnDef::new(MyEntity::Column::Id)
// .integer()
// .not_null()
// )
// //...
// .build(SqliteQueryBuilder)
// );

// // Execute create table statement
// let result = db
// .execute(db.get_database_backend().build(&stmt))
// .await;
}

// #[rstest]
// #[case("/?name=Wonder", "Wonder%")] // Verify that % is appended to the filter
// async fn get_media_items_for_user_success(#[case] uri: &'static str, #[case] expected_filter: &'static str) {

// let mut repo_mock = MockMediaRepositoryTrait::new("sqlite::memory:");
// setup_schema(&db).await?;
// testcase(&db).await?;
// }
}

0 comments on commit 48c0132

Please sign in to comment.