use std::future::poll_fn;
use std::panic::AssertUnwindSafe;
use std::path::PathBuf;
use std::sync::Arc;
use async_trait::async_trait;
use axum::handler::HandlerWithoutStateExt;
use derive_more::with_trait::Debug;
use futures_util::FutureExt;
use http::request::Parts;
use tower::{Layer, Service};
use tracing::{error, info, trace};
use crate::admin::AdminModelManager;
#[cfg(feature = "db")]
use crate::auth::db::DatabaseUserBackend;
use crate::auth::{AuthBackend, NoAuthBackend};
use crate::cli::Cli;
#[cfg(feature = "db")]
use crate::config::DatabaseConfig;
use crate::config::{AuthBackendConfig, ProjectConfig};
#[cfg(feature = "db")]
use crate::db::Database;
#[cfg(feature = "db")]
use crate::db::migrations::{MigrationEngine, SyncDynMigration};
use crate::error::ErrorRepr;
use crate::error_page::{Diagnostics, ErrorPageTrigger};
use crate::handler::BoxedHandler;
use crate::html::Html;
use crate::middleware::{IntoCotError, IntoCotErrorLayer, IntoCotResponse, IntoCotResponseLayer};
use crate::request::{AppName, Request, RequestExt};
use crate::response::{IntoResponse, Response};
use crate::router::{Route, Router, RouterService};
use crate::static_files::StaticFile;
use crate::{Body, Error, StatusCode, cli, error_page};
#[async_trait]
pub trait App: Send + Sync {
fn name(&self) -> &str;
#[expect(unused_variables)]
async fn init(&self, context: &mut ProjectContext) -> crate::Result<()> {
Ok(())
}
fn router(&self) -> Router {
Router::empty()
}
#[cfg(feature = "db")]
fn migrations(&self) -> Vec<Box<SyncDynMigration>> {
vec![]
}
fn admin_model_managers(&self) -> Vec<Box<dyn AdminModelManager>> {
vec![]
}
fn static_files(&self) -> Vec<StaticFile> {
vec![]
}
}
pub trait Project {
fn cli_metadata(&self) -> cli::CliMetadata {
cli::metadata!()
}
fn config(&self, config_name: &str) -> crate::Result<ProjectConfig> {
read_config(config_name)
}
#[expect(unused_variables)]
fn register_tasks(&self, cli: &mut Cli) {}
#[expect(unused_variables)]
fn register_apps(&self, apps: &mut AppBuilder, context: &RegisterAppsContext) {}
fn auth_backend(&self, context: &AuthBackendContext) -> Arc<dyn AuthBackend> {
#[expect(trivial_casts)] match &context.config().auth_backend {
AuthBackendConfig::None => Arc::new(NoAuthBackend) as Arc<dyn AuthBackend>,
#[cfg(feature = "db")]
AuthBackendConfig::Database => Arc::new(DatabaseUserBackend::new(
context
.try_database()
.expect(
"Database missing when constructing database auth backend. \
Make sure the database config is set up correctly or disable \
authentication in the config.",
)
.clone(),
)) as Arc<dyn AuthBackend>,
}
}
#[expect(unused_variables)]
fn middlewares(
&self,
handler: RootHandlerBuilder,
context: &MiddlewareContext,
) -> BoxedHandler {
handler.build()
}
fn server_error_handler(&self) -> Box<dyn ErrorPageHandler> {
Box::new(DefaultServerErrorHandler)
}
fn not_found_handler(&self) -> Box<dyn ErrorPageHandler> {
Box::new(DefaultNotFoundHandler)
}
}
pub type RegisterAppsContext = ProjectContext<WithConfig>;
pub type AuthBackendContext = ProjectContext<WithDatabase>;
pub type MiddlewareContext = ProjectContext<WithDatabase>;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RootHandlerBuilder<S = RouterService> {
handler: S,
}
impl<S> RootHandlerBuilder<S>
where
S: Service<Request, Response = Response, Error = Error> + Send + Sync + Clone + 'static,
S::Future: Send,
{
#[must_use]
pub fn middleware<M>(
self,
middleware: M,
) -> RootHandlerBuilder<IntoCotError<IntoCotResponse<<M as Layer<S>>::Service>>>
where
M: Layer<S>,
{
let layer = (
IntoCotErrorLayer::new(),
IntoCotResponseLayer::new(),
middleware,
);
RootHandlerBuilder {
handler: layer.layer(self.handler),
}
}
pub fn build(self) -> BoxedHandler {
BoxedHandler::new(self.handler)
}
}
#[derive(Debug)]
pub struct AppBuilder {
#[debug("..")]
apps: Vec<Box<dyn App>>,
urls: Vec<Route>,
}
impl AppBuilder {
fn new() -> Self {
Self {
apps: Vec::new(),
urls: Vec::new(),
}
}
pub fn register<T: App + 'static>(&mut self, module: T) {
self.apps.push(Box::new(module));
}
pub fn register_with_views<T: App + 'static>(&mut self, app: T, url_prefix: &str) {
let mut router = app.router();
router.set_app_name(AppName(app.name().to_owned()));
self.urls.push(Route::with_router(url_prefix, router));
self.register(app);
}
}
pub trait ErrorPageHandler: Send + Sync {
fn handle(&self) -> crate::Result<Response>;
}
struct DefaultNotFoundHandler;
impl ErrorPageHandler for DefaultNotFoundHandler {
fn handle(&self) -> crate::Result<Response> {
Html::new(include_str!("../templates/404.html"))
.with_status(StatusCode::NOT_FOUND)
.into_response()
}
}
struct DefaultServerErrorHandler;
impl ErrorPageHandler for DefaultServerErrorHandler {
fn handle(&self) -> crate::Result<Response> {
Html::new(include_str!("../templates/500.html"))
.with_status(StatusCode::INTERNAL_SERVER_ERROR)
.into_response()
}
}
#[derive(Debug)]
pub struct Bootstrapper<S: BootstrapPhase = Initialized> {
#[debug("..")]
project: Box<dyn Project>,
context: ProjectContext<S>,
handler: S::RequestHandler,
}
impl Bootstrapper<Uninitialized> {
#[must_use]
pub fn new<P: Project + 'static>(project: P) -> Self {
Self {
project: Box::new(project),
context: ProjectContext::new(),
handler: (),
}
}
}
impl<S: BootstrapPhase> Bootstrapper<S> {
pub fn project(&self) -> &dyn Project {
self.project.as_ref()
}
#[must_use]
pub fn context(&self) -> &ProjectContext<S> {
&self.context
}
}
impl Bootstrapper<Uninitialized> {
#[expect(clippy::future_not_send)] async fn run_cli(self) -> cot::Result<()> {
let mut cli = Cli::new();
cli.set_metadata(self.project.cli_metadata());
self.project.register_tasks(&mut cli);
let common_options = cli.common_options();
let self_with_context = self.with_config_name(common_options.config())?;
cli.execute(self_with_context).await
}
pub fn with_config_name(self, config_name: &str) -> cot::Result<Bootstrapper<WithConfig>> {
let config = self.project.config(config_name)?;
Ok(self.with_config(config))
}
#[must_use]
pub fn with_config(self, config: ProjectConfig) -> Bootstrapper<WithConfig> {
Bootstrapper {
project: self.project,
context: self.context.with_config(config),
handler: self.handler,
}
}
}
fn read_config(config: &str) -> cot::Result<ProjectConfig> {
trace!(config, "Reading project configuration");
let result = match std::fs::read_to_string(config) {
Ok(config_content) => Ok(config_content),
Err(_err) => {
let path = PathBuf::from("config").join(config).with_extension("toml");
trace!(
config,
path = %path.display(),
"Failed to read config as a file; trying to read from the `config` directory"
);
std::fs::read_to_string(&path)
}
};
let config_content = result.map_err(|err| {
Error::new(ErrorRepr::LoadConfig {
config: config.to_owned(),
source: err,
})
})?;
ProjectConfig::from_toml(&config_content)
}
impl Bootstrapper<WithConfig> {
#[expect(clippy::future_not_send)]
pub async fn boot(self) -> cot::Result<Bootstrapper<Initialized>> {
self.with_apps().boot().await
}
#[must_use]
pub fn with_apps(self) -> Bootstrapper<WithApps> {
let mut module_builder = AppBuilder::new();
self.project
.register_apps(&mut module_builder, &self.context);
let router = Arc::new(Router::with_urls(module_builder.urls));
let context = self.context.with_apps(module_builder.apps, router);
Bootstrapper {
project: self.project,
context,
handler: self.handler,
}
}
}
impl Bootstrapper<WithApps> {
#[expect(clippy::future_not_send)]
pub async fn boot(self) -> cot::Result<Bootstrapper<Initialized>> {
self.with_database().await?.boot().await
}
#[expect(clippy::future_not_send)]
pub async fn with_database(self) -> cot::Result<Bootstrapper<WithDatabase>> {
#[cfg(feature = "db")]
let database = Self::init_database(&self.context.config.database).await?;
let context = self.context.with_database(
#[cfg(feature = "db")]
database,
);
Ok(Bootstrapper {
project: self.project,
context,
handler: self.handler,
})
}
#[cfg(feature = "db")]
async fn init_database(config: &DatabaseConfig) -> cot::Result<Option<Arc<Database>>> {
match &config.url {
Some(url) => {
let database = Database::new(url.as_str()).await?;
Ok(Some(Arc::new(database)))
}
None => Ok(None),
}
}
}
impl Bootstrapper<WithDatabase> {
#[expect(clippy::unused_async, clippy::future_not_send)]
pub async fn boot(self) -> cot::Result<Bootstrapper<Initialized>> {
let router_service = RouterService::new(Arc::clone(&self.context.router));
let handler = RootHandlerBuilder {
handler: router_service,
};
let handler = self.project.middlewares(handler, &self.context);
let auth_backend = self.project.auth_backend(&self.context);
let context = self.context.with_auth(auth_backend);
Ok(Bootstrapper {
project: self.project,
context,
handler,
})
}
}
impl Bootstrapper<Initialized> {
#[must_use]
pub fn into_context_and_handler(self) -> (ProjectContext, BoxedHandler) {
(self.context, self.handler)
}
}
mod sealed {
pub trait Sealed {}
}
pub trait BootstrapPhase: sealed::Sealed {
type RequestHandler: Debug;
type Config: Debug;
type Apps;
type Router: Debug;
#[cfg(feature = "db")]
type Database: Debug;
type AuthBackend;
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Uninitialized {}
impl sealed::Sealed for Uninitialized {}
impl BootstrapPhase for Uninitialized {
type RequestHandler = ();
type Config = ();
type Apps = ();
type Router = ();
#[cfg(feature = "db")]
type Database = ();
type AuthBackend = ();
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum WithConfig {}
impl sealed::Sealed for WithConfig {}
impl BootstrapPhase for WithConfig {
type RequestHandler = ();
type Config = Arc<ProjectConfig>;
type Apps = ();
type Router = ();
#[cfg(feature = "db")]
type Database = ();
type AuthBackend = ();
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum WithApps {}
impl sealed::Sealed for WithApps {}
impl BootstrapPhase for WithApps {
type RequestHandler = ();
type Config = <WithConfig as BootstrapPhase>::Config;
type Apps = Vec<Box<dyn App>>;
type Router = Arc<Router>;
#[cfg(feature = "db")]
type Database = ();
type AuthBackend = ();
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum WithDatabase {}
impl sealed::Sealed for WithDatabase {}
impl BootstrapPhase for WithDatabase {
type RequestHandler = ();
type Config = <WithApps as BootstrapPhase>::Config;
type Apps = <WithApps as BootstrapPhase>::Apps;
type Router = <WithApps as BootstrapPhase>::Router;
#[cfg(feature = "db")]
type Database = Option<Arc<Database>>;
type AuthBackend = <WithApps as BootstrapPhase>::AuthBackend;
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Initialized {}
impl sealed::Sealed for Initialized {}
impl BootstrapPhase for Initialized {
type RequestHandler = BoxedHandler;
type Config = <WithDatabase as BootstrapPhase>::Config;
type Apps = <WithDatabase as BootstrapPhase>::Apps;
type Router = <WithDatabase as BootstrapPhase>::Router;
#[cfg(feature = "db")]
type Database = <WithDatabase as BootstrapPhase>::Database;
type AuthBackend = Arc<dyn AuthBackend>;
}
#[derive(Debug)]
pub struct ProjectContext<S: BootstrapPhase = Initialized> {
config: S::Config,
#[debug("..")]
apps: S::Apps,
router: S::Router,
#[cfg(feature = "db")]
database: S::Database,
#[debug("..")]
auth_backend: S::AuthBackend,
}
impl ProjectContext<Uninitialized> {
#[must_use]
pub(crate) const fn new() -> Self {
Self {
config: (),
apps: (),
router: (),
#[cfg(feature = "db")]
database: (),
auth_backend: (),
}
}
fn with_config(self, config: ProjectConfig) -> ProjectContext<WithConfig> {
ProjectContext {
config: Arc::new(config),
apps: self.apps,
router: self.router,
#[cfg(feature = "db")]
database: self.database,
auth_backend: self.auth_backend,
}
}
}
impl<S: BootstrapPhase<Config = Arc<ProjectConfig>>> ProjectContext<S> {
#[must_use]
pub fn config(&self) -> &ProjectConfig {
&self.config
}
}
impl ProjectContext<WithConfig> {
#[must_use]
fn with_apps(self, apps: Vec<Box<dyn App>>, router: Arc<Router>) -> ProjectContext<WithApps> {
ProjectContext {
config: self.config,
apps,
router,
#[cfg(feature = "db")]
database: self.database,
auth_backend: self.auth_backend,
}
}
}
impl<S: BootstrapPhase<Apps = Vec<Box<dyn App>>>> ProjectContext<S> {
#[must_use]
pub fn apps(&self) -> &[Box<dyn App>] {
&self.apps
}
}
impl ProjectContext<WithApps> {
#[must_use]
fn with_database(
self,
#[cfg(feature = "db")] database: Option<Arc<Database>>,
) -> ProjectContext<WithDatabase> {
ProjectContext {
config: self.config,
apps: self.apps,
router: self.router,
#[cfg(feature = "db")]
database,
auth_backend: self.auth_backend,
}
}
}
impl ProjectContext<WithDatabase> {
#[must_use]
fn with_auth(self, auth_backend: Arc<dyn AuthBackend>) -> ProjectContext<Initialized> {
ProjectContext {
config: self.config,
apps: self.apps,
router: self.router,
auth_backend,
#[cfg(feature = "db")]
database: self.database,
}
}
}
impl ProjectContext<Initialized> {
#[cfg(feature = "test")]
pub(crate) fn initialized(
config: <Initialized as BootstrapPhase>::Config,
apps: <Initialized as BootstrapPhase>::Apps,
router: <Initialized as BootstrapPhase>::Router,
auth_backend: <Initialized as BootstrapPhase>::AuthBackend,
#[cfg(feature = "db")] database: <Initialized as BootstrapPhase>::Database,
) -> Self {
Self {
config,
apps,
router,
#[cfg(feature = "db")]
database,
auth_backend,
}
}
}
impl<S: BootstrapPhase<Router = Arc<Router>>> ProjectContext<S> {
#[must_use]
pub fn router(&self) -> &Arc<Router> {
&self.router
}
}
impl<S: BootstrapPhase<AuthBackend = Arc<dyn AuthBackend>>> ProjectContext<S> {
#[must_use]
pub fn auth_backend(&self) -> &Arc<dyn AuthBackend> {
&self.auth_backend
}
}
#[cfg(feature = "db")]
impl<S: BootstrapPhase<Database = Option<Arc<Database>>>> ProjectContext<S> {
#[must_use]
#[cfg(feature = "db")]
pub fn try_database(&self) -> Option<&Arc<Database>> {
self.database.as_ref()
}
#[cfg(feature = "db")]
#[must_use]
#[track_caller]
pub fn database(&self) -> &Arc<Database> {
self.try_database().expect(
"Database missing. Did you forget to add the database when configuring CotProject?",
)
}
}
#[expect(clippy::future_not_send)]
pub async fn run(bootstrapper: Bootstrapper<Initialized>, address_str: &str) -> cot::Result<()> {
let listener = tokio::net::TcpListener::bind(address_str)
.await
.map_err(|e| ErrorRepr::StartServer { source: e })?;
run_at(bootstrapper, listener).await
}
#[expect(clippy::future_not_send)]
pub async fn run_at(
bootstrapper: Bootstrapper<Initialized>,
listener: tokio::net::TcpListener,
) -> cot::Result<()> {
run_at_with_shutdown(bootstrapper, listener, shutdown_signal()).await
}
#[expect(clippy::future_not_send)]
pub async fn run_at_with_shutdown(
bootstrapper: Bootstrapper<Initialized>,
listener: tokio::net::TcpListener,
shutdown_signal: impl Future<Output = ()> + Send + 'static,
) -> cot::Result<()> {
let not_found_handler: Arc<dyn ErrorPageHandler> =
bootstrapper.project().not_found_handler().into();
let server_error_handler: Arc<dyn ErrorPageHandler> =
bootstrapper.project().server_error_handler().into();
let (mut context, mut project_handler) = bootstrapper.into_context_and_handler();
#[cfg(feature = "db")]
if let Some(database) = &context.database {
let mut migrations: Vec<Box<SyncDynMigration>> = Vec::new();
for app in &context.apps {
migrations.extend(app.migrations());
}
let migration_engine = MigrationEngine::new(migrations)?;
migration_engine.run(database).await?;
}
let mut apps = std::mem::take(&mut context.apps);
for app in &mut apps {
info!("Initializing app: {}", app.name());
app.init(&mut context).await?;
}
context.apps = apps;
let context = Arc::new(context);
let is_debug = context.config().debug;
let register_panic_hook = context.config().register_panic_hook;
#[cfg(feature = "db")]
let context_cleanup = context.clone();
let handler = move |axum_request: axum::extract::Request| async move {
let request = request_axum_to_cot(axum_request, Arc::clone(&context));
let (request_parts, request) = request_parts_for_diagnostics(request);
let catch_unwind_response = AssertUnwindSafe(pass_to_axum(request, &mut project_handler))
.catch_unwind()
.await;
let response: Result<axum::response::Response, ErrorResponse> = match catch_unwind_response
{
Ok(response) => match response {
Ok(response) => match response.extensions().get::<ErrorPageTrigger>() {
Some(trigger) => Err(ErrorResponse::ErrorPageTrigger(trigger.clone())),
None => Ok(response),
},
Err(error) => Err(ErrorResponse::ErrorReturned(error)),
},
Err(error) => Err(ErrorResponse::Panic(error)),
};
match response {
Ok(response) => response,
Err(error_response) => {
if is_debug {
let diagnostics = Diagnostics::new(
context.config().clone(),
Arc::clone(&context.router),
request_parts,
);
build_cot_error_page(error_response, &diagnostics)
} else {
build_custom_error_page(
¬_found_handler,
&server_error_handler,
&error_response,
)
}
}
}
};
eprintln!(
"Starting the server at http://{}",
listener
.local_addr()
.map_err(|e| ErrorRepr::StartServer { source: e })?
);
if register_panic_hook {
let current_hook = std::panic::take_hook();
let new_hook = move |hook_info: &std::panic::PanicHookInfo<'_>| {
current_hook(hook_info);
error_page::error_page_panic_hook(hook_info);
};
std::panic::set_hook(Box::new(new_hook));
}
axum::serve(listener, handler.into_make_service())
.with_graceful_shutdown(shutdown_signal)
.await
.map_err(|e| ErrorRepr::StartServer { source: e })?;
if register_panic_hook {
let _ = std::panic::take_hook();
}
#[cfg(feature = "db")]
if let Some(database) = &context_cleanup.database {
database.close().await?;
}
Ok(())
}
enum ErrorResponse {
ErrorPageTrigger(ErrorPageTrigger),
ErrorReturned(Error),
Panic(Box<dyn std::any::Any + Send>),
}
fn build_cot_error_page(
error_response: ErrorResponse,
diagnostics: &Diagnostics,
) -> axum::response::Response {
match error_response {
ErrorResponse::ErrorPageTrigger(trigger) => match trigger {
ErrorPageTrigger::NotFound { message } => {
error_page::handle_not_found(message, diagnostics)
}
},
ErrorResponse::ErrorReturned(error) => {
error_page::handle_response_error(&error, diagnostics)
}
ErrorResponse::Panic(error) => error_page::handle_response_panic(&error, diagnostics),
}
}
fn build_custom_error_page(
not_found_handler: &Arc<dyn ErrorPageHandler>,
server_error_handler: &Arc<dyn ErrorPageHandler>,
error_response: &ErrorResponse,
) -> axum::response::Response {
match error_response {
ErrorResponse::ErrorPageTrigger(ErrorPageTrigger::NotFound { .. }) => {
not_found_handler.handle().map_or_else(
|error| {
error!(
?error,
"Error occurred while running custom 404 Not Found handler"
);
error_page::build_cot_not_found_page()
},
response_cot_to_axum,
)
}
ErrorResponse::ErrorReturned(_) | ErrorResponse::Panic(_) => {
server_error_handler.handle().map_or_else(
|error| {
error!(
?error,
"Error occurred while running custom 500 Internal Server Error handler"
);
error_page::build_cot_server_error_page()
},
response_cot_to_axum,
)
}
}
}
#[expect(clippy::future_not_send)] pub async fn run_cli(project: impl Project + 'static) -> cot::Result<()> {
Bootstrapper::new(project).run_cli().await
}
fn request_parts_for_diagnostics(request: Request) -> (Option<Parts>, Request) {
if request.project_config().debug {
let (parts, body) = request.into_parts();
let parts_clone = parts.clone();
let request = Request::from_parts(parts, body);
(Some(parts_clone), request)
} else {
(None, request)
}
}
fn request_axum_to_cot(
axum_request: axum::extract::Request,
context: Arc<ProjectContext>,
) -> Request {
let mut request = axum_request.map(Body::axum);
prepare_request(&mut request, context);
request
}
pub(crate) fn prepare_request(request: &mut Request, context: Arc<ProjectContext>) {
request.extensions_mut().insert(context);
}
async fn pass_to_axum(
request: Request,
handler: &mut BoxedHandler,
) -> cot::Result<axum::response::Response> {
poll_fn(|cx| handler.poll_ready(cx)).await?;
let response = handler.call(request).await?;
Ok(response_cot_to_axum(response))
}
fn response_cot_to_axum(response: Response) -> axum::response::Response {
response.map(axum::body::Body::new)
}
async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
() = ctrl_c => {},
() = terminate => {},
}
}
#[cfg(test)]
mod tests {
use cot::test::serial_guard;
use super::*;
use crate::auth::UserId;
use crate::config::SecretKey;
struct TestApp;
impl App for TestApp {
fn name(&self) -> &'static str {
"mock"
}
}
#[cot::test]
async fn app_default_impl() {
let app = TestApp {};
assert_eq!(app.name(), "mock");
assert_eq!(app.router().routes().len(), 0);
assert_eq!(app.migrations().len(), 0);
}
struct TestProject;
impl Project for TestProject {}
#[test]
fn project_default_cli_metadata() {
let metadata = TestProject.cli_metadata();
assert_eq!(metadata.name, "cot");
assert_eq!(metadata.version, env!("CARGO_PKG_VERSION"));
assert_eq!(metadata.authors, env!("CARGO_PKG_AUTHORS"));
assert_eq!(metadata.description, env!("CARGO_PKG_DESCRIPTION"));
}
#[cfg(feature = "live-reload")]
#[cot::test]
async fn project_middlewares() {
struct TestProject;
impl Project for TestProject {
fn config(&self, _config_name: &str) -> cot::Result<ProjectConfig> {
Ok(ProjectConfig::default())
}
fn middlewares(
&self,
handler: RootHandlerBuilder,
context: &MiddlewareContext,
) -> BoxedHandler {
handler
.middleware(crate::static_files::StaticFilesMiddleware::from_context(
context,
))
.middleware(crate::middleware::LiveReloadMiddleware::from_context(
context,
))
.build()
}
}
let response = crate::test::Client::new(TestProject)
.await
.get("/")
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[test]
fn project_default_config() {
let temp_dir = tempfile::tempdir().unwrap();
let config_dir = temp_dir.path().join("config");
std::fs::create_dir(&config_dir).unwrap();
let config = r#"
debug = false
secret_key = "123abc"
"#;
let config_file_path = config_dir.as_path().join("dev.toml");
std::fs::write(config_file_path, config).unwrap();
let _guard = serial_guard();
std::env::set_current_dir(&temp_dir).unwrap();
let config = TestProject.config("dev").unwrap();
assert!(!config.debug);
assert_eq!(config.secret_key, SecretKey::from("123abc".to_string()));
}
#[test]
fn project_default_register_apps() {
let mut apps = AppBuilder::new();
let context = ProjectContext::new().with_config(ProjectConfig::default());
TestProject.register_apps(&mut apps, &context);
assert!(apps.apps.is_empty());
}
#[cot::test]
async fn test_default_auth_backend() {
let context = ProjectContext::new()
.with_config(
ProjectConfig::builder()
.auth_backend(AuthBackendConfig::None)
.build(),
)
.with_apps(vec![], Arc::new(Router::empty()))
.with_database(None);
let auth_backend = TestProject.auth_backend(&context);
assert!(
auth_backend
.get_by_id(UserId::Int(0))
.await
.unwrap()
.is_none()
);
}
#[cot::test]
#[cfg_attr(miri, ignore)] async fn bootstrapper() {
struct TestProject;
impl Project for TestProject {
fn register_apps(&self, apps: &mut AppBuilder, _context: &RegisterAppsContext) {
apps.register_with_views(TestApp {}, "/app");
}
}
let bootstrapper = Bootstrapper::new(TestProject)
.with_config(ProjectConfig::default())
.boot()
.await
.unwrap();
assert_eq!(bootstrapper.context().apps.len(), 1);
assert_eq!(bootstrapper.context().router.routes().len(), 1);
}
}