#![allow(dead_code)]
#![allow(unreachable_pub)]
#![allow(clippy::disallowed_types)]
#![allow(clippy::missing_panics_doc)]
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, RwLock};
use axum::body::{Body, to_bytes};
use axum::http::{Method, Request, StatusCode};
use cratefield_adapter_telegram::fake::FakeTelegramBot;
use cratefield_adapter_telegram::webhook::SECRET_HEADER;
use cratefield_core::{MapConfig, RandomBytes, RandomError};
use cratefield_module_telegram::{
ACTION_SECRET_KEY, StepUp, Telegram, TelegramEvents, WEBHOOK_SECRET_KEY,
};
use cratefield_testing::{FakeAuth, TestHarness};
use http::HeaderMap;
use serde_json::{Value, json};
use tower::ServiceExt;
pub const SECRET: &str = "tg-test-secret-1";
pub const NOW: i64 = 1_800_000_000;
pub const BASE: &str = "/v1/telegram";
pub const ALICE: &str = "acct-alice";
pub const BOB: &str = "acct-bob";
pub const TG_USER: i64 = 42;
pub const CHAT: i64 = 42;
pub const ACTION_KEY: &[u8] = b"tg-test-action-secret";
pub struct FixedRandom(pub u8);
impl RandomBytes for FixedRandom {
fn fill(&self, dest: &mut [u8]) -> Result<(), RandomError> {
dest.fill(self.0);
Ok(())
}
}
pub struct FakeStepUp {
pub ok: AtomicBool,
pub calls: AtomicUsize,
}
impl FakeStepUp {
pub fn accepting() -> Arc<Self> {
Arc::new(Self {
ok: AtomicBool::new(true),
calls: AtomicUsize::new(0),
})
}
pub fn refusing() -> Arc<Self> {
Arc::new(Self {
ok: AtomicBool::new(false),
calls: AtomicUsize::new(0),
})
}
pub fn count(&self) -> usize {
self.calls.load(Ordering::SeqCst)
}
}
#[async_trait::async_trait]
impl StepUp for FakeStepUp {
async fn passkey_verified(&self, _headers: &HeaderMap, _subject: &str) -> bool {
self.calls.fetch_add(1, Ordering::SeqCst);
self.ok.load(Ordering::SeqCst)
}
}
pub type Calls<T> = Arc<RwLock<Vec<T>>>;
pub fn config(extra: Vec<(&'static str, &'static str)>) -> Vec<(&'static str, &'static str)> {
let mut pairs = vec![
(WEBHOOK_SECRET_KEY, SECRET),
(ACTION_SECRET_KEY, "tg-test-action-secret"),
];
pairs.extend(extra);
pairs
}
pub fn kit_with(
events: TelegramEvents,
pairs: Vec<(&'static str, &'static str)>,
step_up: Option<Arc<dyn StepUp>>,
) -> (TestHarness, Arc<FakeTelegramBot>) {
let bot = Arc::new(FakeTelegramBot::new());
let mut module = Telegram::new(events)
.bot(Arc::clone(&bot) as _)
.random(FixedRandom(0xA5));
if let Some(step_up) = step_up {
module = module.step_up(step_up);
}
let kit = TestHarness::with_ports(vec![Box::new(module)], |ports| {
ports.config = Arc::new(MapConfig::from_pairs(pairs));
ports.auth = Some(Arc::new(FakeAuth::subjects()));
});
(kit, bot)
}
pub fn kit(events: TelegramEvents) -> (TestHarness, Arc<FakeTelegramBot>) {
kit_with(events, config(vec![]), Some(FakeStepUp::accepting()))
}
pub async fn deliver(kit: &TestHarness, secret: Option<&str>, body: &[u8]) -> (StatusCode, Value) {
let mut builder = Request::builder()
.method(Method::POST)
.uri(format!("{BASE}/webhook"));
if let Some(secret) = secret {
builder = builder.header(SECRET_HEADER, secret);
}
let response = kit
.router
.clone()
.oneshot(
builder
.header("content-type", "application/json")
.body(Body::from(body.to_vec()))
.expect("request builds"),
)
.await
.expect("router answers");
body_of(response).await
}
pub async fn post(
kit: &TestHarness,
path: &str,
subject: Option<&str>,
body: Value,
) -> (StatusCode, Value) {
let mut builder = Request::builder()
.method(Method::POST)
.uri(format!("{BASE}{path}"));
if let Some(subject) = subject {
builder = builder.header("authorization", format!("Bearer {subject}"));
}
let response = kit
.router
.clone()
.oneshot(
builder
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.expect("request builds"),
)
.await
.expect("router answers");
body_of(response).await
}
pub async fn delete(kit: &TestHarness, path: &str, subject: Option<&str>) -> (StatusCode, Value) {
let mut builder = Request::builder()
.method(Method::DELETE)
.uri(format!("{BASE}{path}"));
if let Some(subject) = subject {
builder = builder.header("authorization", format!("Bearer {subject}"));
}
let response = kit
.router
.clone()
.oneshot(builder.body(Body::empty()).expect("request builds"))
.await
.expect("router answers");
body_of(response).await
}
async fn body_of(response: axum::http::Response<Body>) -> (StatusCode, Value) {
let status = response.status();
let bytes = to_bytes(response.into_body(), 64 * 1024)
.await
.expect("body reads");
let body = if bytes.is_empty() {
Value::Null
} else {
serde_json::from_slice(&bytes)
.unwrap_or(Value::String(String::from_utf8_lossy(&bytes).into_owned()))
};
(status, body)
}
pub fn update(update_id: i64, kind: &str, inner: Value) -> Vec<u8> {
let mut body = json!({ "update_id": update_id });
body[kind] = inner;
body.to_string().into_bytes()
}
pub fn message(text: &str) -> Value {
json!({
"message_id": 11,
"chat": { "id": CHAT, "type": "private" },
"from": { "id": TG_USER, "is_bot": false, "first_name": "Ada" },
"date": NOW,
"text": text,
})
}
pub fn command(text: &str) -> Value {
let length = command_length(text);
json!({
"message_id": 12,
"chat": { "id": CHAT, "type": "private" },
"from": { "id": TG_USER, "is_bot": false, "first_name": "Ada" },
"date": NOW,
"text": text,
"entities": [{ "type": "bot_command", "offset": 0, "length": length }],
})
}
fn command_length(text: &str) -> usize {
text.split([' ', '@']).next().map_or(0, str::len)
}
pub fn group_command(text: &str) -> Value {
json!({
"message_id": 13,
"chat": { "id": -10099, "type": "supergroup", "title": "Ops" },
"from": { "id": TG_USER, "is_bot": false, "first_name": "Ada" },
"date": NOW,
"text": text,
"entities": [
{ "type": "bot_command", "offset": 0, "length": command_length(text) }
],
})
}
pub fn callback_from(data: &str, user_id: i64) -> Value {
json!({
"id": format!("cq{user_id}"),
"from": { "id": user_id, "is_bot": false, "first_name": "Ada" },
"message": {
"message_id": 7,
"chat": { "id": CHAT, "type": "private" },
},
"data": data,
})
}
pub fn callback(data: &str) -> Value {
callback_from(data, TG_USER)
}
pub fn channel_post(text: &str) -> Value {
json!({
"message_id": 21,
"chat": { "id": -1001, "type": "channel", "title": "Announcements" },
"date": NOW,
"text": text,
})
}
pub fn linked_subjects(kit: &TestHarness) -> Vec<(String, i64)> {
use cratefield_core::Statement;
use sea_query::{Alias, Expr, Query};
let mut select = Query::select();
select
.columns(["subject", "telegram_user_id"])
.from(Alias::new("telegram_links"))
.order_by_expr(
Expr::col(Alias::new("subject")).into(),
sea_query::Order::Asc,
);
let rows = pollster::block_on(kit.db.query(&Statement::render(&select)))
.expect("links read")
.rows;
rows.iter()
.map(|row| {
(
row.get("subject").expect("subject reads"),
row.get::<i64>("telegram_user_id").expect("user reads"),
)
})
.collect()
}
pub fn action_status(kit: &TestHarness, action_id: &str) -> Option<String> {
use cratefield_core::Statement;
use sea_query::{Alias, Expr, Query};
let mut select = Query::select();
select
.column(Alias::new("status"))
.from(Alias::new("telegram_actions"))
.and_where(Expr::col(Alias::new("action_id")).eq(action_id));
pollster::block_on(kit.db.query(&Statement::render(&select)))
.expect("action read")
.first()
.and_then(|row| row.get("status"))
}
pub fn stored_code_hashes(kit: &TestHarness) -> Vec<String> {
use cratefield_core::Statement;
use sea_query::{Alias, Query};
let mut select = Query::select();
select
.column(Alias::new("code_hash"))
.from(Alias::new("telegram_link_codes"));
pollster::block_on(kit.db.query(&Statement::render(&select)))
.expect("codes read")
.rows
.iter()
.filter_map(|row| row.get("code_hash"))
.collect()
}
pub fn code_consumed_at(kit: &TestHarness, code: &str) -> Option<String> {
use cratefield_core::Statement;
use sea_query::{Alias, Expr, Query};
let mut select = Query::select();
select
.column(Alias::new("consumed_at"))
.from(Alias::new("telegram_link_codes"))
.and_where(Expr::col(Alias::new("code_hash")).eq(code_hash_of(code)));
pollster::block_on(kit.db.query(&Statement::render(&select)))
.expect("code read")
.first()
.and_then(|row| row.get("consumed_at"))
}
pub fn code_hash_of(code: &str) -> String {
use sha2::{Digest, Sha256};
hex::encode(Sha256::digest(code.as_bytes()))
}
pub fn insert_link(kit: &TestHarness, subject: &str, user_id: i64, chat_id: i64) {
use cratefield_core::Statement;
use sea_query::{Alias, Query};
let mut insert = Query::insert();
insert
.into_table(Alias::new("telegram_links"))
.columns(["subject", "telegram_user_id", "chat_id", "linked_at"])
.values_panic([
subject.into(),
user_id.into(),
chat_id.into(),
rfc3339(NOW).into(),
]);
let affected =
pollster::block_on(kit.db.execute(&Statement::render(&insert))).expect("link inserts");
assert_eq!(affected, 1);
}
pub fn insert_action(
kit: &TestHarness,
action_id: &str,
subject: &str,
status: &str,
value_moving: bool,
expires_in: i64,
) {
use cratefield_core::Statement;
use sea_query::{Alias, Query};
let mut insert = Query::insert();
insert
.into_table(Alias::new("telegram_actions"))
.columns([
"action_id",
"subject",
"action",
"value_moving",
"status",
"expires_at",
"created_at",
])
.values_panic([
action_id.into(),
subject.into(),
"transfer".into(),
i64::from(value_moving).into(),
status.into(),
rfc3339(NOW + expires_in).into(),
rfc3339(NOW).into(),
]);
let affected =
pollster::block_on(kit.db.execute(&Statement::render(&insert))).expect("action inserts");
assert_eq!(affected, 1);
}
pub fn try_send_prompt(
kit: &TestHarness,
bot: &FakeTelegramBot,
subject: &str,
value_moving: bool,
) -> Result<cratefield_module_telegram::ActionPrompt, cratefield_module_telegram::ActionError> {
use cratefield_module_telegram::{ActionRequest, send_action_prompt};
let request = ActionRequest {
subject: subject.to_owned(),
action: "transfer".to_owned(),
text: "Send 50 to the pool?".to_owned(),
value_moving,
ttl: std::time::Duration::from_secs(600),
};
pollster::block_on(send_action_prompt(
&*kit.db,
&kit.clock,
&FixedRandom(0xA5),
bot,
ACTION_KEY,
request,
))
}
pub fn send_prompt(
kit: &TestHarness,
bot: &FakeTelegramBot,
subject: &str,
value_moving: bool,
) -> cratefield_module_telegram::ActionPrompt {
try_send_prompt(kit, bot, subject, value_moving).expect("the prompt sends")
}
pub fn callback_data(bot: &FakeTelegramBot, label: &str) -> Option<String> {
use cratefield_adapter_telegram::ButtonKind;
let sent = bot.sent();
let message = sent.last()?;
message
.buttons
.iter()
.flatten()
.find(|button| button.text == label)
.and_then(|button| match &button.kind {
ButtonKind::Callback(data) => Some(data.clone()),
ButtonKind::Url(_) => None,
})
}
pub fn button_url(bot: &FakeTelegramBot, label: &str) -> Option<String> {
use cratefield_adapter_telegram::ButtonKind;
let sent = bot.sent();
let message = sent.last()?;
message
.buttons
.iter()
.flatten()
.find(|button| button.text == label)
.and_then(|button| match &button.kind {
ButtonKind::Url(url) => Some(url.clone()),
ButtonKind::Callback(_) => None,
})
}
pub fn rfc3339(at: i64) -> String {
time::OffsetDateTime::from_unix_timestamp(at)
.expect("a plain timestamp")
.format(&time::format_description::well_known::Rfc3339)
.expect("a plain timestamp formats")
}