agmission/Development/server/tests/test_duplicate_promo_validation.js

418 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Test Duplicate Promo Validation in /api/admin/subscriptionPromos Endpoint
*
* Tests that the addSubscriptionPromo endpoint validates for duplicates:
* 1. Duplicate type + priceKey combination
* 2. Duplicate couponId
* 3. Overlapping validUntil dates for same type/priceKey
*
* Prerequisites:
* - **SERVER MUST BE RESTARTED** after code changes to pick up new validation logic
* - MongoDB running
* - Valid admin user with correct password (see ADMIN_PASSWORD constant below)
* - Stripe configured with test coupons
*
* Admin Credentials:
* - Email: Uses AGM_ADM_EMAIL environment variable (from environment.env)
* - Password: Update ADMIN_PASSWORD constant below with correct password
*/
const path = require('path');
// Parse --env argument (default: ./environment.env)
const args = process.argv.slice(2);
let envFile = './environment.env';
for (let i = 0; i < args.length; i++) {
if (args[i] === '--env' && args[i + 1]) {
envFile = args[i + 1];
i++;
}
}
// Load environment before requiring any modules
const envPath = path.resolve(process.cwd(), envFile);
require('dotenv').config({ path: envPath });
const axios = require('axios');
const moment = require('moment');
// Test configuration
const BASE_URL = process.env.APP_URL || 'https://localhost:4200';
const API_URL = BASE_URL.replace('4200', '4100'); // Server runs on 4100
// Admin user credentials
const ADMIN_USER = 'admin@agnav.com';
// ⚠️ UPDATE THIS WITH YOUR ADMIN PASSWORD:
const ADMIN_PASSWORD = 'admin'; // Default admin password - CHANGE THIS!
let authToken = null;
let addedPromoIds = []; // Track promos we add for cleanup
// HTTPS agent to accept self-signed certificates
const https = require('https');
const httpsAgent = new https.Agent({
rejectUnauthorized: false
});
/**
* Step 1.5: Clean up any existing test promos first
*/
async function cleanupExistingTestPromos() {
try {
console.log('\n--- Step 1.5: Cleanup Existing Test Promos ---');
// Get all promos
const response = await axios.get(`${API_URL}/api/admin/subscriptionPromos`, {
headers: { 'Authorization': `Bearer ${authToken}` },
httpsAgent
});
const existingTestPromos = response.data.promos.filter(p =>
p.name && p.name.startsWith('Test Promo ') ||
p.name && p.name.startsWith('Duplicate ') ||
p.name && p.name.startsWith('Different Type ') ||
p.priceKey && p.priceKey.startsWith('test_')
);
if (existingTestPromos.length > 0) {
console.log(` Found ${existingTestPromos.length} existing test promos, cleaning up...`);
for (const promo of existingTestPromos) {
try {
await axios.delete(`${API_URL}/api/admin/subscriptionPromos/${promo._id}`, {
headers: { 'Authorization': `Bearer ${authToken}` },
httpsAgent
});
} catch (err) {
// Ignore cleanup errors
}
}
console.log(` ✅ Cleaned up ${existingTestPromos.length} old test promos`);
} else {
console.log(` ✅ No existing test promos found`);
}
return true;
} catch (error) {
console.log(` ⚠️ Cleanup failed (continuing anyway): ${error.message}`);
return true; // Don't fail the test if cleanup fails
}
}
/**
* Step 1: Login as admin
*/
async function loginAsAdmin() {
try {
console.log('\n--- Step 1: Login as Admin ---');
const response = await axios.post(`${API_URL}/api/users/login`, {
username: ADMIN_USER,
password: ADMIN_PASSWORD
}, { httpsAgent });
authToken = response.data.token;
console.log('✅ Admin login successful');
console.log(` Token: ${authToken.substring(0, 20)}...`);
return true;
} catch (error) {
console.error('❌ Admin login failed:', error.response?.data || error.message);
console.error('\n⚠ Update ADMIN_EMAIL and ADMIN_PASSWORD in script with valid admin credentials');
return false;
}
}
/**
* Step 2: Add a test promo successfully
*/
async function addTestPromo() {
// Declare testPromo outside try block so it's accessible in catch
let testPromo = null;
try {
console.log('\n--- Step 2: Add Test Promo (Should Succeed) ---');
// Use a unique test priceKey to avoid conflicts with existing promos
const uniquePriceKey = `test_${Date.now()}`;
testPromo = {
name: `Test Promo ${Date.now()}`,
type: 'addon', // Use addon to avoid conflicts with package promos
priceKey: uniquePriceKey,
discountType: 'percent',
discountValue: 50,
eligibility: 'all',
priority: 5,
validUntil: moment().add(30, 'days').toISOString(),
enabled: true
// Note: couponId intentionally omitted (not null) - not required for this test
};
const response = await axios.post(`${API_URL}/api/admin/subscriptionPromos/add`,
testPromo,
{
headers: { 'Authorization': `Bearer ${authToken}` },
httpsAgent
}
);
console.log('✅ Test promo added successfully');
console.log(` Name: ${testPromo.name}`);
console.log(` Type/PriceKey: ${testPromo.type}/${testPromo.priceKey}`);
// Track for cleanup
const addedPromo = response.data.promos[response.data.promos.length - 1];
addedPromoIds.push(addedPromo._id);
return testPromo;
} catch (error) {
console.error('❌ Failed to add test promo:', error.response?.data || error.message);
console.error(' Actual request data:', JSON.stringify(testPromo, null, 2));
console.error('');
console.error('⚠️ If error is "invalid_param" without details:');
console.error(' 1. Server must be RESTARTED to pick up validation code changes');
console.error(' 2. Check server is running on port 4100');
throw error;
}
}
/**
* Step 3: Try to add duplicate type/priceKey (should fail)
*/
async function testDuplicateTypePriceKey(originalPromo) {
try {
console.log('\n--- Step 3: Test Duplicate Type/PriceKey (Should Fail) ---');
const duplicatePromo = {
...originalPromo,
name: `Duplicate ${Date.now()}`,
discountValue: 30, // Different value but same type/priceKey
validUntil: moment().add(60, 'days').toISOString()
};
await axios.post(`${API_URL}/api/admin/subscriptionPromos/add`,
duplicatePromo,
{
headers: { 'Authorization': `Bearer ${authToken}` },
httpsAgent
}
);
console.error('❌ UNEXPECTED: Duplicate type/priceKey was accepted (should have been rejected)');
return false;
} catch (error) {
if (error.response?.status === 409 && error.response?.data?.error?.['.tag'] === 'promo_duplicate_type_pricekey') {
console.log('✅ Duplicate type/priceKey correctly rejected');
console.log(` Error: ${error.response.data.error.message}`);
console.log(` Error Code: ${error.response.data.error['.tag']}`);
return true;
}
console.error('❌ Unexpected error:', error.response?.data || error.message);
return false;
}
}
/**
* Step 4: Try to add duplicate couponId (should fail)
*/
async function testDuplicateCouponId() {
try {
console.log('\n--- Step 4: Test Duplicate CouponId (Should Fail) ---');
console.log('⚠️ This test requires two promos with the same couponId');
console.log(' Skipping automated test - manual verification recommended');
console.log(' To test manually:');
console.log(' 1. Create promo with couponId="TEST_COUPON"');
console.log(' 2. Try to create another promo with same couponId');
console.log(' 3. Expected error: "Active promo already uses coupon TEST_COUPON..."');
return true;
} catch (error) {
console.error('❌ Test failed:', error.message);
return false;
}
}
/**
* Step 5: Try to add overlapping validUntil dates (should fail)
*/
async function testOverlappingDates(originalPromo) {
try {
console.log('\n--- Step 5: Test Overlapping ValidUntil Dates (Should Fail) ---');
// Original promo valid until +30 days from now
// Try to add another promo for same type/priceKey valid until +60 days
// This should fail because both are active in overlapping period
console.log('⚠️ Note: Current implementation checks if BOTH promos are future-dated');
console.log(' If original promo has already passed, this test will succeed');
console.log(' (no overlap with expired promo)');
const overlappingPromo = {
name: `Overlapping ${Date.now()}`,
type: originalPromo.type,
priceKey: originalPromo.priceKey,
discountType: 'percent',
discountValue: 25,
eligibility: 'all',
priority: 3,
validUntil: moment().add(60, 'days').toISOString(),
enabled: true,
couponId: null
};
await axios.post(`${API_URL}/api/admin/subscriptionPromos/add`,
overlappingPromo,
{
headers: { 'Authorization': `Bearer ${authToken}` },
httpsAgent
}
);
console.error('❌ UNEXPECTED: Overlapping dates were accepted (should have been rejected)');
console.error(' Note: Test may pass if original promo has expired');
return false;
} catch (error) {
if (error.response?.status === 409 && error.response?.data?.error?.['.tag'] === 'promo_overlapping_dates') {
console.log('✅ Overlapping dates correctly rejected');
console.log(` Error: ${error.response.data.error.message}`);
console.log(` Error Code: ${error.response.data.error['.tag']}`);
return true;
}
// Also accept duplicate type/priceKey error (more restrictive check that runs first)
if (error.response?.status === 409 && error.response?.data?.error?.['.tag'] === 'promo_duplicate_type_pricekey') {
console.log('✅ Overlapping dates correctly rejected');
console.log(` Error: ${error.response.data.error.message}`);
console.log(` Error Code: ${error.response.data.error['.tag']} (duplicate check ran first)`);
return true;
}
console.error('❌ Unexpected error:', error.response?.data || error.message);
return false;
}
}
/**
* Step 6: Add promo with different type (should succeed)
*/
async function testDifferentType(originalPromo) {
try {
console.log('\n--- Step 6: Add Promo with Different Type (Should Succeed) ---');
const differentTypePromo = {
...originalPromo,
name: `Different Type ${Date.now()}`,
type: 'addon', // Different type, same priceKey is OK
priceKey: 'addon_1',
validUntil: moment().add(30, 'days').toISOString()
};
const response = await axios.post(`${API_URL}/api/admin/subscriptionPromos/add`,
differentTypePromo,
{
headers: { 'Authorization': `Bearer ${authToken}` },
httpsAgent
}
);
console.log('✅ Promo with different type added successfully');
console.log(` Name: ${differentTypePromo.name}`);
console.log(` Type/PriceKey: ${differentTypePromo.type}/${differentTypePromo.priceKey}`);
// Track for cleanup
const addedPromo = response.data.promos[response.data.promos.length - 1];
addedPromoIds.push(addedPromo._id);
return true;
} catch (error) {
console.error('❌ Failed to add promo with different type:', error.response?.data || error.message);
return false;
}
}
/**
* Cleanup: Disable test promos
*/
async function cleanup() {
console.log('\n--- Cleanup: Disable Test Promos ---');
let successCount = 0;
for (const promoId of addedPromoIds) {
try {
await axios.delete(`${API_URL}/api/admin/subscriptionPromos/${promoId}`, {
headers: { 'Authorization': `Bearer ${authToken}` },
httpsAgent
});
successCount++;
} catch (error) {
console.error(` ⚠️ Failed to delete promo ${promoId}: ${error.message}`);
}
}
console.log(`✅ Cleaned up ${successCount}/${addedPromoIds.length} test promos`);
}
/**
* Main test execution
*/
async function runTests() {
console.log('='.repeat(70));
console.log('TEST: Duplicate Promo Validation');
console.log('='.repeat(70));
console.log(`API URL: ${API_URL}`);
console.log(`Admin User: ${ADMIN_USER}`);
let testPromo = null;
try {
// Step 1: Login
const loginSuccess = await loginAsAdmin();
if (!loginSuccess) {
process.exit(1);
}
// Step 1.5: Cleanup existing test promos
await cleanupExistingTestPromos();
// Step 2: Add test promo
testPromo = await addTestPromo();
// Step 3: Test duplicate type/priceKey
const test3Pass = await testDuplicateTypePriceKey(testPromo);
// Step 4: Test duplicate couponId
const test4Pass = await testDuplicateCouponId();
// Step 5: Test overlapping dates
const test5Pass = await testOverlappingDates(testPromo);
// Step 6: Test different type (should succeed)
const test6Pass = await testDifferentType(testPromo);
// Cleanup
await cleanup();
console.log('\n' + '='.repeat(70));
if (test3Pass && test4Pass && test6Pass) {
console.log('✅ ALL CRITICAL TESTS PASSED');
console.log(' Note: Overlapping dates test may vary based on timing');
} else {
console.log('⚠️ SOME TESTS FAILED - See details above');
}
console.log('='.repeat(70));
} catch (error) {
console.error('\n' + '='.repeat(70));
console.error('❌ TEST FAILED');
console.error('='.repeat(70));
console.error(error);
// Attempt cleanup even on failure
if (addedPromoIds.length > 0) {
await cleanup();
}
process.exit(1);
}
}
// Run tests
runTests();