Recruitment Module — QA Test Plan
Author: Senior QA Engineer
Module: Recruitment (Job Requisitions → Job Openings → Job Applicants → Interview Schedules → Job Offers → Staffing Plans)
Type: Functional, Integration, Negative, Boundary, Permission, UI/UX
Environment: Local Docker (docker compose up) or staging
1. Test Strategy
The recruitment module is a sequential pipeline. Every downstream entity depends on an upstream one (an offer cannot exist without an applicant; an applicant cannot exist without an opening). This means:
- Happy path tests must run in order — you cannot test offers without first creating a requisition → opening → applicant → interview.
- Each stage must be tested in isolation (CRUD, filters, validation) before testing the full pipeline.
- Side effects are the highest-risk area — specifically: offer acceptance auto-promoting the applicant to
Hired, and applicant creation incrementing the opening'sapplicationsCount. These must be verified explicitly. - Permission boundaries must be tested with a non-admin user — not just assumed from the code.
Priority Order
| Priority | Area |
|---|---|
| P0 | End-to-end pipeline (smoke test — if this breaks, nothing else matters) |
| P1 | Side effects (offer → hired, applicant count) |
| P1 | Status transitions (only valid states accepted) |
| P2 | Filters and pagination |
| P2 | Permission enforcement |
| P3 | Validation and error messages |
| P3 | Staffing plans (independent of the hire pipeline) |
2. Test Environment Setup
Prerequisites
# Start the full stack
docker compose up -d
# Confirm backend is healthy
curl -s http://localhost:8080/actuator/health | jq '.status'
# Expected: "UP"
# Confirm frontend is served
curl -s -o /dev/null -w "%{http_code}" http://localhost:5173
# Expected: 200
Test Accounts Required
Create or confirm these accounts exist before starting. Each must have a distinct permission set.
| Account | Role / Permissions | Used For |
|---|---|---|
hr_admin | HR_RECRUITMENT_* (all) | Full pipeline tests |
hr_viewer | HR_RECRUITMENT_VIEW only | Permission boundary tests |
interviewer_user | HR_RECRUITMENT_INTERVIEW_MANAGE + VIEW | Interview-specific tests |
offer_user | HR_RECRUITMENT_OFFER_MANAGE + VIEW | Offer-specific tests |
no_recruitment | Any non-recruitment role | Negative permission tests |
Seed Data Required
| Entity | Minimum needed |
|---|---|
| Departments | At least 2 (e.g., Engineering, HR) |
| Positions | At least 2 (e.g., Software Engineer, HR Specialist) |
| Employees | At least 3 (1 for requested-by, 1 as interviewer, 1 as offer creator) |
3. Stage 1 — Job Requisitions
3.1 Happy Path
TC-REQ-001 — Create a requisition (Draft)
- Log in as
hr_admin. - Navigate to Recruitment → Job Requisitions.
- Click New Requisition.
- Fill in:
- Department:
Engineering - Position:
Software Engineer - Requested By: any employee
- Vacancies:
2 - Estimated Cost:
150000 - Description:
Backend hire for Q3
- Department:
- Submit.
Expected:
- Row appears in the grid with status
Draft. - Toast shows "Requisition created".
createdAtis today's date.
TC-REQ-002 — Edit a requisition
- Find the requisition created in TC-REQ-001.
- Click the Edit (pencil) action.
- Change
Vacanciesfrom2to3. - Save.
Expected:
- Grid row updates to show
3vacancies. - No duplicate rows appear.
TC-REQ-003 — Status transition: Draft → Open → Approved
- On the Draft requisition, click Status.
- Set status to
Open. Save. - Confirm row moves to the Open tab and disappears from Draft tab.
- Click Status again. Set to
Approved. Save. - Confirm row is now on the Approved tab.
Expected: Each status change reflects immediately on grid purge. The "All" tab always shows the row regardless of status.
TC-REQ-004 — Status transition: Approved → Filled
Prerequisite: Complete the full pipeline through TC-OFF-005 (offer accepted). Then return here.
- After a hire is confirmed, set the requisition status to
Filled.
Expected: Row moves to Filled tab. Status badge renders correctly.
TC-REQ-005 — Delete a Draft requisition
- Create a second requisition (leave it as
Draft). - Click Delete.
- Confirm the deletion dialog.
Expected: Row is removed from the grid. A deleted requisition cannot be retrieved by ID (returns 404).
3.2 Column Filters
TC-REQ-006 — Filter by department name
- On the All tab, open the column filter on Department.
- Type
Engineering.
Expected: Only rows with Engineering as department remain. Other rows disappear. Clearing the filter restores all rows.
TC-REQ-007 — Filter by created date range
- Open the Created column filter.
- Set a date range that includes today.
Expected: Rows within the range shown. Set the "from" date to tomorrow — grid shows zero rows.
TC-REQ-008 — Filter by status (Set filter)
- On the All tab, open the Status column filter.
- Select only
Draft.
Expected: Only draft requisitions shown. This is separate from the tab — the tab shows Approved rows, the column filter narrows within that tab's dataset.
3.3 Negative / Validation
TC-REQ-009 — Submit without required fields
- Open New Requisition form.
- Leave Department empty. Submit.
Expected: Validation error shown on the Department field. Form does not close.
TC-REQ-010 — Vacancies must be positive
- Enter
0in the Vacancies field. Submit.
Expected: Validation error. Backend returns 400 if client validation is bypassed.
TC-REQ-011 — Cannot skip status (Draft → Approved directly)
This depends on whether the backend enforces state machine rules. Test it.
- Set a
Draftrequisition status directly toApprovedvia the status dialog. - Check if the backend accepts or rejects it.
Expected (if state machine enforced): 400 Bad Request with a meaningful error message.
Log the actual behavior — if the system accepts it, raise a bug ticket for missing state machine enforcement.
3.4 Permission
TC-REQ-012 — Viewer cannot see New / Edit / Delete
- Log in as
hr_viewer. - Navigate to Job Requisitions.
Expected: New Requisition button is not visible. Edit, Status, and Delete row actions are not visible. Data is readable.
TC-REQ-013 — Unauthenticated access is blocked
- Log out.
- Hit
GET /api/job-requisitionsdirectly.
Expected: 401 Unauthorized.
4. Stage 2 — Job Openings
4.1 Happy Path
TC-OPEN-001 — Create an opening
- Navigate to Recruitment → Job Openings.
- Click New Opening.
- Fill in:
- Title:
Senior Backend Engineer - Description:
We are hiring... - Salary Range:
80000to120000 - Publish Salary Range: checked
- Posted Date: today
- Closing Date: 30 days from today
- Title:
- Submit.
Expected:
- Row appears with status
Openand0applicants. - Posted and Closing dates render in local date format.
TC-OPEN-002 — Applicant count increments on new applicant
Prerequisite: TC-OPEN-001 complete.
- Note the current
Applicantscount (should be0). - Create a Job Applicant (TC-APP-001) linked to this opening.
- Return to Job Openings.
Expected: The Applicants count for this opening is now 1. This is the auto-increment side effect — verify it is not just a stale cache.
TC-OPEN-003 — Public endpoint returns only Open openings
- Set one opening to
Closedand one toOpen. GET /api/public/job-openings(no auth token).
Expected: Only the Open opening is returned. The Closed one is excluded. Response is 200 (not 401).
TC-OPEN-004 — Cannot access a Closed opening via public endpoint
GET /api/public/job-openings/{id}where{id}is aClosedopening.
Expected: 404 Not Found. The system must not expose non-open listings publicly.
4.2 Negative
TC-OPEN-005 — Closing date before posted date
- Set
Closing Dateto yesterday whenPosted Dateis today. Submit.
Expected: Validation error on the closing date field. Log whether this is enforced client-side, server-side, or both.
TC-OPEN-006 — Salary range: From greater than To
- Enter
Salary From: 150000,Salary To: 80000. Submit.
Expected: Validation error. Log the actual behavior.
5. Stage 3 — Job Applicants
5.1 Happy Path
TC-APP-001 — Create an applicant manually
- Navigate to Recruitment → Job Applicants.
- Click Add Applicant.
- Fill in:
- Opening: select the opening from TC-OPEN-001
- First Name:
Juan - Last Name:
dela Cruz - Email:
[email protected] - Phone:
09171234567
- Submit.
Expected:
- Row appears with status
Appliedand today'sappliedDate. - Opening's
applicationsCountincrements (see TC-OPEN-002).
TC-APP-002 — Submit via public application form
POST /api/public/job-applicationswith payload:{"openingId": <id from TC-OPEN-001>,"firstName": "Maria","lastName": "Santos","phone": "09189999999","coverLetter": "I am very interested in this role."}
Expected:
200 OKwith the new applicant DTO.- Status is
Applied. notesfield contains the cover letter text.applicationsCounton the opening increments again (now2).- No auth header required.
TC-APP-003 — Status pipeline: Applied → Shortlisted → Interview
- Set Juan's status to
Shortlisted. - Confirm Juan moves to the Shortlisted tab.
- Set status to
Interview. - Confirm Juan moves to the Interview tab.
Expected: Each status change is reflected immediately. The applicant does not appear in the previous tab after transition.
TC-APP-004 — Status auto-set to Offered on offer creation
Verified as a side effect in TC-OFF-001. Document here as a cross-reference.
When a JobOffer is created for an applicant, the applicant's status must automatically change from Interview to Offered without HR manually updating it.
TC-APP-005 — Status auto-set to Hired on offer acceptance
Verified as a side effect in TC-OFF-005. Document here as a cross-reference.
When the offer status changes to Accepted, the applicant's status must automatically change to Hired.
5.2 Negative
TC-APP-006 — Duplicate email on same opening
- Try to create a second applicant for the same opening with
email: [email protected].
Expected: If the backend enforces uniqueness per opening+email, a 400 or 409 is returned. Log the actual behavior — if duplicates are allowed, raise a bug for missing uniqueness constraint.
TC-APP-007 — Apply to a Closed opening
POST /api/public/job-applicationswithopeningIdpointing to aClosedopening.
Expected: 400 Bad Request or 422 Unprocessable Entity. Applying to a closed opening should be rejected.
Log the actual behavior — this may not be implemented yet.
TC-APP-008 — Filter by applicant name
- Open the All tab on Job Applicants.
- Filter the
Last Namecolumn withdela Cruz.
Expected: Only Juan appears. Maria does not.
TC-APP-009 — Search filters both first and last name
- Use the
applicantNamefilter (or thesearchparam) with valueSantos.
Expected: Maria Santos appears. The filter hits both first and last name columns.
6. Stage 4 — Interview Schedules
6.1 Happy Path
TC-INT-001 — Schedule a first-round interview
- Navigate to Recruitment → Interview Schedules.
- Click Schedule Interview.
- Fill in:
- Applicant: Juan dela Cruz
- Interviewer: any employee (not Juan)
- Round:
Technical Round 1 - Scheduled At: tomorrow at 10:00 AM
- Location:
Conference Room A
- Submit.
Expected:
- Row appears with status
Scheduled. scheduledAtrenders as a full datetime (not just date).
TC-INT-002 — Schedule a second round for the same applicant
- Create another interview for Juan with Round:
HR Final.
Expected: Two separate rows exist for Juan. The system allows multiple interviews per applicant.
TC-INT-003 — Mark interview as Completed
- Set the Round 1 interview status to
Completed.
Expected: Row moves to the Completed tab. The feedback action becomes relevant.
TC-INT-004 — Add feedback to a completed interview
- On the completed interview, click the Feedback (speech bubble) action.
- Add feedback with a rating of
4/5and recommendationProceed to next round. - Save.
Expected:
- Feedback drawer closes without error.
- Feedback can be retrieved via
GET /api/interview-schedules/{id}/feedback.
TC-INT-005 — Mark second interview as No-Show
- Set the HR Final interview status to
No-Show.
Expected: Row moves to the No-Show tab.
6.2 Negative
TC-INT-006 — Schedule interview in the past
- Set
Scheduled Atto yesterday. Submit.
Expected: Validation error for past scheduling. Log whether this is enforced. If not, raise a bug.
TC-INT-007 — Interviewer cannot interview themselves
- Set the applicant and interviewer to the same person (if both have employee records).
Expected: Validation error. Log the actual behavior.
TC-INT-008 — Feedback requires interview to be Completed
- Try to add feedback to a
Scheduled(not yet completed) interview via API:POST /api/interview-schedules/{id}/feedback
Expected: If the backend enforces this, 400 is returned. Log the actual behavior.
TC-INT-009 — Permission: interviewer_user cannot delete interviews
- Log in as
interviewer_user. - Confirm the Delete action is not visible.
- Attempt
DELETE /api/interview-schedules/{id}directly.
Expected: 403 Forbidden.
7. Stage 5 — Job Offers
7.1 Happy Path
TC-OFF-001 — Create an offer (side effect: applicant → Offered)
- Navigate to Recruitment → Job Offers.
- Click Send Offer.
- Fill in:
- Applicant: Juan dela Cruz
- Position:
Software Engineer - Offered Salary:
95000 - Offer Date: today
- Start Date: 2 weeks from today
- Expiry Date: 1 week from today
- Submit.
Expected:
- Offer row appears with status
Pending. - Navigate to Job Applicants — Juan's status must now be
Offered(automatic side effect). - No manual status update was needed.
TC-OFF-002 — Decline an offer
- Set the offer status to
Declined.
Expected:
- Offer moves to Declined tab.
- Juan's applicant status does not automatically revert — HR must manually update it if needed.
- Document the actual behavior.
TC-OFF-003 — Withdraw an offer
- Create a second offer for Maria Santos (after completing TC-APP-001 through INT-001 for her separately).
- Set the offer status to
Withdrawn.
Expected: Offer moves to Withdrawn tab.
TC-OFF-004 — Create a second offer for the same applicant
- With Juan's first offer in
Declinedstate, try creating a second offer for Juan.
Expected: The system should allow a re-offer after decline. Verify this is possible and that Juan's status re-sets to Offered.
TC-OFF-005 — Accept an offer (side effect: applicant → Hired)
- Set Juan's (second) offer status to
Accepted.
Expected:
- Offer moves to Accepted tab.
- Navigate to Job Applicants — Juan's status is now
Hired(automatic side effect). This is the most critical side effect in the entire module. - No manual status change was made to the applicant.
7.2 Negative
TC-OFF-006 — Cannot create offer with expiry before offer date
- Set
Expiry Dateto yesterday relative toOffer Date. Submit.
Expected: Validation error. Log whether enforced.
TC-OFF-007 — Cannot create offer with start date before today
- Set
Start Dateto last month. Submit.
Expected: Validation error or warning. Log the actual behavior.
TC-OFF-008 — Offer requires a valid applicant ID
POST /api/job-offerswith a non-existentapplicantId.
Expected: 404 Not Found with message "Job applicant not found: X".
TC-OFF-009 — Permission: offer_user cannot access Job Requisitions
- Log in as
offer_user. - Navigate to Job Requisitions.
Expected: Page is inaccessible, or all actions are hidden. API GET /api/job-requisitions should still return data (VIEW permission is separate). Verify what offer_user can and cannot see.
8. Staffing Plans
8.1 Happy Path
TC-SP-001 — Create a staffing plan
- Navigate to Recruitment → Staffing Plans.
- Click New Plan.
- Fill in:
- Plan Name:
FY2026 Q3 Engineering - Fiscal Year:
2026 - Department:
Engineering - Total Budget:
5000000 - Created By: any employee
- Plan Name:
- Submit.
Expected:
- Row appears with status
Draft. - Department shows
Engineering. If no department is selected, it should displayCompany-wide.
TC-SP-002 — Add plan items
- Double-click the plan row (or click View Details row action).
- In the detail drawer, add an item:
- Role Title:
Senior Backend Engineer - Vacancies:
2 - Est. Cost:
2000000
- Role Title:
- Add a second item:
Junior QA Engineer,1,800000.
Expected: Both items appear in the items grid inside the drawer.
TC-SP-003 — Remove a plan item
- In the detail drawer, click the delete icon on the Junior QA Engineer item.
Expected: Item is removed. Senior Backend Engineer item remains.
TC-SP-004 — Status transitions: Draft → Active → Closed
- Update the plan status to
Active. - Confirm it moves to the Active tab.
- Update to
Closed. - Confirm it moves to the Closed tab.
TC-SP-005 — Double-click row opens detail drawer
- On any tab, double-click a staffing plan row.
Expected: The detail drawer opens showing the plan's metadata and items. This must work the same as clicking the View Details action.
8.2 Filters
TC-SP-006 — Filter by fiscal year
- Open the
Yearcolumn filter. - Enter
2026(number filter, equals).
Expected: Only plans for fiscal year 2026 appear.
TC-SP-007 — Filter by department name
- Filter the
Departmentcolumn withEngineering.
Expected: Only Engineering plans appear. Plans with null department (Company-wide) do not appear.
9. End-to-End Pipeline Test
This test runs the entire hiring pipeline in one sequence. It is the most important test in this document.
TC-E2E-001 — Full pipeline: Requisition to Hired
| Step | Action | Verify |
|---|---|---|
| 1 | Create a Job Requisition (Dept: HR, Position: HR Specialist, Vacancies: 1) | Status = Draft, row visible in Draft tab |
| 2 | Move requisition to Open | Row moves to Open tab |
| 3 | Move requisition to Approved | Row moves to Approved tab |
| 4 | Create a Job Opening linked to the requisition | Status = Open, applicationsCount = 0 |
| 5 | Submit a public application: POST /api/public/job-applications | Status = Applied, opening count = 1 |
| 6 | Set applicant status to Shortlisted | Row in Shortlisted tab |
| 7 | Set applicant status to Interview | Row in Interview tab |
| 8 | Create an Interview Schedule (Round 1) | Status = Scheduled |
| 9 | Mark interview Completed | Row in Completed tab |
| 10 | Add interview feedback | Feedback visible via GET |
| 11 | Create a Job Offer for the applicant | Offer status = Pending; applicant auto-moves to Offered |
| 12 | Set offer status to Accepted | Offer in Accepted tab; applicant auto-moves to Hired |
| 13 | Set requisition status to Filled | Row in Filled tab |
| 14 | Set opening status to Filled | Row in Filled tab |
Pass criteria: All 14 steps complete without errors. The two automatic side effects (steps 11 and 12) must occur without any manual intervention.
10. Regression Checklist
Run after any change to the recruitment module.
- Creating a job applicant increments the opening's
applicationsCount - Creating a job offer sets the applicant status to
Offered - Accepting a job offer sets the applicant status to
Hired - Declining/withdrawing an offer does not auto-change the applicant status
- The
Alltab always shows all records regardless of status - Per-status tabs only show rows matching that status
- Tab switching does not leak filter state from the previous tab
- Column filters trigger a fresh data fetch (network request visible in DevTools)
- Pagination works — page 2 loads different rows than page 1
-
GET /api/public/job-openingsreturns onlyOpenopenings without auth -
GET /api/job-requisitionsreturns401without auth -
hr_viewercan read all data but cannot create, update, or delete - Status dialog changes persist after closing and reopening the drawer
- Double-clicking a staffing plan row opens the detail drawer
- Adding/removing staffing plan items reflects immediately in the drawer grid
11. Bug Report Template
When a test fails, file the bug using this structure:
Title: [MODULE] Short description of the defect
Environment: local / staging / production
Browser: Chrome 124 / Firefox 126
User account: hr_admin / hr_viewer / etc.
Steps to Reproduce:
1.
2.
3.
Expected Result:
<what should have happened>
Actual Result:
<what actually happened>
Severity: Critical / High / Medium / Low
Priority: P0 / P1 / P2 / P3
Evidence:
- Screenshot or screen recording
- Network request/response (from DevTools)
- Backend log snippet if applicable
Severity Guide
| Severity | Definition |
|---|---|
| Critical | Data loss, security bypass, pipeline completely broken |
| High | A stage of the pipeline cannot be completed, side effect not firing |
| Medium | Filter/sort not working, UI mismatch, wrong status label |
| Low | Cosmetic issue, minor label text, non-blocking UX gap |
12. Known Risks and Areas to Watch
| Risk | Why it matters | How to test |
|---|---|---|
| Side effects breaking on refactor | The Hired auto-promotion is buried inside JobOfferServiceImpl — easy to accidentally break | Always run TC-OFF-005 after any service change |
| Filter JOINs causing duplicate rows | LEFT JOINs for name filters can multiply rows if the relation is one-to-many | Count rows before and after applying a name filter; totals must match |
| Concurrent offer acceptance | Two HR users accepting two offers for the same applicant simultaneously could set status twice | Simulate with two browser tabs if load testing is not available |
| Stale grid after mutation | Grid purge might not fire if the dialog closes before the mutation settles | Add a delay between submitting and checking — observe the network request order |
| Public endpoint exposing draft openings | A regression could accidentally expose non-Open openings | TC-OPEN-003 and TC-OPEN-004 must run on every deploy |
| Missing pagination on staffing plans | The old implementation returned a flat list; the new one paginates — old clients or tests may break | Verify GET /api/staffing-plans returns a Page object, not an array |