2025-09-05 17:26:02 -04:00
|
|
|
import mongoose from 'mongoose';
|
|
|
|
|
import { MongoMemoryServer } from 'mongodb-memory-server';
|
|
|
|
|
import type * as t from '~/types';
|
|
|
|
|
import { createTokenMethods } from './token';
|
|
|
|
|
import tokenSchema from '~/schema/token';
|
|
|
|
|
|
|
|
|
|
/** Mocking logger */
|
|
|
|
|
jest.mock('~/config/winston', () => ({
|
|
|
|
|
error: jest.fn(),
|
|
|
|
|
info: jest.fn(),
|
|
|
|
|
debug: jest.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
let mongoServer: MongoMemoryServer;
|
|
|
|
|
let Token: mongoose.Model<t.IToken>;
|
|
|
|
|
let methods: ReturnType<typeof createTokenMethods>;
|
|
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
|
|
mongoServer = await MongoMemoryServer.create();
|
|
|
|
|
const mongoUri = mongoServer.getUri();
|
|
|
|
|
await mongoose.connect(mongoUri);
|
|
|
|
|
|
|
|
|
|
/** Register models */
|
|
|
|
|
Token = mongoose.models.Token || mongoose.model<t.IToken>('Token', tokenSchema);
|
|
|
|
|
|
|
|
|
|
/** Initialize methods */
|
|
|
|
|
methods = createTokenMethods(mongoose);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
|
await mongoose.disconnect();
|
|
|
|
|
await mongoServer.stop();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
await mongoose.connection.dropDatabase();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Token Methods - Detailed Tests', () => {
|
|
|
|
|
describe('createToken', () => {
|
|
|
|
|
test('should create a token with correct expiry time', async () => {
|
|
|
|
|
const userId = new mongoose.Types.ObjectId();
|
|
|
|
|
const tokenData = {
|
|
|
|
|
token: 'test-token-123',
|
|
|
|
|
userId: userId,
|
|
|
|
|
email: 'test@example.com',
|
|
|
|
|
expiresIn: 3600, // 1 hour
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const token = await methods.createToken(tokenData);
|
|
|
|
|
|
|
|
|
|
expect(token).toBeDefined();
|
|
|
|
|
expect(token.token).toBe(tokenData.token);
|
|
|
|
|
expect(token.userId.toString()).toBe(userId.toString());
|
|
|
|
|
expect(token.email).toBe(tokenData.email);
|
|
|
|
|
|
|
|
|
|
// Check expiry time
|
|
|
|
|
const expectedExpiry = new Date(token.createdAt.getTime() + tokenData.expiresIn * 1000);
|
|
|
|
|
expect(token.expiresAt.getTime()).toBe(expectedExpiry.getTime());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should create token with all required fields', async () => {
|
|
|
|
|
const userId = new mongoose.Types.ObjectId();
|
|
|
|
|
const tokenData = {
|
|
|
|
|
token: 'minimal-token',
|
|
|
|
|
userId: userId,
|
|
|
|
|
expiresIn: 1800,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const token = await methods.createToken(tokenData);
|
|
|
|
|
|
|
|
|
|
expect(token).toBeDefined();
|
|
|
|
|
expect(token.token).toBe(tokenData.token);
|
|
|
|
|
expect(token.userId.toString()).toBe(userId.toString());
|
|
|
|
|
expect(token.email).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should create token with identifier field', async () => {
|
|
|
|
|
const userId = new mongoose.Types.ObjectId();
|
|
|
|
|
const tokenData = {
|
|
|
|
|
token: 'identifier-token',
|
|
|
|
|
userId: userId,
|
|
|
|
|
identifier: 'oauth-identifier-123',
|
|
|
|
|
expiresIn: 7200,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const token = await methods.createToken(tokenData);
|
|
|
|
|
|
|
|
|
|
expect(token).toBeDefined();
|
|
|
|
|
expect(token.identifier).toBe(tokenData.identifier);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('findToken', () => {
|
|
|
|
|
let user1Id: mongoose.Types.ObjectId;
|
|
|
|
|
let user2Id: mongoose.Types.ObjectId;
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
user1Id = new mongoose.Types.ObjectId();
|
|
|
|
|
user2Id = new mongoose.Types.ObjectId();
|
|
|
|
|
|
|
|
|
|
await Token.create([
|
|
|
|
|
{
|
|
|
|
|
token: 'token-1',
|
|
|
|
|
userId: user1Id,
|
|
|
|
|
email: 'user1@example.com',
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
token: 'token-2',
|
|
|
|
|
userId: user2Id,
|
|
|
|
|
email: 'user2@example.com',
|
|
|
|
|
identifier: 'oauth-123',
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
token: 'token-3',
|
|
|
|
|
userId: user1Id,
|
2025-09-10 20:40:58 -04:00
|
|
|
email: 'user1-alt@example.com', // Different email for realistic scenario
|
2025-09-05 17:26:02 -04:00
|
|
|
createdAt: new Date(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should find token by token value', async () => {
|
|
|
|
|
const found = await methods.findToken({ token: 'token-1' });
|
|
|
|
|
|
|
|
|
|
expect(found).toBeDefined();
|
|
|
|
|
expect(found?.token).toBe('token-1');
|
|
|
|
|
expect(found?.userId.toString()).toBe(user1Id.toString());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should find token by userId', async () => {
|
|
|
|
|
const found = await methods.findToken({ userId: user2Id.toString() });
|
|
|
|
|
|
|
|
|
|
expect(found).toBeDefined();
|
|
|
|
|
expect(found?.token).toBe('token-2');
|
|
|
|
|
expect(found?.email).toBe('user2@example.com');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should find token by email', async () => {
|
|
|
|
|
const found = await methods.findToken({ email: 'user2@example.com' });
|
|
|
|
|
|
|
|
|
|
expect(found).toBeDefined();
|
|
|
|
|
expect(found?.token).toBe('token-2');
|
|
|
|
|
expect(found?.userId.toString()).toBe(user2Id.toString());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should find token by identifier', async () => {
|
|
|
|
|
const found = await methods.findToken({ identifier: 'oauth-123' });
|
|
|
|
|
|
|
|
|
|
expect(found).toBeDefined();
|
|
|
|
|
expect(found?.token).toBe('token-2');
|
|
|
|
|
expect(found?.identifier).toBe('oauth-123');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should find token by multiple criteria (AND condition)', async () => {
|
|
|
|
|
const found = await methods.findToken({
|
|
|
|
|
userId: user1Id.toString(),
|
|
|
|
|
email: 'user1@example.com',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(found).toBeDefined();
|
2025-09-10 20:40:58 -04:00
|
|
|
expect(found?.token).toBe('token-1'); // Should find the only token matching both criteria
|
2025-09-05 17:26:02 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should return null for non-existent token', async () => {
|
|
|
|
|
const found = await methods.findToken({ token: 'non-existent' });
|
|
|
|
|
|
|
|
|
|
expect(found).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should return null when criteria do not match together', async () => {
|
|
|
|
|
const found = await methods.findToken({
|
|
|
|
|
userId: user1Id.toString(),
|
|
|
|
|
email: 'user2@example.com', // Mismatched email
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(found).toBeNull();
|
|
|
|
|
});
|
2025-09-29 09:06:40 -04:00
|
|
|
|
|
|
|
|
test('should find most recent token with sort option', async () => {
|
|
|
|
|
const recentUserId = new mongoose.Types.ObjectId();
|
|
|
|
|
|
|
|
|
|
// Create tokens with different timestamps
|
|
|
|
|
const oldDate = new Date(Date.now() - 7200000); // 2 hours ago
|
|
|
|
|
const midDate = new Date(Date.now() - 3600000); // 1 hour ago
|
|
|
|
|
const newDate = new Date(); // now
|
|
|
|
|
|
|
|
|
|
await Token.create([
|
|
|
|
|
{
|
|
|
|
|
token: 'old-token',
|
|
|
|
|
userId: recentUserId,
|
|
|
|
|
email: 'recent@example.com',
|
|
|
|
|
createdAt: oldDate,
|
|
|
|
|
expiresAt: new Date(oldDate.getTime() + 86400000),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
token: 'mid-token',
|
|
|
|
|
userId: recentUserId,
|
|
|
|
|
email: 'recent@example.com',
|
|
|
|
|
createdAt: midDate,
|
|
|
|
|
expiresAt: new Date(midDate.getTime() + 86400000),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
token: 'new-token',
|
|
|
|
|
userId: recentUserId,
|
|
|
|
|
email: 'recent@example.com',
|
|
|
|
|
createdAt: newDate,
|
|
|
|
|
expiresAt: new Date(newDate.getTime() + 86400000),
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Find most recent token for the user with sort option
|
|
|
|
|
const found = await methods.findToken(
|
|
|
|
|
{ userId: recentUserId.toString() },
|
|
|
|
|
{ sort: { createdAt: -1 } },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(found).toBeDefined();
|
|
|
|
|
expect(found?.token).toBe('new-token');
|
|
|
|
|
expect(found?.createdAt.getTime()).toBe(newDate.getTime());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should find oldest token with ascending sort', async () => {
|
|
|
|
|
const sortUserId = new mongoose.Types.ObjectId();
|
|
|
|
|
|
|
|
|
|
const oldDate = new Date(Date.now() - 7200000);
|
|
|
|
|
const midDate = new Date(Date.now() - 3600000);
|
|
|
|
|
const newDate = new Date();
|
|
|
|
|
|
|
|
|
|
await Token.create([
|
|
|
|
|
{
|
|
|
|
|
token: 'sort-old',
|
|
|
|
|
userId: sortUserId,
|
|
|
|
|
email: 'sort@example.com',
|
|
|
|
|
createdAt: oldDate,
|
|
|
|
|
expiresAt: new Date(oldDate.getTime() + 86400000),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
token: 'sort-mid',
|
|
|
|
|
userId: sortUserId,
|
|
|
|
|
email: 'sort@example.com',
|
|
|
|
|
createdAt: midDate,
|
|
|
|
|
expiresAt: new Date(midDate.getTime() + 86400000),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
token: 'sort-new',
|
|
|
|
|
userId: sortUserId,
|
|
|
|
|
email: 'sort@example.com',
|
|
|
|
|
createdAt: newDate,
|
|
|
|
|
expiresAt: new Date(newDate.getTime() + 86400000),
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Find oldest token with ascending sort
|
|
|
|
|
const found = await methods.findToken(
|
|
|
|
|
{ userId: sortUserId.toString() },
|
|
|
|
|
{ sort: { createdAt: 1 } },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(found).toBeDefined();
|
|
|
|
|
expect(found?.token).toBe('sort-old');
|
|
|
|
|
expect(found?.createdAt.getTime()).toBe(oldDate.getTime());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should handle multiple sort criteria', async () => {
|
|
|
|
|
const multiSortUserId = new mongoose.Types.ObjectId();
|
|
|
|
|
const sameDate = new Date();
|
|
|
|
|
|
|
|
|
|
await Token.create([
|
|
|
|
|
{
|
|
|
|
|
token: 'token-a',
|
|
|
|
|
userId: multiSortUserId,
|
|
|
|
|
email: 'z@example.com',
|
|
|
|
|
createdAt: sameDate,
|
|
|
|
|
expiresAt: new Date(sameDate.getTime() + 86400000),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
token: 'token-b',
|
|
|
|
|
userId: multiSortUserId,
|
|
|
|
|
email: 'a@example.com',
|
|
|
|
|
createdAt: sameDate,
|
|
|
|
|
expiresAt: new Date(sameDate.getTime() + 86400000),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
token: 'token-c',
|
|
|
|
|
userId: multiSortUserId,
|
|
|
|
|
email: 'm@example.com',
|
|
|
|
|
createdAt: new Date(Date.now() - 1000), // slightly older
|
|
|
|
|
expiresAt: new Date(Date.now() + 86400000),
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Sort by createdAt descending, then by email ascending
|
|
|
|
|
const found = await methods.findToken(
|
|
|
|
|
{ userId: multiSortUserId.toString() },
|
|
|
|
|
{ sort: { createdAt: -1, email: 1 } },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(found).toBeDefined();
|
|
|
|
|
// Should get token-b (same recent date but 'a@example.com' comes first alphabetically)
|
|
|
|
|
expect(found?.token).toBe('token-b');
|
|
|
|
|
expect(found?.email).toBe('a@example.com');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should find token with projection option', async () => {
|
|
|
|
|
const projectionUserId = new mongoose.Types.ObjectId();
|
|
|
|
|
|
|
|
|
|
await Token.create({
|
|
|
|
|
token: 'projection-token',
|
|
|
|
|
userId: projectionUserId,
|
|
|
|
|
email: 'projection@example.com',
|
|
|
|
|
identifier: 'oauth-projection',
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 86400000),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Find token with projection to only include specific fields
|
|
|
|
|
const found = await methods.findToken(
|
|
|
|
|
{ userId: projectionUserId.toString() },
|
|
|
|
|
{ projection: { token: 1, email: 1 } },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(found).toBeDefined();
|
|
|
|
|
expect(found?.token).toBe('projection-token');
|
|
|
|
|
expect(found?.email).toBe('projection@example.com');
|
|
|
|
|
// Note: _id is usually included by default unless explicitly excluded
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should respect combined query options', async () => {
|
|
|
|
|
const combinedUserId = new mongoose.Types.ObjectId();
|
|
|
|
|
|
|
|
|
|
// Create multiple tokens with different attributes
|
|
|
|
|
await Token.create([
|
|
|
|
|
{
|
|
|
|
|
token: 'combined-1',
|
|
|
|
|
userId: combinedUserId,
|
|
|
|
|
email: 'combined1@example.com',
|
|
|
|
|
createdAt: new Date(Date.now() - 7200000),
|
|
|
|
|
expiresAt: new Date(Date.now() + 86400000),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
token: 'combined-2',
|
|
|
|
|
userId: combinedUserId,
|
|
|
|
|
email: 'combined2@example.com',
|
|
|
|
|
createdAt: new Date(Date.now() - 3600000),
|
|
|
|
|
expiresAt: new Date(Date.now() + 86400000),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
token: 'combined-3',
|
|
|
|
|
userId: combinedUserId,
|
|
|
|
|
email: 'combined3@example.com',
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 86400000),
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Use multiple query options together
|
|
|
|
|
const found = await methods.findToken(
|
|
|
|
|
{ userId: combinedUserId.toString() },
|
|
|
|
|
{
|
|
|
|
|
sort: { createdAt: -1 },
|
|
|
|
|
projection: { token: 1, createdAt: 1 },
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(found).toBeDefined();
|
|
|
|
|
expect(found?.token).toBe('combined-3'); // Most recent
|
|
|
|
|
expect(found?.createdAt).toBeDefined();
|
|
|
|
|
});
|
2025-09-05 17:26:02 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('updateToken', () => {
|
|
|
|
|
let updateUserId: mongoose.Types.ObjectId;
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
updateUserId = new mongoose.Types.ObjectId();
|
|
|
|
|
await Token.create({
|
|
|
|
|
token: 'update-token',
|
|
|
|
|
userId: updateUserId,
|
|
|
|
|
email: 'update@example.com',
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should update token by token value', async () => {
|
|
|
|
|
const updated = await methods.updateToken(
|
|
|
|
|
{ token: 'update-token' },
|
|
|
|
|
{ email: 'newemail@example.com' },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(updated).toBeDefined();
|
|
|
|
|
expect(updated?.email).toBe('newemail@example.com');
|
|
|
|
|
expect(updated?.userId.toString()).toBe(updateUserId.toString()); // Unchanged
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should update token by userId', async () => {
|
|
|
|
|
const updated = await methods.updateToken(
|
|
|
|
|
{ userId: updateUserId.toString() },
|
|
|
|
|
{ email: 'newemail@example.com' },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(updated).toBeDefined();
|
|
|
|
|
expect(updated?.email).toBe('newemail@example.com');
|
|
|
|
|
expect(updated?.token).toBe('update-token'); // Unchanged
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should return null for non-existent token', async () => {
|
|
|
|
|
const updated = await methods.updateToken(
|
|
|
|
|
{ token: 'non-existent' },
|
|
|
|
|
{ email: 'newemail@example.com' },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(updated).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('deleteTokens', () => {
|
|
|
|
|
let user1Id: mongoose.Types.ObjectId;
|
|
|
|
|
let user2Id: mongoose.Types.ObjectId;
|
|
|
|
|
let user3Id: mongoose.Types.ObjectId;
|
|
|
|
|
let oauthUserId: mongoose.Types.ObjectId;
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
user1Id = new mongoose.Types.ObjectId();
|
|
|
|
|
user2Id = new mongoose.Types.ObjectId();
|
|
|
|
|
user3Id = new mongoose.Types.ObjectId();
|
|
|
|
|
oauthUserId = new mongoose.Types.ObjectId();
|
|
|
|
|
|
|
|
|
|
await Token.create([
|
|
|
|
|
{
|
|
|
|
|
token: 'verify-token-1',
|
|
|
|
|
userId: user1Id,
|
|
|
|
|
email: 'user1@example.com',
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
token: 'verify-token-2',
|
|
|
|
|
userId: user2Id,
|
|
|
|
|
email: 'user2@example.com',
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
token: 'verify-token-3',
|
|
|
|
|
userId: user3Id,
|
|
|
|
|
email: 'user3@example.com',
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
token: 'oauth-token',
|
|
|
|
|
userId: oauthUserId,
|
|
|
|
|
identifier: 'oauth-identifier-456',
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should delete only tokens matching specific token value', async () => {
|
|
|
|
|
const result = await methods.deleteTokens({ token: 'verify-token-1' });
|
|
|
|
|
|
|
|
|
|
expect(result.deletedCount).toBe(1);
|
|
|
|
|
|
|
|
|
|
// Verify other tokens still exist
|
|
|
|
|
const remainingTokens = await Token.find({});
|
|
|
|
|
expect(remainingTokens).toHaveLength(3);
|
|
|
|
|
expect(remainingTokens.find((t) => t.token === 'verify-token-2')).toBeDefined();
|
|
|
|
|
expect(remainingTokens.find((t) => t.token === 'verify-token-3')).toBeDefined();
|
|
|
|
|
expect(remainingTokens.find((t) => t.token === 'oauth-token')).toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should delete only tokens matching specific userId', async () => {
|
|
|
|
|
// Create another token for user-1
|
|
|
|
|
await Token.create({
|
|
|
|
|
token: 'another-user-1-token',
|
|
|
|
|
userId: user1Id,
|
|
|
|
|
email: 'user1@example.com',
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await methods.deleteTokens({ userId: user1Id.toString() });
|
|
|
|
|
|
|
|
|
|
expect(result.deletedCount).toBe(2); // Both tokens for user-1
|
|
|
|
|
|
|
|
|
|
const remainingTokens = await Token.find({});
|
|
|
|
|
expect(remainingTokens).toHaveLength(3);
|
|
|
|
|
expect(remainingTokens.every((t) => t.userId.toString() !== user1Id.toString())).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should delete only tokens matching specific email', async () => {
|
|
|
|
|
const result = await methods.deleteTokens({ email: 'user2@example.com' });
|
|
|
|
|
|
|
|
|
|
expect(result.deletedCount).toBe(1);
|
|
|
|
|
|
|
|
|
|
const remainingTokens = await Token.find({});
|
|
|
|
|
expect(remainingTokens).toHaveLength(3);
|
|
|
|
|
expect(remainingTokens.find((t) => t.email === 'user2@example.com')).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should delete only tokens matching specific identifier', async () => {
|
|
|
|
|
const result = await methods.deleteTokens({ identifier: 'oauth-identifier-456' });
|
|
|
|
|
|
|
|
|
|
expect(result.deletedCount).toBe(1);
|
|
|
|
|
|
|
|
|
|
const remainingTokens = await Token.find({});
|
|
|
|
|
expect(remainingTokens).toHaveLength(3);
|
|
|
|
|
expect(remainingTokens.find((t) => t.identifier === 'oauth-identifier-456')).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should not delete tokens when undefined fields are passed', async () => {
|
|
|
|
|
// This is the critical test case for the bug fix
|
|
|
|
|
const result = await methods.deleteTokens({
|
|
|
|
|
token: 'verify-token-1',
|
|
|
|
|
identifier: undefined,
|
|
|
|
|
userId: undefined,
|
|
|
|
|
email: undefined,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.deletedCount).toBe(1); // Only the token with 'verify-token-1'
|
|
|
|
|
|
|
|
|
|
const remainingTokens = await Token.find({});
|
|
|
|
|
expect(remainingTokens).toHaveLength(3);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should delete multiple tokens when they match OR conditions', async () => {
|
|
|
|
|
// Create tokens that will match multiple conditions
|
|
|
|
|
await Token.create({
|
|
|
|
|
token: 'multi-match',
|
|
|
|
|
userId: user2Id, // Will match userId condition
|
|
|
|
|
email: 'different@example.com',
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await methods.deleteTokens({
|
|
|
|
|
token: 'verify-token-1',
|
|
|
|
|
userId: user2Id.toString(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Should delete: verify-token-1 (by token) + verify-token-2 (by userId) + multi-match (by userId)
|
|
|
|
|
expect(result.deletedCount).toBe(3);
|
|
|
|
|
|
|
|
|
|
const remainingTokens = await Token.find({});
|
|
|
|
|
expect(remainingTokens).toHaveLength(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should throw error when no query parameters provided', async () => {
|
|
|
|
|
await expect(methods.deleteTokens({})).rejects.toThrow(
|
|
|
|
|
'At least one query parameter must be provided',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Verify no tokens were deleted
|
|
|
|
|
const tokens = await Token.find({});
|
|
|
|
|
expect(tokens).toHaveLength(4);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should handle deletion when no tokens match', async () => {
|
|
|
|
|
const result = await methods.deleteTokens({ token: 'non-existent-token' });
|
|
|
|
|
|
|
|
|
|
expect(result.deletedCount).toBe(0);
|
|
|
|
|
|
|
|
|
|
// Verify all tokens still exist
|
|
|
|
|
const tokens = await Token.find({});
|
|
|
|
|
expect(tokens).toHaveLength(4);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should handle email verification scenario correctly', async () => {
|
|
|
|
|
// This simulates the exact scenario from the bug report
|
|
|
|
|
// Multiple users register and get verification tokens
|
|
|
|
|
const newUser1Id = new mongoose.Types.ObjectId();
|
|
|
|
|
const newUser2Id = new mongoose.Types.ObjectId();
|
|
|
|
|
const newUser3Id = new mongoose.Types.ObjectId();
|
|
|
|
|
|
|
|
|
|
await Token.create([
|
|
|
|
|
{
|
|
|
|
|
token: 'email-verify-token-1',
|
|
|
|
|
userId: newUser1Id,
|
|
|
|
|
email: 'newuser1@example.com',
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 86400000), // 24 hours
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
token: 'email-verify-token-2',
|
|
|
|
|
userId: newUser2Id,
|
|
|
|
|
email: 'newuser2@example.com',
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 86400000),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
token: 'email-verify-token-3',
|
|
|
|
|
userId: newUser3Id,
|
|
|
|
|
email: 'newuser3@example.com',
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
expiresAt: new Date(Date.now() + 86400000),
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// User 2 verifies their email - only their token should be deleted
|
|
|
|
|
const result = await methods.deleteTokens({ token: 'email-verify-token-2' });
|
|
|
|
|
|
|
|
|
|
expect(result.deletedCount).toBe(1);
|
|
|
|
|
|
|
|
|
|
// Verify other users' tokens still exist
|
|
|
|
|
const remainingTokens = await Token.find({ token: { $regex: /^email-verify-token-/ } });
|
|
|
|
|
expect(remainingTokens).toHaveLength(2);
|
|
|
|
|
expect(remainingTokens.find((t) => t.token === 'email-verify-token-1')).toBeDefined();
|
|
|
|
|
expect(remainingTokens.find((t) => t.token === 'email-verify-token-3')).toBeDefined();
|
|
|
|
|
expect(remainingTokens.find((t) => t.token === 'email-verify-token-2')).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|