Skip to content
Closed

Tests #384

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
25 changes: 24 additions & 1 deletion src/indexer/worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as checkpoint from './checkpoint.js';
import * as contracts from './contracts.js';
import * as rpc from '../stellar/rpc.js';
import { startIndexer } from './worker.js';
import { pollOnce, startIndexer } from './worker.js';

vi.mock('./checkpoint.js');
vi.mock('../stellar/rpc.js');
Expand Down Expand Up @@ -38,4 +38,27 @@ describe('startIndexer', () => {
await vi.runAllTimersAsync();
expect(vi.mocked(rpc.getLatestLedgerSequence).mock.calls.length).toBe(callsBefore);
});

it('recovers when the configured START_LEDGER is outside the RPC retention window', async () => {
vi.spyOn(console, 'warn').mockImplementation(() => {});
vi.mocked(checkpoint.getCheckpointState).mockResolvedValue({
lastLedger: 500,
lastEventId: undefined,
});
vi.mocked(rpc.getLatestLedgerSequence).mockResolvedValue(10000);
vi.mocked(rpc.rpcServer.getEvents).mockRejectedValueOnce({
message: 'startLedger must be within the ledger range [9000, 10000]',
});

const handler = vi.fn();
await pollOnce(handler);

expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining('checkpoint 500 has aged out'),
);
expect(checkpoint.saveCheckpoint).toHaveBeenCalledWith(10000);
expect(handler).not.toHaveBeenCalled();

vi.mocked(console.warn).mockRestore();
});
});
73 changes: 73 additions & 0 deletions test/indexer/donationVault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,4 +504,77 @@ describe('handleDonationVaultEvent', () => {
});
expect(deadLetter).toBeNull();
});

it('writes a StreamEvent row for withdraw with correct type, streamId, ledger, txHash, and payload', async () => {
const donorRow = await prisma.donor.create({ data: { address: fakeAddress('Y') } });
const ngoRow = await prisma.ngo.create({
data: { ownerAddress: fakeAddress('Z'), name: 'NGO Z' },
});
await prisma.stream.create({
data: {
onChainId: 70n,
donorId: donorRow.id,
ngoId: ngoRow.id,
tokenAddress: fakeAddress('AA'),
rate: '10',
balance: '1000',
withdrawn: '0',
status: 'ACTIVE',
},
});

const txHash = 'd4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5';
const event = makeEvent([symbolScVal('withdraw'), u64ScVal(70n)], i128ScVal(300n), {
ledger: 45678,
txHash,
});
await handleDonationVaultEvent(event);

const streamEvent = await prisma.streamEvent.findFirst({
where: { type: 'withdraw', streamId: 70n },
});
expect(streamEvent).not.toBeNull();
expect(streamEvent?.type).toBe('withdraw');
expect(streamEvent?.streamId).toBe(70n);
expect(streamEvent?.ledger).toBe(45678);
expect(streamEvent?.txHash).toBe(txHash);
expect(streamEvent?.payload).toMatchObject({ accrued: '300' });
});

it('writes a StreamEvent row for cancel with correct type, streamId, ledger, txHash, and payload', async () => {
const donorRow = await prisma.donor.create({ data: { address: fakeAddress('AB') } });
const ngoRow = await prisma.ngo.create({
data: { ownerAddress: fakeAddress('AC'), name: 'NGO AC' },
});
await prisma.stream.create({
data: {
onChainId: 80n,
donorId: donorRow.id,
ngoId: ngoRow.id,
tokenAddress: fakeAddress('AD'),
rate: '15',
balance: '2000',
withdrawn: '500',
status: 'ACTIVE',
},
});

const txHash = 'e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6';
const event = makeEvent(
[symbolScVal('cancel'), u64ScVal(80n)],
xdr.ScVal.scvVec([i128ScVal(600n), i128ScVal(1400n)]),
{ ledger: 56789, txHash },
);
await handleDonationVaultEvent(event);

const streamEvent = await prisma.streamEvent.findFirst({
where: { type: 'cancel', streamId: 80n },
});
expect(streamEvent).not.toBeNull();
expect(streamEvent?.type).toBe('cancel');
expect(streamEvent?.streamId).toBe(80n);
expect(streamEvent?.ledger).toBe(56789);
expect(streamEvent?.txHash).toBe(txHash);
expect(streamEvent?.payload).toMatchObject({ accrued: '600', refund: '1400' });
});
});
124 changes: 123 additions & 1 deletion test/indexer/ngoRegistry.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { afterEach, describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';

vi.mock('../../src/notifications/service.js', () => ({ dispatchNotification: vi.fn() }));

import { prisma } from '../../src/db.js';
import { handleNgoRegistryEvent } from '../../src/indexer/handlers/ngoRegistry.js';
import { dispatchNotification } from '../../src/notifications/service.js';
import { fakeAddress, resetDb } from '../helpers/db.js';
import { addressScVal, makeEvent, stringScVal, symbolScVal } from '../helpers/events.js';

describe('handleNgoRegistryEvent', () => {
afterEach(async () => {
vi.mocked(dispatchNotification).mockClear();
await resetDb();
});

Expand Down Expand Up @@ -76,4 +80,122 @@ describe('handleNgoRegistryEvent', () => {
ngo = await prisma.ngo.findUnique({ where: { ownerAddress: owner } });
expect(ngo?.verified).toBe(false);
});

it('dispatches ngo_approved notification for approved event', async () => {
const owner = fakeAddress('E');
await prisma.ngo.create({ data: { ownerAddress: owner, name: 'Save the Whales' } });

const event = makeEvent([symbolScVal('approved'), addressScVal(owner)], stringScVal(''));
await handleNgoRegistryEvent(event);

const ngo = await prisma.ngo.findUnique({ where: { ownerAddress: owner } });
expect(ngo?.verified).toBe(true);

expect(dispatchNotification).toHaveBeenCalledWith({
type: 'ngo_approved',
ownerAddress: owner,
ngoId: ngo!.id,
eventId: event.id,
});
});

it('dispatches ngo_revoked notification for revoked event', async () => {
const owner = fakeAddress('F');
await prisma.ngo.create({ data: { ownerAddress: owner, name: 'Green Earth', verified: true } });

const event = makeEvent([symbolScVal('revoked'), addressScVal(owner)], stringScVal(''));
await handleNgoRegistryEvent(event);

const ngo = await prisma.ngo.findUnique({ where: { ownerAddress: owner } });
expect(ngo?.verified).toBe(false);

expect(dispatchNotification).toHaveBeenCalledWith({
type: 'ngo_revoked',
ownerAddress: owner,
ngoId: ngo!.id,
eventId: event.id,
});
});

it('does not dispatch notification for register event', async () => {
const owner = fakeAddress('G');

await handleNgoRegistryEvent(
makeEvent([symbolScVal('register'), addressScVal(owner)], stringScVal('Ocean Cleanup')),
);

const ngo = await prisma.ngo.findUnique({ where: { ownerAddress: owner } });
expect(ngo?.name).toBe('Ocean Cleanup');
expect(ngo?.verified).toBe(false);
expect(dispatchNotification).not.toHaveBeenCalled();
});

it('writes a StreamEvent row for register with correct type, payload, ledger, and txHash', async () => {
const owner = fakeAddress('H');
const txHash = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2';

const event = makeEvent(
[symbolScVal('register'), addressScVal(owner)],
stringScVal('Wildlife Fund'),
{ ledger: 12345, txHash },
);
await handleNgoRegistryEvent(event);

const streamEvent = await prisma.streamEvent.findFirst({ where: { type: 'register' } });
expect(streamEvent).not.toBeNull();
expect(streamEvent?.type).toBe('register');
expect(streamEvent?.streamId).toBeNull();
expect(streamEvent?.ledger).toBe(12345);
expect(streamEvent?.txHash).toBe(txHash);
expect(streamEvent?.payload).toMatchObject({
ownerAddress: owner,
name: 'Wildlife Fund',
});
});

it('writes a StreamEvent row for approved with correct type, payload, ledger, and txHash', async () => {
const owner = fakeAddress('I');
const txHash = 'b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3';

await prisma.ngo.create({ data: { ownerAddress: owner, name: 'Health Initiative' } });

const event = makeEvent([symbolScVal('approved'), addressScVal(owner)], stringScVal(''), {
ledger: 23456,
txHash,
});
await handleNgoRegistryEvent(event);

const streamEvent = await prisma.streamEvent.findFirst({ where: { type: 'approved' } });
expect(streamEvent).not.toBeNull();
expect(streamEvent?.type).toBe('approved');
expect(streamEvent?.streamId).toBeNull();
expect(streamEvent?.ledger).toBe(23456);
expect(streamEvent?.txHash).toBe(txHash);
expect(streamEvent?.payload).toMatchObject({
ownerAddress: owner,
});
});

it('writes a StreamEvent row for revoked with correct type, payload, ledger, and txHash', async () => {
const owner = fakeAddress('J');
const txHash = 'c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4';

await prisma.ngo.create({ data: { ownerAddress: owner, name: 'Education Fund', verified: true } });

const event = makeEvent([symbolScVal('revoked'), addressScVal(owner)], stringScVal(''), {
ledger: 34567,
txHash,
});
await handleNgoRegistryEvent(event);

const streamEvent = await prisma.streamEvent.findFirst({ where: { type: 'revoked' } });
expect(streamEvent).not.toBeNull();
expect(streamEvent?.type).toBe('revoked');
expect(streamEvent?.streamId).toBeNull();
expect(streamEvent?.ledger).toBe(34567);
expect(streamEvent?.txHash).toBe(txHash);
expect(streamEvent?.payload).toMatchObject({
ownerAddress: owner,
});
});
});
Loading
Loading