Files
starr-philproperties/tests/services/planAccess.service.test.js
T
kennethobsequio 2e9c2ad43f fix
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
2026-08-13 19:58:52 +08:00

128 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
// ── Regression tests for the "revoke plan access, no refund" feature ────────
// revokePlanSubscriberAccess() must replicate revokeTier's single-record
// "auto-downgrade to Free only if the user holds no other active tier" rule
// (controllers/admin/tiers.controller.js) instead of blanket-downgrading
// everyone, and must fire exactly one in-app notification batch plus one
// email per affected user.
jest.mock('../../models/tiers/user_tiers.mdl', () => ({
findAll: jest.fn(),
update: jest.fn(),
bulkCreate: jest.fn(),
}));
jest.mock('../../models/users/users.mdl', () => ({ findAll: jest.fn() }));
jest.mock('../../models/notifications/user_notification.mdl', () => ({ bulkCreate: jest.fn() }));
jest.mock('../../services/email.service', () => ({ sendEmail: jest.fn().mockResolvedValue(true) }));
const mdl_UserTiers = require('../../models/tiers/user_tiers.mdl');
const mdl_Users = require('../../models/users/users.mdl');
const UserNotification = require('../../models/notifications/user_notification.mdl');
const { sendEmail } = require('../../services/email.service');
const { revokePlanSubscriberAccess, revokePlanSubscriberAccessBulk } = require('../../services/planAccess.service');
function makePlan(overrides = {}) {
return { plan_id: 10, label: 'Premium – 1 Month', ...overrides };
}
beforeEach(() => jest.clearAllMocks());
describe('revokePlanSubscriberAccess()', () => {
test('no active subscribers → no-op, returns zero count', async () => {
mdl_UserTiers.findAll.mockResolvedValue([]);
const result = await revokePlanSubscriberAccess(makePlan(), 99);
expect(result).toEqual({ revoked_user_count: 0 });
expect(mdl_UserTiers.update).not.toHaveBeenCalled();
expect(UserNotification.bulkCreate).not.toHaveBeenCalled();
expect(sendEmail).not.toHaveBeenCalled();
});
test('a user with ONLY this plan active gets auto-downgraded to Free', async () => {
mdl_UserTiers.findAll
.mockResolvedValueOnce([{ tier_id: 1, user_id: 5, plan_id: 10 }]) // active rows for the plan
.mockResolvedValueOnce([]); // grouped "still active elsewhere" check — none
mdl_Users.findAll.mockResolvedValue([{ user_id: 5, email: 'a@b.com', personal_info: { name: { full_name: 'Ana' } } }]);
const result = await revokePlanSubscriberAccess(makePlan(), 99);
expect(mdl_UserTiers.update).toHaveBeenCalledWith(
expect.objectContaining({ status: 'revoked', revoked_by: 99 }),
{ where: { tier_id: [1] } }
);
expect(mdl_UserTiers.bulkCreate).toHaveBeenCalledWith([
expect.objectContaining({ user_id: '5', tier: 'free', status: 'active' }),
]);
expect(result).toEqual({ revoked_user_count: 1 });
});
test('a user with ANOTHER concurrently-active plan does NOT get downgraded', async () => {
mdl_UserTiers.findAll
.mockResolvedValueOnce([{ tier_id: 2, user_id: 6, plan_id: 10 }])
.mockResolvedValueOnce([{ user_id: 6 }]); // still holds a different active tier
mdl_Users.findAll.mockResolvedValue([{ user_id: 6, email: 'c@d.com', personal_info: {} }]);
await revokePlanSubscriberAccess(makePlan(), 99);
expect(mdl_UserTiers.bulkCreate).not.toHaveBeenCalled();
});
test('fires exactly one notification batch and one email per affected user', async () => {
mdl_UserTiers.findAll
.mockResolvedValueOnce([
{ tier_id: 1, user_id: 5, plan_id: 10 },
{ tier_id: 2, user_id: 6, plan_id: 10 },
])
.mockResolvedValueOnce([{ user_id: 5 }, { user_id: 6 }]);
mdl_Users.findAll.mockResolvedValue([
{ user_id: 5, email: 'a@b.com', personal_info: {} },
{ user_id: 6, email: 'c@d.com', personal_info: {} },
]);
const result = await revokePlanSubscriberAccess(makePlan(), 99);
expect(UserNotification.bulkCreate).toHaveBeenCalledTimes(1);
expect(UserNotification.bulkCreate.mock.calls[0][0]).toHaveLength(2);
expect(sendEmail).toHaveBeenCalledTimes(2);
expect(sendEmail).toHaveBeenCalledWith(expect.objectContaining({ to: 'a@b.com', type: 'TIER_ACCESS_REVOKED' }));
expect(result).toEqual({ revoked_user_count: 2 });
});
});
describe('revokePlanSubscriberAccessBulk() — N+1 regression', () => {
test('query count stays flat regardless of plan/subscriber count (no per-user or per-plan loop)', async () => {
const plans = [
{ plan_id: 10, label: 'Premium – 1 Month' },
{ plan_id: 11, label: 'Premium – 1 Year' },
{ plan_id: 12, label: 'Basic' },
];
const activeRows = Array.from({ length: 25 }, (_, i) => ({
tier_id: i + 1,
user_id: i + 1,
plan_id: plans[i % plans.length].plan_id,
}));
mdl_UserTiers.findAll
.mockResolvedValueOnce(activeRows) // active rows across all 3 plans
.mockResolvedValueOnce([]); // grouped "still active" check — nobody else active
mdl_Users.findAll.mockResolvedValue(
activeRows.map((r) => ({ user_id: r.user_id, email: `${r.user_id}@x.com`, personal_info: {} })),
);
const result = await revokePlanSubscriberAccessBulk(plans, 99);
// Exactly 2 findAll calls total (active rows + grouped still-active check),
// 1 bulk update, 1 bulk downgrade create, 1 notification bulkCreate —
// no matter how many plans/users were involved.
expect(mdl_UserTiers.findAll).toHaveBeenCalledTimes(2);
expect(mdl_UserTiers.update).toHaveBeenCalledTimes(1);
expect(mdl_UserTiers.bulkCreate).toHaveBeenCalledTimes(1);
expect(mdl_UserTiers.bulkCreate.mock.calls[0][0]).toHaveLength(25);
expect(UserNotification.bulkCreate).toHaveBeenCalledTimes(1);
expect(UserNotification.bulkCreate.mock.calls[0][0]).toHaveLength(25);
expect(result).toEqual({ revoked_user_count: 25 });
});
});