Menguji Operasi Saldo E-Wallet dengan Go Fuzzing

·Abdul Wahid Kahar

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 + amount

Kalau 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 = -1

Pendekatan 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 + amount

Validasi 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 = 10000

Operasi 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.

Screen Shot 2026-07-23 at 11.49.39

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 + amount

apakah 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.Amount

Padahal fungsi yang sudah memiliki validasi overflow sudah tersedia.

Masalahnya menjadi lebih jelas jika melihat alur top up.

  1. Order dibuat dengan status pending.

  2. 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 + amount

Tidak 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.

Screen Shot 2026-07-23 at 11.50.36


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.