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
179 changes: 165 additions & 14 deletions src/clips/nft-mint.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,74 @@ import { NftMintService } from './nft-mint.service';
import { IpfsUploadService } from '../nft/ipfs-upload.service';
import { ConfigService } from '../config/config.service';

// Mock the entire Stellar SDK so non-configurable class properties can be replaced
// ── Mock @stellar/stellar-sdk at module level so Contract() never validates ──
// Shared mock functions so tests can control return values directly.
const mockScValToNative = jest.fn();
const mockFromXDR = jest.fn().mockReturnValue({});

jest.mock('@stellar/stellar-sdk', () => {
const mockOp = { type: 'invokeHostFunction' };
const mockTx = { toXDR: jest.fn().mockReturnValue('xdr-string') };
const mockTx = {
toXDR: jest.fn().mockReturnValue('mock-xdr'),
sign: jest.fn(),
};
const mockBuilder = {
addOperation: jest.fn().mockReturnThis(),
setTimeout: jest.fn().mockReturnThis(),
build: jest.fn().mockReturnValue(mockTx),
};
const mockContract = { call: jest.fn().mockReturnValue(mockOp) };

// Build the shared shape used by both default and named exports
const sdkShape = {
rpc: {
Server: jest.fn().mockImplementation(() => ({
getAccount: jest.fn().mockResolvedValue({}),
simulateTransaction: jest.fn(),
})),
},
Contract: jest.fn().mockImplementation(() => ({
call: jest.fn().mockReturnValue({}),
})),
Account: jest.fn().mockImplementation(() => ({})),
TransactionBuilder: jest.fn().mockImplementation(() => mockBuilder),
TimeoutInfinite: 0,
Address: {
fromString: jest.fn().mockReturnValue({ toScVal: jest.fn().mockReturnValue({}) }),
},
nativeToScVal: jest.fn().mockReturnValue({}),
// Will be overwritten after module creation with the shared reference
scValToNative: jest.fn(),
xdr: {
ScVal: {
fromXDR: jest.fn().mockReturnValue({}),
},
},
};

return {
__esModule: true,
default: {
rpc: { Server: jest.fn() },
Contract: jest.fn().mockReturnValue(mockContract),
TransactionBuilder: jest.fn().mockReturnValue(mockBuilder),
Account: jest.fn().mockReturnValue({}),
Address: { fromString: jest.fn().mockReturnValue({ toScVal: jest.fn() }) },
nativeToScVal: jest.fn().mockReturnValue({}),
scValToNative: jest.fn(),
xdr: { ScVal: { fromXDR: jest.fn().mockReturnValue({}) } },
TimeoutInfinite: 0,
default: sdkShape,
...sdkShape,
};
});

// ── Shared mocks ──────────────────────────────────────────────────────────────

const VALID_WALLET = 'GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGMQ6NX4XUQN7Q6XHPVMUF';

const configMock = {
sorobanNftContractId: 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEU4',
platformWallet: VALID_WALLET,
platformRoyaltyBps: 100,
creatorRoyaltyBps: 1000,
};

// ── NftMintService uploadMetadataToIPFS ───────────────────────────────────────

describe('NftMintService uploadMetadataToIPFS', () => {
const prismaMock = {
clip: {
findUnique: jest.fn(),
update: jest.fn(),
},
};
});
Expand Down Expand Up @@ -355,3 +400,109 @@ describe('NftMintService.verifyNFTOwnership', () => {
expect(result.owned).toBe(false);
});
});

// ── NftMintService verifyNFTOwnership ─────────────────────────────────────────

describe('NftMintService verifyNFTOwnership', () => {
const prismaMock = { clip: { findUnique: jest.fn(), update: jest.fn() } };

const stellarMock = {
networkPassphrase: 'Test SDF Network ; September 2015',
rpcUrl: 'https://soroban-testnet.stellar.org',
network: 'testnet',
validateAddress: jest.fn().mockReturnValue({ valid: true }),
};

const metricsMock = { incrementNftMints: jest.fn() };

let circuitBreakerMock: { execute: jest.Mock };
let service: NftMintService;

// The service does `import StellarSdk from '@stellar/stellar-sdk'` (default import).
// With __esModule:true, require().default is what the service binds to.
// We control scValToNative via sdk.default.scValToNative.
let sdk: any;

beforeAll(() => {
sdk = require('@stellar/stellar-sdk');
});

beforeEach(() => {
jest.clearAllMocks();
sdk.default.xdr.ScVal.fromXDR.mockReturnValue({});
circuitBreakerMock = {
execute: jest.fn().mockImplementation((_config, fn) => fn()),
};
service = new NftMintService(
prismaMock as any,
stellarMock as any,
metricsMock as any,
circuitBreakerMock as any,
configMock as any,
);
});

it('returns owned=true when contract returns matching wallet address', async () => {
circuitBreakerMock.execute.mockImplementationOnce(async () => ({
error: null,
results: [{ xdr: 'AAAAAA==' }],
}));
sdk.default.scValToNative.mockReturnValue(VALID_WALLET);

const result = await service.verifyNFTOwnership('42', VALID_WALLET);
expect(result.owned).toBe(true);
expect(result.error).toBeUndefined();
});

it('returns owned=false when contract returns a different wallet address', async () => {
circuitBreakerMock.execute.mockImplementationOnce(async () => ({
error: null,
results: [{ xdr: 'AAAAAA==' }],
}));
sdk.default.scValToNative.mockReturnValue('GDIFFERENTADDRESS000000000000000000000000000000000000000');

const result = await service.verifyNFTOwnership('42', VALID_WALLET);
expect(result.owned).toBe(false);
expect(result.error).toMatch(/does not own/i);
});

it('returns owned=false with error when simulation returns an error field', async () => {
circuitBreakerMock.execute.mockImplementationOnce(async () => ({
error: 'Contract error',
results: [],
}));

const result = await service.verifyNFTOwnership('1', VALID_WALLET);
expect(result.owned).toBe(false);
expect(result.error).toContain('Simulation failed');
});

it('returns owned=false with error when simulation returns no results', async () => {
circuitBreakerMock.execute.mockImplementationOnce(async () => ({
error: null,
results: [],
}));

const result = await service.verifyNFTOwnership('1', VALID_WALLET);
expect(result.owned).toBe(false);
expect(result.error).toContain('No simulation results');
});

it('returns owned=false when circuit breaker throws ServiceUnavailableException', async () => {
circuitBreakerMock.execute.mockRejectedValueOnce(
Object.assign(new Error('Open'), { name: 'ServiceUnavailableException' }),
);

const result = await service.verifyNFTOwnership('1', VALID_WALLET);
expect(result.owned).toBe(false);
expect(result.error).toMatch(/temporarily unavailable/i);
});

it('returns owned=false and captures error message on unexpected error', async () => {
circuitBreakerMock.execute.mockRejectedValueOnce(new Error('Network timeout'));

const result = await service.verifyNFTOwnership('5', VALID_WALLET);
expect(result.owned).toBe(false);
expect(result.error).toContain('Network timeout');
});
});
6 changes: 0 additions & 6 deletions src/payouts/payouts.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
jest.mock('../stellar/stellar.service', () => ({
StellarService: jest.fn().mockImplementation(() => ({
horizonUrl: 'https://horizon-testnet.stellar.org',
networkPassphrase: 'Test SDF Network ; September 2015',
})),
}));

import { Test, TestingModule } from '@nestjs/testing';
import { getQueueToken } from '@nestjs/bullmq';
Expand Down
Loading
Loading