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
54 changes: 27 additions & 27 deletions src/app/api/upgrade-config/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,33 +69,15 @@ describe('GET /api/upgrade-config', () => {
});

describe('path traversal prevention', () => {
it('returns 400 when network contains ".."', async () => {
const res = await GET(createRequest({ network: '..', upgradeId: 'test' }));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/invalid network/i);
});

it('returns 400 when network contains "/"', async () => {
const res = await GET(createRequest({ network: 'foo/bar', upgradeId: 'test' }));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/invalid network/i);
});

it('returns 400 when network contains spaces', async () => {
const res = await GET(createRequest({ network: 'main net', upgradeId: 'test' }));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/invalid network/i);
});

it('returns 400 when network contains "." (single dot)', async () => {
const res = await GET(createRequest({ network: '.', upgradeId: 'test' }));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/invalid network/i);
});
it.each(['..', 'foo/bar', 'main net', '.'])(
'returns 400 when network is "%s"',
async network => {
const res = await GET(createRequest({ network, upgradeId: 'test' }));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/invalid network/i);
}
);
it('returns 400 when upgradeId contains path separators', async () => {
const res = await GET(createRequest({ network: 'mainnet', upgradeId: 'foo/bar' }));
expect(res.status).toBe(400);
Expand Down Expand Up @@ -132,6 +114,24 @@ describe('GET /api/upgrade-config', () => {
expect(body.configOptions).toEqual([
{ fileName: 'config1', displayName: 'Config1', ledgerId: 0 },
]);
expect(console.warn).toHaveBeenCalledWith(
'Failed to parse config1.json, using default ledgerId: 0'
);
});

it('returns the default config option when reading fails', async () => {
const error = new Error('Read failed');
mockReadFile.mockRejectedValue(error);
const res = await GET(createRequest({ network: 'mainnet', upgradeId: 'test' }));
expect(res.status).toBe(200);
const body = await res.json();
expect(body.configOptions).toEqual([
{ fileName: 'config1', displayName: 'Config1', ledgerId: 0 },
]);
expect(console.warn).toHaveBeenCalledWith(
'Error reading config1.json, using default ledgerId: 0',
error
);
});

it('returns 200 with empty array on ENOENT', async () => {
Expand Down
17 changes: 5 additions & 12 deletions src/app/api/upgrade-config/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,28 +64,21 @@ export async function GET(req: NextRequest) {
const displayName = toDisplayName(baseName);
const filePath = path.join(validationsPath, configFile);

let ledgerId = 0;
try {
const configContent = await fs.readFile(filePath, 'utf-8');
const parsedConfig = parseFromString(configContent);

if (parsedConfig.result.success && 'config' in parsedConfig) {
return {
fileName: baseName,
displayName,
ledgerId: parsedConfig.config.ledgerId,
};
ledgerId = parsedConfig.config.ledgerId;
} else {
console.warn(`Failed to parse ${configFile}, using default ledgerId: 0`);
}

console.warn(`Failed to parse ${configFile}, using default ledgerId: 0`);
} catch (error) {
console.warn(`Error reading ${configFile}, using default ledgerId: 0`, error);
}

return {
fileName: baseName,
displayName,
ledgerId: 0,
};
return { fileName: baseName, displayName, ledgerId };
})
);

Expand Down
39 changes: 4 additions & 35 deletions src/app/api/upgrades/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,6 @@ function createRequest(params: Record<string, string>): NextRequest {
describe('GET /api/upgrades', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGetUpgradeOptions.mockImplementation((network: NetworkType) => {
if (network === NetworkType.Zeronet) {
return [
{
id: '2025-08-01-upgrade-qux',
name: 'Upgrade Qux',
description: 'Zeronet upgrade',
date: '2025-08-01',
network: NetworkType.Zeronet,
status: TaskStatus.ReadyToSign,
},
];
}
return [];
});
});

afterEach(() => {
Expand Down Expand Up @@ -82,6 +67,7 @@ describe('GET /api/upgrades', () => {
});

const res = GET(createRequest({ readyToSign: 'true' }));
expect(res.status).toBe(200);
const body = await res.json();

expect(body.map((t: Upgrade) => t.id)).toEqual([
Expand All @@ -90,13 +76,13 @@ describe('GET /api/upgrades', () => {
]);
});

it('returns only ready-to-sign tasks tagged with the fetched network', async () => {
it('returns only ready-to-sign tasks', async () => {
const readyTask: Upgrade = {
id: '2025-08-01-upgrade-ready',
name: 'Ready',
description: '',
date: '2025-08-01',
network: NetworkType.Sepolia,
network: NetworkType.Mainnet,
status: TaskStatus.ReadyToSign,
};
mockGetUpgradeOptions.mockImplementation((network: NetworkType) =>
Expand All @@ -110,23 +96,6 @@ describe('GET /api/upgrades', () => {

const res = GET(createRequest({ readyToSign: 'true' }));

expect(await res.json()).toEqual([{ ...readyTask, network: NetworkType.Mainnet }]);
});

it('includes zeronet when aggregating ready-to-sign tasks', async () => {
const res = GET(createRequest({ readyToSign: 'true' }));

expect(res.status).toBe(200);

const body = await res.json();
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: '2025-08-01-upgrade-qux',
network: NetworkType.Zeronet,
status: TaskStatus.ReadyToSign,
}),
])
);
expect(await res.json()).toEqual([readyTask]);
});
});
4 changes: 1 addition & 3 deletions src/app/api/upgrades/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ export function GET(req: NextRequest) {

const allReadyToSignTasks = availableNetworks.flatMap(network => {
const upgrades = getUpgradeOptions(network);
return upgrades
.filter(upgrade => upgrade.status === TaskStatus.ReadyToSign)
.map(upgrade => ({ ...upgrade, network }));
return upgrades.filter(upgrade => upgrade.status === TaskStatus.ReadyToSign);
});

// Sort by id (newest first) across all networks; per-network lists are sorted independently.
Expand Down
Loading