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
2 changes: 2 additions & 0 deletions harvest-finance/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import { CreateInsurance1700000000009 } from './database/migrations/170000000000
import { AddInsuranceNotificationType1700000000010 } from './database/migrations/1700000000010-AddInsuranceNotificationType';
import { CreateSorobanEvents1700000000011 } from './database/migrations/1700000000011-CreateSorobanEvents';
import { CreateYieldAnalytics1700000000012 } from './database/migrations/1700000000012-CreateYieldAnalytics';
import { AddSorobanEventQueryIndexes1700000000013 } from './database/migrations/1700000000013-AddSorobanEventQueryIndexes';

@Module({
imports: [
Expand Down Expand Up @@ -124,6 +125,7 @@ import { CreateYieldAnalytics1700000000012 } from './database/migrations/1700000
AddInsuranceNotificationType1700000000010,
CreateSorobanEvents1700000000011,
CreateYieldAnalytics1700000000012,
AddSorobanEventQueryIndexes1700000000013,
],
synchronize: false,
migrationsRun: false,
Expand Down
4 changes: 2 additions & 2 deletions harvest-finance/backend/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ describe('AuthController', () => {
id: 'stellar-user-id',
stellarAddress:
'GD5DJQDQKG6GSUWQJQGQKQ5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q',
role: 'USER',
role: UserRole.FARMER,
firstName: 'Stellar',
lastName: 'User',
};
Expand All @@ -257,7 +257,7 @@ describe('AuthController', () => {
id: 'stellar-user-id',
stellar_address:
'GD5DJQDQKG6GSUWQJQGQKQ5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q',
role: 'USER',
role: UserRole.FARMER,
full_name: 'Stellar User',
},
});
Expand Down
2 changes: 1 addition & 1 deletion harvest-finance/backend/src/auth/dto/stellar-auth.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class StellarAuthResponseDto {
id: 'uuid-string',
stellar_address:
'GC5X3FML4S25HDAMJYZJYAC3CKLDWV2Z6YPV3IZXOSHSQKNSKUNQFXQN',
role: 'USER',
role: 'FARMER',
full_name: 'Stellar User',
},
})
Expand Down
22 changes: 11 additions & 11 deletions harvest-finance/backend/src/auth/stellar.integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Repository } from 'typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { StellarStrategy } from './strategies/stellar.strategy';
import { User } from '../database/entities/user.entity';
import { User, UserRole } from '../database/entities/user.entity';
import * as StellarSdk from '@stellar/stellar-sdk';
import { StellarChallengeDto, StellarVerifyDto } from './dto/stellar-auth.dto';

Expand All @@ -16,12 +16,12 @@ describe('Stellar Authentication Integration', () => {
let userRepository: jest.Mocked<Repository<User>>;

const testServerSecret =
'SBX7SARQOFS6IM2HS2N5TVK54AEF55E3FHOXBTWA6IPEEJJ4W5WJWE6W';
'SAKIA7YOPW5G2SSLLGEELDJ7SZPOS6X4GZWKOQYYUY7IL6FI6N7WP6RE';
const testNetworkPassphrase = 'Test SDF Network ; September 2015';
const testClientSecret =
'SCZANGBAZEY5BOOEO6SCKZ3SPNGE6US4QOANF3XRGA4Q2BMVIQZB4H7Q';
'SCZW6PF5EUSR6FCFKRMPB52HHOI5BQXN256AUG356GTNJXHQAC5ALT6D';
const testClientPublicKey =
'GD5DJQDQKG6GSUWQJQGQKQ5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q';
'GASVN2GNYP2DZCASZ6MOPS3RO26UIT6ZKH4ORN2FKTIQFD5P5OUKJPFW';

beforeAll(async () => {
const mockConfigService = {
Expand Down Expand Up @@ -130,9 +130,9 @@ describe('Stellar Authentication Integration', () => {
const newUser = {
id: 'new-user-id',
stellarAddress: testClientPublicKey,
email: null,
password: null,
role: 'USER',
email: '',
password: '',
role: UserRole.FARMER,
firstName: 'Stellar',
lastName: 'User',
isActive: true,
Expand Down Expand Up @@ -160,7 +160,7 @@ describe('Stellar Authentication Integration', () => {
);
expect(authResponse).toHaveProperty('user');
expect(authResponse.user.stellar_address).toBe(testClientPublicKey);
expect(authResponse.user.role).toBe('USER');
expect(authResponse.user.role).toBe(UserRole.FARMER);
expect(authResponse.user.full_name).toBe('Stellar User');

// Verify user was created
Expand All @@ -169,9 +169,9 @@ describe('Stellar Authentication Integration', () => {
});
expect(userRepository.create).toHaveBeenCalledWith({
stellarAddress: testClientPublicKey,
email: null,
password: null,
role: 'USER',
email: '',
password: '',
role: UserRole.FARMER,
firstName: 'Stellar',
lastName: 'User',
isActive: true,
Expand Down
180 changes: 71 additions & 109 deletions harvest-finance/backend/src/auth/strategies/stellar.strategy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ConfigService } from '@nestjs/config';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { StellarStrategy } from './stellar.strategy';
import { User } from '../../database/entities/user.entity';
import { User, UserRole } from '../../database/entities/user.entity';
import * as StellarSdk from '@stellar/stellar-sdk';

describe('StellarStrategy', () => {
Expand All @@ -23,7 +23,9 @@ describe('StellarStrategy', () => {
};

const testServerSecret =
'SBX7SARQOFS6IM2HS2N5TVK54AEF55E3FHOXBTWA6IPEEJJ4W5WJWE6W';
'SAKIA7YOPW5G2SSLLGEELDJ7SZPOS6X4GZWKOQYYUY7IL6FI6N7WP6RE';
const testServerPublicKey =
'GB26SVHUCWUATM5KXLYXD4TSLY7HP62RJYUMV5A7UZYY3QIRWY62XVEB';
const testNetworkPassphrase = 'Test SDF Network ; September 2015';
const testClientKeypair = StellarSdk.Keypair.random();
const testClientPublicKey = testClientKeypair.publicKey();
Expand Down Expand Up @@ -81,7 +83,7 @@ describe('StellarStrategy', () => {
it('should use testnet network by default', () => {
expect(strategy).toBeDefined();
const publicKey = strategy.getServerPublicKey();
expect(publicKey).toBe(StellarSdk.Keypair.fromSecret(testServerSecret).publicKey());
expect(publicKey).toBe(testServerPublicKey);
});
});

Expand All @@ -99,12 +101,10 @@ describe('StellarStrategy', () => {
) as StellarSdk.Transaction;

// transaction source is server account
const serverPublicKey = StellarSdk.Keypair.fromSecret(testServerSecret).publicKey();
expect(transaction.source).toBe(serverPublicKey);
expect(transaction.source).toBe(testServerPublicKey);
// operation source should be client
expect((transaction.operations[0] as any).source).toBe(testClientPublicKey);
// SDK normalizes sequence to '1' when building from an account with '0'
expect(transaction.sequence).toBe('1');
expect(transaction.sequence).toBe('0');
expect(transaction.operations.length).toBe(1);
expect(transaction.operations[0].type).toBe('manageData');
expect(transaction.operations[0].name).toBe('Harvest Finance auth');
Expand Down Expand Up @@ -151,9 +151,9 @@ describe('StellarStrategy', () => {
const newUser = {
id: 'user-id',
stellarAddress: testClientPublicKey,
email: null,
password: null,
role: 'USER',
email: '',
password: '',
role: UserRole.FARMER,
firstName: 'Stellar',
lastName: 'User',
isActive: true,
Expand Down Expand Up @@ -182,6 +182,15 @@ describe('StellarStrategy', () => {
expect(userRepository.findOne).toHaveBeenCalledWith({
where: { stellarAddress: testClientPublicKey },
});
expect(userRepository.create).toHaveBeenCalledWith({
stellarAddress: testClientPublicKey,
email: '',
password: '',
role: UserRole.FARMER,
firstName: 'Stellar',
lastName: 'User',
isActive: true,
});
expect(userRepository.save).toHaveBeenCalled();
expect(userRepository.update).toHaveBeenCalledWith(newUser.id, {
lastLogin: expect.any(Date),
Expand Down Expand Up @@ -246,119 +255,57 @@ describe('StellarStrategy', () => {
});

it('should throw error for invalid source account', async () => {
// Build a new transaction with invalid source account
const invalidServerPub = StellarSdk.Keypair.random().publicKey();
const serverAccountInvalid = new StellarSdk.Account(invalidServerPub, '0');
const txInvalidSource = new StellarSdk.TransactionBuilder(serverAccountInvalid, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: testNetworkPassphrase,
timebounds: { minTime: '0', maxTime: (Math.floor(Date.now() / 1000) + 300).toString() },
})
.addOperation(
StellarSdk.Operation.manageData({
source: testClientPublicKey,
name: 'Harvest Finance auth',
value: Buffer.from('00'.repeat(32), 'hex'),
}),
)
.build();

// Sign with client and server keypairs
const clientKeypair = testClientKeypair;
txInvalidSource.sign(clientKeypair);

await expect(
strategy.validate(txInvalidSource.toEnvelope().toXDR('base64')),
).rejects.toThrow('Invalid source account');
expect(() =>
(strategy as any).validateTransactionStructure({
source: 'INVALID_SOURCE',
sequence: '0',
timeBounds: currentTimeBounds(),
operations: [validManageDataOperation()],
}),
).toThrow('Invalid source account');
});

it('should throw error for invalid sequence number', async () => {
// Build a new transaction with sequence '1'
const serverAccountSeq = new StellarSdk.Account(StellarSdk.Keypair.fromSecret(testServerSecret).publicKey(), '1');
const txInvalidSeq = new StellarSdk.TransactionBuilder(serverAccountSeq, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: testNetworkPassphrase,
timebounds: { minTime: '0', maxTime: (Math.floor(Date.now() / 1000) + 300).toString() },
})
.addOperation(
StellarSdk.Operation.manageData({
source: testClientPublicKey,
name: 'Harvest Finance auth',
value: Buffer.from('00'.repeat(32), 'hex'),
}),
)
.build();

// Sign with server and client
const serverKeypair = StellarSdk.Keypair.fromSecret(testServerSecret);
const clientKeypair = testClientKeypair;
txInvalidSeq.sign(serverKeypair);
txInvalidSeq.sign(clientKeypair);

await expect(
strategy.validate(txInvalidSeq.toEnvelope().toXDR('base64')),
).rejects.toThrow('Invalid sequence number');
expect(() =>
(strategy as any).validateTransactionStructure({
source: testServerPublicKey,
sequence: '1',
timeBounds: currentTimeBounds(),
operations: [validManageDataOperation()],
}),
).toThrow('Invalid sequence number');
});

it('should throw error for expired transaction', async () => {
// Build a new expired transaction
const serverAccount = new StellarSdk.Account(StellarSdk.Keypair.fromSecret(testServerSecret).publicKey(), '0');
const pastTime = Math.floor(Date.now() / 1000) - 1000;
const txExpired = new StellarSdk.TransactionBuilder(serverAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: testNetworkPassphrase,
timebounds: { minTime: (pastTime - 300).toString(), maxTime: pastTime.toString() },
})
.addOperation(
StellarSdk.Operation.manageData({
source: testClientPublicKey,
name: 'Harvest Finance auth',
value: Buffer.from('00'.repeat(32), 'hex'),
}),
)
.build();

const serverKeypair = StellarSdk.Keypair.fromSecret(testServerSecret);
const clientKeypair = testClientKeypair;
txExpired.sign(serverKeypair);
txExpired.sign(clientKeypair);

await expect(
strategy.validate(txExpired.toEnvelope().toXDR('base64')),
).rejects.toThrow('Challenge transaction expired');
expect(() =>
(strategy as any).validateTransactionStructure({
source: testServerPublicKey,
sequence: '0',
timeBounds: {
minTime: (pastTime - 300).toString(),
maxTime: pastTime.toString(),
},
operations: [validManageDataOperation()],
}),
).toThrow('Challenge transaction expired');
});

it('should throw error for invalid operation type', async () => {
// Build a transaction with a payment operation
const serverAccount = new StellarSdk.Account(StellarSdk.Keypair.fromSecret(testServerSecret).publicKey(), '0');
const txPayment = new StellarSdk.TransactionBuilder(serverAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: testNetworkPassphrase,
timebounds: { minTime: '0', maxTime: (Math.floor(Date.now() / 1000) + 300).toString() },
})
.addOperation(
StellarSdk.Operation.payment({
source: testClientPublicKey,
destination: testClientPublicKey,
asset: StellarSdk.Asset.native(),
amount: '1',
}),
)
.build();

const serverKeypair = StellarSdk.Keypair.fromSecret(testServerSecret);
const clientKeypair = testClientKeypair;
txPayment.sign(serverKeypair);
txPayment.sign(clientKeypair);

await expect(
strategy.validate(txPayment.toEnvelope().toXDR('base64')),
).rejects.toThrow('Invalid operation type');
expect(() =>
(strategy as any).validateTransactionStructure({
source: testServerPublicKey,
sequence: '0',
timeBounds: currentTimeBounds(),
operations: [{ type: 'payment', source: testClientPublicKey }],
}),
).toThrow('Invalid operation type');
});

it('should throw error for missing server signature', async () => {
// Build a valid transaction but only sign with client
const serverAccount = new StellarSdk.Account(StellarSdk.Keypair.fromSecret(testServerSecret).publicKey(), '0');
const serverAccount = new StellarSdk.Account(testServerPublicKey, '-1');
const tx = new StellarSdk.TransactionBuilder(serverAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: testNetworkPassphrase,
Expand Down Expand Up @@ -399,7 +346,7 @@ describe('StellarStrategy', () => {
describe('getServerPublicKey', () => {
it('should return the server public key', () => {
const publicKey = strategy.getServerPublicKey();
expect(publicKey).toBe(StellarSdk.Keypair.fromSecret(testServerSecret).publicKey());
expect(publicKey).toBe(testServerPublicKey);
});
});

Expand Down Expand Up @@ -431,3 +378,18 @@ describe('StellarStrategy', () => {
});
});
});

function currentTimeBounds() {
const now = Math.floor(Date.now() / 1000);
return {
minTime: now.toString(),
maxTime: (now + 300).toString(),
};
}

function validManageDataOperation() {
return {
type: 'manageData',
name: 'Harvest Finance auth',
};
}
Loading
Loading