#![allow(dead_code)]
use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use net_backend_client::protocol::{CloseCode, ServerPush, UnixMillis, UserId, WsCall};
use net_backend_server::auth::steam::{FakeSteamVerifier, SteamIdentity};
use net_backend_server::auth::{Auth, AuthConfig, AuthService};
use net_backend_server::chat::{Chat, ChatConfig, RoomSpec};
use net_backend_server::files::{Files, FilesConfig};
use net_backend_server::friends::Friends;
use net_backend_server::groups::Groups;
use net_backend_server::leaderboards::{BoardSpec, Leaderboards, LeaderboardsConfig};
use net_backend_server::lobbies::Lobbies;
use net_backend_server::mail::MemoryMailer;
use net_backend_server::matchmaking::{Matchmaking, MatchmakingConfig, QueueSpec};
use net_backend_server::notifications::{NewNotification, NotificationService, Notifications};
use net_backend_server::oauth::{OAuth, OAuthConfig};
use net_backend_server::protocol::admin::BanRequest;
use net_backend_server::storage::{Storage, StorageConfig};
use net_backend_server::{AppState, Config, ManualClock, NetBackendServer, SecretString};
use serde::{Deserialize, Serialize};
use tokio::sync::oneshot;
pub const STEAM_IDENTITY: &str = "client-tests";
pub const fn steam_id(n: u64) -> u64 {
76_561_197_960_265_728 + 4_000_000_000 + n
}
pub fn steam_ticket(n: u64) -> String {
format!("0b{n:02x}")
}
static FILES_N: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
pub const PASSWORD: &str = "correct horse battery";
pub const WAIT: Duration = Duration::from_secs(60);
pub const SERVER_THREADS: &str = "test-server";
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Echo {
pub text: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct Echoed {
pub text: String,
pub user: i64,
}
impl WsCall for Echo {
type Response = Echoed;
const KIND: &'static str = "test.echo";
}
pub static SLOW_STARTED: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Slow {
pub millis: u64,
}
impl WsCall for Slow {
type Response = net_backend_client::protocol::Ack;
const KIND: &'static str = "test.slow";
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct Note {
pub n: u32,
}
impl ServerPush for Note {
const KIND: &'static str = "test.note";
}
pub struct Setup {
pub config: Config,
pub auth: AuthConfig,
pub storage: StorageConfig,
pub oauth: OAuthConfig,
pub files: FilesConfig,
}
pub struct Server {
pub state: AppState,
pub base: String,
pub clock: Arc<ManualClock>,
handle: tokio::runtime::Handle,
stop: Option<oneshot::Sender<()>>,
thread: Option<std::thread::JoinHandle<()>>,
}
impl Server {
pub fn start() -> Server {
Self::start_with(|_| {})
}
pub fn start_with(tweak: impl FnOnce(&mut Setup) + Send + 'static) -> Server {
let (ready_sender, ready) = std::sync::mpsc::channel();
let (stop, stopped) = oneshot::channel::<()>();
let thread = std::thread::Builder::new().name(SERVER_THREADS.into()).spawn(move || {
let runtime = tokio::runtime::Builder::new_multi_thread().worker_threads(2).thread_name(SERVER_THREADS).enable_all().build().expect("runtime");
let handle = runtime.handle().clone();
runtime.block_on(async move {
let mut config = Config::default();
config.database.url = SecretString::new("sqlite::memory:");
config.database.migrations_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("target")
.join("tmp")
.join(format!("client-tests-{}", std::process::id()));
config.server.shutdown_grace_secs = 5;
let mut auth = AuthConfig::default();
auth.argon2_memory_kib = 64;
auth.argon2_iterations = 1;
auth.purge_interval_secs = 0;
auth.rate_limits = false;
auth.steam_identity = Some(STEAM_IDENTITY.into());
let mut files = FilesConfig::default();
files.dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..").join("target").join("tmp").join(format!(
"client-files-{}-{}",
std::process::id(),
FILES_N.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
));
files.upload_rate = 0;
let mut setup = Setup { config, auth, storage: StorageConfig::default(), oauth: OAuthConfig::default(), files };
tweak(&mut setup);
let mut chat = ChatConfig::default();
chat.rooms = vec![RoomSpec::new("world").with_name("World")];
let mut boards = LeaderboardsConfig::default();
boards.boards = vec![BoardSpec::new("highscore").with_name("High score")];
let clock = Arc::new(ManualClock::new(UnixMillis::now()));
let prepared = NetBackendServer::new(setup.config)
.clock(Arc::clone(&clock))
.module(Auth::new().with_config(setup.auth).mailer(MemoryMailer::new()).steam_verifier({
let steam = FakeSteamVerifier::new(STEAM_IDENTITY);
for n in 1..=8 {
steam.add_ticket(steam_ticket(n), SteamIdentity::new(steam_id(n)));
}
steam
}))
.module(Storage::new().with_config(setup.storage))
.module(Chat::new().with_config(chat))
.module(Leaderboards::new().with_config(boards))
.module(Notifications::new())
.module(Friends::new())
.module(Groups::new())
.module(OAuth::new().with_config(setup.oauth))
.module(Files::new().with_config(setup.files))
.module(Lobbies::new())
.module(Matchmaking::new().with_config({
let mut matchmaking = MatchmakingConfig::default().with_queue(QueueSpec::new("duel", 2));
matchmaking.interval_ms = 50;
matchmaking
}))
.ws_call::<Echo, _, _>(|ctx, echo: Echo| async move { Ok(Echoed { text: echo.text, user: ctx.auth.user_id.get() }) })
.ws_call::<Slow, _, _>(|_ctx, slow: Slow| async move {
SLOW_STARTED.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(slow.millis)).await;
Ok(net_backend_client::protocol::Ack::new())
})
.build()
.await
.expect("build");
prepared.migrate().await.expect("migrate");
let state = prepared.state().clone();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind");
let base = format!("http://{}", listener.local_addr().expect("addr"));
let _ = ready_sender.send((state, base, clock, handle));
let _ = prepared
.serve_with_shutdown(listener, async move {
let _ = stopped.await;
})
.await;
});
runtime.shutdown_timeout(Duration::from_secs(5));
});
let thread = thread.expect("the test server thread");
let (state, base, clock, handle) = ready.recv_timeout(WAIT).expect("the test server did not start");
Server { state, base, clock, handle, stop: Some(stop), thread: Some(thread) }
}
pub fn client(&self) -> net_backend_client::Client {
net_backend_client::Client::new(&self.base).expect("client")
}
pub fn advance(&self, by: Duration) {
self.clock.advance(i64::try_from(by.as_millis()).unwrap_or(i64::MAX));
}
pub fn run<T: Send + 'static>(&self, future: impl Future<Output = T> + Send + 'static) -> T {
let (sender, receiver) = std::sync::mpsc::channel();
self.handle.spawn(async move {
let _ = sender.send(future.await);
});
receiver.recv_timeout(WAIT).expect("the server task did not finish")
}
pub fn ban(&self, user: UserId) {
let state = self.state.clone();
self.run(async move {
let service = state.get::<AuthService>().expect("auth");
service.ban_user(&state, user, BanRequest::new()).await.expect("ban");
});
}
pub fn notify(&self, user: UserId, kind: &str) -> i64 {
let state = self.state.clone();
let kind = kind.to_string();
self.run(async move {
let service = state.get::<NotificationService>().expect("notifications");
service.send(&state, user, NewNotification::new(kind).with_text("hello")).await.expect("send").id.get()
})
}
pub fn push(&self, user: UserId, note: Note) {
self.state.ws().push_user(user, ¬e).expect("push");
}
pub fn close_user(&self, user: UserId, code: CloseCode) -> usize {
self.state.ws().close_user(user, code, "test")
}
pub fn connections_of(&self, user: UserId) -> usize {
self.state.ws().connections_of(user).len()
}
}
impl Drop for Server {
fn drop(&mut self) {
if let Some(stop) = self.stop.take() {
let _ = stop.send(());
}
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
pub fn email(name: &str) -> String {
format!("{name}@example.com")
}
pub async fn until(what: &str, mut check: impl FnMut() -> bool) {
let deadline = tokio::time::Instant::now() + WAIT;
while !check() {
assert!(tokio::time::Instant::now() < deadline, "timed out waiting for {what}");
tokio::time::sleep(Duration::from_millis(10)).await;
}
}