Skip to content

Implement guard availability slot endpoints (Ticket #20) - #449

Open
Houda135 wants to merge 1 commit into
mainfrom
houda135/feature/guard-availability-slots
Open

Implement guard availability slot endpoints (Ticket #20)#449
Houda135 wants to merge 1 commit into
mainfrom
houda135/feature/guard-availability-slots

Conversation

@Houda135

@Houda135 Houda135 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements Ticket #20 - Guard Availability Slot Endpoints.

The Guard App (guard_app/src/api/availability.ts) already calls four calendar-slot endpoints that did not exist on the backend, so every one of those requests returned 404. This PR builds the backend half, end to end.

Endpoint Purpose
POST /api/v1/availability/slots Create one slot for the authenticated guard
GET /api/v1/availability/slots/my-slots List the guard's slots (optional startDate / endDate range)
DELETE /api/v1/availability/slots/:id Delete one of the guard's own slots
DELETE /api/v1/availability/slots/clear-all Delete all of the guard's slots

What's included

  • New models/AvailabilitySlot.js - per-date slots (guardId, date, fromTime, toTime, optional recurring), many per guard. This is a separate concept from the existing Availability model (one unique doc per user, days[] / timeSlots[] / live status), which is left completely untouched.
  • services/availabilitySlot.service.js - validation and ownership scoping.
  • controllers/availabilitySlot.controller.js - thin HTTP layer, following the controller → service → model direction in docs/system-architecture.md.
  • Routes + Swagger in availability.routes.js.
  • 3 new audit ACTIONS in middleware/logger.js.
  • 23 Jest tests.

Contract

Confirmed with Krisha Patel (Guard App Lead) before implementation, per Lou's pre-req:

  • Create returns { message, availability: <slot> }; list returns { availability: [...] }.
  • date is "YYYY-MM-DD", fromTime / toTime are "HH:MM" 24-hour.
  • Deletes return no meaningful body.
  • /slots/my-slots and /slots/clear-all are registered before /slots/:id so they aren't captured as an :id param.

Security notes for review

  • guardId always comes from req.user, never the request body. The service builds the document from a validated whitelist, so a client posting its own guardId cannot write to another guard's account. Covered by a dedicated test.
  • Ownership is enforced inside the query, not checked after the fetch -findOneAndDelete({ _id, guardId }). One atomic operation, no time-of-check/time-of-use gap.

Decision worth flagging

The ticket plan specified 403 when a slot isn't the caller's. This returns 404 instead, for two reasons:

  1. It matches Krisha's confirmed contract ("should 404 if the slot isn't the guard's").
  2. A 403 confirms the slot exists, which lets a caller probe for other guards' slot IDs. A 404 for both "doesn't exist" and "not yours" reveals nothing.

Happy to switch to 403 if you'd rather stay consistent with the plan.

Testing evidence - live endpoints

Run against the Docker stack on this branch (docker compose up backend mongodb), authenticated as the seeded guard mia.guard@secureshift.test (Mia Thompson, 5ec2f5c3…).

Create - 201, returned under availability as agreed with Krisha

$ curl -X POST /api/v1/availability/slots \
    -d '{"date":"2026-12-25","fromTime":"09:00","toTime":"17:00"}'

{"message":"Availability slot created successfully.",
 "availability":{"guardId":"5ec2f5c39208d038b7573636","date":"2026-12-25",
                 "fromTime":"09:00","toTime":"17:00","_id":"6a6bf553…",
                 "createdAt":"2026-07-31T01:07:31.826Z","updatedAt":"…"}}
← HTTP 201

Create with recurring - 201

$ curl -X POST /api/v1/availability/slots \
    -d '{"date":"2026-12-26","fromTime":"18:00","toTime":"22:00",
         "recurring":{"enabled":true,"pattern":"weekly","endDate":"2027-01-30"}}'

{"message":"Availability slot created successfully.",
 "availability":{…,"recurring":{"enabled":true,"pattern":"weekly","endDate":"2027-01-30"},…}}
← HTTP 201

Validation - 400s with actionable messages

$ … -d '{"date":"2026-13-40","fromTime":"09:00","toTime":"17:00"}'
{"message":"date \"2026-13-40\" is not a valid calendar date."}       ← HTTP 400

$ … -d '{"date":"2026-12-27","fromTime":"17:00","toTime":"09:00"}'
{"message":"fromTime must be earlier than toTime."}                   ← HTTP 400

List - 200, array under availability

$ curl /api/v1/availability/slots/my-slots
{"availability":[{…"date":"2026-12-25"…},{…"date":"2026-12-26"…}]}    ← HTTP 200

Date-range filter - 200, correctly narrowed to one slot

$ curl '/api/v1/availability/slots/my-slots?startDate=2026-12-26&endDate=2026-12-27'
{"availability":[{…"date":"2026-12-26"…}]}                            ← HTTP 200

This also demonstrates the route ordering is correct -/slots/my-slots is resolved as a literal path, not captured as /slots/:id.

Ownership - a forged guardId in the body is ignored

$ … -d '{"guardId":"5a24997bbf18fde63b90c10d",   ← another guard's id
         "date":"2026-12-28","fromTime":"10:00","toTime":"14:00"}'

{"availability":{"guardId":"5ec2f5c39208d038b7573636", …}}            ← HTTP 201

The slot is persisted against the authenticated guard, not the id supplied in the body.

Ownership - deleting another guard's slot returns 404

$ curl -X DELETE /api/v1/availability/slots/6a6bf5603a83edf3a0d1a7bb   ← Isha's slot
{"message":"Availability slot not found."}                            ← HTTP 404

Auth - no token is rejected

$ curl /api/v1/availability/slots/my-slots        (no Authorization header)
{"message":"Access denied. No token provided."}                       ← HTTP 401

Delete own slot, then clear-all - scoped to the caller

$ curl -X DELETE /api/v1/availability/slots/<own id>
{"message":"Availability slot deleted successfully."}                 ← HTTP 200

$ curl -X DELETE /api/v1/availability/slots/clear-all
{"message":"All availability slots cleared successfully.","deletedCount":2}  ← HTTP 200

Database check immediately after: Isha slots remaining: 1 | Mia slots remaining: 0 - clear-all removed only the authenticated guard's slots.

Testing evidence - unit tests

PASS tests/availabilitySlot.controller.test.js
  AvailabilitySlot Controller
    createSlot
      ✓ 401 when unauthenticated
      ✓ 400 when date is missing
      ✓ 400 when date format is wrong
      ✓ 400 when date is not a real calendar date
      ✓ 400 when time format is wrong
      ✓ 400 when fromTime is not before toTime
      ✓ 400 when recurring.enabled is not a boolean
      ✓ 400 when recurring is enabled with an invalid pattern
      ✓ 400 when recurring.endDate is before date
      ✓ 201 and returns the created slot under `availability`
      ✓ takes guardId from the token, never from the request body
    getMySlots
      ✓ 401 when unauthenticated
      ✓ 200 and returns slots under `availability`
      ✓ scopes the query to the authenticated guard
      ✓ 400 when startDate is malformed
      ✓ 400 when startDate is after endDate
      ✓ applies a date range filter when provided
    deleteSlot
      ✓ 401 when unauthenticated
      ✓ 404 when the id is not a valid ObjectId
      ✓ 404 when the slot does not belong to the guard
      ✓ 200 when the guard's own slot is deleted
    clearAllSlots
      ✓ 401 when unauthenticated
      ✓ 200 and returns the deleted count, scoped to the guard

Test Suites: 1 passed, 1 total
Tests:       23 passed, 23 total

Lint and formatting both clean against the CI commands:

npm run lint          → clean
npm run format:check  → All matched files use Prettier code style!

Notes

  • No existing endpoints, models or behaviour were changed - this is additive only.
  • Built on one branch rather than the three weekly slices sketched in the plan, since the work was completed in a single pass.

The Guard App (guard_app/src/api/availability.ts) already calls four
calendar-slot endpoints that did not exist on the backend, so those
requests returned 404. This implements them end to end.

- New AvailabilitySlot model (per-date slots: guardId, date, fromTime,
  toTime, optional recurring). Distinct from the existing Availability
  model, which is left untouched.
- availabilitySlot service + thin controller following the team's
  controller -> service -> model direction. guardId is always taken from
  the authenticated user, never the request body.
- Routes: POST /slots, GET /slots/my-slots, DELETE /slots/clear-all,
  DELETE /slots/:id. Literal paths are registered before /slots/:id so
  they are not captured as an id param.
- Input validation (date/time formats, from<to, recurring object,
  optional startDate/endDate range) with clear 400s; ownership 404s.
- 23 Jest tests covering success, validation, unauth and ownership.
- Swagger docs for the new endpoints; new audit ACTIONS.

Contract confirmed with Krisha Patel (Guard App Lead).
@Houda135
Houda135 requested a review from LoopyB July 31, 2026 01:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant