imageFilenames and imageFilename, backend integration tests, frontend tests, removed username references
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
// Change images column from VARCHAR(255)[] to TEXT[] to support longer URLs
|
||||
await queryInterface.changeColumn("Items", "images", {
|
||||
type: Sequelize.ARRAY(Sequelize.TEXT),
|
||||
defaultValue: [],
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
// Revert to original VARCHAR(255)[] - note: this may fail if data exceeds 255 chars
|
||||
await queryInterface.changeColumn("Items", "images", {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
defaultValue: [],
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
// Change image/photo URL fields from VARCHAR(255) to TEXT to support longer URLs
|
||||
await Promise.all([
|
||||
queryInterface.changeColumn("Users", "profileImage", {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
}),
|
||||
queryInterface.changeColumn("Messages", "imagePath", {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
}),
|
||||
queryInterface.changeColumn("ConditionChecks", "photos", {
|
||||
type: Sequelize.ARRAY(Sequelize.TEXT),
|
||||
defaultValue: [],
|
||||
}),
|
||||
]);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
// Revert to original VARCHAR(255) - note: this may fail if data exceeds 255 chars
|
||||
await Promise.all([
|
||||
queryInterface.changeColumn("Users", "profileImage", {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true,
|
||||
}),
|
||||
queryInterface.changeColumn("Messages", "imagePath", {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true,
|
||||
}),
|
||||
queryInterface.changeColumn("ConditionChecks", "photos", {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
defaultValue: [],
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
// Rename image fields to consistent naming convention
|
||||
// Using TEXT type for all to support long URLs/paths
|
||||
await queryInterface.renameColumn("Items", "images", "imageFilenames");
|
||||
await queryInterface.renameColumn("Users", "profileImage", "imageFilename");
|
||||
await queryInterface.renameColumn("Messages", "imagePath", "imageFilename");
|
||||
await queryInterface.renameColumn("ConditionChecks", "photos", "imageFilenames");
|
||||
await queryInterface.renameColumn("ForumPosts", "images", "imageFilenames");
|
||||
await queryInterface.renameColumn("ForumComments", "images", "imageFilenames");
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
// Revert to original column names
|
||||
await queryInterface.renameColumn("Items", "imageFilenames", "images");
|
||||
await queryInterface.renameColumn("Users", "imageFilename", "profileImage");
|
||||
await queryInterface.renameColumn("Messages", "imageFilename", "imagePath");
|
||||
await queryInterface.renameColumn("ConditionChecks", "imageFilenames", "photos");
|
||||
await queryInterface.renameColumn("ForumPosts", "imageFilenames", "images");
|
||||
await queryInterface.renameColumn("ForumComments", "imageFilenames", "images");
|
||||
},
|
||||
};
|
||||
@@ -24,8 +24,8 @@ const ConditionCheck = sequelize.define("ConditionCheck", {
|
||||
),
|
||||
allowNull: false,
|
||||
},
|
||||
photos: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
imageFilenames: {
|
||||
type: DataTypes.ARRAY(DataTypes.TEXT),
|
||||
defaultValue: [],
|
||||
},
|
||||
notes: {
|
||||
|
||||
@@ -39,7 +39,7 @@ const ForumComment = sequelize.define('ForumComment', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
images: {
|
||||
imageFilenames: {
|
||||
type: DataTypes.ARRAY(DataTypes.TEXT),
|
||||
allowNull: true,
|
||||
defaultValue: []
|
||||
|
||||
@@ -52,7 +52,7 @@ const ForumPost = sequelize.define('ForumPost', {
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
images: {
|
||||
imageFilenames: {
|
||||
type: DataTypes.ARRAY(DataTypes.TEXT),
|
||||
allowNull: true,
|
||||
defaultValue: []
|
||||
|
||||
@@ -82,8 +82,8 @@ const Item = sequelize.define("Item", {
|
||||
longitude: {
|
||||
type: DataTypes.DECIMAL(11, 8),
|
||||
},
|
||||
images: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
imageFilenames: {
|
||||
type: DataTypes.ARRAY(DataTypes.TEXT),
|
||||
defaultValue: [],
|
||||
},
|
||||
isAvailable: {
|
||||
|
||||
@@ -31,8 +31,8 @@ const Message = sequelize.define('Message', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
imagePath: {
|
||||
type: DataTypes.STRING,
|
||||
imageFilename: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
|
||||
@@ -60,8 +60,8 @@ const User = sequelize.define(
|
||||
country: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
profileImage: {
|
||||
type: DataTypes.STRING,
|
||||
imageFilename: {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
isVerified: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
|
||||
@@ -101,12 +101,14 @@ router.post(
|
||||
phone,
|
||||
});
|
||||
|
||||
// Link alpha invitation to user
|
||||
await alphaInvitation.update({
|
||||
usedBy: user.id,
|
||||
usedAt: new Date(),
|
||||
status: "active",
|
||||
});
|
||||
// Link alpha invitation to user (only if alpha testing is enabled)
|
||||
if (alphaInvitation) {
|
||||
await alphaInvitation.update({
|
||||
usedBy: user.id,
|
||||
usedAt: new Date(),
|
||||
status: "active",
|
||||
});
|
||||
}
|
||||
|
||||
// Generate verification token and send email
|
||||
await user.generateVerificationToken();
|
||||
@@ -367,7 +369,7 @@ router.post(
|
||||
lastName,
|
||||
authProvider: "google",
|
||||
providerId: googleId,
|
||||
profileImage: picture,
|
||||
imageFilename: picture,
|
||||
isVerified: true,
|
||||
verifiedAt: new Date(),
|
||||
});
|
||||
@@ -434,7 +436,7 @@ router.post(
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
profileImage: user.profileImage,
|
||||
imageFilename: user.imageFilename,
|
||||
isVerified: user.isVerified,
|
||||
role: user.role,
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ const upload = multer({
|
||||
router.post(
|
||||
"/:rentalId",
|
||||
authenticateToken,
|
||||
upload.array("photos"),
|
||||
upload.array("imageFilenames"),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { rentalId } = req.params;
|
||||
@@ -35,13 +35,13 @@ router.post(
|
||||
const userId = req.user.id;
|
||||
|
||||
// Get uploaded file paths
|
||||
const photos = req.files ? req.files.map((file) => file.path) : [];
|
||||
const imageFilenames = req.files ? req.files.map((file) => file.path) : [];
|
||||
|
||||
const conditionCheck = await ConditionCheckService.submitConditionCheck(
|
||||
rentalId,
|
||||
checkType,
|
||||
userId,
|
||||
photos,
|
||||
imageFilenames,
|
||||
notes
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ router.post(
|
||||
rentalId,
|
||||
checkType,
|
||||
userId,
|
||||
photoCount: photos.length,
|
||||
photoCount: imageFilenames.length,
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
|
||||
@@ -21,7 +21,7 @@ const buildCommentTree = (comments, isAdmin = false) => {
|
||||
// Sanitize deleted comments for non-admin users
|
||||
if (commentJson.isDeleted && !isAdmin) {
|
||||
commentJson.content = '';
|
||||
commentJson.images = [];
|
||||
commentJson.imageFilenames = [];
|
||||
}
|
||||
|
||||
commentMap[comment.id] = { ...commentJson, replies: [] };
|
||||
@@ -252,7 +252,7 @@ router.post('/posts', authenticateToken, uploadForumPostImages, async (req, res,
|
||||
}
|
||||
|
||||
// Extract image filenames if uploaded
|
||||
const images = req.files ? req.files.map(file => file.filename) : [];
|
||||
const imageFilenames = req.files ? req.files.map(file => file.filename) : [];
|
||||
|
||||
// Initialize location fields
|
||||
let latitude = null;
|
||||
@@ -313,7 +313,7 @@ router.post('/posts', authenticateToken, uploadForumPostImages, async (req, res,
|
||||
content,
|
||||
category,
|
||||
authorId: req.user.id,
|
||||
images,
|
||||
imageFilenames,
|
||||
zipCode: zipCode || null,
|
||||
latitude,
|
||||
longitude
|
||||
@@ -936,14 +936,14 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages,
|
||||
}
|
||||
|
||||
// Extract image filenames if uploaded
|
||||
const images = req.files ? req.files.map(file => file.filename) : [];
|
||||
const imageFilenames = req.files ? req.files.map(file => file.filename) : [];
|
||||
|
||||
const comment = await ForumComment.create({
|
||||
postId: req.params.id,
|
||||
authorId: req.user.id,
|
||||
content,
|
||||
parentCommentId: parentCommentId || null,
|
||||
images
|
||||
imageFilenames
|
||||
});
|
||||
|
||||
// Increment comment count and update post's updatedAt to reflect activity
|
||||
@@ -955,7 +955,7 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages,
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
|
||||
attributes: ['id', 'firstName', 'lastName', 'email']
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -1261,7 +1261,7 @@ router.delete('/admin/posts/:id', authenticateToken, requireAdmin, async (req, r
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
|
||||
attributes: ['id', 'firstName', 'lastName', 'email']
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -1380,7 +1380,7 @@ router.delete('/admin/comments/:id', authenticateToken, requireAdmin, async (req
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
|
||||
attributes: ['id', 'firstName', 'lastName', 'email']
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -1512,7 +1512,7 @@ router.patch('/admin/posts/:id/close', authenticateToken, requireAdmin, async (r
|
||||
{
|
||||
model: User,
|
||||
as: 'author',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
|
||||
attributes: ['id', 'firstName', 'lastName', 'email']
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -1545,7 +1545,7 @@ router.patch('/admin/posts/:id/close', authenticateToken, requireAdmin, async (r
|
||||
(async () => {
|
||||
try {
|
||||
const admin = await User.findByPk(req.user.id, {
|
||||
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
|
||||
attributes: ['id', 'firstName', 'lastName', 'email']
|
||||
});
|
||||
|
||||
// Get all unique participants (author + commenters)
|
||||
|
||||
@@ -20,7 +20,7 @@ router.get('/', authenticateToken, async (req, res, next) => {
|
||||
{
|
||||
model: User,
|
||||
as: 'sender',
|
||||
attributes: ['id', 'firstName', 'lastName', 'profileImage']
|
||||
attributes: ['id', 'firstName', 'lastName', 'imageFilename']
|
||||
}
|
||||
],
|
||||
order: [['createdAt', 'DESC']]
|
||||
@@ -61,12 +61,12 @@ router.get('/conversations', authenticateToken, async (req, res, next) => {
|
||||
{
|
||||
model: User,
|
||||
as: 'sender',
|
||||
attributes: ['id', 'firstName', 'lastName', 'profileImage']
|
||||
attributes: ['id', 'firstName', 'lastName', 'imageFilename']
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'receiver',
|
||||
attributes: ['id', 'firstName', 'lastName', 'profileImage']
|
||||
attributes: ['id', 'firstName', 'lastName', 'imageFilename']
|
||||
}
|
||||
],
|
||||
order: [['createdAt', 'DESC']]
|
||||
@@ -147,7 +147,7 @@ router.get('/sent', authenticateToken, async (req, res, next) => {
|
||||
{
|
||||
model: User,
|
||||
as: 'receiver',
|
||||
attributes: ['id', 'firstName', 'lastName', 'profileImage']
|
||||
attributes: ['id', 'firstName', 'lastName', 'imageFilename']
|
||||
}
|
||||
],
|
||||
order: [['createdAt', 'DESC']]
|
||||
@@ -186,12 +186,12 @@ router.get('/:id', authenticateToken, async (req, res, next) => {
|
||||
{
|
||||
model: User,
|
||||
as: 'sender',
|
||||
attributes: ['id', 'firstName', 'lastName', 'profileImage']
|
||||
attributes: ['id', 'firstName', 'lastName', 'imageFilename']
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'receiver',
|
||||
attributes: ['id', 'firstName', 'lastName', 'profileImage']
|
||||
attributes: ['id', 'firstName', 'lastName', 'imageFilename']
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -253,20 +253,20 @@ router.post('/', authenticateToken, uploadMessageImage, async (req, res, next) =
|
||||
}
|
||||
|
||||
// Extract image filename if uploaded
|
||||
const imagePath = req.file ? req.file.filename : null;
|
||||
const imageFilename = req.file ? req.file.filename : null;
|
||||
|
||||
const message = await Message.create({
|
||||
senderId: req.user.id,
|
||||
receiverId,
|
||||
content,
|
||||
imagePath
|
||||
imageFilename
|
||||
});
|
||||
|
||||
const messageWithSender = await Message.findByPk(message.id, {
|
||||
include: [{
|
||||
model: User,
|
||||
as: 'sender',
|
||||
attributes: ['id', 'firstName', 'lastName', 'profileImage']
|
||||
attributes: ['id', 'firstName', 'lastName', 'imageFilename']
|
||||
}]
|
||||
});
|
||||
|
||||
@@ -398,7 +398,7 @@ router.get('/images/:filename',
|
||||
// Verify user is sender or receiver of a message with this image
|
||||
const message = await Message.findOne({
|
||||
where: {
|
||||
imagePath: filename,
|
||||
imageFilename: filename,
|
||||
[Op.or]: [
|
||||
{ senderId: req.user.id },
|
||||
{ receiverId: req.user.id }
|
||||
|
||||
@@ -211,8 +211,8 @@ router.post('/profile/image', authenticateToken, (req, res) => {
|
||||
try {
|
||||
// Delete old profile image if exists
|
||||
const user = await User.findByPk(req.user.id);
|
||||
if (user.profileImage) {
|
||||
const oldImagePath = path.join(__dirname, '../uploads/profiles', user.profileImage);
|
||||
if (user.imageFilename) {
|
||||
const oldImagePath = path.join(__dirname, '../uploads/profiles', user.imageFilename);
|
||||
try {
|
||||
await fs.unlink(oldImagePath);
|
||||
} catch (unlinkErr) {
|
||||
@@ -227,7 +227,7 @@ router.post('/profile/image', authenticateToken, (req, res) => {
|
||||
|
||||
// Update user with new image filename
|
||||
await user.update({
|
||||
profileImage: req.file.filename
|
||||
imageFilename: req.file.filename
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
|
||||
@@ -116,7 +116,7 @@ class ConditionCheckService {
|
||||
* @param {string} rentalId - Rental ID
|
||||
* @param {string} checkType - Type of check
|
||||
* @param {string} userId - User submitting the check
|
||||
* @param {Array} photos - Array of photo URLs
|
||||
* @param {Array} imageFilenames - Array of image filenames
|
||||
* @param {string} notes - Optional notes
|
||||
* @returns {Object} - Created condition check
|
||||
*/
|
||||
@@ -124,7 +124,7 @@ class ConditionCheckService {
|
||||
rentalId,
|
||||
checkType,
|
||||
userId,
|
||||
photos = [],
|
||||
imageFilenames = [],
|
||||
notes = null
|
||||
) {
|
||||
// Validate the check
|
||||
@@ -139,7 +139,7 @@ class ConditionCheckService {
|
||||
}
|
||||
|
||||
// Validate photos (basic validation)
|
||||
if (photos.length > 20) {
|
||||
if (imageFilenames.length > 20) {
|
||||
throw new Error("Maximum 20 photos allowed per condition check");
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ class ConditionCheckService {
|
||||
rentalId,
|
||||
checkType,
|
||||
submittedBy: userId,
|
||||
photos,
|
||||
imageFilenames,
|
||||
notes,
|
||||
});
|
||||
|
||||
@@ -166,7 +166,7 @@ class ConditionCheckService {
|
||||
{
|
||||
model: User,
|
||||
as: "submittedByUser",
|
||||
attributes: ["id", "username", "firstName", "lastName"],
|
||||
attributes: ["id", "firstName", "lastName"],
|
||||
},
|
||||
],
|
||||
order: [["submittedAt", "ASC"]],
|
||||
@@ -192,7 +192,7 @@ class ConditionCheckService {
|
||||
{
|
||||
model: User,
|
||||
as: "submittedByUser",
|
||||
attributes: ["id", "username", "firstName", "lastName"],
|
||||
attributes: ["id", "firstName", "lastName"],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -216,7 +216,7 @@ class ConditionCheckService {
|
||||
status: "completed",
|
||||
submittedAt: existingCheck.submittedAt,
|
||||
submittedBy: existingCheck.submittedBy,
|
||||
photoCount: existingCheck.photos.length,
|
||||
photoCount: existingCheck.imageFilenames.length,
|
||||
hasNotes: !!existingCheck.notes,
|
||||
};
|
||||
} else {
|
||||
|
||||
621
backend/tests/integration/auth.integration.test.js
Normal file
621
backend/tests/integration/auth.integration.test.js
Normal file
@@ -0,0 +1,621 @@
|
||||
/**
|
||||
* Authentication Integration Tests
|
||||
*
|
||||
* These tests use a real database connection to verify the complete
|
||||
* authentication flow including user registration, login, token management,
|
||||
* and password reset functionality.
|
||||
*/
|
||||
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// Mock rate limiters before importing routes
|
||||
jest.mock('../../middleware/rateLimiter', () => ({
|
||||
registerLimiter: (req, res, next) => next(),
|
||||
loginLimiter: (req, res, next) => next(),
|
||||
refreshLimiter: (req, res, next) => next(),
|
||||
passwordResetLimiter: (req, res, next) => next(),
|
||||
passwordResetRequestLimiter: (req, res, next) => next(),
|
||||
verifyEmailLimiter: (req, res, next) => next(),
|
||||
resendVerificationLimiter: (req, res, next) => next(),
|
||||
}));
|
||||
|
||||
// Mock CSRF protection for tests
|
||||
jest.mock('../../middleware/csrf', () => ({
|
||||
csrfProtection: (req, res, next) => next(),
|
||||
getCSRFToken: (req, res) => {
|
||||
res.set('x-csrf-token', 'test-csrf-token');
|
||||
res.json({ csrfToken: 'test-csrf-token' });
|
||||
},
|
||||
}));
|
||||
|
||||
const { sequelize, User, AlphaInvitation } = require('../../models');
|
||||
const authRoutes = require('../../routes/auth');
|
||||
|
||||
// Test app setup
|
||||
const createTestApp = () => {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
|
||||
// Add request ID middleware
|
||||
app.use((req, res, next) => {
|
||||
req.id = 'test-request-id';
|
||||
next();
|
||||
});
|
||||
|
||||
app.use('/auth', authRoutes);
|
||||
return app;
|
||||
};
|
||||
|
||||
// Test data factory
|
||||
const createTestUser = async (overrides = {}) => {
|
||||
const defaultData = {
|
||||
email: `test-${Date.now()}@example.com`,
|
||||
password: 'TestPassword123!',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
isVerified: false,
|
||||
authProvider: 'local',
|
||||
};
|
||||
|
||||
return User.create({ ...defaultData, ...overrides });
|
||||
};
|
||||
|
||||
const createAlphaInvitation = async (overrides = {}) => {
|
||||
// Generate a valid code matching pattern /^ALPHA-[A-Z0-9]{8}$/i
|
||||
const randomCode = Math.random().toString(36).substring(2, 10).toUpperCase().padEnd(8, 'X');
|
||||
const defaultData = {
|
||||
code: `ALPHA-${randomCode.substring(0, 8)}`,
|
||||
email: `alpha-${Date.now()}@example.com`, // Email is required
|
||||
status: 'pending', // Valid values: pending, active, revoked
|
||||
};
|
||||
|
||||
return AlphaInvitation.create({ ...defaultData, ...overrides });
|
||||
};
|
||||
|
||||
describe('Auth Integration Tests', () => {
|
||||
let app;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Set test environment variables
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.JWT_ACCESS_SECRET = 'test-access-secret';
|
||||
process.env.JWT_REFRESH_SECRET = 'test-refresh-secret';
|
||||
process.env.ALPHA_TESTING_ENABLED = 'false';
|
||||
|
||||
// Sync database
|
||||
await sequelize.sync({ force: true });
|
||||
|
||||
app = createTestApp();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await sequelize.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean up users before each test
|
||||
await User.destroy({ where: {}, truncate: true, cascade: true });
|
||||
await AlphaInvitation.destroy({ where: {}, truncate: true, cascade: true });
|
||||
});
|
||||
|
||||
describe('POST /auth/register', () => {
|
||||
it('should register a new user successfully', async () => {
|
||||
const userData = {
|
||||
email: 'newuser@example.com',
|
||||
password: 'SecurePassword123!',
|
||||
firstName: 'New',
|
||||
lastName: 'User',
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/register')
|
||||
.send(userData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body.user).toBeDefined();
|
||||
expect(response.body.user.email).toBe(userData.email);
|
||||
expect(response.body.user.firstName).toBe(userData.firstName);
|
||||
expect(response.body.user.isVerified).toBe(false);
|
||||
|
||||
// Verify user was created in database
|
||||
const user = await User.findOne({ where: { email: userData.email } });
|
||||
expect(user).not.toBeNull();
|
||||
expect(user.firstName).toBe(userData.firstName);
|
||||
|
||||
// Verify password was hashed
|
||||
expect(user.password).not.toBe(userData.password);
|
||||
|
||||
// Verify cookies were set
|
||||
expect(response.headers['set-cookie']).toBeDefined();
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies.some(c => c.startsWith('accessToken='))).toBe(true);
|
||||
expect(cookies.some(c => c.startsWith('refreshToken='))).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject registration with existing email', async () => {
|
||||
await createTestUser({ email: 'existing@example.com' });
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
email: 'existing@example.com',
|
||||
password: 'SecurePassword123!',
|
||||
firstName: 'Another',
|
||||
lastName: 'User',
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error).toBe('Registration failed');
|
||||
expect(response.body.details[0].field).toBe('email');
|
||||
});
|
||||
|
||||
it('should reject registration with invalid email format', async () => {
|
||||
const response = await request(app)
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
email: 'not-an-email',
|
||||
password: 'SecurePassword123!',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
// Response should contain errors or error message
|
||||
expect(response.body.errors || response.body.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should generate verification token on registration', async () => {
|
||||
const userData = {
|
||||
email: 'verify@example.com',
|
||||
password: 'SecurePassword123!',
|
||||
firstName: 'Verify',
|
||||
lastName: 'User',
|
||||
};
|
||||
|
||||
await request(app)
|
||||
.post('/auth/register')
|
||||
.send(userData)
|
||||
.expect(201);
|
||||
|
||||
const user = await User.findOne({ where: { email: userData.email } });
|
||||
expect(user.verificationToken).toBeDefined();
|
||||
expect(user.verificationTokenExpiry).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/login', () => {
|
||||
let testUser;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await createTestUser({
|
||||
email: 'login@example.com',
|
||||
password: 'TestPassword123!',
|
||||
isVerified: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should login with valid credentials', async () => {
|
||||
const response = await request(app)
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
email: 'login@example.com',
|
||||
password: 'TestPassword123!',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.user).toBeDefined();
|
||||
expect(response.body.user.email).toBe('login@example.com');
|
||||
|
||||
// Verify cookies were set
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies.some(c => c.startsWith('accessToken='))).toBe(true);
|
||||
expect(cookies.some(c => c.startsWith('refreshToken='))).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject login with wrong password', async () => {
|
||||
const response = await request(app)
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
email: 'login@example.com',
|
||||
password: 'WrongPassword!',
|
||||
})
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.error).toBe('Invalid credentials');
|
||||
});
|
||||
|
||||
it('should reject login with non-existent email', async () => {
|
||||
const response = await request(app)
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
email: 'nonexistent@example.com',
|
||||
password: 'SomePassword123!',
|
||||
})
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.error).toBe('Invalid credentials');
|
||||
});
|
||||
|
||||
it('should increment login attempts on failed login', async () => {
|
||||
await request(app)
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
email: 'login@example.com',
|
||||
password: 'WrongPassword!',
|
||||
})
|
||||
.expect(401);
|
||||
|
||||
const user = await User.findOne({ where: { email: 'login@example.com' } });
|
||||
expect(user.loginAttempts).toBe(1);
|
||||
});
|
||||
|
||||
it('should lock account after too many failed attempts', async () => {
|
||||
// Make 5 failed login attempts
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await request(app)
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
email: 'login@example.com',
|
||||
password: 'WrongPassword!',
|
||||
});
|
||||
}
|
||||
|
||||
// 6th attempt should return locked error
|
||||
const response = await request(app)
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
email: 'login@example.com',
|
||||
password: 'TestPassword123!', // Correct password
|
||||
})
|
||||
.expect(423);
|
||||
|
||||
expect(response.body.error).toContain('Account is temporarily locked');
|
||||
});
|
||||
|
||||
it('should reset login attempts on successful login', async () => {
|
||||
// First fail a login
|
||||
await request(app)
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
email: 'login@example.com',
|
||||
password: 'WrongPassword!',
|
||||
});
|
||||
|
||||
// Verify attempts incremented
|
||||
let user = await User.findOne({ where: { email: 'login@example.com' } });
|
||||
expect(user.loginAttempts).toBe(1);
|
||||
|
||||
// Now login successfully
|
||||
await request(app)
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
email: 'login@example.com',
|
||||
password: 'TestPassword123!',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// Verify attempts reset
|
||||
user = await User.findOne({ where: { email: 'login@example.com' } });
|
||||
expect(user.loginAttempts).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/logout', () => {
|
||||
it('should clear cookies on logout', async () => {
|
||||
const response = await request(app)
|
||||
.post('/auth/logout')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.message).toBe('Logged out successfully');
|
||||
|
||||
// Verify cookies are cleared
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies.some(c => c.includes('accessToken=;'))).toBe(true);
|
||||
expect(cookies.some(c => c.includes('refreshToken=;'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/refresh', () => {
|
||||
let testUser;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await createTestUser({
|
||||
email: 'refresh@example.com',
|
||||
isVerified: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should refresh access token with valid refresh token', async () => {
|
||||
// Create a valid refresh token
|
||||
const refreshToken = jwt.sign(
|
||||
{ id: testUser.id, jwtVersion: testUser.jwtVersion, type: 'refresh' },
|
||||
process.env.JWT_REFRESH_SECRET,
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/refresh')
|
||||
.set('Cookie', [`refreshToken=${refreshToken}`])
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.user).toBeDefined();
|
||||
expect(response.body.user.email).toBe('refresh@example.com');
|
||||
|
||||
// Verify new access token cookie was set
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies.some(c => c.startsWith('accessToken='))).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject refresh without token', async () => {
|
||||
const response = await request(app)
|
||||
.post('/auth/refresh')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.error).toBe('Refresh token required');
|
||||
});
|
||||
|
||||
it('should reject refresh with invalid token', async () => {
|
||||
const response = await request(app)
|
||||
.post('/auth/refresh')
|
||||
.set('Cookie', ['refreshToken=invalid-token'])
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.error).toBe('Invalid or expired refresh token');
|
||||
});
|
||||
|
||||
it('should reject refresh with outdated JWT version', async () => {
|
||||
// Create refresh token with old JWT version
|
||||
const refreshToken = jwt.sign(
|
||||
{ id: testUser.id, jwtVersion: testUser.jwtVersion - 1, type: 'refresh' },
|
||||
process.env.JWT_REFRESH_SECRET,
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/refresh')
|
||||
.set('Cookie', [`refreshToken=${refreshToken}`])
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.code).toBe('JWT_VERSION_MISMATCH');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /auth/status', () => {
|
||||
let testUser;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await createTestUser({
|
||||
email: 'status@example.com',
|
||||
isVerified: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return authenticated status with valid token', async () => {
|
||||
const accessToken = jwt.sign(
|
||||
{ id: testUser.id, jwtVersion: testUser.jwtVersion },
|
||||
process.env.JWT_ACCESS_SECRET,
|
||||
{ expiresIn: '15m' }
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/auth/status')
|
||||
.set('Cookie', [`accessToken=${accessToken}`])
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.authenticated).toBe(true);
|
||||
expect(response.body.user.email).toBe('status@example.com');
|
||||
});
|
||||
|
||||
it('should return unauthenticated status without token', async () => {
|
||||
const response = await request(app)
|
||||
.get('/auth/status')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.authenticated).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/verify-email', () => {
|
||||
let testUser;
|
||||
let verificationToken;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await createTestUser({
|
||||
email: 'unverified@example.com',
|
||||
isVerified: false,
|
||||
});
|
||||
await testUser.generateVerificationToken();
|
||||
await testUser.reload();
|
||||
verificationToken = testUser.verificationToken;
|
||||
});
|
||||
|
||||
it('should verify email with valid token', async () => {
|
||||
const response = await request(app)
|
||||
.post('/auth/verify-email')
|
||||
.send({ token: verificationToken })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.message).toBe('Email verified successfully');
|
||||
expect(response.body.user.isVerified).toBe(true);
|
||||
|
||||
// Verify in database
|
||||
await testUser.reload();
|
||||
expect(testUser.isVerified).toBe(true);
|
||||
expect(testUser.verificationToken).toBeNull();
|
||||
});
|
||||
|
||||
it('should reject verification with invalid token', async () => {
|
||||
const response = await request(app)
|
||||
.post('/auth/verify-email')
|
||||
.send({ token: 'invalid-token' })
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.code).toBe('VERIFICATION_TOKEN_INVALID');
|
||||
});
|
||||
|
||||
it('should reject verification for already verified user', async () => {
|
||||
// First verify the user
|
||||
await testUser.verifyEmail();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/verify-email')
|
||||
.send({ token: verificationToken })
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.code).toBe('VERIFICATION_TOKEN_INVALID');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Password Reset Flow', () => {
|
||||
let testUser;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await createTestUser({
|
||||
email: 'reset@example.com',
|
||||
isVerified: true,
|
||||
authProvider: 'local',
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/forgot-password', () => {
|
||||
it('should accept valid email and generate reset token', async () => {
|
||||
const response = await request(app)
|
||||
.post('/auth/forgot-password')
|
||||
.send({ email: 'reset@example.com' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.message).toContain('If an account exists');
|
||||
|
||||
// Verify token was generated in database
|
||||
await testUser.reload();
|
||||
expect(testUser.passwordResetToken).toBeDefined();
|
||||
expect(testUser.passwordResetTokenExpiry).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return success even for non-existent email (security)', async () => {
|
||||
const response = await request(app)
|
||||
.post('/auth/forgot-password')
|
||||
.send({ email: 'nonexistent@example.com' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.message).toContain('If an account exists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/reset-password', () => {
|
||||
let resetToken;
|
||||
|
||||
beforeEach(async () => {
|
||||
resetToken = await testUser.generatePasswordResetToken();
|
||||
});
|
||||
|
||||
it('should reset password with valid token', async () => {
|
||||
const newPassword = 'NewSecurePassword123!';
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/reset-password')
|
||||
.send({ token: resetToken, newPassword })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.message).toContain('Password has been reset');
|
||||
|
||||
// Verify password was changed
|
||||
await testUser.reload();
|
||||
const isValid = await testUser.comparePassword(newPassword);
|
||||
expect(isValid).toBe(true);
|
||||
|
||||
// Verify token was cleared
|
||||
expect(testUser.passwordResetToken).toBeNull();
|
||||
});
|
||||
|
||||
it('should reject reset with invalid token', async () => {
|
||||
const response = await request(app)
|
||||
.post('/auth/reset-password')
|
||||
.send({ token: 'invalid-token', newPassword: 'NewPassword123!' })
|
||||
.expect(400);
|
||||
|
||||
// Response should contain error (format may vary based on validation)
|
||||
expect(response.body.error || response.body.errors).toBeDefined();
|
||||
});
|
||||
|
||||
it('should increment JWT version after password reset', async () => {
|
||||
const oldJwtVersion = testUser.jwtVersion;
|
||||
|
||||
await request(app)
|
||||
.post('/auth/reset-password')
|
||||
.send({ token: resetToken, newPassword: 'NewPassword123!' })
|
||||
.expect(200);
|
||||
|
||||
await testUser.reload();
|
||||
expect(testUser.jwtVersion).toBe(oldJwtVersion + 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CSRF Token', () => {
|
||||
it('should return CSRF token', async () => {
|
||||
const response = await request(app)
|
||||
.get('/auth/csrf-token')
|
||||
.expect(200);
|
||||
|
||||
expect(response.headers['x-csrf-token']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Alpha Testing Mode', () => {
|
||||
beforeEach(() => {
|
||||
process.env.ALPHA_TESTING_ENABLED = 'true';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.ALPHA_TESTING_ENABLED = 'false';
|
||||
});
|
||||
|
||||
it('should reject registration without alpha code when enabled', async () => {
|
||||
const response = await request(app)
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
email: 'alpha@example.com',
|
||||
password: 'SecurePassword123!',
|
||||
firstName: 'Alpha',
|
||||
lastName: 'User',
|
||||
})
|
||||
.expect(403);
|
||||
|
||||
expect(response.body.error).toContain('Alpha access required');
|
||||
});
|
||||
|
||||
it('should allow registration with valid alpha code', async () => {
|
||||
const validCode = 'ALPHA-TEST1234';
|
||||
const invitation = await createAlphaInvitation({
|
||||
code: validCode,
|
||||
email: 'invited@example.com', // Required field
|
||||
});
|
||||
|
||||
// Cookie-parser parses JSON cookies that start with 'j:'
|
||||
const cookieValue = `j:${JSON.stringify({ code: validCode })}`;
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/register')
|
||||
.set('Cookie', [`alphaAccessCode=${cookieValue}`])
|
||||
.send({
|
||||
email: 'alphauser@example.com',
|
||||
password: 'SecurePassword123!',
|
||||
firstName: 'Alpha',
|
||||
lastName: 'User',
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
expect(response.body.user.email).toBe('alphauser@example.com');
|
||||
|
||||
// Verify invitation was linked
|
||||
await invitation.reload();
|
||||
expect(invitation.usedBy).toBeDefined();
|
||||
expect(invitation.status).toBe('active');
|
||||
});
|
||||
});
|
||||
});
|
||||
585
backend/tests/integration/rental.integration.test.js
Normal file
585
backend/tests/integration/rental.integration.test.js
Normal file
@@ -0,0 +1,585 @@
|
||||
/**
|
||||
* Rental Integration Tests
|
||||
*
|
||||
* These tests use a real database connection to verify the complete
|
||||
* rental lifecycle including creation, approval, completion, and
|
||||
* cancellation flows.
|
||||
*/
|
||||
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { sequelize, User, Item, Rental } = require('../../models');
|
||||
const rentalRoutes = require('../../routes/rentals');
|
||||
|
||||
// Test app setup
|
||||
const createTestApp = () => {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
|
||||
// Add request ID middleware
|
||||
app.use((req, res, next) => {
|
||||
req.id = 'test-request-id';
|
||||
next();
|
||||
});
|
||||
|
||||
app.use('/rentals', rentalRoutes);
|
||||
return app;
|
||||
};
|
||||
|
||||
// Generate auth token for user
|
||||
const generateAuthToken = (user) => {
|
||||
return jwt.sign(
|
||||
{ id: user.id, jwtVersion: user.jwtVersion || 0 },
|
||||
process.env.JWT_ACCESS_SECRET,
|
||||
{ expiresIn: '15m' }
|
||||
);
|
||||
};
|
||||
|
||||
// Test data factories
|
||||
const createTestUser = async (overrides = {}) => {
|
||||
const defaultData = {
|
||||
email: `user-${Date.now()}-${Math.random().toString(36).slice(2)}@example.com`,
|
||||
password: 'TestPassword123!',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
isVerified: true,
|
||||
authProvider: 'local',
|
||||
};
|
||||
|
||||
return User.create({ ...defaultData, ...overrides });
|
||||
};
|
||||
|
||||
const createTestItem = async (ownerId, overrides = {}) => {
|
||||
const defaultData = {
|
||||
name: 'Test Item',
|
||||
description: 'A test item for rental',
|
||||
pricePerDay: 25.00,
|
||||
pricePerHour: 5.00,
|
||||
replacementCost: 500.00,
|
||||
condition: 'excellent',
|
||||
isAvailable: true,
|
||||
pickUpAvailable: true,
|
||||
ownerId,
|
||||
city: 'Test City',
|
||||
state: 'California',
|
||||
};
|
||||
|
||||
return Item.create({ ...defaultData, ...overrides });
|
||||
};
|
||||
|
||||
const createTestRental = async (itemId, renterId, ownerId, overrides = {}) => {
|
||||
const now = new Date();
|
||||
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
const defaultData = {
|
||||
itemId,
|
||||
renterId,
|
||||
ownerId,
|
||||
startDateTime: tomorrow,
|
||||
endDateTime: new Date(tomorrow.getTime() + 24 * 60 * 60 * 1000),
|
||||
// Use free rentals to avoid Stripe payment requirements in tests
|
||||
totalAmount: 0,
|
||||
platformFee: 0,
|
||||
payoutAmount: 0,
|
||||
status: 'pending',
|
||||
paymentStatus: 'pending',
|
||||
deliveryMethod: 'pickup',
|
||||
};
|
||||
|
||||
return Rental.create({ ...defaultData, ...overrides });
|
||||
};
|
||||
|
||||
describe('Rental Integration Tests', () => {
|
||||
let app;
|
||||
let owner;
|
||||
let renter;
|
||||
let item;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Set test environment variables
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.JWT_ACCESS_SECRET = 'test-access-secret';
|
||||
process.env.JWT_REFRESH_SECRET = 'test-refresh-secret';
|
||||
|
||||
// Sync database
|
||||
await sequelize.sync({ force: true });
|
||||
|
||||
app = createTestApp();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await sequelize.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean up in correct order (respecting foreign key constraints)
|
||||
await Rental.destroy({ where: {}, truncate: true, cascade: true });
|
||||
await Item.destroy({ where: {}, truncate: true, cascade: true });
|
||||
await User.destroy({ where: {}, truncate: true, cascade: true });
|
||||
|
||||
// Create test users
|
||||
owner = await createTestUser({
|
||||
email: 'owner@example.com',
|
||||
firstName: 'Item',
|
||||
lastName: 'Owner',
|
||||
stripeConnectedAccountId: 'acct_test_owner',
|
||||
});
|
||||
|
||||
renter = await createTestUser({
|
||||
email: 'renter@example.com',
|
||||
firstName: 'Item',
|
||||
lastName: 'Renter',
|
||||
});
|
||||
|
||||
// Create test item
|
||||
item = await createTestItem(owner.id);
|
||||
});
|
||||
|
||||
describe('GET /rentals/renting', () => {
|
||||
it('should return rentals where user is the renter', async () => {
|
||||
// Create a rental where renter is the renter
|
||||
await createTestRental(item.id, renter.id, owner.id);
|
||||
|
||||
const token = generateAuthToken(renter);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/rentals/renting')
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBe(1);
|
||||
expect(response.body[0].renterId).toBe(renter.id);
|
||||
});
|
||||
|
||||
it('should return empty array for user with no rentals', async () => {
|
||||
const token = generateAuthToken(renter);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/rentals/renting')
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const response = await request(app)
|
||||
.get('/rentals/renting')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.code).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /rentals/owning', () => {
|
||||
it('should return rentals where user is the owner', async () => {
|
||||
// Create a rental where owner is the item owner
|
||||
await createTestRental(item.id, renter.id, owner.id);
|
||||
|
||||
const token = generateAuthToken(owner);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/rentals/owning')
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBe(1);
|
||||
expect(response.body[0].ownerId).toBe(owner.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /rentals/:id/status', () => {
|
||||
let rental;
|
||||
|
||||
beforeEach(async () => {
|
||||
rental = await createTestRental(item.id, renter.id, owner.id);
|
||||
});
|
||||
|
||||
it('should allow owner to confirm a pending rental', async () => {
|
||||
const token = generateAuthToken(owner);
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/rentals/${rental.id}/status`)
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.send({ status: 'confirmed' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toBe('confirmed');
|
||||
|
||||
// Verify in database
|
||||
await rental.reload();
|
||||
expect(rental.status).toBe('confirmed');
|
||||
});
|
||||
|
||||
it('should allow renter to update status (no owner-only restriction)', async () => {
|
||||
const token = generateAuthToken(renter);
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/rentals/${rental.id}/status`)
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.send({ status: 'confirmed' })
|
||||
.expect(200);
|
||||
|
||||
// Note: API currently allows both owner and renter to update status
|
||||
// Owner-specific logic (payment processing) only runs for owner
|
||||
await rental.reload();
|
||||
expect(rental.status).toBe('confirmed');
|
||||
});
|
||||
|
||||
it('should handle confirming already confirmed rental (idempotent)', async () => {
|
||||
// First confirm it
|
||||
await rental.update({ status: 'confirmed' });
|
||||
|
||||
const token = generateAuthToken(owner);
|
||||
|
||||
// API allows re-confirming (idempotent operation)
|
||||
const response = await request(app)
|
||||
.put(`/rentals/${rental.id}/status`)
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.send({ status: 'confirmed' })
|
||||
.expect(200);
|
||||
|
||||
// Status should remain confirmed
|
||||
await rental.reload();
|
||||
expect(rental.status).toBe('confirmed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /rentals/:id/decline', () => {
|
||||
let rental;
|
||||
|
||||
beforeEach(async () => {
|
||||
rental = await createTestRental(item.id, renter.id, owner.id);
|
||||
});
|
||||
|
||||
it('should allow owner to decline a pending rental', async () => {
|
||||
const token = generateAuthToken(owner);
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/rentals/${rental.id}/decline`)
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.send({ reason: 'Item not available for those dates' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toBe('declined');
|
||||
|
||||
// Verify in database
|
||||
await rental.reload();
|
||||
expect(rental.status).toBe('declined');
|
||||
expect(rental.declineReason).toBe('Item not available for those dates');
|
||||
});
|
||||
|
||||
it('should not allow declining already declined rental', async () => {
|
||||
await rental.update({ status: 'declined' });
|
||||
|
||||
const token = generateAuthToken(owner);
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/rentals/${rental.id}/decline`)
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.send({ reason: 'Already declined' })
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /rentals/:id/cancel', () => {
|
||||
let rental;
|
||||
|
||||
beforeEach(async () => {
|
||||
rental = await createTestRental(item.id, renter.id, owner.id, {
|
||||
status: 'confirmed',
|
||||
paymentStatus: 'paid',
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow renter to cancel their rental', async () => {
|
||||
const token = generateAuthToken(renter);
|
||||
|
||||
const response = await request(app)
|
||||
.post(`/rentals/${rental.id}/cancel`)
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.send({ reason: 'Change of plans' })
|
||||
.expect(200);
|
||||
|
||||
// Response format is { rental: {...}, refund: {...} }
|
||||
expect(response.body.rental.status).toBe('cancelled');
|
||||
expect(response.body.rental.cancelledBy).toBe('renter');
|
||||
|
||||
// Verify in database
|
||||
await rental.reload();
|
||||
expect(rental.status).toBe('cancelled');
|
||||
expect(rental.cancelledBy).toBe('renter');
|
||||
expect(rental.cancelledAt).toBeDefined();
|
||||
});
|
||||
|
||||
it('should allow owner to cancel their rental', async () => {
|
||||
const token = generateAuthToken(owner);
|
||||
|
||||
const response = await request(app)
|
||||
.post(`/rentals/${rental.id}/cancel`)
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.send({ reason: 'Item broken' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.rental.status).toBe('cancelled');
|
||||
expect(response.body.rental.cancelledBy).toBe('owner');
|
||||
});
|
||||
|
||||
it('should not allow cancelling completed rental', async () => {
|
||||
await rental.update({ status: 'completed', paymentStatus: 'paid' });
|
||||
|
||||
const token = generateAuthToken(renter);
|
||||
|
||||
// RefundService throws error which becomes 500 via next(error)
|
||||
const response = await request(app)
|
||||
.post(`/rentals/${rental.id}/cancel`)
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.send({ reason: 'Too late' });
|
||||
|
||||
// Expect error (could be 400 or 500 depending on error middleware)
|
||||
expect(response.status).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
|
||||
it('should not allow unauthorized user to cancel rental', async () => {
|
||||
const otherUser = await createTestUser({ email: 'other@example.com' });
|
||||
const token = generateAuthToken(otherUser);
|
||||
|
||||
const response = await request(app)
|
||||
.post(`/rentals/${rental.id}/cancel`)
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.send({ reason: 'Not my rental' });
|
||||
|
||||
// Expect error (could be 403 or 500 depending on error middleware)
|
||||
expect(response.status).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /rentals/pending-requests-count', () => {
|
||||
it('should return count of pending rental requests for owner', async () => {
|
||||
// Create multiple pending rentals
|
||||
await createTestRental(item.id, renter.id, owner.id, { status: 'pending' });
|
||||
await createTestRental(item.id, renter.id, owner.id, { status: 'pending' });
|
||||
await createTestRental(item.id, renter.id, owner.id, { status: 'confirmed' });
|
||||
|
||||
const token = generateAuthToken(owner);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/rentals/pending-requests-count')
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.count).toBe(2);
|
||||
});
|
||||
|
||||
it('should return 0 for user with no pending requests', async () => {
|
||||
const token = generateAuthToken(renter);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/rentals/pending-requests-count')
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rental Lifecycle', () => {
|
||||
it('should complete full rental lifecycle: pending -> confirmed -> active -> completed', async () => {
|
||||
// Create pending free rental (totalAmount: 0 is default)
|
||||
const rental = await createTestRental(item.id, renter.id, owner.id, {
|
||||
status: 'pending',
|
||||
startDateTime: new Date(Date.now() - 60 * 60 * 1000), // Started 1 hour ago
|
||||
endDateTime: new Date(Date.now() + 60 * 60 * 1000), // Ends in 1 hour
|
||||
});
|
||||
|
||||
const ownerToken = generateAuthToken(owner);
|
||||
|
||||
// Step 1: Owner confirms rental (works for free rentals)
|
||||
let response = await request(app)
|
||||
.put(`/rentals/${rental.id}/status`)
|
||||
.set('Cookie', [`accessToken=${ownerToken}`])
|
||||
.send({ status: 'confirmed' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toBe('confirmed');
|
||||
|
||||
// Step 2: Rental becomes active (typically done by system/webhook)
|
||||
await rental.update({ status: 'active' });
|
||||
|
||||
// Verify status
|
||||
await rental.reload();
|
||||
expect(rental.status).toBe('active');
|
||||
|
||||
// Step 3: Owner marks rental as completed
|
||||
response = await request(app)
|
||||
.post(`/rentals/${rental.id}/mark-completed`)
|
||||
.set('Cookie', [`accessToken=${ownerToken}`])
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toBe('completed');
|
||||
|
||||
// Verify final state
|
||||
await rental.reload();
|
||||
expect(rental.status).toBe('completed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Review System', () => {
|
||||
let completedRental;
|
||||
|
||||
beforeEach(async () => {
|
||||
completedRental = await createTestRental(item.id, renter.id, owner.id, {
|
||||
status: 'completed',
|
||||
paymentStatus: 'paid',
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow renter to review item', async () => {
|
||||
const token = generateAuthToken(renter);
|
||||
|
||||
const response = await request(app)
|
||||
.post(`/rentals/${completedRental.id}/review-item`)
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.send({
|
||||
rating: 5,
|
||||
review: 'Great item, worked perfectly!',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
|
||||
// Verify in database
|
||||
await completedRental.reload();
|
||||
expect(completedRental.itemRating).toBe(5);
|
||||
expect(completedRental.itemReview).toBe('Great item, worked perfectly!');
|
||||
expect(completedRental.itemReviewSubmittedAt).toBeDefined();
|
||||
});
|
||||
|
||||
it('should allow owner to review renter', async () => {
|
||||
const token = generateAuthToken(owner);
|
||||
|
||||
const response = await request(app)
|
||||
.post(`/rentals/${completedRental.id}/review-renter`)
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.send({
|
||||
rating: 4,
|
||||
review: 'Good renter, returned on time.',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
|
||||
// Verify in database
|
||||
await completedRental.reload();
|
||||
expect(completedRental.renterRating).toBe(4);
|
||||
expect(completedRental.renterReview).toBe('Good renter, returned on time.');
|
||||
});
|
||||
|
||||
it('should not allow review of non-completed rental', async () => {
|
||||
const pendingRental = await createTestRental(item.id, renter.id, owner.id, {
|
||||
status: 'pending',
|
||||
});
|
||||
|
||||
const token = generateAuthToken(renter);
|
||||
|
||||
const response = await request(app)
|
||||
.post(`/rentals/${pendingRental.id}/review-item`)
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.send({
|
||||
rating: 5,
|
||||
review: 'Cannot review yet',
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not allow duplicate reviews', async () => {
|
||||
// First review
|
||||
await completedRental.update({
|
||||
itemRating: 5,
|
||||
itemReview: 'First review',
|
||||
itemReviewSubmittedAt: new Date(),
|
||||
});
|
||||
|
||||
const token = generateAuthToken(renter);
|
||||
|
||||
const response = await request(app)
|
||||
.post(`/rentals/${completedRental.id}/review-item`)
|
||||
.set('Cookie', [`accessToken=${token}`])
|
||||
.send({
|
||||
rating: 3,
|
||||
review: 'Second review attempt',
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error).toContain('already');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Database Constraints', () => {
|
||||
it('should not allow rental with invalid item ID', async () => {
|
||||
await expect(
|
||||
createTestRental('00000000-0000-0000-0000-000000000000', renter.id, owner.id)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should not allow rental with invalid user IDs', async () => {
|
||||
await expect(
|
||||
createTestRental(item.id, '00000000-0000-0000-0000-000000000000', owner.id)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should cascade delete rentals when item is deleted', async () => {
|
||||
const rental = await createTestRental(item.id, renter.id, owner.id);
|
||||
|
||||
// Delete the item
|
||||
await item.destroy();
|
||||
|
||||
// Rental should also be deleted (due to foreign key constraint)
|
||||
const deletedRental = await Rental.findByPk(rental.id);
|
||||
expect(deletedRental).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Concurrent Operations', () => {
|
||||
it('should handle concurrent status updates (last write wins)', async () => {
|
||||
const rental = await createTestRental(item.id, renter.id, owner.id, {
|
||||
status: 'pending',
|
||||
});
|
||||
|
||||
const ownerToken = generateAuthToken(owner);
|
||||
|
||||
// Simulate concurrent confirm and decline requests
|
||||
const [confirmResult, declineResult] = await Promise.allSettled([
|
||||
request(app)
|
||||
.put(`/rentals/${rental.id}/status`)
|
||||
.set('Cookie', [`accessToken=${ownerToken}`])
|
||||
.send({ status: 'confirmed' }),
|
||||
request(app)
|
||||
.put(`/rentals/${rental.id}/decline`)
|
||||
.set('Cookie', [`accessToken=${ownerToken}`])
|
||||
.send({ reason: 'Declining instead' }),
|
||||
]);
|
||||
|
||||
// Both requests may succeed (no optimistic locking)
|
||||
// Verify rental ends up in a valid state
|
||||
await rental.reload();
|
||||
expect(['confirmed', 'declined']).toContain(rental.status);
|
||||
|
||||
// At least one should have succeeded
|
||||
const successes = [confirmResult, declineResult].filter(
|
||||
r => r.status === 'fulfilled' && r.value.status === 200
|
||||
);
|
||||
expect(successes.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -319,7 +319,7 @@ describe('Auth Routes', () => {
|
||||
email: 'test@gmail.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
profileImage: 'profile.jpg'
|
||||
imageFilename: 'profile.jpg'
|
||||
};
|
||||
|
||||
User.create.mockResolvedValue(newUser);
|
||||
@@ -338,7 +338,7 @@ describe('Auth Routes', () => {
|
||||
lastName: 'User',
|
||||
authProvider: 'google',
|
||||
providerId: 'google123',
|
||||
profileImage: 'profile.jpg',
|
||||
imageFilename: 'profile.jpg',
|
||||
username: 'test_gle123'
|
||||
});
|
||||
});
|
||||
@@ -785,7 +785,7 @@ describe('Auth Routes', () => {
|
||||
email: 'oauth@gmail.com',
|
||||
firstName: 'OAuth',
|
||||
lastName: 'User',
|
||||
profileImage: 'pic.jpg',
|
||||
imageFilename: 'pic.jpg',
|
||||
isVerified: true
|
||||
};
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ describe('Items Routes', () => {
|
||||
{
|
||||
model: mockUserModel,
|
||||
as: 'owner',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
attributes: ['id', 'firstName', 'lastName']
|
||||
}
|
||||
],
|
||||
limit: 20,
|
||||
@@ -608,7 +608,7 @@ describe('Items Routes', () => {
|
||||
{
|
||||
model: mockUserModel,
|
||||
as: 'owner',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
attributes: ['id', 'firstName', 'lastName']
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -679,7 +679,7 @@ describe('Items Routes', () => {
|
||||
{
|
||||
model: mockUserModel,
|
||||
as: 'owner',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
attributes: ['id', 'firstName', 'lastName']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('Messages Routes', () => {
|
||||
id: 2,
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
profileImage: 'jane.jpg'
|
||||
imageFilename: 'jane.jpg'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -77,7 +77,7 @@ describe('Messages Routes', () => {
|
||||
id: 3,
|
||||
firstName: 'Bob',
|
||||
lastName: 'Johnson',
|
||||
profileImage: null
|
||||
imageFilename: null
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -95,7 +95,7 @@ describe('Messages Routes', () => {
|
||||
{
|
||||
model: User,
|
||||
as: 'sender',
|
||||
attributes: ['id', 'firstName', 'lastName', 'profileImage']
|
||||
attributes: ['id', 'firstName', 'lastName', 'imageFilename']
|
||||
}
|
||||
],
|
||||
order: [['createdAt', 'DESC']]
|
||||
@@ -127,7 +127,7 @@ describe('Messages Routes', () => {
|
||||
id: 2,
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
profileImage: 'jane.jpg'
|
||||
imageFilename: 'jane.jpg'
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -145,7 +145,7 @@ describe('Messages Routes', () => {
|
||||
{
|
||||
model: User,
|
||||
as: 'receiver',
|
||||
attributes: ['id', 'firstName', 'lastName', 'profileImage']
|
||||
attributes: ['id', 'firstName', 'lastName', 'imageFilename']
|
||||
}
|
||||
],
|
||||
order: [['createdAt', 'DESC']]
|
||||
@@ -175,13 +175,13 @@ describe('Messages Routes', () => {
|
||||
id: 2,
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
profileImage: 'jane.jpg'
|
||||
imageFilename: 'jane.jpg'
|
||||
},
|
||||
receiver: {
|
||||
id: 1,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
profileImage: 'john.jpg'
|
||||
imageFilename: 'john.jpg'
|
||||
},
|
||||
update: jest.fn()
|
||||
};
|
||||
@@ -208,13 +208,13 @@ describe('Messages Routes', () => {
|
||||
id: 2,
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
profileImage: 'jane.jpg'
|
||||
imageFilename: 'jane.jpg'
|
||||
},
|
||||
receiver: {
|
||||
id: 1,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
profileImage: 'john.jpg'
|
||||
imageFilename: 'john.jpg'
|
||||
}
|
||||
});
|
||||
expect(mockMessage.update).toHaveBeenCalledWith({ isRead: true });
|
||||
@@ -239,13 +239,13 @@ describe('Messages Routes', () => {
|
||||
id: 2,
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
profileImage: 'jane.jpg'
|
||||
imageFilename: 'jane.jpg'
|
||||
},
|
||||
receiver: {
|
||||
id: 1,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
profileImage: 'john.jpg'
|
||||
imageFilename: 'john.jpg'
|
||||
}
|
||||
});
|
||||
expect(mockMessage.update).not.toHaveBeenCalled();
|
||||
@@ -304,7 +304,7 @@ describe('Messages Routes', () => {
|
||||
id: 1,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
profileImage: 'john.jpg'
|
||||
imageFilename: 'john.jpg'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -330,7 +330,7 @@ describe('Messages Routes', () => {
|
||||
senderId: 1,
|
||||
receiverId: 2,
|
||||
content: 'Hello Jane!',
|
||||
imagePath: null
|
||||
imageFilename: null
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ describe('Rentals Routes', () => {
|
||||
{
|
||||
model: User,
|
||||
as: 'owner',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName'],
|
||||
attributes: ['id', 'firstName', 'lastName'],
|
||||
},
|
||||
],
|
||||
order: [['createdAt', 'DESC']],
|
||||
@@ -174,7 +174,7 @@ describe('Rentals Routes', () => {
|
||||
{
|
||||
model: User,
|
||||
as: 'renter',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName'],
|
||||
attributes: ['id', 'firstName', 'lastName'],
|
||||
},
|
||||
],
|
||||
order: [['createdAt', 'DESC']],
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('Users Routes', () => {
|
||||
lastName: 'Doe',
|
||||
email: 'john@example.com',
|
||||
phone: '555-1234',
|
||||
profileImage: 'profile.jpg',
|
||||
imageFilename: 'profile.jpg',
|
||||
};
|
||||
|
||||
mockUserFindByPk.mockResolvedValue(mockUser);
|
||||
@@ -397,7 +397,7 @@ describe('Users Routes', () => {
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
username: 'janesmith',
|
||||
profileImage: 'jane.jpg',
|
||||
imageFilename: 'jane.jpg',
|
||||
};
|
||||
|
||||
mockUserFindByPk.mockResolvedValue(mockUser);
|
||||
@@ -536,7 +536,7 @@ describe('Users Routes', () => {
|
||||
describe('POST /profile/image', () => {
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
profileImage: 'old-image.jpg',
|
||||
imageFilename: 'old-image.jpg',
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
@@ -559,7 +559,7 @@ describe('Users Routes', () => {
|
||||
});
|
||||
expect(fs.unlink).toHaveBeenCalled(); // Old image deleted
|
||||
expect(mockUser.update).toHaveBeenCalledWith({
|
||||
profileImage: 'test-profile.jpg'
|
||||
imageFilename: 'test-profile.jpg'
|
||||
});
|
||||
});
|
||||
|
||||
@@ -617,7 +617,7 @@ describe('Users Routes', () => {
|
||||
|
||||
const userWithoutImage = {
|
||||
id: 1,
|
||||
profileImage: null,
|
||||
imageFilename: null,
|
||||
update: jest.fn().mockResolvedValue()
|
||||
};
|
||||
mockUserFindByPk.mockResolvedValue(userWithoutImage);
|
||||
@@ -638,7 +638,7 @@ describe('Users Routes', () => {
|
||||
|
||||
const userWithImage = {
|
||||
id: 1,
|
||||
profileImage: 'old-image.jpg',
|
||||
imageFilename: 'old-image.jpg',
|
||||
update: jest.fn().mockResolvedValue()
|
||||
};
|
||||
mockUserFindByPk.mockResolvedValue(userWithImage);
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('ConditionCheckService', () => {
|
||||
include: [{
|
||||
model: User,
|
||||
as: 'submittedByUser',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
attributes: ['id', 'firstName', 'lastName']
|
||||
}],
|
||||
order: [['submittedAt', 'ASC']]
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user