Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions src/common/validators/stellar-address.validator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { validate } from 'class-validator';
import {
isValidStellarAddress,
IsStellarAddress,
} from './stellar-address.validator';

// The jest config maps @stellar/stellar-sdk to a lightweight mock that has no
// StrKey, so this spec substitutes a real StrKey-equivalent implementation
// (base32 + version byte + CRC16-XModem) instead of depending on the mock.
jest.mock('@stellar/stellar-sdk', () => {
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const crc16 = (bytes: number[]): number => {
let crc = 0;
for (const b of bytes) {
crc ^= b << 8;
for (let i = 0; i < 8; i++) {
crc =
crc & 0x8000 ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff;
}
}
return crc;
};
const decode = (s: string): number[] | null => {
let bits = 0;
let acc = 0;
const out: number[] = [];
for (const ch of s) {
const v = ALPHABET.indexOf(ch);
if (v < 0) return null;
acc = (acc << 5) | v;
bits += 5;
if (bits >= 8) {
bits -= 8;
out.push((acc >> bits) & 0xff);
}
}
return out;
};
return {
StrKey: {
isValidEd25519PublicKey: (s: string): boolean => {
if (s.length !== 56 || s[0] !== 'G') return false;
const bytes = decode(s);
if (!bytes || bytes.length !== 35) return false;
const payload = bytes.slice(0, 33);
const crc = crc16(payload);
return bytes[33] === (crc & 0xff) && bytes[34] === crc >> 8;
},
},
};
});

const VALID = 'GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN7';

class Dto {
@IsStellarAddress()
address!: unknown;
}

describe('isValidStellarAddress', () => {
it('accepts a valid G… public key', () => {
expect(isValidStellarAddress(VALID)).toBe(true);
});

it('rejects a key with a bad checksum', () => {
const bad = VALID.slice(0, -1) + (VALID.endsWith('A') ? 'B' : 'A');
expect(isValidStellarAddress(bad)).toBe(false);
});

it('rejects M… (muxed) and C… (contract) addresses', () => {
expect(isValidStellarAddress('M' + VALID.slice(1))).toBe(false);
expect(isValidStellarAddress('C' + VALID.slice(1))).toBe(false);
});

it('rejects a lowercase key', () => {
expect(isValidStellarAddress(VALID.toLowerCase())).toBe(false);
});

it('rejects wrong lengths and empty strings', () => {
expect(isValidStellarAddress(VALID.slice(0, -1))).toBe(false);
expect(isValidStellarAddress('')).toBe(false);
});

it.each([undefined, null, 123, {}, [], true])(
'rejects non-string %p',
(value) => {
expect(isValidStellarAddress(value)).toBe(false);
},
);
});

describe('@IsStellarAddress', () => {
it('passes a valid address', async () => {
const dto = Object.assign(new Dto(), { address: VALID });
expect(await validate(dto)).toHaveLength(0);
});

it('fails an invalid address with the property name in the message', async () => {
const dto = Object.assign(new Dto(), { address: 'nope' });
const errors = await validate(dto);
expect(errors).toHaveLength(1);
expect(errors[0].constraints?.isStellarAddress).toContain(
'address must be a valid Stellar public key',
);
});
});
97 changes: 97 additions & 0 deletions src/config/configuration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import configuration from './configuration';

const ENV_KEYS = [
'NODE_ENV',
'PORT',
'DATABASE_URL',
'DATABASE_SYNCHRONIZE',
'DATABASE_LOGGING',
'JWT_SECRET',
'GITHUB_CLIENT_ID',
'GITHUB_CLIENT_SECRET',
'GITHUB_WEBHOOK_SECRET',
'ANALYTICS_PLATFORM_SUMMARY_TTL_MS',
'ESCROW_CONTRACT_ID',
'MAINTENANCE_POOL_CONTRACT_ID',
'ESCROW_DEADLINE_SECONDS',
'USDC_TOKEN_CONTRACT_ID',
'XLM_TOKEN_CONTRACT_ID',
];

describe('configuration', () => {
const original = { ...process.env };

beforeEach(() => {
for (const key of ENV_KEYS) delete process.env[key];
});

afterAll(() => {
process.env = original;
});

it('falls back to defaults when nothing is set', () => {
const config = configuration();
expect(config.env).toBe('development');
expect(config.port).toBe(3000);
expect(config.database).toEqual({
url: 'postgresql://postgres:postgres@localhost:5432/mergefi',
synchronize: false,
logging: false,
});
expect(config.jwt.secret).toBe('insecure-dev-secret');
expect(config.github.webhookSecret).toBe('');
expect(config.analytics.platformSummaryTtlMs).toBe(60000);
expect(config.stellar.escrowDeadlineSeconds).toBe(7776000);
expect(config.stellar.assetContractIds).toEqual({ USDC: '', XLM: '' });
});

it('parses integers and booleans from the environment', () => {
process.env.PORT = '8080';
process.env.ESCROW_DEADLINE_SECONDS = '120';
process.env.DATABASE_SYNCHRONIZE = 'true';
process.env.DATABASE_LOGGING = 'false';
const config = configuration();
expect(config.port).toBe(8080);
expect(config.stellar.escrowDeadlineSeconds).toBe(120);
expect(config.database.synchronize).toBe(true);
expect(config.database.logging).toBe(false);
});

it('only treats the exact string "true" as a true boolean', () => {
process.env.DATABASE_SYNCHRONIZE = '1';
expect(configuration().database.synchronize).toBe(false);
});

it('keeps an empty-string env var where ?? is used, but falls back where || is used', () => {
process.env.JWT_SECRET = '';
process.env.GITHUB_CLIENT_ID = '';
const config = configuration();
expect(config.jwt.secret).toBe('');
expect(config.github.clientId).toBe('mock_client_id_12345');
});

it('yields NaN for a non-numeric PORT or ESCROW_DEADLINE_SECONDS', () => {
process.env.PORT = 'abc';
process.env.ESCROW_DEADLINE_SECONDS = 'soon';
const config = configuration();
expect(config.port).toBeNaN();
expect(config.stellar.escrowDeadlineSeconds).toBeNaN();
});

describe('maintenancePoolContractId', () => {
it('falls back to ESCROW_CONTRACT_ID when unset', () => {
process.env.ESCROW_CONTRACT_ID = 'CESCROW';
expect(configuration().stellar.maintenancePoolContractId).toBe('CESCROW');
});

it('prefers MAINTENANCE_POOL_CONTRACT_ID when set', () => {
process.env.ESCROW_CONTRACT_ID = 'CESCROW';
process.env.MAINTENANCE_POOL_CONTRACT_ID = 'CPOOL';
expect(configuration().stellar.maintenancePoolContractId).toBe('CPOOL');
});

it('is empty when neither is set', () => {
expect(configuration().stellar.maintenancePoolContractId).toBe('');
});
});
});
108 changes: 108 additions & 0 deletions src/config/validate-required-config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import configuration from './configuration';
import {
assertRequiredConfig,
collectConfigIssues,
DEFAULT_LOCAL_DATABASE_URL,
INSECURE_DEFAULT_JWT_SECRET,
} from './validate-required-config';

type Config = Parameters<typeof collectConfigIssues>[0];

const validProduction = (): Config => ({
...configuration(),
env: 'production',
database: {
url: 'postgresql://user:pw@db.internal:5432/mergefi',
synchronize: false,
logging: false,
},
jwt: { secret: 'a-long-random-secret', expiresIn: '7d' },
github: {
clientId: 'client-id',
clientSecret: 'client-secret',
oauthCallbackUrl: 'https://api.example.com/api/auth/github/callback',
apiToken: 'token',
webhookSecret: 'webhook-secret',
syncAllowedRepos: '',
},
stellar: {
...configuration().stellar,
escrowContractId: 'CESCROW',
treasurySecret: 'STREASURY',
},
});

const keys = (config: Config) => collectConfigIssues(config).map((i) => i.key);

describe('collectConfigIssues', () => {
it('is a no-op outside production, even with every secret missing', () => {
const config: Config = {
...configuration(),
env: 'development',
jwt: { secret: INSECURE_DEFAULT_JWT_SECRET, expiresIn: '7d' },
};
expect(collectConfigIssues(config)).toEqual([]);
expect(() => assertRequiredConfig(config)).not.toThrow();
});

it('reports nothing for a fully configured production environment', () => {
expect(collectConfigIssues(validProduction())).toEqual([]);
});

it.each([
['GITHUB_WEBHOOK_SECRET', (c: Config) => (c.github.webhookSecret = '')],
['GITHUB_CLIENT_ID', (c: Config) => (c.github.clientId = '')],
['GITHUB_CLIENT_SECRET', (c: Config) => (c.github.clientSecret = ' ')],
['ESCROW_CONTRACT_ID', (c: Config) => (c.stellar.escrowContractId = '')],
['TREASURY_SECRET', (c: Config) => (c.stellar.treasurySecret = '')],
['JWT_SECRET', (c: Config) => (c.jwt.secret = '')],
])('flags a missing %s', (key, mutate) => {
const config = validProduction();
mutate(config);
expect(keys(config)).toEqual([key]);
});

it('flags the insecure default JWT secret', () => {
const config = validProduction();
config.jwt.secret = INSECURE_DEFAULT_JWT_SECRET;
const issues = collectConfigIssues(config);
expect(issues).toHaveLength(1);
expect(issues[0].key).toBe('JWT_SECRET');
expect(issues[0].message).toContain('insecure dev default');
});

it('flags the default and an empty DATABASE_URL', () => {
const config = validProduction();
config.database.url = DEFAULT_LOCAL_DATABASE_URL;
expect(keys(config)).toEqual(['DATABASE_URL']);
config.database.url = '';
expect(keys(config)).toEqual(['DATABASE_URL']);
});

it('collects every problem at once', () => {
const config = validProduction();
config.jwt.secret = INSECURE_DEFAULT_JWT_SECRET;
config.database.url = DEFAULT_LOCAL_DATABASE_URL;
config.github.webhookSecret = '';
expect(keys(config)).toEqual([
'JWT_SECRET',
'DATABASE_URL',
'GITHUB_WEBHOOK_SECRET',
]);
});
});

describe('assertRequiredConfig', () => {
it('throws listing every issue in production', () => {
const config = validProduction();
config.jwt.secret = INSECURE_DEFAULT_JWT_SECRET;
config.stellar.treasurySecret = '';
expect(() => assertRequiredConfig(config)).toThrow(
/Refusing to start in production[\s\S]*JWT_SECRET[\s\S]*TREASURY_SECRET/,
);
});

it('does not throw for a valid production config', () => {
expect(() => assertRequiredConfig(validProduction())).not.toThrow();
});
});