use std::{sync::Mutex, collections::BTreeMap, ffi::{CStr, CString}};
use std::os::raw::c_char;
use serde::Deserialize;
use serde_json::{json, Value};
static TRANSACTIONS: Mutex<BTreeMap<String, (Command, u64)>> = Mutex::new(BTreeMap::new());
enum Command {
CardEnquiry(bool),
Deduct(i32, bool),
Settle,
}
impl Command {
fn to_string(&self) -> String {
match self {
Command::CardEnquiry(_) => "card.enquiry".to_string(),
Command::Deduct(_, _) => "deduct".to_string(),
Command::Settle => "settle".to_string(),
}
}
}
#[no_mangle]
pub extern "C" fn Connect(_com: *mut c_char) -> i32 {
0
}
#[no_mangle]
pub extern "C" fn Disconnect() {}
#[no_mangle]
#[allow(non_snake_case)]
pub extern "C" fn CmdExchange(requestJSON: *mut c_char, responseJSON: *mut c_char, maxResponseLen: i32) -> i32 {
if requestJSON.is_null() || responseJSON.is_null() || maxResponseLen <= 0 {
return -2;
}
if maxResponseLen < 4096 {
return -3;
}
let req_str = unsafe {
match CStr::from_ptr(requestJSON).to_str() {
Ok(s) => s,
Err(_) => return -2,
}
};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Request {
command: String,
transaction_id: Option<String>,
transaction_data: Option<TransactionData>,
res_id: Option<String>,
zh: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct TransactionData {
#[serde(rename = "type")]
typ: String,
transaction_value: Option<i32>,
}
let request: Request = match serde_json::from_str(req_str) {
Ok(req) => req,
Err(_) => return -2,
};
if request.command == "config.get" {
return write_json_response(get_config(), responseJSON, maxResponseLen)
} else if request.command == "display.img" {
if let Some(res_id) = request.res_id {
if let Ok(res_id_value) = res_id.parse::<i32>() {
if res_id_value > 0 && res_id_value < 9 {
return write_json_response(json!({
"command": "display.img",
"result": "ok",
}), responseJSON, maxResponseLen)
}
}
}
return write_json_response(json!({
"command": "display.img",
"result": "fail",
"errorMessage": [ "Invalid parameter", "設定錯誤" ],
}), responseJSON, maxResponseLen)
} else if request.command == "scheme.enquiry" {
let simplified_chinese = request.zh.unwrap_or_default() == "cn";
return write_json_response(json!({
"command": "scheme.enquiry",
"result": "ok",
"schemes": [
{ "schemeId":"P01", "schemeDesc": [
"JoyYou Promotion",
if simplified_chinese {
"乐悠卡优惠"
} else {
"樂悠卡優惠"
},
] },
{ "schemeId":"P02", "schemeDesc": [
"Member's Exclusive Offer x Mobile Octopus Promotion",
if simplified_chinese {
"会员专属礼遇 x 手机版八达通特别优惠"
} else {
"會員專屬禮遇 x 手機版八達通特別優惠"
},
] },
{ "schemeId":"P03", "schemeDesc": [
"Festival Promotion x Octopus Special Offer",
if simplified_chinese {
"节日优惠 x 八达通特别优惠"
} else {
"節日優惠 x 八達通特別優惠"
},
] },
{ "schemeId":"P04", "schemeDesc": [
"Breakfast Promotion x Octopus Special Offer",
if simplified_chinese {
"早餐时段 x 八达通特别优惠"
} else {
"早餐時段 x 八達通特別優惠"
},
] },
{ "schemeId":"P05", "schemeDesc": [
"Birthday Offer",
if simplified_chinese {
"生日优惠"
} else {
"生日優惠"
},
] },
{ "schemeId":"P06", "schemeDesc": [
"Public Transport Discount",
if simplified_chinese {
"公共交通折扣"
} else {
"公共交通折扣"
},
] },
{ "schemeId":"P07", "schemeDesc": [
"Mobile Octopus Promotion",
if simplified_chinese {
"手机八达通优惠"
} else {
"手機八達通優惠"
},
] },
{ "schemeId":"P08", "schemeDesc": [
"Shopping Cashback x Octopus Special Offer",
if simplified_chinese {
"购物现金回赠 x 八达通特别优惠"
} else {
"購物現金回贈 x 八達通特別優惠"
},
] },
{ "schemeId":"P09", "schemeDesc": [
"Student Special",
if simplified_chinese {
"学生优惠"
} else {
"學生優惠"
},
] },
{ "schemeId":"P10", "schemeDesc": [
"Seasonal Sale x Octopus Special Offer",
if simplified_chinese {
"季节促销 x 八达通特别优惠"
} else {
"季節促銷 x 八達通特別優惠"
},
] },
],
}), responseJSON, maxResponseLen)
} else if request.command == "trans.request" {
if let Some(transaction_data) = request.transaction_data {
if transaction_data.typ == "card.enquiry" {
let simplified_chinese = request.zh.unwrap_or_default() == "cn";
return write_json_response(new_transaction(Command::CardEnquiry(simplified_chinese)), responseJSON, maxResponseLen)
} else if transaction_data.typ == "deduct" {
let amount = transaction_data.transaction_value.unwrap_or(1);
let simplified_chinese = request.zh.unwrap_or_default() == "cn";
return write_json_response(new_transaction(Command::Deduct(amount, simplified_chinese)), responseJSON, maxResponseLen)
} else if transaction_data.typ == "settle" {
let mut settle = new_transaction(Command::Settle);
let settle = settle.as_object_mut().unwrap();
settle.insert("expiry".to_string(), json!(300));
return write_json_response(serde_json::Value::Object(settle.clone()), responseJSON, maxResponseLen)
}
}
} else if request.command == "trans.perform" {
if let Some(transaction_id) = request.transaction_id {
if let Some((cmd, timestamp)) = TRANSACTIONS.lock().unwrap().get(&transaction_id) {
match cmd {
Command::CardEnquiry(simplified_chinese) => {
return write_json_response(enquiry_card(&transaction_id, *simplified_chinese), responseJSON, maxResponseLen)
},
Command::Deduct(amount, simplified_chinese) => {
return write_json_response(deduct(&transaction_id, *amount, *simplified_chinese), responseJSON, maxResponseLen)
},
Command::Settle => {
if get_current_time() - timestamp > 10 {
return write_json_response(settle(&transaction_id), responseJSON, maxResponseLen)
}
std::thread::sleep(std::time::Duration::from_secs(1));
return write_json_response(json!({
"command": "trans.perform",
"transactionId": transaction_id,
"result": "processing",
"transactionData": {
"deviceId": "FFFFFF",
"type": "settle",
},
}), responseJSON, maxResponseLen)
},
}
}
}
} else if request.command == "trans.cancel" {
if let Some(transaction_id) = request.transaction_id {
TRANSACTIONS.lock().unwrap().remove(&transaction_id);
return write_json_response(json!({
"command": "trans.cancel",
"result": "ok",
}), responseJSON, maxResponseLen)
}
}
-2
}
fn new_transaction(cmd: Command) -> Value {
let typ = cmd.to_string();
let id = generate_random_string(22);
let result = {
let mut tx = TRANSACTIONS.lock().unwrap();
if tx.contains_key(&id) {
"processing"
} else {
let timestamp = get_current_time();
tx.insert(id.clone(), (cmd, timestamp));
"ok"
}
};
json!({
"command": "trans.perform",
"transactionId": id,
"result": result,
"transactionData": {
"deviceId": "FFFFFF",
"type": typ,
},
})
}
fn enquiry_card(id: &str, simplified_chinese: bool) -> Value {
if let Ok(card) = std::env::var("CARD") {
if card == "1" {
return json!({
"command": "trans.perform",
"transactionId": id,
"result": "fail",
"errorMessage": ["Invalid card", "此卡失效"],
"transactionData": {
"deviceId": "FFFFFF",
"type": "card.enquiry",
},
"retry": true,
})
} else if card == "2" {
return json!({
"transactionLogs": [{
"amount": 0,
"date": "2024-08-01",
"machineId": "FFFF",
"spName": ["OCL", "OCL"],
"spNameLong": ["Octopus Cards Limited", if simplified_chinese {
"八达通卡公司"
} else {
"八達通卡公司"
}],
"spType": 255,
"time": "12:34:56",
}],
"command": "trans.perform",
"transactionId": id,
"result": "ok",
"transactionData": {
"IDm": "0101010101010101",
"availableFund": 2500,
"cardId": 25123456,
"date": "2025-02-01",
"deviceId": "FFFFFF",
"identity": 0,
"lang": 0,
"octopusType": 1,
"passenger": 33,
"time": "22:33:44",
"type": "card.enquiry",
},
});
}
}
json!({
"transactionLogs": [{
"amount": 0,
"date": "2024-08-01",
"machineId": "FFFF",
"spName": ["OCL", "OCL"],
"spNameLong": ["Octopus Cards Limited", if simplified_chinese {
"八达通卡公司"
} else {
"八達通卡公司"
}],
"spType": 255,
"time": "12:34:56",
}],
"command": "trans.perform",
"transactionId": id,
"result": "ok",
"transactionData": {
"IDm": "0101010101010101",
"availableFund": 2500,
"cardId": 25123456,
"date": "2025-02-01",
"deviceId": "FFFFFF",
"identity": 0,
"lang": 0,
"lastAddValueDate": "2025-01-01",
"lastAddValueDevice": "FFFFFF",
"lastAddValueType": 1,
"maxAddValueAllow": 30000,
"octopusType": 0,
"passenger": 3,
"remainingValue": 0,
"time": "22:33:44",
"type": "card.enquiry",
},
})
}
fn deduct(id: &str, amount: i32, simplified_chinese: bool) -> Value {
json!({
"command": "trans.perform",
"printData": [
{
"type": "line",
"values": [
concat!(
"This is program test. If you have any queries on the above transaction, ",
"please keep the receipt and call Octopus Customer Service Hotline at ",
"2266-2222 or visit www.octopus.com.hk"
),
if simplified_chinese {
concat!(
"程序测试。客户如对上述的交易有任何查询,请保存此存根及致电八达通顾客",
"服务热线2266-2222,或浏览www.octopus.com.hk",
)
} else {
concat!(
"程序測試。客戶如對上述的交易有任何查詢,請保存此存根及致電八達通顧客",
"服務熱線2266-2222,或瀏覽www.octopus.com.hk",
)
}
],
"format": 0
},
{
"type": "line",
"values": [
"**********************",
"**********************"
],
"format": 1
},
{
"type": "line",
"values": [
"* Invalid transaction *",
if simplified_chinese {
"* 此交易无效 *"
} else {
"* 此交易無效 *"
}
],
"format": 1
},
{
"type": "line",
"values": [
"**********************",
"**********************"
],
"format": 1
},
{
"type": "sep",
"values": [
" "
]
},
{
"type": "l.r",
"values": [
"Transaction Type",
"Terminal",
if simplified_chinese {
"交易类型"
} else {
"交易類型"
},
if simplified_chinese {
"终端机"
} else {
"終端機"
},
]
},
{
"type": "line",
"values": [
"R/W FW V2.05",
"R/W 版本V2.05"
],
"format": 2
},
{
"type": "sep",
"values": [
"-"
]
}
],
"transactionId": id,
"displayData": [
{
"type": "line",
"values": [
"Thank you for using Octopus",
if simplified_chinese {
"多谢使用八达通"
} else {
"多謝使用八達通"
}
],
"format": 1
},
{
"type": "line",
"values": [
"Remarks",
if simplified_chinese {
"备注"
} else {
"備註"
}
],
"format": 0
},
{
"type": "l.r",
"values": [
"Transaction Type",
"Terminal",
if simplified_chinese {
"交易类型"
} else {
"交易類型"
},
if simplified_chinese {
"终端机"
} else {
"終端機"
},
]
},
{
"type": "line",
"values": [
"R/W FW V2.05",
"R/W 版本V2.05"
],
"format": 2
}
],
"result": "ok",
"transactionData": {
"IDm": "0101010101010101",
"availableFund": 0,
"cardId": 25123456,
"date": "2025-02-28",
"deviceId": "FFFFFF",
"identity": 0,
"lang": 0,
"lastAddValueDate": "2025-02-28",
"lastAddValueDevice": "FFFFFF",
"lastAddValueType": 4,
"maxAddValueAllow": 30000,
"octopusType": 0,
"passenger": 3,
"remainingValue": 2500 - amount,
"time": "12:52:57",
"transactionValue": amount,
"type": "deduct",
"schemeName": if amount >= 400 {
Value::Array(vec![
Value::String("VIP x Octopus Offer".to_string()),
Value::String("商戶 x 八達通優惠".to_string())
])
} else {
Value::Null
},
"discountAmount": if amount >= 400 {
json!((amount as f64 * 0.1) as i32)
} else {
Value::Null
},
}
})
}
fn settle(id: &str) -> Value {
json!({
"upgrade": "firmware",
"settlementData": {
"deductCount": 11,
"deductTotal": 11,
"addValueCount": 0,
"addValueTotal": 0,
"xfilename": "MPS.00800.FFFFFF.20250309154455.00001",
"date": "2025-03-09",
"time": "15:44:56",
},
"command": "trans.perform",
"transactionId": id,
"result": "ok",
"transactionData": {
"date": "2025-03-09",
"deviceId": "FFFFFF",
"time": "15:44:56",
"type": "settle",
},
})
}
fn get_config() -> Value {
json!({
"alertMessage": 0,
"hwId": "OR30000000",
"os": "V12.2.5-2.65",
"ssid": "",
"wifi": "off",
"ip": "192.168.1.100",
"carrier": "",
"simId": "454000000000000",
"network": 3,
"ocrl": "7100.64",
"ethDHCP": true,
"command": "config.get",
"result": "ok",
"deviceId": "FFFFFF",
"readerDate": "23-02-2025",
"readerTime": "23:00:00",
"compId": 800,
"keyVersion": 0,
"eod": 3000,
"cobrandwhitelist": 0,
"blacklist": 0,
"firmware": "15073485",
"cchs": 28,
"interimBlackList": 260000,
"spbmlist": 0,
"funcBlacklist": 5000,
"subsidylist": 0,
"actionlist": 0,
"genericResFile": 7,
"specificResFile": 1,
"spntlist": 14,
})
}
#[allow(non_snake_case)]
fn write_json_response(response: Value, responseJSON: *mut c_char, maxResponseLen: i32) -> i32 {
if responseJSON.is_null() || maxResponseLen <= 0 {
return -2;
}
let resp_str = match serde_json::to_string(&response) {
Ok(s) => s,
Err(_) => return -2,
};
let c_str = match CString::new(resp_str) {
Ok(c_str) => c_str,
Err(_) => return -2,
};
if c_str.as_bytes_with_nul().len() > maxResponseLen as usize {
return -3;
}
unsafe {
std::ptr::copy_nonoverlapping(
c_str.as_ptr() as *const u8,
responseJSON as *mut u8,
c_str.as_bytes_with_nul().len()
);
}
0
}
fn generate_random_string(length: usize) -> String {
let chars: &[u8] = b"0123456789ABCDEF";
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
(0..length)
.map(|i| {
let hash = (now.as_micros() as usize)
.wrapping_add(i)
.wrapping_mul(0x9E3779B9);
chars[hash % chars.len()] as char
})
.collect()
}
fn get_current_time() -> u64 {
let start = std::time::SystemTime::now();
let since_epoch = start.duration_since(std::time::UNIX_EPOCH).expect("Time went backwards");
since_epoch.as_secs()
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
use std::ptr;
fn create_c_buffer(size: usize) -> Vec<u8> {
vec![0u8; size]
}
fn get_string_from_buffer(buffer: &[u8]) -> String {
let null_pos = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
String::from_utf8_lossy(&buffer[..null_pos]).to_string()
}
#[test]
fn test_connect_success() {
let com_port = CString::new("/dev/ttyS0").unwrap();
let result = Connect(com_port.as_ptr() as *mut c_char);
assert_eq!(result, 0);
}
#[test]
fn test_connect_null_ptr() {
let result = Connect(ptr::null_mut());
assert_eq!(result, 0);
}
#[test]
fn test_disconnect() {
Disconnect();
}
#[test]
fn test_cmd_exchange_null_pointers() {
let result = CmdExchange(ptr::null_mut(), ptr::null_mut(), 4096);
assert_eq!(result, -2);
}
#[test]
fn test_cmd_exchange_insufficient_buffer() {
let request = CString::new(r#"{"command": "config.get"}"#).unwrap();
let mut response_buffer = create_c_buffer(1000);
let result = CmdExchange(
request.as_ptr() as *mut c_char,
response_buffer.as_mut_ptr() as *mut c_char,
1000
);
assert_eq!(result, -3);
}
#[test]
fn test_config_get() {
let request = CString::new(r#"{"command": "config.get"}"#).unwrap();
let mut response_buffer = create_c_buffer(4096);
let result = CmdExchange(
request.as_ptr() as *mut c_char,
response_buffer.as_mut_ptr() as *mut c_char,
4096
);
assert_eq!(result, 0);
let response = get_string_from_buffer(&response_buffer);
let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
assert_eq!(parsed["command"], "config.get");
assert_eq!(parsed["result"], "ok");
assert!(parsed["deviceId"].is_string());
}
#[test]
fn test_display_img_valid() {
let request = CString::new(r#"{"command": "display.img", "resId": "1"}"#).unwrap();
let mut response_buffer = create_c_buffer(4096);
let result = CmdExchange(
request.as_ptr() as *mut c_char,
response_buffer.as_mut_ptr() as *mut c_char,
4096
);
assert_eq!(result, 0);
let response = get_string_from_buffer(&response_buffer);
let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
assert_eq!(parsed["command"], "display.img");
assert_eq!(parsed["result"], "ok");
}
#[test]
fn test_display_img_invalid() {
let request = CString::new(r#"{"command": "display.img", "resId": "10"}"#).unwrap();
let mut response_buffer = create_c_buffer(4096);
let result = CmdExchange(
request.as_ptr() as *mut c_char,
response_buffer.as_mut_ptr() as *mut c_char,
4096
);
assert_eq!(result, 0);
let response = get_string_from_buffer(&response_buffer);
let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
assert_eq!(parsed["command"], "display.img");
assert_eq!(parsed["result"], "fail");
}
#[test]
fn test_scheme_enquiry() {
let request = CString::new(r#"{"command": "scheme.enquiry"}"#).unwrap();
let mut response_buffer = create_c_buffer(4096);
let result = CmdExchange(
request.as_ptr() as *mut c_char,
response_buffer.as_mut_ptr() as *mut c_char,
4096
);
assert_eq!(result, 0);
let response = get_string_from_buffer(&response_buffer);
let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
assert_eq!(parsed["command"], "scheme.enquiry");
assert_eq!(parsed["result"], "ok");
assert!(parsed["schemes"].is_array());
assert!(parsed["schemes"].as_array().unwrap().len() > 0);
}
#[test]
fn test_scheme_enquiry_simplified_chinese() {
let request = CString::new(r#"{"command": "scheme.enquiry", "zh": "cn"}"#).unwrap();
let mut response_buffer = create_c_buffer(4096);
let result = CmdExchange(
request.as_ptr() as *mut c_char,
response_buffer.as_mut_ptr() as *mut c_char,
4096
);
assert_eq!(result, 0);
let response = get_string_from_buffer(&response_buffer);
let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
assert_eq!(parsed["command"], "scheme.enquiry");
assert_eq!(parsed["result"], "ok");
let schemes = parsed["schemes"].as_array().unwrap();
let first_scheme = &schemes[0];
let desc = first_scheme["schemeDesc"].as_array().unwrap();
assert!(desc[1].as_str().unwrap().contains("乐"));
}
#[test]
fn test_trans_request_card_enquiry() {
let request = CString::new(r#"{"command": "trans.request", "transactionData": {"type": "card.enquiry"}}"#).unwrap();
let mut response_buffer = create_c_buffer(4096);
let result = CmdExchange(
request.as_ptr() as *mut c_char,
response_buffer.as_mut_ptr() as *mut c_char,
4096
);
assert_eq!(result, 0);
let response = get_string_from_buffer(&response_buffer);
let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
assert_eq!(parsed["command"], "trans.perform");
assert!(parsed["result"] == "ok" || parsed["result"] == "processing");
assert!(parsed["transactionId"].is_string());
assert_eq!(parsed["transactionData"]["type"], "card.enquiry");
}
#[test]
fn test_trans_request_deduct() {
let request = CString::new(r#"{"command": "trans.request", "transactionData": {"type": "deduct", "transactionValue": 100}}"#).unwrap();
let mut response_buffer = create_c_buffer(4096);
let result = CmdExchange(
request.as_ptr() as *mut c_char,
response_buffer.as_mut_ptr() as *mut c_char,
4096
);
assert_eq!(result, 0);
let response = get_string_from_buffer(&response_buffer);
let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
assert_eq!(parsed["command"], "trans.perform");
assert!(parsed["result"] == "ok" || parsed["result"] == "processing");
assert!(parsed["transactionId"].is_string());
assert_eq!(parsed["transactionData"]["type"], "deduct");
}
#[test]
fn test_trans_request_settle() {
let request = CString::new(r#"{"command": "trans.request", "transactionData": {"type": "settle"}}"#).unwrap();
let mut response_buffer = create_c_buffer(4096);
let result = CmdExchange(
request.as_ptr() as *mut c_char,
response_buffer.as_mut_ptr() as *mut c_char,
4096
);
assert_eq!(result, 0);
let response = get_string_from_buffer(&response_buffer);
let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
assert_eq!(parsed["command"], "trans.perform");
assert!(parsed["result"] == "ok" || parsed["result"] == "processing");
assert!(parsed["transactionId"].is_string());
assert_eq!(parsed["transactionData"]["type"], "settle");
assert_eq!(parsed["expiry"], 300);
}
#[test]
fn test_trans_perform_card_enquiry() {
let request = CString::new(r#"{"command": "trans.request", "transactionData": {"type": "card.enquiry"}}"#).unwrap();
let mut response_buffer = create_c_buffer(4096);
CmdExchange(
request.as_ptr() as *mut c_char,
response_buffer.as_mut_ptr() as *mut c_char,
4096
);
let response = get_string_from_buffer(&response_buffer);
let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
let transaction_id = parsed["transactionId"].as_str().unwrap();
let perform_request = format!(r#"{{"command": "trans.perform", "transactionId": "{}"}}"#, transaction_id);
let perform_cstr = CString::new(perform_request).unwrap();
let mut perform_buffer = create_c_buffer(4096);
let result = CmdExchange(
perform_cstr.as_ptr() as *mut c_char,
perform_buffer.as_mut_ptr() as *mut c_char,
4096
);
assert_eq!(result, 0);
let perform_response = get_string_from_buffer(&perform_buffer);
let perform_parsed: serde_json::Value = serde_json::from_str(&perform_response).unwrap();
assert_eq!(perform_parsed["command"], "trans.perform");
assert_eq!(perform_parsed["result"], "ok");
assert!(perform_parsed["transactionData"]["IDm"].is_string());
assert!(perform_parsed["transactionData"]["availableFund"].is_number());
}
#[test]
fn test_trans_cancel() {
let request = CString::new(r#"{"command": "trans.request", "transactionData": {"type": "card.enquiry"}}"#).unwrap();
let mut response_buffer = create_c_buffer(4096);
CmdExchange(
request.as_ptr() as *mut c_char,
response_buffer.as_mut_ptr() as *mut c_char,
4096
);
let response = get_string_from_buffer(&response_buffer);
let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
let transaction_id = parsed["transactionId"].as_str().unwrap();
let cancel_request = format!(r#"{{"command": "trans.cancel", "transactionId": "{}"}}"#, transaction_id);
let cancel_cstr = CString::new(cancel_request).unwrap();
let mut cancel_buffer = create_c_buffer(4096);
let result = CmdExchange(
cancel_cstr.as_ptr() as *mut c_char,
cancel_buffer.as_mut_ptr() as *mut c_char,
4096
);
assert_eq!(result, 0);
let cancel_response = get_string_from_buffer(&cancel_buffer);
let cancel_parsed: serde_json::Value = serde_json::from_str(&cancel_response).unwrap();
assert_eq!(cancel_parsed["command"], "trans.cancel");
assert_eq!(cancel_parsed["result"], "ok");
}
#[test]
fn test_invalid_json() {
let request = CString::new(r#"{"invalid": json}"#).unwrap();
let mut response_buffer = create_c_buffer(4096);
let result = CmdExchange(
request.as_ptr() as *mut c_char,
response_buffer.as_mut_ptr() as *mut c_char,
4096
);
assert_eq!(result, -2);
}
#[test]
fn test_unknown_command() {
let request = CString::new(r#"{"command": "unknown.command"}"#).unwrap();
let mut response_buffer = create_c_buffer(4096);
let result = CmdExchange(
request.as_ptr() as *mut c_char,
response_buffer.as_mut_ptr() as *mut c_char,
4096
);
assert_eq!(result, -2);
}
#[test]
fn test_generate_random_string() {
let str1 = generate_random_string(10);
let str2 = generate_random_string(10);
assert_eq!(str1.len(), 10);
assert_eq!(str2.len(), 10);
for ch in str1.chars() {
assert!(ch.is_ascii_hexdigit());
}
for ch in str2.chars() {
assert!(ch.is_ascii_hexdigit());
}
let mut strings = std::collections::HashSet::new();
for _ in 0..100 {
let s = generate_random_string(10);
assert_eq!(s.len(), 10);
strings.insert(s);
}
assert!(strings.len() > 10);
}
#[test]
fn test_get_current_time() {
let time1 = get_current_time();
std::thread::sleep(std::time::Duration::from_millis(10));
let time2 = get_current_time();
assert!(time2 >= time1);
}
#[test]
fn test_card() {
test_card_invalid_scenario();
test_card_with_history_scenario();
}
fn test_card_invalid_scenario() {
std::env::remove_var("CARD");
std::env::set_var("CARD", "1");
assert_eq!(std::env::var("CARD").unwrap(), "1");
let request = CString::new(r#"{"command": "trans.request", "transactionData": {"type": "card.enquiry"}}"#).unwrap();
let mut response_buffer = create_c_buffer(4096);
CmdExchange(
request.as_ptr() as *mut c_char,
response_buffer.as_mut_ptr() as *mut c_char,
4096
);
let response = get_string_from_buffer(&response_buffer);
let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
let transaction_id = parsed["transactionId"].as_str().unwrap();
let perform_request = format!(r#"{{"command": "trans.perform", "transactionId": "{}"}}"#, transaction_id);
let perform_cstr = CString::new(perform_request).unwrap();
let mut perform_buffer = create_c_buffer(4096);
CmdExchange(
perform_cstr.as_ptr() as *mut c_char,
perform_buffer.as_mut_ptr() as *mut c_char,
4096
);
let perform_response = get_string_from_buffer(&perform_buffer);
let perform_parsed: serde_json::Value = serde_json::from_str(&perform_response).unwrap();
assert!(perform_parsed["result"] == "fail");
assert!(perform_parsed["errorMessage"].is_array());
assert_eq!(perform_parsed["retry"], true);
std::env::remove_var("CARD");
}
fn test_card_with_history_scenario() {
std::env::remove_var("CARD");
std::env::set_var("CARD", "2");
let request = CString::new(r#"{"command": "trans.request", "transactionData": {"type": "card.enquiry"}}"#).unwrap();
let mut response_buffer = create_c_buffer(4096);
CmdExchange(
request.as_ptr() as *mut c_char,
response_buffer.as_mut_ptr() as *mut c_char,
4096
);
let response = get_string_from_buffer(&response_buffer);
let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
let transaction_id = parsed["transactionId"].as_str().unwrap();
let perform_request = format!(r#"{{"command": "trans.perform", "transactionId": "{}"}}"#, transaction_id);
let perform_cstr = CString::new(perform_request).unwrap();
let mut perform_buffer = create_c_buffer(4096);
CmdExchange(
perform_cstr.as_ptr() as *mut c_char,
perform_buffer.as_mut_ptr() as *mut c_char,
4096
);
let perform_response = get_string_from_buffer(&perform_buffer);
let perform_parsed: serde_json::Value = serde_json::from_str(&perform_response).unwrap();
assert_eq!(perform_parsed["result"], "ok");
assert!(perform_parsed["transactionLogs"].is_array());
assert!(perform_parsed["transactionData"]["octopusType"].is_number());
std::env::remove_var("CARD");
}
}