From b7bb17802bf7d4327449f53d5864295bff655c3f Mon Sep 17 00:00:00 2001 From: Roi Cohen <213414225+roicohen326@users.noreply.github.com> Date: Sun, 21 Jun 2026 17:12:19 +0300 Subject: [PATCH 1/4] feat: add keywords field to ingestion layer metadata (MAPCO-8284) --- openapi3.yaml | 4 + tests/integration/ingestion/ingestion.spec.ts | 116 ++++++++++++++++++ tests/mocks/mockFactory.ts | 6 +- .../ingestion/models/ingestionManager.spec.ts | 55 +++++++++ 4 files changed, 177 insertions(+), 4 deletions(-) diff --git a/openapi3.yaml b/openapi3.yaml index 4e68b60..e8e224e 100644 --- a/openapi3.yaml +++ b/openapi3.yaml @@ -459,6 +459,8 @@ components: $ref: ./Schema/core/layerMetadata.yaml#/components/schemas/Description classification: $ref: ./Schema/core/layerMetadata.yaml#/components/schemas/Classification + keywords: + $ref: ./Schema/core/layerMetadata.yaml#/components/schemas/Keywords IngestionNewLayerRequest: type: object required: @@ -487,6 +489,8 @@ components: properties: classification: $ref: ./Schema/core/layerMetadata.yaml#/components/schemas/Classification + keywords: + $ref: ./Schema/core/layerMetadata.yaml#/components/schemas/Keywords IngestionUpdateLayerRequest: type: object required: diff --git a/tests/integration/ingestion/ingestion.spec.ts b/tests/integration/ingestion/ingestion.spec.ts index f225481..8bfecba 100644 --- a/tests/integration/ingestion/ingestion.spec.ts +++ b/tests/integration/ingestion/ingestion.spec.ts @@ -148,6 +148,47 @@ describe('Ingestion', () => { expect(response.status).toBe(httpStatusCodes.OK); expect(response.body).toStrictEqual(expectedResponseBody); }); + + it('should return 200 status code when metadata contains keywords', async () => { + const layerRequest = createNewLayerRequest({ + inputFiles: validInputFiles.inputFiles, + metadata: { keywords: faker.lorem.words({ min: 1, max: 5 }) }, + }); + const newLayerName = getMapServingLayerName(layerRequest.metadata.productId, layerRequest.metadata.productType); + const findJobsParams = createFindJobsParams({ + resourceId: layerRequest.metadata.productId, + productType: layerRequest.metadata.productType, + }); + const newJobRequest = createNewJobRequest({ + ingestionNewLayer: layerRequest, + checksums: validInputFiles.checksums, + }); + nock(jobManagerURL).post('/jobs/find', matches(findJobsParams)).reply(httpStatusCodes.OK, []); + nock(jobManagerURL) + .post('/jobs', matches(JSON.parse(JSON.stringify(newJobRequest)))) + .reply(httpStatusCodes.OK, jobResponse); + nock(catalogServiceURL) + .post('/records/find', { + metadata: { + productId: layerRequest.metadata.productId, + productType: layerRequest.metadata.productType, + }, + }) + .reply(httpStatusCodes.OK, []); + nock(mapProxyApiServiceUrl) + .get(`/layer/${encodeURIComponent(newLayerName)}`) + .reply(httpStatusCodes.NOT_FOUND); + const expectedResponseBody: ResponseId = { + jobId: jobResponse.id, + taskId: jobResponse.taskIds[0]!, + }; + + const response = await requestSender.ingestNewLayer(layerRequest); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + expect(response.body).toStrictEqual(expectedResponseBody); + }); }); describe('Bad Path', () => { @@ -525,6 +566,24 @@ describe('Ingestion', () => { { metadata: { productSubType: false } } ), }, + { + testCase: 'keywords in metadata in req body is not a string', + badRequest: merge( + createNewLayerRequest({ + inputFiles: validInputFiles.inputFiles, + }), + { metadata: { keywords: false } } + ), + }, + { + testCase: 'keywords in metadata in req body is an array instead of a string', + badRequest: merge( + createNewLayerRequest({ + inputFiles: validInputFiles.inputFiles, + }), + { metadata: { keywords: ['aerial', 'satellite'] } } + ), + }, { testCase: 'ingestionResolution in req body is not set', badRequest: createNewLayerRequest({ @@ -847,6 +906,45 @@ describe('Ingestion', () => { expect(response.body).toStrictEqual(expectedResponseBody); }); + it('should return 200 status code with update request when metadata contains keywords', async () => { + const layerRequest = createUpdateLayerRequest({ + inputFiles: validInputFiles.inputFiles, + callbackUrls: undefined, + metadata: { keywords: faker.lorem.words({ min: 1, max: 5 }) }, + }); + const updatedLayer = createCatalogLayerResponse(); + const updatedLayerMetadata = updatedLayer.metadata; + const updateLayerName = getMapServingLayerName(updatedLayerMetadata.productId, updatedLayerMetadata.productType); + const findJobsParams = createFindJobsParams({ + resourceId: updatedLayerMetadata.productId, + productType: updatedLayerMetadata.productType, + }); + const updateJobRequest = createUpdateJobRequest({ + ingestionUpdateLayer: layerRequest, + rasterLayerMetadata: updatedLayerMetadata, + checksums: validInputFiles.checksums, + }); + + nock(jobManagerURL).post('/jobs/find', matches(findJobsParams)).reply(httpStatusCodes.OK, []); + nock(jobManagerURL) + .post('/jobs', matches(JSON.parse(JSON.stringify(updateJobRequest)))) + .reply(httpStatusCodes.OK, jobResponse); + nock(catalogServiceURL).post('/records/find', { id: updatedLayerMetadata.id }).reply(httpStatusCodes.OK, [updatedLayer]); + nock(mapProxyApiServiceUrl) + .get(`/layer/${encodeURIComponent(updateLayerName)}`) + .reply(httpStatusCodes.OK); + const expectedResponseBody: ResponseId = { + jobId: jobResponse.id, + taskId: jobResponse.taskIds[0]!, + }; + + const response = await requestSender.updateLayer(updatedLayerMetadata.id, layerRequest); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + expect(response.body).toStrictEqual(expectedResponseBody); + }); + it('should return 200 status code with swap update request when product shapefile is polygon', async () => { const layerRequest = createUpdateLayerRequest({ inputFiles: validInputFiles.inputFiles }); const catalogLayerResponse = createCatalogLayerResponse({ @@ -1091,6 +1189,24 @@ describe('Ingestion', () => { metadata: { classification: '00' }, }), }, + { + testCase: 'keywords in metadata in req body is not a string', + badRequest: merge( + createUpdateLayerRequest({ + inputFiles: validInputFiles.inputFiles, + }), + { metadata: { keywords: false } } + ), + }, + { + testCase: 'keywords in metadata in req body is an array instead of a string', + badRequest: merge( + createUpdateLayerRequest({ + inputFiles: validInputFiles.inputFiles, + }), + { metadata: { keywords: ['aerial', 'satellite'] } } + ), + }, { testCase: 'ingestionResolution in req body is not set', badRequest: createUpdateLayerRequest({ diff --git a/tests/mocks/mockFactory.ts b/tests/mocks/mockFactory.ts index b4e7738..b8cb372 100644 --- a/tests/mocks/mockFactory.ts +++ b/tests/mocks/mockFactory.ts @@ -509,7 +509,7 @@ export const createUpdateJobRequest = ( const { ingestionResolution, inputFiles, - metadata: { classification }, + metadata, callbackUrls, } = ingestionUpdateLayer; const { displayPath, id, productId, productType, productVersion, productName, tileOutputFormat } = rasterLayerMetadata; @@ -524,9 +524,7 @@ export const createUpdateJobRequest = ( status: OperationStatus.PENDING, parameters: { ingestionResolution, - metadata: { - classification, - }, + metadata, inputFiles: { gpkgFilesPath: inputFiles.gpkgFilesPath.map((gpkgFilePath) => relative(sourceMount, join(sourceMount, gpkgFilePath))), metadataShapefilePath: relative(sourceMount, join(sourceMount, inputFiles.metadataShapefilePath)), diff --git a/tests/unit/ingestion/models/ingestionManager.spec.ts b/tests/unit/ingestion/models/ingestionManager.spec.ts index b413703..d6f95da 100644 --- a/tests/unit/ingestion/models/ingestionManager.spec.ts +++ b/tests/unit/ingestion/models/ingestionManager.spec.ts @@ -193,6 +193,33 @@ describe('IngestionManager', () => { ); }); + it('should pass keywords through to the ingestion job params when keywords is set in metadata', async () => { + const keywords = faker.lorem.words({ min: 1, max: 5 }); + const layerRequest = generateNewLayerRequest(); + layerRequest.metadata = { ...layerRequest.metadata, keywords }; + const createJobResponse: ICreateJobResponse = { id: faker.string.uuid(), taskIds: [faker.string.uuid()] }; + mockValidateManager.validateGpkgsSources.mockResolvedValue(undefined); + mockValidateManager.validateShapefiles.mockResolvedValue(undefined); + mockInfoManager.getGpkgsInformation.mockResolvedValue(undefined); + productManager.read.mockResolvedValue(undefined); + mockGeoValidator.validate.mockResolvedValue(undefined); + existsMapproxySpy.mockResolvedValue(false); + existsCatalogSpy.mockResolvedValue(false); + findJobsSpy.mockResolvedValue([]); + calcualteChecksumSpy.mockResolvedValue(generateChecksum()); + createIngestionJobSpy.mockResolvedValue(createJobResponse); + + await ingestionManager.newLayer(layerRequest); + + expect(createIngestionJobSpy).toHaveBeenCalledWith( + expect.objectContaining({ + parameters: expect.objectContaining({ + metadata: expect.objectContaining({ keywords }), + }), + }) + ); + }); + it('should throw unsupported entity error when shapefile not found error', async () => { const layerRequest = generateNewLayerRequest(); const expectedErrorMessage = 'error message'; @@ -431,6 +458,34 @@ describe('IngestionManager', () => { expect(createIngestionJobSpy).toHaveBeenCalledWith(expect.objectContaining({ type: ingestionUpdateJobType })); }); + it('should pass keywords through to the ingestion job params when keywords is set in update layer metadata', async () => { + const keywords = faker.lorem.words({ min: 1, max: 5 }); + const layerRequest = generateUpdateLayerRequest(); + layerRequest.metadata = { ...layerRequest.metadata, keywords }; + const catalogLayerResponse = generateCatalogLayerResponse(); + const createJobResponse: ICreateJobResponse = { id: faker.string.uuid(), taskIds: [faker.string.uuid()] }; + findByIdSpy.mockResolvedValue([catalogLayerResponse]); + mockValidateManager.validateShapefiles.mockResolvedValue(undefined); + mockValidateManager.validateGpkgsSources.mockResolvedValue(undefined); + mockInfoManager.getGpkgsInformation.mockResolvedValue(undefined); + productManager.read.mockResolvedValue(undefined); + mockGeoValidator.validate.mockResolvedValue(undefined); + existsMapproxySpy.mockResolvedValue(true); + findJobsSpy.mockResolvedValue([]); + calcualteChecksumSpy.mockResolvedValue(generateChecksum()); + createIngestionJobSpy.mockResolvedValue(createJobResponse); + + await ingestionManager.updateLayer(catalogLayerResponse.metadata.id, layerRequest); + + expect(createIngestionJobSpy).toHaveBeenCalledWith( + expect.objectContaining({ + parameters: expect.objectContaining({ + metadata: expect.objectContaining({ keywords }), + }), + }) + ); + }); + it('should not throw any errors when the request is valid and create update swap job', async () => { const catalogLayerResponse = generateCatalogLayerResponse(); const layerRequest = { From ba634c679c499cb5e99bdd22de7a88aea2363364 Mon Sep 17 00:00:00 2001 From: Roi Cohen <213414225+roicohen326@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:36:01 +0300 Subject: [PATCH 2/4] fix: define keywords schema inline and remove redundant unit tests --- openapi3.yaml | 6 +- tests/mocks/mockFactory.ts | 7 +-- .../ingestion/models/ingestionManager.spec.ts | 55 ------------------- 3 files changed, 5 insertions(+), 63 deletions(-) diff --git a/openapi3.yaml b/openapi3.yaml index e8e224e..3b4d237 100644 --- a/openapi3.yaml +++ b/openapi3.yaml @@ -460,7 +460,8 @@ components: classification: $ref: ./Schema/core/layerMetadata.yaml#/components/schemas/Classification keywords: - $ref: ./Schema/core/layerMetadata.yaml#/components/schemas/Keywords + type: string + description: Layer's keywords IngestionNewLayerRequest: type: object required: @@ -490,7 +491,8 @@ components: classification: $ref: ./Schema/core/layerMetadata.yaml#/components/schemas/Classification keywords: - $ref: ./Schema/core/layerMetadata.yaml#/components/schemas/Keywords + type: string + description: Layer's keywords IngestionUpdateLayerRequest: type: object required: diff --git a/tests/mocks/mockFactory.ts b/tests/mocks/mockFactory.ts index b8cb372..861227a 100644 --- a/tests/mocks/mockFactory.ts +++ b/tests/mocks/mockFactory.ts @@ -506,12 +506,7 @@ export const createUpdateJobRequest = ( const sourceMount = configMock.get('storageExplorer.layerSourceDir'); const updateJobAction = isSwapUpdate ? swapUpdateJobType : updateJobType; - const { - ingestionResolution, - inputFiles, - metadata, - callbackUrls, - } = ingestionUpdateLayer; + const { ingestionResolution, inputFiles, metadata, callbackUrls } = ingestionUpdateLayer; const { displayPath, id, productId, productType, productVersion, productName, tileOutputFormat } = rasterLayerMetadata; return { diff --git a/tests/unit/ingestion/models/ingestionManager.spec.ts b/tests/unit/ingestion/models/ingestionManager.spec.ts index d6f95da..b413703 100644 --- a/tests/unit/ingestion/models/ingestionManager.spec.ts +++ b/tests/unit/ingestion/models/ingestionManager.spec.ts @@ -193,33 +193,6 @@ describe('IngestionManager', () => { ); }); - it('should pass keywords through to the ingestion job params when keywords is set in metadata', async () => { - const keywords = faker.lorem.words({ min: 1, max: 5 }); - const layerRequest = generateNewLayerRequest(); - layerRequest.metadata = { ...layerRequest.metadata, keywords }; - const createJobResponse: ICreateJobResponse = { id: faker.string.uuid(), taskIds: [faker.string.uuid()] }; - mockValidateManager.validateGpkgsSources.mockResolvedValue(undefined); - mockValidateManager.validateShapefiles.mockResolvedValue(undefined); - mockInfoManager.getGpkgsInformation.mockResolvedValue(undefined); - productManager.read.mockResolvedValue(undefined); - mockGeoValidator.validate.mockResolvedValue(undefined); - existsMapproxySpy.mockResolvedValue(false); - existsCatalogSpy.mockResolvedValue(false); - findJobsSpy.mockResolvedValue([]); - calcualteChecksumSpy.mockResolvedValue(generateChecksum()); - createIngestionJobSpy.mockResolvedValue(createJobResponse); - - await ingestionManager.newLayer(layerRequest); - - expect(createIngestionJobSpy).toHaveBeenCalledWith( - expect.objectContaining({ - parameters: expect.objectContaining({ - metadata: expect.objectContaining({ keywords }), - }), - }) - ); - }); - it('should throw unsupported entity error when shapefile not found error', async () => { const layerRequest = generateNewLayerRequest(); const expectedErrorMessage = 'error message'; @@ -458,34 +431,6 @@ describe('IngestionManager', () => { expect(createIngestionJobSpy).toHaveBeenCalledWith(expect.objectContaining({ type: ingestionUpdateJobType })); }); - it('should pass keywords through to the ingestion job params when keywords is set in update layer metadata', async () => { - const keywords = faker.lorem.words({ min: 1, max: 5 }); - const layerRequest = generateUpdateLayerRequest(); - layerRequest.metadata = { ...layerRequest.metadata, keywords }; - const catalogLayerResponse = generateCatalogLayerResponse(); - const createJobResponse: ICreateJobResponse = { id: faker.string.uuid(), taskIds: [faker.string.uuid()] }; - findByIdSpy.mockResolvedValue([catalogLayerResponse]); - mockValidateManager.validateShapefiles.mockResolvedValue(undefined); - mockValidateManager.validateGpkgsSources.mockResolvedValue(undefined); - mockInfoManager.getGpkgsInformation.mockResolvedValue(undefined); - productManager.read.mockResolvedValue(undefined); - mockGeoValidator.validate.mockResolvedValue(undefined); - existsMapproxySpy.mockResolvedValue(true); - findJobsSpy.mockResolvedValue([]); - calcualteChecksumSpy.mockResolvedValue(generateChecksum()); - createIngestionJobSpy.mockResolvedValue(createJobResponse); - - await ingestionManager.updateLayer(catalogLayerResponse.metadata.id, layerRequest); - - expect(createIngestionJobSpy).toHaveBeenCalledWith( - expect.objectContaining({ - parameters: expect.objectContaining({ - metadata: expect.objectContaining({ keywords }), - }), - }) - ); - }); - it('should not throw any errors when the request is valid and create update swap job', async () => { const catalogLayerResponse = generateCatalogLayerResponse(); const layerRequest = { From 610f54778b8f2a2db9cee14ab0c030a83dbb1b84 Mon Sep 17 00:00:00 2001 From: Roi Cohen <213414225+roicohen326@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:23:12 +0300 Subject: [PATCH 3/4] refactor: reference shared Keywords schema instead of inline definition Replace the temporary inline keywords definition (added while raster-shared lacked the field) with a $ref to the shared Keywords schema, matching every other metadata field. PENDING (do after raster-shared #208 publishes): - bump @map-colonies/raster-shared to the version that includes Keywords - re-run copySchema so Schema/core/layerMetadata.yaml gains Keywords Until then this branch will not build (the $ref target does not yet exist). --- openapi3.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openapi3.yaml b/openapi3.yaml index 3b4d237..e8e224e 100644 --- a/openapi3.yaml +++ b/openapi3.yaml @@ -460,8 +460,7 @@ components: classification: $ref: ./Schema/core/layerMetadata.yaml#/components/schemas/Classification keywords: - type: string - description: Layer's keywords + $ref: ./Schema/core/layerMetadata.yaml#/components/schemas/Keywords IngestionNewLayerRequest: type: object required: @@ -491,8 +490,7 @@ components: classification: $ref: ./Schema/core/layerMetadata.yaml#/components/schemas/Classification keywords: - type: string - description: Layer's keywords + $ref: ./Schema/core/layerMetadata.yaml#/components/schemas/Keywords IngestionUpdateLayerRequest: type: object required: From 70fd23833590910e7cf9750da52e4e1ca3adcf5d Mon Sep 17 00:00:00 2001 From: Roi Cohen <213414225+roicohen326@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:26:42 +0300 Subject: [PATCH 4/4] feat: concatenate keywords on update, overwrite on swap --- src/ingestion/models/ingestionManager.ts | 15 +++++ src/ingestion/schemas/layerCatalogSchema.ts | 1 + tests/mocks/mockFactory.ts | 1 + .../ingestion/models/ingestionManager.spec.ts | 62 +++++++++++++++++++ 4 files changed, 79 insertions(+) diff --git a/src/ingestion/models/ingestionManager.ts b/src/ingestion/models/ingestionManager.ts index 7f09592..6e3540f 100644 --- a/src/ingestion/models/ingestionManager.ts +++ b/src/ingestion/models/ingestionManager.ts @@ -637,8 +637,13 @@ export class IngestionManager { const relativeChecksums = this.convertChecksumsToRelativePaths(checksums); const taskParameters: IngestionValidationTaskParams = { checksums: relativeChecksums }; + const keywords = isSwapUpdate + ? updateLayer.metadata.keywords + : this.mergeKeywords(rasterLayerMetadata.keywords, updateLayer.metadata.keywords); + const updateLayerRelative = { ...updateLayer, + metadata: { ...updateLayer.metadata, keywords }, ...{ inputFiles: { metadataShapefilePath: updateLayer.inputFiles.metadataShapefilePath.relative, @@ -671,6 +676,16 @@ export class IngestionManager { return createJobRequest; } + private mergeKeywords(existing: string | undefined, incoming: string | undefined): string | undefined { + const toTokens = (value: string | undefined): string[] => + (value ?? '') + .split(',') + .map((keyword) => keyword.trim()) + .filter((keyword) => keyword.length > 0); + const merged = [...new Set([...toTokens(existing), ...toTokens(incoming)])]; + return merged.length > 0 ? merged.join(',') : undefined; + } + @withSpanAsyncV4 private async getChecksum(shapefilePath: string): Promise { const checksums = await Promise.all(getShapefileFiles(shapefilePath).map(async (fileName) => this.getFileChecksum(fileName))); diff --git a/src/ingestion/schemas/layerCatalogSchema.ts b/src/ingestion/schemas/layerCatalogSchema.ts index ebe9c2a..ad52a3c 100644 --- a/src/ingestion/schemas/layerCatalogSchema.ts +++ b/src/ingestion/schemas/layerCatalogSchema.ts @@ -91,6 +91,7 @@ export const rasterLayerCatalogSchema = z message: 'Product bounding box must be of the shape min_x,min_y,max_x,max_y', }) .optional(), + keywords: z.string().optional(), displayPath: z.string().uuid(), transparency: z.nativeEnum(Transparency), tileMimeFormat: tilesMimeFormatSchema, diff --git a/tests/mocks/mockFactory.ts b/tests/mocks/mockFactory.ts index 861227a..d09913c 100644 --- a/tests/mocks/mockFactory.ts +++ b/tests/mocks/mockFactory.ts @@ -272,6 +272,7 @@ export const rasterLayerMetadataGenerators: RasterLayerMetadataPropertiesGenerat description: (): string => generateHebrewAlphanumeric({ min: 0, max: 100 }), producerName: (): string => generateHebrewAlphanumeric({ min: 0, max: 100 }), productSubType: (): string => generateHebrewAlphanumeric({ min: 0, max: 100 }), + keywords: (): string => faker.lorem.words({ min: 1, max: 5 }).split(' ').join(','), scale: (): number => faker.number.int({ min: INGESTION_VALIDATIONS.scale.min, max: INGESTION_VALIDATIONS.scale.max }), srs: (): '4326' => '4326', srsName: (): 'WGS84GEO' => 'WGS84GEO', diff --git a/tests/unit/ingestion/models/ingestionManager.spec.ts b/tests/unit/ingestion/models/ingestionManager.spec.ts index b413703..8d0d7e3 100644 --- a/tests/unit/ingestion/models/ingestionManager.spec.ts +++ b/tests/unit/ingestion/models/ingestionManager.spec.ts @@ -460,6 +460,68 @@ describe('IngestionManager', () => { expect(createIngestionJobSpy).toHaveBeenCalledWith(expect.objectContaining({ type: ingestionSwapUpdateJobType })); }); + it('should concatenate new keywords onto the existing ones, deduplicated, on a regular update', async () => { + const catalogLayerResponse = generateCatalogLayerResponse(); + catalogLayerResponse.metadata.keywords = 'forest,urban'; + const layerRequest = generateUpdateLayerRequest(); + layerRequest.metadata.keywords = 'urban,coast'; + const createJobResponse: ICreateJobResponse = { id: faker.string.uuid(), taskIds: [faker.string.uuid()] }; + findByIdSpy.mockResolvedValue([catalogLayerResponse]); + mockValidateManager.validateShapefiles.mockResolvedValue(undefined); + mockValidateManager.validateGpkgsSources.mockResolvedValue(undefined); + mockInfoManager.getGpkgsInformation.mockResolvedValue(undefined); + productManager.read.mockResolvedValue(undefined); + mockGeoValidator.validate.mockResolvedValue(undefined); + existsMapproxySpy.mockResolvedValue(true); + findJobsSpy.mockResolvedValue([]); + calcualteChecksumSpy.mockResolvedValue(generateChecksum()); + createIngestionJobSpy.mockResolvedValue(createJobResponse); + + await ingestionManager.updateLayer(catalogLayerResponse.metadata.id, layerRequest); + + expect(createIngestionJobSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: ingestionUpdateJobType, + parameters: expect.objectContaining({ metadata: expect.objectContaining({ keywords: 'forest,urban,coast' }) }), + }) + ); + }); + + it('should overwrite keywords with the new value on a swap update', async () => { + const baseCatalogLayerResponse = generateCatalogLayerResponse(); + const catalogLayerResponse = { + ...baseCatalogLayerResponse, + metadata: { + ...baseCatalogLayerResponse.metadata, + productType: ingestionSwapUpdateProductType, + productSubType: ingestionSwapUpdateProductSubType, + keywords: 'forest,urban', + }, + }; + const layerRequest = generateUpdateLayerRequest(); + layerRequest.metadata.keywords = 'urban,coast'; + const createJobResponse: ICreateJobResponse = { id: faker.string.uuid(), taskIds: [faker.string.uuid()] }; + findByIdSpy.mockResolvedValue([catalogLayerResponse]); + mockValidateManager.validateShapefiles.mockResolvedValue(undefined); + mockValidateManager.validateGpkgsSources.mockResolvedValue(undefined); + mockInfoManager.getGpkgsInformation.mockResolvedValue(undefined); + productManager.read.mockResolvedValue(undefined); + mockGeoValidator.validate.mockResolvedValue(undefined); + existsMapproxySpy.mockResolvedValue(true); + findJobsSpy.mockResolvedValue([]); + calcualteChecksumSpy.mockResolvedValue(generateChecksum()); + createIngestionJobSpy.mockResolvedValue(createJobResponse); + + await ingestionManager.updateLayer(catalogLayerResponse.metadata.id, layerRequest); + + expect(createIngestionJobSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: ingestionSwapUpdateJobType, + parameters: expect.objectContaining({ metadata: expect.objectContaining({ keywords: 'urban,coast' }) }), + }) + ); + }); + it('should throw not found error when there is no layer in catalog', async () => { const layerRequest = generateUpdateLayerRequest(); const catalogLayerResponse = generateCatalogLayerResponse();