Basic /image and /image/backgrounded tests

This commit is contained in:
asonix 2023-07-20 22:04:50 -05:00
parent 7a21618a54
commit 7c766ab434
14 changed files with 1626 additions and 1 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/target
/.direnv
/.envrc
/result

1193
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,3 +6,17 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-rt = "2.8.0"
awc = { version = "3.1.1", default-features = false }
color-eyre = "0.6.2"
mime = "0.3.17"
multipart-client-stream = "0.1.0"
serde = { version = "1.0.173", features = ["derive"] }
time = { version = "0.3.23", features = ["formatting", "parsing", "serde-well-known"] }
tokio = { version = "1.29.1", features = ["fs"] }
tracing = "0.1.37"
url = { version = "2.4.0", features = ["serde"] }
uuid = { version = "1.4.1", features = ["serde"] }
[dev-dependencies]
serde_json = "1"

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
images/valid/Shenzi.avif Normal file

Binary file not shown.

BIN
images/valid/Shenzi.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
images/valid/Shenzi.jxl Normal file

Binary file not shown.

BIN
images/valid/Shenzi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

BIN
images/valid/Shenzi.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,3 +1,208 @@
fn main() {
mod types;
use std::{path::Path, time::Duration};
use awc::{http::StatusCode, Client, ClientResponse};
use multipart_client_stream::{Body, Part};
use types::{Alias, DeleteToken, Image, ImageResponse, Upload, UploadResponse};
use url::Url;
#[actix_rt::main]
async fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
let client = pict_rs_client();
let endpoint: Url = "http://localhost:8080".parse()?;
let mut readdir = tokio::fs::read_dir("./images/valid").await?;
let mut valid_paths = Vec::new();
while let Some(entry) = readdir.next_entry().await? {
valid_paths.push(entry.path());
}
let images = upload_backgrounded(&client, &endpoint, &valid_paths)
.await?
.into_iter()
.map(check_response)
.collect::<color_eyre::Result<Vec<Vec<Image>>>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>();
// TODO: head images
// TODO: fetch images
// TODO: fetch details
// TODO: process images
for Image {
file, delete_token, ..
} in images
{
delete_image(&client, &endpoint, &file, &delete_token).await?;
}
let images = check_response(upload_inline(&client, &endpoint, &valid_paths).await?)?;
// TODO: head images
// TODO: fetch images
// TODO: fetch details
// TODO: process images
for Image {
file, delete_token, ..
} in images
{
delete_image(&client, &endpoint, &file, &delete_token).await?;
}
// TODO: Healthz
// TODO: Invalid Images
// TODO: Internal APIs
println!("Hello, world!");
Ok(())
}
fn pict_rs_client() -> Client {
Client::builder()
.disable_redirects()
.add_default_header(("user-agent", "pict-rs-integration-tests v0.1.0"))
.timeout(Duration::from_secs(30))
.finish()
}
#[tracing::instrument(skip(client, images))]
async fn post_images<P: AsRef<Path>>(
client: &Client,
endpoint: &Url,
images: &[P],
) -> color_eyre::Result<ClientResponse> {
let mut builder = Body::builder();
for image in images {
let filename = image
.as_ref()
.file_name()
.ok_or(color_eyre::eyre::eyre!("No file name"))?
.to_str()
.ok_or(color_eyre::eyre::eyre!("Invalid filename string"))?;
let file = tokio::fs::File::open(image).await?;
builder = builder.append(Part::new("images[]", file).filename(filename));
}
let body = builder.build();
let response = client
.post(endpoint.as_str())
.insert_header(("content-type", body.content_type()))
.send_stream(body)
.await
.map_err(|e| color_eyre::eyre::eyre!("{e}"))?;
Ok(response)
}
#[tracing::instrument(skip(client, images))]
async fn upload_inline<P: AsRef<Path>>(
client: &Client,
endpoint: &Url,
images: &[P],
) -> color_eyre::Result<ImageResponse> {
let mut endpoint = endpoint.clone();
endpoint.set_path("/image");
let mut response = post_images(client, &endpoint, images).await?;
let image_response: ImageResponse = response.json().await?;
Ok(image_response)
}
#[tracing::instrument(skip(client, images))]
async fn upload_backgrounded<P: AsRef<Path>>(
client: &Client,
endpoint: &Url,
images: &[P],
) -> color_eyre::Result<Vec<ImageResponse>> {
let mut endpoint = endpoint.clone();
endpoint.set_path("/image/backgrounded");
let mut response = post_images(client, &endpoint, images).await?;
let upload_response: UploadResponse = response.json().await?;
let uploads = match upload_response {
UploadResponse::Ok { uploads, .. } => uploads,
UploadResponse::Error { msg } => return Err(color_eyre::eyre::eyre!("{msg}")),
};
let mut tasks = Vec::new();
for upload in uploads {
let client = client.clone();
let mut endpoint = endpoint.clone();
endpoint.set_path("/image/backgrounded/claim");
tasks.push(actix_rt::spawn(async move {
claim_image(&client, &endpoint, &upload).await
}));
}
let mut responses = Vec::new();
for task in tasks {
responses.push(task.await??);
}
Ok(responses)
}
#[tracing::instrument(skip(client))]
async fn claim_image(
client: &Client,
endpoint: &Url,
upload: &Upload,
) -> color_eyre::Result<ImageResponse> {
loop {
let mut response = client
.get(endpoint.as_str())
.timeout(Duration::from_secs(15))
.query(upload)?
.send()
.await
.map_err(|e| color_eyre::eyre::eyre!("{e}"))?;
if response.status() == StatusCode::NO_CONTENT {
continue;
}
return Ok(response.json().await?);
}
}
async fn delete_image(
client: &Client,
endpoint: &Url,
alias: &Alias,
delete_token: &DeleteToken,
) -> color_eyre::Result<()> {
let mut endpoint = endpoint.clone();
endpoint.set_path(&format!("/image/delete/{delete_token}/{alias}"));
let response = client
.delete(endpoint.as_str())
.send()
.await
.map_err(|e| color_eyre::eyre::eyre!("{e}"))?;
if response.status() != StatusCode::NO_CONTENT {
return Err(color_eyre::eyre::eyre!("Failed to delete image"));
}
Ok(())
}
fn check_response(response: ImageResponse) -> color_eyre::Result<Vec<Image>> {
match response {
ImageResponse::Ok { files, .. } => Ok(files),
ImageResponse::Error { msg } => Err(color_eyre::eyre::eyre!("{msg}")),
}
}

212
src/types.rs Normal file
View file

@ -0,0 +1,212 @@
use serde::de::Error;
use time::OffsetDateTime;
use uuid::Uuid;
#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
pub(crate) enum UploadResponse {
Ok {
#[allow(unused)]
msg: OkString,
uploads: Vec<Upload>,
},
Error {
msg: String,
},
}
#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
pub(crate) enum ImageResponse {
Ok {
#[allow(unused)]
msg: OkString,
files: Vec<Image>,
},
Error {
msg: String,
},
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub(crate) struct Upload {
pub(crate) upload_id: Uuid,
}
#[derive(Debug, serde::Deserialize)]
pub(crate) enum OkString {
#[serde(rename = "ok")]
Ok,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub(crate) struct Image {
pub(crate) delete_token: DeleteToken,
pub(crate) file: Alias,
pub(crate) details: Details,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
pub(crate) enum DeleteToken {
Uuid(Uuid),
Old(String),
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
pub(crate) enum Alias {
Alias(AliasInner),
Old(String),
}
#[derive(Debug)]
pub(crate) struct AliasInner {
uuid: Uuid,
extension: String,
}
#[derive(Debug)]
pub(crate) struct AliasError;
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub(crate) struct Details {
pub(crate) width: u16,
pub(crate) height: u16,
pub(crate) frames: Option<u32>,
pub(crate) content_type: Mime,
#[serde(with = "time::serde::rfc3339")]
pub(crate) created_at: OffsetDateTime,
}
#[derive(Debug)]
pub(crate) struct Mime(mime::Mime);
impl std::ops::Deref for Mime {
type Target = mime::Mime;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<mime::Mime> for Mime {
fn as_ref(&self) -> &mime::Mime {
&self.0
}
}
impl std::str::FromStr for AliasInner {
type Err = AliasError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some((left, right)) = s.split_once('.') {
let uuid = left.parse::<Uuid>().map_err(|_| AliasError)?;
Ok(AliasInner {
uuid,
extension: String::from(right),
})
} else {
Err(AliasError)
}
}
}
impl std::fmt::Display for Alias {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Alias(inner) => inner.fmt(f),
Self::Old(s) => s.fmt(f),
}
}
}
impl std::fmt::Display for DeleteToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Uuid(u) => u.fmt(f),
Self::Old(s) => s.fmt(f),
}
}
}
impl std::fmt::Display for AliasInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.uuid, self.extension)
}
}
impl std::fmt::Display for AliasError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Invalid alias format")
}
}
impl std::error::Error for AliasError {}
impl<'de> serde::Deserialize<'de> for AliasInner {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse::<AliasInner>().map_err(D::Error::custom)
}
}
impl serde::Serialize for AliasInner {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let s = self.to_string();
String::serialize(&s, serializer)
}
}
impl<'de> serde::Deserialize<'de> for Mime {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse::<mime::Mime>().map_err(D::Error::custom).map(Mime)
}
}
impl serde::Serialize for Mime {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let s = self.to_string();
String::serialize(&s, serializer)
}
}
#[cfg(test)]
mod tests {
#[test]
fn deserialize_ok() {
#[derive(serde::Deserialize)]
struct TestStruct {
#[serde(rename = "msg")]
_msg: super::OkString,
}
let _: TestStruct = serde_json::from_str(r#"{"msg":"ok"}"#).expect("Deserialized");
}
#[test]
fn deserialize_image_response() {
let response: super::ImageResponse =
serde_json::from_str(r#"{"msg": "ok", "files": []}"#).expect("Deserialized");
assert!(matches!(response, super::ImageResponse::Ok { .. }))
}
}