Skip to content

Aditya-feat: Add Network Failure Handling & Upload Status Feedback on Update Tool/Equipment Status Page#2103

Open
Aditya-gam wants to merge 5 commits intodevelopmentfrom
Aditya-feat/Add-Network–Failure-Handling-Upload-Status-Feedback-on-Update-Tool-Equipment-Status-Page

Hidden character warning

The head ref may contain hidden characters: "Aditya-feat/Add-Network\u2013Failure-Handling-Upload-Status-Feedback-on-Update-Tool-Equipment-Status-Page"
Open

Aditya-feat: Add Network Failure Handling & Upload Status Feedback on Update Tool/Equipment Status Page#2103
Aditya-gam wants to merge 5 commits intodevelopmentfrom
Aditya-feat/Add-Network–Failure-Handling-Upload-Status-Feedback-on-Update-Tool-Equipment-Status-Page

Conversation

@Aditya-gam
Copy link
Copy Markdown
Contributor

@Aditya-gam Aditya-gam commented Mar 14, 2026

Description

Adds backend support for image upload and storage on the BM Dashboard Update Tool/Equipment Status page (tools/:equipmentId/update). The existing status-update API now accepts an optional image (multipart/form-data), validates format and size, uploads to Azure Blob Storage, and persists the image URL on the equipment document and in the new status update record. Error responses are aligned with the frontend contract so users get clear feedback (e.g., "Image selected but not saved", "Invalid image. Use PNG, JPG, or JPEG under 5MB.") and no silent failures.
IssueDescription

Related PRs (if any):

Main changes explained:

Created/Updated Files:

  • src/models/bmdashboard/buildingEquipment.js (+2 lines)
    • Added optional imageUrl: String to the updateRecord subdocument so each status update can store an image URL.
    • Added optional imageUrl: String at the root of the schema so the equipment has a single “current” display image (set when an image is uploaded with a status update).
  • src/middleware/bmEquipmentStatusUpload.js (new, 44 lines)
    • Conditional multer: runs only when Content-Type is multipart/form-data; JSON requests pass through with req.file undefined.
    • Uses upload.single('image') with 5 MB limit and fileFilter allowing only image/png and image/jpeg.
    • On any multer error (e.g., file too large, wrong type), responds 400 with { error: 'Invalid image. Use PNG, JPG, or JPEG under 5MB.' }.
    • Exports constants: MAX_IMAGE_SIZE_BYTES, ALLOWED_IMAGE_MIME_TYPES, INVALID_IMAGE_ERROR, IMAGE_NOT_SAVED_ERROR for use in controller and tests.
  • src/routes/bmdashboard/bmEquipmentRouter.js (+5 lines)
    • Imports bmEquipmentStatusUpload and attaches it to the status route before the controller.
    • PUT /equipment/:equipmentId/status now runs: bmEquipmentStatusUploadcontroller.updateEquipmentStatus.
  • src/controllers/bmdashboard/bmEquipmentController.js (+133 / refactor)
    • New helpers:
      • validateAndUploadImage(file, equipmentId) — validates MIME (reuses middleware constants), checks Azure env vars (returns 503 with "Image storage is not configured on this server." if missing), builds blob path equipment/{equipmentId}/status/{timestamp}_{safeName}.{ext}, calls uploadFileToAzureBlobStorage, returns { imageUrl } or { error, status }.
      • buildUpdateRecord(fields, imageUrl) — builds the updateRecord object (date, createdBy, condition, lastUsedBy, lastUsedFor, replacementRequired, description, notes; adds imageUrl only when provided).
    • updateEquipmentStatus:
      • Validates equipmentId (ObjectId); returns 400 with { error: 'Invalid equipment ID.' } if invalid (previously could fall through).
      • When req.file is present: calls validateAndUploadImage; on error returns 400 / 503 / 500 with the contract message (error field).
      • Builds updateRecord with optional imageUrl; if image uploaded, also $set: { imageUrl } on the root document.
      • Uses findByIdAndUpdate with $push (and optional $set), then populate; returns 200 with updated equipment or 404 if not found.
      • Catch-all errors return 500 with { error: 'Image selected but not saved.' } and use logException for logging.
    • updateLogRecords: uses logException on DB error (consistent error reporting).
      • Imports: uploadFileToAzureBlobStorage, logException, and middleware constants.
  • src/controllers/bmdashboard/__tests__/bmEquipmentController.test.js (major expansion, ~+478 lines)
    • Mocks AzureBlobImages.uploadFileToAzureBlobStorage and logger.logException.
    • updateEquipmentById: tests for invalid equipmentId, invalid projectId, invalid enum, no valid fields, success (200), not found (404), and 500 on DB error.
    • updateLogRecords: tests for non-array body, empty array, invalid equipmentId, missing createdBy/type, success with filter by projectId, and 500 with logException.
    • updateEquipmentStatus: tests for invalid equipmentId (400), missing condition/createdBy (400), invalid createdBy (400), success without image (200), success with image (upload mock, 200, imageUrl in updateRecord and root), 400 on invalid MIME, 503 when Azure not configured, 500 on upload failure, 404 when equipment not found, 500 on DB error with IMAGE_NOT_SAVED_ERROR.
  • src/middleware/bmEquipmentStatusUpload.test.js (new, 176 lines)
    • Tests exported constants (MAX_IMAGE_SIZE_BYTES, ALLOWED_IMAGE_MIME_TYPES, INVALID_IMAGE_ERROR, IMAGE_NOT_SAVED_ERROR).
    • Tests multer fileFilter (accepts image/png, image/jpeg; rejects image/gif, image/webp, application/pdf).
    • Tests limits.fileSize.
    • Tests non-multipart: next() called, no multer.
    • Tests multipart: next() on success; 400 with INVALID_IMAGE_ERROR on LIMIT_FILE_SIZE or invalid type; verifies upload.single('image') is used.

Key Implementation Details:

  • Dual request format: PUT /api/bm/equipment/:equipmentId/status accepts JSON (no image) or multipart/form-data with field image (one file). Middleware runs multer only for multipart so existing JSON clients are unchanged.
  • Validation: Image allowed only as PNG or JPEG, max 5 MB. Multer enforces size and MIME in middleware; controller re-validates MIME and handles Azure/env errors.
  • Blob path: equipment/{equipmentId}/status/{timestamp}_{sanitizedOriginalName}.{ext} (same Azure container as rest of app; env: AZURE_STORAGE_CONNECTION_STRING, AZURE_STORAGE_CONTAINER_NAME).
  • Persistence: New updateRecord entry gets an optional imageUrl; when an image is uploaded, root-level imageUrl is also set so equipmentDetails.imageUrl shows the latest photo.
  • Error contract: All user-facing errors use error in the response body. Messages: Invalid image. Use PNG, JPG, or JPEG under 5MB. (400), Image selected but not saved. (500), Image storage is not configured on this server. (503).
  • No silent failures: Every submission results in either 200 with updated equipment or a 4xx/5xx with a clear error message.

How to test:

  1. Check out the current branch:
    git checkout Aditya-feat/Add-Network–Failure-Handling-Upload-Status-Feedback-on-Update-Tool-Equipment-Status-Page
  2. Reinstall dependencies and clean cache using rm -rf node_modules package-lock.json && npm cache clean --force
  3. Run npm install to install dependencies, then start the backend locally (npm run dev)
  4. Test status update without image (JSON):
    • PUT http://localhost:4500/api/bm/equipment/:equipmentId/status
      • Headers: Content-Type: application/json
      • Body: { "condition": "Good", "createdBy": "<valid-user-object-id>", "lastUsedBy": "", "lastUsedFor": "", "replacementRequired": "", "description": "", "notes": "" }
      • Expect 200 and updated equipment; no imageUrl in latest updateRecord.
  5. Test status update with image (multipart):
    • Same URL, method PUT.
    • Content-Type: multipart/form-data
    • Form fields: condition, createdBy, lastUsedBy, lastUsedFor, replacementRequired, description, notes
    • File field: image — one PNG or JPEG file (< 5 MB).
    • Expect 200; response body should have latest updateRecord entry with imageUrl, and root imageUrl set.
  6. Test validation:
    • Send multipart with a file > 5 MB or MIME other than PNG/JPEG → expect 400 with { error: 'Invalid image. Use PNG, JPG, or JPEG under 5MB.' }.
    • Send invalid equipmentId (e.g. non-ObjectId) → 400 { error: 'Invalid equipment ID.' }.
    • Omit condition or createdBy400 { error: 'Condition and createdBy are required fields.' }.
  7. Test 503 when Azure not configured:
    • Unset Azure env vars, send multipart with valid image → 503 with { error: 'Image storage is not configured on this server.' }.
  8. Unit tests:
    • Run npm test (or npm test -- src/controllers/bmdashboard/__tests__/bmEquipmentController.test.js src/middleware/bmEquipmentStatusUpload.test.js).
    • All tests should pass.
  9. Verify:
    • GET /api/bm/equipment/:equipmentId returns equipment with imageUrl and updateRecord[].imageUrl when set.
    • Frontend Update Equipment page can submit with image and receive success or the agreed error messages for toasts.

Screenshots or videos of changes:

  • Test Video:
TestVideo.mov

Note

  • In my testing video, uploading the image throws an error. That is because I don't have the Azure environment variables for the backend. The rest of the functionality works, and once I get the variables, I will update the test videos.

Optional image can be sent with status updates via multipart/form-data.
Multer middleware validates type (PNG/JPEG) and size (5MB). Image is
uploaded to Azure Blob Storage; URL stored on equipment and in update
record. Schema adds imageUrl to equipment and updateRecord. Controller
uses logException for errors.

Made-with: Cursor
Return 503 when Azure Blob env vars are missing so clients can
distinguish config vs runtime failure. Extract buildUpdateRecord
helper and add JSDoc for PUT /equipment/:equipmentId/status.

Made-with: Cursor
Add tests for updateEquipmentById, updateLogRecords, and
updateEquipmentStatus (validation, image upload, 503/500/404).
Add tests for bmEquipmentStatusUpload middleware (fileFilter, limits,
multipart passthrough).

Made-with: Cursor
Add NOSONAR comments so SonarCloud accepts the 5 MB multer limit as reviewed. Limit is intentional and within guidance for file uploads.

Made-with: Cursor
@sonarqubecloud
Copy link
Copy Markdown

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