From ade315ee0120f8857c84ec795edfc61196bd4262 Mon Sep 17 00:00:00 2001 From: Maria <79389256+marythedev@users.noreply.github.com> Date: Tue, 12 Dec 2023 15:19:28 -0500 Subject: [PATCH] added fragment update option, implemented the usage of Fragment class and added tests --- package-lock.json | 531 ++++++++++++++++++++- package.json | 1 + src/app.js | 1 + src/model/data/memory/memory-db.js | 1 - src/model/fragment.js | 113 ++--- src/response.js | 2 - src/routes/api/byId.js | 207 +++++---- src/routes/api/delete.js | 7 +- src/routes/api/get.js | 15 +- src/routes/api/index.js | 9 +- src/routes/api/info.js | 24 +- src/routes/api/post.js | 28 +- src/routes/api/update.js | 43 ++ tests/data/image.gif | Bin 0 -> 18008 bytes tests/data/image.jpg | Bin 0 -> 4650 bytes tests/data/image.png | Bin 0 -> 37341 bytes tests/data/image.webp | Bin 0 -> 8896 bytes tests/integration/lab-10-dynamodb.hurl | 8 +- tests/integration/post-fragments.hurl | 122 ++++- tests/integration/update-delete.hurl | 612 +++++++++++++++++++++++++ tests/unit/byId.test.js | 142 ++++-- tests/unit/delete.test.js | 80 ++++ tests/unit/fragment.test.js | 5 - tests/unit/get.test.js | 4 +- tests/unit/info.test.js | 13 +- tests/unit/post.test.js | 42 +- tests/unit/update.test.js | 155 +++++++ 27 files changed, 1863 insertions(+), 302 deletions(-) create mode 100644 src/routes/api/update.js create mode 100644 tests/data/image.gif create mode 100644 tests/data/image.jpg create mode 100644 tests/data/image.png create mode 100644 tests/data/image.webp create mode 100644 tests/integration/update-delete.hurl create mode 100644 tests/unit/delete.test.js create mode 100644 tests/unit/update.test.js diff --git a/package-lock.json b/package-lock.json index 717d109..1f555d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "pino": "^8.15.1", "pino-http": "^8.5.0", "pino-pretty": "^10.2.0", + "sharp": "^0.33.0", "stoppable": "^1.1.0" }, "devDependencies": { @@ -2019,6 +2020,15 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@emnapi/runtime": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.44.0.tgz", + "integrity": "sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2108,6 +2118,437 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.0.tgz", + "integrity": "sha512-070tEheekI1LJWTGPC9WlQEa5UoKTXzzlORBHMX4TbfUxMiL336YHR8vBEUNsjse0RJCX8dZ4ZXwT595aEF1ug==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.0.tgz", + "integrity": "sha512-pu/nvn152F3qbPeUkr+4e9zVvEhD3jhwzF473veQfMPkOYo9aoWXSfdZH/E6F+nYC3qvFjbxbvdDbUtEbghLqw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.0.tgz", + "integrity": "sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=11", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.0.tgz", + "integrity": "sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=10.13", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.0.tgz", + "integrity": "sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.0.tgz", + "integrity": "sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.0.tgz", + "integrity": "sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.0.tgz", + "integrity": "sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.0.tgz", + "integrity": "sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.0.tgz", + "integrity": "sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.0.tgz", + "integrity": "sha512-4horD3wMFd5a0ddbDY8/dXU9CaOgHjEHALAddXgafoR5oWq5s8X61PDgsSeh4Qupsdo6ycfPPSSNBrfVQnwwrg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.0.tgz", + "integrity": "sha512-dcomVSrtgF70SyOr8RCOCQ8XGVThXwe71A1d8MGA+mXEVRJ/J6/TrCbBEJh9ddcEIIsrnrkolaEvYSHqVhswQw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.0.tgz", + "integrity": "sha512-TiVJbx38J2rNVfA309ffSOB+3/7wOsZYQEOlKqOUdWD/nqkjNGrX+YQGz7nzcf5oy2lC+d37+w183iNXRZNngQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.0.tgz", + "integrity": "sha512-PaZM4Zi7/Ek71WgTdvR+KzTZpBqrQOFcPe7/8ZoPRlTYYRe43k6TWsf4GVH6XKRLMYeSp8J89RfAhBrSP4itNA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.0.tgz", + "integrity": "sha512-1QLbbN0zt+32eVrg7bb1lwtvEaZwlhEsY1OrijroMkwAqlHqFj6R33Y47s2XUv7P6Ie1PwCxK/uFnNqMnkd5kg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.0.tgz", + "integrity": "sha512-CecqgB/CnkvCWFhmfN9ZhPGMLXaEBXl4o7WtA6U3Ztrlh/s7FUKX4vNxpMSYLIrWuuzjiaYdfU3+Tdqh1xaHfw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.0.tgz", + "integrity": "sha512-Hn4js32gUX9qkISlemZBUPuMs0k/xNJebUNl/L6djnU07B/HAA2KaxRVb3HvbU5fL242hLOcp0+tR+M8dvJUFw==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^0.44.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.0.tgz", + "integrity": "sha512-5HfcsCZi3l5nPRF2q3bllMVMDXBqEWI3Q8KQONfzl0TferFE5lnsIG0A1YrntMAGqvkzdW6y1Ci1A2uTvxhfzg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.0.tgz", + "integrity": "sha512-i3DtP/2ce1yKFj4OzOnOYltOEL/+dp4dc4dJXJBv6god1AFTcmkaA99H/7SwOmkCOBQkbVvA3lCGm3/5nDtf9Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -4219,11 +4660,22 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -4234,8 +4686,16 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } }, "node_modules/colorette": { "version": "2.0.20", @@ -4526,6 +4986,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -6602,7 +7070,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -7773,7 +8240,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -7844,6 +8310,45 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/sharp": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.0.tgz", + "integrity": "sha512-99DZKudjm/Rmz+M0/26t4DKpXyywAOJaayGS9boEn7FvgtG0RYBi46uPE2c+obcJRtA3AZa0QwJot63gJQ1F0Q==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "semver": "^7.5.4" + }, + "engines": { + "libvips": ">=8.15.0", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.0", + "@img/sharp-darwin-x64": "0.33.0", + "@img/sharp-libvips-darwin-arm64": "1.0.0", + "@img/sharp-libvips-darwin-x64": "1.0.0", + "@img/sharp-libvips-linux-arm": "1.0.0", + "@img/sharp-libvips-linux-arm64": "1.0.0", + "@img/sharp-libvips-linux-s390x": "1.0.0", + "@img/sharp-libvips-linux-x64": "1.0.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.0", + "@img/sharp-libvips-linuxmusl-x64": "1.0.0", + "@img/sharp-linux-arm": "0.33.0", + "@img/sharp-linux-arm64": "0.33.0", + "@img/sharp-linux-s390x": "0.33.0", + "@img/sharp-linux-x64": "0.33.0", + "@img/sharp-linuxmusl-arm64": "0.33.0", + "@img/sharp-linuxmusl-x64": "0.33.0", + "@img/sharp-wasm32": "0.33.0", + "@img/sharp-win32-ia32": "0.33.0", + "@img/sharp-win32-x64": "0.33.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7884,6 +8389,19 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -8522,8 +9040,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { "version": "17.7.2", diff --git a/package.json b/package.json index 421063a..793000f 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "pino": "^8.15.1", "pino-http": "^8.5.0", "pino-pretty": "^10.2.0", + "sharp": "^0.33.0", "stoppable": "^1.1.0" } } diff --git a/src/app.js b/src/app.js index 72c9a2a..966bd7f 100644 --- a/src/app.js +++ b/src/app.js @@ -24,6 +24,7 @@ app.use('/', require('./routes')); //Resources Not Found - 404 middleware app.use((req, res) => { + logger.warn("Page not found"); res.status(404).json(createErrorResponse(404, 'Not Found')); }); diff --git a/src/model/data/memory/memory-db.js b/src/model/data/memory/memory-db.js index 5056288..cd03d38 100644 --- a/src/model/data/memory/memory-db.js +++ b/src/model/data/memory/memory-db.js @@ -1,5 +1,4 @@ const logger = require('../../../logger'); - const validateKey = (key) => typeof key === 'string'; class MemoryDB { diff --git a/src/model/fragment.js b/src/model/fragment.js index 74c4b67..6c86a04 100644 --- a/src/model/fragment.js +++ b/src/model/fragment.js @@ -21,53 +21,47 @@ const formats = [ 'text/markdown', 'text/html', 'application/json', - - /* Formats are not supported yet: 'image/png', 'image/jpeg', 'image/webp', 'image/gif' - - */ ]; class Fragment { - constructor({ id = randomUUID(), ownerId, created = new Date(), updated = new Date(), type, size = 0 }) { - if (ownerId == undefined || type == undefined) { - logger.error("Cannot create fragment without ownerId and/or type"); - logger.debug(`given ownerId: ${ownerId}, type: ${type}`); - throw new Error('ownerId and type are required'); + constructor({ id = randomUUID(), ownerId, created = new Date().toISOString(), updated = new Date().toISOString(), type, size = 0 }) { + // check if fragment is even valid + if (ownerId == undefined) { + logger.error("Cannot create fragment without ownerId"); + logger.debug(`given ownerId: ${ownerId}`); + throw new Error('ownerId is required'); } - else { - this.id = id; - this.ownerId = ownerId; - this.created = new Date(created); - this.updated = new Date(updated); - - if (Fragment.isSupportedType(type) == true) - this.type = type; - else { - logger.error("Cannot create fragment of unsupported type"); - logger.debug(`given type: ${type}`); - throw new Error('invalid type'); - } - - if (typeof size == 'number') { - if (size >= 0) - this.size = size; - else { - logger.error("Cannot create fragment of negative size"); - logger.debug(`given size: ${size}`); - throw new Error('size cannot be negative'); - } - } - - else { - logger.error("Cannot create fragment of non-number size"); - logger.debug(`given size: ${size}`); - throw new Error('size must be a number'); - } + if (type == undefined) { + logger.error("Cannot create fragment without type"); + logger.debug(`given type: ${type}`); + throw new Error('type is required'); + } else if (Fragment.isSupportedType(type) != true) { + logger.error("Cannot create fragment of unsupported type"); + logger.debug(`given type: ${type}`); + throw new Error('invalid type'); } + if (typeof size != 'number') { + logger.error("Cannot create fragment of non-number size"); + logger.debug(`given size: ${size}`); + throw new Error('size must be a number'); + } else if (size < 0) { + logger.error("Cannot create fragment of negative size"); + logger.debug(`given size: ${size}`); + throw new Error('size cannot be negative'); + } + + + //otherwise the fragment is valid and can be created + this.id = id; + this.ownerId = ownerId; + this.created = created; + this.updated = updated; + this.type = type; + this.size = size; } /** @@ -93,8 +87,10 @@ class Fragment { logger.debug(`given ownerId: ${ownerId}, id: ${id}`); throw new Error('fragment not found'); } - else - return readFragment(ownerId, id); + else { + logger.debug("Fragment was found for the given ownerId and id" + id) + return new Fragment(result); + } } /** @@ -112,7 +108,7 @@ class Fragment { * @returns Promise */ save() { - this.updated = new Date(); + this.updated = new Date().toISOString(); return writeFragment(this); } @@ -133,8 +129,8 @@ class Fragment { if (data == undefined) this.size = 0; else { - this.updated = new Date(); - this.size = data.length; + this.updated = new Date().toISOString(); + this.size = data.byteLength; } } @@ -149,21 +145,9 @@ class Fragment { logger.debug(`given data: ${data}`); throw new Error('data is required'); } - else { - this.updated = new Date(); - this.size = data.length; - return writeFragmentData(this.ownerId, this.id, data); - } - } - - /** - * Updates the fragment's dates to the string format - * @param none - * @returns nothing - */ - async convertDatestoDateString() { - this.created = this.created.toDateString(); - this.updated = this.updated.toDateString(); + this.updated = new Date().toISOString(); + this.size = data.byteLength; + return writeFragmentData(this.ownerId, this.id, data); } /** @@ -199,18 +183,9 @@ class Fragment { formats = ['text/html', 'text/plain']; else if (this.mimeType == 'application/json') formats = ['application/json', 'text/plain']; - - /* Formats are not supported yet: - else if (this.mimeType == 'image/png') - formats = ['image/png', 'image/jpeg', 'image/webp', 'image/gif']; - else if (this.mimeType == 'image/jpeg') - formats = ['image/png', 'image/jpeg', 'image/webp', 'image/gif']; - else if (this.mimeType == 'image/webp') - formats = ['image/png', 'image/jpeg', 'image/webp', 'image/gif']; - else if (this.mimeType == 'image/gif') + else if (this.mimeType == 'image/png' || this.mimeType == 'image/jpeg' + || this.mimeType == 'image/webp' || this.mimeType == 'image/gif') formats = ['image/png', 'image/jpeg', 'image/webp', 'image/gif']; - - */ return formats; } diff --git a/src/response.js b/src/response.js index 8d37a09..1a04b73 100644 --- a/src/response.js +++ b/src/response.js @@ -8,8 +8,6 @@ module.exports.createSuccessResponse = function (data) { return { status: 'ok', - // Using spread operator to clone `data` into our object, see: - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#spread_in_object_literals ...data, }; }; diff --git a/src/routes/api/byId.js b/src/routes/api/byId.js index 3781dc0..0258544 100644 --- a/src/routes/api/byId.js +++ b/src/routes/api/byId.js @@ -1,8 +1,9 @@ -const { readFragment, readFragmentData } = require('../../model/data'); +const { Fragment } = require('../../model/fragment'); const { createErrorResponse } = require('../../response'); const logger = require('../../logger'); var MarkdownIt = require('markdown-it'); const md = new MarkdownIt(); +const sharp = require('sharp'); // Gets a fragment based on its id module.exports = async (req, res) => { @@ -11,129 +12,125 @@ module.exports = async (req, res) => { id = req.params.id.split('.')[0]; var ext = req.params.id.split('.')[1]; } - - const fragmentMetadata = await readFragment(req.user, id); - - if (fragmentMetadata) { - let fragmentData = await readFragmentData(req.user, id); - res.setHeader('content-type', fragmentMetadata.type); - res.setHeader('Content-Length', fragmentMetadata.size); - - if (fragmentMetadata.type.startsWith('text/plain') - || fragmentMetadata.type.startsWith('text/markdown') - || fragmentMetadata.type.startsWith('text/html')) { - - if (ext) { - //fragmentData = Buffer.from(fragmentData); - //fragmentData = fragmentData.toString('utf8'); - fragmentData = convert(res, fragmentMetadata, fragmentData, ext); + try { + const fragment = await Fragment.byId(req.user, id); + let fragmentData = await fragment.getData(); + + if (ext) { + //determine extention type + let convertTo = undefined; + if (ext == 'txt') + convertTo = 'text/plain'; + else if (ext == 'md') + convertTo = 'text/markdown'; + else if (ext == 'html') + convertTo = 'text/html'; + else if (ext == 'json') + convertTo = 'application/json'; + else if (ext == 'png') + convertTo = 'image/png'; + else if (ext == 'jpg') + convertTo = 'image/jpeg'; + else if (ext == 'webp') + convertTo = 'image/webp'; + else if (ext == 'gif') + convertTo = 'image/gif'; + + if (fragment.formats.includes(convertTo)) { + const convertedData = await convert(res, fragment, fragmentData, convertTo); + + res.setHeader('Content-Type', convertTo); + res.setHeader('Content-Length', convertedData.length); + logger.info(`Retrieved fragment and converted with id ${id} to type ${convertTo} `); + res.status(200).send(convertedData); + } else { + logger.warn("Fragment could not be parsed by the raw body parser. Invalid Content-Type."); + res.status(415).json(createErrorResponse(415, "Content-Type is not supported")); } - - res.status(200).send(fragmentData); } - - else if (fragmentMetadata.type.startsWith('application/json')) { - fragmentData = Buffer.from(fragmentData); - fragmentData = fragmentData.toString(); - - if (ext) - fragmentData = convert(res, fragmentMetadata, fragmentData, ext); - - res.status(200).json(fragmentData); + else { + res.setHeader('Content-Type', fragment.type); + res.setHeader('Content-Length', fragment.size); + logger.info(`Retrieved fragment with id ${id} `); + res.status(200).send(fragmentData); + return; } - - } else { + } + catch (err) { logger.warn(`Requested fragment does not exist in the memory.`); logger.debug(`Fragment not found with ID ${id}`); - + logger.debug(err); res.status(404).json(createErrorResponse(404, `Fragment not found: ${id}`)); + return; } }; //convert fragment data to selected type if possible -const convert = (res, metadata, data, ext) => { - - //determine extention type - let convertTo; - if (ext == 'txt') - convertTo = 'text/plain'; - else if (ext == 'md') - convertTo = 'text/markdown'; - else if (ext == 'html') - convertTo = 'text/html'; - else if (ext == 'json') - convertTo = 'application/json'; - else if (ext == 'png') - convertTo = 'image/png'; - else if (ext == 'jpg') - convertTo = 'image/jpg'; - else if (ext == 'jpeg') - convertTo = 'image/jpeg'; - else if (ext == 'webp') - convertTo = 'image/webp'; - else if (ext == 'gif') - convertTo = 'image/gif'; - +const convert = async (res, metadata, data, convertTo) => { - //determine possible conversion - let possibleConversions; - if (metadata.type.startsWith('text/plain')) - possibleConversions = ['text/plain']; - else if (metadata.type.startsWith('text/markdown')) - possibleConversions = ['text/markdown', 'text/html', 'text/plain']; - else if (metadata.type.startsWith('text/html')) - possibleConversions = ['text/html', 'text/plain']; - else if (metadata.type.startsWith('application/json')) - possibleConversions = ['application/json', 'text/plain']; + //determine if conversion possible and make a conversion + if (metadata.type == "text/markdown") { - /* Formats are not supported yet: - else if (metadata.type .startsWith('image/png')) - possibleConversions = ['image/png', 'image/jpeg', 'image/webp', 'image/gif']; - else if (metadata.type.startsWith('image/jpeg')) - possibleConversions = ['image/png', 'image/jpeg', 'image/webp', 'image/gif']; - else if (metadata.type.startsWith('image/webp')) - possibleConversions = ['image/png', 'image/jpeg', 'image/webp', 'image/gif']; - else if (metadata.type.startsWith('image/gif')) - possibleConversions = ['image/png', 'image/jpeg', 'image/webp', 'image/gif']; - */ - - //determine if conversion is possible - let conversionIsPossible = false; - possibleConversions.map((conversion) => { - if (conversion == convertTo) - conversionIsPossible = true; - }); + if (convertTo == "text/html") { + logger.debug(data) + data = data.toString('utf8'); + data = md.render(data); + } - //convert if possible - if (conversionIsPossible) { + else if (convertTo == "text/plain") { + data = data.toString('utf8'); + data = md.render(data); + data = data.replace(/<[^>]*>/g, ''); + } + } - if (metadata.type == "text/markdown") { + else if (metadata.type == "text/html") { + if (convertTo == "text/plain") { + data = data.toString('utf8'); + data = data.replace(/<[^>]*>/g, ''); + } + } - if (convertTo == "text/html") { - data = data.toString('utf8'); - data = md.render(data); - } + else if (metadata.type == "application/json") { + if (convertTo == "text/plain") + data = JSON.stringify(data); + } - else if (convertTo == "text/plain") { - data = data.toString('utf8'); - data = md.render(data); - data = data.replace(/<[^>]*>/g, ''); - } - } + else if (metadata.type == "image/png") { + if (convertTo == "image/jpeg") + data = await sharp(data).jpeg().toBuffer(); + else if (convertTo == "image/webp") + data = await sharp(data).webp().toBuffer(); + else if (convertTo == "image/gif") + data = await sharp(data).gif().toBuffer(); + } - else if (metadata.type == "text/html") { - if (convertTo == "text/plain") { - data = data.toString('utf8'); - data = data.replace(/<[^>]*>/g, ''); - } - } + else if (metadata.type == "image/jpeg") { + if (convertTo == "image/png") + data = await sharp(data).png().toBuffer(); + else if (convertTo == "image/webp") + data = await sharp(data).webp().toBuffer(); + else if (convertTo == "image/gif") + data = await sharp(data).gif().toBuffer(); + } - return data; - } else { - logger.warn(`Requested conversion is not possible.`); - logger.debug(`Fragment of type ${metadata.type} cannot be converted to ${convertTo}.`); + else if (metadata.type == "image/webp") { + if (convertTo == "image/png") + data = await sharp(data).png().toBuffer(); + else if (convertTo == "image/jpeg") + data = await sharp(data).jpeg().toBuffer(); + else if (convertTo == "image/gif") + data = await sharp(data).gif().toBuffer(); + } - res.status(415).json(createErrorResponse(415, `Fragment cannot be converted to selected type.`)); + else if (metadata.type == "image/gif") { + if (convertTo == "image/png") + data = await sharp(data).png().toBuffer(); + else if (convertTo == "image/jpeg") + data = await sharp(data).jpeg().toBuffer(); + else if (convertTo == "image/webp") + data = await sharp(data).webp().toBuffer(); } + return data; } \ No newline at end of file diff --git a/src/routes/api/delete.js b/src/routes/api/delete.js index cb09a58..62a6f84 100644 --- a/src/routes/api/delete.js +++ b/src/routes/api/delete.js @@ -1,5 +1,5 @@ const { createSuccessResponse, createErrorResponse } = require('../../response'); -const { deleteFragment } = require('../../model/data'); +const { Fragment } = require('../../model/fragment'); const logger = require('../../logger'); // Delete fragment for the current user @@ -8,10 +8,11 @@ module.exports = async (req, res) => { let id = req.params.id; try { - await deleteFragment(ownerId, id); + await Fragment.delete(ownerId, id); + logger.info(`Deleted fragment with id ${id} for user ${ownerId}`); res.status(200).json(createSuccessResponse()); } catch (err) { - logger.info({ err }, 'Trying to delete fragment that does not exist'); + logger.warn({ err }, 'Trying to delete fragment that does not exist'); res.status(404).json(createErrorResponse(404, 'unable to delete fragment')); } }; diff --git a/src/routes/api/get.js b/src/routes/api/get.js index 71dc050..9f350c0 100644 --- a/src/routes/api/get.js +++ b/src/routes/api/get.js @@ -1,15 +1,14 @@ const { createSuccessResponse } = require('../../response'); -const { listFragments } = require('../../model/data'); +const { Fragment } = require('../../model/fragment'); +const logger = require('../../logger'); // Gets a list of fragments for the current user -module.exports = async (req, res) => { +module.exports = async (req, res) => { const ownerId = req.user; const expand = req.query.expand == '1'; - const fragments = await listFragments(ownerId, expand); + const fragments = await Fragment.byUser(ownerId, expand); - let data = { - fragments: fragments - }; + logger.info(`Provided information about fragments for user ${ownerId}`); - res.status(200).json(createSuccessResponse(data)); -}; + res.status(200).json(createSuccessResponse({ fragments: fragments })); +}; \ No newline at end of file diff --git a/src/routes/api/index.js b/src/routes/api/index.js index d62e9e3..ade46ce 100644 --- a/src/routes/api/index.js +++ b/src/routes/api/index.js @@ -18,18 +18,21 @@ const rawBody = () => }, }); -// GET /v1/fragments -router.get('/fragments', require('./get')); - // Use a raw body parser for POST, which will give a `Buffer` Object or `{}` at `req.body` router.post('/fragments', rawBody(), require('./post')); +// GET /v1/fragments +router.get('/fragments', require('./get')); + // GET /v1/fragments/:id - get a fragment by id router.get('/fragments/:id', require('./byId')); // GET /v1/fragments/:id - get a fragment by id router.get('/fragments/:id/info', require('./info')); +// PUT /v1/fragments/:id - update a fragment by id +router.put('/fragments/:id', rawBody(), require('./update')); + // DELETE /fragments/:id - delete a fragment by id router.delete('/fragments/:id', require('./delete')); diff --git a/src/routes/api/info.js b/src/routes/api/info.js index 94f4e01..dd2775b 100644 --- a/src/routes/api/info.js +++ b/src/routes/api/info.js @@ -1,20 +1,18 @@ -const { readFragment } = require('../../model/data'); +const { Fragment } = require('../../model/fragment'); const { createSuccessResponse, createErrorResponse } = require('../../response'); const logger = require('../../logger'); // Gets a fragment based on its id module.exports = async (req, res) => { - const fragmentMetadata = await readFragment(req.user, req.params.id); + try { + let fragment = await Fragment.byId(req.user, req.params.id); - if (fragmentMetadata) { - let data = { - fragment: fragmentMetadata - }; - res.status(200).json(createSuccessResponse(data)); - } else { - logger.warn(`Requested fragment does not exist in the memory.`); - logger.debug(`Fragment not found with ID ${req.params.id}`); - - res.status(404).json(createErrorResponse(404, `Fragment not found: ${req.params.id}`)); - } + logger.info("Provided information about fragment with id " + req.params.id); + res.status(200).json(createSuccessResponse({ fragment: fragment })); + } + catch (err) { + logger.warn(`Requested fragment does not exist in the memory.`); + logger.debug(`Fragment not found with ID ${req.params.id}`); + res.status(404).json(createErrorResponse(404, `Fragment not found: ${req.params.id}`)); + } }; diff --git a/src/routes/api/post.js b/src/routes/api/post.js index d5c1bb4..e0b298d 100644 --- a/src/routes/api/post.js +++ b/src/routes/api/post.js @@ -1,7 +1,6 @@ const { createSuccessResponse, createErrorResponse } = require('../../response'); const { Fragment } = require('../../model/fragment'); const contentType = require('content-type'); -const { writeFragment, writeFragmentData } = require('../../model/data'); const logger = require('../../logger'); // Creates a fragment for the current user @@ -13,30 +12,25 @@ module.exports = async (req, res) => { res.status(415).json(createErrorResponse(415, "Content-Type is not supported")); } else { - let { type, parameters } = contentType.parse(req); - if (parameters && parameters.charset == "utf-8") + const ownerId = req.user; + let { type, parameters } = contentType.parse(req); + if (parameters && parameters.charset == "utf-8") type += "; charset=utf-8"; - const ownerId = req.user; - const fragment = new Fragment({ ownerId, type }); + let fragment = new Fragment({ ownerId, type }); fragment.updateSize(req.body); - if (process.env.AWS_REGION) //save dates as strings when storing with AWS - fragment.convertDatestoDateString(); - await writeFragment(fragment); - await writeFragmentData(ownerId, fragment.id, req.body); - - logger.info(`Created fragment: ${JSON.stringify(fragment)} `); - - let data = { - fragment: fragment - }; + await fragment.setData(req.body); + await fragment.save(); const url = `http://${req.headers.host}` || process.env.API_URL; const location = new URL(`/v1/fragments/${fragment.id}`, url); res.setHeader('Location', location.href); + res.setHeader('Content-Type', type); + + logger.info(`Created fragment: ${JSON.stringify(fragment)} `); - res.status(201).json(createSuccessResponse(data)); + res.status(201).json(createSuccessResponse({ fragment: fragment })); } -}; +}; \ No newline at end of file diff --git a/src/routes/api/update.js b/src/routes/api/update.js new file mode 100644 index 0000000..444a907 --- /dev/null +++ b/src/routes/api/update.js @@ -0,0 +1,43 @@ +const { createSuccessResponse, createErrorResponse } = require('../../response'); +const { Fragment } = require('../../model/fragment'); +const contentType = require('content-type'); +const logger = require('../../logger'); + +// Creates a fragment for the current user +module.exports = async (req, res) => { + + // testing if fragment was parsed by the raw body parser. + if (!Buffer.isBuffer(req.body)) { + logger.warn("Fragment could not be parsed by the raw body parser. Invalid Content-Type."); + res.status(415).json(createErrorResponse(415, "Content-Type is not supported")); + } + else { + + const id = req.params.id; + const ownerId = req.user; + let { type, parameters } = contentType.parse(req); + if (parameters && parameters.charset == "utf-8") + type += "; charset=utf-8"; + + try { + let fragment = await Fragment.byId(ownerId, id); + if (fragment.type == type) { + fragment.updateSize(req.body); + await fragment.setData(req.body); + await fragment.save(); + + logger.info(`Updated fragment with id ${id} `); + + res.setHeader('Content-Type', type); + res.status(200).json(createSuccessResponse({ fragment: fragment })); + } else { + logger.warn(`Trying to update fragment with data of a different type.`); + res.status(400).json(createErrorResponse(400, `Unsuccessful update of fragment (${id}). Fragment's type cannot be changed after creation, only the content can be changed.`)); + } + } + catch (err) { + logger.warn(`Cannot update fragment with id ${id} as it is not found.`); + res.status(404).json(createErrorResponse(404, `Fragment with id ${id} not found.`)); + } + } +}; \ No newline at end of file diff --git a/tests/data/image.gif b/tests/data/image.gif new file mode 100644 index 0000000000000000000000000000000000000000..cce7f9859e04778db8e377207d1043cd6ec35eb5 GIT binary patch literal 18008 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 literal 0 HcmV?d00001 diff --git a/tests/data/image.jpg b/tests/data/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..aafc2803751c6bba25a60c7fc9e712a8fe6ed8fa GIT binary patch literal 4650 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 Yn!Ff5Lhe@*509d)Ty(WHlHYs%FF%mZ)c^nh literal 0 HcmV?d00001 diff --git a/tests/data/image.png b/tests/data/image.png new file mode 100644 index 0000000000000000000000000000000000000000..e18bb1aced7ac468edede9621e6d17cb570efc8e GIT binary patch literal 37341 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 ZzXxKi1GQN3OlklC002ovPDHLkV1nlU@TdR) literal 0 HcmV?d00001 diff --git a/tests/data/image.webp b/tests/data/image.webp new file mode 100644 index 0000000000000000000000000000000000000000..d4d4279f775fe8515f700265b5f221c793d2bcc2 GIT binary patch literal 8896 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 literal 0 HcmV?d00001 diff --git a/tests/integration/lab-10-dynamodb.hurl b/tests/integration/lab-10-dynamodb.hurl index 1d6311d..2e158dc 100644 --- a/tests/integration/lab-10-dynamodb.hurl +++ b/tests/integration/lab-10-dynamodb.hurl @@ -33,8 +33,8 @@ jsonpath "$.status" == "ok" jsonpath "$.fragment.id" == {{fragment1_id}} # Our ownerId hash is a hex encoded string jsonpath "$.fragment.ownerId" matches "^[0-9a-fA-F]+$" -jsonpath "$.fragment.created" isString -jsonpath "$.fragment.updated" isString +jsonpath "$.fragment.created" != null +jsonpath "$.fragment.updated" != null jsonpath "$.fragment.type" == "application/json" jsonpath "$.fragment.size" == 25 @@ -73,8 +73,8 @@ jsonpath "$.status" == "ok" jsonpath "$.fragment.id" == {{fragment2_id}} # Our ownerId hash is a hex encoded string jsonpath "$.fragment.ownerId" matches "^[0-9a-fA-F]+$" -jsonpath "$.fragment.created" isString -jsonpath "$.fragment.updated" isString +jsonpath "$.fragment.created" != null +jsonpath "$.fragment.updated" != null jsonpath "$.fragment.type" == "text/markdown" jsonpath "$.fragment.size" == 22 diff --git a/tests/integration/post-fragments.hurl b/tests/integration/post-fragments.hurl index ee1b80b..c5f5bb5 100644 --- a/tests/integration/post-fragments.hurl +++ b/tests/integration/post-fragments.hurl @@ -97,7 +97,7 @@ HTTP/1.1 401 # Authenticated POST to /v1/fragments POST http://localhost:8080/v1/fragments # Send a json fragment -Content-Type: application/json; charset=utf-8 +Content-Type: application/json # Include HTTP Basic Auth credentials using the [BasicAuth] section [BasicAuth] user1@email.com:password1 @@ -119,7 +119,7 @@ jsonpath "$.fragment.ownerId" matches "^[0-9a-fA-F]+$" # You could also write a regex for this and use matches jsonpath "$.fragment.created" isString jsonpath "$.fragment.updated" isString -jsonpath "$.fragment.type" == "application/json; charset=utf-8" +jsonpath "$.fragment.type" == "application/json" # 21 is the length of our fragment data: {"status": "testing"} jsonpath "$.fragment.size" == 21 # Capture the Location URL into a variable named `url` @@ -131,7 +131,123 @@ GET {{url}} [BasicAuth] user1@email.com:password1 +HTTP/1.1 200 +Content-Type: application/json +[Asserts] +body == "{\"status\": \"testing\"}" + + +# application/json; charset=utf-8 +POST http://localhost:8080/v1/fragments +Content-Type: application/json; charset=utf-8 +[BasicAuth] +user1@email.com:password1 +# Body of the request +{"status": "testing"} + +HTTP/1.1 201 +[Asserts] +jsonpath "$.fragment.type" == "application/json; charset=utf-8" +[Captures] +url: header "Location" + +GET {{url}} +[BasicAuth] +user1@email.com:password1 + HTTP/1.1 200 Content-Type: application/json; charset=utf-8 [Asserts] -body == "\"{\\\"status\\\": \\\"testing\\\"}\"" +body == "{\"status\": \"testing\"}" + + +# text/markdown +POST http://localhost:8080/v1/fragments +Content-Type: text/markdown +[BasicAuth] +user1@email.com:password1 +`# This is a fragment!` + +HTTP/1.1 201 +[Asserts] +jsonpath "$.fragment.type" == "text/markdown" +[Captures] +url: header "Location" + +GET {{url}} +[BasicAuth] +user1@email.com:password1 + +HTTP/1.1 200 +Content-Type: text/markdown +[Asserts] +body == "# This is a fragment!" + + +# text/markdown; charset=utf-8 +POST http://localhost:8080/v1/fragments +Content-Type: text/markdown; charset=utf-8 +[BasicAuth] +user1@email.com:password1 +`# This is a fragment!` + +HTTP/1.1 201 +[Asserts] +jsonpath "$.fragment.type" == "text/markdown; charset=utf-8" +[Captures] +url: header "Location" + +GET {{url}} +[BasicAuth] +user1@email.com:password1 + +HTTP/1.1 200 +Content-Type: text/markdown; charset=utf-8 +[Asserts] +body == "# This is a fragment!" + + +# text/html +POST http://localhost:8080/v1/fragments +Content-Type: text/html +[BasicAuth] +user1@email.com:password1 +`

This is a fragment!

` + +HTTP/1.1 201 +[Asserts] +jsonpath "$.fragment.type" == "text/html" +[Captures] +url: header "Location" + +GET {{url}} +[BasicAuth] +user1@email.com:password1 + +HTTP/1.1 200 +Content-Type: text/html +[Asserts] +body == "

This is a fragment!

" + + +# text/html; charset=utf-8 +POST http://localhost:8080/v1/fragments +Content-Type: text/html; charset=utf-8 +[BasicAuth] +user1@email.com:password1 +`

This is a fragment!

` + +HTTP/1.1 201 +[Asserts] +jsonpath "$.fragment.type" == "text/html; charset=utf-8" +[Captures] +url: header "Location" + +GET {{url}} +[BasicAuth] +user1@email.com:password1 + +HTTP/1.1 200 +Content-Type: text/html; charset=utf-8 +[Asserts] +body == "

This is a fragment!

" \ No newline at end of file diff --git a/tests/integration/update-delete.hurl b/tests/integration/update-delete.hurl new file mode 100644 index 0000000..1303494 --- /dev/null +++ b/tests/integration/update-delete.hurl @@ -0,0 +1,612 @@ +# Update and delete text/plain +POST http://localhost:8080/v1/fragments +Content-Type: text/plain +[BasicAuth] +user1@email.com:password1 +`This is a fragment!` + +# Check that fragment was created successfully +HTTP/1.1 201 +[Captures] +url: header "Location" + +# check the content before update +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/plain +[Asserts] +body == "This is a fragment!" + +#update content +PUT {{url}} +Content-Type: text/plain +[BasicAuth] +user1@email.com:password1 +`This fragment has updated content.` + +# Check that fragment was updated successfully +HTTP/1.1 200 + +# check that fragment was set to the correct content +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/plain +[Asserts] +body == "This fragment has updated content." + +#update content (with a different content type) +PUT {{url}} +Content-Type: text/markdown +[BasicAuth] +user1@email.com:password1 +`# This fragment has updated content with a different content type.` + +# Check that fragment was not updated as fragment's type cannot change after update +HTTP/1.1 400 + +# check that content was not updated +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/plain +[Asserts] +body == "This fragment has updated content." + +# delete fragment +DELETE {{url}} +[BasicAuth] +user1@email.com:password1 + +# Check that request was successful +HTTP/1.1 200 + +# check that fragment doesn't exist anymore +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 404 + + + + + + +# Update and delete text/plain; charset=utf-8 +POST http://localhost:8080/v1/fragments +Content-Type: text/plain; charset=utf-8 +[BasicAuth] +user1@email.com:password1 +`This is a fragment!` + +# Check that fragment was created successfully +HTTP/1.1 201 +[Captures] +url: header "Location" + +# check the content before update +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/plain; charset=utf-8 +[Asserts] +body == "This is a fragment!" + +#update content +PUT {{url}} +Content-Type: text/plain; charset=utf-8 +[BasicAuth] +user1@email.com:password1 +`This fragment has updated content.` + +# Check that fragment was updated successfully +HTTP/1.1 200 + +# check that fragment was set to the correct content +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/plain; charset=utf-8 +[Asserts] +body == "This fragment has updated content." + +#update content (with a different content type) +PUT {{url}} +Content-Type: text/markdown +[BasicAuth] +user1@email.com:password1 +`# This fragment has updated content with a different content type.` + +# Check that fragment was not updated as fragment's type cannot change after update +HTTP/1.1 400 + +# check that content was not updated +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/plain; charset=utf-8 +[Asserts] +body == "This fragment has updated content." + +# delete fragment +DELETE {{url}} +[BasicAuth] +user1@email.com:password1 + +# Check that request was successful +HTTP/1.1 200 + +# check that fragment doesn't exist anymore +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 404 + + + + + +# Update and delete text/markdown +POST http://localhost:8080/v1/fragments +Content-Type: text/markdown +[BasicAuth] +user1@email.com:password1 +`# This is a fragment!` + +# Check that fragment was created successfully +HTTP/1.1 201 +[Captures] +url: header "Location" + +# check the content before update +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/markdown +[Asserts] +body == "# This is a fragment!" + +#update content +PUT {{url}} +Content-Type: text/markdown +[BasicAuth] +user1@email.com:password1 +`# This fragment has updated content.` + +# Check that fragment was updated successfully +HTTP/1.1 200 + +# check that fragment was set to the correct content +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/markdown +[Asserts] +body == "# This fragment has updated content." + +#update content (with a different content type) +PUT {{url}} +Content-Type: text/plain +[BasicAuth] +user1@email.com:password1 +`This fragment has updated content with a different content type.` + +# Check that fragment was not updated as fragment's type cannot change after update +HTTP/1.1 400 + +# check that content was not updated +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/markdown +[Asserts] +body == "# This fragment has updated content." + +# delete fragment +DELETE {{url}} +[BasicAuth] +user1@email.com:password1 + +# Check that request was successful +HTTP/1.1 200 + +# check that fragment doesn't exist anymore +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 404 + + + + + +# Update and delete text/markdown; charset=utf-8 +POST http://localhost:8080/v1/fragments +Content-Type: text/markdown; charset=utf-8 +[BasicAuth] +user1@email.com:password1 +`# This is a fragment!` + +# Check that fragment was created successfully +HTTP/1.1 201 +[Captures] +url: header "Location" + +# check the content before update +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/markdown; charset=utf-8 +[Asserts] +body == "# This is a fragment!" + +#update content +PUT {{url}} +Content-Type: text/markdown; charset=utf-8 +[BasicAuth] +user1@email.com:password1 +`# This fragment has updated content.` + +# Check that fragment was updated successfully +HTTP/1.1 200 + +# check that fragment was set to the correct content +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/markdown; charset=utf-8 +[Asserts] +body == "# This fragment has updated content." + +#update content (with a different content type) +PUT {{url}} +Content-Type: text/plain +[BasicAuth] +user1@email.com:password1 +`This fragment has updated content with a different content type.` + +# Check that fragment was not updated as fragment's type cannot change after update +HTTP/1.1 400 + +# check that content was not updated +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/markdown; charset=utf-8 +[Asserts] +body == "# This fragment has updated content." + +# delete fragment +DELETE {{url}} +[BasicAuth] +user1@email.com:password1 + +# Check that request was successful +HTTP/1.1 200 + +# check that fragment doesn't exist anymore +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 404 + + + + + +# Update and delete text/html +POST http://localhost:8080/v1/fragments +Content-Type: text/html +[BasicAuth] +user1@email.com:password1 +`

This is a fragment!

` + +# Check that fragment was created successfully +HTTP/1.1 201 +[Captures] +url: header "Location" + +# check the content before update +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/html +[Asserts] +body == "

This is a fragment!

" + +#update content +PUT {{url}} +Content-Type: text/html +[BasicAuth] +user1@email.com:password1 +`

This fragment has updated content.

` + +# Check that fragment was updated successfully +HTTP/1.1 200 + +# check that fragment was set to the correct content +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/html +[Asserts] +body == "

This fragment has updated content.

" + +#update content (with a different content type) +PUT {{url}} +Content-Type: text/plain +[BasicAuth] +user1@email.com:password1 +`This fragment has updated content with a different content type.` + +# Check that fragment was not updated as fragment's type cannot change after update +HTTP/1.1 400 + +# check that content was not updated +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/html +[Asserts] +body == "

This fragment has updated content.

" + +# delete fragment +DELETE {{url}} +[BasicAuth] +user1@email.com:password1 + +# Check that request was successful +HTTP/1.1 200 + +# check that fragment doesn't exist anymore +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 404 + + + + + +# Update and delete text/html; charset=utf-8 +POST http://localhost:8080/v1/fragments +Content-Type: text/html; charset=utf-8 +[BasicAuth] +user1@email.com:password1 +`

This is a fragment!

` + +# Check that fragment was created successfully +HTTP/1.1 201 +[Captures] +url: header "Location" + +# check the content before update +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/html; charset=utf-8 +[Asserts] +body == "

This is a fragment!

" + +#update content +PUT {{url}} +Content-Type: text/html; charset=utf-8 +[BasicAuth] +user1@email.com:password1 +`

This fragment has updated content.

` + +# Check that fragment was updated successfully +HTTP/1.1 200 + +# check that fragment was set to the correct content +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/html; charset=utf-8 +[Asserts] +body == "

This fragment has updated content.

" + +#update content (with a different content type) +PUT {{url}} +Content-Type: text/plain +[BasicAuth] +user1@email.com:password1 +`This fragment has updated content with a different content type.` + +# Check that fragment was not updated as fragment's type cannot change after update +HTTP/1.1 400 + +# check that content was not updated +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: text/html; charset=utf-8 +[Asserts] +body == "

This fragment has updated content.

" + +# delete fragment +DELETE {{url}} +[BasicAuth] +user1@email.com:password1 + +# Check that request was successful +HTTP/1.1 200 + +# check that fragment doesn't exist anymore +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 404 + + + + + +# Update and delete application/json +POST http://localhost:8080/v1/fragments +Content-Type: application/json +[BasicAuth] +user1@email.com:password1 +`{"status": "testing"}` + +# Check that fragment was created successfully +HTTP/1.1 201 +[Captures] +url: header "Location" + +# check the content before update +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: application/json +[Asserts] +body == "{\"status\": \"testing\"}" + +#update content +PUT {{url}} +Content-Type: application/json +[BasicAuth] +user1@email.com:password1 +`{"status": "updated testing"}` + +# Check that fragment was updated successfully +HTTP/1.1 200 + +# check that fragment was set to the correct content +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: application/json +[Asserts] +body == "{\"status\": \"updated testing\"}" + +#update content (with a different content type) +PUT {{url}} +Content-Type: text/plain +[BasicAuth] +user1@email.com:password1 +`This fragment has updated content with a different content type.` + +# Check that fragment was not updated as fragment's type cannot change after update +HTTP/1.1 400 + +# check that content was not updated +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: application/json +[Asserts] +body == "{\"status\": \"updated testing\"}" + +# delete fragment +DELETE {{url}} +[BasicAuth] +user1@email.com:password1 + +# Check that request was successful +HTTP/1.1 200 + +# check that fragment doesn't exist anymore +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 404 + + + + + +# Update and delete application/json; charset=utf-8 +POST http://localhost:8080/v1/fragments +Content-Type: application/json; charset=utf-8 +[BasicAuth] +user1@email.com:password1 +`{"status": "testing"}` + +# Check that fragment was created successfully +HTTP/1.1 201 +[Captures] +url: header "Location" + +# check the content before update +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: application/json; charset=utf-8 +[Asserts] +body == "{\"status\": \"testing\"}" + +#update content +PUT {{url}} +Content-Type: application/json; charset=utf-8 +[BasicAuth] +user1@email.com:password1 +`{"status": "updated testing"}` + +# Check that fragment was updated successfully +HTTP/1.1 200 + +# check that fragment was set to the correct content +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: application/json; charset=utf-8 +[Asserts] +body == "{\"status\": \"updated testing\"}" + +#update content (with a different content type) +PUT {{url}} +Content-Type: text/plain +[BasicAuth] +user1@email.com:password1 +`This fragment has updated content with a different content type.` + +# Check that fragment was not updated as fragment's type cannot change after update +HTTP/1.1 400 + +# check that content was not updated +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 200 +Content-Type: application/json; charset=utf-8 +[Asserts] +body == "{\"status\": \"updated testing\"}" + +# delete fragment +DELETE {{url}} +[BasicAuth] +user1@email.com:password1 + +# Check that request was successful +HTTP/1.1 200 + +# check that fragment doesn't exist anymore +GET {{url}} +[BasicAuth] +user1@email.com:password1 +HTTP/1.1 404 \ No newline at end of file diff --git a/tests/unit/byId.test.js b/tests/unit/byId.test.js index 1fb87d4..1d4b73e 100644 --- a/tests/unit/byId.test.js +++ b/tests/unit/byId.test.js @@ -1,4 +1,6 @@ const request = require('supertest'); +const fs = require('fs'); +const path = require('path'); const app = require('../../src/app'); @@ -11,9 +13,10 @@ describe('GET /v1/fragments:id', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/plain') .send("This is a fragment"); + let body = JSON.parse(res.text); // Try to get the existing fragment without authorization - await request(app).get(`/v1/fragments${res.body.fragment.id}`).expect(401); + await request(app).get(`/v1/fragments/${body.fragment.id}`).expect(401); // Try to get a non-existing fragment without authorization await request(app).get(`/v1/fragments123`).expect(401); @@ -27,9 +30,10 @@ describe('GET /v1/fragments:id', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/plain') .send("This is a fragment"); + let body = JSON.parse(res.text); // Try to get the existing fragment with incorrect credentials - await request(app).get(`/v1/fragments${res.body.fragment.id}`) + await request(app).get(`/v1/fragments/${body.fragment.id}`) .auth('invalid@email.com', 'incorrect_password').expect(401); // Try to get a non-existing fragment with incorrect credentials @@ -45,12 +49,13 @@ describe('GET /v1/fragments:id', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/plain') .send("This is a fragment"); + let body = JSON.parse(res.text); - const fragment_by_id = await request(app).get(`/v1/fragments/${res.body.fragment.id}`) + const fragment_by_id = await request(app).get(`/v1/fragments/${body.fragment.id}`) .auth('user1@email.com', 'password1'); - expect(fragment_by_id.header['content-type']).toBe(res.body.fragment.type); - expect(fragment_by_id.header['content-length']).toBe(res.body.fragment.size.toString()); + expect(fragment_by_id.header['content-type']).toBe(body.fragment.type); + expect(fragment_by_id.header['content-length']).toBe(body.fragment.size.toString()); expect(fragment_by_id.text).toBe("This is a fragment"); }); @@ -63,8 +68,9 @@ describe('GET /v1/fragments:id', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/markdown') .send("# This is a fragment"); + let body = JSON.parse(res.text); - const fragment_by_id = await request(app).get(`/v1/fragments/${res.body.fragment.id}`) + const fragment_by_id = await request(app).get(`/v1/fragments/${body.fragment.id}`) .auth('user1@email.com', 'password1'); expect(fragment_by_id.header['content-type']).toBe('text/markdown'); @@ -80,8 +86,9 @@ describe('GET /v1/fragments:id', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/html') .send("

This is a fragment

"); + let body = JSON.parse(res.text); - const fragment_by_id = await request(app).get(`/v1/fragments/${res.body.fragment.id}`) + const fragment_by_id = await request(app).get(`/v1/fragments/${body.fragment.id}`) .auth('user1@email.com', 'password1'); expect(fragment_by_id.header['content-type']).toBe('text/html'); @@ -96,13 +103,13 @@ describe('GET /v1/fragments:id', () => { const res = await request(app).post('/v1/fragments') .auth('user1@email.com', 'password1') .set('Content-Type', 'application/json; charset=utf-8') - .send("{'fragment': 'This is a fragment'}"); + .send('{"fragment": "This is a fragment"}'); const fragment_by_id = await request(app).get(`/v1/fragments/${res.body.fragment.id}`) .auth('user1@email.com', 'password1'); expect(fragment_by_id.header['content-type']).toBe('application/json; charset=utf-8'); - expect(fragment_by_id.text).toContain("{'fragment': 'This is a fragment'}"); + expect(fragment_by_id.text).toContain('{"fragment": "This is a fragment"}'); }); @@ -128,9 +135,10 @@ describe('GET /v1/fragments:id', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/plain') .send("This is a fragment"); + let body = JSON.parse(res.text); //Convert fragment to text/plain - let converted_fragment = await request(app).get(`/v1/fragments/${res.body.fragment.id}.txt`) + let converted_fragment = await request(app).get(`/v1/fragments/${body.fragment.id}.txt`) .auth('user1@email.com', 'password1'); expect(converted_fragment.text).toContain("This is a fragment"); @@ -140,19 +148,20 @@ describe('GET /v1/fragments:id', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/markdown') .send("# This is a fragment"); + body = JSON.parse(res.text); //Convert fragment to text/html - converted_fragment = await request(app).get(`/v1/fragments/${res.body.fragment.id}.html`) + converted_fragment = await request(app).get(`/v1/fragments/${body.fragment.id}.html`) .auth('user1@email.com', 'password1'); expect(converted_fragment.text).toContain("

This is a fragment

"); //Convert fragment to text/plain - converted_fragment = await request(app).get(`/v1/fragments/${res.body.fragment.id}.txt`) + converted_fragment = await request(app).get(`/v1/fragments/${body.fragment.id}.txt`) .auth('user1@email.com', 'password1'); expect(converted_fragment.text).toContain("This is a fragment"); //Convert fragment to text/markdown - converted_fragment = await request(app).get(`/v1/fragments/${res.body.fragment.id}.md`) + converted_fragment = await request(app).get(`/v1/fragments/${body.fragment.id}.md`) .auth('user1@email.com', 'password1'); expect(converted_fragment.text).toContain("# This is a fragment"); @@ -162,14 +171,15 @@ describe('GET /v1/fragments:id', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/html') .send("

This is a fragment

"); + body = JSON.parse(res.text); //Convert fragment to text/plain - converted_fragment = await request(app).get(`/v1/fragments/${res.body.fragment.id}.txt`) + converted_fragment = await request(app).get(`/v1/fragments/${body.fragment.id}.txt`) .auth('user1@email.com', 'password1'); expect(converted_fragment.text).toContain("This is a fragment"); //Convert fragment to text/html - converted_fragment = await request(app).get(`/v1/fragments/${res.body.fragment.id}.html`) + converted_fragment = await request(app).get(`/v1/fragments/${body.fragment.id}.html`) .auth('user1@email.com', 'password1'); expect(converted_fragment.text).toContain("

This is a fragment

"); @@ -177,18 +187,57 @@ describe('GET /v1/fragments:id', () => { res = await request(app).post('/v1/fragments') .auth('user1@email.com', 'password1') .set('Content-Type', 'application/json; charset=utf-8') - .send("{'fragment': 'This is a fragment'}"); + .send('{"fragment": "This is a fragment"}'); //Convert fragment to text/plain converted_fragment = await request(app).get(`/v1/fragments/${res.body.fragment.id}.txt`) .auth('user1@email.com', 'password1'); - expect(converted_fragment.text).toContain("{'fragment': 'This is a fragment'}"); + expect(converted_fragment.text).toContain('{"fragment": "This is a fragment"}'); //Convert fragment to text/json converted_fragment = await request(app).get(`/v1/fragments/${res.body.fragment.id}.json`) .auth('user1@email.com', 'password1'); - expect(converted_fragment.text).toContain("{'fragment': 'This is a fragment'}"); - + expect(converted_fragment.text).toContain('{"fragment": "This is a fragment"}'); + + let data = fs.readFileSync(path.join(__dirname, '../data', 'image.png')); + res = await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'image/png').send(data).expect(201); + let location = res.headers.location; + let url = location.match(/fragments\/([\w-]+)/)[0]; + + await request(app).get(`/v1/${url}.jpg`).auth('user1@email.com', 'password1').expect(200); + await request(app).get(`/v1/${url}.webp`).auth('user1@email.com', 'password1').expect(200); + await request(app).get(`/v1/${url}.gif`).auth('user1@email.com', 'password1').expect(200); + + data = fs.readFileSync(path.join(__dirname, '../data', 'image.jpg')); + res = await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'image/jpeg').send(data).expect(201); + location = res.headers.location; + url = location.match(/fragments\/([\w-]+)/)[0]; + + await request(app).get(`/v1/${url}.png`).auth('user1@email.com', 'password1').expect(200); + await request(app).get(`/v1/${url}.webp`).auth('user1@email.com', 'password1').expect(200); + await request(app).get(`/v1/${url}.gif`).auth('user1@email.com', 'password1').expect(200); + + data = fs.readFileSync(path.join(__dirname, '../data', 'image.webp')); + res = await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'image/webp').send(data).expect(201); + location = res.headers.location; + url = location.match(/fragments\/([\w-]+)/)[0]; + + await request(app).get(`/v1/${url}.png`).auth('user1@email.com', 'password1').expect(200); + await request(app).get(`/v1/${url}.jpg`).auth('user1@email.com', 'password1').expect(200); + await request(app).get(`/v1/${url}.gif`).auth('user1@email.com', 'password1').expect(200); + + data = fs.readFileSync(path.join(__dirname, '../data', 'image.gif')); + res = await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'image/gif').send(data).expect(201); + location = res.headers.location; + url = location.match(/fragments\/([\w-]+)/)[0]; + + await request(app).get(`/v1/${url}.png`).auth('user1@email.com', 'password1').expect(200); + await request(app).get(`/v1/${url}.jpg`).auth('user1@email.com', 'password1').expect(200); + await request(app).get(`/v1/${url}.webp`).auth('user1@email.com', 'password1').expect(200); }); // Unsupported conversions should give an appropriate error @@ -199,27 +248,28 @@ describe('GET /v1/fragments:id', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/plain') .send("This is a fragment"); + let body = JSON.parse(res.text); //Try to convert fragment to text/markdown - await request(app).get(`/v1/fragments/${res.body.fragment.id}.md`) + await request(app).get(`/v1/fragments/${body.fragment.id}.md`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to text/html - await request(app).get(`/v1/fragments/${res.body.fragment.id}.html`) + await request(app).get(`/v1/fragments/${body.fragment.id}.html`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to application/json - await request(app).get(`/v1/fragments/${res.body.fragment.id}.json`) + await request(app).get(`/v1/fragments/${body.fragment.id}.json`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image/png - await request(app).get(`/v1/fragments/${res.body.fragment.id}.png`) + await request(app).get(`/v1/fragments/${body.fragment.id}.png`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image/jpeg - await request(app).get(`/v1/fragments/${res.body.fragment.id}.jpeg`) + await request(app).get(`/v1/fragments/${body.fragment.id}.jpeg`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image/webp - await request(app).get(`/v1/fragments/${res.body.fragment.id}.webp`) + await request(app).get(`/v1/fragments/${body.fragment.id}.webp`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image.gif - await request(app).get(`/v1/fragments/${res.body.fragment.id}.gif`) + await request(app).get(`/v1/fragments/${body.fragment.id}.gif`) .auth('user1@email.com', 'password1').expect(415); @@ -228,21 +278,22 @@ describe('GET /v1/fragments:id', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/markdown') .send("# This is a fragment"); + body = JSON.parse(res.text); //Try to convert fragment to application/json - await request(app).get(`/v1/fragments/${res.body.fragment.id}.json`) + await request(app).get(`/v1/fragments/${body.fragment.id}.json`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image/png - await request(app).get(`/v1/fragments/${res.body.fragment.id}.png`) + await request(app).get(`/v1/fragments/${body.fragment.id}.png`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image/jpeg - await request(app).get(`/v1/fragments/${res.body.fragment.id}.jpeg`) + await request(app).get(`/v1/fragments/${body.fragment.id}.jpeg`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image/webp - await request(app).get(`/v1/fragments/${res.body.fragment.id}.webp`) + await request(app).get(`/v1/fragments/${body.fragment.id}.webp`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image.gif - await request(app).get(`/v1/fragments/${res.body.fragment.id}.gif`) + await request(app).get(`/v1/fragments/${body.fragment.id}.gif`) .auth('user1@email.com', 'password1').expect(415); @@ -251,24 +302,25 @@ describe('GET /v1/fragments:id', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/html') .send("

This is a fragment

"); + body = JSON.parse(res.text); //Try to convert fragment to text/markdown - await request(app).get(`/v1/fragments/${res.body.fragment.id}.md`) + await request(app).get(`/v1/fragments/${body.fragment.id}.md`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to application/json - await request(app).get(`/v1/fragments/${res.body.fragment.id}.json`) + await request(app).get(`/v1/fragments/${body.fragment.id}.json`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image/png - await request(app).get(`/v1/fragments/${res.body.fragment.id}.png`) + await request(app).get(`/v1/fragments/${body.fragment.id}.png`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image/jpeg - await request(app).get(`/v1/fragments/${res.body.fragment.id}.jpeg`) + await request(app).get(`/v1/fragments/${body.fragment.id}.jpeg`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image/webp - await request(app).get(`/v1/fragments/${res.body.fragment.id}.webp`) + await request(app).get(`/v1/fragments/${body.fragment.id}.webp`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image.gif - await request(app).get(`/v1/fragments/${res.body.fragment.id}.gif`) + await request(app).get(`/v1/fragments/${body.fragment.id}.gif`) .auth('user1@email.com', 'password1').expect(415); @@ -276,28 +328,28 @@ describe('GET /v1/fragments:id', () => { res = await request(app).post('/v1/fragments') .auth('user1@email.com', 'password1') .set('Content-Type', 'application/json; charset=utf-8') - .send("{'fragment': 'This is a fragment'}"); + .send('{"fragment": "This is a fragment"}'); + body = JSON.parse(res.text); //Try to convert fragment to text/markdown - await request(app).get(`/v1/fragments/${res.body.fragment.id}.md`) + await request(app).get(`/v1/fragments/${body.fragment.id}.md`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to text/html - await request(app).get(`/v1/fragments/${res.body.fragment.id}.html`) + await request(app).get(`/v1/fragments/${body.fragment.id}.html`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image/png - await request(app).get(`/v1/fragments/${res.body.fragment.id}.png`) + await request(app).get(`/v1/fragments/${body.fragment.id}.png`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image/jpeg - await request(app).get(`/v1/fragments/${res.body.fragment.id}.jpeg`) + await request(app).get(`/v1/fragments/${body.fragment.id}.jpeg`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image/webp - await request(app).get(`/v1/fragments/${res.body.fragment.id}.webp`) + await request(app).get(`/v1/fragments/${body.fragment.id}.webp`) .auth('user1@email.com', 'password1').expect(415); //Try to convert fragment to image.gif - await request(app).get(`/v1/fragments/${res.body.fragment.id}.gif`) + await request(app).get(`/v1/fragments/${body.fragment.id}.gif`) .auth('user1@email.com', 'password1').expect(415); - }); }); diff --git a/tests/unit/delete.test.js b/tests/unit/delete.test.js new file mode 100644 index 0000000..74b48b7 --- /dev/null +++ b/tests/unit/delete.test.js @@ -0,0 +1,80 @@ +const request = require('supertest'); +const fs = require('fs'); +const path = require('path'); + +const app = require('../../src/app'); + +describe('DELETE /v1/fragments', () => { + + test('unauthenticated requests are denied', async () => + await request(app).delete('/v1/fragments/123').expect(401)); + + test('incorrect credentials are denied', async () => + await request(app).delete('/v1/fragments/123') + .auth('invalid@email.com', 'incorrect_password') + .expect(401)); + + test('authenticated user cannot delete fragment that does not exist', async () => + await request(app).delete('/v1/fragments/123') + .auth('user1@email.com', 'password1') + .expect(404)); + + test('authenticated user can delete fragment of that exists of all of the supported types', async () => { + let res = await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'text/plain').send("This is a fragment"); + let body = JSON.parse(res.text); + await request(app).delete(`/v1/fragments/${body.fragment.id}`) + .auth('user1@email.com', 'password1').expect(200); + + res = await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'text/html').send("

This is a fragment

"); + body = JSON.parse(res.text); + await request(app).delete(`/v1/fragments/${body.fragment.id}`) + .auth('user1@email.com', 'password1').expect(200); + + res = await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'text/markdown').send("### This is a fragment"); + body = JSON.parse(res.text); + await request(app).delete(`/v1/fragments/${body.fragment.id}`) + .auth('user1@email.com', 'password1').expect(200); + + res = await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'application/json').send('{"status": "testing"}'); + await request(app).delete(`/v1/fragments/${res.body.fragment.id}`) + .auth('user1@email.com', 'password1').expect(200); + + let data = fs.readFileSync(path.join(__dirname, '../data', 'image.png')); + res = await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'image/png').send(data); + let location = res.headers.location; + let url = location.match(/fragments\/([\w-]+)/)[0]; + await request(app).delete(`/v1/${url}`) + .auth('user1@email.com', 'password1').expect(200); + + data = fs.readFileSync(path.join(__dirname, '../data', 'image.jpg')); + res = await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'image/jpeg').send(data); + location = res.headers.location; + url = location.match(/fragments\/([\w-]+)/)[0]; + await request(app).delete(`/v1/${url}`) + .auth('user1@email.com', 'password1').expect(200); + + data = fs.readFileSync(path.join(__dirname, '../data', 'image.webp')); + res = await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'image/webp').send(data); + location = res.headers.location; + url = location.match(/fragments\/([\w-]+)/)[0]; + await request(app).delete(`/v1/${url}`) + .auth('user1@email.com', 'password1').expect(200); + + data = fs.readFileSync(path.join(__dirname, '../data', 'image.gif')); + res = await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'image/gif').send(data); + location = res.headers.location; + url = location.match(/fragments\/([\w-]+)/)[0]; + await request(app).delete(`/v1/${url}`) + .auth('user1@email.com', 'password1').expect(200); + + }); + +}); diff --git a/tests/unit/fragment.test.js b/tests/unit/fragment.test.js index 37a580b..3312e5c 100644 --- a/tests/unit/fragment.test.js +++ b/tests/unit/fragment.test.js @@ -9,15 +9,10 @@ const validTypes = [ `text/markdown`, `text/html`, `application/json`, - - /* - Currently, only text/plain is supported. Others will be added later. - `image/png`, `image/jpeg`, `image/webp`, `image/gif`, - */ ]; describe('Fragment class', () => { diff --git a/tests/unit/get.test.js b/tests/unit/get.test.js index 0aa41f6..d35eda5 100644 --- a/tests/unit/get.test.js +++ b/tests/unit/get.test.js @@ -24,9 +24,11 @@ describe('GET /v1/fragments', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/plain') .send("This is a fragment"); + let post_body = JSON.parse(post_res.text); const get_res = await request(app).get('/v1/fragments').auth('user1@email.com', 'password1'); + let get_body = JSON.parse(get_res.text); - expect(get_res.body.fragments[0]).toBe(post_res.body.fragment.id); + expect(get_body.fragments[0]).toBe(post_body.fragment.id); }); }); diff --git a/tests/unit/info.test.js b/tests/unit/info.test.js index 7a31ab8..f229b9f 100644 --- a/tests/unit/info.test.js +++ b/tests/unit/info.test.js @@ -11,9 +11,10 @@ describe('GET /v1/fragments:id/info', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/plain') .send("This is a fragment"); + let body = JSON.parse(res.text); // Try to get the existing fragment's metadata without authorization - await request(app).get(`/v1/fragments${res.body.fragment.id}/info`).expect(401); + await request(app).get(`/v1/fragments${body.fragment.id}/info`).expect(401); // Try to get a non-existing fragment without authorization await request(app).get(`/v1/fragments123/info`).expect(401); @@ -27,9 +28,10 @@ describe('GET /v1/fragments:id/info', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/plain') .send("This is a fragment"); + let body = JSON.parse(res.text); // Try to get the existing fragment's metadata with incorrect credentials - await request(app).get(`/v1/fragments${res.body.fragment.id}/info`) + await request(app).get(`/v1/fragments${body.fragment.id}/info`) .auth('invalid@email.com', 'incorrect_password').expect(401); // Try to get a non-existing fragment with incorrect credentials @@ -52,11 +54,12 @@ describe('GET /v1/fragments:id/info', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/plain') .send("This is a fragment"); + let body = JSON.parse(res.text); - let fragment_by_id = await request(app).get(`/v1/fragments/${res.body.fragment.id}/info`) + let fragment_by_id = await request(app).get(`/v1/fragments/${body.fragment.id}/info`) .auth('user1@email.com', 'password1'); - expect(fragment_by_id.text).toContain(res.body.fragment.type); - expect(fragment_by_id.text).toContain(res.body.fragment.size.toString()); + expect(fragment_by_id.text).toContain(body.fragment.type); + expect(fragment_by_id.text).toContain(body.fragment.size.toString()); }); }); diff --git a/tests/unit/post.test.js b/tests/unit/post.test.js index 1d02203..5950a96 100644 --- a/tests/unit/post.test.js +++ b/tests/unit/post.test.js @@ -1,5 +1,7 @@ const request = require('supertest'); const hash = require('../../src/hash'); // for hashing email addresses +const fs = require('fs'); +const path = require('path'); const app = require('../../src/app'); @@ -26,10 +28,10 @@ describe('POST /v1/fragments', () => { .set('Content-Type', 'text/text').send("This is a fragment").expect(415); await request(app).post('/v1/fragments').auth('user2@email.com', 'password2') - .set('Content-Type', 'image/jpg').send("This is a fragment").expect(415); + .set('Content-Type', 'image/psd').send("This is a fragment").expect(415); await request(app).post('/v1/fragments').auth('user2@email.com', 'password2') - .set('Content-Type', 'image/gif').send("This is a fragment").expect(415); + .set('Content-Type', 'application/xml').send("This is a fragment").expect(415); }); test('authenticated user can fragments of supported type', async () => { @@ -37,22 +39,24 @@ describe('POST /v1/fragments', () => { .auth('user1@email.com', 'password1') .set('Content-Type', 'text/plain') .send("This is a fragment"); + let body = JSON.parse(res.text); + expect(res.statusCode).toBe(201); - expect(res.body.status).toBe('ok'); - expect(typeof res.body.fragment).toBe("object"); + expect(body.status).toBe('ok'); + expect(typeof body.fragment).toBe("object"); - expect(res.body.fragment.id).toBeDefined(); + expect(body.fragment.id).toBeDefined(); const hashed_email = hash("user1@email.com"); - expect(res.body.fragment.ownerId).toBe(hashed_email); - expect(res.body.fragment.created).toBeDefined(); - expect(res.body.fragment.updated).toBeDefined(); - expect(res.body.fragment.type).toBe("text/plain"); - expect(res.body.fragment.size).toBe(18); + expect(body.fragment.ownerId).toBe(hashed_email); + expect(body.fragment.created).toBeDefined(); + expect(body.fragment.updated).toBeDefined(); + expect(body.fragment.type).toBe("text/plain"); + expect(body.fragment.size).toBe(18); const location = res.header['location']; const location_without_host = new URL(location).pathname; - const expected_location_without_host = `/v1/fragments/${res.body.fragment.id}`; + const expected_location_without_host = `/v1/fragments/${body.fragment.id}`; expect(location_without_host).toBe(expected_location_without_host); @@ -64,6 +68,22 @@ describe('POST /v1/fragments', () => { await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') .set('Content-Type', 'application/json').send('{"status": "testing"}').expect(201); + + let data = fs.readFileSync(path.join(__dirname, '../data', 'image.png')); + await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'image/png').send(data).expect(201); + + data = fs.readFileSync(path.join(__dirname, '../data', 'image.jpg')); + await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'image/jpeg').send(data).expect(201); + + data = fs.readFileSync(path.join(__dirname, '../data', 'image.webp')); + await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'image/webp').send(data).expect(201); + + data = fs.readFileSync(path.join(__dirname, '../data', 'image.gif')); + await request(app).post('/v1/fragments').auth('user1@email.com', 'password1') + .set('Content-Type', 'image/gif').send(data).expect(201); }); }); diff --git a/tests/unit/update.test.js b/tests/unit/update.test.js new file mode 100644 index 0000000..ecb7849 --- /dev/null +++ b/tests/unit/update.test.js @@ -0,0 +1,155 @@ +const request = require('supertest'); + +const app = require('../../src/app'); + +describe('PUT /v1/fragments', () => { + + test('unauthenticated requests are denied', async () => + await request(app).put('/v1/fragments/123') + .set('Content-Type', 'text/html') + .send("

This is a new fragment

") + .expect(401)); + + test('incorrect credentials are denied', async () => + await request(app).put('/v1/fragments/123') + .set('Content-Type', 'text/html') + .send("

This is a new fragment

") + .expect(401)); + + test('authenticated user cannot update fragment that does not exist', async () => + await request(app).put(`/v1/fragments/123`) + .auth('user1@email.com', 'password1') + .set('Content-Type', 'text/html') + .send("

This is a new fragment

") + .expect(404)); + + test('authenticated user can update fragment of that exists with the same supported type', async () => { + let res = await request(app).post('/v1/fragments') + .auth('user1@email.com', 'password1') + .set('Content-Type', 'text/plain') + .send("This is a fragment"); + let body = JSON.parse(res.text); + + res = await request(app).put(`/v1/fragments/${body.fragment.id}`) + .auth('user1@email.com', 'password1') + .set('Content-Type', 'text/plain') + .send("This is a new fragment").expect(200); + body = JSON.parse(res.text); + + let fragment_by_id = await request(app).get(`/v1/fragments/${body.fragment.id}`) + .auth('user1@email.com', 'password1'); + expect(fragment_by_id.text).toBe("This is a new fragment"); + + + res = await request(app).post('/v1/fragments') + .auth('user1@email.com', 'password1') + .set('Content-Type', 'text/html') + .send("

This is a fragment

"); + body = JSON.parse(res.text); + + res = await request(app).put(`/v1/fragments/${body.fragment.id}`) + .auth('user1@email.com', 'password1') + .set('Content-Type', 'text/html') + .send("

This is a new fragment

").expect(200); + body = JSON.parse(res.text); + + fragment_by_id = await request(app).get(`/v1/fragments/${body.fragment.id}`) + .auth('user1@email.com', 'password1'); + expect(fragment_by_id.text).toBe("

This is a new fragment

"); + + res = await request(app).post('/v1/fragments') + .auth('user1@email.com', 'password1') + .set('Content-Type', 'text/markdown') + .send("### This is a fragment"); + body = JSON.parse(res.text); + + res = await request(app).put(`/v1/fragments/${body.fragment.id}`) + .auth('user1@email.com', 'password1') + .set('Content-Type', 'text/markdown') + .send("### This is a new fragment").expect(200); + body = JSON.parse(res.text); + + fragment_by_id = await request(app).get(`/v1/fragments/${body.fragment.id}`) + .auth('user1@email.com', 'password1'); + expect(fragment_by_id.text).toBe("### This is a new fragment"); + + res = await request(app).post('/v1/fragments') + .auth('user1@email.com', 'password1') + .set('Content-Type', 'application/json') + .send('{"status": "testing"}'); + + res = await request(app).put(`/v1/fragments/${res.body.fragment.id}`) + .auth('user1@email.com', 'password1') + .set('Content-Type', 'application/json') + .send('{"status": "updating"}').expect(200); + body = JSON.parse(res.text); + + fragment_by_id = await request(app).get(`/v1/fragments/${res.body.fragment.id}`) + .auth('user1@email.com', 'password1'); + expect(fragment_by_id.text).toBe('{"status": "updating"}'); + + }); + + test('authenticated user cannot update fragment of that exists with the different type from that a fragment already has', async () => { + let res = await request(app).post('/v1/fragments') + .auth('user1@email.com', 'password1') + .set('Content-Type', 'text/plain') + .send("This is a fragment"); + let body = JSON.parse(res.text); + + res = await request(app).put(`/v1/fragments/${body.fragment.id}`) + .auth('user1@email.com', 'password1') + .set('Content-Type', 'text/html') + .send("

This is a new fragment

").expect(400); + + + res = await request(app).post('/v1/fragments') + .auth('user1@email.com', 'password1') + .set('Content-Type', 'text/html') + .send("

This is a fragment

"); + body = JSON.parse(res.text); + + res = await request(app).put(`/v1/fragments/${body.fragment.id}`) + .auth('user1@email.com', 'password1') + .set('Content-Type', 'text/plain') + .send("This is a new fragment").expect(400); + + + res = await request(app).post('/v1/fragments') + .auth('user1@email.com', 'password1') + .set('Content-Type', 'text/markdown') + .send("### This is a fragment"); + body = JSON.parse(res.text); + + res = await request(app).put(`/v1/fragments/${body.fragment.id}`) + .auth('user1@email.com', 'password1') + .set('Content-Type', 'application/json') + .send('{"status": "updating"}').expect(400); + + + res = await request(app).post('/v1/fragments') + .auth('user1@email.com', 'password1') + .set('Content-Type', 'application/json') + .send('{"status": "testing"}'); + + res = await request(app).put(`/v1/fragments/${res.body.fragment.id}`) + .auth('user1@email.com', 'password1') + .set('Content-Type', 'text/markdown') + .send("### This is a new fragment").expect(400); + + }); + + test('authenticated user cannot update fragment with unsupported type', async () => { + let res = await request(app).post('/v1/fragments') + .auth('user1@email.com', 'password1') + .set('Content-Type', 'text/plain') + .send("This is a fragment"); + let body = JSON.parse(res.text); + + res = await request(app).put(`/v1/fragments/${body.fragment.id}`) + .auth('user1@email.com', 'password1') + .set('Content-Type', 'invalid/type') + .send("This is a new fragment").expect(415) + }); + +});