use eternaltwin_core::api::SyncRef; use eternaltwin_core::clock::{Clock, ClockRef, VirtualClock}; use eternaltwin_core::core::{Duration, Instant, LocaleId, SecretString}; use eternaltwin_core::hammerfest::HammerfestStore; use eternaltwin_core::link::store::LinkStore; use eternaltwin_core::link::VersionedLinks; use eternaltwin_core::user::{ShortUser, User, UserDisplayNameVersion, UserDisplayNameVersions, UserStore}; use eternaltwin_core::uuid::{Uuid4Generator, UuidGenerator}; use eternaltwin_db_schema::force_create_latest; use eternaltwin_hammerfest_store::pg::PgHammerfestStore; use eternaltwin_link_store::pg::PgLinkStore; use eternaltwin_user_store::pg::PgUserStore; use opentelemetry::trace::noop::{NoopTracer, NoopTracerProvider}; use opentelemetry::trace::TracerProvider; use serial_test::serial; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use sqlx::PgPool; use std::sync::Arc; use eternaltwin_auth_store::pg::PgAuthStore; use eternaltwin_core::auth::{ AuthStore, RawUserCredentials, RegisterOrLoginWithEmailOptions, RegisterWithUsernameOptions, RegisterWithVerifiedEmailOptions, Session, UserAndSession, }; use eternaltwin_core::dinoparc::DinoparcStore; use eternaltwin_core::email::{EmailAddress, EmailFormatter, Mailer, VerifyRegistrationEmail}; use eternaltwin_core::oauth::OauthProviderStore; use eternaltwin_core::opentelemetry::DynTracer; use eternaltwin_core::password::{Password, PasswordService}; use eternaltwin_core::twinoid::TwinoidStore; use eternaltwin_dinoparc_store::pg::PgDinoparcStore; use eternaltwin_email_formatter::json::{JsonBody, JsonEmailFormatter}; use eternaltwin_mailer::client::mock::MemMailer; use eternaltwin_oauth_provider_store::pg::PgOauthProviderStore; use eternaltwin_password::scrypt::ScryptPasswordService; use eternaltwin_services::auth::{AuthService, DynAuthService}; use eternaltwin_twinoid_store::pg::PgTwinoidStore; macro_rules! assert_ok { ($result:expr $(,)?) => {{ match &$result { Err(_) => { panic!("assertion failed: `result.is_ok()`: {:?}", &$result) } Ok(()) => {} } }}; } async fn make_test_api() -> TestApi, Arc, Arc> { let config = eternaltwin_config::Config::for_test(); let tracer_provider = NoopTracerProvider::new(); let tracer: NoopTracer = tracer_provider.tracer("eternaltwin_services_test"); let admin_database: PgPool = PgPoolOptions::new() .max_connections(5) .connect_with( PgConnectOptions::new() .host(&config.postgres.host.value) .port(config.postgres.port.value) .database(&config.postgres.name.value) .username(&config.postgres.admin_user.value) .password(&config.postgres.admin_password.value), ) .await .unwrap(); force_create_latest(&admin_database, true).await.unwrap(); admin_database.close().await; let database: PgPool = PgPoolOptions::new() .max_connections(5) .connect_with( PgConnectOptions::new() .host(&config.postgres.host.value) .port(config.postgres.port.value) .database(&config.postgres.name.value) .username(&config.postgres.user.value) .password(&config.postgres.password.value), ) .await .unwrap(); let database = Arc::new(database); let database_secret = SecretString::new("dev_secret".to_string()); let auth_secret: Vec = b"dev_secret".to_vec(); let internal_auth_key: Vec = b"dev_secret_internal_auth".to_vec(); let uuid_generator = Arc::new(Uuid4Generator); let clock: Arc = Arc::new(VirtualClock::new(Instant::ymd_hms(2020, 1, 1, 0, 0, 0))); let hammerfest_store: Arc = Arc::new( PgHammerfestStore::new( Arc::clone(&clock), Arc::clone(&database), database_secret.clone(), Arc::clone(&uuid_generator), ) .await .unwrap(), ); let dinoparc_store: Arc = Arc::new( PgDinoparcStore::new(Arc::clone(&clock), Arc::clone(&database), Arc::clone(&uuid_generator)) .await .unwrap(), ); let twinoid_store: Arc = Arc::new( PgTwinoidStore::new( Arc::clone(&clock), Arc::clone(&database), database_secret.clone(), Arc::clone(&uuid_generator), ) .await .unwrap(), ); let link_store: Arc = Arc::new(PgLinkStore::new(Arc::clone(&clock), Arc::clone(&database))); let user_store: Arc = Arc::new(PgUserStore::new( Arc::clone(&clock), Arc::clone(&database), database_secret.clone(), tracer.clone(), Arc::clone(&uuid_generator), )); let email_formatter: Arc = Arc::new(JsonEmailFormatter); let mailer = Arc::new(MemMailer::new(false)); let password_service = Arc::new(ScryptPasswordService::with_os_rng( config.scrypt.max_time.value, config.scrypt.max_mem_frac.value, )); let auth_store: Arc = Arc::new(PgAuthStore::new( Arc::clone(&clock), Arc::clone(&database), Arc::clone(&uuid_generator), database_secret.clone(), )); let oauth_provider_store: Arc = Arc::new(PgOauthProviderStore::new( Arc::clone(&clock), Arc::clone(&database), Arc::clone(&password_service), Arc::clone(&uuid_generator), database_secret.clone(), )); let auth: Arc = Arc::new(AuthService::new( Arc::clone(&auth_store), Arc::clone(&clock) as Arc, Arc::clone(&dinoparc_store), Arc::clone(&email_formatter) as Arc, Arc::clone(&hammerfest_store), Arc::clone(&link_store), Arc::clone(&mailer) as Arc, Arc::clone(&oauth_provider_store), Arc::clone(&password_service) as Arc, Arc::clone(&user_store), DynTracer::Noop(tracer), Arc::clone(&twinoid_store), Arc::clone(&uuid_generator) as Arc, internal_auth_key, auth_secret, )); TestApi { auth, clock, mailer } } struct TestApi where TyAuth: SyncRef, TyClock: ClockRef, TyMailer: SyncRef, { pub(crate) auth: TyAuth, pub(crate) clock: TyClock, pub(crate) mailer: TyMailer, } #[tokio::test] #[serial] async fn test_register_user_through_mail() { register_user_through_mail(make_test_api().await).await; } #[tokio::test] #[serial] async fn test_register_user_with_username() { register_user_with_username(make_test_api().await).await; } #[tokio::test] #[serial] async fn test_register_user_with_username_and_sign_in() { register_user_with_username_and_sign_in(make_test_api().await).await; } async fn register_user_through_mail(api: TestApi) where TyAuth: SyncRef, TyClock: ClockRef, TyMailer: SyncRef, { api.clock.clock().advance_to(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)); let alice_email: EmailAddress = "alice@example.com".parse().unwrap(); api.mailer.create_inbox(alice_email.clone()); api .auth .register_or_login_with_email(&RegisterOrLoginWithEmailOptions { email: alice_email.clone(), locale: Some(LocaleId::FrFr), }) .await .unwrap(); api.clock.clock().advance_by(Duration::from_seconds(1)); let token = { let mut mailbox = api.mailer.read_inbox(&alice_email).into_iter(); let mail = mailbox.next().unwrap(); assert!(mailbox.next().is_none()); assert_eq!(mail.subject.as_str(), "verifyRegistrationEmail"); let body: JsonBody = serde_json::from_str(mail.body_text.as_str()).unwrap(); let token: String = body.data.token; token }; let actual = api .auth .register_with_verified_email(&RegisterWithVerifiedEmailOptions { email_token: token, display_name: "Alice".parse().unwrap(), password: Password("aaaaaaaaaa".as_bytes().to_vec()), }) .await .unwrap(); let expected = UserAndSession { user: User { id: actual.user.id, created_at: Instant::ymd_hms(2021, 1, 1, 0, 0, 1), deleted_at: None, display_name: UserDisplayNameVersions { current: UserDisplayNameVersion { value: "Alice".parse().unwrap(), }, }, is_administrator: true, links: VersionedLinks::default(), }, is_administrator: true, session: Session { id: actual.session.id, user: ShortUser { id: actual.user.id, display_name: UserDisplayNameVersions { current: UserDisplayNameVersion { value: "Alice".parse().unwrap(), }, }, }, ctime: Instant::ymd_hms(2021, 1, 1, 0, 0, 1), atime: Instant::ymd_hms(2021, 1, 1, 0, 0, 1), }, }; assert_eq!(actual, expected); } async fn register_user_with_username(api: TestApi) where TyAuth: SyncRef, TyClock: ClockRef, TyMailer: SyncRef, { api.clock.clock().advance_to(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)); let actual = api .auth .register_with_username(&RegisterWithUsernameOptions { username: "alice".parse().unwrap(), display_name: "Alice".parse().unwrap(), password: Password("aaaaaaaaaa".as_bytes().to_vec()), }) .await .unwrap(); let expected = UserAndSession { user: User { id: actual.user.id, created_at: Instant::ymd_hms(2021, 1, 1, 0, 0, 0), deleted_at: None, display_name: UserDisplayNameVersions { current: UserDisplayNameVersion { value: "Alice".parse().unwrap(), }, }, is_administrator: true, links: VersionedLinks::default(), }, is_administrator: true, session: Session { id: actual.session.id, user: ShortUser { id: actual.user.id, display_name: UserDisplayNameVersions { current: UserDisplayNameVersion { value: "Alice".parse().unwrap(), }, }, }, ctime: Instant::ymd_hms(2021, 1, 1, 0, 0, 0), atime: Instant::ymd_hms(2021, 1, 1, 0, 0, 0), }, }; assert_eq!(actual, expected); } async fn register_user_with_username_and_sign_in(api: TestApi) where TyAuth: SyncRef, TyClock: ClockRef, TyMailer: SyncRef, { api.clock.clock().advance_to(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)); assert_ok!(api .auth .register_with_username(&RegisterWithUsernameOptions { username: "alice".parse().unwrap(), display_name: "Alice".parse().unwrap(), password: Password("aaaaaaaaaa".as_bytes().to_vec()), }) .await .map(drop)); api.clock.clock().advance_by(Duration::from_seconds(1)); let actual = api .auth .raw_login_with_credentials(&RawUserCredentials { login: "alice".to_string(), password: Password("aaaaaaaaaa".as_bytes().to_vec()), }) .await .unwrap(); let expected = UserAndSession { user: User { id: actual.user.id, created_at: Instant::ymd_hms(2021, 1, 1, 0, 0, 0), deleted_at: None, display_name: UserDisplayNameVersions { current: UserDisplayNameVersion { value: "Alice".parse().unwrap(), }, }, is_administrator: true, links: VersionedLinks::default(), }, is_administrator: true, session: Session { id: actual.session.id, user: ShortUser { id: actual.user.id, display_name: UserDisplayNameVersions { current: UserDisplayNameVersion { value: "Alice".parse().unwrap(), }, }, }, ctime: Instant::ymd_hms(2021, 1, 1, 0, 0, 1), atime: Instant::ymd_hms(2021, 1, 1, 0, 0, 1), }, }; assert_eq!(actual, expected); }