Talon/tests/tests.rs
2023-04-02 18:19:34 +02:00

1433 lines
47 KiB
Rust

mod fixtures;
use std::{fs::File, io::BufReader};
use path_macro::path;
use rstest::rstest;
use fixtures::*;
use talon::db::{Db, DbError};
const ICON_SIZE: u32 = 48;
mod database {
use super::*;
use talon::db::model::WebsiteUpdate;
fn get_export(db: &Db) -> String {
let mut buf: Vec<u8> = Vec::new();
db.export(&mut buf).unwrap();
String::from_utf8(buf).unwrap()
}
#[rstest]
fn export(db: DbTest) {
let data = get_export(&db);
insta::assert_snapshot!("export", data);
}
#[rstest]
fn export_import(db: DbTest) {
let td = temp_testdir::TempDir::default();
let p_export = td.join("export.jsonl");
let p_db2 = td.join("db2");
db.export(File::create(&p_export).unwrap()).unwrap();
let db2 = Db::new(p_db2).unwrap();
db2.import(BufReader::new(File::open(&p_export).unwrap()))
.unwrap();
let data = get_export(&db2);
insta::assert_snapshot!("export", data);
}
#[rstest]
fn get_website(db: DbTest) {
let ws1 = db.get_website(SUBDOMAIN_1).unwrap();
let ws2 = db.get_website("spotify-gender-ex").unwrap();
let ws3 = db.get_website("rustypipe").unwrap();
insta::assert_ron_snapshot!(vec![ws1, ws2, ws3]);
}
#[rstest]
fn delete_website(db: DbTest) {
db.delete_website(SUBDOMAIN_1, true).unwrap();
assert!(matches!(
db.get_website(SUBDOMAIN_1).unwrap_err(),
DbError::NotExists(_, _)
));
assert!(matches!(
db.delete_website(SUBDOMAIN_1, true).unwrap_err(),
DbError::NotExists(_, _)
));
db.delete_website(SUBDOMAIN_1, false).unwrap();
let data = get_export(&db);
insta::assert_snapshot!(data);
}
#[rstest]
fn update_website(db: DbTest) {
db.update_website(
SUBDOMAIN_1,
WebsiteUpdate {
name: Some("ThetaDev2".to_owned()),
color: Some(Some(1000)),
visibility: Some(talon::model::Visibility::Hidden),
source_url: Some(Some("https://example.com".to_owned())),
source_icon: Some(Some(talon::model::SourceIcon::Link)),
..Default::default()
},
)
.unwrap();
let website = db.get_website(SUBDOMAIN_1).unwrap();
insta::assert_ron_snapshot!(website, {".updated_at" => "[date]"});
}
#[rstest]
fn get_websites(db: DbTest) {
let websites = db.get_websites().map(|w| w.unwrap()).collect::<Vec<_>>();
insta::assert_ron_snapshot!(websites);
}
#[rstest]
fn get_version(db: DbTest) {
let version = db.get_version(SUBDOMAIN_1, 1).unwrap();
insta::assert_ron_snapshot!(version);
}
#[rstest]
fn delete_version(db: DbTest) {
db.delete_version(SUBDOMAIN_1, 2, true).unwrap();
assert!(matches!(
db.get_version(SUBDOMAIN_1, 2).unwrap_err(),
DbError::NotExists(_, _)
));
assert!(matches!(
db.delete_version(SUBDOMAIN_1, 2, true).unwrap_err(),
DbError::NotExists(_, _)
));
db.delete_version(SUBDOMAIN_1, 2, false).unwrap();
// Check if files were deleted
assert!(db.get_version_files(SUBDOMAIN_1, 2).next().is_none());
// Check if latest version was updated
let ws = db.get_website(SUBDOMAIN_1).unwrap();
assert_eq!(ws.latest_version, Some(1));
}
#[rstest]
fn get_website_versions(db: DbTest) {
let versions = db
.get_website_versions(SUBDOMAIN_1)
.map(|v| v.unwrap())
.collect::<Vec<_>>();
insta::assert_ron_snapshot!(versions);
}
#[rstest]
fn get_website_version_ids(db: DbTest) {
let ids = db
.get_website_version_ids(SUBDOMAIN_1)
.map(|v| v.unwrap())
.collect::<Vec<_>>();
assert_eq!(ids, vec![1, 2]);
}
#[rstest]
fn get_file(db: DbTest) {
let hash = db.get_file(SUBDOMAIN_1, 1, "index.html").unwrap();
assert_eq!(hash, HASH_1_1_INDEX);
}
#[rstest]
fn delete_file(db: DbTest) {
db.delete_file(SUBDOMAIN_1, 1, "index.html", true).unwrap();
assert!(matches!(
db.get_file(SUBDOMAIN_1, 1, "index.html").unwrap_err(),
DbError::NotExists(_, _)
));
assert!(matches!(
db.delete_file(SUBDOMAIN_1, 1, "index.html", true)
.unwrap_err(),
DbError::NotExists(_, _)
));
db.delete_file(SUBDOMAIN_1, 1, "index.html", false).unwrap();
}
#[rstest]
fn get_version_files(db: DbTest) {
let files = db
.get_version_files(SUBDOMAIN_1, 1)
.map(|f| f.unwrap())
.collect::<Vec<_>>();
assert_eq!(
files,
vec![
("index.html".to_owned(), HASH_1_1_INDEX.to_vec()),
("style.css".to_owned(), HASH_1_1_STYLE.to_vec())
]
);
}
#[rstest]
fn get_file_hashes(db: DbTest) {
let hashes = db.get_file_hashes().unwrap();
assert_eq!(hashes.len(), 11)
}
}
mod storage {
use std::{str::FromStr, time::SystemTime};
use hex::ToHex;
use poem::{
error::StaticFileError,
http::{header, HeaderMap, StatusCode},
web::headers::{self, HeaderMapExt},
};
use talon::storage::{GotFile, Storage};
use talon::{
config::{CompressionCfg, Config, ConfigInner},
storage::CompressionAlg,
};
use time::OffsetDateTime;
use super::*;
#[rstest]
fn insert_files(db_empty: DbTest) {
let dir = path!("tests" / "testfiles" / "ThetaDev1");
let temp = temp_testdir::TempDir::default();
let store = Storage::new(temp.to_path_buf(), db_empty.clone(), Default::default());
store.insert_dir(dir, SUBDOMAIN_1, 1).unwrap();
let files = db_empty
.get_version_files(SUBDOMAIN_1, 1)
.map(|f| f.unwrap())
.collect::<Vec<_>>();
insta::assert_ron_snapshot!("insert_files", files);
for (_, hash) in files {
let hash_str = hash.encode_hex::<String>();
let path = temp.join(&hash_str[..2]).join(&hash_str);
assert!(path.is_file());
}
}
#[rstest]
fn insert_zip_archive(db_empty: DbTest) {
let archive = path!("tests" / "testfiles" / "archive" / "ThetaDev1.zip");
let temp = temp_testdir::TempDir::default();
let store = Storage::new(temp.to_path_buf(), db_empty.clone(), Default::default());
store
.insert_zip_archive(File::open(archive).unwrap(), SUBDOMAIN_1, 1)
.unwrap();
let files = db_empty
.get_version_files(SUBDOMAIN_1, 1)
.map(|f| f.unwrap())
.collect::<Vec<_>>();
insta::assert_ron_snapshot!("insert_files", files);
for (_, hash) in files {
let hash_str = hash.encode_hex::<String>();
let path = temp.join(&hash_str[..2]).join(&hash_str);
assert!(path.is_file());
}
}
#[rstest]
fn insert_tgz_archive(db_empty: DbTest) {
let archive = path!("tests" / "testfiles" / "archive" / "ThetaDev1.tar.gz");
let temp = temp_testdir::TempDir::default();
let store = Storage::new(temp.to_path_buf(), db_empty.clone(), Default::default());
store
.insert_tgz_archive(File::open(archive).unwrap(), SUBDOMAIN_1, 1)
.unwrap();
let files = db_empty
.get_version_files(SUBDOMAIN_1, 1)
.map(|f| f.unwrap())
.collect::<Vec<_>>();
insta::assert_ron_snapshot!("insert_files", files);
for (_, hash) in files {
let hash_str = hash.encode_hex::<String>();
let path = temp.join(&hash_str[..2]).join(&hash_str);
assert!(path.is_file());
}
}
#[rstest]
#[case::gzip(CompressionCfg {gzip_en: true, ..Default::default()}, "gz")]
#[case::brotli(CompressionCfg {brotli_en: true, ..Default::default()}, "br")]
fn insert_files_compressed(
db_empty: DbTest,
#[case] compression: CompressionCfg,
#[case] ext: &str,
) {
let dir = path!("tests" / "testfiles" / "ThetaDev1");
let temp = temp_testdir::TempDir::default();
let cfg = Config::new(ConfigInner {
compression,
..Default::default()
});
let store = Storage::new(temp.to_path_buf(), db_empty.clone(), cfg);
store.insert_dir(dir, SUBDOMAIN_1, 1).unwrap();
for f in db_empty.get_version_files(SUBDOMAIN_1, 1) {
let hash = f.unwrap().1;
let hash_str = hash.encode_hex::<String>();
let path = temp.join(&hash_str[..2]).join(&hash_str);
let path_compressed = path.with_extension(ext);
assert!(path.is_file());
// Images should not be compressed
let expect = &hash_str
!= "901d291a47a8a9b55c06f84e5e5f82fd2dcee65cac1406d6e878b805d45c1e93"
&& &hash_str != "9f7e7971b4bfdb75429e534dea461ed90340886925078cda252cada9aa0e25f7"
&& &hash_str != "a44816e6c3b650bdf88e6532659ba07ef187c2113ae311da9709e056aec8eadb";
assert_eq!(path_compressed.is_file(), expect)
}
}
#[rstest]
#[case::index("br", SUBDOMAIN_1, 2, "", false, "text/html", None)]
#[case::nocmp("", SUBDOMAIN_1, 2, "assets/style.css", true, "text/css", None)]
#[case::gzip("gzip", SUBDOMAIN_1, 2, "assets/style.css", true, "text/css", None)]
#[case::br("br", SUBDOMAIN_1, 2, "assets/style.css", true, "text/css", None)]
#[case::image("br", SUBDOMAIN_1, 2, "assets/image.jpg", false, "image/jpeg", None)]
#[case::subdir("br", SUBDOMAIN_3, 1, "page2", false, "text/html", Some("/page2/"))]
fn get_file(
tln: TalonTest,
#[case] encoding: &str,
#[case] subdomain: &str,
#[case] version: u32,
#[case] path: &str,
#[case] compressible: bool,
#[case] mime: &str,
#[case] rd_path: Option<&str>,
) {
let mut headers = HeaderMap::new();
headers.insert(header::ACCEPT_ENCODING, encoding.parse().unwrap());
let expect_ext = if compressible {
match encoding {
"gzip" => Some("gz"),
"" => None,
e => Some(e),
}
} else {
None
};
let index_file = tln
.storage
.get_file(subdomain, version, path, &headers)
.unwrap();
dbg!(&index_file);
assert!(index_file.file_path.is_file());
assert_eq!(
index_file
.file_path
.extension()
.map(|s| s.to_str().unwrap()),
expect_ext
);
assert_eq!(
index_file.encoding,
Some(encoding).filter(|s| compressible && !s.is_empty())
);
assert_eq!(index_file.mime.unwrap().essence_str(), mime);
assert_eq!(index_file.rd_path.as_deref(), rd_path);
}
#[rstest]
#[case::index(&HASH_1_2_INDEX, "text/html", "")]
#[case::index_gz(&HASH_1_2_INDEX, "text/html", "gzip")]
#[case::index_br(&HASH_1_2_INDEX, "text/html", "br")]
#[case::style(&HASH_1_2_STYLE, "text/css", "")]
fn file_to_response(
tln: TalonTest,
#[case] hash: &[u8],
#[case] mime: &str,
#[case] encoding: &str,
) {
let gf = got_file(&tln, hash, mime);
let file_date = std::fs::metadata(&gf.file_path)
.unwrap()
.modified()
.unwrap();
let mut headers = HeaderMap::new();
if !encoding.is_empty() {
headers.insert(header::ACCEPT_ENCODING, encoding.parse().unwrap());
}
let resp =
tokio_test::block_on(tln.storage.file_to_response(gf.clone(), &headers, true)).unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.header(header::CONTENT_TYPE).unwrap(), mime);
if mime == "text/html" {
assert_eq!(
resp.header(header::ETAG).unwrap(),
format!("\"{}_{}\"", gf.hash, talon::build::PKG_VERSION)
);
} else {
assert_eq!(
resp.header(header::ETAG).unwrap(),
format!("\"{}\"", gf.hash)
);
}
let date = OffsetDateTime::from(SystemTime::from(
resp.headers().typed_get::<headers::LastModified>().unwrap(),
));
assert!(date - file_date < time::Duration::SECOND);
// HTML files should get dynamically compressed
if mime == "text/html" && !encoding.is_empty() {
assert_eq!(resp.header(header::CONTENT_ENCODING).unwrap(), encoding);
} else {
assert!(resp.header(header::CONTENT_ENCODING).is_none())
}
}
fn got_file(tln: &TalonTest, hash: &[u8], mime: &str) -> GotFile {
let hash = hash.encode_hex::<String>();
let file_path = path!(tln.temp / "storage" / &hash[..2] / &hash);
GotFile {
hash: hash.clone(),
file_path,
encoding: None,
mime: Some(mime_guess::Mime::from_str(mime).unwrap()),
rd_path: None,
}
}
fn got_file_html(tln: &TalonTest) -> GotFile {
got_file(tln, &HASH_1_2_INDEX, "text/html")
}
#[rstest]
fn file_to_response_inject(tln: TalonTest) {
let gf = got_file_html(&tln);
let resp = tokio_test::block_on(tln.storage.file_to_response(gf, &HeaderMap::new(), true))
.unwrap();
let body = tokio_test::block_on(resp.into_body().into_string()).unwrap();
assert!(body.contains("<!-- INJECTED BY TALON -->\n"));
}
#[rstest]
#[case::unmodified(true)]
#[case::modified(true)]
fn file_to_response_if_modified(tln: TalonTest, #[case] modified: bool) {
let gf = got_file_html(&tln);
let mut file_date = std::fs::metadata(&gf.file_path)
.unwrap()
.modified()
.unwrap();
if modified {
file_date -= std::time::Duration::from_secs(1);
}
let mut headers = HeaderMap::new();
headers.typed_insert(headers::IfModifiedSince::from(file_date));
let resp = tokio_test::block_on(tln.storage.file_to_response(gf, &headers, true)).unwrap();
assert_eq!(
resp.status(),
if modified {
StatusCode::OK
} else {
StatusCode::NOT_MODIFIED
}
);
assert_eq!(resp.into_body().is_empty(), !modified);
}
#[rstest]
#[case::unmodified(true)]
#[case::modified(true)]
fn file_to_response_if_unmodified(tln: TalonTest, #[case] modified: bool) {
let gf = got_file_html(&tln);
let mut file_date = std::fs::metadata(&gf.file_path)
.unwrap()
.modified()
.unwrap();
if modified {
file_date -= std::time::Duration::from_secs(1);
}
let mut headers = HeaderMap::new();
headers.typed_insert(headers::IfModifiedSince::from(file_date));
let res = tokio_test::block_on(tln.storage.file_to_response(gf, &headers, true));
if modified {
let resp = res.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert!(!resp.into_body().is_empty());
} else {
assert!(matches!(
res.unwrap_err(),
StaticFileError::PreconditionFailed
));
}
}
#[rstest]
#[case::matched(true)]
#[case::no_match(false)]
fn file_to_response_if_match(tln: TalonTest, #[case] matched: bool) {
let gf = got_file_html(&tln);
let etag = format!(
"\"{}_{}\"",
if matched {
gf.hash.clone()
} else {
HASH_2_1_INDEX.encode_hex()
},
talon::build::PKG_VERSION,
);
let mut headers = HeaderMap::new();
headers.typed_insert(headers::IfMatch::from(
headers::ETag::from_str(&etag).unwrap(),
));
let res = tokio_test::block_on(tln.storage.file_to_response(gf, &headers, true));
if matched {
let resp = res.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert!(!resp.into_body().is_empty());
} else {
assert!(matches!(
res.unwrap_err(),
StaticFileError::PreconditionFailed
));
}
}
#[rstest]
#[case::matched(true)]
#[case::no_match(false)]
fn file_to_response_if_none_match(tln: TalonTest, #[case] matched: bool) {
let gf = got_file_html(&tln);
let etag = format!(
"\"{}_{}\"",
if matched {
gf.hash.clone()
} else {
HASH_2_1_INDEX.encode_hex()
},
talon::build::PKG_VERSION,
);
let mut headers = HeaderMap::new();
headers.typed_insert(headers::IfNoneMatch::from(
headers::ETag::from_str(&etag).unwrap(),
));
let resp = tokio_test::block_on(tln.storage.file_to_response(gf, &headers, true)).unwrap();
assert_eq!(
resp.status(),
if matched {
StatusCode::NOT_MODIFIED
} else {
StatusCode::OK
}
);
assert_eq!(resp.into_body().is_empty(), matched);
}
#[rstest]
fn file_to_response_range(tln: TalonTest) {
let gf = got_file(&tln, &HASH_2_1_STYLE, "text/css");
let mut headers = HeaderMap::new();
headers.typed_insert(headers::Range::bytes(0..100).unwrap());
let resp = tokio_test::block_on(tln.storage.file_to_response(gf, &headers, true)).unwrap();
assert_eq!(resp.status(), StatusCode::PARTIAL_CONTENT);
let body = tokio_test::block_on(resp.into_body().into_bytes()).unwrap();
assert_eq!(body.len(), 100);
}
#[rstest]
fn stored_files(tln: TalonTest) {
let mut n = 0;
for file in tln.storage.stored_files() {
let file = file.unwrap();
if file.compression == CompressionAlg::None {
let hash = talon::util::hash_file(&file.path).unwrap();
assert_eq!(file.hash, hash);
}
n += 1;
}
assert_eq!(n, 89);
}
#[rstest]
fn purge(tln: TalonTest) {
tln.db.delete_website("rustypipe", true).unwrap();
assert_eq!(tln.storage.purge().unwrap(), (5, 13798));
}
}
mod icons {
use talon::icons::Icons;
use super::*;
use image::io::Reader as ImageReader;
#[test]
fn insert_icon() {
let temp = temp_testdir::TempDir::default();
let icons = Icons::new(temp.to_path_buf());
let icon = File::open(path!("assets" / "icon.png")).unwrap();
icons.insert_icon(icon, "talon").unwrap();
let stored_path = path!(temp / "talon.png");
let got_path = icons.get_icon("talon").unwrap();
assert_eq!(stored_path, got_path);
assert!(stored_path.is_file());
let stored_img = ImageReader::open(&stored_path).unwrap().decode().unwrap();
assert_eq!(stored_img.height(), ICON_SIZE);
assert_eq!(stored_img.width(), ICON_SIZE);
}
#[test]
fn delete_icon() {
let temp = temp_testdir::TempDir::default();
let icons = Icons::new(temp.to_path_buf());
let icon = File::open(path!("assets" / "icon.png")).unwrap();
icons.insert_icon(icon, "talon").unwrap();
icons.delete_icon("talon").unwrap();
let stored_path = path!(temp / "talon.png");
assert!(!stored_path.exists());
assert!(matches!(
icons.get_icon("talon"),
Err(talon::icons::ImagesError::NotFound(_))
));
}
}
mod config {
use talon::config::Config;
use super::*;
#[rstest]
#[case::default("default", "config.toml")]
#[case::sparse("sparse", "config_sparse.toml")]
fn parse_config(#[case] name: &str, #[case] fname: &str) {
let p = path!("tests" / "testfiles" / "config" / fname);
let cfg = Config::from_file(p).unwrap();
insta::assert_ron_snapshot!(name, &cfg);
}
}
mod page {
use hex::ToHex;
use poem::{
http::{header, StatusCode},
test::TestClient,
};
use super::*;
#[rstest]
#[case::index("", "/", &HASH_1_2_INDEX, "text/html")]
#[case::style("", "/assets/style.css", &HASH_1_2_STYLE, "text/css")]
#[case::rustypipe("rustypipe", "/", &HASH_3_1_INDEX, "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_fallback("spa", "/user/2", &HASH_SPA_FALLBACK, "text/html")]
#[case::version("x--v1", "/", &HASH_1_1_INDEX, "text/html")]
fn page(
tln: TalonTest,
#[case] subdomain: &str,
#[case] path: &str,
#[case] hash: &[u8],
#[case] mime: &str,
) {
let host = if subdomain.is_empty() {
"localhost:3000".to_owned()
} else {
format!("{subdomain}.localhost:3000")
};
let resp = tokio_test::block_on(
TestClient::new(tln.endpoint())
.get(format!("http://{host}{path}"))
.header(header::HOST, host)
.data(tln.clone())
.send(),
);
resp.assert_status_is_ok();
resp.assert_content_type(mime);
resp.assert_header(
header::ETAG,
if mime == "text/html" {
format!(
"\"{}_{}\"",
hash.encode_hex::<String>(),
talon::build::PKG_VERSION
)
} else {
format!("\"{}\"", hash.encode_hex::<String>())
},
);
}
#[rstest]
#[case::spa(Some("fallback.html".to_owned()), true, true, &HASH_1_2_INDEX)]
#[case::index_fb(None, true, true, &HASH_1_1_INDEX)]
#[case::err(Some("fallback.html".to_owned()), false, false, &HASH_1_2_INDEX)]
fn page_fallback(
tln: TalonTest,
#[case] fallback: Option<String>,
#[case] spa: bool,
#[case] ok: bool,
#[case] hash: &[u8],
) {
const SUBDOMAIN: &str = "fallback";
tln.db
.insert_website(
SUBDOMAIN,
&talon::db::model::Website {
latest_version: Some(1),
..Default::default()
},
)
.unwrap();
assert_eq!(
tln.db
.insert_version(
SUBDOMAIN,
&talon::db::model::Version {
fallback,
spa,
..Default::default()
},
)
.unwrap(),
1
);
tln.storage
.insert_file(
path!("tests" / "testfiles" / "ThetaDev0" / "index.html"),
SUBDOMAIN,
1,
"index.html",
)
.unwrap();
tln.storage
.insert_file(
path!("tests" / "testfiles" / "ThetaDev1" / "index.html"),
SUBDOMAIN,
1,
"fallback.html",
)
.unwrap();
let resp = tokio_test::block_on(
TestClient::new(tln.endpoint())
.get("http://fallback.localhost:3000/test")
.header(header::HOST, "fallback.localhost:3000")
.data(tln.clone())
.send(),
);
resp.assert_status(if ok {
StatusCode::OK
} else {
StatusCode::NOT_FOUND
});
resp.assert_content_type("text/html");
if ok {
resp.assert_header(
header::ETAG,
format!(
"\"{}_{}\"",
hash.encode_hex::<String>(),
talon::build::PKG_VERSION
),
)
}
}
}
mod api {
use std::io::{Cursor, Read};
use hex::ToHex;
use hex_literal::hex;
use image::io::Reader as ImageReader;
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("#1db954".to_string()),
visibility: Visibility::Featured,
source_url: Some("https://github.com/Theta-Dev/Spotify-Gender-Ex".to_owned()),
source_icon: Some(SourceIcon::Github),
has_icon: false,
}));
}
#[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("#0003e8".to_owned()),
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]", ".updated_at" => "[date]"}, @r###"
Website(
name: "Test",
created_at: "[date]",
updated_at: "[date]",
latest_version: None,
color: Some(1000),
visibility: searchable,
source_url: Some("example.com"),
source_icon: Some(git),
vid_count: 0,
has_icon: false,
)
"###);
}
#[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("#0003e8".to_owned()),
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("#0003e8".to_owned())),
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, {".updated_at" => "[date]"}, @r###"
Website(
name: "Test",
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
updated_at: "[date]",
latest_version: Some(2),
color: Some(1000),
visibility: searchable,
source_url: Some("example.com"),
source_icon: Some(git),
vid_count: 2,
has_icon: false,
)
"###);
}
#[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("#0003e8".to_owned())),
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_get_icon(tln: TalonTest) {
let icon = File::open(path!("assets" / "icon.png")).unwrap();
tln.icons.insert_icon(icon, "-").unwrap();
let mut resp = tokio_test::block_on(
TestClient::new(tln.endpoint())
.get("http://talon.localhost:3000/icons/-")
.header(header::HOST, "talon.localhost:3000")
.data(tln.clone())
.send(),
);
resp.assert_status_is_ok();
resp.assert_header(header::CONTENT_TYPE, "image/png");
let icon_bts = tokio_test::block_on(resp.0.take_body().into_bytes()).unwrap();
let got_icon = ImageReader::new(BufReader::new(Cursor::new(icon_bts)))
.with_guessed_format()
.unwrap()
.decode()
.unwrap();
assert_eq!(got_icon.height(), ICON_SIZE);
assert_eq!(got_icon.width(), ICON_SIZE);
}
#[rstest]
fn website_get_icon_404(tln: TalonTest) {
let resp = tokio_test::block_on(
TestClient::new(tln.endpoint())
.get("http://talon.localhost:3000/icons/-")
.header(header::HOST, "talon.localhost:3000")
.data(tln.clone())
.send(),
);
resp.assert_status(StatusCode::NOT_FOUND);
}
#[rstest]
fn website_upload_icon(tln: TalonTest) {
let mut icon = File::open(path!("assets" / "icon.png")).unwrap();
let mut icon_bts = Vec::new();
icon.read_to_end(&mut icon_bts).unwrap();
let resp = tokio_test::block_on(
TestClient::new(tln.endpoint())
.put("http://talon.localhost:3000/api/website/-/icon")
.header(header::HOST, "talon.localhost:3000")
.header(header::CONTENT_TYPE, "application/octet-stream")
.header("x-api-key", API_KEY_ROOT)
.data(tln.clone())
.body(icon_bts)
.send(),
);
resp.assert_status_is_ok();
let resp = tokio_test::block_on(
TestClient::new(tln.endpoint())
.get("http://talon.localhost:3000/api/website/-")
.header(header::HOST, "talon.localhost:3000")
.data(tln.clone())
.send(),
);
resp.assert_status_is_ok();
tokio_test::block_on(resp.assert_json(Website {
subdomain: "-".to_string(),
name: "ThetaDev".to_owned(),
created_at: datetime!(2023-02-18 16:30 +0),
latest_version: Some(2),
color: Some("#1f91ee".to_owned()),
visibility: talon::model::Visibility::Featured,
source_icon: None,
source_url: None,
has_icon: true,
}));
}
#[rstest]
fn website_delete_icon(tln: TalonTest) {
let icon = File::open(path!("assets" / "icon.png")).unwrap();
tln.icons.insert_icon(icon, "-").unwrap();
let resp = tokio_test::block_on(
TestClient::new(tln.endpoint())
.delete("http://talon.localhost:3000/api/website/-/icon")
.header(header::HOST, "talon.localhost:3000")
.header("x-api-key", API_KEY_ROOT)
.data(tln.clone())
.send(),
);
resp.assert_status_is_ok();
}
#[rstest]
fn website_delete_icon_404(tln: TalonTest) {
let resp = tokio_test::block_on(
TestClient::new(tln.endpoint())
.delete("http://talon.localhost:3000/api/website/-/icon")
.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 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);
// Check that no zombie version remains
assert_eq!(tln.db.get_website_versions("rustypipe").count(), 1);
assert_eq!(tln.db.get_website("rustypipe").unwrap().vid_count, 1);
}
#[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);
// Check that no zombie version remains
assert_eq!(tln.db.get_website_versions("rustypipe").count(), 1);
assert_eq!(tln.db.get_website("rustypipe").unwrap().vid_count, 1);
}
#[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)]
#[case::icon_upload("website/test/icon", Method::PUT)]
#[case::icon_delete("website/test/icon", Method::DELETE)]
fn unauthorized(tln: TalonTest, #[case] endpoint: &str, #[case] method: Method) {
let resp = tokio_test::block_on(
TestClient::new(tln.endpoint())
.request(
method.clone(),
format!("http://talon.localhost:3000/api/{endpoint}"),
)
.header(header::HOST, "talon.localhost:3000")
.data(tln.clone())
.send(),
);
resp.assert_status(StatusCode::UNAUTHORIZED);
let resp = match method {
Method::POST => tokio_test::block_on(
TestClient::new(tln.endpoint())
.request(
method.clone(),
format!("http://talon.localhost:3000/api/{endpoint}"),
)
.header(header::HOST, "talon.localhost:3000")
.header(header::CONTENT_TYPE, "application/octet-stream")
.header("x-api-key", API_KEY_RO)
.data(tln.clone())
.send(),
),
Method::GET | Method::PUT | Method::PATCH => return,
_ => tokio_test::block_on(
TestClient::new(tln.endpoint())
.request(
method.clone(),
format!("http://talon.localhost:3000/api/{endpoint}"),
)
.header(header::HOST, "talon.localhost:3000")
.header("x-api-key", API_KEY_RO)
.data(tln.clone())
.body_json(&serde_json::Value::Null)
.send(),
),
};
resp.assert_status(StatusCode::FORBIDDEN);
}
}