Skip to content
This repository was archived by the owner on Jan 23, 2024. It is now read-only.

Commit d52efd5

Browse files
Merge pull request #238 from Pocket/katerina/OSL-519
2 parents a277bb5 + be0f6ba commit d52efd5

File tree

7 files changed

+339
-34
lines changed

7 files changed

+339
-34
lines changed

src/express.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { getPublicContext, IPublicContext } from './public/context';
1111
import { getAdminContext, IAdminContext } from './admin/context';
1212
import { startAdminServer } from './admin/server';
1313
import deleteUserDataRouter from './public/routes/deleteUserData';
14+
import deleteShareableListItemsRouter from './public/routes/deleteShareableListItems';
1415

1516
/**
1617
* Initialize an express server.
@@ -41,6 +42,8 @@ export async function startServer(port: number): Promise<{
4142
app.use(express.json());
4243
// Add route to delete user data
4344
app.use('/deleteUserData', deleteUserDataRouter);
45+
// Add route to delete shareable list items for user
46+
app.use('/deleteShareableListItems', deleteShareableListItemsRouter);
4447

4548
// expose a health check url
4649
app.get('/.well-known/apollo/server-health', (req, res) => {
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { expect } from 'chai';
2+
import sinon from 'sinon';
3+
import request from 'supertest';
4+
import { ApolloServer } from '@apollo/server';
5+
import * as Sentry from '@sentry/node';
6+
import { PrismaClient } from '@prisma/client';
7+
import { startServer } from '../../express';
8+
import { IPublicContext } from '../context';
9+
import { client } from '../../database/client';
10+
import {
11+
clearDb,
12+
createShareableListHelper,
13+
createShareableListItemHelper,
14+
mockRedisServer,
15+
} from '../../test/helpers';
16+
import { getShareableListItemUrlsForUser } from './';
17+
import { batchDeleteAllListItemsForUser } from './deleteShareableListItems';
18+
19+
describe('/deleteShareableListItems express endpoint', () => {
20+
let app: Express.Application;
21+
let server: ApolloServer<IPublicContext>;
22+
let graphQLUrl: string;
23+
let db: PrismaClient;
24+
let sentryStub;
25+
let list1;
26+
let list2;
27+
let list3;
28+
let listItem1;
29+
let listItem2;
30+
const endpoint = 'deleteShareableListItems';
31+
32+
const headers = {
33+
userId: '8009882300',
34+
};
35+
36+
const headers2 = {
37+
userId: '76543',
38+
};
39+
40+
beforeAll(async () => {
41+
mockRedisServer();
42+
// port 0 tells express to dynamically assign an available port
43+
({
44+
app,
45+
publicServer: server,
46+
publicUrl: graphQLUrl,
47+
} = await startServer(0));
48+
db = client();
49+
});
50+
51+
afterAll(async () => {
52+
await db.$disconnect();
53+
await server.stop();
54+
});
55+
56+
afterEach(() => {
57+
sentryStub.restore();
58+
});
59+
60+
beforeEach(async () => {
61+
sentryStub = sinon.stub(Sentry, 'captureException').resolves();
62+
await clearDb(db);
63+
64+
// Create a few Lists
65+
list1 = await createShareableListHelper(db, {
66+
userId: parseInt(headers.userId),
67+
title: 'Simon Le Bon List',
68+
});
69+
70+
list2 = await createShareableListHelper(db, {
71+
userId: parseInt(headers.userId),
72+
title: 'Bon Voyage List',
73+
});
74+
75+
// Create a List for user 2
76+
list3 = await createShareableListHelper(db, {
77+
userId: parseInt(headers2.userId),
78+
title: 'Rolling Stones List',
79+
});
80+
81+
// then create ist items for each list
82+
// list 1
83+
listItem1 = await createShareableListItemHelper(db, {
84+
list: list1,
85+
url: 'https://divine-rose.com',
86+
});
87+
listItem2 = await createShareableListItemHelper(db, {
88+
list: list1,
89+
url: 'https://hangover-hotel.com',
90+
});
91+
// list 2
92+
await createShareableListItemHelper(db, {
93+
list: list2,
94+
url: 'https://floral-street.com',
95+
});
96+
await createShareableListItemHelper(db, {
97+
list: list2,
98+
url: 'https://hangover-hotel.com',
99+
});
100+
// list 3
101+
await createShareableListItemHelper(db, {
102+
list: list3,
103+
url: 'https://hangover-hotel.com',
104+
});
105+
});
106+
107+
describe('should delete all shareable list items by url for userId', () => {
108+
it('should not delete any list items for userId with no data and should not return error', async () => {
109+
const result = await request(app)
110+
.post(graphQLUrl + endpoint)
111+
.set('Content-Type', 'application/json')
112+
.send({ userId: '12345', url: 'https://fake-url.com' });
113+
expect(result.body.status).to.equal('OK');
114+
expect(result.body.message).to.contain(
115+
`No shareable list items found for User ID: 12345`
116+
);
117+
});
118+
119+
it('should fail deleteShareableListItemData schema validation if bad userId', async () => {
120+
const result = await request(app)
121+
.post(graphQLUrl + endpoint)
122+
.set('Content-Type', 'application/json')
123+
.send({ userId: 'abc-12345', url: 'https://fake-url.com' });
124+
expect(result.body.errors.length).to.equal(1);
125+
expect(result.body.errors[0].msg).to.equal('Must provide valid userId');
126+
});
127+
128+
it('should fail deleteShareableListItemData schema validation if bad url', async () => {
129+
const result = await request(app)
130+
.post(graphQLUrl + endpoint)
131+
.set('Content-Type', 'application/json')
132+
.send({ userId: '12345', url: 34678 });
133+
expect(result.body.errors.length).to.equal(1);
134+
expect(result.body.errors[0].msg).to.equal('Must provide valid url');
135+
});
136+
137+
it('should successfully deleteShareableListItems by url for a userId', async () => {
138+
// delete items for userId 8009882300
139+
// this user has 2 lists and each list contains 1 listItem with url https://hangover-hotel.com
140+
const result = await request(app)
141+
.post(graphQLUrl + endpoint)
142+
.set('Content-Type', 'application/json')
143+
.send({ userId: headers.userId, url: 'https://hangover-hotel.com' });
144+
expect(result.body.status).to.equal('OK');
145+
expect(result.body.message).to.contain(
146+
`Deleting shareable list items for User ID: ${headers.userId}`
147+
);
148+
// lets manually call getShareableListItemUrlsForUser to check there are no list items with https://hangover-hotel.com url for this user
149+
const urls = await getShareableListItemUrlsForUser(
150+
parseInt(headers.userId),
151+
'https://hangover-hotel.com',
152+
db
153+
);
154+
expect(urls.length).to.equal(0);
155+
});
156+
});
157+
158+
describe('batchDeleteAllListItemsForUser', () => {
159+
it('should return count=0 for deleting 0 list items for user with no list items by url', async () => {
160+
// userId 76543 has one list (list3) with 1 item hangover-hotel.com
161+
// try to delete item divine-rose.com for this user
162+
const count = await batchDeleteAllListItemsForUser(
163+
parseInt(headers2.userId),
164+
listItem1.url
165+
);
166+
// there should be no list items with this url found for list 3
167+
expect(count).to.equal(0);
168+
});
169+
it('should return correct count of deleted list items for user with list items matching url', async () => {
170+
// userId 8009882300 has two lists (list1, list2) and 2 list items per list
171+
// delete list items by hangover-hotel.com (1 list item in each list with this url)
172+
const count = await batchDeleteAllListItemsForUser(
173+
parseInt(headers.userId),
174+
listItem2.url
175+
);
176+
// there should be 2 total list items deleted
177+
expect(count).to.equal(2);
178+
});
179+
});
180+
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { Request, Response } from 'express';
2+
import { Router } from 'express';
3+
import { checkSchema, Schema } from 'express-validator';
4+
import * as Sentry from '@sentry/node';
5+
import { client } from '../../database/client';
6+
import { validate } from './';
7+
8+
const router = Router();
9+
const db = client();
10+
11+
const deleteShareableListItemDataSchema: Schema = {
12+
userId: {
13+
in: ['body'],
14+
errorMessage: 'Must provide valid userId',
15+
isInt: true,
16+
toInt: true,
17+
},
18+
url: {
19+
in: ['body'],
20+
errorMessage: 'Must provide valid url',
21+
isString: true,
22+
notEmpty: true,
23+
},
24+
};
25+
26+
/**
27+
* This method batch deletes shareable list items by url for a user
28+
* @param userId
29+
* @param url
30+
*/
31+
export async function batchDeleteAllListItemsForUser(
32+
userId: number | bigint,
33+
url: string
34+
): Promise<number> {
35+
const batchResult = await db.listItem.deleteMany({
36+
where: { url, list: { userId } },
37+
});
38+
39+
return batchResult.count;
40+
}
41+
42+
/**
43+
* This method deletes all shareable list item data for a user by url
44+
* @param userId
45+
* @param url
46+
*/
47+
async function deleteShareableListItemUserData(
48+
userId: number | bigint,
49+
url: string
50+
): Promise<string> {
51+
// Delete all list items if there are any for user
52+
try {
53+
const deletedListItemCount = await batchDeleteAllListItemsForUser(
54+
userId,
55+
url
56+
);
57+
if (deletedListItemCount === 0) {
58+
return `No shareable list items found for User ID: ${userId}`;
59+
}
60+
return `Deleting shareable list items for User ID: ${userId}`;
61+
} catch (error) {
62+
// some unexpected DB error, log to Sentry, but don't halt program
63+
Sentry.captureException('Failed to delete shareable list data: ', error);
64+
}
65+
}
66+
67+
router.post(
68+
'/',
69+
checkSchema(deleteShareableListItemDataSchema),
70+
validate,
71+
(req: Request, res: Response) => {
72+
const userId = req.body.userId;
73+
const url = req.body.url;
74+
75+
// Delete all shareable list items for userId by url from DB
76+
deleteShareableListItemUserData(userId, url)
77+
.then((result) => {
78+
return res.send({
79+
status: 'OK',
80+
message: result,
81+
});
82+
})
83+
.catch((e) => {
84+
// In the unlikely event that an error is thrown,
85+
// log to Sentry but don't halt program
86+
Sentry.captureException(e);
87+
return res.send({
88+
status: 'BAD_REQUEST',
89+
message: e,
90+
});
91+
});
92+
}
93+
);
94+
95+
export default router;

src/public/routes/deleteUserData.integration.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { IPublicContext } from '../context';
99
import { client } from '../../database/client';
1010
import {
1111
clearDb,
12-
createPilotUserHelper,
1312
createShareableListHelper,
1413
createShareableListItemHelper,
1514
mockRedisServer,
@@ -29,11 +28,14 @@ describe('/deleteUserData express endpoint', () => {
2928
let list2;
3029
let list3;
3130

32-
let pilotUser2;
3331
const headers = {
3432
userId: '8009882300',
3533
};
3634

35+
const headers2 = {
36+
userId: '76543',
37+
};
38+
3739
beforeAll(async () => {
3840
mockRedisServer();
3941
// port 0 tells express to dynamically assign an available port
@@ -58,15 +60,6 @@ describe('/deleteUserData express endpoint', () => {
5860
sentryStub = sinon.stub(Sentry, 'captureException').resolves();
5961
await clearDb(db);
6062

61-
// create pilot users
62-
await createPilotUserHelper(db, {
63-
userId: parseInt(headers.userId),
64-
});
65-
66-
pilotUser2 = await createPilotUserHelper(db, {
67-
userId: 76543,
68-
});
69-
7063
// Create a few Lists
7164
list1 = await createShareableListHelper(db, {
7265
userId: parseInt(headers.userId),
@@ -80,7 +73,7 @@ describe('/deleteUserData express endpoint', () => {
8073

8174
// Create a List for user 2
8275
list3 = await createShareableListHelper(db, {
83-
userId: pilotUser2.userId,
76+
userId: parseInt(headers2.userId),
8477
title: 'Rolling Stones List',
8578
});
8679

@@ -144,7 +137,7 @@ describe('/deleteUserData express endpoint', () => {
144137
expect(ids[1]).to.equal(parseInt(list2.id as unknown as string));
145138

146139
// get listIds for another user
147-
ids = await getAllShareableListIdsForUser(pilotUser2.userId);
140+
ids = await getAllShareableListIdsForUser(parseInt(headers2.userId));
148141
expect(ids.length).to.equal(1);
149142
// check the returned ids are what we expect
150143
expect(ids[0]).to.equal(parseInt(list3.id as unknown as string));

0 commit comments

Comments
 (0)