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
5 changes: 3 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"format": "prettier --write \"src/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\"",
"typecheck": "tsc --noEmit",
"prepare": "husky"
"prepare": "husky || true"
},
"keywords": [
"stellar",
Expand Down Expand Up @@ -60,6 +60,7 @@
"@types/supertest": "^6.0.2",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"eslint": "^8.56.0",
Expand All @@ -72,6 +73,6 @@
"supertest": "^7.2.2",
"ts-jest": "^29.1.1",
"ts-node-dev": "^2.0.0",
"typescript": "^5.3.3"
"typescript": "^5.9.3"
}
}
116 changes: 116 additions & 0 deletions api/src/__tests__/credit.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import axios from 'axios';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
beforeAll(() => {
mockedAxios.create.mockReturnThis();
const axiosResponse = { data: {}, status: 200, statusText: 'OK', headers: {}, config: { url: '' } };
mockedAxios.get.mockResolvedValue(axiosResponse);
mockedAxios.post.mockResolvedValue(axiosResponse);
});
afterEach(() => { jest.clearAllMocks(); });

import { creditDelegationService } from '../services/credit-delegation';

const delegator = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF';
const delegate = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWJF';

describe('CreditDelegationService', () => {
let creditLineId: string;

it('creates a credit line', () => {
const creditLine = creditDelegationService.createCreditLine(
delegator, delegate, '1000000000', '500', '9999999999'
);
expect(creditLine.delegatorAddress).toBe(delegator);
expect(creditLine.delegateAddress).toBe(delegate);
expect(creditLine.maxAmount).toBe('1000000000');
expect(creditLine.status).toBe('active');
creditLineId = creditLine.id;
});

it('creates credit line with collateral', () => {
const cl = creditDelegationService.createCreditLine(
delegator, delegate, '500000000', '300', '9999999999', '200000000'
);
expect(cl.collateral).toBe('200000000');
});

it('draws from credit line', () => {
const draw = creditDelegationService.draw(creditLineId, delegate, '500000000');
expect(draw).not.toBeNull();
expect(draw!.amount).toBe('500000000');
});

it('rejects draw exceeding limit', () => {
expect(() => creditDelegationService.draw(creditLineId, delegate, '1000000000'))
.toThrow('exceeds available credit');
});

it('rejects draw by non-delegate', () => {
expect(() => creditDelegationService.draw(creditLineId, 'GAAAAAANOTALLOWED12345678901234567890123456789012', '1000000'))
.toThrow();
});

it('repays credit line', () => {
const repayment = creditDelegationService.repay(creditLineId, delegate, '200000000');
expect(repayment).not.toBeNull();
expect(repayment!.amount).toBe('200000000');
});

it('adjusts credit limit', () => {
const updated = creditDelegationService.adjustLimit(creditLineId, delegator, '2000000000');
expect(updated).not.toBeNull();
expect(updated!.maxAmount).toBe('2000000000');
});

it('transfers credit line', () => {
const newDelegator = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANEW';
const updated = creditDelegationService.transfer(creditLineId, delegator, newDelegator);
expect(updated).not.toBeNull();
expect(updated!.delegatorAddress).toBe(newDelegator);
expect(updated!.transferCount).toBe(1);
});

it('gets credit line by id', () => {
const cl = creditDelegationService.getCreditLine(creditLineId);
expect(cl).not.toBeNull();
expect(cl!.id).toBe(creditLineId);
});

it('returns null for unknown credit line', () => {
const cl = creditDelegationService.getCreditLine('unknown');
expect(cl).toBeNull();
});

it('lists credit lines by delegator', () => {
const lines = creditDelegationService.getCreditLinesByDelegator(delegator);
expect(Array.isArray(lines)).toBe(true);
});

it('lists credit lines by delegate', () => {
const lines = creditDelegationService.getCreditLinesByDelegate(delegate);
expect(Array.isArray(lines)).toBe(true);
});

it('returns draws for credit line', () => {
const draws = creditDelegationService.getDraws(creditLineId);
expect(Array.isArray(draws)).toBe(true);
expect(draws.length).toBeGreaterThan(0);
});

it('returns repayments for credit line', () => {
const repayments = creditDelegationService.getRepayments(creditLineId);
expect(Array.isArray(repayments)).toBe(true);
expect(repayments.length).toBeGreaterThan(0);
});

it('claims default on matured unpaid credit line', () => {
const cl = creditDelegationService.createCreditLine(delegator, delegate, '1000000000', '500', '100');
const stored = (creditDelegationService as any).creditLines.get(cl.id);
stored.drawnAmount = '500000000';
stored.status = 'drawn';
const defaulted = creditDelegationService.claimDefault(cl.id, delegator);
expect(defaulted).not.toBeNull();
expect(defaulted!.status).toBe('defaulted');
});
});
103 changes: 103 additions & 0 deletions api/src/__tests__/dispute.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import axios from 'axios';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
beforeAll(() => {
mockedAxios.create.mockReturnThis();
const axiosResponse = { data: {}, status: 200, statusText: 'OK', headers: {}, config: { url: '' } };
mockedAxios.get.mockResolvedValue(axiosResponse);
mockedAxios.post.mockResolvedValue(axiosResponse);
});
afterEach(() => { jest.clearAllMocks(); });

import { disputeResolutionService } from '../services/dispute-resolution';

const disputer = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF';
const liquidator = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWJF';
const juror1 = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABC';
const juror2 = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADEF';
const juror3 = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGHI';
const juror4 = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJKL';
const juror5 = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMNO';

describe('DisputeResolutionService', () => {
let disputeId: string;

it('registers jurors', () => {
disputeResolutionService.registerJuror(juror1);
disputeResolutionService.registerJuror(juror2);
disputeResolutionService.registerJuror(juror3);
disputeResolutionService.registerJuror(juror4);
disputeResolutionService.registerJuror(juror5);
expect(true).toBe(true);
});

it('files a dispute', () => {
const dispute = disputeResolutionService.fileDispute(
disputer, liquidator, 'tx_hash_1', '1000000000', 'initial evidence', '10000000'
);
expect(dispute.disputerAddress).toBe(disputer);
expect(dispute.status).toBe('filing');
expect(dispute.disputeFee).toBe('10000000');
disputeId = dispute.id;
});

it('rejects dispute fee below minimum', () => {
expect(() => disputeResolutionService.fileDispute(
disputer, liquidator, 'tx_hash_2', '1000000000', 'evidence', '100'
)).toThrow();
});

it('submits evidence', () => {
const evidence = disputeResolutionService.submitEvidence(
disputeId, disputer, 'Additional evidence', 'more_data'
);
expect(evidence).not.toBeNull();
expect(evidence!.description).toBe('Additional evidence');
});

it('selects jurors', () => {
const jurors = disputeResolutionService.selectJurors(disputeId);
expect(jurors).not.toBeNull();
expect(jurors!.length).toBe(5);
expect(disputeResolutionService.getDispute(disputeId)!.status).toBe('voting');
});

it('casts votes and resolves', () => {
disputeResolutionService.castVote(disputeId, juror1, 'valid', 'Evidence supports validity');
disputeResolutionService.castVote(disputeId, juror2, 'valid', 'Agree');
disputeResolutionService.castVote(disputeId, juror3, 'valid', 'Liquidation was proper');

const dispute = disputeResolutionService.getDispute(disputeId)!;
expect(dispute.votes.length).toBe(3);
});

it('resolves dispute with majority', () => {
disputeResolutionService.castVote(disputeId, juror4, 'valid', 'Concur');
const dispute = disputeResolutionService.getDispute(disputeId)!;
expect(dispute.status).toBe('resolved');
expect(dispute.resolution).toBe('valid');
});

it('allows appeal', () => {
const appealed = disputeResolutionService.appeal(disputeId, disputer, '20000000');
expect(appealed).not.toBeNull();
expect(appealed!.status).toBe('filing');
expect(appealed!.appealParentId).toBe(disputeId);
});

it('gets dispute by id', () => {
const dispute = disputeResolutionService.getDispute(disputeId);
expect(dispute).not.toBeNull();
expect(dispute!.id).toBe(disputeId);
});

it('returns null for unknown dispute', () => {
const dispute = disputeResolutionService.getDispute('unknown');
expect(dispute).toBeNull();
});

it('lists disputes by user', () => {
const disputes = disputeResolutionService.getDisputesByUser(disputer);
expect(disputes.length).toBeGreaterThan(0);
});
});
101 changes: 101 additions & 0 deletions api/src/__tests__/notification.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import axios from 'axios';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
beforeAll(() => {
mockedAxios.create.mockReturnThis();
const axiosResponse = { data: {}, status: 200, statusText: 'OK', headers: {}, config: { url: '' } };
mockedAxios.get.mockResolvedValue(axiosResponse);
mockedAxios.post.mockResolvedValue(axiosResponse);
});
afterEach(() => { jest.clearAllMocks(); });

import { notificationEngine } from '../services/notification-engine';

const userId = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF';

describe('NotificationEngine', () => {
it('creates subscriptions', () => {
const prefs = notificationEngine.subscribe(
userId, 'email', 'user@example.com',
['health_factor_low', 'approaching_liquidation']
);
expect(prefs).toHaveLength(2);
expect(prefs[0]!.channel).toBe('email');
expect(prefs[0]!.enabled).toBe(true);
});

it('returns preferences', () => {
const prefs = notificationEngine.getPreferences(userId);
expect(prefs.length).toBeGreaterThan(0);
});

it('updates preferences', () => {
const updated = notificationEngine.updatePreference(
userId, 'email', 'health_factor_low', false, 150
);
expect(updated).not.toBeNull();
expect(updated!.enabled).toBe(false);
expect(updated!.threshold).toBe(150);
});

it('sends alerts to subscribed channels', async () => {
const messages = await notificationEngine.sendAlert(
userId, 'health_factor_low',
{ healthFactor: '1.2', collateralValue: '1000', debtValue: '800' }
);
expect(Array.isArray(messages)).toBe(true);
});

it('respects rate limiting', async () => {
const messages1 = await notificationEngine.sendAlert(
userId, 'health_factor_low',
{ healthFactor: '1.1', collateralValue: '1000', debtValue: '900' }
);
const messages2 = await notificationEngine.sendAlert(
userId, 'health_factor_low',
{ healthFactor: '1.0', collateralValue: '1000', debtValue: '1000' }
);
expect(messages2.length).toBeLessThanOrEqual(messages1.length);
});

it('returns notification history', () => {
const result = notificationEngine.getHistory(userId);
expect(result.messages).toBeDefined();
expect(Array.isArray(result.messages)).toBe(true);
});

it('filters history by alert type', () => {
const result = notificationEngine.getHistory(userId, { alertType: 'price_alert' });
expect(result.messages.every(m => m.alertType === 'price_alert')).toBe(true);
});

it('marks messages as delivered', () => {
const result = notificationEngine.getHistory(userId);
if (result.messages.length > 0) {
notificationEngine.markDelivered(result.messages[0]!.id);
const updated = notificationEngine.getHistory(userId);
const msg = updated.messages.find(m => m.id === result.messages[0]!.id);
expect(msg).toBeDefined();
}
});

it('marks messages as read', () => {
const result = notificationEngine.getHistory(userId);
if (result.messages.length > 0) {
notificationEngine.markRead(result.messages[0]!.id);
}
});

it('handles unknown alert types gracefully', async () => {
const messages = await notificationEngine.sendAlert(
userId, 'price_alert' as any,
{ asset: 'XLM', direction: 'up', changePercent: '5', currentPrice: '0.5', threshold: '0.45' }
);
expect(Array.isArray(messages)).toBe(true);
});

it('returns empty preferences for unknown user', () => {
const prefs = notificationEngine.getPreferences('unknown');
expect(prefs).toEqual([]);
});
});
Loading
Loading