From 04f4cf4a0f1d0491fa8f8184b2f94a1876802977 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Wed, 10 Jun 2026 16:22:59 -0300 Subject: [PATCH 01/12] 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 e57a8e4d0a7cdb9c38d95a3210fe385833ce0b26 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Wed, 10 Jun 2026 20:24:55 -0300 Subject: [PATCH 02/12] 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 3daff9225e152fadfe4dafd279e381917f212b24 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Wed, 10 Jun 2026 20:33:07 -0300 Subject: [PATCH 03/12] 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 fa0d18a40d0f3ccfbe2045bf703777d6d73e04a9 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Thu, 11 Jun 2026 14:54:37 -0300 Subject: [PATCH 04/12] 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 2f833c221d7e53b6a434539c462baf65f81e7360 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Thu, 11 Jun 2026 15:11:53 -0300 Subject: [PATCH 05/12] 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 76aa667178c494dc3ec28e86cb10b5ee18ca376b Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Thu, 11 Jun 2026 15:48:06 -0300 Subject: [PATCH 06/12] 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 dc5fc6492b29bb587aaeafba7cac2c62d2a11d18 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Thu, 11 Jun 2026 15:48:39 -0300 Subject: [PATCH 07/12] 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 33ccacd77796e90d277f575dada2c11b94ebafa7 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Mon, 22 Jun 2026 15:27:16 -0300 Subject: [PATCH 08/12] ci: add Chrome installation for Puppeteer on Linux Signed-off-by: kauanAfonso --- .github/workflows/continuous-integration.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 8b30c1668cd5..5518dafad678 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -44,6 +44,9 @@ jobs: env: PYTHON: ${{env.pythonLocation}}/bin/python3 run: npm ci + - name: Install Chrome for Puppeteer (Linux only) + if: runner.os == 'Linux' + run: npx puppeteer browsers install chrome - name: Build run: node packages/build/bin/compile-package -b - name: Run package tests From 290435c0a2bd3c8800ddc2fc86d9ff842b84cb89 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Mon, 22 Jun 2026 15:30:04 -0300 Subject: [PATCH 09/12] 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 f730e2c624e86bc02ed468d78b257fc7facf9199 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Mon, 22 Jun 2026 15:33:15 -0300 Subject: [PATCH 10/12] 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 bb8932f33dd65e16a7ea32f16e78f1f19ace9de7 Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Mon, 22 Jun 2026 15:34:42 -0300 Subject: [PATCH 11/12] 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 354c4eb3979d9b47739dec18773f1c67b34226fe Mon Sep 17 00:00:00 2001 From: kauanAfonso Date: Mon, 22 Jun 2026 16:31:53 -0300 Subject: [PATCH 12/12] 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