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
4 changes: 0 additions & 4 deletions .codecov.yml

This file was deleted.

14 changes: 8 additions & 6 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: Node.js CI

on:
push:
branches: [ master ]
branches: [ main ]
pull_request:
branches: [ master ]
branches: [ main ]

jobs:
build:
Expand All @@ -16,15 +16,17 @@ jobs:

strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
node-version: [24.x]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm test -- --silent --coverage --coverageReporters text lcov
- run: bash <(curl -s https://codecov.io/bash)
- run: npm run lint
- run: npm run build

7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,9 @@ test_examples/
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
.pnp.*

.next/
*.tsbuildinfo
next-env.d.ts
.claude/settings.local.json
1 change: 0 additions & 1 deletion .nvmrc

This file was deleted.

28 changes: 28 additions & 0 deletions __tests__/currencies.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import fs from 'fs';
import path from 'path';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { GET } from '../src/app/api/currencies/route';
import { NextRequest } from 'next/server';

const daily = fs.readFileSync(path.join(__dirname, './daily.zip'));

describe('currencies API', () => {
let mock: MockAdapter;

beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(/.*eurofxref\.zip$/).reply(200, daily);
});

afterEach(() => mock.restore());

it('should return supported currencies', async () => {
const request = new NextRequest('http://localhost:3000/api/currencies');
const response = await GET(request);
const currencies = await response.json();

expect(response.status).toBe(200);
expect(currencies).toEqual(['USD', 'JPY']);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import fs from 'fs';
import path from 'path';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { handler } from '../currencyRates';
import { GET } from '../src/app/api/currencyRates/route';
import { NextRequest } from 'next/server';

const daily = fs.readFileSync(path.join(__dirname, './daily.zip'));
const historical = fs.readFileSync(path.join(__dirname, './hist.zip'));

describe('currencyRates', () => {
let mock;
describe('currencyRates API', () => {
let mock: MockAdapter;

beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(/.*eurofxref\.zip$/).reply(200, daily);
Expand All @@ -18,8 +20,11 @@ describe('currencyRates', () => {
afterEach(() => mock.restore());

it('should return daily rates', async () => {
const res = await handler();
const rates = {
const request = new NextRequest('http://localhost:3000/api/currencyRates');
const response = await GET(request);
const rates = await response.json();

const expectedRates = {
'2018-09-17': {
USD: 1.1691,
JPY: 130.77
Expand All @@ -34,19 +39,16 @@ describe('currencyRates', () => {
}
};

expect(res).toEqual({
statusCode: 200,
body: JSON.stringify(rates)
});
expect(response.status).toBe(200);
expect(rates).toEqual(expectedRates);
});

it('should return specific currencies', async () => {
const res = await handler({
queryStringParameters: {
currencies: ['USD']
}
});
const rates = {
const request = new NextRequest('http://localhost:3000/api/currencyRates?currencies=USD');
const response = await GET(request);
const rates = await response.json();

const expectedRates = {
'2018-09-17': {
USD: 1.1691
},
Expand All @@ -58,9 +60,7 @@ describe('currencyRates', () => {
}
};

expect(res).toEqual({
statusCode: 200,
body: JSON.stringify(rates)
});
expect(response.status).toBe(200);
expect(rates).toEqual(expectedRates);
});
});
});
File renamed without changes.
File renamed without changes.
18 changes: 18 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";

const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);

export default eslintConfig;
31 changes: 31 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Config } from 'jest'
import nextJest from 'next/jest.js'
import { createRequire } from 'module'

const require = createRequire(import.meta.url)

const createJestConfig = nextJest({
dir: './',
})

const config: Config = {
coverageProvider: 'v8',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^uuid$': require.resolve('uuid'),
},
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
transformIgnorePatterns: [
'node_modules/(?!(uuid|d3-array|d3-scale-chromatic|react-dates)/)',
],
extensionsToTreatAsEsm: ['.ts', '.tsx'],
globals: {
'ts-jest': {
useESM: true,
},
},
}

export default createJestConfig(config)
71 changes: 71 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import '@testing-library/jest-dom'

// Polyfill setImmediate for jsdom (used by yauzl zip library)
if (typeof globalThis.setImmediate === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).setImmediate = (fn: (...args: unknown[]) => void, ...args: unknown[]) => setTimeout(fn, 0, ...args);
}

// Mock Next.js App Router navigation
jest.mock('next/navigation', () => ({
useRouter() {
return {
push: jest.fn(),
replace: jest.fn(),
prefetch: jest.fn(),
back: jest.fn(),
forward: jest.fn(),
refresh: jest.fn(),
}
},
usePathname() {
return '/'
},
useSearchParams() {
return new URLSearchParams()
},
useParams() {
return {}
},
}))

// Mock Next.js Link
jest.mock('next/link', () => {
return ({ children }: { children: React.ReactNode }) => {
return children
}
})

// Mock UUID
jest.mock('uuid', () => ({
v4: () => 'test-uuid-123'
}))

// Mock Next.js server components
class MockNextRequest {
url: string;
method: string;
headers: Headers;
nextUrl: URL;

constructor(url: string, init?: { method?: string; headers?: HeadersInit }) {
this.url = url;
this.method = init?.method || 'GET';
this.headers = new Headers(init?.headers);
this.nextUrl = new URL(url);
}
}

const MockNextResponse = {
json: (data: unknown, init?: { status?: number }) => {
return {
json: async () => data,
status: init?.status || 200,
};
},
};

jest.mock('next/server', () => ({
NextRequest: MockNextRequest,
NextResponse: MockNextResponse,
}))
5 changes: 2 additions & 3 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
[build]
command = "npm run build && npm run build:lambda"
functions = "lambda"
publish = "build"
command = "npm run build"
publish = ".next"
5 changes: 5 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {}

export default nextConfig
Loading