Skip to main content

ocrl/
lib.rs

1//! # OCRL Mock Library
2//!
3//! A mock implementation of the OCRL (Octopus Card Reader Library) C ABI for testing purposes.
4//! This library provides a test-friendly simulation of payment terminal operations including
5//! card enquiry, deduction, and settlement functions.
6//!
7//! ## API Compliance
8//!
9//! This library implements the exact same C API as the official OCRL Linux library:
10//! - Function signatures match the official specification exactly
11//! - Parameter names use the same camelCase convention as the original API
12//! - Return codes follow the official error code specification
13//! - Buffer size requirements match the official minimum of 4096 bytes
14//!
15//! ## Usage
16//!
17//! The library exports three main C functions:
18//! - `Connect(char* com)`: Open serial port and setup security channel with R/W
19//! - `CmdExchange(char* requestJSON, char* responseJSON, int maxResponseLen)`: Send requests and receive responses
20//! - `Disconnect()`: Close serial port
21//!
22//! ## Error Codes
23//!
24//! - `0`: Success (communication succeeded)
25//! - `-1`: COM not opened
26//! - `-2`: Communication error with R/W
27//! - `-3`: responseJSON buffer size not enough
28//! - `-6`: Link busy
29
30use std::{sync::Mutex, collections::BTreeMap, ffi::{CStr, CString}};
31use std::os::raw::c_char;
32use serde::Deserialize;
33use serde_json::{json, Value};
34
35static TRANSACTIONS: Mutex<BTreeMap<String, (Command, u64)>> = Mutex::new(BTreeMap::new());
36
37enum Command {
38    CardEnquiry(bool),
39    Deduct(i32, bool),
40    Settle,
41}
42
43impl Command {
44    fn to_string(&self) -> String {
45        match self {
46            Command::CardEnquiry(_) => "card.enquiry".to_string(),
47            Command::Deduct(_, _) => "deduct".to_string(),
48            Command::Settle => "settle".to_string(),
49        }
50    }
51}
52
53/// Open serial port and setup security channel with R/W
54///
55/// # Parameters
56/// - `com`: Name of COM port, e.g. "/dev/ttyS0" (unused in mock implementation)
57///
58/// # Returns
59/// - `0`: Communication with R/W succeeded
60/// - `-1`: Cannot open COM
61/// - `-2`: Communication error with R/W
62///
63/// # Safety
64/// This function is safe to call from C code.
65#[no_mangle]
66pub extern "C" fn Connect(_com: *mut c_char) -> i32 {
67    0
68}
69
70/// Close serial port
71///
72/// # Safety
73/// This function is safe to call from C code.
74#[no_mangle]
75pub extern "C" fn Disconnect() {}
76
77/// Blocking call to send out request and wait for response from R/W with 1sec timeout
78///
79/// # Parameters
80/// - `requestJSON`: Request to R/W in JSON format
81/// - `responseJSON`: Buffer to store response from R/W in JSON format
82/// - `maxResponseLen`: Size of responseJSON buffer (minimum size is 4096 bytes)
83///
84/// # Returns
85/// - `0`: Message send/receive succeeded
86/// - `-1`: COM not opened
87/// - `-2`: Communication error with R/W
88/// - `-3`: responseJSON buffer size not enough
89/// - `-6`: Link busy
90///
91/// # Safety
92/// The caller must ensure:
93/// - `requestJSON` points to a valid null-terminated string
94/// - `responseJSON` points to a writable buffer of at least `maxResponseLen` bytes
95/// - `maxResponseLen` is positive and represents the actual buffer size (minimum 4096)
96#[no_mangle]
97#[allow(non_snake_case)]
98pub extern "C" fn CmdExchange(requestJSON: *mut c_char, responseJSON: *mut c_char, maxResponseLen: i32) -> i32 {
99    if requestJSON.is_null() || responseJSON.is_null() || maxResponseLen <= 0 {
100        return -2;
101    }
102
103    // Validate minimum buffer size as per API specification
104    if maxResponseLen < 4096 {
105        return -3;
106    }
107
108    let req_str = unsafe {
109        match CStr::from_ptr(requestJSON).to_str() {
110            Ok(s) => s,
111            Err(_) => return -2,
112        }
113    };
114
115    #[derive(Deserialize)]
116    #[serde(rename_all = "camelCase")]
117    struct Request {
118        command: String,
119        transaction_id: Option<String>,
120        transaction_data: Option<TransactionData>,
121        res_id: Option<String>,
122        zh: Option<String>,
123    }
124
125    #[derive(Deserialize)]
126    #[serde(rename_all = "camelCase")]
127    struct TransactionData {
128        #[serde(rename = "type")]
129        typ: String,
130        transaction_value: Option<i32>,
131    }
132
133    let request: Request = match serde_json::from_str(req_str) {
134        Ok(req) => req,
135        Err(_) => return -2,
136    };
137
138    if request.command == "config.get" {
139        return write_json_response(get_config(), responseJSON, maxResponseLen)
140    } else if request.command == "display.img" {
141        if let Some(res_id) = request.res_id {
142            if let Ok(res_id_value) = res_id.parse::<i32>() {
143                if res_id_value > 0 && res_id_value < 9 {
144                    return write_json_response(json!({
145                        "command": "display.img",
146                        "result": "ok",
147                    }), responseJSON, maxResponseLen)
148                }
149            }
150        }
151        return write_json_response(json!({
152            "command": "display.img",
153            "result": "fail",
154            "errorMessage": [ "Invalid parameter", "設定錯誤" ],
155        }), responseJSON, maxResponseLen)
156    } else if request.command == "scheme.enquiry" {
157        let simplified_chinese = request.zh.unwrap_or_default() == "cn";
158        return write_json_response(json!({
159            "command": "scheme.enquiry",
160            "result": "ok",
161            "schemes": [
162                { "schemeId":"P01", "schemeDesc": [
163                    "JoyYou Promotion",
164                    if simplified_chinese {
165                        "乐悠卡优惠"
166                    } else {
167                        "樂悠卡優惠"
168                    },
169                ] },
170                { "schemeId":"P02", "schemeDesc": [
171                    "Member's Exclusive Offer x Mobile Octopus Promotion",
172                    if simplified_chinese {
173                        "会员专属礼遇 x 手机版八达通特别优惠"
174                    } else {
175                        "會員專屬禮遇 x 手機版八達通特別優惠"
176                    },
177                ] },
178                { "schemeId":"P03", "schemeDesc": [
179                    "Festival Promotion x Octopus Special Offer",
180                    if simplified_chinese {
181                        "节日优惠 x 八达通特别优惠"
182                    } else {
183                        "節日優惠 x 八達通特別優惠"
184                    },
185                ] },
186                { "schemeId":"P04", "schemeDesc": [
187                    "Breakfast Promotion x Octopus Special Offer",
188                    if simplified_chinese {
189                        "早餐时段 x 八达通特别优惠"
190                    } else {
191                        "早餐時段 x 八達通特別優惠"
192                    },
193                ] },
194                { "schemeId":"P05", "schemeDesc": [
195                    "Birthday Offer",
196                    if simplified_chinese {
197                        "生日优惠"
198                    } else {
199                        "生日優惠"
200                    },
201                ] },
202                { "schemeId":"P06", "schemeDesc": [
203                    "Public Transport Discount",
204                    if simplified_chinese {
205                        "公共交通折扣"
206                    } else {
207                        "公共交通折扣"
208                    },
209                ] },
210                { "schemeId":"P07", "schemeDesc": [
211                    "Mobile Octopus Promotion",
212                    if simplified_chinese {
213                        "手机八达通优惠"
214                    } else {
215                        "手機八達通優惠"
216                    },
217                ] },
218                { "schemeId":"P08", "schemeDesc": [
219                    "Shopping Cashback x Octopus Special Offer",
220                    if simplified_chinese {
221                        "购物现金回赠 x 八达通特别优惠"
222                    } else {
223                        "購物現金回贈 x 八達通特別優惠"
224                    },
225                ] },
226                { "schemeId":"P09", "schemeDesc": [
227                    "Student Special",
228                    if simplified_chinese {
229                        "学生优惠"
230                    } else {
231                        "學生優惠"
232                    },
233                ] },
234                { "schemeId":"P10", "schemeDesc": [
235                    "Seasonal Sale x Octopus Special Offer",
236                    if simplified_chinese {
237                        "季节促销 x 八达通特别优惠"
238                    } else {
239                        "季節促銷 x 八達通特別優惠"
240                    },
241                ] },
242            ],
243        }), responseJSON, maxResponseLen)
244    } else if request.command == "trans.request" {
245        if let Some(transaction_data) = request.transaction_data {
246            if transaction_data.typ == "card.enquiry" {
247                let simplified_chinese = request.zh.unwrap_or_default() == "cn";
248                return write_json_response(new_transaction(Command::CardEnquiry(simplified_chinese)), responseJSON, maxResponseLen)
249            } else if transaction_data.typ == "deduct" {
250                let amount = transaction_data.transaction_value.unwrap_or(1);
251                let simplified_chinese = request.zh.unwrap_or_default() == "cn";
252                return write_json_response(new_transaction(Command::Deduct(amount, simplified_chinese)), responseJSON, maxResponseLen)
253            } else if transaction_data.typ == "settle" {
254                let mut settle = new_transaction(Command::Settle);
255                let settle = settle.as_object_mut().unwrap();
256                settle.insert("expiry".to_string(), json!(300));
257                return write_json_response(serde_json::Value::Object(settle.clone()), responseJSON, maxResponseLen)
258            }
259        }
260    } else if request.command == "trans.perform" {
261        if let Some(transaction_id) = request.transaction_id {
262            if let Some((cmd, timestamp)) = TRANSACTIONS.lock().unwrap().get(&transaction_id) {
263                match cmd {
264                    Command::CardEnquiry(simplified_chinese) => {
265                        return write_json_response(enquiry_card(&transaction_id, *simplified_chinese), responseJSON, maxResponseLen)
266                    },
267                    Command::Deduct(amount, simplified_chinese) => {
268                        return write_json_response(deduct(&transaction_id, *amount, *simplified_chinese), responseJSON, maxResponseLen)
269                    },
270                    Command::Settle => {
271                        if get_current_time() - timestamp > 10 {
272                            return write_json_response(settle(&transaction_id), responseJSON, maxResponseLen)
273                        }
274                        std::thread::sleep(std::time::Duration::from_secs(1));
275                        return write_json_response(json!({
276                            "command": "trans.perform",
277                            "transactionId": transaction_id,
278                            "result": "processing",
279                            "transactionData": {
280                                "deviceId": "FFFFFF",
281                                "type": "settle",
282                            },
283                        }), responseJSON, maxResponseLen)
284                    },
285                }
286            }
287        }
288    } else if request.command == "trans.cancel" {
289        if let Some(transaction_id) = request.transaction_id {
290            TRANSACTIONS.lock().unwrap().remove(&transaction_id);
291            return write_json_response(json!({
292                "command": "trans.cancel",
293                "result": "ok",
294            }), responseJSON, maxResponseLen)
295        }
296    }
297    -2
298}
299
300fn new_transaction(cmd: Command) -> Value {
301    let typ = cmd.to_string();
302    let id = generate_random_string(22);
303    let result = {
304        let mut tx = TRANSACTIONS.lock().unwrap();
305        if tx.contains_key(&id) {
306            "processing"
307        } else {
308            let timestamp = get_current_time();
309            tx.insert(id.clone(), (cmd, timestamp));
310            "ok"
311        }
312    };
313    json!({
314        "command": "trans.perform",
315        "transactionId": id,
316        "result": result,
317        "transactionData": {
318            "deviceId": "FFFFFF",
319            "type": typ,
320        },
321    })
322}
323
324fn enquiry_card(id: &str, simplified_chinese: bool) -> Value {
325    if let Ok(card) = std::env::var("CARD") {
326        if card == "1" {
327            return json!({
328                "command": "trans.perform",
329                "transactionId": id,
330                "result": "fail",
331                "errorMessage": ["Invalid card", "此卡失效"],
332                "transactionData": {
333                    "deviceId": "FFFFFF",
334                    "type": "card.enquiry",
335                },
336                "retry": true,
337            })
338        } else if card == "2" {
339            return json!({
340                "transactionLogs": [{
341                    "amount": 0,
342                    "date": "2024-08-01",
343                    "machineId": "FFFF",
344                    "spName": ["OCL", "OCL"],
345                    "spNameLong": ["Octopus Cards Limited", if simplified_chinese {
346                        "八达通卡公司"
347                    } else {
348                        "八達通卡公司"
349                    }],
350                    "spType": 255,
351                    "time": "12:34:56",
352                }],
353                "command": "trans.perform",
354                "transactionId": id,
355                "result": "ok",
356                "transactionData": {
357                    "IDm": "0101010101010101",
358                    "availableFund": 2500,
359                    "cardId": 25123456,
360                    "date": "2025-02-01",
361                    "deviceId": "FFFFFF",
362                    "identity": 0,
363                    "lang": 0,
364                    "octopusType": 1,
365                    "passenger": 33,
366                    "time": "22:33:44",
367                    "type": "card.enquiry",
368                },
369            });
370        }
371    }
372    json!({
373        "transactionLogs": [{
374            "amount": 0,
375            "date": "2024-08-01",
376            "machineId": "FFFF",
377            "spName": ["OCL", "OCL"],
378            "spNameLong": ["Octopus Cards Limited", if simplified_chinese {
379                "八达通卡公司"
380            } else {
381                "八達通卡公司"
382            }],
383            "spType": 255,
384            "time": "12:34:56",
385        }],
386        "command": "trans.perform",
387        "transactionId": id,
388        "result": "ok",
389        "transactionData": {
390            "IDm": "0101010101010101",
391            "availableFund": 2500,
392            "cardId": 25123456,
393            "date": "2025-02-01",
394            "deviceId": "FFFFFF",
395            "identity": 0,
396            "lang": 0,
397            "lastAddValueDate": "2025-01-01",
398            "lastAddValueDevice": "FFFFFF",
399            "lastAddValueType": 1,
400            "maxAddValueAllow": 30000,
401            "octopusType": 0,
402            "passenger": 3,
403            "remainingValue": 0,
404            "time": "22:33:44",
405            "type": "card.enquiry",
406        },
407    })
408}
409
410fn deduct(id: &str, amount: i32, simplified_chinese: bool) -> Value {
411    json!({
412        "command": "trans.perform",
413        "printData": [
414            {
415                "type": "line",
416                "values": [
417                    concat!(
418                        "This is program test. If you have any queries on the above transaction, ",
419                        "please keep the receipt and call Octopus Customer Service Hotline at ",
420                        "2266-2222 or visit www.octopus.com.hk"
421                    ),
422                    if simplified_chinese {
423                        concat!(
424                            "程序测试。客户如对上述的交易有任何查询,请保存此存根及致电八达通顾客",
425                            "服务热线2266-2222,或浏览www.octopus.com.hk",
426                        )
427                    } else {
428                        concat!(
429                            "程序測試。客戶如對上述的交易有任何查詢,請保存此存根及致電八達通顧客",
430                            "服務熱線2266-2222,或瀏覽www.octopus.com.hk",
431                        )
432                    }
433                ],
434                "format": 0
435            },
436            {
437                "type": "line",
438                "values": [
439                    "**********************",
440                    "**********************"
441                ],
442                "format": 1
443            },
444            {
445                "type": "line",
446                "values": [
447                    "* Invalid transaction *",
448                    if simplified_chinese {
449                        "*      此交易无效     *"
450                    } else {
451                        "*      此交易無效     *"
452                    }
453                ],
454                "format": 1
455            },
456            {
457                "type": "line",
458                "values": [
459                    "**********************",
460                    "**********************"
461                ],
462                "format": 1
463            },
464            {
465                "type": "sep",
466                "values": [
467                    " "
468                ]
469            },
470            {
471                "type": "l.r",
472                "values": [
473                    "Transaction Type",
474                    "Terminal",
475                    if simplified_chinese {
476                        "交易类型"
477                    } else {
478                        "交易類型"
479                    },
480                    if simplified_chinese {
481                        "终端机"
482                    } else {
483                        "終端機"
484                    },
485                ]
486            },
487            {
488                "type": "line",
489                "values": [
490                    "R/W FW V2.05",
491                    "R/W 版本V2.05"
492                ],
493                "format": 2
494            },
495            {
496                "type": "sep",
497                "values": [
498                    "-"
499                ]
500            }
501        ],
502        "transactionId": id,
503        "displayData": [
504            {
505                "type": "line",
506                "values": [
507                    "Thank you for using Octopus",
508                    if simplified_chinese {
509                        "多谢使用八达通"
510                    } else {
511                        "多謝使用八達通"
512                    }
513                ],
514                "format": 1
515            },
516            {
517                "type": "line",
518                "values": [
519                    "Remarks",
520                    if simplified_chinese {
521                        "备注"
522                    } else {
523                        "備註"
524                    }
525                ],
526                "format": 0
527            },
528            {
529                "type": "l.r",
530                "values": [
531                    "Transaction Type",
532                    "Terminal",
533                    if simplified_chinese {
534                        "交易类型"
535                    } else {
536                        "交易類型"
537                    },
538                    if simplified_chinese {
539                        "终端机"
540                    } else {
541                        "終端機"
542                    },
543                ]
544            },
545            {
546                "type": "line",
547                "values": [
548                    "R/W FW V2.05",
549                    "R/W 版本V2.05"
550                ],
551                "format": 2
552            }
553        ],
554        "result": "ok",
555        "transactionData": {
556            "IDm": "0101010101010101",
557            "availableFund": 0,
558            "cardId": 25123456,
559            "date": "2025-02-28",
560            "deviceId": "FFFFFF",
561            "identity": 0,
562            "lang": 0,
563            "lastAddValueDate": "2025-02-28",
564            "lastAddValueDevice": "FFFFFF",
565            "lastAddValueType": 4,
566            "maxAddValueAllow": 30000,
567            "octopusType": 0,
568            "passenger": 3,
569            "remainingValue": 2500 - amount,
570            "time": "12:52:57",
571            "transactionValue": amount,
572            "type": "deduct",
573            "schemeName": if amount >= 400 {
574                Value::Array(vec![
575                    Value::String("VIP x Octopus Offer".to_string()),
576                    Value::String("商戶 x 八達通優惠".to_string())
577                ])
578            } else {
579                Value::Null
580            },
581            "discountAmount": if amount >= 400 {
582                json!((amount as f64 * 0.1) as i32)
583            } else {
584                Value::Null
585            },
586        }
587    })
588}
589
590fn settle(id: &str) -> Value {
591    json!({
592        "upgrade": "firmware",
593        "settlementData": {
594            "deductCount": 11,
595            "deductTotal": 11,
596            "addValueCount": 0,
597            "addValueTotal": 0,
598            "xfilename": "MPS.00800.FFFFFF.20250309154455.00001",
599            "date": "2025-03-09",
600            "time": "15:44:56",
601        },
602        "command": "trans.perform",
603        "transactionId": id,
604        "result": "ok",
605        "transactionData": {
606            "date": "2025-03-09",
607            "deviceId": "FFFFFF",
608            "time": "15:44:56",
609            "type": "settle",
610        },
611    })
612}
613
614fn get_config() -> Value {
615    json!({
616        "alertMessage": 0,
617        "hwId": "OR30000000",
618        "os": "V12.2.5-2.65",
619        "ssid": "",
620        "wifi": "off",
621        "ip": "192.168.1.100",
622        "carrier": "",
623        "simId": "454000000000000",
624        "network": 3,
625        "ocrl": "7100.64",
626        "ethDHCP": true,
627        "command": "config.get",
628        "result": "ok",
629        "deviceId": "FFFFFF",
630        "readerDate": "23-02-2025",
631        "readerTime": "23:00:00",
632        "compId": 800,
633        "keyVersion": 0,
634        "eod": 3000,
635        "cobrandwhitelist": 0,
636        "blacklist": 0,
637        "firmware": "15073485",
638        "cchs": 28,
639        "interimBlackList": 260000,
640        "spbmlist": 0,
641        "funcBlacklist": 5000,
642        "subsidylist": 0,
643        "actionlist": 0,
644        "genericResFile": 7,
645        "specificResFile": 1,
646        "spntlist": 14,
647    })
648}
649
650#[allow(non_snake_case)]
651fn write_json_response(response: Value, responseJSON: *mut c_char, maxResponseLen: i32) -> i32 {
652    if responseJSON.is_null() || maxResponseLen <= 0 {
653        return -2;
654    }
655
656    let resp_str = match serde_json::to_string(&response) {
657        Ok(s) => s,
658        Err(_) => return -2,
659    };
660
661    let c_str = match CString::new(resp_str) {
662        Ok(c_str) => c_str,
663        Err(_) => return -2,
664    };
665
666    if c_str.as_bytes_with_nul().len() > maxResponseLen as usize {
667        return -3;
668    }
669
670    unsafe {
671        std::ptr::copy_nonoverlapping(
672            c_str.as_ptr() as *const u8,
673            responseJSON as *mut u8,
674            c_str.as_bytes_with_nul().len()
675        );
676    }
677
678    0
679}
680
681fn generate_random_string(length: usize) -> String {
682    let chars: &[u8] = b"0123456789ABCDEF";
683    let now = std::time::SystemTime::now()
684        .duration_since(std::time::UNIX_EPOCH)
685        .unwrap_or_default();
686    (0..length)
687        .map(|i| {
688            let hash = (now.as_micros() as usize)
689                .wrapping_add(i)
690                .wrapping_mul(0x9E3779B9);
691            chars[hash % chars.len()] as char
692        })
693        .collect()
694}
695
696fn get_current_time() -> u64 {
697    let start = std::time::SystemTime::now();
698    let since_epoch = start.duration_since(std::time::UNIX_EPOCH).expect("Time went backwards");
699    since_epoch.as_secs()
700}
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705    use std::ffi::CString;
706    use std::ptr;
707
708    fn create_c_buffer(size: usize) -> Vec<u8> {
709        vec![0u8; size]
710    }
711
712    fn get_string_from_buffer(buffer: &[u8]) -> String {
713        let null_pos = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
714        String::from_utf8_lossy(&buffer[..null_pos]).to_string()
715    }
716
717    #[test]
718    fn test_connect_success() {
719        let com_port = CString::new("/dev/ttyS0").unwrap();
720        let result = Connect(com_port.as_ptr() as *mut c_char);
721        assert_eq!(result, 0);
722    }
723
724    #[test]
725    fn test_connect_null_ptr() {
726        let result = Connect(ptr::null_mut());
727        assert_eq!(result, 0);
728    }
729
730    #[test]
731    fn test_disconnect() {
732        Disconnect();
733    }
734
735    #[test]
736    fn test_cmd_exchange_null_pointers() {
737        let result = CmdExchange(ptr::null_mut(), ptr::null_mut(), 4096);
738        assert_eq!(result, -2);
739    }
740
741    #[test]
742    fn test_cmd_exchange_insufficient_buffer() {
743        let request = CString::new(r#"{"command": "config.get"}"#).unwrap();
744        let mut response_buffer = create_c_buffer(1000);
745
746        let result = CmdExchange(
747            request.as_ptr() as *mut c_char,
748            response_buffer.as_mut_ptr() as *mut c_char,
749            1000
750        );
751        assert_eq!(result, -3);
752    }
753
754    #[test]
755    fn test_config_get() {
756        let request = CString::new(r#"{"command": "config.get"}"#).unwrap();
757        let mut response_buffer = create_c_buffer(4096);
758
759        let result = CmdExchange(
760            request.as_ptr() as *mut c_char,
761            response_buffer.as_mut_ptr() as *mut c_char,
762            4096
763        );
764
765        assert_eq!(result, 0);
766        let response = get_string_from_buffer(&response_buffer);
767        let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
768        assert_eq!(parsed["command"], "config.get");
769        assert_eq!(parsed["result"], "ok");
770        assert!(parsed["deviceId"].is_string());
771    }
772
773    #[test]
774    fn test_display_img_valid() {
775        let request = CString::new(r#"{"command": "display.img", "resId": "1"}"#).unwrap();
776        let mut response_buffer = create_c_buffer(4096);
777
778        let result = CmdExchange(
779            request.as_ptr() as *mut c_char,
780            response_buffer.as_mut_ptr() as *mut c_char,
781            4096
782        );
783
784        assert_eq!(result, 0);
785        let response = get_string_from_buffer(&response_buffer);
786        let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
787        assert_eq!(parsed["command"], "display.img");
788        assert_eq!(parsed["result"], "ok");
789    }
790
791    #[test]
792    fn test_display_img_invalid() {
793        let request = CString::new(r#"{"command": "display.img", "resId": "10"}"#).unwrap();
794        let mut response_buffer = create_c_buffer(4096);
795
796        let result = CmdExchange(
797            request.as_ptr() as *mut c_char,
798            response_buffer.as_mut_ptr() as *mut c_char,
799            4096
800        );
801
802        assert_eq!(result, 0);
803        let response = get_string_from_buffer(&response_buffer);
804        let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
805        assert_eq!(parsed["command"], "display.img");
806        assert_eq!(parsed["result"], "fail");
807    }
808
809    #[test]
810    fn test_scheme_enquiry() {
811        let request = CString::new(r#"{"command": "scheme.enquiry"}"#).unwrap();
812        let mut response_buffer = create_c_buffer(4096);
813
814        let result = CmdExchange(
815            request.as_ptr() as *mut c_char,
816            response_buffer.as_mut_ptr() as *mut c_char,
817            4096
818        );
819
820        assert_eq!(result, 0);
821        let response = get_string_from_buffer(&response_buffer);
822        let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
823        assert_eq!(parsed["command"], "scheme.enquiry");
824        assert_eq!(parsed["result"], "ok");
825        assert!(parsed["schemes"].is_array());
826        assert!(parsed["schemes"].as_array().unwrap().len() > 0);
827    }
828
829    #[test]
830    fn test_scheme_enquiry_simplified_chinese() {
831        let request = CString::new(r#"{"command": "scheme.enquiry", "zh": "cn"}"#).unwrap();
832        let mut response_buffer = create_c_buffer(4096);
833
834        let result = CmdExchange(
835            request.as_ptr() as *mut c_char,
836            response_buffer.as_mut_ptr() as *mut c_char,
837            4096
838        );
839
840        assert_eq!(result, 0);
841        let response = get_string_from_buffer(&response_buffer);
842        let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
843        assert_eq!(parsed["command"], "scheme.enquiry");
844        assert_eq!(parsed["result"], "ok");
845
846        let schemes = parsed["schemes"].as_array().unwrap();
847        let first_scheme = &schemes[0];
848        let desc = first_scheme["schemeDesc"].as_array().unwrap();
849        assert!(desc[1].as_str().unwrap().contains("乐"));
850    }
851
852    #[test]
853    fn test_trans_request_card_enquiry() {
854        let request = CString::new(r#"{"command": "trans.request", "transactionData": {"type": "card.enquiry"}}"#).unwrap();
855        let mut response_buffer = create_c_buffer(4096);
856
857        let result = CmdExchange(
858            request.as_ptr() as *mut c_char,
859            response_buffer.as_mut_ptr() as *mut c_char,
860            4096
861        );
862
863        assert_eq!(result, 0);
864        let response = get_string_from_buffer(&response_buffer);
865        let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
866        assert_eq!(parsed["command"], "trans.perform");
867        assert!(parsed["result"] == "ok" || parsed["result"] == "processing");
868        assert!(parsed["transactionId"].is_string());
869        assert_eq!(parsed["transactionData"]["type"], "card.enquiry");
870    }
871
872    #[test]
873    fn test_trans_request_deduct() {
874        let request = CString::new(r#"{"command": "trans.request", "transactionData": {"type": "deduct", "transactionValue": 100}}"#).unwrap();
875        let mut response_buffer = create_c_buffer(4096);
876
877        let result = CmdExchange(
878            request.as_ptr() as *mut c_char,
879            response_buffer.as_mut_ptr() as *mut c_char,
880            4096
881        );
882
883        assert_eq!(result, 0);
884        let response = get_string_from_buffer(&response_buffer);
885        let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
886        assert_eq!(parsed["command"], "trans.perform");
887        assert!(parsed["result"] == "ok" || parsed["result"] == "processing");
888        assert!(parsed["transactionId"].is_string());
889        assert_eq!(parsed["transactionData"]["type"], "deduct");
890    }
891
892    #[test]
893    fn test_trans_request_settle() {
894        let request = CString::new(r#"{"command": "trans.request", "transactionData": {"type": "settle"}}"#).unwrap();
895        let mut response_buffer = create_c_buffer(4096);
896
897        let result = CmdExchange(
898            request.as_ptr() as *mut c_char,
899            response_buffer.as_mut_ptr() as *mut c_char,
900            4096
901        );
902
903        assert_eq!(result, 0);
904        let response = get_string_from_buffer(&response_buffer);
905        let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
906        assert_eq!(parsed["command"], "trans.perform");
907        assert!(parsed["result"] == "ok" || parsed["result"] == "processing");
908        assert!(parsed["transactionId"].is_string());
909        assert_eq!(parsed["transactionData"]["type"], "settle");
910        assert_eq!(parsed["expiry"], 300);
911    }
912
913    #[test]
914    fn test_trans_perform_card_enquiry() {
915        let request = CString::new(r#"{"command": "trans.request", "transactionData": {"type": "card.enquiry"}}"#).unwrap();
916        let mut response_buffer = create_c_buffer(4096);
917
918        CmdExchange(
919            request.as_ptr() as *mut c_char,
920            response_buffer.as_mut_ptr() as *mut c_char,
921            4096
922        );
923
924        let response = get_string_from_buffer(&response_buffer);
925        let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
926        let transaction_id = parsed["transactionId"].as_str().unwrap();
927
928        let perform_request = format!(r#"{{"command": "trans.perform", "transactionId": "{}"}}"#, transaction_id);
929        let perform_cstr = CString::new(perform_request).unwrap();
930        let mut perform_buffer = create_c_buffer(4096);
931
932        let result = CmdExchange(
933            perform_cstr.as_ptr() as *mut c_char,
934            perform_buffer.as_mut_ptr() as *mut c_char,
935            4096
936        );
937
938        assert_eq!(result, 0);
939        let perform_response = get_string_from_buffer(&perform_buffer);
940        let perform_parsed: serde_json::Value = serde_json::from_str(&perform_response).unwrap();
941        assert_eq!(perform_parsed["command"], "trans.perform");
942        assert_eq!(perform_parsed["result"], "ok");
943        assert!(perform_parsed["transactionData"]["IDm"].is_string());
944        assert!(perform_parsed["transactionData"]["availableFund"].is_number());
945    }
946
947    #[test]
948    fn test_trans_cancel() {
949        let request = CString::new(r#"{"command": "trans.request", "transactionData": {"type": "card.enquiry"}}"#).unwrap();
950        let mut response_buffer = create_c_buffer(4096);
951
952        CmdExchange(
953            request.as_ptr() as *mut c_char,
954            response_buffer.as_mut_ptr() as *mut c_char,
955            4096
956        );
957
958        let response = get_string_from_buffer(&response_buffer);
959        let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
960        let transaction_id = parsed["transactionId"].as_str().unwrap();
961
962        let cancel_request = format!(r#"{{"command": "trans.cancel", "transactionId": "{}"}}"#, transaction_id);
963        let cancel_cstr = CString::new(cancel_request).unwrap();
964        let mut cancel_buffer = create_c_buffer(4096);
965
966        let result = CmdExchange(
967            cancel_cstr.as_ptr() as *mut c_char,
968            cancel_buffer.as_mut_ptr() as *mut c_char,
969            4096
970        );
971
972        assert_eq!(result, 0);
973        let cancel_response = get_string_from_buffer(&cancel_buffer);
974        let cancel_parsed: serde_json::Value = serde_json::from_str(&cancel_response).unwrap();
975        assert_eq!(cancel_parsed["command"], "trans.cancel");
976        assert_eq!(cancel_parsed["result"], "ok");
977    }
978
979    #[test]
980    fn test_invalid_json() {
981        let request = CString::new(r#"{"invalid": json}"#).unwrap();
982        let mut response_buffer = create_c_buffer(4096);
983
984        let result = CmdExchange(
985            request.as_ptr() as *mut c_char,
986            response_buffer.as_mut_ptr() as *mut c_char,
987            4096
988        );
989
990        assert_eq!(result, -2);
991    }
992
993    #[test]
994    fn test_unknown_command() {
995        let request = CString::new(r#"{"command": "unknown.command"}"#).unwrap();
996        let mut response_buffer = create_c_buffer(4096);
997
998        let result = CmdExchange(
999            request.as_ptr() as *mut c_char,
1000            response_buffer.as_mut_ptr() as *mut c_char,
1001            4096
1002        );
1003
1004        assert_eq!(result, -2);
1005    }
1006
1007    #[test]
1008    fn test_generate_random_string() {
1009        let str1 = generate_random_string(10);
1010        let str2 = generate_random_string(10);
1011
1012        assert_eq!(str1.len(), 10);
1013        assert_eq!(str2.len(), 10);
1014
1015        for ch in str1.chars() {
1016            assert!(ch.is_ascii_hexdigit());
1017        }
1018        for ch in str2.chars() {
1019            assert!(ch.is_ascii_hexdigit());
1020        }
1021
1022        let mut strings = std::collections::HashSet::new();
1023        for _ in 0..100 {
1024            let s = generate_random_string(10);
1025            assert_eq!(s.len(), 10);
1026            strings.insert(s);
1027        }
1028        assert!(strings.len() > 10);
1029    }
1030
1031    #[test]
1032    fn test_get_current_time() {
1033        let time1 = get_current_time();
1034        std::thread::sleep(std::time::Duration::from_millis(10));
1035        let time2 = get_current_time();
1036
1037        assert!(time2 >= time1);
1038    }
1039
1040    #[test]
1041    fn test_card() {
1042        test_card_invalid_scenario();
1043        test_card_with_history_scenario();
1044    }
1045
1046    fn test_card_invalid_scenario() {
1047        std::env::remove_var("CARD");
1048        std::env::set_var("CARD", "1");
1049
1050        assert_eq!(std::env::var("CARD").unwrap(), "1");
1051
1052        let request = CString::new(r#"{"command": "trans.request", "transactionData": {"type": "card.enquiry"}}"#).unwrap();
1053        let mut response_buffer = create_c_buffer(4096);
1054
1055        CmdExchange(
1056            request.as_ptr() as *mut c_char,
1057            response_buffer.as_mut_ptr() as *mut c_char,
1058            4096
1059        );
1060
1061        let response = get_string_from_buffer(&response_buffer);
1062        let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
1063        let transaction_id = parsed["transactionId"].as_str().unwrap();
1064
1065        let perform_request = format!(r#"{{"command": "trans.perform", "transactionId": "{}"}}"#, transaction_id);
1066        let perform_cstr = CString::new(perform_request).unwrap();
1067        let mut perform_buffer = create_c_buffer(4096);
1068
1069        CmdExchange(
1070            perform_cstr.as_ptr() as *mut c_char,
1071            perform_buffer.as_mut_ptr() as *mut c_char,
1072            4096
1073        );
1074
1075        let perform_response = get_string_from_buffer(&perform_buffer);
1076        let perform_parsed: serde_json::Value = serde_json::from_str(&perform_response).unwrap();
1077
1078        assert!(perform_parsed["result"] == "fail");
1079        assert!(perform_parsed["errorMessage"].is_array());
1080        assert_eq!(perform_parsed["retry"], true);
1081
1082        std::env::remove_var("CARD");
1083    }
1084
1085    fn test_card_with_history_scenario() {
1086        std::env::remove_var("CARD");
1087        std::env::set_var("CARD", "2");
1088
1089        let request = CString::new(r#"{"command": "trans.request", "transactionData": {"type": "card.enquiry"}}"#).unwrap();
1090        let mut response_buffer = create_c_buffer(4096);
1091
1092        CmdExchange(
1093            request.as_ptr() as *mut c_char,
1094            response_buffer.as_mut_ptr() as *mut c_char,
1095            4096
1096        );
1097
1098        let response = get_string_from_buffer(&response_buffer);
1099        let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
1100        let transaction_id = parsed["transactionId"].as_str().unwrap();
1101
1102        let perform_request = format!(r#"{{"command": "trans.perform", "transactionId": "{}"}}"#, transaction_id);
1103        let perform_cstr = CString::new(perform_request).unwrap();
1104        let mut perform_buffer = create_c_buffer(4096);
1105
1106        CmdExchange(
1107            perform_cstr.as_ptr() as *mut c_char,
1108            perform_buffer.as_mut_ptr() as *mut c_char,
1109            4096
1110        );
1111
1112        let perform_response = get_string_from_buffer(&perform_buffer);
1113        let perform_parsed: serde_json::Value = serde_json::from_str(&perform_response).unwrap();
1114        assert_eq!(perform_parsed["result"], "ok");
1115        assert!(perform_parsed["transactionLogs"].is_array());
1116        assert!(perform_parsed["transactionData"]["octopusType"].is_number());
1117
1118        std::env::remove_var("CARD");
1119    }
1120}