Add integration tests and organize them by requirements.master
@@ -18,14 +18,23 @@ | |||
- [ ] In the front-end, the client requests provides a search keyword to request for composers and ringtones from the back-end. | |||
- [ ] In the back-end, the server retrieves the composers and ringtones whose name matches the search keyword provided | |||
by the front-end. | |||
- As a client, I want to browse ringtones. | |||
- [ ] In the front-end, the client provides an optional skip and take arguments to request multiple ringtones from the back-end. | |||
- [X] In the back-end, the server sends the ringtones to the front-end. | |||
- As a composer, I want to create a ringtone. | |||
- [ ] In the front-end, the client inputs ringtone data to the view. | |||
- [ ] In the front-end, the client sends the ringtone data to the back-end. | |||
- [ ] In the back-end, the server stores the ringtone. | |||
- [X] In the back-end, the server stores the ringtone data. | |||
- As a composer, I want to update a ringtone. | |||
- [ ] In the front-end, the client modifies the ringtone data retrieved from the back-end and loaded on the view. | |||
- [ ] In the front-end, the client sends the ringtone data to the back-end. | |||
- [ ] In the back-end, the server stores the ringtone. | |||
- As a composer, I want to delete a ringtone. | |||
- [ ] In the front-end, the client provides a ringtone ID to request a ringtone's deletion to the back-end. | |||
- [ ] In the back-end, the server deletes the ringtone if its ID matches the one provided by the front-end. | |||
- [X] In the back-end, the server stores the updated ringtone data. | |||
- As a composer, I want to soft-delete a ringtone. | |||
- [ ] In the front-end, the client provides a ringtone ID to request a ringtone's soft deletion to the back-end. | |||
- [X] In the back-end, the server tags a ringtone as deleted if its ID matches the one provided by the front-end. | |||
- As a composer, I want to undo deletion of a soft-deleted ringtone. | |||
- [ ] In the front-end, the client provides a ringtone ID to request a ringtone's deletion rollback to the back-end. | |||
- [X] In the back-end, the server untags a ringtone as deleted if its ID matches the one provided by the front-end. | |||
- As a composer, I want to hard-delete of a ringtone. | |||
- [ ] In the front-end, the client provides a ringtone ID to request a ringtone's deletion rollback to the back-end. | |||
- [X] In the back-end, the server removes a ringtone in the database if its ID matches the one provided by the front-end. |
@@ -0,0 +1,3 @@ | |||
import { config } from 'dotenv' | |||
config() |
@@ -1,5 +1,4 @@ | |||
import '@abraham/reflection' | |||
import { config } from 'dotenv' | |||
import {join} from 'path' | |||
import AutoLoad, {AutoloadPluginOptions} from 'fastify-autoload' | |||
@@ -34,7 +33,5 @@ const app: FastifyPluginAsync<AppOptions> = async ( | |||
}; | |||
config() | |||
export default app; | |||
export {app}; |
@@ -14,17 +14,21 @@ export class RingtoneController { | |||
try { | |||
const data = await this.ringtoneService.get(request.params['id']) | |||
if (typeof (data.deletedAt as Date) !== 'undefined') { | |||
reply.raw.statusMessage = 'Ringtone Deleted Previously' | |||
reply.gone() | |||
return | |||
} | |||
if (!data) { | |||
reply.raw.statusMessage = 'Ringtone Not Found' | |||
reply.notFound() | |||
return | |||
} | |||
reply.raw.statusMessage = 'Single Ringtone Retrieved' | |||
return { | |||
data, | |||
} | |||
} catch (err) { | |||
reply.raw.statusMessage = 'Get Ringtone Error' | |||
reply.internalServerError(err.message) | |||
} | |||
} | |||
@@ -39,12 +43,14 @@ export class RingtoneController { | |||
const skip = !isNaN(skipNumber) ? skipNumber : undefined | |||
const take = !isNaN(takeNumber) ? takeNumber : undefined | |||
reply.raw.statusMessage = 'Multiple Ringtones Retrieved' | |||
return { | |||
data: await this.ringtoneService.browse(skip, take), | |||
skip, | |||
take, | |||
} | |||
} catch (err) { | |||
reply.raw.statusMessage = 'Browse Ringtones Error' | |||
reply.internalServerError(err.message) | |||
} | |||
} | |||
@@ -52,11 +58,13 @@ export class RingtoneController { | |||
search = async (request: any, reply: any) => { | |||
try { | |||
const { 'q': query } = request.query | |||
reply.raw.statusMessage = 'Search Results Retrieved' | |||
return { | |||
data: await this.ringtoneService.search(query), | |||
query, | |||
} | |||
} catch (err) { | |||
reply.raw.statusMessage = 'Search Error' | |||
reply.internalServerError(err.message) | |||
} | |||
} | |||
@@ -65,10 +73,12 @@ export class RingtoneController { | |||
try { | |||
const data = await this.ringtoneService.create(request.body) | |||
reply.status(201) | |||
reply.raw.statusMessage = 'Ringtone Created' | |||
return { | |||
data, | |||
} | |||
} catch (err) { | |||
reply.raw.statusMessage = 'Create Ringtone Error' | |||
reply.internalServerError(err.message) | |||
} | |||
} | |||
@@ -81,13 +91,16 @@ export class RingtoneController { | |||
id: request.params['id'], | |||
}) | |||
if (data.deletedAt !== null) { | |||
reply.raw.statusMessage = 'Ringtone Deleted Previously' | |||
reply.gone() | |||
return | |||
} | |||
if (!data) { | |||
reply.raw.statusMessage = 'Ringtone Not Found' | |||
reply.notFound() | |||
return | |||
} | |||
reply.raw.statusMessage = 'Ringtone Updated' | |||
return { | |||
data, | |||
} | |||
@@ -101,13 +114,16 @@ export class RingtoneController { | |||
// TODO validate data | |||
const data = await this.ringtoneService.softDelete(request.params['id']) | |||
if (!data) { | |||
reply.raw.statusMessage = 'Ringtone Not Found' | |||
reply.notFound() | |||
return | |||
} | |||
reply.raw.statusMessage = 'Ringtone Soft-Deleted' | |||
return { | |||
data, | |||
} | |||
} catch (err) { | |||
reply.raw.statusMessage = 'Soft-Delete Ringtone Error' | |||
reply.internalServerError(err.message) | |||
} | |||
} | |||
@@ -117,13 +133,16 @@ export class RingtoneController { | |||
// TODO validate data | |||
const data = await this.ringtoneService.undoDelete(request.params['id']) | |||
if (!data) { | |||
reply.raw.statusMessage = 'Ringtone Not Found' | |||
reply.notFound() | |||
return | |||
} | |||
reply.raw.statusMessage = 'Ringtone Restored' | |||
return { | |||
data, | |||
} | |||
} catch (err) { | |||
reply.raw.statusMessage = 'Restore Ringtone Error' | |||
reply.internalServerError(err.message) | |||
} | |||
} | |||
@@ -133,10 +152,13 @@ export class RingtoneController { | |||
// TODO validate data | |||
await this.ringtoneService.hardDelete(request.params['id']) | |||
reply.status(204) | |||
reply.raw.statusMessage = 'Ringtone Hard-Deleted' | |||
} catch (err) { | |||
if (err instanceof DoubleDeletionError) { | |||
reply.raw.statusMessage = 'Ringtone Not Found' | |||
reply.notFound(err.message) | |||
} | |||
reply.raw.statusMessage = 'Delete Ringtone Error' | |||
reply.internalServerError(err.message) | |||
} | |||
} | |||
@@ -3,7 +3,7 @@ import {build} from '../../helper' | |||
import ringtoneModule from '../../../src/modules/ringtone' | |||
import MockRingtoneRepository from '../../mocks/repositories/Ringtone' | |||
describe('ringtone resource', () => { | |||
describe('ringtone', () => { | |||
let app: FastifyInstance | |||
beforeEach(async () => { | |||
@@ -44,8 +44,14 @@ describe('ringtone resource', () => { | |||
await app.close() | |||
}) | |||
describe('collection', () => { | |||
it('should be browsable', async () => { | |||
describe('on searching', () => { | |||
it('should send the data to the front-end', async () => { | |||
// TODO | |||
}) | |||
}) | |||
describe('on browsing', () => { | |||
it('should send the data to the front-end', async () => { | |||
const res = await app.inject({ | |||
url: '/api/ringtones', | |||
method: 'GET', | |||
@@ -53,12 +59,10 @@ describe('ringtone resource', () => { | |||
const parsedPayload = JSON.parse(res.payload) | |||
expect(Array.isArray(parsedPayload.data)).toBe(true) | |||
}) | |||
}) | |||
it('should be searchable', async () => { | |||
// TODO | |||
}) | |||
it('should be extendable', async () => { | |||
describe('on creation', () => { | |||
it('should store the data', async () => { | |||
const res = await app.inject({ | |||
url: '/api/ringtones', | |||
method: 'POST', | |||
@@ -72,16 +76,73 @@ describe('ringtone resource', () => { | |||
}) | |||
}) | |||
const parsedPayload = JSON.parse(res.payload) | |||
expect(parsedPayload.data).toEqual({ | |||
id: expect.any(String), | |||
expect(parsedPayload.data).toEqual(expect.objectContaining({ | |||
name: 'New Ringtone', | |||
data: '4c4', | |||
createdAt: expect.any(String), | |||
updatedAt: expect.any(String), | |||
deletedAt: null, | |||
composerId: '00000000-0000-0000-000000000000', | |||
})) | |||
}) | |||
}) | |||
describe('on updating', () => { | |||
it('should store the updated data', async () => { | |||
const res = await app.inject({ | |||
url: '/api/ringtones/00000000-0000-0000-000000000000', | |||
method: 'PATCH', | |||
headers: { | |||
'Content-Type': 'application/json', | |||
}, | |||
payload: JSON.stringify({ | |||
name: 'Updated Ringtone', | |||
data: '4c8', | |||
composerId: '00000000-0000-0000-000000000000', | |||
}) | |||
}) | |||
const parsedPayload = JSON.parse(res.payload) | |||
expect(parsedPayload.data).toEqual(expect.objectContaining({ | |||
name: 'Updated Ringtone', | |||
data: '4c8', | |||
composerId: '00000000-0000-0000-000000000000', | |||
})) | |||
expect(parsedPayload.data.createdAt).not.toEqual(parsedPayload.data.updatedAt) | |||
}) | |||
}) | |||
describe('on soft deletion', () => { | |||
it('should be tagged as deleted', async () => { | |||
const res = await app.inject({ | |||
url: '/api/ringtones/00000000-0000-0000-000000000000/delete', | |||
method: 'POST', | |||
}) | |||
const parsedPayload = JSON.parse(res.payload) | |||
expect(parsedPayload.data).toEqual(expect.objectContaining({ | |||
id: '00000000-0000-0000-000000000000', | |||
deletedAt: expect.any(String), | |||
})) | |||
}) | |||
}) | |||
describe('on undoing deletion', () => { | |||
it('should be untagged as deleted', async () => { | |||
const res = await app.inject({ | |||
url: '/api/ringtones/00000000-0000-0000-000000000000/delete', | |||
method: 'DELETE', | |||
}) | |||
const parsedPayload = JSON.parse(res.payload) | |||
expect(parsedPayload.data).toEqual(expect.objectContaining({ | |||
id: '00000000-0000-0000-000000000000', | |||
deletedAt: null, | |||
})) | |||
}) | |||
}) | |||
describe('on hard deletion', () => { | |||
it('should be removed', async () => { | |||
const res = await app.inject({ | |||
url: '/api/ringtones/00000000-0000-0000-000000000000', | |||
method: 'DELETE', | |||
}) | |||
console.log(parsedPayload) | |||
expect(res.statusCode).toBe(204) | |||
}) | |||
}) | |||
}) |