Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions src/controllers/locations.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const locationAssociations = {
include: [
{
model: models.Organization,
attributes: models.Organization.getPublicAttributes(),
include: [models.Phone],
},
models.Phone,
Expand Down
42 changes: 38 additions & 4 deletions src/controllers/organizations.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import models from '../models';
import { updateInstance, createInstance } from '../services/data-changes';
import { NotFoundError } from '../utils/errors';

const normalizeEmail = (email) => {
if (email == null) {
return email;
}

const trimmedEmail = email.trim();
return trimmedEmail || null;
};

export default {
find: async (req, res, next) => {
try {
Expand All @@ -12,7 +21,9 @@ export default {
const { searchString } = req.query;
const filterParameters = searchString ? { searchString: searchString.trim() } : {};

const organizations = await models.Organization.findMatching(filterParameters);
const organizations = await models.Organization.findMatching(filterParameters, 10, {
attributes: models.Organization.getPublicAttributes(),
});
res.send(organizations);
} catch (err) {
next(err);
Expand All @@ -26,14 +37,17 @@ export default {
const {
name,
description,
email: rawEmail,
url,
metadata,
} = req.body;
const email = normalizeEmail(rawEmail);

const modelCreateFunction = models.Organization.create.bind(models.Organization);
const createdOrganization = await createInstance(req.user, modelCreateFunction, {
name,
description,
email,
url,
}, { metadata });

Expand All @@ -54,12 +68,16 @@ export default {
throw new NotFoundError('Organization not found');
}

const editableFields = ['name', 'description', 'url'];
const { metadata, ...updateParams } = req.body;
const editableFields = ['name', 'description', 'email', 'url'];
const { metadata, email, ...updateParams } = req.body;
const normalizedUpdateParams = email === undefined ? updateParams : {
...updateParams,
email: normalizeEmail(email),
};
await updateInstance(
req.user,
organization,
updateParams,
normalizedUpdateParams,
{ fields: editableFields, metadata },
);

Expand Down Expand Up @@ -104,4 +122,20 @@ export default {
next(err);
}
},

get: async (req, res, next) => {
try {
await Joi.validate(req, organizationSchemas.get, { allowUnknown: true });

const { organizationId } = req.params;
const organization = await models.Organization.findByPk(organizationId);
if (!organization) {
throw new NotFoundError('Organization not found');
}

res.send(organization);
} catch (err) {
next(err);
}
},
};
9 changes: 9 additions & 0 deletions src/controllers/validation/organizations.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const updateMetadataSchema = Joi.object().keys({
source: Joi.string(),
lastUpdated: Joi.date().iso(),
});
const emailSchema = Joi.string().trim().email().allow('', null);

export default {
find: {
Expand All @@ -16,6 +17,7 @@ export default {
body: Joi.object().keys({
name: Joi.string().required(),
description: Joi.string(),
email: emailSchema,
url: Joi.string(),
metadata: updateMetadataSchema,
}).required(),
Expand All @@ -28,11 +30,18 @@ export default {
body: Joi.object().keys({
name: Joi.string(),
description: Joi.string().allow(''),
email: emailSchema,
url: Joi.string().allow(''),
metadata: updateMetadataSchema,
}).required(),
},

get: {
params: Joi.object().keys({
organizationId: Joi.string().guid().required(),
}).required(),
},

getLocations: {
params: Joi.object().keys({
organizationId: Joi.string().guid().required(),
Expand Down
5 changes: 4 additions & 1 deletion src/models/location.js
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,10 @@ module.exports = (sequelize, DataTypes, Op) => {
],
},
] : [
sequelize.models.Organization,
{
model: sequelize.models.Organization,
attributes: sequelize.models.Organization.getPublicAttributes(),
},
sequelize.models.EventRelatedInfo,
{
model: sequelize.models.Service,
Expand Down
8 changes: 6 additions & 2 deletions src/models/organization.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,20 @@ module.exports = (sequelize, DataTypes, Op) => {
Organization.hasMany(models.Phone, { foreignKey: 'organization_id' });
};

Organization.findMatching = (filterParameters, limit = 10) => {
Organization.findMatching = (filterParameters, limit = 10, queryOptions = {}) => {
const { searchString } = filterParameters;

const where = {};
if (searchString) {
where.name = { [Op.iLike]: `%${searchString}%` };
}

return Organization.findAll({ limit, where });
return Organization.findAll({ limit, where, ...queryOptions });
};

Organization.getPublicAttributes = () => ({
exclude: ['email'],
});

return Organization;
};
1 change: 1 addition & 0 deletions src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {

export default (app) => {
app.get('/organizations', organizations.find);
app.get('/organizations/:organizationId', getUser, organizations.get);
app.post('/organizations', getUser, dataEntryAuth, organizations.create);
app.patch('/organizations/:organizationId', getUser, dataEntryAuth, organizations.update);
app.get('/organizations/:organizationId/locations', organizations.getLocations);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Object {
"Organization": Object {
"Phones": Object {},
"description": "An organization meant for testing purposes.",
"email": null,
"name": "The Test Org",
"url": "www.streetlives.com",
},
Expand Down
16 changes: 16 additions & 0 deletions test/integration/create-organization.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ describe('create organization', () => {
expect(dbOrg).toHaveProperty('url', orgParams.url);
});

it('should trim and persist a valid email address', async () => {
await request(app)
.post('/organizations')
.send({ ...orgParams, name: 'Org With Email', email: ' intake@streetlives.org ' })
.expect(201);

const dbOrg = await models.Organization.findOne({ where: { name: 'Org With Email' } });
expect(dbOrg).toHaveProperty('email', 'intake@streetlives.org');
});

it('should reject an invalid email address', () =>
request(app)
.post('/organizations')
.send({ ...orgParams, name: 'Org With Invalid Email', email: 'not-an-email' })
.expect(400));

describe('when no custom metadata is specified', () => {
it('should create metadata with current time as the last action date', async () => {
const startTime = Date.now();
Expand Down
16 changes: 16 additions & 0 deletions test/integration/find-locations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ describe('find locations', () => {
{
name: 'The Test Org',
description: 'An organization meant for testing purposes.',
email: 'contact@streetlives.org',
Services: [
{
name: 'A specific offering',
Expand Down Expand Up @@ -209,6 +210,21 @@ describe('find locations', () => {
.expect(200)
.then(expectMatchNearbyLocations));

it('should not expose organization email in public location results', () =>
request(app)
.get('/locations')
.query({
latitude: originLatitude,
longitude: originLongitude,
radius,
})
.expect(200)
.then((res) => {
res.body.forEach((returnedLocation) => {
expect(returnedLocation.Organization).not.toHaveProperty('email');
});
}));

it('should not return locations marked "hidden from search" (meant for comments only)', () =>
request(app)
.get('/locations')
Expand Down
2 changes: 2 additions & 0 deletions test/integration/get-location-info.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('get location info', () => {
{
name: 'The Test Org',
description: 'An organization meant for testing purposes.',
email: 'contact@streetlives.org',
url: 'www.streetlives.com',
Services: [{
name: 'A specific offering',
Expand Down Expand Up @@ -86,6 +87,7 @@ describe('get location info', () => {
.get(`/locations/${location.id}`)
.expect(200)
.then((res) => {
expect(res.body.Organization).not.toHaveProperty('email');
const strippedFields = stripTimestampsAndIds(res.body);
expect(strippedFields).toMatchSnapshot();
}));
Expand Down
40 changes: 40 additions & 0 deletions test/integration/get-organizations.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @jest-environment node
*/

import request from 'supertest';
import app from '../../src/app';
import models from '../../src/models';

describe('get organizations', () => {
let organization;

beforeEach(async () => {
organization = await models.Organization.create({
name: 'Organization With Email',
description: 'An organization used to test public/private payloads.',
email: 'contact@streetlives.org',
url: 'www.streetlives.com',
});
});

afterEach(() => models.Organization.destroy({ where: {} }));

it('should not expose email in the public organizations list', async () => {
const res = await request(app)
.get('/organizations')
.expect(200);

const returnedOrganization = res.body.find(org => org.id === organization.id);
expect(returnedOrganization).toBeDefined();
expect(returnedOrganization).not.toHaveProperty('email');
});

it('should return email when fetching a specific organization', async () => {
const res = await request(app)
.get(`/organizations/${organization.id}`)
.expect(200);

expect(res.body).toHaveProperty('email', organization.email);
});
});
51 changes: 51 additions & 0 deletions test/integration/update-organization.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @jest-environment node
*/

import request from 'supertest';
import app from '../../src/app';
import models from '../../src/models';

describe('update organization', () => {
let organization;

beforeEach(async () => {
organization = await models.Organization.create({
name: 'Existing Org',
description: 'An organization for update tests.',
email: 'existing@streetlives.org',
url: 'www.streetlives.com',
});
});

afterEach(() => Promise.all([
models.Organization.destroy({ where: {} }),
models.Metadata.destroy({ where: {} }),
]));

it('should trim and persist a valid email address', async () => {
await request(app)
.patch(`/organizations/${organization.id}`)
.send({ email: ' updated@streetlives.org ' })
.expect(204);

const updatedOrganization = await models.Organization.findByPk(organization.id);
expect(updatedOrganization).toHaveProperty('email', 'updated@streetlives.org');
});

it('should clear email when updated with a blank string', async () => {
await request(app)
.patch(`/organizations/${organization.id}`)
.send({ email: ' ' })
.expect(204);

const updatedOrganization = await models.Organization.findByPk(organization.id);
expect(updatedOrganization).toHaveProperty('email', null);
});

it('should reject an invalid email address', () =>
request(app)
.patch(`/organizations/${organization.id}`)
.send({ email: 'invalid-email' })
.expect(400));
});