mailgun-optin-cli
Version:
CLI tool for sending opt-in confirmation emails via Mailgun
297 lines (223 loc) • 10.7 kB
JavaScript
import { expect } from 'chai';
import { URL } from 'url';
import {
generateConfirmationToken,
createConfirmationLink,
parseConfirmationToken,
validateConfirmationToken,
generateTokenForSubscriber,
} from '../src/confirmation-links.js';
describe('Confirmation Links Module', () => {
describe('generateConfirmationToken', () => {
it('should generate a unique token', () => {
const token = generateConfirmationToken();
expect(token).to.be.a('string');
expect(token).to.have.length.greaterThan(0);
});
it('should generate different tokens on multiple calls', () => {
const token1 = generateConfirmationToken();
const token2 = generateConfirmationToken();
expect(token1).to.not.equal(token2);
});
it('should generate tokens without special characters that need URL encoding', () => {
const token = generateConfirmationToken();
// Should not contain characters that need URL encoding
expect(token).to.not.match(/[+/=]/);
});
it('should generate tokens of consistent format', () => {
const tokens = Array.from({ length: 10 }, () => generateConfirmationToken());
tokens.forEach(token => {
expect(token).to.be.a('string');
expect(token.length).to.be.greaterThan(20); // Should be reasonably long
expect(token).to.match(/^[a-zA-Z0-9-_]+$/); // URL-safe characters only
});
});
});
describe('generateTokenForSubscriber', () => {
it('should generate token with subscriber email', () => {
const subscriber = {
email: 'test@example.com',
first_name: 'John',
last_name: 'Doe',
};
const token = generateTokenForSubscriber(subscriber);
expect(token).to.be.a('string');
expect(token).to.have.length.greaterThan(0);
});
it('should generate different tokens for different subscribers', () => {
const subscriber1 = { email: 'user1@example.com' };
const subscriber2 = { email: 'user2@example.com' };
const token1 = generateTokenForSubscriber(subscriber1);
const token2 = generateTokenForSubscriber(subscriber2);
expect(token1).to.not.equal(token2);
});
it('should generate same token for same subscriber when called multiple times', () => {
const subscriber = { email: 'test@example.com' };
const token1 = generateTokenForSubscriber(subscriber);
const token2 = generateTokenForSubscriber(subscriber);
expect(token1).to.equal(token2);
});
it('should handle special characters in email', () => {
const subscriber = { email: 'user+tag@example.com' };
const token = generateTokenForSubscriber(subscriber);
expect(token).to.be.a('string');
expect(token).to.have.length.greaterThan(0);
});
it('should throw error for subscriber without email', () => {
const subscriber = { first_name: 'John' };
expect(() => generateTokenForSubscriber(subscriber)).to.throw(
'Subscriber must have an email address',
);
});
});
describe('createConfirmationLink', () => {
const baseUrl = 'https://example.com/confirm';
const token = 'test-token-123';
const email = 'test@example.com';
it('should create basic confirmation link', () => {
const link = createConfirmationLink(baseUrl, token, email);
expect(link).to.include(baseUrl);
expect(link).to.include(`token=${token}`);
expect(link).to.include(`email=${encodeURIComponent(email)}`);
});
it('should handle base URL with existing query parameters', () => {
const baseUrlWithParams = 'https://example.com/confirm?source=email';
const link = createConfirmationLink(baseUrlWithParams, token, email);
expect(link).to.include('source=email');
expect(link).to.include(`token=${token}`);
expect(link).to.include(`email=${encodeURIComponent(email)}`);
expect(link).to.include('&'); // Should use & to separate parameters
});
it('should URL encode email addresses with special characters', () => {
const specialEmail = 'user+tag@example.com';
const link = createConfirmationLink(baseUrl, token, specialEmail);
expect(link).to.include(`email=${encodeURIComponent(specialEmail)}`);
expect(link).to.include('user%2Btag%40example.com');
});
it('should handle additional parameters', () => {
const additionalParams = { source: 'newsletter', campaign: 'welcome' };
const link = createConfirmationLink(baseUrl, token, email, additionalParams);
expect(link).to.include('source=newsletter');
expect(link).to.include('campaign=welcome');
expect(link).to.include(`token=${token}`);
expect(link).to.include(`email=${encodeURIComponent(email)}`);
});
it('should throw error for invalid base URL', () => {
expect(() => createConfirmationLink('', token, email)).to.throw('Base URL is required');
expect(() => createConfirmationLink(null, token, email)).to.throw('Base URL is required');
});
it('should throw error for missing token', () => {
expect(() => createConfirmationLink(baseUrl, '', email)).to.throw('Token is required');
expect(() => createConfirmationLink(baseUrl, null, email)).to.throw('Token is required');
});
it('should throw error for missing email', () => {
expect(() => createConfirmationLink(baseUrl, token, '')).to.throw('Email is required');
expect(() => createConfirmationLink(baseUrl, token, null)).to.throw('Email is required');
});
});
describe('parseConfirmationToken', () => {
it('should parse valid token and return email', () => {
const subscriber = { email: 'test@example.com' };
const token = generateTokenForSubscriber(subscriber);
const parsed = parseConfirmationToken(token);
expect(parsed).to.be.an('object');
expect(parsed.email).to.equal(subscriber.email);
expect(parsed.valid).to.be.true;
});
it('should handle invalid token format', () => {
const invalidToken = 'invalid-token-format';
const parsed = parseConfirmationToken(invalidToken);
expect(parsed.valid).to.be.false;
expect(parsed.error).to.include('Invalid token format');
});
it('should handle corrupted token', () => {
const corruptedToken = 'corrupted.token.data';
const parsed = parseConfirmationToken(corruptedToken);
expect(parsed.valid).to.be.false;
expect(parsed.error).to.be.a('string');
});
it('should handle empty or null token', () => {
expect(parseConfirmationToken('')).to.have.property('valid', false);
expect(parseConfirmationToken(null)).to.have.property('valid', false);
expect(parseConfirmationToken(undefined)).to.have.property('valid', false);
});
});
describe('validateConfirmationToken', () => {
it('should validate token for correct email', () => {
const subscriber = { email: 'test@example.com' };
const token = generateTokenForSubscriber(subscriber);
const isValid = validateConfirmationToken(token, subscriber.email);
expect(isValid).to.be.true;
});
it('should reject token for wrong email', () => {
const subscriber = { email: 'test@example.com' };
const token = generateTokenForSubscriber(subscriber);
const isValid = validateConfirmationToken(token, 'wrong@example.com');
expect(isValid).to.be.false;
});
it('should reject invalid token format', () => {
const isValid = validateConfirmationToken('invalid-token', 'test@example.com');
expect(isValid).to.be.false;
});
it('should handle case sensitivity in email comparison', () => {
const subscriber = { email: 'Test@Example.com' };
const token = generateTokenForSubscriber(subscriber);
// Should be case insensitive
const isValid1 = validateConfirmationToken(token, 'test@example.com');
const isValid2 = validateConfirmationToken(token, 'TEST@EXAMPLE.COM');
expect(isValid1).to.be.true;
expect(isValid2).to.be.true;
});
it('should handle empty or null inputs', () => {
expect(validateConfirmationToken('', 'test@example.com')).to.be.false;
expect(validateConfirmationToken('token', '')).to.be.false;
expect(validateConfirmationToken(null, 'test@example.com')).to.be.false;
expect(validateConfirmationToken('token', null)).to.be.false;
});
});
describe('Token Security', () => {
it('should generate tokens that are not easily guessable', () => {
const subscriber = { email: 'test@example.com' };
const tokens = Array.from({ length: 100 }, () => generateTokenForSubscriber(subscriber));
// All tokens should be the same for the same subscriber
const uniqueTokens = new Set(tokens);
expect(uniqueTokens.size).to.equal(1);
// But different subscribers should have different tokens
const subscriber2 = { email: 'test2@example.com' };
const token2 = generateTokenForSubscriber(subscriber2);
expect(tokens[0]).to.not.equal(token2);
});
it('should not expose email in plain text within token', () => {
const subscriber = { email: 'test@example.com' };
const token = generateTokenForSubscriber(subscriber);
// Token should not contain the email in plain text
expect(token).to.not.include('test@example.com');
expect(token).to.not.include('test');
expect(token).to.not.include('@example.com');
});
it('should generate URL-safe tokens', () => {
const subscriber = { email: 'test+tag@example.com' };
const token = generateTokenForSubscriber(subscriber);
// Token should be URL-safe (no need for additional encoding)
expect(encodeURIComponent(token)).to.equal(token);
});
});
describe('Integration with URL Generation', () => {
it('should work end-to-end with link creation and validation', () => {
const subscriber = { email: 'integration@example.com' };
const baseUrl = 'https://example.com/confirm';
// Generate token
const token = generateTokenForSubscriber(subscriber);
// Create confirmation link
const link = createConfirmationLink(baseUrl, token, subscriber.email);
// Extract token from URL (simulate receiving it back)
const urlParams = new URL(link).searchParams;
const extractedToken = urlParams.get('token');
const extractedEmail = urlParams.get('email');
// Validate the extracted token
const isValid = validateConfirmationToken(extractedToken, extractedEmail);
expect(isValid).to.be.true;
expect(extractedEmail).to.equal(subscriber.email);
});
});
});