From 073391997a9bdcfca523dbbc208a5f1a75147b20 Mon Sep 17 00:00:00 2001 From: Daniel <70107167+Programador-jr@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:59:54 -0500 Subject: [PATCH 1/9] ci: run tests and lint in GitHub Actions --- .github/workflows/ci.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2daa9db --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: API CI + +on: + pull_request: + branches: + - main + push: + branches: + - main + +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 From 6d80cd36e0cdf71afa38e9228bb1fb5555ae0f48 Mon Sep 17 00:00:00 2001 From: Daniel <70107167+Programador-jr@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:01:35 -0500 Subject: [PATCH 2/9] ci: allow workflow validation on CI branches --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2daa9db..7ae3665 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,8 @@ on: push: branches: - main + - "ci/**" + workflow_dispatch: permissions: contents: read From cd960051edd309fc99cac6839bdefd7be276d436 Mon Sep 17 00:00:00 2001 From: Daniel <70107167+Programador-jr@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:02:36 -0500 Subject: [PATCH 3/9] fix: classify oversized pagination limits correctly --- api/routes/exercises/shared.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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( From 06124abda778349574726faa98226e4a8cd4259b Mon Sep 17 00:00:00 2001 From: Daniel <70107167+Programador-jr@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:02:39 -0500 Subject: [PATCH 4/9] test: expect sanitized exercise list errors --- test/routes/exercises.list.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"); }); }); From 1c5feda76f4ceacea7efd0ab742f180f2955ea99 Mon Sep 17 00:00:00 2001 From: Daniel <70107167+Programador-jr@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:02:43 -0500 Subject: [PATCH 5/9] test: align invalid limit messages with API responses --- test/routes/exercises.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); From 2568114c5a17b07c2c2d748cf616b2bf060bda8e Mon Sep 17 00:00:00 2001 From: Daniel <70107167+Programador-jr@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:02:46 -0500 Subject: [PATCH 6/9] test: assert filter errors do not expose internals --- test/routes/exercises.filters.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"); }); }); From ad159b121d3fa38251a0eaba8ad65785ec0789b8 Mon Sep 17 00:00:00 2001 From: Daniel <70107167+Programador-jr@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:03:50 -0500 Subject: [PATCH 7/9] build: declare ESLint recommended config dependency --- package.json | 1 + 1 file changed, 1 insertion(+) 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", From a1a27f4370207a312c855c1de6ed5e3481eb6a78 Mon Sep 17 00:00:00 2001 From: Daniel <70107167+Programador-jr@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:03:55 -0500 Subject: [PATCH 8/9] build: lock ESLint recommended config dependency --- package-lock.json | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) 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", From 69e496cc9e1cca9f7912c563cda23a998c63d8dc Mon Sep 17 00:00:00 2001 From: Daniel <70107167+Programador-jr@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:04:29 -0500 Subject: [PATCH 9/9] lint: declare Node fetch global --- eslint.config.js | 1 + 1 file changed, 1 insertion(+) 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",