Skip to content

Commit

Permalink
Make sure Nammatham v2 is testable (#106)
Browse files Browse the repository at this point in the history
  • Loading branch information
mildronize authored Jan 19, 2024
2 parents 64c2a1e + d81ee04 commit 9183d56
Show file tree
Hide file tree
Showing 42 changed files with 1,300 additions and 702 deletions.
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

0 comments on commit 9183d56

Please sign in to comment.