ThetaDev
c94915e351
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
feat: add last-version tag to PageInfoModal fix: set icon size to 48px
1433 lines
47 KiB
Rust
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]",
|
|
latest_version: None,
|
|
color: Some(1000),
|
|
visibility: searchable,
|
|
source_url: Some("example.com"),
|
|
source_icon: Some(git),
|
|
vid_count: 0,
|
|
has_icon: false,
|
|
updated_at: "[date]",
|
|
)
|
|
"###);
|
|
}
|
|
|
|
#[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),
|
|
latest_version: Some(2),
|
|
color: Some(1000),
|
|
visibility: searchable,
|
|
source_url: Some("example.com"),
|
|
source_icon: Some(git),
|
|
vid_count: 2,
|
|
has_icon: false,
|
|
updated_at: "[date]",
|
|
)
|
|
"###);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
}
|