Skip to content

Commit

Permalink
Submission score entity (makeopensource#44)
Browse files Browse the repository at this point in the history
Add submissionScore entity to the API. This entity stores a score and feedback attached to a single submission
  • Loading branch information
jmabramo authored Nov 16, 2022
1 parent ee8185b commit 52188cb
Show file tree
Hide file tree
Showing 17 changed files with 471 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from 'express'

import CodeAssignmentService from '../services/codeAssignments.service'
import { serialize } from '../utils/serializer/codeAssignments.serializer'
import CodeAssignmentService from '../services/codeAssignment.service'
import { serialize } from '../utils/serializer/codeAssignment.serializer'

import { GenericResponse, NotFound, Updated } from '../utils/apiResponse.utils'

Expand Down
73 changes: 73 additions & 0 deletions src/controller/submissionScore.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Request, Response, NextFunction } from 'express'
import submissionScoreService from '../services/submissionScore.service'

import SubmissionScoreService from '../services/submissionScore.service'

import { GenericResponse, NotFound, Updated } from '../utils/apiResponse.utils'

import { serialize } from '../utils/serializer/submissionScore.serializer'

export async function get(req: Request, res: Response, next: NextFunction) {
try {
const submissionScores = await SubmissionScoreService.list()
const response = submissionScores.map(serialize)

res.status(200).json(response)
} catch (err) {
next(err)
}
}

export async function detail(req: Request, res: Response, next: NextFunction) {
try {
const id = parseInt(req.params.id)
const submissionScore = await submissionScoreService.retrieve(id)

if (!submissionScore) return res.status(404).json(NotFound)

const response = serialize(submissionScore)

res.status(200).json(response)
} catch (err) {
next(err)
}
}

export async function post(req: Request, res: Response, next: NextFunction) {
try {
const submissionScore = await SubmissionScoreService.create(req.body)
const response = serialize(submissionScore)

res.status(201).json(response)
} catch (err) {
res.status(400).json(new GenericResponse(err.message))
}
}

export async function put(req: Request, res: Response, next: NextFunction) {
try {
req.body.id = parseInt(req.params.id)
const results = await SubmissionScoreService.update(req.body)

if (!results.affected) return res.status(404).json(NotFound)

res.status(200).json(Updated)
} catch (err) {
next(err)
}
}

export async function _delete(req: Request, res: Response, next: NextFunction) {
try {
const id = parseInt(req.params.id)
const results = await SubmissionScoreService._delete(id)

if (!results.affected) return res.status(404).json(NotFound)

res.status(204).send()
} catch (err) {
next(err)
}
}

export default { get, detail, post, put, _delete }
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { UpdateResult } from 'typeorm'

import { CodeAssignment } from 'devu-shared-modules'

import controller from '../codeAssignments.controller'
import controller from '../codeAssignment.controller'

import CodeAssignmentModel from '../../model/codeAssignments.model'
import CodeAssignmentModel from '../../model/codeAssignment.model'

import CodeAssignmentService from '../../services/codeAssignments.service'
import CodeAssignmentService from '../../services/codeAssignment.service'

import { serialize } from '../../utils/serializer/codeAssignments.serializer'
import { serialize } from '../../utils/serializer/codeAssignment.serializer'

import Testing from '../../utils/testing.utils'
import { GenericResponse, NotFound, Updated } from '../../utils/apiResponse.utils'
Expand Down
208 changes: 208 additions & 0 deletions src/controller/tests/submissionScore.controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { UpdateResult } from 'typeorm'

import { SubmissionScore } from 'devu-shared-modules'

import controller from '../submissionScore.controller'

import SubmissionScoreModel from '../../model/submissionScore.model'

import SubmissionScoreService from '../../services/submissionScore.service'

import { serialize } from '../../utils/serializer/submissionScore.serializer'

import Testing from '../../utils/testing.utils'
import { GenericResponse, NotFound, Updated } from '../../utils/apiResponse.utils'

// Testing Globals
let req: any
let res: any
let next: any

let mockedSubmissionScores: SubmissionScoreModel[]
let mockedSubmissionScore: SubmissionScoreModel
let expectedResults: SubmissionScore[]
let expectedResult: SubmissionScore
let expectedError: Error

let expectedDbResult: UpdateResult

describe('SubmissionScoreController', () => {
beforeEach(() => {
req = Testing.fakeRequest()
res = Testing.fakeResponse()
next = Testing.fakeNext()

mockedSubmissionScores = Testing.generateTypeOrmArray(SubmissionScoreModel, 3)
mockedSubmissionScore = Testing.generateTypeOrm(SubmissionScoreModel)

expectedResults = mockedSubmissionScores.map(serialize)
expectedResult = serialize(mockedSubmissionScore)
expectedError = new Error('Expected Error')

expectedDbResult = {} as UpdateResult
})

describe('GET - /submission-score', () => {
describe('200 - Ok', () => {
beforeEach(async () => {
SubmissionScoreService.list = jest.fn().mockImplementation(() => Promise.resolve(mockedSubmissionScores))
await controller.get(req, res, next) // what we're testing
})

test('Returns list of submissionScores', () => expect(res.json).toBeCalledWith(expectedResults))
test('Status code is 200', () => expect(res.status).toBeCalledWith(200))
})

describe('400 - Bad request', () => {
test('Next called with expected error', async () => {
SubmissionScoreService.list = jest.fn().mockImplementation(() => Promise.reject(expectedError))

try {
await controller.get(req, res, next)

fail('Expected test to throw')
} catch {
expect(next).toBeCalledWith(expectedError)
}
})
})
})

describe('GET - /submission-score/:id', () => {
describe('200 - Ok', () => {
beforeEach(async () => {
SubmissionScoreService.retrieve = jest.fn().mockImplementation(() => Promise.resolve(mockedSubmissionScore))
await controller.detail(req, res, next)
})

test('Returns expected submissionScore', () => expect(res.json).toBeCalledWith(expectedResult))
test('Status code is 200', () => expect(res.status).toBeCalledWith(200))
})

describe('404 - Not Found', () => {
beforeEach(async () => {
SubmissionScoreService.retrieve = jest.fn().mockImplementation(() => Promise.resolve()) // No results
await controller.detail(req, res, next)
})

test('Status code is 404 on missing submissionScore', () => expect(res.status).toBeCalledWith(404))
test('Responds with NotFound on missing submissionScore', () => expect(res.json).toBeCalledWith(NotFound))
test('Next not called on missing submissionScore', () => expect(next).toBeCalledTimes(0))
})

describe('400 - Bad Request', () => {
test('Next called with expected error', async () => {
SubmissionScoreService.retrieve = jest.fn().mockImplementation(() => Promise.reject(expectedError))

try {
await controller.detail(req, res, next)

fail('Expected test to throw')
} catch {
expect(next).toBeCalledWith(expectedError)
}
})
})
})

describe('POST - /submission-score/', () => {
describe('201 - Created', () => {
beforeEach(async () => {
SubmissionScoreService.create = jest.fn().mockImplementation(() => Promise.resolve(mockedSubmissionScore))
await controller.post(req, res, next)
})

test('Returns expected submissionScore', () => expect(res.json).toBeCalledWith(expectedResult))
test('Status code is 201', () => expect(res.status).toBeCalledWith(201))
})

describe('400 - Bad Request', () => {
beforeEach(async () => {
SubmissionScoreService.create = jest.fn().mockImplementation(() => Promise.reject(expectedError))

try {
await controller.post(req, res, next)

fail('Expected test to throw')
} catch {
// continue to tests
}
})

test('Status code is 400', () => expect(res.status).toBeCalledWith(400))
test('Responds with generic error', () =>
expect(res.json).toBeCalledWith(new GenericResponse(expectedError.message)))
test('Next not called', () => expect(next).toBeCalledTimes(0))
})
})

describe('PUT - /submission-score/:id', () => {
describe('200 - Ok', () => {
beforeEach(async () => {
expectedDbResult.affected = 1 // mocking service return shape
SubmissionScoreService.update = jest.fn().mockImplementation(() => Promise.resolve(expectedDbResult))
await controller.put(req, res, next)
})

test('Status code is 200', () => expect(res.status).toBeCalledWith(200))
test('Returns Updated message', () => expect(res.json).toBeCalledWith(Updated))
test('Next is not called', () => expect(next).toHaveBeenCalledTimes(0))
})

describe('404 - Not Found', () => {
beforeEach(async () => {
expectedDbResult.affected = 0 // No records affected in db
SubmissionScoreService.update = jest.fn().mockImplementation(() => Promise.resolve(expectedDbResult))
await controller.put(req, res, next)
})

test('Status code is 404', () => expect(res.status).toBeCalledWith(404))
test('Returns Not found message', () => expect(res.json).toBeCalledWith(NotFound))
test('Next is not called', () => expect(next).toHaveBeenCalledTimes(0))
})

describe('400 - Bad Request', () => {
beforeEach(async () => {
SubmissionScoreService.update = jest.fn().mockImplementation(() => Promise.reject(expectedError))
await controller.put(req, res, next)
})

test('Next is called with error', () => expect(next).toBeCalledWith(expectedError))
})
})

describe('DELETE - /submission-score/:id', () => {
describe('204 - No Content', () => {
beforeEach(async () => {
expectedDbResult.affected = 1
SubmissionScoreService._delete = jest.fn().mockImplementation(() => Promise.resolve(expectedDbResult))
await controller._delete(req, res, next)
})

test('Status code is 204', () => expect(res.status).toBeCalledWith(204))
test('Response to have no content', () => expect(res.send).toBeCalledWith())
test('Next not called', () => expect(next).toBeCalledTimes(0))
})

describe('404 - Not Found', () => {
beforeEach(async () => {
expectedDbResult.affected = 0
SubmissionScoreService._delete = jest.fn().mockImplementation(() => Promise.resolve(expectedDbResult))
await controller._delete(req, res, next)
})

test('Status code is 404', () => expect(res.status).toBeCalledWith(404))
test('Response to have no content', () => expect(res.json).toBeCalledWith(NotFound))
test('Next not called', () => expect(next).toBeCalledTimes(0))
})

describe('400 - Bad Request', () => {
beforeEach(async () => {
SubmissionScoreService._delete = jest.fn().mockImplementation(() => Promise.reject(expectedError))
await controller._delete(req, res, next)
})

test('Next called with expected error', () => expect(next).toBeCalledWith(expectedError))
})
})
})
12 changes: 12 additions & 0 deletions src/middleware/validator/submissionScore.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { check } from 'express-validator'

import validate from './generic.validator'

const submissionId = check('submissionId').isNumeric()
const score = check('score').isNumeric().optional({ nullable: true })
const feedback = check('feedback').isString().trim().optional({ nullable: true })
const releasedAt = check('releasedAt').trim().isISO8601().toDate()

const validator = [submissionId, score, feedback, releasedAt, validate]

export default validator
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import AssignmentModel from './assignment.model'
* such assignments should not have and other such assignments types (eg. NonCodeAssignment, ManualAssignment).
*/
@Entity('code_assignments')
export default class CodeAssignmentsModel {
export default class CodeAssignmentModel {
@PrimaryGeneratedColumn()
id: number

Expand Down
4 changes: 2 additions & 2 deletions src/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import UserModel from './user.model'
import UserCourseModel from './userCourse.model'
import SubmissionProblemScoreModel from './submissionProblemScore.model'
import SubmissionScoreModel from './submissionScore.model'
import CodeAssignmentsModel from './codeAssignments.model'
import CodeAssignmentModel from './codeAssignment.model'

type Models =
| AssignmentModel
Expand All @@ -15,6 +15,6 @@ type Models =
| UserModel
| SubmissionProblemScoreModel
| SubmissionScoreModel
| CodeAssignmentsModel
| CodeAssignmentModel

export default Models
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import express from 'express'
import multer from 'multer'

import validator from '../middleware/validator/codeAssignments.validator'
import validator from '../middleware/validator/codeAssignment.validator'
import { asInt } from '../middleware/validator/generic.validator'

import CodeAssignmentController from '../controller/codeAssignments.controller'
import CodeAssignmentController from '../controller/codeAssignment.controller'

const Router = express.Router()
const upload = multer()
Expand Down
4 changes: 3 additions & 1 deletion src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import swaggerUi from 'swagger-ui-express'

import swagger from '../utils/swagger.utils'

import codeAssignment from './codeAssignments.router'
import codeAssignment from './codeAssignment.router'
import userCourse from './userCourse.router'
import assignments from './assignment.router'
import courses from './course.router'
Expand All @@ -12,6 +12,7 @@ import logout from './logout.router'
import status from './status.router'
import submissions from './submission.router'
import users from './user.router'
import submissionScore from './submissionScore.router'

import { isAuthorized } from '../middleware/auth.middleware'

Expand All @@ -26,6 +27,7 @@ Router.use('/code-assignments', isAuthorized, codeAssignment)
Router.use('/docs', swaggerUi.serve, swaggerUi.setup(swagger))
Router.use('/submissions', isAuthorized, submissions)
Router.use('/users', isAuthorized, users)
Router.use('/submission-scores', isAuthorized, submissionScore)

Router.use('/login', login)
Router.use('/logout', logout)
Expand Down
Loading

0 comments on commit 52188cb

Please sign in to comment.