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
164 changes: 164 additions & 0 deletions __tests__/contract/role-detection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, vi, beforeEach } from 'vitest';

const { mockServer } = vi.hoisted(() => {
const mockServer: any = {
getHealth: vi.fn(() => Promise.resolve({ status: 'healthy' })),
simulateTransaction: vi.fn(),
};
return { mockServer };
});

vi.mock('@stellar/stellar-sdk', async (importOriginal) => {
const actual = await importOriginal<typeof import('@stellar/stellar-sdk')>();
function MockRpcServer() {
return mockServer;
}
return {
...actual,
rpc: {
Server: MockRpcServer,
Api: {
isSimulationSuccess: vi.fn((res: any) => Boolean(res?.result?.retval !== undefined)),
},
},
scValToNative: vi.fn((val: any) => val),
nativeToScVal: vi.fn((val: any) => val),
Address: {
fromString: vi.fn(() => ({
toScVal: vi.fn(() => ({})),
})),
},
TransactionBuilder: vi.fn(function (this: any) {
this.addOperation = vi.fn().mockReturnThis();
this.setTimeout = vi.fn().mockReturnThis();
this.build = vi.fn(() => ({}));
}),
Operation: {
invokeContractFunction: vi.fn(() => ({})),
},
Account: vi.fn(function (this: any, address: string) {
this.accountId = () => address;
}),
BASE_FEE: '100',
};
});

vi.mock('@/constants', () => ({
CONTRACT_ID: 'CCONTRACTIDTEST000000000000000000000000000000000000000000',
NETWORK_PASSPHRASE: 'Test SDF Network ; September 2015',
RPC_URL: 'https://soroban-testnet.stellar.org',
TESTNET_USDC_TOKEN_ID: 'CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75',
TESTNET_EURC_TOKEN_ID: 'CDTKPWPLOURQA2SGTKTUQOWRCBZEORB4BWBOMJ3D3ZTQQSGE5F6JBQLV',
TESTNET_XLM_TOKEN_ID: 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC',
}));

import {
getWalletRoles,
listInvoicesBySubmitter,
listInvoicesByPayer,
listInvoicesByLp,
} from '@/utils/soroban';

describe('Wallet Role Detection Integration Tests', () => {
const TEST_ADDR = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF';

beforeEach(() => {
vi.clearAllMocks();
});

it('detects freelancer role when list_invoices_by_submitter returns invoices', async () => {
mockServer.simulateTransaction.mockImplementation(async () => {
return {
result: {
retval: [
{
id: 1n,
freelancer: TEST_ADDR,
payer: 'GPAYER123',
amount: 1000n,
due_date: 1700000000n,
discount_rate: 200,
status: { Pending: {} },
},
],
},
};
});

const submitterInvoices = await listInvoicesBySubmitter(TEST_ADDR);
const payerInvoices = await listInvoicesByPayer(TEST_ADDR);
const lpInvoices = await listInvoicesByLp(TEST_ADDR);
expect(submitterInvoices.length).toBe(1);
expect(payerInvoices.length).toBe(1);
expect(lpInvoices.length).toBe(1);

const roles = await getWalletRoles(TEST_ADDR);
expect(roles).toContain('freelancer');
});

it('detects multiple roles (freelancer, payer, lp) when all view functions return invoices', async () => {
mockServer.simulateTransaction.mockImplementation(async () => {
return {
result: {
retval: [
{
id: 101n,
freelancer: TEST_ADDR,
payer: TEST_ADDR,
funder: TEST_ADDR,
amount: 5000n,
due_date: 1700000000n,
discount_rate: 150,
status: { Funded: {} },
},
],
},
};
});

const roles = await getWalletRoles(TEST_ADDR);
expect(roles).toContain('freelancer');
expect(roles).toContain('payer');
expect(roles).toContain('lp');
});

it('returns empty roles array when user has no invoices across views', async () => {
mockServer.simulateTransaction.mockImplementation(async () => {
return {
result: {
retval: [],
},
};
});

const roles = await getWalletRoles('GUSERWITHNOINVOICES');
expect(roles).toEqual([]);
});

it('falls back gracefully to table scan if dedicated contract view functions fail', async () => {
let callCount = 0;
mockServer.simulateTransaction.mockImplementation(async () => {
callCount++;
if (callCount <= 4) {
return { result: null };
}
return {
result: {
retval: {
id: 1n,
freelancer: TEST_ADDR,
payer: 'GPAYER',
amount: 100n,
due_date: 1700000000n,
discount_rate: 100,
status: 'Pending',
},
},
};
});

const roles = await getWalletRoles(TEST_ADDR);
expect(roles).toContain('freelancer');
});
});
24 changes: 9 additions & 15 deletions __tests__/contract/soroban-extended.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,9 @@ describe('soroban – getWalletRoles', () => {
due_date: 1n,
discount_rate: 100,
};
(scValToNative as any).mockReturnValueOnce(inv);
(rpc.Api.isSimulationSuccess as any).mockReturnValueOnce(true).mockReturnValueOnce(false);
mockServer.simulateTransaction
.mockResolvedValueOnce({ result: { retval: {} } })
.mockResolvedValueOnce({ error: 'fail' });
(scValToNative as any).mockReturnValue([inv]);
(rpc.Api.isSimulationSuccess as any).mockReturnValue(true);
mockServer.simulateTransaction.mockResolvedValue({ result: { retval: [inv] } });
const roles = await getWalletRoles(ADDR);
expect(roles).toContain('freelancer');
});
Expand Down Expand Up @@ -730,11 +728,9 @@ describe('soroban – getWalletRoles extended', () => {
due_date: 1n,
discount_rate: 100,
};
(scValToNative as any).mockReturnValueOnce(inv);
(rpc.Api.isSimulationSuccess as any).mockReturnValueOnce(true).mockReturnValueOnce(false);
mockServer.simulateTransaction
.mockResolvedValueOnce({ result: { retval: {} } })
.mockResolvedValueOnce({ error: 'fail' });
(scValToNative as any).mockReturnValue([inv]);
(rpc.Api.isSimulationSuccess as any).mockReturnValue(true);
mockServer.simulateTransaction.mockResolvedValue({ result: { retval: [inv] } });
const roles = await getWalletRoles(ADDR);
expect(roles).toContain('payer');
});
Expand All @@ -751,11 +747,9 @@ describe('soroban – getWalletRoles extended', () => {
due_date: 1n,
discount_rate: 100,
};
(scValToNative as any).mockReturnValueOnce(inv);
(rpc.Api.isSimulationSuccess as any).mockReturnValueOnce(true).mockReturnValueOnce(false);
mockServer.simulateTransaction
.mockResolvedValueOnce({ result: { retval: {} } })
.mockResolvedValueOnce({ error: 'fail' });
(scValToNative as any).mockReturnValue([inv]);
(rpc.Api.isSimulationSuccess as any).mockReturnValue(true);
mockServer.simulateTransaction.mockResolvedValue({ result: { retval: [inv] } });
const roles = await getWalletRoles(ADDR);
expect(roles).toContain('lp');
});
Expand Down
59 changes: 59 additions & 0 deletions __tests__/feature-flags-no-render.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import OracleBadge from '../src/components/OracleBadge';
import InvoiceNftCard from '../src/components/InvoiceNftCard';
import InsurancePoolPanel from '../src/components/InsurancePoolPanel';

vi.mock('../src/hooks/useInvoiceNft', () => ({
useInvoiceNft: vi.fn(() => ({ state: null, loading: false, reload: vi.fn() })),
}));

vi.mock('../src/hooks/useInsurance', () => ({
useInsurance: vi.fn(() => ({
poolInfo: null,
isEnrolled: false,
isLoading: false,
refresh: vi.fn(),
})),
}));

vi.mock('../src/hooks/useTransaction', () => ({
useTransaction: vi.fn(() => ({ execute: vi.fn() })),
}));

vi.mock('../src/hooks/useApprovedTokens', () => ({
useApprovedTokens: vi.fn(() => ({ defaultToken: { symbol: 'USDC', decimals: 7 } })),
}));

vi.mock('../src/context/WalletContext', () => ({
useWallet: () => ({ address: 'GCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC6' }),
}));

describe('Feature Flags Graceful No-Render Verification', () => {
beforeEach(() => {
vi.stubEnv('NEXT_PUBLIC_ORACLE_ENABLED', 'false');
vi.stubEnv('NEXT_PUBLIC_NFT_ENABLED', 'false');
vi.stubEnv('NEXT_PUBLIC_INSURANCE_POOL_ENABLED', 'false');
});

it('renders nothing when NEXT_PUBLIC_ORACLE_ENABLED is false', () => {
const { container } = render(<OracleBadge verified={true} />);
expect(container.firstChild).toBeNull();
expect(screen.queryByText(/Oracle Verified/i)).not.toBeInTheDocument();
});

it('renders nothing when NEXT_PUBLIC_NFT_ENABLED is false', () => {
const { container } = render(
<InvoiceNftCard invoiceId={1n} invoiceStatus="Pending" walletAddress={null} />
);
expect(container.firstChild).toBeNull();
expect(screen.queryByText(/Invoice NFT/i)).not.toBeInTheDocument();
});

it('renders nothing when NEXT_PUBLIC_INSURANCE_POOL_ENABLED is false', () => {
const { container } = render(<InsurancePoolPanel />);
expect(container.firstChild).toBeNull();
expect(screen.queryByText(/Default Protection/i)).not.toBeInTheDocument();
});
});
Loading
Loading