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
14 changes: 12 additions & 2 deletions jest.config.cjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: [
'**/__tests__/**/*.ts',
'**/?(*.)+(spec|test).ts'
],
// Source files import each other with the ESM-required '.js' suffix while the files
// on disk are '.ts' — strip the suffix so jest resolves them.
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
'^.+\\.ts$': ['ts-jest', {
useESM: true,
tsconfig: 'tsconfig.test.json'
}]
},
Expand All @@ -17,6 +25,8 @@ module.exports = {
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
// src/index.ts is a listen-only shim after the buildServer extraction; the real
// bootstrap logic it used to hold now lives in src/app.ts and IS measured.
'!src/index.ts'
],
coverageDirectory: 'coverage',
Expand All @@ -27,4 +37,4 @@ module.exports = {
clearMocks: true,
resetMocks: true,
restoreMocks: true
};
};
11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:verbose": "jest --verbose",
"test:config": "jest tests/config.test.ts",
"test:github": "jest tests/githubSync.test.ts",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
"test:verbose": "node --experimental-vm-modules node_modules/jest/bin/jest.js --verbose",
"test:config": "node --experimental-vm-modules node_modules/jest/bin/jest.js tests/config.test.ts",
"lint": "eslint . --ext .ts",
"format": "prettier --write \"src/**/*.ts\""
},
Expand Down
74 changes: 74 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { fastify, FastifyInstance, FastifyServerOptions } from 'fastify';
import cors from '@fastify/cors';
import rateLimit from '@fastify/rate-limit';
import swagger from '@fastify/swagger';
import swaggerUi from '@fastify/swagger-ui';
import 'dotenv/config';

import { registerRoutes } from './routes/index.js';
import { config } from './config.js';

export interface BuildServerOptions {
logger?: FastifyServerOptions['logger'];
}

const defaultLogger: FastifyServerOptions['logger'] = {
transport: {
target: 'pino-pretty',
options: {
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname',
},
},
};

/**
* Builds a fully configured server without binding a port, so tests can drive it
* through fastify's `.inject()` instead of standing up a real listener.
*/
export const buildServer = (options: BuildServerOptions = {}): FastifyInstance => {
const server: FastifyInstance = fastify({
logger: options.logger === undefined ? defaultLogger : options.logger,
});

// Register plugins
server.register(cors, {
origin: true,
credentials: true,
});

// Register rate limiting to prevent resource exhaustion
server.register(rateLimit, {
max: config.contentstack.rateLimit?.max || 100, // Maximum 100 requests per window
timeWindow: config.contentstack.rateLimit?.timeWindow || '1 minute',
errorResponseBuilder: () => ({
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded. Please try again later.',
}),
});

// Register Swagger
server.register(swagger, {
swagger: {
info: {
title: 'Shopify Live Preview API',
description: 'API documentation for Shopify Live Preview Middleware',
version: '1.0.0',
},
host: 'localhost:3002',
schemes: ['http'],
consumes: ['application/json'],
produces: ['application/json'],
},
});

server.register(swaggerUi, {
routePrefix: '/documentation',
});

// Register routes
registerRoutes(server);

return server;
};
62 changes: 3 additions & 59 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,7 @@
import { fastify, FastifyInstance } from 'fastify';
import cors from '@fastify/cors';
import rateLimit from '@fastify/rate-limit';
import swagger from '@fastify/swagger';
import swaggerUi from '@fastify/swagger-ui';
import 'dotenv/config';

import { registerRoutes } from './routes/index.js';
import { buildServer } from './app.js';
import { config } from './config.js';

const server: FastifyInstance = fastify({
logger: {
transport: {
target: 'pino-pretty',
options: {
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname',
},
},
},
});

// Register plugins
server.register(cors, {
origin: true,
credentials: true,
});

// Register rate limiting to prevent resource exhaustion
server.register(rateLimit, {
max: config.contentstack.rateLimit?.max || 100, // Maximum 100 requests per window
timeWindow: config.contentstack.rateLimit?.timeWindow || '1 minute',
errorResponseBuilder: () => ({
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded. Please try again later.',
}),
});

// Register Swagger
server.register(swagger, {
swagger: {
info: {
title: 'Shopify Live Preview API',
description: 'API documentation for Shopify Live Preview Middleware',
version: '1.0.0',
},
host: 'localhost:3002',
schemes: ['http'],
consumes: ['application/json'],
produces: ['application/json'],
},
});

server.register(swaggerUi, {
routePrefix: '/documentation',
});

// Register routes
registerRoutes(server);
const server = buildServer();

const start = async () => {
try {
Expand All @@ -69,4 +13,4 @@ const start = async () => {
}
};

start();
start();
26 changes: 16 additions & 10 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ This directory contains the test suite for the Shopify Live Preview Middleware s

## Test Structure

- `config.test.ts` - Configuration module tests
- `githubSync.test.ts` - GitHub sync logic tests
- `environment.test.ts` - Environment utilities tests
- `setup.ts` - Global test setup
- `config.test.ts` - Configuration module tests, one `describe` per env permutation
- `getPreviewData.test.ts` - `getPreviewData` handler tests, driven through `fastify.inject()`
- `setup.ts` - Global test setup (installs the env the controller needs at import time)

## Running Tests

Expand All @@ -19,15 +18,14 @@ This directory contains the test suite for the Shopify Live Preview Middleware s

### Specific Tests
- `npm run test:config` - Run config tests only
- `npm run test:github` - Run GitHub sync tests only

## Current Coverage

✅ **Configuration Module** - Fully tested
✅ **GitHub Sync Logic** - Business logic tested
✅ **Environment Utilities** - Utility functions tested
✅ **Configuration Module** - Fully covered, including the undefended edge cases
✅ **getPreviewData handler** - Success, validation and error paths covered
✅ **Server bootstrap** (`src/app.ts`) - Covered via `buildServer()`

⚠️ **Controllers & Routes** - Need integration testing
⚠️ **githubSyncController / viewsHealthController** - Not yet covered

## Test Patterns

Expand All @@ -37,8 +35,16 @@ Tests follow Jest conventions with describe/it blocks and focus on:
- Business logic
- Edge cases

Two conventions worth knowing before adding tests here:

- **Native ESM.** The `jest` object is not injected as a global, so anything needing
`jest.resetModules()` / `jest.fn()` must `import { jest } from '@jest/globals'` first.
- **Module-level env reads.** `src/config.ts` snapshots `process.env` at import time, so a test
that needs a different env has to `jest.resetModules()` and re-`import()` the module rather
than mutate `process.env` and expect the existing `config` object to change.

## Configuration

- Jest config: `jest.config.cjs`
- TypeScript config: `tsconfig.test.json`
- Test setup: `tests/setup.ts`
- Test setup: `tests/setup.ts`
Loading
Loading