10
Pengujian dan Pipeline CI
Pipeline CI repository ini berada di satu workflow, .github/workflows/ci.yml, yang berjalan pada push ke master, push tag v*, dan setiap pull request. Workflow itu memecah pekerjaan menjadi tiga job, yaitu backend, frontend, dan image, dan job image menunggu kedua job lain selesai. .github/workflows/ci.yml:1-81
graph TD
trg["push master / tag v* / pull_request"] --> be["job backend"]
trg --> fe["job frontend"]
be --> lint["ruff check + ruff format --check"]
lint --> mypy["mypy ."]
mypy --> mig["makemigrations --check --dry-run"]
mig --> cek["manage.py check"]
cek --> pt["pytest --create-db"]
pt --> pa["pip-audit -r requirements.txt"]
pa --> skema["diff /tmp/schema.yaml schema.yaml"]
fe --> tipe["npm run tipe-api + git diff api-types.ts"]
tipe --> tc["typecheck + build"]
tc --> na["npm audit --omit=dev"]
skema --> img["job image, hanya tag v*"]
na --> img
img --> build["docker build hydro-api + hydro-web"]
build --> tv["Trivy --severity CRITICAL"]
tv --> ghcr["docker push ke GHCR"]
style be fill:#f59e0b,color:#000
style fe fill:#f59e0b,color:#000
style pt fill:#2d7d46,color:#fff
style skema fill:#2d7d46,color:#fff
style tv fill:#dc2626,color:#fff
style ghcr fill:#336791,color:#fff
style img fill:#f59e0b,color:#000
%% fig: Tahap pipeline CI
%% desc: Dua job paralel memeriksa mutu backend dan frontend, lalu job image membangun image, memindainya dengan Trivy, dan mengirimnya ke GHCR hanya pada tag.Job backend: database, lint, dan uji
Job backend berjalan di ubuntu-24.04 dengan working-directory: backend. Job itu menyediakan service db berbasis postgres:18.1 pada port 5433:5432, lengkap dengan health check pg_isready -U hydro, interval 5 detik, dan 20 kali retry, sehingga langkah uji tidak dimulai sebelum database siap. .github/workflows/ci.yml:13-272
| Urutan | Perintah | Kegagalan yang dicegah |
|---|---|---|
| 1 | ruff check . | pelanggaran lint |
| 2 | ruff format --check . | format berkas yang tidak sesuai |
| 3 | mypy . | tipe yang tidak cocok |
| 4 | python manage.py makemigrations --check --dry-run | perubahan model tanpa migration |
| 5 | python manage.py check | konfigurasi Django yang salah |
| 6 | pytest --create-db | uji yang gagal |
| 7 | pip-audit -r requirements.txt | dependensi rentan |
| 8 | spectacular --file /tmp/schema.yaml --fail-on-warn lalu diff | schema.yaml usang |
Seluruh langkah itu ditulis berurutan dalam job backend. .github/workflows/ci.yml:30-453
Python dipasang versi 3.13 dengan cache pip yang kuncinya backend/requirements-dev.txt. .github/workflows/ci.yml:32-364 Sebelum uji, workflow memasang libpango-1.0-0, libpangoft2-1.0-0, dan libharfbuzz-subset0 lewat apt-get, kemudian memasang requirements-dev.txt. .github/workflows/ci.yml:37-385 Paket Pango itu yang membuat pembuatan PDF bisa diuji di runner; bila pustaka tidak ada, uji PDF melewati dirinya sendiri seperti dibahas di bagian akhir halaman ini.
Langkah terakhir job backend membandingkan skema yang dihasilkan dengan berkas yang tersimpan. Perintah spectacular --file /tmp/schema.yaml --fail-on-warn menulis skema baru, lalu diff /tmp/schema.yaml schema.yaml menggagalkan pipeline bila keduanya berbeda. .github/workflows/ci.yml:46-496 Bentuk endpoint yang menjadi sumber skema itu dijelaskan di Endpoint API dan Skema OpenAPI.
Berkas requirements-dev.txt memuat pytest==8.*, pytest-django==4.14.0, ruff==0.17.3, mypy==2.4.1, django-stubs==6.1.2, djangorestframework-stubs==3.18.1, pip-audit, dan types-openpyxl, sehingga versi alat di lokal dan di CI berasal dari satu daftar. backend/requirements-dev.txt:1-107
Aturan lint dan pemeriksaan tipe
ruff dikonfigurasi dengan line-length = 100, target-version = "py313", dan pengecualian direktori migrations serta .venv. Aturan yang dipilih adalah E, F, I, B, UP, S, dan DJ, dengan S101, DJ001, dan DJ008 diabaikan; berkas di tests/* mendapat pengecualian tambahan S105, S106, dan S107 karena memakai sandi uji. backend/pyproject.toml:1-128
mypy berjalan pada python_version = "3.13" dengan plugin mypy_django_plugin.main dan mypy_drf_plugin.main. Direktori migrations, .venv, dan tests dikecualikan, dan kode galat attr-defined serta union-attr dimatikan karena kolom hasil annotate() dan objek openpyxl tidak terbaca oleh stub. backend/pyproject.toml:14-239
Konfigurasi pytest dan fixture peran
pytest.ini menetapkan DJANGO_SETTINGS_MODULE = config.settings, pola berkas test_*.py, dan addopts = -q --reuse-db. backend/pytest.ini:1-410 CI menambahkan --create-db pada pemanggilan pytest, jadi database uji selalu dibangun ulang di runner meskipun konfigurasi lokal memakai --reuse-db.
conftest.py menyediakan satu pintu masuk pembuatan pengguna. Fungsi buat_pengguna memanggil Peran.objects.get_or_create karena tes transactional mengosongkan tabel termasuk data awal dari migration, lalu menyimpan sandi sandi-uji-12345; bila peran yang diminta adalah PEMILIK, fungsi itu juga membuat Authenticator bertipe TOTP. backend/tests/conftest.py:17-2811
| Fixture | Peran atau isi |
|---|---|
admin | pengguna dengan ADMIN_PENJUALAN |
gudang | pengguna dengan STAF_GUDANG |
pemilik | pengguna dengan PEMILIK dan TOTP |
klien_admin, klien_gudang, klien_pemilik | APIClient hasil force_login |
pelanggan, produk, botol, dus | data dasar transaksi dan stok |
Ketiga fixture peran dibangun di atas buat_pengguna. backend/tests/conftest.py:31-4312 Klien uji dibuat oleh klien_untuk, yang memanggil force_login dengan django.contrib.auth.backends.ModelBackend, lalu dibungkus menjadi klien_admin, klien_gudang, dan klien_pemilik. backend/tests/conftest.py:46-6413 Data pendukung lainnya adalah pelanggan bernama Toko Uji, helper stok_masuk yang memanggil catat_barang_masuk dengan kedaluwarsa bawaan 100 hari, produk berkode AMK-600, serta satuan botol dan dus. backend/tests/conftest.py:67-10714
Matriks izin sebagai uji
Berkas test_izin.py memuat daftar MATRIKS berisi pasangan metode, url, dan himpunan peran yang boleh. Komentarnya menjelaskan trik yang dipakai: id 1 tidak ada, sehingga permintaan yang lolos izin berakhir 404 dan yang ditolak berakhir 403. backend/tests/test_izin.py:9-6715
| Endpoint | admin_penjualan | staf_gudang | pemilik |
|---|---|---|---|
get /api/produk/ | boleh | boleh | boleh |
post /api/produk/ | ditolak | ditolak | boleh |
get /api/barang-masuk/ | ditolak | boleh | boleh |
post /api/opname/1/setujui/ | ditolak | ditolak | boleh |
get /api/transaksi/ | boleh | ditolak | boleh |
get /api/laporan/penjualan/ | ditolak | ditolak | boleh |
get /api/dasbor/ | boleh | boleh | boleh |
Fixture klien_peran membuat satu klien untuk setiap peran, lalu test_matriks_izin_setiap_peran dijalankan lewat pytest.mark.parametrize dengan id "{metode} {url}". Untuk peran yang berhak, assertion hanya menuntut kode bukan 403; untuk peran lain, kode wajib 403. backend/tests/test_izin.py:70-8216
test_tabel_izin_sesuai_plan mengunci isi IZIN_PERAN: admin_penjualan memegang lihat_produk dan kelola_transaksi, staf_gudang memegang lihat_produk dan kelola_stok, dan pemilik memegang seluruh SEMUA_IZIN. backend/tests/test_izin.py:85-8817 Rincian peran dan izinnya dijelaskan di Autentikasi, 2FA, dan Izin Peran.
Uji berikutnya menelusuri seluruh pola url lewat _semua_pola, melewati api/_allauth dan himpunan PUBLIK berisi api/kesehatan/ dan api/auth/sesi/, lalu memastikan setiap endpoint hanya mengembalikan kode dalam {401, 405} saat diakses tanpa login. Uji itu juga menuntut jumlah endpoint yang diperiksa melebihi 25. backend/tests/test_izin.py:91-12018
Dua uji menutup celah deklarasi. test_view_baru_tanpa_deklarasi_izin_ditolak memanggil PunyaIzin().has_permission pada view yang tidak punya atribut izin dan menuntut hasil False. backend/tests/test_izin.py:123-13119 Uji terakhir memastikan pengguna tanpa peran mendapat 403 dan pemilik yang belum mengaktifkan 2FA mendapat 403 dengan kode bernilai wajib_2fa. backend/tests/test_izin.py:134-14920
Uji konkurensi
Berkas test_konkurensi.py diberi penanda modul pytest.mark.django_db(transaction=True) agar data yang ditulis satu utas terlihat oleh utas lain. backend/tests/test_konkurensi.py:14-1621 Helper _siapkan membuat pengguna, produk, satu batch berisi satu botol, dan pelanggan. backend/tests/test_konkurensi.py:19-2522
Helper _jalankan_bersamaan adalah inti pengujian ini. Fungsi itu membuat threading.Barrier(n), menjalankan n utas yang menunggu di gerbang sebelum bekerja, menangkap AturanBisnisError dan menyimpan exc.kode sebagai hasil, menutup koneksi di blok finally, lalu menunggu setiap utas dengan join(30). backend/tests/test_konkurensi.py:28-4623
sequenceDiagram
participant T as test_stok_terakhir_tidak_terjual_dua_kali
participant B as threading.Barrier
participant U as utas buat_transaksi
participant DB as PostgreSQL
T->>B: _jalankan_bersamaan(8, kerja)
T->>U: start 8 thread
U->>B: gerbang.wait()
B-->>U: lepas serentak
U->>DB: buat_transaksi(kunci_idempoten=uuid4)
DB-->>U: AturanBisnisError stok_tidak_cukup atau nomor nota
U->>DB: connection.close()
T->>T: assert hasil.count(stok_tidak_cukup) == 7
%% fig: Uji konkurensi batch terakhir
%% desc: Delapan utas menunggu di Barrier lalu berebut membeli batch terakhir; tujuh gagal dengan kode stok_tidak_cukup dan hanya satu nota tersimpan.Tiga skenario diuji. test_stok_terakhir_tidak_terjual_dua_kali menjalankan 8 utas pada batch berisi 1 botol dan menuntut hasil berisi 7 kode stok_tidak_cukup, satu Transaksi tersimpan, dan sisa batch menjadi 0. backend/tests/test_konkurensi.py:49-6824
test_kunci_idempoten_sama_bersamaan_hanya_satu_nota memakai satu kunci yang sama untuk 6 utas, dengan sisa batch dinaikkan ke 50. Uji itu menuntut semua hasil identik, hanya satu Transaksi, dan sisa menjadi 49. backend/tests/test_konkurensi.py:71-9225 Skenario ketiga mengirim 6 utas dengan kunci_idempoten berbeda dan menuntut 6 nomor nota yang semuanya unik. backend/tests/test_konkurensi.py:95-11326 Perilaku buat_transaksi yang dijaga uji ini, termasuk pengembalian nota lama saat IntegrityError, dijelaskan di Alur Transaksi, Pembayaran, Retur, dan Piutang.
Uji laporan dan PDF
Uji PDF dibuat toleran terhadap mesin tanpa Pango. test_nota_dan_laporan_pdf mencoba import weasyprint; bila muncul OSError, uji itu memanggil pytest.skip dengan pesan "Pustaka Pango tidak tersedia di mesin ini". backend/tests/test_laporan.py:125-12927 Di CI, langkah apt-get tadi memasok pustaka tersebut sehingga uji berjalan penuh. Isi laporan dan ekspornya dibahas di Dasbor, Laporan, dan Ekspor Berkas.
⚠️
pytest.inimemakai--reuse-db, sedangkan CI memakaipytest --create-db. Jalankanpytest --create-dbsebelum mengandalkan hasil lokal, karena database uji yang dipakai ulang bisa menyembunyikanmigrationyang belum diterapkan.
Job frontend
Job frontend berjalan di ubuntu-24.04 dengan working-directory: frontend, memasang Node versi 26 dan cache npm yang kuncinya frontend/package-lock.json, lalu menjalankan npm ci, langkah kesesuaian tipe API, npm run typecheck, npm run build, dan npm audit --omit=dev --audit-level=critical. .github/workflows/ci.yml:51-7028
Langkah kesesuaian tipe API menjalankan npm run tipe-api lalu git diff --exit-code src/lib/api-types.ts. Bila hasil pembangkitan berbeda dari berkas yang di-commit, pipeline gagal. .github/workflows/ci.yml:64-6729 Kedua skrip itu didefinisikan di package.json: tipe-api memanggil openapi-typescript ../backend/schema.yaml -o src/lib/api-types.ts, dan typecheck memanggil tsc --noEmit. frontend/package.json:9-1030 Pemeriksaan ini mengikat frontend ke skema backend yang sama dengan yang diuji diff di job backend.
Job image dan pemindaian Trivy
Job image hanya berjalan bila github.ref diawali refs/tags/v, menunggu needs: [backend, frontend], dan meminta izin contents: read serta packages: write. .github/workflows/ci.yml:72-7831
Langkah Build image mengambil versi dari GITHUB_REF_NAME#v, membangun image hydro-api dari direktori backend dan hydro-web dari deploy/web.Dockerfile, lalu menyimpan VERSI ke GITHUB_ENV. Langkah berikutnya menjalankan aquasec/trivy:0.67.2 untuk kedua image dengan --exit-code 1 --severity CRITICAL --ignore-unfixed. .github/workflows/ci.yml:86-9732 Langkah terakhir mendorong kedua image ke ghcr.io. .github/workflows/ci.yml:98-10133
⚠️ Pemindaian Trivy dan push ke
ghcr.iohanya berjalan saat tagv*dibuat.pull requesttidak pernah melewati kedua langkah itu, jadi kerentananCRITICALbaru terlihat ketika tag sudah dipotong. Opsi--ignore-unfixedjuga membuat kerentanan yang belum punya perbaikan tidak menggagalkan build. Rangkaian deploy yang memakai image hasil job ini dibahas di Deploy, Backup, dan Operasi VPS.