Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions api/controllers/v1/organization/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ const ControllerService = require('../../../services/ControllerService');
const GrottoService = require('../../../services/GrottoService');
const { toOrganization } = require('../../../services/mapping/converters');
const { validateNameLength } = require('../../../utils/nameValidation');
const {
validatePostalCodeLength,
} = require('../../../utils/postalCodeValidation');

module.exports = async (req, res) => {
// Check params
Expand All @@ -23,6 +26,12 @@ module.exports = async (req, res) => {
dateInscription: new Date(),
};

// Validate postalCode length (checked on the trimmed value)
const postalCodeError = validatePostalCodeLength(cleanedData.postalCode);
if (postalCodeError) {
return res.badRequest(postalCodeError);
}

const nameData = {
author: req.token.id,
language: req.param('name').language,
Expand Down
10 changes: 10 additions & 0 deletions api/controllers/v1/organization/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ const NotificationService = require('../../../services/NotificationService');
const EnrichmentQueueService = require('../../../services/EnrichmentQueueService');
const { toOrganization } = require('../../../services/mapping/converters');
const { validateNameLength } = require('../../../utils/nameValidation');
const {
validatePostalCodeLength,
} = require('../../../utils/postalCodeValidation');

module.exports = async (req, res) => {
// Check if organization exists
Expand Down Expand Up @@ -36,6 +39,13 @@ module.exports = async (req, res) => {
coordinatesChanged = true;
}

// Validate postalCode length before any write (checked on the trimmed value,
// so a value that fits once trimmed is not rejected)
const postalCodeError = validatePostalCodeLength(cleanedData.postalCode);
if (postalCodeError) {
return res.badRequest(postalCodeError);
}

// Validate name length
const nameText = req.body.name?.text;
const nameError = validateNameLength(nameText);
Expand Down
21 changes: 21 additions & 0 deletions api/utils/postalCodeValidation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { validateStringLength } = require('./stringLengthValidation');

const POSTAL_CODE_MAX_LENGTH = 10; // TGrotto.postalCode maxLength

/**
* Validates that a postal code does not exceed the DB column limit.
* Returns an error message if invalid, or null if valid.
*
* @param {string} postalCode - The postal code to validate
* @returns {string|null} Error message or null
*/
const validatePostalCodeLength = (postalCode) => {
const error = validateStringLength(
'Postal code',
postalCode,
POSTAL_CODE_MAX_LENGTH
);
return error ? error.message : null;
};

module.exports = { POSTAL_CODE_MAX_LENGTH, validatePostalCodeLength };
9 changes: 6 additions & 3 deletions assets/swaggerV1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5931,7 +5931,8 @@ paths:
- name: postalCode
in: query
schema:
type: number
type: string
maxLength: 10
- name: region
in: query
schema:
Expand All @@ -5954,7 +5955,7 @@ paths:
items:
$ref: '#/components/schemas/Organization'
'400':
description: You must provide at least a name to create an organization.
description: You must provide at least a name to create an organization, or the postal code is too long.
'403':
description: You are not authorized to create an organization.

Expand Down Expand Up @@ -6040,6 +6041,8 @@ paths:
type: string
postalCode:
type: string
description: Postal code (max 10 characters)
maxLength: 10
region:
type: string
url:
Expand All @@ -6066,7 +6069,7 @@ paths:
schema:
$ref: '#/components/schemas/Organization'
400:
description: Validation error (name too long, language null or not found)
description: Validation error (name too long, postal code too long, language null or not found)
403:
description: You are not authorized to update an organization.
404:
Expand Down
38 changes: 38 additions & 0 deletions test/integration/2_utils/postalCodeValidation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const should = require('should');
const {
POSTAL_CODE_MAX_LENGTH,
validatePostalCodeLength,
} = require('../../../api/utils/postalCodeValidation');

describe('postalCodeValidation', () => {
describe('validatePostalCodeLength', () => {
it('should expose the TGrotto.postalCode maxLength', () => {
should(POSTAL_CODE_MAX_LENGTH).equal(10);
});

it('should return null for a short postal code', () => {
should(validatePostalCodeLength('84000')).be.null();
});

it('should return null for a postal code at exactly the limit', () => {
should(validatePostalCodeLength('1234567890')).be.null();
});

// Value reported in https://github.com/GrottoCenter/grottocenter-api/issues/1774
it('should return an error message for a too long postal code', () => {
const result = validatePostalCodeLength('4400 Flémalle');
should(result).be.a.String();
should(result).containEql('Postal code');
should(result).containEql('exceeds maximum length of 10');
should(result).containEql('got 13');
should(result).containEql('3 over limit');
});

it('should return null for null, undefined, empty string and non-string values', () => {
should(validatePostalCodeLength(null)).be.null();
should(validatePostalCodeLength(undefined)).be.null();
should(validatePostalCodeLength('')).be.null();
should(validatePostalCodeLength(84000)).be.null();
});
});
});
21 changes: 21 additions & 0 deletions test/integration/4_routes/Organization/create.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,26 @@ describe('Organization features', () => {
});
}).timeout(4000);
});

// https://github.com/GrottoCenter/grottocenter-api/issues/1774
describe('Invalid data', () => {
it('should return 400 when postalCode is too long', (done) => {
supertest(sails.hooks.http.app)
.post('/api/v1/organizations')
.send({
name: { text: 'Organisation Flémalle', language: 'fr' },
postalCode: '4400 Flémalle',
})
.set('Authorization', userToken)
.set('Content-type', 'application/json')
.set('Accept', 'application/json')
.expect(400)
.end((err, res) => {
if (err) return done(err);
should(res.body.message).containEql('Postal code');
return done();
});
});
});
});
});
45 changes: 45 additions & 0 deletions test/integration/4_routes/Organization/update.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,51 @@ describe('Organization features', () => {
.send({ name: { text: 'A'.repeat(500) } })
.expect(400, done);
});

// https://github.com/GrottoCenter/grottocenter-api/issues/1774
it('should return 400 when postalCode is too long', (done) => {
supertest(sails.hooks.http.app)
.put('/api/v1/organizations/1')
.set('Authorization', userToken)
.set('Content-type', 'application/json')
.set('Accept', 'application/json')
.send({ postalCode: '4400 Flémalle' })
.expect(400)
.end(async (err, res) => {
if (err) return done(err);
try {
should(res.body.message).containEql('Postal code');
// Nothing must have been persisted
const organization = await TGrotto.findOne({ id: 1 });
should(organization.postalCode).not.equal('4400 Flémalle');
return done();
} catch (testErr) {
return done(testErr);
}
});
});

it('should return 200 when postalCode is exactly 10 characters', (done) => {
supertest(sails.hooks.http.app)
.put('/api/v1/organizations/1')
.set('Authorization', userToken)
.set('Content-type', 'application/json')
.set('Accept', 'application/json')
.send({ postalCode: '1234567890' })
.expect(200)
.end(async (err, res) => {
if (err) return done(err);
try {
should(res.body.postalCode).equal('1234567890');

// Reset
await TGrotto.updateOne({ id: 1 }).set({ postalCode: '92130' });
return done();
} catch (testErr) {
return done(testErr);
}
});
});
});
});
});