mirror of
https://github.com/agersant/polaris
synced 2024-11-10 10:14:12 +00:00
integration test sharing between backends
This commit is contained in:
parent
a83e1af69b
commit
61c221a2d2
17 changed files with 221 additions and 281 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2071,6 +2071,7 @@ dependencies = [
|
|||
"anyhow 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ape 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"app_dirs 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"async-trait 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"diesel 1.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"diesel_migrations 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
|
|
@ -8,17 +8,18 @@ edition = "2018"
|
|||
default = ["service-actix"]
|
||||
ui = []
|
||||
profile-index = ["flame", "flamer"]
|
||||
service-actix = ["actix-files", "actix-http", "actix-rt", "actix-web"]
|
||||
service-actix = ["actix-files", "actix-http", "actix-web"]
|
||||
service-rocket = ["rocket", "rocket_contrib"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.2", optional = true }
|
||||
actix-http = { version = "1.0", optional = true }
|
||||
actix-web = { version = "2.0", optional = true }
|
||||
actix-rt = { version = "1.0", optional = true }
|
||||
actix-rt = { version = "1.0"}
|
||||
anyhow = "1.0"
|
||||
ape = "0.2.0"
|
||||
app_dirs = "1.1.1"
|
||||
async-trait = "0.1.22"
|
||||
base64 = "0.11.0"
|
||||
diesel = { version = "1.4", features = ["sqlite", "r2d2"] }
|
||||
diesel_migrations = { version = "1.4", features = ["sqlite"] }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#![recursion_limit = "256"]
|
||||
#![feature(proc_macro_hygiene, decl_macro)]
|
||||
#![feature(proc_macro_hygiene, decl_macro, type_alias_impl_trait)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
|
|
|
@ -2,7 +2,7 @@ use actix_http::ResponseBuilder;
|
|||
use actix_web::{error, get, http::header, http::StatusCode, put, web, HttpResponse};
|
||||
use anyhow::*;
|
||||
|
||||
use crate::config::{self, Config, Preferences};
|
||||
use crate::config::{self, Config};
|
||||
use crate::db::DB;
|
||||
use crate::service::constants::*;
|
||||
use crate::service::dto;
|
||||
|
@ -46,7 +46,7 @@ pub async fn get_initial_setup(db: web::Data<DB>) -> Result<HttpResponse, APIErr
|
|||
#[put("/settings")]
|
||||
pub async fn put_settings(
|
||||
db: web::Data<DB>,
|
||||
// _admin_rights: AdminRights, // TODO
|
||||
// _admin_rights: AdminRights, // TODO.important
|
||||
config: web::Json<Config>,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
web::block(move || config::amend(&db, &config))
|
||||
|
|
|
@ -8,7 +8,7 @@ pub mod server;
|
|||
|
||||
mod api;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod test;
|
||||
|
||||
fn configure_app(
|
||||
cfg: &mut web::ServiceConfig,
|
||||
|
|
84
src/service/actix/test.rs
Normal file
84
src/service/actix/test.rs
Normal file
|
@ -0,0 +1,84 @@
|
|||
use actix_http::{Error, Request};
|
||||
use actix_web::dev::{Body, ResponseBody, Service, ServiceResponse};
|
||||
use actix_web::test::TestRequest;
|
||||
use actix_web::{test, App};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::db::DB;
|
||||
|
||||
fn configure_test_app(cfg: &mut actix_web::web::ServiceConfig, db_name: &str) {
|
||||
let web_url = "/";
|
||||
let web_dir_path = PathBuf::from("web");
|
||||
|
||||
let swagger_url = "swagger";
|
||||
let mut swagger_dir_path = PathBuf::from("docs");
|
||||
swagger_dir_path.push("swagger");
|
||||
|
||||
let mut db_path = PathBuf::new();
|
||||
db_path.push("test");
|
||||
db_path.push(format!("{}.sqlite", db_name));
|
||||
if db_path.exists() {
|
||||
fs::remove_file(&db_path).unwrap();
|
||||
}
|
||||
let db = DB::new(&db_path).unwrap();
|
||||
|
||||
super::configure_app(
|
||||
cfg,
|
||||
web_url,
|
||||
web_dir_path.as_path(),
|
||||
swagger_url,
|
||||
swagger_dir_path.as_path(),
|
||||
&db,
|
||||
);
|
||||
}
|
||||
|
||||
pub type ServiceType = impl Service<Request = Request, Response = ServiceResponse, Error = Error>;
|
||||
|
||||
pub async fn make_service(test_name: &str) -> ServiceType {
|
||||
let app = App::new().configure(|cfg| configure_test_app(cfg, test_name));
|
||||
let service = test::init_service(app).await;
|
||||
service
|
||||
}
|
||||
|
||||
pub async fn get(service: &mut ServiceType, url: &str) {
|
||||
let req = TestRequest::get().uri(url).to_request();
|
||||
let resp = service.call(req).await.unwrap();
|
||||
assert!(resp.status().is_success());
|
||||
}
|
||||
|
||||
pub async fn get_json<T: DeserializeOwned>(service: &mut ServiceType, url: &str) -> T {
|
||||
let req = TestRequest::get().uri(url).to_request();
|
||||
let resp = service.call(req).await.unwrap();
|
||||
assert!(resp.status().is_success());
|
||||
let body = resp.response().body().as_u8();
|
||||
let response_json: T = serde_json::from_slice(body).unwrap();
|
||||
response_json
|
||||
}
|
||||
|
||||
pub async fn put_json<T: Serialize>(service: &mut ServiceType, url: &str, payload: &T) {
|
||||
let req = TestRequest::put().uri(url).set_json(payload).to_request();
|
||||
let resp = service.call(req).await.unwrap();
|
||||
assert!(resp.status().is_success());
|
||||
}
|
||||
|
||||
trait BodyToBytes {
|
||||
fn as_u8(&self) -> &[u8];
|
||||
}
|
||||
|
||||
impl BodyToBytes for ResponseBody<Body> {
|
||||
fn as_u8(&self) -> &[u8] {
|
||||
match self {
|
||||
ResponseBody::Body(ref b) => match b {
|
||||
Body::Bytes(ref by) => by.as_ref(),
|
||||
_ => panic!(),
|
||||
},
|
||||
ResponseBody::Other(ref b) => match b {
|
||||
Body::Bytes(ref by) => by.as_ref(),
|
||||
_ => panic!(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
use actix_http::Request;
|
||||
use actix_web::dev::*;
|
||||
use actix_web::test::TestRequest;
|
||||
use actix_web::{test, App};
|
||||
use function_name::named;
|
||||
|
||||
use super::configure_test_app;
|
||||
use crate::config;
|
||||
use crate::service::dto;
|
||||
use crate::vfs;
|
||||
|
||||
const TEST_USERNAME: &str = "test_user";
|
||||
const TEST_PASSWORD: &str = "test_password";
|
||||
const TEST_MOUNT_NAME: &str = "collection";
|
||||
const TEST_MOUNT_SOURCE: &str = "test/collection";
|
||||
|
||||
trait BodyTest {
|
||||
fn as_u8(&self) -> &[u8];
|
||||
}
|
||||
|
||||
impl BodyTest for ResponseBody<Body> {
|
||||
fn as_u8(&self) -> &[u8] {
|
||||
match self {
|
||||
ResponseBody::Body(ref b) => match b {
|
||||
Body::Bytes(ref by) => by.as_ref(),
|
||||
_ => panic!(),
|
||||
},
|
||||
ResponseBody::Other(ref b) => match b {
|
||||
Body::Bytes(ref by) => by.as_ref(),
|
||||
_ => panic!(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_setup() -> Request {
|
||||
let configuration = config::Config {
|
||||
album_art_pattern: None,
|
||||
prefix_url: None,
|
||||
reindex_every_n_seconds: None,
|
||||
ydns: None,
|
||||
users: Some(vec![config::ConfigUser {
|
||||
name: TEST_USERNAME.into(),
|
||||
password: TEST_PASSWORD.into(),
|
||||
admin: true,
|
||||
}]),
|
||||
mount_dirs: Some(vec![vfs::MountPoint {
|
||||
name: TEST_MOUNT_NAME.into(),
|
||||
source: TEST_MOUNT_SOURCE.into(),
|
||||
}]),
|
||||
};
|
||||
|
||||
TestRequest::put()
|
||||
.uri("/api/settings")
|
||||
.set_json(&configuration)
|
||||
.to_request()
|
||||
}
|
||||
|
||||
#[named]
|
||||
#[actix_rt::test]
|
||||
async fn test_version() {
|
||||
let app = App::new().configure(|cfg| configure_test_app(cfg, function_name!()));
|
||||
let mut service = test::init_service(app).await;
|
||||
let req = TestRequest::get().uri("/api/version").to_request();
|
||||
let resp = service.call(req).await.unwrap();
|
||||
assert!(resp.status().is_success());
|
||||
|
||||
let body = resp.response().body().as_u8();
|
||||
let response_json: dto::Version = serde_json::from_slice(body).unwrap();
|
||||
assert_eq!(response_json, dto::Version { major: 4, minor: 0 });
|
||||
}
|
||||
|
||||
#[named]
|
||||
#[actix_rt::test]
|
||||
async fn test_initial_setup() {
|
||||
let app = App::new().configure(|cfg| configure_test_app(cfg, function_name!()));
|
||||
let mut service = test::init_service(app).await;
|
||||
|
||||
{
|
||||
let req = TestRequest::get().uri("/api/initial_setup").to_request();
|
||||
let resp = service.call(req).await.unwrap();
|
||||
assert!(resp.status().is_success());
|
||||
|
||||
let body = resp.response().body().as_u8();
|
||||
let response_json: dto::InitialSetup = serde_json::from_slice(body).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
response_json,
|
||||
dto::InitialSetup {
|
||||
has_any_users: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
assert!(service
|
||||
.call(initial_setup())
|
||||
.await
|
||||
.unwrap()
|
||||
.status()
|
||||
.is_success());
|
||||
|
||||
{
|
||||
let req = TestRequest::get().uri("/api/initial_setup").to_request();
|
||||
let resp = service.call(req).await.unwrap();
|
||||
assert!(resp.status().is_success());
|
||||
|
||||
let body = resp.response().body().as_u8();
|
||||
let response_json: dto::InitialSetup = serde_json::from_slice(body).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
response_json,
|
||||
dto::InitialSetup {
|
||||
has_any_users: true
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::db::DB;
|
||||
|
||||
mod api;
|
||||
mod swagger;
|
||||
mod web;
|
||||
|
||||
fn configure_test_app(cfg: &mut actix_web::web::ServiceConfig, db_name: &str) {
|
||||
let web_url = "/";
|
||||
let web_dir_path = PathBuf::from("web");
|
||||
|
||||
let swagger_url = "swagger";
|
||||
let mut swagger_dir_path = PathBuf::from("docs");
|
||||
swagger_dir_path.push("swagger");
|
||||
|
||||
let mut db_path = PathBuf::new();
|
||||
db_path.push("test");
|
||||
db_path.push(format!("{}.sqlite", db_name));
|
||||
if db_path.exists() {
|
||||
fs::remove_file(&db_path).unwrap();
|
||||
}
|
||||
let db = DB::new(&db_path).unwrap();
|
||||
|
||||
super::configure_app(
|
||||
cfg,
|
||||
web_url,
|
||||
web_dir_path.as_path(),
|
||||
swagger_url,
|
||||
swagger_dir_path.as_path(),
|
||||
&db,
|
||||
);
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
use actix_web::dev::Service;
|
||||
use actix_web::test::TestRequest;
|
||||
use actix_web::{test, App};
|
||||
use function_name::named;
|
||||
|
||||
use super::configure_test_app;
|
||||
|
||||
#[named]
|
||||
#[actix_rt::test]
|
||||
async fn test_swagger_index() {
|
||||
let app = App::new().configure(|cfg| configure_test_app(cfg, function_name!()));
|
||||
let mut service = test::init_service(app).await;
|
||||
let req = TestRequest::get().uri("/swagger").to_request();
|
||||
let resp = service.call(req).await.unwrap();
|
||||
assert!(resp.status().is_success());
|
||||
}
|
||||
|
||||
#[named]
|
||||
#[actix_rt::test]
|
||||
async fn test_swagger_index_with_trailing_slash() {
|
||||
let app = App::new().configure(|cfg| configure_test_app(cfg, function_name!()));
|
||||
let mut service = test::init_service(app).await;
|
||||
let req = TestRequest::get().uri("/swagger/").to_request();
|
||||
let resp = service.call(req).await.unwrap();
|
||||
assert!(resp.status().is_success());
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
use actix_web::dev::Service;
|
||||
use actix_web::test::TestRequest;
|
||||
use actix_web::{test, App};
|
||||
use function_name::named;
|
||||
|
||||
use super::configure_test_app;
|
||||
|
||||
#[named]
|
||||
#[actix_rt::test]
|
||||
async fn test_index() {
|
||||
let app = App::new().configure(|cfg| configure_test_app(cfg, function_name!()));
|
||||
let mut service = test::init_service(app).await;
|
||||
let req = TestRequest::get().uri("/").to_request();
|
||||
let resp = service.call(req).await.unwrap();
|
||||
assert!(resp.status().is_success());
|
||||
}
|
|
@ -2,6 +2,9 @@ mod constants;
|
|||
mod dto;
|
||||
mod error;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "service-actix")]
|
||||
mod actix;
|
||||
#[cfg(feature = "service-actix")]
|
||||
|
|
|
@ -8,7 +8,6 @@ use super::api;
|
|||
use crate::config;
|
||||
use crate::ddns;
|
||||
use crate::index;
|
||||
use crate::service::dto;
|
||||
use crate::vfs;
|
||||
|
||||
use super::test::get_test_environment;
|
||||
|
@ -49,53 +48,6 @@ fn do_auth(client: &Client) {
|
|||
assert_eq!(response.status(), Status::Ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version() {
|
||||
let env = get_test_environment("api_version.sqlite");
|
||||
let client = &env.client;
|
||||
let mut response = client.get("/api/version").dispatch();
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
let response_body = response.body_string().unwrap();
|
||||
let response_json: dto::Version = serde_json::from_str(&response_body).unwrap();
|
||||
assert_eq!(response_json, dto::Version { major: 4, minor: 0 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_setup() {
|
||||
let env = get_test_environment("api_initial_setup.sqlite");
|
||||
let client = &env.client;
|
||||
|
||||
{
|
||||
let mut response = client.get("/api/initial_setup").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let response_body = response.body_string().unwrap();
|
||||
let response_json: dto::InitialSetup = serde_json::from_str(&response_body).unwrap();
|
||||
assert_eq!(
|
||||
response_json,
|
||||
dto::InitialSetup {
|
||||
has_any_users: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
complete_initial_setup(client);
|
||||
|
||||
{
|
||||
let mut response = client.get("/api/initial_setup").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let response_body = response.body_string().unwrap();
|
||||
let response_json: dto::InitialSetup = serde_json::from_str(&response_body).unwrap();
|
||||
assert_eq!(
|
||||
response_json,
|
||||
dto::InitialSetup {
|
||||
has_any_users: true
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings() {
|
||||
let env = get_test_environment("api_settings.sqlite");
|
||||
|
|
|
@ -6,8 +6,4 @@ pub mod server;
|
|||
#[cfg(test)]
|
||||
mod api_tests;
|
||||
#[cfg(test)]
|
||||
mod swagger;
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
#[cfg(test)]
|
||||
mod web;
|
||||
pub mod test;
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
use super::test::get_test_environment;
|
||||
|
||||
#[test]
|
||||
fn test_index() {
|
||||
use rocket::http::Status;
|
||||
let env = get_test_environment("swagger_index.sqlite");
|
||||
let client = &env.client;
|
||||
let response = client.get("/swagger").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_index_with_trailing_slash() {
|
||||
use rocket::http::Status;
|
||||
let env = get_test_environment("swagger_index_with_trailing_slash.sqlite");
|
||||
let client = &env.client;
|
||||
let response = client.get("/swagger/").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
use rocket;
|
||||
use rocket::http::Status;
|
||||
use rocket::local::Client;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::fs;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
|
@ -60,3 +63,30 @@ pub fn get_test_environment(db_name: &str) -> TestEnvironment {
|
|||
db,
|
||||
}
|
||||
}
|
||||
|
||||
pub type ServiceType = TestEnvironment;
|
||||
|
||||
pub async fn make_service(test_name: &str) -> TestEnvironment {
|
||||
get_test_environment(&format!("{}.sqlite", test_name))
|
||||
}
|
||||
|
||||
pub async fn get(service: &mut TestEnvironment, url: &str) {
|
||||
let client = &service.client;
|
||||
let response = client.get(url).dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
}
|
||||
|
||||
pub async fn get_json<T: DeserializeOwned>(service: &mut TestEnvironment, url: &str) -> T {
|
||||
let client = &service.client;
|
||||
let mut response = client.get(url).dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let response_body = response.body_string().unwrap();
|
||||
serde_json::from_str(&response_body).unwrap()
|
||||
}
|
||||
|
||||
pub async fn put_json<T: Serialize>(service: &mut TestEnvironment, url: &str, payload: &T) {
|
||||
let client = &service.client;
|
||||
let body = serde_json::to_string(payload).unwrap();
|
||||
let response = client.put(url).body(&body).dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
use super::test::get_test_environment;
|
||||
use rocket::http::Status;
|
||||
|
||||
#[test]
|
||||
fn test_index() {
|
||||
let env = get_test_environment("web_index.sqlite");
|
||||
let client = &env.client;
|
||||
let response = client.get("/").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
}
|
95
src/service/tests.rs
Normal file
95
src/service/tests.rs
Normal file
|
@ -0,0 +1,95 @@
|
|||
use function_name::named;
|
||||
|
||||
mod api;
|
||||
mod static_files;
|
||||
|
||||
use crate::config;
|
||||
use crate::service::dto;
|
||||
use crate::vfs;
|
||||
|
||||
#[cfg(feature = "service-actix")]
|
||||
pub use crate::service::actix::test::*;
|
||||
|
||||
#[cfg(feature = "service-rocket")]
|
||||
pub use crate::service::rocket::test::*;
|
||||
|
||||
const TEST_USERNAME: &str = "test_user";
|
||||
const TEST_PASSWORD: &str = "test_password";
|
||||
const TEST_MOUNT_NAME: &str = "collection";
|
||||
const TEST_MOUNT_SOURCE: &str = "test/collection";
|
||||
|
||||
#[named]
|
||||
#[actix_rt::test]
|
||||
async fn test_index() {
|
||||
let mut service = make_service(function_name!()).await;
|
||||
get(&mut service, "/").await;
|
||||
}
|
||||
|
||||
#[named]
|
||||
#[actix_rt::test]
|
||||
async fn test_swagger_index() {
|
||||
let mut service = make_service(function_name!()).await;
|
||||
get(&mut service, "/swagger").await;
|
||||
}
|
||||
|
||||
#[named]
|
||||
#[actix_rt::test]
|
||||
async fn test_swagger_index_with_trailing_slash() {
|
||||
let mut service = make_service(function_name!()).await;
|
||||
get(&mut service, "/swagger/").await;
|
||||
}
|
||||
|
||||
async fn complete_initial_setup(service: &mut ServiceType) {
|
||||
let configuration = config::Config {
|
||||
album_art_pattern: None,
|
||||
prefix_url: None,
|
||||
reindex_every_n_seconds: None,
|
||||
ydns: None,
|
||||
users: Some(vec![config::ConfigUser {
|
||||
name: TEST_USERNAME.into(),
|
||||
password: TEST_PASSWORD.into(),
|
||||
admin: true,
|
||||
}]),
|
||||
mount_dirs: Some(vec![vfs::MountPoint {
|
||||
name: TEST_MOUNT_NAME.into(),
|
||||
source: TEST_MOUNT_SOURCE.into(),
|
||||
}]),
|
||||
};
|
||||
put_json(service, "/api/settings", &configuration).await;
|
||||
}
|
||||
|
||||
#[named]
|
||||
#[actix_rt::test]
|
||||
async fn test_version() {
|
||||
let mut service = make_service(function_name!()).await;
|
||||
let version: dto::Version = get_json(&mut service, "/api/version").await;
|
||||
assert_eq!(version, dto::Version { major: 4, minor: 0 });
|
||||
}
|
||||
|
||||
#[named]
|
||||
#[actix_rt::test]
|
||||
async fn test_initial_setup() {
|
||||
let mut service = make_service(function_name!()).await;
|
||||
|
||||
{
|
||||
let initial_setup: dto::InitialSetup = get_json(&mut service, "/api/initial_setup").await;
|
||||
assert_eq!(
|
||||
initial_setup,
|
||||
dto::InitialSetup {
|
||||
has_any_users: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
complete_initial_setup(&mut service).await;
|
||||
|
||||
{
|
||||
let initial_setup: dto::InitialSetup = get_json(&mut service, "/api/initial_setup").await;
|
||||
assert_eq!(
|
||||
initial_setup,
|
||||
dto::InitialSetup {
|
||||
has_any_users: true
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue