139 lines
4.7 KiB
TypeScript
139 lines
4.7 KiB
TypeScript
import { Logger } from '@nestjs/common';
|
|||
|
|
import { assertSmsConfigured, smsConfig, smsConfigured } from './sms.service';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Covers the boot-time contract the owner specified:
|
||
|
|
* * while no key is set, development behaviour is unchanged;
|
||
|
|
* * in production a missing key is a clear, explicit boot failure.
|
||
|
|
*/
|
||
|
|
describe('SMS configuration', () => {
|
||
|
|
const saved = { ...process.env };
|
||
|
|
// Silence the warnings these tests deliberately provoke.
|
||
|
|
const logger = { warn: jest.fn(), log: jest.fn(), error: jest.fn() } as unknown as Logger;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
for (const key of [
|
||
|
|
'SMS_PROVIDER',
|
||
|
|
'SMS_TREZ_URL',
|
||
|
|
'SMS_TREZ_USERNAME',
|
||
|
|
'SMS_TREZ_PASSWORD',
|
||
|
|
'SMS_TREZ_SENDER_LINE',
|
||
|
|
'NODE_ENV',
|
||
|
|
]) {
|
||
|
|
delete process.env[key];
|
||
|
|
}
|
||
|
|
jest.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(() => {
|
||
|
|
process.env = { ...saved };
|
||
|
|
});
|
||
|
|
|
||
|
|
const configureFully = () => {
|
||
|
|
process.env.SMS_TREZ_USERNAME = 'u';
|
||
|
|
process.env.SMS_TREZ_PASSWORD = 'p';
|
||
|
|
process.env.SMS_TREZ_SENDER_LINE = '9998882797';
|
||
|
|
};
|
||
|
|
|
||
|
|
describe('smsConfig', () => {
|
||
|
|
it('defaults the provider to trez and the URL to the reference endpoint', () => {
|
||
|
|
const c = smsConfig();
|
||
|
|
expect(c.provider).toBe('trez');
|
||
|
|
expect(c.url).toBe('https://smspanel.trez.ir/SendMessageWithCode.ashx');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('lets the URL be overridden for staging', () => {
|
||
|
|
process.env.SMS_TREZ_URL = 'https://staging.example/send';
|
||
|
|
expect(smsConfig().url).toBe('https://staging.example/send');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('smsConfigured', () => {
|
||
|
|
it('is false when nothing is set', () => {
|
||
|
|
expect(smsConfigured()).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('is false when only some credentials are set', () => {
|
||
|
|
process.env.SMS_TREZ_USERNAME = 'u';
|
||
|
|
process.env.SMS_TREZ_PASSWORD = 'p';
|
||
|
|
expect(smsConfigured()).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('is true once all three are set', () => {
|
||
|
|
configureFully();
|
||
|
|
expect(smsConfigured()).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('is false when the provider is explicitly disabled', () => {
|
||
|
|
configureFully();
|
||
|
|
process.env.SMS_PROVIDER = 'none';
|
||
|
|
expect(smsConfigured()).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('assertSmsConfigured', () => {
|
||
|
|
it('only warns outside production when keys are missing', () => {
|
||
|
|
process.env.NODE_ENV = 'development';
|
||
|
|
expect(() => assertSmsConfigured(logger)).not.toThrow();
|
||
|
|
expect(logger.warn).toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('THROWS in production when keys are missing, naming them', () => {
|
||
|
|
process.env.NODE_ENV = 'production';
|
||
|
|
expect(() => assertSmsConfigured(logger)).toThrow(/SMS_TREZ_USERNAME/);
|
||
|
|
expect(() => assertSmsConfigured(logger)).toThrow(/SMS_TREZ_PASSWORD/);
|
||
|
|
expect(() => assertSmsConfigured(logger)).toThrow(/SMS_TREZ_SENDER_LINE/);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('passes in production once everything is set', () => {
|
||
|
|
process.env.NODE_ENV = 'production';
|
||
|
|
configureFully();
|
||
|
|
expect(() => assertSmsConfigured(logger)).not.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('THROWS in production when SMS is explicitly disabled', () => {
|
||
|
|
// Disabling SMS in production means nobody can log in; that must be a
|
||
|
|
// deliberate, visible failure rather than a silent one.
|
||
|
|
process.env.NODE_ENV = 'production';
|
||
|
|
process.env.SMS_PROVIDER = 'none';
|
||
|
|
expect(() => assertSmsConfigured(logger)).toThrow(/Refusing to start in production/);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('allows SMS to be disabled outside production', () => {
|
||
|
|
process.env.NODE_ENV = 'test';
|
||
|
|
process.env.SMS_PROVIDER = 'none';
|
||
|
|
expect(() => assertSmsConfigured(logger)).not.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rejects an unknown provider name outright', () => {
|
||
|
|
process.env.SMS_PROVIDER = 'kavenegar';
|
||
|
|
expect(() => assertSmsConfigured(logger)).toThrow(/Unknown SMS_PROVIDER/);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The OTP text must stay byte-identical to the reference
|
||
|
|
* (ApiMobileController.php:47) — the first line and the trailing token are the
|
||
|
|
* Android SMS Retriever hash, and autofill breaks if either changes.
|
||
|
|
*/
|
||
|
|
describe('OTP message template', () => {
|
||
|
|
const build = (code: string) =>
|
||
|
|
`<#> code is ${code}\n` +
|
||
|
|
`رمز عبور یکبار مصرف شما جهت ورود به اپلیکشن هنزا گلد ${code} می باشد\n` +
|
||
|
|
`aMo6iFyUQaY`;
|
||
|
|
|
||
|
|
it('matches the reference byte for byte', () => {
|
||
|
|
expect(build('1234')).toBe(
|
||
|
|
'<#> code is 1234\nرمز عبور یکبار مصرف شما جهت ورود به اپلیکشن هنزا گلد 1234 می باشد\naMo6iFyUQaY',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('keeps the SMS Retriever markers', () => {
|
||
|
|
const text = build('9876');
|
||
|
|
expect(text.startsWith('<#> code is ')).toBe(true);
|
||
|
|
expect(text.endsWith('\naMo6iFyUQaY')).toBe(true);
|
||
|
|
expect(text.split('\n')).toHaveLength(3);
|
||
|
|
});
|
||
|
|
});
|