Skip to content
Closed
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
26 changes: 26 additions & 0 deletions apps/api/src/routes/escrow/__tests__/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Shared mock helpers for escrow handler tests.
*
* mockReq(body) — creates a minimal Express-like req object.
* mockRes() — creates a res stub that captures .status() and .json() calls.
*/

export function mockReq(body = {}) {
return { body };
}

export function mockRes() {
const res = { _status: 200, _body: null };

res.status = (code) => {
res._status = code;
return res;
};

res.json = (data) => {
res._body = data;
return res;
};

return res;
}
112 changes: 112 additions & 0 deletions apps/api/src/routes/escrow/__tests__/milestone-status.handler.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { changeMilestoneStatusHandler } from '../milestone-status.handler.js';

vi.mock('../../../lib/trustlesswork.js', () => ({
trustlessWork: { post: vi.fn() },
}));

import { trustlessWork } from '../../../lib/trustlesswork.js';
import { mockReq, mockRes } from './helpers.js';

describe('changeMilestoneStatusHandler', () => {

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

it('returns 400 when contractId is missing', async () => {
const res = mockRes();
await changeMilestoneStatusHandler(
mockReq({ milestoneIndex: 0, newEvidence: 'done', newStatus: 'completed', serviceProvider: 'GOWNER' }),
res,
);
expect(res._status).toBe(400);
expect(res._body.error).toContain('contractId');
});

it('returns 400 when newEvidence is missing', async () => {
const res = mockRes();
await changeMilestoneStatusHandler(
mockReq({ contractId: 'CAZT001', milestoneIndex: 0, newStatus: 'completed', serviceProvider: 'GOWNER' }),
res,
);
expect(res._status).toBe(400);
expect(res._body.error).toContain('newEvidence');
});

it('returns 400 when serviceProvider is missing', async () => {
const res = mockRes();
await changeMilestoneStatusHandler(
mockReq({ contractId: 'CAZT001', milestoneIndex: 0, newEvidence: 'done', newStatus: 'completed' }),
res,
);
expect(res._status).toBe(400);
expect(res._body.error).toContain('serviceProvider');
});

it('calls TrustlessWork with correct milestone payload including milestoneIndex', async () => {
trustlessWork.post.mockResolvedValueOnce({
data: { unsignedTransaction: 'MILESTONE_XDR_001' },
});

await changeMilestoneStatusHandler(
mockReq({
contractId: 'CAZT001',
milestoneIndex: 0,
newEvidence: 'Rental period completed',
newStatus: 'completed',
serviceProvider: 'GOWNER111',
}),
mockRes(),
);

expect(trustlessWork.post).toHaveBeenCalledWith(
'/escrow/single-release/change-milestone-status',
{
contractId: 'CAZT001',
milestoneIndex: 0,
newEvidence: 'Rental period completed',
newStatus: 'completed',
serviceProvider: 'GOWNER111',
},
);
});

it('returns 201 with unsignedTransaction on success', async () => {
trustlessWork.post.mockResolvedValueOnce({
data: { unsignedTransaction: 'MILESTONE_XDR_001' },
});

const res = mockRes();
await changeMilestoneStatusHandler(
mockReq({
contractId: 'CAZT001',
milestoneIndex: 0,
newEvidence: 'done',
newStatus: 'completed',
serviceProvider: 'GOWNER',
}),
res,
);

expect(res._status).toBe(201);
expect(res._body).toEqual({ unsignedTransaction: 'MILESTONE_XDR_001' });
});

it('returns 500 on TrustlessWork network failure', async () => {
trustlessWork.post.mockRejectedValueOnce(new Error('Timeout'));

const res = mockRes();
await changeMilestoneStatusHandler(
mockReq({
contractId: 'CAZT001',
milestoneIndex: 0,
newEvidence: 'done',
newStatus: 'completed',
serviceProvider: 'GOWNER',
}),
res,
);

expect(res._status).toBe(500);
expect(res._body.error).toBe('Failed to change milestone status');
});
});