EventBuzz_Backend/src/services/attendanceCheckIn.sync.test.js

328 lines
9.4 KiB
JavaScript

jest.mock('../models/Attendance', () => ({
findOne: jest.fn(),
}));
jest.mock('../models/AttendanceParticipant', () => ({
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
}));
jest.mock('../models/Participant', () => ({
findById: jest.fn(),
}));
jest.mock('../utils/participantMembership', () => ({
getMembership: jest.fn(),
flattenParticipantForEvent: jest.fn((p) => p),
}));
const mongoose = require('mongoose');
const Attendance = require('../models/Attendance');
const AttendanceParticipant = require('../models/AttendanceParticipant');
const Participant = require('../models/Participant');
const { getMembership } = require('../utils/participantMembership');
const { performBulkCheckInSync } = require('./attendanceCheckIn.service');
const eventId = new mongoose.Types.ObjectId().toString();
const attendanceId = new mongoose.Types.ObjectId().toString();
const participantId = new mongoose.Types.ObjectId().toString();
describe('performBulkCheckInSync', () => {
beforeEach(() => {
jest.resetAllMocks();
Attendance.findOne.mockReturnValue({
lean: () =>
Promise.resolve({
_id: attendanceId,
event: eventId,
name: 'Door A',
status: 'active',
settings: { allowDuplicateCheckIn: false },
}),
});
AttendanceParticipant.find.mockReturnValue({
select: () => ({ lean: () => Promise.resolve([]) }),
});
AttendanceParticipant.findOne.mockReset();
AttendanceParticipant.create.mockReset();
Participant.findById.mockReset();
getMembership.mockReset();
});
test('rejects empty operations', async () => {
const out = await performBulkCheckInSync({
attendanceId,
eventId,
operations: [],
checkedInBy: 'user1',
});
expect(out.err).toBeTruthy();
expect(out.statusCode).toBe(400);
});
test('rejects invalid checkedInAt', async () => {
AttendanceParticipant.find.mockReturnValue({
select: () => ({ lean: () => Promise.resolve([]) }),
});
const out = await performBulkCheckInSync({
attendanceId,
eventId,
checkedInBy: 'user1',
operations: [
{
clientOpId: 'op-1',
participantId,
checkedInAt: 'not-a-date',
},
],
});
expect(out.results[0].status).toBe('rejected');
expect(out.results[0].message).toMatch(/checkedInAt/i);
});
test('returns duplicate for existing clientOpId', async () => {
AttendanceParticipant.find.mockReturnValue({
select: () => ({
lean: () =>
Promise.resolve([
{
participant: participantId,
checkIns: [
{
clientOpId: 'op-dup',
checkedInAt: new Date('2026-08-01T10:00:00.000Z'),
},
],
},
]),
}),
});
const out = await performBulkCheckInSync({
attendanceId,
eventId,
checkedInBy: 'user1',
operations: [
{
clientOpId: 'op-dup',
participantId,
checkedInAt: '2026-08-01T10:00:00.000Z',
},
],
});
expect(out.results[0].status).toBe('duplicate');
});
test('conflicts when already checked in and duplicates disallowed', async () => {
AttendanceParticipant.find.mockReturnValue({
select: () => ({ lean: () => Promise.resolve([]) }),
});
getMembership.mockReturnValue({ checkedIn: false });
Participant.findById.mockResolvedValue({
_id: participantId,
firstName: 'Ada',
lastName: 'Lovelace',
email: 'ada@example.com',
phone: '',
toObject: () => ({}),
markModified: jest.fn(),
save: jest.fn(),
});
AttendanceParticipant.findOne
.mockResolvedValueOnce({
checkIns: [{ checkedInAt: new Date('2026-08-01T09:00:00.000Z') }],
})
.mockReturnValueOnce({
select: () => ({ lean: () => Promise.resolve(null) }),
});
const out = await performBulkCheckInSync({
attendanceId,
eventId,
checkedInBy: 'user1',
operations: [
{
clientOpId: 'op-conflict',
participantId,
checkedInAt: '2026-08-01T11:00:00.000Z',
},
],
});
expect(out.results[0].status).toBe('conflict');
});
test('applies offline check-in with client timestamp', async () => {
AttendanceParticipant.find.mockReturnValue({
select: () => ({ lean: () => Promise.resolve([]) }),
});
getMembership.mockReturnValue({ checkedIn: false });
const participantDoc = {
_id: participantId,
firstName: 'Ada',
lastName: 'Lovelace',
email: 'ada@example.com',
phone: '',
toObject: () => ({}),
markModified: jest.fn(),
save: jest.fn().mockResolvedValue(undefined),
};
Participant.findById.mockResolvedValue(participantDoc);
AttendanceParticipant.findOne
.mockResolvedValueOnce(null)
.mockReturnValueOnce({
select: () => ({ lean: () => Promise.resolve(null) }),
});
const created = {
checkIns: [
{
clientOpId: 'op-new',
checkedInAt: new Date('2026-08-01T11:00:00.000Z'),
source: 'offline_sync',
},
],
};
AttendanceParticipant.create.mockResolvedValue(created);
const out = await performBulkCheckInSync({
attendanceId,
eventId,
checkedInBy: 'user1',
deviceId: 'device-1',
operations: [
{
clientOpId: 'op-new',
participantId,
checkedInAt: '2026-08-01T11:00:00.000Z',
},
],
});
expect(out.results[0].status).toBe('applied');
expect(AttendanceParticipant.create).toHaveBeenCalled();
const entry = AttendanceParticipant.create.mock.calls[0][0].checkIns[0];
expect(entry.source).toBe('offline_sync');
expect(entry.clientOpId).toBe('op-new');
expect(new Date(entry.checkedInAt).toISOString()).toBe('2026-08-01T11:00:00.000Z');
});
test('rejects assign-blank profile when setting is off', async () => {
AttendanceParticipant.find.mockReturnValue({
select: () => ({ lean: () => Promise.resolve([]) }),
});
const out = await performBulkCheckInSync({
attendanceId,
eventId,
checkedInBy: 'user1',
operations: [
{
clientOpId: 'op-blank',
participantId,
checkedInAt: '2026-08-01T11:00:00.000Z',
profile: { firstName: 'Walk', lastName: 'In', email: '', phone: '' },
},
],
});
expect(out.results[0].status).toBe('rejected');
expect(out.results[0].message).toMatch(/Assign a Blank/i);
});
test('applies undo by popping last check-in', async () => {
AttendanceParticipant.find.mockReturnValue({
select: () => ({ lean: () => Promise.resolve([]) }),
});
getMembership.mockReturnValue({ checkedIn: true });
Participant.findById.mockResolvedValue({
_id: participantId,
firstName: 'Ada',
lastName: 'Lovelace',
email: 'ada@example.com',
phone: '',
});
const apDoc = {
checkIns: [
{ checkedInAt: new Date('2026-08-01T09:00:00.000Z'), clientOpId: 'op-in' },
],
undos: [],
save: jest.fn().mockResolvedValue(undefined),
};
AttendanceParticipant.findOne.mockResolvedValue(apDoc);
const out = await performBulkCheckInSync({
attendanceId,
eventId,
checkedInBy: 'user1',
operations: [
{
clientOpId: 'op-undo',
participantId,
checkedInAt: '2026-08-01T11:00:00.000Z',
action: 'undo',
},
],
});
expect(out.results[0].status).toBe('applied');
expect(out.results[0].checkInCount).toBe(0);
expect(apDoc.checkIns).toHaveLength(0);
expect(apDoc.undos[0].clientOpId).toBe('op-undo');
expect(apDoc.save).toHaveBeenCalled();
});
test('returns duplicate for existing undo clientOpId', async () => {
AttendanceParticipant.find.mockReturnValue({
select: () => ({
lean: () =>
Promise.resolve([
{
participant: participantId,
checkIns: [],
undos: [{ clientOpId: 'op-undo-dup', undoneAt: new Date() }],
},
]),
}),
});
const out = await performBulkCheckInSync({
attendanceId,
eventId,
checkedInBy: 'user1',
operations: [
{
clientOpId: 'op-undo-dup',
participantId,
checkedInAt: '2026-08-01T11:00:00.000Z',
action: 'undo',
},
],
});
expect(out.results[0].status).toBe('duplicate');
});
test('conflicts undo when participant is not checked in', async () => {
AttendanceParticipant.find.mockReturnValue({
select: () => ({ lean: () => Promise.resolve([]) }),
});
getMembership.mockReturnValue({ checkedIn: false });
Participant.findById.mockResolvedValue({
_id: participantId,
firstName: 'Ada',
lastName: 'Lovelace',
});
AttendanceParticipant.findOne.mockResolvedValue({ checkIns: [], undos: [] });
const out = await performBulkCheckInSync({
attendanceId,
eventId,
checkedInBy: 'user1',
operations: [
{
clientOpId: 'op-undo-empty',
participantId,
checkedInAt: '2026-08-01T11:00:00.000Z',
action: 'undo',
},
],
});
expect(out.results[0].status).toBe('conflict');
expect(out.results[0].message).toMatch(/not checked in/i);
});
});