Test Cases
Test Strategy
| Test type |
Target |
Tool |
Current state |
| Unit tests (backend) |
Pure-function business logic |
Vitest |
Implemented (2 files) |
| Integration tests (backend) |
API and DB |
Vitest |
Not implemented (only the test:integration script exists) |
| Unit / E2E (frontend) |
Screens |
- |
Not set up (no test runner) |
Coverage is currently limited
Automated tests cover only the backend's services/scoring.ts and services/candidate.ts (parseResponseFilter). Everything else relies on Swagger UI (/docs) and manual verification in the UI. No coverage target has been set.
Running Tests
cd heineken-interview-arrange-backend
bun run test # vitest run (everything)
bun run test:unit # __tests__/unit only
bun run test:integration # __tests__/integration only (currently empty)
bun run test:watch # Watch mode
bun run test:coverage # Coverage (v8)
bunx vitest run __tests__/unit/services/scoring.test.ts # A single file
bunx vitest run -t "should cap score at maxScore" # A single test
The frontend has no test runner. Its quality gates are:
cd heineken-interview-arrange-frontend
bun run check-types
bun run lint
Test Configuration
vitest.config.ts:
| Item |
Value |
environment |
node |
globals |
true (no import needed for describe / it / expect) |
include |
__tests__/**/*.test.ts |
testTimeout / hookTimeout |
10000 ms |
| Coverage provider |
v8 (text / json / html reporters) |
Automated Test Cases
Scoring (__tests__/unit/services/scoring.test.ts)
calculateScore:
| ID |
Scenario |
Expected result |
Priority |
| SC-001 |
No rules |
Returns 0 |
High |
| SC-002 |
equals condition matches |
Adds the rule's score |
High |
| SC-003 |
not_equals condition |
Adds the score when it does not match |
High |
| SC-004 |
in condition |
Adds the score when the value is in the list |
High |
| SC-005 |
between condition within range |
Adds the score |
High |
| SC-006 |
between condition out of range |
Contributes 0 |
High |
| SC-007 |
contains condition (array value) |
Adds the score when the element is present |
Medium |
| SC-008 |
greater_than condition |
Adds the score above the threshold |
Medium |
| SC-009 |
less_than condition |
Adds the score below the threshold |
Medium |
| SC-010 |
Multiple rules |
Sums each rule's score |
High |
| SC-011 |
Total exceeds maxScore |
Capped at maxScore |
High |
validateScoringRules:
| ID |
Scenario |
Expected result |
Priority |
| SC-012 |
Well-formed rules |
Returns true |
High |
| SC-013 |
Malformed rules |
Returns false |
High |
| SC-014 |
Unknown condition |
Returns false |
High |
Response Filter Parsing (__tests__/unit/services/candidate.test.ts)
parseResponseFilter (the responseFilter query on the candidate list):
| ID |
Scenario |
Expected result |
Priority |
| CF-001 |
Empty string |
Returns undefined |
High |
| CF-002 |
undefined |
Returns undefined |
High |
| CF-003 |
String that is not valid JSON |
Throws BadRequestError |
High |
| CF-004 |
JSON that does not match the schema |
Throws BadRequestError |
High |
| CF-005 |
Array form with one condition |
Interpreted as AND |
High |
| CF-006 |
Array form with several conditions |
All combined with AND |
High |
| CF-007 |
Object form with logic: "and" |
Interpreted as AND |
High |
| CF-008 |
Object form with logic: "or" |
Interpreted as OR |
High |
| CF-009 |
logic omitted |
Defaults to AND |
High |
| CF-010 |
eq operator |
Accepted |
Medium |
| CF-011 |
neq operator |
Accepted |
Medium |
| CF-012 |
contains operator |
Accepted |
Medium |
| CF-013 |
in operator with an array value |
Accepted |
Medium |
| CF-014 |
Unknown operator |
Rejected |
High |
| CF-015 |
String value |
Accepted |
Medium |
| CF-016 |
Numeric value |
Accepted |
Medium |
| CF-017 |
Array of strings |
Accepted |
Medium |
| CF-018 |
Array of numbers |
Accepted |
Medium |
| CF-019 |
Mixed array of strings and numbers |
Accepted |
Low |
| CF-020 |
Empty conditions array |
Handled without error |
Medium |
| CF-021 |
Field name containing dots |
Treated as a nested path |
Medium |
| CF-022 |
Japanese field names |
Handled correctly |
High |
| CF-023 |
Special characters in values |
Handled correctly |
Medium |
Areas Requiring Manual Verification
With no automated coverage, the following must be checked through Swagger UI (http://localhost:8080/docs) or the UI itself.
Authentication
| ID |
Scenario |
Expected result |
Priority |
| TC-101 |
Log in with a Google account |
Lands on the top page with accessToken in localStorage |
High |
| TC-102 |
Log in with an unregistered account |
A users row is created with role = member |
High |
| TC-103 |
Call a protected API without a token |
401 UNAUTHORIZED |
High |
| TC-104 |
Call an API with an expired token |
401 (Token has expired) |
High |
| TC-105 |
Call a write API as viewer |
403 FORBIDDEN |
High |
Arrange Flow
| ID |
Scenario |
Expected result |
Priority |
| TC-201 |
Calculate slots in STEP1 |
Slots come back from the interviewers' Google Calendars |
High |
| TC-202 |
Place tentative holds in STEP1 |
Events are created and stored in arrange_settings.step1.tentativeEventIds |
High |
| TC-203 |
Recreate tentative holds |
Existing holds are deleted first, then recreated |
Medium |
| TC-204 |
Define and save segments in STEP2 |
Persisted to arrange_settings.step2.segments |
High |
| TC-205 |
Mark primary / reserve in STEP2 |
project_candidates.selection_type is updated |
High |
| TC-206 |
Register additional questions in STEP3 |
Persisted to arrange_settings.step3.questions |
Medium |
| TC-207 |
Place holds with PROTOTYPE_MODE=true |
Nothing is written to the calendar |
High |
Candidate Import
| ID |
Scenario |
Expected result |
Priority |
| TC-301 |
Upload a UTF-16LE TSV |
Columns, sample values and a suggested mapping are returned |
High |
| TC-302 |
Upload a UTF-8 TSV |
400 from a parse error |
Medium |
| TC-303 |
Execute an import with a mapping |
Returns 201 and progress becomes processing |
High |
| TC-304 |
Import data containing an existing external_user_id |
Counted as a duplicate, not registered twice |
High |
| TC-305 |
Run a survey import preview |
Returns the filtered count and samples |
High |
| TC-306 |
Run a survey import |
An import_logs row is created and counts are filled in on completion |
High |
Status Management
| ID |
Scenario |
Expected result |
Priority |
| TC-401 |
Change not_contacted โ contacted |
Succeeds and appends one row to status_histories |
High |
| TC-402 |
Change away from completed |
400 INVALID_STATUS_TRANSITION |
High |
| TC-403 |
Move bounced โ contacted |
Succeeds |
Medium |
| TC-404 |
Move cancelled โ not_contacted |
Succeeds |
Medium |
Scheduling
| ID |
Scenario |
Expected result |
Priority |
| TC-501 |
Open the public page with a valid token |
Shows the request details and the candidate's name |
High |
| TC-502 |
Open with an expired token |
Returns is_expired: true or an error |
High |
| TC-503 |
Submit one preferred slot |
Returns selectedCount: 1 and the status becomes scheduling |
High |
| TC-504 |
Submit 21 preferred slots |
400 (validation error) |
Medium |
| TC-505 |
Submit preferred slots again |
Existing slots are fully replaced |
High |
| TC-506 |
Submit for a candidate already confirmed |
409 SLOT_NOT_AVAILABLE |
High |
| TC-507 |
Confirm the schedule as an admin |
A reservations row is created and the tentative event becomes confirmed |
High |
| TC-508 |
Confirm an already-confirmed candidate again |
409 SLOT_NOT_AVAILABLE |
High |
Email
| ID |
Scenario |
Expected result |
Priority |
| TC-601 |
Preview a template |
Returns the rendered result with placeholders substituted |
High |
| TC-602 |
Bulk send to several candidates |
Returns sent_count / failed_count and records email_logs |
High |
| TC-603 |
Send with an empty candidateIds |
400 (validation error) |
Medium |
| TC-604 |
Send with PROTOTYPE_MODE=true |
No email is actually sent |
High |
| TC-605 |
Delete a template |
Past delivery logs remain |
Medium |
Databricks Integration
| ID |
Scenario |
Expected result |
Priority |
| TC-701 |
Run the metadata refresh with dryRun |
Fetches metadata for 611 surveys and writes nothing |
High |
| TC-702 |
Run the metadata refresh |
databricks_surveys is upserted and existing rows survive |
High |
| TC-703 |
List surveys with Databricks unconfigured |
503 DATABRICKS_NOT_CONFIGURED |
Medium |
| TC-704 |
Fetch a survey absent from databricks_surveys |
404, never 502 |
High |
| TC-705 |
Fetch a survey with a non-numeric surveyId |
400 INVALID_SURVEY_ID; it never reaches a table name |
High |
| TC-706 |
Import a panel with several answers in one field |
Kept as a list rather than truncated to the first value |
High |
| TC-707 |
Filter responses on a question containing newlines |
200, never 500 |
High |
| TC-708 |
Sign in from a disallowed email domain |
Redirected back to login as domain_not_allowed |
High |
Adding a Test
- Place it at
__tests__/unit/services/<service-name>.test.ts
- Prioritize pure functions with no external dependencies (DB, Google API, Databricks)
- Structure
describe in three levels โ service name โ function name โ aspect โ matching the existing tests
- Running a single file with
bunx vitest run <path> is the fastest loop