Skip to main content

ยท 2 min read

Alurkerja sendiri sudah menghandle beberapa exception seperti AlurkerjaException, BadRequestException, dan lain-lain. Namun hal ini menjadikan masalah lain, yang mana jika ada project tertentu ingin menampilkan error yang berbeda dari yang Alurkerja define.

Pada versi minimum Alurkerja ini

  • CRUD : 3.0.0-20241218.035527-172
  • BPMN : 3.0.0-20241218.035527-178

Sudah ditambahkan untuk mengatasi hal ini, dengan cara membuat file messages.properties pada folder 'Resources' di project masing-masing.

Pada versi minimum ini, Alurkerja akan ambil message dari file messages.properties terlebih dahulu. Jika ada, maka akan di return dengan message yang di di declare pada file messages.properties.

variable yang diambil oleh Alurkerja untuk HandleException berikut

Exception ClassVariable messages.properties
MethodArgumentNotValidException.classmethodArgumentNotValidException
HttpMessageNotReadableException.classhttpMessageNotReadableException
ConstraintViolationException.classconstraintViolationException
MissingServletRequestParameterException.classmissingServletRequestParameterException
IllegalArgumentException.classillegalArgumentException
JDBCConnectionException.classjDBCConnectionException
SQLException.classsQLException

messages.properties ini tidak hanya untuk mengubah error message dari exception diatas saja. Namun, bisa dipakai juga untuk AlurKerjaException.class.

throw new AlurKerjaException(404, "kompetisi_spesifikasi_wilayah_tidak_ditemukan.label", id);

yang dimana kompetisi_spesifikasi_wilayah_tidak_ditemukan.label berbentuk seperti ini di messages.properties

kompetisi_spesifikasi_wilayah_tidak_ditemukan.label = Kompetisi spesifikasi wilayah dengan ID {0} tidak ditemukan.

Contoh untuk merubah error message handle exception 'HttpMessageNotReadableException'

@GetMapping("/test-ex")
public ResponseEntity<Object> testException() {
throw new HttpMessageNotReadableException("testException");
}
httpMessageNotReadableException = Error Baru

maka, ketika dicoba, didapatkan hasil berikut Picture 0


Pada sentry, error yang dikirim sesuai dengan yang seharusnya Picture 1

ยท 2 min read

Javan CodeCovโ€‹

Javan CodeCov adalah sebuah aplikasi yagn di gunakan untuk running code coverage sesuai dengan command yang sudah di siapkan

Code cov dapat di download pada url https://github.com/purwadarozatun/docker-test/blob/main/javan-codecov

atau release terbaru yang ada di sini https://github.com/purwadarozatun/docker-test/releases

pindahkan file tersebut ke dalam folder bin pada komputer anda

untuk linux ada di

mv javan-codecov /usr/local/bin

untuk windows bisa di copy ke folder C:\Windows\System32

untuk mac bisa di copy ke folder /usr/local/bin dengan command

mv javan-codecov /usr/local/bin

setelah di download, lakukan perintah berikut untuk menjalankan aplikasi tersebut

Sebelum menjalankan pastikan docker sudah terinstall pada komputer anda

docker ps

Setelah itu pastikan anda sudah membuat test-config.yml pada root project anda

Untuk node js bisa menggunakan contoh berikut

# test-config.yml
cache:
paths:
- /node_modules
test:
image: node:20-slim
before_script:
- npm ci --legacy-peer-deps
script:
- npm run test

untuk php 8.2 bisa menggunakan contoh berikut

cache:
paths:
- /root/.config/composer
test:
image: harbor.merapi.javan.id/tools/javanlabs-php8.2-pcov:latest
script:
- composer install
- php -dpcov.enabled=1 -dpcov.directory=. -dpcov.exclude="~vendor~" ./vendor/bin/pest --parallel --coverage-text --coverage-clover=coverage-report.xml

sesuaikan command composer install dengan command yang anda gunakan untuk install package pada project anda

untuk java bisa menggunakan contoh berikut

cache:
paths:
- /root/.m2
test:
image: harbor.merapi.javan.id/tools/javanlabs-java-standard:latest
script:
- echo "Testing the application"
- mvn -Dmaven.repo.local=/root/.m2/repository clean install -Dmaven.test.failure.ignore=true

Setelah itu jalankan perintah berikut

anda bisa menjalankan perintah berikut untuk menjalankan unittest sesuai dengan config yang ada

javan-codecov test-config.yml  

jika ingin langsung push ke sonar maka

javan-codecov test-config.yml -s

ยท One min read

Dear all setelah sekian lama menjadi concern skarang Alurkerja Laravel sudah bisa melakukan activity log dengan mudah dan cepat.

teman teman hanya perlu mengupdate versi yang terbaru Alurkerja Laravel dengan cara composer update dan php artisan migrate untuk mengupdate table yang baru.

setelah itu teman teman bisa langsung menggunakan activity log dengan mudah dan cepat.

table yang akan kita gunakan adalah ak_activity_log dengan field yang lumayan lengkap dan InsyaAllah Mempermudah teman teman untuk mendefinisikan activity log

picture 0

Untuk menambahkan activity log teman teman bisa menggunakan class AkActivityLog yang sudah di sediakan oleh Alurkerja Laravel yang memiliki 3 fungsi yaitu createBpmnLog , createUserTaskLog , createCrudLog atau bahkan bisa membuat sendiri di aplikasi teman teman ya

Salah satu contoh penggunaaannya

<?php
use Laravolt\Crud\Sys\ActivityLog\AkActivityLog;

AkActivityLog::createCrudlog(
$model,
"UPDATE_DATA"
);

dengan mengirim model ke fungsi tersebut request json , request form , request multipart form , request multipart form dengan file bisa di log dengan mudah dan cepat selain itu authentikasi juga sudah ter record sesuai dengan user yang sedang login

Sudah keren kan , Update yuk !!

ยท 4 min read

Hello Semua , skarang sudah ada fitur assigne dan claim ya nah fitur ini di gunakan untuk melakukan setting assigne task ke user tertentu dan claim task yang sudah di assigne ke user lain

Fitur itu ada di tasklist nya camunda ya picture 0

Dan juga ada di tampilan detail process instance di Cocpitnya picture 1

Fitur tersebut dapat di ambil dari appi usertask ya sebagai berikut

Claim Taskโ€‹

digunakan untuk melakukan set task ke user yang sedang login

[POST] /api/rtgs-approvals/tm-maintenance-review/claim/{{user_task_id}}/{{busniess_key}}

Assign Taskโ€‹

sementara assign digunakan untuk assign task ke user yang di pilih

[POST] /api/rtgs-approvals/tm-maintenance-review/assign/{{user_task_id}}/{{busniess_key}}

dengan payload

{"user_id":19999}

Tampilan pada alurkerja tableโ€‹

selain api teman teman juga bisa menambahkan tampilan assign delegate dan claim ini pada table alurkerja nya dengan cara

membuat assigne component dengan


import { AuthContext, Button, Input, Modal, Select } from "alurkerja-ui"
import { useContext, useEffect, useState } from "react"
import Swal from "sweetalert2"

const toKebabCase = (str: string) => {

if (str != null) {
return str.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
?.map(x => x.toLowerCase())
.join('-')
}
return ""
}


export default ({ task , id }: { task: any, id:string }) => {
const axiosInstance = useContext(AuthContext)


const claimTask = () => {

Swal.fire({
icon: 'warning',
title: "Claim Task",
text: "Apakah Anda Yakin Untuk Claim Task",
showCancelButton: true,
confirmButtonColor: '#0095E8',
cancelButtonColor: '#d33',
cancelButtonText: "Batalkan",
confirmButtonText: "Konfirmasi",
}).then(async (result) => {
if (result.isConfirmed) {
console.log(task)
const taskName = toKebabCase(task.taskDefinitionKey)
axiosInstance.post(`/api/rtgs-approvals/${taskName}/assign/${id}/${task.id}`, {user_id:19999}).then(
// axiosInstance.post(`/api/rtgs-approvals/${taskName}/claim/${id}/${task.id}`).then(
(response) => {
// Todo add handler
Swal.fire("Claim Task", "Behasil Melakukan Claim Task", "success")
}
).catch(() => {

Swal.fire("Oops", "Terjadi kesalahan ketika claim task", "error")
})
}
})

}
return <>
<div className="flex flex-row justify-between">
<div className="flex flex-col">

<div >
<b>{task.name}</b>
</div>
<div>

{task.assignee || "Not Assigned"}
</div>
</div>
<div>

{task.assignee &&
<div>
<DelegateModal task={task} id={id}></DelegateModal>
</div>
}

{task.assignee == null &&
<div>
<Button size="sm" onClick={() => {
claimTask()
}}>Claim</Button>
</div>
}
</div>
</div>
</>
}



const DelegateModal = ({ task , id }: {id : string , task: any}) => {

const action ="Delegate"
const taskName = toKebabCase(task.taskDefinitionKey)
const axiosInstance = useContext(AuthContext)
const [userList, setUserList] = useState<any[]>();
const getAssigne = () => {
axiosInstance.post(`/api/rtgs-approvals/${taskName}/assignee-candidate`, task).then((response) => {
setUserList(response.data.data)
})
}
const assign = () => {
Swal.fire({
icon: 'warning',
title: action + " Task",
text: "Apakah Anda Yakin Untuk " + action + " Task",
showCancelButton: true,
confirmButtonColor: '#0095E8',
cancelButtonColor: '#d33',
cancelButtonText: "Batalkan",
confirmButtonText: "Konfirmasi",
}).then(async (result) => {taskName
if (result.isConfirmed) {
console.log(task)
const taskName = toKebabCase(task.taskDefinitionKey)
axiosInstance.post(`/api/rtgs-approvals/${taskName}/assign/${id}/${task.id}`, {user_id:selectedAssigne}).then(
(response) => {
// Todo add handler
Swal.fire("Claim Task", "Behasil Melakukan Delegate Task", "success")
}
).catch(() => {

Swal.fire("Oops", "Terjadi kesalahan ketika delegate task", "error")
})
}
})
}
const [selectedAssigne, setSelectedAssigne] = useState();
const [message, setMessage] = useState("");
useEffect(() => {
getAssigne()
}, []);
return <Modal
title={action}
triggerButton={<Button size="sm">{action}</Button>}
>
<div>
<div className='px-4 pt-4'>
<label>Select Assigne</label>
<Select
onChange={(e: any) => { setSelectedAssigne(e.value) } }
options={userList}
/>
</div>
<div className='px-4 pt-4'>
<label>Message</label>
<Input type="text"
textArea={true}
onChange={(e: any) => { setMessage(e.target.value) }}
/>
</div>
<div className='px-4 pt-4'>

<Button onClick={() => { assign() }}>{action}</Button>
</div>

</div>


</Modal>
}

Tampilan yang akan di hasilkan dari component tersebut adalah sebuah cell yang bisa di masukan ke dalam table alurkerja dengan cara


<TableLowcode

...

customCell={({ name, fields, value, rowValue, defaultCell, }) => {


....


if (name == "status") {
return <>
{rowValue.available_task.map((item: any) => {

return <AssigneComponent task={item} id={rowValue.id}></AssigneComponent>
})}
</>
}

....


}}
/>

Dan akan menghasilkan tampilan seperti ini

picture 2

tombol claim akan muncul ketika assinge masih kosong dan tombol delegate akan muncul ketika assigne sudah di isi

picture 3

masing masing button akan memunculkan tampilan modal seperti ini

picture 4

Delegate Candidateโ€‹

ada juga api untuk menentukan assigne candidate yaitu

[POST] /api/rtgs-approvals/${taskName}/assignee-candidate

api tersebut bisa di override dari service usertask yang di inginkan sesuai dengan parameter tertentu yang di post ke api tersebut

contoh pada react di atas

   const getAssigne = () => {
axiosInstance.post(`/api/rtgs-approvals/${taskName}/assignee-candidate`, task).then((response) => {
setUserList(response.data.data)
})
}

ketika mengambil ke api tersebut kita mengirimkan kembali task yang sedang di kerjakan agar bisa dicheck sesuai dengan kriteria tasknya juga untuk itu silahkan override pada Usertaskservice bagian



public function assigneeCandidate($request)
{
// lakukan manipulasi pengambilan data user di sini
return User::all()->map(function ($data) {
return [
"value" => $data->id,
"label" => $data->name,
];
});
}


Berikut penjelasan dari masing masing component yang di gunakan

Silahkan jika ada yang kurang jelas bisa di tanyakan di grup telegram

Terimakasih

ยท 2 min read

Pada alurkeja sebelumnya, kita sudah membuat fitur auto mode. Fitur ini akan mengubah mode dari Whitelist menjadi blacklist dan sebaliknya. Fitur ini akan kita buat dengan menggunakan Invert Search , Filter dan Sort. fitur ini akan memudahkan teman teman untuk mendefinisikan field mana yang ingin dicari filter maupun sort

untuk menggunakan fitur ini teman teman tinggal menambahkan atribut filterMode, searchMode dan sortMode pada class Model yang teman teman buat.

Setelah ini di isi dengan AutoMode::BLACKLIST atau AutoMode::WHITELIST.


public AutoMode $filterMode = AutoMode::BLACKLIST;
public AutoMode $searchMode = AutoMode::BLACKLIST;
public AutoMode $sortMode = AutoMode::BLACKLIST;

ketika AutoMode::BLACKLIST maka field yang ada pada searchableColumns . filterableColumns dan sortableColumns akan di berubah menjadi blacklist. dimana field yang ada pada searchableColumns . filterableColumns dan sortableColumns tidak akan di cari, filter dan sort.

contoh datanya seperti ini terdapat table dengan field id, nama , alamat ketika menggunakan whitelist maka field id tidak akan di cari, filter dan sort. ketika di deklarasikan seperti ini

    public array $searchableColumns = [
'nama',
'alamat',
];
public array $filterableColumns = [
'nama',
'alamat',
];
public array $sortableColumns = [
'nama',
'alamat',
];

sedangkan sebaliknya ketika menggunakan blacklist maka field id akan di cari, filter dan sort. ketika di deklarasikan seperti ini

    public array $searchableColumns = [
'nama',
'alamat',
];
public array $filterableColumns = [
'nama',
'alamat',
];
public array $sortableColumns = [
'nama',
'alamat',
];

Sekian pemaparannya

Salam

ยท One min read

Pada alurkerja terdapat BpmnModel yang dapat diimplementasikan ke model yang menjadi awal dari bisnis proses. Dari BpmnModel kita bisa mendapatkan list user task dari BPMN yang berkaitan dengan model tersebut. Namun, jika BPMN anda memiliki sub process dan anda ingin mengambil user task dari sub process tersebut, anda perlu meng-override fungsi getUserTask.

    public function getUserTask(): array
{
//get user task from main bpmn
$userTask = parent::getUserTask();

//get user task from sub process
$userTaskSubProcess = $this->pareseBpmnFromProcessDefinition('sub_process_name');

//merge user task from main bpmn and sub process
$userTask = array_merge($userTask, $userTaskSubProcess);

return $userTask->toArray();
}

Kode di atas merupakan contoh dari implementasi fungsi getUserTask yang telah di-override. Awalnya kita mendapatkan user task dari BPMN utama dengan parent::getUserTask. Kemudian anda bisa mendapatkan user task dari sub process dengan menggunakan fungsi pareseBpmnFromProcessDefinition(). Untuk sub_process_name silahkan disesuaikan dengan sub process ID dari yang anda gunakan.

Setelah mendapatkan semua user task yang diinginkan, selanjutnya anda gabungkan user task tersebut, disini menggunakan array_merge. Dengan cara ini, anda bisa mendapatkan semua user task yang terkait dengan BPMN anda, baik yang utama maupun yang sub proceess.

ยท 4 min read

Apa itu state managementโ€‹

State management adalah sebuah cara untuk mengelola state pada aplikasi kita, state management ini biasanya digunakan pada aplikasi yang memiliki state yang kompleks, seperti aplikasi yang memiliki banyak komponen yang saling berhubungan, atau aplikasi yang memiliki banyak state yang saling berhubungan.

biasanya state management ini digunakan pada aplikasi yang menggunakan framework seperti react, vue, angular, dll. karena pada framework tersebut, state management ini tidak disediakan secara default, sehingga kita harus menggunakan library pihak ketiga untuk mengelola state pada aplikasi kita.

Mengapa kita perlu menggunakan state managementโ€‹

Ketika kita membuat aplikasi yang memiliki state yang kompleks, maka kita akan kesulitan untuk mengelola state tersebut, karena kita harus memikirkan bagaimana cara mengupdate state tersebut, bagaimana cara mengambil state tersebut, bagaimana cara menghapus state tersebut, dan lain sebagainya. sehingga kita akan kesulitan untuk mengelola state tersebut.

Apa itu Zustandโ€‹

Zustand adalah sebuah library state management sederhana yang dapat di gunakan dengan cepat dan mudah, zustand ini memiliki fitur yang lengkap, sehingga kita dapat mengelola state dengan mudah. zustand ini juga memiliki performa yang sangat cepat, sehingga cocok untuk digunakan pada aplikasi yang memiliki state yang kompleks. zustand ini juga sangat mudah untuk di gunakan, sehingga cocok untuk digunakan oleh pemula.

Cara menggunakan Zustandโ€‹

Instalasiโ€‹

Untuk menggunakan zustand, kita harus menginstall library zustand terlebih dahulu, kita dapat menginstall library zustand dengan cara berikut:

npm install zustand

Membuat storeโ€‹

Setelah kita menginstall library zustand, kita dapat membuat store dengan cara berikut:

buatlah file dengan nama useLoginStore.ts pada folder src/stores, lalu tambahkan kode berikut:



import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'

interface LoginState {
userData: { [x: string]: any } | null,
dataFetched: boolean,
setUser: (userData: { [x: string]: any }) => void
fetchUser: (axiosInstance: any) => void
logout: () => void
}

export const useLoginStore = create<LoginState>()(
devtools(
persist(
(set) => ({
userData: {},
setUser: (userData) => set(() => ({ userData })),
fetchUser: async (axiosInstance: any) => {

const url = `/api/auth/info`;
const opsResponse = await axiosInstance.post(url);
try {


const data = opsResponse.data.data;
return set({ userData: await data, dataFetched: true })
} catch (error) {

return set({ userData:null, dataFetched: true })
}

},
logout: () => set(() => ({ userData: {}, dataFetched: true })),
dataFetched: false,
}),
{
name: 'user-storage',
}
)
)
)

pada contoh di atas, kita membuat store dengan nama useLoginStore, pada store tersebut kita membuat state userData yang berisi data user, lalu kita membuat action setUser yang berfungsi untuk mengupdate state userData, lalu kita membuat action fetchUser yang berfungsi untuk mengambil data user dari server, lalu kita membuat action logout yang berfungsi untuk menghapus data user, lalu kita membuat state dataFetched yang berfungsi untuk menandakan apakah data user sudah diambil atau belum.

pada contoh di atas, kita juga menggunakan middleware devtools dan persist, devtools berfungsi untuk menampilkan state pada devtools, dan persist berfungsi untuk menyimpan state pada localstorage.

Menggunakan storeโ€‹

Setelah kita membuat store, kita dapat menggunakan store tersebut dengan cara berikut:

import { useLoginStore } from 'src/stores/useLoginStore' // import store


const { userData, setUser, logout } = useLoginStore() // gunakan store

pada contoh di atas, kita mengimport store useLoginStore yang telah kita buat sebelumnya, lalu kita menggunakan store tersebut dengan cara memanggil store tersebut. pada contoh di atas, kita memanggil state userData, dan action setUser dan logout dari store tersebut.

Memangambil user dataโ€‹

pada fungsi fetchUser, kita dapat mengambil data user dengan cara berikut:

import { useLoginStore } from 'src/stores/useLoginStore' // import store


const { userData, setUser, logout, fetchUser } = useLoginStore() // gunakan store

fetchUser(axiosInstance) // ambil data user

pada contoh di atas, kita mengambil data user dengan cara memanggil action fetchUser dari store tersebut.

Mengupdate stateโ€‹

Setelah kita menggunakan store, kita dapat mengupdate state dari store tersebut dengan cara berikut:

import { useLoginStore } from 'src/stores/useLoginStore' // import store


const { userData, setUser, logout } = useLoginStore() // gunakan store

setUser({ name: 'John Doe' }) // update state

pada contoh di atas, kita mengupdate state userData dengan cara memanggil action setUser dari store tersebut.

Menghapus stateโ€‹

Setelah kita menggunakan store, kita dapat menghapus state dari store tersebut dengan cara berikut:

import { useLoginStore } from 'src/stores/useLoginStore' // import store


const { userData, setUser, logout } = useLoginStore() // gunakan store

logout() // hapus state

pada contoh di atas, kita menghapus state userData dengan cara memanggil action logout dari store tersebut.

Mengambil Data Userโ€‹

Setelah kita menggunakan store, kita dapat mengambil data user dari store tersebut dengan cara berikut:

import { useLoginStore } from 'src/stores/useLoginStore' // import store


const { userData, setUser, logout } = useLoginStore() // gunakan store

console.log(userData) // ambil data user

ยท One min read

Penjelasan utama bisa dibaca disini

Disini lebih menjelaskan mengenai Pemakaian Spring sebagai Camunda Engine untuk PHP BE

Apakah harus pakai AlurKerja?โ€‹

Jawaban singkatnya tidak. Karena kalau hanya digunakan untuk memanfaatkan camunda saja tanpa ada hal lain, maka tidak perlu memakai Library AlurKerja.

Kenapa Memakai Spring Boot as Camunda Engine?โ€‹

Untuk memudahkan dalam implementasi Service Task, karena di Sppring Boot telah embedded dengan camunda, maka akses untuk camunda nya itu sendiri sangat mudah. Banyak implementasi Service Task yang memudahkan untuk Spring Boot. Dan juga memudahkan untuk mengatur port yang digunakan untuk Camunda saat Start Service nya

Bagaimana sistem upload BPMN jika ada perubahan ?โ€‹

Jika BPMN mengalami perubahan, sangat diwajibkan untuk update file di folder resoucre nya Spring Boot (By Default)

Kenapa tidak upload lewat API engine-rest saja ?

Perlu diingat bahwa setiap Start Up project Spring Boot nya, dia akan melakukan deployment file BPMN yang ada di folder resource nya.

Jika bersikeras untuk by API engine-rest, bisa..

Tambahkan properties berikut di file application.properties / application.yaml nya Spring Boot

camunda.bpm.auto-deployment-enabled=false

ยท 11 min read

Why ?โ€‹

Unit testing adalah praktik pengujian perangkat lunak di mana setiap bagian kecil (unit) dari program diuji secara terpisah untuk memastikan bahwa masing-masing unit berfungsi sebagaimana mestinya. Berikut adalah beberapa alasan mengapa unit testing sangat penting dalam pengembangan perangkat lunak:

  1. Mendeteksi Kesalahan Secara Dini : Unit testing membantu dalam mendeteksi kesalahan atau bug sedini mungkin dalam siklus pengembangan perangkat lunak. Dengan mengidentifikasi masalah di tingkat unit, Anda dapat mengatasi kesalahan sebelum mereka menjadi masalah yang lebih besar.

  2. Mempermudah Refaktorisasi : Unit testing memberikan keamanan saat melakukan refaktorisasi kode. Saat Anda merubah struktur atau logika dalam sebuah unit, unit test akan memberikan umpan balik segera apakah perubahan tersebut merusak fungsionalitas yang sudah ada.

  1. Dokumentasi Hidup : Unit test dapat berfungsi sebagai dokumentasi hidup karena mereka memberikan contoh konkret tentang cara menggunakan fungsi atau kelas tertentu. Pemeliharaan dan pengembangan lebih lanjut dapat dipermudah karena unit test memberikan gambaran jelas tentang bagaimana komponen seharusnya berperilaku.

  2. Meningkatkan Kepercayaan : Dengan adanya unit test yang sukses, tim pengembangan dan pemangku kepentingan dapat memiliki tingkat kepercayaan yang lebih tinggi terhadap keberfungsian perangkat lunak. Ini membantu mengurangi ketidakpastian dan meningkatkan kualitas perangkat lunak secara keseluruhan.

  3. Memfasilitasi Continuous Integration : Unit testing mendukung praktik Continuous Integration (CI), di mana perubahan kode diintegrasikan ke dalam repository secara otomatis dan diuji secara otomatis. Hal ini membantu dalam mengidentifikasi konflik dan masalah integrasi dengan cepat.

  4. Mempercepat Pengembangan : Meskipun mungkin memerlukan waktu dan usaha tambahan untuk menulis unit test, namun hal ini dapat menghemat waktu dalam jangka panjang. Dengan unit test yang baik, tim dapat mengurangi waktu yang diperlukan untuk debugging dan pemeliharaan.

  5. Memudahkan Identifikasi dan Isolasi Masalah : Jika kesalahan terjadi, unit test membantu dengan cepat mengidentifikasi unit mana yang mengalami kegagalan. Ini memudahkan dalam isolasi masalah dan mempercepat proses debugging.

  6. Mendorong Desain yang Lebih Baik : Menulis unit test sering kali mendorong pengembang untuk membuat desain yang lebih baik dan lebih terpisah. Desain yang baik dapat membantu dalam meningkatkan keterbacaan kode, pemeliharaan, dan skalabilitas.

Dengan menerapkan unit testing secara efektif, pengembang dapat membangun dan memelihara perangkat lunak dengan lebih percaya diri dan efisien.

Spring Bootโ€‹

Class Controllerโ€‹

Classโ€‹

@RestController
@RequestMapping("/crud/vulner")
public class VulnerController extends CrudController<ScanResult, VulnerDto, VulnerService, ScanResultRepository> {

@Autowired
MinioService minioService;

@Autowired
ScanResultRepository scanResultRepository;
@Autowired
RemediationDocumentRepository remediationDocumentRepository;
protected VulnerController(VulnerService crudService) { super(crudService); }

@PostMapping("/uploadremediation")
public ResponseEntity<Object> getUploadLinkRemediation (@RequestPart("file") MultipartFile[] files) throws AlurKerjaException {
List<MinioDto> listMinio = new ArrayList<>();
List<String> allowedContentTypes = Arrays.asList(
"application/pdf",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"text/csv");

for (MultipartFile file : files) {
// if file size more than 5MB
if (file.getSize() > 5242880) {throw new AlurKerjaException(400, "Maximum file size is 5MB");}

if (!allowedContentTypes.contains(file.getContentType())) {
throw new AlurKerjaException(400, "Invalid file type. Only PDF, Excel, Word and CSV files are allowed.");
}

listMinio.add( minioService.getUploadLink("remediation", file.getOriginalFilename(), file.getContentType()));
}

return this.success(listMinio, "Ok");
}
}

Unit Testโ€‹

@ExtendWith(SpringExtension.class)
@WebMvcTest(VulnerController.class)
@ContextConfiguration(classes = {VulnerController.class, CurrentUser.class})
class VulnerControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private VulnerService mockCrudService;
@MockBean
private MinioService mockMinioService;
@MockBean
private ScanResultRepository mockScanResultRepository;
@MockBean
private RemediationDocumentRepository mockRemediationDocumentRepository;

@Test
void testGetUploadLinkRemediation() throws Exception {
// Setup
// Configure MinioService.getUploadLink(...).
final MinioDto minioDto = new MinioDto();
minioDto.setOriginalFileName("originalFileName");
minioDto.setIdentifier(UUID.fromString("3f13acea-040d-411b-89d7-6464946cd669"));
minioDto.setFilePath("filePath");
minioDto.setMediaType("mediaType");
minioDto.setUploadUrl("uploadUrl");
when(mockMinioService.getUploadLink(any(), any(), any())).thenReturn(minioDto);

// Run the test
final MockHttpServletResponse response = mockMvc.perform(multipart("/crud/vulner/uploadremediation")
.file(new MockMultipartFile("file", "originalFilename.pdf", MediaType.APPLICATION_PDF_VALUE,
"content".getBytes()))
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();

// Verify the results
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo("{\"status\":200,\"message\":\"Ok\",\"data\":[{\"originalFileName\":\"originalFileName\",\"identifier\":\"3f13acea-040d-411b-89d7-6464946cd669\",\"filePath\":\"filePath\",\"mediaType\":\"mediaType\",\"uploadUrl\":\"uploadUrl\"}]}");
}
}

Class Serviceโ€‹

Classโ€‹

@Service
public class VulnerService extends CrudService<ScanResult, VulnerDto, ScanResultRepository> {
private static final Logger logger = LoggerFactory.getLogger(VulnerService.class.getName());
@Autowired
CurrentUser currentUser;
@Autowired
RemediationDocumentRepository remediationDocumentRepository;
@Autowired
NotificationRepository notificationRepository;
@Autowired
EmailService emailService;
@Autowired
UserRepository userRepository;
@Autowired
AgingRepository agingRepository;

@Value("${it_security_frontend_url}")
private String feLink;
private final ScanResultRepository scanResultRepository;

protected VulnerService(ScanResultRepository simpleJpaRepository,
ScanResultRepository scanResultRepository) { super(simpleJpaRepository);
this.scanResultRepository = scanResultRepository;
}

@Override
public String getCurrentUser() {
return currentUser.getUserId();
}
}

Unit Testโ€‹

@ExtendWith(MockitoExtension.class)
class VulnerServiceTest {

@Mock
private ScanResultRepository mockSimpleJpaRepository;
@Mock
private ScanResultRepository mockScanResultRepository;
@Mock
private CurrentUser mockCurrentUser;
@Mock
private RemediationDocumentRepository mockRemediationDocumentRepository;
@Mock
private NotificationRepository mockNotificationRepository;
@Mock
private EmailService mockEmailService;
@Mock
private UserRepository mockUserRepository;
@Mock
private AgingRepository mockAgingRepository;

private VulnerService vulnerServiceUnderTest;

@BeforeEach
void setUp() {
vulnerServiceUnderTest = new VulnerService(mockSimpleJpaRepository, mockScanResultRepository);
ReflectionTestUtils.setField(vulnerServiceUnderTest, "feLink", "feLink");
vulnerServiceUnderTest.currentUser = mockCurrentUser;
vulnerServiceUnderTest.remediationDocumentRepository = mockRemediationDocumentRepository;
vulnerServiceUnderTest.notificationRepository = mockNotificationRepository;
vulnerServiceUnderTest.emailService = mockEmailService;
vulnerServiceUnderTest.userRepository = mockUserRepository;
vulnerServiceUnderTest.agingRepository = mockAgingRepository;
}

@Test
void testGetCurrentUser() {
// Setup
when(mockCurrentUser.getUserId()).thenReturn("Reminder");

// Run the test
final String result = vulnerServiceUnderTest.getCurrentUser();

// Verify the results
assertThat(result).isEqualTo("Reminder");
}
}

Class Specificationโ€‹

Classโ€‹

public class UserSpecification extends BaseSearchSpecification<User, UserDto> {
protected UserSpecification(UserDto dto) {
super(dto);
}

public UserSpecification create(UserDto dto){
UserSpecification specifications = new UserSpecification(dto);
specifications.buildSpecification(dto);
return specifications;
}

@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicates = new ArrayList<>();

if (!ObjectUtils.isEmpty(dto.getSearch())) {
String search = URLDecoder.decode(dto.getSearch());
search = "%" + search + "%";

predicates.add(criteriaBuilder.and(criteriaBuilder.or(
criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), search),
criteriaBuilder.like(criteriaBuilder.lower(root.get("email")), search),
criteriaBuilder.like(criteriaBuilder.lower(root.get("phone")), search)
)));
}

BasePredicate basePredicate = new BasePredicate();
if (!ObjectUtils.isEmpty(searchCriteriaList)) {
predicates.addAll(
basePredicate.predicate(root, criteriaQuery, criteriaBuilder, this.searchCriteriaList)
);
}

return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
}
}

Unit Testโ€‹

@ExtendWith(MockitoExtension.class)
class UserSpecificationTest {

@Mock
private UserDto mockDto;

private UserSpecification userSpecificationUnderTest;

@BeforeEach
void setUp() {
userSpecificationUnderTest = new UserSpecification(mockDto);
}

@Test
void testCreate() {
// Setup
final UserDto dto = new UserDto();
dto.setId(UUID.fromString("48d8e0a7-d941-4b39-8c19-0854cd5d95a5"));
dto.setName("48d8e0a7-d941-4b39-8c19-0854cd5d95a5");
dto.setEmail("48d8e0a7-d941-4b39-8c19-0854cd5d95a5");
dto.setPhone("48d8e0a7-d941-4b39-8c19-0854cd5d95a5");
final RoleDto role = new RoleDto();
role.setId(UUID.fromString("48d8e0a7-d941-4b39-8c19-0854cd5d95a5"));
dto.setRole(role);
dto.setWorkgroup("48d8e0a7-d941-4b39-8c19-0854cd5d95a5");
dto.setLevel("48d8e0a7-d941-4b39-8c19-0854cd5d95a5");
dto.setOrg("48d8e0a7-d941-4b39-8c19-0854cd5d95a5");
dto.setStatus("48d8e0a7-d941-4b39-8c19-0854cd5d95a5");
dto.setSearch("search");

// Run the test
final UserSpecification result = userSpecificationUnderTest.create(dto);
Assertions.assertNotNull(result);
// Verify the results
}

@Test
void testToPredicate() {
// Setup
final UserDto dto = new UserDto();
dto.setSearch("search");

Root<User> mockRoot = mock(Root.class);
CriteriaQuery<?> mockCriteriaQuery = mock(CriteriaQuery.class);
CriteriaBuilder mockCriteriaBuilder = mock(CriteriaBuilder.class);

when(mockDto.getSearch()).thenReturn("search");

// Run the test
Predicate result = userSpecificationUnderTest.toPredicate(mockRoot, mockCriteriaQuery, mockCriteriaBuilder);
Assertions.assertNotNull(result);
}
}

Class DTOโ€‹

Classโ€‹

@Getter
@Setter
public class VulnerDto extends BaseDto<ScanResult, VulnerDto> {
@SearchQueryCriteria(ignoreGenerated = true)
private Integer folderId;

@SearchQueryCriteria(ignoreGenerated = true)
private Integer scanNessusId;

@SearchQueryCriteria(ignoreGenerated = true)
private String name;

@SearchQueryCriteria(ignoreGenerated = true)
private Integer month;

@SearchQueryCriteria(ignoreGenerated = true)
private Date vaDate;

@SearchQueryCriteria(ignoreGenerated = true)
private Integer hostId;

@SearchQueryCriteria(ignoreGenerated = true)
private String riskLevel;

@SearchQueryCriteria(ignoreGenerated = true)
private String system;

@SearchQueryCriteria(ignoreGenerated = true)
private String protocol;

@SearchQueryCriteria(ignoreGenerated = true)
private String port;

@SearchQueryCriteria(ignoreGenerated = true)
private String pluginName;

@SearchQueryCriteria(ignoreGenerated = true)
private String synopsis;

@SearchQueryCriteria(ignoreGenerated = true)
private String detail;

@SearchQueryCriteria(ignoreGenerated = true)
private String solution;

@SearchQueryCriteria(ignoreGenerated = true)
private Boolean isApprove;

@SearchQueryCriteria(ignoreGenerated = true)
private String assetTag;

@SearchQueryCriteria(ignoreGenerated = true)
private String assetOwner;

@SearchQueryCriteria(ignoreGenerated = true)
private Long aging;

@SearchQueryCriteria(ignoreGenerated = true)
private String managedBy;

@SearchQueryCriteria(ignoreGenerated = true)
private String status;

@SearchQueryCriteria(ignoreGenerated = true)
private String searchAll;

@SearchQueryCriteria(ignoreGenerated = true)
private RavulnerDto ravulner;

@SearchQueryCriteria(ignoreGenerated = true)
private String applicationFunction;

@SearchQueryCriteria(ignoreGenerated = true)
private String assetSeverity;

@SearchQueryCriteria(ignoreGenerated = true)
private String typeVulner;

private UUID parentId;
private Boolean isActive;

private Boolean isApproval;

@Override
public ScanResult fromDto() {
ScanResult entity = new ScanResult();
BeanUtils.copyProperties(this, entity, getNullPropertyNames(this));
if (!ObjectUtils.isEmpty(ravulner)) {
Ravulner newRavulner = new Ravulner();
newRavulner.setId(ravulner.getId());
entity.setRavulner(newRavulner);
}
return entity;
}
}

Unit Testโ€‹

@ExtendWith(MockitoExtension.class)
class VulnerDtoTest {

@Mock
private Date mockVaDate;

private RavulnerDto ravulner;

private VulnerDto vulnerDtoUnderTest;

@BeforeEach
void setUp() throws ParseException {
ravulner = new RavulnerDto();

// Mengeset nilai-nilai ke properti
ravulner.setRiskLevel("High"); // asumsikan "High" sebagai level risiko
ravulner.setSystem("Windows"); // misalnya, sistemnya adalah "Windows"
ravulner.setServer("Server01"); // nama server
ravulner.setName("RavulnerName"); // nama ravulner
ravulner.setAssetTag("Asset001"); // tag aset

// Untuk tanggal, kita perlu parsing dari string atau mendapatkannya dari sumber lain
SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy");
Date expireDate = dateFormat.parse("01-12-2023"); // misalnya, tanggal kedaluwarsa adalah 1 Desember 2023
ravulner.setExpireDate(expireDate);

// Informasi tentang pembuat dan tanggal
ravulner.setCreatedBy("Admin"); // misalnya, dibuat oleh "Admin"
ravulner.setCreatedDate(new Date()); // tanggal saat ini sebagai tanggal dibuat
// Untuk tanggal update, bisa diset saat terjadi perubahan, misalnya kita set sama dengan tanggal dibuat untuk contoh ini
ravulner.setUpdatedDate(new Date()); // tanggal saat ini sebagai tanggal update
ravulner.setUpdatedBy("UpdaterName"); // nama individu atau sistem yang melakukan update

vulnerDtoUnderTest = new VulnerDto();
vulnerDtoUnderTest.setVaDate(mockVaDate);
vulnerDtoUnderTest.setRavulner(ravulner);
}

@Test
void testFromDto() {
// Run the test
final ScanResult result = vulnerDtoUnderTest.fromDto();
Assertions.assertNotNull(result);
Assertions.assertNotNull(result.getRavulner());
// Verify the results
}
}

Class Entity / Class Variableโ€‹

Classโ€‹

@Entity(name = "scan_result")
@Getter
@Setter
public class ScanResult extends CrudEntity {

private Integer folderId;
private Integer scanNessusId;
private String name;
private Integer month;

private Date vaDate;

private Integer hostId;
private String riskLevel;
@Type(type = "text")
private String system;
private String protocol;
private String port;
private String pluginId;
@Type(type = "text")
private String pluginName;
@Type(type = "text")
private String synopsis;
@Type(type = "text")
private String detail;
@Type(type = "text")
private String solution;
@Type(type = "text")
private String pluginOutput;
private String status;
private Boolean isApprove;
private String assetTag;
private String assetOwner;
private Long aging;
private String managedBy;
@Type(type = "text")
private String reason;
private String submitedBy;
private String assetSeverity;
@Type(type = "text")
private String assetOwnerEmail;
private String applicationFunction;
private String typeVulner;
private Integer monthHostStart;
private Date hostStart;
@Column(columnDefinition = "boolean default true")
private Boolean isActive;
private UUID parentId;
@OneToOne
private Ravulner ravulner;
@Transient
private List<RemediationDocument> remediationDocument;
}

Unit Testโ€‹

class ScanResultTest {
private ScanResult scanResultUnderTest;

@BeforeEach
void setUp() {
scanResultUnderTest = new ScanResult();
}

@Test
void testScanNessusIdGetterAndSetter() {
final Integer scanNessusId = 1;
scanResultUnderTest.setScanNessusId(scanNessusId);
assertThat(scanResultUnderTest.getScanNessusId()).isEqualTo(scanNessusId);
}

@Test
void testNameGetterAndSetter() {
final String Name = "Name";
scanResultUnderTest.setName(Name);
assertThat(scanResultUnderTest.getName()).isEqualTo(Name);
}

@Test
void testMonthGetterAndSetter() {
final Integer month = 1;
scanResultUnderTest.setMonth(month);
assertThat(scanResultUnderTest.getMonth()).isEqualTo(month);
}

// and so on
}

Super Class / Class To Excelโ€‹

Classโ€‹

public abstract class FTTHExcelWriter<E> extends ExcelWriter<E> {
protected FTTHExcelWriter(List<E> entities) {
super(entities);
}

protected FTTHExcelWriter() {
super();
}

public abstract void getRows(Row row, E entity, XSSFWorkbook workbook);

private static final Logger LOGGER = Logger.getLogger(FTTHExcelWriter.class.getName());
@Override
public byte[] exportByte() {
Row header;
CellStyle headerStyle;
try (XSSFWorkbook workbook = new XSSFWorkbook()) {

Sheet sheet = workbook.createSheet(this.getClass().getSimpleName());

header = sheet.createRow(0);

headerStyle = workbook.createCellStyle();
headerStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

int i = 0;
for(String headerTitle : this.getHeaders()) {
Cell headerCell = header.createCell(i++);
headerCell.setCellValue(headerTitle);
headerCell.setCellStyle(headerStyle);
}
CellStyle style = workbook.createCellStyle();
style.setWrapText(true);

int j = 0;
for(E entity : this.entities) {
sheet.autoSizeColumn(j);
Row row = sheet.createRow(1 + j++);
this.getRows(row, entity, workbook);
}

for(int columnIndex = 0; columnIndex < 99; columnIndex++) {
sheet.autoSizeColumn(columnIndex);
}
ByteArrayOutputStream bos = this.createByteArrayOutputStream();

try {
workbook.write(bos);
}
finally {
bos.close();
}

return bos.toByteArray();
} catch (IOException e) {
LOGGER.log(Level.WARNING, e.getMessage(), e);
}
return new byte[0];
}

public ByteArrayOutputStream createByteArrayOutputStream() {
return new ByteArrayOutputStream();
}
}

Unit Testโ€‹

Jika class To Excel, cukup ikuti metode function nya saja, tidak perlu private class.

@ExtendWith(MockitoExtension.class)
class FTTHExcelWriterTest {
private static class TestFTTHExcelWriterTest extends FTTHExcelWriter<HomePlanning>{
public TestFTTHExcelWriterTest(List<HomePlanning> categories) {
super(categories);
}
public TestFTTHExcelWriterTest() {
super();
}

@Override
public void getRows(Row row, HomePlanning entity, XSSFWorkbook workbook) {

}

@Override
public List<String> getHeaders() {
ArrayList<String> headers = new ArrayList<>();
headers.add("Status");
headers.add("SPK Number");
headers.add("FLP Partner");
headers.add("PKS No");
headers.add("Cleanlist Grand Total");
headers.add("Created Date");
headers.add("Created By");
headers.add("Approved Date");
headers.add("Approved By");
return headers;
}

}

private TestFTTHExcelWriterTest ftthExcelWriterTest;

@Test
void testConstructorWithArgs() {
final HomePlanning homePlanning = new HomePlanning();
homePlanning.setCreatedDate(new GregorianCalendar(2020, Calendar.JANUARY, 1).getTime());
homePlanning.setCreatedBy("createdBy");
homePlanning.setApprovedDate(new GregorianCalendar(2020, Calendar.JANUARY, 1).getTime());
homePlanning.setApprovedBy("approvedBy");
homePlanning.setSpkNumber("spkNumber");
homePlanning.setPksNo("pksNo");
homePlanning.setStatus("status");
homePlanning.setGrandTotalHomepass(new BigInteger("100"));
final List<HomePlanning> categories = Arrays.asList(homePlanning);
ftthExcelWriterTest = new TestFTTHExcelWriterTest(categories);
assertThat(ftthExcelWriterTest).isNotNull();
}

@Test
void testConstructorWithoutArgs(){
ftthExcelWriterTest = new TestFTTHExcelWriterTest();
assertThat(ftthExcelWriterTest).isNotNull();
}

@Test
void testExportByte() {
final HomePlanning homePlanning = new HomePlanning();
homePlanning.setCreatedDate(new GregorianCalendar(2020, Calendar.JANUARY, 1).getTime());
homePlanning.setCreatedBy("createdBy");
homePlanning.setApprovedDate(new GregorianCalendar(2020, Calendar.JANUARY, 1).getTime());
homePlanning.setApprovedBy("approvedBy");
homePlanning.setSpkNumber("spkNumber");
homePlanning.setPksNo("pksNo");
homePlanning.setStatus("status");
homePlanning.setGrandTotalHomepass(new BigInteger("100"));
final List<HomePlanning> categories = Arrays.asList(homePlanning);
ftthExcelWriterTest = new TestFTTHExcelWriterTest(categories);
assertThat(ftthExcelWriterTest.exportByte()).isNotNull();
}

@Test
void testExportByteIOException() throws IOException {
final HomePlanning homePlanning = new HomePlanning();
final List<HomePlanning> categories = Arrays.asList(homePlanning);
ftthExcelWriterTest = spy(new TestFTTHExcelWriterTest(categories));
ByteArrayOutputStream byteArrayOutputStream = mock(ByteArrayOutputStream.class);
doReturn(byteArrayOutputStream).when(ftthExcelWriterTest).createByteArrayOutputStream();
doThrow(new IOException("IO Exception")).when(byteArrayOutputStream).close();
assertThat(ftthExcelWriterTest.exportByte()).isNotNull();
}
}

Class From Excelโ€‹

Classโ€‹

public class HpdbDocFromExcel extends ExcelReader<Hpdb> {
@Override
public int skippedRow() {
return 0;
}

@Override
public List<Hpdb> readXlsx(InputStream file) throws IOException {
Workbook workbook = new XSSFWorkbook(file);
Sheet sheet;
try{
sheet = workbook.getSheetAt(2);
} catch (IllegalArgumentException e){
throw new BadRequestException("HPDB data in HPDB List excel file must be on the third sheet");
}

List<Hpdb> result = new ArrayList<>();
int i = 0;

try{
Iterator<Row> rowIterator = sheet.rowIterator();
while (rowIterator.hasNext()) {
Row row = rowIterator.next();
if (i > this.skippedRow()) {
ArrayList<Object> columns = new ArrayList<>();
Iterator<Cell> cellIterator = row.cellIterator();
while (cellIterator.hasNext()) {
Cell cell = cellIterator.next();
if (cell.getCellType().name().equals("NUMERIC")) {
columns.add(cell.getNumericCellValue());
} else {
columns.add(cell.getStringCellValue());
}
}
result.add(this.perLine(columns.toArray()));
}
++i;
}
} catch (Exception e){
Sentry.captureEvent(new SentryEvent(e));
throw new BadRequestException("The HPDB List file that you uploaded is not appropriate");
}

return result;
}
}

Unit Testโ€‹

class HpdbDocFromExcelTest {

private HpdbDocFromExcel hpdbDocFromExcelUnderTest;

@BeforeEach
void setUp() {
hpdbDocFromExcelUnderTest = new HpdbDocFromExcel();
}

@Test
void testSkippedRow() {
assertThat(hpdbDocFromExcelUnderTest.skippedRow()).isEqualTo(0);
}

@Test
void testRead1() throws Exception {
FileInputStream file = new FileInputStream("src/test/resources/hpdb/hpdb1.xlsx");

// Run the test
final List<Hpdb> result = hpdbDocFromExcelUnderTest.readXlsx(file);

// Verify the results
assertNotNull(result);
}
}

ยท One min read

Untuk membuat warna mudah di maintain dan tersentralisasi, tailwind sudah menyediakan sebuah konfigurasi untuk menyimpan palet warna.

  1. Edit tailwind.config.js untuk memodifikasi atau menambah warna baru.

    theme: {
    extend: {
    colors: {
    primary: '#00529C',
    'primary-accent': '#004080',
    secondary: '#0095E8',
    danger: '#EC1E1E',
    success: '#1CC11C',
    warning: '#FF6100',
    default: '#5C5F62',
    },
    },
    },
  2. Untuk menggunakannya sama seperti penggunaan warna bawaan dari tailwind.

    .button.primary {
    @apply text-white bg-primary hover:bg-primary-accent;
    }

    .button.success {
    @apply text-white bg-success;
    }

    .button.default {
    @apply text-white bg-default;
    }