diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7ae3665 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: API CI + +on: + pull_request: + branches: + - main + push: + branches: + - main + - "ci/**" + workflow_dispatch: + +permissions: + contents: read + +jobs: + test-and-lint: + name: Tests and lint + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run Jest tests + run: npm test -- --runInBand + + - name: Run ESLint + run: npm run lint diff --git a/api/routes/exercises/shared.js b/api/routes/exercises/shared.js index 2b557d4..3d33a27 100644 --- a/api/routes/exercises/shared.js +++ b/api/routes/exercises/shared.js @@ -311,12 +311,12 @@ const parsePagination = (pageParam, limitParam, lang) => { : errorMessages.invalidPage[lang], ); } - if (!Number.isSafeInteger(limit) || limit < 1) { - return validationError(errorMessages.invalidLimit[lang]); - } if (limit > maxLimit) { return validationError(errorMessages.limitTooLarge[lang]); } + if (!Number.isSafeInteger(limit) || limit < 1) { + return validationError(errorMessages.invalidLimit[lang]); + } if (!Number.isSafeInteger(page * limit)) { return validationError(errorMessages.offsetTooLarge[lang].replace( diff --git a/eslint.config.js b/eslint.config.js index 16b7698..21f21b2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,6 +5,7 @@ const nodeGlobals = { __dirname: "readonly", Buffer: "readonly", console: "readonly", + fetch: "readonly", global: "readonly", module: "readonly", process: "readonly", diff --git a/package-lock.json b/package-lock.json index 009c03e..4ae352c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "musclelib-api", - "version": "2.1.0", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { @@ -23,6 +23,7 @@ "walk-up-path": "^4.0.0" }, "devDependencies": { + "@eslint/js": "^10.0.1", "eslint": "^10.4.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", @@ -671,6 +672,27 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, "node_modules/@eslint/object-schema": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", diff --git a/package.json b/package.json index e34ce4e..4d7fc24 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "node": ">=20 <23" }, "devDependencies": { + "@eslint/js": "^10.0.1", "eslint": "^10.4.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", diff --git a/test/routes/exercises.filters.test.js b/test/routes/exercises.filters.test.js index b9f1dcd..34799a5 100644 --- a/test/routes/exercises.filters.test.js +++ b/test/routes/exercises.filters.test.js @@ -139,6 +139,6 @@ describe("GET /api/exercises/filters", () => { const response = await request(app).get("/api/exercises/filters"); expect(response.status).toBe(500); expect(response.body.message).toBe("Error fetching exercises."); - expect(response.body.error).toBe("DB connection lost"); + expect(response.body).not.toHaveProperty("error"); }); }); diff --git a/test/routes/exercises.list.test.js b/test/routes/exercises.list.test.js index 94393e9..e4d8b76 100644 --- a/test/routes/exercises.list.test.js +++ b/test/routes/exercises.list.test.js @@ -436,7 +436,7 @@ describe("exercise list database filtering and pagination", () => { expect(response.status).toBe(500); expect(response.body).toEqual({ message: errorMessages.fetchError.es, - error: "Database unavailable", }); + expect(response.body).not.toHaveProperty("error"); }); }); diff --git a/test/routes/exercises.test.js b/test/routes/exercises.test.js index 74fd403..04210a3 100644 --- a/test/routes/exercises.test.js +++ b/test/routes/exercises.test.js @@ -103,11 +103,11 @@ describe("exercise routes", () => { it.each([ ["limit=1000000", "The 'limit' parameter must not exceed 100."], - ["limit=10abc", "parameter 'limit' is invalid. use a value greater than 0"], + ["limit=10abc", "parameter 'limit' is invalid. use a value greater than 0."], ["page=1abc", "parameter 'page' is invalid. use a value greater than or equal to 0."], ["page=-1", "parameter 'page' is invalid. use a value greater than or equal to 0."], ["page=9007199254740992", "The 'page' parameter must not exceed 9007199254740991."], - ["limit=1&limit=2", "parameter 'limit' is invalid. use a value greater than 0"], + ["limit=1&limit=2", "parameter 'limit' is invalid. use a value greater than 0."], ])("rejects invalid pagination input %s", async (query, message) => { const response = await request(app).get(`/api/exercises?${query}`); expect(response.status).toBe(400);