Menguji Operasi Saldo E-Wallet dengan Go Fuzzing
Pendahuluan
Saat mengembangkan sistem e-wallet, saya memiliki validasi yang cukup umum pada proses top up.
if req.Amount <= 0 {
return errors.New("amount must be greater than 0")
}Sekilas validasi tersebut terlihat cukup. Selama nominal transaksi lebih dari nol, proses dapat dilanjutkan.
Namun setelah melihat kembali implementasinya, saya menyadari bahwa validasi tersebut hanya memeriksa input, bukan hasil perhitungan.
newBalance := wallet.Balance + amountKalau wallet.Balance sudah mendekati batas maksimum int64, operasi tersebut berpotensi mengalami integer overflow. Nilai amount tetap valid, tetapi hasil akhirnya tidak lagi valid.
Untuk memastikan apakah masih ada kondisi yang terlewat, saya mencoba menggunakan Go Fuzzing.
Kenapa Go Fuzzing?
referensi: https://go.dev/doc/tutorial/fuzz
Unit test bekerja berdasarkan skenario yang kita tentukan sendiri.
amount = 1000
amount = 50000
amount = -1Pendekatan tersebut cukup untuk menguji alur bisnis, tetapi kurang efektif untuk menemukan kombinasi input ekstrem yang tidak pernah terpikirkan sebelumnya.
Go Fuzzing mengambil pendekatan berbeda. Framework akan menghasilkan ribuan kombinasi input secara otomatis, kemudian mencoba mencari kondisi yang melanggar aturan yang sudah kita tentukan.
Pada artikel ini saya menggunakan fuzzing bukan untuk mencari panic, tetapi untuk memastikan operasi perubahan saldo selalu memenuhi invariant yang seharusnya berlaku.
Top Up: Validasi Input Belum Cukup
Implementasi awal top up melakukan penambahan saldo secara langsung.
newBalance := wallet.Balance + amountValidasi hanya memastikan amount > 0.
Masalahnya, tidak ada yang memastikan hasil penjumlahan masih berada dalam rentang int64.
Misalnya saldo wallet berada sangat dekat dengan math.MaxInt64.
balance = 9223372036854775800
amount = 10000Operasi tersebut akan mengalami integer overflow.
Masalahnya bukan pada input, tetapi pada hasil perhitungannya.
Memisahkan Business Logic
Saya tidak langsung menjalankan fuzzing terhadap service karena service masih bergantung pada database.
Sebagai gantinya, logika perhitungan dipindahkan ke fungsi murni.
func validateAndApplyTopUp(currentBalance, amount int64) (int64, error)Dengan cara ini, fuzz test hanya menguji proses perhitungan saldo tanpa dipengaruhi transaksi database.
Selain lebih cepat, hasil pengujiannya juga lebih konsisten.
Invariant yang Diuji
Daripada memeriksa nilai tertentu, fuzz test memverifikasi beberapa aturan.
-
overflow harus menghasilkan error
-
saldo baru tidak boleh lebih kecil dari saldo lama
-
jika tidak ada error, hasil perhitungan harus valid
Hasil Pengujian
Setelah validasi overflow ditambahkan, saya menjalankan Go Fuzzing selama 10 detik.

Go berhasil mengeksekusi sekitar 145.796 kombinasi input dengan kecepatan sekitar 14 ribu eksekusi per detik tanpa menemukan pelanggaran terhadap invariant yang sudah ditentukan.
Hasil tersebut tidak membuktikan implementasi bebas bug. Namun, setidaknya memberikan keyakinan bahwa logika top up mampu menangani berbagai kombinasi input, termasuk nilai-nilai ekstrem yang sulit dicakup oleh unit test biasa.
Dari Satu Bug Menjadi Audit Seluruh Sistem
Perbaikan pada top up memunculkan pertanyaan lain.
Kalau operasi berikut memiliki potensi overflow,
balance + amountapakah pola yang sama juga muncul di bagian lain sistem?
Saya kemudian menelusuri seluruh proses yang melakukan perubahan saldo.
Beberapa di antaranya adalah:
-
ConfirmTopUp -
TransferWithMetadataTx
Audit tersebut menghasilkan dua temuan berikutnya.
ConfirmTopUp: Validasi Tidak Digunakan Kembali
Pada proses konfirmasi top up, saldo kembali dihitung secara manual.
balanceAfter := wallet.Balance + order.AmountPadahal fungsi yang sudah memiliki validasi overflow sudah tersedia.
Masalahnya menjadi lebih jelas jika melihat alur top up.
-
Order dibuat dengan status
pending. -
Saldo baru benar-benar ditambahkan ketika order dikonfirmasi.
Selama jeda tersebut, saldo wallet dapat berubah karena transaksi lain.
Akibatnya, validasi yang benar ketika order dibuat belum tentu masih berlaku ketika order dikonfirmasi.
Masalah seperti ini dikenal sebagai time-of-check to time-of-use (TOCTOU).
Perbaikannya bukan membuat validasi baru, tetapi menggunakan kembali fungsi yang sebelumnya sudah diuji melalui fuzz test.
balanceAfter, err := validateAndApplyTopUp(...)Dengan begitu, proses create maupun confirm menggunakan aturan yang sama.
Transfer: Proteksi yang Hanya Setengah Jalan
Kasus terakhir ditemukan pada proses transfer.
Saldo pengirim sudah memiliki validasi.
if amount > fromWallet.Balance {
return ErrInsufficientBalance
}Namun saldo penerima langsung ditambahkan.
recipientBalanceAfter := toWallet.Balance + amountTidak ada pemeriksaan overflow.
Artinya, transaksi hanya terlindungi pada satu sisi.
Memisahkan Logika Transfer
Seperti kasus top up, saya memindahkan proses perhitungan transfer ke fungsi murni agar dapat diuji secara terpisah.
func validateAndApplyTransfer(...)Invariant yang Diuji
Fuzz test memverifikasi tiga aturan.
-
saldo pengirim tidak boleh negatif
-
saldo penerima tidak boleh overflow
-
total saldo sebelum dan sesudah transfer harus tetap sama
Hasil Pengujian
Setelah validasi overflow ditambahkan pada sisi penerima, Go Fuzzing berhasil menjalankan lebih dari 242 ribu kombinasi input tanpa menemukan pelanggaran terhadap ketiga invariant tersebut.

Pola yang Terlihat
Menariknya, ketiga masalah tersebut memiliki akar penyebab yang sama.
Operasi perubahan saldo tersebar di beberapa bagian kode dengan implementasi yang berbeda-beda.
Akibatnya, validasi overflow diterapkan pada satu fitur, tetapi terlupakan pada fitur lain.
Semakin banyak tempat yang melakukan operasi saldo secara mandiri, semakin besar peluang munculnya inkonsistensi seperti ini.
Pendekatan yang lebih baik adalah menjadikan operasi perubahan saldo sebagai satu abstraksi yang digunakan bersama, sehingga validasi cukup ditulis dan diuji di satu tempat.
Penutup
Awalnya saya menggunakan Go Fuzzing untuk mencari kemungkinan integer overflow pada proses top up.
Yang saya temukan ternyata bukan hanya satu bug, tetapi sebuah pola implementasi yang tersebar di beberapa bagian sistem.
Temuan pertama membawa saya mengaudit proses lain yang melakukan perubahan saldo. Dari sana muncul dua masalah tambahan: validasi yang tidak digunakan kembali pada proses konfirmasi top up dan validasi yang hanya diterapkan pada salah satu sisi transfer.
Bagi saya, manfaat terbesar fuzzing bukan sekadar menghasilkan ribuan kombinasi input.
Manfaat terbesarnya adalah membantu mendefinisikan invariant yang harus selalu benar, lalu memverifikasi apakah invariant tersebut benar-benar diterapkan secara konsisten di seluruh sistem.