Compare commits
No commits in common. "ae59478dd6ef034a7168be2edcbae8d5fab3f693" and "0352989083b4ad4fbdc7fe991d1da2d6e82b3f9a" have entirely different histories.
ae59478dd6
...
0352989083
16 changed files with 125 additions and 961 deletions
52
Cargo.lock
generated
52
Cargo.lock
generated
|
@ -936,8 +936,6 @@ dependencies = [
|
||||||
"console",
|
"console",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"linked-hash-map",
|
"linked-hash-map",
|
||||||
"pest",
|
|
||||||
"pest_derive",
|
|
||||||
"ron",
|
"ron",
|
||||||
"serde",
|
"serde",
|
||||||
"similar",
|
"similar",
|
||||||
|
@ -1242,50 +1240,6 @@ version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pest"
|
|
||||||
version = "2.5.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8cbd939b234e95d72bc393d51788aec68aeeb5d51e748ca08ff3aad58cb722f7"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror",
|
|
||||||
"ucd-trie",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pest_derive"
|
|
||||||
version = "2.5.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a81186863f3d0a27340815be8f2078dd8050b14cd71913db9fbda795e5f707d7"
|
|
||||||
dependencies = [
|
|
||||||
"pest",
|
|
||||||
"pest_generator",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pest_generator"
|
|
||||||
version = "2.5.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "75a1ef20bf3193c15ac345acb32e26b3dc3223aff4d77ae4fc5359567683796b"
|
|
||||||
dependencies = [
|
|
||||||
"pest",
|
|
||||||
"pest_meta",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pest_meta"
|
|
||||||
version = "2.5.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5e3b284b1f13a20dc5ebc90aff59a51b8d7137c221131b52a7260c08cbc1cc80"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
"pest",
|
|
||||||
"sha2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "0.4.30"
|
version = "0.4.30"
|
||||||
|
@ -2160,12 +2114,6 @@ version = "1.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ucd-trie"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uncased"
|
name = "uncased"
|
||||||
version = "0.9.7"
|
version = "0.9.7"
|
||||||
|
|
|
@ -23,7 +23,6 @@ time = { version = "0.3.15", features = [
|
||||||
] }
|
] }
|
||||||
sha2 = "0.10.6"
|
sha2 = "0.10.6"
|
||||||
path_macro = "1.0.0"
|
path_macro = "1.0.0"
|
||||||
hex-literal = "0.3.4"
|
|
||||||
hex = { version = "0.4.3", features = ["serde"] }
|
hex = { version = "0.4.3", features = ["serde"] }
|
||||||
temp-dir = "0.1.11"
|
temp-dir = "0.1.11"
|
||||||
zip = { version = "0.6.4", default-features = false, features = [
|
zip = { version = "0.6.4", default-features = false, features = [
|
||||||
|
@ -50,4 +49,5 @@ rstest = "0.16.0"
|
||||||
poem = { version = "1.3.55", features = ["test"] }
|
poem = { version = "1.3.55", features = ["test"] }
|
||||||
tokio-test = "0.4.2"
|
tokio-test = "0.4.2"
|
||||||
temp_testdir = "0.2.3"
|
temp_testdir = "0.2.3"
|
||||||
insta = { version = "1.17.1", features = ["ron", "redactions"] }
|
insta = { version = "1.17.1", features = ["ron"] }
|
||||||
|
hex-literal = "0.3.4"
|
||||||
|
|
190
src/api.rs
190
src/api.rs
|
@ -1,6 +1,5 @@
|
||||||
use std::io::Cursor;
|
use std::{collections::BTreeMap, io::Cursor};
|
||||||
|
|
||||||
use hex_literal::hex;
|
|
||||||
use poem::{
|
use poem::{
|
||||||
error::{Error, ResponseError},
|
error::{Error, ResponseError},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
|
@ -10,7 +9,7 @@ use poem::{
|
||||||
use poem_openapi::{
|
use poem_openapi::{
|
||||||
auth::ApiKey,
|
auth::ApiKey,
|
||||||
param::{Path, Query},
|
param::{Path, Query},
|
||||||
payload::{Binary, Html, Json},
|
payload::{Binary, Json},
|
||||||
OpenApi, SecurityScheme,
|
OpenApi, SecurityScheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,7 +17,7 @@ use crate::{
|
||||||
config::{Access, KeyCfg},
|
config::{Access, KeyCfg},
|
||||||
db,
|
db,
|
||||||
model::*,
|
model::*,
|
||||||
oai::{DynParams, FileResponse},
|
oai::DynParams,
|
||||||
util, Talon,
|
util, Talon,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -35,8 +34,7 @@ struct ApiKeyAuthorization(KeyCfg);
|
||||||
|
|
||||||
async fn api_key_checker(req: &Request, api_key: ApiKey) -> Option<KeyCfg> {
|
async fn api_key_checker(req: &Request, api_key: ApiKey) -> Option<KeyCfg> {
|
||||||
let talon = req.data::<Talon>()?;
|
let talon = req.data::<Talon>()?;
|
||||||
let x = talon.cfg.keys.get(&api_key.key).cloned();
|
talon.cfg.keys.get(&api_key.key).cloned()
|
||||||
x
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApiKeyAuthorization {
|
impl ApiKeyAuthorization {
|
||||||
|
@ -59,16 +57,12 @@ enum ApiError {
|
||||||
NoAccess,
|
NoAccess,
|
||||||
#[error("invalid fallback: {0}")]
|
#[error("invalid fallback: {0}")]
|
||||||
InvalidFallback(String),
|
InvalidFallback(String),
|
||||||
#[error("invalid archive type")]
|
|
||||||
InvalidArchiveType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for ApiError {
|
impl ResponseError for ApiError {
|
||||||
fn status(&self) -> StatusCode {
|
fn status(&self) -> StatusCode {
|
||||||
match self {
|
match self {
|
||||||
ApiError::InvalidSubdomain
|
ApiError::InvalidSubdomain | ApiError::InvalidFallback(_) => StatusCode::BAD_REQUEST,
|
||||||
| ApiError::InvalidFallback(_)
|
|
||||||
| ApiError::InvalidArchiveType => StatusCode::BAD_REQUEST,
|
|
||||||
ApiError::NoAccess => StatusCode::UNAUTHORIZED,
|
ApiError::NoAccess => StatusCode::UNAUTHORIZED,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,19 +71,6 @@ impl ResponseError for ApiError {
|
||||||
#[OpenApi]
|
#[OpenApi]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
impl TalonApi {
|
impl TalonApi {
|
||||||
/// Show some information about the API
|
|
||||||
#[oai(path = "/", method = "get", hidden)]
|
|
||||||
async fn root(&self) -> Html<&str> {
|
|
||||||
// TODO: use a pretty template for this
|
|
||||||
Html(
|
|
||||||
r#"<html>
|
|
||||||
<h1>Talon API</h1>
|
|
||||||
<p><a href="/api/swagger">Rumfingern</a></p>
|
|
||||||
<p><a href="/api/spec">OpenAPI specification</a></p>
|
|
||||||
</html>"#,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a website
|
/// Get a website
|
||||||
#[oai(path = "/website/:subdomain", method = "get")]
|
#[oai(path = "/website/:subdomain", method = "get")]
|
||||||
async fn website_get(
|
async fn website_get(
|
||||||
|
@ -106,7 +87,7 @@ impl TalonApi {
|
||||||
|
|
||||||
/// Create a new website
|
/// Create a new website
|
||||||
#[oai(path = "/website/:subdomain", method = "put")]
|
#[oai(path = "/website/:subdomain", method = "put")]
|
||||||
async fn website_create(
|
async fn website_post(
|
||||||
&self,
|
&self,
|
||||||
auth: ApiKeyAuthorization,
|
auth: ApiKeyAuthorization,
|
||||||
talon: Data<&Talon>,
|
talon: Data<&Talon>,
|
||||||
|
@ -251,12 +232,12 @@ impl TalonApi {
|
||||||
talon: Data<&Talon>,
|
talon: Data<&Talon>,
|
||||||
subdomain: Path<String>,
|
subdomain: Path<String>,
|
||||||
version: Path<u32>,
|
version: Path<u32>,
|
||||||
) -> Result<Json<Vec<VersionFile>>> {
|
) -> Result<Json<Vec<String>>> {
|
||||||
talon.db.version_exists(&subdomain, *version)?;
|
talon.db.version_exists(&subdomain, *version)?;
|
||||||
talon
|
talon
|
||||||
.db
|
.db
|
||||||
.get_version_files(&subdomain, *version)
|
.get_version_files(&subdomain, *version)
|
||||||
.map(|r| r.map(VersionFile::from))
|
.map(|r| r.map(|f| f.0))
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.map(Json)
|
.map(Json)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
|
@ -277,9 +258,62 @@ impl TalonApi {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upload a new version
|
/// Insert a new version into the database
|
||||||
#[oai(path = "/website/:subdomain/upload", method = "post")]
|
fn insert_version(
|
||||||
async fn version_upload(
|
talon: &Talon,
|
||||||
|
subdomain: &str,
|
||||||
|
fallback: Option<String>,
|
||||||
|
spa: bool,
|
||||||
|
mut version_data: BTreeMap<String, String>,
|
||||||
|
) -> Result<u32> {
|
||||||
|
version_data.remove("fallback");
|
||||||
|
version_data.remove("spa");
|
||||||
|
|
||||||
|
let id = talon.db.insert_version(
|
||||||
|
subdomain,
|
||||||
|
&db::model::Version {
|
||||||
|
data: version_data,
|
||||||
|
fallback,
|
||||||
|
spa,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the given version as the most recent one
|
||||||
|
fn finalize_version(
|
||||||
|
talon: &Talon,
|
||||||
|
subdomain: &str,
|
||||||
|
version: u32,
|
||||||
|
fallback: Option<&str>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Validata fallback path
|
||||||
|
if let Some(fallback) = fallback {
|
||||||
|
if let Err(e) =
|
||||||
|
talon
|
||||||
|
.storage
|
||||||
|
.get_file(subdomain, version, fallback, &Default::default())
|
||||||
|
{
|
||||||
|
// Remove the bad version
|
||||||
|
let _ = talon.db.delete_version(subdomain, version, false);
|
||||||
|
return Err(ApiError::InvalidFallback(e.to_string()).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
talon.db.update_website(
|
||||||
|
subdomain,
|
||||||
|
db::model::WebsiteUpdate {
|
||||||
|
latest_version: Some(Some(version)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload a new version (.zip archive)
|
||||||
|
#[oai(path = "/website/:subdomain/uploadZip", method = "post")]
|
||||||
|
async fn version_upload_zip(
|
||||||
&self,
|
&self,
|
||||||
auth: ApiKeyAuthorization,
|
auth: ApiKeyAuthorization,
|
||||||
talon: Data<&Talon>,
|
talon: Data<&Talon>,
|
||||||
|
@ -296,80 +330,46 @@ impl TalonApi {
|
||||||
/// This is an arbitrary string map that can hold build information and other stuff
|
/// This is an arbitrary string map that can hold build information and other stuff
|
||||||
/// and will be displayed in the site info dialog.
|
/// and will be displayed in the site info dialog.
|
||||||
version_data: DynParams,
|
version_data: DynParams,
|
||||||
/// Archive containing the website files.
|
/// zip archive with the website files
|
||||||
///
|
|
||||||
/// Supported types: zip, tar.gz
|
|
||||||
data: Binary<Vec<u8>>,
|
data: Binary<Vec<u8>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
auth.check_subdomain(&subdomain, Access::Upload)?;
|
auth.check_subdomain(&subdomain, Access::Upload)?;
|
||||||
let mut version_data = version_data.0;
|
let version =
|
||||||
version_data.remove("fallback");
|
Self::insert_version(&talon, &subdomain, fallback.clone(), spa.0, version_data.0)?;
|
||||||
version_data.remove("spa");
|
|
||||||
|
|
||||||
let version = talon.db.insert_version(
|
|
||||||
&subdomain,
|
|
||||||
&db::model::Version {
|
|
||||||
data: version_data,
|
|
||||||
fallback: fallback.0.clone(),
|
|
||||||
spa: spa.0,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if data.starts_with(&hex!("1f8b")) {
|
|
||||||
talon
|
|
||||||
.storage
|
|
||||||
.insert_tgz_archive(data.as_slice(), &subdomain, version)?;
|
|
||||||
} else if data.starts_with(&hex!("504b0304")) {
|
|
||||||
talon
|
talon
|
||||||
.storage
|
.storage
|
||||||
.insert_zip_archive(Cursor::new(data.as_slice()), &subdomain, version)?;
|
.insert_zip_archive(Cursor::new(data.as_slice()), &subdomain, version)?;
|
||||||
} else {
|
Self::finalize_version(&talon, &subdomain, version, fallback.as_deref())
|
||||||
return Err(ApiError::InvalidArchiveType.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validata fallback path
|
/// Upload a new version (.tar.gz archive)
|
||||||
if let Some(fallback) = &fallback.0 {
|
#[oai(path = "/website/:subdomain/uploadTgz", method = "post")]
|
||||||
if let Err(e) =
|
async fn version_upload_tgz(
|
||||||
|
&self,
|
||||||
|
auth: ApiKeyAuthorization,
|
||||||
|
talon: Data<&Talon>,
|
||||||
|
subdomain: Path<String>,
|
||||||
|
/// Fallback page
|
||||||
|
///
|
||||||
|
/// The fallback page gets returned when the requested page does not exist
|
||||||
|
fallback: Query<Option<String>>,
|
||||||
|
/// SPA mode (return fallback page with OK status)
|
||||||
|
#[oai(default)]
|
||||||
|
spa: Query<bool>,
|
||||||
|
/// Associated version data
|
||||||
|
///
|
||||||
|
/// This is an arbitrary string map that can hold build information and other stuff
|
||||||
|
/// and will be displayed in the site info dialog.
|
||||||
|
version_data: DynParams,
|
||||||
|
/// tar.gz archive with the website files
|
||||||
|
data: Binary<Vec<u8>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
auth.check_subdomain(&subdomain, Access::Upload)?;
|
||||||
|
let version =
|
||||||
|
Self::insert_version(&talon, &subdomain, fallback.clone(), spa.0, version_data.0)?;
|
||||||
talon
|
talon
|
||||||
.storage
|
.storage
|
||||||
.get_file(&subdomain, version, fallback, &Default::default())
|
.insert_tgz_archive(data.as_slice(), &subdomain, version)?;
|
||||||
{
|
Self::finalize_version(&talon, &subdomain, version, fallback.as_deref())
|
||||||
// Remove the bad version
|
|
||||||
let _ = talon.db.delete_version(&subdomain, version, false);
|
|
||||||
return Err(ApiError::InvalidFallback(e.to_string()).into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
talon.db.update_website(
|
|
||||||
&subdomain,
|
|
||||||
db::model::WebsiteUpdate {
|
|
||||||
latest_version: Some(Some(version)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve a file
|
|
||||||
#[oai(path = "/file/:hash", method = "get")]
|
|
||||||
async fn get_file(
|
|
||||||
&self,
|
|
||||||
talon: Data<&Talon>,
|
|
||||||
request: &Request,
|
|
||||||
hash: Path<String>,
|
|
||||||
) -> Result<FileResponse> {
|
|
||||||
let hash = hex::decode(hash.as_bytes()).map_err(|_| poem::http::StatusCode::BAD_REQUEST)?;
|
|
||||||
let gf = talon.storage.get_file_from_hash(
|
|
||||||
&hash,
|
|
||||||
Some(mime_guess::mime::APPLICATION_OCTET_STREAM),
|
|
||||||
None,
|
|
||||||
request.headers(),
|
|
||||||
)?;
|
|
||||||
let resp = talon
|
|
||||||
.storage
|
|
||||||
.file_to_response(gf, request.headers(), true)
|
|
||||||
.await?;
|
|
||||||
Ok(FileResponse(resp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -313,7 +313,7 @@ impl Db {
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
},
|
},
|
||||||
None => None,
|
None => todo!(),
|
||||||
})?
|
})?
|
||||||
.and_then(|data| rmp_serde::from_slice::<Website>(&data).ok());
|
.and_then(|data| rmp_serde::from_slice::<Website>(&data).ok());
|
||||||
|
|
||||||
|
|
34
src/model.rs
34
src/model.rs
|
@ -1,6 +1,5 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use hex::ToHex;
|
|
||||||
use poem_openapi::{Enum, Object};
|
use poem_openapi::{Enum, Object};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
@ -30,16 +29,14 @@ pub struct Website {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new website
|
/// Create a new website
|
||||||
#[derive(Debug, Clone, Object, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Object)]
|
||||||
pub struct WebsiteNew {
|
pub struct WebsiteNew {
|
||||||
/// Website name
|
/// Website name
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// Color of the page icon
|
/// Color of the page icon
|
||||||
pub color: Option<u32>,
|
pub color: Option<u32>,
|
||||||
/// Visibility of the page in the sidebar menu
|
/// Visibility of the page in the sidebar menu
|
||||||
#[serde(default)]
|
pub visibility: Option<Visibility>,
|
||||||
#[oai(default)]
|
|
||||||
pub visibility: Visibility,
|
|
||||||
/// Link to the source of the page
|
/// Link to the source of the page
|
||||||
pub source_url: Option<String>,
|
pub source_url: Option<String>,
|
||||||
/// Icon for the source link
|
/// Icon for the source link
|
||||||
|
@ -49,7 +46,7 @@ pub struct WebsiteNew {
|
||||||
/// Update a website with the contained values
|
/// Update a website with the contained values
|
||||||
///
|
///
|
||||||
/// Values set to `None` remain unchanged.
|
/// Values set to `None` remain unchanged.
|
||||||
#[derive(Debug, Clone, Object, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Object)]
|
||||||
pub struct WebsiteUpdate {
|
pub struct WebsiteUpdate {
|
||||||
/// Website name
|
/// Website name
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
@ -78,17 +75,6 @@ pub struct Version {
|
||||||
pub data: BTreeMap<String, String>,
|
pub data: BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Website file
|
|
||||||
#[derive(Debug, Clone, Object, Serialize, Deserialize)]
|
|
||||||
pub struct VersionFile {
|
|
||||||
/// File path
|
|
||||||
pub path: String,
|
|
||||||
/// File hash
|
|
||||||
pub hash: String,
|
|
||||||
/// MIME file type
|
|
||||||
pub mime: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Enum, Serialize, Deserialize,
|
Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Enum, Serialize, Deserialize,
|
||||||
)]
|
)]
|
||||||
|
@ -134,7 +120,7 @@ impl From<WebsiteNew> for db::model::Website {
|
||||||
Self {
|
Self {
|
||||||
name: value.name,
|
name: value.name,
|
||||||
color: value.color,
|
color: value.color,
|
||||||
visibility: value.visibility,
|
visibility: value.visibility.unwrap_or_default(),
|
||||||
source_url: value.source_url,
|
source_url: value.source_url,
|
||||||
source_icon: value.source_icon,
|
source_icon: value.source_icon,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -165,15 +151,3 @@ impl From<(u32, db::model::Version)> for Version {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(String, Vec<u8>)> for VersionFile {
|
|
||||||
fn from(value: (String, Vec<u8>)) -> Self {
|
|
||||||
Self {
|
|
||||||
mime: mime_guess::from_path(&value.0)
|
|
||||||
.first()
|
|
||||||
.map(|m| m.essence_str().to_owned()),
|
|
||||||
path: value.0,
|
|
||||||
hash: value.1.encode_hex(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
50
src/oai.rs
50
src/oai.rs
|
@ -1,16 +1,11 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use poem::{IntoResponse, Request, RequestBody, Response, Result};
|
use poem::{Request, RequestBody, Result};
|
||||||
use poem_openapi::{
|
use poem_openapi::{
|
||||||
ApiExtractor, ApiExtractorType, ExtractParamOptions,
|
ApiExtractor, ApiExtractorType, ExtractParamOptions,
|
||||||
__private::UrlQuery,
|
__private::UrlQuery,
|
||||||
payload::Payload,
|
registry::{MetaParamIn, MetaSchemaRef, Registry},
|
||||||
registry::{
|
|
||||||
MetaHeader, MetaMediaType, MetaParamIn, MetaResponse, MetaResponses, MetaSchemaRef,
|
|
||||||
Registry,
|
|
||||||
},
|
|
||||||
types::Type,
|
types::Type,
|
||||||
ApiResponse,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct DynParams(pub BTreeMap<String, String>);
|
pub struct DynParams(pub BTreeMap<String, String>);
|
||||||
|
@ -56,44 +51,3 @@ impl<'a> ApiExtractor<'a> for DynParams {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FileResponse(pub Response);
|
|
||||||
|
|
||||||
impl IntoResponse for FileResponse {
|
|
||||||
fn into_response(self) -> Response {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApiResponse for FileResponse {
|
|
||||||
fn meta() -> MetaResponses {
|
|
||||||
MetaResponses {
|
|
||||||
responses: vec![MetaResponse {
|
|
||||||
description: "File content",
|
|
||||||
status: Some(200),
|
|
||||||
content: vec![MetaMediaType {
|
|
||||||
content_type: "application/octet-stream",
|
|
||||||
schema: poem_openapi::payload::Binary::<()>::schema_ref(),
|
|
||||||
}],
|
|
||||||
headers: vec![
|
|
||||||
MetaHeader {
|
|
||||||
name: "etag".to_owned(),
|
|
||||||
description: Some("File hash".to_owned()),
|
|
||||||
required: true,
|
|
||||||
deprecated: false,
|
|
||||||
schema: String::schema_ref(),
|
|
||||||
},
|
|
||||||
MetaHeader {
|
|
||||||
name: "last-modified".to_owned(),
|
|
||||||
description: Some("Date when the file was last modified".to_owned()),
|
|
||||||
required: true,
|
|
||||||
deprecated: false,
|
|
||||||
schema: String::schema_ref(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register(_registry: &mut Registry) {}
|
|
||||||
}
|
|
||||||
|
|
18
src/page.rs
18
src/page.rs
|
@ -6,7 +6,7 @@ use poem::{
|
||||||
IntoResponse, Request, Response, Result,
|
IntoResponse, Request, Response, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{storage::StorageError, util, Talon};
|
use crate::{storage::StorageError, Talon};
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum PageError {
|
pub enum PageError {
|
||||||
|
@ -27,17 +27,15 @@ pub async fn page(request: &Request, talon: Data<&Talon>) -> Result<Response> {
|
||||||
let host = request
|
let host = request
|
||||||
.header(header::HOST)
|
.header(header::HOST)
|
||||||
.ok_or(PageError::InvalidSubdomain)?;
|
.ok_or(PageError::InvalidSubdomain)?;
|
||||||
let (subdomain, vid) =
|
let subdomain = if host == talon.cfg.server.root_domain {
|
||||||
util::parse_host(host, &talon.cfg.server.root_domain).ok_or(PageError::InvalidSubdomain)?;
|
"-"
|
||||||
|
} else {
|
||||||
let vid = match vid {
|
host.strip_suffix(&format!(".{}", talon.cfg.server.root_domain))
|
||||||
Some(vid) => vid,
|
.ok_or(PageError::InvalidSubdomain)?
|
||||||
None => {
|
|
||||||
let ws = talon.db.get_website(subdomain)?;
|
|
||||||
ws.latest_version.ok_or(PageError::NoVersion)?
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let ws = talon.db.get_website(subdomain)?;
|
||||||
|
let vid = ws.latest_version.ok_or(PageError::NoVersion)?;
|
||||||
let (file, ok) =
|
let (file, ok) =
|
||||||
match talon
|
match talon
|
||||||
.storage
|
.storage
|
||||||
|
|
|
@ -75,12 +75,10 @@ pub enum StorageError {
|
||||||
InvalidFile(PathBuf),
|
InvalidFile(PathBuf),
|
||||||
#[error("zip archive error: {0}")]
|
#[error("zip archive error: {0}")]
|
||||||
Zip(#[from] zip::result::ZipError),
|
Zip(#[from] zip::result::ZipError),
|
||||||
#[error("tar.gz archive error: {0}")]
|
|
||||||
Tgz(String),
|
|
||||||
#[error("page `{0}` not found")]
|
#[error("page `{0}` not found")]
|
||||||
NotFound(String),
|
NotFound(String),
|
||||||
#[error("file `{0}` missing from storage")]
|
#[error("file `{0}` of page `{1}` missing from storage")]
|
||||||
MissingFile(String),
|
MissingFile(String, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for StorageError {
|
impl ResponseError for StorageError {
|
||||||
|
@ -88,10 +86,7 @@ impl ResponseError for StorageError {
|
||||||
match self {
|
match self {
|
||||||
StorageError::Db(e) => e.status(),
|
StorageError::Db(e) => e.status(),
|
||||||
StorageError::NotFound(_) => StatusCode::NOT_FOUND,
|
StorageError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||||
StorageError::InvalidFile(_) | StorageError::Zip(_) | StorageError::Tgz(_) => {
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
StatusCode::BAD_REQUEST
|
|
||||||
}
|
|
||||||
StorageError::Io(_) | StorageError::MissingFile(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -247,9 +242,7 @@ impl Storage {
|
||||||
let temp = TempDir::with_prefix(TMPDIR_PREFIX)?;
|
let temp = TempDir::with_prefix(TMPDIR_PREFIX)?;
|
||||||
let decoder = GzDecoder::new(reader);
|
let decoder = GzDecoder::new(reader);
|
||||||
let mut archive = tar::Archive::new(decoder);
|
let mut archive = tar::Archive::new(decoder);
|
||||||
archive
|
archive.unpack(temp.path())?;
|
||||||
.unpack(temp.path())
|
|
||||||
.map_err(|e| StorageError::Tgz(e.to_string()))?;
|
|
||||||
let import_path = Self::fix_archive_path(temp.path())?;
|
let import_path = Self::fix_archive_path(temp.path())?;
|
||||||
self.insert_dir(import_path, subdomain, version)
|
self.insert_dir(import_path, subdomain, version)
|
||||||
}
|
}
|
||||||
|
@ -364,18 +357,8 @@ impl Storage {
|
||||||
|
|
||||||
let mime = util::site_path_mime(&new_path);
|
let mime = util::site_path_mime(&new_path);
|
||||||
|
|
||||||
self.get_file_from_hash(&hash, mime, rd_path, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_file_from_hash(
|
|
||||||
&self,
|
|
||||||
hash: &[u8],
|
|
||||||
mime: Option<Mime>,
|
|
||||||
rd_path: Option<String>,
|
|
||||||
headers: &HeaderMap,
|
|
||||||
) -> Result<GotFile> {
|
|
||||||
let algorithms = self.file_compressions(
|
let algorithms = self.file_compressions(
|
||||||
hash,
|
&hash,
|
||||||
mime.as_ref()
|
mime.as_ref()
|
||||||
.map(|m| Self::is_compressible(m.essence_str()))
|
.map(|m| Self::is_compressible(m.essence_str()))
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
@ -385,12 +368,15 @@ impl Storage {
|
||||||
match alg {
|
match alg {
|
||||||
Some(alg) => Ok(GotFile {
|
Some(alg) => Ok(GotFile {
|
||||||
hash: hash.encode_hex(),
|
hash: hash.encode_hex(),
|
||||||
file_path: self.file_path_compressed(hash, alg),
|
file_path: self.file_path_compressed(&hash, alg),
|
||||||
encoding: alg.encoding(),
|
encoding: alg.encoding(),
|
||||||
mime,
|
mime,
|
||||||
rd_path,
|
rd_path,
|
||||||
}),
|
}),
|
||||||
None => Err(StorageError::MissingFile(hash.encode_hex())),
|
None => Err(StorageError::MissingFile(
|
||||||
|
hash.encode_hex(),
|
||||||
|
new_path.into(),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -444,7 +430,6 @@ impl Storage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML files are not precompressed and need to have UI code injected
|
|
||||||
if gf
|
if gf
|
||||||
.mime
|
.mime
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
32
src/util.rs
32
src/util.rs
|
@ -97,11 +97,9 @@ pub fn parse_accept_encoding(
|
||||||
/// Subdomains may only contain letters a-z, numbers 0-9 and dashes.
|
/// Subdomains may only contain letters a-z, numbers 0-9 and dashes.
|
||||||
/// They must not start or end with dashes.
|
/// They must not start or end with dashes.
|
||||||
///
|
///
|
||||||
/// Forbidden subdomains: `xn` (punycode), `x` (reserved placeholder)
|
|
||||||
///
|
|
||||||
/// Special case: the root domain is described by subdomain `-`
|
/// Special case: the root domain is described by subdomain `-`
|
||||||
pub fn validate_subdomain(subdomain: &str) -> bool {
|
pub fn validate_subdomain(subdomain: &str) -> bool {
|
||||||
if subdomain.is_empty() || subdomain.len() > 200 || subdomain == "xn" || subdomain == "x" {
|
if subdomain.is_empty() || subdomain.len() > 200 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,34 +116,6 @@ pub fn validate_subdomain(subdomain: &str) -> bool {
|
||||||
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
|
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the given hostname
|
|
||||||
///
|
|
||||||
/// Returns the page subdomain and optional version ID
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// `example.com` (root domain)
|
|
||||||
///
|
|
||||||
/// `x--v1.example.com` (root domain + version id)
|
|
||||||
///
|
|
||||||
/// `talon.example.com` (subdomain)
|
|
||||||
///
|
|
||||||
/// `talon--v1.example.com` (subdomain + version id)
|
|
||||||
pub fn parse_host<'a>(host: &'a str, root_domain: &str) -> Option<(&'a str, Option<u32>)> {
|
|
||||||
if host == root_domain {
|
|
||||||
Some(("-", None))
|
|
||||||
} else {
|
|
||||||
let subdomain = host.strip_suffix(&format!(".{}", root_domain))?;
|
|
||||||
|
|
||||||
if let Some((subdomain, vstr)) = subdomain.split_once("--") {
|
|
||||||
let version = vstr.strip_prefix('v')?.parse().ok()?;
|
|
||||||
let subdomain = if subdomain == "x" { "-" } else { subdomain };
|
|
||||||
Some((subdomain, Some(version)))
|
|
||||||
} else {
|
|
||||||
Some((subdomain, None))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_dir_ne<P: AsRef<Path>>(path: P) -> Result<(), std::io::Error> {
|
pub fn create_dir_ne<P: AsRef<Path>>(path: P) -> Result<(), std::io::Error> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
if !path.is_dir() {
|
if !path.is_dir() {
|
||||||
|
|
9
tests/fixtures/mod.rs
vendored
9
tests/fixtures/mod.rs
vendored
|
@ -43,10 +43,6 @@ pub const HASH_SPA_INDEX: [u8; 32] =
|
||||||
pub const HASH_SPA_FALLBACK: [u8; 32] =
|
pub const HASH_SPA_FALLBACK: [u8; 32] =
|
||||||
hex!("4ee0d3f7522f620a2a69b39b7443f8fe65029e1324cefaf797b8cad2b223cf7b");
|
hex!("4ee0d3f7522f620a2a69b39b7443f8fe65029e1324cefaf797b8cad2b223cf7b");
|
||||||
|
|
||||||
pub const API_KEY_ROOT: &str = "c32ff286c8ac1c3102625badf38ffd251ae0c4a56079d8ba490f320af63f1f47";
|
|
||||||
pub const API_KEY_2: &str = "21bdac19ffd22870d561b1d55b35eddd9029497107edb7b926aa3e7856bb409b";
|
|
||||||
// pub const API_KEY_3: &str = "04e99561e3824f387a217d141d2a3b46375de6864afbedf9c9a2cc102bc946a4";
|
|
||||||
|
|
||||||
pub struct DbTest {
|
pub struct DbTest {
|
||||||
db: Db,
|
db: Db,
|
||||||
_temp: TempDir,
|
_temp: TempDir,
|
||||||
|
@ -265,11 +261,6 @@ impl Deref for TalonTest {
|
||||||
#[fixture]
|
#[fixture]
|
||||||
pub fn tln() -> TalonTest {
|
pub fn tln() -> TalonTest {
|
||||||
let temp = temp_testdir::TempDir::default();
|
let temp = temp_testdir::TempDir::default();
|
||||||
std::fs::copy(
|
|
||||||
path!("tests" / "testfiles" / "config" / "config_test.toml"),
|
|
||||||
path!(temp / "config.toml"),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let talon = Talon::new(&temp).unwrap();
|
let talon = Talon::new(&temp).unwrap();
|
||||||
|
|
||||||
insert_websites(&talon.db);
|
insert_websites(&talon.db);
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/tests.rs
|
|
||||||
expression: files
|
|
||||||
---
|
|
||||||
[
|
|
||||||
VersionFile(
|
|
||||||
path: "assets/example.txt",
|
|
||||||
hash: "bae6bdae8097c24f9a99028e04bfc8d5e0a0c318955316db0e7b955def9c1dbb",
|
|
||||||
mime: Some("text/plain"),
|
|
||||||
),
|
|
||||||
VersionFile(
|
|
||||||
path: "assets/image.jpg",
|
|
||||||
hash: "901d291a47a8a9b55c06f84e5e5f82fd2dcee65cac1406d6e878b805d45c1e93",
|
|
||||||
mime: Some("image/jpeg"),
|
|
||||||
),
|
|
||||||
VersionFile(
|
|
||||||
path: "assets/style.css",
|
|
||||||
hash: "356f131c825fbf604797c7e9c85352549d81db8af91fee834016d075110af026",
|
|
||||||
mime: Some("text/css"),
|
|
||||||
),
|
|
||||||
VersionFile(
|
|
||||||
path: "assets/test.js",
|
|
||||||
hash: "b6ed35f5ae339a35a8babb11a91ff90c1a62ef250d30fa98e59500e8dbb896fa",
|
|
||||||
mime: Some("application/javascript"),
|
|
||||||
),
|
|
||||||
VersionFile(
|
|
||||||
path: "assets/thetadev-blue.svg",
|
|
||||||
hash: "9c37a2cb1230a9cbe7911d34404d4fb03b27552e56b2173683cf9fc52be7bc99",
|
|
||||||
mime: Some("image/svg+xml"),
|
|
||||||
),
|
|
||||||
VersionFile(
|
|
||||||
path: "index.html",
|
|
||||||
hash: "a44816e6c3b650bdf88e6532659ba07ef187c2113ae311da9709e056aec8eadb",
|
|
||||||
mime: Some("text/html"),
|
|
||||||
),
|
|
||||||
VersionFile(
|
|
||||||
path: "logo.png",
|
|
||||||
hash: "9f7e7971b4bfdb75429e534dea461ed90340886925078cda252cada9aa0e25f7",
|
|
||||||
mime: Some("image/png"),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,22 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/tests.rs
|
|
||||||
expression: versions
|
|
||||||
---
|
|
||||||
[
|
|
||||||
Version(
|
|
||||||
id: 1,
|
|
||||||
created_at: "2023-02-18T16:30:00Z",
|
|
||||||
data: {
|
|
||||||
"Deployed by": "https://github.com/Theta-Dev/Talon/actions/runs/1352014628",
|
|
||||||
"Version": "v0.1.0",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Version(
|
|
||||||
id: 2,
|
|
||||||
created_at: "2023-02-18T16:52:00Z",
|
|
||||||
data: {
|
|
||||||
"Deployed by": "https://github.com/Theta-Dev/Talon/actions/runs/1354755231",
|
|
||||||
"Version": "v0.1.1",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,36 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/tests.rs
|
|
||||||
expression: websites
|
|
||||||
---
|
|
||||||
[
|
|
||||||
Website(
|
|
||||||
subdomain: "-",
|
|
||||||
name: "ThetaDev",
|
|
||||||
created_at: "2023-02-18T16:30:00Z",
|
|
||||||
latest_version: Some(2),
|
|
||||||
color: Some(2068974),
|
|
||||||
visibility: featured,
|
|
||||||
source_url: None,
|
|
||||||
source_icon: None,
|
|
||||||
),
|
|
||||||
Website(
|
|
||||||
subdomain: "rustypipe",
|
|
||||||
name: "RustyPipe",
|
|
||||||
created_at: "2023-02-20T18:30:00Z",
|
|
||||||
latest_version: Some(1),
|
|
||||||
color: Some(7943647),
|
|
||||||
visibility: featured,
|
|
||||||
source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe"),
|
|
||||||
source_icon: Some(gitea),
|
|
||||||
),
|
|
||||||
Website(
|
|
||||||
subdomain: "spotify-gender-ex",
|
|
||||||
name: "Spotify-Gender-Ex",
|
|
||||||
created_at: "2023-02-18T16:30:00Z",
|
|
||||||
latest_version: Some(1),
|
|
||||||
color: Some(1947988),
|
|
||||||
visibility: featured,
|
|
||||||
source_url: Some("https://github.com/Theta-Dev/Spotify-Gender-Ex"),
|
|
||||||
source_icon: Some(github),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,46 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/tests.rs
|
|
||||||
expression: websites
|
|
||||||
---
|
|
||||||
[
|
|
||||||
Website(
|
|
||||||
subdomain: "-",
|
|
||||||
name: "ThetaDev",
|
|
||||||
created_at: "2023-02-18T16:30:00Z",
|
|
||||||
latest_version: Some(2),
|
|
||||||
color: Some(2068974),
|
|
||||||
visibility: featured,
|
|
||||||
source_url: None,
|
|
||||||
source_icon: None,
|
|
||||||
),
|
|
||||||
Website(
|
|
||||||
subdomain: "rustypipe",
|
|
||||||
name: "RustyPipe",
|
|
||||||
created_at: "2023-02-20T18:30:00Z",
|
|
||||||
latest_version: Some(1),
|
|
||||||
color: Some(7943647),
|
|
||||||
visibility: featured,
|
|
||||||
source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe"),
|
|
||||||
source_icon: Some(gitea),
|
|
||||||
),
|
|
||||||
Website(
|
|
||||||
subdomain: "spa",
|
|
||||||
name: "SvelteKit SPA",
|
|
||||||
created_at: "2023-03-03T22:00:00Z",
|
|
||||||
latest_version: Some(1),
|
|
||||||
color: Some(16727552),
|
|
||||||
visibility: hidden,
|
|
||||||
source_url: None,
|
|
||||||
source_icon: None,
|
|
||||||
),
|
|
||||||
Website(
|
|
||||||
subdomain: "spotify-gender-ex",
|
|
||||||
name: "Spotify-Gender-Ex",
|
|
||||||
created_at: "2023-02-18T16:30:00Z",
|
|
||||||
latest_version: Some(1),
|
|
||||||
color: Some(1947988),
|
|
||||||
visibility: featured,
|
|
||||||
source_url: Some("https://github.com/Theta-Dev/Spotify-Gender-Ex"),
|
|
||||||
source_icon: Some(github),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,18 +0,0 @@
|
||||||
# Config file for running tests
|
|
||||||
|
|
||||||
[keys.c32ff286c8ac1c3102625badf38ffd251ae0c4a56079d8ba490f320af63f1f47]
|
|
||||||
domains = "*"
|
|
||||||
upload = true
|
|
||||||
modify = true
|
|
||||||
|
|
||||||
[keys.21bdac19ffd22870d561b1d55b35eddd9029497107edb7b926aa3e7856bb409b]
|
|
||||||
domains = ["spotify-gender-ex", "rustypipe", "test"]
|
|
||||||
upload = true
|
|
||||||
|
|
||||||
[keys.04e99561e3824f387a217d141d2a3b46375de6864afbedf9c9a2cc102bc946a4]
|
|
||||||
domains = "/^talon-\\d+/"
|
|
||||||
upload = true
|
|
||||||
modify = true
|
|
||||||
|
|
||||||
[keys.48691ad9f42bb12e61e259b5e90dc941a293cfae11af18c9e6557f92557f0086]
|
|
||||||
domains = "*"
|
|
493
tests/tests.rs
493
tests/tests.rs
|
@ -587,7 +587,6 @@ mod page {
|
||||||
#[case::rustypipe2("rustypipe", "/page2/index.html", &HASH_3_1_PAGE2, "text/html")]
|
#[case::rustypipe2("rustypipe", "/page2/index.html", &HASH_3_1_PAGE2, "text/html")]
|
||||||
#[case::spa_index("spa", "/", &HASH_SPA_INDEX, "text/html")]
|
#[case::spa_index("spa", "/", &HASH_SPA_INDEX, "text/html")]
|
||||||
#[case::spa_fallback("spa", "/user/2", &HASH_SPA_FALLBACK, "text/html")]
|
#[case::spa_fallback("spa", "/user/2", &HASH_SPA_FALLBACK, "text/html")]
|
||||||
#[case::version("x--v1", "/", &HASH_1_1_INDEX, "text/html")]
|
|
||||||
fn page(
|
fn page(
|
||||||
tln: TalonTest,
|
tln: TalonTest,
|
||||||
#[case] subdomain: &str,
|
#[case] subdomain: &str,
|
||||||
|
@ -685,495 +684,3 @@ mod page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod api {
|
|
||||||
use hex::ToHex;
|
|
||||||
use hex_literal::hex;
|
|
||||||
use poem::{
|
|
||||||
http::{header, Method, StatusCode},
|
|
||||||
test::TestClient,
|
|
||||||
};
|
|
||||||
use talon::model::*;
|
|
||||||
use time::macros::datetime;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn website_get(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.get("http://talon.localhost:3000/api/website/spotify-gender-ex")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status_is_ok();
|
|
||||||
tokio_test::block_on(resp.assert_json(Website {
|
|
||||||
subdomain: "spotify-gender-ex".to_owned(),
|
|
||||||
name: "Spotify-Gender-Ex".to_owned(),
|
|
||||||
created_at: datetime!(2023-02-18 16:30 +0),
|
|
||||||
latest_version: Some(1),
|
|
||||||
color: Some(1947988),
|
|
||||||
visibility: Visibility::Featured,
|
|
||||||
source_url: Some("https://github.com/Theta-Dev/Spotify-Gender-Ex".to_owned()),
|
|
||||||
source_icon: Some(SourceIcon::Github),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn website_get_404(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.get("http://talon.localhost:3000/api/website/foo")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status(StatusCode::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn website_create(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.put("http://talon.localhost:3000/api/website/test")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.header("x-api-key", API_KEY_ROOT)
|
|
||||||
.data(tln.clone())
|
|
||||||
.body_json(&WebsiteNew {
|
|
||||||
name: "Test".to_owned(),
|
|
||||||
color: Some(1000),
|
|
||||||
visibility: Visibility::Searchable,
|
|
||||||
source_icon: Some(SourceIcon::Git),
|
|
||||||
source_url: Some("example.com".to_owned()),
|
|
||||||
})
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status_is_ok();
|
|
||||||
|
|
||||||
let ws = tln.db.get_website("test").unwrap();
|
|
||||||
insta::assert_ron_snapshot!(ws, {".created_at" => "[date]"}, @r###"
|
|
||||||
Website(
|
|
||||||
name: "Test",
|
|
||||||
created_at: "[date]",
|
|
||||||
latest_version: None,
|
|
||||||
color: Some(1000),
|
|
||||||
visibility: searchable,
|
|
||||||
source_url: Some("example.com"),
|
|
||||||
source_icon: Some(git),
|
|
||||||
vid_count: 0,
|
|
||||||
)
|
|
||||||
"###);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn website_create_conflict(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.put("http://talon.localhost:3000/api/website/-")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.header("x-api-key", API_KEY_ROOT)
|
|
||||||
.data(tln.clone())
|
|
||||||
.body_json(&WebsiteNew {
|
|
||||||
name: "Test".to_owned(),
|
|
||||||
color: Some(1000),
|
|
||||||
visibility: Visibility::Searchable,
|
|
||||||
source_icon: Some(SourceIcon::Git),
|
|
||||||
source_url: Some("example.com".to_owned()),
|
|
||||||
})
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status(StatusCode::CONFLICT);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn website_update(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.patch("http://talon.localhost:3000/api/website/-")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.header("x-api-key", API_KEY_ROOT)
|
|
||||||
.data(tln.clone())
|
|
||||||
.body_json(&WebsiteUpdate {
|
|
||||||
name: Some("Test".to_owned()),
|
|
||||||
color: Some(Some(1000)),
|
|
||||||
visibility: Some(Visibility::Searchable),
|
|
||||||
source_icon: Some(Some(SourceIcon::Git)),
|
|
||||||
source_url: Some(Some("example.com".to_owned())),
|
|
||||||
})
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status_is_ok();
|
|
||||||
|
|
||||||
let ws = tln.db.get_website("-").unwrap();
|
|
||||||
insta::assert_ron_snapshot!(ws, @r###"
|
|
||||||
Website(
|
|
||||||
name: "Test",
|
|
||||||
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
|
||||||
latest_version: Some(2),
|
|
||||||
color: Some(1000),
|
|
||||||
visibility: searchable,
|
|
||||||
source_url: Some("example.com"),
|
|
||||||
source_icon: Some(git),
|
|
||||||
vid_count: 2,
|
|
||||||
)
|
|
||||||
"###);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn website_update_404(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.patch("http://talon.localhost:3000/api/website/foo")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.header("x-api-key", API_KEY_ROOT)
|
|
||||||
.data(tln.clone())
|
|
||||||
.body_json(&WebsiteUpdate {
|
|
||||||
name: Some("Test".to_owned()),
|
|
||||||
color: Some(Some(1000)),
|
|
||||||
visibility: Some(Visibility::Searchable),
|
|
||||||
source_icon: Some(Some(SourceIcon::Git)),
|
|
||||||
source_url: Some(Some("example.com".to_owned())),
|
|
||||||
})
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status(StatusCode::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn website_delete(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.delete("http://talon.localhost:3000/api/website/-")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.header("x-api-key", API_KEY_ROOT)
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status_is_ok();
|
|
||||||
|
|
||||||
let err = tln.db.get_website("-").unwrap_err();
|
|
||||||
assert!(matches!(err, DbError::NotExists(_, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn website_delete_404(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.delete("http://talon.localhost:3000/api/website/foo")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.header("x-api-key", API_KEY_ROOT)
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status(StatusCode::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn websites_get(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.get("http://talon.localhost:3000/api/websites")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status(StatusCode::OK);
|
|
||||||
let websites =
|
|
||||||
tokio_test::block_on(resp.0.into_body().into_json::<Vec<Website>>()).unwrap();
|
|
||||||
insta::assert_ron_snapshot!(websites);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn websites_get_all(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.get("http://talon.localhost:3000/api/websitesAll")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.header("x-api-key", API_KEY_ROOT)
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status(StatusCode::OK);
|
|
||||||
let websites =
|
|
||||||
tokio_test::block_on(resp.0.into_body().into_json::<Vec<Website>>()).unwrap();
|
|
||||||
insta::assert_ron_snapshot!(websites);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `websitesAll` should only return hidden websites if the user can access them
|
|
||||||
#[rstest]
|
|
||||||
fn websites_get_all_noperm(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.get("http://talon.localhost:3000/api/websitesAll")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.header("x-api-key", API_KEY_2)
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status(StatusCode::OK);
|
|
||||||
let websites =
|
|
||||||
tokio_test::block_on(resp.0.into_body().into_json::<Vec<Website>>()).unwrap();
|
|
||||||
insta::assert_ron_snapshot!("websites_get", websites);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn website_versions(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.get("http://talon.localhost:3000/api/website/-/versions")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status(StatusCode::OK);
|
|
||||||
let versions =
|
|
||||||
tokio_test::block_on(resp.0.into_body().into_json::<Vec<Version>>()).unwrap();
|
|
||||||
insta::assert_ron_snapshot!(versions);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn website_versions_404(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.get("http://talon.localhost:3000/api/website/foo/versions")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status(StatusCode::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn version_files(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.get("http://talon.localhost:3000/api/website/-/version/2/files")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status(StatusCode::OK);
|
|
||||||
let files =
|
|
||||||
tokio_test::block_on(resp.0.into_body().into_json::<Vec<VersionFile>>()).unwrap();
|
|
||||||
insta::assert_ron_snapshot!(files);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn version_files_404(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.get("http://talon.localhost:3000/api/website/-/version/3/files")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status(StatusCode::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn version_delete(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.delete("http://talon.localhost:3000/api/website/-/version/2")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.header("x-api-key", API_KEY_ROOT)
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status_is_ok();
|
|
||||||
|
|
||||||
let err = tln.db.get_version("-", 2).unwrap_err();
|
|
||||||
assert!(matches!(err, DbError::NotExists(_, _)));
|
|
||||||
|
|
||||||
let ws = tln.db.get_website("-").unwrap();
|
|
||||||
assert_eq!(ws.latest_version, Some(1));
|
|
||||||
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.delete("http://talon.localhost:3000/api/website/-/version/1")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.header("x-api-key", API_KEY_ROOT)
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status_is_ok();
|
|
||||||
|
|
||||||
let err = tln.db.get_version("-", 1).unwrap_err();
|
|
||||||
assert!(matches!(err, DbError::NotExists(_, _)));
|
|
||||||
|
|
||||||
let ws = tln.db.get_website("-").unwrap();
|
|
||||||
assert_eq!(ws.latest_version, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn version_delete_404(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.delete("http://talon.localhost:3000/api/website/-/version/3")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.header("x-api-key", API_KEY_ROOT)
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status(StatusCode::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn version_upload_zip(tln: TalonTest) {
|
|
||||||
let path = path!("tests" / "testfiles" / "archive" / "ThetaDev1.zip");
|
|
||||||
let archive = std::fs::read(path).unwrap();
|
|
||||||
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.post("http://talon.localhost:3000/api/website/rustypipe/upload?version=1.2.3&hello=world")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
|
||||||
.header("x-api-key", API_KEY_ROOT)
|
|
||||||
.data(tln.clone())
|
|
||||||
.body(archive)
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status_is_ok();
|
|
||||||
|
|
||||||
let ws = tln.db.get_website("rustypipe").unwrap();
|
|
||||||
assert_eq!(ws.latest_version, Some(2));
|
|
||||||
|
|
||||||
let version = tln.db.get_version("rustypipe", 2).unwrap();
|
|
||||||
insta::assert_ron_snapshot!(version, {".created_at" => "[date]"}, @r###"
|
|
||||||
Version(
|
|
||||||
created_at: "[date]",
|
|
||||||
data: {
|
|
||||||
"hello": "world",
|
|
||||||
"version": "1.2.3",
|
|
||||||
},
|
|
||||||
fallback: None,
|
|
||||||
spa: false,
|
|
||||||
)
|
|
||||||
"###);
|
|
||||||
|
|
||||||
let files = tln
|
|
||||||
.db
|
|
||||||
.get_version_files("rustypipe", 2)
|
|
||||||
.collect::<Result<Vec<_>, _>>()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(files.len(), 7);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn version_upload_tgz(tln: TalonTest) {
|
|
||||||
let path = path!("tests" / "testfiles" / "archive" / "spa.tar.gz");
|
|
||||||
let archive = std::fs::read(path).unwrap();
|
|
||||||
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.post("http://talon.localhost:3000/api/website/rustypipe/upload?spa=true&fallback=200.html&version=1.2.3")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
|
||||||
.header("x-api-key", API_KEY_ROOT)
|
|
||||||
.data(tln.clone())
|
|
||||||
.body(archive)
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status_is_ok();
|
|
||||||
|
|
||||||
let ws = tln.db.get_website("rustypipe").unwrap();
|
|
||||||
assert_eq!(ws.latest_version, Some(2));
|
|
||||||
|
|
||||||
let version = tln.db.get_version("rustypipe", 2).unwrap();
|
|
||||||
insta::assert_ron_snapshot!(version, {".created_at" => "[date]"}, @r###"
|
|
||||||
Version(
|
|
||||||
created_at: "[date]",
|
|
||||||
data: {
|
|
||||||
"version": "1.2.3",
|
|
||||||
},
|
|
||||||
fallback: Some("200.html"),
|
|
||||||
spa: true,
|
|
||||||
)
|
|
||||||
"###);
|
|
||||||
|
|
||||||
let files = tln
|
|
||||||
.db
|
|
||||||
.get_version_files("rustypipe", 2)
|
|
||||||
.collect::<Result<Vec<_>, _>>()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(files.len(), 23);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn version_upload_fallback_not_found(tln: TalonTest) {
|
|
||||||
let path = path!("tests" / "testfiles" / "archive" / "ThetaDev1.zip");
|
|
||||||
let archive = std::fs::read(path).unwrap();
|
|
||||||
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.post("http://talon.localhost:3000/api/website/rustypipe/upload?spa=true&fallback=foo.html")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
|
||||||
.header("x-api-key", API_KEY_ROOT)
|
|
||||||
.data(tln.clone())
|
|
||||||
.body(archive)
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status(StatusCode::BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case::no_archive(&hex!("badeaffe"))]
|
|
||||||
#[case::bad_zip(&hex!("504b0304badeaffe"))]
|
|
||||||
#[case::bad_tgz(&hex!("1f8bbadeaffe"))]
|
|
||||||
fn version_upload_invalid(tln: TalonTest, #[case] data: &[u8]) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.post("http://talon.localhost:3000/api/website/rustypipe/upload?spa=true&fallback=foo.html")
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
|
||||||
.header("x-api-key", API_KEY_ROOT)
|
|
||||||
.data(tln.clone())
|
|
||||||
.body(data.to_vec())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status(StatusCode::BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn file(tln: TalonTest) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.get(format!(
|
|
||||||
"http://talon.localhost:3000/api/file/{}",
|
|
||||||
HASH_1_1_INDEX.encode_hex::<String>()
|
|
||||||
))
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status_is_ok();
|
|
||||||
|
|
||||||
let expect =
|
|
||||||
std::fs::read_to_string(path!("tests" / "testfiles" / "ThetaDev0" / "index.html"))
|
|
||||||
.unwrap();
|
|
||||||
tokio_test::block_on(resp.assert_text(expect));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case::website_create("website/test", Method::PUT)]
|
|
||||||
#[case::website_update("website/test", Method::PATCH)]
|
|
||||||
#[case::website_delete("website/test", Method::DELETE)]
|
|
||||||
#[case::websites_all("websitesAll", Method::GET)]
|
|
||||||
#[case::version_delete("website/test/version/1", Method::DELETE)]
|
|
||||||
#[case::version_upload("website/test/upload", Method::POST)]
|
|
||||||
fn unauthorized(tln: TalonTest, #[case] endpoint: &str, #[case] method: Method) {
|
|
||||||
let resp = tokio_test::block_on(
|
|
||||||
TestClient::new(tln.endpoint())
|
|
||||||
.request(
|
|
||||||
method,
|
|
||||||
format!("http://talon.localhost:3000/api/{endpoint}"),
|
|
||||||
)
|
|
||||||
.header(header::HOST, "talon.localhost:3000")
|
|
||||||
.data(tln.clone())
|
|
||||||
.send(),
|
|
||||||
);
|
|
||||||
resp.assert_status(StatusCode::UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue