Skip to content

Commit fe42f33

Browse files
authored
Merge pull request algorandfoundation#6 from algorandfoundation/tests/use_testing_frameworks
test(algo_models_ffi): use bun test runner and pytest
2 parents 5f81454 + 1def1eb commit fe42f33

File tree

13 files changed

+917
-307
lines changed

13 files changed

+917
-307
lines changed

.gitignore

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
/target
2-
32
.vscode/
4-
!/.vscode/settings.recommended.json
5-
63
.zed/
7-
!/.zed/settings.recommended.json
4+
.venv/

crates/algo_models/src/lib.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ pub enum MsgPackError {
2222

2323
#[error("Unknown transaction type")]
2424
UnknownTransactionType,
25+
26+
#[error("Invalid input: {0}")]
27+
InputError(String),
2528
}
2629

2730
pub trait AlgorandMsgpack: Serialize + for<'de> Deserialize<'de> {
@@ -49,7 +52,13 @@ pub trait AlgorandMsgpack: Serialize + for<'de> Deserialize<'de> {
4952

5053
/// Decode the bytes into Self. "TX" prefix is ignored if present
5154
fn decode(bytes: &[u8]) -> Result<Self, MsgPackError> {
52-
if bytes[0] == b'T' && bytes[1] == b'X' {
55+
if bytes.is_empty() {
56+
return Err(MsgPackError::InputError(
57+
"attempted to decode 0 bytes".to_string(),
58+
));
59+
}
60+
61+
if bytes.len() > 2 && bytes[0] == b'T' && bytes[1] == b'X' {
5362
let without_prefix = bytes[2..].to_vec();
5463
Ok(rmp_serde::from_slice(&without_prefix)?)
5564
} else {

crates/algo_models_ffi/src/lib.rs

+42-3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ impl From<algo_models::MsgPackError> for MsgPackError {
3838
algo_models::MsgPackError::UnknownTransactionType => {
3939
MsgPackError::DecodingError(e.to_string())
4040
}
41+
algo_models::MsgPackError::InputError(e) => MsgPackError::DecodingError(e.to_string()),
4142
}
4243
}
4344
}
@@ -373,9 +374,12 @@ impl From<algo_models::TransactionType> for TransactionType {
373374
/// Get the transaction type from the encoded transaction.
374375
/// This is particularly useful when decoding a transaction that has a unknow type
375376
pub fn get_encoded_transaction_type(bytes: &[u8]) -> Result<TransactionType, MsgPackError> {
376-
let header: TransactionHeader =
377-
rmp_serde::from_slice(bytes).map_err(|w| MsgPackError::DecodingError(w.to_string()))?;
378-
Ok(header.transaction_type)
377+
let decoded = algo_models::Transaction::decode(bytes)?;
378+
379+
match decoded {
380+
algo_models::Transaction::Payment(_) => Ok(TransactionType::Payment),
381+
algo_models::Transaction::AssetTransfer(_) => Ok(TransactionType::AssetTransfer),
382+
}
379383
}
380384

381385
#[cfg_attr(feature = "ffi_wasm", wasm_bindgen(js_name = "encodePayment"))]
@@ -416,3 +420,38 @@ pub fn attach_signature(encoded_tx: &[u8], signature: &[u8]) -> Result<Vec<u8>,
416420
};
417421
Ok(signed_tx.encode()?)
418422
}
423+
424+
#[cfg(test)]
425+
mod tests {
426+
use super::*;
427+
428+
#[test]
429+
fn test_get_encoded_transaction_type() {
430+
// Create a minimal payment transaction
431+
let tx = PayTransactionFields {
432+
header: TransactionHeader {
433+
transaction_type: TransactionType::Payment,
434+
sender: ByteBuf::from(vec![0; 32]), // 32-byte dummy public key
435+
fee: 1000,
436+
first_valid: 1000,
437+
last_valid: 2000,
438+
genesis_hash: None,
439+
genesis_id: None,
440+
note: None,
441+
rekey_to: None,
442+
lease: None,
443+
group: None,
444+
},
445+
receiver: ByteBuf::from(vec![1; 32]), // 32-byte dummy receiver
446+
amount: 1000000,
447+
close_remainder_to: None,
448+
};
449+
450+
// Encode the transaction
451+
let encoded = encode_payment(tx).unwrap();
452+
453+
// Test the get_encoded_transaction_type function
454+
let tx_type = get_encoded_transaction_type(&encoded).unwrap();
455+
assert_eq!(tx_type, TransactionType::Payment);
456+
}
457+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# This script is used to generate test_data.json, which is then used in the language binding tests
2+
# This script should only be ran when the data is updated (i.e. a new transaction type is added)
3+
4+
from algosdk import account, transaction, encoding, constants
5+
from algosdk.v2client.algod import AlgodClient
6+
import base64
7+
import json
8+
9+
[sender_sk, sender_addr] = account.generate_account()
10+
[_, receiver_addr] = account.generate_account()
11+
12+
13+
algod = AlgodClient("", "https://testnet-api.4160.nodely.dev")
14+
sp = algod.suggested_params()
15+
16+
17+
def get_bytes_for_signing(tx: transaction.Transaction):
18+
return list(constants.txid_prefix + base64.b64decode(encoding.msgpack_encode(pay)))
19+
20+
21+
def addr_bytes(addr):
22+
return list(encoding.decode_address(addr))
23+
24+
25+
def b64_bytes(data):
26+
return list(base64.b64decode(data))
27+
28+
29+
pay = transaction.PaymentTxn(
30+
sender=sender_addr,
31+
receiver=receiver_addr,
32+
amt=1000,
33+
close_remainder_to=None,
34+
note=None,
35+
lease=None,
36+
rekey_to=None,
37+
sp=sp,
38+
)
39+
40+
stxn = pay.sign(sender_sk)
41+
42+
data = {
43+
"privKey": list(base64.b64decode(sender_sk))[0:32],
44+
"fields": {
45+
"header": {
46+
"sender": addr_bytes(pay.sender),
47+
"fee": pay.fee,
48+
"transactionType": "Payment",
49+
"firstValid": pay.first_valid_round,
50+
"lastValid": pay.last_valid_round,
51+
"genesisHash": b64_bytes(pay.genesis_hash),
52+
"genesisId": pay.genesis_id,
53+
},
54+
"receiver": addr_bytes(pay.receiver),
55+
"amount": pay.amt,
56+
},
57+
"expectedBytesForSigning": get_bytes_for_signing(pay),
58+
"expectedSignedTxn": b64_bytes(encoding.msgpack_encode(stxn)),
59+
}
60+
61+
62+
with open("./test_data.json", "w") as f:
63+
json.dump(data, f, indent=2)
1.75 KB
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { expect, test, describe } from "bun:test";
2+
import * as ed from "@noble/ed25519";
3+
import init, {
4+
type PayTransactionFields,
5+
encodePayment,
6+
attachSignature,
7+
decodePayment,
8+
getEncodedTransactionType,
9+
} from "./pkg/algo_models_ffi";
10+
import path from "path";
11+
12+
describe("algo_models WASM", async () => {
13+
await init();
14+
15+
const jsonString = await Bun.file(
16+
path.join(__dirname, "../test_data.json")
17+
).text();
18+
19+
const testData = JSON.parse(jsonString, (_, value) => {
20+
if (Array.isArray(value) && value.every((n) => typeof n === "number")) {
21+
return new Uint8Array(value);
22+
}
23+
24+
if (
25+
typeof value === "number" &&
26+
["fee", "amount", "firstValid", "lastValid"].includes(_)
27+
) {
28+
return BigInt(value);
29+
}
30+
31+
return value;
32+
});
33+
34+
const privKey = testData.privKey;
35+
36+
describe("payment", () => {
37+
const fields: PayTransactionFields = testData.fields;
38+
const expectedSignedTxn = testData.expectedSignedTxn;
39+
const expectedBytesForSigning = testData.expectedBytesForSigning;
40+
41+
test("encode", () => {
42+
expect(encodePayment(fields)).toEqual(expectedBytesForSigning);
43+
});
44+
45+
test("encode with signature", async () => {
46+
const sig = await ed.signAsync(expectedBytesForSigning, privKey);
47+
const signedTx = attachSignature(expectedBytesForSigning, sig);
48+
expect(signedTx).toEqual(expectedSignedTxn);
49+
});
50+
51+
test("decode (with TX prefix)", () => {
52+
expect(decodePayment(expectedBytesForSigning)).toEqual(fields);
53+
});
54+
55+
test("decode (without TX prefix)", () => {
56+
expect(decodePayment(expectedBytesForSigning.slice(2))).toEqual(fields);
57+
});
58+
59+
test("getEncodedTransactionType", () => {
60+
expect(getEncodedTransactionType(expectedBytesForSigning)).toBe(
61+
"Payment"
62+
);
63+
});
64+
65+
// TODO: Decide if this is the behavior we want or if there should be input validation on encode
66+
test("encode/decode with extra field", () => {
67+
const extraField = { ...fields, foo: "bar" };
68+
const encoded = encodePayment(extraField);
69+
expect(decodePayment(encoded)).not.toContainKey("foo");
70+
});
71+
72+
test("DecodingError: 0 bytes", () => {
73+
expect(() => decodePayment(new Uint8Array(0))).toThrow(
74+
"DecodingError: attempted to decode 0 bytes"
75+
);
76+
});
77+
78+
test("DecodingError: malformed bytes", () => {
79+
const badBytes = expectedBytesForSigning.slice();
80+
badBytes[13] = 37;
81+
expect(() => decodePayment(badBytes)).toThrow(
82+
"DecodingError: Error ocurred during decoding: missing field `fee`"
83+
);
84+
});
85+
86+
test("Error: invalid type", () => {
87+
const badFields = { ...fields, header: { fee: "foo" } };
88+
// @ts-expect-error known bad type for testing purposes
89+
expect(() => encodePayment(badFields)).toThrow(
90+
'Error: invalid type: string "foo", expected u64'
91+
);
92+
});
93+
});
94+
});

crates/algo_models_ffi/tests/js/index.ts

-104
This file was deleted.

crates/algo_models_ffi/tests/js/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,8 @@
1313
"@algorandfoundation/algo-models": "^0.0.2",
1414
"@noble/ed25519": "^2.1.0",
1515
"algo-msgpack-with-bigint": "^2.1.1"
16+
},
17+
"devDependencies": {
18+
"@types/bun": "^1.1.14"
1619
}
1720
}

crates/algo_models_ffi/tests/js/tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
4343
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
4444
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
45-
// "resolveJsonModule": true, /* Enable importing .json files. */
45+
"resolveJsonModule": true, /* Enable importing .json files. */
4646
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
4747
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
4848

0 commit comments

Comments
 (0)