Checklist contract testing untuk API auth dan webhook dibutuhkan ketika integrasi lintas layanan tidak cukup dijaga oleh unit test. Pada level fungsi, kode Anda mungkin benar. Namun pada level integrasi, perubahan kecil seperti nama field, status code, urutan event, format timestamp, atau perilaku retry dapat membuat sistem rusak tanpa error yang jelas.

Masalah ini paling sering muncul pada dua area yang sensitif: API auth dan webhook. API auth biasanya menjadi gerbang semua request lain, sehingga perubahan kontrak token, header, atau error response akan menyebar ke seluruh alur. Webhook lebih rawan lagi karena sifatnya asinkron, bisa retry, bisa terkirim duplikat, dan sering bergantung pada signature verification serta asumsi urutan event.

Jika Anda mengikuti diskusi yang sering muncul di komunitas engineering tentang rapuhnya software modern, konteksnya bukan sekadar bahwa testing harus lebih banyak, tetapi bahwa jenis testing harus tepat. Untuk integrasi API yang rawan gagal diam-diam, contract testing memberi perlindungan yang tidak bisa digantikan unit test biasa.

Mengapa unit test saja tidak cukup

Unit test memverifikasi logika internal aplikasi Anda dalam isolasi. Itu penting, tetapi tidak membuktikan bahwa aplikasi Anda masih kompatibel dengan layanan eksternal. Saat integrasi bergantung pada kontrak yang berada di luar repositori Anda, sumber kegagalannya bukan hanya bug lokal, melainkan juga perubahan perilaku dari provider atau kesalahpahaman terhadap spesifikasi.

Contoh kegagalan yang sering terjadi:

  • Provider auth mengubah field expires_in dari integer menjadi string.
  • Endpoint token yang dulu mengembalikan 200 kini mengembalikan 201 atau 204 pada kondisi tertentu.
  • Field error tetap ada, tetapi struktur details berubah sehingga parser gagal.
  • Webhook pembayaran yang biasanya mengirim payment.succeeded setelah payment.created tiba-tiba mengirim urutan terbalik.
  • Provider melakukan retry webhook dengan event yang sama, tetapi sistem Anda memprosesnya dua kali karena tidak idempotent.

Semua kasus di atas bisa lolos dari unit test jika unit test menggunakan mock yang terlalu ideal atau fixture yang tidak mewakili realitas. Contract testing menutup celah itu dengan menguji ekspektasi antarlayanan, bukan hanya implementasi internal.

Apa yang harus diuji dalam contract testing

Pada integrasi API auth dan webhook, kontrak tidak berhenti di bentuk JSON. Kontrak juga mencakup perilaku protokol, kode status, header, retry, timeout, dan semantik event.

1. Skema request dan response

Ini lapisan paling dasar:

  • Field wajib dan opsional.
  • Tipe data: string, integer, boolean, object, array.
  • Format nilai: ISO-8601 timestamp, UUID, currency code, enum status.
  • Header yang wajib ada, misalnya Authorization, Content-Type, atau header signature.
  • Apakah field tambahan boleh diabaikan atau dianggap error.

Untuk API auth, contohnya meliputi request token, response access token, token type, masa berlaku, dan error jika credential salah. Untuk webhook, contohnya payload event, event id, resource id, tipe event, timestamp, dan signature header.

2. Error contract

Banyak integrasi rusak justru pada jalur error karena hanya jalur sukses yang diuji. Error contract perlu menjawab:

  • Status code apa yang dipakai untuk auth gagal, token kedaluwarsa, rate limit, atau payload invalid.
  • Apakah body error konsisten dan bisa diparse.
  • Field apa yang stabil untuk pengambilan keputusan, misalnya error_code dibanding pesan teks.
  • Apakah retry aman untuk jenis error tertentu.

Hindari mengandalkan pesan error bebas seperti "invalid request" sebagai dasar logika aplikasi. Gunakan kode error yang stabil bila tersedia.

3. Versioning

Versi kontrak harus eksplisit. Bentuknya bisa melalui path, header, media type, atau dokumentasi perubahan yang jelas. Yang penting, tim consumer tahu kapan perubahan bersifat backward compatible dan kapan tidak.

Yang perlu diuji:

  • Apakah versi lama masih diterima selama masa transisi.
  • Apakah field baru benar-benar opsional bagi consumer lama.
  • Apakah penghapusan field lama terdeteksi oleh contract test sebelum rilis.

4. Idempotency key

Pada API auth, idempotency sering tidak dominan untuk endpoint token biasa, tetapi sangat penting untuk endpoint yang memicu perubahan state, misalnya pembuatan session, link login, atau proses refresh tertentu di provider tertentu. Pada webhook, konsepnya lebih jelas: event yang sama bisa dikirim lebih dari sekali.

Yang perlu diuji:

  • Request dengan Idempotency-Key yang sama tidak menyebabkan duplikasi state.
  • Response untuk retry tetap konsisten atau setidaknya aman ditangani.
  • Event webhook dengan event_id yang sama hanya diproses sekali.

5. Signature verification

Webhook tanpa verifikasi signature berarti membuka pintu untuk request palsu. Namun verifikasi signature juga sering menjadi sumber bug karena perbedaan canonical payload, encoding, newline, atau penggunaan timestamp.

Yang perlu diuji:

  • Header signature wajib ada.
  • Payload diverifikasi terhadap raw body, bukan JSON yang sudah diparse lalu diserialisasi ulang.
  • Timestamp signature diperiksa untuk mencegah replay attack.
  • Request dengan signature salah ditolak dengan jelas.

6. Retry behavior, timeout, dan duplicate delivery

Webhook dan API eksternal tidak selalu deterministik. Contract test perlu menguji perilaku, bukan hanya struktur data:

  • Berapa lama consumer boleh menunggu sebelum menganggap timeout.
  • Apakah provider akan retry jika endpoint mengembalikan 5xx atau timeout.
  • Apakah provider menganggap 2xx sebagai ack final.
  • Apakah duplicate delivery mungkin terjadi walau response sebelumnya sudah sukses.

Jangan mendesain sistem webhook dengan asumsi “satu event datang satu kali dan urut”. Itu asumsi yang rapuh.

Contoh kasus nyata yang sering merusak integrasi

Field berubah tanpa breaking change yang diumumkan jelas

Misalnya provider auth sebelumnya mengirim:

{
  "access_token": "abc",
  "expires_in": 3600,
  "token_type": "Bearer"
}

Lalu di lingkungan tertentu atau rollout baru menjadi:

{
  "access_token": "abc",
  "expires_in": "3600",
  "token_type": "Bearer"
}

Jika parser Anda ketat dan mengharapkan integer murni, proses refresh token bisa gagal. Jika test Anda hanya memakai fixture lama, perubahan ini tidak akan terdeteksi sampai produksi.

Status code berubah, body tetap mirip

Aplikasi Anda mungkin hanya memperlakukan 200 sebagai sukses, padahal provider mulai mengembalikan 201 atau 204 pada skenario tertentu. Akibatnya, request sebenarnya berhasil tetapi aplikasi menandainya gagal dan memicu retry yang tidak perlu.

Urutan event berubah

Untuk webhook, banyak sistem secara tidak sadar mengasumsikan urutan event. Contoh: aplikasi hanya mau memproses payment.succeeded jika payment.created sudah lebih dulu masuk. Saat provider mengirim terbalik karena sistem event mereka bersifat eventual consistency, order internal Anda tidak pernah terupdate, atau lebih buruk, terupdate salah.

Pertahanan yang lebih aman adalah menjadikan event sebagai signal lalu menarik state terbaru dari API provider bila diperlukan, atau mendesain state machine lokal yang toleran terhadap urutan tidak pasti.

Checklist contract testing untuk API auth dan webhook

Bagian ini bisa dipakai langsung sebagai acuan review implementasi dan test plan.

Checklist untuk API auth

  1. Request contract
    • Method, path, query, dan header tervalidasi.
    • Content type sesuai yang didukung provider.
    • Field credential wajib diuji untuk kondisi ada, kosong, salah format, dan tidak dikirim.
  2. Response sukses
    • Field token wajib ada dan bertipe benar.
    • Masa berlaku token diparse aman.
    • token_type tidak diasumsikan case-sensitive jika dokumentasi tidak menjamin.
    • Field tambahan diabaikan dengan aman.
  3. Error contract
    • 401, 403, 400, 429, dan 5xx dipetakan ke perilaku yang tepat.
    • Retry hanya untuk error transient, bukan credential salah.
    • Body error tanpa field opsional tetap bisa ditangani.
  4. Versioning
    • Perubahan field opsional tidak mematahkan consumer.
    • Field yang deprecated masih tertangani selama masa transisi.
  5. Timeout dan resiliency
    • Timeout koneksi dan read timeout ditetapkan eksplisit.
    • Refresh token tidak dilakukan paralel tanpa kontrol jika bisa memicu race condition.
  6. Keamanan
    • Token tidak ditulis ke log mentah.
    • Secret tidak ikut muncul di pesan error.

Checklist untuk webhook

  1. Request contract
    • Header signature, event id, dan timestamp tersedia bila dijanjikan provider.
    • Payload minimum untuk identifikasi event tervalidasi.
    • Event type yang tidak dikenal tidak membuat sistem crash.
  2. Signature verification
    • Verifikasi memakai raw body.
    • Request dengan signature salah ditolak.
    • Timestamp lama atau replay di luar toleransi ditolak bila skema mendukung.
  3. Idempotency dan duplicate delivery
    • Event yang sama hanya diproses sekali berdasarkan event id stabil.
    • Retry dari provider tidak menggandakan side effect.
  4. Retry behavior
    • Handler mengembalikan 2xx hanya jika event sudah diterima dengan aman.
    • Jika pemrosesan berat, event segera diack lalu dipindahkan ke queue internal.
    • 4xx dan 5xx dipilih dengan sengaja karena memengaruhi retry provider.
  5. Ordering dan state
    • Sistem tidak mengasumsikan urutan event.
    • Event lama yang datang belakangan tidak menimpa state lebih baru tanpa validasi versi atau timestamp.
  6. Observability
    • Log menyimpan event id, provider, tipe event, hasil verifikasi, dan hasil deduplikasi.
    • Metrik retry, duplicate delivery, dan signature failure tersedia.

Strategi consumer-driven contract yang praktis

Consumer-driven contract berarti consumer mendefinisikan ekspektasi minimum yang ia butuhkan dari provider, lalu ekspektasi itu diverifikasi terhadap implementasi provider atau simulasi yang dikontrol. Pendekatan ini cocok ketika tim consumer lebih dulu merasakan dampak perubahan kontrak.

Apa yang sebaiknya didefinisikan oleh consumer

  • Field minimum yang benar-benar dipakai aplikasi.
  • Nilai enum atau status yang diharapkan.
  • Status code yang dianggap sukses atau retryable.
  • Contoh error yang harus bisa ditangani.

Hindari menulis kontrak yang terlalu ketat pada field yang tidak Anda pakai. Jika provider menambah field baru, itu seharusnya tidak memecahkan consumer yang didesain robust. Kontrak yang terlalu kaku justru menghasilkan noise dan biaya maintenance tinggi.

Pola kontrak yang sehat

  • Tegas pada hal penting: field wajib, tipe data, kode status, header keamanan.
  • Longgar pada hal tambahan: field ekstra, urutan properti JSON, whitespace, deskripsi teks error.
  • Pisahkan jalur sukses dan gagal: jangan hanya punya satu fixture happy path.

Fixture yang stabil: jangan terlalu ideal, jangan terlalu rapuh

Fixture sering menjadi sumber ilusi keamanan. Jika fixture dibuat terlalu bersih, test lulus tetapi tidak mewakili produksi. Jika fixture terlalu detail dan mengikuti seluruh payload provider, sedikit perubahan yang tidak relevan akan membuat test berisik.

Prinsip fixture yang stabil:

  • Simpan payload nyata yang sudah disanitasi dari data sensitif.
  • Pertahankan field yang benar-benar penting bagi logika bisnis Anda.
  • Tambahkan variasi untuk kasus field opsional hilang, nilai null, tipe salah, event duplikat, dan event out-of-order.
  • Jangan mengasumsikan urutan object JSON.

Untuk webhook, simpan juga raw body dan header signature contoh. Ini penting karena verifikasi signature tidak bisa diuji akurat jika hanya memakai object JSON hasil parse.

Negative test yang wajib ada

Negative test sering lebih bernilai daripada happy path karena integrasi gagal justru saat data tidak ideal.

Negative test untuk API auth

  • Credential salah.
  • Response sukses tetapi field token hilang.
  • Status code sukses dengan body kosong.
  • 429 rate limit.
  • 5xx dan timeout.
  • Response JSON invalid atau content type tidak sesuai.

Negative test untuk webhook

  • Signature salah.
  • Timestamp replay terlalu lama.
  • Payload valid tetapi event type tidak dikenal.
  • Event id sama dikirim dua kali.
  • Event datang terbalik.
  • Body rusak atau JSON tidak valid.

Tujuan negative test bukan membuktikan semua input salah bisa diproses, tetapi memastikan sistem gagal dengan aman, dapat diamati, dan tidak menghasilkan side effect berbahaya.

Kapan memakai sandbox, kapan mock

Keduanya dibutuhkan, tetapi perannya berbeda.

Mock cocok untuk

  • Test cepat di CI.
  • Simulasi skenario error yang sulit dipicu di sandbox.
  • Verifikasi logika consumer secara deterministik.
  • Pengujian kontrak minimum yang Anda kontrol penuh.

Kelemahan mock adalah ia hanya sebaik asumsi Anda. Jika mock tidak mencerminkan provider yang sebenarnya, test bisa memberi rasa aman palsu.

Sandbox cocok untuk

  • Memverifikasi integrasi dengan perilaku provider yang lebih nyata.
  • Menguji handshake auth, sertifikat, timeout, dan beberapa edge case protokol.
  • Validasi akhir sebelum rilis untuk alur kritis.

Kelemahan sandbox adalah sering tidak sepenuhnya sama dengan produksi. Data bisa terbatas, perilaku retry bisa berbeda, dan stabilitas lingkungan belum tentu baik untuk test yang deterministik.

Aturan praktis

Gunakan mock untuk cakupan luas dan cepat, lalu gunakan sandbox untuk validasi integrasi nyata pada jalur paling kritis. Jangan menggantungkan seluruh strategi testing hanya pada salah satunya.

Contoh implementasi praktis

Berikut contoh sederhana struktur verifikasi webhook yang fokus pada kontrak penting: raw body, signature, idempotency, dan ack cepat.

function handleWebhook(request) {
  const rawBody = request.rawBody;
  const signature = request.headers['x-signature'];
  const eventId = request.headers['x-event-id'];

  if (!signature || !verifySignature(rawBody, signature)) {
    return { status: 401, body: 'invalid signature' };
  }

  if (!eventId) {
    return { status: 400, body: 'missing event id' };
  }

  if (isDuplicateEvent(eventId)) {
    return { status: 200, body: 'duplicate ignored' };
  }

  const event = JSON.parse(rawBody);

  enqueueForAsyncProcessing({ eventId, event });
  markEventReceived(eventId);

  return { status: 202, body: 'accepted' };
}

Poin penting dari contoh ini:

  • Verifikasi dilakukan sebelum parse bisnis lebih jauh.
  • Duplicate delivery ditangani eksplisit.
  • Pemrosesan berat dipindahkan ke queue.
  • Response cepat mengurangi timeout dan retry yang tidak perlu.

Untuk API auth, contoh kontrak minimum yang layak diuji bisa berupa validasi struktur response:

function validateTokenResponse(body) {
  if (!body || typeof body.access_token !== 'string') {
    throw new Error('invalid token response: access_token missing');
  }

  if (body.expires_in != null && Number.isNaN(Number(body.expires_in))) {
    throw new Error('invalid token response: expires_in is not numeric');
  }

  return {
    accessToken: body.access_token,
    expiresIn: body.expires_in != null ? Number(body.expires_in) : null,
    tokenType: body.token_type || 'Bearer'
  };
}

Contoh ini sengaja tidak terlalu ketat pada hal yang bisa berubah tanpa merusak semantik, tetapi tetap tegas pada field inti yang dibutuhkan consumer.

Debugging tips saat contract test gagal

  • Bandingkan raw request/response, bukan hanya object hasil parse.
  • Cek header: banyak bug ada pada Content-Type, charset, signature, atau authorization scheme.
  • Log event id dan correlation id untuk melacak retry dan duplicate delivery.
  • Bedakan error transient dan permanen agar retry tidak memperburuk keadaan.
  • Uji dengan payload nyata yang disanitasi, bukan hanya contoh dari dokumentasi.
  • Periksa asumsi ordering jika bug hanya muncul sesekali pada webhook.

Kesalahan umum yang perlu dihindari

  • Menganggap dokumentasi provider selalu sama dengan perilaku aktual.
  • Menggunakan mock yang terlalu sempurna dan tidak pernah mengirim data aneh.
  • Tidak menguji jalur error dan retry.
  • Mengandalkan pesan error bebas untuk logika program.
  • Tidak menyimpan event id untuk deduplikasi.
  • Memverifikasi signature dari JSON yang sudah dimodifikasi parser.
  • Menganggap event webhook selalu datang sekali dan berurutan.

Penutup

Checklist contract testing untuk API auth dan webhook pada dasarnya adalah cara mengubah integrasi yang rapuh menjadi sistem yang bisa diverifikasi. Fokus utamanya bukan menambah test sebanyak mungkin, melainkan menguji kontrak yang benar-benar menentukan apakah dua layanan masih kompatibel: skema request/response, error contract, versioning, idempotency key, signature verification, retry behavior, timeout, dan duplicate delivery.

Jika Anda hanya mengandalkan unit test, banyak kegagalan integrasi akan baru terlihat setelah produksi. Dengan contract testing yang pragmatis, fixture yang stabil, negative test yang serius, dan pemakaian sandbox serta mock di tempat yang tepat, perubahan kecil dari provider tidak lagi mudah merusak sistem Anda secara diam-diam.