Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make sure Nammatham v2 is testable #106

Merged
merged 15 commits into from
Jan 19, 2024
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
40 changes: 40 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* eslint-env node */
module.exports = {
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'perfectionist', 'unused-imports'],
root: true,
ignorePatterns: ['**.test.ts'],
rules: {
/**
* Unused import and vars:
* https://github.com/sweepline/eslint-plugin-unused-imports
*/
'unused-imports/no-unused-imports': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
/**
* For config: https://eslint-plugin-perfectionist.azat.io/rules/sort-imports
*/
'perfectionist/sort-imports': [
'error',
{
type: 'line-length',
order: 'asc',
groups: [
'type',
['builtin', 'external'],
'internal-type',
'internal',
['parent-type', 'sibling-type', 'index-type'],
['parent', 'sibling', 'index'],
'side-effect',
'style',
'object',
'unknown',
],
'newlines-between': 'always',
'internal-pattern': ['@/nammatham/**'],
},
],
},
};
24 changes: 0 additions & 24 deletions .eslintrc.js

This file was deleted.

14 changes: 6 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ name: "Build & Test"

on:
push:
branches: [ main, dev ]
branches: [ main ]
paths-ignore:
- '**/*.md'
- '.github'
pull_request:
branches: [ main, dev ]
branches: [ main ]
paths-ignore:
- '**/*.md'
- '.github'

env:
pnpm_version: 7
pnpm_version: 8

jobs:
build:
Expand All @@ -22,7 +22,7 @@ jobs:

strategy:
matrix:
node-version: [16, 18]
node-version: [16, 18, 20]
os: [ubuntu-latest, windows-latest]

steps:
Expand All @@ -36,10 +36,8 @@ jobs:
with:
version: ${{ env.pnpm_version }}
- run: pnpm install
- run: npx nx run nammatham:build
- run: npx nx run nammatham:typecheck
- run: npx nx run nammatham:test
- run: npx nx run nammatham:test:coverage
- run: pnpm build
- run: pnpm test:coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
Expand Down
15 changes: 10 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
"version": "2.0.0-alpha.8",
"description": "Azure Function Nodejs Lightweight framework with Dependency Injection",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"format": "prettier --write '**/*.ts'",
"test": "vitest",
"test:coverage": "vitest run --coverage",
"dev": "nx run @nammatham/core:build && nx run-many -t dev --projects=@nammatham/* --parallel=5",
"pre-local": "tsx ./scripts/pre-local.ts",
"post-local": "tsx ./scripts/post-local.ts",
"release": "run-s build releaseOnly",
"releaseOnly": "tsx ./scripts/release.ts",
"format": "nx run-many -t format --projects=@nammatham/* --parallel=5",
"lint": "nx run-many -t lint --projects=@nammatham/* --parallel=5",
"lint:fix": "nx run-many -t lint:fix --projects=@nammatham/* --parallel=5",
"build": "nx run-many -t build --parallel=10",
"azurite": "pnpx azurite --silent --location ./.azurite --debug ./.azurite/debug.log"
},
Expand All @@ -32,9 +35,10 @@
"@types/node": "^18.11.18",
"@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0",
"ava": "^5.1.1",
"c8": "^7.12.0",
"@vitest/coverage-v8": "^1.1.3",
"eslint": "^8.36.0",
"eslint-plugin-perfectionist": "^2.5.0",
"eslint-plugin-unused-imports": "^3.0.0",
"execa": "^8.0.1",
"fs-extra": "^11.2.0",
"nodemon": "^2.0.20",
Expand All @@ -43,7 +47,8 @@
"prettier": "^2.8.3",
"tsup": "^8.0.1",
"tsx": "^4.7.0",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"vitest": "^1.1.3"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/"
Expand Down
9 changes: 6 additions & 3 deletions packages/azure-functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"test": "echo \"Error: no test specified\" && exit 1",
"prepublishOnly": "npm run build",
"build": "tsup src/main.ts --dts",
"lint": "tsc --noEmit",
"format": "prettier -w src",
"lint": "tsc --noEmit && eslint ./src && prettier -c src",
"lint:fix": "eslint --fix ./src && prettier -c src",
"dev": "nodemon --watch src --ext ts --exec 'npm run build'"
},
"keywords": [
Expand All @@ -18,8 +20,8 @@
"author": "Thada Wangthammang",
"license": "MIT",
"dependencies": {
"@nammatham/core": "2.0.0-alpha.8",
"@azure/functions": "^4.1.0",
"@nammatham/core": "2.0.0-alpha.8",
"colorette": "^2.0.20",
"express": "^4.18.2",
"undici": "5.20.0",
Expand All @@ -34,6 +36,7 @@
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/uuid": "^9.0.7"
"@types/uuid": "^9.0.7",
"node-mocks-http": "^1.14.1"
}
}
20 changes: 20 additions & 0 deletions packages/azure-functions/src/adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { expect, test } from 'vitest';
import { AzureFunctionsAdapter } from './adapter';
import { AzureFunctionsTrigger } from './trigger';
import { NammathamApp } from '@nammatham/core';
import { AzureFunctionsHandlerResolver } from './handler-resolver';

test(`${AzureFunctionsAdapter.name} should be created correctly`, async () => {
// Arrange
const adapter = new AzureFunctionsAdapter();
// Act
const app = adapter.createApp();
const func = adapter.createTrigger();

// Assert
expect(func).toBeInstanceOf(AzureFunctionsTrigger);
expect(app).toBeInstanceOf(NammathamApp);
expect(app.runtime === 'azure-functions').toBe(true);
expect(app.isDevelopment).toBe(false);
expect(app.handlerResolver).toBeInstanceOf(AzureFunctionsHandlerResolver);
});
3 changes: 2 additions & 1 deletion packages/azure-functions/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BaseRuntimeAdapter, NammathamApp } from '@nammatham/core';
import { AzureFunctionsHandlerResolver } from './handler-resolver';

import { AzureFunctionsTrigger } from './trigger';
import { AzureFunctionsHandlerResolver } from './handler-resolver';

export class AzureFunctionsAdapter extends BaseRuntimeAdapter<AzureFunctionsTrigger> {
createTrigger() {
Expand Down
26 changes: 16 additions & 10 deletions packages/azure-functions/src/handler-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { BaseHandlerResolver, NammathamApp, logger, AfterServerStartedMetadata } from '@nammatham/core';
import { Cookie, HttpResponse, InvocationContext, LogLevel } from '@azure/functions';
import { AzureFunctionsEndpoint } from './types';
import type { Cookie, LogLevel } from '@azure/functions';
import type { NammathamApp, AfterServerStartedMetadata } from '@nammatham/core';
import type {
Request as ExpressRequest,
Response as ExpressResponse,
CookieOptions as ExpressCookieOptions,
} from 'express';
import { HttpRequest } from './http/HttpRequest';

import { yellow } from 'colorette';
import { v4 as uuidv4 } from 'uuid';
import { BaseHandlerResolver, logger } from '@nammatham/core';
import { HttpResponse, InvocationContext } from '@azure/functions';

import type { AzureFunctionsEndpoint } from './types';

import { HttpRequest } from './http/HttpRequest';
import { printRegisteredFunctions, printRegisteredNonHttpFunctions } from './utils';
import { yellow } from 'colorette';

function logExecutedFunction(
startTime: number,
Expand All @@ -29,7 +34,7 @@ function logExecutedFunction(
* Map InvocationContext log level to logger
*/

function logHandler(level: LogLevel, ...args: any[]) {
function logHandler(level: LogLevel, ...args: unknown[]) {
if (level === 'information') {
logger.info(...args);
} else if (level === 'error') {
Expand Down Expand Up @@ -117,11 +122,12 @@ export class AzureFunctionsHandlerResolver extends BaseHandlerResolver {
logger.info(
`Executing 'Functions.${endpoint.name}' (Reason='This function was programmatically called via the host APIs.', Id=${context.invocationId})`
);
let result: any;
let result: HttpResponse | string | undefined | unknown;
try {
result = await endpoint.invokeHandler(new HttpRequest(req), context);
logExecutedFunction(startTime, endpoint, context, 'Succeeded');
if (result === undefined) return;
if (result === undefined || result === null) return;
if (typeof result === 'string') return res.send(result);
const response = result instanceof HttpResponse ? result : new HttpResponse(result);
return await convertHttpResponseToExpressResponse(res, response);
} catch (error) {
Expand All @@ -134,8 +140,8 @@ export class AzureFunctionsHandlerResolver extends BaseHandlerResolver {
override async resolveRegisterHandler(app: NammathamApp) {
logger.debug(`Starting using Azure Functions register handler`);
const azureFunctions = app.functions.filter(func => func.type === 'azure-functions') as AzureFunctionsEndpoint<
any,
any
unknown,
unknown
>[];

if (azureFunctions.length === 0) {
Expand Down
37 changes: 37 additions & 0 deletions packages/azure-functions/src/handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect, test } from 'vitest';
import { AzureFunctionsHandler } from './handler';
import { InvocationContext } from '@azure/functions';
test(`${AzureFunctionsHandler.name}.handler should be invoked correctly`, async () => {
// Arrange
const handler = new AzureFunctionsHandler(
'test',
{
extraInputs: [],
extraOutputs: [],
endpointOption: {
type: 'http',
route: 'test',
methods: ['GET'],
},
},
() => ''
);

// Act
const endpoint = handler.handler(() => 'handlerResult');
const result = endpoint.invokeHandler({}, new InvocationContext());

// Assert
expect(result).toBe('handlerResult');
// NOTE: invokeHandler should test end-to-end
expect(endpoint.invokeHandler).toBeInstanceOf(Function);
// NOTE: registerFunc should test end-to-end
expect(endpoint.registerFunc).toBeInstanceOf(Function);

expect(handler).toBeInstanceOf(AzureFunctionsHandler);
expect(endpoint.endpointOption?.type).toBe('http');
expect(endpoint.endpointOption?.route).toBe('test');
expect(endpoint.endpointOption?.methods).toEqual(['GET']);
expect(endpoint.type).toBe('azure-functions');
expect(endpoint.name).toBe('test');
});
7 changes: 5 additions & 2 deletions packages/azure-functions/src/handler.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { NammathamContext } from './nammatham-context';
import type { InvocationContext } from '@azure/functions';
import type { WithEndpointOption } from '@nammatham/core';

import type { HandlerFunction, RegisterFunctionOption, AzureFunctionsEndpoint, FunctionOption } from './types';

import { NammathamContext } from './nammatham-context';

export class AzureFunctionsHandler<TTriggerType, TReturnType> {
constructor(
public funcName: string,
public functionOption: FunctionOption,
public functionOption: WithEndpointOption & FunctionOption,
public registerFunc: RegisterFunctionOption
) {}

Expand Down
37 changes: 37 additions & 0 deletions packages/azure-functions/src/http/HttpRequest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect, test } from 'vitest';
import { HttpRequest } from './HttpRequest';
import httpMocks from 'node-mocks-http';
import exp from 'constants';

test('Test HttpRequest', () => {
// Arrange
const req = httpMocks.createRequest({
method: 'GET',
protocol: 'https',
// get: (key: string) => 'localhost',
url: '/api/test',
query: {
a: '1',
b: '2',
},
headers: {
x: '1',
y: '2',
},
});

// Act
const result = new HttpRequest(req);

// Assert
expect(result.method).toBe('GET');
expect(result.url).toBe('https://undefined/api/test');
expect(result.query.get('a')).toBe('1');
expect(result.query.get('b')).toBe('2');
expect(result.headers.get('x')).toBe('1');
expect(result.headers.get('y')).toBe('2');
expect(result.body).toBe(null);
expect(result.params).toStrictEqual({});
expect(result.user).toBe(null);
expect(result.bodyUsed).toBe(false);
});
Loading