1use 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#[no_mangle]
66pub extern "C" fn Connect(_com: *mut c_char) -> i32 {
67 0
68}
69
70#[no_mangle]
75pub extern "C" fn Disconnect() {}
76
77#[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 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}