mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
revise ads and alerts
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
'use strict';
|
||||
|
||||
// ── Regression test for the "declined payment still granted access" bug ──────
|
||||
// PayPal's v2 capture-order endpoint can respond with HTTP 2xx even when the
|
||||
// charge itself was declined or held for review — the failure only shows up
|
||||
// in the response body's status field (e.g. capture.status: "DECLINED" or
|
||||
// "PENDING"), not as a thrown HTTP error. Before the fix, captureOrder() only
|
||||
// guarded against a *thrown* error from the provider and unconditionally
|
||||
// granted the tier on any non-throwing response. These tests pin the fix:
|
||||
// a non-COMPLETED capture must mark the payment failed and must NOT create
|
||||
// or extend a user_tiers row.
|
||||
|
||||
jest.mock('../../models/tiers/tier_categories.mdl', () => ({}));
|
||||
jest.mock('../../models/tiers/tier_plans.mdl', () => ({ findOne: jest.fn() }));
|
||||
jest.mock('../../models/tiers/user_tiers.mdl', () => ({ findOne: jest.fn(), create: jest.fn() }));
|
||||
jest.mock('../../models/tiers/payments.mdl', () => ({ findOne: jest.fn() }));
|
||||
jest.mock('../../models/system_badges/system_badges.mdl', () => ({}));
|
||||
jest.mock('../../models/assets/assets.mdl', () => ({}));
|
||||
jest.mock('../../models/notifications/user_notification.mdl', () => ({ create: jest.fn() }));
|
||||
jest.mock('../../services/achievements.service', () => ({ onTierActivated: jest.fn() }));
|
||||
jest.mock('../../models/courses/courses.mdl', () => ({ Course: {} }));
|
||||
jest.mock('../../services/payment.service', () => ({ captureOrder: jest.fn() }));
|
||||
jest.mock('../../data/notifications.data', () => ({ NOTIFICATION_REGISTRY: {} }));
|
||||
jest.mock('../../models/tiers/tier.associations', () => ({}));
|
||||
|
||||
const mdl_UserTiers = require('../../models/tiers/user_tiers.mdl');
|
||||
const mdl_Payments = require('../../models/tiers/payments.mdl');
|
||||
const paymentSvc = require('../../services/payment.service');
|
||||
const controller = require('../../controllers/client/tiers.controller');
|
||||
|
||||
function mockRes() {
|
||||
const res = {};
|
||||
res.status = jest.fn().mockReturnValue(res);
|
||||
res.json = jest.fn().mockReturnValue(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
function makePayment(overrides = {}) {
|
||||
return {
|
||||
payment_id: 1,
|
||||
plan_id: 10,
|
||||
provider: 'paypal',
|
||||
provider_payload: { order_id: 'ORDER-1' },
|
||||
plan: { plan_id: 10, tier: 'premium', duration_days: 30, is_active: true },
|
||||
update: jest.fn().mockResolvedValue(true),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('captureOrder() — declined/incomplete captures must not grant access', () => {
|
||||
|
||||
test('capture.status "DECLINED" marks the payment failed and creates no tier', async () => {
|
||||
const payment = makePayment();
|
||||
mdl_Payments.findOne.mockResolvedValue(payment);
|
||||
paymentSvc.captureOrder.mockResolvedValue({
|
||||
status: 'COMPLETED', // outer order status can still say COMPLETED
|
||||
purchase_units: [{ payments: { captures: [{ id: 'CAP-1', status: 'DECLINED' }] } }],
|
||||
});
|
||||
|
||||
const req = { user: { user_id: 1 }, body: { order_id: 'ORDER-1' } };
|
||||
const res = mockRes();
|
||||
|
||||
await controller.captureOrder(req, res);
|
||||
|
||||
expect(mdl_UserTiers.create).not.toHaveBeenCalled();
|
||||
expect(payment.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ status: 'failed' })
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(402);
|
||||
});
|
||||
|
||||
test('capture.status "PENDING" (e.g. eCheck review) also withholds access', async () => {
|
||||
const payment = makePayment();
|
||||
mdl_Payments.findOne.mockResolvedValue(payment);
|
||||
paymentSvc.captureOrder.mockResolvedValue({
|
||||
status: 'COMPLETED',
|
||||
purchase_units: [{ payments: { captures: [{ id: 'CAP-2', status: 'PENDING' }] } }],
|
||||
});
|
||||
|
||||
const req = { user: { user_id: 1 }, body: { order_id: 'ORDER-1' } };
|
||||
const res = mockRes();
|
||||
|
||||
await controller.captureOrder(req, res);
|
||||
|
||||
expect(mdl_UserTiers.create).not.toHaveBeenCalled();
|
||||
expect(payment.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ status: 'failed' })
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(402);
|
||||
});
|
||||
|
||||
test('capture.status "COMPLETED" still grants the tier (control case)', async () => {
|
||||
const payment = makePayment();
|
||||
mdl_Payments.findOne.mockResolvedValue(payment);
|
||||
mdl_UserTiers.findOne.mockResolvedValue(null); // no existing active tier
|
||||
mdl_UserTiers.create.mockResolvedValue({ tier_id: 99, tier: 'premium', expires_at: new Date() });
|
||||
paymentSvc.captureOrder.mockResolvedValue({
|
||||
status: 'COMPLETED',
|
||||
payer: { payer_id: 'PAYER-1' },
|
||||
purchase_units: [{ payments: { captures: [{ id: 'CAP-3', status: 'COMPLETED' }] } }],
|
||||
});
|
||||
|
||||
const req = { user: { user_id: 1 }, body: { order_id: 'ORDER-1' } };
|
||||
const res = mockRes();
|
||||
|
||||
await controller.captureOrder(req, res);
|
||||
|
||||
expect(mdl_UserTiers.create).toHaveBeenCalled();
|
||||
expect(payment.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ status: 'completed' })
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
jest.mock('../../models/tiers/payments.mdl', () => ({ findAll: jest.fn() }));
|
||||
jest.mock('../../models/courses/course_purchases.mdl', () => ({ findAll: jest.fn() }));
|
||||
|
||||
const mdl_Payments = require('../../models/tiers/payments.mdl');
|
||||
const mdl_CoursePurchase = require('../../models/courses/course_purchases.mdl');
|
||||
const job = require('../../cron/jobs/fail_stale_payments.cron');
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
test('marks stale pending payments and purchases as failed', async () => {
|
||||
const payment = { provider_payload: { order_id: 'O-1' }, update: jest.fn().mockResolvedValue(true) };
|
||||
const purchase = { provider_payload: { order_id: 'O-2' }, update: jest.fn().mockResolvedValue(true) };
|
||||
|
||||
mdl_Payments.findAll.mockResolvedValue([payment]);
|
||||
mdl_CoursePurchase.findAll.mockResolvedValue([purchase]);
|
||||
|
||||
await job.run();
|
||||
|
||||
expect(payment.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: 'failed',
|
||||
provider_payload: expect.objectContaining({ failed_reason: 'abandoned_checkout' }),
|
||||
})
|
||||
);
|
||||
expect(purchase.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: 'failed',
|
||||
provider_payload: expect.objectContaining({ failed_reason: 'abandoned_checkout' }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('does nothing when there are no stale rows', async () => {
|
||||
mdl_Payments.findAll.mockResolvedValue([]);
|
||||
mdl_CoursePurchase.findAll.mockResolvedValue([]);
|
||||
|
||||
await expect(job.run()).resolves.not.toThrow();
|
||||
});
|
||||
Reference in New Issue
Block a user