diff --git a/api/apps/api/src/modules/geo-feature-tags/dto/update-geo-feature-tag.dto.ts b/api/apps/api/src/modules/geo-feature-tags/dto/update-geo-feature-tag.dto.ts index 0015d1d8d1..1534ca4ba1 100644 --- a/api/apps/api/src/modules/geo-feature-tags/dto/update-geo-feature-tag.dto.ts +++ b/api/apps/api/src/modules/geo-feature-tags/dto/update-geo-feature-tag.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, MaxLength, Validate } from 'class-validator'; +import { IsNotEmpty, IsString, MaxLength, Validate } from 'class-validator'; import { IsValidTagNameValidator } from '@marxan-api/modules/geo-feature-tags/validators/is-valid-tag-name.custom.validator'; +import { Transform } from 'class-transformer'; export const tagMaxlength = 30; @@ -9,7 +10,9 @@ export const tagMaxLengthErrorMessage = `A tag should not be longer than ${tagMa export class UpdateGeoFeatureTagDTO { @ApiProperty() @IsNotEmpty({ message: 'The Tag cannot not be empty' }) + @IsString() @Validate(IsValidTagNameValidator) + @Transform((value: string): string => value.trim()) @MaxLength(tagMaxlength, { message: tagMaxLengthErrorMessage, }) diff --git a/api/apps/api/src/modules/projects/dto/update-project-tag.dto.ts b/api/apps/api/src/modules/projects/dto/update-project-tag.dto.ts index b5792bacc0..e5d49bcd78 100644 --- a/api/apps/api/src/modules/projects/dto/update-project-tag.dto.ts +++ b/api/apps/api/src/modules/projects/dto/update-project-tag.dto.ts @@ -1,10 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, MaxLength, Validate } from 'class-validator'; +import { IsNotEmpty, IsString, MaxLength, Validate } from 'class-validator'; import { IsValidTagNameValidator } from '@marxan-api/modules/geo-feature-tags/validators/is-valid-tag-name.custom.validator'; import { tagMaxlength, tagMaxLengthErrorMessage, } from '@marxan-api/modules/geo-feature-tags/dto/update-geo-feature-tag.dto'; +import { Transform } from 'class-transformer'; export class UpdateProjectTagDTO { @ApiProperty() @@ -13,7 +14,9 @@ export class UpdateProjectTagDTO { @ApiProperty() @IsNotEmpty({ message: 'The Tag cannot not be empty' }) + @IsString() @Validate(IsValidTagNameValidator) + @Transform((value: string): string => value.trim()) @MaxLength(tagMaxlength, { message: tagMaxLengthErrorMessage, }) diff --git a/api/apps/api/src/modules/projects/dto/upload-shapefile.dto.ts b/api/apps/api/src/modules/projects/dto/upload-shapefile.dto.ts index f631111765..f3b91a5d8c 100644 --- a/api/apps/api/src/modules/projects/dto/upload-shapefile.dto.ts +++ b/api/apps/api/src/modules/projects/dto/upload-shapefile.dto.ts @@ -5,6 +5,7 @@ import { tagMaxLengthErrorMessage, } from '@marxan-api/modules/geo-feature-tags/dto/update-geo-feature-tag.dto'; import { IsValidTagNameValidator } from '@marxan-api/modules/geo-feature-tags/validators/is-valid-tag-name.custom.validator'; +import { Transform } from 'class-transformer'; export class UploadShapefileDTO { @ApiProperty() @@ -18,7 +19,9 @@ export class UploadShapefileDTO { @ApiPropertyOptional() @IsOptional() + @IsString() @Validate(IsValidTagNameValidator) + @Transform((value: string): string => value.trim()) @MaxLength(tagMaxlength, { message: tagMaxLengthErrorMessage, }) diff --git a/api/apps/api/test/geo-features/geo-feature-tags.e2e-spec.ts b/api/apps/api/test/geo-features/geo-feature-tags.e2e-spec.ts index cdc0fec9d9..ba3dcc2826 100644 --- a/api/apps/api/test/geo-features/geo-feature-tags.e2e-spec.ts +++ b/api/apps/api/test/geo-features/geo-feature-tags.e2e-spec.ts @@ -250,6 +250,30 @@ describe('GeoFeatureTag PATCH (e2e)', () => { await fixtures.ThenFeatureHasTag(projectId, featureId, newTag); }); + test('should update the tag of the geo feature with trimmed down leading and trailing white spaces', async () => { + // ARRANGE + const projectId = await fixtures.GivenProject('someProject'); + const featureId = await fixtures.GivenFeatureOnProject( + projectId, + 'someFeature', + ); + await fixtures.GivenTagOnFeature(projectId, featureId, 'oldTag'); + const newPaddedTag = ' padded TAG '; + + // ACT + const response = await fixtures.WhenPatchingAGeoFeatureTag( + projectId, + featureId, + newPaddedTag, + ); + + // ASSERT + expect(response.status).toBe(HttpStatus.OK); + expect(response.body.data.type).toBe('geo_features'); + expect(response.body.data.attributes.featureClassName).toBe('someFeature'); + await fixtures.ThenFeatureHasTag(projectId, featureId, newPaddedTag.trim()); + }); + test('should tag the feature properly, even if the feature does not previously have a tag', async () => { // ARRANGE const projectId = await fixtures.GivenProject('someProject'); diff --git a/api/apps/api/test/projects/project-feature-tags/project-tags.e2e-spec.ts b/api/apps/api/test/projects/project-feature-tags/project-tags.e2e-spec.ts index 0d2dd7260b..a535092d31 100644 --- a/api/apps/api/test/projects/project-feature-tags/project-tags.e2e-spec.ts +++ b/api/apps/api/test/projects/project-feature-tags/project-tags.e2e-spec.ts @@ -242,6 +242,29 @@ describe('Projects Tag PATCH (e2e)', () => { await fixtures.ThenFeatureHasTag(projectId1, featureId11, 'updatedTAG'); await fixtures.ThenFeatureHasTag(projectId1, featureId14, 'updatedTAG'); }); + + test('should update all feature tag rows that match exactly with the tag to be updated trimmed down of leading and trailing white spaces', async () => { + // ARRANGE + const projectId = await fixtures.GivenProject('someProject'); + const featureId1 = await fixtures.GivenFeatureOnProject(projectId, 'f1'); + const featureId2 = await fixtures.GivenFeatureOnProject(projectId, 'f2'); + + await fixtures.GivenTagOnFeature(projectId, featureId1, 'toBeUpdated'); + await fixtures.GivenTagOnFeature(projectId, featureId2, 'toBeUpdated'); + const paddedTag = ' paddedTAG '; + + //ACT + const response = await fixtures.WhenPatchingAProjectTag( + projectId, + 'toBeUpdated', + paddedTag, + ); + + //ASSERT + expect(response.status).toBe(HttpStatus.OK); + await fixtures.ThenFeatureHasTag(projectId, featureId1, paddedTag.trim()); + await fixtures.ThenFeatureHasTag(projectId, featureId2, paddedTag.trim()); + }); }); describe('Projects Tag DELETE (e2e)', () => { diff --git a/api/apps/api/test/upload-feature/upload-feature.e2e-spec.ts b/api/apps/api/test/upload-feature/upload-feature.e2e-spec.ts index d76f006e54..d07d51519f 100644 --- a/api/apps/api/test/upload-feature/upload-feature.e2e-spec.ts +++ b/api/apps/api/test/upload-feature/upload-feature.e2e-spec.ts @@ -71,6 +71,30 @@ test(`if tagging info is included in DTO and valid, created feature should be ta await fixtures.ThenGeoFeatureTagIsCreated(name, tag); }); +test(`if tagging info is included in, the feature's tag should be trimmed down of white spaces `, async () => { + // ARRANGE + const name = 'someFeature'; + const description = 'someDescrip'; + const paddedTag = ' paddedTag '; + await fixtures.GivenProjectPusWithGeometryForProject(); + + // ACT + const result = await fixtures.WhenUploadingCustomFeature( + name, + description, + paddedTag, + ); + + // ASSERT + await fixtures.ThenGeoFeaturesAreCreated( + result, + name, + description, + paddedTag.trim(), + ); + await fixtures.ThenGeoFeatureTagIsCreated(name, paddedTag.trim()); +}); + test(`if there is already an existing feature with a tag that has equivalent capitalization to the one included in the custom feature DTO, the existing one will be used for the new feature`, async () => { // ARRANGE const equivalentTag = 'some-Tag';