require '../common' utils = require '../utils' _ = require 'lodash' Promise = require 'bluebird' request = require '../request' requestAsync = Promise.promisify(request, {multiArgs: true}) Course = require '../../../server/models/Course' User = require '../../../server/models/User' Classroom = require '../../../server/models/Classroom' Campaign = require '../../../server/models/Campaign' Level = require '../../../server/models/Level' Patch = require '../../../server/models/Patch' courseFixture = { name: 'Unnamed course' campaignID: ObjectId("55b29efd1cd6abe8ce07db0d") concepts: ['basic_syntax', 'arguments', 'while_loops', 'strings', 'variables'] description: "Learn basic syntax, while loops, and the CodeCombat environment." screenshot: "/images/pages/courses/101_info.png" } describe 'GET /db/course', -> beforeEach utils.wrap (done) -> yield utils.clearModels([Course, User]) yield new Course({ name: 'Course 1', releasePhase: 'released' }).save() yield new Course({ name: 'Course 2', releasePhase: 'released' }).save() yield utils.becomeAnonymous() done() it 'returns an array of Course objects', utils.wrap (done) -> [res, body] = yield request.getAsync { uri: getURL('/db/course'), json: true } expect(body.length).toBe(2) done() describe 'GET /db/course/:handle', -> beforeEach utils.wrap (done) -> yield utils.clearModels([Course, User]) @course = yield new Course({ name: 'Some Name', releasePhase: 'released' }).save() yield utils.becomeAnonymous() done() it 'returns Course by id', utils.wrap (done) -> [res, body] = yield request.getAsync {uri: getURL("/db/course/#{@course.id}"), json: true} expect(res.statusCode).toBe(200) expect(body._id).toBe(@course.id) done() it 'returns Course by slug', utils.wrap (done) -> [res, body] = yield request.getAsync {uri: getURL("/db/course/some-name"), json: true} expect(res.statusCode).toBe(200) expect(body._id).toBe(@course.id) done() it 'returns not found if handle does not exist in the db', utils.wrap (done) -> [res, body] = yield request.getAsync {uri: getURL("/db/course/dne"), json: true} expect(res.statusCode).toBe(404) done() describe 'PUT /db/course/:handle', -> beforeEach utils.wrap (done) -> yield utils.clearModels([Course, User]) @course = yield new Course({ name: 'Some Name', releasePhase: 'released' }).save() yield utils.becomeAnonymous() @url = getURL("/db/course/#{@course.id}") done() it 'allows changes to i18n and i18nCoverage', utils.wrap (done) -> admin = yield utils.initAdmin() yield utils.loginUser(admin) json = { i18n: { de: { name: 'German translation' } } i18nCoverage: ['de'] } [res, body] = yield request.putAsync { @url, json } expect(res.statusCode).toBe(200) expect(body._id).toBe(@course.id) course = yield Course.findById(@course.id) expect(course.get('i18n').de.name).toBe('German translation') expect(course.get('i18nCoverage')).toBeDefined() done() it 'returns 403 to non-admins', utils.wrap (done) -> user = yield utils.initUser() yield utils.loginUser(user) json = { i18n: { es: { name: 'Spanish translation' } } } [res, body] = yield request.putAsync { @url, json } expect(res.statusCode).toBe(403) course = yield Course.findById(@course.id) expect(course.get('i18n')).toBeUndefined() expect(course.get('i18nCoverage').length).toBe(0) done() describe 'GET /db/course/:handle/levels/:levelOriginal/next', -> beforeEach utils.wrap (done) -> yield utils.clearModels [User, Classroom, Course, Level, Campaign] admin = yield utils.initAdmin() yield utils.loginUser(admin) levelJSON = { name: 'A', permissions: [{access: 'owner', target: admin.id}], type: 'course' } [res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON}) expect(res.statusCode).toBe(200) @levelA = yield Level.findById(res.body._id) paredLevelA = _.pick(res.body, 'name', 'original', 'type') levelJSON = { name: 'B', permissions: [{access: 'owner', target: admin.id}], type: 'course' } [res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON}) expect(res.statusCode).toBe(200) @levelB = yield Level.findById(res.body._id) paredLevelB = _.pick(res.body, 'name', 'original', 'type') levelJSON = { name: 'C', permissions: [{access: 'owner', target: admin.id}], type: 'course' } [res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON}) expect(res.statusCode).toBe(200) @levelC = yield Level.findById(res.body._id) paredLevelC = _.pick(res.body, 'name', 'original', 'type') campaignJSONA = { name: 'Campaign A', levels: {} } campaignJSONA.levels[paredLevelA.original] = paredLevelA campaignJSONA.levels[paredLevelB.original] = paredLevelB [res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSONA}) @campaignA = yield Campaign.findById(res.body._id) campaignJSONB = { name: 'Campaign B', levels: {} } campaignJSONB.levels[paredLevelC.original] = paredLevelC [res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSONB}) @campaignB = yield Campaign.findById(res.body._id) @courseA = Course({name: 'Course A', campaignID: @campaignA._id, releasePhase: 'released'}) yield @courseA.save() @courseB = Course({name: 'Course B', campaignID: @campaignB._id, releasePhase: 'released'}) yield @courseB.save() teacher = yield utils.initUser({role: 'teacher'}) yield utils.loginUser(teacher) data = { name: 'Classroom 1' } classroomsURL = getURL('/db/classroom') [res, body] = yield request.postAsync {uri: classroomsURL, json: data } expect(res.statusCode).toBe(201) @classroom = yield Classroom.findById(res.body._id) url = getURL('/db/course') done() it 'returns the next level for the course in the linked classroom', utils.wrap (done) -> [res, body] = yield request.getAsync { uri: utils.getURL("/db/course/#{@courseA.id}/levels/#{@levelA.id}/next"), json: true } expect(res.statusCode).toBe(200) expect(res.body.original).toBe(@levelB.original.toString()) done() it 'returns empty object if the given level is the last level in its course', utils.wrap (done) -> [res, body] = yield request.getAsync { uri: utils.getURL("/db/course/#{@courseA.id}/levels/#{@levelB.id}/next"), json: true } expect(res.statusCode).toBe(200) expect(res.body).toEqual({}) done() it 'returns 404 if the given level is not in the course instance\'s course', utils.wrap (done) -> [res, body] = yield request.getAsync { uri: utils.getURL("/db/course/#{@courseB.id}/levels/#{@levelA.id}/next"), json: true } expect(res.statusCode).toBe(404) done() describe 'GET /db/course/:handle/level-solutions', -> beforeEach utils.wrap (done) -> yield utils.clearModels [User, Classroom, Course, Level, Campaign] admin = yield utils.initAdmin() yield utils.loginUser(admin) levelJSON = { name: 'A', permissions: [{access: 'owner', target: admin.id}], type: 'course' } [res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON}) expect(res.statusCode).toBe(200) @levelA = yield Level.findById(res.body._id) paredLevelA = _.pick(res.body, 'name', 'original', 'type') levelJSON = { name: 'B', permissions: [{access: 'owner', target: admin.id}], type: 'course' } [res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON}) expect(res.statusCode).toBe(200) @levelB = yield Level.findById(res.body._id) paredLevelB = _.pick(res.body, 'name', 'original', 'type') campaignJSONA = { name: 'Campaign A', levels: {} } campaignJSONA.levels[paredLevelB.original] = paredLevelB campaignJSONA.levels[paredLevelA.original] = paredLevelA [res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSONA}) @campaignA = yield Campaign.findById(res.body._id) @courseA = Course({name: 'Course A', campaignID: @campaignA._id, releasePhase: 'released'}) yield @courseA.save() done() describe 'when admin', -> it 'returns level solutions', utils.wrap (done) -> [res, body] = yield request.getAsync { uri: utils.getURL("/db/course/#{@courseA.id}/level-solutions"), json: true } expect(res.statusCode).toBe(200) expect(body.length).toEqual(2) expect(body[0].slug).toEqual('a') done() describe 'when teacher', -> beforeEach utils.wrap (done) -> teacher = yield utils.initUser({role: 'teacher'}) yield utils.loginUser(teacher) done() it 'returns level solutions', utils.wrap (done) -> [res, body] = yield request.getAsync { uri: utils.getURL("/db/course/#{@courseA.id}/level-solutions"), json: true } expect(res.statusCode).toBe(200) expect(body.length).toEqual(2) expect(body[1].slug).toEqual('b') done() describe 'when anonymous', -> beforeEach utils.wrap (done) -> yield utils.logout() done() it 'returns 403', utils.wrap (done) -> [res, body] = yield request.getAsync { uri: utils.getURL("/db/course/#{@courseA.id}/level-solutions"), json: true } expect(res.statusCode).toBe(403) done() describe 'POST /db/course/:handle/patch', -> beforeEach utils.wrap (done) -> yield utils.clearModels [User, Course] admin = yield utils.initAdmin() yield utils.loginUser(admin) @course = yield utils.makeCourse({ name: 'Test Course' description: 'A test course' i18n: { de: { name: 'existing translation' } } }) @url = utils.getURL("/db/course/#{@course.id}/patch") @json = { commitMessage: 'Server test commit' target: { collection: 'course' id: @course.id } } done() it 'saves the changes immediately if just adding new translations to existing langauge', utils.wrap (done) -> originalCourse = _.cloneDeep(@course.toObject()) changedCourse = _.cloneDeep(@course.toObject()) changedCourse.i18n.de.description = 'German translation!' @json.delta = jsondiffpatch.diff(originalCourse, changedCourse) [res, body] = yield request.postAsync({ @url, @json }) expect(res.statusCode).toBe(201) expect(res.body.status).toBe('accepted') course = yield Course.findById(@course.id) expect(course.get('i18n').de.description).toBe('German translation!') expect(course.get('patches')).toBeUndefined() expect(_.contains(course.get('i18nCoverage'),'de')).toBe(true) done() it 'saves the changes immediately if translations are for a new langauge', utils.wrap (done) -> originalCourse = _.cloneDeep(@course.toObject()) changedCourse = _.cloneDeep(@course.toObject()) changedCourse.i18n.fr = { description: 'French translation!' } @json.delta = jsondiffpatch.diff(originalCourse, changedCourse) [res, body] = yield request.postAsync({ @url, @json }) expect(res.statusCode).toBe(201) expect(res.body.status).toBe('accepted') course = yield Course.findById(@course.id) expect(course.get('i18n').fr.description).toBe('French translation!') expect(course.get('patches')).toBeUndefined() done() it 'saves a patch if it has some replacement translations', utils.wrap (done) -> originalCourse = _.cloneDeep(@course.toObject()) changedCourse = _.cloneDeep(@course.toObject()) changedCourse.i18n.de.name = 'replacement' @json.delta = jsondiffpatch.diff(originalCourse, changedCourse) [res, body] = yield request.postAsync({ @url, @json }) expect(res.statusCode).toBe(201) expect(res.body.status).toBe('pending') course = yield Course.findById(@course.id) expect(course.get('i18n').de.name).toBe('existing translation') expect(course.get('patches').length).toBe(1) patch = yield Patch.findById(course.get('patches')[0]) expect(_.isEqual(patch.get('delta'), @json.delta)).toBe(true) expect(patch.get('reasonNotAutoAccepted')).toBe('Adding to existing translations.') [res, body] = yield request.getAsync({ url: utils.getURL("/db/course/#{@course.id}/patches?status=pending"), json: true }) expect(res.body[0]._id).toBe(patch.id) done() it 'saves a patch if applying the patch would invalidate the course data', utils.wrap (done) -> originalCourse = _.cloneDeep(@course.toObject()) changedCourse = _.cloneDeep(@course.toObject()) changedCourse.notAProperty = 'this should not get saved to the course' @json.delta = jsondiffpatch.diff(originalCourse, changedCourse) [res, body] = yield request.postAsync({ @url, @json }) expect(res.statusCode).toBe(201) expect(res.body.status).toBe('pending') course = yield Course.findById(@course.id) expect(course.get('notAProperty')).toBeUndefined() expect(course.get('patches').length).toBe(1) patch = yield Patch.findById(course.get('patches')[0]) expect(_.isEqual(patch.get('delta'), @json.delta)).toBe(true) expect(patch.get('reasonNotAutoAccepted')).toBe('Did not pass json schema.') done() it 'saves a patch if submission loses race with another translator', utils.wrap (done) -> originalCourse = _.cloneDeep(@course.toObject()) changedCourse = _.cloneDeep(@course.toObject()) changedCourse.i18n.de.description = 'German translation!' yield @course.update({$set: {'i18n.de.description': 'Race condition'}}) # another change got saved first @json.delta = jsondiffpatch.diff(originalCourse, changedCourse) [res, body] = yield request.postAsync({ @url, @json }) expect(res.body.status).toBe('pending') expect(res.statusCode).toBe(201) course = yield Course.findById(@course.id) expect(course.get('i18n').de.description).toBe('Race condition') expect(course.get('patches').length).toBe(1) patch = yield Patch.findById(course.get('patches')[0]) expect(_.isEqual(patch.get('delta'), @json.delta)).toBe(true) expect(patch.get('reasonNotAutoAccepted')).toBe('Adding to existing translations.') done()