2026-09-22 08:08:50 +03:30
|
|
|
import { Logger } from '@nestjs/common';
|
|
|
|
|
import { assertSmsConfigured, smsConfig, smsConfigured } from './sms.service';
|
|
|
|
|
|
|
|
|
|
/**
|
2026-09-22 18:35:33 +03:30
|
|
|
* Covers the boot-time contract as REVISED by D20:
|
2026-09-22 08:08:50 +03:30
|
|
|
* * while no key is set, development behaviour is unchanged;
|
2026-09-22 18:35:33 +03:30
|
|
|
* * a missing key NEVER stops the boot, in any environment — it warns, and
|
|
|
|
|
* sending silently declines. (D14's "fail to boot in production" rule was
|
|
|
|
|
* removed by the owner for the delivery.)
|
2026-09-22 08:08:50 +03:30
|
|
|
*/
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-22 18:35:33 +03:30
|
|
|
/**
|
|
|
|
|
* D20 REVERSED the original boot contract. SMS must never stop the server
|
|
|
|
|
* from starting, in any environment — an earlier revision of these tests
|
|
|
|
|
* asserted the opposite (a throw in production) and is intentionally gone.
|
|
|
|
|
*/
|
|
|
|
|
describe('assertSmsConfigured (D20 — never throws)', () => {
|
|
|
|
|
it('warns outside production when keys are missing', () => {
|
2026-09-22 08:08:50 +03:30
|
|
|
process.env.NODE_ENV = 'development';
|
|
|
|
|
expect(() => assertSmsConfigured(logger)).not.toThrow();
|
|
|
|
|
expect(logger.warn).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-22 18:35:33 +03:30
|
|
|
it('warns in production when keys are missing, naming them, and still boots', () => {
|
2026-09-22 08:08:50 +03:30
|
|
|
process.env.NODE_ENV = 'production';
|
2026-09-22 18:35:33 +03:30
|
|
|
expect(() => assertSmsConfigured(logger)).not.toThrow();
|
|
|
|
|
const warned = (logger.warn as jest.Mock).mock.calls.flat().join(' ');
|
|
|
|
|
expect(warned).toMatch(/SMS_TREZ_USERNAME/);
|
|
|
|
|
expect(warned).toMatch(/SMS_TREZ_PASSWORD/);
|
|
|
|
|
expect(warned).toMatch(/SMS_TREZ_SENDER_LINE/);
|
2026-09-22 08:08:50 +03:30
|
|
|
});
|
|
|
|
|
|
2026-09-22 18:35:33 +03:30
|
|
|
it('passes silently in production once everything is set', () => {
|
2026-09-22 08:08:50 +03:30
|
|
|
process.env.NODE_ENV = 'production';
|
|
|
|
|
configureFully();
|
|
|
|
|
expect(() => assertSmsConfigured(logger)).not.toThrow();
|
2026-09-22 18:35:33 +03:30
|
|
|
expect(logger.warn).not.toHaveBeenCalled();
|
2026-09-22 08:08:50 +03:30
|
|
|
});
|
|
|
|
|
|
2026-09-22 18:35:33 +03:30
|
|
|
it('allows SMS to be explicitly disabled in production', () => {
|
2026-09-22 08:08:50 +03:30
|
|
|
process.env.NODE_ENV = 'production';
|
|
|
|
|
process.env.SMS_PROVIDER = 'none';
|
2026-09-22 18:35:33 +03:30
|
|
|
expect(() => assertSmsConfigured(logger)).not.toThrow();
|
|
|
|
|
expect(logger.warn).toHaveBeenCalled();
|
2026-09-22 08:08:50 +03:30
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('allows SMS to be disabled outside production', () => {
|
|
|
|
|
process.env.NODE_ENV = 'test';
|
|
|
|
|
process.env.SMS_PROVIDER = 'none';
|
|
|
|
|
expect(() => assertSmsConfigured(logger)).not.toThrow();
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-22 18:35:33 +03:30
|
|
|
it('warns about an unknown provider name instead of refusing to boot', () => {
|
2026-09-22 08:08:50 +03:30
|
|
|
process.env.SMS_PROVIDER = 'kavenegar';
|
2026-09-22 18:35:33 +03:30
|
|
|
expect(() => assertSmsConfigured(logger)).not.toThrow();
|
|
|
|
|
expect((logger.warn as jest.Mock).mock.calls.flat().join(' ')).toMatch(
|
|
|
|
|
/Unknown SMS_PROVIDER/,
|
|
|
|
|
);
|
2026-09-22 08:08:50 +03:30
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
});
|
|
|
|
|
});
|