1026 lines
28 KiB
JavaScript
1026 lines
28 KiB
JavaScript
const request = require('supertest');
|
|
const express = require('express');
|
|
|
|
// Mock Sequelize operators
|
|
jest.mock('sequelize', () => ({
|
|
Op: {
|
|
gte: 'gte',
|
|
lte: 'lte',
|
|
iLike: 'iLike',
|
|
or: 'or',
|
|
not: 'not'
|
|
}
|
|
}));
|
|
|
|
// Mock models
|
|
const mockItemFindAndCountAll = jest.fn();
|
|
const mockItemFindByPk = jest.fn();
|
|
const mockItemCreate = jest.fn();
|
|
const mockItemUpdate = jest.fn();
|
|
const mockItemDestroy = jest.fn();
|
|
const mockItemFindAll = jest.fn();
|
|
const mockRentalFindAll = jest.fn();
|
|
const mockUserModel = jest.fn();
|
|
|
|
jest.mock('../../../models', () => ({
|
|
Item: {
|
|
findAndCountAll: mockItemFindAndCountAll,
|
|
findByPk: mockItemFindByPk,
|
|
create: mockItemCreate,
|
|
findAll: mockItemFindAll
|
|
},
|
|
User: mockUserModel,
|
|
Rental: {
|
|
findAll: mockRentalFindAll
|
|
}
|
|
}));
|
|
|
|
// Mock auth middleware
|
|
jest.mock('../../../middleware/auth', () => ({
|
|
authenticateToken: (req, res, next) => {
|
|
if (req.headers.authorization) {
|
|
req.user = { id: 1 };
|
|
next();
|
|
} else {
|
|
res.status(401).json({ error: 'No token provided' });
|
|
}
|
|
}
|
|
}));
|
|
|
|
const { Item, User, Rental } = require('../../../models');
|
|
const { Op } = require('sequelize');
|
|
const itemsRoutes = require('../../../routes/items');
|
|
|
|
// Set up Express app for testing
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.use('/items', itemsRoutes);
|
|
|
|
describe('Items Routes', () => {
|
|
let consoleSpy;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
});
|
|
|
|
afterEach(() => {
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
describe('GET /', () => {
|
|
const mockItems = [
|
|
{
|
|
id: 1,
|
|
name: 'Camping Tent',
|
|
description: 'Great for camping',
|
|
pricePerDay: 25.99,
|
|
city: 'New York',
|
|
zipCode: '10001',
|
|
latitude: 40.7484405,
|
|
longitude: -73.9856644,
|
|
createdAt: new Date('2023-01-01'),
|
|
toJSON: () => ({
|
|
id: 1,
|
|
name: 'Camping Tent',
|
|
description: 'Great for camping',
|
|
pricePerDay: 25.99,
|
|
city: 'New York',
|
|
zipCode: '10001',
|
|
latitude: 40.7484405,
|
|
longitude: -73.9856644,
|
|
createdAt: new Date('2023-01-01'),
|
|
owner: { id: 2, username: 'owner1', firstName: 'John', lastName: 'Doe' }
|
|
})
|
|
},
|
|
{
|
|
id: 2,
|
|
name: 'Mountain Bike',
|
|
description: 'Perfect for trails',
|
|
pricePerDay: 35.50,
|
|
city: 'Los Angeles',
|
|
zipCode: '90210',
|
|
latitude: 34.0522265,
|
|
longitude: -118.2436596,
|
|
createdAt: new Date('2023-01-02'),
|
|
toJSON: () => ({
|
|
id: 2,
|
|
name: 'Mountain Bike',
|
|
description: 'Perfect for trails',
|
|
pricePerDay: 35.50,
|
|
city: 'Los Angeles',
|
|
zipCode: '90210',
|
|
latitude: 34.0522265,
|
|
longitude: -118.2436596,
|
|
createdAt: new Date('2023-01-02'),
|
|
owner: { id: 3, username: 'owner2', firstName: 'Jane', lastName: 'Smith' }
|
|
})
|
|
}
|
|
];
|
|
|
|
it('should get all items with default pagination', async () => {
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 2,
|
|
rows: mockItems
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual({
|
|
items: [
|
|
{
|
|
id: 1,
|
|
name: 'Camping Tent',
|
|
description: 'Great for camping',
|
|
pricePerDay: 25.99,
|
|
city: 'New York',
|
|
zipCode: '10001',
|
|
latitude: 40.75, // Rounded to 2 decimal places
|
|
longitude: -73.99, // Rounded to 2 decimal places
|
|
createdAt: mockItems[0].createdAt.toISOString(),
|
|
owner: { id: 2, username: 'owner1', firstName: 'John', lastName: 'Doe' }
|
|
},
|
|
{
|
|
id: 2,
|
|
name: 'Mountain Bike',
|
|
description: 'Perfect for trails',
|
|
pricePerDay: 35.50,
|
|
city: 'Los Angeles',
|
|
zipCode: '90210',
|
|
latitude: 34.05, // Rounded to 2 decimal places
|
|
longitude: -118.24, // Rounded to 2 decimal places
|
|
createdAt: mockItems[1].createdAt.toISOString(),
|
|
owner: { id: 3, username: 'owner2', firstName: 'Jane', lastName: 'Smith' }
|
|
}
|
|
],
|
|
totalPages: 1,
|
|
currentPage: 1,
|
|
totalItems: 2
|
|
});
|
|
|
|
expect(mockItemFindAndCountAll).toHaveBeenCalledWith({
|
|
where: {},
|
|
include: [
|
|
{
|
|
model: mockUserModel,
|
|
as: 'owner',
|
|
attributes: ['id', 'username', 'firstName', 'lastName']
|
|
}
|
|
],
|
|
limit: 20,
|
|
offset: 0,
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
});
|
|
|
|
it('should handle custom pagination', async () => {
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 50,
|
|
rows: mockItems
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items?page=3&limit=10');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.totalPages).toBe(5);
|
|
expect(response.body.currentPage).toBe(3);
|
|
expect(response.body.totalItems).toBe(50);
|
|
|
|
expect(mockItemFindAndCountAll).toHaveBeenCalledWith({
|
|
where: {},
|
|
include: expect.any(Array),
|
|
limit: 10,
|
|
offset: 20, // (page 3 - 1) * limit 10
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
});
|
|
|
|
it('should filter by price range', async () => {
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 1,
|
|
rows: [mockItems[0]]
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items?minPrice=20&maxPrice=30');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockItemFindAndCountAll).toHaveBeenCalledWith({
|
|
where: {
|
|
pricePerDay: {
|
|
gte: '20',
|
|
lte: '30'
|
|
}
|
|
},
|
|
include: expect.any(Array),
|
|
limit: 20,
|
|
offset: 0,
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
});
|
|
|
|
it('should filter by minimum price only', async () => {
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 1,
|
|
rows: [mockItems[1]]
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items?minPrice=30');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockItemFindAndCountAll).toHaveBeenCalledWith({
|
|
where: {
|
|
pricePerDay: {
|
|
gte: '30'
|
|
}
|
|
},
|
|
include: expect.any(Array),
|
|
limit: 20,
|
|
offset: 0,
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
});
|
|
|
|
it('should filter by maximum price only', async () => {
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 1,
|
|
rows: [mockItems[0]]
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items?maxPrice=30');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockItemFindAndCountAll).toHaveBeenCalledWith({
|
|
where: {
|
|
pricePerDay: {
|
|
lte: '30'
|
|
}
|
|
},
|
|
include: expect.any(Array),
|
|
limit: 20,
|
|
offset: 0,
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
});
|
|
|
|
it('should filter by city', async () => {
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 1,
|
|
rows: [mockItems[0]]
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items?city=New York');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockItemFindAndCountAll).toHaveBeenCalledWith({
|
|
where: {
|
|
city: { iLike: '%New York%' }
|
|
},
|
|
include: expect.any(Array),
|
|
limit: 20,
|
|
offset: 0,
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
});
|
|
|
|
it('should filter by zip code', async () => {
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 1,
|
|
rows: [mockItems[0]]
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items?zipCode=10001');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockItemFindAndCountAll).toHaveBeenCalledWith({
|
|
where: {
|
|
zipCode: { iLike: '%10001%' }
|
|
},
|
|
include: expect.any(Array),
|
|
limit: 20,
|
|
offset: 0,
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
});
|
|
|
|
it('should search by name and description', async () => {
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 1,
|
|
rows: [mockItems[0]]
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items?search=camping');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockItemFindAndCountAll).toHaveBeenCalledWith({
|
|
where: {
|
|
or: [
|
|
{ name: { iLike: '%camping%' } },
|
|
{ description: { iLike: '%camping%' } }
|
|
]
|
|
},
|
|
include: expect.any(Array),
|
|
limit: 20,
|
|
offset: 0,
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
});
|
|
|
|
it('should combine multiple filters', async () => {
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 1,
|
|
rows: [mockItems[0]]
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items?search=tent&city=New&minPrice=20&maxPrice=30');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockItemFindAndCountAll).toHaveBeenCalledWith({
|
|
where: {
|
|
pricePerDay: {
|
|
gte: '20',
|
|
lte: '30'
|
|
},
|
|
city: { iLike: '%New%' },
|
|
or: [
|
|
{ name: { iLike: '%tent%' } },
|
|
{ description: { iLike: '%tent%' } }
|
|
]
|
|
},
|
|
include: expect.any(Array),
|
|
limit: 20,
|
|
offset: 0,
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
});
|
|
|
|
it('should handle coordinates rounding with null values', async () => {
|
|
const itemWithNullCoords = {
|
|
id: 3,
|
|
name: 'Test Item',
|
|
latitude: null,
|
|
longitude: null,
|
|
toJSON: () => ({
|
|
id: 3,
|
|
name: 'Test Item',
|
|
latitude: null,
|
|
longitude: null
|
|
})
|
|
};
|
|
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 1,
|
|
rows: [itemWithNullCoords]
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.items[0].latitude).toBeNull();
|
|
expect(response.body.items[0].longitude).toBeNull();
|
|
});
|
|
|
|
it('should handle database errors', async () => {
|
|
const dbError = new Error('Database connection failed');
|
|
mockItemFindAndCountAll.mockRejectedValue(dbError);
|
|
|
|
const response = await request(app)
|
|
.get('/items');
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(response.body).toEqual({ error: 'Database connection failed' });
|
|
});
|
|
|
|
it('should return empty results when no items found', async () => {
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 0,
|
|
rows: []
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual({
|
|
items: [],
|
|
totalPages: 0,
|
|
currentPage: 1,
|
|
totalItems: 0
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('GET /recommendations', () => {
|
|
const mockRecommendations = [
|
|
{ id: 1, name: 'Item 1', isAvailable: true },
|
|
{ id: 2, name: 'Item 2', isAvailable: true }
|
|
];
|
|
|
|
it('should get recommendations for authenticated user', async () => {
|
|
mockRentalFindAll.mockResolvedValue([]);
|
|
mockItemFindAll.mockResolvedValue(mockRecommendations);
|
|
|
|
const response = await request(app)
|
|
.get('/items/recommendations')
|
|
.set('Authorization', 'Bearer valid_token');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockRecommendations);
|
|
|
|
expect(mockRentalFindAll).toHaveBeenCalledWith({
|
|
where: { renterId: 1 },
|
|
include: [{ model: Item, as: 'item' }]
|
|
});
|
|
|
|
expect(mockItemFindAll).toHaveBeenCalledWith({
|
|
where: { isAvailable: true, isDeleted: false },
|
|
limit: 10,
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
});
|
|
|
|
it('should require authentication', async () => {
|
|
const response = await request(app)
|
|
.get('/items/recommendations');
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(response.body).toEqual({ error: 'No token provided' });
|
|
});
|
|
|
|
it('should handle database errors', async () => {
|
|
const dbError = new Error('Database error');
|
|
mockRentalFindAll.mockRejectedValue(dbError);
|
|
|
|
const response = await request(app)
|
|
.get('/items/recommendations')
|
|
.set('Authorization', 'Bearer valid_token');
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(response.body).toEqual({ error: 'Database error' });
|
|
});
|
|
});
|
|
|
|
describe('GET /:id/reviews', () => {
|
|
const mockReviews = [
|
|
{
|
|
id: 1,
|
|
itemId: 1,
|
|
itemRating: 5,
|
|
itemReview: 'Great item!',
|
|
itemReviewVisible: true,
|
|
status: 'completed',
|
|
createdAt: new Date('2023-01-01'),
|
|
renter: { id: 1, firstName: 'John', lastName: 'Doe' }
|
|
},
|
|
{
|
|
id: 2,
|
|
itemId: 1,
|
|
itemRating: 4,
|
|
itemReview: 'Good quality',
|
|
itemReviewVisible: true,
|
|
status: 'completed',
|
|
createdAt: new Date('2023-01-02'),
|
|
renter: { id: 2, firstName: 'Jane', lastName: 'Smith' }
|
|
}
|
|
];
|
|
|
|
it('should get reviews for a specific item', async () => {
|
|
mockRentalFindAll.mockResolvedValue(mockReviews);
|
|
|
|
const response = await request(app)
|
|
.get('/items/1/reviews');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual({
|
|
reviews: [
|
|
{
|
|
id: 1,
|
|
itemId: 1,
|
|
itemRating: 5,
|
|
itemReview: 'Great item!',
|
|
itemReviewVisible: true,
|
|
status: 'completed',
|
|
createdAt: '2023-01-01T00:00:00.000Z',
|
|
renter: { id: 1, firstName: 'John', lastName: 'Doe' }
|
|
},
|
|
{
|
|
id: 2,
|
|
itemId: 1,
|
|
itemRating: 4,
|
|
itemReview: 'Good quality',
|
|
itemReviewVisible: true,
|
|
status: 'completed',
|
|
createdAt: '2023-01-02T00:00:00.000Z',
|
|
renter: { id: 2, firstName: 'Jane', lastName: 'Smith' }
|
|
}
|
|
],
|
|
averageRating: 4.5,
|
|
totalReviews: 2
|
|
});
|
|
|
|
expect(mockRentalFindAll).toHaveBeenCalledWith({
|
|
where: {
|
|
itemId: '1',
|
|
status: 'completed',
|
|
itemRating: { not: null },
|
|
itemReview: { not: null },
|
|
itemReviewVisible: true
|
|
},
|
|
include: [
|
|
{
|
|
model: mockUserModel,
|
|
as: 'renter',
|
|
attributes: ['id', 'firstName', 'lastName']
|
|
}
|
|
],
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
});
|
|
|
|
it('should handle no reviews found', async () => {
|
|
mockRentalFindAll.mockResolvedValue([]);
|
|
|
|
const response = await request(app)
|
|
.get('/items/1/reviews');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual({
|
|
reviews: [],
|
|
averageRating: 0,
|
|
totalReviews: 0
|
|
});
|
|
});
|
|
|
|
it('should handle database errors', async () => {
|
|
const dbError = new Error('Database error');
|
|
mockRentalFindAll.mockRejectedValue(dbError);
|
|
|
|
const response = await request(app)
|
|
.get('/items/1/reviews');
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(response.body).toEqual({ error: 'Database error' });
|
|
});
|
|
});
|
|
|
|
describe('GET /:id', () => {
|
|
const mockItem = {
|
|
id: 1,
|
|
name: 'Test Item',
|
|
latitude: 40.7484405,
|
|
longitude: -73.9856644,
|
|
toJSON: () => ({
|
|
id: 1,
|
|
name: 'Test Item',
|
|
latitude: 40.7484405,
|
|
longitude: -73.9856644,
|
|
owner: { id: 2, username: 'owner1', firstName: 'John', lastName: 'Doe' }
|
|
})
|
|
};
|
|
|
|
it('should get a specific item by ID', async () => {
|
|
mockItemFindByPk.mockResolvedValue(mockItem);
|
|
|
|
const response = await request(app)
|
|
.get('/items/1');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual({
|
|
id: 1,
|
|
name: 'Test Item',
|
|
latitude: 40.75, // Rounded
|
|
longitude: -73.99, // Rounded
|
|
owner: { id: 2, username: 'owner1', firstName: 'John', lastName: 'Doe' }
|
|
});
|
|
|
|
expect(mockItemFindByPk).toHaveBeenCalledWith('1', {
|
|
include: [
|
|
{
|
|
model: mockUserModel,
|
|
as: 'owner',
|
|
attributes: ['id', 'username', 'firstName', 'lastName']
|
|
}
|
|
]
|
|
});
|
|
});
|
|
|
|
it('should return 404 for non-existent item', async () => {
|
|
mockItemFindByPk.mockResolvedValue(null);
|
|
|
|
const response = await request(app)
|
|
.get('/items/999');
|
|
|
|
expect(response.status).toBe(404);
|
|
expect(response.body).toEqual({ error: 'Item not found' });
|
|
});
|
|
|
|
it('should handle database errors', async () => {
|
|
const dbError = new Error('Database error');
|
|
mockItemFindByPk.mockRejectedValue(dbError);
|
|
|
|
const response = await request(app)
|
|
.get('/items/1');
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(response.body).toEqual({ error: 'Database error' });
|
|
});
|
|
});
|
|
|
|
describe('POST /', () => {
|
|
const newItemData = {
|
|
name: 'New Item',
|
|
description: 'A new test item',
|
|
pricePerDay: 25.99,
|
|
category: 'electronics'
|
|
};
|
|
|
|
const mockCreatedItem = {
|
|
id: 1,
|
|
...newItemData,
|
|
ownerId: 1
|
|
};
|
|
|
|
const mockItemWithOwner = {
|
|
id: 1,
|
|
...newItemData,
|
|
ownerId: 1,
|
|
owner: { id: 1, username: 'user1', firstName: 'John', lastName: 'Doe' }
|
|
};
|
|
|
|
it('should create a new item', async () => {
|
|
mockItemCreate.mockResolvedValue(mockCreatedItem);
|
|
mockItemFindByPk.mockResolvedValue(mockItemWithOwner);
|
|
|
|
const response = await request(app)
|
|
.post('/items')
|
|
.set('Authorization', 'Bearer valid_token')
|
|
.send(newItemData);
|
|
|
|
expect(response.status).toBe(201);
|
|
expect(response.body).toEqual(mockItemWithOwner);
|
|
|
|
expect(mockItemCreate).toHaveBeenCalledWith({
|
|
...newItemData,
|
|
ownerId: 1
|
|
});
|
|
|
|
expect(mockItemFindByPk).toHaveBeenCalledWith(1, {
|
|
include: [
|
|
{
|
|
model: mockUserModel,
|
|
as: 'owner',
|
|
attributes: ['id', 'username', 'firstName', 'lastName']
|
|
}
|
|
]
|
|
});
|
|
});
|
|
|
|
it('should require authentication', async () => {
|
|
const response = await request(app)
|
|
.post('/items')
|
|
.send(newItemData);
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(response.body).toEqual({ error: 'No token provided' });
|
|
});
|
|
|
|
it('should handle database errors during creation', async () => {
|
|
const dbError = new Error('Database error');
|
|
mockItemCreate.mockRejectedValue(dbError);
|
|
|
|
const response = await request(app)
|
|
.post('/items')
|
|
.set('Authorization', 'Bearer valid_token')
|
|
.send(newItemData);
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(response.body).toEqual({ error: 'Database error' });
|
|
});
|
|
|
|
it('should handle errors during owner fetch', async () => {
|
|
mockItemCreate.mockResolvedValue(mockCreatedItem);
|
|
const dbError = new Error('Database error');
|
|
mockItemFindByPk.mockRejectedValue(dbError);
|
|
|
|
const response = await request(app)
|
|
.post('/items')
|
|
.set('Authorization', 'Bearer valid_token')
|
|
.send(newItemData);
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(response.body).toEqual({ error: 'Database error' });
|
|
});
|
|
});
|
|
|
|
describe('PUT /:id', () => {
|
|
const updateData = {
|
|
name: 'Updated Item',
|
|
description: 'Updated description',
|
|
pricePerDay: 35.99
|
|
};
|
|
|
|
const mockItem = {
|
|
id: 1,
|
|
name: 'Original Item',
|
|
ownerId: 1,
|
|
update: jest.fn()
|
|
};
|
|
|
|
const mockUpdatedItem = {
|
|
id: 1,
|
|
...updateData,
|
|
ownerId: 1,
|
|
owner: { id: 1, username: 'user1', firstName: 'John', lastName: 'Doe' }
|
|
};
|
|
|
|
beforeEach(() => {
|
|
mockItem.update.mockReset();
|
|
});
|
|
|
|
it('should update an item when user is owner', async () => {
|
|
mockItemFindByPk
|
|
.mockResolvedValueOnce(mockItem) // First call for authorization check
|
|
.mockResolvedValueOnce(mockUpdatedItem); // Second call for returning updated item
|
|
mockItem.update.mockResolvedValue();
|
|
|
|
const response = await request(app)
|
|
.put('/items/1')
|
|
.set('Authorization', 'Bearer valid_token')
|
|
.send(updateData);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockUpdatedItem);
|
|
|
|
expect(mockItem.update).toHaveBeenCalledWith(updateData);
|
|
});
|
|
|
|
it('should return 404 for non-existent item', async () => {
|
|
mockItemFindByPk.mockResolvedValue(null);
|
|
|
|
const response = await request(app)
|
|
.put('/items/999')
|
|
.set('Authorization', 'Bearer valid_token')
|
|
.send(updateData);
|
|
|
|
expect(response.status).toBe(404);
|
|
expect(response.body).toEqual({ error: 'Item not found' });
|
|
});
|
|
|
|
it('should return 403 when user is not owner', async () => {
|
|
const itemOwnedByOther = { ...mockItem, ownerId: 2 };
|
|
mockItemFindByPk.mockResolvedValue(itemOwnedByOther);
|
|
|
|
const response = await request(app)
|
|
.put('/items/1')
|
|
.set('Authorization', 'Bearer valid_token')
|
|
.send(updateData);
|
|
|
|
expect(response.status).toBe(403);
|
|
expect(response.body).toEqual({ error: 'Unauthorized' });
|
|
});
|
|
|
|
it('should require authentication', async () => {
|
|
const response = await request(app)
|
|
.put('/items/1')
|
|
.send(updateData);
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(response.body).toEqual({ error: 'No token provided' });
|
|
});
|
|
|
|
it('should handle database errors', async () => {
|
|
const dbError = new Error('Database error');
|
|
mockItemFindByPk.mockRejectedValue(dbError);
|
|
|
|
const response = await request(app)
|
|
.put('/items/1')
|
|
.set('Authorization', 'Bearer valid_token')
|
|
.send(updateData);
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(response.body).toEqual({ error: 'Database error' });
|
|
});
|
|
|
|
it('should handle update errors', async () => {
|
|
const updateError = new Error('Update failed');
|
|
mockItemFindByPk.mockResolvedValue(mockItem);
|
|
mockItem.update.mockRejectedValue(updateError);
|
|
|
|
const response = await request(app)
|
|
.put('/items/1')
|
|
.set('Authorization', 'Bearer valid_token')
|
|
.send(updateData);
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(response.body).toEqual({ error: 'Update failed' });
|
|
});
|
|
});
|
|
|
|
describe('DELETE /:id', () => {
|
|
const mockItem = {
|
|
id: 1,
|
|
name: 'Item to delete',
|
|
ownerId: 1,
|
|
destroy: jest.fn()
|
|
};
|
|
|
|
beforeEach(() => {
|
|
mockItem.destroy.mockReset();
|
|
});
|
|
|
|
it('should delete an item when user is owner', async () => {
|
|
mockItemFindByPk.mockResolvedValue(mockItem);
|
|
mockItem.destroy.mockResolvedValue();
|
|
|
|
const response = await request(app)
|
|
.delete('/items/1')
|
|
.set('Authorization', 'Bearer valid_token');
|
|
|
|
expect(response.status).toBe(204);
|
|
expect(response.body).toEqual({});
|
|
expect(mockItem.destroy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return 404 for non-existent item', async () => {
|
|
mockItemFindByPk.mockResolvedValue(null);
|
|
|
|
const response = await request(app)
|
|
.delete('/items/999')
|
|
.set('Authorization', 'Bearer valid_token');
|
|
|
|
expect(response.status).toBe(404);
|
|
expect(response.body).toEqual({ error: 'Item not found' });
|
|
});
|
|
|
|
it('should return 403 when user is not owner', async () => {
|
|
const itemOwnedByOther = { ...mockItem, ownerId: 2 };
|
|
mockItemFindByPk.mockResolvedValue(itemOwnedByOther);
|
|
|
|
const response = await request(app)
|
|
.delete('/items/1')
|
|
.set('Authorization', 'Bearer valid_token');
|
|
|
|
expect(response.status).toBe(403);
|
|
expect(response.body).toEqual({ error: 'Unauthorized' });
|
|
});
|
|
|
|
it('should require authentication', async () => {
|
|
const response = await request(app)
|
|
.delete('/items/1');
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(response.body).toEqual({ error: 'No token provided' });
|
|
});
|
|
|
|
it('should handle database errors', async () => {
|
|
const dbError = new Error('Database error');
|
|
mockItemFindByPk.mockRejectedValue(dbError);
|
|
|
|
const response = await request(app)
|
|
.delete('/items/1')
|
|
.set('Authorization', 'Bearer valid_token');
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(response.body).toEqual({ error: 'Database error' });
|
|
});
|
|
|
|
it('should handle deletion errors', async () => {
|
|
const deleteError = new Error('Delete failed');
|
|
mockItemFindByPk.mockResolvedValue(mockItem);
|
|
mockItem.destroy.mockRejectedValue(deleteError);
|
|
|
|
const response = await request(app)
|
|
.delete('/items/1')
|
|
.set('Authorization', 'Bearer valid_token');
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(response.body).toEqual({ error: 'Delete failed' });
|
|
});
|
|
});
|
|
|
|
describe('Coordinate Rounding', () => {
|
|
it('should round coordinates to 2 decimal places', async () => {
|
|
const itemWithPreciseCoords = {
|
|
toJSON: () => ({
|
|
id: 1,
|
|
latitude: 40.748440123456,
|
|
longitude: -73.985664789012
|
|
})
|
|
};
|
|
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 1,
|
|
rows: [itemWithPreciseCoords]
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.items[0].latitude).toBe(40.75);
|
|
expect(response.body.items[0].longitude).toBe(-73.99);
|
|
});
|
|
|
|
it('should handle undefined coordinates', async () => {
|
|
const itemWithUndefinedCoords = {
|
|
toJSON: () => ({
|
|
id: 1,
|
|
latitude: undefined,
|
|
longitude: undefined
|
|
})
|
|
};
|
|
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 1,
|
|
rows: [itemWithUndefinedCoords]
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.items[0].latitude).toBeUndefined();
|
|
expect(response.body.items[0].longitude).toBeUndefined();
|
|
});
|
|
|
|
it('should handle zero coordinates', async () => {
|
|
const itemWithZeroCoords = {
|
|
toJSON: () => ({
|
|
id: 1,
|
|
latitude: 0,
|
|
longitude: 0
|
|
})
|
|
};
|
|
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 1,
|
|
rows: [itemWithZeroCoords]
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.items[0].latitude).toBe(0);
|
|
expect(response.body.items[0].longitude).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle very large page numbers', async () => {
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 0,
|
|
rows: []
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items?page=999999&limit=10');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.currentPage).toBe(999999);
|
|
expect(response.body.totalPages).toBe(0);
|
|
});
|
|
|
|
it('should handle invalid query parameters gracefully', async () => {
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 0,
|
|
rows: []
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items?page=invalid&limit=abc&minPrice=notanumber');
|
|
|
|
expect(response.status).toBe(200);
|
|
// Express will handle invalid numbers, typically converting to NaN or 0
|
|
});
|
|
|
|
it('should handle empty search queries', async () => {
|
|
mockItemFindAndCountAll.mockResolvedValue({
|
|
count: 0,
|
|
rows: []
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/items?search=');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockItemFindAndCountAll).toHaveBeenCalledWith({
|
|
where: {},
|
|
include: expect.any(Array),
|
|
limit: 20,
|
|
offset: 0,
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
});
|
|
});
|
|
}); |