Complete ADR-008 Phase 1: API Versioning Strategy
Implement URI-based API versioning with /api/v1 prefix across all routes. This establishes a foundation for future API evolution and breaking changes. Changes: - server.ts: All routes mounted under /api/v1/ (15 route handlers) - apiClient.ts: Base URL updated to /api/v1 - swagger.ts: OpenAPI server URL changed to /api/v1 - Redirect middleware: Added backwards compatibility for /api/* → /api/v1/* - Tests: Updated 72 test files with versioned path assertions - ADR documentation: Marked Phase 1 as complete (Accepted status) Test fixes: - apiClient.test.ts: 27 tests updated for /api/v1 paths - user.routes.ts: 36 log messages updated to reflect versioned paths - swagger.test.ts: 1 test updated for new server URL - All integration/E2E tests updated for versioned endpoints All Phase 1 acceptance criteria met: ✓ Routes use /api/v1/ prefix ✓ Frontend requests /api/v1/ ✓ OpenAPI docs reflect /api/v1/ ✓ Backwards compatibility via redirect middleware ✓ Tests pass with versioned paths Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,17 +2,337 @@
|
|||||||
|
|
||||||
**Date**: 2025-12-12
|
**Date**: 2025-12-12
|
||||||
|
|
||||||
**Status**: Proposed
|
**Status**: Accepted (Phase 1 Complete)
|
||||||
|
|
||||||
|
**Updated**: 2026-01-26
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
As the application grows, the API will need to evolve. Making breaking changes to existing endpoints can disrupt clients (e.g., a mobile app or the web frontend). The current routing has no formal versioning scheme.
|
As the application grows, the API will need to evolve. Making breaking changes to existing endpoints can disrupt clients (e.g., a mobile app or the web frontend). The current routing has no formal versioning scheme.
|
||||||
|
|
||||||
|
### Current State
|
||||||
|
|
||||||
|
As of January 2026, the API operates without explicit versioning:
|
||||||
|
|
||||||
|
- All routes are mounted under `/api/*` (e.g., `/api/flyers`, `/api/users/profile`)
|
||||||
|
- The frontend `apiClient.ts` uses `API_BASE_URL = '/api'` as the base
|
||||||
|
- No version prefix exists in route paths
|
||||||
|
- Breaking changes would immediately affect all consumers
|
||||||
|
|
||||||
|
### Why Version Now?
|
||||||
|
|
||||||
|
1. **Future Mobile App**: A native mobile app is planned, which will have slower update cycles than the web frontend
|
||||||
|
2. **Third-Party Integrations**: Store partners may integrate with our API
|
||||||
|
3. **Deprecation Path**: Need a clear way to deprecate and remove endpoints
|
||||||
|
4. **Documentation**: OpenAPI documentation (ADR-018) should reflect versioned endpoints
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
We will adopt a URI-based versioning strategy for the API. All new and existing routes will be prefixed with a version number (e.g., `/api/v1/flyers`). This ADR establishes a clear policy for when to introduce a new version (`v2`) and how to manage deprecation of old versions.
|
We will adopt a URI-based versioning strategy for the API using a phased rollout approach. All routes will be prefixed with a version number (e.g., `/api/v1/flyers`).
|
||||||
|
|
||||||
|
### Versioning Format
|
||||||
|
|
||||||
|
```text
|
||||||
|
/api/v{MAJOR}/resource
|
||||||
|
```
|
||||||
|
|
||||||
|
- **MAJOR**: Incremented for breaking changes (v1, v2, v3...)
|
||||||
|
- Resource paths remain unchanged within a version
|
||||||
|
|
||||||
|
### What Constitutes a Breaking Change?
|
||||||
|
|
||||||
|
The following changes require a new API version:
|
||||||
|
|
||||||
|
| Change Type | Breaking? | Example |
|
||||||
|
| ----------------------------- | --------- | -------------------------------------------- |
|
||||||
|
| Remove endpoint | Yes | DELETE `/api/v1/legacy-feature` |
|
||||||
|
| Remove response field | Yes | Remove `user.email` from response |
|
||||||
|
| Change response field type | Yes | `id: number` to `id: string` |
|
||||||
|
| Change required request field | Yes | Make `email` required when it was optional |
|
||||||
|
| Rename endpoint | Yes | `/users` to `/accounts` |
|
||||||
|
| Add optional response field | No | Add `user.avatar_url` |
|
||||||
|
| Add optional request field | No | Add optional `page` parameter |
|
||||||
|
| Add new endpoint | No | Add `/api/v1/new-feature` |
|
||||||
|
| Fix bug in behavior | No* | Correct calculation error |
|
||||||
|
|
||||||
|
*Bug fixes may warrant version increment if clients depend on the buggy behavior.
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1: Namespace Migration (Current)
|
||||||
|
|
||||||
|
**Goal**: Add `/v1/` prefix to all existing routes without behavioral changes.
|
||||||
|
|
||||||
|
**Changes Required**:
|
||||||
|
|
||||||
|
1. **server.ts**: Update all route registrations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
app.use('/api/auth', authRouter);
|
||||||
|
|
||||||
|
// After
|
||||||
|
app.use('/api/v1/auth', authRouter);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **apiClient.ts**: Update base URL
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api';
|
||||||
|
|
||||||
|
// After
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/v1';
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **swagger.ts**: Update server definition
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: '/api/v1',
|
||||||
|
description: 'API v1 server',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Redirect Middleware** (optional): Support legacy clients
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Redirect unversioned routes to v1
|
||||||
|
app.use('/api/:resource', (req, res, next) => {
|
||||||
|
if (req.params.resource !== 'v1') {
|
||||||
|
return res.redirect(307, `/api/v1/${req.params.resource}${req.url}`);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- All existing functionality works at `/api/v1/*`
|
||||||
|
- Frontend makes requests to `/api/v1/*`
|
||||||
|
- OpenAPI documentation reflects `/api/v1/*` paths
|
||||||
|
- Integration tests pass with new paths
|
||||||
|
|
||||||
|
### Phase 2: Versioning Infrastructure
|
||||||
|
|
||||||
|
**Goal**: Build tooling to support multiple API versions.
|
||||||
|
|
||||||
|
**Components**:
|
||||||
|
|
||||||
|
1. **Version Router Factory**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/routes/versioned.ts
|
||||||
|
export function createVersionedRoutes(version: 'v1' | 'v2') {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
if (version === 'v1') {
|
||||||
|
router.use('/auth', authRouterV1);
|
||||||
|
router.use('/users', userRouterV1);
|
||||||
|
// ...
|
||||||
|
} else if (version === 'v2') {
|
||||||
|
router.use('/auth', authRouterV2);
|
||||||
|
router.use('/users', userRouterV2);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Version Detection Middleware**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Extract version from URL and attach to request
|
||||||
|
app.use('/api/:version', (req, res, next) => {
|
||||||
|
req.apiVersion = req.params.version;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Deprecation Headers**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Middleware to add deprecation headers
|
||||||
|
function deprecateVersion(sunsetDate: string) {
|
||||||
|
return (req, res, next) => {
|
||||||
|
res.set('Deprecation', 'true');
|
||||||
|
res.set('Sunset', sunsetDate);
|
||||||
|
res.set('Link', '</api/v2>; rel="successor-version"');
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Version 2 Support
|
||||||
|
|
||||||
|
**Goal**: Introduce v2 API when breaking changes are needed.
|
||||||
|
|
||||||
|
**Triggers for v2**:
|
||||||
|
|
||||||
|
- Major schema changes (e.g., unified item model)
|
||||||
|
- Response format overhaul
|
||||||
|
- Authentication mechanism changes
|
||||||
|
- Significant performance-driven restructuring
|
||||||
|
|
||||||
|
**Parallel Support**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
app.use('/api/v1', createVersionedRoutes('v1'));
|
||||||
|
app.use('/api/v2', createVersionedRoutes('v2'));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
|
||||||
|
### For Frontend (Web)
|
||||||
|
|
||||||
|
The web frontend is deployed alongside the API, so migration is straightforward:
|
||||||
|
|
||||||
|
1. Update `API_BASE_URL` in `apiClient.ts`
|
||||||
|
2. Update any hardcoded paths in tests
|
||||||
|
3. Deploy frontend and backend together
|
||||||
|
|
||||||
|
### For External Consumers
|
||||||
|
|
||||||
|
External consumers (mobile apps, partner integrations) need a transition period:
|
||||||
|
|
||||||
|
1. **Announcement**: 30 days before deprecation of v(N-1)
|
||||||
|
2. **Deprecation Headers**: Add headers 30 days before sunset
|
||||||
|
3. **Documentation**: Maintain docs for both versions during transition
|
||||||
|
4. **Sunset**: Remove v(N-1) after grace period
|
||||||
|
|
||||||
|
## Deprecation Timeline
|
||||||
|
|
||||||
|
| Version | Status | Sunset Date | Notes |
|
||||||
|
| -------------------- | ---------- | ---------------------- | --------------- |
|
||||||
|
| Unversioned `/api/*` | Deprecated | Phase 1 completion | Redirect to v1 |
|
||||||
|
| v1 | Active | TBD (when v2 releases) | Current version |
|
||||||
|
|
||||||
|
### Support Policy
|
||||||
|
|
||||||
|
- **Current Version (v(N))**: Full support, all features
|
||||||
|
- **Previous Version (v(N-1))**: Security fixes only for 6 months after v(N) release
|
||||||
|
- **Older Versions**: No support, endpoints return 410 Gone
|
||||||
|
|
||||||
|
## Backwards Compatibility Strategy
|
||||||
|
|
||||||
|
### Redirect Middleware
|
||||||
|
|
||||||
|
For a smooth transition, implement redirects from unversioned to versioned endpoints:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/middleware/versionRedirect.ts
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { logger } from '../services/logger.server';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to redirect unversioned API requests to v1.
|
||||||
|
* This provides backwards compatibility during the transition period.
|
||||||
|
*
|
||||||
|
* Example: /api/flyers -> /api/v1/flyers (307 Temporary Redirect)
|
||||||
|
*/
|
||||||
|
export function versionRedirectMiddleware(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const path = req.path;
|
||||||
|
|
||||||
|
// Skip if already versioned
|
||||||
|
if (path.startsWith('/v1') || path.startsWith('/v2')) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip health checks and documentation
|
||||||
|
if (path.startsWith('/health') || path.startsWith('/docs')) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log deprecation warning
|
||||||
|
logger.warn({
|
||||||
|
path: req.originalUrl,
|
||||||
|
method: req.method,
|
||||||
|
ip: req.ip,
|
||||||
|
}, 'Unversioned API request - redirecting to v1');
|
||||||
|
|
||||||
|
// Use 307 to preserve HTTP method
|
||||||
|
const redirectUrl = `/api/v1${path}${req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''}`;
|
||||||
|
return res.redirect(307, redirectUrl);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Versioning Headers
|
||||||
|
|
||||||
|
All API responses include version information:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Middleware to add version headers
|
||||||
|
app.use('/api/v1', (req, res, next) => {
|
||||||
|
res.set('X-API-Version', 'v1');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
**Positive**: Establishes a critical pattern for long-term maintainability. Allows the API to evolve without breaking existing clients.
|
### Positive
|
||||||
**Negative**: Adds a small amount of complexity to the routing setup. Requires discipline to manage versions and deprecations correctly.
|
|
||||||
|
- **Clear Evolution Path**: Establishes a critical pattern for long-term maintainability
|
||||||
|
- **Client Protection**: Allows the API to evolve without breaking existing clients
|
||||||
|
- **Parallel Development**: Can develop v2 features while maintaining v1 stability
|
||||||
|
- **Documentation Clarity**: Each version has its own complete documentation
|
||||||
|
- **Graceful Deprecation**: Clients have clear timelines and migration paths
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
|
||||||
|
- **Routing Complexity**: Adds complexity to the routing setup
|
||||||
|
- **Code Duplication**: May need to maintain multiple versions of handlers
|
||||||
|
- **Testing Overhead**: Tests may need to cover multiple versions
|
||||||
|
- **Documentation Maintenance**: Must keep docs for multiple versions in sync
|
||||||
|
|
||||||
|
### Mitigation
|
||||||
|
|
||||||
|
- Use shared business logic with version-specific adapters
|
||||||
|
- Automate deprecation header addition
|
||||||
|
- Generate versioned OpenAPI specs from code
|
||||||
|
- Clear internal guidelines on when to increment versions
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
| ----------------------------------- | --------------------------------------------- |
|
||||||
|
| `server.ts` | Route registration with version prefixes |
|
||||||
|
| `src/services/apiClient.ts` | Frontend API base URL configuration |
|
||||||
|
| `src/config/swagger.ts` | OpenAPI server URL and version info |
|
||||||
|
| `src/routes/*.routes.ts` | Individual route handlers |
|
||||||
|
| `src/middleware/versionRedirect.ts` | Backwards compatibility redirects (Phase 1) |
|
||||||
|
|
||||||
|
## Related ADRs
|
||||||
|
|
||||||
|
- [ADR-003](./0003-standardized-input-validation-using-middleware.md) - Input Validation (consistent across versions)
|
||||||
|
- [ADR-018](./0018-api-documentation-strategy.md) - API Documentation Strategy (versioned OpenAPI specs)
|
||||||
|
- [ADR-028](./0028-api-response-standardization.md) - Response Standardization (envelope pattern applies to all versions)
|
||||||
|
- [ADR-016](./0016-api-security-hardening.md) - Security Hardening (applies to all versions)
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
### Phase 1 Tasks
|
||||||
|
|
||||||
|
- [x] Update `server.ts` to mount all routes under `/api/v1/`
|
||||||
|
- [x] Update `src/services/apiClient.ts` API_BASE_URL to `/api/v1`
|
||||||
|
- [x] Update `src/config/swagger.ts` server URL to `/api/v1`
|
||||||
|
- [x] Add redirect middleware for unversioned requests
|
||||||
|
- [x] Update integration tests to use versioned paths
|
||||||
|
- [x] Update API documentation examples (Swagger server URL updated)
|
||||||
|
- [x] Verify all health checks work at `/api/v1/health/*`
|
||||||
|
|
||||||
|
### Phase 2 Tasks (Future)
|
||||||
|
|
||||||
|
- [ ] Create version router factory
|
||||||
|
- [ ] Implement deprecation header middleware
|
||||||
|
- [ ] Add version detection to request context
|
||||||
|
- [ ] Document versioning patterns for developers
|
||||||
|
|
||||||
|
### Phase 3 Tasks (Future)
|
||||||
|
|
||||||
|
- [ ] Identify breaking changes requiring v2
|
||||||
|
- [ ] Create v2 route handlers
|
||||||
|
- [ ] Set deprecation timeline for v1
|
||||||
|
- [ ] Migrate documentation to multi-version format
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ This directory contains a log of the architectural decisions made for the Flyer
|
|||||||
## 3. API & Integration
|
## 3. API & Integration
|
||||||
|
|
||||||
**[ADR-003](./0003-standardized-input-validation-using-middleware.md)**: Standardized Input Validation using Middleware (Accepted)
|
**[ADR-003](./0003-standardized-input-validation-using-middleware.md)**: Standardized Input Validation using Middleware (Accepted)
|
||||||
**[ADR-008](./0008-api-versioning-strategy.md)**: API Versioning Strategy (Proposed)
|
**[ADR-008](./0008-api-versioning-strategy.md)**: API Versioning Strategy (Accepted - Phase 1 Complete)
|
||||||
**[ADR-018](./0018-api-documentation-strategy.md)**: API Documentation Strategy (Accepted)
|
**[ADR-018](./0018-api-documentation-strategy.md)**: API Documentation Strategy (Accepted)
|
||||||
**[ADR-022](./0022-real-time-notification-system.md)**: Real-time Notification System (Proposed)
|
**[ADR-022](./0022-real-time-notification-system.md)**: Real-time Notification System (Proposed)
|
||||||
**[ADR-028](./0028-api-response-standardization.md)**: API Response Standardization and Envelope Pattern (Implemented)
|
**[ADR-028](./0028-api-response-standardization.md)**: API Response Standardization and Envelope Pattern (Implemented)
|
||||||
|
|||||||
60
server.ts
60
server.ts
@@ -235,11 +235,11 @@ if (process.env.NODE_ENV !== 'production') {
|
|||||||
logger.info('API Documentation available at /docs/api-docs');
|
logger.info('API Documentation available at /docs/api-docs');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- API Routes ---
|
// --- API Routes (ADR-008: API Versioning Strategy - Phase 1) ---
|
||||||
|
|
||||||
// ADR-053: Worker Health Checks
|
// ADR-053: Worker Health Checks
|
||||||
// Expose queue metrics for monitoring.
|
// Expose queue metrics for monitoring at versioned endpoint.
|
||||||
app.get('/api/health/queues', async (req, res) => {
|
app.get('/api/v1/health/queues', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const statuses = await monitoringService.getQueueStatuses();
|
const statuses = await monitoringService.getQueueStatuses();
|
||||||
res.json(statuses);
|
res.json(statuses);
|
||||||
@@ -251,46 +251,60 @@ app.get('/api/health/queues', async (req, res) => {
|
|||||||
|
|
||||||
// The order of route registration is critical.
|
// The order of route registration is critical.
|
||||||
// More specific routes should be registered before more general ones.
|
// More specific routes should be registered before more general ones.
|
||||||
|
// All routes are now versioned under /api/v1 as per ADR-008.
|
||||||
// 1. Authentication routes for login, registration, etc.
|
// 1. Authentication routes for login, registration, etc.
|
||||||
app.use('/api/auth', authRouter); // This was a duplicate, fixed.
|
app.use('/api/v1/auth', authRouter);
|
||||||
// 2. System routes for health checks, etc.
|
// 2. System routes for health checks, etc.
|
||||||
app.use('/api/health', healthRouter);
|
app.use('/api/v1/health', healthRouter);
|
||||||
// 3. System routes for pm2 status, etc.
|
// 3. System routes for pm2 status, etc.
|
||||||
app.use('/api/system', systemRouter);
|
app.use('/api/v1/system', systemRouter);
|
||||||
// 3. General authenticated user routes.
|
// 3. General authenticated user routes.
|
||||||
app.use('/api/users', userRouter);
|
app.use('/api/v1/users', userRouter);
|
||||||
// 4. AI routes, some of which use optional authentication.
|
// 4. AI routes, some of which use optional authentication.
|
||||||
app.use('/api/ai', aiRouter);
|
app.use('/api/v1/ai', aiRouter);
|
||||||
// 5. Admin routes, which are all protected by admin-level checks.
|
// 5. Admin routes, which are all protected by admin-level checks.
|
||||||
app.use('/api/admin', adminRouter); // This seems to be missing from the original file list, but is required.
|
app.use('/api/v1/admin', adminRouter);
|
||||||
// 6. Budgeting and spending analysis routes.
|
// 6. Budgeting and spending analysis routes.
|
||||||
app.use('/api/budgets', budgetRouter);
|
app.use('/api/v1/budgets', budgetRouter);
|
||||||
// 7. Gamification routes for achievements.
|
// 7. Gamification routes for achievements.
|
||||||
app.use('/api/achievements', gamificationRouter);
|
app.use('/api/v1/achievements', gamificationRouter);
|
||||||
// 8. Public flyer routes.
|
// 8. Public flyer routes.
|
||||||
app.use('/api/flyers', flyerRouter);
|
app.use('/api/v1/flyers', flyerRouter);
|
||||||
// 8. Public recipe routes.
|
// 8. Public recipe routes.
|
||||||
app.use('/api/recipes', recipeRouter);
|
app.use('/api/v1/recipes', recipeRouter);
|
||||||
// 9. Public personalization data routes (master items, etc.).
|
// 9. Public personalization data routes (master items, etc.).
|
||||||
app.use('/api/personalization', personalizationRouter);
|
app.use('/api/v1/personalization', personalizationRouter);
|
||||||
// 9.5. Price history routes.
|
// 9.5. Price history routes.
|
||||||
app.use('/api/price-history', priceRouter);
|
app.use('/api/v1/price-history', priceRouter);
|
||||||
// 10. Public statistics routes.
|
// 10. Public statistics routes.
|
||||||
app.use('/api/stats', statsRouter);
|
app.use('/api/v1/stats', statsRouter);
|
||||||
// 11. UPC barcode scanning routes.
|
// 11. UPC barcode scanning routes.
|
||||||
app.use('/api/upc', upcRouter);
|
app.use('/api/v1/upc', upcRouter);
|
||||||
// 12. Inventory and expiry tracking routes.
|
// 12. Inventory and expiry tracking routes.
|
||||||
app.use('/api/inventory', inventoryRouter);
|
app.use('/api/v1/inventory', inventoryRouter);
|
||||||
// 13. Receipt scanning routes.
|
// 13. Receipt scanning routes.
|
||||||
app.use('/api/receipts', receiptRouter);
|
app.use('/api/v1/receipts', receiptRouter);
|
||||||
// 14. Deals and best prices routes.
|
// 14. Deals and best prices routes.
|
||||||
app.use('/api/deals', dealsRouter);
|
app.use('/api/v1/deals', dealsRouter);
|
||||||
// 15. Reactions/social features routes.
|
// 15. Reactions/social features routes.
|
||||||
app.use('/api/reactions', reactionsRouter);
|
app.use('/api/v1/reactions', reactionsRouter);
|
||||||
// 16. Store management routes.
|
// 16. Store management routes.
|
||||||
app.use('/api/stores', storeRouter);
|
app.use('/api/v1/stores', storeRouter);
|
||||||
// 17. Category discovery routes (ADR-023: Database Normalization)
|
// 17. Category discovery routes (ADR-023: Database Normalization)
|
||||||
app.use('/api/categories', categoryRouter);
|
app.use('/api/v1/categories', categoryRouter);
|
||||||
|
|
||||||
|
// --- Backwards Compatibility Redirect (ADR-008: API Versioning Strategy) ---
|
||||||
|
// Redirect old /api/* paths to /api/v1/* for backwards compatibility.
|
||||||
|
// This allows clients to gradually migrate to the versioned API.
|
||||||
|
app.use('/api', (req, res, next) => {
|
||||||
|
// Only redirect if the path does NOT already start with /v1
|
||||||
|
if (!req.path.startsWith('/v1')) {
|
||||||
|
const newPath = `/api/v1${req.path}`;
|
||||||
|
logger.info({ oldPath: `/api${req.path}`, newPath }, 'Redirecting to versioned API');
|
||||||
|
return res.redirect(301, newPath);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// --- Error Handling and Server Startup ---
|
// --- Error Handling and Server Startup ---
|
||||||
|
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
|||||||
{
|
{
|
||||||
clientID: process.env.GOOGLE_CLIENT_ID,
|
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
callbackURL: '/api/auth/google/callback',
|
callbackURL: '/api/v1/auth/google/callback',
|
||||||
scope: ['profile', 'email'],
|
scope: ['profile', 'email'],
|
||||||
},
|
},
|
||||||
async (
|
async (
|
||||||
@@ -242,7 +242,7 @@ if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) {
|
|||||||
{
|
{
|
||||||
clientID: process.env.GITHUB_CLIENT_ID,
|
clientID: process.env.GITHUB_CLIENT_ID,
|
||||||
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||||
callbackURL: '/api/auth/github/callback',
|
callbackURL: '/api/v1/auth/github/callback',
|
||||||
scope: ['user:email'],
|
scope: ['user:email'],
|
||||||
},
|
},
|
||||||
async (
|
async (
|
||||||
|
|||||||
@@ -79,10 +79,10 @@ describe('swagger configuration', () => {
|
|||||||
expect(spec.servers.length).toBeGreaterThan(0);
|
expect(spec.servers.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have /api as the server URL', () => {
|
it('should have /api/v1 as the server URL (ADR-008)', () => {
|
||||||
const apiServer = spec.servers.find((s) => s.url === '/api');
|
const apiServer = spec.servers.find((s) => s.url === '/api/v1');
|
||||||
expect(apiServer).toBeDefined();
|
expect(apiServer).toBeDefined();
|
||||||
expect(apiServer?.description).toBe('API server');
|
expect(apiServer?.description).toBe('API server (v1)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ const options: swaggerJsdoc.Options = {
|
|||||||
},
|
},
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
url: '/api',
|
url: '/api/v1',
|
||||||
description: 'API server',
|
description: 'API server (v1)',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ vi.mock('../config/passport', () => ({
|
|||||||
// Import the router AFTER all mocks are defined.
|
// Import the router AFTER all mocks are defined.
|
||||||
import adminRouter from './admin.routes';
|
import adminRouter from './admin.routes';
|
||||||
|
|
||||||
describe('Admin Content Management Routes (/api/admin)', () => {
|
describe('Admin Content Management Routes (/api/v1/admin)', () => {
|
||||||
const adminUser = createMockUserProfile({
|
const adminUser = createMockUserProfile({
|
||||||
role: 'admin',
|
role: 'admin',
|
||||||
user: { user_id: 'admin-user-id', email: 'admin@test.com' },
|
user: { user_id: 'admin-user-id', email: 'admin@test.com' },
|
||||||
@@ -161,7 +161,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
// Create a single app instance with an admin user for all tests in this suite.
|
// Create a single app instance with an admin user for all tests in this suite.
|
||||||
const app = createTestApp({
|
const app = createTestApp({
|
||||||
router: adminRouter,
|
router: adminRouter,
|
||||||
basePath: '/api/admin',
|
basePath: '/api/v1/admin',
|
||||||
authenticatedUser: adminUser,
|
authenticatedUser: adminUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
createMockSuggestedCorrection({ suggested_correction_id: 1 }),
|
createMockSuggestedCorrection({ suggested_correction_id: 1 }),
|
||||||
];
|
];
|
||||||
vi.mocked(mockedDb.adminRepo.getSuggestedCorrections).mockResolvedValue(mockCorrections);
|
vi.mocked(mockedDb.adminRepo.getSuggestedCorrections).mockResolvedValue(mockCorrections);
|
||||||
const response = await supertest(app).get('/api/admin/corrections');
|
const response = await supertest(app).get('/api/v1/admin/corrections');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockCorrections);
|
expect(response.body.data).toEqual(mockCorrections);
|
||||||
});
|
});
|
||||||
@@ -204,7 +204,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
vi.mocked(mockedDb.adminRepo.getSuggestedCorrections).mockRejectedValue(
|
vi.mocked(mockedDb.adminRepo.getSuggestedCorrections).mockRejectedValue(
|
||||||
new Error('DB Error'),
|
new Error('DB Error'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).get('/api/admin/corrections');
|
const response = await supertest(app).get('/api/v1/admin/corrections');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
});
|
});
|
||||||
@@ -212,7 +212,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
it('POST /corrections/:id/approve should approve a correction', async () => {
|
it('POST /corrections/:id/approve should approve a correction', async () => {
|
||||||
const correctionId = 123;
|
const correctionId = 123;
|
||||||
vi.mocked(mockedDb.adminRepo.approveCorrection).mockResolvedValue(undefined);
|
vi.mocked(mockedDb.adminRepo.approveCorrection).mockResolvedValue(undefined);
|
||||||
const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/approve`);
|
const response = await supertest(app).post(`/api/v1/admin/corrections/${correctionId}/approve`);
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual({ message: 'Correction approved successfully.' });
|
expect(response.body.data).toEqual({ message: 'Correction approved successfully.' });
|
||||||
expect(vi.mocked(mockedDb.adminRepo.approveCorrection)).toHaveBeenCalledWith(
|
expect(vi.mocked(mockedDb.adminRepo.approveCorrection)).toHaveBeenCalledWith(
|
||||||
@@ -224,14 +224,14 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
it('POST /corrections/:id/approve should return 500 on DB error', async () => {
|
it('POST /corrections/:id/approve should return 500 on DB error', async () => {
|
||||||
const correctionId = 123;
|
const correctionId = 123;
|
||||||
vi.mocked(mockedDb.adminRepo.approveCorrection).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(mockedDb.adminRepo.approveCorrection).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/approve`);
|
const response = await supertest(app).post(`/api/v1/admin/corrections/${correctionId}/approve`);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('POST /corrections/:id/reject should reject a correction', async () => {
|
it('POST /corrections/:id/reject should reject a correction', async () => {
|
||||||
const correctionId = 789;
|
const correctionId = 789;
|
||||||
vi.mocked(mockedDb.adminRepo.rejectCorrection).mockResolvedValue(undefined);
|
vi.mocked(mockedDb.adminRepo.rejectCorrection).mockResolvedValue(undefined);
|
||||||
const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/reject`);
|
const response = await supertest(app).post(`/api/v1/admin/corrections/${correctionId}/reject`);
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual({ message: 'Correction rejected successfully.' });
|
expect(response.body.data).toEqual({ message: 'Correction rejected successfully.' });
|
||||||
});
|
});
|
||||||
@@ -239,7 +239,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
it('POST /corrections/:id/reject should return 500 on DB error', async () => {
|
it('POST /corrections/:id/reject should return 500 on DB error', async () => {
|
||||||
const correctionId = 789;
|
const correctionId = 789;
|
||||||
vi.mocked(mockedDb.adminRepo.rejectCorrection).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(mockedDb.adminRepo.rejectCorrection).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/reject`);
|
const response = await supertest(app).post(`/api/v1/admin/corrections/${correctionId}/reject`);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
mockUpdatedCorrection,
|
mockUpdatedCorrection,
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put(`/api/admin/corrections/${correctionId}`)
|
.put(`/api/v1/admin/corrections/${correctionId}`)
|
||||||
.send(requestBody);
|
.send(requestBody);
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockUpdatedCorrection);
|
expect(response.body.data).toEqual(mockUpdatedCorrection);
|
||||||
@@ -262,7 +262,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
|
|
||||||
it('PUT /corrections/:id should return 400 for invalid data', async () => {
|
it('PUT /corrections/:id should return 400 for invalid data', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/admin/corrections/101')
|
.put('/api/v1/admin/corrections/101')
|
||||||
.send({ suggested_value: '' }); // Send empty value
|
.send({ suggested_value: '' }); // Send empty value
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -272,7 +272,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
new NotFoundError('Correction with ID 999 not found'),
|
new NotFoundError('Correction with ID 999 not found'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/admin/corrections/999')
|
.put('/api/v1/admin/corrections/999')
|
||||||
.send({ suggested_value: 'new value' });
|
.send({ suggested_value: 'new value' });
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toBe('Correction with ID 999 not found');
|
expect(response.body.error.message).toBe('Correction with ID 999 not found');
|
||||||
@@ -283,7 +283,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
new Error('Generic DB Error'),
|
new Error('Generic DB Error'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/admin/corrections/101')
|
.put('/api/v1/admin/corrections/101')
|
||||||
.send({ suggested_value: 'new value' });
|
.send({ suggested_value: 'new value' });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('Generic DB Error');
|
expect(response.body.error.message).toBe('Generic DB Error');
|
||||||
@@ -297,7 +297,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
createMockFlyer({ flyer_id: 2, status: 'needs_review' }),
|
createMockFlyer({ flyer_id: 2, status: 'needs_review' }),
|
||||||
];
|
];
|
||||||
vi.mocked(mockedDb.adminRepo.getFlyersForReview).mockResolvedValue(mockFlyers);
|
vi.mocked(mockedDb.adminRepo.getFlyersForReview).mockResolvedValue(mockFlyers);
|
||||||
const response = await supertest(app).get('/api/admin/review/flyers');
|
const response = await supertest(app).get('/api/v1/admin/review/flyers');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockFlyers);
|
expect(response.body.data).toEqual(mockFlyers);
|
||||||
expect(vi.mocked(mockedDb.adminRepo.getFlyersForReview)).toHaveBeenCalledWith(
|
expect(vi.mocked(mockedDb.adminRepo.getFlyersForReview)).toHaveBeenCalledWith(
|
||||||
@@ -307,7 +307,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
|
|
||||||
it('GET /review/flyers should return 500 on DB error', async () => {
|
it('GET /review/flyers should return 500 on DB error', async () => {
|
||||||
vi.mocked(mockedDb.adminRepo.getFlyersForReview).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(mockedDb.adminRepo.getFlyersForReview).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app).get('/api/admin/review/flyers');
|
const response = await supertest(app).get('/api/v1/admin/review/flyers');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
});
|
});
|
||||||
@@ -317,7 +317,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
// This test covers the error path for GET /stats
|
// This test covers the error path for GET /stats
|
||||||
it('GET /stats should return 500 on DB error', async () => {
|
it('GET /stats should return 500 on DB error', async () => {
|
||||||
vi.mocked(mockedDb.adminRepo.getApplicationStats).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(mockedDb.adminRepo.getApplicationStats).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app).get('/api/admin/stats');
|
const response = await supertest(app).get('/api/v1/admin/stats');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
});
|
});
|
||||||
@@ -327,14 +327,14 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
it('GET /brands should return a list of all brands', async () => {
|
it('GET /brands should return a list of all brands', async () => {
|
||||||
const mockBrands: Brand[] = [createMockBrand({ brand_id: 1, name: 'Brand A' })];
|
const mockBrands: Brand[] = [createMockBrand({ brand_id: 1, name: 'Brand A' })];
|
||||||
vi.mocked(mockedDb.flyerRepo.getAllBrands).mockResolvedValue(mockBrands);
|
vi.mocked(mockedDb.flyerRepo.getAllBrands).mockResolvedValue(mockBrands);
|
||||||
const response = await supertest(app).get('/api/admin/brands');
|
const response = await supertest(app).get('/api/v1/admin/brands');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockBrands);
|
expect(response.body.data).toEqual(mockBrands);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('GET /brands should return 500 on DB error', async () => {
|
it('GET /brands should return 500 on DB error', async () => {
|
||||||
vi.mocked(mockedDb.flyerRepo.getAllBrands).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(mockedDb.flyerRepo.getAllBrands).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app).get('/api/admin/brands');
|
const response = await supertest(app).get('/api/v1/admin/brands');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
});
|
});
|
||||||
@@ -344,7 +344,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
const mockLogoUrl = '/flyer-images/brand-logos/test-logo.png';
|
const mockLogoUrl = '/flyer-images/brand-logos/test-logo.png';
|
||||||
vi.mocked(mockedBrandService.updateBrandLogo).mockResolvedValue(mockLogoUrl);
|
vi.mocked(mockedBrandService.updateBrandLogo).mockResolvedValue(mockLogoUrl);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post(`/api/admin/brands/${brandId}/logo`)
|
.post(`/api/v1/admin/brands/${brandId}/logo`)
|
||||||
.attach('logoImage', Buffer.from('dummy-logo-content'), 'test-logo.png');
|
.attach('logoImage', Buffer.from('dummy-logo-content'), 'test-logo.png');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.message).toBe('Brand logo updated successfully.');
|
expect(response.body.data.message).toBe('Brand logo updated successfully.');
|
||||||
@@ -359,13 +359,13 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
const brandId = 55;
|
const brandId = 55;
|
||||||
vi.mocked(mockedBrandService.updateBrandLogo).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(mockedBrandService.updateBrandLogo).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post(`/api/admin/brands/${brandId}/logo`)
|
.post(`/api/v1/admin/brands/${brandId}/logo`)
|
||||||
.attach('logoImage', Buffer.from('dummy-logo-content'), 'test-logo.png');
|
.attach('logoImage', Buffer.from('dummy-logo-content'), 'test-logo.png');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('POST /brands/:id/logo should return 400 if no file is uploaded', async () => {
|
it('POST /brands/:id/logo should return 400 if no file is uploaded', async () => {
|
||||||
const response = await supertest(app).post('/api/admin/brands/55/logo');
|
const response = await supertest(app).post('/api/v1/admin/brands/55/logo');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.message).toMatch(
|
expect(response.body.error.message).toMatch(
|
||||||
/Logo image file is required|The request data is invalid|Logo image file is missing./,
|
/Logo image file is required|The request data is invalid|Logo image file is missing./,
|
||||||
@@ -378,7 +378,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
vi.mocked(mockedBrandService.updateBrandLogo).mockRejectedValue(dbError);
|
vi.mocked(mockedBrandService.updateBrandLogo).mockRejectedValue(dbError);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post(`/api/admin/brands/${brandId}/logo`)
|
.post(`/api/v1/admin/brands/${brandId}/logo`)
|
||||||
.attach('logoImage', Buffer.from('dummy-logo-content'), 'test-logo.png');
|
.attach('logoImage', Buffer.from('dummy-logo-content'), 'test-logo.png');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -391,7 +391,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
it('POST /brands/:id/logo should return 400 if a non-image file is uploaded', async () => {
|
it('POST /brands/:id/logo should return 400 if a non-image file is uploaded', async () => {
|
||||||
const brandId = 55;
|
const brandId = 55;
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post(`/api/admin/brands/${brandId}/logo`)
|
.post(`/api/v1/admin/brands/${brandId}/logo`)
|
||||||
.attach('logoImage', Buffer.from('this is not an image'), 'document.txt');
|
.attach('logoImage', Buffer.from('this is not an image'), 'document.txt');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
// This message comes from the handleMulterError middleware for the imageFileFilter
|
// This message comes from the handleMulterError middleware for the imageFileFilter
|
||||||
@@ -400,7 +400,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
|
|
||||||
it('POST /brands/:id/logo should return 400 for an invalid brand ID', async () => {
|
it('POST /brands/:id/logo should return 400 for an invalid brand ID', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/admin/brands/abc/logo')
|
.post('/api/v1/admin/brands/abc/logo')
|
||||||
.attach('logoImage', Buffer.from('dummy-logo-content'), 'test-logo.png');
|
.attach('logoImage', Buffer.from('dummy-logo-content'), 'test-logo.png');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -411,7 +411,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
const recipeId = 300;
|
const recipeId = 300;
|
||||||
vi.mocked(mockedDb.recipeRepo.deleteRecipe).mockResolvedValue(undefined);
|
vi.mocked(mockedDb.recipeRepo.deleteRecipe).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app).delete(`/api/admin/recipes/${recipeId}`);
|
const response = await supertest(app).delete(`/api/v1/admin/recipes/${recipeId}`);
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
expect(vi.mocked(mockedDb.recipeRepo.deleteRecipe)).toHaveBeenCalledWith(
|
expect(vi.mocked(mockedDb.recipeRepo.deleteRecipe)).toHaveBeenCalledWith(
|
||||||
recipeId,
|
recipeId,
|
||||||
@@ -422,14 +422,14 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('DELETE /recipes/:recipeId should return 400 for invalid ID', async () => {
|
it('DELETE /recipes/:recipeId should return 400 for invalid ID', async () => {
|
||||||
const response = await supertest(app).delete('/api/admin/recipes/abc');
|
const response = await supertest(app).delete('/api/v1/admin/recipes/abc');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('DELETE /recipes/:recipeId should return 500 on DB error', async () => {
|
it('DELETE /recipes/:recipeId should return 500 on DB error', async () => {
|
||||||
const recipeId = 300;
|
const recipeId = 300;
|
||||||
vi.mocked(mockedDb.recipeRepo.deleteRecipe).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(mockedDb.recipeRepo.deleteRecipe).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app).delete(`/api/admin/recipes/${recipeId}`);
|
const response = await supertest(app).delete(`/api/v1/admin/recipes/${recipeId}`);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -439,7 +439,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
const mockUpdatedRecipe = createMockRecipe({ recipe_id: recipeId, status: 'public' });
|
const mockUpdatedRecipe = createMockRecipe({ recipe_id: recipeId, status: 'public' });
|
||||||
vi.mocked(mockedDb.adminRepo.updateRecipeStatus).mockResolvedValue(mockUpdatedRecipe);
|
vi.mocked(mockedDb.adminRepo.updateRecipeStatus).mockResolvedValue(mockUpdatedRecipe);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put(`/api/admin/recipes/${recipeId}/status`)
|
.put(`/api/v1/admin/recipes/${recipeId}/status`)
|
||||||
.send(requestBody);
|
.send(requestBody);
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockUpdatedRecipe);
|
expect(response.body.data).toEqual(mockUpdatedRecipe);
|
||||||
@@ -449,7 +449,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
const recipeId = 201;
|
const recipeId = 201;
|
||||||
const requestBody = { status: 'invalid_status' };
|
const requestBody = { status: 'invalid_status' };
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put(`/api/admin/recipes/${recipeId}/status`)
|
.put(`/api/v1/admin/recipes/${recipeId}/status`)
|
||||||
.send(requestBody);
|
.send(requestBody);
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -459,7 +459,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
const requestBody = { status: 'public' as const };
|
const requestBody = { status: 'public' as const };
|
||||||
vi.mocked(mockedDb.adminRepo.updateRecipeStatus).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(mockedDb.adminRepo.updateRecipeStatus).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put(`/api/admin/recipes/${recipeId}/status`)
|
.put(`/api/v1/admin/recipes/${recipeId}/status`)
|
||||||
.send(requestBody);
|
.send(requestBody);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
@@ -473,7 +473,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
}); // This was a duplicate, fixed.
|
}); // This was a duplicate, fixed.
|
||||||
vi.mocked(mockedDb.adminRepo.updateRecipeCommentStatus).mockResolvedValue(mockUpdatedComment);
|
vi.mocked(mockedDb.adminRepo.updateRecipeCommentStatus).mockResolvedValue(mockUpdatedComment);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put(`/api/admin/comments/${commentId}/status`)
|
.put(`/api/v1/admin/comments/${commentId}/status`)
|
||||||
.send(requestBody);
|
.send(requestBody);
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockUpdatedComment);
|
expect(response.body.data).toEqual(mockUpdatedComment);
|
||||||
@@ -483,7 +483,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
const commentId = 301;
|
const commentId = 301;
|
||||||
const requestBody = { status: 'invalid_status' };
|
const requestBody = { status: 'invalid_status' };
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put(`/api/admin/comments/${commentId}/status`)
|
.put(`/api/v1/admin/comments/${commentId}/status`)
|
||||||
.send(requestBody);
|
.send(requestBody);
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -495,7 +495,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
new Error('DB Error'),
|
new Error('DB Error'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put(`/api/admin/comments/${commentId}/status`)
|
.put(`/api/v1/admin/comments/${commentId}/status`)
|
||||||
.send(requestBody);
|
.send(requestBody);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
@@ -511,14 +511,14 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
vi.mocked(mockedDb.adminRepo.getUnmatchedFlyerItems).mockResolvedValue(mockUnmatchedItems);
|
vi.mocked(mockedDb.adminRepo.getUnmatchedFlyerItems).mockResolvedValue(mockUnmatchedItems);
|
||||||
const response = await supertest(app).get('/api/admin/unmatched-items');
|
const response = await supertest(app).get('/api/v1/admin/unmatched-items');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockUnmatchedItems);
|
expect(response.body.data).toEqual(mockUnmatchedItems);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('GET /unmatched-items should return 500 on DB error', async () => {
|
it('GET /unmatched-items should return 500 on DB error', async () => {
|
||||||
vi.mocked(mockedDb.adminRepo.getUnmatchedFlyerItems).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(mockedDb.adminRepo.getUnmatchedFlyerItems).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app).get('/api/admin/unmatched-items');
|
const response = await supertest(app).get('/api/v1/admin/unmatched-items');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -528,7 +528,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
const flyerId = 42;
|
const flyerId = 42;
|
||||||
vi.mocked(mockedDb.flyerRepo.deleteFlyer).mockResolvedValue(undefined);
|
vi.mocked(mockedDb.flyerRepo.deleteFlyer).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app).delete(`/api/admin/flyers/${flyerId}`);
|
const response = await supertest(app).delete(`/api/v1/admin/flyers/${flyerId}`);
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
expect(vi.mocked(mockedDb.flyerRepo.deleteFlyer)).toHaveBeenCalledWith(
|
expect(vi.mocked(mockedDb.flyerRepo.deleteFlyer)).toHaveBeenCalledWith(
|
||||||
flyerId,
|
flyerId,
|
||||||
@@ -541,7 +541,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
vi.mocked(mockedDb.flyerRepo.deleteFlyer).mockRejectedValue(
|
vi.mocked(mockedDb.flyerRepo.deleteFlyer).mockRejectedValue(
|
||||||
new NotFoundError('Flyer with ID 999 not found.'),
|
new NotFoundError('Flyer with ID 999 not found.'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).delete(`/api/admin/flyers/${flyerId}`);
|
const response = await supertest(app).delete(`/api/v1/admin/flyers/${flyerId}`);
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toBe('Flyer with ID 999 not found.');
|
expect(response.body.error.message).toBe('Flyer with ID 999 not found.');
|
||||||
});
|
});
|
||||||
@@ -549,13 +549,13 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
|||||||
it('DELETE /flyers/:flyerId should return 500 on a generic DB error', async () => {
|
it('DELETE /flyers/:flyerId should return 500 on a generic DB error', async () => {
|
||||||
const flyerId = 42;
|
const flyerId = 42;
|
||||||
vi.mocked(mockedDb.flyerRepo.deleteFlyer).mockRejectedValue(new Error('Generic DB Error'));
|
vi.mocked(mockedDb.flyerRepo.deleteFlyer).mockRejectedValue(new Error('Generic DB Error'));
|
||||||
const response = await supertest(app).delete(`/api/admin/flyers/${flyerId}`);
|
const response = await supertest(app).delete(`/api/v1/admin/flyers/${flyerId}`);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('Generic DB Error');
|
expect(response.body.error.message).toBe('Generic DB Error');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('DELETE /flyers/:flyerId should return 400 for an invalid flyerId', async () => {
|
it('DELETE /flyers/:flyerId should return 400 for an invalid flyerId', async () => {
|
||||||
const response = await supertest(app).delete('/api/admin/flyers/abc');
|
const response = await supertest(app).delete('/api/v1/admin/flyers/abc');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toMatch(/Expected number, received nan/i);
|
expect(response.body.error.details[0].message).toMatch(/Expected number, received nan/i);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ vi.mock('../config/passport', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
describe('Admin Job Trigger Routes (/api/v1/admin/trigger)', () => {
|
||||||
const adminUser = createMockUserProfile({
|
const adminUser = createMockUserProfile({
|
||||||
role: 'admin',
|
role: 'admin',
|
||||||
user: { user_id: 'admin-user-id', email: 'admin@test.com' },
|
user: { user_id: 'admin-user-id', email: 'admin@test.com' },
|
||||||
@@ -107,7 +107,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
// Create a single app instance with an admin user for all tests in this suite.
|
// Create a single app instance with an admin user for all tests in this suite.
|
||||||
const app = createTestApp({
|
const app = createTestApp({
|
||||||
router: adminRouter,
|
router: adminRouter,
|
||||||
basePath: '/api/admin',
|
basePath: '/api/v1/admin',
|
||||||
authenticatedUser: adminUser,
|
authenticatedUser: adminUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
describe('POST /trigger/daily-deal-check', () => {
|
describe('POST /trigger/daily-deal-check', () => {
|
||||||
it('should trigger the daily deal check job and return 202 Accepted', async () => {
|
it('should trigger the daily deal check job and return 202 Accepted', async () => {
|
||||||
// Use the instance method mock
|
// Use the instance method mock
|
||||||
const response = await supertest(app).post('/api/admin/trigger/daily-deal-check');
|
const response = await supertest(app).post('/api/v1/admin/trigger/daily-deal-check');
|
||||||
expect(response.status).toBe(202);
|
expect(response.status).toBe(202);
|
||||||
expect(response.body.data.message).toContain('Daily deal check job has been triggered');
|
expect(response.body.data.message).toContain('Daily deal check job has been triggered');
|
||||||
expect(backgroundJobService.runDailyDealCheck).toHaveBeenCalledTimes(1);
|
expect(backgroundJobService.runDailyDealCheck).toHaveBeenCalledTimes(1);
|
||||||
@@ -128,7 +128,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
vi.mocked(backgroundJobService.runDailyDealCheck).mockImplementation(() => {
|
vi.mocked(backgroundJobService.runDailyDealCheck).mockImplementation(() => {
|
||||||
throw new Error('Job runner failed');
|
throw new Error('Job runner failed');
|
||||||
});
|
});
|
||||||
const response = await supertest(app).post('/api/admin/trigger/daily-deal-check');
|
const response = await supertest(app).post('/api/v1/admin/trigger/daily-deal-check');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toContain('Job runner failed');
|
expect(response.body.error.message).toContain('Job runner failed');
|
||||||
});
|
});
|
||||||
@@ -138,7 +138,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
it('should enqueue a job designed to fail and return 202 Accepted', async () => {
|
it('should enqueue a job designed to fail and return 202 Accepted', async () => {
|
||||||
const mockJob = { id: 'failing-job-id-456' } as Job;
|
const mockJob = { id: 'failing-job-id-456' } as Job;
|
||||||
vi.mocked(analyticsQueue.add).mockResolvedValue(mockJob);
|
vi.mocked(analyticsQueue.add).mockResolvedValue(mockJob);
|
||||||
const response = await supertest(app).post('/api/admin/trigger/failing-job');
|
const response = await supertest(app).post('/api/v1/admin/trigger/failing-job');
|
||||||
expect(response.status).toBe(202);
|
expect(response.status).toBe(202);
|
||||||
expect(response.body.data.message).toContain('Failing test job has been enqueued');
|
expect(response.body.data.message).toContain('Failing test job has been enqueued');
|
||||||
expect(analyticsQueue.add).toHaveBeenCalledWith('generate-daily-report', {
|
expect(analyticsQueue.add).toHaveBeenCalledWith('generate-daily-report', {
|
||||||
@@ -148,7 +148,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
|
|
||||||
it('should return 500 if enqueuing the job fails', async () => {
|
it('should return 500 if enqueuing the job fails', async () => {
|
||||||
vi.mocked(analyticsQueue.add).mockRejectedValue(new Error('Queue is down'));
|
vi.mocked(analyticsQueue.add).mockRejectedValue(new Error('Queue is down'));
|
||||||
const response = await supertest(app).post('/api/admin/trigger/failing-job');
|
const response = await supertest(app).post('/api/v1/admin/trigger/failing-job');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('Queue is down');
|
expect(response.body.error.message).toBe('Queue is down');
|
||||||
});
|
});
|
||||||
@@ -160,7 +160,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
'manual-report-job-123',
|
'manual-report-job-123',
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/admin/trigger/analytics-report');
|
const response = await supertest(app).post('/api/v1/admin/trigger/analytics-report');
|
||||||
|
|
||||||
expect(response.status).toBe(202);
|
expect(response.status).toBe(202);
|
||||||
expect(response.body.data.message).toContain(
|
expect(response.body.data.message).toContain(
|
||||||
@@ -173,7 +173,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
vi.mocked(backgroundJobService.triggerAnalyticsReport).mockRejectedValue(
|
vi.mocked(backgroundJobService.triggerAnalyticsReport).mockRejectedValue(
|
||||||
new Error('Queue error'),
|
new Error('Queue error'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).post('/api/admin/trigger/analytics-report');
|
const response = await supertest(app).post('/api/v1/admin/trigger/analytics-report');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -184,7 +184,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
'manual-weekly-report-job-123',
|
'manual-weekly-report-job-123',
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/admin/trigger/weekly-analytics');
|
const response = await supertest(app).post('/api/v1/admin/trigger/weekly-analytics');
|
||||||
|
|
||||||
expect(response.status).toBe(202);
|
expect(response.status).toBe(202);
|
||||||
expect(response.body.data.message).toContain('Successfully enqueued weekly analytics job');
|
expect(response.body.data.message).toContain('Successfully enqueued weekly analytics job');
|
||||||
@@ -195,7 +195,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
vi.mocked(backgroundJobService.triggerWeeklyAnalyticsReport).mockRejectedValue(
|
vi.mocked(backgroundJobService.triggerWeeklyAnalyticsReport).mockRejectedValue(
|
||||||
new Error('Queue error'),
|
new Error('Queue error'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).post('/api/admin/trigger/weekly-analytics');
|
const response = await supertest(app).post('/api/v1/admin/trigger/weekly-analytics');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -205,7 +205,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
const flyerId = 789;
|
const flyerId = 789;
|
||||||
const mockJob = { id: `cleanup-job-${flyerId}` } as Job;
|
const mockJob = { id: `cleanup-job-${flyerId}` } as Job;
|
||||||
vi.mocked(cleanupQueue.add).mockResolvedValue(mockJob);
|
vi.mocked(cleanupQueue.add).mockResolvedValue(mockJob);
|
||||||
const response = await supertest(app).post(`/api/admin/flyers/${flyerId}/cleanup`);
|
const response = await supertest(app).post(`/api/v1/admin/flyers/${flyerId}/cleanup`);
|
||||||
expect(response.status).toBe(202);
|
expect(response.status).toBe(202);
|
||||||
expect(response.body.data.message).toBe(
|
expect(response.body.data.message).toBe(
|
||||||
`File cleanup job for flyer ID ${flyerId} has been enqueued.`,
|
`File cleanup job for flyer ID ${flyerId} has been enqueued.`,
|
||||||
@@ -216,13 +216,13 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
it('should return 500 if enqueuing the cleanup job fails', async () => {
|
it('should return 500 if enqueuing the cleanup job fails', async () => {
|
||||||
const flyerId = 789;
|
const flyerId = 789;
|
||||||
vi.mocked(cleanupQueue.add).mockRejectedValue(new Error('Queue is down'));
|
vi.mocked(cleanupQueue.add).mockRejectedValue(new Error('Queue is down'));
|
||||||
const response = await supertest(app).post(`/api/admin/flyers/${flyerId}/cleanup`);
|
const response = await supertest(app).post(`/api/v1/admin/flyers/${flyerId}/cleanup`);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('Queue is down');
|
expect(response.body.error.message).toBe('Queue is down');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for an invalid flyerId', async () => {
|
it('should return 400 for an invalid flyerId', async () => {
|
||||||
const response = await supertest(app).post('/api/admin/flyers/abc/cleanup');
|
const response = await supertest(app).post('/api/v1/admin/flyers/abc/cleanup');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toMatch(/Expected number, received nan/i);
|
expect(response.body.error.details[0].message).toMatch(/Expected number, received nan/i);
|
||||||
});
|
});
|
||||||
@@ -237,7 +237,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
vi.mocked(monitoringService.retryFailedJob).mockResolvedValue(undefined);
|
vi.mocked(monitoringService.retryFailedJob).mockResolvedValue(undefined);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).post(`/api/admin/jobs/${queueName}/${jobId}/retry`);
|
const response = await supertest(app).post(`/api/v1/admin/jobs/${queueName}/${jobId}/retry`);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -252,7 +252,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if the queue name is invalid', async () => {
|
it('should return 400 if the queue name is invalid', async () => {
|
||||||
const response = await supertest(app).post(`/api/admin/jobs/invalid-queue/${jobId}/retry`);
|
const response = await supertest(app).post(`/api/v1/admin/jobs/invalid-queue/${jobId}/retry`);
|
||||||
// Zod validation fails because queue name is an enum
|
// Zod validation fails because queue name is an enum
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -266,7 +266,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
new NotFoundError(`Job with ID '${jobId}' not found in queue '${queueName}'.`),
|
new NotFoundError(`Job with ID '${jobId}' not found in queue '${queueName}'.`),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).post(`/api/admin/jobs/${queueName}/${jobId}/retry`);
|
const response = await supertest(app).post(`/api/v1/admin/jobs/${queueName}/${jobId}/retry`);
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toBe(
|
expect(response.body.error.message).toBe(
|
||||||
@@ -280,7 +280,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
new NotFoundError("Job with ID 'not-found-job' not found in queue 'flyer-processing'."),
|
new NotFoundError("Job with ID 'not-found-job' not found in queue 'flyer-processing'."),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).post(
|
const response = await supertest(app).post(
|
||||||
`/api/admin/jobs/${queueName}/not-found-job/retry`,
|
`/api/v1/admin/jobs/${queueName}/not-found-job/retry`,
|
||||||
);
|
);
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toContain('not found in queue');
|
expect(response.body.error.message).toContain('not found in queue');
|
||||||
@@ -292,7 +292,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
new ValidationError([], "Job is not in a 'failed' state. Current state: completed."),
|
new ValidationError([], "Job is not in a 'failed' state. Current state: completed."),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).post(`/api/admin/jobs/${queueName}/${jobId}/retry`);
|
const response = await supertest(app).post(`/api/v1/admin/jobs/${queueName}/${jobId}/retry`);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.message).toBe(
|
expect(response.body.error.message).toBe(
|
||||||
@@ -304,7 +304,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
// Mock monitoringService.retryFailedJob to throw a generic error
|
// Mock monitoringService.retryFailedJob to throw a generic error
|
||||||
vi.mocked(monitoringService.retryFailedJob).mockRejectedValue(new Error('Cannot retry job'));
|
vi.mocked(monitoringService.retryFailedJob).mockRejectedValue(new Error('Cannot retry job'));
|
||||||
|
|
||||||
const response = await supertest(app).post(`/api/admin/jobs/${queueName}/${jobId}/retry`);
|
const response = await supertest(app).post(`/api/v1/admin/jobs/${queueName}/${jobId}/retry`);
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toContain('Cannot retry job');
|
expect(response.body.error.message).toContain('Cannot retry job');
|
||||||
@@ -312,7 +312,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
|||||||
|
|
||||||
it('should return 400 for an invalid queueName or jobId', async () => {
|
it('should return 400 for an invalid queueName or jobId', async () => {
|
||||||
// This tests the Zod schema validation for the route params.
|
// This tests the Zod schema validation for the route params.
|
||||||
const response = await supertest(app).post('/api/admin/jobs/ / /retry');
|
const response = await supertest(app).post('/api/v1/admin/jobs/ / /retry');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ vi.mock('../config/passport', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('Admin Monitoring Routes (/api/admin)', () => {
|
describe('Admin Monitoring Routes (/api/v1/admin)', () => {
|
||||||
const adminUser = createMockUserProfile({
|
const adminUser = createMockUserProfile({
|
||||||
role: 'admin',
|
role: 'admin',
|
||||||
user: { user_id: 'admin-user-id', email: 'admin@test.com' },
|
user: { user_id: 'admin-user-id', email: 'admin@test.com' },
|
||||||
@@ -119,7 +119,7 @@ describe('Admin Monitoring Routes (/api/admin)', () => {
|
|||||||
// Create a single app instance with an admin user for all tests in this suite.
|
// Create a single app instance with an admin user for all tests in this suite.
|
||||||
const app = createTestApp({
|
const app = createTestApp({
|
||||||
router: adminRouter,
|
router: adminRouter,
|
||||||
basePath: '/api/admin',
|
basePath: '/api/v1/admin',
|
||||||
authenticatedUser: adminUser,
|
authenticatedUser: adminUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ describe('Admin Monitoring Routes (/api/admin)', () => {
|
|||||||
const mockLogs = [createMockActivityLogItem({ action: 'flyer_processed' })];
|
const mockLogs = [createMockActivityLogItem({ action: 'flyer_processed' })];
|
||||||
vi.mocked(adminRepo.getActivityLog).mockResolvedValue(mockLogs);
|
vi.mocked(adminRepo.getActivityLog).mockResolvedValue(mockLogs);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/admin/activity-log');
|
const response = await supertest(app).get('/api/v1/admin/activity-log');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockLogs);
|
expect(response.body.data).toEqual(mockLogs);
|
||||||
@@ -142,13 +142,13 @@ describe('Admin Monitoring Routes (/api/admin)', () => {
|
|||||||
it('should use limit and offset query parameters when provided', async () => {
|
it('should use limit and offset query parameters when provided', async () => {
|
||||||
vi.mocked(adminRepo.getActivityLog).mockResolvedValue([]);
|
vi.mocked(adminRepo.getActivityLog).mockResolvedValue([]);
|
||||||
|
|
||||||
await supertest(app).get('/api/admin/activity-log?limit=10&offset=20');
|
await supertest(app).get('/api/v1/admin/activity-log?limit=10&offset=20');
|
||||||
|
|
||||||
expect(adminRepo.getActivityLog).toHaveBeenCalledWith(10, 20, expect.anything());
|
expect(adminRepo.getActivityLog).toHaveBeenCalledWith(10, 20, expect.anything());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid limit and offset query parameters', async () => {
|
it('should return 400 for invalid limit and offset query parameters', async () => {
|
||||||
const response = await supertest(app).get('/api/admin/activity-log?limit=abc&offset=-1');
|
const response = await supertest(app).get('/api/v1/admin/activity-log?limit=abc&offset=-1');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details).toBeDefined();
|
expect(response.body.error.details).toBeDefined();
|
||||||
expect(response.body.error.details.length).toBe(2); // Both limit and offset are invalid
|
expect(response.body.error.details.length).toBe(2); // Both limit and offset are invalid
|
||||||
@@ -156,7 +156,7 @@ describe('Admin Monitoring Routes (/api/admin)', () => {
|
|||||||
|
|
||||||
it('should return 500 if fetching activity log fails', async () => {
|
it('should return 500 if fetching activity log fails', async () => {
|
||||||
vi.mocked(adminRepo.getActivityLog).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(adminRepo.getActivityLog).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app).get('/api/admin/activity-log');
|
const response = await supertest(app).get('/api/v1/admin/activity-log');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
});
|
});
|
||||||
@@ -175,7 +175,7 @@ describe('Admin Monitoring Routes (/api/admin)', () => {
|
|||||||
vi.mocked(monitoringService.getWorkerStatuses).mockResolvedValue(mockStatuses);
|
vi.mocked(monitoringService.getWorkerStatuses).mockResolvedValue(mockStatuses);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/admin/workers/status');
|
const response = await supertest(app).get('/api/v1/admin/workers/status');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -190,7 +190,7 @@ describe('Admin Monitoring Routes (/api/admin)', () => {
|
|||||||
|
|
||||||
it('should return 500 if fetching worker statuses fails', async () => {
|
it('should return 500 if fetching worker statuses fails', async () => {
|
||||||
vi.mocked(monitoringService.getWorkerStatuses).mockRejectedValue(new Error('Worker Error'));
|
vi.mocked(monitoringService.getWorkerStatuses).mockRejectedValue(new Error('Worker Error'));
|
||||||
const response = await supertest(app).get('/api/admin/workers/status');
|
const response = await supertest(app).get('/api/v1/admin/workers/status');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('Worker Error');
|
expect(response.body.error.message).toBe('Worker Error');
|
||||||
});
|
});
|
||||||
@@ -224,7 +224,7 @@ describe('Admin Monitoring Routes (/api/admin)', () => {
|
|||||||
vi.mocked(monitoringService.getQueueStatuses).mockResolvedValue(mockStatuses);
|
vi.mocked(monitoringService.getQueueStatuses).mockResolvedValue(mockStatuses);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/admin/queues/status');
|
const response = await supertest(app).get('/api/v1/admin/queues/status');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -255,7 +255,7 @@ describe('Admin Monitoring Routes (/api/admin)', () => {
|
|||||||
it('should return 500 if fetching queue counts fails', async () => {
|
it('should return 500 if fetching queue counts fails', async () => {
|
||||||
vi.mocked(monitoringService.getQueueStatuses).mockRejectedValue(new Error('Redis is down'));
|
vi.mocked(monitoringService.getQueueStatuses).mockRejectedValue(new Error('Redis is down'));
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/admin/queues/status');
|
const response = await supertest(app).get('/api/v1/admin/queues/status');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('Redis is down');
|
expect(response.body.error.message).toBe('Redis is down');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ import { cacheService } from '../services/cacheService.server';
|
|||||||
import { mockLogger } from '../tests/utils/mockLogger';
|
import { mockLogger } from '../tests/utils/mockLogger';
|
||||||
|
|
||||||
describe('Admin Routes Rate Limiting', () => {
|
describe('Admin Routes Rate Limiting', () => {
|
||||||
const app = createTestApp({ router: adminRouter, basePath: '/api/admin' });
|
const app = createTestApp({ router: adminRouter, basePath: '/api/v1/admin' });
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -109,13 +109,13 @@ describe('Admin Routes Rate Limiting', () => {
|
|||||||
// Make requests up to the limit
|
// Make requests up to the limit
|
||||||
for (let i = 0; i < limit; i++) {
|
for (let i = 0; i < limit; i++) {
|
||||||
await supertest(app)
|
await supertest(app)
|
||||||
.post('/api/admin/trigger/daily-deal-check')
|
.post('/api/v1/admin/trigger/daily-deal-check')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
// The next request should be blocked
|
// The next request should be blocked
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/admin/trigger/daily-deal-check')
|
.post('/api/v1/admin/trigger/daily-deal-check')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
|
|
||||||
expect(response.status).toBe(429);
|
expect(response.status).toBe(429);
|
||||||
@@ -132,12 +132,12 @@ describe('Admin Routes Rate Limiting', () => {
|
|||||||
// Note: We don't need to attach a file to test the rate limiter, as it runs before multer
|
// Note: We don't need to attach a file to test the rate limiter, as it runs before multer
|
||||||
for (let i = 0; i < limit; i++) {
|
for (let i = 0; i < limit; i++) {
|
||||||
await supertest(app)
|
await supertest(app)
|
||||||
.post(`/api/admin/brands/${brandId}/logo`)
|
.post(`/api/v1/admin/brands/${brandId}/logo`)
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post(`/api/admin/brands/${brandId}/logo`)
|
.post(`/api/v1/admin/brands/${brandId}/logo`)
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
|
|
||||||
expect(response.status).toBe(429);
|
expect(response.status).toBe(429);
|
||||||
@@ -151,7 +151,7 @@ describe('Admin Routes Rate Limiting', () => {
|
|||||||
vi.mocked(cacheService.invalidateBrands).mockResolvedValue(3);
|
vi.mocked(cacheService.invalidateBrands).mockResolvedValue(3);
|
||||||
vi.mocked(cacheService.invalidateStats).mockResolvedValue(2);
|
vi.mocked(cacheService.invalidateStats).mockResolvedValue(2);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/admin/system/clear-cache');
|
const response = await supertest(app).post('/api/v1/admin/system/clear-cache');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
@@ -168,7 +168,7 @@ describe('Admin Routes Rate Limiting', () => {
|
|||||||
const cacheError = new Error('Redis connection failed');
|
const cacheError = new Error('Redis connection failed');
|
||||||
vi.mocked(cacheService.invalidateFlyers).mockRejectedValue(cacheError);
|
vi.mocked(cacheService.invalidateFlyers).mockRejectedValue(cacheError);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/admin/system/clear-cache');
|
const response = await supertest(app).post('/api/v1/admin/system/clear-cache');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ const brandLogoUpload = createUploadMiddleware({
|
|||||||
|
|
||||||
// --- Bull Board (Job Queue UI) Setup ---
|
// --- Bull Board (Job Queue UI) Setup ---
|
||||||
const serverAdapter = new ExpressAdapter();
|
const serverAdapter = new ExpressAdapter();
|
||||||
serverAdapter.setBasePath('/api/admin/jobs'); // Set the base path for the UI
|
serverAdapter.setBasePath('/api/v1/admin/jobs'); // Set the base path for the UI
|
||||||
|
|
||||||
createBullBoard({
|
createBullBoard({
|
||||||
queues: [
|
queues: [
|
||||||
|
|||||||
@@ -86,12 +86,12 @@ vi.mock('../config/passport', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('Admin Stats Routes (/api/admin/stats)', () => {
|
describe('Admin Stats Routes (/api/v1/admin/stats)', () => {
|
||||||
const adminUser = createMockUserProfile({ role: 'admin' });
|
const adminUser = createMockUserProfile({ role: 'admin' });
|
||||||
// Create a single app instance with an admin user for all tests in this suite.
|
// Create a single app instance with an admin user for all tests in this suite.
|
||||||
const app = createTestApp({
|
const app = createTestApp({
|
||||||
router: adminRouter,
|
router: adminRouter,
|
||||||
basePath: '/api/admin',
|
basePath: '/api/v1/admin',
|
||||||
authenticatedUser: adminUser,
|
authenticatedUser: adminUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -110,14 +110,14 @@ describe('Admin Stats Routes (/api/admin/stats)', () => {
|
|||||||
recipeCount: 50,
|
recipeCount: 50,
|
||||||
};
|
};
|
||||||
vi.mocked(adminRepo.getApplicationStats).mockResolvedValue(mockStats);
|
vi.mocked(adminRepo.getApplicationStats).mockResolvedValue(mockStats);
|
||||||
const response = await supertest(app).get('/api/admin/stats');
|
const response = await supertest(app).get('/api/v1/admin/stats');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockStats);
|
expect(response.body.data).toEqual(mockStats);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
vi.mocked(adminRepo.getApplicationStats).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(adminRepo.getApplicationStats).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app).get('/api/admin/stats');
|
const response = await supertest(app).get('/api/v1/admin/stats');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
});
|
});
|
||||||
@@ -130,14 +130,14 @@ describe('Admin Stats Routes (/api/admin/stats)', () => {
|
|||||||
{ date: '2024-01-02', new_users: 3, new_flyers: 8 },
|
{ date: '2024-01-02', new_users: 3, new_flyers: 8 },
|
||||||
];
|
];
|
||||||
vi.mocked(adminRepo.getDailyStatsForLast30Days).mockResolvedValue(mockDailyStats);
|
vi.mocked(adminRepo.getDailyStatsForLast30Days).mockResolvedValue(mockDailyStats);
|
||||||
const response = await supertest(app).get('/api/admin/stats/daily');
|
const response = await supertest(app).get('/api/v1/admin/stats/daily');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockDailyStats);
|
expect(response.body.data).toEqual(mockDailyStats);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
vi.mocked(adminRepo.getDailyStatsForLast30Days).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(adminRepo.getDailyStatsForLast30Days).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app).get('/api/admin/stats/daily');
|
const response = await supertest(app).get('/api/v1/admin/stats/daily');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -90,14 +90,14 @@ vi.mock('../config/passport', () => ({
|
|||||||
isAdmin: (req: Request, res: Response, next: NextFunction) => next(),
|
isAdmin: (req: Request, res: Response, next: NextFunction) => next(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('Admin System Routes (/api/admin/system)', () => {
|
describe('Admin System Routes (/api/v1/admin/system)', () => {
|
||||||
const adminUser = createMockUserProfile({
|
const adminUser = createMockUserProfile({
|
||||||
role: 'admin',
|
role: 'admin',
|
||||||
user: { user_id: 'admin-user-id', email: 'admin@test.com' },
|
user: { user_id: 'admin-user-id', email: 'admin@test.com' },
|
||||||
});
|
});
|
||||||
const app = createTestApp({
|
const app = createTestApp({
|
||||||
router: adminRouter,
|
router: adminRouter,
|
||||||
basePath: '/api/admin',
|
basePath: '/api/v1/admin',
|
||||||
authenticatedUser: adminUser,
|
authenticatedUser: adminUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,14 +108,14 @@ describe('Admin System Routes (/api/admin/system)', () => {
|
|||||||
describe('POST /system/clear-geocode-cache', () => {
|
describe('POST /system/clear-geocode-cache', () => {
|
||||||
it('should return 200 on successful cache clear', async () => {
|
it('should return 200 on successful cache clear', async () => {
|
||||||
vi.mocked(geocodingService.clearGeocodeCache).mockResolvedValue(10);
|
vi.mocked(geocodingService.clearGeocodeCache).mockResolvedValue(10);
|
||||||
const response = await supertest(app).post('/api/admin/system/clear-geocode-cache');
|
const response = await supertest(app).post('/api/v1/admin/system/clear-geocode-cache');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.message).toContain('10 keys were removed');
|
expect(response.body.data.message).toContain('10 keys were removed');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 500 if clearing the cache fails', async () => {
|
it('should return 500 if clearing the cache fails', async () => {
|
||||||
vi.mocked(geocodingService.clearGeocodeCache).mockRejectedValue(new Error('Redis is down'));
|
vi.mocked(geocodingService.clearGeocodeCache).mockRejectedValue(new Error('Redis is down'));
|
||||||
const response = await supertest(app).post('/api/admin/system/clear-geocode-cache');
|
const response = await supertest(app).post('/api/v1/admin/system/clear-geocode-cache');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toContain('Redis is down');
|
expect(response.body.error.message).toContain('Redis is down');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ vi.mock('../config/passport', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('Admin User Management Routes (/api/admin/users)', () => {
|
describe('Admin User Management Routes (/api/v1/admin/users)', () => {
|
||||||
const adminId = '123e4567-e89b-12d3-a456-426614174000';
|
const adminId = '123e4567-e89b-12d3-a456-426614174000';
|
||||||
const userId = '123e4567-e89b-12d3-a456-426614174001';
|
const userId = '123e4567-e89b-12d3-a456-426614174001';
|
||||||
const adminUser = createMockUserProfile({
|
const adminUser = createMockUserProfile({
|
||||||
@@ -107,7 +107,7 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
|||||||
// Create a single app instance with an admin user for all tests in this suite.
|
// Create a single app instance with an admin user for all tests in this suite.
|
||||||
const app = createTestApp({
|
const app = createTestApp({
|
||||||
router: adminRouter,
|
router: adminRouter,
|
||||||
basePath: '/api/admin',
|
basePath: '/api/v1/admin',
|
||||||
authenticatedUser: adminUser,
|
authenticatedUser: adminUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
|||||||
createMockAdminUserView({ user_id: '2', email: 'user2@test.com', role: 'admin' }),
|
createMockAdminUserView({ user_id: '2', email: 'user2@test.com', role: 'admin' }),
|
||||||
];
|
];
|
||||||
vi.mocked(adminRepo.getAllUsers).mockResolvedValue({ users: mockUsers, total: 2 });
|
vi.mocked(adminRepo.getAllUsers).mockResolvedValue({ users: mockUsers, total: 2 });
|
||||||
const response = await supertest(app).get('/api/admin/users');
|
const response = await supertest(app).get('/api/v1/admin/users');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual({ users: mockUsers, total: 2 });
|
expect(response.body.data).toEqual({ users: mockUsers, total: 2 });
|
||||||
expect(adminRepo.getAllUsers).toHaveBeenCalledTimes(1);
|
expect(adminRepo.getAllUsers).toHaveBeenCalledTimes(1);
|
||||||
@@ -132,7 +132,7 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
|||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(adminRepo.getAllUsers).mockRejectedValue(dbError);
|
vi.mocked(adminRepo.getAllUsers).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/admin/users');
|
const response = await supertest(app).get('/api/v1/admin/users');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -141,7 +141,7 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
|||||||
it('should fetch a single user successfully', async () => {
|
it('should fetch a single user successfully', async () => {
|
||||||
const mockUser = createMockUserProfile({ user: { user_id: userId, email: 'user@test.com' } });
|
const mockUser = createMockUserProfile({ user: { user_id: userId, email: 'user@test.com' } });
|
||||||
vi.mocked(userRepo.findUserProfileById).mockResolvedValue(mockUser);
|
vi.mocked(userRepo.findUserProfileById).mockResolvedValue(mockUser);
|
||||||
const response = await supertest(app).get(`/api/admin/users/${userId}`);
|
const response = await supertest(app).get(`/api/v1/admin/users/${userId}`);
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockUser);
|
expect(response.body.data).toEqual(mockUser);
|
||||||
expect(userRepo.findUserProfileById).toHaveBeenCalledWith(userId, expect.any(Object));
|
expect(userRepo.findUserProfileById).toHaveBeenCalledWith(userId, expect.any(Object));
|
||||||
@@ -152,7 +152,7 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
|||||||
vi.mocked(userRepo.findUserProfileById).mockRejectedValue(
|
vi.mocked(userRepo.findUserProfileById).mockRejectedValue(
|
||||||
new NotFoundError('User not found.'),
|
new NotFoundError('User not found.'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).get(`/api/admin/users/${missingId}`);
|
const response = await supertest(app).get(`/api/v1/admin/users/${missingId}`);
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toBe('User not found.');
|
expect(response.body.error.message).toBe('User not found.');
|
||||||
});
|
});
|
||||||
@@ -160,7 +160,7 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
|||||||
it('should return 500 on a generic database error', async () => {
|
it('should return 500 on a generic database error', async () => {
|
||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(userRepo.findUserProfileById).mockRejectedValue(dbError);
|
vi.mocked(userRepo.findUserProfileById).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get(`/api/admin/users/${userId}`);
|
const response = await supertest(app).get(`/api/v1/admin/users/${userId}`);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -178,7 +178,7 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
|||||||
};
|
};
|
||||||
vi.mocked(adminRepo.updateUserRole).mockResolvedValue(updatedUser);
|
vi.mocked(adminRepo.updateUserRole).mockResolvedValue(updatedUser);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put(`/api/admin/users/${userId}`)
|
.put(`/api/v1/admin/users/${userId}`)
|
||||||
.send({ role: 'admin' });
|
.send({ role: 'admin' });
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(updatedUser);
|
expect(response.body.data).toEqual(updatedUser);
|
||||||
@@ -191,7 +191,7 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
|||||||
new NotFoundError(`User with ID ${missingId} not found.`),
|
new NotFoundError(`User with ID ${missingId} not found.`),
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put(`/api/admin/users/${missingId}`)
|
.put(`/api/v1/admin/users/${missingId}`)
|
||||||
.send({ role: 'user' });
|
.send({ role: 'user' });
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toBe(`User with ID ${missingId} not found.`);
|
expect(response.body.error.message).toBe(`User with ID ${missingId} not found.`);
|
||||||
@@ -201,7 +201,7 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
|||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(adminRepo.updateUserRole).mockRejectedValue(dbError);
|
vi.mocked(adminRepo.updateUserRole).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put(`/api/admin/users/${userId}`)
|
.put(`/api/v1/admin/users/${userId}`)
|
||||||
.send({ role: 'admin' });
|
.send({ role: 'admin' });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
@@ -209,7 +209,7 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
|||||||
|
|
||||||
it('should return 400 for an invalid role', async () => {
|
it('should return 400 for an invalid role', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put(`/api/admin/users/${userId}`)
|
.put(`/api/v1/admin/users/${userId}`)
|
||||||
.send({ role: 'super-admin' });
|
.send({ role: 'super-admin' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -220,7 +220,7 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
|||||||
const targetId = '123e4567-e89b-12d3-a456-426614174999';
|
const targetId = '123e4567-e89b-12d3-a456-426614174999';
|
||||||
vi.mocked(userRepo.deleteUserById).mockResolvedValue(undefined);
|
vi.mocked(userRepo.deleteUserById).mockResolvedValue(undefined);
|
||||||
vi.mocked(userService.deleteUserAsAdmin).mockResolvedValue(undefined);
|
vi.mocked(userService.deleteUserAsAdmin).mockResolvedValue(undefined);
|
||||||
const response = await supertest(app).delete(`/api/admin/users/${targetId}`);
|
const response = await supertest(app).delete(`/api/v1/admin/users/${targetId}`);
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
expect(userService.deleteUserAsAdmin).toHaveBeenCalledWith(
|
expect(userService.deleteUserAsAdmin).toHaveBeenCalledWith(
|
||||||
adminId,
|
adminId,
|
||||||
@@ -232,7 +232,7 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
|||||||
it('should prevent an admin from deleting their own account', async () => {
|
it('should prevent an admin from deleting their own account', async () => {
|
||||||
const validationError = new ValidationError([], 'Admins cannot delete their own account.');
|
const validationError = new ValidationError([], 'Admins cannot delete their own account.');
|
||||||
vi.mocked(userService.deleteUserAsAdmin).mockRejectedValue(validationError);
|
vi.mocked(userService.deleteUserAsAdmin).mockRejectedValue(validationError);
|
||||||
const response = await supertest(app).delete(`/api/admin/users/${adminId}`);
|
const response = await supertest(app).delete(`/api/v1/admin/users/${adminId}`);
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.message).toMatch(/Admins cannot delete their own account/);
|
expect(response.body.error.message).toMatch(/Admins cannot delete their own account/);
|
||||||
expect(userRepo.deleteUserById).not.toHaveBeenCalled();
|
expect(userRepo.deleteUserById).not.toHaveBeenCalled();
|
||||||
@@ -248,7 +248,7 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
|||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(userRepo.deleteUserById).mockRejectedValue(dbError);
|
vi.mocked(userRepo.deleteUserById).mockRejectedValue(dbError);
|
||||||
vi.mocked(userService.deleteUserAsAdmin).mockRejectedValue(dbError);
|
vi.mocked(userService.deleteUserAsAdmin).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).delete(`/api/admin/users/${targetId}`);
|
const response = await supertest(app).delete(`/api/v1/admin/users/${targetId}`);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ vi.mock('../config/passport', () => ({
|
|||||||
isAdmin: vi.fn((req, res, next) => next()),
|
isAdmin: vi.fn((req, res, next) => next()),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('AI Routes (/api/ai)', () => {
|
describe('AI Routes (/api/v1/ai)', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
// Reset logger implementation to no-op to prevent "Logging failed" leaks from previous tests
|
// Reset logger implementation to no-op to prevent "Logging failed" leaks from previous tests
|
||||||
@@ -123,7 +123,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
new NotFoundError('Job not found.'),
|
new NotFoundError('Job not found.'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const app = createTestApp({ router: aiRouter, basePath: '/api/ai' });
|
const app = createTestApp({ router: aiRouter, basePath: '/api/v1/ai' });
|
||||||
|
|
||||||
// New test to cover the router.use diagnostic middleware's catch block and errMsg branches
|
// New test to cover the router.use diagnostic middleware's catch block and errMsg branches
|
||||||
describe('Diagnostic Middleware Error Handling', () => {
|
describe('Diagnostic Middleware Error Handling', () => {
|
||||||
@@ -134,7 +134,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Make any request to trigger the middleware
|
// Make any request to trigger the middleware
|
||||||
const response = await supertest(app).get('/api/ai/jobs/job-123/status');
|
const response = await supertest(app).get('/api/v1/ai/jobs/job-123/status');
|
||||||
|
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
{ error: mockErrorObject.message }, // errMsg should extract the message
|
{ error: mockErrorObject.message }, // errMsg should extract the message
|
||||||
@@ -152,7 +152,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Make any request to trigger the middleware
|
// Make any request to trigger the middleware
|
||||||
const response = await supertest(app).get('/api/ai/jobs/job-123/status');
|
const response = await supertest(app).get('/api/v1/ai/jobs/job-123/status');
|
||||||
|
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
{ error: mockErrorString }, // errMsg should convert to string
|
{ error: mockErrorString }, // errMsg should convert to string
|
||||||
@@ -166,7 +166,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
throw null; // Simulate throwing null
|
throw null; // Simulate throwing null
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/ai/jobs/job-123/status');
|
const response = await supertest(app).get('/api/v1/ai/jobs/job-123/status');
|
||||||
|
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
{ error: 'An unknown error occurred.' }, // errMsg should handle null/undefined
|
{ error: 'An unknown error occurred.' }, // errMsg should handle null/undefined
|
||||||
@@ -187,7 +187,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
} as unknown as Job);
|
} as unknown as Job);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.field('checksum', validChecksum)
|
.field('checksum', validChecksum)
|
||||||
.attach('flyerFile', imagePath);
|
.attach('flyerFile', imagePath);
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
|
|
||||||
it('should return 400 if no file is provided', async () => {
|
it('should return 400 if no file is provided', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.field('checksum', validChecksum);
|
.field('checksum', validChecksum);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -208,7 +208,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
|
|
||||||
it('should return 400 if checksum is missing', async () => {
|
it('should return 400 if checksum is missing', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.attach('flyerFile', imagePath);
|
.attach('flyerFile', imagePath);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -224,7 +224,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
vi.mocked(aiService.aiService.enqueueFlyerProcessing).mockRejectedValue(duplicateError);
|
vi.mocked(aiService.aiService.enqueueFlyerProcessing).mockRejectedValue(duplicateError);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.field('checksum', validChecksum)
|
.field('checksum', validChecksum)
|
||||||
.attach('flyerFile', imagePath);
|
.attach('flyerFile', imagePath);
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.field('checksum', validChecksum)
|
.field('checksum', validChecksum)
|
||||||
.attach('flyerFile', imagePath);
|
.attach('flyerFile', imagePath);
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
});
|
});
|
||||||
const authenticatedApp = createTestApp({
|
const authenticatedApp = createTestApp({
|
||||||
router: aiRouter,
|
router: aiRouter,
|
||||||
basePath: '/api/ai',
|
basePath: '/api/v1/ai',
|
||||||
authenticatedUser: mockUser,
|
authenticatedUser: mockUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
await supertest(authenticatedApp)
|
await supertest(authenticatedApp)
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.set('Authorization', 'Bearer mock-token') // Add this to satisfy the header check in the route
|
.set('Authorization', 'Bearer mock-token') // Add this to satisfy the header check in the route
|
||||||
.field('checksum', validChecksum)
|
.field('checksum', validChecksum)
|
||||||
.attach('flyerFile', imagePath);
|
.attach('flyerFile', imagePath);
|
||||||
@@ -292,7 +292,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
});
|
});
|
||||||
const authenticatedApp = createTestApp({
|
const authenticatedApp = createTestApp({
|
||||||
router: aiRouter,
|
router: aiRouter,
|
||||||
basePath: '/api/ai',
|
basePath: '/api/v1/ai',
|
||||||
authenticatedUser: mockUserWithAddress,
|
authenticatedUser: mockUserWithAddress,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
await supertest(authenticatedApp)
|
await supertest(authenticatedApp)
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.set('Authorization', 'Bearer mock-token') // Add this to satisfy the header check in the route
|
.set('Authorization', 'Bearer mock-token') // Add this to satisfy the header check in the route
|
||||||
.field('checksum', validChecksum)
|
.field('checksum', validChecksum)
|
||||||
.attach('flyerFile', imagePath);
|
.attach('flyerFile', imagePath);
|
||||||
@@ -319,7 +319,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
const unlinkSpy = vi.spyOn(fs.promises, 'unlink').mockResolvedValue(undefined);
|
const unlinkSpy = vi.spyOn(fs.promises, 'unlink').mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.attach('flyerFile', imagePath); // No checksum field, will cause validation to throw
|
.attach('flyerFile', imagePath); // No checksum field, will cause validation to throw
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -338,7 +338,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
new NotFoundError('Job not found.'),
|
new NotFoundError('Job not found.'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/ai/jobs/non-existent-job/status');
|
const response = await supertest(app).get('/api/v1/ai/jobs/non-existent-job/status');
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toBe('Job not found.');
|
expect(response.body.error.message).toBe('Job not found.');
|
||||||
@@ -353,7 +353,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
};
|
};
|
||||||
vi.mocked(mockedMonitoringService.getFlyerJobStatus).mockResolvedValue(mockJobStatus);
|
vi.mocked(mockedMonitoringService.getFlyerJobStatus).mockResolvedValue(mockJobStatus);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/ai/jobs/job-123/status');
|
const response = await supertest(app).get('/api/v1/ai/jobs/job-123/status');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.state).toBe('completed');
|
expect(response.body.data.state).toBe('completed');
|
||||||
@@ -371,7 +371,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
// This route requires authentication, so we create an app instance with a user.
|
// This route requires authentication, so we create an app instance with a user.
|
||||||
const authenticatedApp = createTestApp({
|
const authenticatedApp = createTestApp({
|
||||||
router: aiRouter,
|
router: aiRouter,
|
||||||
basePath: '/api/ai',
|
basePath: '/api/v1/ai',
|
||||||
authenticatedUser: mockUser,
|
authenticatedUser: mockUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -382,7 +382,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(authenticatedApp)
|
const response = await supertest(authenticatedApp)
|
||||||
.post('/api/ai/upload-legacy')
|
.post('/api/v1/ai/upload-legacy')
|
||||||
.field('some_legacy_field', 'value') // simulate some body data
|
.field('some_legacy_field', 'value') // simulate some body data
|
||||||
.attach('flyerFile', imagePath);
|
.attach('flyerFile', imagePath);
|
||||||
|
|
||||||
@@ -399,7 +399,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
|
|
||||||
it('should return 400 if no flyer file is uploaded', async () => {
|
it('should return 400 if no flyer file is uploaded', async () => {
|
||||||
const response = await supertest(authenticatedApp)
|
const response = await supertest(authenticatedApp)
|
||||||
.post('/api/ai/upload-legacy')
|
.post('/api/v1/ai/upload-legacy')
|
||||||
.field('some_legacy_field', 'value');
|
.field('some_legacy_field', 'value');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -412,7 +412,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
const unlinkSpy = vi.spyOn(fs.promises, 'unlink').mockResolvedValue(undefined);
|
const unlinkSpy = vi.spyOn(fs.promises, 'unlink').mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(authenticatedApp)
|
const response = await supertest(authenticatedApp)
|
||||||
.post('/api/ai/upload-legacy')
|
.post('/api/v1/ai/upload-legacy')
|
||||||
.attach('flyerFile', imagePath);
|
.attach('flyerFile', imagePath);
|
||||||
|
|
||||||
expect(response.status).toBe(409);
|
expect(response.status).toBe(409);
|
||||||
@@ -429,7 +429,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
const unlinkSpy = vi.spyOn(fs.promises, 'unlink').mockResolvedValue(undefined);
|
const unlinkSpy = vi.spyOn(fs.promises, 'unlink').mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(authenticatedApp)
|
const response = await supertest(authenticatedApp)
|
||||||
.post('/api/ai/upload-legacy')
|
.post('/api/v1/ai/upload-legacy')
|
||||||
.attach('flyerFile', imagePath);
|
.attach('flyerFile', imagePath);
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -457,7 +457,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/flyers/process')
|
.post('/api/v1/ai/flyers/process')
|
||||||
.field('data', JSON.stringify(mockDataPayload))
|
.field('data', JSON.stringify(mockDataPayload))
|
||||||
.attach('flyerImage', imagePath);
|
.attach('flyerImage', imagePath);
|
||||||
|
|
||||||
@@ -469,7 +469,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
|
|
||||||
it('should return 400 if no flyer image is provided', async () => {
|
it('should return 400 if no flyer image is provided', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/flyers/process')
|
.post('/api/v1/ai/flyers/process')
|
||||||
.field('data', JSON.stringify(mockDataPayload));
|
.field('data', JSON.stringify(mockDataPayload));
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -485,7 +485,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/flyers/process')
|
.post('/api/v1/ai/flyers/process')
|
||||||
.field('data', JSON.stringify(mockDataPayload))
|
.field('data', JSON.stringify(mockDataPayload))
|
||||||
.attach('flyerImage', imagePath);
|
.attach('flyerImage', imagePath);
|
||||||
|
|
||||||
@@ -514,7 +514,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/flyers/process')
|
.post('/api/v1/ai/flyers/process')
|
||||||
.field('data', JSON.stringify(partialPayload))
|
.field('data', JSON.stringify(partialPayload))
|
||||||
.attach('flyerImage', imagePath);
|
.attach('flyerImage', imagePath);
|
||||||
|
|
||||||
@@ -534,7 +534,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/flyers/process')
|
.post('/api/v1/ai/flyers/process')
|
||||||
.field('data', JSON.stringify(payloadNoStore))
|
.field('data', JSON.stringify(payloadNoStore))
|
||||||
.attach('flyerImage', imagePath);
|
.attach('flyerImage', imagePath);
|
||||||
|
|
||||||
@@ -548,7 +548,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/flyers/process')
|
.post('/api/v1/ai/flyers/process')
|
||||||
.field('data', JSON.stringify(mockDataPayload))
|
.field('data', JSON.stringify(mockDataPayload))
|
||||||
.attach('flyerImage', imagePath);
|
.attach('flyerImage', imagePath);
|
||||||
|
|
||||||
@@ -571,7 +571,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
|
|
||||||
it('should handle payload where "data" field is an object, not stringified JSON', async () => {
|
it('should handle payload where "data" field is an object, not stringified JSON', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/flyers/process')
|
.post('/api/v1/ai/flyers/process')
|
||||||
.field('data', JSON.stringify(mockDataPayload)) // Supertest stringifies this, but Express JSON parser will make it an object
|
.field('data', JSON.stringify(mockDataPayload)) // Supertest stringifies this, but Express JSON parser will make it an object
|
||||||
.attach('flyerImage', imagePath);
|
.attach('flyerImage', imagePath);
|
||||||
|
|
||||||
@@ -587,7 +587,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/flyers/process')
|
.post('/api/v1/ai/flyers/process')
|
||||||
.field('data', JSON.stringify(payloadWithNullExtractedData))
|
.field('data', JSON.stringify(payloadWithNullExtractedData))
|
||||||
.attach('flyerImage', imagePath);
|
.attach('flyerImage', imagePath);
|
||||||
|
|
||||||
@@ -603,7 +603,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/flyers/process')
|
.post('/api/v1/ai/flyers/process')
|
||||||
.field('data', JSON.stringify(payloadWithStringExtractedData))
|
.field('data', JSON.stringify(payloadWithStringExtractedData))
|
||||||
.attach('flyerImage', imagePath);
|
.attach('flyerImage', imagePath);
|
||||||
|
|
||||||
@@ -614,7 +614,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
it('should handle payload where extractedData is at the root of the body', async () => {
|
it('should handle payload where extractedData is at the root of the body', async () => {
|
||||||
// This simulates a client sending multipart fields for each property of extractedData
|
// This simulates a client sending multipart fields for each property of extractedData
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/flyers/process')
|
.post('/api/v1/ai/flyers/process')
|
||||||
.field('checksum', 'root-checksum')
|
.field('checksum', 'root-checksum')
|
||||||
.field('originalFileName', 'flyer.jpg')
|
.field('originalFileName', 'flyer.jpg')
|
||||||
.field('store_name', 'Root Store')
|
.field('store_name', 'Root Store')
|
||||||
@@ -636,7 +636,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/flyers/process')
|
.post('/api/v1/ai/flyers/process')
|
||||||
.field('data', JSON.stringify(payloadMissingQuantity))
|
.field('data', JSON.stringify(payloadMissingQuantity))
|
||||||
.attach('flyerImage', imagePath);
|
.attach('flyerImage', imagePath);
|
||||||
|
|
||||||
@@ -658,7 +658,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/flyers/process')
|
.post('/api/v1/ai/flyers/process')
|
||||||
.field('data', malformedDataString)
|
.field('data', malformedDataString)
|
||||||
.attach('flyerImage', imagePath);
|
.attach('flyerImage', imagePath);
|
||||||
|
|
||||||
@@ -684,7 +684,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/flyers/process')
|
.post('/api/v1/ai/flyers/process')
|
||||||
.field('data', JSON.stringify(payloadWithoutChecksum))
|
.field('data', JSON.stringify(payloadWithoutChecksum))
|
||||||
.attach('flyerImage', imagePath);
|
.attach('flyerImage', imagePath);
|
||||||
|
|
||||||
@@ -700,12 +700,12 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
describe('POST /check-flyer', () => {
|
describe('POST /check-flyer', () => {
|
||||||
const imagePath = path.resolve(__dirname, '../tests/assets/test-flyer-image.jpg');
|
const imagePath = path.resolve(__dirname, '../tests/assets/test-flyer-image.jpg');
|
||||||
it('should return 400 if no image is provided', async () => {
|
it('should return 400 if no image is provided', async () => {
|
||||||
const response = await supertest(app).post('/api/ai/check-flyer');
|
const response = await supertest(app).post('/api/v1/ai/check-flyer');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 200 with a stubbed response on success', async () => {
|
it('should return 200 with a stubbed response on success', async () => {
|
||||||
const response = await supertest(app).post('/api/ai/check-flyer').attach('image', imagePath);
|
const response = await supertest(app).post('/api/v1/ai/check-flyer').attach('image', imagePath);
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.is_flyer).toBe(true);
|
expect(response.body.data.is_flyer).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -717,7 +717,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
throw new Error('Logging failed');
|
throw new Error('Logging failed');
|
||||||
});
|
});
|
||||||
// Attach a valid file to get past the `if (!req.file)` check.
|
// Attach a valid file to get past the `if (!req.file)` check.
|
||||||
const response = await supertest(app).post('/api/ai/check-flyer').attach('image', imagePath);
|
const response = await supertest(app).post('/api/v1/ai/check-flyer').attach('image', imagePath);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -726,7 +726,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
const imagePath = path.resolve(__dirname, '../tests/assets/test-flyer-image.jpg');
|
const imagePath = path.resolve(__dirname, '../tests/assets/test-flyer-image.jpg');
|
||||||
it('should return 400 if image file is missing', async () => {
|
it('should return 400 if image file is missing', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/rescan-area')
|
.post('/api/v1/ai/rescan-area')
|
||||||
.field('cropArea', JSON.stringify({ x: 0, y: 0, width: 10, height: 10 }))
|
.field('cropArea', JSON.stringify({ x: 0, y: 0, width: 10, height: 10 }))
|
||||||
.field('extractionType', 'store_name');
|
.field('extractionType', 'store_name');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -734,7 +734,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
|
|
||||||
it('should return 400 if cropArea or extractionType is missing', async () => {
|
it('should return 400 if cropArea or extractionType is missing', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/rescan-area')
|
.post('/api/v1/ai/rescan-area')
|
||||||
.attach('image', imagePath)
|
.attach('image', imagePath)
|
||||||
.field('extractionType', 'store_name'); // Missing cropArea
|
.field('extractionType', 'store_name'); // Missing cropArea
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -745,7 +745,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
|
|
||||||
it('should return 400 if cropArea is malformed JSON', async () => {
|
it('should return 400 if cropArea is malformed JSON', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/rescan-area')
|
.post('/api/v1/ai/rescan-area')
|
||||||
.attach('image', imagePath)
|
.attach('image', imagePath)
|
||||||
.field('cropArea', '{ "x": 0, "y": 0, "width": 10, "height": 10'); // Malformed
|
.field('cropArea', '{ "x": 0, "y": 0, "width": 10, "height": 10'); // Malformed
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -755,13 +755,13 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
describe('POST /extract-address', () => {
|
describe('POST /extract-address', () => {
|
||||||
const imagePath = path.resolve(__dirname, '../tests/assets/test-flyer-image.jpg');
|
const imagePath = path.resolve(__dirname, '../tests/assets/test-flyer-image.jpg');
|
||||||
it('should return 400 if no image is provided', async () => {
|
it('should return 400 if no image is provided', async () => {
|
||||||
const response = await supertest(app).post('/api/ai/extract-address');
|
const response = await supertest(app).post('/api/v1/ai/extract-address');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 200 with a stubbed response on success', async () => {
|
it('should return 200 with a stubbed response on success', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/extract-address')
|
.post('/api/v1/ai/extract-address')
|
||||||
.attach('image', imagePath);
|
.attach('image', imagePath);
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.address).toBe('not identified');
|
expect(response.body.data.address).toBe('not identified');
|
||||||
@@ -774,7 +774,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
throw new Error('Logging failed');
|
throw new Error('Logging failed');
|
||||||
});
|
});
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/extract-address')
|
.post('/api/v1/ai/extract-address')
|
||||||
.attach('image', imagePath);
|
.attach('image', imagePath);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
@@ -783,13 +783,13 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
describe('POST /extract-logo', () => {
|
describe('POST /extract-logo', () => {
|
||||||
const imagePath = path.resolve(__dirname, '../tests/assets/test-flyer-image.jpg');
|
const imagePath = path.resolve(__dirname, '../tests/assets/test-flyer-image.jpg');
|
||||||
it('should return 400 if no images are provided', async () => {
|
it('should return 400 if no images are provided', async () => {
|
||||||
const response = await supertest(app).post('/api/ai/extract-logo');
|
const response = await supertest(app).post('/api/v1/ai/extract-logo');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 200 with a stubbed response on success', async () => {
|
it('should return 200 with a stubbed response on success', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/extract-logo')
|
.post('/api/v1/ai/extract-logo')
|
||||||
.attach('images', imagePath);
|
.attach('images', imagePath);
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.store_logo_base_64).toBeNull();
|
expect(response.body.data.store_logo_base_64).toBeNull();
|
||||||
@@ -802,7 +802,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
throw new Error('Logging failed');
|
throw new Error('Logging failed');
|
||||||
});
|
});
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/extract-logo')
|
.post('/api/v1/ai/extract-logo')
|
||||||
.attach('images', imagePath);
|
.attach('images', imagePath);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
@@ -816,7 +816,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
});
|
});
|
||||||
const authenticatedApp = createTestApp({
|
const authenticatedApp = createTestApp({
|
||||||
router: aiRouter,
|
router: aiRouter,
|
||||||
basePath: '/api/ai',
|
basePath: '/api/v1/ai',
|
||||||
authenticatedUser: mockUser,
|
authenticatedUser: mockUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -833,7 +833,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
vi.mocked(aiService.aiService.extractTextFromImageArea).mockResolvedValueOnce(mockResult);
|
vi.mocked(aiService.aiService.extractTextFromImageArea).mockResolvedValueOnce(mockResult);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/rescan-area')
|
.post('/api/v1/ai/rescan-area')
|
||||||
.field('cropArea', JSON.stringify({ x: 10, y: 10, width: 50, height: 50 }))
|
.field('cropArea', JSON.stringify({ x: 10, y: 10, width: 50, height: 50 }))
|
||||||
.field('extractionType', 'item_details')
|
.field('extractionType', 'item_details')
|
||||||
.attach('image', imagePath);
|
.attach('image', imagePath);
|
||||||
@@ -849,7 +849,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(authenticatedApp)
|
const response = await supertest(authenticatedApp)
|
||||||
.post('/api/ai/rescan-area')
|
.post('/api/v1/ai/rescan-area')
|
||||||
.field('cropArea', JSON.stringify({ x: 10, y: 10, width: 50, height: 50 }))
|
.field('cropArea', JSON.stringify({ x: 10, y: 10, width: 50, height: 50 }))
|
||||||
.field('extractionType', 'item_details')
|
.field('extractionType', 'item_details')
|
||||||
.attach('image', imagePath);
|
.attach('image', imagePath);
|
||||||
@@ -865,7 +865,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
|
|
||||||
it('POST /quick-insights should return the stubbed response', async () => {
|
it('POST /quick-insights should return the stubbed response', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/quick-insights')
|
.post('/api/v1/ai/quick-insights')
|
||||||
.send({ items: [{ name: 'test' }] });
|
.send({ items: [{ name: 'test' }] });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -874,7 +874,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
|
|
||||||
it('POST /quick-insights should accept items with "item" property instead of "name"', async () => {
|
it('POST /quick-insights should accept items with "item" property instead of "name"', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/quick-insights')
|
.post('/api/v1/ai/quick-insights')
|
||||||
.send({ items: [{ item: 'test item' }] });
|
.send({ items: [{ item: 'test item' }] });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -886,35 +886,35 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
throw new Error('Logging failed');
|
throw new Error('Logging failed');
|
||||||
});
|
});
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/quick-insights')
|
.post('/api/v1/ai/quick-insights')
|
||||||
.send({ items: [{ name: 'test' }] });
|
.send({ items: [{ name: 'test' }] });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('POST /deep-dive should return the stubbed response', async () => {
|
it('POST /deep-dive should return the stubbed response', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/deep-dive')
|
.post('/api/v1/ai/deep-dive')
|
||||||
.send({ items: [{ name: 'test' }] });
|
.send({ items: [{ name: 'test' }] });
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.text).toContain('server-generated deep dive');
|
expect(response.body.data.text).toContain('server-generated deep dive');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('POST /generate-image should return 501 Not Implemented', async () => {
|
it('POST /generate-image should return 501 Not Implemented', async () => {
|
||||||
const response = await supertest(app).post('/api/ai/generate-image').send({ prompt: 'test' });
|
const response = await supertest(app).post('/api/v1/ai/generate-image').send({ prompt: 'test' });
|
||||||
|
|
||||||
expect(response.status).toBe(501);
|
expect(response.status).toBe(501);
|
||||||
expect(response.body.error.message).toBe('Image generation is not yet implemented.');
|
expect(response.body.error.message).toBe('Image generation is not yet implemented.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('POST /generate-speech should return 501 Not Implemented', async () => {
|
it('POST /generate-speech should return 501 Not Implemented', async () => {
|
||||||
const response = await supertest(app).post('/api/ai/generate-speech').send({ text: 'test' });
|
const response = await supertest(app).post('/api/v1/ai/generate-speech').send({ text: 'test' });
|
||||||
expect(response.status).toBe(501);
|
expect(response.status).toBe(501);
|
||||||
expect(response.body.error.message).toBe('Speech generation is not yet implemented.');
|
expect(response.body.error.message).toBe('Speech generation is not yet implemented.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('POST /search-web should return the stubbed response', async () => {
|
it('POST /search-web should return the stubbed response', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/search-web')
|
.post('/api/v1/ai/search-web')
|
||||||
.send({ query: 'test query' });
|
.send({ query: 'test query' });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -923,7 +923,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
|
|
||||||
it('POST /compare-prices should return the stubbed response', async () => {
|
it('POST /compare-prices should return the stubbed response', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/compare-prices')
|
.post('/api/v1/ai/compare-prices')
|
||||||
.send({ items: [{ name: 'Milk' }] });
|
.send({ items: [{ name: 'Milk' }] });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -935,7 +935,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
vi.mocked(aiService.aiService.planTripWithMaps).mockResolvedValueOnce(mockResult);
|
vi.mocked(aiService.aiService.planTripWithMaps).mockResolvedValueOnce(mockResult);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/plan-trip')
|
.post('/api/v1/ai/plan-trip')
|
||||||
.send({
|
.send({
|
||||||
items: [],
|
items: [],
|
||||||
store: { name: 'Test Store' },
|
store: { name: 'Test Store' },
|
||||||
@@ -952,7 +952,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/plan-trip')
|
.post('/api/v1/ai/plan-trip')
|
||||||
.send({
|
.send({
|
||||||
items: [],
|
items: [],
|
||||||
store: { name: 'Test Store' },
|
store: { name: 'Test Store' },
|
||||||
@@ -968,7 +968,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
throw new Error('Deep dive logging failed');
|
throw new Error('Deep dive logging failed');
|
||||||
});
|
});
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/deep-dive')
|
.post('/api/v1/ai/deep-dive')
|
||||||
.send({ items: [{ name: 'test' }] });
|
.send({ items: [{ name: 'test' }] });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('Deep dive logging failed');
|
expect(response.body.error.message).toBe('Deep dive logging failed');
|
||||||
@@ -979,7 +979,7 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
throw new Error('Search web logging failed');
|
throw new Error('Search web logging failed');
|
||||||
});
|
});
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/search-web')
|
.post('/api/v1/ai/search-web')
|
||||||
.send({ query: 'test query' });
|
.send({ query: 'test query' });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('Search web logging failed');
|
expect(response.body.error.message).toBe('Search web logging failed');
|
||||||
@@ -990,29 +990,29 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
throw new Error('Compare prices logging failed');
|
throw new Error('Compare prices logging failed');
|
||||||
});
|
});
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/ai/compare-prices')
|
.post('/api/v1/ai/compare-prices')
|
||||||
.send({ items: [{ name: 'Milk' }] });
|
.send({ items: [{ name: 'Milk' }] });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('Compare prices logging failed');
|
expect(response.body.error.message).toBe('Compare prices logging failed');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('POST /quick-insights should return 400 if items are missing', async () => {
|
it('POST /quick-insights should return 400 if items are missing', async () => {
|
||||||
const response = await supertest(app).post('/api/ai/quick-insights').send({});
|
const response = await supertest(app).post('/api/v1/ai/quick-insights').send({});
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('POST /search-web should return 400 if query is missing', async () => {
|
it('POST /search-web should return 400 if query is missing', async () => {
|
||||||
const response = await supertest(app).post('/api/ai/search-web').send({});
|
const response = await supertest(app).post('/api/v1/ai/search-web').send({});
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('POST /compare-prices should return 400 if items are missing', async () => {
|
it('POST /compare-prices should return 400 if items are missing', async () => {
|
||||||
const response = await supertest(app).post('/api/ai/compare-prices').send({});
|
const response = await supertest(app).post('/api/v1/ai/compare-prices').send({});
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('POST /plan-trip should return 400 if required fields are missing', async () => {
|
it('POST /plan-trip should return 400 if required fields are missing', async () => {
|
||||||
const response = await supertest(app).post('/api/ai/plan-trip').send({ items: [] });
|
const response = await supertest(app).post('/api/v1/ai/plan-trip').send({ items: [] });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ import { createTestApp } from '../tests/utils/createTestApp';
|
|||||||
// --- 4. App Setup using createTestApp ---
|
// --- 4. App Setup using createTestApp ---
|
||||||
const app = createTestApp({
|
const app = createTestApp({
|
||||||
router: authRouter,
|
router: authRouter,
|
||||||
basePath: '/api/auth',
|
basePath: '/api/v1/auth',
|
||||||
// Inject cookieParser via the new middleware option
|
// Inject cookieParser via the new middleware option
|
||||||
middleware: [cookieParser()],
|
middleware: [cookieParser()],
|
||||||
});
|
});
|
||||||
@@ -107,7 +107,7 @@ const app = createTestApp({
|
|||||||
const { mockLogger } = await import('../tests/utils/mockLogger');
|
const { mockLogger } = await import('../tests/utils/mockLogger');
|
||||||
|
|
||||||
// --- 5. Tests ---
|
// --- 5. Tests ---
|
||||||
describe('Auth Routes (/api/auth)', () => {
|
describe('Auth Routes (/api/v1/auth)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
vi.restoreAllMocks(); // Restore spies on prototypes
|
vi.restoreAllMocks(); // Restore spies on prototypes
|
||||||
@@ -130,7 +130,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).post('/api/auth/register').send({
|
const response = await supertest(app).post('/api/v1/auth/register').send({
|
||||||
email: newUserEmail,
|
email: newUserEmail,
|
||||||
password: strongPassword,
|
password: strongPassword,
|
||||||
full_name: 'Test User',
|
full_name: 'Test User',
|
||||||
@@ -162,7 +162,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).post('/api/auth/register').send({
|
const response = await supertest(app).post('/api/v1/auth/register').send({
|
||||||
email,
|
email,
|
||||||
password: strongPassword,
|
password: strongPassword,
|
||||||
full_name: 'Avatar User',
|
full_name: 'Avatar User',
|
||||||
@@ -191,7 +191,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).post('/api/auth/register').send({
|
const response = await supertest(app).post('/api/v1/auth/register').send({
|
||||||
email,
|
email,
|
||||||
password: strongPassword,
|
password: strongPassword,
|
||||||
full_name: '', // Send an empty string
|
full_name: '', // Send an empty string
|
||||||
@@ -218,7 +218,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
refreshToken: 'new-refresh-token',
|
refreshToken: 'new-refresh-token',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/auth/register').send({
|
const response = await supertest(app).post('/api/v1/auth/register').send({
|
||||||
email: 'cookie@test.com',
|
email: 'cookie@test.com',
|
||||||
password: 'StrongPassword123!',
|
password: 'StrongPassword123!',
|
||||||
});
|
});
|
||||||
@@ -231,7 +231,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
it('should reject registration with a weak password', async () => {
|
it('should reject registration with a weak password', async () => {
|
||||||
const weakPassword = 'password';
|
const weakPassword = 'password';
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/auth/register').send({
|
const response = await supertest(app).post('/api/v1/auth/register').send({
|
||||||
email: 'anotheruser@test.com',
|
email: 'anotheruser@test.com',
|
||||||
password: weakPassword,
|
password: weakPassword,
|
||||||
});
|
});
|
||||||
@@ -256,7 +256,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
mockedAuthService.registerAndLoginUser.mockRejectedValue(dbError);
|
mockedAuthService.registerAndLoginUser.mockRejectedValue(dbError);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email: newUserEmail, password: strongPassword });
|
.send({ email: newUserEmail, password: strongPassword });
|
||||||
|
|
||||||
expect(response.status).toBe(409); // 409 Conflict
|
expect(response.status).toBe(409); // 409 Conflict
|
||||||
@@ -268,7 +268,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
mockedAuthService.registerAndLoginUser.mockRejectedValue(dbError);
|
mockedAuthService.registerAndLoginUser.mockRejectedValue(dbError);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email: 'fail@test.com', password: strongPassword });
|
.send({ email: 'fail@test.com', password: strongPassword });
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -277,7 +277,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
it('should return 400 for an invalid email format', async () => {
|
it('should return 400 for an invalid email format', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email: 'not-an-email', password: strongPassword });
|
.send({ email: 'not-an-email', password: strongPassword });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -286,7 +286,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
it('should return 400 for a password that is too short', async () => {
|
it('should return 400 for a password that is too short', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email: newUserEmail, password: 'short' });
|
.send({ email: newUserEmail, password: 'short' });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -306,7 +306,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).post('/api/auth/login').send(loginCredentials);
|
const response = await supertest(app).post('/api/v1/auth/login').send(loginCredentials);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -325,7 +325,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
it('should reject login for incorrect credentials', async () => {
|
it('should reject login for incorrect credentials', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: 'test@test.com', password: 'wrong_password' });
|
.send({ email: 'test@test.com', password: 'wrong_password' });
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
@@ -334,7 +334,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
it('should reject login for a locked account', async () => {
|
it('should reject login for a locked account', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: 'locked@test.com', password: 'password123' });
|
.send({ email: 'locked@test.com', password: 'password123' });
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
@@ -345,7 +345,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
it('should return 401 if user is not found', async () => {
|
it('should return 401 if user is not found', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/login') // This was a duplicate, fixed.
|
.post('/api/v1/auth/login') // This was a duplicate, fixed.
|
||||||
.send({ email: 'notfound@test.com', password: 'password123' });
|
.send({ email: 'notfound@test.com', password: 'password123' });
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
@@ -357,7 +357,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
mockedAuthService.handleSuccessfulLogin.mockRejectedValue(new Error('DB write failed'));
|
mockedAuthService.handleSuccessfulLogin.mockRejectedValue(new Error('DB write failed'));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).post('/api/auth/login').send(loginCredentials);
|
const response = await supertest(app).post('/api/v1/auth/login').send(loginCredentials);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -369,7 +369,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
// when the email is 'dberror@test.com'.
|
// when the email is 'dberror@test.com'.
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: 'dberror@test.com', password: 'any_password' });
|
.send({ email: 'dberror@test.com', password: 'any_password' });
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -379,7 +379,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
it('should log a warning when passport authentication fails without a user', async () => {
|
it('should log a warning when passport authentication fails without a user', async () => {
|
||||||
// This test specifically covers the `if (!user)` debug log line in the route.
|
// This test specifically covers the `if (!user)` debug log line in the route.
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: 'notfound@test.com', password: 'any_password' });
|
.send({ email: 'notfound@test.com', password: 'any_password' });
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
@@ -402,7 +402,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).post('/api/auth/login').send(loginCredentials);
|
const response = await supertest(app).post('/api/v1/auth/login').send(loginCredentials);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -412,7 +412,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
it('should return 400 for an invalid email format', async () => {
|
it('should return 400 for an invalid email format', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: 'not-an-email', password: 'password123' });
|
.send({ email: 'not-an-email', password: 'password123' });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -421,7 +421,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
it('should return 400 if password is missing', async () => {
|
it('should return 400 if password is missing', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: 'test@test.com' });
|
.send({ email: 'test@test.com' });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -436,7 +436,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/forgot-password')
|
.post('/api/v1/auth/forgot-password')
|
||||||
.send({ email: 'test@test.com' });
|
.send({ email: 'test@test.com' });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -449,7 +449,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
mockedAuthService.resetPassword.mockResolvedValue(undefined);
|
mockedAuthService.resetPassword.mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/forgot-password')
|
.post('/api/v1/auth/forgot-password')
|
||||||
.send({ email: 'nouser@test.com' });
|
.send({ email: 'nouser@test.com' });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -459,7 +459,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
mockedAuthService.resetPassword.mockRejectedValue(new Error('DB connection failed'));
|
mockedAuthService.resetPassword.mockRejectedValue(new Error('DB connection failed'));
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/forgot-password')
|
.post('/api/v1/auth/forgot-password')
|
||||||
.send({ email: 'any@test.com' });
|
.send({ email: 'any@test.com' });
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -467,7 +467,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
it('should return 400 for an invalid email format', async () => {
|
it('should return 400 for an invalid email format', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/forgot-password')
|
.post('/api/v1/auth/forgot-password')
|
||||||
.send({ email: 'invalid-email' });
|
.send({ email: 'invalid-email' });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -480,7 +480,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
mockedAuthService.updatePassword.mockResolvedValue(true);
|
mockedAuthService.updatePassword.mockResolvedValue(true);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/reset-password')
|
.post('/api/v1/auth/reset-password')
|
||||||
.send({ token: 'valid-token', newPassword: 'a-Very-Strong-Password-789!' });
|
.send({ token: 'valid-token', newPassword: 'a-Very-Strong-Password-789!' });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -491,7 +491,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
mockedAuthService.updatePassword.mockResolvedValue(null);
|
mockedAuthService.updatePassword.mockResolvedValue(null);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/reset-password')
|
.post('/api/v1/auth/reset-password')
|
||||||
.send({ token: 'invalid-token', newPassword: 'a-Very-Strong-Password-123!' }); // Use strong password to pass validation
|
.send({ token: 'invalid-token', newPassword: 'a-Very-Strong-Password-123!' }); // Use strong password to pass validation
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -502,14 +502,14 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
// No need to mock the service here as validation runs first
|
// No need to mock the service here as validation runs first
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/reset-password')
|
.post('/api/v1/auth/reset-password')
|
||||||
.send({ token: 'valid-token', newPassword: 'weak' });
|
.send({ token: 'valid-token', newPassword: 'weak' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if token is missing', async () => {
|
it('should return 400 if token is missing', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/reset-password')
|
.post('/api/v1/auth/reset-password')
|
||||||
.send({ newPassword: 'a-Very-Strong-Password-789!' });
|
.send({ newPassword: 'a-Very-Strong-Password-789!' });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -521,7 +521,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
mockedAuthService.updatePassword.mockRejectedValue(dbError);
|
mockedAuthService.updatePassword.mockRejectedValue(dbError);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/reset-password')
|
.post('/api/v1/auth/reset-password')
|
||||||
.send({ token: 'valid-token', newPassword: 'a-Very-Strong-Password-789!' });
|
.send({ token: 'valid-token', newPassword: 'a-Very-Strong-Password-789!' });
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -537,7 +537,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
mockedAuthService.refreshAccessToken.mockResolvedValue({ accessToken: 'new-access-token' });
|
mockedAuthService.refreshAccessToken.mockResolvedValue({ accessToken: 'new-access-token' });
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/refresh-token')
|
.post('/api/v1/auth/refresh-token')
|
||||||
.set('Cookie', 'refreshToken=valid-refresh-token');
|
.set('Cookie', 'refreshToken=valid-refresh-token');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -545,7 +545,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 401 if no refresh token cookie is provided', async () => {
|
it('should return 401 if no refresh token cookie is provided', async () => {
|
||||||
const response = await supertest(app).post('/api/auth/refresh-token');
|
const response = await supertest(app).post('/api/v1/auth/refresh-token');
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
expect(response.body.error.message).toBe('Refresh token not found.');
|
expect(response.body.error.message).toBe('Refresh token not found.');
|
||||||
});
|
});
|
||||||
@@ -554,7 +554,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
mockedAuthService.refreshAccessToken.mockResolvedValue(null);
|
mockedAuthService.refreshAccessToken.mockResolvedValue(null);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/refresh-token')
|
.post('/api/v1/auth/refresh-token')
|
||||||
.set('Cookie', 'refreshToken=invalid-token');
|
.set('Cookie', 'refreshToken=invalid-token');
|
||||||
|
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
@@ -566,7 +566,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/refresh-token')
|
.post('/api/v1/auth/refresh-token')
|
||||||
.set('Cookie', 'refreshToken=any-token');
|
.set('Cookie', 'refreshToken=any-token');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toMatch(/DB Error/);
|
expect(response.body.error.message).toMatch(/DB Error/);
|
||||||
@@ -580,7 +580,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/logout')
|
.post('/api/v1/auth/logout')
|
||||||
.set('Cookie', 'refreshToken=some-valid-token');
|
.set('Cookie', 'refreshToken=some-valid-token');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -607,7 +607,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/logout')
|
.post('/api/v1/auth/logout')
|
||||||
.set('Cookie', 'refreshToken=some-token');
|
.set('Cookie', 'refreshToken=some-token');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -625,7 +625,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
it('should return 200 OK and clear the cookie even if no refresh token is provided', async () => {
|
it('should return 200 OK and clear the cookie even if no refresh token is provided', async () => {
|
||||||
// Act: Make a request without a cookie.
|
// Act: Make a request without a cookie.
|
||||||
const response = await supertest(app).post('/api/auth/logout');
|
const response = await supertest(app).post('/api/v1/auth/logout');
|
||||||
|
|
||||||
// Assert: The response should still be successful and attempt to clear the cookie.
|
// Assert: The response should still be successful and attempt to clear the cookie.
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -643,7 +643,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
// Act: Make `maxRequests` successful calls with the special header
|
// Act: Make `maxRequests` successful calls with the special header
|
||||||
for (let i = 0; i < maxRequests; i++) {
|
for (let i = 0; i < maxRequests; i++) {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/forgot-password')
|
.post('/api/v1/auth/forgot-password')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true') // Opt-in to the rate limiter for this test
|
.set('X-Test-Rate-Limit-Enable', 'true') // Opt-in to the rate limiter for this test
|
||||||
.send({ email });
|
.send({ email });
|
||||||
expect(response.status, `Request ${i + 1} should succeed`).toBe(200);
|
expect(response.status, `Request ${i + 1} should succeed`).toBe(200);
|
||||||
@@ -651,7 +651,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
// Act: Make one more call, which should be blocked
|
// Act: Make one more call, which should be blocked
|
||||||
const blockedResponse = await supertest(app)
|
const blockedResponse = await supertest(app)
|
||||||
.post('/api/auth/forgot-password')
|
.post('/api/v1/auth/forgot-password')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ email });
|
.send({ email });
|
||||||
|
|
||||||
@@ -669,7 +669,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
// Act: Make more calls than the limit. They should all succeed because the limiter is skipped.
|
// Act: Make more calls than the limit. They should all succeed because the limiter is skipped.
|
||||||
for (let i = 0; i < overLimitRequests; i++) {
|
for (let i = 0; i < overLimitRequests; i++) {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/forgot-password')
|
.post('/api/v1/auth/forgot-password')
|
||||||
// NO 'X-Test-Rate-Limit-Enable' header is sent
|
// NO 'X-Test-Rate-Limit-Enable' header is sent
|
||||||
.send({ email });
|
.send({ email });
|
||||||
expect(response.status, `Request ${i + 1} should succeed`).toBe(200);
|
expect(response.status, `Request ${i + 1} should succeed`).toBe(200);
|
||||||
@@ -692,7 +692,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
// Act: Make `maxRequests` calls. They should not be rate-limited.
|
// Act: Make `maxRequests` calls. They should not be rate-limited.
|
||||||
for (let i = 0; i < maxRequests; i++) {
|
for (let i = 0; i < maxRequests; i++) {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/reset-password')
|
.post('/api/v1/auth/reset-password')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true') // Opt-in to the rate limiter
|
.set('X-Test-Rate-Limit-Enable', 'true') // Opt-in to the rate limiter
|
||||||
.send({ token, newPassword });
|
.send({ token, newPassword });
|
||||||
// The expected status is 400 because the token is invalid, but not 429.
|
// The expected status is 400 because the token is invalid, but not 429.
|
||||||
@@ -701,7 +701,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
// Act: Make one more call, which should be blocked by the rate limiter.
|
// Act: Make one more call, which should be blocked by the rate limiter.
|
||||||
const blockedResponse = await supertest(app)
|
const blockedResponse = await supertest(app)
|
||||||
.post('/api/auth/reset-password')
|
.post('/api/v1/auth/reset-password')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ token, newPassword });
|
.send({ token, newPassword });
|
||||||
|
|
||||||
@@ -721,7 +721,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
// Act: Make more calls than the limit.
|
// Act: Make more calls than the limit.
|
||||||
for (let i = 0; i < maxRequests; i++) {
|
for (let i = 0; i < maxRequests; i++) {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/reset-password')
|
.post('/api/v1/auth/reset-password')
|
||||||
.send({ token, newPassword });
|
.send({ token, newPassword });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
}
|
}
|
||||||
@@ -748,7 +748,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
// Act: Make maxRequests calls
|
// Act: Make maxRequests calls
|
||||||
for (let i = 0; i < maxRequests; i++) {
|
for (let i = 0; i < maxRequests; i++) {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send(newUser);
|
.send(newUser);
|
||||||
expect(response.status).not.toBe(429);
|
expect(response.status).not.toBe(429);
|
||||||
@@ -756,7 +756,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
// Act: Make one more call
|
// Act: Make one more call
|
||||||
const blockedResponse = await supertest(app)
|
const blockedResponse = await supertest(app)
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send(newUser);
|
.send(newUser);
|
||||||
|
|
||||||
@@ -780,7 +780,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i < maxRequests; i++) {
|
for (let i = 0; i < maxRequests; i++) {
|
||||||
const response = await supertest(app).post('/api/auth/register').send(newUser);
|
const response = await supertest(app).post('/api/v1/auth/register').send(newUser);
|
||||||
expect(response.status).not.toBe(429);
|
expect(response.status).not.toBe(429);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -800,14 +800,14 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
// Act
|
// Act
|
||||||
for (let i = 0; i < maxRequests; i++) {
|
for (let i = 0; i < maxRequests; i++) {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send(credentials);
|
.send(credentials);
|
||||||
expect(response.status).not.toBe(429);
|
expect(response.status).not.toBe(429);
|
||||||
}
|
}
|
||||||
|
|
||||||
const blockedResponse = await supertest(app)
|
const blockedResponse = await supertest(app)
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send(credentials);
|
.send(credentials);
|
||||||
|
|
||||||
@@ -826,7 +826,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i < maxRequests; i++) {
|
for (let i = 0; i < maxRequests; i++) {
|
||||||
const response = await supertest(app).post('/api/auth/login').send(credentials);
|
const response = await supertest(app).post('/api/v1/auth/login').send(credentials);
|
||||||
expect(response.status).not.toBe(429);
|
expect(response.status).not.toBe(429);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -841,7 +841,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
// Act: Make maxRequests calls
|
// Act: Make maxRequests calls
|
||||||
for (let i = 0; i < maxRequests; i++) {
|
for (let i = 0; i < maxRequests; i++) {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/refresh-token')
|
.post('/api/v1/auth/refresh-token')
|
||||||
.set('Cookie', 'refreshToken=valid-token')
|
.set('Cookie', 'refreshToken=valid-token')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
expect(response.status).not.toBe(429);
|
expect(response.status).not.toBe(429);
|
||||||
@@ -849,7 +849,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
// Act: Make one more call
|
// Act: Make one more call
|
||||||
const blockedResponse = await supertest(app)
|
const blockedResponse = await supertest(app)
|
||||||
.post('/api/auth/refresh-token')
|
.post('/api/v1/auth/refresh-token')
|
||||||
.set('Cookie', 'refreshToken=valid-token')
|
.set('Cookie', 'refreshToken=valid-token')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
|
|
||||||
@@ -864,7 +864,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
for (let i = 0; i < maxRequests; i++) {
|
for (let i = 0; i < maxRequests; i++) {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/refresh-token')
|
.post('/api/v1/auth/refresh-token')
|
||||||
.set('Cookie', 'refreshToken=valid-token');
|
.set('Cookie', 'refreshToken=valid-token');
|
||||||
expect(response.status).not.toBe(429);
|
expect(response.status).not.toBe(429);
|
||||||
}
|
}
|
||||||
@@ -880,14 +880,14 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
// Act
|
// Act
|
||||||
for (let i = 0; i < maxRequests; i++) {
|
for (let i = 0; i < maxRequests; i++) {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/logout')
|
.post('/api/v1/auth/logout')
|
||||||
.set('Cookie', 'refreshToken=valid-token')
|
.set('Cookie', 'refreshToken=valid-token')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
expect(response.status).not.toBe(429);
|
expect(response.status).not.toBe(429);
|
||||||
}
|
}
|
||||||
|
|
||||||
const blockedResponse = await supertest(app)
|
const blockedResponse = await supertest(app)
|
||||||
.post('/api/auth/logout')
|
.post('/api/v1/auth/logout')
|
||||||
.set('Cookie', 'refreshToken=valid-token')
|
.set('Cookie', 'refreshToken=valid-token')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
|
|
||||||
@@ -902,7 +902,7 @@ describe('Auth Routes (/api/auth)', () => {
|
|||||||
|
|
||||||
for (let i = 0; i < maxRequests; i++) {
|
for (let i = 0; i < maxRequests; i++) {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/auth/logout')
|
.post('/api/v1/auth/logout')
|
||||||
.set('Cookie', 'refreshToken=valid-token');
|
.set('Cookie', 'refreshToken=valid-token');
|
||||||
expect(response.status).not.toBe(429);
|
expect(response.status).not.toBe(429);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const expectLogger = expect.objectContaining({
|
|||||||
error: expect.any(Function),
|
error: expect.any(Function),
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Budget Routes (/api/budgets)', () => {
|
describe('Budget Routes (/api/v1/budgets)', () => {
|
||||||
const mockUserProfile = createMockUserProfile({
|
const mockUserProfile = createMockUserProfile({
|
||||||
user: { user_id: 'user-123', email: 'test@test.com' },
|
user: { user_id: 'user-123', email: 'test@test.com' },
|
||||||
points: 100,
|
points: 100,
|
||||||
@@ -71,7 +71,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
|
|
||||||
const app = createTestApp({
|
const app = createTestApp({
|
||||||
router: budgetRouter,
|
router: budgetRouter,
|
||||||
basePath: '/api/budgets',
|
basePath: '/api/v1/budgets',
|
||||||
authenticatedUser: mockUserProfile,
|
authenticatedUser: mockUserProfile,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
// Mock the service function directly
|
// Mock the service function directly
|
||||||
vi.mocked(db.budgetRepo.getBudgetsForUser).mockResolvedValue(mockBudgets);
|
vi.mocked(db.budgetRepo.getBudgetsForUser).mockResolvedValue(mockBudgets);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/budgets');
|
const response = await supertest(app).get('/api/v1/budgets');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockBudgets);
|
expect(response.body.data).toEqual(mockBudgets);
|
||||||
@@ -93,7 +93,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
|
|
||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
vi.mocked(db.budgetRepo.getBudgetsForUser).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(db.budgetRepo.getBudgetsForUser).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app).get('/api/budgets');
|
const response = await supertest(app).get('/api/v1/budgets');
|
||||||
expect(response.status).toBe(500); // The custom handler will now be used
|
expect(response.status).toBe(500); // The custom handler will now be used
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
});
|
});
|
||||||
@@ -115,7 +115,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
// Mock the service function
|
// Mock the service function
|
||||||
vi.mocked(db.budgetRepo.createBudget).mockResolvedValue(mockCreatedBudget);
|
vi.mocked(db.budgetRepo.createBudget).mockResolvedValue(mockCreatedBudget);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/budgets').send(newBudgetData);
|
const response = await supertest(app).post('/api/v1/budgets').send(newBudgetData);
|
||||||
|
|
||||||
expect(response.status).toBe(201);
|
expect(response.status).toBe(201);
|
||||||
expect(response.body.data).toEqual(mockCreatedBudget);
|
expect(response.body.data).toEqual(mockCreatedBudget);
|
||||||
@@ -131,7 +131,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
vi.mocked(db.budgetRepo.createBudget).mockRejectedValue(
|
vi.mocked(db.budgetRepo.createBudget).mockRejectedValue(
|
||||||
new ForeignKeyConstraintError('User not found'),
|
new ForeignKeyConstraintError('User not found'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).post('/api/budgets').send(newBudgetData);
|
const response = await supertest(app).post('/api/v1/budgets').send(newBudgetData);
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.message).toBe('User not found');
|
expect(response.body.error.message).toBe('User not found');
|
||||||
});
|
});
|
||||||
@@ -144,7 +144,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
start_date: '2024-01-01',
|
start_date: '2024-01-01',
|
||||||
};
|
};
|
||||||
vi.mocked(db.budgetRepo.createBudget).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(db.budgetRepo.createBudget).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app).post('/api/budgets').send(newBudgetData);
|
const response = await supertest(app).post('/api/v1/budgets').send(newBudgetData);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
});
|
});
|
||||||
@@ -157,7 +157,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
start_date: 'not-a-date', // invalid date
|
start_date: 'not-a-date', // invalid date
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/budgets').send(invalidData);
|
const response = await supertest(app).post('/api/v1/budgets').send(invalidData);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details).toHaveLength(4);
|
expect(response.body.error.details).toHaveLength(4);
|
||||||
@@ -166,7 +166,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
it('should return 400 if required fields are missing', async () => {
|
it('should return 400 if required fields are missing', async () => {
|
||||||
// This test covers the `val ?? ''` part of the `requiredString` helper
|
// This test covers the `val ?? ''` part of the `requiredString` helper
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.send({ amount_cents: 10000, period: 'monthly', start_date: '2024-01-01' });
|
.send({ amount_cents: 10000, period: 'monthly', start_date: '2024-01-01' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toBe('Budget name is required.');
|
expect(response.body.error.details[0].message).toBe('Budget name is required.');
|
||||||
@@ -184,7 +184,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
// Mock the service function
|
// Mock the service function
|
||||||
vi.mocked(db.budgetRepo.updateBudget).mockResolvedValue(mockUpdatedBudget);
|
vi.mocked(db.budgetRepo.updateBudget).mockResolvedValue(mockUpdatedBudget);
|
||||||
|
|
||||||
const response = await supertest(app).put('/api/budgets/1').send(budgetUpdates);
|
const response = await supertest(app).put('/api/v1/budgets/1').send(budgetUpdates);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockUpdatedBudget);
|
expect(response.body.data).toEqual(mockUpdatedBudget);
|
||||||
@@ -194,7 +194,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
vi.mocked(db.budgetRepo.updateBudget).mockRejectedValue(
|
vi.mocked(db.budgetRepo.updateBudget).mockRejectedValue(
|
||||||
new NotFoundError('Budget not found'),
|
new NotFoundError('Budget not found'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).put('/api/budgets/999').send({ amount_cents: 1 });
|
const response = await supertest(app).put('/api/v1/budgets/999').send({ amount_cents: 1 });
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toBe('Budget not found');
|
expect(response.body.error.message).toBe('Budget not found');
|
||||||
});
|
});
|
||||||
@@ -202,13 +202,13 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
it('should return 500 if a generic database error occurs', async () => {
|
it('should return 500 if a generic database error occurs', async () => {
|
||||||
const budgetUpdates = { amount_cents: 60000 };
|
const budgetUpdates = { amount_cents: 60000 };
|
||||||
vi.mocked(db.budgetRepo.updateBudget).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(db.budgetRepo.updateBudget).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app).put('/api/budgets/1').send(budgetUpdates);
|
const response = await supertest(app).put('/api/v1/budgets/1').send(budgetUpdates);
|
||||||
expect(response.status).toBe(500); // The custom handler will now be used
|
expect(response.status).toBe(500); // The custom handler will now be used
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if no update fields are provided', async () => {
|
it('should return 400 if no update fields are provided', async () => {
|
||||||
const response = await supertest(app).put('/api/budgets/1').send({});
|
const response = await supertest(app).put('/api/v1/budgets/1').send({});
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toBe(
|
expect(response.body.error.details[0].message).toBe(
|
||||||
'At least one field to update must be provided.',
|
'At least one field to update must be provided.',
|
||||||
@@ -216,7 +216,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for an invalid budget ID', async () => {
|
it('should return 400 for an invalid budget ID', async () => {
|
||||||
const response = await supertest(app).put('/api/budgets/abc').send({ amount_cents: 5000 });
|
const response = await supertest(app).put('/api/v1/budgets/abc').send({ amount_cents: 5000 });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toMatch(/Invalid ID|number/i);
|
expect(response.body.error.details[0].message).toMatch(/Invalid ID|number/i);
|
||||||
});
|
});
|
||||||
@@ -227,7 +227,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
// Mock the service function to resolve (void)
|
// Mock the service function to resolve (void)
|
||||||
vi.mocked(db.budgetRepo.deleteBudget).mockResolvedValue(undefined);
|
vi.mocked(db.budgetRepo.deleteBudget).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app).delete('/api/budgets/1');
|
const response = await supertest(app).delete('/api/v1/budgets/1');
|
||||||
|
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
expect(db.budgetRepo.deleteBudget).toHaveBeenCalledWith(
|
expect(db.budgetRepo.deleteBudget).toHaveBeenCalledWith(
|
||||||
@@ -241,20 +241,20 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
vi.mocked(db.budgetRepo.deleteBudget).mockRejectedValue(
|
vi.mocked(db.budgetRepo.deleteBudget).mockRejectedValue(
|
||||||
new NotFoundError('Budget not found'),
|
new NotFoundError('Budget not found'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).delete('/api/budgets/999');
|
const response = await supertest(app).delete('/api/v1/budgets/999');
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toBe('Budget not found');
|
expect(response.body.error.message).toBe('Budget not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 500 if a generic database error occurs', async () => {
|
it('should return 500 if a generic database error occurs', async () => {
|
||||||
vi.mocked(db.budgetRepo.deleteBudget).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(db.budgetRepo.deleteBudget).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app).delete('/api/budgets/1');
|
const response = await supertest(app).delete('/api/v1/budgets/1');
|
||||||
expect(response.status).toBe(500); // The custom handler will now be used
|
expect(response.status).toBe(500); // The custom handler will now be used
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for an invalid budget ID', async () => {
|
it('should return 400 for an invalid budget ID', async () => {
|
||||||
const response = await supertest(app).delete('/api/budgets/abc');
|
const response = await supertest(app).delete('/api/v1/budgets/abc');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toMatch(/Invalid ID|number/i);
|
expect(response.body.error.details[0].message).toMatch(/Invalid ID|number/i);
|
||||||
});
|
});
|
||||||
@@ -269,7 +269,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
vi.mocked(db.budgetRepo.getSpendingByCategory).mockResolvedValue(mockSpendingData);
|
vi.mocked(db.budgetRepo.getSpendingByCategory).mockResolvedValue(mockSpendingData);
|
||||||
|
|
||||||
const response = await supertest(app).get(
|
const response = await supertest(app).get(
|
||||||
'/api/budgets/spending-analysis?startDate=2024-01-01&endDate=2024-01-31',
|
'/api/v1/budgets/spending-analysis?startDate=2024-01-01&endDate=2024-01-31',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -281,7 +281,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
vi.mocked(db.budgetRepo.getSpendingByCategory).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(db.budgetRepo.getSpendingByCategory).mockRejectedValue(new Error('DB Error'));
|
||||||
|
|
||||||
const response = await supertest(app).get(
|
const response = await supertest(app).get(
|
||||||
'/api/budgets/spending-analysis?startDate=2024-01-01&endDate=2024-01-31',
|
'/api/v1/budgets/spending-analysis?startDate=2024-01-01&endDate=2024-01-31',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -290,14 +290,14 @@ describe('Budget Routes (/api/budgets)', () => {
|
|||||||
|
|
||||||
it('should return 400 for invalid date formats', async () => {
|
it('should return 400 for invalid date formats', async () => {
|
||||||
const response = await supertest(app).get(
|
const response = await supertest(app).get(
|
||||||
'/api/budgets/spending-analysis?startDate=2024/01/01&endDate=invalid',
|
'/api/v1/budgets/spending-analysis?startDate=2024/01/01&endDate=invalid',
|
||||||
);
|
);
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details).toHaveLength(2);
|
expect(response.body.error.details).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if required query parameters are missing', async () => {
|
it('should return 400 if required query parameters are missing', async () => {
|
||||||
const response = await supertest(app).get('/api/budgets/spending-analysis');
|
const response = await supertest(app).get('/api/v1/budgets/spending-analysis');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
// Expect errors for both startDate and endDate
|
// Expect errors for both startDate and endDate
|
||||||
expect(response.body.error.details).toHaveLength(2);
|
expect(response.body.error.details).toHaveLength(2);
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ const expectLogger = expect.objectContaining({
|
|||||||
error: expect.any(Function),
|
error: expect.any(Function),
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Deals Routes (/api/users/deals)', () => {
|
describe('Deals Routes (/api/v1/users/deals)', () => {
|
||||||
const mockUser = createMockUserProfile({ user: { user_id: 'user-123', email: 'test@test.com' } });
|
const mockUser = createMockUserProfile({ user: { user_id: 'user-123', email: 'test@test.com' } });
|
||||||
const basePath = '/api/users/deals';
|
const basePath = '/api/v1/users/deals';
|
||||||
const authenticatedApp = createTestApp({
|
const authenticatedApp = createTestApp({
|
||||||
router: dealsRouter,
|
router: dealsRouter,
|
||||||
basePath,
|
basePath,
|
||||||
@@ -69,7 +69,7 @@ describe('Deals Routes (/api/users/deals)', () => {
|
|||||||
describe('GET /best-watched-prices', () => {
|
describe('GET /best-watched-prices', () => {
|
||||||
it('should return 401 Unauthorized if user is not authenticated', async () => {
|
it('should return 401 Unauthorized if user is not authenticated', async () => {
|
||||||
const response = await supertest(unauthenticatedApp).get(
|
const response = await supertest(unauthenticatedApp).get(
|
||||||
'/api/users/deals/best-watched-prices',
|
'/api/v1/users/deals/best-watched-prices',
|
||||||
);
|
);
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
});
|
});
|
||||||
@@ -79,7 +79,7 @@ describe('Deals Routes (/api/users/deals)', () => {
|
|||||||
vi.mocked(dealsRepo.findBestPricesForWatchedItems).mockResolvedValue(mockDeals);
|
vi.mocked(dealsRepo.findBestPricesForWatchedItems).mockResolvedValue(mockDeals);
|
||||||
|
|
||||||
const response = await supertest(authenticatedApp).get(
|
const response = await supertest(authenticatedApp).get(
|
||||||
'/api/users/deals/best-watched-prices',
|
'/api/v1/users/deals/best-watched-prices',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -99,7 +99,7 @@ describe('Deals Routes (/api/users/deals)', () => {
|
|||||||
vi.mocked(dealsRepo.findBestPricesForWatchedItems).mockRejectedValue(dbError);
|
vi.mocked(dealsRepo.findBestPricesForWatchedItems).mockRejectedValue(dbError);
|
||||||
|
|
||||||
const response = await supertest(authenticatedApp).get(
|
const response = await supertest(authenticatedApp).get(
|
||||||
'/api/users/deals/best-watched-prices',
|
'/api/v1/users/deals/best-watched-prices',
|
||||||
);
|
);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
@@ -115,7 +115,7 @@ describe('Deals Routes (/api/users/deals)', () => {
|
|||||||
vi.mocked(dealsRepo.findBestPricesForWatchedItems).mockResolvedValue([]);
|
vi.mocked(dealsRepo.findBestPricesForWatchedItems).mockResolvedValue([]);
|
||||||
|
|
||||||
const response = await supertest(authenticatedApp)
|
const response = await supertest(authenticatedApp)
|
||||||
.get('/api/users/deals/best-watched-prices')
|
.get('/api/v1/users/deals/best-watched-prices')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
|
|||||||
@@ -34,19 +34,19 @@ const expectLogger = expect.objectContaining({
|
|||||||
error: expect.any(Function),
|
error: expect.any(Function),
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Flyer Routes (/api/flyers)', () => {
|
describe('Flyer Routes (/api/v1/flyers)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = createTestApp({ router: flyerRouter, basePath: '/api/flyers' });
|
const app = createTestApp({ router: flyerRouter, basePath: '/api/v1/flyers' });
|
||||||
|
|
||||||
describe('GET /', () => {
|
describe('GET /', () => {
|
||||||
it('should return a list of flyers on success', async () => {
|
it('should return a list of flyers on success', async () => {
|
||||||
const mockFlyers = [createMockFlyer({ flyer_id: 1 }), createMockFlyer({ flyer_id: 2 })];
|
const mockFlyers = [createMockFlyer({ flyer_id: 1 }), createMockFlyer({ flyer_id: 2 })];
|
||||||
vi.mocked(db.flyerRepo.getFlyers).mockResolvedValue(mockFlyers);
|
vi.mocked(db.flyerRepo.getFlyers).mockResolvedValue(mockFlyers);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/flyers');
|
const response = await supertest(app).get('/api/v1/flyers');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockFlyers);
|
expect(response.body.data).toEqual(mockFlyers);
|
||||||
@@ -56,36 +56,36 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
|
|
||||||
it('should pass limit and offset query parameters to the db function', async () => {
|
it('should pass limit and offset query parameters to the db function', async () => {
|
||||||
vi.mocked(db.flyerRepo.getFlyers).mockResolvedValue([]);
|
vi.mocked(db.flyerRepo.getFlyers).mockResolvedValue([]);
|
||||||
await supertest(app).get('/api/flyers?limit=15&offset=30');
|
await supertest(app).get('/api/v1/flyers?limit=15&offset=30');
|
||||||
expect(db.flyerRepo.getFlyers).toHaveBeenCalledWith(expectLogger, 15, 30);
|
expect(db.flyerRepo.getFlyers).toHaveBeenCalledWith(expectLogger, 15, 30);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use default for offset when only limit is provided', async () => {
|
it('should use default for offset when only limit is provided', async () => {
|
||||||
vi.mocked(db.flyerRepo.getFlyers).mockResolvedValue([]);
|
vi.mocked(db.flyerRepo.getFlyers).mockResolvedValue([]);
|
||||||
await supertest(app).get('/api/flyers?limit=5');
|
await supertest(app).get('/api/v1/flyers?limit=5');
|
||||||
expect(db.flyerRepo.getFlyers).toHaveBeenCalledWith(expectLogger, 5, 0);
|
expect(db.flyerRepo.getFlyers).toHaveBeenCalledWith(expectLogger, 5, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use default for limit when only offset is provided', async () => {
|
it('should use default for limit when only offset is provided', async () => {
|
||||||
vi.mocked(db.flyerRepo.getFlyers).mockResolvedValue([]);
|
vi.mocked(db.flyerRepo.getFlyers).mockResolvedValue([]);
|
||||||
await supertest(app).get('/api/flyers?offset=10');
|
await supertest(app).get('/api/v1/flyers?offset=10');
|
||||||
expect(db.flyerRepo.getFlyers).toHaveBeenCalledWith(expectLogger, 20, 10);
|
expect(db.flyerRepo.getFlyers).toHaveBeenCalledWith(expectLogger, 20, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(db.flyerRepo.getFlyers).mockRejectedValue(dbError);
|
vi.mocked(db.flyerRepo.getFlyers).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/flyers');
|
const response = await supertest(app).get('/api/v1/flyers');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
'Error fetching flyers in /api/flyers:',
|
'Error fetching flyers in /api/v1/flyers:',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid query parameters', async () => {
|
it('should return 400 for invalid query parameters', async () => {
|
||||||
const response = await supertest(app).get('/api/flyers?limit=abc&offset=-5');
|
const response = await supertest(app).get('/api/v1/flyers?limit=abc&offset=-5');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details).toBeDefined();
|
expect(response.body.error.details).toBeDefined();
|
||||||
expect(response.body.error.details.length).toBe(2);
|
expect(response.body.error.details.length).toBe(2);
|
||||||
@@ -97,7 +97,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
const mockFlyer = createMockFlyer({ flyer_id: 123 });
|
const mockFlyer = createMockFlyer({ flyer_id: 123 });
|
||||||
vi.mocked(db.flyerRepo.getFlyerById).mockResolvedValue(mockFlyer);
|
vi.mocked(db.flyerRepo.getFlyerById).mockResolvedValue(mockFlyer);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/flyers/123');
|
const response = await supertest(app).get('/api/v1/flyers/123');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockFlyer);
|
expect(response.body.data).toEqual(mockFlyer);
|
||||||
@@ -111,14 +111,14 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
vi.mocked(db.flyerRepo.getFlyerById).mockRejectedValue(
|
vi.mocked(db.flyerRepo.getFlyerById).mockRejectedValue(
|
||||||
new NotFoundError(`Flyer with ID 999 not found.`),
|
new NotFoundError(`Flyer with ID 999 not found.`),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).get('/api/flyers/999');
|
const response = await supertest(app).get('/api/v1/flyers/999');
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toContain('not found');
|
expect(response.body.error.message).toContain('not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for an invalid flyer ID', async () => {
|
it('should return 400 for an invalid flyer ID', async () => {
|
||||||
const response = await supertest(app).get('/api/flyers/abc');
|
const response = await supertest(app).get('/api/v1/flyers/abc');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
// Zod coercion results in NaN for "abc", which triggers a type error before our custom message
|
// Zod coercion results in NaN for "abc", which triggers a type error before our custom message
|
||||||
expect(response.body.error.details[0].message).toMatch(
|
expect(response.body.error.details[0].message).toMatch(
|
||||||
@@ -129,7 +129,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(db.flyerRepo.getFlyerById).mockRejectedValue(dbError);
|
vi.mocked(db.flyerRepo.getFlyerById).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/flyers/123');
|
const response = await supertest(app).get('/api/v1/flyers/123');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
@@ -144,14 +144,14 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
const mockFlyerItems = [createMockFlyerItem({ flyer_item_id: 1, flyer_id: 123 })];
|
const mockFlyerItems = [createMockFlyerItem({ flyer_item_id: 1, flyer_id: 123 })];
|
||||||
vi.mocked(db.flyerRepo.getFlyerItems).mockResolvedValue(mockFlyerItems);
|
vi.mocked(db.flyerRepo.getFlyerItems).mockResolvedValue(mockFlyerItems);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/flyers/123/items');
|
const response = await supertest(app).get('/api/v1/flyers/123/items');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockFlyerItems);
|
expect(response.body.data).toEqual(mockFlyerItems);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for an invalid flyer ID', async () => {
|
it('should return 400 for an invalid flyer ID', async () => {
|
||||||
const response = await supertest(app).get('/api/flyers/abc/items');
|
const response = await supertest(app).get('/api/v1/flyers/abc/items');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toMatch(
|
expect(response.body.error.details[0].message).toMatch(
|
||||||
/Invalid flyer ID provided|expected number, received NaN/,
|
/Invalid flyer ID provided|expected number, received NaN/,
|
||||||
@@ -161,12 +161,12 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(db.flyerRepo.getFlyerItems).mockRejectedValue(dbError);
|
vi.mocked(db.flyerRepo.getFlyerItems).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/flyers/123/items');
|
const response = await supertest(app).get('/api/v1/flyers/123/items');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError, flyerId: 123 },
|
{ error: dbError, flyerId: 123 },
|
||||||
'Error fetching flyer items in /api/flyers/:id/items:',
|
'Error fetching flyer items in /api/v1/flyers/:id/items:',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -177,7 +177,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
vi.mocked(db.flyerRepo.getFlyerItemsForFlyers).mockResolvedValue(mockFlyerItems);
|
vi.mocked(db.flyerRepo.getFlyerItemsForFlyers).mockResolvedValue(mockFlyerItems);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/batch-fetch')
|
.post('/api/v1/flyers/items/batch-fetch')
|
||||||
.send({ flyerIds: [1, 2] });
|
.send({ flyerIds: [1, 2] });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -186,7 +186,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
|
|
||||||
it('should return 400 if flyerIds is not an array', async () => {
|
it('should return 400 if flyerIds is not an array', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/batch-fetch')
|
.post('/api/v1/flyers/items/batch-fetch')
|
||||||
.send({ flyerIds: 'not-an-array' });
|
.send({ flyerIds: 'not-an-array' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toMatch(/expected array/);
|
expect(response.body.error.details[0].message).toMatch(/expected array/);
|
||||||
@@ -194,7 +194,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
|
|
||||||
it('should return 400 if flyerIds is an empty array, as per schema validation', async () => {
|
it('should return 400 if flyerIds is an empty array, as per schema validation', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/batch-fetch')
|
.post('/api/v1/flyers/items/batch-fetch')
|
||||||
.send({ flyerIds: [] });
|
.send({ flyerIds: [] });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
// Check for the specific Zod error message.
|
// Check for the specific Zod error message.
|
||||||
@@ -204,7 +204,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
vi.mocked(db.flyerRepo.getFlyerItemsForFlyers).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(db.flyerRepo.getFlyerItemsForFlyers).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/batch-fetch')
|
.post('/api/v1/flyers/items/batch-fetch')
|
||||||
.send({ flyerIds: [1] });
|
.send({ flyerIds: [1] });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
@@ -216,7 +216,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
vi.mocked(db.flyerRepo.countFlyerItemsForFlyers).mockResolvedValueOnce(42);
|
vi.mocked(db.flyerRepo.countFlyerItemsForFlyers).mockResolvedValueOnce(42);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/batch-count')
|
.post('/api/v1/flyers/items/batch-count')
|
||||||
.send({ flyerIds: [1, 2, 3] });
|
.send({ flyerIds: [1, 2, 3] });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -225,7 +225,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
|
|
||||||
it('should return 400 if flyerIds is not an array', async () => {
|
it('should return 400 if flyerIds is not an array', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/batch-count')
|
.post('/api/v1/flyers/items/batch-count')
|
||||||
.send({ flyerIds: 'not-an-array' });
|
.send({ flyerIds: 'not-an-array' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -234,7 +234,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
vi.mocked(db.flyerRepo.countFlyerItemsForFlyers).mockResolvedValueOnce(0);
|
vi.mocked(db.flyerRepo.countFlyerItemsForFlyers).mockResolvedValueOnce(0);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/batch-count')
|
.post('/api/v1/flyers/items/batch-count')
|
||||||
.send({ flyerIds: [] });
|
.send({ flyerIds: [] });
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual({ count: 0 });
|
expect(response.body.data).toEqual({ count: 0 });
|
||||||
@@ -243,7 +243,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
vi.mocked(db.flyerRepo.countFlyerItemsForFlyers).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(db.flyerRepo.countFlyerItemsForFlyers).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/batch-count')
|
.post('/api/v1/flyers/items/batch-count')
|
||||||
.send({ flyerIds: [1] });
|
.send({ flyerIds: [1] });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
@@ -253,7 +253,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
describe('POST /items/:itemId/track', () => {
|
describe('POST /items/:itemId/track', () => {
|
||||||
it('should return 202 Accepted and call the tracking function for "click"', async () => {
|
it('should return 202 Accepted and call the tracking function for "click"', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/99/track')
|
.post('/api/v1/flyers/items/99/track')
|
||||||
.send({ type: 'click' });
|
.send({ type: 'click' });
|
||||||
|
|
||||||
expect(response.status).toBe(202);
|
expect(response.status).toBe(202);
|
||||||
@@ -266,7 +266,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
|
|
||||||
it('should return 202 Accepted and call the tracking function for "view"', async () => {
|
it('should return 202 Accepted and call the tracking function for "view"', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/101/track')
|
.post('/api/v1/flyers/items/101/track')
|
||||||
.send({ type: 'view' });
|
.send({ type: 'view' });
|
||||||
|
|
||||||
expect(response.status).toBe(202);
|
expect(response.status).toBe(202);
|
||||||
@@ -279,14 +279,14 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
|
|
||||||
it('should return 400 for an invalid item ID', async () => {
|
it('should return 400 for an invalid item ID', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/abc/track')
|
.post('/api/v1/flyers/items/abc/track')
|
||||||
.send({ type: 'click' });
|
.send({ type: 'click' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for an invalid interaction type', async () => {
|
it('should return 400 for an invalid interaction type', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/99/track')
|
.post('/api/v1/flyers/items/99/track')
|
||||||
.send({ type: 'invalid' });
|
.send({ type: 'invalid' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -296,7 +296,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
vi.mocked(db.flyerRepo.trackFlyerItemInteraction).mockRejectedValue(trackingError);
|
vi.mocked(db.flyerRepo.trackFlyerItemInteraction).mockRejectedValue(trackingError);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/99/track')
|
.post('/api/v1/flyers/items/99/track')
|
||||||
.send({ type: 'click' });
|
.send({ type: 'click' });
|
||||||
|
|
||||||
expect(response.status).toBe(202);
|
expect(response.status).toBe(202);
|
||||||
@@ -317,7 +317,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/99/track')
|
.post('/api/v1/flyers/items/99/track')
|
||||||
.send({ type: 'click' });
|
.send({ type: 'click' });
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -328,7 +328,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
it('should apply publicReadLimiter to GET /', async () => {
|
it('should apply publicReadLimiter to GET /', async () => {
|
||||||
vi.mocked(db.flyerRepo.getFlyers).mockResolvedValue([]);
|
vi.mocked(db.flyerRepo.getFlyers).mockResolvedValue([]);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.get('/api/flyers')
|
.get('/api/v1/flyers')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -339,7 +339,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
it('should apply batchLimiter to POST /items/batch-fetch', async () => {
|
it('should apply batchLimiter to POST /items/batch-fetch', async () => {
|
||||||
vi.mocked(db.flyerRepo.getFlyerItemsForFlyers).mockResolvedValue([]);
|
vi.mocked(db.flyerRepo.getFlyerItemsForFlyers).mockResolvedValue([]);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/batch-fetch')
|
.post('/api/v1/flyers/items/batch-fetch')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ flyerIds: [1] });
|
.send({ flyerIds: [1] });
|
||||||
|
|
||||||
@@ -351,7 +351,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
it('should apply batchLimiter to POST /items/batch-count', async () => {
|
it('should apply batchLimiter to POST /items/batch-count', async () => {
|
||||||
vi.mocked(db.flyerRepo.countFlyerItemsForFlyers).mockResolvedValue(0);
|
vi.mocked(db.flyerRepo.countFlyerItemsForFlyers).mockResolvedValue(0);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/batch-count')
|
.post('/api/v1/flyers/items/batch-count')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ flyerIds: [1] });
|
.send({ flyerIds: [1] });
|
||||||
|
|
||||||
@@ -365,7 +365,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
|||||||
vi.mocked(db.flyerRepo.trackFlyerItemInteraction).mockResolvedValue(undefined);
|
vi.mocked(db.flyerRepo.trackFlyerItemInteraction).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/flyers/items/1/track')
|
.post('/api/v1/flyers/items/1/track')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ type: 'view' });
|
.send({ type: 'view' });
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const expectLogger = expect.objectContaining({
|
|||||||
error: expect.any(Function),
|
error: expect.any(Function),
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Gamification Routes (/api/achievements)', () => {
|
describe('Gamification Routes (/api/v1/achievements)', () => {
|
||||||
const mockUserProfile = createMockUserProfile({
|
const mockUserProfile = createMockUserProfile({
|
||||||
user: { user_id: 'user-123', email: 'user@test.com' },
|
user: { user_id: 'user-123', email: 'user@test.com' },
|
||||||
points: 100,
|
points: 100,
|
||||||
@@ -75,7 +75,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const basePath = '/api/achievements';
|
const basePath = '/api/v1/achievements';
|
||||||
const unauthenticatedApp = createTestApp({ router: gamificationRouter, basePath });
|
const unauthenticatedApp = createTestApp({ router: gamificationRouter, basePath });
|
||||||
const authenticatedApp = createTestApp({
|
const authenticatedApp = createTestApp({
|
||||||
router: gamificationRouter,
|
router: gamificationRouter,
|
||||||
@@ -96,7 +96,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
];
|
];
|
||||||
vi.mocked(db.gamificationRepo.getAllAchievements).mockResolvedValue(mockAchievements);
|
vi.mocked(db.gamificationRepo.getAllAchievements).mockResolvedValue(mockAchievements);
|
||||||
|
|
||||||
const response = await supertest(unauthenticatedApp).get('/api/achievements');
|
const response = await supertest(unauthenticatedApp).get('/api/v1/achievements');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockAchievements);
|
expect(response.body.data).toEqual(mockAchievements);
|
||||||
expect(db.gamificationRepo.getAllAchievements).toHaveBeenCalledWith(expectLogger);
|
expect(db.gamificationRepo.getAllAchievements).toHaveBeenCalledWith(expectLogger);
|
||||||
@@ -106,7 +106,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.gamificationRepo.getAllAchievements).mockRejectedValue(dbError);
|
vi.mocked(db.gamificationRepo.getAllAchievements).mockRejectedValue(dbError);
|
||||||
|
|
||||||
const response = await supertest(unauthenticatedApp).get('/api/achievements');
|
const response = await supertest(unauthenticatedApp).get('/api/v1/achievements');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Connection Failed');
|
expect(response.body.error.message).toBe('DB Connection Failed');
|
||||||
});
|
});
|
||||||
@@ -122,7 +122,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(adminApp)
|
const response = await supertest(adminApp)
|
||||||
.post('/api/achievements/award')
|
.post('/api/v1/achievements/award')
|
||||||
.send({ userId: 'non-existent', achievementName: 'Test Award' });
|
.send({ userId: 'non-existent', achievementName: 'Test Award' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.message).toBe('User not found');
|
expect(response.body.error.message).toBe('User not found');
|
||||||
@@ -131,7 +131,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
|
|
||||||
describe('GET /me', () => {
|
describe('GET /me', () => {
|
||||||
it('should return 401 Unauthorized when user is not authenticated', async () => {
|
it('should return 401 Unauthorized when user is not authenticated', async () => {
|
||||||
const response = await supertest(unauthenticatedApp).get('/api/achievements/me');
|
const response = await supertest(unauthenticatedApp).get('/api/v1/achievements/me');
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
];
|
];
|
||||||
vi.mocked(db.gamificationRepo.getUserAchievements).mockResolvedValue(mockUserAchievements);
|
vi.mocked(db.gamificationRepo.getUserAchievements).mockResolvedValue(mockUserAchievements);
|
||||||
|
|
||||||
const response = await supertest(authenticatedApp).get('/api/achievements/me');
|
const response = await supertest(authenticatedApp).get('/api/v1/achievements/me');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockUserAchievements);
|
expect(response.body.data).toEqual(mockUserAchievements);
|
||||||
@@ -165,7 +165,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
});
|
});
|
||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(db.gamificationRepo.getUserAchievements).mockRejectedValue(dbError);
|
vi.mocked(db.gamificationRepo.getUserAchievements).mockRejectedValue(dbError);
|
||||||
const response = await supertest(authenticatedApp).get('/api/achievements/me');
|
const response = await supertest(authenticatedApp).get('/api/v1/achievements/me');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
});
|
});
|
||||||
@@ -176,7 +176,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
|
|
||||||
it('should return 401 Unauthorized if user is not authenticated', async () => {
|
it('should return 401 Unauthorized if user is not authenticated', async () => {
|
||||||
const response = await supertest(unauthenticatedApp)
|
const response = await supertest(unauthenticatedApp)
|
||||||
.post('/api/achievements/award')
|
.post('/api/v1/achievements/award')
|
||||||
.send(awardPayload);
|
.send(awardPayload);
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
});
|
});
|
||||||
@@ -190,7 +190,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
// Let the default isAdmin mock (set in beforeEach) run, which denies access
|
// Let the default isAdmin mock (set in beforeEach) run, which denies access
|
||||||
|
|
||||||
const response = await supertest(authenticatedApp)
|
const response = await supertest(authenticatedApp)
|
||||||
.post('/api/achievements/award')
|
.post('/api/v1/achievements/award')
|
||||||
.send(awardPayload);
|
.send(awardPayload);
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
});
|
});
|
||||||
@@ -204,7 +204,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next()); // Grant admin access
|
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next()); // Grant admin access
|
||||||
vi.mocked(db.gamificationRepo.awardAchievement).mockResolvedValue(undefined);
|
vi.mocked(db.gamificationRepo.awardAchievement).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(adminApp).post('/api/achievements/award').send(awardPayload);
|
const response = await supertest(adminApp).post('/api/v1/achievements/award').send(awardPayload);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.message).toContain('Successfully awarded');
|
expect(response.body.data.message).toContain('Successfully awarded');
|
||||||
@@ -224,7 +224,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next());
|
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next());
|
||||||
vi.mocked(db.gamificationRepo.awardAchievement).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(db.gamificationRepo.awardAchievement).mockRejectedValue(new Error('DB Error'));
|
||||||
|
|
||||||
const response = await supertest(adminApp).post('/api/achievements/award').send(awardPayload);
|
const response = await supertest(adminApp).post('/api/v1/achievements/award').send(awardPayload);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
});
|
});
|
||||||
@@ -237,7 +237,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next());
|
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next());
|
||||||
|
|
||||||
const response = await supertest(adminApp)
|
const response = await supertest(adminApp)
|
||||||
.post('/api/achievements/award')
|
.post('/api/v1/achievements/award')
|
||||||
.send({ userId: '', achievementName: '' });
|
.send({ userId: '', achievementName: '' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details).toHaveLength(2);
|
expect(response.body.error.details).toHaveLength(2);
|
||||||
@@ -251,13 +251,13 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next());
|
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next());
|
||||||
|
|
||||||
const response1 = await supertest(adminApp)
|
const response1 = await supertest(adminApp)
|
||||||
.post('/api/achievements/award')
|
.post('/api/v1/achievements/award')
|
||||||
.send({ achievementName: 'Test Award' });
|
.send({ achievementName: 'Test Award' });
|
||||||
expect(response1.status).toBe(400);
|
expect(response1.status).toBe(400);
|
||||||
expect(response1.body.error.details[0].message).toBe('userId is required.');
|
expect(response1.body.error.details[0].message).toBe('userId is required.');
|
||||||
|
|
||||||
const response2 = await supertest(adminApp)
|
const response2 = await supertest(adminApp)
|
||||||
.post('/api/achievements/award')
|
.post('/api/v1/achievements/award')
|
||||||
.send({ userId: 'user-789' });
|
.send({ userId: 'user-789' });
|
||||||
expect(response2.status).toBe(400);
|
expect(response2.status).toBe(400);
|
||||||
expect(response2.body.error.details[0].message).toBe('achievementName is required.');
|
expect(response2.body.error.details[0].message).toBe('achievementName is required.');
|
||||||
@@ -274,7 +274,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(adminApp)
|
const response = await supertest(adminApp)
|
||||||
.post('/api/achievements/award')
|
.post('/api/v1/achievements/award')
|
||||||
.send({ userId: 'non-existent', achievementName: 'Test Award' });
|
.send({ userId: 'non-existent', achievementName: 'Test Award' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.message).toBe('User not found');
|
expect(response.body.error.message).toBe('User not found');
|
||||||
@@ -294,7 +294,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
vi.mocked(db.gamificationRepo.getLeaderboard).mockResolvedValue(mockLeaderboard);
|
vi.mocked(db.gamificationRepo.getLeaderboard).mockResolvedValue(mockLeaderboard);
|
||||||
|
|
||||||
const response = await supertest(unauthenticatedApp).get(
|
const response = await supertest(unauthenticatedApp).get(
|
||||||
'/api/achievements/leaderboard?limit=5',
|
'/api/v1/achievements/leaderboard?limit=5',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -313,7 +313,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
];
|
];
|
||||||
vi.mocked(db.gamificationRepo.getLeaderboard).mockResolvedValue(mockLeaderboard);
|
vi.mocked(db.gamificationRepo.getLeaderboard).mockResolvedValue(mockLeaderboard);
|
||||||
|
|
||||||
const response = await supertest(unauthenticatedApp).get('/api/achievements/leaderboard');
|
const response = await supertest(unauthenticatedApp).get('/api/v1/achievements/leaderboard');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockLeaderboard);
|
expect(response.body.data).toEqual(mockLeaderboard);
|
||||||
@@ -322,14 +322,14 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
|
|
||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
vi.mocked(db.gamificationRepo.getLeaderboard).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(db.gamificationRepo.getLeaderboard).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(unauthenticatedApp).get('/api/achievements/leaderboard');
|
const response = await supertest(unauthenticatedApp).get('/api/v1/achievements/leaderboard');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for an invalid limit parameter', async () => {
|
it('should return 400 for an invalid limit parameter', async () => {
|
||||||
const response = await supertest(unauthenticatedApp).get(
|
const response = await supertest(unauthenticatedApp).get(
|
||||||
'/api/achievements/leaderboard?limit=100',
|
'/api/v1/achievements/leaderboard?limit=100',
|
||||||
);
|
);
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details).toBeDefined();
|
expect(response.body.error.details).toBeDefined();
|
||||||
@@ -341,7 +341,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
it('should apply publicReadLimiter to GET /', async () => {
|
it('should apply publicReadLimiter to GET /', async () => {
|
||||||
vi.mocked(db.gamificationRepo.getAllAchievements).mockResolvedValue([]);
|
vi.mocked(db.gamificationRepo.getAllAchievements).mockResolvedValue([]);
|
||||||
const response = await supertest(unauthenticatedApp)
|
const response = await supertest(unauthenticatedApp)
|
||||||
.get('/api/achievements')
|
.get('/api/v1/achievements')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -356,7 +356,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
});
|
});
|
||||||
vi.mocked(db.gamificationRepo.getUserAchievements).mockResolvedValue([]);
|
vi.mocked(db.gamificationRepo.getUserAchievements).mockResolvedValue([]);
|
||||||
const response = await supertest(authenticatedApp)
|
const response = await supertest(authenticatedApp)
|
||||||
.get('/api/achievements/me')
|
.get('/api/v1/achievements/me')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -373,7 +373,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
|||||||
vi.mocked(db.gamificationRepo.awardAchievement).mockResolvedValue(undefined);
|
vi.mocked(db.gamificationRepo.awardAchievement).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(adminApp)
|
const response = await supertest(adminApp)
|
||||||
.post('/api/achievements/award')
|
.post('/api/v1/achievements/award')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ userId: 'some-user', achievementName: 'some-achievement' });
|
.send({ userId: 'some-user', achievementName: 'some-achievement' });
|
||||||
|
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ const mockedFs = fs as Mocked<typeof fs>;
|
|||||||
const { logger } = await import('../services/logger.server');
|
const { logger } = await import('../services/logger.server');
|
||||||
|
|
||||||
// 2. Create a minimal Express app to host the router for testing.
|
// 2. Create a minimal Express app to host the router for testing.
|
||||||
const app = createTestApp({ router: healthRouter, basePath: '/api/health' });
|
const app = createTestApp({ router: healthRouter, basePath: '/api/v1/health' });
|
||||||
|
|
||||||
describe('Health Routes (/api/health)', () => {
|
describe('Health Routes (/api/v1/health)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Clear mock history before each test to ensure isolation.
|
// Clear mock history before each test to ensure isolation.
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -61,7 +61,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
describe('GET /ping', () => {
|
describe('GET /ping', () => {
|
||||||
it('should return 200 OK with "pong"', async () => {
|
it('should return 200 OK with "pong"', async () => {
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/health/ping');
|
const response = await supertest(app).get('/api/v1/health/ping');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -75,7 +75,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedRedisConnection.ping.mockResolvedValue('PONG');
|
mockedRedisConnection.ping.mockResolvedValue('PONG');
|
||||||
|
|
||||||
// Act: Make a request to the endpoint.
|
// Act: Make a request to the endpoint.
|
||||||
const response = await supertest(app).get('/api/health/redis');
|
const response = await supertest(app).get('/api/v1/health/redis');
|
||||||
|
|
||||||
// Assert: Check for the correct status and response body.
|
// Assert: Check for the correct status and response body.
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -89,7 +89,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedRedisConnection.ping.mockRejectedValue(redisError);
|
mockedRedisConnection.ping.mockRejectedValue(redisError);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/health/redis');
|
const response = await supertest(app).get('/api/v1/health/redis');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -101,7 +101,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedRedisConnection.ping.mockResolvedValue('OK'); // Not 'PONG'
|
mockedRedisConnection.ping.mockResolvedValue('OK'); // Not 'PONG'
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/health/redis');
|
const response = await supertest(app).get('/api/v1/health/redis');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -117,7 +117,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
vi.setSystemTime(fakeDate);
|
vi.setSystemTime(fakeDate);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/health/time');
|
const response = await supertest(app).get('/api/v1/health/time');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -133,7 +133,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedDbConnection.checkTablesExist.mockResolvedValue([]);
|
mockedDbConnection.checkTablesExist.mockResolvedValue([]);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/health/db-schema');
|
const response = await supertest(app).get('/api/v1/health/db-schema');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -145,7 +145,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
// Arrange: Mock the service function to return missing table names
|
// Arrange: Mock the service function to return missing table names
|
||||||
mockedDbConnection.checkTablesExist.mockResolvedValue(['missing_table_1', 'missing_table_2']);
|
mockedDbConnection.checkTablesExist.mockResolvedValue(['missing_table_1', 'missing_table_2']);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/db-schema');
|
const response = await supertest(app).get('/api/v1/health/db-schema');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toContain(
|
expect(response.body.error.message).toContain(
|
||||||
@@ -159,7 +159,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
const dbError = new Error('DB connection failed');
|
const dbError = new Error('DB connection failed');
|
||||||
mockedDbConnection.checkTablesExist.mockRejectedValue(dbError);
|
mockedDbConnection.checkTablesExist.mockRejectedValue(dbError);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/db-schema');
|
const response = await supertest(app).get('/api/v1/health/db-schema');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB connection failed'); // This is the message from the original error
|
expect(response.body.error.message).toBe('DB connection failed'); // This is the message from the original error
|
||||||
@@ -181,7 +181,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
const dbError = { message: 'DB connection failed' };
|
const dbError = { message: 'DB connection failed' };
|
||||||
mockedDbConnection.checkTablesExist.mockRejectedValue(dbError);
|
mockedDbConnection.checkTablesExist.mockRejectedValue(dbError);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/db-schema');
|
const response = await supertest(app).get('/api/v1/health/db-schema');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB connection failed'); // This is the message from the original error
|
expect(response.body.error.message).toBe('DB connection failed'); // This is the message from the original error
|
||||||
@@ -201,7 +201,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedFs.access.mockResolvedValue(undefined);
|
mockedFs.access.mockResolvedValue(undefined);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/health/storage');
|
const response = await supertest(app).get('/api/v1/health/storage');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -215,7 +215,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedFs.access.mockRejectedValue(accessError);
|
mockedFs.access.mockRejectedValue(accessError);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/health/storage');
|
const response = await supertest(app).get('/api/v1/health/storage');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -234,7 +234,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedFs.access.mockRejectedValue(accessError);
|
mockedFs.access.mockRejectedValue(accessError);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/health/storage');
|
const response = await supertest(app).get('/api/v1/health/storage');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -258,7 +258,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/health/db-pool');
|
const response = await supertest(app).get('/api/v1/health/db-pool');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -275,7 +275,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/health/db-pool');
|
const response = await supertest(app).get('/api/v1/health/db-pool');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -295,7 +295,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
throw poolError;
|
throw poolError;
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/db-pool');
|
const response = await supertest(app).get('/api/v1/health/db-pool');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('Pool is not initialized'); // This is the message from the original error
|
expect(response.body.error.message).toBe('Pool is not initialized'); // This is the message from the original error
|
||||||
@@ -315,7 +315,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
throw poolError;
|
throw poolError;
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/db-pool');
|
const response = await supertest(app).get('/api/v1/health/db-pool');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('Pool is not initialized'); // This is the message from the original error
|
expect(response.body.error.message).toBe('Pool is not initialized'); // This is the message from the original error
|
||||||
@@ -334,7 +334,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
const redisError = new Error('Connection timed out');
|
const redisError = new Error('Connection timed out');
|
||||||
mockedRedisConnection.ping.mockRejectedValue(redisError);
|
mockedRedisConnection.ping.mockRejectedValue(redisError);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/redis');
|
const response = await supertest(app).get('/api/v1/health/redis');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('Connection timed out');
|
expect(response.body.error.message).toBe('Connection timed out');
|
||||||
@@ -354,7 +354,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
it('should return 500 if Redis ping returns an unexpected response', async () => {
|
it('should return 500 if Redis ping returns an unexpected response', async () => {
|
||||||
mockedRedisConnection.ping.mockResolvedValue('OK'); // Not 'PONG'
|
mockedRedisConnection.ping.mockResolvedValue('OK'); // Not 'PONG'
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/redis');
|
const response = await supertest(app).get('/api/v1/health/redis');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toContain('Unexpected Redis ping response: OK');
|
expect(response.body.error.message).toContain('Unexpected Redis ping response: OK');
|
||||||
@@ -373,7 +373,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
const redisError = { message: 'Non-error rejection' };
|
const redisError = { message: 'Non-error rejection' };
|
||||||
mockedRedisConnection.ping.mockRejectedValue(redisError);
|
mockedRedisConnection.ping.mockRejectedValue(redisError);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/redis');
|
const response = await supertest(app).get('/api/v1/health/redis');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('Non-error rejection');
|
expect(response.body.error.message).toBe('Non-error rejection');
|
||||||
@@ -386,7 +386,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
|
|
||||||
describe('GET /live', () => {
|
describe('GET /live', () => {
|
||||||
it('should return 200 OK with status ok', async () => {
|
it('should return 200 OK with status ok', async () => {
|
||||||
const response = await supertest(app).get('/api/health/live');
|
const response = await supertest(app).get('/api/v1/health/live');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
@@ -408,7 +408,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedRedisConnection.ping.mockResolvedValue('PONG');
|
mockedRedisConnection.ping.mockResolvedValue('PONG');
|
||||||
mockedFs.access.mockResolvedValue(undefined);
|
mockedFs.access.mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/ready');
|
const response = await supertest(app).get('/api/v1/health/ready');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
@@ -432,7 +432,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedRedisConnection.ping.mockResolvedValue('PONG');
|
mockedRedisConnection.ping.mockResolvedValue('PONG');
|
||||||
mockedFs.access.mockResolvedValue(undefined);
|
mockedFs.access.mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/ready');
|
const response = await supertest(app).get('/api/v1/health/ready');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
@@ -447,7 +447,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedRedisConnection.ping.mockResolvedValue('PONG');
|
mockedRedisConnection.ping.mockResolvedValue('PONG');
|
||||||
mockedFs.access.mockResolvedValue(undefined);
|
mockedFs.access.mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/ready');
|
const response = await supertest(app).get('/api/v1/health/ready');
|
||||||
|
|
||||||
expect(response.status).toBe(503);
|
expect(response.status).toBe(503);
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
@@ -468,7 +468,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedRedisConnection.ping.mockRejectedValue(new Error('Redis connection refused'));
|
mockedRedisConnection.ping.mockRejectedValue(new Error('Redis connection refused'));
|
||||||
mockedFs.access.mockResolvedValue(undefined);
|
mockedFs.access.mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/ready');
|
const response = await supertest(app).get('/api/v1/health/ready');
|
||||||
|
|
||||||
expect(response.status).toBe(503);
|
expect(response.status).toBe(503);
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
@@ -489,7 +489,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedRedisConnection.ping.mockResolvedValue('UNEXPECTED');
|
mockedRedisConnection.ping.mockResolvedValue('UNEXPECTED');
|
||||||
mockedFs.access.mockResolvedValue(undefined);
|
mockedFs.access.mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/ready');
|
const response = await supertest(app).get('/api/v1/health/ready');
|
||||||
|
|
||||||
expect(response.status).toBe(503);
|
expect(response.status).toBe(503);
|
||||||
expect(response.body.error.details.services.redis.status).toBe('unhealthy');
|
expect(response.body.error.details.services.redis.status).toBe('unhealthy');
|
||||||
@@ -510,7 +510,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedRedisConnection.ping.mockResolvedValue('PONG');
|
mockedRedisConnection.ping.mockResolvedValue('PONG');
|
||||||
mockedFs.access.mockRejectedValue(new Error('Permission denied'));
|
mockedFs.access.mockRejectedValue(new Error('Permission denied'));
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/ready');
|
const response = await supertest(app).get('/api/v1/health/ready');
|
||||||
|
|
||||||
// Storage is not a critical service, so it should still return 200
|
// Storage is not a critical service, so it should still return 200
|
||||||
// but overall status should reflect storage issue
|
// but overall status should reflect storage issue
|
||||||
@@ -525,7 +525,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedRedisConnection.ping.mockResolvedValue('PONG');
|
mockedRedisConnection.ping.mockResolvedValue('PONG');
|
||||||
mockedFs.access.mockResolvedValue(undefined);
|
mockedFs.access.mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/ready');
|
const response = await supertest(app).get('/api/v1/health/ready');
|
||||||
|
|
||||||
expect(response.status).toBe(503);
|
expect(response.status).toBe(503);
|
||||||
expect(response.body.error.details.services.database.status).toBe('unhealthy');
|
expect(response.body.error.details.services.database.status).toBe('unhealthy');
|
||||||
@@ -546,7 +546,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedRedisConnection.ping.mockRejectedValue('String error');
|
mockedRedisConnection.ping.mockRejectedValue('String error');
|
||||||
mockedFs.access.mockResolvedValue(undefined);
|
mockedFs.access.mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/ready');
|
const response = await supertest(app).get('/api/v1/health/ready');
|
||||||
|
|
||||||
expect(response.status).toBe(503);
|
expect(response.status).toBe(503);
|
||||||
expect(response.body.error.details.services.redis.status).toBe('unhealthy');
|
expect(response.body.error.details.services.redis.status).toBe('unhealthy');
|
||||||
@@ -565,7 +565,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
waitingCount: 1,
|
waitingCount: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/startup');
|
const response = await supertest(app).get('/api/v1/health/startup');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
@@ -579,7 +579,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
const mockPool = { query: vi.fn().mockRejectedValue(new Error('Database not ready')) };
|
const mockPool = { query: vi.fn().mockRejectedValue(new Error('Database not ready')) };
|
||||||
mockedDbConnection.getPool.mockReturnValue(mockPool as never);
|
mockedDbConnection.getPool.mockReturnValue(mockPool as never);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/startup');
|
const response = await supertest(app).get('/api/v1/health/startup');
|
||||||
|
|
||||||
expect(response.status).toBe(503);
|
expect(response.status).toBe(503);
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
@@ -599,7 +599,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
waitingCount: 5, // > 3 triggers degraded
|
waitingCount: 5, // > 3 triggers degraded
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/startup');
|
const response = await supertest(app).get('/api/v1/health/startup');
|
||||||
|
|
||||||
// Degraded is not unhealthy, so startup should succeed
|
// Degraded is not unhealthy, so startup should succeed
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -612,7 +612,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
const mockPool = { query: vi.fn().mockRejectedValue({ code: 'ECONNREFUSED' }) };
|
const mockPool = { query: vi.fn().mockRejectedValue({ code: 'ECONNREFUSED' }) };
|
||||||
mockedDbConnection.getPool.mockReturnValue(mockPool as never);
|
mockedDbConnection.getPool.mockReturnValue(mockPool as never);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/health/startup');
|
const response = await supertest(app).get('/api/v1/health/startup');
|
||||||
|
|
||||||
expect(response.status).toBe(503);
|
expect(response.status).toBe(503);
|
||||||
expect(response.body.error.details.database.status).toBe('unhealthy');
|
expect(response.body.error.details.database.status).toBe('unhealthy');
|
||||||
@@ -665,7 +665,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedRedisConnection.get = vi.fn().mockResolvedValue(heartbeatValue);
|
mockedRedisConnection.get = vi.fn().mockResolvedValue(heartbeatValue);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/health/queues');
|
const response = await supertest(app).get('/api/v1/health/queues');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -719,7 +719,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedRedisConnection.get = vi.fn().mockResolvedValue(null);
|
mockedRedisConnection.get = vi.fn().mockResolvedValue(null);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/health/queues');
|
const response = await supertest(app).get('/api/v1/health/queues');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(503);
|
expect(response.status).toBe(503);
|
||||||
@@ -769,7 +769,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/health/queues');
|
const response = await supertest(app).get('/api/v1/health/queues');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(503);
|
expect(response.status).toBe(503);
|
||||||
@@ -804,7 +804,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedRedisConnection.get = vi.fn().mockResolvedValue(null);
|
mockedRedisConnection.get = vi.fn().mockResolvedValue(null);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/health/queues');
|
const response = await supertest(app).get('/api/v1/health/queues');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(503);
|
expect(response.status).toBe(503);
|
||||||
@@ -839,7 +839,7 @@ describe('Health Routes (/api/health)', () => {
|
|||||||
mockedRedisConnection.get = vi.fn().mockRejectedValue(new Error('Redis connection lost'));
|
mockedRedisConnection.get = vi.fn().mockRejectedValue(new Error('Redis connection lost'));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/health/queues');
|
const response = await supertest(app).get('/api/v1/health/queues');
|
||||||
|
|
||||||
// Assert: Should still return queue metrics but mark workers as unhealthy
|
// Assert: Should still return queue metrics but mark workers as unhealthy
|
||||||
expect(response.status).toBe(503);
|
expect(response.status).toBe(503);
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ function createMockInventoryItem(overrides: Partial<UserInventoryItem> = {}): Us
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Inventory Routes (/api/inventory)', () => {
|
describe('Inventory Routes (/api/v1/inventory)', () => {
|
||||||
const mockUserProfile = createMockUserProfile({
|
const mockUserProfile = createMockUserProfile({
|
||||||
user: { user_id: 'user-123', email: 'test@test.com' },
|
user: { user_id: 'user-123', email: 'test@test.com' },
|
||||||
});
|
});
|
||||||
@@ -98,7 +98,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
|
|
||||||
const app = createTestApp({
|
const app = createTestApp({
|
||||||
router: inventoryRouter,
|
router: inventoryRouter,
|
||||||
basePath: '/api/inventory',
|
basePath: '/api/v1/inventory',
|
||||||
authenticatedUser: mockUserProfile,
|
authenticatedUser: mockUserProfile,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
total: 1,
|
total: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory');
|
const response = await supertest(app).get('/api/v1/inventory');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.items).toHaveLength(1);
|
expect(response.body.data.items).toHaveLength(1);
|
||||||
@@ -124,7 +124,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
it('should support filtering by location', async () => {
|
it('should support filtering by location', async () => {
|
||||||
vi.mocked(expiryService.getInventory).mockResolvedValue({ items: [], total: 0 });
|
vi.mocked(expiryService.getInventory).mockResolvedValue({ items: [], total: 0 });
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory?location=fridge');
|
const response = await supertest(app).get('/api/v1/inventory?location=fridge');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(expiryService.getInventory).toHaveBeenCalledWith(
|
expect(expiryService.getInventory).toHaveBeenCalledWith(
|
||||||
@@ -136,7 +136,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
it('should support filtering by expiring_within_days', async () => {
|
it('should support filtering by expiring_within_days', async () => {
|
||||||
vi.mocked(expiryService.getInventory).mockResolvedValue({ items: [], total: 0 });
|
vi.mocked(expiryService.getInventory).mockResolvedValue({ items: [], total: 0 });
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory?expiring_within_days=7');
|
const response = await supertest(app).get('/api/v1/inventory?expiring_within_days=7');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(expiryService.getInventory).toHaveBeenCalledWith(
|
expect(expiryService.getInventory).toHaveBeenCalledWith(
|
||||||
@@ -148,7 +148,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
it('should support search filter', async () => {
|
it('should support search filter', async () => {
|
||||||
vi.mocked(expiryService.getInventory).mockResolvedValue({ items: [], total: 0 });
|
vi.mocked(expiryService.getInventory).mockResolvedValue({ items: [], total: 0 });
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory?search=milk');
|
const response = await supertest(app).get('/api/v1/inventory?search=milk');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(expiryService.getInventory).toHaveBeenCalledWith(
|
expect(expiryService.getInventory).toHaveBeenCalledWith(
|
||||||
@@ -161,7 +161,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
vi.mocked(expiryService.getInventory).mockResolvedValue({ items: [], total: 0 });
|
vi.mocked(expiryService.getInventory).mockResolvedValue({ items: [], total: 0 });
|
||||||
|
|
||||||
const response = await supertest(app).get(
|
const response = await supertest(app).get(
|
||||||
'/api/inventory?sort_by=expiry_date&sort_order=asc',
|
'/api/v1/inventory?sort_by=expiry_date&sort_order=asc',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -175,7 +175,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid location', async () => {
|
it('should return 400 for invalid location', async () => {
|
||||||
const response = await supertest(app).get('/api/inventory?location=invalid');
|
const response = await supertest(app).get('/api/v1/inventory?location=invalid');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -183,7 +183,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
it('should return 500 if service fails', async () => {
|
it('should return 500 if service fails', async () => {
|
||||||
vi.mocked(expiryService.getInventory).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(expiryService.getInventory).mockRejectedValue(new Error('DB Error'));
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory');
|
const response = await supertest(app).get('/api/v1/inventory');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
@@ -194,7 +194,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
const mockItem = createMockInventoryItem();
|
const mockItem = createMockInventoryItem();
|
||||||
vi.mocked(expiryService.addInventoryItem).mockResolvedValue(mockItem);
|
vi.mocked(expiryService.addInventoryItem).mockResolvedValue(mockItem);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/inventory').send({
|
const response = await supertest(app).post('/api/v1/inventory').send({
|
||||||
item_name: 'Milk',
|
item_name: 'Milk',
|
||||||
source: 'manual',
|
source: 'manual',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
@@ -215,7 +215,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if item_name is missing', async () => {
|
it('should return 400 if item_name is missing', async () => {
|
||||||
const response = await supertest(app).post('/api/inventory').send({
|
const response = await supertest(app).post('/api/v1/inventory').send({
|
||||||
source: 'manual',
|
source: 'manual',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid source', async () => {
|
it('should return 400 for invalid source', async () => {
|
||||||
const response = await supertest(app).post('/api/inventory').send({
|
const response = await supertest(app).post('/api/v1/inventory').send({
|
||||||
item_name: 'Milk',
|
item_name: 'Milk',
|
||||||
source: 'invalid_source',
|
source: 'invalid_source',
|
||||||
});
|
});
|
||||||
@@ -234,7 +234,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid expiry_date format', async () => {
|
it('should return 400 for invalid expiry_date format', async () => {
|
||||||
const response = await supertest(app).post('/api/inventory').send({
|
const response = await supertest(app).post('/api/v1/inventory').send({
|
||||||
item_name: 'Milk',
|
item_name: 'Milk',
|
||||||
source: 'manual',
|
source: 'manual',
|
||||||
expiry_date: '01-10-2024',
|
expiry_date: '01-10-2024',
|
||||||
@@ -247,7 +247,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
it('should return 500 if service fails', async () => {
|
it('should return 500 if service fails', async () => {
|
||||||
vi.mocked(expiryService.addInventoryItem).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(expiryService.addInventoryItem).mockRejectedValue(new Error('DB Error'));
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/inventory').send({
|
const response = await supertest(app).post('/api/v1/inventory').send({
|
||||||
item_name: 'Milk',
|
item_name: 'Milk',
|
||||||
source: 'manual',
|
source: 'manual',
|
||||||
});
|
});
|
||||||
@@ -261,7 +261,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
const mockItem = createMockInventoryItem();
|
const mockItem = createMockInventoryItem();
|
||||||
vi.mocked(expiryService.getInventoryItemById).mockResolvedValue(mockItem);
|
vi.mocked(expiryService.getInventoryItemById).mockResolvedValue(mockItem);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory/1');
|
const response = await supertest(app).get('/api/v1/inventory/1');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.inventory_id).toBe(1);
|
expect(response.body.data.inventory_id).toBe(1);
|
||||||
@@ -277,13 +277,13 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
new NotFoundError('Item not found'),
|
new NotFoundError('Item not found'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory/999');
|
const response = await supertest(app).get('/api/v1/inventory/999');
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid inventory ID', async () => {
|
it('should return 400 for invalid inventory ID', async () => {
|
||||||
const response = await supertest(app).get('/api/inventory/abc');
|
const response = await supertest(app).get('/api/v1/inventory/abc');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -294,7 +294,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
const mockItem = createMockInventoryItem({ quantity: 2 });
|
const mockItem = createMockInventoryItem({ quantity: 2 });
|
||||||
vi.mocked(expiryService.updateInventoryItem).mockResolvedValue(mockItem);
|
vi.mocked(expiryService.updateInventoryItem).mockResolvedValue(mockItem);
|
||||||
|
|
||||||
const response = await supertest(app).put('/api/inventory/1').send({
|
const response = await supertest(app).put('/api/v1/inventory/1').send({
|
||||||
quantity: 2,
|
quantity: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -306,7 +306,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
const mockItem = createMockInventoryItem({ expiry_date: '2024-03-01' });
|
const mockItem = createMockInventoryItem({ expiry_date: '2024-03-01' });
|
||||||
vi.mocked(expiryService.updateInventoryItem).mockResolvedValue(mockItem);
|
vi.mocked(expiryService.updateInventoryItem).mockResolvedValue(mockItem);
|
||||||
|
|
||||||
const response = await supertest(app).put('/api/inventory/1').send({
|
const response = await supertest(app).put('/api/v1/inventory/1').send({
|
||||||
expiry_date: '2024-03-01',
|
expiry_date: '2024-03-01',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -320,7 +320,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if no update fields provided', async () => {
|
it('should return 400 if no update fields provided', async () => {
|
||||||
const response = await supertest(app).put('/api/inventory/1').send({});
|
const response = await supertest(app).put('/api/v1/inventory/1').send({});
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toMatch(/At least one field/);
|
expect(response.body.error.details[0].message).toMatch(/At least one field/);
|
||||||
@@ -331,7 +331,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
new NotFoundError('Item not found'),
|
new NotFoundError('Item not found'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).put('/api/inventory/999').send({
|
const response = await supertest(app).put('/api/v1/inventory/999').send({
|
||||||
quantity: 2,
|
quantity: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -343,7 +343,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
it('should delete an inventory item', async () => {
|
it('should delete an inventory item', async () => {
|
||||||
vi.mocked(expiryService.deleteInventoryItem).mockResolvedValue(undefined);
|
vi.mocked(expiryService.deleteInventoryItem).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app).delete('/api/inventory/1');
|
const response = await supertest(app).delete('/api/v1/inventory/1');
|
||||||
|
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
expect(expiryService.deleteInventoryItem).toHaveBeenCalledWith(
|
expect(expiryService.deleteInventoryItem).toHaveBeenCalledWith(
|
||||||
@@ -358,7 +358,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
new NotFoundError('Item not found'),
|
new NotFoundError('Item not found'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).delete('/api/inventory/999');
|
const response = await supertest(app).delete('/api/v1/inventory/999');
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
});
|
});
|
||||||
@@ -368,7 +368,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
it('should mark item as consumed', async () => {
|
it('should mark item as consumed', async () => {
|
||||||
vi.mocked(expiryService.markItemConsumed).mockResolvedValue(undefined);
|
vi.mocked(expiryService.markItemConsumed).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/inventory/1/consume');
|
const response = await supertest(app).post('/api/v1/inventory/1/consume');
|
||||||
|
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
expect(expiryService.markItemConsumed).toHaveBeenCalledWith(
|
expect(expiryService.markItemConsumed).toHaveBeenCalledWith(
|
||||||
@@ -383,7 +383,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
new NotFoundError('Item not found'),
|
new NotFoundError('Item not found'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/inventory/999/consume');
|
const response = await supertest(app).post('/api/v1/inventory/999/consume');
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
});
|
});
|
||||||
@@ -411,7 +411,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
|
|
||||||
vi.mocked(expiryService.getExpiringItemsGrouped).mockResolvedValue(mockSummary);
|
vi.mocked(expiryService.getExpiringItemsGrouped).mockResolvedValue(mockSummary);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory/expiring/summary');
|
const response = await supertest(app).get('/api/v1/inventory/expiring/summary');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.counts.total).toBe(4);
|
expect(response.body.data.counts.total).toBe(4);
|
||||||
@@ -420,7 +420,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
it('should return 500 if service fails', async () => {
|
it('should return 500 if service fails', async () => {
|
||||||
vi.mocked(expiryService.getExpiringItemsGrouped).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(expiryService.getExpiringItemsGrouped).mockRejectedValue(new Error('DB Error'));
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory/expiring/summary');
|
const response = await supertest(app).get('/api/v1/inventory/expiring/summary');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
@@ -431,7 +431,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
const mockItems = [createMockInventoryItem({ days_until_expiry: 5 })];
|
const mockItems = [createMockInventoryItem({ days_until_expiry: 5 })];
|
||||||
vi.mocked(expiryService.getExpiringItems).mockResolvedValue(mockItems);
|
vi.mocked(expiryService.getExpiringItems).mockResolvedValue(mockItems);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory/expiring');
|
const response = await supertest(app).get('/api/v1/inventory/expiring');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.items).toHaveLength(1);
|
expect(response.body.data.items).toHaveLength(1);
|
||||||
@@ -445,7 +445,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
it('should accept custom days parameter', async () => {
|
it('should accept custom days parameter', async () => {
|
||||||
vi.mocked(expiryService.getExpiringItems).mockResolvedValue([]);
|
vi.mocked(expiryService.getExpiringItems).mockResolvedValue([]);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory/expiring?days=14');
|
const response = await supertest(app).get('/api/v1/inventory/expiring?days=14');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(expiryService.getExpiringItems).toHaveBeenCalledWith(
|
expect(expiryService.getExpiringItems).toHaveBeenCalledWith(
|
||||||
@@ -456,7 +456,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid days parameter', async () => {
|
it('should return 400 for invalid days parameter', async () => {
|
||||||
const response = await supertest(app).get('/api/inventory/expiring?days=100');
|
const response = await supertest(app).get('/api/v1/inventory/expiring?days=100');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -469,7 +469,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
];
|
];
|
||||||
vi.mocked(expiryService.getExpiredItems).mockResolvedValue(mockItems);
|
vi.mocked(expiryService.getExpiredItems).mockResolvedValue(mockItems);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory/expired');
|
const response = await supertest(app).get('/api/v1/inventory/expired');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.items).toHaveLength(1);
|
expect(response.body.data.items).toHaveLength(1);
|
||||||
@@ -482,7 +482,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
it('should return 500 if service fails', async () => {
|
it('should return 500 if service fails', async () => {
|
||||||
vi.mocked(expiryService.getExpiredItems).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(expiryService.getExpiredItems).mockRejectedValue(new Error('DB Error'));
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory/expired');
|
const response = await supertest(app).get('/api/v1/inventory/expired');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
@@ -509,7 +509,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
|
|
||||||
vi.mocked(expiryService.getAlertSettings).mockResolvedValue(mockSettings);
|
vi.mocked(expiryService.getAlertSettings).mockResolvedValue(mockSettings);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory/alerts');
|
const response = await supertest(app).get('/api/v1/inventory/alerts');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toHaveLength(1);
|
expect(response.body.data).toHaveLength(1);
|
||||||
@@ -519,7 +519,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
it('should return 500 if service fails', async () => {
|
it('should return 500 if service fails', async () => {
|
||||||
vi.mocked(expiryService.getAlertSettings).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(expiryService.getAlertSettings).mockRejectedValue(new Error('DB Error'));
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory/alerts');
|
const response = await supertest(app).get('/api/v1/inventory/alerts');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
@@ -540,7 +540,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
|
|
||||||
vi.mocked(expiryService.updateAlertSettings).mockResolvedValue(mockSettings);
|
vi.mocked(expiryService.updateAlertSettings).mockResolvedValue(mockSettings);
|
||||||
|
|
||||||
const response = await supertest(app).put('/api/inventory/alerts/email').send({
|
const response = await supertest(app).put('/api/v1/inventory/alerts/email').send({
|
||||||
days_before_expiry: 5,
|
days_before_expiry: 5,
|
||||||
is_enabled: true,
|
is_enabled: true,
|
||||||
});
|
});
|
||||||
@@ -556,7 +556,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid alert method', async () => {
|
it('should return 400 for invalid alert method', async () => {
|
||||||
const response = await supertest(app).put('/api/inventory/alerts/sms').send({
|
const response = await supertest(app).put('/api/v1/inventory/alerts/sms').send({
|
||||||
is_enabled: true,
|
is_enabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -564,7 +564,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid days_before_expiry', async () => {
|
it('should return 400 for invalid days_before_expiry', async () => {
|
||||||
const response = await supertest(app).put('/api/inventory/alerts/email').send({
|
const response = await supertest(app).put('/api/v1/inventory/alerts/email').send({
|
||||||
days_before_expiry: 0,
|
days_before_expiry: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -572,7 +572,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if days_before_expiry exceeds maximum', async () => {
|
it('should return 400 if days_before_expiry exceeds maximum', async () => {
|
||||||
const response = await supertest(app).put('/api/inventory/alerts/email').send({
|
const response = await supertest(app).put('/api/v1/inventory/alerts/email').send({
|
||||||
days_before_expiry: 31,
|
days_before_expiry: 31,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -582,7 +582,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
it('should return 500 if service fails', async () => {
|
it('should return 500 if service fails', async () => {
|
||||||
vi.mocked(expiryService.updateAlertSettings).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(expiryService.updateAlertSettings).mockRejectedValue(new Error('DB Error'));
|
||||||
|
|
||||||
const response = await supertest(app).put('/api/inventory/alerts/email').send({
|
const response = await supertest(app).put('/api/v1/inventory/alerts/email').send({
|
||||||
is_enabled: false,
|
is_enabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -619,7 +619,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
mockResult as any,
|
mockResult as any,
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory/recipes/suggestions');
|
const response = await supertest(app).get('/api/v1/inventory/recipes/suggestions');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.recipes).toHaveLength(1);
|
expect(response.body.data.recipes).toHaveLength(1);
|
||||||
@@ -634,7 +634,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const response = await supertest(app).get(
|
const response = await supertest(app).get(
|
||||||
'/api/inventory/recipes/suggestions?days=14&limit=5&offset=10',
|
'/api/v1/inventory/recipes/suggestions?days=14&limit=5&offset=10',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -647,7 +647,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid days parameter', async () => {
|
it('should return 400 for invalid days parameter', async () => {
|
||||||
const response = await supertest(app).get('/api/inventory/recipes/suggestions?days=100');
|
const response = await supertest(app).get('/api/v1/inventory/recipes/suggestions?days=100');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -657,7 +657,7 @@ describe('Inventory Routes (/api/inventory)', () => {
|
|||||||
new Error('DB Error'),
|
new Error('DB Error'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/inventory/recipes/suggestions');
|
const response = await supertest(app).get('/api/v1/inventory/recipes/suggestions');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ vi.mock('../services/logger.server', async () => ({
|
|||||||
logger: (await import('../tests/utils/mockLogger')).mockLogger,
|
logger: (await import('../tests/utils/mockLogger')).mockLogger,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('Personalization Routes (/api/personalization)', () => {
|
describe('Personalization Routes (/api/v1/personalization)', () => {
|
||||||
const app = createTestApp({ router: personalizationRouter, basePath: '/api/personalization' });
|
const app = createTestApp({ router: personalizationRouter, basePath: '/api/v1/personalization' });
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -44,7 +44,7 @@ describe('Personalization Routes (/api/personalization)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.get('/api/personalization/master-items')
|
.get('/api/v1/personalization/master-items')
|
||||||
.set('x-test-rate-limit-enable', 'true');
|
.set('x-test-rate-limit-enable', 'true');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -55,13 +55,13 @@ describe('Personalization Routes (/api/personalization)', () => {
|
|||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(db.personalizationRepo.getAllMasterItems).mockRejectedValue(dbError);
|
vi.mocked(db.personalizationRepo.getAllMasterItems).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.get('/api/personalization/master-items')
|
.get('/api/v1/personalization/master-items')
|
||||||
.set('x-test-rate-limit-enable', 'true');
|
.set('x-test-rate-limit-enable', 'true');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
'Error fetching master items in /api/personalization/master-items:',
|
'Error fetching master items in /api/v1/personalization/master-items:',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -71,7 +71,7 @@ describe('Personalization Routes (/api/personalization)', () => {
|
|||||||
const mockRestrictions = [createMockDietaryRestriction({ name: 'Gluten-Free' })];
|
const mockRestrictions = [createMockDietaryRestriction({ name: 'Gluten-Free' })];
|
||||||
vi.mocked(db.personalizationRepo.getDietaryRestrictions).mockResolvedValue(mockRestrictions);
|
vi.mocked(db.personalizationRepo.getDietaryRestrictions).mockResolvedValue(mockRestrictions);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/personalization/dietary-restrictions');
|
const response = await supertest(app).get('/api/v1/personalization/dietary-restrictions');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockRestrictions);
|
expect(response.body.data).toEqual(mockRestrictions);
|
||||||
@@ -80,12 +80,12 @@ describe('Personalization Routes (/api/personalization)', () => {
|
|||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(db.personalizationRepo.getDietaryRestrictions).mockRejectedValue(dbError);
|
vi.mocked(db.personalizationRepo.getDietaryRestrictions).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/personalization/dietary-restrictions');
|
const response = await supertest(app).get('/api/v1/personalization/dietary-restrictions');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
'Error fetching dietary restrictions in /api/personalization/dietary-restrictions:',
|
'Error fetching dietary restrictions in /api/v1/personalization/dietary-restrictions:',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -95,7 +95,7 @@ describe('Personalization Routes (/api/personalization)', () => {
|
|||||||
const mockAppliances = [createMockAppliance({ name: 'Air Fryer' })];
|
const mockAppliances = [createMockAppliance({ name: 'Air Fryer' })];
|
||||||
vi.mocked(db.personalizationRepo.getAppliances).mockResolvedValue(mockAppliances);
|
vi.mocked(db.personalizationRepo.getAppliances).mockResolvedValue(mockAppliances);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/personalization/appliances');
|
const response = await supertest(app).get('/api/v1/personalization/appliances');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockAppliances);
|
expect(response.body.data).toEqual(mockAppliances);
|
||||||
@@ -104,12 +104,12 @@ describe('Personalization Routes (/api/personalization)', () => {
|
|||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(db.personalizationRepo.getAppliances).mockRejectedValue(dbError);
|
vi.mocked(db.personalizationRepo.getAppliances).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/personalization/appliances');
|
const response = await supertest(app).get('/api/v1/personalization/appliances');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
'Error fetching appliances in /api/personalization/appliances:',
|
'Error fetching appliances in /api/v1/personalization/appliances:',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -121,7 +121,7 @@ describe('Personalization Routes (/api/personalization)', () => {
|
|||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.get('/api/personalization/master-items')
|
.get('/api/v1/personalization/master-items')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ vi.mock('../config/passport', () => ({
|
|||||||
import priceRouter from './price.routes';
|
import priceRouter from './price.routes';
|
||||||
import { priceRepo } from '../services/db/price.db';
|
import { priceRepo } from '../services/db/price.db';
|
||||||
|
|
||||||
describe('Price Routes (/api/price-history)', () => {
|
describe('Price Routes (/api/v1/price-history)', () => {
|
||||||
const mockUser = createMockUserProfile({ user: { user_id: 'price-user-123' } });
|
const mockUser = createMockUserProfile({ user: { user_id: 'price-user-123' } });
|
||||||
const app = createTestApp({
|
const app = createTestApp({
|
||||||
router: priceRouter,
|
router: priceRouter,
|
||||||
basePath: '/api/price-history',
|
basePath: '/api/v1/price-history',
|
||||||
authenticatedUser: mockUser,
|
authenticatedUser: mockUser,
|
||||||
});
|
});
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -57,7 +57,7 @@ describe('Price Routes (/api/price-history)', () => {
|
|||||||
vi.mocked(priceRepo.getPriceHistory).mockResolvedValue(mockHistory);
|
vi.mocked(priceRepo.getPriceHistory).mockResolvedValue(mockHistory);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/price-history')
|
.post('/api/v1/price-history')
|
||||||
.send({ masterItemIds: [1, 2] });
|
.send({ masterItemIds: [1, 2] });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -68,7 +68,7 @@ describe('Price Routes (/api/price-history)', () => {
|
|||||||
it('should pass limit and offset from the body to the repository', async () => {
|
it('should pass limit and offset from the body to the repository', async () => {
|
||||||
vi.mocked(priceRepo.getPriceHistory).mockResolvedValue([]);
|
vi.mocked(priceRepo.getPriceHistory).mockResolvedValue([]);
|
||||||
await supertest(app)
|
await supertest(app)
|
||||||
.post('/api/price-history')
|
.post('/api/v1/price-history')
|
||||||
.send({ masterItemIds: [1, 2, 3], limit: 50, offset: 10 });
|
.send({ masterItemIds: [1, 2, 3], limit: 50, offset: 10 });
|
||||||
|
|
||||||
expect(priceRepo.getPriceHistory).toHaveBeenCalledWith([1, 2, 3], expect.any(Object), 50, 10);
|
expect(priceRepo.getPriceHistory).toHaveBeenCalledWith([1, 2, 3], expect.any(Object), 50, 10);
|
||||||
@@ -77,7 +77,7 @@ describe('Price Routes (/api/price-history)', () => {
|
|||||||
it('should log the request info', async () => {
|
it('should log the request info', async () => {
|
||||||
vi.mocked(priceRepo.getPriceHistory).mockResolvedValue([]);
|
vi.mocked(priceRepo.getPriceHistory).mockResolvedValue([]);
|
||||||
await supertest(app)
|
await supertest(app)
|
||||||
.post('/api/price-history')
|
.post('/api/v1/price-history')
|
||||||
.send({ masterItemIds: [1, 2, 3], limit: 25, offset: 5 });
|
.send({ masterItemIds: [1, 2, 3], limit: 25, offset: 5 });
|
||||||
|
|
||||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||||
@@ -91,7 +91,7 @@ describe('Price Routes (/api/price-history)', () => {
|
|||||||
vi.mocked(priceRepo.getPriceHistory).mockRejectedValue(dbError);
|
vi.mocked(priceRepo.getPriceHistory).mockRejectedValue(dbError);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/price-history')
|
.post('/api/v1/price-history')
|
||||||
.send({ masterItemIds: [1, 2, 3] });
|
.send({ masterItemIds: [1, 2, 3] });
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -99,7 +99,7 @@ describe('Price Routes (/api/price-history)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if masterItemIds is an empty array', async () => {
|
it('should return 400 if masterItemIds is an empty array', async () => {
|
||||||
const response = await supertest(app).post('/api/price-history').send({ masterItemIds: [] });
|
const response = await supertest(app).post('/api/v1/price-history').send({ masterItemIds: [] });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toBe(
|
expect(response.body.error.details[0].message).toBe(
|
||||||
@@ -109,7 +109,7 @@ describe('Price Routes (/api/price-history)', () => {
|
|||||||
|
|
||||||
it('should return 400 if masterItemIds is not an array', async () => {
|
it('should return 400 if masterItemIds is not an array', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/price-history')
|
.post('/api/v1/price-history')
|
||||||
.send({ masterItemIds: 'not-an-array' });
|
.send({ masterItemIds: 'not-an-array' });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -121,7 +121,7 @@ describe('Price Routes (/api/price-history)', () => {
|
|||||||
|
|
||||||
it('should return 400 if masterItemIds contains non-positive integers', async () => {
|
it('should return 400 if masterItemIds contains non-positive integers', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/price-history')
|
.post('/api/v1/price-history')
|
||||||
.send({ masterItemIds: [1, -2, 3] });
|
.send({ masterItemIds: [1, -2, 3] });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -129,7 +129,7 @@ describe('Price Routes (/api/price-history)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if masterItemIds is missing', async () => {
|
it('should return 400 if masterItemIds is missing', async () => {
|
||||||
const response = await supertest(app).post('/api/price-history').send({});
|
const response = await supertest(app).post('/api/v1/price-history').send({});
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
// The actual message is "Invalid input: expected array, received undefined"
|
// The actual message is "Invalid input: expected array, received undefined"
|
||||||
@@ -140,7 +140,7 @@ describe('Price Routes (/api/price-history)', () => {
|
|||||||
|
|
||||||
it('should return 400 for invalid limit and offset', async () => {
|
it('should return 400 for invalid limit and offset', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/price-history')
|
.post('/api/v1/price-history')
|
||||||
.send({ masterItemIds: [1], limit: -1, offset: 'abc' });
|
.send({ masterItemIds: [1], limit: -1, offset: 'abc' });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -157,7 +157,7 @@ describe('Price Routes (/api/price-history)', () => {
|
|||||||
it('should apply priceHistoryLimiter to POST /', async () => {
|
it('should apply priceHistoryLimiter to POST /', async () => {
|
||||||
vi.mocked(priceRepo.getPriceHistory).mockResolvedValue([]);
|
vi.mocked(priceRepo.getPriceHistory).mockResolvedValue([]);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/price-history')
|
.post('/api/v1/price-history')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ masterItemIds: [1, 2] });
|
.send({ masterItemIds: [1, 2] });
|
||||||
|
|
||||||
|
|||||||
@@ -42,13 +42,13 @@ const expectLogger = expect.objectContaining({
|
|||||||
error: expect.any(Function),
|
error: expect.any(Function),
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Reaction Routes (/api/reactions)', () => {
|
describe('Reaction Routes (/api/v1/reactions)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /', () => {
|
describe('GET /', () => {
|
||||||
const app = createTestApp({ router: reactionsRouter, basePath: '/api/reactions' });
|
const app = createTestApp({ router: reactionsRouter, basePath: '/api/v1/reactions' });
|
||||||
|
|
||||||
it('should return a list of reactions', async () => {
|
it('should return a list of reactions', async () => {
|
||||||
const mockReactions = [
|
const mockReactions = [
|
||||||
@@ -56,7 +56,7 @@ describe('Reaction Routes (/api/reactions)', () => {
|
|||||||
] as unknown as UserReaction[];
|
] as unknown as UserReaction[];
|
||||||
vi.mocked(reactionRepo.getReactions).mockResolvedValue(mockReactions);
|
vi.mocked(reactionRepo.getReactions).mockResolvedValue(mockReactions);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/reactions');
|
const response = await supertest(app).get('/api/v1/reactions');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockReactions);
|
expect(response.body.data).toEqual(mockReactions);
|
||||||
@@ -72,7 +72,7 @@ describe('Reaction Routes (/api/reactions)', () => {
|
|||||||
const validUuid = '123e4567-e89b-12d3-a456-426614174000';
|
const validUuid = '123e4567-e89b-12d3-a456-426614174000';
|
||||||
const query = { userId: validUuid, entityType: 'recipe', entityId: '1' };
|
const query = { userId: validUuid, entityType: 'recipe', entityId: '1' };
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/reactions').query(query);
|
const response = await supertest(app).get('/api/v1/reactions').query(query);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(reactionRepo.getReactions).toHaveBeenCalledWith(
|
expect(reactionRepo.getReactions).toHaveBeenCalledWith(
|
||||||
@@ -85,7 +85,7 @@ describe('Reaction Routes (/api/reactions)', () => {
|
|||||||
const error = new Error('DB Error');
|
const error = new Error('DB Error');
|
||||||
vi.mocked(reactionRepo.getReactions).mockRejectedValue(error);
|
vi.mocked(reactionRepo.getReactions).mockRejectedValue(error);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/reactions');
|
const response = await supertest(app).get('/api/v1/reactions');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith({ error }, 'Error fetching user reactions');
|
expect(mockLogger.error).toHaveBeenCalledWith({ error }, 'Error fetching user reactions');
|
||||||
@@ -93,7 +93,7 @@ describe('Reaction Routes (/api/reactions)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /summary', () => {
|
describe('GET /summary', () => {
|
||||||
const app = createTestApp({ router: reactionsRouter, basePath: '/api/reactions' });
|
const app = createTestApp({ router: reactionsRouter, basePath: '/api/v1/reactions' });
|
||||||
|
|
||||||
it('should return reaction summary for an entity', async () => {
|
it('should return reaction summary for an entity', async () => {
|
||||||
const mockSummary = [
|
const mockSummary = [
|
||||||
@@ -103,7 +103,7 @@ describe('Reaction Routes (/api/reactions)', () => {
|
|||||||
vi.mocked(reactionRepo.getReactionSummary).mockResolvedValue(mockSummary);
|
vi.mocked(reactionRepo.getReactionSummary).mockResolvedValue(mockSummary);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.get('/api/reactions/summary')
|
.get('/api/v1/reactions/summary')
|
||||||
.query({ entityType: 'recipe', entityId: '123' });
|
.query({ entityType: 'recipe', entityId: '123' });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -112,7 +112,7 @@ describe('Reaction Routes (/api/reactions)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if required parameters are missing', async () => {
|
it('should return 400 if required parameters are missing', async () => {
|
||||||
const response = await supertest(app).get('/api/reactions/summary');
|
const response = await supertest(app).get('/api/v1/reactions/summary');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toContain('required');
|
expect(response.body.error.details[0].message).toContain('required');
|
||||||
});
|
});
|
||||||
@@ -122,7 +122,7 @@ describe('Reaction Routes (/api/reactions)', () => {
|
|||||||
vi.mocked(reactionRepo.getReactionSummary).mockRejectedValue(error);
|
vi.mocked(reactionRepo.getReactionSummary).mockRejectedValue(error);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.get('/api/reactions/summary')
|
.get('/api/v1/reactions/summary')
|
||||||
.query({ entityType: 'recipe', entityId: '123' });
|
.query({ entityType: 'recipe', entityId: '123' });
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -134,7 +134,7 @@ describe('Reaction Routes (/api/reactions)', () => {
|
|||||||
const mockUser = createMockUserProfile({ user: { user_id: 'user-123' } });
|
const mockUser = createMockUserProfile({ user: { user_id: 'user-123' } });
|
||||||
const app = createTestApp({
|
const app = createTestApp({
|
||||||
router: reactionsRouter,
|
router: reactionsRouter,
|
||||||
basePath: '/api/reactions',
|
basePath: '/api/v1/reactions',
|
||||||
authenticatedUser: mockUser,
|
authenticatedUser: mockUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ describe('Reaction Routes (/api/reactions)', () => {
|
|||||||
} as unknown as UserReaction;
|
} as unknown as UserReaction;
|
||||||
vi.mocked(reactionRepo.toggleReaction).mockResolvedValue(mockResult);
|
vi.mocked(reactionRepo.toggleReaction).mockResolvedValue(mockResult);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/reactions/toggle').send(validBody);
|
const response = await supertest(app).post('/api/v1/reactions/toggle').send(validBody);
|
||||||
|
|
||||||
expect(response.status).toBe(201);
|
expect(response.status).toBe(201);
|
||||||
expect(response.body.data).toEqual({ message: 'Reaction added.', reaction: mockResult });
|
expect(response.body.data).toEqual({ message: 'Reaction added.', reaction: mockResult });
|
||||||
@@ -166,7 +166,7 @@ describe('Reaction Routes (/api/reactions)', () => {
|
|||||||
// Returning null/false from toggleReaction implies the reaction was removed
|
// Returning null/false from toggleReaction implies the reaction was removed
|
||||||
vi.mocked(reactionRepo.toggleReaction).mockResolvedValue(null);
|
vi.mocked(reactionRepo.toggleReaction).mockResolvedValue(null);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/reactions/toggle').send(validBody);
|
const response = await supertest(app).post('/api/v1/reactions/toggle').send(validBody);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual({ message: 'Reaction removed.' });
|
expect(response.body.data).toEqual({ message: 'Reaction removed.' });
|
||||||
@@ -174,7 +174,7 @@ describe('Reaction Routes (/api/reactions)', () => {
|
|||||||
|
|
||||||
it('should return 400 if body is invalid', async () => {
|
it('should return 400 if body is invalid', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/reactions/toggle')
|
.post('/api/v1/reactions/toggle')
|
||||||
.send({ entity_type: 'recipe' }); // Missing other required fields
|
.send({ entity_type: 'recipe' }); // Missing other required fields
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -182,8 +182,8 @@ describe('Reaction Routes (/api/reactions)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 401 if not authenticated', async () => {
|
it('should return 401 if not authenticated', async () => {
|
||||||
const unauthApp = createTestApp({ router: reactionsRouter, basePath: '/api/reactions' });
|
const unauthApp = createTestApp({ router: reactionsRouter, basePath: '/api/v1/reactions' });
|
||||||
const response = await supertest(unauthApp).post('/api/reactions/toggle').send(validBody);
|
const response = await supertest(unauthApp).post('/api/v1/reactions/toggle').send(validBody);
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
});
|
});
|
||||||
@@ -192,7 +192,7 @@ describe('Reaction Routes (/api/reactions)', () => {
|
|||||||
const error = new Error('DB Error');
|
const error = new Error('DB Error');
|
||||||
vi.mocked(reactionRepo.toggleReaction).mockRejectedValue(error);
|
vi.mocked(reactionRepo.toggleReaction).mockRejectedValue(error);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/reactions/toggle').send(validBody);
|
const response = await supertest(app).post('/api/v1/reactions/toggle').send(validBody);
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
@@ -204,10 +204,10 @@ describe('Reaction Routes (/api/reactions)', () => {
|
|||||||
|
|
||||||
describe('Rate Limiting', () => {
|
describe('Rate Limiting', () => {
|
||||||
it('should apply publicReadLimiter to GET /', async () => {
|
it('should apply publicReadLimiter to GET /', async () => {
|
||||||
const app = createTestApp({ router: reactionsRouter, basePath: '/api/reactions' });
|
const app = createTestApp({ router: reactionsRouter, basePath: '/api/v1/reactions' });
|
||||||
vi.mocked(reactionRepo.getReactions).mockResolvedValue([]);
|
vi.mocked(reactionRepo.getReactions).mockResolvedValue([]);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.get('/api/reactions')
|
.get('/api/v1/reactions')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -218,13 +218,13 @@ describe('Reaction Routes (/api/reactions)', () => {
|
|||||||
const mockUser = createMockUserProfile({ user: { user_id: 'user-123' } });
|
const mockUser = createMockUserProfile({ user: { user_id: 'user-123' } });
|
||||||
const app = createTestApp({
|
const app = createTestApp({
|
||||||
router: reactionsRouter,
|
router: reactionsRouter,
|
||||||
basePath: '/api/reactions',
|
basePath: '/api/v1/reactions',
|
||||||
authenticatedUser: mockUser,
|
authenticatedUser: mockUser,
|
||||||
});
|
});
|
||||||
vi.mocked(reactionRepo.toggleReaction).mockResolvedValue(null);
|
vi.mocked(reactionRepo.toggleReaction).mockResolvedValue(null);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/reactions/toggle')
|
.post('/api/v1/reactions/toggle')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ entity_type: 'recipe', entity_id: '1', reaction_type: 'like' });
|
.send({ entity_type: 'recipe', entity_id: '1', reaction_type: 'like' });
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ const expectLogger = expect.objectContaining({
|
|||||||
error: expect.any(Function),
|
error: expect.any(Function),
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Recipe Routes (/api/recipes)', () => {
|
describe('Recipe Routes (/api/v1/recipes)', () => {
|
||||||
const app = createTestApp({ router: recipeRouter, basePath: '/api/recipes' });
|
const app = createTestApp({ router: recipeRouter, basePath: '/api/v1/recipes' });
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -70,7 +70,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
const mockRecipes = [createMockRecipe({ recipe_id: 1, name: 'Pasta' })];
|
const mockRecipes = [createMockRecipe({ recipe_id: 1, name: 'Pasta' })];
|
||||||
vi.mocked(db.recipeRepo.getRecipesBySalePercentage).mockResolvedValue(mockRecipes);
|
vi.mocked(db.recipeRepo.getRecipesBySalePercentage).mockResolvedValue(mockRecipes);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/recipes/by-sale-percentage?minPercentage=75');
|
const response = await supertest(app).get('/api/v1/recipes/by-sale-percentage?minPercentage=75');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockRecipes);
|
expect(response.body.data).toEqual(mockRecipes);
|
||||||
@@ -79,25 +79,25 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
|
|
||||||
it('should use the default minPercentage of 50 when none is provided', async () => {
|
it('should use the default minPercentage of 50 when none is provided', async () => {
|
||||||
vi.mocked(db.recipeRepo.getRecipesBySalePercentage).mockResolvedValue([]);
|
vi.mocked(db.recipeRepo.getRecipesBySalePercentage).mockResolvedValue([]);
|
||||||
await supertest(app).get('/api/recipes/by-sale-percentage');
|
await supertest(app).get('/api/v1/recipes/by-sale-percentage');
|
||||||
expect(db.recipeRepo.getRecipesBySalePercentage).toHaveBeenCalledWith(50, expectLogger);
|
expect(db.recipeRepo.getRecipesBySalePercentage).toHaveBeenCalledWith(50, expectLogger);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(db.recipeRepo.getRecipesBySalePercentage).mockRejectedValue(dbError);
|
vi.mocked(db.recipeRepo.getRecipesBySalePercentage).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/recipes/by-sale-percentage');
|
const response = await supertest(app).get('/api/v1/recipes/by-sale-percentage');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
'Error fetching recipes in /api/recipes/by-sale-percentage:',
|
'Error fetching recipes in /api/v1/recipes/by-sale-percentage:',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for an invalid minPercentage', async () => {
|
it('should return 400 for an invalid minPercentage', async () => {
|
||||||
const response = await supertest(app).get(
|
const response = await supertest(app).get(
|
||||||
'/api/recipes/by-sale-percentage?minPercentage=101',
|
'/api/v1/recipes/by-sale-percentage?minPercentage=101',
|
||||||
);
|
);
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toContain('Too big');
|
expect(response.body.error.details[0].message).toContain('Too big');
|
||||||
@@ -107,32 +107,32 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
describe('GET /by-sale-ingredients', () => {
|
describe('GET /by-sale-ingredients', () => {
|
||||||
it('should return recipes with default minIngredients', async () => {
|
it('should return recipes with default minIngredients', async () => {
|
||||||
vi.mocked(db.recipeRepo.getRecipesByMinSaleIngredients).mockResolvedValue([]);
|
vi.mocked(db.recipeRepo.getRecipesByMinSaleIngredients).mockResolvedValue([]);
|
||||||
const response = await supertest(app).get('/api/recipes/by-sale-ingredients');
|
const response = await supertest(app).get('/api/v1/recipes/by-sale-ingredients');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(db.recipeRepo.getRecipesByMinSaleIngredients).toHaveBeenCalledWith(3, expectLogger);
|
expect(db.recipeRepo.getRecipesByMinSaleIngredients).toHaveBeenCalledWith(3, expectLogger);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use provided minIngredients query parameter', async () => {
|
it('should use provided minIngredients query parameter', async () => {
|
||||||
vi.mocked(db.recipeRepo.getRecipesByMinSaleIngredients).mockResolvedValue([]);
|
vi.mocked(db.recipeRepo.getRecipesByMinSaleIngredients).mockResolvedValue([]);
|
||||||
await supertest(app).get('/api/recipes/by-sale-ingredients?minIngredients=5');
|
await supertest(app).get('/api/v1/recipes/by-sale-ingredients?minIngredients=5');
|
||||||
expect(db.recipeRepo.getRecipesByMinSaleIngredients).toHaveBeenCalledWith(5, expectLogger);
|
expect(db.recipeRepo.getRecipesByMinSaleIngredients).toHaveBeenCalledWith(5, expectLogger);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(db.recipeRepo.getRecipesByMinSaleIngredients).mockRejectedValue(dbError);
|
vi.mocked(db.recipeRepo.getRecipesByMinSaleIngredients).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/recipes/by-sale-ingredients');
|
const response = await supertest(app).get('/api/v1/recipes/by-sale-ingredients');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
'Error fetching recipes in /api/recipes/by-sale-ingredients:',
|
'Error fetching recipes in /api/v1/recipes/by-sale-ingredients:',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for an invalid minIngredients', async () => {
|
it('should return 400 for an invalid minIngredients', async () => {
|
||||||
const response = await supertest(app).get(
|
const response = await supertest(app).get(
|
||||||
'/api/recipes/by-sale-ingredients?minIngredients=abc',
|
'/api/v1/recipes/by-sale-ingredients?minIngredients=abc',
|
||||||
);
|
);
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toContain('received NaN');
|
expect(response.body.error.details[0].message).toContain('received NaN');
|
||||||
@@ -145,7 +145,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
vi.mocked(db.recipeRepo.findRecipesByIngredientAndTag).mockResolvedValue(mockRecipes);
|
vi.mocked(db.recipeRepo.findRecipesByIngredientAndTag).mockResolvedValue(mockRecipes);
|
||||||
|
|
||||||
const response = await supertest(app).get(
|
const response = await supertest(app).get(
|
||||||
'/api/recipes/by-ingredient-and-tag?ingredient=chicken&tag=quick',
|
'/api/v1/recipes/by-ingredient-and-tag?ingredient=chicken&tag=quick',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -156,19 +156,19 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(db.recipeRepo.findRecipesByIngredientAndTag).mockRejectedValue(dbError);
|
vi.mocked(db.recipeRepo.findRecipesByIngredientAndTag).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get(
|
const response = await supertest(app).get(
|
||||||
'/api/recipes/by-ingredient-and-tag?ingredient=chicken&tag=quick',
|
'/api/v1/recipes/by-ingredient-and-tag?ingredient=chicken&tag=quick',
|
||||||
);
|
);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
'Error fetching recipes in /api/recipes/by-ingredient-and-tag:',
|
'Error fetching recipes in /api/v1/recipes/by-ingredient-and-tag:',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if required query parameters are missing', async () => {
|
it('should return 400 if required query parameters are missing', async () => {
|
||||||
const response = await supertest(app).get(
|
const response = await supertest(app).get(
|
||||||
'/api/recipes/by-ingredient-and-tag?ingredient=chicken',
|
'/api/v1/recipes/by-ingredient-and-tag?ingredient=chicken',
|
||||||
);
|
);
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toBe('Query parameter "tag" is required.');
|
expect(response.body.error.details[0].message).toBe('Query parameter "tag" is required.');
|
||||||
@@ -180,7 +180,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
const mockComments = [createMockRecipeComment({ recipe_id: 1, content: 'Great recipe!' })];
|
const mockComments = [createMockRecipeComment({ recipe_id: 1, content: 'Great recipe!' })];
|
||||||
vi.mocked(db.recipeRepo.getRecipeComments).mockResolvedValue(mockComments);
|
vi.mocked(db.recipeRepo.getRecipeComments).mockResolvedValue(mockComments);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/recipes/1/comments');
|
const response = await supertest(app).get('/api/v1/recipes/1/comments');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockComments);
|
expect(response.body.data).toEqual(mockComments);
|
||||||
@@ -189,14 +189,14 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
|
|
||||||
it('should return an empty array if recipe has no comments', async () => {
|
it('should return an empty array if recipe has no comments', async () => {
|
||||||
vi.mocked(db.recipeRepo.getRecipeComments).mockResolvedValue([]);
|
vi.mocked(db.recipeRepo.getRecipeComments).mockResolvedValue([]);
|
||||||
const response = await supertest(app).get('/api/recipes/2/comments');
|
const response = await supertest(app).get('/api/v1/recipes/2/comments');
|
||||||
expect(response.body.data).toEqual([]);
|
expect(response.body.data).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(db.recipeRepo.getRecipeComments).mockRejectedValue(dbError);
|
vi.mocked(db.recipeRepo.getRecipeComments).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/recipes/1/comments');
|
const response = await supertest(app).get('/api/v1/recipes/1/comments');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
@@ -206,7 +206,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for an invalid recipeId', async () => {
|
it('should return 400 for an invalid recipeId', async () => {
|
||||||
const response = await supertest(app).get('/api/recipes/abc/comments');
|
const response = await supertest(app).get('/api/v1/recipes/abc/comments');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toContain('received NaN');
|
expect(response.body.error.details[0].message).toContain('received NaN');
|
||||||
});
|
});
|
||||||
@@ -217,7 +217,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
const mockRecipe = createMockRecipe({ recipe_id: 456, name: 'Specific Recipe' });
|
const mockRecipe = createMockRecipe({ recipe_id: 456, name: 'Specific Recipe' });
|
||||||
vi.mocked(db.recipeRepo.getRecipeById).mockResolvedValue(mockRecipe);
|
vi.mocked(db.recipeRepo.getRecipeById).mockResolvedValue(mockRecipe);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/recipes/456');
|
const response = await supertest(app).get('/api/v1/recipes/456');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockRecipe);
|
expect(response.body.data).toEqual(mockRecipe);
|
||||||
@@ -227,7 +227,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
it('should return 404 if the recipe is not found', async () => {
|
it('should return 404 if the recipe is not found', async () => {
|
||||||
const notFoundError = new NotFoundError('Recipe not found');
|
const notFoundError = new NotFoundError('Recipe not found');
|
||||||
vi.mocked(db.recipeRepo.getRecipeById).mockRejectedValue(notFoundError);
|
vi.mocked(db.recipeRepo.getRecipeById).mockRejectedValue(notFoundError);
|
||||||
const response = await supertest(app).get('/api/recipes/999');
|
const response = await supertest(app).get('/api/v1/recipes/999');
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toContain('not found');
|
expect(response.body.error.message).toContain('not found');
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
@@ -239,7 +239,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(db.recipeRepo.getRecipeById).mockRejectedValue(dbError);
|
vi.mocked(db.recipeRepo.getRecipeById).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/recipes/456');
|
const response = await supertest(app).get('/api/v1/recipes/456');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
@@ -249,7 +249,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for an invalid recipeId', async () => {
|
it('should return 400 for an invalid recipeId', async () => {
|
||||||
const response = await supertest(app).get('/api/recipes/abc');
|
const response = await supertest(app).get('/api/v1/recipes/abc');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toContain('received NaN');
|
expect(response.body.error.details[0].message).toContain('received NaN');
|
||||||
});
|
});
|
||||||
@@ -259,7 +259,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
const mockUser = createMockUserProfile({ user: { user_id: 'user-123' } });
|
const mockUser = createMockUserProfile({ user: { user_id: 'user-123' } });
|
||||||
const authApp = createTestApp({
|
const authApp = createTestApp({
|
||||||
router: recipeRouter,
|
router: recipeRouter,
|
||||||
basePath: '/api/recipes',
|
basePath: '/api/v1/recipes',
|
||||||
authenticatedUser: mockUser,
|
authenticatedUser: mockUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -268,7 +268,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
const mockSuggestion = 'Chicken and Rice Casserole...';
|
const mockSuggestion = 'Chicken and Rice Casserole...';
|
||||||
vi.mocked(aiService.generateRecipeSuggestion).mockResolvedValue(mockSuggestion);
|
vi.mocked(aiService.generateRecipeSuggestion).mockResolvedValue(mockSuggestion);
|
||||||
|
|
||||||
const response = await supertest(authApp).post('/api/recipes/suggest').send({ ingredients });
|
const response = await supertest(authApp).post('/api/v1/recipes/suggest').send({ ingredients });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual({ suggestion: mockSuggestion });
|
expect(response.body.data).toEqual({ suggestion: mockSuggestion });
|
||||||
@@ -279,7 +279,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
vi.mocked(aiService.generateRecipeSuggestion).mockResolvedValue(null);
|
vi.mocked(aiService.generateRecipeSuggestion).mockResolvedValue(null);
|
||||||
|
|
||||||
const response = await supertest(authApp)
|
const response = await supertest(authApp)
|
||||||
.post('/api/recipes/suggest')
|
.post('/api/v1/recipes/suggest')
|
||||||
.send({ ingredients: ['water'] });
|
.send({ ingredients: ['water'] });
|
||||||
|
|
||||||
expect(response.status).toBe(503);
|
expect(response.status).toBe(503);
|
||||||
@@ -288,7 +288,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
|
|
||||||
it('should return 400 if ingredients list is empty', async () => {
|
it('should return 400 if ingredients list is empty', async () => {
|
||||||
const response = await supertest(authApp)
|
const response = await supertest(authApp)
|
||||||
.post('/api/recipes/suggest')
|
.post('/api/v1/recipes/suggest')
|
||||||
.send({ ingredients: [] });
|
.send({ ingredients: [] });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -298,9 +298,9 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 401 if not authenticated', async () => {
|
it('should return 401 if not authenticated', async () => {
|
||||||
const unauthApp = createTestApp({ router: recipeRouter, basePath: '/api/recipes' });
|
const unauthApp = createTestApp({ router: recipeRouter, basePath: '/api/v1/recipes' });
|
||||||
const response = await supertest(unauthApp)
|
const response = await supertest(unauthApp)
|
||||||
.post('/api/recipes/suggest')
|
.post('/api/v1/recipes/suggest')
|
||||||
.send({ ingredients: ['chicken'] });
|
.send({ ingredients: ['chicken'] });
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
@@ -311,7 +311,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
vi.mocked(aiService.generateRecipeSuggestion).mockRejectedValue(error);
|
vi.mocked(aiService.generateRecipeSuggestion).mockRejectedValue(error);
|
||||||
|
|
||||||
const response = await supertest(authApp)
|
const response = await supertest(authApp)
|
||||||
.post('/api/recipes/suggest')
|
.post('/api/v1/recipes/suggest')
|
||||||
.send({ ingredients: ['chicken'] });
|
.send({ ingredients: ['chicken'] });
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -326,7 +326,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
const mockUser = createMockUserProfile({ user: { user_id: 'rate-limit-user' } });
|
const mockUser = createMockUserProfile({ user: { user_id: 'rate-limit-user' } });
|
||||||
const authApp = createTestApp({
|
const authApp = createTestApp({
|
||||||
router: recipeRouter,
|
router: recipeRouter,
|
||||||
basePath: '/api/recipes',
|
basePath: '/api/v1/recipes',
|
||||||
authenticatedUser: mockUser,
|
authenticatedUser: mockUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -339,7 +339,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
// Act: Make maxRequests calls
|
// Act: Make maxRequests calls
|
||||||
for (let i = 0; i < maxRequests; i++) {
|
for (let i = 0; i < maxRequests; i++) {
|
||||||
const response = await supertest(authApp)
|
const response = await supertest(authApp)
|
||||||
.post('/api/recipes/suggest')
|
.post('/api/v1/recipes/suggest')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ ingredients });
|
.send({ ingredients });
|
||||||
expect(response.status).not.toBe(429);
|
expect(response.status).not.toBe(429);
|
||||||
@@ -347,7 +347,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
|
|
||||||
// Act: Make one more call
|
// Act: Make one more call
|
||||||
const blockedResponse = await supertest(authApp)
|
const blockedResponse = await supertest(authApp)
|
||||||
.post('/api/recipes/suggest')
|
.post('/api/v1/recipes/suggest')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ ingredients });
|
.send({ ingredients });
|
||||||
|
|
||||||
@@ -363,7 +363,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
|
|
||||||
for (let i = 0; i < maxRequests; i++) {
|
for (let i = 0; i < maxRequests; i++) {
|
||||||
const response = await supertest(authApp)
|
const response = await supertest(authApp)
|
||||||
.post('/api/recipes/suggest')
|
.post('/api/v1/recipes/suggest')
|
||||||
.send({ ingredients });
|
.send({ ingredients });
|
||||||
expect(response.status).not.toBe(429);
|
expect(response.status).not.toBe(429);
|
||||||
}
|
}
|
||||||
@@ -374,7 +374,7 @@ describe('Recipe Routes (/api/recipes)', () => {
|
|||||||
it('should apply publicReadLimiter to GET /:recipeId', async () => {
|
it('should apply publicReadLimiter to GET /:recipeId', async () => {
|
||||||
vi.mocked(db.recipeRepo.getRecipeById).mockResolvedValue(createMockRecipe({}));
|
vi.mocked(db.recipeRepo.getRecipeById).mockResolvedValue(createMockRecipe({}));
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.get('/api/recipes/1')
|
.get('/api/v1/recipes/1')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ const expectLogger = expect.objectContaining({
|
|||||||
error: expect.any(Function),
|
error: expect.any(Function),
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Stats Routes (/api/stats)', () => {
|
describe('Stats Routes (/api/v1/stats)', () => {
|
||||||
const app = createTestApp({ router: statsRouter, basePath: '/api/stats' });
|
const app = createTestApp({ router: statsRouter, basePath: '/api/v1/stats' });
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -36,31 +36,31 @@ describe('Stats Routes (/api/stats)', () => {
|
|||||||
describe('GET /most-frequent-sales', () => {
|
describe('GET /most-frequent-sales', () => {
|
||||||
it('should return most frequent sale items with default parameters', async () => {
|
it('should return most frequent sale items with default parameters', async () => {
|
||||||
vi.mocked(db.adminRepo.getMostFrequentSaleItems).mockResolvedValue([]);
|
vi.mocked(db.adminRepo.getMostFrequentSaleItems).mockResolvedValue([]);
|
||||||
const response = await supertest(app).get('/api/stats/most-frequent-sales');
|
const response = await supertest(app).get('/api/v1/stats/most-frequent-sales');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(db.adminRepo.getMostFrequentSaleItems).toHaveBeenCalledWith(30, 10, expectLogger);
|
expect(db.adminRepo.getMostFrequentSaleItems).toHaveBeenCalledWith(30, 10, expectLogger);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use provided query parameters', async () => {
|
it('should use provided query parameters', async () => {
|
||||||
vi.mocked(db.adminRepo.getMostFrequentSaleItems).mockResolvedValue([]);
|
vi.mocked(db.adminRepo.getMostFrequentSaleItems).mockResolvedValue([]);
|
||||||
await supertest(app).get('/api/stats/most-frequent-sales?days=90&limit=5');
|
await supertest(app).get('/api/v1/stats/most-frequent-sales?days=90&limit=5');
|
||||||
expect(db.adminRepo.getMostFrequentSaleItems).toHaveBeenCalledWith(90, 5, expectLogger);
|
expect(db.adminRepo.getMostFrequentSaleItems).toHaveBeenCalledWith(90, 5, expectLogger);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 500 if the database call fails', async () => {
|
it('should return 500 if the database call fails', async () => {
|
||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(db.adminRepo.getMostFrequentSaleItems).mockRejectedValue(dbError);
|
vi.mocked(db.adminRepo.getMostFrequentSaleItems).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/stats/most-frequent-sales');
|
const response = await supertest(app).get('/api/v1/stats/most-frequent-sales');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
'Error fetching most frequent sale items in /api/stats/most-frequent-sales:',
|
'Error fetching most frequent sale items in /api/v1/stats/most-frequent-sales:',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid query parameters', async () => {
|
it('should return 400 for invalid query parameters', async () => {
|
||||||
const response = await supertest(app).get('/api/stats/most-frequent-sales?days=0&limit=abc');
|
const response = await supertest(app).get('/api/v1/stats/most-frequent-sales?days=0&limit=abc');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details).toBeDefined();
|
expect(response.body.error.details).toBeDefined();
|
||||||
expect(response.body.error.details.length).toBe(2);
|
expect(response.body.error.details.length).toBe(2);
|
||||||
@@ -71,7 +71,7 @@ describe('Stats Routes (/api/stats)', () => {
|
|||||||
it('should apply publicReadLimiter to GET /most-frequent-sales', async () => {
|
it('should apply publicReadLimiter to GET /most-frequent-sales', async () => {
|
||||||
vi.mocked(db.adminRepo.getMostFrequentSaleItems).mockResolvedValue([]);
|
vi.mocked(db.adminRepo.getMostFrequentSaleItems).mockResolvedValue([]);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.get('/api/stats/most-frequent-sales')
|
.get('/api/v1/stats/most-frequent-sales')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true');
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
|
|||||||
@@ -112,12 +112,12 @@ const expectLogger = expect.objectContaining({
|
|||||||
error: expect.any(Function),
|
error: expect.any(Function),
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Store Routes (/api/stores)', () => {
|
describe('Store Routes (/api/v1/stores)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = createTestApp({ router: storeRouter, basePath: '/api/stores' });
|
const app = createTestApp({ router: storeRouter, basePath: '/api/v1/stores' });
|
||||||
|
|
||||||
describe('GET /', () => {
|
describe('GET /', () => {
|
||||||
it('should return all stores without locations by default', async () => {
|
it('should return all stores without locations by default', async () => {
|
||||||
@@ -142,7 +142,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
|
|
||||||
mockStoreRepoMethods.getAllStores.mockResolvedValue(mockStores);
|
mockStoreRepoMethods.getAllStores.mockResolvedValue(mockStores);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/stores');
|
const response = await supertest(app).get('/api/v1/stores');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockStores);
|
expect(response.body.data).toEqual(mockStores);
|
||||||
@@ -167,7 +167,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
mockStoresWithLocations,
|
mockStoresWithLocations,
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/stores?includeLocations=true');
|
const response = await supertest(app).get('/api/v1/stores?includeLocations=true');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockStoresWithLocations);
|
expect(response.body.data).toEqual(mockStoresWithLocations);
|
||||||
@@ -181,7 +181,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
mockStoreRepoMethods.getAllStores.mockRejectedValue(dbError);
|
mockStoreRepoMethods.getAllStores.mockRejectedValue(dbError);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/stores');
|
const response = await supertest(app).get('/api/v1/stores');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Error');
|
expect(response.body.error.message).toBe('DB Error');
|
||||||
@@ -223,7 +223,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
|
|
||||||
mockStoreLocationRepoMethods.getStoreWithLocations.mockResolvedValue(mockStore);
|
mockStoreLocationRepoMethods.getStoreWithLocations.mockResolvedValue(mockStore);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/stores/1');
|
const response = await supertest(app).get('/api/v1/stores/1');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockStore);
|
expect(response.body.data).toEqual(mockStore);
|
||||||
@@ -238,13 +238,13 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
new NotFoundError('Store with ID 999 not found.'),
|
new NotFoundError('Store with ID 999 not found.'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/stores/999');
|
const response = await supertest(app).get('/api/v1/stores/999');
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid store ID', async () => {
|
it('should return 400 for invalid store ID', async () => {
|
||||||
const response = await supertest(app).get('/api/stores/invalid');
|
const response = await supertest(app).get('/api/v1/stores/invalid');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -262,7 +262,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
|
|
||||||
mockStoreRepoMethods.createStore.mockResolvedValue(1);
|
mockStoreRepoMethods.createStore.mockResolvedValue(1);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/stores').send({
|
const response = await supertest(app).post('/api/v1/stores').send({
|
||||||
name: 'New Store',
|
name: 'New Store',
|
||||||
logo_url: 'https://example.com/logo.png',
|
logo_url: 'https://example.com/logo.png',
|
||||||
});
|
});
|
||||||
@@ -288,7 +288,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
mockStoreLocationRepoMethods.createStoreLocation.mockResolvedValue(1);
|
mockStoreLocationRepoMethods.createStoreLocation.mockResolvedValue(1);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/stores')
|
.post('/api/v1/stores')
|
||||||
.send({
|
.send({
|
||||||
name: 'New Store',
|
name: 'New Store',
|
||||||
address: {
|
address: {
|
||||||
@@ -316,7 +316,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
|
|
||||||
mockStoreRepoMethods.createStore.mockRejectedValue(new Error('DB Error'));
|
mockStoreRepoMethods.createStore.mockRejectedValue(new Error('DB Error'));
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/stores').send({
|
const response = await supertest(app).post('/api/v1/stores').send({
|
||||||
name: 'New Store',
|
name: 'New Store',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -326,7 +326,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid request body', async () => {
|
it('should return 400 for invalid request body', async () => {
|
||||||
const response = await supertest(app).post('/api/stores').send({});
|
const response = await supertest(app).post('/api/v1/stores').send({});
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -336,7 +336,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
it('should update a store', async () => {
|
it('should update a store', async () => {
|
||||||
mockStoreRepoMethods.updateStore.mockResolvedValue(undefined);
|
mockStoreRepoMethods.updateStore.mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app).put('/api/stores/1').send({
|
const response = await supertest(app).put('/api/v1/stores/1').send({
|
||||||
name: 'Updated Store Name',
|
name: 'Updated Store Name',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
new NotFoundError('Store with ID 999 not found.'),
|
new NotFoundError('Store with ID 999 not found.'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).put('/api/stores/999').send({
|
const response = await supertest(app).put('/api/v1/stores/999').send({
|
||||||
name: 'Updated Name',
|
name: 'Updated Name',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -362,7 +362,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
|
|
||||||
it('should return 400 for invalid request body', async () => {
|
it('should return 400 for invalid request body', async () => {
|
||||||
// Send invalid data: logo_url must be a valid URL
|
// Send invalid data: logo_url must be a valid URL
|
||||||
const response = await supertest(app).put('/api/stores/1').send({
|
const response = await supertest(app).put('/api/v1/stores/1').send({
|
||||||
logo_url: 'not-a-valid-url',
|
logo_url: 'not-a-valid-url',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -374,7 +374,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
it('should delete a store', async () => {
|
it('should delete a store', async () => {
|
||||||
mockStoreRepoMethods.deleteStore.mockResolvedValue(undefined);
|
mockStoreRepoMethods.deleteStore.mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app).delete('/api/stores/1');
|
const response = await supertest(app).delete('/api/v1/stores/1');
|
||||||
|
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
expect(mockStoreRepoMethods.deleteStore).toHaveBeenCalledWith(1, expectLogger);
|
expect(mockStoreRepoMethods.deleteStore).toHaveBeenCalledWith(1, expectLogger);
|
||||||
@@ -385,7 +385,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
new NotFoundError('Store with ID 999 not found.'),
|
new NotFoundError('Store with ID 999 not found.'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).delete('/api/stores/999');
|
const response = await supertest(app).delete('/api/v1/stores/999');
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
});
|
});
|
||||||
@@ -404,7 +404,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
mockAddressRepoMethods.upsertAddress.mockResolvedValue(1);
|
mockAddressRepoMethods.upsertAddress.mockResolvedValue(1);
|
||||||
mockStoreLocationRepoMethods.createStoreLocation.mockResolvedValue(1);
|
mockStoreLocationRepoMethods.createStoreLocation.mockResolvedValue(1);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/stores/1/locations').send({
|
const response = await supertest(app).post('/api/v1/stores/1/locations').send({
|
||||||
address_line_1: '456 New St',
|
address_line_1: '456 New St',
|
||||||
city: 'Vancouver',
|
city: 'Vancouver',
|
||||||
province_state: 'BC',
|
province_state: 'BC',
|
||||||
@@ -417,7 +417,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid request body', async () => {
|
it('should return 400 for invalid request body', async () => {
|
||||||
const response = await supertest(app).post('/api/stores/1/locations').send({});
|
const response = await supertest(app).post('/api/v1/stores/1/locations').send({});
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -427,7 +427,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
it('should delete a store location', async () => {
|
it('should delete a store location', async () => {
|
||||||
mockStoreLocationRepoMethods.deleteStoreLocation.mockResolvedValue(undefined);
|
mockStoreLocationRepoMethods.deleteStoreLocation.mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(app).delete('/api/stores/1/locations/1');
|
const response = await supertest(app).delete('/api/v1/stores/1/locations/1');
|
||||||
|
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
expect(mockStoreLocationRepoMethods.deleteStoreLocation).toHaveBeenCalledWith(
|
expect(mockStoreLocationRepoMethods.deleteStoreLocation).toHaveBeenCalledWith(
|
||||||
@@ -441,7 +441,7 @@ describe('Store Routes (/api/stores)', () => {
|
|||||||
new NotFoundError('Store location with ID 999 not found.'),
|
new NotFoundError('Store location with ID 999 not found.'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app).delete('/api/stores/1/locations/999');
|
const response = await supertest(app).delete('/api/v1/stores/1/locations/999');
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ import { systemService } from '../services/systemService';
|
|||||||
import systemRouter from './system.routes';
|
import systemRouter from './system.routes';
|
||||||
import { geocodingService } from '../services/geocodingService.server';
|
import { geocodingService } from '../services/geocodingService.server';
|
||||||
|
|
||||||
describe('System Routes (/api/system)', () => {
|
describe('System Routes (/api/v1/system)', () => {
|
||||||
const app = createTestApp({ router: systemRouter, basePath: '/api/system' });
|
const app = createTestApp({ router: systemRouter, basePath: '/api/v1/system' });
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -49,7 +49,7 @@ describe('System Routes (/api/system)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/system/pm2-status');
|
const response = await supertest(app).get('/api/v1/system/pm2-status');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -65,7 +65,7 @@ describe('System Routes (/api/system)', () => {
|
|||||||
message: 'Application process exists but is not online.',
|
message: 'Application process exists but is not online.',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/system/pm2-status');
|
const response = await supertest(app).get('/api/v1/system/pm2-status');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -80,7 +80,7 @@ describe('System Routes (/api/system)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/system/pm2-status');
|
const response = await supertest(app).get('/api/v1/system/pm2-status');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -97,7 +97,7 @@ describe('System Routes (/api/system)', () => {
|
|||||||
);
|
);
|
||||||
vi.mocked(systemService.getPm2Status).mockRejectedValue(serviceError);
|
vi.mocked(systemService.getPm2Status).mockRejectedValue(serviceError);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/system/pm2-status');
|
const response = await supertest(app).get('/api/v1/system/pm2-status');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe(serviceError.message);
|
expect(response.body.error.message).toBe(serviceError.message);
|
||||||
});
|
});
|
||||||
@@ -107,7 +107,7 @@ describe('System Routes (/api/system)', () => {
|
|||||||
vi.mocked(systemService.getPm2Status).mockRejectedValue(serviceError);
|
vi.mocked(systemService.getPm2Status).mockRejectedValue(serviceError);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).get('/api/system/pm2-status');
|
const response = await supertest(app).get('/api/v1/system/pm2-status');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -123,7 +123,7 @@ describe('System Routes (/api/system)', () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/system/geocode')
|
.post('/api/v1/system/geocode')
|
||||||
.send({ address: 'Victoria, BC' });
|
.send({ address: 'Victoria, BC' });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -134,7 +134,7 @@ describe('System Routes (/api/system)', () => {
|
|||||||
it('should return 404 if the address cannot be geocoded', async () => {
|
it('should return 404 if the address cannot be geocoded', async () => {
|
||||||
vi.mocked(geocodingService.geocodeAddress).mockResolvedValue(null);
|
vi.mocked(geocodingService.geocodeAddress).mockResolvedValue(null);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/system/geocode')
|
.post('/api/v1/system/geocode')
|
||||||
.send({ address: 'Invalid Address' });
|
.send({ address: 'Invalid Address' });
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toBe('Could not geocode the provided address.');
|
expect(response.body.error.message).toBe('Could not geocode the provided address.');
|
||||||
@@ -144,14 +144,14 @@ describe('System Routes (/api/system)', () => {
|
|||||||
const geocodeError = new Error('Geocoding service unavailable');
|
const geocodeError = new Error('Geocoding service unavailable');
|
||||||
vi.mocked(geocodingService.geocodeAddress).mockRejectedValue(geocodeError);
|
vi.mocked(geocodingService.geocodeAddress).mockRejectedValue(geocodeError);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/system/geocode')
|
.post('/api/v1/system/geocode')
|
||||||
.send({ address: 'Any Address' });
|
.send({ address: 'Any Address' });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if the address is missing from the body', async () => {
|
it('should return 400 if the address is missing from the body', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/system/geocode')
|
.post('/api/v1/system/geocode')
|
||||||
.send({ not_address: 'Victoria, BC' });
|
.send({ not_address: 'Victoria, BC' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
// Zod validation error message can vary slightly depending on configuration or version
|
// Zod validation error message can vary slightly depending on configuration or version
|
||||||
@@ -170,7 +170,7 @@ describe('System Routes (/api/system)', () => {
|
|||||||
// We only need to verify it blocks eventually.
|
// We only need to verify it blocks eventually.
|
||||||
// Instead of running 100 requests, we check for the headers which confirm the middleware is active.
|
// Instead of running 100 requests, we check for the headers which confirm the middleware is active.
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/system/geocode')
|
.post('/api/v1/system/geocode')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ address });
|
.send({ address });
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const expectLogger = expect.objectContaining({
|
|||||||
error: expect.any(Function),
|
error: expect.any(Function),
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('UPC Routes (/api/upc)', () => {
|
describe('UPC Routes (/api/v1/upc)', () => {
|
||||||
const mockUserProfile = createMockUserProfile({
|
const mockUserProfile = createMockUserProfile({
|
||||||
user: { user_id: 'user-123', email: 'test@test.com' },
|
user: { user_id: 'user-123', email: 'test@test.com' },
|
||||||
});
|
});
|
||||||
@@ -89,13 +89,13 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
|
|
||||||
const app = createTestApp({
|
const app = createTestApp({
|
||||||
router: upcRouter,
|
router: upcRouter,
|
||||||
basePath: '/api/upc',
|
basePath: '/api/v1/upc',
|
||||||
authenticatedUser: mockUserProfile,
|
authenticatedUser: mockUserProfile,
|
||||||
});
|
});
|
||||||
|
|
||||||
const adminApp = createTestApp({
|
const adminApp = createTestApp({
|
||||||
router: upcRouter,
|
router: upcRouter,
|
||||||
basePath: '/api/upc',
|
basePath: '/api/v1/upc',
|
||||||
authenticatedUser: mockAdminProfile,
|
authenticatedUser: mockAdminProfile,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
|
|
||||||
vi.mocked(upcService.scanUpc).mockResolvedValue(mockScanResult);
|
vi.mocked(upcService.scanUpc).mockResolvedValue(mockScanResult);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/upc/scan').send({
|
const response = await supertest(app).post('/api/v1/upc/scan').send({
|
||||||
upc_code: '012345678905',
|
upc_code: '012345678905',
|
||||||
scan_source: 'manual_entry',
|
scan_source: 'manual_entry',
|
||||||
});
|
});
|
||||||
@@ -161,7 +161,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
|
|
||||||
vi.mocked(upcService.scanUpc).mockResolvedValue(mockScanResult);
|
vi.mocked(upcService.scanUpc).mockResolvedValue(mockScanResult);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/upc/scan').send({
|
const response = await supertest(app).post('/api/v1/upc/scan').send({
|
||||||
image_base64: 'SGVsbG8gV29ybGQ=',
|
image_base64: 'SGVsbG8gV29ybGQ=',
|
||||||
scan_source: 'image_upload',
|
scan_source: 'image_upload',
|
||||||
});
|
});
|
||||||
@@ -172,7 +172,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 when neither upc_code nor image_base64 is provided', async () => {
|
it('should return 400 when neither upc_code nor image_base64 is provided', async () => {
|
||||||
const response = await supertest(app).post('/api/upc/scan').send({
|
const response = await supertest(app).post('/api/v1/upc/scan').send({
|
||||||
scan_source: 'manual_entry',
|
scan_source: 'manual_entry',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid scan_source', async () => {
|
it('should return 400 for invalid scan_source', async () => {
|
||||||
const response = await supertest(app).post('/api/upc/scan').send({
|
const response = await supertest(app).post('/api/v1/upc/scan').send({
|
||||||
upc_code: '012345678905',
|
upc_code: '012345678905',
|
||||||
scan_source: 'invalid_source',
|
scan_source: 'invalid_source',
|
||||||
});
|
});
|
||||||
@@ -192,7 +192,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
it('should return 500 if the scan service fails', async () => {
|
it('should return 500 if the scan service fails', async () => {
|
||||||
vi.mocked(upcService.scanUpc).mockRejectedValue(new Error('Scan service error'));
|
vi.mocked(upcService.scanUpc).mockRejectedValue(new Error('Scan service error'));
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/upc/scan').send({
|
const response = await supertest(app).post('/api/v1/upc/scan').send({
|
||||||
upc_code: '012345678905',
|
upc_code: '012345678905',
|
||||||
scan_source: 'manual_entry',
|
scan_source: 'manual_entry',
|
||||||
});
|
});
|
||||||
@@ -224,7 +224,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
|
|
||||||
vi.mocked(upcService.lookupUpc).mockResolvedValue(mockLookupResult);
|
vi.mocked(upcService.lookupUpc).mockResolvedValue(mockLookupResult);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/upc/lookup?upc_code=012345678905');
|
const response = await supertest(app).get('/api/v1/upc/lookup?upc_code=012345678905');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.upc_code).toBe('012345678905');
|
expect(response.body.data.upc_code).toBe('012345678905');
|
||||||
@@ -250,7 +250,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
vi.mocked(upcService.lookupUpc).mockResolvedValue(mockLookupResult);
|
vi.mocked(upcService.lookupUpc).mockResolvedValue(mockLookupResult);
|
||||||
|
|
||||||
const response = await supertest(app).get(
|
const response = await supertest(app).get(
|
||||||
'/api/upc/lookup?upc_code=012345678905&include_external=true&force_refresh=true',
|
'/api/v1/upc/lookup?upc_code=012345678905&include_external=true&force_refresh=true',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -264,14 +264,14 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid UPC code format', async () => {
|
it('should return 400 for invalid UPC code format', async () => {
|
||||||
const response = await supertest(app).get('/api/upc/lookup?upc_code=123');
|
const response = await supertest(app).get('/api/v1/upc/lookup?upc_code=123');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toMatch(/8-14 digits/);
|
expect(response.body.error.details[0].message).toMatch(/8-14 digits/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 when upc_code is missing', async () => {
|
it('should return 400 when upc_code is missing', async () => {
|
||||||
const response = await supertest(app).get('/api/upc/lookup');
|
const response = await supertest(app).get('/api/v1/upc/lookup');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -279,7 +279,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
it('should return 500 if the lookup service fails', async () => {
|
it('should return 500 if the lookup service fails', async () => {
|
||||||
vi.mocked(upcService.lookupUpc).mockRejectedValue(new Error('Lookup error'));
|
vi.mocked(upcService.lookupUpc).mockRejectedValue(new Error('Lookup error'));
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/upc/lookup?upc_code=012345678905');
|
const response = await supertest(app).get('/api/v1/upc/lookup?upc_code=012345678905');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
@@ -307,7 +307,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
|
|
||||||
vi.mocked(upcService.getScanHistory).mockResolvedValue(mockHistory);
|
vi.mocked(upcService.getScanHistory).mockResolvedValue(mockHistory);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/upc/history?limit=10&offset=0');
|
const response = await supertest(app).get('/api/v1/upc/history?limit=10&offset=0');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.scans).toHaveLength(1);
|
expect(response.body.data.scans).toHaveLength(1);
|
||||||
@@ -325,7 +325,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
it('should support filtering by lookup_successful', async () => {
|
it('should support filtering by lookup_successful', async () => {
|
||||||
vi.mocked(upcService.getScanHistory).mockResolvedValue({ scans: [], total: 0 });
|
vi.mocked(upcService.getScanHistory).mockResolvedValue({ scans: [], total: 0 });
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/upc/history?lookup_successful=true');
|
const response = await supertest(app).get('/api/v1/upc/history?lookup_successful=true');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(upcService.getScanHistory).toHaveBeenCalledWith(
|
expect(upcService.getScanHistory).toHaveBeenCalledWith(
|
||||||
@@ -339,7 +339,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
it('should support filtering by scan_source', async () => {
|
it('should support filtering by scan_source', async () => {
|
||||||
vi.mocked(upcService.getScanHistory).mockResolvedValue({ scans: [], total: 0 });
|
vi.mocked(upcService.getScanHistory).mockResolvedValue({ scans: [], total: 0 });
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/upc/history?scan_source=image_upload');
|
const response = await supertest(app).get('/api/v1/upc/history?scan_source=image_upload');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(upcService.getScanHistory).toHaveBeenCalledWith(
|
expect(upcService.getScanHistory).toHaveBeenCalledWith(
|
||||||
@@ -354,7 +354,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
vi.mocked(upcService.getScanHistory).mockResolvedValue({ scans: [], total: 0 });
|
vi.mocked(upcService.getScanHistory).mockResolvedValue({ scans: [], total: 0 });
|
||||||
|
|
||||||
const response = await supertest(app).get(
|
const response = await supertest(app).get(
|
||||||
'/api/upc/history?from_date=2024-01-01&to_date=2024-01-31',
|
'/api/v1/upc/history?from_date=2024-01-01&to_date=2024-01-31',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -368,7 +368,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid date format', async () => {
|
it('should return 400 for invalid date format', async () => {
|
||||||
const response = await supertest(app).get('/api/upc/history?from_date=01-01-2024');
|
const response = await supertest(app).get('/api/v1/upc/history?from_date=01-01-2024');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -376,7 +376,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
it('should return 500 if the history service fails', async () => {
|
it('should return 500 if the history service fails', async () => {
|
||||||
vi.mocked(upcService.getScanHistory).mockRejectedValue(new Error('History error'));
|
vi.mocked(upcService.getScanHistory).mockRejectedValue(new Error('History error'));
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/upc/history');
|
const response = await supertest(app).get('/api/v1/upc/history');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
@@ -399,7 +399,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
|
|
||||||
vi.mocked(upcService.getScanById).mockResolvedValue(mockScan);
|
vi.mocked(upcService.getScanById).mockResolvedValue(mockScan);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/upc/history/1');
|
const response = await supertest(app).get('/api/v1/upc/history/1');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.scan_id).toBe(1);
|
expect(response.body.data.scan_id).toBe(1);
|
||||||
@@ -413,14 +413,14 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
it('should return 404 when scan not found', async () => {
|
it('should return 404 when scan not found', async () => {
|
||||||
vi.mocked(upcService.getScanById).mockRejectedValue(new NotFoundError('Scan not found'));
|
vi.mocked(upcService.getScanById).mockRejectedValue(new NotFoundError('Scan not found'));
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/upc/history/999');
|
const response = await supertest(app).get('/api/v1/upc/history/999');
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toBe('Scan not found');
|
expect(response.body.error.message).toBe('Scan not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid scan ID', async () => {
|
it('should return 400 for invalid scan ID', async () => {
|
||||||
const response = await supertest(app).get('/api/upc/history/abc');
|
const response = await supertest(app).get('/api/v1/upc/history/abc');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toMatch(/Invalid ID|number/i);
|
expect(response.body.error.details[0].message).toMatch(/Invalid ID|number/i);
|
||||||
@@ -439,7 +439,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
|
|
||||||
vi.mocked(upcService.getScanStats).mockResolvedValue(mockStats);
|
vi.mocked(upcService.getScanStats).mockResolvedValue(mockStats);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/upc/stats');
|
const response = await supertest(app).get('/api/v1/upc/stats');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.total_scans).toBe(100);
|
expect(response.body.data.total_scans).toBe(100);
|
||||||
@@ -453,7 +453,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
it('should return 500 if the stats service fails', async () => {
|
it('should return 500 if the stats service fails', async () => {
|
||||||
vi.mocked(upcService.getScanStats).mockRejectedValue(new Error('Stats error'));
|
vi.mocked(upcService.getScanStats).mockRejectedValue(new Error('Stats error'));
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/upc/stats');
|
const response = await supertest(app).get('/api/v1/upc/stats');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
@@ -463,7 +463,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
it('should link UPC to product (admin only)', async () => {
|
it('should link UPC to product (admin only)', async () => {
|
||||||
vi.mocked(upcService.linkUpcToProduct).mockResolvedValue(undefined);
|
vi.mocked(upcService.linkUpcToProduct).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const response = await supertest(adminApp).post('/api/upc/link').send({
|
const response = await supertest(adminApp).post('/api/v1/upc/link').send({
|
||||||
upc_code: '012345678905',
|
upc_code: '012345678905',
|
||||||
product_id: 1,
|
product_id: 1,
|
||||||
});
|
});
|
||||||
@@ -473,7 +473,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 403 for non-admin users', async () => {
|
it('should return 403 for non-admin users', async () => {
|
||||||
const response = await supertest(app).post('/api/upc/link').send({
|
const response = await supertest(app).post('/api/v1/upc/link').send({
|
||||||
upc_code: '012345678905',
|
upc_code: '012345678905',
|
||||||
product_id: 1,
|
product_id: 1,
|
||||||
});
|
});
|
||||||
@@ -483,7 +483,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid UPC code format', async () => {
|
it('should return 400 for invalid UPC code format', async () => {
|
||||||
const response = await supertest(adminApp).post('/api/upc/link').send({
|
const response = await supertest(adminApp).post('/api/v1/upc/link').send({
|
||||||
upc_code: '123',
|
upc_code: '123',
|
||||||
product_id: 1,
|
product_id: 1,
|
||||||
});
|
});
|
||||||
@@ -493,7 +493,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid product_id', async () => {
|
it('should return 400 for invalid product_id', async () => {
|
||||||
const response = await supertest(adminApp).post('/api/upc/link').send({
|
const response = await supertest(adminApp).post('/api/v1/upc/link').send({
|
||||||
upc_code: '012345678905',
|
upc_code: '012345678905',
|
||||||
product_id: -1,
|
product_id: -1,
|
||||||
});
|
});
|
||||||
@@ -506,7 +506,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
new NotFoundError('Product not found'),
|
new NotFoundError('Product not found'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(adminApp).post('/api/upc/link').send({
|
const response = await supertest(adminApp).post('/api/v1/upc/link').send({
|
||||||
upc_code: '012345678905',
|
upc_code: '012345678905',
|
||||||
product_id: 999,
|
product_id: 999,
|
||||||
});
|
});
|
||||||
@@ -518,7 +518,7 @@ describe('UPC Routes (/api/upc)', () => {
|
|||||||
it('should return 500 if the link service fails', async () => {
|
it('should return 500 if the link service fails', async () => {
|
||||||
vi.mocked(upcService.linkUpcToProduct).mockRejectedValue(new Error('Link error'));
|
vi.mocked(upcService.linkUpcToProduct).mockRejectedValue(new Error('Link error'));
|
||||||
|
|
||||||
const response = await supertest(adminApp).post('/api/upc/link').send({
|
const response = await supertest(adminApp).post('/api/v1/upc/link').send({
|
||||||
upc_code: '012345678905',
|
upc_code: '012345678905',
|
||||||
product_id: 1,
|
product_id: 1,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const expectLogger = expect.objectContaining({
|
|||||||
info: expect.any(Function),
|
info: expect.any(Function),
|
||||||
error: expect.any(Function),
|
error: expect.any(Function),
|
||||||
});
|
});
|
||||||
describe('User Routes (/api/users)', () => {
|
describe('User Routes (/api/v1/users)', () => {
|
||||||
// This test needs to be separate because the code it tests runs on module load.
|
// This test needs to be separate because the code it tests runs on module load.
|
||||||
describe('Avatar Upload Directory Creation', () => {
|
describe('Avatar Upload Directory Creation', () => {
|
||||||
it('should log an error if avatar directory creation fails', async () => {
|
it('should log an error if avatar directory creation fails', async () => {
|
||||||
@@ -107,12 +107,12 @@ describe('User Routes (/api/users)', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
const basePath = '/api/users';
|
const basePath = '/api/v1/users';
|
||||||
|
|
||||||
describe('when user is not authenticated', () => {
|
describe('when user is not authenticated', () => {
|
||||||
it('GET /profile should return 401', async () => {
|
it('GET /profile should return 401', async () => {
|
||||||
const app = createTestApp({ router: userRouter, basePath }); // No user injected
|
const app = createTestApp({ router: userRouter, basePath }); // No user injected
|
||||||
const response = await supertest(app).get('/api/users/profile');
|
const response = await supertest(app).get('/api/v1/users/profile');
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -149,7 +149,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
describe('GET /profile', () => {
|
describe('GET /profile', () => {
|
||||||
it('should return the full user profile', async () => {
|
it('should return the full user profile', async () => {
|
||||||
vi.mocked(db.userRepo.findUserProfileById).mockResolvedValue(mockUserProfile);
|
vi.mocked(db.userRepo.findUserProfileById).mockResolvedValue(mockUserProfile);
|
||||||
const response = await supertest(app).get('/api/users/profile');
|
const response = await supertest(app).get('/api/v1/users/profile');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockUserProfile);
|
expect(response.body.data).toEqual(mockUserProfile);
|
||||||
expect(db.userRepo.findUserProfileById).toHaveBeenCalledWith(
|
expect(db.userRepo.findUserProfileById).toHaveBeenCalledWith(
|
||||||
@@ -162,7 +162,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
vi.mocked(db.userRepo.findUserProfileById).mockRejectedValue(
|
vi.mocked(db.userRepo.findUserProfileById).mockRejectedValue(
|
||||||
new NotFoundError('Profile not found for this user.'),
|
new NotFoundError('Profile not found for this user.'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).get('/api/users/profile');
|
const response = await supertest(app).get('/api/v1/users/profile');
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toContain('Profile not found');
|
expect(response.body.error.message).toContain('Profile not found');
|
||||||
});
|
});
|
||||||
@@ -170,11 +170,11 @@ describe('User Routes (/api/users)', () => {
|
|||||||
it('should return 500 on a generic database error', async () => {
|
it('should return 500 on a generic database error', async () => {
|
||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.userRepo.findUserProfileById).mockRejectedValue(dbError);
|
vi.mocked(db.userRepo.findUserProfileById).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/users/profile');
|
const response = await supertest(app).get('/api/v1/users/profile');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
`[ROUTE] GET /api/users/profile - ERROR`,
|
`[ROUTE] GET /api/v1/users/profile - ERROR`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -185,7 +185,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Milk' }),
|
createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Milk' }),
|
||||||
];
|
];
|
||||||
vi.mocked(db.personalizationRepo.getWatchedItems).mockResolvedValue(mockItems);
|
vi.mocked(db.personalizationRepo.getWatchedItems).mockResolvedValue(mockItems);
|
||||||
const response = await supertest(app).get('/api/users/watched-items');
|
const response = await supertest(app).get('/api/v1/users/watched-items');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockItems);
|
expect(response.body.data).toEqual(mockItems);
|
||||||
});
|
});
|
||||||
@@ -193,11 +193,11 @@ describe('User Routes (/api/users)', () => {
|
|||||||
it('should return 500 on a generic database error', async () => {
|
it('should return 500 on a generic database error', async () => {
|
||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.personalizationRepo.getWatchedItems).mockRejectedValue(dbError);
|
vi.mocked(db.personalizationRepo.getWatchedItems).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/users/watched-items');
|
const response = await supertest(app).get('/api/v1/users/watched-items');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
`[ROUTE] GET /api/users/watched-items - ERROR`,
|
`[ROUTE] GET /api/v1/users/watched-items - ERROR`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -211,7 +211,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
category_name: 'Produce',
|
category_name: 'Produce',
|
||||||
});
|
});
|
||||||
vi.mocked(db.personalizationRepo.addWatchedItem).mockResolvedValue(mockAddedItem);
|
vi.mocked(db.personalizationRepo.addWatchedItem).mockResolvedValue(mockAddedItem);
|
||||||
const response = await supertest(app).post('/api/users/watched-items').send(newItem);
|
const response = await supertest(app).post('/api/v1/users/watched-items').send(newItem);
|
||||||
expect(response.status).toBe(201);
|
expect(response.status).toBe(201);
|
||||||
expect(response.body.data).toEqual(mockAddedItem);
|
expect(response.body.data).toEqual(mockAddedItem);
|
||||||
});
|
});
|
||||||
@@ -220,7 +220,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.personalizationRepo.addWatchedItem).mockRejectedValue(dbError);
|
vi.mocked(db.personalizationRepo.addWatchedItem).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/watched-items')
|
.post('/api/v1/users/watched-items')
|
||||||
.send({ itemName: 'Test', category_id: 5 });
|
.send({ itemName: 'Test', category_id: 5 });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalled();
|
expect(logger.error).toHaveBeenCalled();
|
||||||
@@ -230,7 +230,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
describe('POST /watched-items (Validation)', () => {
|
describe('POST /watched-items (Validation)', () => {
|
||||||
it('should return 400 if itemName is missing', async () => {
|
it('should return 400 if itemName is missing', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/watched-items')
|
.post('/api/v1/users/watched-items')
|
||||||
.send({ category_id: 5 });
|
.send({ category_id: 5 });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
// Check the 'error.details' array for the specific validation message.
|
// Check the 'error.details' array for the specific validation message.
|
||||||
@@ -239,7 +239,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
|
|
||||||
it('should return 400 if category_id is missing', async () => {
|
it('should return 400 if category_id is missing', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/watched-items')
|
.post('/api/v1/users/watched-items')
|
||||||
.send({ itemName: 'Apples' });
|
.send({ itemName: 'Apples' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
// Check the 'error.details' array for the specific validation message.
|
// Check the 'error.details' array for the specific validation message.
|
||||||
@@ -252,7 +252,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
new ForeignKeyConstraintError('Category not found'),
|
new ForeignKeyConstraintError('Category not found'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/watched-items')
|
.post('/api/v1/users/watched-items')
|
||||||
.send({ itemName: 'Test', category_id: 999 });
|
.send({ itemName: 'Test', category_id: 999 });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -260,7 +260,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
describe('DELETE /watched-items/:masterItemId', () => {
|
describe('DELETE /watched-items/:masterItemId', () => {
|
||||||
it('should remove an item from the watchlist', async () => {
|
it('should remove an item from the watchlist', async () => {
|
||||||
vi.mocked(db.personalizationRepo.removeWatchedItem).mockResolvedValue(undefined);
|
vi.mocked(db.personalizationRepo.removeWatchedItem).mockResolvedValue(undefined);
|
||||||
const response = await supertest(app).delete(`/api/users/watched-items/99`);
|
const response = await supertest(app).delete(`/api/v1/users/watched-items/99`);
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
expect(db.personalizationRepo.removeWatchedItem).toHaveBeenCalledWith(
|
expect(db.personalizationRepo.removeWatchedItem).toHaveBeenCalledWith(
|
||||||
mockUserProfile.user.user_id,
|
mockUserProfile.user.user_id,
|
||||||
@@ -272,11 +272,11 @@ describe('User Routes (/api/users)', () => {
|
|||||||
it('should return 500 on a generic database error', async () => {
|
it('should return 500 on a generic database error', async () => {
|
||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.personalizationRepo.removeWatchedItem).mockRejectedValue(dbError);
|
vi.mocked(db.personalizationRepo.removeWatchedItem).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).delete(`/api/users/watched-items/99`);
|
const response = await supertest(app).delete(`/api/v1/users/watched-items/99`);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
`[ROUTE] DELETE /api/users/watched-items/:masterItemId - ERROR`,
|
`[ROUTE] DELETE /api/v1/users/watched-items/:masterItemId - ERROR`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -287,7 +287,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
createMockShoppingList({ shopping_list_id: 1, user_id: mockUserProfile.user.user_id }),
|
createMockShoppingList({ shopping_list_id: 1, user_id: mockUserProfile.user.user_id }),
|
||||||
];
|
];
|
||||||
vi.mocked(db.shoppingRepo.getShoppingLists).mockResolvedValue(mockLists);
|
vi.mocked(db.shoppingRepo.getShoppingLists).mockResolvedValue(mockLists);
|
||||||
const response = await supertest(app).get('/api/users/shopping-lists');
|
const response = await supertest(app).get('/api/v1/users/shopping-lists');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockLists);
|
expect(response.body.data).toEqual(mockLists);
|
||||||
});
|
});
|
||||||
@@ -295,11 +295,11 @@ describe('User Routes (/api/users)', () => {
|
|||||||
it('should return 500 on a generic database error', async () => {
|
it('should return 500 on a generic database error', async () => {
|
||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.shoppingRepo.getShoppingLists).mockRejectedValue(dbError);
|
vi.mocked(db.shoppingRepo.getShoppingLists).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/users/shopping-lists');
|
const response = await supertest(app).get('/api/v1/users/shopping-lists');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
`[ROUTE] GET /api/users/shopping-lists - ERROR`,
|
`[ROUTE] GET /api/v1/users/shopping-lists - ERROR`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -311,7 +311,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
});
|
});
|
||||||
vi.mocked(db.shoppingRepo.createShoppingList).mockResolvedValue(mockNewList);
|
vi.mocked(db.shoppingRepo.createShoppingList).mockResolvedValue(mockNewList);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.send({ name: 'Party Supplies' });
|
.send({ name: 'Party Supplies' });
|
||||||
|
|
||||||
expect(response.status).toBe(201);
|
expect(response.status).toBe(201);
|
||||||
@@ -319,7 +319,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if name is missing', async () => {
|
it('should return 400 if name is missing', async () => {
|
||||||
const response = await supertest(app).post('/api/users/shopping-lists').send({});
|
const response = await supertest(app).post('/api/v1/users/shopping-lists').send({});
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
// Check the 'error.details' array for the specific validation message.
|
// Check the 'error.details' array for the specific validation message.
|
||||||
expect(response.body.error.details[0].message).toBe("Field 'name' is required.");
|
expect(response.body.error.details[0].message).toBe("Field 'name' is required.");
|
||||||
@@ -330,7 +330,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
new ForeignKeyConstraintError('User not found'),
|
new ForeignKeyConstraintError('User not found'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.send({ name: 'Failing List' });
|
.send({ name: 'Failing List' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.message).toBe('User not found');
|
expect(response.body.error.message).toBe('User not found');
|
||||||
@@ -340,7 +340,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.shoppingRepo.createShoppingList).mockRejectedValue(dbError);
|
vi.mocked(db.shoppingRepo.createShoppingList).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.send({ name: 'Failing List' });
|
.send({ name: 'Failing List' });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.error.message).toBe('DB Connection Failed');
|
expect(response.body.error.message).toBe('DB Connection Failed');
|
||||||
@@ -348,7 +348,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for an invalid listId on DELETE', async () => {
|
it('should return 400 for an invalid listId on DELETE', async () => {
|
||||||
const response = await supertest(app).delete('/api/users/shopping-lists/abc');
|
const response = await supertest(app).delete('/api/v1/users/shopping-lists/abc');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
// Check the 'error.details' array for the specific validation message.
|
// Check the 'error.details' array for the specific validation message.
|
||||||
expect(response.body.error.details[0].message).toContain('received NaN');
|
expect(response.body.error.details[0].message).toContain('received NaN');
|
||||||
@@ -357,7 +357,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
describe('DELETE /shopping-lists/:listId', () => {
|
describe('DELETE /shopping-lists/:listId', () => {
|
||||||
it('should delete a list', async () => {
|
it('should delete a list', async () => {
|
||||||
vi.mocked(db.shoppingRepo.deleteShoppingList).mockResolvedValue(undefined);
|
vi.mocked(db.shoppingRepo.deleteShoppingList).mockResolvedValue(undefined);
|
||||||
const response = await supertest(app).delete('/api/users/shopping-lists/1');
|
const response = await supertest(app).delete('/api/v1/users/shopping-lists/1');
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -366,20 +366,20 @@ describe('User Routes (/api/users)', () => {
|
|||||||
vi.mocked(db.shoppingRepo.deleteShoppingList).mockRejectedValue(
|
vi.mocked(db.shoppingRepo.deleteShoppingList).mockRejectedValue(
|
||||||
new NotFoundError('not found'),
|
new NotFoundError('not found'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).delete('/api/users/shopping-lists/999');
|
const response = await supertest(app).delete('/api/v1/users/shopping-lists/999');
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 500 on a generic database error', async () => {
|
it('should return 500 on a generic database error', async () => {
|
||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.shoppingRepo.deleteShoppingList).mockRejectedValue(dbError);
|
vi.mocked(db.shoppingRepo.deleteShoppingList).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).delete('/api/users/shopping-lists/1');
|
const response = await supertest(app).delete('/api/v1/users/shopping-lists/1');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalled();
|
expect(logger.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for an invalid listId', async () => {
|
it('should return 400 for an invalid listId', async () => {
|
||||||
const response = await supertest(app).delete('/api/users/shopping-lists/abc');
|
const response = await supertest(app).delete('/api/v1/users/shopping-lists/abc');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toContain('received NaN');
|
expect(response.body.error.details[0].message).toContain('received NaN');
|
||||||
});
|
});
|
||||||
@@ -388,7 +388,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
describe('Shopping List Item Routes', () => {
|
describe('Shopping List Item Routes', () => {
|
||||||
describe('POST /shopping-lists/:listId/items (Validation)', () => {
|
describe('POST /shopping-lists/:listId/items (Validation)', () => {
|
||||||
it('should return 400 if neither masterItemId nor customItemName are provided', async () => {
|
it('should return 400 if neither masterItemId nor customItemName are provided', async () => {
|
||||||
const response = await supertest(app).post('/api/users/shopping-lists/1/items').send({});
|
const response = await supertest(app).post('/api/v1/users/shopping-lists/1/items').send({});
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toBe(
|
expect(response.body.error.details[0].message).toBe(
|
||||||
'Either masterItemId or customItemName must be provided.',
|
'Either masterItemId or customItemName must be provided.',
|
||||||
@@ -400,7 +400,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
createMockShoppingListItem({}),
|
createMockShoppingListItem({}),
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/shopping-lists/1/items')
|
.post('/api/v1/users/shopping-lists/1/items')
|
||||||
.send({ masterItemId: 123 });
|
.send({ masterItemId: 123 });
|
||||||
expect(response.status).toBe(201);
|
expect(response.status).toBe(201);
|
||||||
});
|
});
|
||||||
@@ -410,7 +410,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
createMockShoppingListItem({}),
|
createMockShoppingListItem({}),
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/shopping-lists/1/items')
|
.post('/api/v1/users/shopping-lists/1/items')
|
||||||
.send({ customItemName: 'Custom Item' });
|
.send({ customItemName: 'Custom Item' });
|
||||||
expect(response.status).toBe(201);
|
expect(response.status).toBe(201);
|
||||||
});
|
});
|
||||||
@@ -420,7 +420,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
createMockShoppingListItem({}),
|
createMockShoppingListItem({}),
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/shopping-lists/1/items')
|
.post('/api/v1/users/shopping-lists/1/items')
|
||||||
.send({ masterItemId: 123, customItemName: 'Custom Item' });
|
.send({ masterItemId: 123, customItemName: 'Custom Item' });
|
||||||
expect(response.status).toBe(201);
|
expect(response.status).toBe(201);
|
||||||
});
|
});
|
||||||
@@ -435,7 +435,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
});
|
});
|
||||||
vi.mocked(db.shoppingRepo.addShoppingListItem).mockResolvedValue(mockAddedItem);
|
vi.mocked(db.shoppingRepo.addShoppingListItem).mockResolvedValue(mockAddedItem);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post(`/api/users/shopping-lists/${listId}/items`)
|
.post(`/api/v1/users/shopping-lists/${listId}/items`)
|
||||||
.send(itemData);
|
.send(itemData);
|
||||||
|
|
||||||
expect(response.status).toBe(201);
|
expect(response.status).toBe(201);
|
||||||
@@ -453,7 +453,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
new ForeignKeyConstraintError('List not found'),
|
new ForeignKeyConstraintError('List not found'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/shopping-lists/999/items')
|
.post('/api/v1/users/shopping-lists/999/items')
|
||||||
.send({ customItemName: 'Test' });
|
.send({ customItemName: 'Test' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -462,7 +462,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.shoppingRepo.addShoppingListItem).mockRejectedValue(dbError);
|
vi.mocked(db.shoppingRepo.addShoppingListItem).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/shopping-lists/1/items')
|
.post('/api/v1/users/shopping-lists/1/items')
|
||||||
.send({ customItemName: 'Test' });
|
.send({ customItemName: 'Test' });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalled();
|
expect(logger.error).toHaveBeenCalled();
|
||||||
@@ -478,7 +478,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
});
|
});
|
||||||
vi.mocked(db.shoppingRepo.updateShoppingListItem).mockResolvedValue(mockUpdatedItem);
|
vi.mocked(db.shoppingRepo.updateShoppingListItem).mockResolvedValue(mockUpdatedItem);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put(`/api/users/shopping-lists/items/${itemId}`)
|
.put(`/api/v1/users/shopping-lists/items/${itemId}`)
|
||||||
.send(updates);
|
.send(updates);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -496,7 +496,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
new NotFoundError('not found'),
|
new NotFoundError('not found'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/shopping-lists/items/999')
|
.put('/api/v1/users/shopping-lists/items/999')
|
||||||
.send({ is_purchased: true });
|
.send({ is_purchased: true });
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
});
|
});
|
||||||
@@ -505,14 +505,14 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.shoppingRepo.updateShoppingListItem).mockRejectedValue(dbError);
|
vi.mocked(db.shoppingRepo.updateShoppingListItem).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/shopping-lists/items/101')
|
.put('/api/v1/users/shopping-lists/items/101')
|
||||||
.send({ is_purchased: true });
|
.send({ is_purchased: true });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalled();
|
expect(logger.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if no update fields are provided for an item', async () => {
|
it('should return 400 if no update fields are provided for an item', async () => {
|
||||||
const response = await supertest(app).put(`/api/users/shopping-lists/items/101`).send({});
|
const response = await supertest(app).put(`/api/v1/users/shopping-lists/items/101`).send({});
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toContain(
|
expect(response.body.error.details[0].message).toContain(
|
||||||
'At least one field (quantity, is_purchased) must be provided.',
|
'At least one field (quantity, is_purchased) must be provided.',
|
||||||
@@ -522,7 +522,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
describe('DELETE /shopping-lists/items/:itemId', () => {
|
describe('DELETE /shopping-lists/items/:itemId', () => {
|
||||||
it('should delete an item', async () => {
|
it('should delete an item', async () => {
|
||||||
vi.mocked(db.shoppingRepo.removeShoppingListItem).mockResolvedValue(undefined);
|
vi.mocked(db.shoppingRepo.removeShoppingListItem).mockResolvedValue(undefined);
|
||||||
const response = await supertest(app).delete('/api/users/shopping-lists/items/101');
|
const response = await supertest(app).delete('/api/v1/users/shopping-lists/items/101');
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
expect(db.shoppingRepo.removeShoppingListItem).toHaveBeenCalledWith(
|
expect(db.shoppingRepo.removeShoppingListItem).toHaveBeenCalledWith(
|
||||||
101,
|
101,
|
||||||
@@ -535,14 +535,14 @@ describe('User Routes (/api/users)', () => {
|
|||||||
vi.mocked(db.shoppingRepo.removeShoppingListItem).mockRejectedValue(
|
vi.mocked(db.shoppingRepo.removeShoppingListItem).mockRejectedValue(
|
||||||
new NotFoundError('not found'),
|
new NotFoundError('not found'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).delete('/api/users/shopping-lists/items/999');
|
const response = await supertest(app).delete('/api/v1/users/shopping-lists/items/999');
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 500 on a generic database error', async () => {
|
it('should return 500 on a generic database error', async () => {
|
||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.shoppingRepo.removeShoppingListItem).mockRejectedValue(dbError);
|
vi.mocked(db.shoppingRepo.removeShoppingListItem).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).delete('/api/users/shopping-lists/items/101');
|
const response = await supertest(app).delete('/api/v1/users/shopping-lists/items/101');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalled();
|
expect(logger.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -554,7 +554,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const profileUpdates = { full_name: 'New Name' };
|
const profileUpdates = { full_name: 'New Name' };
|
||||||
const updatedProfile = createMockUserProfile({ ...mockUserProfile, ...profileUpdates });
|
const updatedProfile = createMockUserProfile({ ...mockUserProfile, ...profileUpdates });
|
||||||
vi.mocked(db.userRepo.updateUserProfile).mockResolvedValue(updatedProfile);
|
vi.mocked(db.userRepo.updateUserProfile).mockResolvedValue(updatedProfile);
|
||||||
const response = await supertest(app).put('/api/users/profile').send(profileUpdates);
|
const response = await supertest(app).put('/api/v1/users/profile').send(profileUpdates);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(updatedProfile);
|
expect(response.body.data).toEqual(updatedProfile);
|
||||||
@@ -568,7 +568,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
vi.mocked(db.userRepo.updateUserProfile).mockResolvedValue(updatedProfile);
|
vi.mocked(db.userRepo.updateUserProfile).mockResolvedValue(updatedProfile);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await supertest(app).put('/api/users/profile').send(profileUpdates);
|
const response = await supertest(app).put('/api/v1/users/profile').send(profileUpdates);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -585,17 +585,17 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.userRepo.updateUserProfile).mockRejectedValue(dbError);
|
vi.mocked(db.userRepo.updateUserProfile).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/profile')
|
.put('/api/v1/users/profile')
|
||||||
.send({ full_name: 'New Name' });
|
.send({ full_name: 'New Name' });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
`[ROUTE] PUT /api/users/profile - ERROR`,
|
`[ROUTE] PUT /api/v1/users/profile - ERROR`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if the body is empty', async () => {
|
it('should return 400 if the body is empty', async () => {
|
||||||
const response = await supertest(app).put('/api/users/profile').send({});
|
const response = await supertest(app).put('/api/v1/users/profile').send({});
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toBe(
|
expect(response.body.error.details[0].message).toBe(
|
||||||
'At least one field to update must be provided.',
|
'At least one field to update must be provided.',
|
||||||
@@ -607,7 +607,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
it('should update the password successfully with a strong password', async () => {
|
it('should update the password successfully with a strong password', async () => {
|
||||||
vi.mocked(userService.updateUserPassword).mockResolvedValue(undefined);
|
vi.mocked(userService.updateUserPassword).mockResolvedValue(undefined);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/profile/password')
|
.put('/api/v1/users/profile/password')
|
||||||
.send({ newPassword: 'a-Very-Strong-Password-456!' });
|
.send({ newPassword: 'a-Very-Strong-Password-456!' });
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.message).toBe('Password updated successfully.');
|
expect(response.body.data.message).toBe('Password updated successfully.');
|
||||||
@@ -617,18 +617,18 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(userService.updateUserPassword).mockRejectedValue(dbError);
|
vi.mocked(userService.updateUserPassword).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/profile/password')
|
.put('/api/v1/users/profile/password')
|
||||||
.send({ newPassword: 'a-Very-Strong-Password-456!' });
|
.send({ newPassword: 'a-Very-Strong-Password-456!' });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
`[ROUTE] PUT /api/users/profile/password - ERROR`,
|
`[ROUTE] PUT /api/v1/users/profile/password - ERROR`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for a weak password', async () => {
|
it('should return 400 for a weak password', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/profile/password')
|
.put('/api/v1/users/profile/password')
|
||||||
.send({ newPassword: 'password123' });
|
.send({ newPassword: 'password123' });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -640,7 +640,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
it('should delete the account with the correct password', async () => {
|
it('should delete the account with the correct password', async () => {
|
||||||
vi.mocked(userService.deleteUserAccount).mockResolvedValue(undefined);
|
vi.mocked(userService.deleteUserAccount).mockResolvedValue(undefined);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.delete('/api/users/account')
|
.delete('/api/v1/users/account')
|
||||||
.send({ password: 'correct-password' });
|
.send({ password: 'correct-password' });
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.message).toBe('Account deleted successfully.');
|
expect(response.body.data.message).toBe('Account deleted successfully.');
|
||||||
@@ -656,7 +656,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
new ValidationError([], 'Incorrect password.'),
|
new ValidationError([], 'Incorrect password.'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.delete('/api/users/account')
|
.delete('/api/v1/users/account')
|
||||||
.send({ password: 'wrong-password' });
|
.send({ password: 'wrong-password' });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -669,7 +669,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.delete('/api/users/account')
|
.delete('/api/v1/users/account')
|
||||||
.send({ password: 'any-password' });
|
.send({ password: 'any-password' });
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
@@ -681,12 +681,12 @@ describe('User Routes (/api/users)', () => {
|
|||||||
new Error('DB Connection Failed'),
|
new Error('DB Connection Failed'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.delete('/api/users/account')
|
.delete('/api/v1/users/account')
|
||||||
.send({ password: 'correct-password' });
|
.send({ password: 'correct-password' });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
{ error: new Error('DB Connection Failed') },
|
{ error: new Error('DB Connection Failed') },
|
||||||
`[ROUTE] DELETE /api/users/account - ERROR`,
|
`[ROUTE] DELETE /api/v1/users/account - ERROR`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -701,7 +701,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
});
|
});
|
||||||
vi.mocked(db.userRepo.updateUserPreferences).mockResolvedValue(updatedProfile);
|
vi.mocked(db.userRepo.updateUserPreferences).mockResolvedValue(updatedProfile);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/profile/preferences')
|
.put('/api/v1/users/profile/preferences')
|
||||||
.send(preferencesUpdate);
|
.send(preferencesUpdate);
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(updatedProfile);
|
expect(response.body.data).toEqual(updatedProfile);
|
||||||
@@ -711,18 +711,18 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.userRepo.updateUserPreferences).mockRejectedValue(dbError);
|
vi.mocked(db.userRepo.updateUserPreferences).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/profile/preferences')
|
.put('/api/v1/users/profile/preferences')
|
||||||
.send({ darkMode: true });
|
.send({ darkMode: true });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
`[ROUTE] PUT /api/users/profile/preferences - ERROR`,
|
`[ROUTE] PUT /api/v1/users/profile/preferences - ERROR`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if the request body is not a valid object', async () => {
|
it('should return 400 if the request body is not a valid object', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/profile/preferences')
|
.put('/api/v1/users/profile/preferences')
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.send('"not-an-object"');
|
.send('"not-an-object"');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -739,7 +739,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
vi.mocked(db.personalizationRepo.getUserDietaryRestrictions).mockResolvedValue(
|
vi.mocked(db.personalizationRepo.getUserDietaryRestrictions).mockResolvedValue(
|
||||||
mockRestrictions,
|
mockRestrictions,
|
||||||
);
|
);
|
||||||
const response = await supertest(app).get('/api/users/me/dietary-restrictions');
|
const response = await supertest(app).get('/api/v1/users/me/dietary-restrictions');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockRestrictions);
|
expect(response.body.data).toEqual(mockRestrictions);
|
||||||
});
|
});
|
||||||
@@ -747,16 +747,16 @@ describe('User Routes (/api/users)', () => {
|
|||||||
it('GET should return 500 on a generic database error', async () => {
|
it('GET should return 500 on a generic database error', async () => {
|
||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.personalizationRepo.getUserDietaryRestrictions).mockRejectedValue(dbError);
|
vi.mocked(db.personalizationRepo.getUserDietaryRestrictions).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/users/me/dietary-restrictions');
|
const response = await supertest(app).get('/api/v1/users/me/dietary-restrictions');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
`[ROUTE] GET /api/users/me/dietary-restrictions - ERROR`,
|
`[ROUTE] GET /api/v1/users/me/dietary-restrictions - ERROR`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for an invalid masterItemId', async () => {
|
it('should return 400 for an invalid masterItemId', async () => {
|
||||||
const response = await supertest(app).delete('/api/users/watched-items/abc');
|
const response = await supertest(app).delete('/api/v1/users/watched-items/abc');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
// Check the 'error.details' array for the specific validation message.
|
// Check the 'error.details' array for the specific validation message.
|
||||||
expect(response.body.error.details[0].message).toContain('received NaN');
|
expect(response.body.error.details[0].message).toContain('received NaN');
|
||||||
@@ -766,7 +766,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
vi.mocked(db.personalizationRepo.setUserDietaryRestrictions).mockResolvedValue(undefined);
|
vi.mocked(db.personalizationRepo.setUserDietaryRestrictions).mockResolvedValue(undefined);
|
||||||
const restrictionIds = [1, 3, 5];
|
const restrictionIds = [1, 3, 5];
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/me/dietary-restrictions')
|
.put('/api/v1/users/me/dietary-restrictions')
|
||||||
.send({ restrictionIds });
|
.send({ restrictionIds });
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
});
|
});
|
||||||
@@ -776,7 +776,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
new ForeignKeyConstraintError('Invalid restriction ID'),
|
new ForeignKeyConstraintError('Invalid restriction ID'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/me/dietary-restrictions')
|
.put('/api/v1/users/me/dietary-restrictions')
|
||||||
.send({ restrictionIds: [999] }); // Invalid ID
|
.send({ restrictionIds: [999] }); // Invalid ID
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -785,7 +785,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.personalizationRepo.setUserDietaryRestrictions).mockRejectedValue(dbError);
|
vi.mocked(db.personalizationRepo.setUserDietaryRestrictions).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/me/dietary-restrictions')
|
.put('/api/v1/users/me/dietary-restrictions')
|
||||||
.send({ restrictionIds: [1] });
|
.send({ restrictionIds: [1] });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalled();
|
expect(logger.error).toHaveBeenCalled();
|
||||||
@@ -793,7 +793,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
|
|
||||||
it('PUT should return 400 if restrictionIds is not an array', async () => {
|
it('PUT should return 400 if restrictionIds is not an array', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/me/dietary-restrictions')
|
.put('/api/v1/users/me/dietary-restrictions')
|
||||||
.send({ restrictionIds: 'not-an-array' });
|
.send({ restrictionIds: 'not-an-array' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -803,7 +803,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
it('GET should return a list of appliance IDs', async () => {
|
it('GET should return a list of appliance IDs', async () => {
|
||||||
const mockAppliances: Appliance[] = [createMockAppliance({ name: 'Air Fryer' })];
|
const mockAppliances: Appliance[] = [createMockAppliance({ name: 'Air Fryer' })];
|
||||||
vi.mocked(db.personalizationRepo.getUserAppliances).mockResolvedValue(mockAppliances);
|
vi.mocked(db.personalizationRepo.getUserAppliances).mockResolvedValue(mockAppliances);
|
||||||
const response = await supertest(app).get('/api/users/me/appliances');
|
const response = await supertest(app).get('/api/v1/users/me/appliances');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockAppliances);
|
expect(response.body.data).toEqual(mockAppliances);
|
||||||
});
|
});
|
||||||
@@ -811,11 +811,11 @@ describe('User Routes (/api/users)', () => {
|
|||||||
it('GET should return 500 on a generic database error', async () => {
|
it('GET should return 500 on a generic database error', async () => {
|
||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.personalizationRepo.getUserAppliances).mockRejectedValue(dbError);
|
vi.mocked(db.personalizationRepo.getUserAppliances).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/users/me/appliances');
|
const response = await supertest(app).get('/api/v1/users/me/appliances');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
{ error: dbError },
|
{ error: dbError },
|
||||||
`[ROUTE] GET /api/users/me/appliances - ERROR`,
|
`[ROUTE] GET /api/v1/users/me/appliances - ERROR`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -823,7 +823,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
vi.mocked(db.personalizationRepo.setUserAppliances).mockResolvedValue([]);
|
vi.mocked(db.personalizationRepo.setUserAppliances).mockResolvedValue([]);
|
||||||
const applianceIds = [2, 4, 6];
|
const applianceIds = [2, 4, 6];
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/me/appliances')
|
.put('/api/v1/users/me/appliances')
|
||||||
.send({ applianceIds });
|
.send({ applianceIds });
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
});
|
});
|
||||||
@@ -833,7 +833,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
new ForeignKeyConstraintError('Invalid appliance ID'),
|
new ForeignKeyConstraintError('Invalid appliance ID'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/me/appliances')
|
.put('/api/v1/users/me/appliances')
|
||||||
.send({ applianceIds: [999] }); // Invalid ID
|
.send({ applianceIds: [999] }); // Invalid ID
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.message).toBe('Invalid appliance ID');
|
expect(response.body.error.message).toBe('Invalid appliance ID');
|
||||||
@@ -843,7 +843,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.personalizationRepo.setUserAppliances).mockRejectedValue(dbError);
|
vi.mocked(db.personalizationRepo.setUserAppliances).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/me/appliances')
|
.put('/api/v1/users/me/appliances')
|
||||||
.send({ applianceIds: [1] });
|
.send({ applianceIds: [1] });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalled();
|
expect(logger.error).toHaveBeenCalled();
|
||||||
@@ -851,7 +851,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
|
|
||||||
it('PUT should return 400 if applianceIds is not an array', async () => {
|
it('PUT should return 400 if applianceIds is not an array', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/me/appliances')
|
.put('/api/v1/users/me/appliances')
|
||||||
.send({ applianceIds: 'not-an-array' });
|
.send({ applianceIds: 'not-an-array' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@@ -865,7 +865,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
];
|
];
|
||||||
vi.mocked(db.notificationRepo.getNotificationsForUser).mockResolvedValue(mockNotifications);
|
vi.mocked(db.notificationRepo.getNotificationsForUser).mockResolvedValue(mockNotifications);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/users/notifications?limit=10');
|
const response = await supertest(app).get('/api/v1/users/notifications?limit=10');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockNotifications);
|
expect(response.body.data).toEqual(mockNotifications);
|
||||||
@@ -885,7 +885,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
];
|
];
|
||||||
vi.mocked(db.notificationRepo.getNotificationsForUser).mockResolvedValue(mockNotifications);
|
vi.mocked(db.notificationRepo.getNotificationsForUser).mockResolvedValue(mockNotifications);
|
||||||
|
|
||||||
const response = await supertest(app).get('/api/users/notifications?includeRead=true');
|
const response = await supertest(app).get('/api/v1/users/notifications?includeRead=true');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockNotifications);
|
expect(response.body.data).toEqual(mockNotifications);
|
||||||
@@ -901,13 +901,13 @@ describe('User Routes (/api/users)', () => {
|
|||||||
it('GET /notifications should return 500 on a generic database error', async () => {
|
it('GET /notifications should return 500 on a generic database error', async () => {
|
||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.notificationRepo.getNotificationsForUser).mockRejectedValue(dbError);
|
vi.mocked(db.notificationRepo.getNotificationsForUser).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/users/notifications');
|
const response = await supertest(app).get('/api/v1/users/notifications');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('POST /notifications/mark-all-read should return 204', async () => {
|
it('POST /notifications/mark-all-read should return 204', async () => {
|
||||||
vi.mocked(db.notificationRepo.markAllNotificationsAsRead).mockResolvedValue(undefined);
|
vi.mocked(db.notificationRepo.markAllNotificationsAsRead).mockResolvedValue(undefined);
|
||||||
const response = await supertest(app).post('/api/users/notifications/mark-all-read');
|
const response = await supertest(app).post('/api/v1/users/notifications/mark-all-read');
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
expect(db.notificationRepo.markAllNotificationsAsRead).toHaveBeenCalledWith(
|
expect(db.notificationRepo.markAllNotificationsAsRead).toHaveBeenCalledWith(
|
||||||
'user-123',
|
'user-123',
|
||||||
@@ -918,7 +918,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
it('POST /notifications/mark-all-read should return 500 on a generic database error', async () => {
|
it('POST /notifications/mark-all-read should return 500 on a generic database error', async () => {
|
||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.notificationRepo.markAllNotificationsAsRead).mockRejectedValue(dbError);
|
vi.mocked(db.notificationRepo.markAllNotificationsAsRead).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).post('/api/users/notifications/mark-all-read');
|
const response = await supertest(app).post('/api/v1/users/notifications/mark-all-read');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -927,7 +927,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
vi.mocked(db.notificationRepo.markNotificationAsRead).mockResolvedValue(
|
vi.mocked(db.notificationRepo.markNotificationAsRead).mockResolvedValue(
|
||||||
createMockNotification({ notification_id: 1, user_id: 'user-123' }),
|
createMockNotification({ notification_id: 1, user_id: 'user-123' }),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).post('/api/users/notifications/1/mark-read');
|
const response = await supertest(app).post('/api/v1/users/notifications/1/mark-read');
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
expect(db.notificationRepo.markNotificationAsRead).toHaveBeenCalledWith(
|
expect(db.notificationRepo.markNotificationAsRead).toHaveBeenCalledWith(
|
||||||
1,
|
1,
|
||||||
@@ -939,13 +939,13 @@ describe('User Routes (/api/users)', () => {
|
|||||||
it('POST /notifications/:notificationId/mark-read should return 500 on a generic database error', async () => {
|
it('POST /notifications/:notificationId/mark-read should return 500 on a generic database error', async () => {
|
||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.notificationRepo.markNotificationAsRead).mockRejectedValue(dbError);
|
vi.mocked(db.notificationRepo.markNotificationAsRead).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).post('/api/users/notifications/1/mark-read');
|
const response = await supertest(app).post('/api/v1/users/notifications/1/mark-read');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for an invalid notificationId', async () => {
|
it('should return 400 for an invalid notificationId', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/notifications/abc/mark-read')
|
.post('/api/v1/users/notifications/abc/mark-read')
|
||||||
.send({});
|
.send({});
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toContain('received NaN');
|
expect(response.body.error.details[0].message).toContain('received NaN');
|
||||||
@@ -961,7 +961,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
});
|
});
|
||||||
const mockAddress = createMockAddress({ address_id: 1, address_line_1: '123 Main St' });
|
const mockAddress = createMockAddress({ address_id: 1, address_line_1: '123 Main St' });
|
||||||
vi.mocked(userService.getUserAddress).mockResolvedValue(mockAddress);
|
vi.mocked(userService.getUserAddress).mockResolvedValue(mockAddress);
|
||||||
const response = await supertest(appWithUser).get('/api/users/addresses/1');
|
const response = await supertest(appWithUser).get('/api/v1/users/addresses/1');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockAddress);
|
expect(response.body.data).toEqual(mockAddress);
|
||||||
});
|
});
|
||||||
@@ -973,13 +973,13 @@ describe('User Routes (/api/users)', () => {
|
|||||||
authenticatedUser: { ...mockUserProfile, address_id: 1 },
|
authenticatedUser: { ...mockUserProfile, address_id: 1 },
|
||||||
});
|
});
|
||||||
vi.mocked(userService.getUserAddress).mockRejectedValue(new Error('DB Error'));
|
vi.mocked(userService.getUserAddress).mockRejectedValue(new Error('DB Error'));
|
||||||
const response = await supertest(appWithUser).get('/api/users/addresses/1');
|
const response = await supertest(appWithUser).get('/api/v1/users/addresses/1');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /addresses/:addressId', () => {
|
describe('GET /addresses/:addressId', () => {
|
||||||
it('should return 400 for a non-numeric address ID', async () => {
|
it('should return 400 for a non-numeric address ID', async () => {
|
||||||
const response = await supertest(app).get('/api/users/addresses/abc'); // This was a duplicate, fixed.
|
const response = await supertest(app).get('/api/v1/users/addresses/abc'); // This was a duplicate, fixed.
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -988,7 +988,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
vi.mocked(userService.getUserAddress).mockRejectedValue(
|
vi.mocked(userService.getUserAddress).mockRejectedValue(
|
||||||
new ValidationError([], 'Forbidden'),
|
new ValidationError([], 'Forbidden'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).get('/api/users/addresses/2'); // Requesting address 2
|
const response = await supertest(app).get('/api/v1/users/addresses/2'); // Requesting address 2
|
||||||
expect(response.status).toBe(400); // ValidationError maps to 400 by default in the test error handler
|
expect(response.status).toBe(400); // ValidationError maps to 400 by default in the test error handler
|
||||||
expect(response.body.error.message).toBe('Forbidden');
|
expect(response.body.error.message).toBe('Forbidden');
|
||||||
});
|
});
|
||||||
@@ -1002,7 +1002,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
vi.mocked(userService.getUserAddress).mockRejectedValue(
|
vi.mocked(userService.getUserAddress).mockRejectedValue(
|
||||||
new NotFoundError('Address not found.'),
|
new NotFoundError('Address not found.'),
|
||||||
);
|
);
|
||||||
const response = await supertest(appWithUser).get('/api/users/addresses/1');
|
const response = await supertest(appWithUser).get('/api/v1/users/addresses/1');
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toBe('Address not found.');
|
expect(response.body.error.message).toBe('Address not found.');
|
||||||
});
|
});
|
||||||
@@ -1011,7 +1011,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const addressData = { address_line_1: '123 New St' };
|
const addressData = { address_line_1: '123 New St' };
|
||||||
vi.mocked(userService.upsertUserAddress).mockResolvedValue(5);
|
vi.mocked(userService.upsertUserAddress).mockResolvedValue(5);
|
||||||
|
|
||||||
const response = await supertest(app).put('/api/users/profile/address').send(addressData);
|
const response = await supertest(app).put('/api/v1/users/profile/address').send(addressData);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(userService.upsertUserAddress).toHaveBeenCalledWith(
|
expect(userService.upsertUserAddress).toHaveBeenCalledWith(
|
||||||
@@ -1025,13 +1025,13 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(userService.upsertUserAddress).mockRejectedValue(dbError);
|
vi.mocked(userService.upsertUserAddress).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/profile/address')
|
.put('/api/v1/users/profile/address')
|
||||||
.send({ address_line_1: '123 New St' });
|
.send({ address_line_1: '123 New St' });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if the address body is empty', async () => {
|
it('should return 400 if the address body is empty', async () => {
|
||||||
const response = await supertest(app).put('/api/users/profile/address').send({});
|
const response = await supertest(app).put('/api/v1/users/profile/address').send({});
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toContain(
|
expect(response.body.error.details[0].message).toContain(
|
||||||
'At least one address field must be provided',
|
'At least one address field must be provided',
|
||||||
@@ -1051,7 +1051,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dummyImagePath = 'test-avatar.png';
|
const dummyImagePath = 'test-avatar.png';
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/profile/avatar')
|
.post('/api/v1/users/profile/avatar')
|
||||||
.attach('avatar', Buffer.from('dummy-image-content'), dummyImagePath);
|
.attach('avatar', Buffer.from('dummy-image-content'), dummyImagePath);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -1068,7 +1068,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
vi.mocked(userService.updateUserAvatar).mockRejectedValue(dbError);
|
vi.mocked(userService.updateUserAvatar).mockRejectedValue(dbError);
|
||||||
const dummyImagePath = 'test-avatar.png';
|
const dummyImagePath = 'test-avatar.png';
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/profile/avatar')
|
.post('/api/v1/users/profile/avatar')
|
||||||
.attach('avatar', Buffer.from('dummy-image-content'), dummyImagePath);
|
.attach('avatar', Buffer.from('dummy-image-content'), dummyImagePath);
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
@@ -1077,7 +1077,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dummyTextPath = 'document.txt';
|
const dummyTextPath = 'document.txt';
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/profile/avatar')
|
.post('/api/v1/users/profile/avatar')
|
||||||
.attach('avatar', Buffer.from('this is not an image'), dummyTextPath);
|
.attach('avatar', Buffer.from('this is not an image'), dummyTextPath);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -1090,7 +1090,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dummyImagePath = 'large-avatar.png';
|
const dummyImagePath = 'large-avatar.png';
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/profile/avatar')
|
.post('/api/v1/users/profile/avatar')
|
||||||
.attach('avatar', largeFile, dummyImagePath);
|
.attach('avatar', largeFile, dummyImagePath);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -1099,7 +1099,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if no file is uploaded', async () => {
|
it('should return 400 if no file is uploaded', async () => {
|
||||||
const response = await supertest(app).post('/api/users/profile/avatar'); // No .attach() call
|
const response = await supertest(app).post('/api/v1/users/profile/avatar'); // No .attach() call
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.message).toBe('No avatar file uploaded.');
|
expect(response.body.error.message).toBe('No avatar file uploaded.');
|
||||||
@@ -1114,7 +1114,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dummyImagePath = 'test-avatar.png';
|
const dummyImagePath = 'test-avatar.png';
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/profile/avatar')
|
.post('/api/v1/users/profile/avatar')
|
||||||
.attach('avatar', Buffer.from('dummy-image-content'), dummyImagePath);
|
.attach('avatar', Buffer.from('dummy-image-content'), dummyImagePath);
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -1127,7 +1127,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for a non-numeric address ID', async () => {
|
it('should return 400 for a non-numeric address ID', async () => {
|
||||||
const response = await supertest(app).get('/api/users/addresses/abc');
|
const response = await supertest(app).get('/api/v1/users/addresses/abc');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toContain('received NaN');
|
expect(response.body.error.details[0].message).toContain('received NaN');
|
||||||
});
|
});
|
||||||
@@ -1143,7 +1143,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const mockCreatedRecipe = createMockRecipe({ recipe_id: 1, ...recipeData });
|
const mockCreatedRecipe = createMockRecipe({ recipe_id: 1, ...recipeData });
|
||||||
vi.mocked(db.recipeRepo.createRecipe).mockResolvedValue(mockCreatedRecipe);
|
vi.mocked(db.recipeRepo.createRecipe).mockResolvedValue(mockCreatedRecipe);
|
||||||
|
|
||||||
const response = await supertest(app).post('/api/users/recipes').send(recipeData);
|
const response = await supertest(app).post('/api/v1/users/recipes').send(recipeData);
|
||||||
|
|
||||||
expect(response.status).toBe(201);
|
expect(response.status).toBe(201);
|
||||||
expect(response.body.data).toEqual(mockCreatedRecipe);
|
expect(response.body.data).toEqual(mockCreatedRecipe);
|
||||||
@@ -1163,7 +1163,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
description: 'A delicious test recipe',
|
description: 'A delicious test recipe',
|
||||||
instructions: 'Mix everything together',
|
instructions: 'Mix everything together',
|
||||||
};
|
};
|
||||||
const response = await supertest(app).post('/api/users/recipes').send(recipeData);
|
const response = await supertest(app).post('/api/v1/users/recipes').send(recipeData);
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalled();
|
expect(logger.error).toHaveBeenCalled();
|
||||||
@@ -1171,7 +1171,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
|
|
||||||
it("DELETE /recipes/:recipeId should delete a user's own recipe", async () => {
|
it("DELETE /recipes/:recipeId should delete a user's own recipe", async () => {
|
||||||
vi.mocked(db.recipeRepo.deleteRecipe).mockResolvedValue(undefined);
|
vi.mocked(db.recipeRepo.deleteRecipe).mockResolvedValue(undefined);
|
||||||
const response = await supertest(app).delete('/api/users/recipes/1');
|
const response = await supertest(app).delete('/api/v1/users/recipes/1');
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
expect(db.recipeRepo.deleteRecipe).toHaveBeenCalledWith(
|
expect(db.recipeRepo.deleteRecipe).toHaveBeenCalledWith(
|
||||||
1,
|
1,
|
||||||
@@ -1184,7 +1184,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
it('DELETE /recipes/:recipeId should return 500 on a generic database error', async () => {
|
it('DELETE /recipes/:recipeId should return 500 on a generic database error', async () => {
|
||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.recipeRepo.deleteRecipe).mockRejectedValue(dbError);
|
vi.mocked(db.recipeRepo.deleteRecipe).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).delete('/api/users/recipes/1');
|
const response = await supertest(app).delete('/api/v1/users/recipes/1');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalled();
|
expect(logger.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -1193,13 +1193,13 @@ describe('User Routes (/api/users)', () => {
|
|||||||
vi.mocked(db.recipeRepo.deleteRecipe).mockRejectedValue(
|
vi.mocked(db.recipeRepo.deleteRecipe).mockRejectedValue(
|
||||||
new NotFoundError('Recipe not found'),
|
new NotFoundError('Recipe not found'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).delete('/api/users/recipes/999');
|
const response = await supertest(app).delete('/api/v1/users/recipes/999');
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toBe('Recipe not found');
|
expect(response.body.error.message).toBe('Recipe not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('DELETE /recipes/:recipeId should return 400 for invalid recipe ID', async () => {
|
it('DELETE /recipes/:recipeId should return 400 for invalid recipe ID', async () => {
|
||||||
const response = await supertest(app).delete('/api/users/recipes/abc');
|
const response = await supertest(app).delete('/api/v1/users/recipes/abc');
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toContain('received NaN');
|
expect(response.body.error.details[0].message).toContain('received NaN');
|
||||||
});
|
});
|
||||||
@@ -1209,7 +1209,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const mockUpdatedRecipe = createMockRecipe({ recipe_id: 1, ...updates });
|
const mockUpdatedRecipe = createMockRecipe({ recipe_id: 1, ...updates });
|
||||||
vi.mocked(db.recipeRepo.updateRecipe).mockResolvedValue(mockUpdatedRecipe);
|
vi.mocked(db.recipeRepo.updateRecipe).mockResolvedValue(mockUpdatedRecipe);
|
||||||
|
|
||||||
const response = await supertest(app).put('/api/users/recipes/1').send(updates);
|
const response = await supertest(app).put('/api/v1/users/recipes/1').send(updates);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockUpdatedRecipe);
|
expect(response.body.data).toEqual(mockUpdatedRecipe);
|
||||||
@@ -1224,7 +1224,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
it('PUT /recipes/:recipeId should return 404 if recipe not found', async () => {
|
it('PUT /recipes/:recipeId should return 404 if recipe not found', async () => {
|
||||||
vi.mocked(db.recipeRepo.updateRecipe).mockRejectedValue(new NotFoundError('not found'));
|
vi.mocked(db.recipeRepo.updateRecipe).mockRejectedValue(new NotFoundError('not found'));
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/recipes/999')
|
.put('/api/v1/users/recipes/999')
|
||||||
.send({ name: 'New Name' });
|
.send({ name: 'New Name' });
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
});
|
});
|
||||||
@@ -1233,21 +1233,21 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.recipeRepo.updateRecipe).mockRejectedValue(dbError);
|
vi.mocked(db.recipeRepo.updateRecipe).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/recipes/1')
|
.put('/api/v1/users/recipes/1')
|
||||||
.send({ name: 'New Name' });
|
.send({ name: 'New Name' });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalled();
|
expect(logger.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('PUT /recipes/:recipeId should return 400 if no update fields are provided', async () => {
|
it('PUT /recipes/:recipeId should return 400 if no update fields are provided', async () => {
|
||||||
const response = await supertest(app).put('/api/users/recipes/1').send({});
|
const response = await supertest(app).put('/api/v1/users/recipes/1').send({});
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toBe('No fields provided to update.');
|
expect(response.body.error.details[0].message).toBe('No fields provided to update.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('PUT /recipes/:recipeId should return 400 for invalid recipe ID', async () => {
|
it('PUT /recipes/:recipeId should return 400 for invalid recipe ID', async () => {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/recipes/abc')
|
.put('/api/v1/users/recipes/abc')
|
||||||
.send({ name: 'New Name' });
|
.send({ name: 'New Name' });
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.error.details[0].message).toContain('received NaN');
|
expect(response.body.error.details[0].message).toContain('received NaN');
|
||||||
@@ -1257,7 +1257,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
vi.mocked(db.shoppingRepo.getShoppingListById).mockRejectedValue(
|
vi.mocked(db.shoppingRepo.getShoppingListById).mockRejectedValue(
|
||||||
new NotFoundError('Shopping list not found'),
|
new NotFoundError('Shopping list not found'),
|
||||||
);
|
);
|
||||||
const response = await supertest(app).get('/api/users/shopping-lists/999');
|
const response = await supertest(app).get('/api/v1/users/shopping-lists/999');
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.error.message).toBe('Shopping list not found');
|
expect(response.body.error.message).toBe('Shopping list not found');
|
||||||
});
|
});
|
||||||
@@ -1268,7 +1268,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
user_id: mockUserProfile.user.user_id,
|
user_id: mockUserProfile.user.user_id,
|
||||||
});
|
});
|
||||||
vi.mocked(db.shoppingRepo.getShoppingListById).mockResolvedValue(mockList);
|
vi.mocked(db.shoppingRepo.getShoppingListById).mockResolvedValue(mockList);
|
||||||
const response = await supertest(app).get('/api/users/shopping-lists/1');
|
const response = await supertest(app).get('/api/v1/users/shopping-lists/1');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toEqual(mockList);
|
expect(response.body.data).toEqual(mockList);
|
||||||
expect(db.shoppingRepo.getShoppingListById).toHaveBeenCalledWith(
|
expect(db.shoppingRepo.getShoppingListById).toHaveBeenCalledWith(
|
||||||
@@ -1281,7 +1281,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
it('GET /shopping-lists/:listId should return 500 on a generic database error', async () => {
|
it('GET /shopping-lists/:listId should return 500 on a generic database error', async () => {
|
||||||
const dbError = new Error('DB Connection Failed');
|
const dbError = new Error('DB Connection Failed');
|
||||||
vi.mocked(db.shoppingRepo.getShoppingListById).mockRejectedValue(dbError);
|
vi.mocked(db.shoppingRepo.getShoppingListById).mockRejectedValue(dbError);
|
||||||
const response = await supertest(app).get('/api/users/shopping-lists/1');
|
const response = await supertest(app).get('/api/v1/users/shopping-lists/1');
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(logger.error).toHaveBeenCalled();
|
expect(logger.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -1305,7 +1305,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
vi.mocked(db.userRepo.updateUserProfile).mockResolvedValue(mockUserProfile);
|
vi.mocked(db.userRepo.updateUserProfile).mockResolvedValue(mockUserProfile);
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/profile')
|
.put('/api/v1/users/profile')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ full_name: 'Rate Limit Test' });
|
.send({ full_name: 'Rate Limit Test' });
|
||||||
|
|
||||||
@@ -1321,7 +1321,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
// Consume the limit
|
// Consume the limit
|
||||||
for (let i = 0; i < limit; i++) {
|
for (let i = 0; i < limit; i++) {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/profile/password')
|
.put('/api/v1/users/profile/password')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ newPassword: 'StrongPassword123!' });
|
.send({ newPassword: 'StrongPassword123!' });
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -1329,7 +1329,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
|
|
||||||
// Next request should be blocked
|
// Next request should be blocked
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.put('/api/users/profile/password')
|
.put('/api/v1/users/profile/password')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ newPassword: 'StrongPassword123!' });
|
.send({ newPassword: 'StrongPassword123!' });
|
||||||
|
|
||||||
@@ -1342,7 +1342,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
const dummyImagePath = 'test-avatar.png';
|
const dummyImagePath = 'test-avatar.png';
|
||||||
|
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.post('/api/users/profile/avatar')
|
.post('/api/v1/users/profile/avatar')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.attach('avatar', Buffer.from('dummy-image-content'), dummyImagePath);
|
.attach('avatar', Buffer.from('dummy-image-content'), dummyImagePath);
|
||||||
|
|
||||||
@@ -1361,7 +1361,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
// Consume the limit
|
// Consume the limit
|
||||||
for (let i = 0; i < limit; i++) {
|
for (let i = 0; i < limit; i++) {
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.delete('/api/users/account')
|
.delete('/api/v1/users/account')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ password: 'correct-password' });
|
.send({ password: 'correct-password' });
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -1369,7 +1369,7 @@ describe('User Routes (/api/users)', () => {
|
|||||||
|
|
||||||
// Next request should be blocked
|
// Next request should be blocked
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.delete('/api/users/account')
|
.delete('/api/v1/users/account')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ password: 'correct-password' });
|
.send({ password: 'correct-password' });
|
||||||
|
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ router.delete(
|
|||||||
* description: Unauthorized - invalid or missing token
|
* description: Unauthorized - invalid or missing token
|
||||||
*/
|
*/
|
||||||
router.get('/profile', validateRequest(emptySchema), async (req, res, next: NextFunction) => {
|
router.get('/profile', validateRequest(emptySchema), async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] GET /api/users/profile - ENTER`);
|
req.log.debug(`[ROUTE] GET /api/v1/users/profile - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
try {
|
try {
|
||||||
req.log.debug(
|
req.log.debug(
|
||||||
@@ -437,7 +437,7 @@ router.get('/profile', validateRequest(emptySchema), async (req, res, next: Next
|
|||||||
);
|
);
|
||||||
sendSuccess(res, fullUserProfile);
|
sendSuccess(res, fullUserProfile);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
req.log.error({ error }, `[ROUTE] GET /api/users/profile - ERROR`);
|
req.log.error({ error }, `[ROUTE] GET /api/v1/users/profile - ERROR`);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -483,7 +483,7 @@ router.put(
|
|||||||
userUpdateLimiter,
|
userUpdateLimiter,
|
||||||
validateRequest(updateProfileSchema),
|
validateRequest(updateProfileSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] PUT /api/users/profile - ENTER`);
|
req.log.debug(`[ROUTE] PUT /api/v1/users/profile - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
// Apply ADR-003 pattern for type safety
|
// Apply ADR-003 pattern for type safety
|
||||||
const { body } = req as unknown as UpdateProfileRequest;
|
const { body } = req as unknown as UpdateProfileRequest;
|
||||||
@@ -495,7 +495,7 @@ router.put(
|
|||||||
);
|
);
|
||||||
sendSuccess(res, updatedProfile);
|
sendSuccess(res, updatedProfile);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
req.log.error({ error }, `[ROUTE] PUT /api/users/profile - ERROR`);
|
req.log.error({ error }, `[ROUTE] PUT /api/v1/users/profile - ERROR`);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -541,7 +541,7 @@ router.put(
|
|||||||
userSensitiveUpdateLimiter,
|
userSensitiveUpdateLimiter,
|
||||||
validateRequest(updatePasswordSchema),
|
validateRequest(updatePasswordSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] PUT /api/users/profile/password - ENTER`);
|
req.log.debug(`[ROUTE] PUT /api/v1/users/profile/password - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
// Apply ADR-003 pattern for type safety
|
// Apply ADR-003 pattern for type safety
|
||||||
const { body } = req as unknown as UpdatePasswordRequest;
|
const { body } = req as unknown as UpdatePasswordRequest;
|
||||||
@@ -550,7 +550,7 @@ router.put(
|
|||||||
await userService.updateUserPassword(userProfile.user.user_id, body.newPassword, req.log);
|
await userService.updateUserPassword(userProfile.user.user_id, body.newPassword, req.log);
|
||||||
sendSuccess(res, { message: 'Password updated successfully.' });
|
sendSuccess(res, { message: 'Password updated successfully.' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
req.log.error({ error }, `[ROUTE] PUT /api/users/profile/password - ERROR`);
|
req.log.error({ error }, `[ROUTE] PUT /api/v1/users/profile/password - ERROR`);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -593,7 +593,7 @@ router.delete(
|
|||||||
userSensitiveUpdateLimiter,
|
userSensitiveUpdateLimiter,
|
||||||
validateRequest(deleteAccountSchema),
|
validateRequest(deleteAccountSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] DELETE /api/users/account - ENTER`);
|
req.log.debug(`[ROUTE] DELETE /api/v1/users/account - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
// Apply ADR-003 pattern for type safety
|
// Apply ADR-003 pattern for type safety
|
||||||
const { body } = req as unknown as DeleteAccountRequest;
|
const { body } = req as unknown as DeleteAccountRequest;
|
||||||
@@ -602,7 +602,7 @@ router.delete(
|
|||||||
await userService.deleteUserAccount(userProfile.user.user_id, body.password, req.log);
|
await userService.deleteUserAccount(userProfile.user.user_id, body.password, req.log);
|
||||||
sendSuccess(res, { message: 'Account deleted successfully.' });
|
sendSuccess(res, { message: 'Account deleted successfully.' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
req.log.error({ error }, `[ROUTE] DELETE /api/users/account - ERROR`);
|
req.log.error({ error }, `[ROUTE] DELETE /api/v1/users/account - ERROR`);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -628,13 +628,13 @@ router.delete(
|
|||||||
* description: Unauthorized - invalid or missing token
|
* description: Unauthorized - invalid or missing token
|
||||||
*/
|
*/
|
||||||
router.get('/watched-items', validateRequest(emptySchema), async (req, res, next: NextFunction) => {
|
router.get('/watched-items', validateRequest(emptySchema), async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] GET /api/users/watched-items - ENTER`);
|
req.log.debug(`[ROUTE] GET /api/v1/users/watched-items - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
try {
|
try {
|
||||||
const items = await db.personalizationRepo.getWatchedItems(userProfile.user.user_id, req.log);
|
const items = await db.personalizationRepo.getWatchedItems(userProfile.user.user_id, req.log);
|
||||||
sendSuccess(res, items);
|
sendSuccess(res, items);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
req.log.error({ error }, `[ROUTE] GET /api/users/watched-items - ERROR`);
|
req.log.error({ error }, `[ROUTE] GET /api/v1/users/watched-items - ERROR`);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -682,7 +682,7 @@ router.post(
|
|||||||
userUpdateLimiter,
|
userUpdateLimiter,
|
||||||
validateRequest(addWatchedItemSchema),
|
validateRequest(addWatchedItemSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] POST /api/users/watched-items - ENTER`);
|
req.log.debug(`[ROUTE] POST /api/v1/users/watched-items - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
// Apply ADR-003 pattern for type safety
|
// Apply ADR-003 pattern for type safety
|
||||||
const { body } = req as unknown as AddWatchedItemRequest;
|
const { body } = req as unknown as AddWatchedItemRequest;
|
||||||
@@ -735,7 +735,7 @@ router.delete(
|
|||||||
userUpdateLimiter,
|
userUpdateLimiter,
|
||||||
validateRequest(watchedItemIdSchema),
|
validateRequest(watchedItemIdSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] DELETE /api/users/watched-items/:masterItemId - ENTER`);
|
req.log.debug(`[ROUTE] DELETE /api/v1/users/watched-items/:masterItemId - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
// Apply ADR-003 pattern for type safety
|
// Apply ADR-003 pattern for type safety
|
||||||
const { params } = req as unknown as DeleteWatchedItemRequest;
|
const { params } = req as unknown as DeleteWatchedItemRequest;
|
||||||
@@ -747,7 +747,7 @@ router.delete(
|
|||||||
);
|
);
|
||||||
sendNoContent(res);
|
sendNoContent(res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
req.log.error({ error }, `[ROUTE] DELETE /api/users/watched-items/:masterItemId - ERROR`);
|
req.log.error({ error }, `[ROUTE] DELETE /api/v1/users/watched-items/:masterItemId - ERROR`);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -776,13 +776,13 @@ router.get(
|
|||||||
'/shopping-lists',
|
'/shopping-lists',
|
||||||
validateRequest(emptySchema),
|
validateRequest(emptySchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] GET /api/users/shopping-lists - ENTER`);
|
req.log.debug(`[ROUTE] GET /api/v1/users/shopping-lists - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
try {
|
try {
|
||||||
const lists = await db.shoppingRepo.getShoppingLists(userProfile.user.user_id, req.log);
|
const lists = await db.shoppingRepo.getShoppingLists(userProfile.user.user_id, req.log);
|
||||||
sendSuccess(res, lists);
|
sendSuccess(res, lists);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
req.log.error({ error }, `[ROUTE] GET /api/users/shopping-lists - ERROR`);
|
req.log.error({ error }, `[ROUTE] GET /api/v1/users/shopping-lists - ERROR`);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -822,7 +822,7 @@ router.get(
|
|||||||
'/shopping-lists/:listId',
|
'/shopping-lists/:listId',
|
||||||
validateRequest(shoppingListIdSchema),
|
validateRequest(shoppingListIdSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] GET /api/users/shopping-lists/:listId - ENTER`);
|
req.log.debug(`[ROUTE] GET /api/v1/users/shopping-lists/:listId - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
const { params } = req as unknown as GetShoppingListRequest;
|
const { params } = req as unknown as GetShoppingListRequest;
|
||||||
try {
|
try {
|
||||||
@@ -835,7 +835,7 @@ router.get(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
req.log.error(
|
req.log.error(
|
||||||
{ error, listId: params.listId },
|
{ error, listId: params.listId },
|
||||||
`[ROUTE] GET /api/users/shopping-lists/:listId - ERROR`,
|
`[ROUTE] GET /api/v1/users/shopping-lists/:listId - ERROR`,
|
||||||
);
|
);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -881,7 +881,7 @@ router.post(
|
|||||||
userUpdateLimiter,
|
userUpdateLimiter,
|
||||||
validateRequest(createShoppingListSchema),
|
validateRequest(createShoppingListSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] POST /api/users/shopping-lists - ENTER`);
|
req.log.debug(`[ROUTE] POST /api/v1/users/shopping-lists - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
// Apply ADR-003 pattern for type safety
|
// Apply ADR-003 pattern for type safety
|
||||||
const { body } = req as unknown as CreateShoppingListRequest;
|
const { body } = req as unknown as CreateShoppingListRequest;
|
||||||
@@ -931,7 +931,7 @@ router.delete(
|
|||||||
userUpdateLimiter,
|
userUpdateLimiter,
|
||||||
validateRequest(shoppingListIdSchema),
|
validateRequest(shoppingListIdSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] DELETE /api/users/shopping-lists/:listId - ENTER`);
|
req.log.debug(`[ROUTE] DELETE /api/v1/users/shopping-lists/:listId - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
// Apply ADR-003 pattern for type safety
|
// Apply ADR-003 pattern for type safety
|
||||||
const { params } = req as unknown as GetShoppingListRequest;
|
const { params } = req as unknown as GetShoppingListRequest;
|
||||||
@@ -942,7 +942,7 @@ router.delete(
|
|||||||
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
|
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
|
||||||
req.log.error(
|
req.log.error(
|
||||||
{ errorMessage, params: req.params },
|
{ errorMessage, params: req.params },
|
||||||
`[ROUTE] DELETE /api/users/shopping-lists/:listId - ERROR`,
|
`[ROUTE] DELETE /api/v1/users/shopping-lists/:listId - ERROR`,
|
||||||
);
|
);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -1012,7 +1012,7 @@ router.post(
|
|||||||
userUpdateLimiter,
|
userUpdateLimiter,
|
||||||
validateRequest(addShoppingListItemSchema),
|
validateRequest(addShoppingListItemSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] POST /api/users/shopping-lists/:listId/items - ENTER`);
|
req.log.debug(`[ROUTE] POST /api/v1/users/shopping-lists/:listId/items - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
// Apply ADR-003 pattern for type safety
|
// Apply ADR-003 pattern for type safety
|
||||||
const { params, body } = req as unknown as AddShoppingListItemRequest;
|
const { params, body } = req as unknown as AddShoppingListItemRequest;
|
||||||
@@ -1097,7 +1097,7 @@ router.put(
|
|||||||
userUpdateLimiter,
|
userUpdateLimiter,
|
||||||
validateRequest(updateShoppingListItemSchema),
|
validateRequest(updateShoppingListItemSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] PUT /api/users/shopping-lists/items/:itemId - ENTER`);
|
req.log.debug(`[ROUTE] PUT /api/v1/users/shopping-lists/items/:itemId - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
// Apply ADR-003 pattern for type safety
|
// Apply ADR-003 pattern for type safety
|
||||||
const { params, body } = req as unknown as UpdateShoppingListItemRequest;
|
const { params, body } = req as unknown as UpdateShoppingListItemRequest;
|
||||||
@@ -1112,7 +1112,7 @@ router.put(
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
req.log.error(
|
req.log.error(
|
||||||
{ error, params: req.params, body: req.body },
|
{ error, params: req.params, body: req.body },
|
||||||
`[ROUTE] PUT /api/users/shopping-lists/items/:itemId - ERROR`,
|
`[ROUTE] PUT /api/v1/users/shopping-lists/items/:itemId - ERROR`,
|
||||||
);
|
);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -1150,7 +1150,7 @@ router.delete(
|
|||||||
userUpdateLimiter,
|
userUpdateLimiter,
|
||||||
validateRequest(shoppingListItemIdSchema),
|
validateRequest(shoppingListItemIdSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] DELETE /api/users/shopping-lists/items/:itemId - ENTER`);
|
req.log.debug(`[ROUTE] DELETE /api/v1/users/shopping-lists/items/:itemId - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
// Apply ADR-003 pattern for type safety
|
// Apply ADR-003 pattern for type safety
|
||||||
const { params } = req as unknown as DeleteShoppingListItemRequest;
|
const { params } = req as unknown as DeleteShoppingListItemRequest;
|
||||||
@@ -1164,7 +1164,7 @@ router.delete(
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
req.log.error(
|
req.log.error(
|
||||||
{ error, params: req.params },
|
{ error, params: req.params },
|
||||||
`[ROUTE] DELETE /api/users/shopping-lists/items/:itemId - ERROR`,
|
`[ROUTE] DELETE /api/v1/users/shopping-lists/items/:itemId - ERROR`,
|
||||||
);
|
);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -1207,7 +1207,7 @@ router.put(
|
|||||||
userUpdateLimiter,
|
userUpdateLimiter,
|
||||||
validateRequest(updatePreferencesSchema),
|
validateRequest(updatePreferencesSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] PUT /api/users/profile/preferences - ENTER`);
|
req.log.debug(`[ROUTE] PUT /api/v1/users/profile/preferences - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
// Apply ADR-003 pattern for type safety
|
// Apply ADR-003 pattern for type safety
|
||||||
const { body } = req as unknown as UpdatePreferencesRequest;
|
const { body } = req as unknown as UpdatePreferencesRequest;
|
||||||
@@ -1219,7 +1219,7 @@ router.put(
|
|||||||
);
|
);
|
||||||
sendSuccess(res, updatedProfile);
|
sendSuccess(res, updatedProfile);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
req.log.error({ error }, `[ROUTE] PUT /api/users/profile/preferences - ERROR`);
|
req.log.error({ error }, `[ROUTE] PUT /api/v1/users/profile/preferences - ERROR`);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1248,7 +1248,7 @@ router.get(
|
|||||||
'/me/dietary-restrictions',
|
'/me/dietary-restrictions',
|
||||||
validateRequest(emptySchema),
|
validateRequest(emptySchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] GET /api/users/me/dietary-restrictions - ENTER`);
|
req.log.debug(`[ROUTE] GET /api/v1/users/me/dietary-restrictions - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
try {
|
try {
|
||||||
const restrictions = await db.personalizationRepo.getUserDietaryRestrictions(
|
const restrictions = await db.personalizationRepo.getUserDietaryRestrictions(
|
||||||
@@ -1257,7 +1257,7 @@ router.get(
|
|||||||
);
|
);
|
||||||
sendSuccess(res, restrictions);
|
sendSuccess(res, restrictions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
req.log.error({ error }, `[ROUTE] GET /api/users/me/dietary-restrictions - ERROR`);
|
req.log.error({ error }, `[ROUTE] GET /api/v1/users/me/dietary-restrictions - ERROR`);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1303,7 +1303,7 @@ router.put(
|
|||||||
userUpdateLimiter,
|
userUpdateLimiter,
|
||||||
validateRequest(setUserRestrictionsSchema),
|
validateRequest(setUserRestrictionsSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] PUT /api/users/me/dietary-restrictions - ENTER`);
|
req.log.debug(`[ROUTE] PUT /api/v1/users/me/dietary-restrictions - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
// Apply ADR-003 pattern for type safety
|
// Apply ADR-003 pattern for type safety
|
||||||
const { body } = req as unknown as SetUserRestrictionsRequest;
|
const { body } = req as unknown as SetUserRestrictionsRequest;
|
||||||
@@ -1344,7 +1344,7 @@ router.put(
|
|||||||
* description: Unauthorized - invalid or missing token
|
* description: Unauthorized - invalid or missing token
|
||||||
*/
|
*/
|
||||||
router.get('/me/appliances', validateRequest(emptySchema), async (req, res, next: NextFunction) => {
|
router.get('/me/appliances', validateRequest(emptySchema), async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] GET /api/users/me/appliances - ENTER`);
|
req.log.debug(`[ROUTE] GET /api/v1/users/me/appliances - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
try {
|
try {
|
||||||
const appliances = await db.personalizationRepo.getUserAppliances(
|
const appliances = await db.personalizationRepo.getUserAppliances(
|
||||||
@@ -1353,7 +1353,7 @@ router.get('/me/appliances', validateRequest(emptySchema), async (req, res, next
|
|||||||
);
|
);
|
||||||
sendSuccess(res, appliances);
|
sendSuccess(res, appliances);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
req.log.error({ error }, `[ROUTE] GET /api/users/me/appliances - ERROR`);
|
req.log.error({ error }, `[ROUTE] GET /api/v1/users/me/appliances - ERROR`);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1398,7 +1398,7 @@ router.put(
|
|||||||
userUpdateLimiter,
|
userUpdateLimiter,
|
||||||
validateRequest(setUserAppliancesSchema),
|
validateRequest(setUserAppliancesSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] PUT /api/users/me/appliances - ENTER`);
|
req.log.debug(`[ROUTE] PUT /api/v1/users/me/appliances - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
// Apply ADR-003 pattern for type safety
|
// Apply ADR-003 pattern for type safety
|
||||||
const { body } = req as unknown as SetUserAppliancesRequest;
|
const { body } = req as unknown as SetUserAppliancesRequest;
|
||||||
@@ -1654,7 +1654,7 @@ router.delete(
|
|||||||
userUpdateLimiter,
|
userUpdateLimiter,
|
||||||
validateRequest(recipeIdSchema),
|
validateRequest(recipeIdSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] DELETE /api/users/recipes/:recipeId - ENTER`);
|
req.log.debug(`[ROUTE] DELETE /api/v1/users/recipes/:recipeId - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
// Apply ADR-003 pattern for type safety
|
// Apply ADR-003 pattern for type safety
|
||||||
const { params } = req as unknown as DeleteRecipeRequest;
|
const { params } = req as unknown as DeleteRecipeRequest;
|
||||||
@@ -1664,7 +1664,7 @@ router.delete(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
req.log.error(
|
req.log.error(
|
||||||
{ error, params: req.params },
|
{ error, params: req.params },
|
||||||
`[ROUTE] DELETE /api/users/recipes/:recipeId - ERROR`,
|
`[ROUTE] DELETE /api/v1/users/recipes/:recipeId - ERROR`,
|
||||||
);
|
);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -1749,7 +1749,7 @@ router.put(
|
|||||||
userUpdateLimiter,
|
userUpdateLimiter,
|
||||||
validateRequest(updateRecipeSchema),
|
validateRequest(updateRecipeSchema),
|
||||||
async (req, res, next: NextFunction) => {
|
async (req, res, next: NextFunction) => {
|
||||||
req.log.debug(`[ROUTE] PUT /api/users/recipes/:recipeId - ENTER`);
|
req.log.debug(`[ROUTE] PUT /api/v1/users/recipes/:recipeId - ENTER`);
|
||||||
const userProfile = req.user as UserProfile;
|
const userProfile = req.user as UserProfile;
|
||||||
// Apply ADR-003 pattern for type safety
|
// Apply ADR-003 pattern for type safety
|
||||||
const { params, body } = req as unknown as UpdateRecipeRequest;
|
const { params, body } = req as unknown as UpdateRecipeRequest;
|
||||||
@@ -1765,7 +1765,7 @@ router.put(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
req.log.error(
|
req.log.error(
|
||||||
{ error, params: req.params, body: req.body },
|
{ error, params: req.params, body: req.body },
|
||||||
`[ROUTE] PUT /api/users/recipes/:recipeId - ERROR`,
|
`[ROUTE] PUT /api/v1/users/recipes/:recipeId - ERROR`,
|
||||||
);
|
);
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ describe('API Client', () => {
|
|||||||
const mockFile = new File(['logo-content'], 'store-logo.png', { type: 'image/png' });
|
const mockFile = new File(['logo-content'], 'store-logo.png', { type: 'image/png' });
|
||||||
await apiClient.uploadLogoAndUpdateStore(1, mockFile);
|
await apiClient.uploadLogoAndUpdateStore(1, mockFile);
|
||||||
|
|
||||||
expect(capturedUrl?.pathname).toBe('/api/stores/1/logo');
|
expect(capturedUrl?.pathname).toBe('/api/v1/stores/1/logo');
|
||||||
expect(capturedBody).toBeInstanceOf(FormData);
|
expect(capturedBody).toBeInstanceOf(FormData);
|
||||||
const uploadedFile = (capturedBody as FormData).get('logoImage') as File;
|
const uploadedFile = (capturedBody as FormData).get('logoImage') as File;
|
||||||
expect(uploadedFile.name).toBe('store-logo.png');
|
expect(uploadedFile.name).toBe('store-logo.png');
|
||||||
@@ -297,7 +297,7 @@ describe('API Client', () => {
|
|||||||
});
|
});
|
||||||
await apiClient.uploadBrandLogo(2, mockFile);
|
await apiClient.uploadBrandLogo(2, mockFile);
|
||||||
|
|
||||||
expect(capturedUrl?.pathname).toBe('/api/admin/brands/2/logo');
|
expect(capturedUrl?.pathname).toBe('/api/v1/admin/brands/2/logo');
|
||||||
expect(capturedBody).toBeInstanceOf(FormData);
|
expect(capturedBody).toBeInstanceOf(FormData);
|
||||||
const uploadedFile = (capturedBody as FormData).get('logoImage') as File;
|
const uploadedFile = (capturedBody as FormData).get('logoImage') as File;
|
||||||
expect(uploadedFile.name).toBe('brand-logo.svg');
|
expect(uploadedFile.name).toBe('brand-logo.svg');
|
||||||
@@ -311,25 +311,25 @@ describe('API Client', () => {
|
|||||||
|
|
||||||
it('getAuthenticatedUserProfile should call the correct endpoint', async () => {
|
it('getAuthenticatedUserProfile should call the correct endpoint', async () => {
|
||||||
await apiClient.getAuthenticatedUserProfile();
|
await apiClient.getAuthenticatedUserProfile();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/profile');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/profile');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetchWatchedItems should call the correct endpoint', async () => {
|
it('fetchWatchedItems should call the correct endpoint', async () => {
|
||||||
await apiClient.fetchWatchedItems();
|
await apiClient.fetchWatchedItems();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/watched-items');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/watched-items');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('addWatchedItem should send a POST request with the correct body', async () => {
|
it('addWatchedItem should send a POST request with the correct body', async () => {
|
||||||
const watchedItemData = { itemName: 'Apples', category_id: 5 };
|
const watchedItemData = { itemName: 'Apples', category_id: 5 };
|
||||||
await apiClient.addWatchedItem(watchedItemData.itemName, watchedItemData.category_id);
|
await apiClient.addWatchedItem(watchedItemData.itemName, watchedItemData.category_id);
|
||||||
|
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/watched-items');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/watched-items');
|
||||||
expect(capturedBody).toEqual(watchedItemData);
|
expect(capturedBody).toEqual(watchedItemData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removeWatchedItem should send a DELETE request to the correct URL', async () => {
|
it('removeWatchedItem should send a DELETE request to the correct URL', async () => {
|
||||||
await apiClient.removeWatchedItem(99);
|
await apiClient.removeWatchedItem(99);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/watched-items/99');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/watched-items/99');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -337,7 +337,7 @@ describe('API Client', () => {
|
|||||||
it('getBudgets should call the correct endpoint', async () => {
|
it('getBudgets should call the correct endpoint', async () => {
|
||||||
server.use(http.get('http://localhost/api/budgets', () => HttpResponse.json([])));
|
server.use(http.get('http://localhost/api/budgets', () => HttpResponse.json([])));
|
||||||
await apiClient.getBudgets();
|
await apiClient.getBudgets();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/budgets');
|
expect(capturedUrl?.pathname).toBe('/api/v1/budgets');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('createBudget should send a POST request with budget data', async () => {
|
it('createBudget should send a POST request with budget data', async () => {
|
||||||
@@ -349,7 +349,7 @@ describe('API Client', () => {
|
|||||||
});
|
});
|
||||||
await apiClient.createBudget(budgetData);
|
await apiClient.createBudget(budgetData);
|
||||||
|
|
||||||
expect(capturedUrl?.pathname).toBe('/api/budgets');
|
expect(capturedUrl?.pathname).toBe('/api/v1/budgets');
|
||||||
expect(capturedBody).toEqual(budgetData);
|
expect(capturedBody).toEqual(budgetData);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -357,13 +357,13 @@ describe('API Client', () => {
|
|||||||
const budgetUpdates = { amount_cents: 60000 };
|
const budgetUpdates = { amount_cents: 60000 };
|
||||||
await apiClient.updateBudget(123, budgetUpdates);
|
await apiClient.updateBudget(123, budgetUpdates);
|
||||||
|
|
||||||
expect(capturedUrl?.pathname).toBe('/api/budgets/123');
|
expect(capturedUrl?.pathname).toBe('/api/v1/budgets/123');
|
||||||
expect(capturedBody).toEqual(budgetUpdates);
|
expect(capturedBody).toEqual(budgetUpdates);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deleteBudget should send a DELETE request to the correct URL', async () => {
|
it('deleteBudget should send a DELETE request to the correct URL', async () => {
|
||||||
await apiClient.deleteBudget(456);
|
await apiClient.deleteBudget(456);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/budgets/456');
|
expect(capturedUrl?.pathname).toBe('/api/v1/budgets/456');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getSpendingAnalysis should send a GET request with correct query params', async () => {
|
it('getSpendingAnalysis should send a GET request with correct query params', async () => {
|
||||||
@@ -378,7 +378,7 @@ describe('API Client', () => {
|
|||||||
localStorage.setItem('authToken', 'gamify-token');
|
localStorage.setItem('authToken', 'gamify-token');
|
||||||
await apiClient.getUserAchievements();
|
await apiClient.getUserAchievements();
|
||||||
|
|
||||||
expect(capturedUrl?.pathname).toBe('/api/achievements/me');
|
expect(capturedUrl?.pathname).toBe('/api/v1/achievements/me');
|
||||||
expect(capturedHeaders).not.toBeNull();
|
expect(capturedHeaders).not.toBeNull();
|
||||||
expect(capturedHeaders!.get('Authorization')).toBe('Bearer gamify-token');
|
expect(capturedHeaders!.get('Authorization')).toBe('Bearer gamify-token');
|
||||||
});
|
});
|
||||||
@@ -387,14 +387,14 @@ describe('API Client', () => {
|
|||||||
await apiClient.fetchLeaderboard(5);
|
await apiClient.fetchLeaderboard(5);
|
||||||
|
|
||||||
expect(capturedUrl).not.toBeNull(); // This assertion ensures capturedUrl is not null for the next line
|
expect(capturedUrl).not.toBeNull(); // This assertion ensures capturedUrl is not null for the next line
|
||||||
expect(capturedUrl!.pathname).toBe('/api/achievements/leaderboard');
|
expect(capturedUrl!.pathname).toBe('/api/v1/achievements/leaderboard');
|
||||||
expect(capturedUrl!.searchParams.get('limit')).toBe('5');
|
expect(capturedUrl!.searchParams.get('limit')).toBe('5');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getAchievements should call the public endpoint', async () => {
|
it('getAchievements should call the public endpoint', async () => {
|
||||||
// This is a public endpoint, so no token is needed.
|
// This is a public endpoint, so no token is needed.
|
||||||
await apiClient.getAchievements();
|
await apiClient.getAchievements();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/achievements');
|
expect(capturedUrl?.pathname).toBe('/api/v1/achievements');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uploadAvatar should send FormData with the avatar file', async () => {
|
it('uploadAvatar should send FormData with the avatar file', async () => {
|
||||||
@@ -415,20 +415,20 @@ describe('API Client', () => {
|
|||||||
await apiClient.getNotifications(10, 20);
|
await apiClient.getNotifications(10, 20);
|
||||||
|
|
||||||
expect(capturedUrl).not.toBeNull();
|
expect(capturedUrl).not.toBeNull();
|
||||||
expect(capturedUrl!.pathname).toBe('/api/users/notifications');
|
expect(capturedUrl!.pathname).toBe('/api/v1/users/notifications');
|
||||||
expect(capturedUrl!.searchParams.get('limit')).toBe('10');
|
expect(capturedUrl!.searchParams.get('limit')).toBe('10');
|
||||||
expect(capturedUrl!.searchParams.get('offset')).toBe('20');
|
expect(capturedUrl!.searchParams.get('offset')).toBe('20');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('markAllNotificationsAsRead should send a POST request', async () => {
|
it('markAllNotificationsAsRead should send a POST request', async () => {
|
||||||
await apiClient.markAllNotificationsAsRead();
|
await apiClient.markAllNotificationsAsRead();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/notifications/mark-all-read');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/notifications/mark-all-read');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('markNotificationAsRead should send a POST request to the correct URL', async () => {
|
it('markNotificationAsRead should send a POST request to the correct URL', async () => {
|
||||||
const notificationId = 123;
|
const notificationId = 123;
|
||||||
await apiClient.markNotificationAsRead(notificationId);
|
await apiClient.markNotificationAsRead(notificationId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/users/notifications/${notificationId}/mark-read`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/users/notifications/${notificationId}/mark-read`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -436,31 +436,31 @@ describe('API Client', () => {
|
|||||||
// The beforeEach was testing fetchShoppingLists, so we move that into its own test.
|
// The beforeEach was testing fetchShoppingLists, so we move that into its own test.
|
||||||
it('fetchShoppingLists should call the correct endpoint', async () => {
|
it('fetchShoppingLists should call the correct endpoint', async () => {
|
||||||
await apiClient.fetchShoppingLists();
|
await apiClient.fetchShoppingLists();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/shopping-lists');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/shopping-lists');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetchShoppingListById should call the correct endpoint', async () => {
|
it('fetchShoppingListById should call the correct endpoint', async () => {
|
||||||
const listId = 5;
|
const listId = 5;
|
||||||
await apiClient.fetchShoppingListById(listId);
|
await apiClient.fetchShoppingListById(listId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/users/shopping-lists/${listId}`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/users/shopping-lists/${listId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('createShoppingList should send a POST request with the list name', async () => {
|
it('createShoppingList should send a POST request with the list name', async () => {
|
||||||
await apiClient.createShoppingList('Weekly Groceries');
|
await apiClient.createShoppingList('Weekly Groceries');
|
||||||
|
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/shopping-lists');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/shopping-lists');
|
||||||
expect(capturedBody).toEqual({ name: 'Weekly Groceries' });
|
expect(capturedBody).toEqual({ name: 'Weekly Groceries' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deleteShoppingList should send a DELETE request to the correct URL', async () => {
|
it('deleteShoppingList should send a DELETE request to the correct URL', async () => {
|
||||||
const listId = 42;
|
const listId = 42;
|
||||||
server.use(
|
server.use(
|
||||||
http.delete(`http://localhost/api/users/shopping-lists/${listId}`, () => {
|
http.delete(`http://localhost/api/v1/users/shopping-lists/${listId}`, () => {
|
||||||
return new HttpResponse(null, { status: 204 });
|
return new HttpResponse(null, { status: 204 });
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await apiClient.deleteShoppingList(listId);
|
await apiClient.deleteShoppingList(listId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/users/shopping-lists/${listId}`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/users/shopping-lists/${listId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('addShoppingListItem should send a POST request with item data', async () => {
|
it('addShoppingListItem should send a POST request with item data', async () => {
|
||||||
@@ -468,7 +468,7 @@ describe('API Client', () => {
|
|||||||
const itemData = createMockShoppingListItemPayload({ customItemName: 'Paper Towels' });
|
const itemData = createMockShoppingListItemPayload({ customItemName: 'Paper Towels' });
|
||||||
await apiClient.addShoppingListItem(listId, itemData);
|
await apiClient.addShoppingListItem(listId, itemData);
|
||||||
|
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/users/shopping-lists/${listId}/items`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/users/shopping-lists/${listId}/items`);
|
||||||
expect(capturedBody).toEqual(itemData);
|
expect(capturedBody).toEqual(itemData);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -477,14 +477,14 @@ describe('API Client', () => {
|
|||||||
const updates = { is_purchased: true };
|
const updates = { is_purchased: true };
|
||||||
await apiClient.updateShoppingListItem(itemId, updates);
|
await apiClient.updateShoppingListItem(itemId, updates);
|
||||||
|
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/users/shopping-lists/items/${itemId}`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/users/shopping-lists/items/${itemId}`);
|
||||||
expect(capturedBody).toEqual(updates);
|
expect(capturedBody).toEqual(updates);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removeShoppingListItem should send a DELETE request to the correct URL', async () => {
|
it('removeShoppingListItem should send a DELETE request to the correct URL', async () => {
|
||||||
const itemId = 101;
|
const itemId = 101;
|
||||||
await apiClient.removeShoppingListItem(itemId);
|
await apiClient.removeShoppingListItem(itemId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/users/shopping-lists/items/${itemId}`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/users/shopping-lists/items/${itemId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('completeShoppingList should send a POST request with total spent', async () => {
|
it('completeShoppingList should send a POST request with total spent', async () => {
|
||||||
@@ -492,7 +492,7 @@ describe('API Client', () => {
|
|||||||
const totalSpentCents = 12345;
|
const totalSpentCents = 12345;
|
||||||
await apiClient.completeShoppingList(listId, totalSpentCents);
|
await apiClient.completeShoppingList(listId, totalSpentCents);
|
||||||
|
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/users/shopping-lists/${listId}/complete`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/users/shopping-lists/${listId}/complete`);
|
||||||
expect(capturedBody).toEqual({ totalSpentCents });
|
expect(capturedBody).toEqual({ totalSpentCents });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -505,48 +505,48 @@ describe('API Client', () => {
|
|||||||
|
|
||||||
it('getCompatibleRecipes should call the correct endpoint', async () => {
|
it('getCompatibleRecipes should call the correct endpoint', async () => {
|
||||||
await apiClient.getCompatibleRecipes();
|
await apiClient.getCompatibleRecipes();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/me/compatible-recipes');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/me/compatible-recipes');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('forkRecipe should send a POST request to the correct URL', async () => {
|
it('forkRecipe should send a POST request to the correct URL', async () => {
|
||||||
const recipeId = 99;
|
const recipeId = 99;
|
||||||
server.use(
|
server.use(
|
||||||
http.post(`http://localhost/api/recipes/${recipeId}/fork`, () => {
|
http.post(`http://localhost/api/v1/recipes/${recipeId}/fork`, () => {
|
||||||
return HttpResponse.json({ success: true });
|
return HttpResponse.json({ success: true });
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await apiClient.forkRecipe(recipeId);
|
await apiClient.forkRecipe(recipeId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/recipes/${recipeId}/fork`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/recipes/${recipeId}/fork`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getUserFavoriteRecipes should call the correct endpoint', async () => {
|
it('getUserFavoriteRecipes should call the correct endpoint', async () => {
|
||||||
await apiClient.getUserFavoriteRecipes();
|
await apiClient.getUserFavoriteRecipes();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/me/favorite-recipes');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/me/favorite-recipes');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('addFavoriteRecipe should send a POST request with the recipeId', async () => {
|
it('addFavoriteRecipe should send a POST request with the recipeId', async () => {
|
||||||
const recipeId = 123;
|
const recipeId = 123;
|
||||||
await apiClient.addFavoriteRecipe(recipeId);
|
await apiClient.addFavoriteRecipe(recipeId);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/me/favorite-recipes');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/me/favorite-recipes');
|
||||||
expect(capturedBody).toEqual({ recipeId });
|
expect(capturedBody).toEqual({ recipeId });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removeFavoriteRecipe should send a DELETE request to the correct URL', async () => {
|
it('removeFavoriteRecipe should send a DELETE request to the correct URL', async () => {
|
||||||
const recipeId = 123;
|
const recipeId = 123;
|
||||||
await apiClient.removeFavoriteRecipe(recipeId);
|
await apiClient.removeFavoriteRecipe(recipeId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/users/me/favorite-recipes/${recipeId}`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/users/me/favorite-recipes/${recipeId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getRecipeComments should call the public endpoint', async () => {
|
it('getRecipeComments should call the public endpoint', async () => {
|
||||||
const recipeId = 456;
|
const recipeId = 456;
|
||||||
await apiClient.getRecipeComments(recipeId);
|
await apiClient.getRecipeComments(recipeId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/recipes/${recipeId}/comments`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/recipes/${recipeId}/comments`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getRecipeById should call the correct public endpoint', async () => {
|
it('getRecipeById should call the correct public endpoint', async () => {
|
||||||
const recipeId = 789;
|
const recipeId = 789;
|
||||||
await apiClient.getRecipeById(recipeId);
|
await apiClient.getRecipeById(recipeId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/recipes/${recipeId}`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/recipes/${recipeId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('addRecipeComment should send a POST request with content and optional parentId', async () => {
|
it('addRecipeComment should send a POST request with content and optional parentId', async () => {
|
||||||
@@ -556,20 +556,20 @@ describe('API Client', () => {
|
|||||||
parentCommentId: 789,
|
parentCommentId: 789,
|
||||||
});
|
});
|
||||||
await apiClient.addRecipeComment(recipeId, commentData.content, commentData.parentCommentId);
|
await apiClient.addRecipeComment(recipeId, commentData.content, commentData.parentCommentId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/recipes/${recipeId}/comments`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/recipes/${recipeId}/comments`);
|
||||||
expect(capturedBody).toEqual(commentData);
|
expect(capturedBody).toEqual(commentData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deleteRecipe should send a DELETE request to the correct URL', async () => {
|
it('deleteRecipe should send a DELETE request to the correct URL', async () => {
|
||||||
const recipeId = 101;
|
const recipeId = 101;
|
||||||
await apiClient.deleteRecipe(recipeId);
|
await apiClient.deleteRecipe(recipeId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/recipes/${recipeId}`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/recipes/${recipeId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('suggestRecipe should send a POST request with ingredients', async () => {
|
it('suggestRecipe should send a POST request with ingredients', async () => {
|
||||||
const ingredients = ['chicken', 'rice'];
|
const ingredients = ['chicken', 'rice'];
|
||||||
await apiClient.suggestRecipe(ingredients);
|
await apiClient.suggestRecipe(ingredients);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/recipes/suggest');
|
expect(capturedUrl?.pathname).toBe('/api/v1/recipes/suggest');
|
||||||
expect(capturedBody).toEqual({ ingredients });
|
expect(capturedBody).toEqual({ ingredients });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -579,7 +579,7 @@ describe('API Client', () => {
|
|||||||
localStorage.setItem('authToken', 'user-settings-token');
|
localStorage.setItem('authToken', 'user-settings-token');
|
||||||
const profileData = createMockProfileUpdatePayload({ full_name: 'John Doe' });
|
const profileData = createMockProfileUpdatePayload({ full_name: 'John Doe' });
|
||||||
await apiClient.updateUserProfile(profileData, { tokenOverride: 'override-token' });
|
await apiClient.updateUserProfile(profileData, { tokenOverride: 'override-token' });
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/profile');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/profile');
|
||||||
expect(capturedBody).toEqual(profileData);
|
expect(capturedBody).toEqual(profileData);
|
||||||
expect(capturedHeaders!.get('Authorization')).toBe('Bearer override-token');
|
expect(capturedHeaders!.get('Authorization')).toBe('Bearer override-token');
|
||||||
});
|
});
|
||||||
@@ -587,7 +587,7 @@ describe('API Client', () => {
|
|||||||
it('updateUserPreferences should send a PUT request with preferences data', async () => {
|
it('updateUserPreferences should send a PUT request with preferences data', async () => {
|
||||||
const preferences = { darkMode: true };
|
const preferences = { darkMode: true };
|
||||||
await apiClient.updateUserPreferences(preferences);
|
await apiClient.updateUserPreferences(preferences);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/profile/preferences');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/profile/preferences');
|
||||||
expect(capturedBody).toEqual(preferences);
|
expect(capturedBody).toEqual(preferences);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -596,7 +596,7 @@ describe('API Client', () => {
|
|||||||
await apiClient.updateUserPassword(passwordData.newPassword, {
|
await apiClient.updateUserPassword(passwordData.newPassword, {
|
||||||
tokenOverride: 'pw-override-token',
|
tokenOverride: 'pw-override-token',
|
||||||
});
|
});
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/profile/password');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/profile/password');
|
||||||
expect(capturedBody).toEqual(passwordData);
|
expect(capturedBody).toEqual(passwordData);
|
||||||
expect(capturedHeaders!.get('Authorization')).toBe('Bearer pw-override-token');
|
expect(capturedHeaders!.get('Authorization')).toBe('Bearer pw-override-token');
|
||||||
});
|
});
|
||||||
@@ -604,18 +604,18 @@ describe('API Client', () => {
|
|||||||
it('updateUserPassword should send a PUT request with the new password', async () => {
|
it('updateUserPassword should send a PUT request with the new password', async () => {
|
||||||
const newPassword = 'new-secure-password';
|
const newPassword = 'new-secure-password';
|
||||||
await apiClient.updateUserPassword(newPassword);
|
await apiClient.updateUserPassword(newPassword);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/profile/password');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/profile/password');
|
||||||
expect(capturedBody).toEqual({ newPassword });
|
expect(capturedBody).toEqual({ newPassword });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('exportUserData should call the correct endpoint', async () => {
|
it('exportUserData should call the correct endpoint', async () => {
|
||||||
await apiClient.exportUserData();
|
await apiClient.exportUserData();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/data-export');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/data-export');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getUserFeed should call the correct endpoint with query params', async () => {
|
it('getUserFeed should call the correct endpoint with query params', async () => {
|
||||||
await apiClient.getUserFeed(10, 5);
|
await apiClient.getUserFeed(10, 5);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/feed');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/feed');
|
||||||
expect(capturedUrl!.searchParams.get('limit')).toBe('10');
|
expect(capturedUrl!.searchParams.get('limit')).toBe('10');
|
||||||
expect(capturedUrl!.searchParams.get('offset')).toBe('5');
|
expect(capturedUrl!.searchParams.get('offset')).toBe('5');
|
||||||
});
|
});
|
||||||
@@ -623,13 +623,13 @@ describe('API Client', () => {
|
|||||||
it('followUser should send a POST request to the correct URL', async () => {
|
it('followUser should send a POST request to the correct URL', async () => {
|
||||||
const userIdToFollow = 'user-to-follow';
|
const userIdToFollow = 'user-to-follow';
|
||||||
await apiClient.followUser(userIdToFollow);
|
await apiClient.followUser(userIdToFollow);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/users/${userIdToFollow}/follow`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/users/${userIdToFollow}/follow`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unfollowUser should send a DELETE request to the correct URL', async () => {
|
it('unfollowUser should send a DELETE request to the correct URL', async () => {
|
||||||
const userIdToUnfollow = 'user-to-unfollow';
|
const userIdToUnfollow = 'user-to-unfollow';
|
||||||
await apiClient.unfollowUser(userIdToUnfollow);
|
await apiClient.unfollowUser(userIdToUnfollow);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/users/${userIdToUnfollow}/follow`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/users/${userIdToUnfollow}/follow`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('registerUser should send a POST request with user data', async () => {
|
it('registerUser should send a POST request with user data', async () => {
|
||||||
@@ -639,21 +639,21 @@ describe('API Client', () => {
|
|||||||
full_name: 'Test User',
|
full_name: 'Test User',
|
||||||
});
|
});
|
||||||
await apiClient.registerUser(userData.email, userData.password, userData.full_name);
|
await apiClient.registerUser(userData.email, userData.password, userData.full_name);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/auth/register');
|
expect(capturedUrl?.pathname).toBe('/api/v1/auth/register');
|
||||||
expect(capturedBody).toEqual(userData);
|
expect(capturedBody).toEqual(userData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deleteUserAccount should send a DELETE request with the confirmation password', async () => {
|
it('deleteUserAccount should send a DELETE request with the confirmation password', async () => {
|
||||||
const passwordData = { password: 'current-password-for-confirmation' };
|
const passwordData = { password: 'current-password-for-confirmation' };
|
||||||
await apiClient.deleteUserAccount(passwordData.password);
|
await apiClient.deleteUserAccount(passwordData.password);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/account');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/account');
|
||||||
expect(capturedBody).toEqual(passwordData);
|
expect(capturedBody).toEqual(passwordData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('setUserDietaryRestrictions should send a PUT request with restriction IDs', async () => {
|
it('setUserDietaryRestrictions should send a PUT request with restriction IDs', async () => {
|
||||||
const restrictionData = { restrictionIds: [1, 5] };
|
const restrictionData = { restrictionIds: [1, 5] };
|
||||||
await apiClient.setUserDietaryRestrictions(restrictionData.restrictionIds);
|
await apiClient.setUserDietaryRestrictions(restrictionData.restrictionIds);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/me/dietary-restrictions');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/me/dietary-restrictions');
|
||||||
expect(capturedBody).toEqual(restrictionData);
|
expect(capturedBody).toEqual(restrictionData);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -662,7 +662,7 @@ describe('API Client', () => {
|
|||||||
await apiClient.setUserAppliances(applianceData.applianceIds, {
|
await apiClient.setUserAppliances(applianceData.applianceIds, {
|
||||||
tokenOverride: 'appliance-override',
|
tokenOverride: 'appliance-override',
|
||||||
});
|
});
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/appliances');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/appliances');
|
||||||
expect(capturedBody).toEqual(applianceData);
|
expect(capturedBody).toEqual(applianceData);
|
||||||
expect(capturedHeaders!.get('Authorization')).toBe('Bearer appliance-override');
|
expect(capturedHeaders!.get('Authorization')).toBe('Bearer appliance-override');
|
||||||
});
|
});
|
||||||
@@ -673,52 +673,52 @@ describe('API Client', () => {
|
|||||||
city: 'Anytown',
|
city: 'Anytown',
|
||||||
});
|
});
|
||||||
await apiClient.updateUserAddress(addressData);
|
await apiClient.updateUserAddress(addressData);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/profile/address');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/profile/address');
|
||||||
expect(capturedBody).toEqual(addressData);
|
expect(capturedBody).toEqual(addressData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('geocodeAddress should send a POST request with address data', async () => {
|
it('geocodeAddress should send a POST request with address data', async () => {
|
||||||
const address = '1600 Amphitheatre Parkway, Mountain View, CA';
|
const address = '1600 Amphitheatre Parkway, Mountain View, CA';
|
||||||
await apiClient.geocodeAddress(address);
|
await apiClient.geocodeAddress(address);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/system/geocode');
|
expect(capturedUrl?.pathname).toBe('/api/v1/system/geocode');
|
||||||
expect(capturedBody).toEqual({ address });
|
expect(capturedBody).toEqual({ address });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getUserAddress should call the correct endpoint', async () => {
|
it('getUserAddress should call the correct endpoint', async () => {
|
||||||
const addressId = 99;
|
const addressId = 99;
|
||||||
await apiClient.getUserAddress(addressId);
|
await apiClient.getUserAddress(addressId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/users/addresses/${addressId}`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/users/addresses/${addressId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getPantryLocations should call the correct endpoint', async () => {
|
it('getPantryLocations should call the correct endpoint', async () => {
|
||||||
await apiClient.getPantryLocations();
|
await apiClient.getPantryLocations();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/pantry/locations');
|
expect(capturedUrl?.pathname).toBe('/api/v1/pantry/locations');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('createPantryLocation should send a POST request with the location name', async () => {
|
it('createPantryLocation should send a POST request with the location name', async () => {
|
||||||
await apiClient.createPantryLocation('Fridge');
|
await apiClient.createPantryLocation('Fridge');
|
||||||
expect(capturedUrl?.pathname).toBe('/api/pantry/locations');
|
expect(capturedUrl?.pathname).toBe('/api/v1/pantry/locations');
|
||||||
expect(capturedBody).toEqual({ name: 'Fridge' });
|
expect(capturedBody).toEqual({ name: 'Fridge' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getShoppingTripHistory should call the correct endpoint', async () => {
|
it('getShoppingTripHistory should call the correct endpoint', async () => {
|
||||||
localStorage.setItem('authToken', 'user-settings-token');
|
localStorage.setItem('authToken', 'user-settings-token');
|
||||||
await apiClient.getShoppingTripHistory();
|
await apiClient.getShoppingTripHistory();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/shopping-history');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/shopping-history');
|
||||||
expect(capturedHeaders!.get('Authorization')).toBe('Bearer user-settings-token');
|
expect(capturedHeaders!.get('Authorization')).toBe('Bearer user-settings-token');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('requestPasswordReset should send a POST request with email', async () => {
|
it('requestPasswordReset should send a POST request with email', async () => {
|
||||||
const email = 'forgot@example.com';
|
const email = 'forgot@example.com';
|
||||||
await apiClient.requestPasswordReset(email);
|
await apiClient.requestPasswordReset(email);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/auth/forgot-password');
|
expect(capturedUrl?.pathname).toBe('/api/v1/auth/forgot-password');
|
||||||
expect(capturedBody).toEqual({ email });
|
expect(capturedBody).toEqual({ email });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resetPassword should send a POST request with token and new password', async () => {
|
it('resetPassword should send a POST request with token and new password', async () => {
|
||||||
const data = { token: 'reset-token', newPassword: 'new-password' };
|
const data = { token: 'reset-token', newPassword: 'new-password' };
|
||||||
await apiClient.resetPassword(data.token, data.newPassword);
|
await apiClient.resetPassword(data.token, data.newPassword);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/auth/reset-password');
|
expect(capturedUrl?.pathname).toBe('/api/v1/auth/reset-password');
|
||||||
expect(capturedBody).toEqual(data);
|
expect(capturedBody).toEqual(data);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -726,7 +726,7 @@ describe('API Client', () => {
|
|||||||
describe('Public API Functions', () => {
|
describe('Public API Functions', () => {
|
||||||
it('pingBackend should call the correct health check endpoint', async () => {
|
it('pingBackend should call the correct health check endpoint', async () => {
|
||||||
await apiClient.pingBackend();
|
await apiClient.pingBackend();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/health/ping');
|
expect(capturedUrl?.pathname).toBe('/api/v1/health/ping');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checkDbSchema should call the correct health check endpoint', async () => {
|
it('checkDbSchema should call the correct health check endpoint', async () => {
|
||||||
@@ -736,7 +736,7 @@ describe('API Client', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await apiClient.checkDbSchema();
|
await apiClient.checkDbSchema();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/health/db-schema');
|
expect(capturedUrl?.pathname).toBe('/api/v1/health/db-schema');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checkStorage should call the correct health check endpoint', async () => {
|
it('checkStorage should call the correct health check endpoint', async () => {
|
||||||
@@ -746,7 +746,7 @@ describe('API Client', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await apiClient.checkStorage();
|
await apiClient.checkStorage();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/health/storage');
|
expect(capturedUrl?.pathname).toBe('/api/v1/health/storage');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checkDbPoolHealth should call the correct health check endpoint', async () => {
|
it('checkDbPoolHealth should call the correct health check endpoint', async () => {
|
||||||
@@ -756,7 +756,7 @@ describe('API Client', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await apiClient.checkDbPoolHealth();
|
await apiClient.checkDbPoolHealth();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/health/db-pool');
|
expect(capturedUrl?.pathname).toBe('/api/v1/health/db-pool');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checkRedisHealth should call the correct health check endpoint', async () => {
|
it('checkRedisHealth should call the correct health check endpoint', async () => {
|
||||||
@@ -766,7 +766,7 @@ describe('API Client', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await apiClient.checkRedisHealth();
|
await apiClient.checkRedisHealth();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/health/redis');
|
expect(capturedUrl?.pathname).toBe('/api/v1/health/redis');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getQueueHealth should call the correct health check endpoint', async () => {
|
it('getQueueHealth should call the correct health check endpoint', async () => {
|
||||||
@@ -776,7 +776,7 @@ describe('API Client', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await apiClient.getQueueHealth();
|
await apiClient.getQueueHealth();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/health/queues');
|
expect(capturedUrl?.pathname).toBe('/api/v1/health/queues');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checkPm2Status should call the correct system endpoint', async () => {
|
it('checkPm2Status should call the correct system endpoint', async () => {
|
||||||
@@ -786,7 +786,7 @@ describe('API Client', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await apiClient.checkPm2Status();
|
await apiClient.checkPm2Status();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/system/pm2-status');
|
expect(capturedUrl?.pathname).toBe('/api/v1/system/pm2-status');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetchFlyers should call the correct public endpoint', async () => {
|
it('fetchFlyers should call the correct public endpoint', async () => {
|
||||||
@@ -796,7 +796,7 @@ describe('API Client', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await apiClient.fetchFlyers();
|
await apiClient.fetchFlyers();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/flyers');
|
expect(capturedUrl?.pathname).toBe('/api/v1/flyers');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetchMasterItems should call the correct public endpoint', async () => {
|
it('fetchMasterItems should call the correct public endpoint', async () => {
|
||||||
@@ -806,7 +806,7 @@ describe('API Client', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await apiClient.fetchMasterItems();
|
await apiClient.fetchMasterItems();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/personalization/master-items');
|
expect(capturedUrl?.pathname).toBe('/api/v1/personalization/master-items');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetchCategories should call the correct public endpoint', async () => {
|
it('fetchCategories should call the correct public endpoint', async () => {
|
||||||
@@ -816,30 +816,30 @@ describe('API Client', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await apiClient.fetchCategories();
|
await apiClient.fetchCategories();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/categories');
|
expect(capturedUrl?.pathname).toBe('/api/v1/categories');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetchFlyerItems should call the correct public endpoint for a specific flyer', async () => {
|
it('fetchFlyerItems should call the correct public endpoint for a specific flyer', async () => {
|
||||||
const flyerId = 123;
|
const flyerId = 123;
|
||||||
server.use(
|
server.use(
|
||||||
http.get(`http://localhost/api/flyers/${flyerId}/items`, () => {
|
http.get(`http://localhost/api/v1/flyers/${flyerId}/items`, () => {
|
||||||
return HttpResponse.json([]);
|
return HttpResponse.json([]);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await apiClient.fetchFlyerItems(flyerId);
|
await apiClient.fetchFlyerItems(flyerId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/flyers/${flyerId}/items`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/flyers/${flyerId}/items`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetchFlyerById should call the correct public endpoint for a specific flyer', async () => {
|
it('fetchFlyerById should call the correct public endpoint for a specific flyer', async () => {
|
||||||
const flyerId = 456;
|
const flyerId = 456;
|
||||||
await apiClient.fetchFlyerById(flyerId);
|
await apiClient.fetchFlyerById(flyerId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/flyers/${flyerId}`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/flyers/${flyerId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetchFlyerItemsForFlyers should send a POST request with flyer IDs', async () => {
|
it('fetchFlyerItemsForFlyers should send a POST request with flyer IDs', async () => {
|
||||||
const flyerIds = [1, 2, 3];
|
const flyerIds = [1, 2, 3];
|
||||||
await apiClient.fetchFlyerItemsForFlyers(flyerIds);
|
await apiClient.fetchFlyerItemsForFlyers(flyerIds);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/flyers/items/batch-fetch');
|
expect(capturedUrl?.pathname).toBe('/api/v1/flyers/items/batch-fetch');
|
||||||
expect(capturedBody).toEqual({ flyerIds });
|
expect(capturedBody).toEqual({ flyerIds });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -858,14 +858,14 @@ describe('API Client', () => {
|
|||||||
it('countFlyerItemsForFlyers should send a POST request with flyer IDs', async () => {
|
it('countFlyerItemsForFlyers should send a POST request with flyer IDs', async () => {
|
||||||
const flyerIds = [1, 2, 3];
|
const flyerIds = [1, 2, 3];
|
||||||
await apiClient.countFlyerItemsForFlyers(flyerIds);
|
await apiClient.countFlyerItemsForFlyers(flyerIds);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/flyers/items/batch-count');
|
expect(capturedUrl?.pathname).toBe('/api/v1/flyers/items/batch-count');
|
||||||
expect(capturedBody).toEqual({ flyerIds });
|
expect(capturedBody).toEqual({ flyerIds });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetchHistoricalPriceData should send a POST request with master item IDs', async () => {
|
it('fetchHistoricalPriceData should send a POST request with master item IDs', async () => {
|
||||||
const masterItemIds = [10, 20];
|
const masterItemIds = [10, 20];
|
||||||
await apiClient.fetchHistoricalPriceData(masterItemIds);
|
await apiClient.fetchHistoricalPriceData(masterItemIds);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/price-history');
|
expect(capturedUrl?.pathname).toBe('/api/v1/price-history');
|
||||||
expect(capturedBody).toEqual({ masterItemIds });
|
expect(capturedBody).toEqual({ masterItemIds });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -880,88 +880,88 @@ describe('API Client', () => {
|
|||||||
it('approveCorrection should send a POST request to the correct URL', async () => {
|
it('approveCorrection should send a POST request to the correct URL', async () => {
|
||||||
const correctionId = 45;
|
const correctionId = 45;
|
||||||
await apiClient.approveCorrection(correctionId);
|
await apiClient.approveCorrection(correctionId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/admin/corrections/${correctionId}/approve`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/admin/corrections/${correctionId}/approve`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updateRecipeStatus should send a PUT request with the correct body', async () => {
|
it('updateRecipeStatus should send a PUT request with the correct body', async () => {
|
||||||
const recipeId = 78;
|
const recipeId = 78;
|
||||||
const statusUpdate = { status: 'public' as const };
|
const statusUpdate = { status: 'public' as const };
|
||||||
await apiClient.updateRecipeStatus(recipeId, 'public');
|
await apiClient.updateRecipeStatus(recipeId, 'public');
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/admin/recipes/${recipeId}/status`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/admin/recipes/${recipeId}/status`);
|
||||||
expect(capturedBody).toEqual(statusUpdate);
|
expect(capturedBody).toEqual(statusUpdate);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cleanupFlyerFiles should send a POST request to the correct URL', async () => {
|
it('cleanupFlyerFiles should send a POST request to the correct URL', async () => {
|
||||||
const flyerId = 99;
|
const flyerId = 99;
|
||||||
await apiClient.cleanupFlyerFiles(flyerId);
|
await apiClient.cleanupFlyerFiles(flyerId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/admin/flyers/${flyerId}/cleanup`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/admin/flyers/${flyerId}/cleanup`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('triggerFailingJob should send a POST request to the correct URL', async () => {
|
it('triggerFailingJob should send a POST request to the correct URL', async () => {
|
||||||
await apiClient.triggerFailingJob();
|
await apiClient.triggerFailingJob();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/admin/trigger/failing-job');
|
expect(capturedUrl?.pathname).toBe('/api/v1/admin/trigger/failing-job');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clearGeocodeCache should send a POST request to the correct URL', async () => {
|
it('clearGeocodeCache should send a POST request to the correct URL', async () => {
|
||||||
await apiClient.clearGeocodeCache();
|
await apiClient.clearGeocodeCache();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/admin/system/clear-geocode-cache');
|
expect(capturedUrl?.pathname).toBe('/api/v1/admin/system/clear-geocode-cache');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getApplicationStats should call the correct endpoint', async () => {
|
it('getApplicationStats should call the correct endpoint', async () => {
|
||||||
await apiClient.getApplicationStats();
|
await apiClient.getApplicationStats();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/admin/stats');
|
expect(capturedUrl?.pathname).toBe('/api/v1/admin/stats');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getSuggestedCorrections should call the correct endpoint', async () => {
|
it('getSuggestedCorrections should call the correct endpoint', async () => {
|
||||||
await apiClient.getSuggestedCorrections();
|
await apiClient.getSuggestedCorrections();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/admin/corrections');
|
expect(capturedUrl?.pathname).toBe('/api/v1/admin/corrections');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getFlyersForReview should call the correct endpoint', async () => {
|
it('getFlyersForReview should call the correct endpoint', async () => {
|
||||||
await apiClient.getFlyersForReview();
|
await apiClient.getFlyersForReview();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/admin/review/flyers');
|
expect(capturedUrl?.pathname).toBe('/api/v1/admin/review/flyers');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejectCorrection should send a POST request to the correct URL', async () => {
|
it('rejectCorrection should send a POST request to the correct URL', async () => {
|
||||||
const correctionId = 46;
|
const correctionId = 46;
|
||||||
await apiClient.rejectCorrection(correctionId);
|
await apiClient.rejectCorrection(correctionId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/admin/corrections/${correctionId}/reject`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/admin/corrections/${correctionId}/reject`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updateSuggestedCorrection should send a PUT request with the new value', async () => {
|
it('updateSuggestedCorrection should send a PUT request with the new value', async () => {
|
||||||
const correctionId = 47;
|
const correctionId = 47;
|
||||||
const newValue = 'new value';
|
const newValue = 'new value';
|
||||||
await apiClient.updateSuggestedCorrection(correctionId, newValue);
|
await apiClient.updateSuggestedCorrection(correctionId, newValue);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/admin/corrections/${correctionId}`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/admin/corrections/${correctionId}`);
|
||||||
expect(capturedBody).toEqual({ suggested_value: newValue });
|
expect(capturedBody).toEqual({ suggested_value: newValue });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getUnmatchedFlyerItems should call the correct endpoint', async () => {
|
it('getUnmatchedFlyerItems should call the correct endpoint', async () => {
|
||||||
await apiClient.getUnmatchedFlyerItems();
|
await apiClient.getUnmatchedFlyerItems();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/admin/unmatched-items');
|
expect(capturedUrl?.pathname).toBe('/api/v1/admin/unmatched-items');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updateRecipeCommentStatus should send a PUT request with the status', async () => {
|
it('updateRecipeCommentStatus should send a PUT request with the status', async () => {
|
||||||
const commentId = 88;
|
const commentId = 88;
|
||||||
await apiClient.updateRecipeCommentStatus(commentId, 'hidden');
|
await apiClient.updateRecipeCommentStatus(commentId, 'hidden');
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/admin/comments/${commentId}/status`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/admin/comments/${commentId}/status`);
|
||||||
expect(capturedBody).toEqual({ status: 'hidden' });
|
expect(capturedBody).toEqual({ status: 'hidden' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetchAllBrands should call the correct endpoint', async () => {
|
it('fetchAllBrands should call the correct endpoint', async () => {
|
||||||
await apiClient.fetchAllBrands();
|
await apiClient.fetchAllBrands();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/admin/brands');
|
expect(capturedUrl?.pathname).toBe('/api/v1/admin/brands');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getDailyStats should call the correct endpoint', async () => {
|
it('getDailyStats should call the correct endpoint', async () => {
|
||||||
await apiClient.getDailyStats();
|
await apiClient.getDailyStats();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/admin/stats/daily');
|
expect(capturedUrl?.pathname).toBe('/api/v1/admin/stats/daily');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updateUserRole should send a PUT request with the new role', async () => {
|
it('updateUserRole should send a PUT request with the new role', async () => {
|
||||||
const userId = 'user-to-promote';
|
const userId = 'user-to-promote';
|
||||||
await apiClient.updateUserRole(userId, 'admin');
|
await apiClient.updateUserRole(userId, 'admin');
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/admin/users/${userId}/role`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/admin/users/${userId}/role`);
|
||||||
expect(capturedBody).toEqual({ role: 'admin' });
|
expect(capturedBody).toEqual({ role: 'admin' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -969,7 +969,7 @@ describe('API Client', () => {
|
|||||||
describe('Analytics API Functions', () => {
|
describe('Analytics API Functions', () => {
|
||||||
it('trackFlyerItemInteraction should send a POST request with interaction type', async () => {
|
it('trackFlyerItemInteraction should send a POST request with interaction type', async () => {
|
||||||
await apiClient.trackFlyerItemInteraction(123, 'click');
|
await apiClient.trackFlyerItemInteraction(123, 'click');
|
||||||
expect(capturedUrl?.pathname).toBe('/api/flyer-items/123/track');
|
expect(capturedUrl?.pathname).toBe('/api/v1/flyer-items/123/track');
|
||||||
expect(capturedBody).toEqual({ type: 'click' });
|
expect(capturedBody).toEqual({ type: 'click' });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -980,7 +980,7 @@ describe('API Client', () => {
|
|||||||
was_successful: true,
|
was_successful: true,
|
||||||
});
|
});
|
||||||
await apiClient.logSearchQuery(queryData as any);
|
await apiClient.logSearchQuery(queryData as any);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/search/log');
|
expect(capturedUrl?.pathname).toBe('/api/v1/search/log');
|
||||||
expect(capturedBody).toEqual(queryData);
|
expect(capturedBody).toEqual(queryData);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1025,7 +1025,7 @@ describe('API Client', () => {
|
|||||||
rememberMe: true,
|
rememberMe: true,
|
||||||
});
|
});
|
||||||
await apiClient.loginUser(loginData.email, loginData.password, loginData.rememberMe);
|
await apiClient.loginUser(loginData.email, loginData.password, loginData.rememberMe);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/auth/login');
|
expect(capturedUrl?.pathname).toBe('/api/v1/auth/login');
|
||||||
expect(capturedBody).toEqual(loginData);
|
expect(capturedBody).toEqual(loginData);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1033,7 +1033,7 @@ describe('API Client', () => {
|
|||||||
describe('Admin Activity Log', () => {
|
describe('Admin Activity Log', () => {
|
||||||
it('fetchActivityLog should call the correct endpoint with query params', async () => {
|
it('fetchActivityLog should call the correct endpoint with query params', async () => {
|
||||||
await apiClient.fetchActivityLog(50, 10);
|
await apiClient.fetchActivityLog(50, 10);
|
||||||
expect(capturedUrl?.pathname).toBe('/api/admin/activity-log');
|
expect(capturedUrl?.pathname).toBe('/api/v1/admin/activity-log');
|
||||||
expect(capturedUrl!.searchParams.get('limit')).toBe('50');
|
expect(capturedUrl!.searchParams.get('limit')).toBe('50');
|
||||||
expect(capturedUrl!.searchParams.get('offset')).toBe('10');
|
expect(capturedUrl!.searchParams.get('offset')).toBe('10');
|
||||||
});
|
});
|
||||||
@@ -1045,7 +1045,7 @@ describe('API Client', () => {
|
|||||||
const checksum = 'checksum-abc-123';
|
const checksum = 'checksum-abc-123';
|
||||||
await apiClient.uploadAndProcessFlyer(mockFile, checksum);
|
await apiClient.uploadAndProcessFlyer(mockFile, checksum);
|
||||||
|
|
||||||
expect(capturedUrl?.pathname).toBe('/api/ai/upload-and-process');
|
expect(capturedUrl?.pathname).toBe('/api/v1/ai/upload-and-process');
|
||||||
expect(capturedBody).toBeInstanceOf(FormData);
|
expect(capturedBody).toBeInstanceOf(FormData);
|
||||||
const uploadedFile = (capturedBody as FormData).get('flyerFile') as File;
|
const uploadedFile = (capturedBody as FormData).get('flyerFile') as File;
|
||||||
const sentChecksum = (capturedBody as FormData).get('checksum');
|
const sentChecksum = (capturedBody as FormData).get('checksum');
|
||||||
@@ -1056,7 +1056,7 @@ describe('API Client', () => {
|
|||||||
it('getJobStatus should call the correct endpoint', async () => {
|
it('getJobStatus should call the correct endpoint', async () => {
|
||||||
const jobId = 'job-xyz-789';
|
const jobId = 'job-xyz-789';
|
||||||
await apiClient.getJobStatus(jobId);
|
await apiClient.getJobStatus(jobId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/ai/jobs/${jobId}/status`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/ai/jobs/${jobId}/status`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1065,7 +1065,7 @@ describe('API Client', () => {
|
|||||||
const mockFile = new File(['receipt-content'], 'receipt.jpg', { type: 'image/jpeg' });
|
const mockFile = new File(['receipt-content'], 'receipt.jpg', { type: 'image/jpeg' });
|
||||||
await apiClient.uploadReceipt(mockFile);
|
await apiClient.uploadReceipt(mockFile);
|
||||||
|
|
||||||
expect(capturedUrl?.pathname).toBe('/api/receipts/upload');
|
expect(capturedUrl?.pathname).toBe('/api/v1/receipts/upload');
|
||||||
expect(capturedBody).toBeInstanceOf(FormData);
|
expect(capturedBody).toBeInstanceOf(FormData);
|
||||||
const uploadedFile = (capturedBody as FormData).get('receiptImage') as File;
|
const uploadedFile = (capturedBody as FormData).get('receiptImage') as File;
|
||||||
expect(uploadedFile.name).toBe('receipt.jpg');
|
expect(uploadedFile.name).toBe('receipt.jpg');
|
||||||
@@ -1074,29 +1074,29 @@ describe('API Client', () => {
|
|||||||
it('getDealsForReceipt should call the correct endpoint', async () => {
|
it('getDealsForReceipt should call the correct endpoint', async () => {
|
||||||
const receiptId = 55;
|
const receiptId = 55;
|
||||||
await apiClient.getDealsForReceipt(receiptId);
|
await apiClient.getDealsForReceipt(receiptId);
|
||||||
expect(capturedUrl?.pathname).toBe(`/api/receipts/${receiptId}/deals`);
|
expect(capturedUrl?.pathname).toBe(`/api/v1/receipts/${receiptId}/deals`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Public Personalization API Functions', () => {
|
describe('Public Personalization API Functions', () => {
|
||||||
it('getDietaryRestrictions should call the correct endpoint', async () => {
|
it('getDietaryRestrictions should call the correct endpoint', async () => {
|
||||||
await apiClient.getDietaryRestrictions();
|
await apiClient.getDietaryRestrictions();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/personalization/dietary-restrictions');
|
expect(capturedUrl?.pathname).toBe('/api/v1/personalization/dietary-restrictions');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getAppliances should call the correct endpoint', async () => {
|
it('getAppliances should call the correct endpoint', async () => {
|
||||||
await apiClient.getAppliances();
|
await apiClient.getAppliances();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/personalization/appliances');
|
expect(capturedUrl?.pathname).toBe('/api/v1/personalization/appliances');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getUserDietaryRestrictions should call the correct endpoint', async () => {
|
it('getUserDietaryRestrictions should call the correct endpoint', async () => {
|
||||||
await apiClient.getUserDietaryRestrictions();
|
await apiClient.getUserDietaryRestrictions();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/me/dietary-restrictions');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/me/dietary-restrictions');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getUserAppliances should call the correct endpoint', async () => {
|
it('getUserAppliances should call the correct endpoint', async () => {
|
||||||
await apiClient.getUserAppliances();
|
await apiClient.getUserAppliances();
|
||||||
expect(capturedUrl?.pathname).toBe('/api/users/appliances');
|
expect(capturedUrl?.pathname).toBe('/api/v1/users/appliances');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ try {
|
|||||||
|
|
||||||
// This constant should point to your backend API.
|
// This constant should point to your backend API.
|
||||||
// It's often a good practice to store this in an environment variable.
|
// It's often a good practice to store this in an environment variable.
|
||||||
// Using a relative path '/api' is the most robust method for production.
|
// Using a relative path '/api/v1' is the most robust method for production.
|
||||||
// It makes API calls to the same host that served the frontend files,
|
// It makes API calls to the same host that served the frontend files,
|
||||||
// which is then handled by the Nginx reverse proxy.
|
// which is then handled by the Nginx reverse proxy.
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api';
|
// ADR-008: API Versioning Strategy - Phase 1 migrates all routes to /api/v1.
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/v1';
|
||||||
|
|
||||||
export interface ApiOptions {
|
export interface ApiOptions {
|
||||||
tokenOverride?: string;
|
tokenOverride?: string;
|
||||||
|
|||||||
@@ -330,9 +330,9 @@ describe('sentry.server', () => {
|
|||||||
const breadcrumb = {
|
const breadcrumb = {
|
||||||
message: 'API call',
|
message: 'API call',
|
||||||
category: 'http',
|
category: 'http',
|
||||||
data: { url: '/api/test', method: 'GET' },
|
data: { url: '/api/v1/test', method: 'GET' },
|
||||||
};
|
};
|
||||||
expect(breadcrumb.data).toEqual({ url: '/api/test', method: 'GET' });
|
expect(breadcrumb.data).toEqual({ url: '/api/v1/test', method: 'GET' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,27 +38,27 @@ describe('Admin Route Authorization', () => {
|
|||||||
const adminEndpoints = [
|
const adminEndpoints = [
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/api/admin/stats',
|
path: '/api/v1/admin/stats',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/api/admin/users',
|
path: '/api/v1/admin/users',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/api/admin/corrections',
|
path: '/api/v1/admin/corrections',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/admin/corrections/1/approve',
|
path: '/api/v1/admin/corrections/1/approve',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/admin/trigger/daily-deal-check',
|
path: '/api/v1/admin/trigger/daily-deal-check',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/api/admin/queues/status',
|
path: '/api/v1/admin/queues/status',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ describe('E2E Admin Dashboard Flow', () => {
|
|||||||
it('should allow an admin to log in and access dashboard features', async () => {
|
it('should allow an admin to log in and access dashboard features', async () => {
|
||||||
// 1. Register a new user (initially a regular user)
|
// 1. Register a new user (initially a regular user)
|
||||||
const registerResponse = await getRequest()
|
const registerResponse = await getRequest()
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email: adminEmail, password: adminPassword, full_name: 'E2E Admin User' });
|
.send({ email: adminEmail, password: adminPassword, full_name: 'E2E Admin User' });
|
||||||
|
|
||||||
expect(registerResponse.status).toBe(201);
|
expect(registerResponse.status).toBe(201);
|
||||||
@@ -51,7 +51,7 @@ describe('E2E Admin Dashboard Flow', () => {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
const loginResponse = await getRequest()
|
const loginResponse = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: adminEmail, password: adminPassword, rememberMe: false });
|
.send({ email: adminEmail, password: adminPassword, rememberMe: false });
|
||||||
|
|
||||||
expect(loginResponse.status).toBe(200);
|
expect(loginResponse.status).toBe(200);
|
||||||
@@ -62,7 +62,7 @@ describe('E2E Admin Dashboard Flow', () => {
|
|||||||
|
|
||||||
// 4. Fetch System Stats (Protected Admin Route)
|
// 4. Fetch System Stats (Protected Admin Route)
|
||||||
const statsResponse = await getRequest()
|
const statsResponse = await getRequest()
|
||||||
.get('/api/admin/stats')
|
.get('/api/v1/admin/stats')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(statsResponse.status).toBe(200);
|
expect(statsResponse.status).toBe(200);
|
||||||
@@ -71,7 +71,7 @@ describe('E2E Admin Dashboard Flow', () => {
|
|||||||
|
|
||||||
// 5. Fetch User List (Protected Admin Route)
|
// 5. Fetch User List (Protected Admin Route)
|
||||||
const usersResponse = await getRequest()
|
const usersResponse = await getRequest()
|
||||||
.get('/api/admin/users')
|
.get('/api/v1/admin/users')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(usersResponse.status).toBe(200);
|
expect(usersResponse.status).toBe(200);
|
||||||
@@ -84,7 +84,7 @@ describe('E2E Admin Dashboard Flow', () => {
|
|||||||
|
|
||||||
// 6. Check Queue Status (Protected Admin Route)
|
// 6. Check Queue Status (Protected Admin Route)
|
||||||
const queueResponse = await getRequest()
|
const queueResponse = await getRequest()
|
||||||
.get('/api/admin/queues/status')
|
.get('/api/v1/admin/queues/status')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(queueResponse.status).toBe(200);
|
expect(queueResponse.status).toBe(200);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email, password: TEST_PASSWORD, full_name: fullName });
|
.send({ email, password: TEST_PASSWORD, full_name: fullName });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -68,7 +68,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email, password: weakPassword, full_name: 'Weak Pass User' });
|
.send({ email, password: weakPassword, full_name: 'Weak Pass User' });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -83,14 +83,14 @@ describe('Authentication E2E Flow', () => {
|
|||||||
|
|
||||||
// Act 1: Register the user successfully
|
// Act 1: Register the user successfully
|
||||||
const firstResponse = await getRequest()
|
const firstResponse = await getRequest()
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email, password: TEST_PASSWORD, full_name: 'Duplicate User' });
|
.send({ email, password: TEST_PASSWORD, full_name: 'Duplicate User' });
|
||||||
expect(firstResponse.status).toBe(201);
|
expect(firstResponse.status).toBe(201);
|
||||||
createdUserIds.push(firstResponse.body.data.userprofile.user.user_id);
|
createdUserIds.push(firstResponse.body.data.userprofile.user.user_id);
|
||||||
|
|
||||||
// Act 2: Attempt to register the same user again
|
// Act 2: Attempt to register the same user again
|
||||||
const secondResponse = await getRequest()
|
const secondResponse = await getRequest()
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email, password: TEST_PASSWORD, full_name: 'Duplicate User' });
|
.send({ email, password: TEST_PASSWORD, full_name: 'Duplicate User' });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -105,7 +105,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
it('should successfully log in a registered user', async () => {
|
it('should successfully log in a registered user', async () => {
|
||||||
// Act: Attempt to log in with the user created in beforeAll
|
// Act: Attempt to log in with the user created in beforeAll
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: testUser.user.email, password: TEST_PASSWORD, rememberMe: false });
|
.send({ email: testUser.user.email, password: TEST_PASSWORD, rememberMe: false });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -118,7 +118,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
it('should fail to log in with an incorrect password', async () => {
|
it('should fail to log in with an incorrect password', async () => {
|
||||||
// Act: Attempt to log in with the wrong password
|
// Act: Attempt to log in with the wrong password
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: testUser.user.email, password: 'wrong-password', rememberMe: false });
|
.send({ email: testUser.user.email, password: 'wrong-password', rememberMe: false });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -128,7 +128,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
|
|
||||||
it('should fail to log in with a non-existent email', async () => {
|
it('should fail to log in with a non-existent email', async () => {
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: 'no-one-here@example.com', password: TEST_PASSWORD, rememberMe: false });
|
.send({ email: 'no-one-here@example.com', password: TEST_PASSWORD, rememberMe: false });
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
@@ -142,7 +142,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
|
|
||||||
// Act: Use the token to access a protected route
|
// Act: Use the token to access a protected route
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', `Bearer ${token}`);
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -165,7 +165,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
|
|
||||||
// Act: Call the update endpoint
|
// Act: Call the update endpoint
|
||||||
const updateResponse = await getRequest()
|
const updateResponse = await getRequest()
|
||||||
.put('/api/users/profile')
|
.put('/api/v1/users/profile')
|
||||||
.set('Authorization', `Bearer ${token}`)
|
.set('Authorization', `Bearer ${token}`)
|
||||||
.send(profileUpdates);
|
.send(profileUpdates);
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
|
|
||||||
// Act 2: Fetch the profile again to verify persistence
|
// Act 2: Fetch the profile again to verify persistence
|
||||||
const verifyResponse = await getRequest()
|
const verifyResponse = await getRequest()
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', `Bearer ${token}`);
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
// Assert 2: Check the fetched data
|
// Assert 2: Check the fetched data
|
||||||
@@ -190,7 +190,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
// Arrange: Create a user to reset the password for
|
// Arrange: Create a user to reset the password for
|
||||||
const email = `e2e-reset-pass-${Date.now()}@example.com`;
|
const email = `e2e-reset-pass-${Date.now()}@example.com`;
|
||||||
const registerResponse = await getRequest()
|
const registerResponse = await getRequest()
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email, password: TEST_PASSWORD, full_name: 'Reset Pass User' });
|
.send({ email, password: TEST_PASSWORD, full_name: 'Reset Pass User' });
|
||||||
expect(registerResponse.status).toBe(201);
|
expect(registerResponse.status).toBe(201);
|
||||||
createdUserIds.push(registerResponse.body.data.userprofile.user.user_id);
|
createdUserIds.push(registerResponse.body.data.userprofile.user.user_id);
|
||||||
@@ -200,7 +200,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
let loginResponse;
|
let loginResponse;
|
||||||
while (loginAttempts < 10) {
|
while (loginAttempts < 10) {
|
||||||
loginResponse = await getRequest()
|
loginResponse = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email, password: TEST_PASSWORD, rememberMe: false });
|
.send({ email, password: TEST_PASSWORD, rememberMe: false });
|
||||||
if (loginResponse.status === 200) break;
|
if (loginResponse.status === 200) break;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
@@ -209,7 +209,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
expect(loginResponse?.status).toBe(200);
|
expect(loginResponse?.status).toBe(200);
|
||||||
|
|
||||||
// Request password reset (do not poll, as this endpoint is rate-limited)
|
// Request password reset (do not poll, as this endpoint is rate-limited)
|
||||||
const forgotResponse = await getRequest().post('/api/auth/forgot-password').send({ email });
|
const forgotResponse = await getRequest().post('/api/v1/auth/forgot-password').send({ email });
|
||||||
expect(forgotResponse.status).toBe(200);
|
expect(forgotResponse.status).toBe(200);
|
||||||
const resetToken = forgotResponse.body.data.token;
|
const resetToken = forgotResponse.body.data.token;
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
// Act 2: Use the token to set a new password.
|
// Act 2: Use the token to set a new password.
|
||||||
const newPassword = 'my-new-e2e-password-!@#$';
|
const newPassword = 'my-new-e2e-password-!@#$';
|
||||||
const resetResponse = await getRequest()
|
const resetResponse = await getRequest()
|
||||||
.post('/api/auth/reset-password')
|
.post('/api/v1/auth/reset-password')
|
||||||
.send({ token: resetToken, newPassword });
|
.send({ token: resetToken, newPassword });
|
||||||
|
|
||||||
// Assert 2: Check for a successful password reset message.
|
// Assert 2: Check for a successful password reset message.
|
||||||
@@ -232,7 +232,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
|
|
||||||
// Act 3: Log in with the NEW password
|
// Act 3: Log in with the NEW password
|
||||||
const newLoginResponse = await getRequest()
|
const newLoginResponse = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email, password: newPassword, rememberMe: false });
|
.send({ email, password: newPassword, rememberMe: false });
|
||||||
|
|
||||||
expect(newLoginResponse.status).toBe(200);
|
expect(newLoginResponse.status).toBe(200);
|
||||||
@@ -246,7 +246,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
|
|
||||||
const nonExistentEmail = `non-existent-e2e-${Date.now()}@example.com`;
|
const nonExistentEmail = `non-existent-e2e-${Date.now()}@example.com`;
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/forgot-password')
|
.post('/api/v1/auth/forgot-password')
|
||||||
.send({ email: nonExistentEmail });
|
.send({ email: nonExistentEmail });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -261,7 +261,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
it('should allow an authenticated user to refresh their access token and use it', async () => {
|
it('should allow an authenticated user to refresh their access token and use it', async () => {
|
||||||
// 1. Log in to get the refresh token cookie and an initial access token.
|
// 1. Log in to get the refresh token cookie and an initial access token.
|
||||||
const loginResponse = await getRequest()
|
const loginResponse = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: testUser.user.email, password: TEST_PASSWORD, rememberMe: false });
|
.send({ email: testUser.user.email, password: TEST_PASSWORD, rememberMe: false });
|
||||||
expect(loginResponse.status).toBe(200);
|
expect(loginResponse.status).toBe(200);
|
||||||
const initialAccessToken = loginResponse.body.data.token;
|
const initialAccessToken = loginResponse.body.data.token;
|
||||||
@@ -284,7 +284,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
|
|
||||||
// 3. Call the refresh token endpoint, passing the cookie.
|
// 3. Call the refresh token endpoint, passing the cookie.
|
||||||
const refreshResponse = await getRequest()
|
const refreshResponse = await getRequest()
|
||||||
.post('/api/auth/refresh-token')
|
.post('/api/v1/auth/refresh-token')
|
||||||
.set('Cookie', refreshTokenCookie!);
|
.set('Cookie', refreshTokenCookie!);
|
||||||
|
|
||||||
// 4. Assert the refresh was successful and we got a new token.
|
// 4. Assert the refresh was successful and we got a new token.
|
||||||
@@ -295,7 +295,7 @@ describe('Authentication E2E Flow', () => {
|
|||||||
|
|
||||||
// 5. Use the new access token to access a protected route.
|
// 5. Use the new access token to access a protected route.
|
||||||
const profileResponse = await getRequest()
|
const profileResponse = await getRequest()
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', `Bearer ${newAccessToken}`);
|
.set('Authorization', `Bearer ${newAccessToken}`);
|
||||||
expect(profileResponse.status).toBe(200);
|
expect(profileResponse.status).toBe(200);
|
||||||
expect(profileResponse.body.data.user.user_id).toBe(testUser.user.user_id);
|
expect(profileResponse.body.data.user.user_id).toBe(testUser.user.user_id);
|
||||||
@@ -303,12 +303,12 @@ describe('Authentication E2E Flow', () => {
|
|||||||
|
|
||||||
it('should fail to refresh with an invalid or missing token', async () => {
|
it('should fail to refresh with an invalid or missing token', async () => {
|
||||||
// Case 1: No cookie provided
|
// Case 1: No cookie provided
|
||||||
const noCookieResponse = await getRequest().post('/api/auth/refresh-token');
|
const noCookieResponse = await getRequest().post('/api/v1/auth/refresh-token');
|
||||||
expect(noCookieResponse.status).toBe(401);
|
expect(noCookieResponse.status).toBe(401);
|
||||||
|
|
||||||
// Case 2: Invalid cookie provided
|
// Case 2: Invalid cookie provided
|
||||||
const invalidCookieResponse = await getRequest()
|
const invalidCookieResponse = await getRequest()
|
||||||
.post('/api/auth/refresh-token')
|
.post('/api/v1/auth/refresh-token')
|
||||||
.set('Cookie', 'refreshToken=invalid-garbage-token');
|
.set('Cookie', 'refreshToken=invalid-garbage-token');
|
||||||
expect(invalidCookieResponse.status).toBe(403);
|
expect(invalidCookieResponse.status).toBe(403);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ describe('E2E Budget Management Journey', () => {
|
|||||||
|
|
||||||
it('should complete budget journey: Register -> Create Budget -> Track Spending -> Update -> Delete', async () => {
|
it('should complete budget journey: Register -> Create Budget -> Track Spending -> Update -> Delete', async () => {
|
||||||
// Step 1: Register a new user
|
// Step 1: Register a new user
|
||||||
const registerResponse = await getRequest().post('/api/auth/register').send({
|
const registerResponse = await getRequest().post('/api/v1/auth/register').send({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
password: userPassword,
|
password: userPassword,
|
||||||
full_name: 'Budget E2E User',
|
full_name: 'Budget E2E User',
|
||||||
@@ -75,7 +75,7 @@ describe('E2E Budget Management Journey', () => {
|
|||||||
const { response: loginResponse, responseBody: loginResponseBody } = await poll(
|
const { response: loginResponse, responseBody: loginResponseBody } = await poll(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
||||||
const responseBody = response.status === 200 ? response.body : {};
|
const responseBody = response.status === 200 ? response.body : {};
|
||||||
return { response, responseBody };
|
return { response, responseBody };
|
||||||
@@ -95,7 +95,7 @@ describe('E2E Budget Management Journey', () => {
|
|||||||
const formatDate = (d: Date) => d.toISOString().split('T')[0];
|
const formatDate = (d: Date) => d.toISOString().split('T')[0];
|
||||||
|
|
||||||
const createBudgetResponse = await getRequest()
|
const createBudgetResponse = await getRequest()
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
name: 'Monthly Groceries',
|
name: 'Monthly Groceries',
|
||||||
@@ -113,7 +113,7 @@ describe('E2E Budget Management Journey', () => {
|
|||||||
|
|
||||||
// Step 4: Create a weekly budget
|
// Step 4: Create a weekly budget
|
||||||
const weeklyBudgetResponse = await getRequest()
|
const weeklyBudgetResponse = await getRequest()
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
name: 'Weekly Dining Out',
|
name: 'Weekly Dining Out',
|
||||||
@@ -128,7 +128,7 @@ describe('E2E Budget Management Journey', () => {
|
|||||||
|
|
||||||
// Step 5: View all budgets
|
// Step 5: View all budgets
|
||||||
const listBudgetsResponse = await getRequest()
|
const listBudgetsResponse = await getRequest()
|
||||||
.get('/api/budgets')
|
.get('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(listBudgetsResponse.status).toBe(200);
|
expect(listBudgetsResponse.status).toBe(200);
|
||||||
@@ -203,7 +203,7 @@ describe('E2E Budget Management Journey', () => {
|
|||||||
|
|
||||||
// Step 9: Test budget validation - try to create invalid budget
|
// Step 9: Test budget validation - try to create invalid budget
|
||||||
const invalidBudgetResponse = await getRequest()
|
const invalidBudgetResponse = await getRequest()
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
name: 'Invalid Budget',
|
name: 'Invalid Budget',
|
||||||
@@ -216,7 +216,7 @@ describe('E2E Budget Management Journey', () => {
|
|||||||
|
|
||||||
// Step 10: Test budget validation - missing required fields
|
// Step 10: Test budget validation - missing required fields
|
||||||
const missingFieldsResponse = await getRequest()
|
const missingFieldsResponse = await getRequest()
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
name: 'Incomplete Budget',
|
name: 'Incomplete Budget',
|
||||||
@@ -236,13 +236,13 @@ describe('E2E Budget Management Journey', () => {
|
|||||||
// Step 12: Verify another user cannot access our budgets
|
// Step 12: Verify another user cannot access our budgets
|
||||||
const otherUserEmail = `other-budget-e2e-${uniqueId}@example.com`;
|
const otherUserEmail = `other-budget-e2e-${uniqueId}@example.com`;
|
||||||
await getRequest()
|
await getRequest()
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email: otherUserEmail, password: userPassword, full_name: 'Other Budget User' });
|
.send({ email: otherUserEmail, password: userPassword, full_name: 'Other Budget User' });
|
||||||
|
|
||||||
const { responseBody: otherLoginData } = await poll(
|
const { responseBody: otherLoginData } = await poll(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: otherUserEmail, password: userPassword, rememberMe: false });
|
.send({ email: otherUserEmail, password: userPassword, rememberMe: false });
|
||||||
const responseBody = response.status === 200 ? response.body : {};
|
const responseBody = response.status === 200 ? response.body : {};
|
||||||
return { response, responseBody };
|
return { response, responseBody };
|
||||||
@@ -256,7 +256,7 @@ describe('E2E Budget Management Journey', () => {
|
|||||||
|
|
||||||
// Other user should not see our budgets
|
// Other user should not see our budgets
|
||||||
const otherBudgetsResponse = await getRequest()
|
const otherBudgetsResponse = await getRequest()
|
||||||
.get('/api/budgets')
|
.get('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${otherToken}`);
|
.set('Authorization', `Bearer ${otherToken}`);
|
||||||
|
|
||||||
expect(otherBudgetsResponse.status).toBe(200);
|
expect(otherBudgetsResponse.status).toBe(200);
|
||||||
@@ -297,7 +297,7 @@ describe('E2E Budget Management Journey', () => {
|
|||||||
|
|
||||||
// Step 14: Verify deletion
|
// Step 14: Verify deletion
|
||||||
const verifyDeleteResponse = await getRequest()
|
const verifyDeleteResponse = await getRequest()
|
||||||
.get('/api/budgets')
|
.get('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(verifyDeleteResponse.status).toBe(200);
|
expect(verifyDeleteResponse.status).toBe(200);
|
||||||
@@ -310,7 +310,7 @@ describe('E2E Budget Management Journey', () => {
|
|||||||
|
|
||||||
// Step 15: Delete account
|
// Step 15: Delete account
|
||||||
const deleteAccountResponse = await getRequest()
|
const deleteAccountResponse = await getRequest()
|
||||||
.delete('/api/users/account')
|
.delete('/api/v1/users/account')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ password: userPassword });
|
.send({ password: userPassword });
|
||||||
|
|
||||||
|
|||||||
@@ -79,14 +79,14 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
|||||||
// will support both category names and IDs in the watched items API.
|
// will support both category names and IDs in the watched items API.
|
||||||
|
|
||||||
// Get all available categories
|
// Get all available categories
|
||||||
const categoriesResponse = await getRequest().get('/api/categories');
|
const categoriesResponse = await getRequest().get('/api/v1/categories');
|
||||||
expect(categoriesResponse.status).toBe(200);
|
expect(categoriesResponse.status).toBe(200);
|
||||||
expect(categoriesResponse.body.success).toBe(true);
|
expect(categoriesResponse.body.success).toBe(true);
|
||||||
expect(categoriesResponse.body.data.length).toBeGreaterThan(0);
|
expect(categoriesResponse.body.data.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Find "Dairy & Eggs" category by name using the lookup endpoint
|
// Find "Dairy & Eggs" category by name using the lookup endpoint
|
||||||
const categoryLookupResponse = await getRequest().get(
|
const categoryLookupResponse = await getRequest().get(
|
||||||
'/api/categories/lookup?name=' + encodeURIComponent('Dairy & Eggs'),
|
'/api/v1/categories/lookup?name=' + encodeURIComponent('Dairy & Eggs'),
|
||||||
);
|
);
|
||||||
expect(categoryLookupResponse.status).toBe(200);
|
expect(categoryLookupResponse.status).toBe(200);
|
||||||
expect(categoryLookupResponse.body.success).toBe(true);
|
expect(categoryLookupResponse.body.success).toBe(true);
|
||||||
@@ -104,20 +104,20 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
|||||||
|
|
||||||
// Look up other category IDs we'll need
|
// Look up other category IDs we'll need
|
||||||
const bakeryResponse = await getRequest().get(
|
const bakeryResponse = await getRequest().get(
|
||||||
'/api/categories/lookup?name=' + encodeURIComponent('Bakery & Bread'),
|
'/api/v1/categories/lookup?name=' + encodeURIComponent('Bakery & Bread'),
|
||||||
);
|
);
|
||||||
const bakeryCategoryId = bakeryResponse.body.data.category_id;
|
const bakeryCategoryId = bakeryResponse.body.data.category_id;
|
||||||
|
|
||||||
const beveragesResponse = await getRequest().get('/api/categories/lookup?name=Beverages');
|
const beveragesResponse = await getRequest().get('/api/v1/categories/lookup?name=Beverages');
|
||||||
const beveragesCategoryId = beveragesResponse.body.data.category_id;
|
const beveragesCategoryId = beveragesResponse.body.data.category_id;
|
||||||
|
|
||||||
const produceResponse = await getRequest().get(
|
const produceResponse = await getRequest().get(
|
||||||
'/api/categories/lookup?name=' + encodeURIComponent('Fruits & Vegetables'),
|
'/api/v1/categories/lookup?name=' + encodeURIComponent('Fruits & Vegetables'),
|
||||||
);
|
);
|
||||||
const produceCategoryId = produceResponse.body.data.category_id;
|
const produceCategoryId = produceResponse.body.data.category_id;
|
||||||
|
|
||||||
const meatResponse = await getRequest().get(
|
const meatResponse = await getRequest().get(
|
||||||
'/api/categories/lookup?name=' + encodeURIComponent('Meat & Seafood'),
|
'/api/v1/categories/lookup?name=' + encodeURIComponent('Meat & Seafood'),
|
||||||
);
|
);
|
||||||
const meatCategoryId = meatResponse.body.data.category_id;
|
const meatCategoryId = meatResponse.body.data.category_id;
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
|||||||
// to look up category IDs before creating watched items.
|
// to look up category IDs before creating watched items.
|
||||||
|
|
||||||
// Step 1: Register a new user
|
// Step 1: Register a new user
|
||||||
const registerResponse = await getRequest().post('/api/auth/register').send({
|
const registerResponse = await getRequest().post('/api/v1/auth/register').send({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
password: userPassword,
|
password: userPassword,
|
||||||
full_name: 'Deals E2E User',
|
full_name: 'Deals E2E User',
|
||||||
@@ -137,7 +137,7 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
|||||||
const { response: loginResponse, responseBody: loginResponseBody } = await poll(
|
const { response: loginResponse, responseBody: loginResponseBody } = await poll(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
||||||
const responseBody = response.status === 200 ? response.body : {};
|
const responseBody = response.status === 200 ? response.body : {};
|
||||||
return { response, responseBody };
|
return { response, responseBody };
|
||||||
@@ -245,7 +245,7 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
|||||||
|
|
||||||
// Step 4: Add items to watch list (using category_id from lookups above)
|
// Step 4: Add items to watch list (using category_id from lookups above)
|
||||||
const watchItem1Response = await getRequest()
|
const watchItem1Response = await getRequest()
|
||||||
.post('/api/users/watched-items')
|
.post('/api/v1/users/watched-items')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
itemName: 'E2E Milk 2%',
|
itemName: 'E2E Milk 2%',
|
||||||
@@ -263,7 +263,7 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
|||||||
|
|
||||||
for (const item of itemsToWatch) {
|
for (const item of itemsToWatch) {
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/users/watched-items')
|
.post('/api/v1/users/watched-items')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send(item);
|
.send(item);
|
||||||
expect(response.status).toBe(201);
|
expect(response.status).toBe(201);
|
||||||
@@ -271,7 +271,7 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
|||||||
|
|
||||||
// Step 5: View all watched items
|
// Step 5: View all watched items
|
||||||
const watchedListResponse = await getRequest()
|
const watchedListResponse = await getRequest()
|
||||||
.get('/api/users/watched-items')
|
.get('/api/v1/users/watched-items')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(watchedListResponse.status).toBe(200);
|
expect(watchedListResponse.status).toBe(200);
|
||||||
@@ -286,7 +286,7 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
|||||||
|
|
||||||
// Step 6: Get best prices for watched items
|
// Step 6: Get best prices for watched items
|
||||||
const bestPricesResponse = await getRequest()
|
const bestPricesResponse = await getRequest()
|
||||||
.get('/api/deals/best-watched-prices')
|
.get('/api/v1/deals/best-watched-prices')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(bestPricesResponse.status).toBe(200);
|
expect(bestPricesResponse.status).toBe(200);
|
||||||
@@ -321,7 +321,7 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
|||||||
|
|
||||||
// Step 9: Verify item was removed
|
// Step 9: Verify item was removed
|
||||||
const updatedWatchedListResponse = await getRequest()
|
const updatedWatchedListResponse = await getRequest()
|
||||||
.get('/api/users/watched-items')
|
.get('/api/v1/users/watched-items')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(updatedWatchedListResponse.status).toBe(200);
|
expect(updatedWatchedListResponse.status).toBe(200);
|
||||||
@@ -334,13 +334,13 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
|||||||
// Step 10: Verify another user cannot see our watched items
|
// Step 10: Verify another user cannot see our watched items
|
||||||
const otherUserEmail = `other-deals-e2e-${uniqueId}@example.com`;
|
const otherUserEmail = `other-deals-e2e-${uniqueId}@example.com`;
|
||||||
await getRequest()
|
await getRequest()
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email: otherUserEmail, password: userPassword, full_name: 'Other Deals User' });
|
.send({ email: otherUserEmail, password: userPassword, full_name: 'Other Deals User' });
|
||||||
|
|
||||||
const { responseBody: otherLoginData } = await poll(
|
const { responseBody: otherLoginData } = await poll(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: otherUserEmail, password: userPassword, rememberMe: false });
|
.send({ email: otherUserEmail, password: userPassword, rememberMe: false });
|
||||||
const responseBody = response.status === 200 ? response.body : {};
|
const responseBody = response.status === 200 ? response.body : {};
|
||||||
return { response, responseBody };
|
return { response, responseBody };
|
||||||
@@ -354,7 +354,7 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
|||||||
|
|
||||||
// Other user's watched items should be empty
|
// Other user's watched items should be empty
|
||||||
const otherWatchedResponse = await getRequest()
|
const otherWatchedResponse = await getRequest()
|
||||||
.get('/api/users/watched-items')
|
.get('/api/v1/users/watched-items')
|
||||||
.set('Authorization', `Bearer ${otherToken}`);
|
.set('Authorization', `Bearer ${otherToken}`);
|
||||||
|
|
||||||
expect(otherWatchedResponse.status).toBe(200);
|
expect(otherWatchedResponse.status).toBe(200);
|
||||||
@@ -362,7 +362,7 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
|||||||
|
|
||||||
// Other user's deals should be empty
|
// Other user's deals should be empty
|
||||||
const otherDealsResponse = await getRequest()
|
const otherDealsResponse = await getRequest()
|
||||||
.get('/api/deals/best-watched-prices')
|
.get('/api/v1/deals/best-watched-prices')
|
||||||
.set('Authorization', `Bearer ${otherToken}`);
|
.set('Authorization', `Bearer ${otherToken}`);
|
||||||
|
|
||||||
expect(otherDealsResponse.status).toBe(200);
|
expect(otherDealsResponse.status).toBe(200);
|
||||||
@@ -373,7 +373,7 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
|||||||
|
|
||||||
// Step 11: Delete account
|
// Step 11: Delete account
|
||||||
const deleteAccountResponse = await getRequest()
|
const deleteAccountResponse = await getRequest()
|
||||||
.delete('/api/users/account')
|
.delete('/api/v1/users/account')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ password: userPassword });
|
.send({ password: userPassword });
|
||||||
|
|
||||||
|
|||||||
@@ -45,21 +45,21 @@ describe('Error Reporting E2E', () => {
|
|||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// Test route that throws a 500 error
|
// Test route that throws a 500 error
|
||||||
app.get('/api/test/error-500', (_req, res, next) => {
|
app.get('/api/v1/test/error-500', (_req, res, next) => {
|
||||||
const error = new Error('Test 500 error for Sentry');
|
const error = new Error('Test 500 error for Sentry');
|
||||||
(error as Error & { statusCode: number }).statusCode = 500;
|
(error as Error & { statusCode: number }).statusCode = 500;
|
||||||
next(error);
|
next(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test route that throws a 400 error (should NOT be sent to Sentry)
|
// Test route that throws a 400 error (should NOT be sent to Sentry)
|
||||||
app.get('/api/test/error-400', (_req, res, next) => {
|
app.get('/api/v1/test/error-400', (_req, res, next) => {
|
||||||
const error = new Error('Test 400 error');
|
const error = new Error('Test 400 error');
|
||||||
(error as Error & { statusCode: number }).statusCode = 400;
|
(error as Error & { statusCode: number }).statusCode = 400;
|
||||||
next(error);
|
next(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test route that succeeds
|
// Test route that succeeds
|
||||||
app.get('/api/test/success', (_req, res) => {
|
app.get('/api/v1/test/success', (_req, res) => {
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -82,7 +82,7 @@ describe('Error Reporting E2E', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await request(app).get('/api/test/error-500');
|
const response = await request(app).get('/api/v1/test/error-500');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body).toEqual({ error: 'Test 500 error for Sentry' });
|
expect(response.body).toEqual({ error: 'Test 500 error for Sentry' });
|
||||||
@@ -102,14 +102,14 @@ describe('Error Reporting E2E', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await request(app).get('/api/test/error-400');
|
const response = await request(app).get('/api/v1/test/error-400');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body).toEqual({ error: 'Test 400 error' });
|
expect(response.body).toEqual({ error: 'Test 400 error' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have a success endpoint that returns 200', async () => {
|
it('should have a success endpoint that returns 200', async () => {
|
||||||
const response = await request(app).get('/api/test/success');
|
const response = await request(app).get('/api/v1/test/success');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body).toEqual({ success: true });
|
expect(response.body).toEqual({ success: true });
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ describe('E2E Flyer Upload and Processing Workflow', () => {
|
|||||||
|
|
||||||
it('should allow a user to upload a flyer and wait for processing to complete', async () => {
|
it('should allow a user to upload a flyer and wait for processing to complete', async () => {
|
||||||
// 1. Register a new user
|
// 1. Register a new user
|
||||||
const registerResponse = await getRequest().post('/api/auth/register').send({
|
const registerResponse = await getRequest().post('/api/v1/auth/register').send({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
password: userPassword,
|
password: userPassword,
|
||||||
full_name: 'E2E Flyer Uploader',
|
full_name: 'E2E Flyer Uploader',
|
||||||
@@ -46,7 +46,7 @@ describe('E2E Flyer Upload and Processing Workflow', () => {
|
|||||||
|
|
||||||
// 2. Login to get the access token
|
// 2. Login to get the access token
|
||||||
const loginResponse = await getRequest()
|
const loginResponse = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
||||||
expect(loginResponse.status).toBe(200);
|
expect(loginResponse.status).toBe(200);
|
||||||
authToken = loginResponse.body.data.token;
|
authToken = loginResponse.body.data.token;
|
||||||
@@ -79,7 +79,7 @@ describe('E2E Flyer Upload and Processing Workflow', () => {
|
|||||||
|
|
||||||
// 4. Upload the flyer
|
// 4. Upload the flyer
|
||||||
const uploadResponse = await getRequest()
|
const uploadResponse = await getRequest()
|
||||||
.post('/api/flyers/upload')
|
.post('/api/v1/flyers/upload')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.attach('flyer', fileBuffer, fileName)
|
.attach('flyer', fileBuffer, fileName)
|
||||||
.field('checksum', checksum);
|
.field('checksum', checksum);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ describe('E2E Inventory/Expiry Management Journey', () => {
|
|||||||
|
|
||||||
it('should complete inventory journey: Register -> Add Items -> Track Expiry -> Consume -> Configure Alerts', async () => {
|
it('should complete inventory journey: Register -> Add Items -> Track Expiry -> Consume -> Configure Alerts', async () => {
|
||||||
// Step 1: Register a new user
|
// Step 1: Register a new user
|
||||||
const registerResponse = await getRequest().post('/api/auth/register').send({
|
const registerResponse = await getRequest().post('/api/v1/auth/register').send({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
password: userPassword,
|
password: userPassword,
|
||||||
full_name: 'Inventory E2E User',
|
full_name: 'Inventory E2E User',
|
||||||
@@ -68,7 +68,7 @@ describe('E2E Inventory/Expiry Management Journey', () => {
|
|||||||
const { response: loginResponse, responseBody: loginResponseBody } = await poll(
|
const { response: loginResponse, responseBody: loginResponseBody } = await poll(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
||||||
const responseBody = response.status === 200 ? response.body : {};
|
const responseBody = response.status === 200 ? response.body : {};
|
||||||
return { response, responseBody };
|
return { response, responseBody };
|
||||||
@@ -156,7 +156,7 @@ describe('E2E Inventory/Expiry Management Journey', () => {
|
|||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const addResponse = await getRequest()
|
const addResponse = await getRequest()
|
||||||
.post('/api/inventory')
|
.post('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send(item);
|
.send(item);
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ describe('E2E Inventory/Expiry Management Journey', () => {
|
|||||||
|
|
||||||
// Step 4: View all inventory
|
// Step 4: View all inventory
|
||||||
const listResponse = await getRequest()
|
const listResponse = await getRequest()
|
||||||
.get('/api/inventory')
|
.get('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(listResponse.status).toBe(200);
|
expect(listResponse.status).toBe(200);
|
||||||
@@ -208,7 +208,7 @@ describe('E2E Inventory/Expiry Management Journey', () => {
|
|||||||
|
|
||||||
// Step 5: Filter by location
|
// Step 5: Filter by location
|
||||||
const fridgeResponse = await getRequest()
|
const fridgeResponse = await getRequest()
|
||||||
.get('/api/inventory?location=fridge')
|
.get('/api/v1/inventory?location=fridge')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(fridgeResponse.status).toBe(200);
|
expect(fridgeResponse.status).toBe(200);
|
||||||
@@ -219,7 +219,7 @@ describe('E2E Inventory/Expiry Management Journey', () => {
|
|||||||
|
|
||||||
// Step 6: View expiring items
|
// Step 6: View expiring items
|
||||||
const expiringResponse = await getRequest()
|
const expiringResponse = await getRequest()
|
||||||
.get('/api/inventory/expiring?days=3')
|
.get('/api/v1/inventory/expiring?days=3')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(expiringResponse.status).toBe(200);
|
expect(expiringResponse.status).toBe(200);
|
||||||
@@ -228,7 +228,7 @@ describe('E2E Inventory/Expiry Management Journey', () => {
|
|||||||
|
|
||||||
// Step 7: View expired items
|
// Step 7: View expired items
|
||||||
const expiredResponse = await getRequest()
|
const expiredResponse = await getRequest()
|
||||||
.get('/api/inventory/expired')
|
.get('/api/v1/inventory/expired')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(expiredResponse.status).toBe(200);
|
expect(expiredResponse.status).toBe(200);
|
||||||
@@ -276,7 +276,7 @@ describe('E2E Inventory/Expiry Management Journey', () => {
|
|||||||
// Step 11: Configure alert settings for email
|
// Step 11: Configure alert settings for email
|
||||||
// The API uses PUT /inventory/alerts/:alertMethod with days_before_expiry and is_enabled
|
// The API uses PUT /inventory/alerts/:alertMethod with days_before_expiry and is_enabled
|
||||||
const alertSettingsResponse = await getRequest()
|
const alertSettingsResponse = await getRequest()
|
||||||
.put('/api/inventory/alerts/email')
|
.put('/api/v1/inventory/alerts/email')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
is_enabled: true,
|
is_enabled: true,
|
||||||
@@ -289,7 +289,7 @@ describe('E2E Inventory/Expiry Management Journey', () => {
|
|||||||
|
|
||||||
// Step 12: Verify alert settings were saved
|
// Step 12: Verify alert settings were saved
|
||||||
const getSettingsResponse = await getRequest()
|
const getSettingsResponse = await getRequest()
|
||||||
.get('/api/inventory/alerts')
|
.get('/api/v1/inventory/alerts')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(getSettingsResponse.status).toBe(200);
|
expect(getSettingsResponse.status).toBe(200);
|
||||||
@@ -301,7 +301,7 @@ describe('E2E Inventory/Expiry Management Journey', () => {
|
|||||||
|
|
||||||
// Step 13: Get recipe suggestions based on expiring items
|
// Step 13: Get recipe suggestions based on expiring items
|
||||||
const suggestionsResponse = await getRequest()
|
const suggestionsResponse = await getRequest()
|
||||||
.get('/api/inventory/recipes/suggestions')
|
.get('/api/v1/inventory/recipes/suggestions')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(suggestionsResponse.status).toBe(200);
|
expect(suggestionsResponse.status).toBe(200);
|
||||||
@@ -346,13 +346,13 @@ describe('E2E Inventory/Expiry Management Journey', () => {
|
|||||||
// Step 17: Verify another user cannot access our inventory
|
// Step 17: Verify another user cannot access our inventory
|
||||||
const otherUserEmail = `other-inventory-e2e-${uniqueId}@example.com`;
|
const otherUserEmail = `other-inventory-e2e-${uniqueId}@example.com`;
|
||||||
await getRequest()
|
await getRequest()
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email: otherUserEmail, password: userPassword, full_name: 'Other Inventory User' });
|
.send({ email: otherUserEmail, password: userPassword, full_name: 'Other Inventory User' });
|
||||||
|
|
||||||
const { responseBody: otherLoginData } = await poll(
|
const { responseBody: otherLoginData } = await poll(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: otherUserEmail, password: userPassword, rememberMe: false });
|
.send({ email: otherUserEmail, password: userPassword, rememberMe: false });
|
||||||
const responseBody = response.status === 200 ? response.body : {};
|
const responseBody = response.status === 200 ? response.body : {};
|
||||||
return { response, responseBody };
|
return { response, responseBody };
|
||||||
@@ -373,7 +373,7 @@ describe('E2E Inventory/Expiry Management Journey', () => {
|
|||||||
|
|
||||||
// Other user's inventory should be empty
|
// Other user's inventory should be empty
|
||||||
const otherListResponse = await getRequest()
|
const otherListResponse = await getRequest()
|
||||||
.get('/api/inventory')
|
.get('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${otherToken}`);
|
.set('Authorization', `Bearer ${otherToken}`);
|
||||||
|
|
||||||
expect(otherListResponse.status).toBe(200);
|
expect(otherListResponse.status).toBe(200);
|
||||||
@@ -398,7 +398,7 @@ describe('E2E Inventory/Expiry Management Journey', () => {
|
|||||||
|
|
||||||
// Step 19: Final inventory check
|
// Step 19: Final inventory check
|
||||||
const finalListResponse = await getRequest()
|
const finalListResponse = await getRequest()
|
||||||
.get('/api/inventory')
|
.get('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(finalListResponse.status).toBe(200);
|
expect(finalListResponse.status).toBe(200);
|
||||||
@@ -408,7 +408,7 @@ describe('E2E Inventory/Expiry Management Journey', () => {
|
|||||||
|
|
||||||
// Step 20: Delete account
|
// Step 20: Delete account
|
||||||
const deleteAccountResponse = await getRequest()
|
const deleteAccountResponse = await getRequest()
|
||||||
.delete('/api/users/account')
|
.delete('/api/v1/users/account')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ password: userPassword });
|
.send({ password: userPassword });
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ describe('E2E Receipt Processing Journey', () => {
|
|||||||
|
|
||||||
it('should complete receipt journey: Register -> Upload -> View -> Manage Items -> Add to Inventory', async () => {
|
it('should complete receipt journey: Register -> Upload -> View -> Manage Items -> Add to Inventory', async () => {
|
||||||
// Step 1: Register a new user
|
// Step 1: Register a new user
|
||||||
const registerResponse = await getRequest().post('/api/auth/register').send({
|
const registerResponse = await getRequest().post('/api/v1/auth/register').send({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
password: userPassword,
|
password: userPassword,
|
||||||
full_name: 'Receipt E2E User',
|
full_name: 'Receipt E2E User',
|
||||||
@@ -79,7 +79,7 @@ describe('E2E Receipt Processing Journey', () => {
|
|||||||
const { response: loginResponse, responseBody: loginResponseBody } = await poll(
|
const { response: loginResponse, responseBody: loginResponseBody } = await poll(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
||||||
const responseBody = response.status === 200 ? response.body : {};
|
const responseBody = response.status === 200 ? response.body : {};
|
||||||
return { response, responseBody };
|
return { response, responseBody };
|
||||||
@@ -133,7 +133,7 @@ describe('E2E Receipt Processing Journey', () => {
|
|||||||
|
|
||||||
// Step 4: View receipt list
|
// Step 4: View receipt list
|
||||||
const listResponse = await getRequest()
|
const listResponse = await getRequest()
|
||||||
.get('/api/receipts')
|
.get('/api/v1/receipts')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(listResponse.status).toBe(200);
|
expect(listResponse.status).toBe(200);
|
||||||
@@ -226,7 +226,7 @@ describe('E2E Receipt Processing Journey', () => {
|
|||||||
|
|
||||||
// Step 10: Verify items in inventory
|
// Step 10: Verify items in inventory
|
||||||
const inventoryResponse = await getRequest()
|
const inventoryResponse = await getRequest()
|
||||||
.get('/api/inventory')
|
.get('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(inventoryResponse.status).toBe(200);
|
expect(inventoryResponse.status).toBe(200);
|
||||||
@@ -240,13 +240,13 @@ describe('E2E Receipt Processing Journey', () => {
|
|||||||
// Step 13: Verify another user cannot access our receipt
|
// Step 13: Verify another user cannot access our receipt
|
||||||
const otherUserEmail = `other-receipt-e2e-${uniqueId}@example.com`;
|
const otherUserEmail = `other-receipt-e2e-${uniqueId}@example.com`;
|
||||||
await getRequest()
|
await getRequest()
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email: otherUserEmail, password: userPassword, full_name: 'Other Receipt User' });
|
.send({ email: otherUserEmail, password: userPassword, full_name: 'Other Receipt User' });
|
||||||
|
|
||||||
const { responseBody: otherLoginData } = await poll(
|
const { responseBody: otherLoginData } = await poll(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: otherUserEmail, password: userPassword, rememberMe: false });
|
.send({ email: otherUserEmail, password: userPassword, rememberMe: false });
|
||||||
const responseBody = response.status === 200 ? response.body : {};
|
const responseBody = response.status === 200 ? response.body : {};
|
||||||
return { response, responseBody };
|
return { response, responseBody };
|
||||||
@@ -280,7 +280,7 @@ describe('E2E Receipt Processing Journey', () => {
|
|||||||
|
|
||||||
// Step 15: Test filtering by status
|
// Step 15: Test filtering by status
|
||||||
const completedResponse = await getRequest()
|
const completedResponse = await getRequest()
|
||||||
.get('/api/receipts?status=completed')
|
.get('/api/v1/receipts?status=completed')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(completedResponse.status).toBe(200);
|
expect(completedResponse.status).toBe(200);
|
||||||
@@ -318,7 +318,7 @@ describe('E2E Receipt Processing Journey', () => {
|
|||||||
|
|
||||||
// Step 19: Delete account
|
// Step 19: Delete account
|
||||||
const deleteAccountResponse = await getRequest()
|
const deleteAccountResponse = await getRequest()
|
||||||
.delete('/api/users/account')
|
.delete('/api/v1/users/account')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ password: userPassword });
|
.send({ password: userPassword });
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ describe('E2E UPC Scanning Journey', () => {
|
|||||||
it('should complete full UPC scanning journey: Register -> Scan -> Lookup -> History -> Stats', async () => {
|
it('should complete full UPC scanning journey: Register -> Scan -> Lookup -> History -> Stats', async () => {
|
||||||
// Step 1: Register a new user
|
// Step 1: Register a new user
|
||||||
const registerResponse = await getRequest()
|
const registerResponse = await getRequest()
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email: userEmail, password: userPassword, full_name: 'UPC E2E User' });
|
.send({ email: userEmail, password: userPassword, full_name: 'UPC E2E User' });
|
||||||
expect(registerResponse.status).toBe(201);
|
expect(registerResponse.status).toBe(201);
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ describe('E2E UPC Scanning Journey', () => {
|
|||||||
const { response: loginResponse, responseBody: loginResponseBody } = await poll(
|
const { response: loginResponse, responseBody: loginResponseBody } = await poll(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
||||||
const responseBody = response.status === 200 ? response.body : {};
|
const responseBody = response.status === 200 ? response.body : {};
|
||||||
return { response, responseBody };
|
return { response, responseBody };
|
||||||
@@ -100,7 +100,7 @@ describe('E2E UPC Scanning Journey', () => {
|
|||||||
|
|
||||||
// Step 4: Scan the UPC code
|
// Step 4: Scan the UPC code
|
||||||
const scanResponse = await getRequest()
|
const scanResponse = await getRequest()
|
||||||
.post('/api/upc/scan')
|
.post('/api/v1/upc/scan')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
upc_code: testUpc,
|
upc_code: testUpc,
|
||||||
@@ -126,7 +126,7 @@ describe('E2E UPC Scanning Journey', () => {
|
|||||||
// Step 6: Scan a few more items to build history
|
// Step 6: Scan a few more items to build history
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
const additionalScan = await getRequest()
|
const additionalScan = await getRequest()
|
||||||
.post('/api/upc/scan')
|
.post('/api/v1/upc/scan')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
upc_code: `00000000000${i}`,
|
upc_code: `00000000000${i}`,
|
||||||
@@ -142,7 +142,7 @@ describe('E2E UPC Scanning Journey', () => {
|
|||||||
|
|
||||||
// Step 7: View scan history
|
// Step 7: View scan history
|
||||||
const historyResponse = await getRequest()
|
const historyResponse = await getRequest()
|
||||||
.get('/api/upc/history')
|
.get('/api/v1/upc/history')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(historyResponse.status).toBe(200);
|
expect(historyResponse.status).toBe(200);
|
||||||
@@ -161,7 +161,7 @@ describe('E2E UPC Scanning Journey', () => {
|
|||||||
|
|
||||||
// Step 9: Check user scan statistics
|
// Step 9: Check user scan statistics
|
||||||
const statsResponse = await getRequest()
|
const statsResponse = await getRequest()
|
||||||
.get('/api/upc/stats')
|
.get('/api/v1/upc/stats')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(statsResponse.status).toBe(200);
|
expect(statsResponse.status).toBe(200);
|
||||||
@@ -170,7 +170,7 @@ describe('E2E UPC Scanning Journey', () => {
|
|||||||
|
|
||||||
// Step 10: Test history filtering by scan_source
|
// Step 10: Test history filtering by scan_source
|
||||||
const filteredHistoryResponse = await getRequest()
|
const filteredHistoryResponse = await getRequest()
|
||||||
.get('/api/upc/history?scan_source=manual_entry')
|
.get('/api/v1/upc/history?scan_source=manual_entry')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(filteredHistoryResponse.status).toBe(200);
|
expect(filteredHistoryResponse.status).toBe(200);
|
||||||
@@ -181,13 +181,13 @@ describe('E2E UPC Scanning Journey', () => {
|
|||||||
// Step 11: Verify another user cannot see our scans
|
// Step 11: Verify another user cannot see our scans
|
||||||
const otherUserEmail = `other-upc-e2e-${uniqueId}@example.com`;
|
const otherUserEmail = `other-upc-e2e-${uniqueId}@example.com`;
|
||||||
await getRequest()
|
await getRequest()
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email: otherUserEmail, password: userPassword, full_name: 'Other UPC User' });
|
.send({ email: otherUserEmail, password: userPassword, full_name: 'Other UPC User' });
|
||||||
|
|
||||||
const { responseBody: otherLoginData } = await poll(
|
const { responseBody: otherLoginData } = await poll(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await getRequest()
|
const response = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: otherUserEmail, password: userPassword, rememberMe: false });
|
.send({ email: otherUserEmail, password: userPassword, rememberMe: false });
|
||||||
const responseBody = response.status === 200 ? response.body : {};
|
const responseBody = response.status === 200 ? response.body : {};
|
||||||
return { response, responseBody };
|
return { response, responseBody };
|
||||||
@@ -208,7 +208,7 @@ describe('E2E UPC Scanning Journey', () => {
|
|||||||
|
|
||||||
// Other user's history should be empty
|
// Other user's history should be empty
|
||||||
const otherHistoryResponse = await getRequest()
|
const otherHistoryResponse = await getRequest()
|
||||||
.get('/api/upc/history')
|
.get('/api/v1/upc/history')
|
||||||
.set('Authorization', `Bearer ${otherToken}`);
|
.set('Authorization', `Bearer ${otherToken}`);
|
||||||
|
|
||||||
expect(otherHistoryResponse.status).toBe(200);
|
expect(otherHistoryResponse.status).toBe(200);
|
||||||
@@ -219,7 +219,7 @@ describe('E2E UPC Scanning Journey', () => {
|
|||||||
|
|
||||||
// Step 12: Delete account (self-service)
|
// Step 12: Delete account (self-service)
|
||||||
const deleteAccountResponse = await getRequest()
|
const deleteAccountResponse = await getRequest()
|
||||||
.delete('/api/users/account')
|
.delete('/api/v1/users/account')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ password: userPassword });
|
.send({ password: userPassword });
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ describe('E2E User Journey', () => {
|
|||||||
it('should complete a full user lifecycle: Register -> Login -> Manage List -> Delete Account', async () => {
|
it('should complete a full user lifecycle: Register -> Login -> Manage List -> Delete Account', async () => {
|
||||||
// 1. Register a new user
|
// 1. Register a new user
|
||||||
const registerResponse = await getRequest()
|
const registerResponse = await getRequest()
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email: userEmail, password: userPassword, full_name: 'E2E Traveler' });
|
.send({ email: userEmail, password: userPassword, full_name: 'E2E Traveler' });
|
||||||
|
|
||||||
expect(registerResponse.status).toBe(201);
|
expect(registerResponse.status).toBe(201);
|
||||||
@@ -46,7 +46,7 @@ describe('E2E User Journey', () => {
|
|||||||
let loginAttempts = 0;
|
let loginAttempts = 0;
|
||||||
while (loginAttempts < 10) {
|
while (loginAttempts < 10) {
|
||||||
loginResponse = await getRequest()
|
loginResponse = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
||||||
if (loginResponse.status === 200) break;
|
if (loginResponse.status === 200) break;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
@@ -62,7 +62,7 @@ describe('E2E User Journey', () => {
|
|||||||
|
|
||||||
// 3. Create a Shopping List
|
// 3. Create a Shopping List
|
||||||
const createListResponse = await getRequest()
|
const createListResponse = await getRequest()
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ name: 'E2E Party List' });
|
.send({ name: 'E2E Party List' });
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ describe('E2E User Journey', () => {
|
|||||||
|
|
||||||
// 5. Verify the list and item exist via GET
|
// 5. Verify the list and item exist via GET
|
||||||
const getListsResponse = await getRequest()
|
const getListsResponse = await getRequest()
|
||||||
.get('/api/users/shopping-lists')
|
.get('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(getListsResponse.status).toBe(200);
|
expect(getListsResponse.status).toBe(200);
|
||||||
@@ -94,7 +94,7 @@ describe('E2E User Journey', () => {
|
|||||||
|
|
||||||
// 6. Delete the User Account (Self-Service)
|
// 6. Delete the User Account (Self-Service)
|
||||||
const deleteAccountResponse = await getRequest()
|
const deleteAccountResponse = await getRequest()
|
||||||
.delete('/api/users/account')
|
.delete('/api/v1/users/account')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ password: userPassword });
|
.send({ password: userPassword });
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ describe('E2E User Journey', () => {
|
|||||||
|
|
||||||
// 7. Verify Login is no longer possible
|
// 7. Verify Login is no longer possible
|
||||||
const failLoginResponse = await getRequest()
|
const failLoginResponse = await getRequest()
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
.send({ email: userEmail, password: userPassword, rememberMe: false });
|
||||||
|
|
||||||
expect(failLoginResponse.status).toBe(401);
|
expect(failLoginResponse.status).toBe(401);
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
describe('GET /api/admin/stats', () => {
|
describe('GET /api/admin/stats', () => {
|
||||||
it('should allow an admin to fetch application stats', async () => {
|
it('should allow an admin to fetch application stats', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/admin/stats')
|
.get('/api/v1/admin/stats')
|
||||||
.set('Authorization', `Bearer ${adminToken}`);
|
.set('Authorization', `Bearer ${adminToken}`);
|
||||||
const stats = response.body.data;
|
const stats = response.body.data;
|
||||||
// DEBUG: Log response if it fails expectation
|
// DEBUG: Log response if it fails expectation
|
||||||
@@ -77,7 +77,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should forbid a regular user from fetching application stats', async () => {
|
it('should forbid a regular user from fetching application stats', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/admin/stats')
|
.get('/api/v1/admin/stats')
|
||||||
.set('Authorization', `Bearer ${regularUserToken}`);
|
.set('Authorization', `Bearer ${regularUserToken}`);
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
const errorData = response.body.error;
|
const errorData = response.body.error;
|
||||||
@@ -88,7 +88,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
describe('GET /api/admin/stats/daily', () => {
|
describe('GET /api/admin/stats/daily', () => {
|
||||||
it('should allow an admin to fetch daily stats', async () => {
|
it('should allow an admin to fetch daily stats', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/admin/stats/daily')
|
.get('/api/v1/admin/stats/daily')
|
||||||
.set('Authorization', `Bearer ${adminToken}`);
|
.set('Authorization', `Bearer ${adminToken}`);
|
||||||
const dailyStats = response.body.data;
|
const dailyStats = response.body.data;
|
||||||
expect(dailyStats).toBeDefined();
|
expect(dailyStats).toBeDefined();
|
||||||
@@ -102,7 +102,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should forbid a regular user from fetching daily stats', async () => {
|
it('should forbid a regular user from fetching daily stats', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/admin/stats/daily')
|
.get('/api/v1/admin/stats/daily')
|
||||||
.set('Authorization', `Bearer ${regularUserToken}`);
|
.set('Authorization', `Bearer ${regularUserToken}`);
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
const errorData = response.body.error;
|
const errorData = response.body.error;
|
||||||
@@ -115,7 +115,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
// This test just verifies access and correct response shape.
|
// This test just verifies access and correct response shape.
|
||||||
// More detailed tests would require seeding corrections.
|
// More detailed tests would require seeding corrections.
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/admin/corrections')
|
.get('/api/v1/admin/corrections')
|
||||||
.set('Authorization', `Bearer ${adminToken}`);
|
.set('Authorization', `Bearer ${adminToken}`);
|
||||||
const corrections = response.body.data;
|
const corrections = response.body.data;
|
||||||
expect(corrections).toBeDefined();
|
expect(corrections).toBeDefined();
|
||||||
@@ -124,7 +124,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should forbid a regular user from fetching suggested corrections', async () => {
|
it('should forbid a regular user from fetching suggested corrections', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/admin/corrections')
|
.get('/api/v1/admin/corrections')
|
||||||
.set('Authorization', `Bearer ${regularUserToken}`);
|
.set('Authorization', `Bearer ${regularUserToken}`);
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
const errorData = response.body.error;
|
const errorData = response.body.error;
|
||||||
@@ -135,7 +135,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
describe('GET /api/admin/brands', () => {
|
describe('GET /api/admin/brands', () => {
|
||||||
it('should allow an admin to fetch all brands', async () => {
|
it('should allow an admin to fetch all brands', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/admin/brands')
|
.get('/api/v1/admin/brands')
|
||||||
.set('Authorization', `Bearer ${adminToken}`);
|
.set('Authorization', `Bearer ${adminToken}`);
|
||||||
const brands = response.body.data;
|
const brands = response.body.data;
|
||||||
expect(brands).toBeDefined();
|
expect(brands).toBeDefined();
|
||||||
@@ -147,7 +147,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should forbid a regular user from fetching all brands', async () => {
|
it('should forbid a regular user from fetching all brands', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/admin/brands')
|
.get('/api/v1/admin/brands')
|
||||||
.set('Authorization', `Bearer ${regularUserToken}`);
|
.set('Authorization', `Bearer ${regularUserToken}`);
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
const errorData = response.body.error;
|
const errorData = response.body.error;
|
||||||
@@ -335,7 +335,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
describe('GET /api/admin/queues/status', () => {
|
describe('GET /api/admin/queues/status', () => {
|
||||||
it('should return queue status for all queues', async () => {
|
it('should return queue status for all queues', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/admin/queues/status')
|
.get('/api/v1/admin/queues/status')
|
||||||
.set('Authorization', `Bearer ${adminToken}`);
|
.set('Authorization', `Bearer ${adminToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -352,7 +352,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should forbid regular users from viewing queue status', async () => {
|
it('should forbid regular users from viewing queue status', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/admin/queues/status')
|
.get('/api/v1/admin/queues/status')
|
||||||
.set('Authorization', `Bearer ${regularUserToken}`);
|
.set('Authorization', `Bearer ${regularUserToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
@@ -363,7 +363,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
describe('POST /api/admin/trigger/analytics-report', () => {
|
describe('POST /api/admin/trigger/analytics-report', () => {
|
||||||
it('should enqueue an analytics report job', async () => {
|
it('should enqueue an analytics report job', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/admin/trigger/analytics-report')
|
.post('/api/v1/admin/trigger/analytics-report')
|
||||||
.set('Authorization', `Bearer ${adminToken}`);
|
.set('Authorization', `Bearer ${adminToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(202); // 202 Accepted for async job enqueue
|
expect(response.status).toBe(202); // 202 Accepted for async job enqueue
|
||||||
@@ -373,7 +373,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should forbid regular users from triggering analytics report', async () => {
|
it('should forbid regular users from triggering analytics report', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/admin/trigger/analytics-report')
|
.post('/api/v1/admin/trigger/analytics-report')
|
||||||
.set('Authorization', `Bearer ${regularUserToken}`);
|
.set('Authorization', `Bearer ${regularUserToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
@@ -383,7 +383,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
describe('POST /api/admin/trigger/weekly-analytics', () => {
|
describe('POST /api/admin/trigger/weekly-analytics', () => {
|
||||||
it('should enqueue a weekly analytics job', async () => {
|
it('should enqueue a weekly analytics job', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/admin/trigger/weekly-analytics')
|
.post('/api/v1/admin/trigger/weekly-analytics')
|
||||||
.set('Authorization', `Bearer ${adminToken}`);
|
.set('Authorization', `Bearer ${adminToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(202); // 202 Accepted for async job enqueue
|
expect(response.status).toBe(202); // 202 Accepted for async job enqueue
|
||||||
@@ -393,7 +393,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should forbid regular users from triggering weekly analytics', async () => {
|
it('should forbid regular users from triggering weekly analytics', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/admin/trigger/weekly-analytics')
|
.post('/api/v1/admin/trigger/weekly-analytics')
|
||||||
.set('Authorization', `Bearer ${regularUserToken}`);
|
.set('Authorization', `Bearer ${regularUserToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
@@ -403,7 +403,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
describe('POST /api/admin/trigger/daily-deal-check', () => {
|
describe('POST /api/admin/trigger/daily-deal-check', () => {
|
||||||
it('should enqueue a daily deal check job', async () => {
|
it('should enqueue a daily deal check job', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/admin/trigger/daily-deal-check')
|
.post('/api/v1/admin/trigger/daily-deal-check')
|
||||||
.set('Authorization', `Bearer ${adminToken}`);
|
.set('Authorization', `Bearer ${adminToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(202); // 202 Accepted for async job trigger
|
expect(response.status).toBe(202); // 202 Accepted for async job trigger
|
||||||
@@ -413,7 +413,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should forbid regular users from triggering daily deal check', async () => {
|
it('should forbid regular users from triggering daily deal check', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/admin/trigger/daily-deal-check')
|
.post('/api/v1/admin/trigger/daily-deal-check')
|
||||||
.set('Authorization', `Bearer ${regularUserToken}`);
|
.set('Authorization', `Bearer ${regularUserToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
@@ -423,7 +423,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
describe('POST /api/admin/system/clear-cache', () => {
|
describe('POST /api/admin/system/clear-cache', () => {
|
||||||
it('should clear the application cache', async () => {
|
it('should clear the application cache', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/admin/system/clear-cache')
|
.post('/api/v1/admin/system/clear-cache')
|
||||||
.set('Authorization', `Bearer ${adminToken}`);
|
.set('Authorization', `Bearer ${adminToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -433,7 +433,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should forbid regular users from clearing cache', async () => {
|
it('should forbid regular users from clearing cache', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/admin/system/clear-cache')
|
.post('/api/v1/admin/system/clear-cache')
|
||||||
.set('Authorization', `Bearer ${regularUserToken}`);
|
.set('Authorization', `Bearer ${regularUserToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
@@ -443,7 +443,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
describe('POST /api/admin/jobs/:queue/:id/retry', () => {
|
describe('POST /api/admin/jobs/:queue/:id/retry', () => {
|
||||||
it('should return validation error for invalid queue name', async () => {
|
it('should return validation error for invalid queue name', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/admin/jobs/invalid-queue-name/1/retry')
|
.post('/api/v1/admin/jobs/invalid-queue-name/1/retry')
|
||||||
.set('Authorization', `Bearer ${adminToken}`);
|
.set('Authorization', `Bearer ${adminToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -453,7 +453,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should return 404 for non-existent job', async () => {
|
it('should return 404 for non-existent job', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/admin/jobs/flyer-processing/999999999/retry')
|
.post('/api/v1/admin/jobs/flyer-processing/999999999/retry')
|
||||||
.set('Authorization', `Bearer ${adminToken}`);
|
.set('Authorization', `Bearer ${adminToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
@@ -462,7 +462,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should forbid regular users from retrying jobs', async () => {
|
it('should forbid regular users from retrying jobs', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/admin/jobs/flyer-processing/1/retry')
|
.post('/api/v1/admin/jobs/flyer-processing/1/retry')
|
||||||
.set('Authorization', `Bearer ${regularUserToken}`);
|
.set('Authorization', `Bearer ${regularUserToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
@@ -473,7 +473,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
describe('GET /api/admin/users', () => {
|
describe('GET /api/admin/users', () => {
|
||||||
it('should return all users for admin', async () => {
|
it('should return all users for admin', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/admin/users')
|
.get('/api/v1/admin/users')
|
||||||
.set('Authorization', `Bearer ${adminToken}`);
|
.set('Authorization', `Bearer ${adminToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -487,7 +487,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should forbid regular users from listing all users', async () => {
|
it('should forbid regular users from listing all users', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/admin/users')
|
.get('/api/v1/admin/users')
|
||||||
.set('Authorization', `Bearer ${regularUserToken}`);
|
.set('Authorization', `Bearer ${regularUserToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
@@ -497,7 +497,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
describe('GET /api/admin/review/flyers', () => {
|
describe('GET /api/admin/review/flyers', () => {
|
||||||
it('should return pending review flyers for admin', async () => {
|
it('should return pending review flyers for admin', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/admin/review/flyers')
|
.get('/api/v1/admin/review/flyers')
|
||||||
.set('Authorization', `Bearer ${adminToken}`);
|
.set('Authorization', `Bearer ${adminToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -507,7 +507,7 @@ describe('Admin API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should forbid regular users from viewing pending flyers', async () => {
|
it('should forbid regular users from viewing pending flyers', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/admin/review/flyers')
|
.get('/api/v1/admin/review/flyers')
|
||||||
.set('Authorization', `Bearer ${regularUserToken}`);
|
.set('Authorization', `Bearer ${regularUserToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('POST /api/ai/check-flyer should return a boolean', async () => {
|
it('POST /api/ai/check-flyer should return a boolean', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/ai/check-flyer')
|
.post('/api/v1/ai/check-flyer')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.attach('image', Buffer.from('content'), 'test.jpg');
|
.attach('image', Buffer.from('content'), 'test.jpg');
|
||||||
const result = response.body.data;
|
const result = response.body.data;
|
||||||
@@ -75,7 +75,7 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('POST /api/ai/extract-address should return a stubbed address', async () => {
|
it('POST /api/ai/extract-address should return a stubbed address', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/ai/extract-address')
|
.post('/api/v1/ai/extract-address')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.attach('image', Buffer.from('content'), 'test.jpg');
|
.attach('image', Buffer.from('content'), 'test.jpg');
|
||||||
const result = response.body.data;
|
const result = response.body.data;
|
||||||
@@ -85,7 +85,7 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('POST /api/ai/extract-logo should return a stubbed response', async () => {
|
it('POST /api/ai/extract-logo should return a stubbed response', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/ai/extract-logo')
|
.post('/api/v1/ai/extract-logo')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.attach('images', Buffer.from('content'), 'test.jpg');
|
.attach('images', Buffer.from('content'), 'test.jpg');
|
||||||
const result = response.body.data;
|
const result = response.body.data;
|
||||||
@@ -95,7 +95,7 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('POST /api/ai/quick-insights should return a stubbed insight', async () => {
|
it('POST /api/ai/quick-insights should return a stubbed insight', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/ai/quick-insights')
|
.post('/api/v1/ai/quick-insights')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ items: [{ item: 'test' }] });
|
.send({ items: [{ item: 'test' }] });
|
||||||
const result = response.body.data;
|
const result = response.body.data;
|
||||||
@@ -109,7 +109,7 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('POST /api/ai/deep-dive should return a stubbed analysis', async () => {
|
it('POST /api/ai/deep-dive should return a stubbed analysis', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/ai/deep-dive')
|
.post('/api/v1/ai/deep-dive')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ items: [{ item: 'test' }] });
|
.send({ items: [{ item: 'test' }] });
|
||||||
const result = response.body.data;
|
const result = response.body.data;
|
||||||
@@ -123,7 +123,7 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('POST /api/ai/search-web should return a stubbed search result', async () => {
|
it('POST /api/ai/search-web should return a stubbed search result', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/ai/search-web')
|
.post('/api/v1/ai/search-web')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ query: 'test query' });
|
.send({ query: 'test query' });
|
||||||
const result = response.body.data;
|
const result = response.body.data;
|
||||||
@@ -165,7 +165,7 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/ai/plan-trip')
|
.post('/api/v1/ai/plan-trip')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ items: [], store: mockStore, userLocation: mockLocation });
|
.send({ items: [], store: mockStore, userLocation: mockLocation });
|
||||||
// The service for this endpoint is disabled and throws an error, which results in a 500.
|
// The service for this endpoint is disabled and throws an error, which results in a 500.
|
||||||
@@ -182,7 +182,7 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
// The backend for this is not stubbed and will throw an error.
|
// The backend for this is not stubbed and will throw an error.
|
||||||
// This test confirms that the endpoint is protected and responds as expected to a failure.
|
// This test confirms that the endpoint is protected and responds as expected to a failure.
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/ai/generate-image')
|
.post('/api/v1/ai/generate-image')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ prompt: 'a test prompt' });
|
.send({ prompt: 'a test prompt' });
|
||||||
expect(response.status).toBe(501);
|
expect(response.status).toBe(501);
|
||||||
@@ -191,7 +191,7 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
it('POST /api/ai/generate-speech should reject because it is not implemented', async () => {
|
it('POST /api/ai/generate-speech should reject because it is not implemented', async () => {
|
||||||
// The backend for this is not stubbed and will throw an error.
|
// The backend for this is not stubbed and will throw an error.
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/ai/generate-speech')
|
.post('/api/v1/ai/generate-speech')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ text: 'a test prompt' });
|
.send({ text: 'a test prompt' });
|
||||||
expect(response.status).toBe(501);
|
expect(response.status).toBe(501);
|
||||||
@@ -205,7 +205,7 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
// Send requests up to the limit
|
// Send requests up to the limit
|
||||||
for (let i = 0; i < limit; i++) {
|
for (let i = 0; i < limit; i++) {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/ai/quick-insights')
|
.post('/api/v1/ai/quick-insights')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ items });
|
.send({ items });
|
||||||
@@ -214,7 +214,7 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// The next request should be blocked
|
// The next request should be blocked
|
||||||
const blockedResponse = await request
|
const blockedResponse = await request
|
||||||
.post('/api/ai/quick-insights')
|
.post('/api/v1/ai/quick-insights')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ items });
|
.send({ items });
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ describe('Authentication API Integration', () => {
|
|||||||
it('should successfully log in a registered user', async () => {
|
it('should successfully log in a registered user', async () => {
|
||||||
// The `rememberMe` parameter is required. For a test, `false` is a safe default.
|
// The `rememberMe` parameter is required. For a test, `false` is a safe default.
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: false });
|
.send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: false });
|
||||||
const data = response.body.data;
|
const data = response.body.data;
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ describe('Authentication API Integration', () => {
|
|||||||
|
|
||||||
// The loginUser function returns a Response object. We check its status.
|
// The loginUser function returns a Response object. We check its status.
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: adminEmail, password: wrongPassword, rememberMe: false });
|
.send({ email: adminEmail, password: wrongPassword, rememberMe: false });
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
const errorData = response.body.error;
|
const errorData = response.body.error;
|
||||||
@@ -83,7 +83,7 @@ describe('Authentication API Integration', () => {
|
|||||||
|
|
||||||
// The loginUser function returns a Response object. We check its status.
|
// The loginUser function returns a Response object. We check its status.
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: nonExistentEmail, password: anyPassword, rememberMe: false });
|
.send({ email: nonExistentEmail, password: anyPassword, rememberMe: false });
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
const errorData = response.body.error;
|
const errorData = response.body.error;
|
||||||
@@ -103,7 +103,7 @@ describe('Authentication API Integration', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Act: Register the new user.
|
// Act: Register the new user.
|
||||||
const registerResponse = await request.post('/api/auth/register').send(userData);
|
const registerResponse = await request.post('/api/v1/auth/register').send(userData);
|
||||||
|
|
||||||
// Assert 1: Check that the registration was successful and the returned profile is correct.
|
// Assert 1: Check that the registration was successful and the returned profile is correct.
|
||||||
expect(registerResponse.status).toBe(201);
|
expect(registerResponse.status).toBe(201);
|
||||||
@@ -117,7 +117,7 @@ describe('Authentication API Integration', () => {
|
|||||||
|
|
||||||
// Assert 2 (Verification): Fetch the profile using the new token to confirm the value in the DB is null.
|
// Assert 2 (Verification): Fetch the profile using the new token to confirm the value in the DB is null.
|
||||||
const profileResponse = await request
|
const profileResponse = await request
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', `Bearer ${registeredToken}`);
|
.set('Authorization', `Bearer ${registeredToken}`);
|
||||||
|
|
||||||
expect(profileResponse.status).toBe(200);
|
expect(profileResponse.status).toBe(200);
|
||||||
@@ -128,7 +128,7 @@ describe('Authentication API Integration', () => {
|
|||||||
// Arrange: Log in to get a fresh, valid refresh token cookie for this specific test.
|
// Arrange: Log in to get a fresh, valid refresh token cookie for this specific test.
|
||||||
// This ensures the test is self-contained and not affected by other tests.
|
// This ensures the test is self-contained and not affected by other tests.
|
||||||
const loginResponse = await request
|
const loginResponse = await request
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: true });
|
.send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: true });
|
||||||
const refreshTokenCookie = loginResponse.headers['set-cookie'][0].split(';')[0];
|
const refreshTokenCookie = loginResponse.headers['set-cookie'][0].split(';')[0];
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ describe('Authentication API Integration', () => {
|
|||||||
|
|
||||||
// Act: Make a request to the refresh-token endpoint, including the cookie.
|
// Act: Make a request to the refresh-token endpoint, including the cookie.
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/auth/refresh-token')
|
.post('/api/v1/auth/refresh-token')
|
||||||
.set('Cookie', refreshTokenCookie!);
|
.set('Cookie', refreshTokenCookie!);
|
||||||
|
|
||||||
// Assert: Check for a successful response and a new access token.
|
// Assert: Check for a successful response and a new access token.
|
||||||
@@ -151,7 +151,7 @@ describe('Authentication API Integration', () => {
|
|||||||
|
|
||||||
// Act: Make a request to the refresh-token endpoint with the invalid cookie.
|
// Act: Make a request to the refresh-token endpoint with the invalid cookie.
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/auth/refresh-token')
|
.post('/api/v1/auth/refresh-token')
|
||||||
.set('Cookie', invalidRefreshTokenCookie);
|
.set('Cookie', invalidRefreshTokenCookie);
|
||||||
|
|
||||||
// Assert: Check for a 403 Forbidden response.
|
// Assert: Check for a 403 Forbidden response.
|
||||||
@@ -163,13 +163,13 @@ describe('Authentication API Integration', () => {
|
|||||||
it('should successfully log out and clear the refresh token cookie', async () => {
|
it('should successfully log out and clear the refresh token cookie', async () => {
|
||||||
// Arrange: Log in to get a valid refresh token cookie.
|
// Arrange: Log in to get a valid refresh token cookie.
|
||||||
const loginResponse = await request
|
const loginResponse = await request
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: true });
|
.send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: true });
|
||||||
const refreshTokenCookie = loginResponse.headers['set-cookie'][0].split(';')[0];
|
const refreshTokenCookie = loginResponse.headers['set-cookie'][0].split(';')[0];
|
||||||
expect(refreshTokenCookie).toBeDefined();
|
expect(refreshTokenCookie).toBeDefined();
|
||||||
|
|
||||||
// Act: Make a request to the new logout endpoint, including the cookie.
|
// Act: Make a request to the new logout endpoint, including the cookie.
|
||||||
const response = await request.post('/api/auth/logout').set('Cookie', refreshTokenCookie!);
|
const response = await request.post('/api/v1/auth/logout').set('Cookie', refreshTokenCookie!);
|
||||||
|
|
||||||
// Assert: Check for a successful response and a cookie-clearing header.
|
// Assert: Check for a successful response and a cookie-clearing header.
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -186,7 +186,7 @@ describe('Authentication API Integration', () => {
|
|||||||
// Send requests up to the limit. These should all pass.
|
// Send requests up to the limit. These should all pass.
|
||||||
for (let i = 0; i < limit; i++) {
|
for (let i = 0; i < limit; i++) {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/auth/forgot-password')
|
.post('/api/v1/auth/forgot-password')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ email });
|
.send({ email });
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ describe('Authentication API Integration', () => {
|
|||||||
|
|
||||||
// The next request (the 6th one) should be blocked.
|
// The next request (the 6th one) should be blocked.
|
||||||
const blockedResponse = await request
|
const blockedResponse = await request
|
||||||
.post('/api/auth/forgot-password')
|
.post('/api/v1/auth/forgot-password')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true')
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
||||||
.send({ email });
|
.send({ email });
|
||||||
|
|
||||||
@@ -209,14 +209,14 @@ describe('Authentication API Integration', () => {
|
|||||||
|
|
||||||
describe('Token Edge Cases', () => {
|
describe('Token Edge Cases', () => {
|
||||||
it('should reject empty Bearer token', async () => {
|
it('should reject empty Bearer token', async () => {
|
||||||
const response = await request.get('/api/users/profile').set('Authorization', 'Bearer ');
|
const response = await request.get('/api/v1/users/profile').set('Authorization', 'Bearer ');
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject token without dots (invalid JWT structure)', async () => {
|
it('should reject token without dots (invalid JWT structure)', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', 'Bearer notavalidtoken');
|
.set('Authorization', 'Bearer notavalidtoken');
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
@@ -224,7 +224,7 @@ describe('Authentication API Integration', () => {
|
|||||||
|
|
||||||
it('should reject token with only 2 parts (missing signature)', async () => {
|
it('should reject token with only 2 parts (missing signature)', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', 'Bearer header.payload');
|
.set('Authorization', 'Bearer header.payload');
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
@@ -233,7 +233,7 @@ describe('Authentication API Integration', () => {
|
|||||||
it('should reject token with invalid signature', async () => {
|
it('should reject token with invalid signature', async () => {
|
||||||
// Valid structure but tampered signature
|
// Valid structure but tampered signature
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.invalidsig');
|
.set('Authorization', 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.invalidsig');
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
@@ -242,13 +242,13 @@ describe('Authentication API Integration', () => {
|
|||||||
it('should accept lowercase "bearer" scheme (case-insensitive)', async () => {
|
it('should accept lowercase "bearer" scheme (case-insensitive)', async () => {
|
||||||
// First get a valid token
|
// First get a valid token
|
||||||
const loginResponse = await request
|
const loginResponse = await request
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: false });
|
.send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: false });
|
||||||
const token = loginResponse.body.data.token;
|
const token = loginResponse.body.data.token;
|
||||||
|
|
||||||
// Use lowercase "bearer"
|
// Use lowercase "bearer"
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', `bearer ${token}`);
|
.set('Authorization', `bearer ${token}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -256,14 +256,14 @@ describe('Authentication API Integration', () => {
|
|||||||
|
|
||||||
it('should reject Basic auth scheme', async () => {
|
it('should reject Basic auth scheme', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
|
.set('Authorization', 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject missing Authorization header', async () => {
|
it('should reject missing Authorization header', async () => {
|
||||||
const response = await request.get('/api/users/profile');
|
const response = await request.get('/api/v1/users/profile');
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
});
|
});
|
||||||
@@ -273,12 +273,12 @@ describe('Authentication API Integration', () => {
|
|||||||
it('should return same error for wrong password and non-existent user', async () => {
|
it('should return same error for wrong password and non-existent user', async () => {
|
||||||
// Wrong password for existing user
|
// Wrong password for existing user
|
||||||
const wrongPassResponse = await request
|
const wrongPassResponse = await request
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: testUserEmail, password: 'wrong-password', rememberMe: false });
|
.send({ email: testUserEmail, password: 'wrong-password', rememberMe: false });
|
||||||
|
|
||||||
// Non-existent user
|
// Non-existent user
|
||||||
const nonExistentResponse = await request
|
const nonExistentResponse = await request
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: 'nonexistent@example.com', password: 'any-password', rememberMe: false });
|
.send({ email: 'nonexistent@example.com', password: 'any-password', rememberMe: false });
|
||||||
|
|
||||||
// Both should return 401 with the same message
|
// Both should return 401 with the same message
|
||||||
@@ -291,12 +291,12 @@ describe('Authentication API Integration', () => {
|
|||||||
it('should return same response for forgot-password on existing and non-existing email', async () => {
|
it('should return same response for forgot-password on existing and non-existing email', async () => {
|
||||||
// Request for existing user
|
// Request for existing user
|
||||||
const existingResponse = await request
|
const existingResponse = await request
|
||||||
.post('/api/auth/forgot-password')
|
.post('/api/v1/auth/forgot-password')
|
||||||
.send({ email: testUserEmail });
|
.send({ email: testUserEmail });
|
||||||
|
|
||||||
// Request for non-existing user
|
// Request for non-existing user
|
||||||
const nonExistingResponse = await request
|
const nonExistingResponse = await request
|
||||||
.post('/api/auth/forgot-password')
|
.post('/api/v1/auth/forgot-password')
|
||||||
.send({ email: 'nonexistent-user@example.com' });
|
.send({ email: 'nonexistent-user@example.com' });
|
||||||
|
|
||||||
// Both should return 200 with similar success message (prevents email enumeration)
|
// Both should return 200 with similar success message (prevents email enumeration)
|
||||||
@@ -307,7 +307,7 @@ describe('Authentication API Integration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return validation error for missing login fields', async () => {
|
it('should return validation error for missing login fields', async () => {
|
||||||
const response = await request.post('/api/auth/login').send({ email: testUserEmail }); // Missing password
|
const response = await request.post('/api/v1/auth/login').send({ email: testUserEmail }); // Missing password
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
@@ -317,7 +317,7 @@ describe('Authentication API Integration', () => {
|
|||||||
|
|
||||||
describe('Password Reset', () => {
|
describe('Password Reset', () => {
|
||||||
it('should reject reset with invalid token', async () => {
|
it('should reject reset with invalid token', async () => {
|
||||||
const response = await request.post('/api/auth/reset-password').send({
|
const response = await request.post('/api/v1/auth/reset-password').send({
|
||||||
token: 'invalid-reset-token',
|
token: 'invalid-reset-token',
|
||||||
newPassword: TEST_PASSWORD,
|
newPassword: TEST_PASSWORD,
|
||||||
});
|
});
|
||||||
@@ -329,7 +329,7 @@ describe('Authentication API Integration', () => {
|
|||||||
|
|
||||||
describe('Registration Validation', () => {
|
describe('Registration Validation', () => {
|
||||||
it('should reject duplicate email registration', async () => {
|
it('should reject duplicate email registration', async () => {
|
||||||
const response = await request.post('/api/auth/register').send({
|
const response = await request.post('/api/v1/auth/register').send({
|
||||||
email: testUserEmail, // Already exists
|
email: testUserEmail, // Already exists
|
||||||
password: TEST_PASSWORD,
|
password: TEST_PASSWORD,
|
||||||
full_name: 'Duplicate User',
|
full_name: 'Duplicate User',
|
||||||
@@ -341,7 +341,7 @@ describe('Authentication API Integration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reject invalid email format', async () => {
|
it('should reject invalid email format', async () => {
|
||||||
const response = await request.post('/api/auth/register').send({
|
const response = await request.post('/api/v1/auth/register').send({
|
||||||
email: 'not-an-email',
|
email: 'not-an-email',
|
||||||
password: TEST_PASSWORD,
|
password: TEST_PASSWORD,
|
||||||
full_name: 'Invalid Email User',
|
full_name: 'Invalid Email User',
|
||||||
@@ -353,7 +353,7 @@ describe('Authentication API Integration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reject weak password', async () => {
|
it('should reject weak password', async () => {
|
||||||
const response = await request.post('/api/auth/register').send({
|
const response = await request.post('/api/v1/auth/register').send({
|
||||||
email: `weak-pass-${Date.now()}@example.com`,
|
email: `weak-pass-${Date.now()}@example.com`,
|
||||||
password: '123456', // Too weak
|
password: '123456', // Too weak
|
||||||
full_name: 'Weak Password User',
|
full_name: 'Weak Password User',
|
||||||
@@ -366,7 +366,7 @@ describe('Authentication API Integration', () => {
|
|||||||
|
|
||||||
describe('Refresh Token Edge Cases', () => {
|
describe('Refresh Token Edge Cases', () => {
|
||||||
it('should return error when refresh token cookie is missing', async () => {
|
it('should return error when refresh token cookie is missing', async () => {
|
||||||
const response = await request.post('/api/auth/refresh-token');
|
const response = await request.post('/api/v1/auth/refresh-token');
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
expect(response.body.error.message).toBe('Refresh token not found.');
|
expect(response.body.error.message).toBe('Refresh token not found.');
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
describe('GET /api/budgets', () => {
|
describe('GET /api/budgets', () => {
|
||||||
it('should fetch budgets for the authenticated user', async () => {
|
it('should fetch budgets for the authenticated user', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/budgets')
|
.get('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -79,7 +79,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 401 if user is not authenticated', async () => {
|
it('should return 401 if user is not authenticated', async () => {
|
||||||
const response = await request.get('/api/budgets');
|
const response = await request.get('/api/v1/budgets');
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -94,7 +94,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send(newBudgetData);
|
.send(newBudgetData);
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send(invalidBudgetData);
|
.send(invalidBudgetData);
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 401 if user is not authenticated', async () => {
|
it('should return 401 if user is not authenticated', async () => {
|
||||||
const response = await request.post('/api/budgets').send({
|
const response = await request.post('/api/v1/budgets').send({
|
||||||
name: 'Unauthorized Budget',
|
name: 'Unauthorized Budget',
|
||||||
amount_cents: 10000,
|
amount_cents: 10000,
|
||||||
period: 'monthly',
|
period: 'monthly',
|
||||||
@@ -146,7 +146,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should reject period="yearly" (only weekly/monthly allowed)', async () => {
|
it('should reject period="yearly" (only weekly/monthly allowed)', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
name: 'Yearly Budget',
|
name: 'Yearly Budget',
|
||||||
@@ -162,7 +162,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should reject negative amount_cents', async () => {
|
it('should reject negative amount_cents', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
name: 'Negative Budget',
|
name: 'Negative Budget',
|
||||||
@@ -177,7 +177,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should reject invalid date format', async () => {
|
it('should reject invalid date format', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
name: 'Invalid Date Budget',
|
name: 'Invalid Date Budget',
|
||||||
@@ -192,7 +192,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should require name field', async () => {
|
it('should require name field', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
amount_cents: 10000,
|
amount_cents: 10000,
|
||||||
@@ -235,7 +235,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should return 404 when updating a non-existent budget', async () => {
|
it('should return 404 when updating a non-existent budget', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.put('/api/budgets/999999')
|
.put('/api/v1/budgets/999999')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ name: 'Non-existent' });
|
.send({ name: 'Non-existent' });
|
||||||
|
|
||||||
@@ -271,7 +271,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createResponse = await request
|
const createResponse = await request
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send(budgetToDelete);
|
.send(budgetToDelete);
|
||||||
|
|
||||||
@@ -287,7 +287,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Verify it's actually deleted
|
// Verify it's actually deleted
|
||||||
const getResponse = await request
|
const getResponse = await request
|
||||||
.get('/api/budgets')
|
.get('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
const budgets: Budget[] = getResponse.body.data;
|
const budgets: Budget[] = getResponse.body.data;
|
||||||
@@ -296,7 +296,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should return 404 when deleting a non-existent budget', async () => {
|
it('should return 404 when deleting a non-existent budget', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.delete('/api/budgets/999999')
|
.delete('/api/v1/budgets/999999')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
@@ -314,7 +314,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
// Note: This test verifies the endpoint works and returns the correct structure.
|
// Note: This test verifies the endpoint works and returns the correct structure.
|
||||||
// In a real scenario with seeded shopping trip data, we'd verify actual values.
|
// In a real scenario with seeded shopping trip data, we'd verify actual values.
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/budgets/spending-analysis')
|
.get('/api/v1/budgets/spending-analysis')
|
||||||
.query({ startDate: '2025-01-01', endDate: '2025-12-31' })
|
.query({ startDate: '2025-01-01', endDate: '2025-12-31' })
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
@@ -332,7 +332,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should return 400 for invalid date format', async () => {
|
it('should return 400 for invalid date format', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/budgets/spending-analysis')
|
.get('/api/v1/budgets/spending-analysis')
|
||||||
.query({ startDate: 'invalid-date', endDate: '2025-12-31' })
|
.query({ startDate: 'invalid-date', endDate: '2025-12-31' })
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
@@ -341,7 +341,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should return 400 when required query params are missing', async () => {
|
it('should return 400 when required query params are missing', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/budgets/spending-analysis')
|
.get('/api/v1/budgets/spending-analysis')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -349,7 +349,7 @@ describe('Budget API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should return 401 if user is not authenticated', async () => {
|
it('should return 401 if user is not authenticated', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/budgets/spending-analysis')
|
.get('/api/v1/budgets/spending-analysis')
|
||||||
.query({ startDate: '2025-01-01', endDate: '2025-12-31' });
|
.query({ startDate: '2025-01-01', endDate: '2025-12-31' });
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
|
|
||||||
describe('GET /api/categories', () => {
|
describe('GET /api/categories', () => {
|
||||||
it('should return list of all categories', async () => {
|
it('should return list of all categories', async () => {
|
||||||
const response = await request.get('/api/categories');
|
const response = await request.get('/api/v1/categories');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
@@ -34,7 +34,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return categories in alphabetical order', async () => {
|
it('should return categories in alphabetical order', async () => {
|
||||||
const response = await request.get('/api/categories');
|
const response = await request.get('/api/v1/categories');
|
||||||
const categories = response.body.data;
|
const categories = response.body.data;
|
||||||
|
|
||||||
// Verify alphabetical ordering
|
// Verify alphabetical ordering
|
||||||
@@ -46,7 +46,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should include expected categories', async () => {
|
it('should include expected categories', async () => {
|
||||||
const response = await request.get('/api/categories');
|
const response = await request.get('/api/v1/categories');
|
||||||
const categories = response.body.data;
|
const categories = response.body.data;
|
||||||
const categoryNames = categories.map((c: { name: string }) => c.name);
|
const categoryNames = categories.map((c: { name: string }) => c.name);
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
describe('GET /api/categories/:id', () => {
|
describe('GET /api/categories/:id', () => {
|
||||||
it('should return specific category by valid ID', async () => {
|
it('should return specific category by valid ID', async () => {
|
||||||
// First get all categories to find a valid ID
|
// First get all categories to find a valid ID
|
||||||
const listResponse = await request.get('/api/categories');
|
const listResponse = await request.get('/api/v1/categories');
|
||||||
const firstCategory = listResponse.body.data[0];
|
const firstCategory = listResponse.body.data[0];
|
||||||
|
|
||||||
const response = await request.get(`/api/categories/${firstCategory.category_id}`);
|
const response = await request.get(`/api/categories/${firstCategory.category_id}`);
|
||||||
@@ -73,7 +73,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 404 for non-existent category ID', async () => {
|
it('should return 404 for non-existent category ID', async () => {
|
||||||
const response = await request.get('/api/categories/999999');
|
const response = await request.get('/api/v1/categories/999999');
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
@@ -81,7 +81,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for invalid category ID (not a number)', async () => {
|
it('should return 400 for invalid category ID (not a number)', async () => {
|
||||||
const response = await request.get('/api/categories/invalid');
|
const response = await request.get('/api/v1/categories/invalid');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
@@ -89,7 +89,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for negative category ID', async () => {
|
it('should return 400 for negative category ID', async () => {
|
||||||
const response = await request.get('/api/categories/-1');
|
const response = await request.get('/api/v1/categories/-1');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
@@ -97,7 +97,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for zero category ID', async () => {
|
it('should return 400 for zero category ID', async () => {
|
||||||
const response = await request.get('/api/categories/0');
|
const response = await request.get('/api/v1/categories/0');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
@@ -107,7 +107,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
|
|
||||||
describe('GET /api/categories/lookup', () => {
|
describe('GET /api/categories/lookup', () => {
|
||||||
it('should find category by exact name', async () => {
|
it('should find category by exact name', async () => {
|
||||||
const response = await request.get('/api/categories/lookup?name=Dairy%20%26%20Eggs');
|
const response = await request.get('/api/v1/categories/lookup?name=Dairy%20%26%20Eggs');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
@@ -116,7 +116,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should find category by case-insensitive name', async () => {
|
it('should find category by case-insensitive name', async () => {
|
||||||
const response = await request.get('/api/categories/lookup?name=dairy%20%26%20eggs');
|
const response = await request.get('/api/v1/categories/lookup?name=dairy%20%26%20eggs');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
@@ -124,7 +124,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should find category with mixed case', async () => {
|
it('should find category with mixed case', async () => {
|
||||||
const response = await request.get('/api/categories/lookup?name=DaIrY%20%26%20eGgS');
|
const response = await request.get('/api/v1/categories/lookup?name=DaIrY%20%26%20eGgS');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
@@ -132,7 +132,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 404 for non-existent category name', async () => {
|
it('should return 404 for non-existent category name', async () => {
|
||||||
const response = await request.get('/api/categories/lookup?name=NonExistentCategory');
|
const response = await request.get('/api/v1/categories/lookup?name=NonExistentCategory');
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
@@ -140,7 +140,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 if name parameter is missing', async () => {
|
it('should return 400 if name parameter is missing', async () => {
|
||||||
const response = await request.get('/api/categories/lookup');
|
const response = await request.get('/api/v1/categories/lookup');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
@@ -148,7 +148,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for empty name parameter', async () => {
|
it('should return 400 for empty name parameter', async () => {
|
||||||
const response = await request.get('/api/categories/lookup?name=');
|
const response = await request.get('/api/v1/categories/lookup?name=');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
@@ -156,7 +156,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 for whitespace-only name parameter', async () => {
|
it('should return 400 for whitespace-only name parameter', async () => {
|
||||||
const response = await request.get('/api/categories/lookup?name= ');
|
const response = await request.get('/api/v1/categories/lookup?name= ');
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
@@ -164,7 +164,7 @@ describe('Category API Routes (Integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle URL-encoded category names', async () => {
|
it('should handle URL-encoded category names', async () => {
|
||||||
const response = await request.get('/api/categories/lookup?name=Dairy%20%26%20Eggs');
|
const response = await request.get('/api/v1/categories/lookup?name=Dairy%20%26%20Eggs');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ describe('Data Integrity Integration Tests', () => {
|
|||||||
|
|
||||||
// Create some shopping lists
|
// Create some shopping lists
|
||||||
const listResponse = await request
|
const listResponse = await request
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${token}`)
|
.set('Authorization', `Bearer ${token}`)
|
||||||
.send({ name: 'Cascade Test List' });
|
.send({ name: 'Cascade Test List' });
|
||||||
expect(listResponse.status).toBe(201);
|
expect(listResponse.status).toBe(201);
|
||||||
@@ -65,7 +65,7 @@ describe('Data Integrity Integration Tests', () => {
|
|||||||
|
|
||||||
// Delete the user account
|
// Delete the user account
|
||||||
const deleteResponse = await request
|
const deleteResponse = await request
|
||||||
.delete('/api/users/account')
|
.delete('/api/v1/users/account')
|
||||||
.set('Authorization', `Bearer ${token}`)
|
.set('Authorization', `Bearer ${token}`)
|
||||||
.send({ password: TEST_PASSWORD });
|
.send({ password: TEST_PASSWORD });
|
||||||
expect(deleteResponse.status).toBe(200);
|
expect(deleteResponse.status).toBe(200);
|
||||||
@@ -88,7 +88,7 @@ describe('Data Integrity Integration Tests', () => {
|
|||||||
|
|
||||||
// Create a budget
|
// Create a budget
|
||||||
const budgetResponse = await request
|
const budgetResponse = await request
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${token}`)
|
.set('Authorization', `Bearer ${token}`)
|
||||||
.send({
|
.send({
|
||||||
name: 'Cascade Test Budget',
|
name: 'Cascade Test Budget',
|
||||||
@@ -108,7 +108,7 @@ describe('Data Integrity Integration Tests', () => {
|
|||||||
|
|
||||||
// Delete the user account
|
// Delete the user account
|
||||||
const deleteResponse = await request
|
const deleteResponse = await request
|
||||||
.delete('/api/users/account')
|
.delete('/api/v1/users/account')
|
||||||
.set('Authorization', `Bearer ${token}`)
|
.set('Authorization', `Bearer ${token}`)
|
||||||
.send({ password: TEST_PASSWORD });
|
.send({ password: TEST_PASSWORD });
|
||||||
expect(deleteResponse.status).toBe(200);
|
expect(deleteResponse.status).toBe(200);
|
||||||
@@ -131,7 +131,7 @@ describe('Data Integrity Integration Tests', () => {
|
|||||||
|
|
||||||
// Create a shopping list
|
// Create a shopping list
|
||||||
const listResponse = await request
|
const listResponse = await request
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${token}`)
|
.set('Authorization', `Bearer ${token}`)
|
||||||
.send({ name: 'Item Cascade List' });
|
.send({ name: 'Item Cascade List' });
|
||||||
expect(listResponse.status).toBe(201);
|
expect(listResponse.status).toBe(201);
|
||||||
@@ -194,7 +194,7 @@ describe('Data Integrity Integration Tests', () => {
|
|||||||
|
|
||||||
// Try to add item to non-existent list
|
// Try to add item to non-existent list
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/users/shopping-lists/999999/items')
|
.post('/api/v1/users/shopping-lists/999999/items')
|
||||||
.set('Authorization', `Bearer ${token}`)
|
.set('Authorization', `Bearer ${token}`)
|
||||||
.send({ customItemName: 'Invalid List Item', quantity: 1 });
|
.send({ customItemName: 'Invalid List Item', quantity: 1 });
|
||||||
|
|
||||||
@@ -227,13 +227,13 @@ describe('Data Integrity Integration Tests', () => {
|
|||||||
|
|
||||||
// Register first user
|
// Register first user
|
||||||
const firstResponse = await request
|
const firstResponse = await request
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email, password: TEST_PASSWORD, full_name: 'First User' });
|
.send({ email, password: TEST_PASSWORD, full_name: 'First User' });
|
||||||
expect(firstResponse.status).toBe(201);
|
expect(firstResponse.status).toBe(201);
|
||||||
|
|
||||||
// Try to register second user with same email
|
// Try to register second user with same email
|
||||||
const secondResponse = await request
|
const secondResponse = await request
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email, password: TEST_PASSWORD, full_name: 'Second User' });
|
.send({ email, password: TEST_PASSWORD, full_name: 'Second User' });
|
||||||
|
|
||||||
expect(secondResponse.status).toBe(409); // CONFLICT
|
expect(secondResponse.status).toBe(409); // CONFLICT
|
||||||
@@ -255,7 +255,7 @@ describe('Data Integrity Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${token}`)
|
.set('Authorization', `Bearer ${token}`)
|
||||||
.send({
|
.send({
|
||||||
name: 'Invalid Period Budget',
|
name: 'Invalid Period Budget',
|
||||||
@@ -279,7 +279,7 @@ describe('Data Integrity Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${token}`)
|
.set('Authorization', `Bearer ${token}`)
|
||||||
.send({
|
.send({
|
||||||
name: 'Negative Amount Budget',
|
name: 'Negative Amount Budget',
|
||||||
@@ -331,7 +331,7 @@ describe('Data Integrity Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${token}`)
|
.set('Authorization', `Bearer ${token}`)
|
||||||
.send({
|
.send({
|
||||||
// name is missing - required field
|
// name is missing - required field
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ describe('Deals API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
describe('GET /api/deals/best-watched-prices', () => {
|
describe('GET /api/deals/best-watched-prices', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const response = await request.get('/api/deals/best-watched-prices');
|
const response = await request.get('/api/v1/deals/best-watched-prices');
|
||||||
|
|
||||||
// Passport returns 401 Unauthorized for unauthenticated requests
|
// Passport returns 401 Unauthorized for unauthenticated requests
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
@@ -49,7 +49,7 @@ describe('Deals API Routes Integration Tests', () => {
|
|||||||
it('should return empty array for authenticated user with no watched items', async () => {
|
it('should return empty array for authenticated user with no watched items', async () => {
|
||||||
// The test user has no watched items by default, so should get empty array
|
// The test user has no watched items by default, so should get empty array
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/deals/best-watched-prices')
|
.get('/api/v1/deals/best-watched-prices')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -59,7 +59,7 @@ describe('Deals API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should reject invalid JWT token', async () => {
|
it('should reject invalid JWT token', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/deals/best-watched-prices')
|
.get('/api/v1/deals/best-watched-prices')
|
||||||
.set('Authorization', 'Bearer invalid.token.here');
|
.set('Authorization', 'Bearer invalid.token.here');
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
@@ -67,7 +67,7 @@ describe('Deals API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should reject missing Bearer prefix', async () => {
|
it('should reject missing Bearer prefix', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/deals/best-watched-prices')
|
.get('/api/v1/deals/best-watched-prices')
|
||||||
.set('Authorization', authToken);
|
.set('Authorization', authToken);
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.attach('flyerFile', testImagePath);
|
.attach('flyerFile', testImagePath);
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.attach('flyerFile', testImagePath)
|
.attach('flyerFile', testImagePath)
|
||||||
.field('checksum', 'not-a-valid-hex-checksum-at-all!!!!');
|
.field('checksum', 'not-a-valid-hex-checksum-at-all!!!!');
|
||||||
@@ -96,7 +96,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.attach('flyerFile', testImagePath)
|
.attach('flyerFile', testImagePath)
|
||||||
.field('checksum', 'abc123'); // Too short
|
.field('checksum', 'abc123'); // Too short
|
||||||
@@ -111,7 +111,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
const checksum = crypto.randomBytes(32).toString('hex');
|
const checksum = crypto.randomBytes(32).toString('hex');
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.field('checksum', checksum);
|
.field('checksum', checksum);
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
describe('Shopping List Names', () => {
|
describe('Shopping List Names', () => {
|
||||||
it('should accept unicode characters and emojis', async () => {
|
it('should accept unicode characters and emojis', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ name: 'Grocery List 🛒 日本語 émoji' });
|
.send({ name: 'Grocery List 🛒 日本語 émoji' });
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
it('should store XSS payloads as-is (frontend must escape)', async () => {
|
it('should store XSS payloads as-is (frontend must escape)', async () => {
|
||||||
const xssPayload = '<script>alert("xss")</script>';
|
const xssPayload = '<script>alert("xss")</script>';
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ name: xssPayload });
|
.send({ name: xssPayload });
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
it('should reject null bytes in JSON', async () => {
|
it('should reject null bytes in JSON', async () => {
|
||||||
// Null bytes in JSON should be rejected by the JSON parser
|
// Null bytes in JSON should be rejected by the JSON parser
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.send('{"name":"test\u0000value"}');
|
.send('{"name":"test\u0000value"}');
|
||||||
@@ -175,7 +175,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
it("should return 404 (not 403) for accessing another user's shopping list", async () => {
|
it("should return 404 (not 403) for accessing another user's shopping list", async () => {
|
||||||
// Create a shopping list as the primary user
|
// Create a shopping list as the primary user
|
||||||
const createResponse = await request
|
const createResponse = await request
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ name: 'Private List' });
|
.send({ name: 'Private List' });
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
it("should return 404 when trying to update another user's shopping list", async () => {
|
it("should return 404 when trying to update another user's shopping list", async () => {
|
||||||
// Create a shopping list as the primary user
|
// Create a shopping list as the primary user
|
||||||
const createResponse = await request
|
const createResponse = await request
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ name: 'Another Private List' });
|
.send({ name: 'Another Private List' });
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
it("should return 404 when trying to delete another user's shopping list", async () => {
|
it("should return 404 when trying to delete another user's shopping list", async () => {
|
||||||
// Create a shopping list as the primary user
|
// Create a shopping list as the primary user
|
||||||
const createResponse = await request
|
const createResponse = await request
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ name: 'Delete Test List' });
|
.send({ name: 'Delete Test List' });
|
||||||
|
|
||||||
@@ -240,7 +240,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
it('should safely handle SQL injection in query params', async () => {
|
it('should safely handle SQL injection in query params', async () => {
|
||||||
// Attempt SQL injection in limit param
|
// Attempt SQL injection in limit param
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/personalization/master-items')
|
.get('/api/v1/personalization/master-items')
|
||||||
.query({ limit: '10; DROP TABLE users; --' });
|
.query({ limit: '10; DROP TABLE users; --' });
|
||||||
|
|
||||||
// Should either return normal data or a validation error, not crash
|
// Should either return normal data or a validation error, not crash
|
||||||
@@ -250,7 +250,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
|
|
||||||
it('should safely handle SQL injection in search params', async () => {
|
it('should safely handle SQL injection in search params', async () => {
|
||||||
// Attempt SQL injection in flyer search
|
// Attempt SQL injection in flyer search
|
||||||
const response = await request.get('/api/flyers').query({
|
const response = await request.get('/api/v1/flyers').query({
|
||||||
search: "'; DROP TABLE flyers; --",
|
search: "'; DROP TABLE flyers; --",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
describe('API Error Handling', () => {
|
describe('API Error Handling', () => {
|
||||||
it('should return 404 for non-existent resources with clear message', async () => {
|
it('should return 404 for non-existent resources with clear message', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/flyers/99999999')
|
.get('/api/v1/flyers/99999999')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
@@ -273,7 +273,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
|
|
||||||
it('should return validation error for malformed JSON body', async () => {
|
it('should return validation error for malformed JSON body', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.send('{ invalid json }');
|
.send('{ invalid json }');
|
||||||
@@ -283,7 +283,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
|
|
||||||
it('should return validation error for missing required fields', async () => {
|
it('should return validation error for missing required fields', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({}); // Empty body
|
.send({}); // Empty body
|
||||||
|
|
||||||
@@ -294,7 +294,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
|
|
||||||
it('should return validation error for invalid data types', async () => {
|
it('should return validation error for invalid data types', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/budgets')
|
.post('/api/v1/budgets')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
name: 'Test Budget',
|
name: 'Test Budget',
|
||||||
@@ -313,7 +313,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
// Create 5 shopping lists concurrently
|
// Create 5 shopping lists concurrently
|
||||||
const promises = Array.from({ length: 5 }, (_, i) =>
|
const promises = Array.from({ length: 5 }, (_, i) =>
|
||||||
request
|
request
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ name: `Concurrent List ${i + 1}` }),
|
.send({ name: `Concurrent List ${i + 1}` }),
|
||||||
);
|
);
|
||||||
@@ -331,7 +331,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
|
|
||||||
// Verify all lists were created
|
// Verify all lists were created
|
||||||
const listResponse = await request
|
const listResponse = await request
|
||||||
.get('/api/users/shopping-lists')
|
.get('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(listResponse.status).toBe(200);
|
expect(listResponse.status).toBe(200);
|
||||||
@@ -345,7 +345,7 @@ describe('Edge Cases Integration Tests', () => {
|
|||||||
it('should handle concurrent reads without errors', async () => {
|
it('should handle concurrent reads without errors', async () => {
|
||||||
// Make 10 concurrent read requests
|
// Make 10 concurrent read requests
|
||||||
const promises = Array.from({ length: 10 }, () =>
|
const promises = Array.from({ length: 10 }, () =>
|
||||||
request.get('/api/personalization/master-items'),
|
request.get('/api/v1/personalization/master-items'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
const results = await Promise.all(promises);
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
|||||||
console.error('[TEST ACTION] Uploading file with baseUrl:', testBaseUrl);
|
console.error('[TEST ACTION] Uploading file with baseUrl:', testBaseUrl);
|
||||||
|
|
||||||
const uploadReq = request
|
const uploadReq = request
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.field('checksum', checksum)
|
.field('checksum', checksum)
|
||||||
// Pass the baseUrl directly in the form data to ensure the worker receives it,
|
// Pass the baseUrl directly in the form data to ensure the worker receives it,
|
||||||
// bypassing issues with vi.stubEnv in multi-threaded test environments.
|
// bypassing issues with vi.stubEnv in multi-threaded test environments.
|
||||||
@@ -426,7 +426,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
|||||||
|
|
||||||
// 2. Act: Upload the file and wait for processing
|
// 2. Act: Upload the file and wait for processing
|
||||||
const uploadResponse = await request
|
const uploadResponse = await request
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.set('Authorization', `Bearer ${token}`)
|
.set('Authorization', `Bearer ${token}`)
|
||||||
.field('baseUrl', 'https://example.com')
|
.field('baseUrl', 'https://example.com')
|
||||||
.field('checksum', checksum)
|
.field('checksum', checksum)
|
||||||
@@ -552,7 +552,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
|||||||
|
|
||||||
// 2. Act: Upload the file and wait for processing
|
// 2. Act: Upload the file and wait for processing
|
||||||
const uploadResponse = await request
|
const uploadResponse = await request
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.set('Authorization', `Bearer ${token}`)
|
.set('Authorization', `Bearer ${token}`)
|
||||||
.field('baseUrl', 'https://example.com')
|
.field('baseUrl', 'https://example.com')
|
||||||
.field('checksum', checksum)
|
.field('checksum', checksum)
|
||||||
@@ -688,7 +688,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
|||||||
|
|
||||||
// Act 1: Upload the file to start the background job.
|
// Act 1: Upload the file to start the background job.
|
||||||
const uploadResponse = await request
|
const uploadResponse = await request
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.field('baseUrl', 'https://example.com')
|
.field('baseUrl', 'https://example.com')
|
||||||
.field('checksum', checksum)
|
.field('checksum', checksum)
|
||||||
.attach('flyerFile', uniqueContent, uniqueFileName);
|
.attach('flyerFile', uniqueContent, uniqueFileName);
|
||||||
@@ -776,7 +776,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
|||||||
|
|
||||||
// Act 1: Upload the file to start the background job.
|
// Act 1: Upload the file to start the background job.
|
||||||
const uploadResponse = await request
|
const uploadResponse = await request
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.field('baseUrl', 'https://example.com')
|
.field('baseUrl', 'https://example.com')
|
||||||
.field('checksum', checksum)
|
.field('checksum', checksum)
|
||||||
.attach('flyerFile', uniqueContent, uniqueFileName);
|
.attach('flyerFile', uniqueContent, uniqueFileName);
|
||||||
@@ -843,7 +843,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
|||||||
|
|
||||||
// Act 1: Upload the file to start the background job.
|
// Act 1: Upload the file to start the background job.
|
||||||
const uploadResponse = await request
|
const uploadResponse = await request
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.field('baseUrl', 'https://example.com')
|
.field('baseUrl', 'https://example.com')
|
||||||
.field('checksum', checksum)
|
.field('checksum', checksum)
|
||||||
.attach('flyerFile', uniqueContent, uniqueFileName);
|
.attach('flyerFile', uniqueContent, uniqueFileName);
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ describe('Public Flyer API Routes Integration Tests', () => {
|
|||||||
[createdFlyerId],
|
[createdFlyerId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await request.get('/api/flyers');
|
const response = await request.get('/api/v1/flyers');
|
||||||
flyers = response.body.data;
|
flyers = response.body.data;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ describe('Public Flyer API Routes Integration Tests', () => {
|
|||||||
describe('GET /api/flyers', () => {
|
describe('GET /api/flyers', () => {
|
||||||
it('should return a list of flyers', async () => {
|
it('should return a list of flyers', async () => {
|
||||||
// Act: Call the API endpoint using the client function.
|
// Act: Call the API endpoint using the client function.
|
||||||
const response = await request.get('/api/flyers');
|
const response = await request.get('/api/v1/flyers');
|
||||||
const flyers: Flyer[] = response.body.data;
|
const flyers: Flyer[] = response.body.data;
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(flyers).toBeInstanceOf(Array);
|
expect(flyers).toBeInstanceOf(Array);
|
||||||
@@ -121,7 +121,7 @@ describe('Public Flyer API Routes Integration Tests', () => {
|
|||||||
expect(flyerIds.length).toBeGreaterThan(0);
|
expect(flyerIds.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Act: Fetch items for all available flyers.
|
// Act: Fetch items for all available flyers.
|
||||||
const response = await request.post('/api/flyers/items/batch-fetch').send({ flyerIds });
|
const response = await request.post('/api/v1/flyers/items/batch-fetch').send({ flyerIds });
|
||||||
const items: FlyerItem[] = response.body.data;
|
const items: FlyerItem[] = response.body.data;
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(items).toBeInstanceOf(Array);
|
expect(items).toBeInstanceOf(Array);
|
||||||
@@ -139,7 +139,7 @@ describe('Public Flyer API Routes Integration Tests', () => {
|
|||||||
expect(flyerIds.length).toBeGreaterThan(0);
|
expect(flyerIds.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const response = await request.post('/api/flyers/items/batch-count').send({ flyerIds });
|
const response = await request.post('/api/v1/flyers/items/batch-count').send({ flyerIds });
|
||||||
const result = response.body.data;
|
const result = response.body.data;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ describe('Gamification Flow Integration Test', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const uploadResponse = await request
|
const uploadResponse = await request
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/v1/ai/upload-and-process')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.field('checksum', checksum)
|
.field('checksum', checksum)
|
||||||
.field('baseUrl', testBaseUrl)
|
.field('baseUrl', testBaseUrl)
|
||||||
@@ -234,7 +234,7 @@ describe('Gamification Flow Integration Test', () => {
|
|||||||
createdFilePaths.push(savedImagePath);
|
createdFilePaths.push(savedImagePath);
|
||||||
|
|
||||||
// --- Act 3: Fetch the user's achievements (triggers endpoint, response not needed) ---
|
// --- Act 3: Fetch the user's achievements (triggers endpoint, response not needed) ---
|
||||||
await request.get('/api/achievements/me').set('Authorization', `Bearer ${authToken}`);
|
await request.get('/api/v1/achievements/me').set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
// --- Assert 2: Verify the "First-Upload" achievement was awarded ---
|
// --- Assert 2: Verify the "First-Upload" achievement was awarded ---
|
||||||
// The 'Welcome Aboard' achievement is awarded on user creation, so we expect at least two.
|
// The 'Welcome Aboard' achievement is awarded on user creation, so we expect at least two.
|
||||||
@@ -261,7 +261,7 @@ describe('Gamification Flow Integration Test', () => {
|
|||||||
expect(firstUploadAchievement?.points_value).toBeGreaterThan(0);
|
expect(firstUploadAchievement?.points_value).toBeGreaterThan(0);
|
||||||
|
|
||||||
// --- Act 4: Fetch the leaderboard ---
|
// --- Act 4: Fetch the leaderboard ---
|
||||||
const leaderboardResponse = await request.get('/api/achievements/leaderboard');
|
const leaderboardResponse = await request.get('/api/v1/achievements/leaderboard');
|
||||||
const leaderboard: LeaderboardUser[] = leaderboardResponse.body.data;
|
const leaderboard: LeaderboardUser[] = leaderboardResponse.body.data;
|
||||||
|
|
||||||
// --- Assert 3: Verify the user is on the leaderboard with points ---
|
// --- Assert 3: Verify the user is on the leaderboard with points ---
|
||||||
@@ -309,7 +309,7 @@ describe('Gamification Flow Integration Test', () => {
|
|||||||
// Note: This assumes a legacy endpoint exists at `/api/ai/upload-legacy`.
|
// Note: This assumes a legacy endpoint exists at `/api/ai/upload-legacy`.
|
||||||
// This endpoint would be responsible for calling `aiService.processLegacyFlyerUpload`.
|
// This endpoint would be responsible for calling `aiService.processLegacyFlyerUpload`.
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/ai/upload-legacy')
|
.post('/api/v1/ai/upload-legacy')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.field('data', JSON.stringify(legacyPayload))
|
.field('data', JSON.stringify(legacyPayload))
|
||||||
.attach('flyerFile', imageBuffer, uniqueFileName);
|
.attach('flyerFile', imageBuffer, uniqueFileName);
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
.toISOString()
|
.toISOString()
|
||||||
.split('T')[0];
|
.split('T')[0];
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/inventory')
|
.post('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
item_name: 'Milk 2%', // Note: API uses master_item_id to resolve name from master_grocery_items
|
item_name: 'Milk 2%', // Note: API uses master_item_id to resolve name from master_grocery_items
|
||||||
@@ -115,7 +115,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
it('should add item without expiry date', async () => {
|
it('should add item without expiry date', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/inventory')
|
.post('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
item_name: 'Rice',
|
item_name: 'Rice',
|
||||||
@@ -139,7 +139,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
.split('T')[0];
|
.split('T')[0];
|
||||||
const purchaseDate = new Date().toISOString().split('T')[0];
|
const purchaseDate = new Date().toISOString().split('T')[0];
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/inventory')
|
.post('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
item_name: 'Cheese',
|
item_name: 'Cheese',
|
||||||
@@ -161,7 +161,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
it('should reject invalid location', async () => {
|
it('should reject invalid location', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/inventory')
|
.post('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
item_name: 'Test Item',
|
item_name: 'Test Item',
|
||||||
@@ -175,7 +175,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
it('should reject missing item_name', async () => {
|
it('should reject missing item_name', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/inventory')
|
.post('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
@@ -187,7 +187,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reject unauthenticated requests', async () => {
|
it('should reject unauthenticated requests', async () => {
|
||||||
const response = await request.post('/api/inventory').send({
|
const response = await request.post('/api/v1/inventory').send({
|
||||||
item_name: 'Test Item',
|
item_name: 'Test Item',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
location: 'fridge',
|
location: 'fridge',
|
||||||
@@ -210,7 +210,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/inventory')
|
.post('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
item_name: item.name,
|
item_name: item.name,
|
||||||
@@ -229,7 +229,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
it('should return all inventory items', async () => {
|
it('should return all inventory items', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/inventory')
|
.get('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -241,7 +241,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
it('should filter by location', async () => {
|
it('should filter by location', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/inventory')
|
.get('/api/v1/inventory')
|
||||||
.query({ location: 'fridge' })
|
.query({ location: 'fridge' })
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
it('should support pagination', async () => {
|
it('should support pagination', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/inventory')
|
.get('/api/v1/inventory')
|
||||||
.query({ limit: 2, offset: 0 })
|
.query({ limit: 2, offset: 0 })
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
@@ -265,7 +265,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
// Note: expiry_status is computed server-side based on best_before_date, not a query filter
|
// Note: expiry_status is computed server-side based on best_before_date, not a query filter
|
||||||
// This test verifies that items created in this test suite with future dates have correct status
|
// This test verifies that items created in this test suite with future dates have correct status
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/inventory')
|
.get('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -296,7 +296,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
createdUserIds.push(otherUser.user.user_id);
|
createdUserIds.push(otherUser.user.user_id);
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/inventory')
|
.get('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${otherToken}`);
|
.set('Authorization', `Bearer ${otherToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -313,7 +313,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
.toISOString()
|
.toISOString()
|
||||||
.split('T')[0];
|
.split('T')[0];
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/inventory')
|
.post('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
item_name: 'Single Item Test', // Note: API resolves name from master_item_id
|
item_name: 'Single Item Test', // Note: API resolves name from master_item_id
|
||||||
@@ -343,7 +343,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
it('should return 404 for non-existent item', async () => {
|
it('should return 404 for non-existent item', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/inventory/999999')
|
.get('/api/v1/inventory/999999')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
@@ -370,7 +370,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/inventory')
|
.post('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
item_name: 'Update Test Item',
|
item_name: 'Update Test Item',
|
||||||
@@ -440,7 +440,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
it('should delete an inventory item', async () => {
|
it('should delete an inventory item', async () => {
|
||||||
// Create item to delete
|
// Create item to delete
|
||||||
const createResponse = await request
|
const createResponse = await request
|
||||||
.post('/api/inventory')
|
.post('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
item_name: 'Delete Test Item',
|
item_name: 'Delete Test Item',
|
||||||
@@ -473,7 +473,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/inventory')
|
.post('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
item_name: 'Consume Test Item',
|
item_name: 'Consume Test Item',
|
||||||
@@ -512,7 +512,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
it('should return 404 for already consumed or non-existent item', async () => {
|
it('should return 404 for already consumed or non-existent item', async () => {
|
||||||
// Create new item to test double consumption
|
// Create new item to test double consumption
|
||||||
const createResponse = await request
|
const createResponse = await request
|
||||||
.post('/api/inventory')
|
.post('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
item_name: 'Double Consume Test',
|
item_name: 'Double Consume Test',
|
||||||
@@ -565,7 +565,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/inventory')
|
.post('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
item_name: item.name,
|
item_name: item.name,
|
||||||
@@ -584,7 +584,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
it('should return items expiring within default days', async () => {
|
it('should return items expiring within default days', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/inventory/expiring')
|
.get('/api/v1/inventory/expiring')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -595,7 +595,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
it('should respect days parameter', async () => {
|
it('should respect days parameter', async () => {
|
||||||
// Note: The API uses "days" not "days_ahead" parameter
|
// Note: The API uses "days" not "days_ahead" parameter
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/inventory/expiring')
|
.get('/api/v1/inventory/expiring')
|
||||||
.query({ days: 2 })
|
.query({ days: 2 })
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
@@ -610,7 +610,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
// The API handles pantry_locations and item creation properly
|
// The API handles pantry_locations and item creation properly
|
||||||
const pastDate = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
const pastDate = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/inventory')
|
.post('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
item_name: 'Expired Item',
|
item_name: 'Expired Item',
|
||||||
@@ -629,7 +629,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
it('should return expired items', async () => {
|
it('should return expired items', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/inventory/expired')
|
.get('/api/v1/inventory/expired')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -647,7 +647,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
describe('GET /api/inventory/alerts', () => {
|
describe('GET /api/inventory/alerts', () => {
|
||||||
it('should return alert settings', async () => {
|
it('should return alert settings', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/inventory/alerts')
|
.get('/api/v1/inventory/alerts')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -659,7 +659,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
describe('PUT /api/inventory/alerts/:alertMethod', () => {
|
describe('PUT /api/inventory/alerts/:alertMethod', () => {
|
||||||
it('should update alert settings for email method', async () => {
|
it('should update alert settings for email method', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.put('/api/inventory/alerts/email')
|
.put('/api/v1/inventory/alerts/email')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
is_enabled: true,
|
is_enabled: true,
|
||||||
@@ -672,7 +672,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
it('should reject invalid days_before_expiry', async () => {
|
it('should reject invalid days_before_expiry', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.put('/api/inventory/alerts/email')
|
.put('/api/v1/inventory/alerts/email')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
days_before_expiry: 0, // Must be at least 1
|
days_before_expiry: 0, // Must be at least 1
|
||||||
@@ -683,7 +683,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
it('should reject invalid alert method', async () => {
|
it('should reject invalid alert method', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.put('/api/inventory/alerts/invalid_method')
|
.put('/api/v1/inventory/alerts/invalid_method')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
days_before_expiry: 5,
|
days_before_expiry: 5,
|
||||||
@@ -697,7 +697,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
describe('GET /api/inventory/recipes/suggestions - Recipe Suggestions', () => {
|
describe('GET /api/inventory/recipes/suggestions - Recipe Suggestions', () => {
|
||||||
it('should return recipe suggestions for expiring items', async () => {
|
it('should return recipe suggestions for expiring items', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/inventory/recipes/suggestions')
|
.get('/api/v1/inventory/recipes/suggestions')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -710,7 +710,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
it('should handle full add-track-consume workflow', async () => {
|
it('should handle full add-track-consume workflow', async () => {
|
||||||
// Step 1: Add item
|
// Step 1: Add item
|
||||||
const addResponse = await request
|
const addResponse = await request
|
||||||
.post('/api/inventory')
|
.post('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
item_name: 'Workflow Test Item',
|
item_name: 'Workflow Test Item',
|
||||||
@@ -728,7 +728,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
// Step 2: Verify in list
|
// Step 2: Verify in list
|
||||||
const listResponse = await request
|
const listResponse = await request
|
||||||
.get('/api/inventory')
|
.get('/api/v1/inventory')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
const found = listResponse.body.data.items.find(
|
const found = listResponse.body.data.items.find(
|
||||||
@@ -738,7 +738,7 @@ describe('Inventory/Expiry Integration Tests (/api/inventory)', () => {
|
|||||||
|
|
||||||
// Step 3: Check in expiring items (using correct param name: days)
|
// Step 3: Check in expiring items (using correct param name: days)
|
||||||
const expiringResponse = await request
|
const expiringResponse = await request
|
||||||
.get('/api/inventory/expiring')
|
.get('/api/v1/inventory/expiring')
|
||||||
.query({ days: 10 })
|
.query({ days: 10 })
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ describe('Notification API Routes Integration Tests', () => {
|
|||||||
describe('GET /api/users/notifications', () => {
|
describe('GET /api/users/notifications', () => {
|
||||||
it('should fetch unread notifications for the authenticated user by default', async () => {
|
it('should fetch unread notifications for the authenticated user by default', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/users/notifications')
|
.get('/api/v1/users/notifications')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -69,7 +69,7 @@ describe('Notification API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should fetch all notifications when includeRead=true', async () => {
|
it('should fetch all notifications when includeRead=true', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/users/notifications?includeRead=true')
|
.get('/api/v1/users/notifications?includeRead=true')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -80,7 +80,7 @@ describe('Notification API Routes Integration Tests', () => {
|
|||||||
it('should respect pagination with limit and offset', async () => {
|
it('should respect pagination with limit and offset', async () => {
|
||||||
// Fetch with limit=1, should get the latest unread notification
|
// Fetch with limit=1, should get the latest unread notification
|
||||||
const response1 = await request
|
const response1 = await request
|
||||||
.get('/api/users/notifications?limit=1')
|
.get('/api/v1/users/notifications?limit=1')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response1.status).toBe(200);
|
expect(response1.status).toBe(200);
|
||||||
@@ -90,7 +90,7 @@ describe('Notification API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Fetch with limit=1 and offset=1, should get the older unread notification
|
// Fetch with limit=1 and offset=1, should get the older unread notification
|
||||||
const response2 = await request
|
const response2 = await request
|
||||||
.get('/api/users/notifications?limit=1&offset=1')
|
.get('/api/v1/users/notifications?limit=1&offset=1')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response2.status).toBe(200);
|
expect(response2.status).toBe(200);
|
||||||
@@ -100,7 +100,7 @@ describe('Notification API Routes Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 401 if user is not authenticated', async () => {
|
it('should return 401 if user is not authenticated', async () => {
|
||||||
const response = await request.get('/api/users/notifications');
|
const response = await request.get('/api/v1/users/notifications');
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -132,7 +132,7 @@ describe('Notification API Routes Integration Tests', () => {
|
|||||||
describe('POST /api/users/notifications/mark-all-read', () => {
|
describe('POST /api/users/notifications/mark-all-read', () => {
|
||||||
it('should mark all unread notifications as read', async () => {
|
it('should mark all unread notifications as read', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/users/notifications/mark-all-read')
|
.post('/api/v1/users/notifications/mark-all-read')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(204);
|
expect(response.status).toBe(204);
|
||||||
@@ -149,7 +149,7 @@ describe('Notification API Routes Integration Tests', () => {
|
|||||||
describe('Job Status Polling', () => {
|
describe('Job Status Polling', () => {
|
||||||
describe('GET /api/ai/jobs/:id/status', () => {
|
describe('GET /api/ai/jobs/:id/status', () => {
|
||||||
it('should return 404 for non-existent job', async () => {
|
it('should return 404 for non-existent job', async () => {
|
||||||
const response = await request.get('/api/ai/jobs/nonexistent-job-id/status');
|
const response = await request.get('/api/v1/ai/jobs/nonexistent-job-id/status');
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
@@ -159,7 +159,7 @@ describe('Notification API Routes Integration Tests', () => {
|
|||||||
it('should be accessible without authentication (public endpoint)', async () => {
|
it('should be accessible without authentication (public endpoint)', async () => {
|
||||||
// This verifies that job status can be polled without auth
|
// This verifies that job status can be polled without auth
|
||||||
// This is important for UX where users may poll status from frontend
|
// This is important for UX where users may poll status from frontend
|
||||||
const response = await request.get('/api/ai/jobs/test-job-123/status');
|
const response = await request.get('/api/v1/ai/jobs/test-job-123/status');
|
||||||
|
|
||||||
// Should return 404 (job not found) rather than 401 (unauthorized)
|
// Should return 404 (job not found) rather than 401 (unauthorized)
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
@@ -195,7 +195,7 @@ describe('Notification API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should return 404 for non-existent notification', async () => {
|
it('should return 404 for non-existent notification', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.delete('/api/users/notifications/999999')
|
.delete('/api/v1/users/notifications/999999')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ describe('Price History API Integration Test (/api/price-history)', () => {
|
|||||||
|
|
||||||
it('should return the correct price history for a given master item ID', async () => {
|
it('should return the correct price history for a given master item ID', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/price-history')
|
.post('/api/v1/price-history')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ masterItemIds: [masterItemId] });
|
.send({ masterItemIds: [masterItemId] });
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ describe('Price History API Integration Test (/api/price-history)', () => {
|
|||||||
|
|
||||||
it('should respect the limit parameter', async () => {
|
it('should respect the limit parameter', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/price-history')
|
.post('/api/v1/price-history')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ masterItemIds: [masterItemId], limit: 2 });
|
.send({ masterItemIds: [masterItemId], limit: 2 });
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ describe('Price History API Integration Test (/api/price-history)', () => {
|
|||||||
|
|
||||||
it('should respect the offset parameter', async () => {
|
it('should respect the offset parameter', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/price-history')
|
.post('/api/v1/price-history')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ masterItemIds: [masterItemId], limit: 2, offset: 1 });
|
.send({ masterItemIds: [masterItemId], limit: 2, offset: 1 });
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ describe('Price History API Integration Test (/api/price-history)', () => {
|
|||||||
|
|
||||||
it('should return price history sorted by date in ascending order', async () => {
|
it('should return price history sorted by date in ascending order', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/price-history')
|
.post('/api/v1/price-history')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ masterItemIds: [masterItemId] });
|
.send({ masterItemIds: [masterItemId] });
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ describe('Price History API Integration Test (/api/price-history)', () => {
|
|||||||
|
|
||||||
it('should return an empty array for a master item ID with no price history', async () => {
|
it('should return an empty array for a master item ID with no price history', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/price-history')
|
.post('/api/v1/price-history')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ masterItemIds: [999999] });
|
.send({ masterItemIds: [999999] });
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
|
|||||||
@@ -109,31 +109,31 @@ describe('Public API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
describe('Health Check Endpoints', () => {
|
describe('Health Check Endpoints', () => {
|
||||||
it('GET /api/health/ping should return "pong"', async () => {
|
it('GET /api/health/ping should return "pong"', async () => {
|
||||||
const response = await request.get('/api/health/ping');
|
const response = await request.get('/api/v1/health/ping');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.message).toBe('pong');
|
expect(response.body.data.message).toBe('pong');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('GET /api/health/db-schema should return success', async () => {
|
it('GET /api/health/db-schema should return success', async () => {
|
||||||
const response = await request.get('/api/health/db-schema');
|
const response = await request.get('/api/v1/health/db-schema');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('GET /api/health/storage should return success', async () => {
|
it('GET /api/health/storage should return success', async () => {
|
||||||
const response = await request.get('/api/health/storage');
|
const response = await request.get('/api/v1/health/storage');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('GET /api/health/db-pool should return success', async () => {
|
it('GET /api/health/db-pool should return success', async () => {
|
||||||
const response = await request.get('/api/health/db-pool');
|
const response = await request.get('/api/v1/health/db-pool');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('GET /api/health/time should return the server time', async () => {
|
it('GET /api/health/time should return the server time', async () => {
|
||||||
const response = await request.get('/api/health/time');
|
const response = await request.get('/api/v1/health/time');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data).toHaveProperty('currentTime');
|
expect(response.body.data).toHaveProperty('currentTime');
|
||||||
expect(response.body.data).toHaveProperty('year');
|
expect(response.body.data).toHaveProperty('year');
|
||||||
@@ -143,7 +143,7 @@ describe('Public API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
describe('Public Data Endpoints', () => {
|
describe('Public Data Endpoints', () => {
|
||||||
it('GET /api/flyers should return a list of flyers', async () => {
|
it('GET /api/flyers should return a list of flyers', async () => {
|
||||||
const response = await request.get('/api/flyers');
|
const response = await request.get('/api/v1/flyers');
|
||||||
const flyers: Flyer[] = response.body.data;
|
const flyers: Flyer[] = response.body.data;
|
||||||
expect(flyers.length).toBeGreaterThan(0);
|
expect(flyers.length).toBeGreaterThan(0);
|
||||||
const foundFlyer = flyers.find((f) => f.flyer_id === testFlyer.flyer_id);
|
const foundFlyer = flyers.find((f) => f.flyer_id === testFlyer.flyer_id);
|
||||||
@@ -162,7 +162,7 @@ describe('Public API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('POST /api/flyers/items/batch-fetch should return items for multiple flyers', async () => {
|
it('POST /api/flyers/items/batch-fetch should return items for multiple flyers', async () => {
|
||||||
const flyerIds = [testFlyer.flyer_id];
|
const flyerIds = [testFlyer.flyer_id];
|
||||||
const response = await request.post('/api/flyers/items/batch-fetch').send({ flyerIds });
|
const response = await request.post('/api/v1/flyers/items/batch-fetch').send({ flyerIds });
|
||||||
const items: FlyerItem[] = response.body.data;
|
const items: FlyerItem[] = response.body.data;
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(items).toBeInstanceOf(Array);
|
expect(items).toBeInstanceOf(Array);
|
||||||
@@ -171,14 +171,14 @@ describe('Public API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('POST /api/flyers/items/batch-count should return a count for multiple flyers', async () => {
|
it('POST /api/flyers/items/batch-count should return a count for multiple flyers', async () => {
|
||||||
const flyerIds = [testFlyer.flyer_id];
|
const flyerIds = [testFlyer.flyer_id];
|
||||||
const response = await request.post('/api/flyers/items/batch-count').send({ flyerIds });
|
const response = await request.post('/api/v1/flyers/items/batch-count').send({ flyerIds });
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.count).toBeTypeOf('number');
|
expect(response.body.data.count).toBeTypeOf('number');
|
||||||
expect(response.body.data.count).toBeGreaterThan(0);
|
expect(response.body.data.count).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('GET /api/personalization/master-items should return a list of master grocery items', async () => {
|
it('GET /api/personalization/master-items should return a list of master grocery items', async () => {
|
||||||
const response = await request.get('/api/personalization/master-items');
|
const response = await request.get('/api/v1/personalization/master-items');
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
// The endpoint returns { items: [...], total: N } for pagination support
|
// The endpoint returns { items: [...], total: N } for pagination support
|
||||||
expect(response.body.data).toHaveProperty('items');
|
expect(response.body.data).toHaveProperty('items');
|
||||||
@@ -190,7 +190,7 @@ describe('Public API Routes Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('GET /api/recipes/by-sale-percentage should return recipes', async () => {
|
it('GET /api/recipes/by-sale-percentage should return recipes', async () => {
|
||||||
const response = await request.get('/api/recipes/by-sale-percentage?minPercentage=10');
|
const response = await request.get('/api/v1/recipes/by-sale-percentage?minPercentage=10');
|
||||||
const recipes: Recipe[] = response.body.data;
|
const recipes: Recipe[] = response.body.data;
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(recipes).toBeInstanceOf(Array);
|
expect(recipes).toBeInstanceOf(Array);
|
||||||
@@ -199,7 +199,7 @@ describe('Public API Routes Integration Tests', () => {
|
|||||||
it('GET /api/recipes/by-ingredient-and-tag should return recipes', async () => {
|
it('GET /api/recipes/by-ingredient-and-tag should return recipes', async () => {
|
||||||
// This test is now less brittle. It might return our created recipe or others.
|
// This test is now less brittle. It might return our created recipe or others.
|
||||||
const response = await request.get(
|
const response = await request.get(
|
||||||
'/api/recipes/by-ingredient-and-tag?ingredient=Test&tag=Public',
|
'/api/v1/recipes/by-ingredient-and-tag?ingredient=Test&tag=Public',
|
||||||
);
|
);
|
||||||
const recipes: Recipe[] = response.body.data;
|
const recipes: Recipe[] = response.body.data;
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -222,7 +222,7 @@ describe('Public API Routes Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('GET /api/stats/most-frequent-sales should return frequent items', async () => {
|
it('GET /api/stats/most-frequent-sales should return frequent items', async () => {
|
||||||
const response = await request.get('/api/stats/most-frequent-sales?days=365&limit=5');
|
const response = await request.get('/api/v1/stats/most-frequent-sales?days=365&limit=5');
|
||||||
const items = response.body.data;
|
const items = response.body.data;
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(items).toBeInstanceOf(Array);
|
expect(items).toBeInstanceOf(Array);
|
||||||
@@ -230,7 +230,7 @@ describe('Public API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('GET /api/personalization/dietary-restrictions should return a list of restrictions', async () => {
|
it('GET /api/personalization/dietary-restrictions should return a list of restrictions', async () => {
|
||||||
// This test relies on static seed data for a lookup table, which is acceptable.
|
// This test relies on static seed data for a lookup table, which is acceptable.
|
||||||
const response = await request.get('/api/personalization/dietary-restrictions');
|
const response = await request.get('/api/v1/personalization/dietary-restrictions');
|
||||||
const restrictions: DietaryRestriction[] = response.body.data;
|
const restrictions: DietaryRestriction[] = response.body.data;
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(restrictions).toBeInstanceOf(Array);
|
expect(restrictions).toBeInstanceOf(Array);
|
||||||
@@ -239,7 +239,7 @@ describe('Public API Routes Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('GET /api/personalization/appliances should return a list of appliances', async () => {
|
it('GET /api/personalization/appliances should return a list of appliances', async () => {
|
||||||
const response = await request.get('/api/personalization/appliances');
|
const response = await request.get('/api/v1/personalization/appliances');
|
||||||
const appliances: Appliance[] = response.body.data;
|
const appliances: Appliance[] = response.body.data;
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(appliances).toBeInstanceOf(Array);
|
expect(appliances).toBeInstanceOf(Array);
|
||||||
@@ -256,7 +256,7 @@ describe('Public API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
for (let i = 0; i < maxRequests; i++) {
|
for (let i = 0; i < maxRequests; i++) {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/personalization/master-items')
|
.get('/api/v1/personalization/master-items')
|
||||||
.set('X-Test-Rate-Limit-Enable', 'true'); // Enable rate limiter middleware
|
.set('X-Test-Rate-Limit-Enable', 'true'); // Enable rate limiter middleware
|
||||||
|
|
||||||
if (response.status === 429) {
|
if (response.status === 429) {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ describe('Reactions API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
describe('GET /api/reactions', () => {
|
describe('GET /api/reactions', () => {
|
||||||
it('should return reactions (public endpoint)', async () => {
|
it('should return reactions (public endpoint)', async () => {
|
||||||
const response = await request.get('/api/reactions');
|
const response = await request.get('/api/v1/reactions');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
@@ -72,7 +72,7 @@ describe('Reactions API Routes Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should filter reactions by entityType', async () => {
|
it('should filter reactions by entityType', async () => {
|
||||||
const response = await request.get('/api/reactions').query({ entityType: 'recipe' });
|
const response = await request.get('/api/v1/reactions').query({ entityType: 'recipe' });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
@@ -81,7 +81,7 @@ describe('Reactions API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should filter reactions by entityId', async () => {
|
it('should filter reactions by entityId', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/reactions')
|
.get('/api/v1/reactions')
|
||||||
.query({ entityType: 'recipe', entityId: String(testRecipeId) });
|
.query({ entityType: 'recipe', entityId: String(testRecipeId) });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -93,7 +93,7 @@ describe('Reactions API Routes Integration Tests', () => {
|
|||||||
describe('GET /api/reactions/summary', () => {
|
describe('GET /api/reactions/summary', () => {
|
||||||
it('should return reaction summary for an entity', async () => {
|
it('should return reaction summary for an entity', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/reactions/summary')
|
.get('/api/v1/reactions/summary')
|
||||||
.query({ entityType: 'recipe', entityId: String(testRecipeId) });
|
.query({ entityType: 'recipe', entityId: String(testRecipeId) });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -104,7 +104,7 @@ describe('Reactions API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should return 400 when entityType is missing', async () => {
|
it('should return 400 when entityType is missing', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/reactions/summary')
|
.get('/api/v1/reactions/summary')
|
||||||
.query({ entityId: String(testRecipeId) });
|
.query({ entityId: String(testRecipeId) });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -112,7 +112,7 @@ describe('Reactions API Routes Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 when entityId is missing', async () => {
|
it('should return 400 when entityId is missing', async () => {
|
||||||
const response = await request.get('/api/reactions/summary').query({ entityType: 'recipe' });
|
const response = await request.get('/api/v1/reactions/summary').query({ entityType: 'recipe' });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
@@ -121,7 +121,7 @@ describe('Reactions API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
describe('POST /api/reactions/toggle', () => {
|
describe('POST /api/reactions/toggle', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const response = await request.post('/api/reactions/toggle').send({
|
const response = await request.post('/api/v1/reactions/toggle').send({
|
||||||
entity_type: 'recipe',
|
entity_type: 'recipe',
|
||||||
entity_id: String(testRecipeId),
|
entity_id: String(testRecipeId),
|
||||||
reaction_type: 'like',
|
reaction_type: 'like',
|
||||||
@@ -132,7 +132,7 @@ describe('Reactions API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should add a reaction when none exists', async () => {
|
it('should add a reaction when none exists', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/reactions/toggle')
|
.post('/api/v1/reactions/toggle')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
entity_type: 'recipe',
|
entity_type: 'recipe',
|
||||||
@@ -154,7 +154,7 @@ describe('Reactions API Routes Integration Tests', () => {
|
|||||||
it('should remove the reaction when toggled again', async () => {
|
it('should remove the reaction when toggled again', async () => {
|
||||||
// First add the reaction
|
// First add the reaction
|
||||||
const addResponse = await request
|
const addResponse = await request
|
||||||
.post('/api/reactions/toggle')
|
.post('/api/v1/reactions/toggle')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
entity_type: 'recipe',
|
entity_type: 'recipe',
|
||||||
@@ -169,7 +169,7 @@ describe('Reactions API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Then toggle it off
|
// Then toggle it off
|
||||||
const removeResponse = await request
|
const removeResponse = await request
|
||||||
.post('/api/reactions/toggle')
|
.post('/api/v1/reactions/toggle')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
entity_type: 'recipe',
|
entity_type: 'recipe',
|
||||||
@@ -184,7 +184,7 @@ describe('Reactions API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should return 400 for missing entity_type', async () => {
|
it('should return 400 for missing entity_type', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/reactions/toggle')
|
.post('/api/v1/reactions/toggle')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
entity_id: String(testRecipeId),
|
entity_id: String(testRecipeId),
|
||||||
@@ -197,7 +197,7 @@ describe('Reactions API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should return 400 for missing entity_id', async () => {
|
it('should return 400 for missing entity_id', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/reactions/toggle')
|
.post('/api/v1/reactions/toggle')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
entity_type: 'recipe',
|
entity_type: 'recipe',
|
||||||
@@ -210,7 +210,7 @@ describe('Reactions API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
it('should return 400 for missing reaction_type', async () => {
|
it('should return 400 for missing reaction_type', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/reactions/toggle')
|
.post('/api/v1/reactions/toggle')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
entity_type: 'recipe',
|
entity_type: 'recipe',
|
||||||
@@ -224,7 +224,7 @@ describe('Reactions API Routes Integration Tests', () => {
|
|||||||
it('should accept entity_id as string (required format)', async () => {
|
it('should accept entity_id as string (required format)', async () => {
|
||||||
// entity_id must be a string per the Zod schema
|
// entity_id must be a string per the Zod schema
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/reactions/toggle')
|
.post('/api/v1/reactions/toggle')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
entity_type: 'recipe',
|
entity_type: 'recipe',
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/receipts')
|
.post('/api/v1/receipts')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.attach('receipt', testImageBuffer, 'test-receipt.png')
|
.attach('receipt', testImageBuffer, 'test-receipt.png')
|
||||||
.field('store_location_id', testStoreLocationId.toString())
|
.field('store_location_id', testStoreLocationId.toString())
|
||||||
@@ -229,7 +229,7 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/receipts')
|
.post('/api/v1/receipts')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.attach('receipt', testImageBuffer, 'test-receipt-2.png');
|
.attach('receipt', testImageBuffer, 'test-receipt-2.png');
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
|||||||
|
|
||||||
it('should reject request without file', async () => {
|
it('should reject request without file', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/receipts')
|
.post('/api/v1/receipts')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -257,7 +257,7 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/receipts')
|
.post('/api/v1/receipts')
|
||||||
.attach('receipt', testImageBuffer, 'test-receipt.png');
|
.attach('receipt', testImageBuffer, 'test-receipt.png');
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
@@ -285,7 +285,7 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
|||||||
|
|
||||||
it('should return paginated list of receipts', async () => {
|
it('should return paginated list of receipts', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/receipts')
|
.get('/api/v1/receipts')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -297,7 +297,7 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
|||||||
|
|
||||||
it('should support status filter', async () => {
|
it('should support status filter', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/receipts')
|
.get('/api/v1/receipts')
|
||||||
.query({ status: 'completed' })
|
.query({ status: 'completed' })
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
@@ -309,7 +309,7 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
|||||||
|
|
||||||
it('should support pagination', async () => {
|
it('should support pagination', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/receipts')
|
.get('/api/v1/receipts')
|
||||||
.query({ limit: 2, offset: 0 })
|
.query({ limit: 2, offset: 0 })
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
@@ -327,7 +327,7 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
|||||||
createdUserIds.push(otherUser.user.user_id);
|
createdUserIds.push(otherUser.user.user_id);
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/receipts')
|
.get('/api/v1/receipts')
|
||||||
.set('Authorization', `Bearer ${otherToken}`);
|
.set('Authorization', `Bearer ${otherToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -386,7 +386,7 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
|||||||
|
|
||||||
it('should return 404 for non-existent receipt', async () => {
|
it('should return 404 for non-existent receipt', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/receipts/999999')
|
.get('/api/v1/receipts/999999')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
@@ -463,7 +463,7 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
|||||||
|
|
||||||
it('should return 404 for non-existent receipt', async () => {
|
it('should return 404 for non-existent receipt', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/receipts/999999/reprocess')
|
.post('/api/v1/receipts/999999/reprocess')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
@@ -692,7 +692,7 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const uploadResponse = await request
|
const uploadResponse = await request
|
||||||
.post('/api/receipts')
|
.post('/api/v1/receipts')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.attach('receipt', testImageBuffer, 'workflow-test.png')
|
.attach('receipt', testImageBuffer, 'workflow-test.png')
|
||||||
.field('transaction_date', '2024-01-20');
|
.field('transaction_date', '2024-01-20');
|
||||||
@@ -711,7 +711,7 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
|||||||
|
|
||||||
// Step 3: Check it appears in list
|
// Step 3: Check it appears in list
|
||||||
const listResponse = await request
|
const listResponse = await request
|
||||||
.get('/api/receipts')
|
.get('/api/v1/receipts')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(listResponse.status).toBe(200);
|
expect(listResponse.status).toBe(200);
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ describe('Recipe API Routes Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 404 for a non-existent recipe ID', async () => {
|
it('should return 404 for a non-existent recipe ID', async () => {
|
||||||
const response = await request.get('/api/recipes/999999');
|
const response = await request.get('/api/v1/recipes/999999');
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -88,7 +88,7 @@ describe('Recipe API Routes Integration Tests', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/users/recipes')
|
.post('/api/v1/users/recipes')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send(newRecipeData);
|
.send(newRecipeData);
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ describe('Recipe API Routes Integration Tests', () => {
|
|||||||
it('should allow an authenticated user to delete their own recipe', async () => {
|
it('should allow an authenticated user to delete their own recipe', async () => {
|
||||||
// Create a recipe specifically for deletion
|
// Create a recipe specifically for deletion
|
||||||
const createRes = await request
|
const createRes = await request
|
||||||
.post('/api/users/recipes')
|
.post('/api/v1/users/recipes')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
name: 'Recipe To Delete',
|
name: 'Recipe To Delete',
|
||||||
@@ -294,7 +294,7 @@ describe('Recipe API Routes Integration Tests', () => {
|
|||||||
it('should return empty array for recipe with no comments', async () => {
|
it('should return empty array for recipe with no comments', async () => {
|
||||||
// Create a recipe specifically with no comments
|
// Create a recipe specifically with no comments
|
||||||
const createRes = await request
|
const createRes = await request
|
||||||
.post('/api/users/recipes')
|
.post('/api/v1/users/recipes')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
name: 'Recipe With No Comments',
|
name: 'Recipe With No Comments',
|
||||||
@@ -321,7 +321,7 @@ describe('Recipe API Routes Integration Tests', () => {
|
|||||||
vi.mocked(aiService.generateRecipeSuggestion).mockResolvedValue(mockSuggestion);
|
vi.mocked(aiService.generateRecipeSuggestion).mockResolvedValue(mockSuggestion);
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/recipes/suggest')
|
.post('/api/v1/recipes/suggest')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ ingredients });
|
.send({ ingredients });
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ describe('Server Initialization Smoke Test', () => {
|
|||||||
|
|
||||||
it('should respond with 200 OK and "pong" for GET /api/health/ping', async () => {
|
it('should respond with 200 OK and "pong" for GET /api/health/ping', async () => {
|
||||||
// Use supertest to make a request to the Express app instance.
|
// Use supertest to make a request to the Express app instance.
|
||||||
const response = await supertest(app).get('/api/health/ping');
|
const response = await supertest(app).get('/api/v1/health/ping');
|
||||||
|
|
||||||
// Assert that the server responds with the correct status code and body.
|
// Assert that the server responds with the correct status code and body.
|
||||||
// This confirms that the routing is set up correctly and the endpoint is reachable.
|
// This confirms that the routing is set up correctly and the endpoint is reachable.
|
||||||
@@ -38,7 +38,7 @@ describe('Server Initialization Smoke Test', () => {
|
|||||||
|
|
||||||
it('should respond with 200 OK for GET /api/health/db-schema', async () => {
|
it('should respond with 200 OK for GET /api/health/db-schema', async () => {
|
||||||
// Use supertest to make a request to the Express app instance.
|
// Use supertest to make a request to the Express app instance.
|
||||||
const response = await supertest(app).get('/api/health/db-schema');
|
const response = await supertest(app).get('/api/v1/health/db-schema');
|
||||||
|
|
||||||
// Assert that the server responds with a success message.
|
// Assert that the server responds with a success message.
|
||||||
// This confirms that the database connection is working and the essential tables exist.
|
// This confirms that the database connection is working and the essential tables exist.
|
||||||
@@ -52,7 +52,7 @@ describe('Server Initialization Smoke Test', () => {
|
|||||||
|
|
||||||
it('should respond with 200 OK for GET /api/health/storage', async () => {
|
it('should respond with 200 OK for GET /api/health/storage', async () => {
|
||||||
// Use supertest to make a request to the Express app instance.
|
// Use supertest to make a request to the Express app instance.
|
||||||
const response = await supertest(app).get('/api/health/storage');
|
const response = await supertest(app).get('/api/v1/health/storage');
|
||||||
|
|
||||||
// Assert that the server responds with a success message.
|
// Assert that the server responds with a success message.
|
||||||
// This confirms that the directory specified by STORAGE_PATH exists and is writable
|
// This confirms that the directory specified by STORAGE_PATH exists and is writable
|
||||||
@@ -64,7 +64,7 @@ describe('Server Initialization Smoke Test', () => {
|
|||||||
|
|
||||||
it('should respond with 200 OK for GET /api/health/redis', async () => {
|
it('should respond with 200 OK for GET /api/health/redis', async () => {
|
||||||
// Use supertest to make a request to the Express app instance.
|
// Use supertest to make a request to the Express app instance.
|
||||||
const response = await supertest(app).get('/api/health/redis');
|
const response = await supertest(app).get('/api/v1/health/redis');
|
||||||
|
|
||||||
// Assert that the server responds with a success message.
|
// Assert that the server responds with a success message.
|
||||||
// This confirms that the connection to the Redis server is active, which is
|
// This confirms that the connection to the Redis server is active, which is
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ describe('System API Routes Integration Tests', () => {
|
|||||||
const request = supertest(app);
|
const request = supertest(app);
|
||||||
// In a typical CI environment without PM2, this will fail gracefully.
|
// In a typical CI environment without PM2, this will fail gracefully.
|
||||||
// The test verifies that the endpoint responds correctly, even if PM2 isn't running.
|
// The test verifies that the endpoint responds correctly, even if PM2 isn't running.
|
||||||
const response = await request.get('/api/system/pm2-status');
|
const response = await request.get('/api/v1/system/pm2-status');
|
||||||
const result = response.body;
|
const result = response.body;
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
describe('POST /api/upc/scan - Manual UPC Entry', () => {
|
describe('POST /api/upc/scan - Manual UPC Entry', () => {
|
||||||
it('should record a manual UPC scan successfully', async () => {
|
it('should record a manual UPC scan successfully', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/upc/scan')
|
.post('/api/v1/upc/scan')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
upc_code: '012345678905',
|
upc_code: '012345678905',
|
||||||
@@ -114,7 +114,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
createdProductIds.push(productId);
|
createdProductIds.push(productId);
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/upc/scan')
|
.post('/api/v1/upc/scan')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
upc_code: '111222333444',
|
upc_code: '111222333444',
|
||||||
@@ -133,7 +133,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
|
|
||||||
it('should reject invalid UPC code format', async () => {
|
it('should reject invalid UPC code format', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/upc/scan')
|
.post('/api/v1/upc/scan')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
upc_code: 'invalid',
|
upc_code: 'invalid',
|
||||||
@@ -149,7 +149,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
|
|
||||||
it('should reject invalid scan_source', async () => {
|
it('should reject invalid scan_source', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/upc/scan')
|
.post('/api/v1/upc/scan')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
upc_code: '012345678905',
|
upc_code: '012345678905',
|
||||||
@@ -160,7 +160,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reject unauthenticated requests', async () => {
|
it('should reject unauthenticated requests', async () => {
|
||||||
const response = await request.post('/api/upc/scan').send({
|
const response = await request.post('/api/v1/upc/scan').send({
|
||||||
upc_code: '012345678905',
|
upc_code: '012345678905',
|
||||||
scan_source: 'manual_entry',
|
scan_source: 'manual_entry',
|
||||||
});
|
});
|
||||||
@@ -172,7 +172,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
describe('GET /api/upc/lookup - Product Lookup', () => {
|
describe('GET /api/upc/lookup - Product Lookup', () => {
|
||||||
it('should return null for unknown UPC code', async () => {
|
it('should return null for unknown UPC code', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/upc/lookup')
|
.get('/api/v1/upc/lookup')
|
||||||
.query({ upc_code: '999888777666' })
|
.query({ upc_code: '999888777666' })
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
createdProductIds.push(productId);
|
createdProductIds.push(productId);
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/upc/lookup')
|
.get('/api/v1/upc/lookup')
|
||||||
.query({ upc_code: '555666777888' })
|
.query({ upc_code: '555666777888' })
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
@@ -214,7 +214,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
|
|
||||||
it('should reject missing upc_code parameter', async () => {
|
it('should reject missing upc_code parameter', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/upc/lookup')
|
.get('/api/v1/upc/lookup')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
@@ -226,7 +226,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
// Create some scan history for testing
|
// Create some scan history for testing
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/upc/scan')
|
.post('/api/v1/upc/scan')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
upc_code: `00000000000${i}`,
|
upc_code: `00000000000${i}`,
|
||||||
@@ -241,7 +241,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
|
|
||||||
it('should return paginated scan history', async () => {
|
it('should return paginated scan history', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/upc/history')
|
.get('/api/v1/upc/history')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -253,7 +253,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
|
|
||||||
it('should support pagination parameters', async () => {
|
it('should support pagination parameters', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/upc/history')
|
.get('/api/v1/upc/history')
|
||||||
.query({ limit: 2, offset: 0 })
|
.query({ limit: 2, offset: 0 })
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
|
|
||||||
it('should filter by scan_source', async () => {
|
it('should filter by scan_source', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/upc/history')
|
.get('/api/v1/upc/history')
|
||||||
.query({ scan_source: 'manual_entry' })
|
.query({ scan_source: 'manual_entry' })
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
createdUserIds.push(otherUser.user.user_id);
|
createdUserIds.push(otherUser.user.user_id);
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/upc/history')
|
.get('/api/v1/upc/history')
|
||||||
.set('Authorization', `Bearer ${otherToken}`);
|
.set('Authorization', `Bearer ${otherToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -298,7 +298,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// Create a scan to retrieve
|
// Create a scan to retrieve
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/upc/scan')
|
.post('/api/v1/upc/scan')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
upc_code: '123456789012',
|
upc_code: '123456789012',
|
||||||
@@ -323,7 +323,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
|
|
||||||
it('should return 404 for non-existent scan', async () => {
|
it('should return 404 for non-existent scan', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/upc/history/999999')
|
.get('/api/v1/upc/history/999999')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
@@ -348,7 +348,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
describe('GET /api/upc/stats - User Statistics', () => {
|
describe('GET /api/upc/stats - User Statistics', () => {
|
||||||
it('should return user scan statistics', async () => {
|
it('should return user scan statistics', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/upc/stats')
|
.get('/api/v1/upc/stats')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -385,7 +385,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
|
|
||||||
it('should allow admin to link UPC to product', async () => {
|
it('should allow admin to link UPC to product', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/upc/link')
|
.post('/api/v1/upc/link')
|
||||||
.set('Authorization', `Bearer ${adminToken}`)
|
.set('Authorization', `Bearer ${adminToken}`)
|
||||||
.send({
|
.send({
|
||||||
product_id: testProductId,
|
product_id: testProductId,
|
||||||
@@ -398,7 +398,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
|
|
||||||
it('should reject non-admin users', async () => {
|
it('should reject non-admin users', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/upc/link')
|
.post('/api/v1/upc/link')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
product_id: testProductId,
|
product_id: testProductId,
|
||||||
@@ -410,7 +410,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
|
|
||||||
it('should reject invalid product_id', async () => {
|
it('should reject invalid product_id', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/upc/link')
|
.post('/api/v1/upc/link')
|
||||||
.set('Authorization', `Bearer ${adminToken}`)
|
.set('Authorization', `Bearer ${adminToken}`)
|
||||||
.send({
|
.send({
|
||||||
product_id: 999999,
|
product_id: 999999,
|
||||||
@@ -444,7 +444,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
|
|
||||||
// Step 2: Scan the UPC
|
// Step 2: Scan the UPC
|
||||||
const scanResponse = await request
|
const scanResponse = await request
|
||||||
.post('/api/upc/scan')
|
.post('/api/v1/upc/scan')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
upc_code: uniqueUpc,
|
upc_code: uniqueUpc,
|
||||||
@@ -457,7 +457,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
|
|
||||||
// Step 3: Lookup the product
|
// Step 3: Lookup the product
|
||||||
const lookupResponse = await request
|
const lookupResponse = await request
|
||||||
.get('/api/upc/lookup')
|
.get('/api/v1/upc/lookup')
|
||||||
.query({ upc_code: uniqueUpc })
|
.query({ upc_code: uniqueUpc })
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
@@ -476,7 +476,7 @@ describe('UPC Scanning Integration Tests (/api/upc)', () => {
|
|||||||
|
|
||||||
// Step 5: Check stats updated
|
// Step 5: Check stats updated
|
||||||
const statsResponse = await request
|
const statsResponse = await request
|
||||||
.get('/api/upc/stats')
|
.get('/api/v1/upc/stats')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(statsResponse.status).toBe(200);
|
expect(statsResponse.status).toBe(200);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
it('should fetch the authenticated user profile via GET /api/users/profile', async () => {
|
it('should fetch the authenticated user profile via GET /api/users/profile', async () => {
|
||||||
// Act: Call the API endpoint using the authenticated token.
|
// Act: Call the API endpoint using the authenticated token.
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
const profile = response.body.data;
|
const profile = response.body.data;
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Act: Call the update endpoint with the new data and the auth token.
|
// Act: Call the update endpoint with the new data and the auth token.
|
||||||
const response = await request
|
const response = await request
|
||||||
.put('/api/users/profile')
|
.put('/api/v1/users/profile')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send(profileUpdates);
|
.send(profileUpdates);
|
||||||
const updatedProfile = response.body.data;
|
const updatedProfile = response.body.data;
|
||||||
@@ -96,7 +96,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Also, fetch the profile again to ensure the change was persisted.
|
// Also, fetch the profile again to ensure the change was persisted.
|
||||||
const refetchResponse = await request
|
const refetchResponse = await request
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
const refetchedProfile = refetchResponse.body.data;
|
const refetchedProfile = refetchResponse.body.data;
|
||||||
expect(refetchedProfile.full_name).toBe('Updated Test User');
|
expect(refetchedProfile.full_name).toBe('Updated Test User');
|
||||||
@@ -111,7 +111,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Act: Call the update endpoint with the new data and the auth token.
|
// Act: Call the update endpoint with the new data and the auth token.
|
||||||
const response = await request
|
const response = await request
|
||||||
.put('/api/users/profile')
|
.put('/api/v1/users/profile')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send(profileUpdates);
|
.send(profileUpdates);
|
||||||
const updatedProfile = response.body.data;
|
const updatedProfile = response.body.data;
|
||||||
@@ -123,7 +123,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Also, fetch the profile again to ensure the change was persisted in the database as NULL.
|
// Also, fetch the profile again to ensure the change was persisted in the database as NULL.
|
||||||
const refetchResponse = await request
|
const refetchResponse = await request
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
expect(refetchResponse.body.data.avatar_url).toBeNull();
|
expect(refetchResponse.body.data.avatar_url).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -136,7 +136,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Act: Call the update endpoint.
|
// Act: Call the update endpoint.
|
||||||
const response = await request
|
const response = await request
|
||||||
.put('/api/users/profile/preferences')
|
.put('/api/v1/users/profile/preferences')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send(preferenceUpdates);
|
.send(preferenceUpdates);
|
||||||
const updatedProfile = response.body.data;
|
const updatedProfile = response.body.data;
|
||||||
@@ -153,7 +153,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Act & Assert: Attempt to register and expect the promise to reject
|
// Act & Assert: Attempt to register and expect the promise to reject
|
||||||
// with an error message indicating the password is too weak.
|
// with an error message indicating the password is too weak.
|
||||||
const response = await request.post('/api/auth/register').send({
|
const response = await request.post('/api/v1/auth/register').send({
|
||||||
email,
|
email,
|
||||||
password: weakPassword,
|
password: weakPassword,
|
||||||
full_name: 'Weak Password User',
|
full_name: 'Weak Password User',
|
||||||
@@ -178,7 +178,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Act: Call the delete endpoint with the correct password and token.
|
// Act: Call the delete endpoint with the correct password and token.
|
||||||
const response = await request
|
const response = await request
|
||||||
.delete('/api/users/account')
|
.delete('/api/v1/users/account')
|
||||||
.set('Authorization', `Bearer ${deletionToken}`)
|
.set('Authorization', `Bearer ${deletionToken}`)
|
||||||
.send({ password: TEST_PASSWORD });
|
.send({ password: TEST_PASSWORD });
|
||||||
const deleteResponse = response.body;
|
const deleteResponse = response.body;
|
||||||
@@ -189,7 +189,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Assert (Verification): Attempting to log in again with the same credentials should now fail.
|
// Assert (Verification): Attempting to log in again with the same credentials should now fail.
|
||||||
const loginResponse = await request
|
const loginResponse = await request
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: deletionEmail, password: TEST_PASSWORD });
|
.send({ email: deletionEmail, password: TEST_PASSWORD });
|
||||||
expect(loginResponse.status).toBe(401);
|
expect(loginResponse.status).toBe(401);
|
||||||
const errorData = loginResponse.body.error;
|
const errorData = loginResponse.body.error;
|
||||||
@@ -204,7 +204,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Act 1: Request a password reset. In our test environment, the token is returned in the response.
|
// Act 1: Request a password reset. In our test environment, the token is returned in the response.
|
||||||
const resetRequestRawResponse = await request
|
const resetRequestRawResponse = await request
|
||||||
.post('/api/auth/forgot-password')
|
.post('/api/v1/auth/forgot-password')
|
||||||
.send({ email: resetEmail });
|
.send({ email: resetEmail });
|
||||||
if (resetRequestRawResponse.status !== 200) {
|
if (resetRequestRawResponse.status !== 200) {
|
||||||
const errorData = resetRequestRawResponse.body;
|
const errorData = resetRequestRawResponse.body;
|
||||||
@@ -220,7 +220,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
// Act 2: Use the token to set a new password.
|
// Act 2: Use the token to set a new password.
|
||||||
const newPassword = 'my-new-secure-password-!@#$';
|
const newPassword = 'my-new-secure-password-!@#$';
|
||||||
const resetRawResponse = await request
|
const resetRawResponse = await request
|
||||||
.post('/api/auth/reset-password')
|
.post('/api/v1/auth/reset-password')
|
||||||
.send({ token: resetToken!, newPassword });
|
.send({ token: resetToken!, newPassword });
|
||||||
if (resetRawResponse.status !== 200) {
|
if (resetRawResponse.status !== 200) {
|
||||||
const errorData = resetRawResponse.body;
|
const errorData = resetRawResponse.body;
|
||||||
@@ -233,7 +233,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Act 3 & Assert 3 (Verification): Log in with the NEW password to confirm the change.
|
// Act 3 & Assert 3 (Verification): Log in with the NEW password to confirm the change.
|
||||||
const loginResponse = await request
|
const loginResponse = await request
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email: resetEmail, password: newPassword });
|
.send({ email: resetEmail, password: newPassword });
|
||||||
const loginData = loginResponse.body.data;
|
const loginData = loginResponse.body.data;
|
||||||
expect(loginData.userprofile).toBeDefined();
|
expect(loginData.userprofile).toBeDefined();
|
||||||
@@ -244,14 +244,14 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
it('should allow a user to add and remove a watched item', async () => {
|
it('should allow a user to add and remove a watched item', async () => {
|
||||||
// First, look up the category ID for "Other/Miscellaneous"
|
// First, look up the category ID for "Other/Miscellaneous"
|
||||||
const categoryResponse = await request.get(
|
const categoryResponse = await request.get(
|
||||||
'/api/categories/lookup?name=' + encodeURIComponent('Other/Miscellaneous'),
|
'/api/v1/categories/lookup?name=' + encodeURIComponent('Other/Miscellaneous'),
|
||||||
);
|
);
|
||||||
expect(categoryResponse.status).toBe(200);
|
expect(categoryResponse.status).toBe(200);
|
||||||
const categoryId = categoryResponse.body.data.category_id;
|
const categoryId = categoryResponse.body.data.category_id;
|
||||||
|
|
||||||
// Act 1: Add a new watched item. The API returns the created master item.
|
// Act 1: Add a new watched item. The API returns the created master item.
|
||||||
const addResponse = await request
|
const addResponse = await request
|
||||||
.post('/api/users/watched-items')
|
.post('/api/v1/users/watched-items')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ itemName: 'Integration Test Item', category_id: categoryId });
|
.send({ itemName: 'Integration Test Item', category_id: categoryId });
|
||||||
const newItem = addResponse.body.data;
|
const newItem = addResponse.body.data;
|
||||||
@@ -264,7 +264,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Act 2: Fetch all watched items for the user.
|
// Act 2: Fetch all watched items for the user.
|
||||||
const watchedItemsResponse = await request
|
const watchedItemsResponse = await request
|
||||||
.get('/api/users/watched-items')
|
.get('/api/v1/users/watched-items')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
const watchedItems = watchedItemsResponse.body.data;
|
const watchedItems = watchedItemsResponse.body.data;
|
||||||
|
|
||||||
@@ -284,7 +284,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Assert 3: Fetch again and verify the item is gone.
|
// Assert 3: Fetch again and verify the item is gone.
|
||||||
const finalWatchedItemsResponse = await request
|
const finalWatchedItemsResponse = await request
|
||||||
.get('/api/users/watched-items')
|
.get('/api/v1/users/watched-items')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
const finalWatchedItems = finalWatchedItemsResponse.body.data;
|
const finalWatchedItems = finalWatchedItemsResponse.body.data;
|
||||||
expect(
|
expect(
|
||||||
@@ -298,7 +298,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
it('should allow a user to manage a shopping list', async () => {
|
it('should allow a user to manage a shopping list', async () => {
|
||||||
// Act 1: Create a new shopping list.
|
// Act 1: Create a new shopping list.
|
||||||
const createListResponse = await request
|
const createListResponse = await request
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ name: 'My Integration Test List' });
|
.send({ name: 'My Integration Test List' });
|
||||||
const newList = createListResponse.body.data;
|
const newList = createListResponse.body.data;
|
||||||
@@ -320,7 +320,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Assert 3: Fetch all lists and verify the new item is present in the correct list.
|
// Assert 3: Fetch all lists and verify the new item is present in the correct list.
|
||||||
const fetchResponse = await request
|
const fetchResponse = await request
|
||||||
.get('/api/users/shopping-lists')
|
.get('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
const lists = fetchResponse.body.data;
|
const lists = fetchResponse.body.data;
|
||||||
expect(fetchResponse.status).toBe(200);
|
expect(fetchResponse.status).toBe(200);
|
||||||
@@ -341,7 +341,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Act: Make the POST request to upload the avatar
|
// Act: Make the POST request to upload the avatar
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/users/profile/avatar')
|
.post('/api/v1/users/profile/avatar')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.attach('avatar', dummyImagePath);
|
.attach('avatar', dummyImagePath);
|
||||||
|
|
||||||
@@ -354,7 +354,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Assert (Verification): Fetch the profile again to ensure the change was persisted
|
// Assert (Verification): Fetch the profile again to ensure the change was persisted
|
||||||
const verifyResponse = await request
|
const verifyResponse = await request
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
const refetchedProfile = verifyResponse.body.data;
|
const refetchedProfile = verifyResponse.body.data;
|
||||||
expect(refetchedProfile.avatar_url).toBe(updatedProfile.avatar_url);
|
expect(refetchedProfile.avatar_url).toBe(updatedProfile.avatar_url);
|
||||||
@@ -367,7 +367,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Act: Attempt to upload the text file to the avatar endpoint.
|
// Act: Attempt to upload the text file to the avatar endpoint.
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/users/profile/avatar')
|
.post('/api/v1/users/profile/avatar')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.attach('avatar', invalidFileBuffer, invalidFileName);
|
.attach('avatar', invalidFileBuffer, invalidFileName);
|
||||||
|
|
||||||
@@ -386,7 +386,7 @@ describe('User API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
// Act: Attempt to upload the large file.
|
// Act: Attempt to upload the large file.
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/users/profile/avatar')
|
.post('/api/v1/users/profile/avatar')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.attach('avatar', largeFileBuffer, largeFileName);
|
.attach('avatar', largeFileBuffer, largeFileName);
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ describe('User Routes Integration Tests (/api/users)', () => {
|
|||||||
describe('GET /api/users/profile', () => {
|
describe('GET /api/users/profile', () => {
|
||||||
it('should return the profile for the authenticated user', async () => {
|
it('should return the profile for the authenticated user', async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
@@ -49,7 +49,7 @@ describe('User Routes Integration Tests (/api/users)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 401 Unauthorized if no token is provided', async () => {
|
it('should return 401 Unauthorized if no token is provided', async () => {
|
||||||
const response = await request.get('/api/users/profile');
|
const response = await request.get('/api/v1/users/profile');
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -58,7 +58,7 @@ describe('User Routes Integration Tests (/api/users)', () => {
|
|||||||
it('should update the user profile', async () => {
|
it('should update the user profile', async () => {
|
||||||
const newName = `Test User ${Date.now()}`;
|
const newName = `Test User ${Date.now()}`;
|
||||||
const response = await request
|
const response = await request
|
||||||
.put('/api/users/profile')
|
.put('/api/v1/users/profile')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ full_name: newName });
|
.send({ full_name: newName });
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ describe('User Routes Integration Tests (/api/users)', () => {
|
|||||||
|
|
||||||
// Verify the change by fetching the profile again
|
// Verify the change by fetching the profile again
|
||||||
const verifyResponse = await request
|
const verifyResponse = await request
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(verifyResponse.body.data.full_name).toBe(newName);
|
expect(verifyResponse.body.data.full_name).toBe(newName);
|
||||||
@@ -78,7 +78,7 @@ describe('User Routes Integration Tests (/api/users)', () => {
|
|||||||
it('should update user preferences', async () => {
|
it('should update user preferences', async () => {
|
||||||
const preferences = { darkMode: true, unitSystem: 'metric' };
|
const preferences = { darkMode: true, unitSystem: 'metric' };
|
||||||
const response = await request
|
const response = await request
|
||||||
.put('/api/users/profile/preferences')
|
.put('/api/v1/users/profile/preferences')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send(preferences);
|
.send(preferences);
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ describe('User Routes Integration Tests (/api/users)', () => {
|
|||||||
|
|
||||||
// Verify the change by fetching the profile again
|
// Verify the change by fetching the profile again
|
||||||
const verifyResponse = await request
|
const verifyResponse = await request
|
||||||
.get('/api/users/profile')
|
.get('/api/v1/users/profile')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(verifyResponse.body.data.preferences?.darkMode).toBe(true);
|
expect(verifyResponse.body.data.preferences?.darkMode).toBe(true);
|
||||||
@@ -100,7 +100,7 @@ describe('User Routes Integration Tests (/api/users)', () => {
|
|||||||
// 1. Create
|
// 1. Create
|
||||||
const listName = `My Test List - ${Date.now()}`;
|
const listName = `My Test List - ${Date.now()}`;
|
||||||
const createResponse = await request
|
const createResponse = await request
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ name: listName });
|
.send({ name: listName });
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ describe('User Routes Integration Tests (/api/users)', () => {
|
|||||||
|
|
||||||
// 2. Retrieve
|
// 2. Retrieve
|
||||||
const getResponse = await request
|
const getResponse = await request
|
||||||
.get('/api/users/shopping-lists')
|
.get('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
|
||||||
expect(getResponse.status).toBe(200);
|
expect(getResponse.status).toBe(200);
|
||||||
@@ -128,7 +128,7 @@ describe('User Routes Integration Tests (/api/users)', () => {
|
|||||||
|
|
||||||
// 4. Verify Deletion
|
// 4. Verify Deletion
|
||||||
const verifyResponse = await request
|
const verifyResponse = await request
|
||||||
.get('/api/users/shopping-lists')
|
.get('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
const notFoundList = verifyResponse.body.data.find(
|
const notFoundList = verifyResponse.body.data.find(
|
||||||
(l: { shopping_list_id: number }) => l.shopping_list_id === listId,
|
(l: { shopping_list_id: number }) => l.shopping_list_id === listId,
|
||||||
@@ -140,7 +140,7 @@ describe('User Routes Integration Tests (/api/users)', () => {
|
|||||||
// Arrange: Create a shopping list owned by the primary testUser.
|
// Arrange: Create a shopping list owned by the primary testUser.
|
||||||
const listName = `Owner's List - ${Date.now()}`;
|
const listName = `Owner's List - ${Date.now()}`;
|
||||||
const createListResponse = await request
|
const createListResponse = await request
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`) // Use owner's token
|
.set('Authorization', `Bearer ${authToken}`) // Use owner's token
|
||||||
.send({ name: listName });
|
.send({ name: listName });
|
||||||
expect(createListResponse.status).toBe(201);
|
expect(createListResponse.status).toBe(201);
|
||||||
@@ -207,7 +207,7 @@ describe('User Routes Integration Tests (/api/users)', () => {
|
|||||||
// Create a dedicated list for these item tests
|
// Create a dedicated list for these item tests
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.post('/api/users/shopping-lists')
|
.post('/api/v1/users/shopping-lists')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ name: 'Item Test List' });
|
.send({ name: 'Item Test List' });
|
||||||
listId = response.body.data.shopping_list_id;
|
listId = response.body.data.shopping_list_id;
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export const createAndLoginUser = async (
|
|||||||
if (options.request) {
|
if (options.request) {
|
||||||
// Use supertest for integration tests (hits the app instance directly)
|
// Use supertest for integration tests (hits the app instance directly)
|
||||||
const registerRes = await options.request
|
const registerRes = await options.request
|
||||||
.post('/api/auth/register')
|
.post('/api/v1/auth/register')
|
||||||
.send({ email, password, full_name: fullName });
|
.send({ email, password, full_name: fullName });
|
||||||
|
|
||||||
if (registerRes.status !== 201 && registerRes.status !== 200) {
|
if (registerRes.status !== 201 && registerRes.status !== 200) {
|
||||||
@@ -92,7 +92,7 @@ export const createAndLoginUser = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loginRes = await options.request
|
const loginRes = await options.request
|
||||||
.post('/api/auth/login')
|
.post('/api/v1/auth/login')
|
||||||
.send({ email, password, rememberMe: false });
|
.send({ email, password, rememberMe: false });
|
||||||
|
|
||||||
if (loginRes.status !== 200) {
|
if (loginRes.status !== 200) {
|
||||||
|
|||||||
Reference in New Issue
Block a user