Skip to content

Commit

Permalink
Merge branch 'main' into feat/sort-lists-by-creation-or-name
Browse files Browse the repository at this point in the history
Signed-off-by: Kevin Szuchet <31735779+kevinszuchet@users.noreply.github.com>
  • Loading branch information
kevinszuchet committed May 9, 2023
2 parents 469b1aa + 00f9fb1 commit 6f5d98c
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 12 deletions.
48 changes: 48 additions & 0 deletions src/controllers/handlers/lists-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,51 @@ export async function createListHandler(
throw error
}
}

export async function deleteListHandler(
context: Pick<HandlerContextWithPath<'lists', '/v1/lists/:id'>, 'components' | 'params' | 'request' | 'verification'>
): Promise<HTTPResponse<undefined>> {
const {
components: { lists },
verification,
params
} = context
const userAddress: string | undefined = verification?.auth.toLowerCase()
const { id } = params

if (!userAddress) {
return {
status: StatusCode.UNAUTHORIZED,
body: {
ok: false,
message: 'Unauthorized'
}
}
}

try {
await lists.deleteList(id, userAddress)
return {
status: StatusCode.OK,
body: {
ok: true,
data: undefined
}
}
} catch (error) {
if (error instanceof ListNotFoundError) {
return {
status: StatusCode.NOT_FOUND,
body: {
ok: false,
message: error.message,
data: {
listId: error.listId
}
}
}
}

throw error
}
}
14 changes: 12 additions & 2 deletions src/controllers/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
createPickInListHandler,
deletePickInListHandler,
getPicksByListIdHandler,
getListsHandler,
createListHandler
createListHandler,
deleteListHandler,
getListsHandler
} from './handlers/lists-handlers'
import { getPickStatsHandler, getPicksByItemIdHandler, getPickStatsOfItemHandler } from './handlers/picks-handlers'
import { pingHandler } from './handlers/ping-handler'
Expand Down Expand Up @@ -86,5 +87,14 @@ export async function setupRouter(_globalContext: GlobalContext): Promise<Router
createListHandler
)

router.delete(
'/v1/lists/:id',
authorizationMiddleware.wellKnownComponents({
optional: false,
expiration: FIVE_MINUTES
}),
deleteListHandler
)

return router
}
30 changes: 30 additions & 0 deletions src/migrations/1683320488882_acl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'
import { LISTS_TABLE } from './1677778846950_lists-and-picks'

export const shorthands: ColumnDefinitions | undefined = undefined
const ACL_TABLE = 'acl'
const PERMISSION_TYPE = 'permissions'

export async function up(pgm: MigrationBuilder): Promise<void> {
pgm.createType(PERMISSION_TYPE, ['edit', 'view'])
pgm.createTable(ACL_TABLE, {
list_id: {
type: 'uuid',
notNull: true,
unique: false,
references: `${LISTS_TABLE}(id)`,
onDelete: 'CASCADE'
},
permission: { type: 'permissions', notNull: true },
grantee: { type: 'text', notNull: true }
})
pgm.addConstraint(ACL_TABLE, 'list_id_permissions_grantee_primary_key', {
primaryKey: ['list_id', 'permission', 'grantee']
})
}

export async function down(pgm: MigrationBuilder): Promise<void> {
pgm.dropTable(ACL_TABLE)
pgm.dropType(PERMISSION_TYPE)
}
13 changes: 12 additions & 1 deletion src/ports/lists/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,16 @@ export function createListsComponent(
}
}

return { getPicksByListId, addPickToList, deletePickInList, getLists, addList }
async function deleteList(id: string, userAddress: string): Promise<void> {
const result = await pg.query(
SQL`DELETE FROM favorites.lists
WHERE favorites.lists.id = ${id}
AND favorites.lists.user_address = ${userAddress}`
)
if (result.rowCount === 0) {
throw new ListNotFoundError(id)
}
}

return { getPicksByListId, addPickToList, deletePickInList, getLists, addList, deleteList }
}
2 changes: 1 addition & 1 deletion src/ports/lists/errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export class ListNotFoundError extends Error {
constructor(public listId: string) {
super('The favorites list was not found.')
super('The list was not found.')
}
}

Expand Down
1 change: 1 addition & 0 deletions src/ports/lists/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface IListsComponents {
deletePickInList(listId: string, itemId: string, userAddress: string): Promise<void>
getLists(options?: GetListsParameters): Promise<DBGetListsWithCount[]>
addList(newList: AddListRequestBody): Promise<DBList>
deleteList(id: string, userAddress: string): Promise<void>
}

export type GetAuthenticatedAndPaginatedParameters = {
Expand Down
15 changes: 12 additions & 3 deletions test/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,29 @@ export function createTestSnapshotComponent({ getScore = jest.fn() } = { getScor
}

export function createTestListsComponent(
{ getPicksByListId = jest.fn(), addPickToList = jest.fn(), deletePickInList = jest.fn(), getLists = jest.fn(), addList = jest.fn() } = {
{
getPicksByListId = jest.fn(),
addPickToList = jest.fn(),
deletePickInList = jest.fn(),
getLists = jest.fn(),
addList = jest.fn(),
deleteList = jest.fn()
} = {
getPicksByListId: jest.fn(),
addPickToList: jest.fn(),
deletePickInList: jest.fn(),
getLists: jest.fn(),
addList: jest.fn()
addList: jest.fn(),
deleteList: jest.fn()
}
): IListsComponents {
return {
getPicksByListId,
addPickToList,
deletePickInList,
getLists,
addList
addList,
deleteList
}
}

Expand Down
50 changes: 50 additions & 0 deletions test/unit/lists-component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,3 +507,53 @@ describe('when creating a new list', () => {
})
})
})

describe('when deleting a list', () => {
describe('and the list was not found or was not accessible by the user', () => {
let error: Error

beforeEach(() => {
error = new ListNotFoundError(listId)
dbQueryMock.mockResolvedValueOnce({ rowCount: 0 })
})

it('should throw a list not found error', () => {
return expect(listsComponent.deleteList(listId, userAddress)).rejects.toEqual(error)
})
})

describe('and the list was successfully deleted', () => {
let result: void

beforeEach(async () => {
dbQueryMock.mockResolvedValueOnce({ rowCount: 1 })
result = await listsComponent.deleteList(listId, userAddress)
})

it('should have made the query to delete the list', async () => {
expect(dbQueryMock).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.stringContaining('DELETE FROM favorites.lists')
})
)

expect(dbQueryMock).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.stringContaining('WHERE favorites.lists.id = $1'),
values: expect.arrayContaining([listId])
})
)

expect(dbQueryMock).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.stringContaining('AND favorites.lists.user_address = $2'),
values: expect.arrayContaining([userAddress])
})
)
})

it('should resolve', () => {
return expect(result).toEqual(undefined)
})
})
})
91 changes: 86 additions & 5 deletions test/unit/lists-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
createPickInListHandler,
deletePickInListHandler,
getPicksByListIdHandler,
getListsHandler
getListsHandler,
deleteListHandler
} from '../../src/controllers/handlers/lists-handlers'
import { DBGetListsWithCount, DBList } from '../../src/ports/lists'
import {
Expand Down Expand Up @@ -233,7 +234,7 @@ describe('when creating a pick', () => {
status: StatusCode.NOT_FOUND,
body: {
ok: false,
message: 'The favorites list was not found.',
message: 'The list was not found.',
data: {
listId
}
Expand Down Expand Up @@ -342,7 +343,7 @@ describe('when deleting a pick', () => {
deletePickInList: deletePickInListMock
})
}
request = {} as HandlerContextWithPath<'lists', '/v1/lists/:id/picks'>['request']
request = {} as HandlerContextWithPath<'lists', '/v1/lists/:id/picks/:itemId'>['request']
params = { id: listId, itemId }
})

Expand All @@ -352,7 +353,7 @@ describe('when deleting a pick', () => {
})

it('should return an unauthorized response', () => {
return expect(createPickInListHandler({ components, verification, request, params })).resolves.toEqual({
return expect(deletePickInListHandler({ components, verification, request, params })).resolves.toEqual({
status: StatusCode.UNAUTHORIZED,
body: {
ok: false,
Expand Down Expand Up @@ -397,7 +398,7 @@ describe('when deleting a pick', () => {
})
})

describe('and the process to add the picks fails with an unknown error', () => {
describe('and the process to delete a pick fails with an unknown error', () => {
const error = new Error('anError')

beforeEach(() => {
Expand Down Expand Up @@ -699,3 +700,83 @@ describe('when creating a list', () => {
})
})
})

describe('when deleting a list', () => {
let request: HandlerContextWithPath<'lists', '/v1/lists/:id'>['request']
let params: HandlerContextWithPath<'lists', '/v1/lists/:id'>['params']
let deleteListMock: jest.Mock

beforeEach(() => {
listId = 'list-id'
deleteListMock = jest.fn()
components = {
lists: createTestListsComponent({
deleteList: deleteListMock
})
}
request = {} as HandlerContextWithPath<'lists', '/v1/lists/:id'>['request']
params = { id: listId }
})

describe('and the request is not authenticated', () => {
beforeEach(() => {
verification = undefined
})

it('should return an unauthorized response', () => {
return expect(deleteListHandler({ components, verification, request, params })).resolves.toEqual({
status: StatusCode.UNAUTHORIZED,
body: {
ok: false,
message: 'Unauthorized'
}
})
})
})

describe('and the request failed due to the list not existing or not being accessible', () => {
beforeEach(() => {
deleteListMock.mockRejectedValueOnce(new ListNotFoundError(listId))
})

it('should return a not found response', () => {
return expect(deleteListHandler({ components, verification, request, params })).resolves.toEqual({
status: StatusCode.NOT_FOUND,
body: {
ok: false,
message: 'The list was not found.',
data: {
listId
}
}
})
})
})

describe('and the request is successful', () => {
beforeEach(() => {
deleteListMock.mockResolvedValueOnce(undefined)
})

it('should return an ok response', () => {
return expect(deleteListHandler({ components, verification, request, params })).resolves.toEqual({
status: StatusCode.OK,
body: {
ok: true
}
})
})
})

describe('and the process to delete a list fails with an unknown error', () => {
const error = new Error('anError')

beforeEach(() => {
deleteListMock.mockRejectedValueOnce(error)
})

it('should propagate the error', () => {
return expect(deleteListHandler({ components, verification, request, params })).rejects.toEqual(error)
})
})
})

0 comments on commit 6f5d98c

Please sign in to comment.