├── .eslintrc ├── .github └── workflows │ ├── lint-and-test-merge.yml │ └── lint-and-test-pull-request.yml ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src └── index.ts ├── test ├── index.test.ts └── tests-data │ ├── add-and-delete-file.patch │ ├── complex.patch │ ├── hyphen.patch │ ├── many-files.patch │ ├── one-file-author-line-break.patch │ ├── one-file-diff.patch │ ├── one-file.patch │ └── rename-file.patch └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dherault-typescript" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-test-merge.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Lint and test 5 | on: 6 | push: 7 | branches: 8 | - master 9 | jobs: 10 | lint_and_test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - run: npm install && npm run lint && npm run test 15 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-test-pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Lint and test 5 | on: pull_request 6 | jobs: 7 | lint_and_test: 8 | if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - run: npm install && npm run lint && npm run test 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | jest.config.js 3 | .eslintrc 4 | .vscode 5 | test 6 | src 7 | coverage 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | }, 5 | "eslint.validate": ["javascript", "typescript"], 6 | "eslint.workingDirectories": [ 7 | { 8 | "mode": "auto" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v2.0.0 2 | 3 | ## Breaking changes 4 | - Fix line numbers (see [#16](https://github.com/dherault/parse-git-patch/issues/16)) 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 David Hérault 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parse-git-patch 2 | 3 | *Parse git patches with ease* 4 | 5 | This NPM package allows you to parse git patches and diffs into an human and machine-readable object. 6 | 7 | ## Installation 8 | 9 | `npm i parse-git-patch` 10 | 11 | ## Usage 12 | 13 | ```patch 14 | From 0f6f88c98fff3afa0289f46bf4eab469f45eebc6 Mon Sep 17 00:00:00 2001 15 | From: A dev 16 | Date: Sat, 25 Jan 2020 19:21:35 +0200 17 | Subject: [PATCH] JSON stringify string responses 18 | 19 | --- 20 | src/events/http/HttpServer.js | 4 +++- 21 | 1 file changed, 3 insertions(+), 1 deletion(-) 22 | 23 | diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js 24 | index 20bf454..c0fdafb 100644 25 | --- a/src/events/http/HttpServer.js 26 | +++ b/src/events/http/HttpServer.js 27 | @@ -770,7 +770,9 @@ export default class HttpServer { 28 | override: false, 29 | }) 30 | 31 | - if (result && typeof result.body !== 'undefined') { 32 | + if (typeof result === 'string') { 33 | + response.source = JSON.stringify(result) 34 | + } else if (result && typeof result.body !== 'undefined') { 35 | if (result.isBase64Encoded) { 36 | response.encoding = 'binary' 37 | response.source = Buffer.from(result.body, 'base64') 38 | -- 39 | 2.21.1 (Apple Git-122.3) 40 | ``` 41 | 42 | ```js 43 | const parseGitPatch = require('parse-git-patch') 44 | 45 | const patch = fs.readFileSync(patchLocation, 'utf-8') 46 | const parsedPatch = parseGitPatch(patch) 47 | ``` 48 | 49 | ```js 50 | { 51 | hash: '0f6f88c98fff3afa0289f46bf4eab469f45eebc6', 52 | date: 'Sat, 25 Jan 2020 19:21:35 +0200', 53 | message: '[PATCH] JSON stringify string responses', 54 | authorEmail: 'a-dev@users.noreply.github.com', 55 | authorName: 'A dev', 56 | files: [ 57 | { 58 | added: false, 59 | deleted: false, 60 | beforeName: 'src/events/http/HttpServer.js', 61 | afterName: 'src/events/http/HttpServer.js', 62 | modifiedLines: [ 63 | { 64 | line: ' if (result && typeof result.body !== \'undefined\') {', 65 | lineNumber: 773, 66 | added: false, 67 | }, 68 | { 69 | line: ' if (typeof result === \'string\') {', 70 | lineNumber: 773, 71 | added: true, 72 | }, 73 | { 74 | line: ' response.source = JSON.stringify(result)', 75 | lineNumber: 774, 76 | added: true, 77 | }, 78 | { 79 | line: ' } else if (result && typeof result.body !== \'undefined\') {', 80 | lineNumber: 775, 81 | added: true, 82 | }, 83 | ], 84 | }, 85 | ], 86 | } 87 | ``` 88 | 89 | ## License 90 | 91 | MIT 92 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | testPathIgnorePatterns: ['/node_modules/', '/dist/'], 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-git-patch", 3 | "version": "2.1.1", 4 | "description": "Parse git patches with ease", 5 | "main": "dist/src/index.js", 6 | "types": "dist/src/index.d.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/dherault/parse-git-patch.git" 10 | }, 11 | "keywords": [ 12 | "git", 13 | "patch", 14 | "patches", 15 | "diff", 16 | "parse", 17 | "parser" 18 | ], 19 | "author": "David Hérault (https://github.com/dherault)", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/dherault/parse-git-patch/issues" 23 | }, 24 | "homepage": "https://github.com/dherault/parse-git-patch#readme", 25 | "scripts": { 26 | "lint": "tsc --noEmit && eslint ./src --ext ts --report-unused-disable-directives --max-warnings 0", 27 | "test": "jest --coverage", 28 | "build": "tsc --declaration", 29 | "prepublishOnly": "npm run build" 30 | }, 31 | "devDependencies": { 32 | "@types/jest": "^29.5.14", 33 | "@types/node": "^22.10.10", 34 | "eslint": "^8.46.0", 35 | "eslint-config-dherault-typescript": "^1.4.0", 36 | "jest": "^29.7.0", 37 | "ts-jest": "^29.2.5", 38 | "typescript": "^5.7.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | const hashRegex = /^From (\S*)/ 2 | const authorRegex = /^From:\s?([^<].*[^>])?\s+(<(.*)>)?/ 3 | const fileNameRegex = /^diff --git "?a\/(.*)"?\s*"?b\/(.*)"?/ 4 | const fileLinesRegex = /^@@ -([0-9]*),?\S* \+([0-9]*),?/ 5 | const similarityIndexRegex = /^similarity index / 6 | const addedFileModeRegex = /^new file mode / 7 | const deletedFileModeRegex = /^deleted file mode / 8 | 9 | export type ParsedPatchModifiedLineType = { 10 | added: boolean 11 | lineNumber: number 12 | line: string 13 | } 14 | 15 | export type ParsedPatchFileDataType = { 16 | added: boolean 17 | deleted: boolean 18 | beforeName: string 19 | afterName: string 20 | modifiedLines: ParsedPatchModifiedLineType[] 21 | } 22 | 23 | export type ParsedPatchType = { 24 | hash?: string 25 | authorName?: string 26 | authorEmail?: string 27 | date?: string 28 | message?: string 29 | files: ParsedPatchFileDataType[] 30 | } 31 | 32 | function parseGitPatch(patch: string) { 33 | if (typeof patch !== 'string') { 34 | throw new Error('Expected first argument (patch) to be a string') 35 | } 36 | 37 | const lines = patch.split('\n') 38 | 39 | const gitPatchMetaInfo = splitMetaInfo(patch, lines) 40 | 41 | if (!gitPatchMetaInfo) return null 42 | 43 | const parsedPatch: ParsedPatchType = { 44 | ...gitPatchMetaInfo, 45 | files: [] as ParsedPatchFileDataType[], 46 | } 47 | 48 | splitIntoParts(lines, 'diff --git').forEach(diff => { 49 | const fileNameLine = diff.shift() 50 | 51 | if (!fileNameLine) return 52 | 53 | const match3 = fileNameLine.match(fileNameRegex) 54 | 55 | if (!match3) return 56 | 57 | const [, a, b] = match3 58 | const metaLine = diff.shift() 59 | 60 | if (!metaLine) return 61 | 62 | const fileData: ParsedPatchFileDataType = { 63 | added: false, 64 | deleted: false, 65 | beforeName: a.trim(), 66 | afterName: b.trim(), 67 | modifiedLines: [], 68 | } 69 | 70 | parsedPatch.files.push(fileData) 71 | 72 | if (addedFileModeRegex.test(metaLine)) { 73 | fileData.added = true 74 | } 75 | if (deletedFileModeRegex.test(metaLine)) { 76 | fileData.deleted = true 77 | } 78 | if (similarityIndexRegex.test(metaLine)) { 79 | return 80 | } 81 | 82 | splitIntoParts(diff, '@@ ').forEach(lines => { 83 | const fileLinesLine = lines.shift() 84 | 85 | if (!fileLinesLine) return 86 | 87 | const match4 = fileLinesLine.match(fileLinesRegex) 88 | 89 | if (!match4) return 90 | 91 | const [, a, b] = match4 92 | 93 | let nA = parseInt(a) - 1 94 | let nB = parseInt(b) - 1 95 | 96 | lines.forEach(line => { 97 | nA++ 98 | nB++ 99 | 100 | if (line === '-- ' || line === '--') { 101 | return 102 | } 103 | if (line.startsWith('+')) { 104 | nA-- 105 | 106 | fileData.modifiedLines.push({ 107 | added: true, 108 | lineNumber: nB, 109 | line: line.substring(1), 110 | }) 111 | } 112 | else if (line.startsWith('-')) { 113 | nB-- 114 | 115 | fileData.modifiedLines.push({ 116 | added: false, 117 | lineNumber: nA, 118 | line: line.substring(1), 119 | }) 120 | } 121 | }) 122 | }) 123 | }) 124 | 125 | return parsedPatch 126 | } 127 | 128 | function splitMetaInfo(patch: string, lines: string[]) { 129 | // Compatible with git output 130 | if (!/^From/g.test(patch)) { 131 | return {} 132 | } 133 | 134 | const hashLine = lines.shift() 135 | 136 | if (!hashLine) return null 137 | 138 | const match1 = hashLine.match(hashRegex) 139 | 140 | if (!match1) return null 141 | 142 | const [, hash] = match1 143 | 144 | let authorLine = lines.shift() 145 | 146 | // Parsing of long names 147 | while (lines[0].startsWith(' ')) { 148 | authorLine += ` ${lines.shift()}` 149 | } 150 | 151 | if (!authorLine) return null 152 | 153 | const match2 = authorLine.match(authorRegex) 154 | 155 | if (!match2) return null 156 | 157 | const [, authorName, , authorEmail] = match2 158 | 159 | const dateLine = lines.shift() 160 | 161 | if (!dateLine) return null 162 | 163 | const [, date] = dateLine.split('Date: ') 164 | 165 | const messageLine = lines.shift() 166 | 167 | if (!messageLine) return null 168 | 169 | const [, message] = messageLine.split('Subject: ') 170 | 171 | return { 172 | hash, 173 | authorName: formatAuthorName(authorName), 174 | authorEmail, 175 | date, 176 | message, 177 | } 178 | } 179 | 180 | function splitIntoParts(lines: string[], separator: string) { 181 | const parts = [] 182 | let currentPart: string[] | undefined 183 | 184 | lines.forEach(line => { 185 | if (line.startsWith(separator)) { 186 | if (currentPart) { 187 | parts.push(currentPart) 188 | } 189 | 190 | currentPart = [line] 191 | } 192 | else if (currentPart) { 193 | currentPart.push(line) 194 | } 195 | }) 196 | 197 | if (currentPart) { 198 | parts.push(currentPart) 199 | } 200 | 201 | return parts 202 | } 203 | 204 | function formatAuthorName(name: string) { 205 | return (name.startsWith('"') && name.endsWith('"') ? name.slice(1, -1) : name).trim() 206 | } 207 | 208 | export default parseGitPatch 209 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import parse from '../src' 5 | 6 | const dataLocation = path.resolve(__dirname, 'tests-data') 7 | const data: Record = {} 8 | 9 | fs.readdirSync(dataLocation).forEach(fileName => { 10 | data[fileName] = fs.readFileSync(path.resolve(dataLocation, fileName), 'utf-8') 11 | }) 12 | 13 | test('is a function', () => { 14 | expect(typeof parse).toBe('function') 15 | }) 16 | 17 | test('is a function that accepts a string', () => { 18 | expect(() => parse('')).not.toThrow() 19 | // @ts-ignore 20 | expect(() => parse(1)).toThrow() 21 | }) 22 | 23 | test('parses a simple patch', () => { 24 | const patchResult = parse(data['one-file.patch']) 25 | const diffResult = parse(data['one-file-diff.patch']) 26 | 27 | expect.assertions(2) 28 | 29 | const expectResultFiles = [ 30 | { 31 | added: false, 32 | deleted: false, 33 | beforeName: 'src/events/http/HttpServer.js', 34 | afterName: 'src/events/http/HttpServer.js', 35 | modifiedLines: [ 36 | { 37 | line: ' if (result && typeof result.body !== \'undefined\') {', 38 | lineNumber: 773, 39 | added: false, 40 | }, 41 | { 42 | line: ' if (typeof result === \'string\') {', 43 | lineNumber: 773, 44 | added: true, 45 | }, 46 | { 47 | line: ' response.source = JSON.stringify(result)', 48 | lineNumber: 774, 49 | added: true, 50 | }, 51 | { 52 | line: ' } else if (result && typeof result.body !== \'undefined\') {', 53 | lineNumber: 775, 54 | added: true, 55 | }, 56 | ], 57 | }, 58 | ] 59 | 60 | expect(patchResult).toEqual({ 61 | hash: '0f6f88c98fff3afa0289f46bf4eab469f45eebc6', 62 | date: 'Sat, 25 Jan 2020 19:21:35 +0200', 63 | message: '[PATCH] JSON stringify string responses', 64 | authorEmail: '13507001+arnas@users.noreply.github.com', 65 | authorName: 'Arnas Gecas', 66 | files: expectResultFiles, 67 | }) 68 | 69 | expect(diffResult).toEqual({ 70 | files: expectResultFiles, 71 | }) 72 | }) 73 | 74 | test('parses a complex patch', () => { 75 | const result = parse(data['many-files.patch']) 76 | 77 | expect(result).toEqual({ 78 | hash: 'a7696becf41fa2b5c9c93770e25a5cce6174d3b8', 79 | date: 'Sat, 11 Jan 2020 08:19:48 -0500', 80 | message: '[PATCH] Fix path/resource/resourcePath in Lambda events, fixes #868', 81 | authorEmail: 'dnalborczyk@gmail.com', 82 | authorName: 'Daniel Nalborczyk', 83 | files: [ 84 | { 85 | added: false, 86 | deleted: false, 87 | beforeName: 'src/events/http/HttpServer.js', 88 | afterName: 'src/events/http/HttpServer.js', 89 | modifiedLines: [ 90 | { 91 | added: true, 92 | line: ' _path,', 93 | lineNumber: 476, 94 | }, 95 | { 96 | added: true, 97 | line: ' _path,', 98 | lineNumber: 492, 99 | }, 100 | ], 101 | }, 102 | { 103 | added: false, 104 | deleted: false, 105 | beforeName: 'src/events/http/lambda-events/LambdaIntegrationEvent.js', 106 | afterName: 'src/events/http/lambda-events/LambdaIntegrationEvent.js', 107 | modifiedLines: [ 108 | { 109 | added: true, 110 | line: ' #path = null', 111 | lineNumber: 5, 112 | }, 113 | { 114 | added: false, 115 | line: ' constructor(request, stage, requestTemplate) {', 116 | lineNumber: 9, 117 | }, 118 | { 119 | added: true, 120 | line: ' constructor(request, stage, requestTemplate, path) {', 121 | lineNumber: 10, 122 | }, 123 | { 124 | added: true, 125 | line: ' this.#path = path', 126 | lineNumber: 11, 127 | }, 128 | { 129 | added: true, 130 | line: ' this.#path,', 131 | lineNumber: 22, 132 | }, 133 | ], 134 | }, 135 | { 136 | added: false, 137 | deleted: false, 138 | beforeName: 'src/events/http/lambda-events/LambdaProxyIntegrationEvent.js', 139 | afterName: 'src/events/http/lambda-events/LambdaProxyIntegrationEvent.js', 140 | modifiedLines: [ 141 | { 142 | added: true, 143 | line: ' #path = null', 144 | lineNumber: 19, 145 | }, 146 | { 147 | added: false, 148 | line: ' constructor(request, stage) {', 149 | lineNumber: 22, 150 | }, 151 | { 152 | added: true, 153 | line: ' constructor(request, stage, path) {', 154 | lineNumber: 23, 155 | }, 156 | { 157 | added: true, 158 | line: ' this.#path = path', 159 | lineNumber: 24, 160 | }, 161 | { 162 | added: false, 163 | line: ' path,', 164 | lineNumber: 109, 165 | }, 166 | { 167 | added: false, 168 | line: ' path,', 169 | lineNumber: 128, 170 | }, 171 | { 172 | added: true, 173 | line: ' path: this.#path,', 174 | lineNumber: 129, 175 | }, 176 | { 177 | added: false, 178 | // eslint-disable-next-line 179 | line: ' path: `/${this.#stage}${this.#request.route.path}`,', 180 | lineNumber: 173, 181 | }, 182 | { 183 | added: true, 184 | line: ' path: this.#request.route.path,', 185 | lineNumber: 174, 186 | }, 187 | { 188 | added: false, 189 | line: ' resourcePath: this.#request.route.path,', 190 | lineNumber: 179, 191 | }, 192 | { 193 | added: true, 194 | line: ' resourcePath: this.#path,', 195 | lineNumber: 180, 196 | }, 197 | { 198 | added: false, 199 | line: ' resource: this.#request.route.path,', 200 | lineNumber: 182, 201 | }, 202 | { 203 | added: true, 204 | line: ' resource: this.#path,', 205 | lineNumber: 183, 206 | }, 207 | ], 208 | }, 209 | { 210 | added: false, 211 | deleted: false, 212 | beforeName: 'src/events/http/lambda-events/VelocityContext.js', 213 | afterName: 'src/events/http/lambda-events/VelocityContext.js', 214 | modifiedLines: [ 215 | { 216 | added: true, 217 | line: ' #path = null', 218 | lineNumber: 39, 219 | }, 220 | { 221 | added: false, 222 | line: ' constructor(request, stage, payload) {', 223 | lineNumber: 43, 224 | }, 225 | { 226 | added: true, 227 | line: ' constructor(request, stage, payload, path) {', 228 | lineNumber: 44, 229 | }, 230 | { 231 | added: true, 232 | line: ' this.#path = path', 233 | lineNumber: 45, 234 | }, 235 | { 236 | added: false, 237 | line: ' resourcePath: this.#request.route.path,', 238 | lineNumber: 109, 239 | }, 240 | { 241 | added: true, 242 | line: ' resourcePath: this.#path,', 243 | lineNumber: 111, 244 | }, 245 | ], 246 | }, 247 | ], 248 | }) 249 | }) 250 | 251 | test('parses a renaming patch', () => { 252 | const result = parse(data['rename-file.patch']) 253 | 254 | expect(result).toEqual({ 255 | hash: '68ec4bbde5244929afee1b39e09dced6fad1a725', 256 | date: 'Mon, 27 Jan 2020 17:35:01 +0100', 257 | message: '[PATCH] Rename README', 258 | authorEmail: 'dherault@gmail.com', 259 | authorName: '=?UTF-8?q?David=20H=C3=A9rault?=', 260 | files: [ 261 | { 262 | added: false, 263 | deleted: false, 264 | beforeName: 'README.md', 265 | afterName: 'README.mdx', 266 | modifiedLines: [ 267 | ], 268 | }, 269 | ], 270 | }) 271 | }) 272 | 273 | test('parses a add and delete patch', () => { 274 | const result = parse(data['add-and-delete-file.patch']) 275 | 276 | expect(result).toEqual({ 277 | hash: '74d652cd9cda9849591d1c414caae0af23b19c8d', 278 | message: '[PATCH] Rename and edit README', 279 | authorEmail: 'dherault@gmail.com', 280 | authorName: '=?UTF-8?q?David=20H=C3=A9rault?=', 281 | date: 'Mon, 27 Jan 2020 17:36:29 +0100', 282 | files: [ 283 | { 284 | added: false, 285 | deleted: true, 286 | afterName: 'README.md', 287 | beforeName: 'README.md', 288 | modifiedLines: [ 289 | { 290 | added: false, 291 | line: '# stars-in-motion', 292 | lineNumber: 1, 293 | }, 294 | { 295 | added: false, 296 | line: '', 297 | lineNumber: 2, 298 | }, 299 | { 300 | added: false, 301 | line: 'A canvas full of stars', 302 | lineNumber: 3, 303 | }, 304 | ], 305 | }, 306 | { 307 | added: true, 308 | deleted: false, 309 | afterName: 'README.mdx', 310 | beforeName: 'README.mdx', 311 | modifiedLines: [ 312 | { 313 | added: true, 314 | line: '# stars-in-motion', 315 | lineNumber: 1, 316 | }, 317 | { 318 | added: true, 319 | line: '', 320 | lineNumber: 2, 321 | }, 322 | { 323 | added: true, 324 | line: 'A canvas full of stars.', 325 | lineNumber: 3, 326 | }, 327 | ], 328 | }, 329 | ], 330 | }) 331 | }) 332 | 333 | test('parses a add and delete patch with hyphen', () => { 334 | const result = parse(data['hyphen.patch']) 335 | 336 | expect(result).toEqual({ 337 | hash: '89afcd42fb6f2602fbcd03d6e5573b1859347787', 338 | authorName: 'Restyled.io', 339 | authorEmail: 'commits@restyled.io', 340 | date: 'Fri, 17 Jan 2025 18:09:56 +0000', 341 | message: '[PATCH 2/2] Restyled by prettier-yaml', 342 | files: [ 343 | { 344 | added: false, 345 | deleted: false, 346 | beforeName: 'hlint/.hlint.yaml', 347 | afterName: 'hlint/.hlint.yaml', 348 | modifiedLines: [ 349 | { 350 | added: false, 351 | lineNumber: 27, 352 | line: '', 353 | }, 354 | { 355 | added: false, 356 | lineNumber: 29, 357 | line: '- error: {name: ""}', 358 | }, 359 | { 360 | added: true, 361 | lineNumber: 28, 362 | line: '- error: { name: "" }', 363 | }, 364 | ], 365 | }, 366 | ], 367 | }) 368 | }) 369 | 370 | test('parses a complex patch 2', () => { 371 | parse(data['complex.patch']) 372 | }) 373 | 374 | test('parses a long author name', () => { 375 | const result = parse(data['one-file-author-line-break.patch']) 376 | 377 | expect(result).toEqual({ 378 | hash: '0f6f88c98fff3afa0289f46bf4eab469f45eebc6', 379 | date: 'Sat, 25 Jan 2020 19:21:35 +0200', 380 | message: '[PATCH] JSON stringify string responses', 381 | authorEmail: 'someone@example.com', 382 | authorName: 'Really long name spanning lots of characters', 383 | files: [ 384 | { 385 | added: false, 386 | deleted: false, 387 | beforeName: 'src/events/http/HttpServer.js', 388 | afterName: 'src/events/http/HttpServer.js', 389 | modifiedLines: [ 390 | { 391 | line: ' if (result && typeof result.body !== \'undefined\') {', 392 | lineNumber: 773, 393 | added: false, 394 | }, 395 | { 396 | line: ' if (typeof result === \'string\') {', 397 | lineNumber: 773, 398 | added: true, 399 | }, 400 | { 401 | line: ' response.source = JSON.stringify(result)', 402 | lineNumber: 774, 403 | added: true, 404 | }, 405 | { 406 | line: ' } else if (result && typeof result.body !== \'undefined\') {', 407 | lineNumber: 775, 408 | added: true, 409 | }, 410 | ], 411 | }, 412 | ], 413 | }) 414 | }) 415 | -------------------------------------------------------------------------------- /test/tests-data/add-and-delete-file.patch: -------------------------------------------------------------------------------- 1 | From 74d652cd9cda9849591d1c414caae0af23b19c8d Mon Sep 17 00:00:00 2001 2 | From: =?UTF-8?q?David=20H=C3=A9rault?= 3 | Date: Mon, 27 Jan 2020 17:36:29 +0100 4 | Subject: [PATCH] Rename and edit README 5 | 6 | --- 7 | README.md | 3 --- 8 | README.mdx | 3 +++ 9 | 2 files changed, 3 insertions(+), 3 deletions(-) 10 | delete mode 100644 README.md 11 | create mode 100644 README.mdx 12 | 13 | diff --git a/README.md b/README.md 14 | deleted file mode 100644 15 | index e0b718c..0000000 16 | --- a/README.md 17 | +++ /dev/null 18 | @@ -1,3 +0,0 @@ 19 | -# stars-in-motion 20 | - 21 | -A canvas full of stars 22 | diff --git a/README.mdx b/README.mdx 23 | new file mode 100644 24 | index 0000000..35af58f 25 | --- /dev/null 26 | +++ b/README.mdx 27 | @@ -0,0 +1,3 @@ 28 | +# stars-in-motion 29 | + 30 | +A canvas full of stars. 31 | -- 32 | 2.21.1 (Apple Git-122.3) 33 | 34 | -------------------------------------------------------------------------------- /test/tests-data/complex.patch: -------------------------------------------------------------------------------- 1 | From e54cdc33da095bebd6c6bf0d2cc502bb7549b072 Mon Sep 17 00:00:00 2001 2 | From: =?UTF-8?q?David=20H=C3=A9rault?= 3 | Date: Sat, 22 Jun 2019 18:41:22 +0200 4 | Subject: [PATCH] Lint and rename folders 5 | 6 | --- 7 | .eslintignore | 5 +- 8 | .eslintrc.js | 4 + 9 | manual_test_nodejs/handler.js | 3 +- 10 | manual_test_nodejs/subprocess.js | 1 - 11 | .../RouteSelection/handler.js | 31 ++ 12 | .../package-lock.json | 0 13 | .../package.json | 0 14 | .../scripts/deploy_to_aws.sh | 0 15 | .../scripts/deploy_to_offline.sh | 0 16 | .../scripts/serverless..yml | 0 17 | .../scripts/serverless.aws.yml | 0 18 | .../scripts/serverless.offline.yml | 0 19 | .../serverless.yml | 0 20 | .../RouteSelection/test/e2e/ws.e2e.js | 59 +++ 21 | .../test/support/WebSocketTester.js | 62 +++ 22 | manual_test_websocket/main/handler.js | 131 +++++++ 23 | .../package-lock.json | 0 24 | .../package.json | 0 25 | .../scripts/deploy_to_aws.sh | 0 26 | .../scripts/deploy_to_offline.sh | 0 27 | .../scripts/serverless..yml | 0 28 | .../scripts/serverless.aws.yml | 0 29 | .../scripts/serverless.offline.yml | 0 30 | .../serverless.yml | 0 31 | manual_test_websocket/main/test/e2e/ws.e2e.js | 360 ++++++++++++++++++ 32 | .../main/test/support/WebSocketTester.js | 62 +++ 33 | .../handler.js | 37 -- 34 | .../test/e2e/ws.e2e.js | 53 --- 35 | .../test/support/WebSocketTester.js | 62 --- 36 | .../manual_test_websocket_main/handler.js | 150 -------- 37 | .../test/e2e/ws.e2e.js | 342 ----------------- 38 | .../test/support/WebSocketTester.js | 60 --- 39 | 32 files changed, 711 insertions(+), 711 deletions(-) 40 | create mode 100644 manual_test_websocket/RouteSelection/handler.js 41 | rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/package-lock.json (100%) 42 | rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/package.json (100%) 43 | rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/scripts/deploy_to_aws.sh (100%) 44 | rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/scripts/deploy_to_offline.sh (100%) 45 | rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/scripts/serverless..yml (100%) 46 | rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/scripts/serverless.aws.yml (100%) 47 | rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/scripts/serverless.offline.yml (100%) 48 | rename manual_test_websocket/{manual_test_websocket_RouteSelection => RouteSelection}/serverless.yml (100%) 49 | create mode 100644 manual_test_websocket/RouteSelection/test/e2e/ws.e2e.js 50 | create mode 100644 manual_test_websocket/RouteSelection/test/support/WebSocketTester.js 51 | create mode 100644 manual_test_websocket/main/handler.js 52 | rename manual_test_websocket/{manual_test_websocket_main => main}/package-lock.json (100%) 53 | rename manual_test_websocket/{manual_test_websocket_main => main}/package.json (100%) 54 | rename manual_test_websocket/{manual_test_websocket_main => main}/scripts/deploy_to_aws.sh (100%) 55 | rename manual_test_websocket/{manual_test_websocket_main => main}/scripts/deploy_to_offline.sh (100%) 56 | rename manual_test_websocket/{manual_test_websocket_main => main}/scripts/serverless..yml (100%) 57 | rename manual_test_websocket/{manual_test_websocket_main => main}/scripts/serverless.aws.yml (100%) 58 | rename manual_test_websocket/{manual_test_websocket_main => main}/scripts/serverless.offline.yml (100%) 59 | rename manual_test_websocket/{manual_test_websocket_main => main}/serverless.yml (100%) 60 | create mode 100644 manual_test_websocket/main/test/e2e/ws.e2e.js 61 | create mode 100644 manual_test_websocket/main/test/support/WebSocketTester.js 62 | delete mode 100644 manual_test_websocket/manual_test_websocket_RouteSelection/handler.js 63 | delete mode 100644 manual_test_websocket/manual_test_websocket_RouteSelection/test/e2e/ws.e2e.js 64 | delete mode 100644 manual_test_websocket/manual_test_websocket_RouteSelection/test/support/WebSocketTester.js 65 | delete mode 100644 manual_test_websocket/manual_test_websocket_main/handler.js 66 | delete mode 100644 manual_test_websocket/manual_test_websocket_main/test/e2e/ws.e2e.js 67 | delete mode 100644 manual_test_websocket/manual_test_websocket_main/test/support/WebSocketTester.js 68 | 69 | diff --git a/.eslintignore b/.eslintignore 70 | index 211df65..cf70988 100644 71 | --- a/.eslintignore 72 | +++ b/.eslintignore 73 | @@ -1,4 +1 @@ 74 | -manual_test_nodejs 75 | -manual_test_python 76 | -manual_test_ruby 77 | -manual_test_websocket 78 | +**/node_modules 79 | diff --git a/.eslintrc.js b/.eslintrc.js 80 | index 5c7d6c5..cf14cb5 100644 81 | --- a/.eslintrc.js 82 | +++ b/.eslintrc.js 83 | @@ -21,4 +21,8 @@ if (env.TRAVIS && platform === 'win32') { 84 | module.exports = { 85 | extends: 'dherault', 86 | rules, 87 | + env: { 88 | + node: true, 89 | + mocha: true, 90 | + }, 91 | }; 92 | diff --git a/manual_test_nodejs/handler.js b/manual_test_nodejs/handler.js 93 | index 568b4dc..12b454a 100644 94 | --- a/manual_test_nodejs/handler.js 95 | +++ b/manual_test_nodejs/handler.js 96 | @@ -1,4 +1,3 @@ 97 | -'use strict'; 98 | 99 | module.exports.hello = (event, context, callback) => { 100 | const response = { 101 | @@ -28,7 +27,7 @@ module.exports.rejectedPromise = (event, context, callback) => { 102 | callback(null, response); 103 | }; 104 | 105 | -module.exports.authFunction = (event, context, callback) => { 106 | +module.exports.authFunction = (event, context) => { 107 | context.succeed({ 108 | principalId: 'xxxxxxx', // the principal user identification associated with the token send by the client 109 | policyDocument: { 110 | diff --git a/manual_test_nodejs/subprocess.js b/manual_test_nodejs/subprocess.js 111 | index 9132207..7794c64 100644 112 | --- a/manual_test_nodejs/subprocess.js 113 | +++ b/manual_test_nodejs/subprocess.js 114 | @@ -1,4 +1,3 @@ 115 | -'use strict'; 116 | 117 | const { exec } = require('child_process'); 118 | 119 | diff --git a/manual_test_websocket/RouteSelection/handler.js b/manual_test_websocket/RouteSelection/handler.js 120 | new file mode 100644 121 | index 0000000..d43ffeb 122 | --- /dev/null 123 | +++ b/manual_test_websocket/RouteSelection/handler.js 124 | @@ -0,0 +1,31 @@ 125 | +const AWS = require('aws-sdk'); 126 | + 127 | +const successfullResponse = { 128 | + statusCode: 200, 129 | + body: 'Request is OK.', 130 | +}; 131 | + 132 | +module.exports.echo = async (event, context) => { 133 | + const action = JSON.parse(event.body); 134 | + 135 | + await sendToClient(action.message, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)); 136 | + 137 | + return successfullResponse; 138 | +}; 139 | + 140 | +const newAWSApiGatewayManagementApi = event => { 141 | + let endpoint = event.apiGatewayUrl; 142 | + 143 | + if (!endpoint) endpoint = `${event.requestContext.domainName}/${event.requestContext.stage}`; 144 | + const apiVersion = '2018-11-29'; 145 | + 146 | + return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); 147 | +}; 148 | + 149 | +const sendToClient = (data, connectionId, apigwManagementApi) => { 150 | + // console.log(`sendToClient:${connectionId}`); 151 | + let sendee = data; 152 | + if (typeof data === 'object') sendee = JSON.stringify(data); 153 | + 154 | + return apigwManagementApi.postToConnection({ ConnectionId: connectionId, Data: sendee }).promise(); 155 | +}; 156 | diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/package-lock.json b/manual_test_websocket/RouteSelection/package-lock.json 157 | similarity index 100% 158 | rename from manual_test_websocket/manual_test_websocket_RouteSelection/package-lock.json 159 | rename to manual_test_websocket/RouteSelection/package-lock.json 160 | diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/package.json b/manual_test_websocket/RouteSelection/package.json 161 | similarity index 100% 162 | rename from manual_test_websocket/manual_test_websocket_RouteSelection/package.json 163 | rename to manual_test_websocket/RouteSelection/package.json 164 | diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_aws.sh b/manual_test_websocket/RouteSelection/scripts/deploy_to_aws.sh 165 | similarity index 100% 166 | rename from manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_aws.sh 167 | rename to manual_test_websocket/RouteSelection/scripts/deploy_to_aws.sh 168 | diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_offline.sh b/manual_test_websocket/RouteSelection/scripts/deploy_to_offline.sh 169 | similarity index 100% 170 | rename from manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_offline.sh 171 | rename to manual_test_websocket/RouteSelection/scripts/deploy_to_offline.sh 172 | diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless..yml b/manual_test_websocket/RouteSelection/scripts/serverless..yml 173 | similarity index 100% 174 | rename from manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless..yml 175 | rename to manual_test_websocket/RouteSelection/scripts/serverless..yml 176 | diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.aws.yml b/manual_test_websocket/RouteSelection/scripts/serverless.aws.yml 177 | similarity index 100% 178 | rename from manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.aws.yml 179 | rename to manual_test_websocket/RouteSelection/scripts/serverless.aws.yml 180 | diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.offline.yml b/manual_test_websocket/RouteSelection/scripts/serverless.offline.yml 181 | similarity index 100% 182 | rename from manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.offline.yml 183 | rename to manual_test_websocket/RouteSelection/scripts/serverless.offline.yml 184 | diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/serverless.yml b/manual_test_websocket/RouteSelection/serverless.yml 185 | similarity index 100% 186 | rename from manual_test_websocket/manual_test_websocket_RouteSelection/serverless.yml 187 | rename to manual_test_websocket/RouteSelection/serverless.yml 188 | diff --git a/manual_test_websocket/RouteSelection/test/e2e/ws.e2e.js b/manual_test_websocket/RouteSelection/test/e2e/ws.e2e.js 189 | new file mode 100644 190 | index 0000000..fcd11c0 191 | --- /dev/null 192 | +++ b/manual_test_websocket/RouteSelection/test/e2e/ws.e2e.js 193 | @@ -0,0 +1,59 @@ 194 | +/* eslint-disable import/no-extraneous-dependencies */ 195 | + 196 | +const chai = require('chai'); 197 | + 198 | +const WebSocketTester = require('../support/WebSocketTester'); 199 | + 200 | +const expect = chai.expect; 201 | +const endpoint = process.env.npm_config_endpoint || 'ws://localhost:3005'; 202 | +const timeout = process.env.npm_config_timeout ? parseInt(process.env.npm_config_timeout) : 1000; 203 | + 204 | +describe('serverless', () => { 205 | + describe('with WebSocket support', () => { 206 | + let clients = []; 207 | + 208 | + const createWebSocket = async qs => { 209 | + const ws = new WebSocketTester(); 210 | + let url = endpoint; 211 | + 212 | + if (qs) url = `${endpoint}?${qs}`; 213 | + 214 | + await ws.open(url); 215 | + 216 | + clients.push(ws); 217 | + 218 | + return ws; 219 | + }; 220 | + 221 | + beforeEach(() => { 222 | + clients = []; 223 | + }); 224 | + 225 | + afterEach(async () => { 226 | + await Promise.all(clients.map(async (ws, i) => { 227 | + const n = ws.countUnrecived(); 228 | + 229 | + if (n > 0) { 230 | + console.log(`unreceived:[i=${i}]`); 231 | + (await ws.receive(n)).forEach(m => console.log(m)); 232 | + } 233 | + 234 | + expect(n).to.equal(0); 235 | + ws.close(); 236 | + })); 237 | + 238 | + clients = []; 239 | + }); 240 | + 241 | + it('should call action \'echo\' handler located at service.do', async () => { 242 | + const ws = await createWebSocket(); 243 | + const now = `${Date.now()}`; 244 | + const payload = JSON.stringify({ service:{ do:'echo' }, message:now }); 245 | + 246 | + ws.send(payload); 247 | + 248 | + expect(await ws.receive1()).to.equal(`${now}`); 249 | + }).timeout(timeout); 250 | + 251 | + }); 252 | +}); 253 | diff --git a/manual_test_websocket/RouteSelection/test/support/WebSocketTester.js b/manual_test_websocket/RouteSelection/test/support/WebSocketTester.js 254 | new file mode 100644 255 | index 0000000..aaeff5a 256 | --- /dev/null 257 | +++ b/manual_test_websocket/RouteSelection/test/support/WebSocketTester.js 258 | @@ -0,0 +1,62 @@ 259 | +/* eslint-disable import/no-extraneous-dependencies */ 260 | +const WebSocket = require('ws'); 261 | + 262 | +class WebSocketTester { 263 | + constructor() { 264 | + this.messages = []; this.receivers = []; 265 | + } 266 | + 267 | + open(url) { 268 | + if (this.ws != null) return; 269 | + const ws = this.ws = new WebSocket(url); 270 | + ws.on('message', message => { 271 | + // console.log('Received: '+message); 272 | + if (this.receivers.length > 0) this.receivers.shift()(message); 273 | + else this.messages.push(message); 274 | + }); 275 | + 276 | + return new Promise(resolve => { 277 | + ws.on('open', () => { 278 | + resolve(true); 279 | + }); 280 | + }); 281 | + } 282 | + 283 | + send(data) { 284 | + this.ws.send(data); 285 | + } 286 | + 287 | + receive1() { 288 | + return new Promise(resolve => { 289 | + if (this.messages.length > 0) resolve(this.messages.shift()); 290 | + else this.receivers.push(resolve); 291 | + }); 292 | + } 293 | + 294 | + receive(n) { 295 | + return new Promise(resolve => { 296 | + const messages = []; 297 | + for (let i = 0; i < n; i += 1) { 298 | + this.receive1().then(message => { 299 | + messages[i] = message; 300 | + if (i === n - 1) resolve(messages); 301 | + }); 302 | + } 303 | + }); 304 | + } 305 | + 306 | + skip() { 307 | + if (this.messages.length > 0) this.messages.shift(); 308 | + else this.receivers.push(() => {}); 309 | + } 310 | + 311 | + countUnrecived() { 312 | + return this.messages.length; 313 | + } 314 | + 315 | + close() { 316 | + if (this.ws != null) this.ws.close(); 317 | + } 318 | +} 319 | + 320 | +module.exports = WebSocketTester; 321 | diff --git a/manual_test_websocket/main/handler.js b/manual_test_websocket/main/handler.js 322 | new file mode 100644 323 | index 0000000..58c4d7d 324 | --- /dev/null 325 | +++ b/manual_test_websocket/main/handler.js 326 | @@ -0,0 +1,131 @@ 327 | +const AWS = require('aws-sdk'); 328 | + 329 | +const ddb = (() => { 330 | + if (process.env.IS_OFFLINE) return new AWS.DynamoDB.DocumentClient({ region: 'localhost', endpoint: 'http://localhost:8000' }); 331 | + 332 | + return new AWS.DynamoDB.DocumentClient(); 333 | +})(); 334 | + 335 | +const successfullResponse = { 336 | + statusCode: 200, 337 | + body: 'Request is OK.', 338 | +}; 339 | + 340 | +module.exports.connect = async (event, context) => { 341 | + // console.log('connect:'); 342 | + const listener = await ddb.get({ TableName:'listeners', Key:{ name:'default' } }).promise(); 343 | + 344 | + if (listener.Item) { 345 | + const timeout = new Promise(resolve => setTimeout(resolve, 100)); 346 | + const send = sendToClient( // sendToClient won't return on AWS when client doesn't exits so we set a timeout 347 | + JSON.stringify({ action:'update', event:'connect', info:{ id:event.requestContext.connectionId, event:{ ...event, apiGatewayUrl:`${event.apiGatewayUrl}` }, context } }), 348 | + listener.Item.id, 349 | + newAWSApiGatewayManagementApi(event, context)).catch(() => {}); 350 | + await Promise.race([send, timeout]); 351 | + } 352 | + 353 | + return successfullResponse; 354 | +}; 355 | + 356 | +// module.export.auth = (event, context, callback) => { 357 | +// //console.log('auth:'); 358 | +// const token = event.headers["Authorization"]; 359 | + 360 | +// if ('deny'===token) callback(null, generatePolicy('user', 'Deny', event.methodArn)); 361 | +// else callback(null, generatePolicy('user', 'Allow', event.methodArn));; 362 | +// }; 363 | + 364 | +module.exports.disconnect = async (event, context) => { 365 | + const listener = await ddb.get({ TableName:'listeners', Key:{ name:'default' } }).promise(); 366 | + if (listener.Item) await sendToClient(JSON.stringify({ action:'update', event:'disconnect', info:{ id:event.requestContext.connectionId, event:{ ...event, apiGatewayUrl:`${event.apiGatewayUrl}` }, context } }), listener.Item.id, newAWSApiGatewayManagementApi(event, context)).catch(() => {}); 367 | + 368 | + return successfullResponse; 369 | +}; 370 | + 371 | +module.exports.defaultHandler = async (event, context) => { 372 | + await sendToClient(`Error: No Supported Action in Payload '${event.body}'`, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err => console.log(err)); 373 | + 374 | + return successfullResponse; 375 | +}; 376 | + 377 | +module.exports.getClientInfo = async (event, context) => { 378 | + // console.log('getClientInfo:'); 379 | + await sendToClient({ action:'update', event:'client-info', info:{ id:event.requestContext.connectionId } }, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err => console.log(err)); 380 | + 381 | + return successfullResponse; 382 | +}; 383 | + 384 | +module.exports.getCallInfo = async (event, context) => { 385 | + await sendToClient({ action:'update', event:'call-info', info:{ event:{ ...event, apiGatewayUrl:`${event.apiGatewayUrl}` }, context } }, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err => console.log(err)); 386 | + 387 | + return successfullResponse; 388 | +}; 389 | + 390 | +module.exports.makeError = async () => { 391 | + const obj = null; 392 | + obj.non.non = 1; 393 | + 394 | + return successfullResponse; 395 | +}; 396 | + 397 | +module.exports.replyViaCallback = (event, context, callback) => { 398 | + sendToClient({ action:'update', event:'reply-via-callback' }, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err => console.log(err)); 399 | + callback(); 400 | +}; 401 | + 402 | +module.exports.replyErrorViaCallback = (event, context, callback) => callback('error error error'); 403 | + 404 | +module.exports.multiCall1 = async (event, context) => { 405 | + await sendToClient({ action:'update', event:'made-call-1' }, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err => console.log(err)); 406 | + 407 | + return successfullResponse; 408 | +}; 409 | + 410 | +module.exports.multiCall2 = async (event, context) => { 411 | + await sendToClient({ action:'update', event:'made-call-2' }, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err => console.log(err)); 412 | + 413 | + return successfullResponse; 414 | +}; 415 | + 416 | +module.exports.send = async (event, context) => { 417 | + const action = JSON.parse(event.body); 418 | + const sents = []; 419 | + action.clients.forEach(connectionId => { 420 | + const sent = sendToClient(action.data, connectionId, newAWSApiGatewayManagementApi(event, context)); 421 | + sents.push(sent); 422 | + }); 423 | + const noErr = await Promise.all(sents).then(() => true).catch(() => false); 424 | + if (!noErr) await sendToClient('Error: Could not Send all Messages', event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)); 425 | + 426 | + return successfullResponse; 427 | +}; 428 | + 429 | +module.exports.registerListener = async (event, context) => { 430 | + await ddb.put({ TableName:'listeners', Item:{ name:'default', id:event.requestContext.connectionId } }).promise(); 431 | + await sendToClient({ action:'update', event:'register-listener', info:{ id:event.requestContext.connectionId } }, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err => console.log(err)); 432 | + 433 | + return successfullResponse; 434 | +}; 435 | + 436 | +module.exports.deleteListener = async () => { 437 | + await ddb.delete({ TableName:'listeners', Key:{ name:'default' } }).promise(); 438 | + 439 | + return successfullResponse; 440 | +}; 441 | + 442 | +const newAWSApiGatewayManagementApi = event => { 443 | + let endpoint = event.apiGatewayUrl; 444 | + 445 | + if (!endpoint) endpoint = `${event.requestContext.domainName}/${event.requestContext.stage}`; 446 | + const apiVersion = '2018-11-29'; 447 | + 448 | + return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); 449 | +}; 450 | + 451 | +const sendToClient = (data, connectionId, apigwManagementApi) => { 452 | + // console.log(`sendToClient:${connectionId}`); 453 | + let sendee = data; 454 | + if (typeof data === 'object') sendee = JSON.stringify(data); 455 | + 456 | + return apigwManagementApi.postToConnection({ ConnectionId: connectionId, Data: sendee }).promise(); 457 | +}; 458 | diff --git a/manual_test_websocket/manual_test_websocket_main/package-lock.json b/manual_test_websocket/main/package-lock.json 459 | similarity index 100% 460 | rename from manual_test_websocket/manual_test_websocket_main/package-lock.json 461 | rename to manual_test_websocket/main/package-lock.json 462 | diff --git a/manual_test_websocket/manual_test_websocket_main/package.json b/manual_test_websocket/main/package.json 463 | similarity index 100% 464 | rename from manual_test_websocket/manual_test_websocket_main/package.json 465 | rename to manual_test_websocket/main/package.json 466 | diff --git a/manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_aws.sh b/manual_test_websocket/main/scripts/deploy_to_aws.sh 467 | similarity index 100% 468 | rename from manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_aws.sh 469 | rename to manual_test_websocket/main/scripts/deploy_to_aws.sh 470 | diff --git a/manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_offline.sh b/manual_test_websocket/main/scripts/deploy_to_offline.sh 471 | similarity index 100% 472 | rename from manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_offline.sh 473 | rename to manual_test_websocket/main/scripts/deploy_to_offline.sh 474 | diff --git a/manual_test_websocket/manual_test_websocket_main/scripts/serverless..yml b/manual_test_websocket/main/scripts/serverless..yml 475 | similarity index 100% 476 | rename from manual_test_websocket/manual_test_websocket_main/scripts/serverless..yml 477 | rename to manual_test_websocket/main/scripts/serverless..yml 478 | diff --git a/manual_test_websocket/manual_test_websocket_main/scripts/serverless.aws.yml b/manual_test_websocket/main/scripts/serverless.aws.yml 479 | similarity index 100% 480 | rename from manual_test_websocket/manual_test_websocket_main/scripts/serverless.aws.yml 481 | rename to manual_test_websocket/main/scripts/serverless.aws.yml 482 | diff --git a/manual_test_websocket/manual_test_websocket_main/scripts/serverless.offline.yml b/manual_test_websocket/main/scripts/serverless.offline.yml 483 | similarity index 100% 484 | rename from manual_test_websocket/manual_test_websocket_main/scripts/serverless.offline.yml 485 | rename to manual_test_websocket/main/scripts/serverless.offline.yml 486 | diff --git a/manual_test_websocket/manual_test_websocket_main/serverless.yml b/manual_test_websocket/main/serverless.yml 487 | similarity index 100% 488 | rename from manual_test_websocket/manual_test_websocket_main/serverless.yml 489 | rename to manual_test_websocket/main/serverless.yml 490 | diff --git a/manual_test_websocket/main/test/e2e/ws.e2e.js b/manual_test_websocket/main/test/e2e/ws.e2e.js 491 | new file mode 100644 492 | index 0000000..60f8a37 493 | --- /dev/null 494 | +++ b/manual_test_websocket/main/test/e2e/ws.e2e.js 495 | @@ -0,0 +1,360 @@ 496 | +/* eslint-disable import/no-extraneous-dependencies */ 497 | +/* eslint-disable no-unused-expressions */ 498 | +const chai = require('chai'); 499 | +const chaiHttp = require('chai-http'); 500 | + 501 | +chai.use(chaiHttp); 502 | +const expect = chai.expect; 503 | +const aws4 = require('aws4'); 504 | +const awscred = require('awscred'); 505 | +const moment = require('moment'); 506 | + 507 | +const endpoint = process.env.npm_config_endpoint || 'ws://localhost:3001'; 508 | +const timeout = process.env.npm_config_timeout ? parseInt(process.env.npm_config_timeout) : 1000; 509 | +const WebSocketTester = require('../support/WebSocketTester'); 510 | + 511 | +describe('serverless', () => { 512 | + describe('with WebSocket support', () => { 513 | + let clients = []; let req = null; let cred = null; 514 | + const createWebSocket = async qs => { 515 | + const ws = new WebSocketTester(); 516 | + let url = endpoint; 517 | + if (qs) url = `${endpoint}?${qs}`; 518 | + await ws.open(url); 519 | + clients.push(ws); 520 | + 521 | + return ws; 522 | + }; 523 | + const createClient = async qs => { 524 | + const ws = await createWebSocket(qs); 525 | + ws.send(JSON.stringify({ action:'getClientInfo' })); 526 | + const json = await ws.receive1(); 527 | + const id = JSON.parse(json).info.id; 528 | + 529 | + return { ws, id }; 530 | + }; 531 | + before(async () => { 532 | + req = chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); 533 | + // req=chai.request('http://localhost:3001/dev').keepOpen(); 534 | + cred = await new Promise((resolve, reject) => { 535 | + awscred.loadCredentials((err, data) => { 536 | + if (err) reject(err); else resolve(data); 537 | + }); 538 | + }); 539 | + }); 540 | + 541 | + beforeEach(() => { 542 | + clients = []; 543 | + }); 544 | + afterEach(async () => { 545 | + await Promise.all(clients.map(async (ws, i) => { 546 | + const n = ws.countUnrecived(); 547 | + 548 | + if (n > 0) { 549 | + console.log(`unreceived:[i=${i}]`); 550 | + (await ws.receive(n)).forEach(m => console.log(m)); 551 | + } 552 | + 553 | + expect(n).to.equal(0); 554 | + ws.close(); 555 | + })); 556 | + clients = []; 557 | + }); 558 | + 559 | + it('should request to upgade to WebSocket when receving an HTTP request', async () => { 560 | + const req = chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); 561 | + let res = await req.get(`/${Date.now()}`);// .set('Authorization', user.accessToken); 562 | + 563 | + expect(res).to.have.status(426); 564 | + 565 | + res = await req.get(`/${Date.now()}/${Date.now()}`);// .set('Authorization', user.accessToken); 566 | + 567 | + expect(res).to.have.status(426); 568 | + }).timeout(timeout); 569 | + 570 | + it('should open a WebSocket', async () => { 571 | + const ws = await createWebSocket(); 572 | + expect(ws).not.to.be.undefined; 573 | + }).timeout(timeout); 574 | + 575 | + it('should receive client connection info', async () => { 576 | + const ws = await createWebSocket(); 577 | + ws.send(JSON.stringify({ action:'getClientInfo' })); 578 | + const clientInfo = JSON.parse(await ws.receive1()); 579 | + 580 | + expect(clientInfo).to.deep.equal({ action:'update', event:'client-info', info:{ id:clientInfo.info.id } }); 581 | + }).timeout(timeout); 582 | + 583 | + it('should call default handler when no such action exists', async () => { 584 | + const ws = await createWebSocket(); 585 | + const payload = JSON.stringify({ action:`action${Date.now()}` }); 586 | + ws.send(payload); 587 | + 588 | + expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '${payload}'`); 589 | + }).timeout(timeout); 590 | + 591 | + it('should call default handler when no action provided', async () => { 592 | + const ws = await createWebSocket(); 593 | + ws.send(JSON.stringify({ hello:'world' })); 594 | + 595 | + expect(await ws.receive1()).to.equal('Error: No Supported Action in Payload \'{"hello":"world"}\''); 596 | + }).timeout(timeout); 597 | + 598 | + it('should send & receive data', async () => { 599 | + const c1 = await createClient(); 600 | + const c2 = await createClient(); 601 | + c1.ws.send(JSON.stringify({ action:'send', data:'Hello World!', clients:[c1.id, c2.id] })); 602 | + 603 | + expect(await c1.ws.receive1()).to.equal('Hello World!'); 604 | + expect(await c2.ws.receive1()).to.equal('Hello World!'); 605 | + }).timeout(timeout); 606 | + 607 | + it('should respond when having an internal server error', async () => { 608 | + const conn = await createClient(); 609 | + conn.ws.send(JSON.stringify({ action:'makeError' })); 610 | + const res = JSON.parse(await conn.ws.receive1()); 611 | + 612 | + expect(res).to.deep.equal({ message:'Internal server error', connectionId:conn.id, requestId:res.requestId }); 613 | + }).timeout(timeout); 614 | + 615 | + it('should respond via callback', async () => { 616 | + const ws = await createWebSocket(); 617 | + ws.send(JSON.stringify({ action:'replyViaCallback' })); 618 | + const res = JSON.parse(await ws.receive1()); 619 | + expect(res).to.deep.equal({ action:'update', event:'reply-via-callback' }); 620 | + }).timeout(timeout); 621 | + 622 | + it('should respond with error when calling callback(error)', async () => { 623 | + const conn = await createClient(); 624 | + conn.ws.send(JSON.stringify({ action:'replyErrorViaCallback' })); 625 | + const res = JSON.parse(await conn.ws.receive1()); 626 | + expect(res).to.deep.equal({ message:'Internal server error', connectionId:conn.id, requestId:res.requestId }); 627 | + }).timeout(timeout); 628 | + 629 | + it('should respond with only the last action when there are more than one in the serverless.yml file', async () => { 630 | + const ws = await createWebSocket(); 631 | + ws.send(JSON.stringify({ action:'makeMultiCalls' })); 632 | + const res = JSON.parse(await ws.receive1()); 633 | + 634 | + expect(res).to.deep.equal({ action:'update', event:'made-call-2' }); 635 | + }).timeout(timeout); 636 | + 637 | + it('should not send to non existing client', async () => { 638 | + const c1 = await createClient(); 639 | + c1.ws.send(JSON.stringify({ action:'send', data:'Hello World!', clients:['non-existing-id'] })); 640 | + 641 | + expect(await c1.ws.receive1()).to.equal('Error: Could not Send all Messages'); 642 | + }).timeout(timeout); 643 | + 644 | + it('should connect & disconnect', async () => { 645 | + const ws = await createWebSocket(); 646 | + await ws.send(JSON.stringify({ action:'registerListener' })); 647 | + await ws.receive1(); 648 | + 649 | + const c1 = await createClient(); 650 | + const connect1 = JSON.parse(await ws.receive1()); delete connect1.info.event; delete delete connect1.info.context; 651 | + expect(connect1).to.deep.equal({ action:'update', event:'connect', info:{ id:c1.id } }); 652 | + 653 | + const c2 = await createClient(); 654 | + const connect2 = JSON.parse(await ws.receive1()); delete connect2.info.event; delete delete connect2.info.context; 655 | + expect(connect2).to.deep.equal({ action:'update', event:'connect', info:{ id:c2.id } }); 656 | + 657 | + c2.ws.close(); 658 | + const disconnect2 = JSON.parse(await ws.receive1()); delete disconnect2.info.event; delete delete disconnect2.info.context; 659 | + expect(disconnect2).to.deep.equal({ action:'update', event:'disconnect', info:{ id:c2.id } }); 660 | + 661 | + const c3 = await createClient(); 662 | + const connect3 = JSON.parse(await ws.receive1()); delete connect3.info.event; delete delete connect3.info.context; 663 | + expect(connect3).to.deep.equal({ action:'update', event:'connect', info:{ id:c3.id } }); 664 | + 665 | + c1.ws.close(); 666 | + const disconnect1 = JSON.parse(await ws.receive1()); delete disconnect1.info.event; delete delete disconnect1.info.context; 667 | + expect(disconnect1).to.deep.equal({ action:'update', event:'disconnect', info:{ id:c1.id } }); 668 | + 669 | + c3.ws.close(); 670 | + const disconnect3 = JSON.parse(await ws.receive1()); delete disconnect3.info.event; delete delete disconnect3.info.context; 671 | + expect(disconnect3).to.deep.equal({ action:'update', event:'disconnect', info:{ id:c3.id } }); 672 | + }).timeout(timeout); 673 | + 674 | + const createExpectedEvent = (connectionId, action, eventType, actualEvent) => { 675 | + const url = new URL(endpoint); 676 | + const expected = { 677 | + apiGatewayUrl: `${actualEvent.apiGatewayUrl}`, 678 | + isBase64Encoded: false, 679 | + requestContext: { 680 | + apiId: actualEvent.requestContext.apiId, 681 | + connectedAt: actualEvent.requestContext.connectedAt, 682 | + connectionId: `${connectionId}`, 683 | + domainName: url.hostname, 684 | + eventType, 685 | + extendedRequestId: actualEvent.requestContext.extendedRequestId, 686 | + identity: { 687 | + accessKey: null, 688 | + accountId: null, 689 | + caller: null, 690 | + cognitoAuthenticationProvider: null, 691 | + cognitoAuthenticationType: null, 692 | + cognitoIdentityId: null, 693 | + cognitoIdentityPoolId: null, 694 | + principalOrgId: null, 695 | + sourceIp: actualEvent.requestContext.identity.sourceIp, 696 | + user: null, 697 | + userAgent: null, 698 | + userArn: null, 699 | + }, 700 | + messageDirection: 'IN', 701 | + messageId: actualEvent.requestContext.messageId, 702 | + requestId: actualEvent.requestContext.requestId, 703 | + requestTime: actualEvent.requestContext.requestTime, 704 | + requestTimeEpoch: actualEvent.requestContext.requestTimeEpoch, 705 | + routeKey: action, 706 | + stage: actualEvent.requestContext.stage, 707 | + }, 708 | + }; 709 | + 710 | + return expected; 711 | + }; 712 | + 713 | + const createExpectedContext = actualContext => { 714 | + const expected = { 715 | + awsRequestId: actualContext.awsRequestId, 716 | + callbackWaitsForEmptyEventLoop: true, 717 | + functionName: actualContext.functionName, 718 | + functionVersion: '$LATEST', 719 | + invokedFunctionArn: actualContext.invokedFunctionArn, 720 | + invokeid: actualContext.invokeid, 721 | + logGroupName: actualContext.logGroupName, 722 | + logStreamName: actualContext.logStreamName, 723 | + memoryLimitInMB: actualContext.memoryLimitInMB, 724 | + }; 725 | + 726 | + return expected; 727 | + }; 728 | + 729 | + const createExpectedConnectHeaders = actualHeaders => { 730 | + const url = new URL(endpoint); 731 | + const expected = { 732 | + Host: url.hostname, 733 | + 'Sec-WebSocket-Extensions': actualHeaders['Sec-WebSocket-Extensions'], 734 | + 'Sec-WebSocket-Key': actualHeaders['Sec-WebSocket-Key'], 735 | + 'Sec-WebSocket-Version': actualHeaders['Sec-WebSocket-Version'], 736 | + 'X-Amzn-Trace-Id': actualHeaders['X-Amzn-Trace-Id'], 737 | + 'X-Forwarded-For': actualHeaders['X-Forwarded-For'], 738 | + 'X-Forwarded-Port': `${url.port || 443}`, 739 | + 'X-Forwarded-Proto': `${url.protocol.replace('ws', 'http').replace('wss', 'https').replace(':', '')}`, 740 | + }; 741 | + 742 | + return expected; 743 | + }; 744 | + 745 | + const createExpectedDisconnectHeaders = () => { 746 | + const url = new URL(endpoint); 747 | + const expected = { 748 | + Host: url.hostname, 749 | + 'x-api-key': '', 750 | + 'x-restapi': '', 751 | + }; 752 | + 753 | + return expected; 754 | + }; 755 | + 756 | + const createExpectedConnectMultiValueHeaders = actualHeaders => { 757 | + const expected = createExpectedConnectHeaders(actualHeaders); 758 | + Object.keys(expected).forEach(key => { 759 | + expected[key] = [expected[key]]; 760 | + }); 761 | + 762 | + return expected; 763 | + }; 764 | + 765 | + const createExpectedDisconnectMultiValueHeaders = actualHeaders => { 766 | + const expected = createExpectedDisconnectHeaders(actualHeaders); 767 | + Object.keys(expected).forEach(key => { 768 | + expected[key] = [expected[key]]; 769 | + }); 770 | + 771 | + return expected; 772 | + }; 773 | + 774 | + it('should receive correct call info', async () => { 775 | + const ws = await createWebSocket(); 776 | + await ws.send(JSON.stringify({ action:'registerListener' })); 777 | + await ws.receive1(); 778 | + 779 | + // connect 780 | + const c = await createClient(); 781 | + const connect = JSON.parse(await ws.receive1()); 782 | + let now = Date.now(); 783 | + let expectedCallInfo = { id:c.id, event:{ headers:createExpectedConnectHeaders(connect.info.event.headers), multiValueHeaders:createExpectedConnectMultiValueHeaders(connect.info.event.headers), ...createExpectedEvent(c.id, '$connect', 'CONNECT', connect.info.event) }, context:createExpectedContext(connect.info.context) }; 784 | + 785 | + expect(connect).to.deep.equal({ action:'update', event:'connect', info:expectedCallInfo }); 786 | + expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(connect.info.event.requestContext.connectedAt - 10, connect.info.event.requestContext.requestTimeEpoch + 10); 787 | + expect(connect.info.event.requestContext.connectedAt).to.be.within(now - timeout, now); 788 | + expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(now - timeout, now); 789 | + expect(moment.utc(connect.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now - timeout, now); 790 | + 791 | + if (endpoint.startsWith('ws://locahost')) { 792 | + expect(connect.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); 793 | + expect(connect.info.event.headers['X-Forwarded-For']).to.be.equal('127.0.0.1'); 794 | + } 795 | + 796 | + // getCallInfo 797 | + c.ws.send(JSON.stringify({ action:'getCallInfo' })); 798 | + const callInfo = JSON.parse(await c.ws.receive1()); 799 | + now = Date.now(); 800 | + expectedCallInfo = { event:{ body: '{"action":"getCallInfo"}', ...createExpectedEvent(c.id, 'getCallInfo', 'MESSAGE', callInfo.info.event) }, context:createExpectedContext(callInfo.info.context) }; 801 | + 802 | + expect(callInfo).to.deep.equal({ action:'update', event:'call-info', info:expectedCallInfo }); 803 | + expect(callInfo.info.event.requestContext.connectedAt).to.be.lt(callInfo.info.event.requestContext.requestTimeEpoch); 804 | + expect(callInfo.info.event.requestContext.connectedAt).to.be.within(now - timeout, now); 805 | + expect(callInfo.info.event.requestContext.requestTimeEpoch).to.be.within(now - timeout, now); 806 | + expect(moment.utc(callInfo.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now - timeout, now); 807 | + if (endpoint.startsWith('ws://locahost')) expect(callInfo.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); 808 | + 809 | + // disconnect 810 | + c.ws.close(); 811 | + const disconnect = JSON.parse(await ws.receive1()); 812 | + now = Date.now(); 813 | + expectedCallInfo = { id:c.id, event:{ headers:createExpectedDisconnectHeaders(disconnect.info.event.headers), multiValueHeaders:createExpectedDisconnectMultiValueHeaders(disconnect.info.event.headers), ...createExpectedEvent(c.id, '$disconnect', 'DISCONNECT', disconnect.info.event) }, context:createExpectedContext(disconnect.info.context) }; 814 | + 815 | + expect(disconnect).to.deep.equal({ action:'update', event:'disconnect', info:expectedCallInfo }); 816 | + }).timeout(timeout); 817 | + 818 | + it('should be able to parse query string', async () => { 819 | + const now = `${Date.now()}`; 820 | + const ws = await createWebSocket(); 821 | + await ws.send(JSON.stringify({ action:'registerListener' })); 822 | + await ws.receive1(); 823 | + 824 | + await createClient(); 825 | + await createClient(`now=${now}&before=123456789`); 826 | + 827 | + expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.be.undefined; 828 | + expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.deep.equal({ now, before:'123456789' }); 829 | + }).timeout(timeout); 830 | + 831 | + it('should be able to receive messages via REST API', async () => { 832 | + await createClient(); 833 | + const c2 = await createClient(); 834 | + const url = new URL(endpoint); 835 | + const signature = { service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c2.id}`, method: 'POST', body:'Hello World!', headers:{ 'Content-Type':'text/plain'/* 'application/text' */ } }; 836 | + aws4.sign(signature, { accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey }); 837 | + const res = await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers.Authorization).set('Content-Type', signature.headers['Content-Type']) 838 | +.send('Hello World!'); 839 | + 840 | + expect(res).to.have.status(200); 841 | + expect(await c2.ws.receive1()).to.equal('Hello World!'); 842 | + }).timeout(timeout); 843 | + 844 | + it('should receive error code when sending to non existing client via REST API', async () => { 845 | + const c = 'aJz0Md6VoAMCIbQ='; 846 | + const url = new URL(endpoint); 847 | + const signature = { service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c}`, method: 'POST', body:'Hello World!', headers:{ 'Content-Type':'text/plain'/* 'application/text' */ } }; 848 | + aws4.sign(signature, { accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey }); 849 | + const res = await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers.Authorization).set('Content-Type', signature.headers['Content-Type']) 850 | +.send('Hello World!'); 851 | + 852 | + expect(res).to.have.status(410); 853 | + }).timeout(timeout); 854 | + }); 855 | +}); 856 | diff --git a/manual_test_websocket/main/test/support/WebSocketTester.js b/manual_test_websocket/main/test/support/WebSocketTester.js 857 | new file mode 100644 858 | index 0000000..aaeff5a 859 | --- /dev/null 860 | +++ b/manual_test_websocket/main/test/support/WebSocketTester.js 861 | @@ -0,0 +1,62 @@ 862 | +/* eslint-disable import/no-extraneous-dependencies */ 863 | +const WebSocket = require('ws'); 864 | + 865 | +class WebSocketTester { 866 | + constructor() { 867 | + this.messages = []; this.receivers = []; 868 | + } 869 | + 870 | + open(url) { 871 | + if (this.ws != null) return; 872 | + const ws = this.ws = new WebSocket(url); 873 | + ws.on('message', message => { 874 | + // console.log('Received: '+message); 875 | + if (this.receivers.length > 0) this.receivers.shift()(message); 876 | + else this.messages.push(message); 877 | + }); 878 | + 879 | + return new Promise(resolve => { 880 | + ws.on('open', () => { 881 | + resolve(true); 882 | + }); 883 | + }); 884 | + } 885 | + 886 | + send(data) { 887 | + this.ws.send(data); 888 | + } 889 | + 890 | + receive1() { 891 | + return new Promise(resolve => { 892 | + if (this.messages.length > 0) resolve(this.messages.shift()); 893 | + else this.receivers.push(resolve); 894 | + }); 895 | + } 896 | + 897 | + receive(n) { 898 | + return new Promise(resolve => { 899 | + const messages = []; 900 | + for (let i = 0; i < n; i += 1) { 901 | + this.receive1().then(message => { 902 | + messages[i] = message; 903 | + if (i === n - 1) resolve(messages); 904 | + }); 905 | + } 906 | + }); 907 | + } 908 | + 909 | + skip() { 910 | + if (this.messages.length > 0) this.messages.shift(); 911 | + else this.receivers.push(() => {}); 912 | + } 913 | + 914 | + countUnrecived() { 915 | + return this.messages.length; 916 | + } 917 | + 918 | + close() { 919 | + if (this.ws != null) this.ws.close(); 920 | + } 921 | +} 922 | + 923 | +module.exports = WebSocketTester; 924 | diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/handler.js b/manual_test_websocket/manual_test_websocket_RouteSelection/handler.js 925 | deleted file mode 100644 926 | index e8da78f..0000000 927 | --- a/manual_test_websocket/manual_test_websocket_RouteSelection/handler.js 928 | +++ /dev/null 929 | @@ -1,37 +0,0 @@ 930 | -'use strict'; 931 | - 932 | -const AWS = require('aws-sdk'); 933 | - 934 | - 935 | -const successfullResponse = { 936 | - statusCode: 200, 937 | - body: 'Request is OK.' 938 | -}; 939 | - 940 | -const errorResponse = { 941 | - statusCode: 400, 942 | - body: 'Request is not OK.' 943 | -}; 944 | - 945 | -module.exports.echo = async (event, context) => { 946 | - const action = JSON.parse(event.body); 947 | - 948 | - await sendToClient(action.message, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)); 949 | - return successfullResponse; 950 | -}; 951 | - 952 | -const newAWSApiGatewayManagementApi=(event, context)=>{ 953 | - let endpoint=event.apiGatewayUrl; 954 | - 955 | - if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; 956 | - const apiVersion='2018-11-29'; 957 | - return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); 958 | -}; 959 | - 960 | -const sendToClient = (data, connectionId, apigwManagementApi) => { 961 | - // console.log(`sendToClient:${connectionId}`); 962 | - let sendee=data; 963 | - if ('object'==typeof data) sendee=JSON.stringify(data); 964 | - 965 | - return apigwManagementApi.postToConnection({ConnectionId: connectionId, Data: sendee}).promise(); 966 | -}; 967 | diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/test/e2e/ws.e2e.js b/manual_test_websocket/manual_test_websocket_RouteSelection/test/e2e/ws.e2e.js 968 | deleted file mode 100644 969 | index a565936..0000000 970 | --- a/manual_test_websocket/manual_test_websocket_RouteSelection/test/e2e/ws.e2e.js 971 | +++ /dev/null 972 | @@ -1,53 +0,0 @@ 973 | -const chai = require('chai'); 974 | -const expect = chai.expect; 975 | -const endpoint = process.env.npm_config_endpoint||'ws://localhost:3005'; 976 | -const timeout = process.env.npm_config_timeout?parseInt(process.env.npm_config_timeout):1000; 977 | -const WebSocketTester=require('../support/WebSocketTester'); 978 | - 979 | -describe('serverless', ()=>{ 980 | - describe('with WebSocket support', ()=>{ 981 | - let clients=[]; let req=null; let cred=null; 982 | - const createWebSocket=async (qs)=>{ 983 | - const ws=new WebSocketTester(); 984 | - let url=endpoint; 985 | - if (qs) url=`${endpoint}?${qs}`; 986 | - await ws.open(url); 987 | - clients.push(ws); 988 | - return ws; 989 | - }; 990 | - const createClient=async (qs)=>{ 991 | - const ws=await createWebSocket(qs); 992 | - ws.send(JSON.stringify({action:'getClientInfo'})); 993 | - const json=await ws.receive1(); 994 | - const id=JSON.parse(json).info.id; 995 | - return {ws, id}; 996 | - }; 997 | - 998 | - beforeEach(()=>{ 999 | - clients=[]; 1000 | - }); 1001 | - afterEach(async ()=>{ 1002 | - await Promise.all(clients.map(async (ws, i)=>{ 1003 | - const n=ws.countUnrecived(); 1004 | - 1005 | - if (n>0) { 1006 | - console.log(`unreceived:[i=${i}]`); 1007 | - (await ws.receive(n)).forEach(m=>console.log(m)); 1008 | - } 1009 | - expect(n).to.equal(0); 1010 | - ws.close(); 1011 | - })); 1012 | - clients=[]; 1013 | - }); 1014 | - 1015 | - it(`should call action 'echo' handler located at service.do`, async ()=>{ 1016 | - const ws=await createWebSocket(); 1017 | - const now=""+Date.now(); 1018 | - const payload=JSON.stringify({service:{do:'echo'}, message:now}); 1019 | - ws.send(payload); 1020 | - expect(await ws.receive1()).to.equal(`${now}`); 1021 | - }).timeout(timeout); 1022 | - 1023 | - 1024 | - }); 1025 | -}); 1026 | \ No newline at end of file 1027 | diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/test/support/WebSocketTester.js b/manual_test_websocket/manual_test_websocket_RouteSelection/test/support/WebSocketTester.js 1028 | deleted file mode 100644 1029 | index d5e8005..0000000 1030 | --- a/manual_test_websocket/manual_test_websocket_RouteSelection/test/support/WebSocketTester.js 1031 | +++ /dev/null 1032 | @@ -1,62 +0,0 @@ 1033 | -'use strict'; 1034 | - 1035 | -const WebSocket = require('ws'); 1036 | - 1037 | -class WebSocketTester { 1038 | - constructor() { 1039 | - this.messages=[]; this.receivers=[]; 1040 | - } 1041 | - 1042 | - open(url) { 1043 | - if (null!=this.ws) return; 1044 | - const ws=this.ws=new WebSocket(url); 1045 | - ws.on('message', (message)=>{ 1046 | - // console.log('Received: '+message); 1047 | - if (0 { 1051 | - ws.on('open', ()=>{ 1052 | - resolve(true); 1053 | - }); 1054 | - }); 1055 | - } 1056 | - 1057 | - send(data) { 1058 | - this.ws.send(data); 1059 | - } 1060 | - 1061 | - receive1() { 1062 | - return new Promise((resolve/*, reject*/)=>{ 1063 | - if (0{ 1070 | - const messages=[]; 1071 | - for (let i=0; i{ 1073 | - messages[i]=message; 1074 | - if (i===n-1) resolve(messages); 1075 | - }); 1076 | - } 1077 | - }); 1078 | - } 1079 | - 1080 | - skip() { 1081 | - if (0{}); 1083 | - } 1084 | - 1085 | - countUnrecived() { 1086 | - return this.messages.length; 1087 | - } 1088 | - 1089 | - close() { 1090 | - if (null!=this.ws) this.ws.close(); 1091 | - } 1092 | -}; 1093 | - 1094 | -module.exports=WebSocketTester; 1095 | diff --git a/manual_test_websocket/manual_test_websocket_main/handler.js b/manual_test_websocket/manual_test_websocket_main/handler.js 1096 | deleted file mode 100644 1097 | index 7ada0f0..0000000 1098 | --- a/manual_test_websocket/manual_test_websocket_main/handler.js 1099 | +++ /dev/null 1100 | @@ -1,150 +0,0 @@ 1101 | -'use strict'; 1102 | - 1103 | -const AWS = require('aws-sdk'); 1104 | -const ddb = (()=>{ 1105 | - if (process.env.IS_OFFLINE) return new AWS.DynamoDB.DocumentClient({region: "localhost", endpoint: "http://localhost:8000"}); 1106 | - return new AWS.DynamoDB.DocumentClient(); 1107 | -})(); 1108 | - 1109 | - 1110 | -const successfullResponse = { 1111 | - statusCode: 200, 1112 | - body: 'Request is OK.' 1113 | -}; 1114 | - 1115 | -const errorResponse = { 1116 | - statusCode: 400, 1117 | - body: 'Request is not OK.' 1118 | -}; 1119 | - 1120 | -// const generatePolicy = function(principalId, effect, resource) { 1121 | -// const authResponse = {}; 1122 | -// authResponse.principalId = principalId; 1123 | -// if (effect && resource) { 1124 | -// const policyDocument = {}; 1125 | -// policyDocument.Version = '2012-10-17'; 1126 | -// policyDocument.Statement = []; 1127 | -// const statementOne = {}; 1128 | -// statementOne.Action = 'execute-api:Invoke'; 1129 | -// statementOne.Effect = effect; 1130 | -// statementOne.Resource = resource; 1131 | -// policyDocument.Statement[0] = statementOne; 1132 | -// authResponse.policyDocument = policyDocument; 1133 | -// } 1134 | -// return authResponse; 1135 | -// }; 1136 | - 1137 | -// module.exports.http = async (event, context) => { 1138 | -// return successfullResponse; 1139 | -// }; 1140 | - 1141 | -module.exports.connect = async (event, context) => { 1142 | - // console.log('connect:'); 1143 | - const listener=await ddb.get({TableName:'listeners', Key:{name:'default'}}).promise(); 1144 | - 1145 | - if (listener.Item) { 1146 | - const timeout=new Promise((resolve) => setTimeout(resolve,100)); 1147 | - const send=sendToClient( // sendToClient won't return on AWS when client doesn't exits so we set a timeout 1148 | - JSON.stringify({action:'update', event:'connect', info:{id:event.requestContext.connectionId, event:{...event, apiGatewayUrl:`${event.apiGatewayUrl}`}, context}}), 1149 | - listener.Item.id, 1150 | - newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); 1151 | - await Promise.race([send, timeout]); 1152 | - } 1153 | - return successfullResponse; 1154 | -}; 1155 | - 1156 | -// module.export.auth = (event, context, callback) => { 1157 | -// //console.log('auth:'); 1158 | -// const token = event.headers["Authorization"]; 1159 | - 1160 | -// if ('deny'===token) callback(null, generatePolicy('user', 'Deny', event.methodArn)); 1161 | -// else callback(null, generatePolicy('user', 'Allow', event.methodArn));; 1162 | -// }; 1163 | - 1164 | -module.exports.disconnect = async (event, context) => { 1165 | - const listener=await ddb.get({TableName:'listeners', Key:{name:'default'}}).promise(); 1166 | - if (listener.Item) await sendToClient(JSON.stringify({action:'update', event:'disconnect', info:{id:event.requestContext.connectionId, event:{...event, apiGatewayUrl:`${event.apiGatewayUrl}`}, context}}), listener.Item.id, newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); 1167 | - return successfullResponse; 1168 | -}; 1169 | - 1170 | -module.exports.defaultHandler = async (event, context) => { 1171 | - await sendToClient(`Error: No Supported Action in Payload '${event.body}'`, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); 1172 | - return successfullResponse; 1173 | -}; 1174 | - 1175 | -module.exports.getClientInfo = async (event, context) => { 1176 | - // console.log('getClientInfo:'); 1177 | - await sendToClient({action:'update', event:'client-info', info:{id:event.requestContext.connectionId}}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); 1178 | - return successfullResponse; 1179 | -}; 1180 | - 1181 | -module.exports.getCallInfo = async (event, context) => { 1182 | - await sendToClient({action:'update', event:'call-info', info:{event:{...event, apiGatewayUrl:`${event.apiGatewayUrl}`}, context}}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); 1183 | - return successfullResponse; 1184 | -}; 1185 | - 1186 | -module.exports.makeError = async (event, context) => { 1187 | - const obj=null; 1188 | - obj.non.non=1; 1189 | - return successfullResponse; 1190 | -}; 1191 | - 1192 | -module.exports.replyViaCallback = (event, context, callback) => { 1193 | - sendToClient({action:'update', event:'reply-via-callback'}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); 1194 | - callback(); 1195 | -}; 1196 | - 1197 | -module.exports.replyErrorViaCallback = (event, context, callback) => { 1198 | - return callback("error error error"); 1199 | -}; 1200 | - 1201 | -module.exports.multiCall1 = async (event, context) => { 1202 | - await sendToClient({action:'update', event:'made-call-1'}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); 1203 | - return successfullResponse; 1204 | -}; 1205 | - 1206 | -module.exports.multiCall2 = async (event, context) => { 1207 | - await sendToClient({action:'update', event:'made-call-2'}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); 1208 | - return successfullResponse; 1209 | -}; 1210 | - 1211 | - 1212 | -module.exports.send = async (event, context) => { 1213 | - const action = JSON.parse(event.body); 1214 | - const sents=[]; 1215 | - action.clients.forEach((connectionId)=>{ 1216 | - const sent=sendToClient(action.data, connectionId, newAWSApiGatewayManagementApi(event, context)); 1217 | - sents.push(sent); 1218 | - }); 1219 | - const noErr=await Promise.all(sents).then(()=>true).catch(()=>false); 1220 | - if (!noErr) await sendToClient('Error: Could not Send all Messages', event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)); 1221 | - return successfullResponse; 1222 | -}; 1223 | - 1224 | -module.exports.registerListener = async (event, context) => { 1225 | - await ddb.put({TableName:'listeners', Item:{name:'default', id:event.requestContext.connectionId}}).promise(); 1226 | - await sendToClient({action:'update', event:'register-listener', info:{id:event.requestContext.connectionId}}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); 1227 | - return successfullResponse; 1228 | -}; 1229 | - 1230 | -module.exports.deleteListener = async (event, context) => { 1231 | - await ddb.delete({TableName:'listeners', Key:{name:'default'}}).promise(); 1232 | - 1233 | - return successfullResponse; 1234 | -}; 1235 | - 1236 | -const newAWSApiGatewayManagementApi=(event, context)=>{ 1237 | - let endpoint=event.apiGatewayUrl; 1238 | - 1239 | - if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; 1240 | - const apiVersion='2018-11-29'; 1241 | - return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); 1242 | -}; 1243 | - 1244 | -const sendToClient = (data, connectionId, apigwManagementApi) => { 1245 | - // console.log(`sendToClient:${connectionId}`); 1246 | - let sendee=data; 1247 | - if ('object'==typeof data) sendee=JSON.stringify(data); 1248 | - 1249 | - return apigwManagementApi.postToConnection({ConnectionId: connectionId, Data: sendee}).promise(); 1250 | -}; 1251 | diff --git a/manual_test_websocket/manual_test_websocket_main/test/e2e/ws.e2e.js b/manual_test_websocket/manual_test_websocket_main/test/e2e/ws.e2e.js 1252 | deleted file mode 100644 1253 | index d8db0b7..0000000 1254 | --- a/manual_test_websocket/manual_test_websocket_main/test/e2e/ws.e2e.js 1255 | +++ /dev/null 1256 | @@ -1,342 +0,0 @@ 1257 | -const chai = require('chai'); 1258 | -const chaiHttp = require('chai-http'); 1259 | -chai.use(chaiHttp); 1260 | -const expect = chai.expect; 1261 | -const aws4 = require('aws4'); 1262 | -const awscred = require('awscred'); 1263 | -const moment = require('moment'); 1264 | -const endpoint = process.env.npm_config_endpoint||'ws://localhost:3001'; 1265 | -const timeout = process.env.npm_config_timeout?parseInt(process.env.npm_config_timeout):1000; 1266 | -const WebSocketTester=require('../support/WebSocketTester'); 1267 | - 1268 | -describe('serverless', ()=>{ 1269 | - describe('with WebSocket support', ()=>{ 1270 | - let clients=[]; let req=null; let cred=null; 1271 | - const createWebSocket=async (qs)=>{ 1272 | - const ws=new WebSocketTester(); 1273 | - let url=endpoint; 1274 | - if (qs) url=`${endpoint}?${qs}`; 1275 | - await ws.open(url); 1276 | - clients.push(ws); 1277 | - return ws; 1278 | - }; 1279 | - const createClient=async (qs)=>{ 1280 | - const ws=await createWebSocket(qs); 1281 | - ws.send(JSON.stringify({action:'getClientInfo'})); 1282 | - const json=await ws.receive1(); 1283 | - const id=JSON.parse(json).info.id; 1284 | - return {ws, id}; 1285 | - }; 1286 | - before(async ()=>{ 1287 | - req=chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); 1288 | - // req=chai.request('http://localhost:3001/dev').keepOpen(); 1289 | - cred=await new Promise((resolve, reject)=>{ 1290 | - awscred.loadCredentials(function(err, data) { if (err) reject(err); else resolve(data); }); 1291 | - }); 1292 | - }); 1293 | - 1294 | - beforeEach(()=>{ 1295 | - clients=[]; 1296 | - }); 1297 | - afterEach(async ()=>{ 1298 | - await Promise.all(clients.map(async (ws, i)=>{ 1299 | - const n=ws.countUnrecived(); 1300 | - 1301 | - if (n>0) { 1302 | - console.log(`unreceived:[i=${i}]`); 1303 | - (await ws.receive(n)).forEach(m=>console.log(m)); 1304 | - } 1305 | - expect(n).to.equal(0); 1306 | - ws.close(); 1307 | - })); 1308 | - clients=[]; 1309 | - }); 1310 | - 1311 | - it('should request to upgade to WebSocket when receving an HTTP request', async ()=>{ 1312 | - const req=chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); 1313 | - let res=await req.get(`/${Date.now()}`);//.set('Authorization', user.accessToken); 1314 | - expect(res).to.have.status(426); 1315 | - res=await req.get(`/${Date.now()}/${Date.now()}`);//.set('Authorization', user.accessToken); 1316 | - expect(res).to.have.status(426); 1317 | - }).timeout(timeout); 1318 | - 1319 | - it('should open a WebSocket', async ()=>{ 1320 | - const ws=await createWebSocket(); 1321 | - expect(ws).not.to.be.undefined; 1322 | - }).timeout(timeout); 1323 | - 1324 | - it('should receive client connection info', async ()=>{ 1325 | - const ws=await createWebSocket(); 1326 | - ws.send(JSON.stringify({action:'getClientInfo'})); 1327 | - const clientInfo=JSON.parse(await ws.receive1()); 1328 | - expect(clientInfo).to.deep.equal({action:'update', event:'client-info', info:{id:clientInfo.info.id}}); 1329 | - }).timeout(timeout); 1330 | - 1331 | - it('should call default handler when no such action exists', async ()=>{ 1332 | - const ws=await createWebSocket(); 1333 | - const payload=JSON.stringify({action:'action'+Date.now()}); 1334 | - ws.send(payload); 1335 | - expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '${payload}'`); 1336 | - }).timeout(timeout); 1337 | - 1338 | - it('should call default handler when no action provided', async ()=>{ 1339 | - const ws=await createWebSocket(); 1340 | - ws.send(JSON.stringify({hello:'world'})); 1341 | - expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '{"hello":"world"}'`); 1342 | - }).timeout(timeout); 1343 | - 1344 | - it('should send & receive data', async ()=>{ 1345 | - const c1=await createClient(); 1346 | - const c2=await createClient(); 1347 | - const c3=await createClient(); 1348 | - c1.ws.send(JSON.stringify({action:'send', data:'Hello World!', clients:[c1.id, c3.id]})); 1349 | - expect(await c1.ws.receive1()).to.equal('Hello World!'); 1350 | - expect(await c3.ws.receive1()).to.equal('Hello World!'); 1351 | - }).timeout(timeout); 1352 | - 1353 | - it('should respond when having an internal server error', async ()=>{ 1354 | - const conn=await createClient(); 1355 | - conn.ws.send(JSON.stringify({action:'makeError'})); 1356 | - const res=JSON.parse(await conn.ws.receive1()); 1357 | - expect(res).to.deep.equal({message:'Internal server error', connectionId:conn.id, requestId:res.requestId}); 1358 | - }).timeout(timeout); 1359 | - 1360 | - it('should respond via callback', async ()=>{ 1361 | - const ws=await createWebSocket(); 1362 | - ws.send(JSON.stringify({action:'replyViaCallback'})); 1363 | - const res=JSON.parse(await ws.receive1()); 1364 | - expect(res).to.deep.equal({action:'update', event:'reply-via-callback'}); 1365 | - }).timeout(timeout); 1366 | - 1367 | - it('should respond with error when calling callback(error)', async ()=>{ 1368 | - const conn=await createClient(); 1369 | - conn.ws.send(JSON.stringify({action:'replyErrorViaCallback'})); 1370 | - const res=JSON.parse(await conn.ws.receive1()); 1371 | - expect(res).to.deep.equal({message:'Internal server error', connectionId:conn.id, requestId:res.requestId}); 1372 | - }).timeout(timeout); 1373 | - 1374 | - it('should respond with only the last action when there are more than one in the serverless.yml file', async ()=>{ 1375 | - const ws=await createWebSocket(); 1376 | - ws.send(JSON.stringify({action:'makeMultiCalls'})); 1377 | - const res=JSON.parse(await ws.receive1()); 1378 | - expect(res).to.deep.equal({action:'update', event:'made-call-2'}); 1379 | - }).timeout(timeout); 1380 | - 1381 | - it('should not send to non existing client', async ()=>{ 1382 | - const c1=await createClient(); 1383 | - c1.ws.send(JSON.stringify({action:'send', data:'Hello World!', clients:["non-existing-id"]})); 1384 | - expect(await c1.ws.receive1()).to.equal('Error: Could not Send all Messages'); 1385 | - }).timeout(timeout); 1386 | - 1387 | - it('should connect & disconnect', async ()=>{ 1388 | - const ws=await createWebSocket(); 1389 | - await ws.send(JSON.stringify({action:'registerListener'})); 1390 | - await ws.receive1(); 1391 | - 1392 | - const c1=await createClient(); 1393 | - const connect1 = JSON.parse(await ws.receive1()); delete connect1.info.event; delete delete connect1.info.context; 1394 | - expect(connect1).to.deep.equal({action:'update', event:'connect', info:{id:c1.id}}); 1395 | - 1396 | - const c2=await createClient(); 1397 | - const connect2 = JSON.parse(await ws.receive1()); delete connect2.info.event; delete delete connect2.info.context; 1398 | - expect(connect2).to.deep.equal({action:'update', event:'connect', info:{id:c2.id}}); 1399 | - 1400 | - c2.ws.close(); 1401 | - const disconnect2 = JSON.parse(await ws.receive1()); delete disconnect2.info.event; delete delete disconnect2.info.context; 1402 | - expect(disconnect2).to.deep.equal({action:'update', event:'disconnect', info:{id:c2.id}}); 1403 | - 1404 | - const c3=await createClient(); 1405 | - const connect3 = JSON.parse(await ws.receive1()); delete connect3.info.event; delete delete connect3.info.context; 1406 | - expect(connect3).to.deep.equal({action:'update', event:'connect', info:{id:c3.id}}); 1407 | - 1408 | - c1.ws.close(); 1409 | - const disconnect1 = JSON.parse(await ws.receive1()); delete disconnect1.info.event; delete delete disconnect1.info.context; 1410 | - expect(disconnect1).to.deep.equal({action:'update', event:'disconnect', info:{id:c1.id}}); 1411 | - 1412 | - c3.ws.close(); 1413 | - const disconnect3 = JSON.parse(await ws.receive1()); delete disconnect3.info.event; delete delete disconnect3.info.context; 1414 | - expect(disconnect3).to.deep.equal({action:'update', event:'disconnect', info:{id:c3.id}}); 1415 | - }).timeout(timeout); 1416 | - 1417 | - const createExpectedEvent=(connectionId, action, eventType, actualEvent)=>{ 1418 | - const url=new URL(endpoint); 1419 | - const expected={ 1420 | - apiGatewayUrl: `${actualEvent.apiGatewayUrl}`, 1421 | - isBase64Encoded: false, 1422 | - requestContext: { 1423 | - apiId: actualEvent.requestContext.apiId, 1424 | - connectedAt: actualEvent.requestContext.connectedAt, 1425 | - connectionId: `${connectionId}`, 1426 | - domainName: url.hostname, 1427 | - eventType, 1428 | - extendedRequestId: actualEvent.requestContext.extendedRequestId, 1429 | - identity: { 1430 | - accessKey: null, 1431 | - accountId: null, 1432 | - caller: null, 1433 | - cognitoAuthenticationProvider: null, 1434 | - cognitoAuthenticationType: null, 1435 | - cognitoIdentityId: null, 1436 | - cognitoIdentityPoolId: null, 1437 | - principalOrgId: null, 1438 | - sourceIp: actualEvent.requestContext.identity.sourceIp, 1439 | - user: null, 1440 | - userAgent: null, 1441 | - userArn: null, 1442 | - }, 1443 | - messageDirection: 'IN', 1444 | - messageId: actualEvent.requestContext.messageId, 1445 | - requestId: actualEvent.requestContext.requestId, 1446 | - requestTime: actualEvent.requestContext.requestTime, 1447 | - requestTimeEpoch: actualEvent.requestContext.requestTimeEpoch, 1448 | - routeKey: action, 1449 | - stage: actualEvent.requestContext.stage, 1450 | - }, 1451 | - }; 1452 | - 1453 | - return expected; 1454 | - }; 1455 | - 1456 | - const createExpectedContext=(actualContext)=>{ 1457 | - const expected={ 1458 | - awsRequestId: actualContext.awsRequestId, 1459 | - callbackWaitsForEmptyEventLoop: true, 1460 | - functionName: actualContext.functionName, 1461 | - functionVersion: '$LATEST', 1462 | - invokedFunctionArn: actualContext.invokedFunctionArn, 1463 | - invokeid: actualContext.invokeid, 1464 | - logGroupName: actualContext.logGroupName, 1465 | - logStreamName: actualContext.logStreamName, 1466 | - memoryLimitInMB: actualContext.memoryLimitInMB, 1467 | - }; 1468 | - 1469 | - return expected; 1470 | - }; 1471 | - 1472 | - const createExpectedConnectHeaders=(actualHeaders)=>{ 1473 | - const url=new URL(endpoint); 1474 | - const expected={ 1475 | - Host: url.hostname, 1476 | - 'Sec-WebSocket-Extensions': actualHeaders['Sec-WebSocket-Extensions'], 1477 | - 'Sec-WebSocket-Key': actualHeaders['Sec-WebSocket-Key'], 1478 | - 'Sec-WebSocket-Version': actualHeaders['Sec-WebSocket-Version'], 1479 | - 'X-Amzn-Trace-Id': actualHeaders['X-Amzn-Trace-Id'], 1480 | - 'X-Forwarded-For': actualHeaders['X-Forwarded-For'], 1481 | - 'X-Forwarded-Port': `${url.port||443}`, 1482 | - 'X-Forwarded-Proto': `${url.protocol.replace('ws', 'http').replace('wss', 'https').replace(':', '')}` 1483 | - }; 1484 | - 1485 | - return expected; 1486 | - }; 1487 | - 1488 | - const createExpectedDisconnectHeaders=(actualHeaders)=>{ 1489 | - const url=new URL(endpoint); 1490 | - const expected={ 1491 | - Host: url.hostname, 1492 | - 'x-api-key': '', 1493 | - 'x-restapi': '', 1494 | - }; 1495 | - 1496 | - return expected; 1497 | - }; 1498 | - 1499 | - const createExpectedConnectMultiValueHeaders=(actualHeaders)=>{ 1500 | - const expected=createExpectedConnectHeaders(actualHeaders); 1501 | - Object.keys(expected).map((key, index)=>{ 1502 | - expected[key] = [expected[key]]; 1503 | - }); 1504 | - return expected; 1505 | - }; 1506 | - 1507 | - const createExpectedDisconnectMultiValueHeaders=(actualHeaders)=>{ 1508 | - const expected=createExpectedDisconnectHeaders(actualHeaders); 1509 | - Object.keys(expected).map((key, index)=>{ 1510 | - expected[key] = [expected[key]]; 1511 | - }); 1512 | - return expected; 1513 | - }; 1514 | - 1515 | - it('should receive correct call info', async ()=>{ 1516 | - const ws=await createWebSocket(); 1517 | - await ws.send(JSON.stringify({action:'registerListener'})); 1518 | - await ws.receive1(); 1519 | - 1520 | - // connect 1521 | - const c=await createClient(); 1522 | - const connect=JSON.parse(await ws.receive1()); 1523 | - let now=Date.now(); 1524 | - let expectedCallInfo={id:c.id, event:{headers:createExpectedConnectHeaders(connect.info.event.headers), multiValueHeaders:createExpectedConnectMultiValueHeaders(connect.info.event.headers), ...createExpectedEvent(c.id, '$connect', 'CONNECT', connect.info.event)}, context:createExpectedContext(connect.info.context)}; 1525 | - expect(connect).to.deep.equal({action:'update', event:'connect', info:expectedCallInfo}); 1526 | - expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(connect.info.event.requestContext.connectedAt-10, connect.info.event.requestContext.requestTimeEpoch+10); 1527 | - expect(connect.info.event.requestContext.connectedAt).to.be.within(now-timeout, now); 1528 | - expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(now-timeout, now); 1529 | - expect(moment.utc(connect.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now-timeout, now); 1530 | - if (endpoint.startsWith('ws://locahost')) { 1531 | - expect(connect.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); 1532 | - expect(connect.info.event.headers['X-Forwarded-For']).to.be.equal('127.0.0.1'); 1533 | - } 1534 | - 1535 | - // getCallInfo 1536 | - c.ws.send(JSON.stringify({action:'getCallInfo'})); 1537 | - const callInfo=JSON.parse(await c.ws.receive1()); 1538 | - now=Date.now(); 1539 | - expectedCallInfo={event:{body: '{\"action\":\"getCallInfo\"}', ...createExpectedEvent(c.id, 'getCallInfo', 'MESSAGE', callInfo.info.event)}, context:createExpectedContext(callInfo.info.context)}; 1540 | - expect(callInfo).to.deep.equal({action:'update', event:'call-info', info:expectedCallInfo}); 1541 | - expect(callInfo.info.event.requestContext.connectedAt).to.be.lt(callInfo.info.event.requestContext.requestTimeEpoch); 1542 | - expect(callInfo.info.event.requestContext.connectedAt).to.be.within(now-timeout, now); 1543 | - expect(callInfo.info.event.requestContext.requestTimeEpoch).to.be.within(now-timeout, now); 1544 | - expect(moment.utc(callInfo.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now-timeout, now); 1545 | - if (endpoint.startsWith('ws://locahost')) expect(callInfo.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); 1546 | - 1547 | - // disconnect 1548 | - c.ws.close(); 1549 | - const disconnect=JSON.parse(await ws.receive1()); 1550 | - now=Date.now(); 1551 | - expectedCallInfo={id:c.id, event:{headers:createExpectedDisconnectHeaders(disconnect.info.event.headers), multiValueHeaders:createExpectedDisconnectMultiValueHeaders(disconnect.info.event.headers), ...createExpectedEvent(c.id, '$disconnect', 'DISCONNECT', disconnect.info.event)}, context:createExpectedContext(disconnect.info.context)}; 1552 | - expect(disconnect).to.deep.equal({action:'update', event:'disconnect', info:expectedCallInfo}); 1553 | - }).timeout(timeout); 1554 | - 1555 | - it('should be able to parse query string', async ()=>{ 1556 | - const now=''+Date.now(); 1557 | - const ws=await createWebSocket(); 1558 | - await ws.send(JSON.stringify({action:'registerListener'})); 1559 | - await ws.receive1(); 1560 | - 1561 | - const c1=await createClient(); 1562 | - const c2=await createClient(`now=${now}&before=123456789`); 1563 | - expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.be.undefined; 1564 | - expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.deep.equal({now, before:'123456789'}); 1565 | - }).timeout(timeout); 1566 | - 1567 | - it('should be able to receive messages via REST API', async ()=>{ 1568 | - const c1=await createClient(); 1569 | - const c2=await createClient(); 1570 | - const url=new URL(endpoint); 1571 | - const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c2.id}`, method: 'POST', body:'Hello World!', headers:{'Content-Type':'text/plain'/*'application/text'*/}}; 1572 | - aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); 1573 | - const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send('Hello World!'); 1574 | - expect(res).to.have.status(200); 1575 | - expect(await c2.ws.receive1()).to.equal('Hello World!'); 1576 | - }).timeout(timeout); 1577 | - 1578 | - it('should receive error code when sending to non existing client via REST API', async ()=>{ 1579 | - const c='aJz0Md6VoAMCIbQ='; 1580 | - const url=new URL(endpoint); 1581 | - const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c}`, method: 'POST', body:'Hello World!', headers:{'Content-Type':'text/plain'/*'application/text'*/}}; 1582 | - aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); 1583 | - const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send('Hello World!'); 1584 | - expect(res).to.have.status(410); 1585 | - }).timeout(timeout); 1586 | - 1587 | - // UNABLE TO TEST HIS SCENARIO BECAUSE AWS DOESN'T RETURN ANYTHING 1588 | - // it('should not receive anything when POSTing nothing', async ()=>{ 1589 | - // const c1=await createClient(); 1590 | - // const url=new URL(endpoint); 1591 | - // const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c1.id}`, method: 'POST'/*, body:'Hello World!'*/, headers:{'Content-Type':'text/plain'/*'application/text'*/}}; 1592 | - // aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); 1593 | - // const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send(/*'Hello World!'*/); 1594 | - // expect(res).to.have.status(200); 1595 | - // }).timeout(timeout); 1596 | - 1597 | - }); 1598 | -}); 1599 | \ No newline at end of file 1600 | diff --git a/manual_test_websocket/manual_test_websocket_main/test/support/WebSocketTester.js b/manual_test_websocket/manual_test_websocket_main/test/support/WebSocketTester.js 1601 | deleted file mode 100644 1602 | index ee5c8d6..0000000 1603 | --- a/manual_test_websocket/manual_test_websocket_main/test/support/WebSocketTester.js 1604 | +++ /dev/null 1605 | @@ -1,60 +0,0 @@ 1606 | -const WebSocket = require('ws'); 1607 | - 1608 | -class WebSocketTester { 1609 | - constructor() { 1610 | - this.messages=[]; this.receivers=[]; 1611 | - } 1612 | - 1613 | - open(url) { 1614 | - if (null!=this.ws) return; 1615 | - const ws=this.ws=new WebSocket(url); 1616 | - ws.on('message', (message)=>{ 1617 | - // console.log('Received: '+message); 1618 | - if (0 { 1622 | - ws.on('open', ()=>{ 1623 | - resolve(true); 1624 | - }); 1625 | - }); 1626 | - } 1627 | - 1628 | - send(data) { 1629 | - this.ws.send(data); 1630 | - } 1631 | - 1632 | - receive1() { 1633 | - return new Promise((resolve/*, reject*/)=>{ 1634 | - if (0{ 1641 | - const messages=[]; 1642 | - for (let i=0; i{ 1644 | - messages[i]=message; 1645 | - if (i===n-1) resolve(messages); 1646 | - }); 1647 | - } 1648 | - }); 1649 | - } 1650 | - 1651 | - skip() { 1652 | - if (0{}); 1654 | - } 1655 | - 1656 | - countUnrecived() { 1657 | - return this.messages.length; 1658 | - } 1659 | - 1660 | - close() { 1661 | - if (null!=this.ws) this.ws.close(); 1662 | - } 1663 | -}; 1664 | - 1665 | -module.exports=WebSocketTester; 1666 | \ No newline at end of file 1667 | -- 1668 | 2.21.1 (Apple Git-122.3) 1669 | 1670 | -------------------------------------------------------------------------------- /test/tests-data/hyphen.patch: -------------------------------------------------------------------------------- 1 | From 89afcd42fb6f2602fbcd03d6e5573b1859347787 Mon Sep 17 00:00:00 2001 2 | From: "Restyled.io" 3 | Date: Fri, 17 Jan 2025 18:09:56 +0000 4 | Subject: [PATCH 2/2] Restyled by prettier-yaml 5 | 6 | --- 7 | hlint/.hlint.yaml | 155 +++++++++++++++++++++++----------------------- 8 | 1 file changed, 77 insertions(+), 78 deletions(-) 9 | 10 | diff --git a/hlint/.hlint.yaml b/hlint/.hlint.yaml 11 | index 1e09829..19356c5 100644 12 | --- a/hlint/.hlint.yaml 13 | +++ b/hlint/.hlint.yaml 14 | @@ -24,34 +24,33 @@ 15 | # for ad hoc ways to suppress hlint. 16 | 17 | --- 18 | - 19 | # By default, everything is an error 20 | -- error: {name: ""} 21 | +- error: { name: "" } 22 | -------------------------------------------------------------------------------- /test/tests-data/many-files.patch: -------------------------------------------------------------------------------- 1 | From a7696becf41fa2b5c9c93770e25a5cce6174d3b8 Mon Sep 17 00:00:00 2001 2 | From: Daniel Nalborczyk 3 | Date: Sat, 11 Jan 2020 08:19:48 -0500 4 | Subject: [PATCH] Fix path/resource/resourcePath in Lambda events, fixes #868 5 | 6 | --- 7 | src/events/http/HttpServer.js | 2 ++ 8 | .../http/lambda-events/LambdaIntegrationEvent.js | 5 ++++- 9 | .../lambda-events/LambdaProxyIntegrationEvent.js | 13 +++++++------ 10 | src/events/http/lambda-events/VelocityContext.js | 6 ++++-- 11 | 4 files changed, 17 insertions(+), 9 deletions(-) 12 | 13 | diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js 14 | index c0fbe5a..ea7135b 100644 15 | --- a/src/events/http/HttpServer.js 16 | +++ b/src/events/http/HttpServer.js 17 | @@ -473,6 +473,7 @@ export default class HttpServer { 18 | request, 19 | this.#serverless.service.provider.stage, 20 | requestTemplate, 21 | + _path, 22 | ).create() 23 | } catch (err) { 24 | return this._reply500( 25 | @@ -488,6 +489,7 @@ export default class HttpServer { 26 | const lambdaProxyIntegrationEvent = new LambdaProxyIntegrationEvent( 27 | request, 28 | this.#serverless.service.provider.stage, 29 | + _path, 30 | ) 31 | 32 | event = lambdaProxyIntegrationEvent.create() 33 | diff --git a/src/events/http/lambda-events/LambdaIntegrationEvent.js b/src/events/http/lambda-events/LambdaIntegrationEvent.js 34 | index 4a9a0a4..c4a72d0 100644 35 | --- a/src/events/http/lambda-events/LambdaIntegrationEvent.js 36 | +++ b/src/events/http/lambda-events/LambdaIntegrationEvent.js 37 | @@ -2,11 +2,13 @@ import renderVelocityTemplateObject from './renderVelocityTemplateObject.js' 38 | import VelocityContext from './VelocityContext.js' 39 | 40 | export default class LambdaIntegrationEvent { 41 | + #path = null 42 | #request = null 43 | #requestTemplate = null 44 | #stage = null 45 | 46 | - constructor(request, stage, requestTemplate) { 47 | + constructor(request, stage, requestTemplate, path) { 48 | + this.#path = path 49 | this.#request = request 50 | this.#requestTemplate = requestTemplate 51 | this.#stage = stage 52 | @@ -17,6 +19,7 @@ export default class LambdaIntegrationEvent { 53 | this.#request, 54 | this.#stage, 55 | this.#request.payload || {}, 56 | + this.#path, 57 | ).getContext() 58 | 59 | const event = renderVelocityTemplateObject( 60 | diff --git a/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js b/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js 61 | index 12e388b..78ea853 100644 62 | --- a/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js 63 | +++ b/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js 64 | @@ -16,10 +16,12 @@ const { parse } = JSON 65 | // https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html 66 | // http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-lambda.html 67 | export default class LambdaProxyIntegrationEvent { 68 | + #path = null 69 | #request = null 70 | #stage = null 71 | 72 | - constructor(request, stage) { 73 | + constructor(request, stage, path) { 74 | + this.#path = path 75 | this.#request = request 76 | this.#stage = stage 77 | } 78 | @@ -106,7 +108,6 @@ export default class LambdaProxyIntegrationEvent { 79 | const { 80 | info: { received, remoteAddress }, 81 | method, 82 | - path, 83 | } = this.#request 84 | 85 | const httpMethod = method.toUpperCase() 86 | @@ -125,7 +126,7 @@ export default class LambdaProxyIntegrationEvent { 87 | multiValueQueryStringParameters: parseMultiValueQueryStringParameters( 88 | url, 89 | ), 90 | - path, 91 | + path: this.#path, 92 | pathParameters: nullIfEmpty(pathParams), 93 | queryStringParameters: parseQueryStringParameters(url), 94 | requestContext: { 95 | @@ -170,16 +171,16 @@ export default class LambdaProxyIntegrationEvent { 96 | userAgent: this.#request.headers['user-agent'] || '', 97 | userArn: 'offlineContext_userArn', 98 | }, 99 | - path: `/${this.#stage}${this.#request.route.path}`, 100 | + path: this.#request.route.path, 101 | protocol: 'HTTP/1.1', 102 | requestId: createUniqueId(), 103 | requestTime, 104 | requestTimeEpoch, 105 | resourceId: 'offlineContext_resourceId', 106 | - resourcePath: this.#request.route.path, 107 | + resourcePath: this.#path, 108 | stage: this.#stage, 109 | }, 110 | - resource: this.#request.route.path, 111 | + resource: this.#path, 112 | stageVariables: null, 113 | } 114 | } 115 | diff --git a/src/events/http/lambda-events/VelocityContext.js b/src/events/http/lambda-events/VelocityContext.js 116 | index 613c83b..7a490f4 100644 117 | --- a/src/events/http/lambda-events/VelocityContext.js 118 | +++ b/src/events/http/lambda-events/VelocityContext.js 119 | @@ -36,11 +36,13 @@ function escapeJavaScript(x) { 120 | http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html 121 | */ 122 | export default class VelocityContext { 123 | + #path = null 124 | #payload = null 125 | #request = null 126 | #stage = null 127 | 128 | - constructor(request, stage, payload) { 129 | + constructor(request, stage, payload, path) { 130 | + this.#path = path 131 | this.#payload = payload 132 | this.#request = request 133 | this.#stage = stage 134 | @@ -106,7 +108,7 @@ export default class VelocityContext { 135 | }, 136 | requestId: createUniqueId(), 137 | resourceId: 'offlineContext_resourceId', 138 | - resourcePath: this.#request.route.path, 139 | + resourcePath: this.#path, 140 | stage: this.#stage, 141 | }, 142 | input: { 143 | -- 144 | 2.21.1 (Apple Git-122.3) 145 | 146 | -------------------------------------------------------------------------------- /test/tests-data/one-file-author-line-break.patch: -------------------------------------------------------------------------------- 1 | From 0f6f88c98fff3afa0289f46bf4eab469f45eebc6 Mon Sep 17 00:00:00 2001 2 | From: Really long name spanning lots of characters 3 | 4 | Date: Sat, 25 Jan 2020 19:21:35 +0200 5 | Subject: [PATCH] JSON stringify string responses 6 | 7 | --- 8 | src/events/http/HttpServer.js | 4 +++- 9 | 1 file changed, 3 insertions(+), 1 deletion(-) 10 | 11 | diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js 12 | index 20bf454..c0fdafb 100644 13 | --- a/src/events/http/HttpServer.js 14 | +++ b/src/events/http/HttpServer.js 15 | @@ -770,7 +770,9 @@ export default class HttpServer { 16 | override: false, 17 | }) 18 | 19 | - if (result && typeof result.body !== 'undefined') { 20 | + if (typeof result === 'string') { 21 | + response.source = JSON.stringify(result) 22 | + } else if (result && typeof result.body !== 'undefined') { 23 | if (result.isBase64Encoded) { 24 | response.encoding = 'binary' 25 | response.source = Buffer.from(result.body, 'base64') 26 | -- 27 | 2.21.1 (Apple Git-122.3) 28 | -------------------------------------------------------------------------------- /test/tests-data/one-file-diff.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js 2 | index 20bf454..c0fdafb 100644 3 | --- a/src/events/http/HttpServer.js 4 | +++ b/src/events/http/HttpServer.js 5 | @@ -770,7 +770,9 @@ export default class HttpServer { 6 | override: false, 7 | }) 8 | 9 | - if (result && typeof result.body !== 'undefined') { 10 | + if (typeof result === 'string') { 11 | + response.source = JSON.stringify(result) 12 | + } else if (result && typeof result.body !== 'undefined') { 13 | if (result.isBase64Encoded) { 14 | response.encoding = 'binary' 15 | response.source = Buffer.from(result.body, 'base64') 16 | -- 17 | 2.21.1 (Apple Git-122.3) 18 | 19 | -------------------------------------------------------------------------------- /test/tests-data/one-file.patch: -------------------------------------------------------------------------------- 1 | From 0f6f88c98fff3afa0289f46bf4eab469f45eebc6 Mon Sep 17 00:00:00 2001 2 | From: Arnas Gecas <13507001+arnas@users.noreply.github.com> 3 | Date: Sat, 25 Jan 2020 19:21:35 +0200 4 | Subject: [PATCH] JSON stringify string responses 5 | 6 | --- 7 | src/events/http/HttpServer.js | 4 +++- 8 | 1 file changed, 3 insertions(+), 1 deletion(-) 9 | 10 | diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js 11 | index 20bf454..c0fdafb 100644 12 | --- a/src/events/http/HttpServer.js 13 | +++ b/src/events/http/HttpServer.js 14 | @@ -770,7 +770,9 @@ export default class HttpServer { 15 | override: false, 16 | }) 17 | 18 | - if (result && typeof result.body !== 'undefined') { 19 | + if (typeof result === 'string') { 20 | + response.source = JSON.stringify(result) 21 | + } else if (result && typeof result.body !== 'undefined') { 22 | if (result.isBase64Encoded) { 23 | response.encoding = 'binary' 24 | response.source = Buffer.from(result.body, 'base64') 25 | -- 26 | 2.21.1 (Apple Git-122.3) 27 | 28 | -------------------------------------------------------------------------------- /test/tests-data/rename-file.patch: -------------------------------------------------------------------------------- 1 | From 68ec4bbde5244929afee1b39e09dced6fad1a725 Mon Sep 17 00:00:00 2001 2 | From: =?UTF-8?q?David=20H=C3=A9rault?= 3 | Date: Mon, 27 Jan 2020 17:35:01 +0100 4 | Subject: [PATCH] Rename README 5 | 6 | --- 7 | README.md => README.mdx | 0 8 | 1 file changed, 0 insertions(+), 0 deletions(-) 9 | rename README.md => README.mdx (100%) 10 | 11 | diff --git a/README.md b/README.mdx 12 | similarity index 100% 13 | rename from README.md 14 | rename to README.mdx 15 | -- 16 | 2.21.1 (Apple Git-122.3) 17 | 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2018", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["ES2018"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./src", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | "rootDirs": ["./src", "./test"], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | --------------------------------------------------------------------------------