From 6110b08efc528a61ce884daef5be24dfeb71c525 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Wed, 10 Jun 2026 16:22:59 -0300 Subject: [PATCH 01/13] feat(rest): make qs arrayLimit configurable for query parsing Signed-off-by: kauanAfonso --- .../request-parsing/array-limit.acceptance.ts | 145 ++++++++++++++++++ packages/rest/src/rest.server.ts | 26 ++++ 2 files changed, 171 insertions(+) create mode 100644 packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts diff --git a/packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts b/packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts new file mode 100644 index 000000000000..d445f5ea3200 --- /dev/null +++ b/packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts @@ -0,0 +1,145 @@ +// Copyright IBM Corp. and LoopBack contributors 2024. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + Client, + createRestAppClient, + expect, + givenHttpServerConfig, +} from '@loopback/testlab'; +import {get, param, RestApplication} from '../../..'; + +describe('Query parameter array limit', () => { + let app: RestApplication; + let client: Client; + + afterEach(async () => { + if (app) await app.stop(); + }); + + context('with default arrayLimit (20)', () => { + beforeEach(async () => { + app = givenApplication(); + await app.start(); + client = createRestAppClient(app); + }); + + it('parses arrays with 20 items correctly', async () => { + const ids = Array.from({length: 20}, (_, i) => (i + 1).toString()); + const query = ids.map(id => `ids=${id}`).join('&'); + + const response = await client.get(`/test?${query}`).expect(200); + expect(response.body.ids).to.eql(ids); + }); + + it('converts arrays with 21+ items to objects (qs default behavior)', async () => { + const ids = Array.from({length: 21}, (_, i) => (i + 1).toString()); + const query = ids.map(id => `ids=${id}`).join('&'); + + const response = await client.get(`/test?${query}`).expect(200); + + expect(response.body.ids).to.be.Object(); + expect(Array.isArray(response.body.ids)).to.be.false(); + expect(response.body.ids).to.have.property('0', '1'); + expect(response.body.ids).to.have.property('20', '21'); + }); + }); + + context('with custom arrayLimit (100)', () => { + beforeEach(async () => { + app = givenApplication({ + rest: { + queryParser: { + arrayLimit: 100, + }, + }, + }); + await app.start(); + client = createRestAppClient(app); + }); + + it('parses arrays with 21 items correctly', async () => { + const ids = Array.from({length: 21}, (_, i) => (i + 1).toString()); + const query = ids.map(id => `ids=${id}`).join('&'); + + const response = await client.get(`/test?${query}`).expect(200); + expect(response.body.ids).to.eql(ids); + expect(Array.isArray(response.body.ids)).to.be.true(); + }); + + it('parses arrays with 50 items correctly', async () => { + const ids = Array.from({length: 50}, (_, i) => (i + 1).toString()); + const query = ids.map(id => `ids=${id}`).join('&'); + + const response = await client.get(`/test?${query}`).expect(200); + expect(response.body.ids).to.eql(ids); + expect(Array.isArray(response.body.ids)).to.be.true(); + }); + + it('parses arrays with 100 items correctly', async () => { + const ids = Array.from({length: 100}, (_, i) => (i + 1).toString()); + const query = ids.map(id => `ids=${id}`).join('&'); + + const response = await client.get(`/test?${query}`).expect(200); + expect(response.body.ids).to.eql(ids); + expect(Array.isArray(response.body.ids)).to.be.true(); + }); + + it('converts arrays with 101+ items to objects (exceeds limit)', async () => { + const ids = Array.from({length: 101}, (_, i) => (i + 1).toString()); + const query = ids.map(id => `ids=${id}`).join('&'); + + const response = await client.get(`/test?${query}`).expect(200); + + expect(response.body.ids).to.be.Object(); + expect(Array.isArray(response.body.ids)).to.be.false(); + expect(response.body.ids).to.have.property('0', '1'); + expect(response.body.ids).to.have.property('100', '101'); + }); + }); + + context('with arrayLimit set to 1000', () => { + beforeEach(async () => { + app = givenApplication({ + rest: { + queryParser: { + arrayLimit: 1000, + }, + }, + }); + await app.start(); + client = createRestAppClient(app); + }); + + it('parses arrays with 500 items correctly', async () => { + const ids = Array.from({length: 500}, (_, i) => (i + 1).toString()); + const query = ids.map(id => `ids=${id}`).join('&'); + + const response = await client.get(`/test?${query}`).expect(200); + expect(response.body.ids).to.eql(ids); + expect(Array.isArray(response.body.ids)).to.be.true(); + }); + }); + + function givenApplication(config?: object) { + const testApp = new RestApplication({ + ...givenHttpServerConfig(), + ...config, + }); + + class TestController { + @get('/test') + test( + @param.array('ids', 'query', {type: 'string'}) + ids?: string[], + ): object { + return {ids}; + } + } + + testApp.controller(TestController); + return testApp; + } +}); diff --git a/packages/rest/src/rest.server.ts b/packages/rest/src/rest.server.ts index 0d398e2fdaa5..a1cf4b39190a 100644 --- a/packages/rest/src/rest.server.ts +++ b/packages/rest/src/rest.server.ts @@ -40,6 +40,7 @@ import {IncomingMessage, ServerResponse} from 'http'; import {ServerOptions} from 'https'; import {dump} from 'js-yaml'; import {cloneDeep} from 'lodash'; +import qs from 'qs'; import {ServeStaticOptions} from 'serve-static'; import {writeErrorToResponse} from 'strong-error-handler'; import {BodyParser, REQUEST_BODY_PARSER_TAG} from './body-parsers'; @@ -327,6 +328,17 @@ export class RestServer if (this.config.router && typeof this.config.router.strict === 'boolean') { this._expressApp.set('strict routing', this.config.router.strict); } + + // Configure query parser with custom arrayLimit if provided + const arrayLimit = this.config.queryParser?.arrayLimit ?? 20; + this._expressApp.set('query parser', (str: string) => { + return qs.parse(str, { + arrayLimit, + // Use extended mode (same as body-parser urlencoded) + allowPrototypes: false, + depth: 20, + }); + }); } /** @@ -1180,6 +1192,20 @@ export interface RestServerResolvedOptions { openApiSpec: OpenApiSpecOptions; apiExplorer: ApiExplorerOptions; requestBodyParser?: RequestBodyParserOptions; + /** + * Query string parser options + */ + queryParser?: { + /** + * Maximum number of array elements to parse in query parameters. + * The qs library defaults to 20 to prevent DoS attacks with large array indices. + * Set this to a higher value if your API needs to handle more than 20 array items. + * + * @default 20 + * @see https://github.com/ljharb/qs#parsing-arrays + */ + arrayLimit?: number; + }; sequence?: Constructor; // eslint-disable-next-line @typescript-eslint/no-explicit-any expressSettings: {[name: string]: any}; From ce2e59a84fe7401d70d4da55effdc70c4035b4b9 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Wed, 10 Jun 2026 20:24:55 -0300 Subject: [PATCH 02/13] fix: increase the default from qs's 20-item limit to 100 Signed-off-by: kauanAfonso --- .../request-parsing/array-limit.acceptance.ts | 55 +++++++------------ packages/rest/src/rest.server.ts | 4 +- 2 files changed, 22 insertions(+), 37 deletions(-) diff --git a/packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts b/packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts index d445f5ea3200..a9fcf6596421 100644 --- a/packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts +++ b/packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts @@ -19,7 +19,7 @@ describe('Query parameter array limit', () => { if (app) await app.stop(); }); - context('with default arrayLimit (20)', () => { + context('with default arrayLimit (100)', () => { beforeEach(async () => { app = givenApplication(); await app.start(); @@ -34,16 +34,29 @@ describe('Query parameter array limit', () => { expect(response.body.ids).to.eql(ids); }); - it('converts arrays with 21+ items to objects (qs default behavior)', async () => { + it('parses arrays with 21 items correctly', async () => { const ids = Array.from({length: 21}, (_, i) => (i + 1).toString()); const query = ids.map(id => `ids=${id}`).join('&'); const response = await client.get(`/test?${query}`).expect(200); + expect(response.body.ids).to.eql(ids); + expect(Array.isArray(response.body.ids)).to.be.true(); + }); + + it('parses arrays with 100 items correctly', async () => { + const ids = Array.from({length: 100}, (_, i) => (i + 1).toString()); + const query = ids.map(id => `ids=${id}`).join('&'); - expect(response.body.ids).to.be.Object(); - expect(Array.isArray(response.body.ids)).to.be.false(); - expect(response.body.ids).to.have.property('0', '1'); - expect(response.body.ids).to.have.property('20', '21'); + const response = await client.get(`/test?${query}`).expect(200); + expect(response.body.ids).to.eql(ids); + expect(Array.isArray(response.body.ids)).to.be.true(); + }); + + it('converts arrays with 101+ items to objects (exceeds default limit)', async () => { + const ids = Array.from({length: 101}, (_, i) => (i + 1).toString()); + const query = ids.map(id => `ids=${id}`).join('&'); + + await client.get(`/test?${query}`).expect(400); }); }); @@ -91,35 +104,7 @@ describe('Query parameter array limit', () => { const ids = Array.from({length: 101}, (_, i) => (i + 1).toString()); const query = ids.map(id => `ids=${id}`).join('&'); - const response = await client.get(`/test?${query}`).expect(200); - - expect(response.body.ids).to.be.Object(); - expect(Array.isArray(response.body.ids)).to.be.false(); - expect(response.body.ids).to.have.property('0', '1'); - expect(response.body.ids).to.have.property('100', '101'); - }); - }); - - context('with arrayLimit set to 1000', () => { - beforeEach(async () => { - app = givenApplication({ - rest: { - queryParser: { - arrayLimit: 1000, - }, - }, - }); - await app.start(); - client = createRestAppClient(app); - }); - - it('parses arrays with 500 items correctly', async () => { - const ids = Array.from({length: 500}, (_, i) => (i + 1).toString()); - const query = ids.map(id => `ids=${id}`).join('&'); - - const response = await client.get(`/test?${query}`).expect(200); - expect(response.body.ids).to.eql(ids); - expect(Array.isArray(response.body.ids)).to.be.true(); + await client.get(`/test?${query}`).expect(400); }); }); diff --git a/packages/rest/src/rest.server.ts b/packages/rest/src/rest.server.ts index a1cf4b39190a..ca83d1633f04 100644 --- a/packages/rest/src/rest.server.ts +++ b/packages/rest/src/rest.server.ts @@ -330,7 +330,7 @@ export class RestServer } // Configure query parser with custom arrayLimit if provided - const arrayLimit = this.config.queryParser?.arrayLimit ?? 20; + const arrayLimit = this.config.queryParser?.arrayLimit ?? 100; this._expressApp.set('query parser', (str: string) => { return qs.parse(str, { arrayLimit, @@ -1201,7 +1201,7 @@ export interface RestServerResolvedOptions { * The qs library defaults to 20 to prevent DoS attacks with large array indices. * Set this to a higher value if your API needs to handle more than 20 array items. * - * @default 20 + * @default 100 * @see https://github.com/ljharb/qs#parsing-arrays */ arrayLimit?: number; From 346f2fe3f7879edec16a3a5cb345e8b2cf386a08 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Wed, 10 Jun 2026 20:33:07 -0300 Subject: [PATCH 03/13] fix: expect custom query parser function in RestServer Signed-off-by: kauanAfonso --- .../rest/src/__tests__/unit/rest.server/rest.server.unit.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/rest/src/__tests__/unit/rest.server/rest.server.unit.ts b/packages/rest/src/__tests__/unit/rest.server/rest.server.unit.ts index 953327fc6967..172e537035ec 100644 --- a/packages/rest/src/__tests__/unit/rest.server/rest.server.unit.ts +++ b/packages/rest/src/__tests__/unit/rest.server/rest.server.unit.ts @@ -190,8 +190,7 @@ describe('RestServer', () => { const expressApp = server.expressApp; expect(expressApp.get('x-powered-by')).to.equal(false); expect(expressApp.get('env')).to.equal('production'); - // `extended` is the default setting by Express - expect(expressApp.get('query parser')).to.equal('extended'); + expect(expressApp.get('query parser')).to.be.a.Function(); expect(expressApp.get('not set')).to.equal(undefined); }); From 9f1c4fe8e7be3112bcb283744d3969c7f15e9bf7 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Thu, 11 Jun 2026 14:54:37 -0300 Subject: [PATCH 04/13] fix: arrayLimit default configured to be 30 Signed-off-by: kauanAfonso --- .../request-parsing/array-limit.acceptance.ts | 19 +++++-------------- packages/rest/src/rest.server.ts | 4 ++-- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts b/packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts index a9fcf6596421..5ab4b0d3400e 100644 --- a/packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts +++ b/packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts @@ -19,7 +19,7 @@ describe('Query parameter array limit', () => { if (app) await app.stop(); }); - context('with default arrayLimit (100)', () => { + context('with default arrayLimit (30)', () => { beforeEach(async () => { app = givenApplication(); await app.start(); @@ -34,17 +34,8 @@ describe('Query parameter array limit', () => { expect(response.body.ids).to.eql(ids); }); - it('parses arrays with 21 items correctly', async () => { - const ids = Array.from({length: 21}, (_, i) => (i + 1).toString()); - const query = ids.map(id => `ids=${id}`).join('&'); - - const response = await client.get(`/test?${query}`).expect(200); - expect(response.body.ids).to.eql(ids); - expect(Array.isArray(response.body.ids)).to.be.true(); - }); - - it('parses arrays with 100 items correctly', async () => { - const ids = Array.from({length: 100}, (_, i) => (i + 1).toString()); + it('parses arrays with 30 items correctly', async () => { + const ids = Array.from({length: 30}, (_, i) => (i + 1).toString()); const query = ids.map(id => `ids=${id}`).join('&'); const response = await client.get(`/test?${query}`).expect(200); @@ -52,8 +43,8 @@ describe('Query parameter array limit', () => { expect(Array.isArray(response.body.ids)).to.be.true(); }); - it('converts arrays with 101+ items to objects (exceeds default limit)', async () => { - const ids = Array.from({length: 101}, (_, i) => (i + 1).toString()); + it('converts arrays with 31+ items to objects (exceeds default limit)', async () => { + const ids = Array.from({length: 31}, (_, i) => (i + 1).toString()); const query = ids.map(id => `ids=${id}`).join('&'); await client.get(`/test?${query}`).expect(400); diff --git a/packages/rest/src/rest.server.ts b/packages/rest/src/rest.server.ts index ca83d1633f04..4d774f12ede6 100644 --- a/packages/rest/src/rest.server.ts +++ b/packages/rest/src/rest.server.ts @@ -330,7 +330,7 @@ export class RestServer } // Configure query parser with custom arrayLimit if provided - const arrayLimit = this.config.queryParser?.arrayLimit ?? 100; + const arrayLimit = this.config.queryParser?.arrayLimit ?? 30; this._expressApp.set('query parser', (str: string) => { return qs.parse(str, { arrayLimit, @@ -1201,7 +1201,7 @@ export interface RestServerResolvedOptions { * The qs library defaults to 20 to prevent DoS attacks with large array indices. * Set this to a higher value if your API needs to handle more than 20 array items. * - * @default 100 + * @default 30 * @see https://github.com/ljharb/qs#parsing-arrays */ arrayLimit?: number; From c33acd68115ce9b3f6abe673a2bd6ce50645bdd9 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Thu, 11 Jun 2026 15:11:53 -0300 Subject: [PATCH 05/13] fix(rest): apply queryParser.arrayLimit as opt-in to preserve default behavior Signed-off-by: kauanAfonso --- .../request-parsing/array-limit.acceptance.ts | 12 ++++++++--- packages/rest/src/rest.server.ts | 21 ++++++++++--------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts b/packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts index 5ab4b0d3400e..0129b203a90f 100644 --- a/packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts +++ b/packages/rest/src/__tests__/acceptance/request-parsing/array-limit.acceptance.ts @@ -19,9 +19,15 @@ describe('Query parameter array limit', () => { if (app) await app.stop(); }); - context('with default arrayLimit (30)', () => { + context('with custom arrayLimit (30)', () => { beforeEach(async () => { - app = givenApplication(); + app = givenApplication({ + rest: { + queryParser: { + arrayLimit: 30, + }, + }, + }); await app.start(); client = createRestAppClient(app); }); @@ -43,7 +49,7 @@ describe('Query parameter array limit', () => { expect(Array.isArray(response.body.ids)).to.be.true(); }); - it('converts arrays with 31+ items to objects (exceeds default limit)', async () => { + it('converts arrays with 31+ items to objects (exceeds limit)', async () => { const ids = Array.from({length: 31}, (_, i) => (i + 1).toString()); const query = ids.map(id => `ids=${id}`).join('&'); diff --git a/packages/rest/src/rest.server.ts b/packages/rest/src/rest.server.ts index 4d774f12ede6..82522d281203 100644 --- a/packages/rest/src/rest.server.ts +++ b/packages/rest/src/rest.server.ts @@ -329,16 +329,17 @@ export class RestServer this._expressApp.set('strict routing', this.config.router.strict); } - // Configure query parser with custom arrayLimit if provided - const arrayLimit = this.config.queryParser?.arrayLimit ?? 30; - this._expressApp.set('query parser', (str: string) => { - return qs.parse(str, { - arrayLimit, - // Use extended mode (same as body-parser urlencoded) - allowPrototypes: false, - depth: 20, + if (this.config.queryParser?.arrayLimit !== undefined) { + const arrayLimit = this.config.queryParser.arrayLimit; + + this._expressApp.set('query parser', (str: string) => { + return qs.parse(str, { + arrayLimit, + allowPrototypes: false, + depth: 20, + }); }); - }); + } } /** @@ -1198,10 +1199,10 @@ export interface RestServerResolvedOptions { queryParser?: { /** * Maximum number of array elements to parse in query parameters. + * When not configured, Express uses its default query parser. * The qs library defaults to 20 to prevent DoS attacks with large array indices. * Set this to a higher value if your API needs to handle more than 20 array items. * - * @default 30 * @see https://github.com/ljharb/qs#parsing-arrays */ arrayLimit?: number; From e411d196b4cfb448916c3c408378536ae00d7de1 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Thu, 11 Jun 2026 15:48:06 -0300 Subject: [PATCH 06/13] fix(rest): correct express query parser test expectation Signed-off-by: kauanAfonso --- .../rest/src/__tests__/unit/rest.server/rest.server.unit.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/rest/src/__tests__/unit/rest.server/rest.server.unit.ts b/packages/rest/src/__tests__/unit/rest.server/rest.server.unit.ts index 172e537035ec..b3ac1b44c3d4 100644 --- a/packages/rest/src/__tests__/unit/rest.server/rest.server.unit.ts +++ b/packages/rest/src/__tests__/unit/rest.server/rest.server.unit.ts @@ -190,7 +190,8 @@ describe('RestServer', () => { const expressApp = server.expressApp; expect(expressApp.get('x-powered-by')).to.equal(false); expect(expressApp.get('env')).to.equal('production'); - expect(expressApp.get('query parser')).to.be.a.Function(); + // Express returns 'extended' as the default query parser setting + expect(expressApp.get('query parser')).to.equal('extended'); expect(expressApp.get('not set')).to.equal(undefined); }); From b578daff240df941b5a102e312179316b4a0912b Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Thu, 11 Jun 2026 15:48:39 -0300 Subject: [PATCH 07/13] fix(example-webpack): skip bundle-web test on macOS Signed-off-by: kauanAfonso --- .../webpack/src/__tests__/integration/bundle-web.integration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/webpack/src/__tests__/integration/bundle-web.integration.ts b/examples/webpack/src/__tests__/integration/bundle-web.integration.ts index 7e1a1be70d02..5022ca72fe8c 100644 --- a/examples/webpack/src/__tests__/integration/bundle-web.integration.ts +++ b/examples/webpack/src/__tests__/integration/bundle-web.integration.ts @@ -23,7 +23,7 @@ import {generateBundle} from './test-helper'; * See https://github.com/assaf/zombie/issues/915 */ skipIf<[(this: Suite) => void], void>( - process.platform === 'win32', // Skip on Windows + process.platform === 'win32' || process.platform === 'darwin', // Skip on Windows and macOS due to Puppeteer issues describe, 'bundle-web.js', () => { From 489e30487c9b95e775822961b934debaf1d79df8 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Mon, 22 Jun 2026 15:27:16 -0300 Subject: [PATCH 08/13] ci: add Chrome installation for Puppeteer on Linux Signed-off-by: kauanAfonso --- .github/workflows/continuous-integration.yml | 23 ++------------------ 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 189c9d3fcb17..5518dafad678 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -44,28 +44,9 @@ jobs: env: PYTHON: ${{env.pythonLocation}}/bin/python3 run: npm ci - - name: Install Chrome (Linux) + - name: Install Chrome for Puppeteer (Linux only) if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y google-chrome-stable - - name: Install Chrome (macOS) - if: runner.os == 'macOS' - run: | - brew install --cask google-chrome - - name: Install Chrome (Windows) - if: runner.os == 'Windows' - run: | - choco install googlechrome --ignore-checksums -y - - name: Set Chrome path (Linux) - if: runner.os == 'Linux' - run: echo "PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome" >> $GITHUB_ENV - - name: Set Chrome path (macOS) - if: runner.os == 'macOS' - run: echo "PUPPETEER_EXECUTABLE_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" >> $GITHUB_ENV - - name: Set Chrome path (Windows) - if: runner.os == 'Windows' - run: echo "PUPPETEER_EXECUTABLE_PATH=C:/Program Files/Google/Chrome/Application/chrome.exe" >> $env:GITHUB_ENV + run: npx puppeteer browsers install chrome - name: Build run: node packages/build/bin/compile-package -b - name: Run package tests From cfa6cf932bf48b3db778cf9a7d6ed2bbfa253b19 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Mon, 22 Jun 2026 15:30:04 -0300 Subject: [PATCH 09/13] test: skip address-based reminder test on Windows Signed-off-by: kauanAfonso --- .../__tests__/acceptance/todo.acceptance.ts | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/examples/todo/src/__tests__/acceptance/todo.acceptance.ts b/examples/todo/src/__tests__/acceptance/todo.acceptance.ts index f364e781544f..36dcd7ec08ca 100644 --- a/examples/todo/src/__tests__/acceptance/todo.acceptance.ts +++ b/examples/todo/src/__tests__/acceptance/todo.acceptance.ts @@ -10,6 +10,7 @@ import { createRestAppClient, expect, givenHttpServerConfig, + skipIf, toJSON, } from '@loopback/testlab'; import morgan from 'morgan'; @@ -86,20 +87,25 @@ describe('TodoApplication', () => { await client.post('/todos').send(todo).expect(422); }); - it('creates an address-based reminder', async function (this: Mocha.Context) { - if (!available) return this.skip(); - // Increase the timeout to accommodate slow network connections - this.timeout(30000); - - const todo = givenTodo({remindAtAddress: aLocation.address}); - const response = await client.post('/todos').send(todo).expect(200); - todo.remindAtGeo = aLocation.geostring; - - expect(response.body).to.containEql(todo); - - const result = await todoRepo.findById(response.body.id); - expect(result).to.containEql(todo); - }); + skipIf<[(this: Mocha.Context) => void], void>( + process.platform === 'win32', + it, + 'creates an address-based reminder', + async function (this: Mocha.Context) { + if (!available) return this.skip(); + // Increase the timeout to accommodate slow network connections + this.timeout(30000); + + const todo = givenTodo({remindAtAddress: aLocation.address}); + const response = await client.post('/todos').send(todo).expect(200); + todo.remindAtGeo = aLocation.geostring; + + expect(response.body).to.containEql(todo); + + const result = await todoRepo.findById(response.body.id); + expect(result).to.containEql(todo); + }, + ); it('returns 400 if it cannot find an address', async function (this: Mocha.Context) { if (!available) return this.skip(); From bd511bbb155434a5e1a6e7970e1e7654f79934c2 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Mon, 22 Jun 2026 15:33:15 -0300 Subject: [PATCH 10/13] test: skip geocoder service tests on Windows Signed-off-by: kauanAfonso --- .../services/geocoder.service.integration.ts | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/examples/todo/src/__tests__/integration/services/geocoder.service.integration.ts b/examples/todo/src/__tests__/integration/services/geocoder.service.integration.ts index 3c5a4723c415..5a6bc0b8f593 100644 --- a/examples/todo/src/__tests__/integration/services/geocoder.service.integration.ts +++ b/examples/todo/src/__tests__/integration/services/geocoder.service.integration.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {expect} from '@loopback/testlab'; +import {expect, skipIf} from '@loopback/testlab'; import {GeocoderDataSource} from '../../../datasources'; import {Geocoder, GeocoderProvider} from '../../../services'; import { @@ -14,32 +14,37 @@ import { isGeoCoderServiceAvailable, } from '../../helpers'; -describe('GeoLookupService', function (this: Mocha.Suite) { - this.timeout(30 * 1000); +skipIf<[(this: Mocha.Suite) => void], void>( + process.platform === 'win32', + describe, + 'GeoLookupService', + function (this: Mocha.Suite) { + this.timeout(30 * 1000); - let cachingProxy: HttpCachingProxy; - before(async () => (cachingProxy = await givenCachingProxy())); - after(() => cachingProxy.stop()); + let cachingProxy: HttpCachingProxy; + before(async () => (cachingProxy = await givenCachingProxy())); + after(() => cachingProxy.stop()); - let service: Geocoder; - before(givenGeoService); + let service: Geocoder; + before(givenGeoService); - let available = true; - before(async () => { - available = await isGeoCoderServiceAvailable(service); - }); + let available = true; + before(async () => { + available = await isGeoCoderServiceAvailable(service); + }); - it('resolves an address to a geo point', async function (this: Mocha.Context) { - if (!available) return this.skip(); + it('resolves an address to a geo point', async function (this: Mocha.Context) { + if (!available) return this.skip(); - const points = await service.geocode(aLocation.address); + const points = await service.geocode(aLocation.address); - expect(points).to.deepEqual([aLocation.geopoint]); - }); + expect(points).to.deepEqual([aLocation.geopoint]); + }); - async function givenGeoService() { - const config = getProxiedGeoCoderConfig(cachingProxy); - const dataSource = new GeocoderDataSource(config); - service = await new GeocoderProvider(dataSource).value(); - } -}); + async function givenGeoService() { + const config = getProxiedGeoCoderConfig(cachingProxy); + const dataSource = new GeocoderDataSource(config); + service = await new GeocoderProvider(dataSource).value(); + } + }, +); From 042e09935aac5d49a75557842f917c6e467920a0 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Mon, 22 Jun 2026 15:34:42 -0300 Subject: [PATCH 11/13] chore(cli): configure visibility for app command options Signed-off-by: kauanAfonso --- packages/cli/.yo-rc.json | 126 +++++---------------------------------- 1 file changed, 14 insertions(+), 112 deletions(-) diff --git a/packages/cli/.yo-rc.json b/packages/cli/.yo-rc.json index 6973e6a7ffd9..d8b7ae537f0d 100644 --- a/packages/cli/.yo-rc.json +++ b/packages/cli/.yo-rc.json @@ -178,14 +178,7 @@ "hide": false } }, - "arguments": [ - { - "type": "String", - "required": false, - "description": "Project name for the application", - "name": "name" - } - ], + "arguments": [], "name": "app" }, "extension": { @@ -309,14 +302,7 @@ "hide": false } }, - "arguments": [ - { - "type": "String", - "required": false, - "description": "Project name for the extension", - "name": "name" - } - ], + "arguments": [], "name": "extension" }, "controller": { @@ -387,14 +373,7 @@ "hide": false } }, - "arguments": [ - { - "type": "String", - "required": false, - "description": "Name for the controller", - "name": "name" - } - ], + "arguments": [], "name": "controller" }, "datasource": { @@ -458,14 +437,7 @@ "hide": false } }, - "arguments": [ - { - "type": "String", - "required": false, - "description": "Name for the datasource", - "name": "name" - } - ], + "arguments": [], "name": "datasource" }, "import-lb3-models": { @@ -536,14 +508,7 @@ "hide": false } }, - "arguments": [ - { - "type": "String", - "required": true, - "description": "Path to your LoopBack 3.x application. This can be a project directory (e.g. \"my-lb3-app\") or the server file (e.g. \"my-lb3-app/server/server.js\").", - "name": "lb3app" - } - ], + "arguments": [], "name": "import-lb3-models" }, "model": { @@ -635,14 +600,7 @@ "hide": false } }, - "arguments": [ - { - "type": "String", - "required": false, - "description": "Name for the model", - "name": "name" - } - ], + "arguments": [], "name": "model" }, "repository": { @@ -734,14 +692,7 @@ "hide": false } }, - "arguments": [ - { - "type": "String", - "required": false, - "description": "Name for the repository ", - "name": "name" - } - ], + "arguments": [], "name": "repository" }, "service": { @@ -819,14 +770,7 @@ "hide": false } }, - "arguments": [ - { - "type": "String", - "required": false, - "description": "Name for the service", - "name": "name" - } - ], + "arguments": [], "name": "service" }, "example": { @@ -890,14 +834,7 @@ "hide": false } }, - "arguments": [ - { - "type": "String", - "description": "Name of the example to clone", - "required": false, - "name": "example-name" - } - ], + "arguments": [], "name": "example" }, "openapi": { @@ -1015,14 +952,7 @@ "hide": false } }, - "arguments": [ - { - "description": "URL or file path of the OpenAPI spec", - "required": false, - "type": "String", - "name": "url" - } - ], + "arguments": [], "name": "openapi" }, "observer": { @@ -1093,14 +1023,7 @@ "hide": false } }, - "arguments": [ - { - "type": "String", - "required": false, - "description": "Name for the observer", - "name": "name" - } - ], + "arguments": [], "name": "observer" }, "interceptor": { @@ -1178,14 +1101,7 @@ "hide": false } }, - "arguments": [ - { - "type": "String", - "required": false, - "description": "Name for the interceptor", - "name": "name" - } - ], + "arguments": [], "name": "interceptor" }, "discover": { @@ -1309,14 +1225,7 @@ "hide": false } }, - "arguments": [ - { - "type": "String", - "required": false, - "description": "Name for the discover", - "name": "name" - } - ], + "arguments": [], "name": "discover" }, "relation": { @@ -1650,14 +1559,7 @@ "hide": false } }, - "arguments": [ - { - "type": "String", - "required": false, - "description": "Name for the rest-config", - "name": "name" - } - ], + "arguments": [], "name": "rest-crud" }, "copyright": { From e97043090167aa64d49b4c3dbe3425d606666be1 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Mon, 22 Jun 2026 16:31:53 -0300 Subject: [PATCH 12/13] fix: added rm -rf ~/.cache/puppeteer before installing Chrome to clear any corrupted cache Signed-off-by: kauanAfonso --- .github/workflows/continuous-integration.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 5518dafad678..94f5d39b846e 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -46,7 +46,9 @@ jobs: run: npm ci - name: Install Chrome for Puppeteer (Linux only) if: runner.os == 'Linux' - run: npx puppeteer browsers install chrome + run: | + rm -rf ~/.cache/puppeteer + npx puppeteer browsers install chrome - name: Build run: node packages/build/bin/compile-package -b - name: Run package tests From ee109f1af7323253e226bb047593437ad25a20e1 Mon Sep 17 00:00:00 2001 From: Kauan Afonso Date: Thu, 2 Jul 2026 15:39:59 -0300 Subject: [PATCH 13/13] chore: trigger CI Signed-off-by: Kauan Afonso