From f06c68742b080a993ac82ce5524df29cf4a3c20a Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Mon, 21 Nov 2022 15:25:53 +0100 Subject: [PATCH 01/24] fix: lint fix --- src/imports/api/csp/cspOptions.js | 3 +- .../api/utils/filesystem/fileExists.js | 23 ++ .../lessons/runtime/removeDocuments.js | 2 +- .../lessons/tests/LessonRuntime.tests.js | 2 +- .../document/converter/documentConverter.js | 90 +++++++- .../list/documentFilesListRenderer.html | 8 +- .../list/documentFilesListRenderer.js | 13 +- .../material/resolveMaterialReference.js | 24 +- .../methods/sendResearchConfirmationEmail.js | 6 +- .../documentList/documentResultsRenderer.html | 9 +- .../documentList/documentResultsRenderer.js | 32 +++ .../tasks/results/TaskWorkingState.js | 207 ------------------ .../results/tests/TaskWorkingState.tests.js | 3 +- .../contexts/tasks/state/TaskWorkingState.js | 93 ++++++++ .../contexts/tasks/state/methods/byLesson.js | 12 + .../state/methods/getMyTaskWorkingState.js | 19 ++ .../state/methods/saveTaskWorkingState.js | 96 ++++++++ .../collection/ClientCollection.js | 87 -------- .../collection/collectionHooks.js | 1 + .../collection/tests/collectionHooks.tests.js | 2 +- .../infrastructure/context/ContextRegistry.js | 4 +- .../datastructures/ContextBuilder.js | 8 +- .../infrastructure/loading/lazyInitialize.js | 10 +- .../loading/loadIntoCollection.js | 25 ++- .../pipelines/client/buildPipeline.js | 1 + .../pipelines/server/buildPipeline.js | 5 +- src/imports/startup/both/student/contexts.js | 2 +- src/imports/startup/both/teacher/contexts.js | 3 +- src/imports/startup/client/all/profile.js | 2 - .../startup/client/contexts/initContext.js | 2 +- src/imports/startup/client/index.js | 8 +- .../startup/client/minimal/bootstrap.js | 8 +- src/imports/startup/client/minimal/helpers.js | 7 +- src/imports/startup/client/minimal/logos.js | 5 +- .../startup/client/minimal/serviceWorker.js | 5 +- src/imports/startup/client/teacher/beamer.js | 2 +- src/imports/startup/client/teacher/routes.js | 11 +- src/imports/startup/server/accounts/inform.js | 32 ++- .../startup/server/accounts/userPresence.js | 6 +- src/imports/startup/server/contexts/build.js | 1 + .../startup/server/contexts/classroom.js | 2 +- src/imports/startup/server/sync/cleanFiles.js | 18 +- src/imports/startup/server/sync/sync.js | 17 +- src/imports/startup/server/system/defaults.js | 1 - .../patches/removeMaterialReferences.js | 15 +- .../system/patches/renameAdminsCollection.js | 2 + .../system/patches/renameFullImagePaths.js | 1 + .../server/system/patches/renameImageFiles.js | 3 + .../startup/server/system/rateLimit.js | 1 + src/imports/startup/server/system/settings.js | 8 +- .../ui/blaze/initTemplateDependencies.js | 25 ++- .../components/documentState/documentState.js | 1 - .../ui/components/download/downloadButton.js | 1 + .../groupbuilder/api/editGroupSchema.js | 2 +- .../pdfViewer/PDFViewerApplication.js | 3 - .../student/task/status/taskWorkingStatus.js | 4 +- .../ui/controllers/document/callMethod.js | 25 ++- .../curriculum/view/objectives/objectives.js | 1 + .../curriculum/view/pockets/pockets.js | 1 + .../editors/task/pagecontent/pageContent.js | 36 +-- .../ui/editors/task/units/taskUnits.js | 3 +- .../editors/unit/views/basicinfo/basicInfo.js | 4 +- .../unit/views/material/MaterialSubviews.js | 1 - ...> createSelectableMaterialEntriesQuery.js} | 12 +- .../editors/unit/views/material/material.js | 4 +- .../ui/editors/unit/views/phases/phases.js | 4 +- src/imports/ui/forms/doclist/docList.js | 1 + src/imports/ui/forms/unit/unitSelect.html | 2 +- src/imports/ui/forms/unit/unitSelect.js | 11 +- .../{users.html => userGroupSelect.html} | 0 .../users/{users.js => userGroupSelect.js} | 17 +- .../{users.scss => userGroupSelect.scss} | 0 .../ui/generic/actionbutton/actionbutton.js | 4 - .../ui/generic/disconnected/disconnected.js | 8 +- src/imports/ui/generic/fail/fail.js | 1 - src/imports/ui/layout/submenu/submenu.js | 2 - src/imports/ui/pages/admin/admin.js | 1 + src/imports/ui/pages/admin/adminSubsKey.js | 1 - .../ui/pages/admin/views/users/users.js | 6 +- src/imports/ui/pages/lesson/lesson.js | 2 +- .../ui/pages/lesson/views/info/groupForms.js | 1 - .../lesson/views/material/lessonMaterial.js | 7 +- .../taskResultTable/taskResultTable.js | 3 +- .../lesson/views/progress/taskProgress.js | 4 +- .../lesson/views/safeguard/lessonSafeguard.js | 2 +- src/imports/ui/pages/lessons/lessons.js | 1 - src/imports/ui/pages/prepare/prepare.js | 4 +- .../ui/pages/prepare/views/create/create.js | 5 - src/imports/ui/pages/present/present.js | 10 +- src/imports/ui/pages/profile/profile.js | 2 +- .../student/material/task/ItemHandlers.js | 2 +- .../ui/pages/student/material/task/task.js | 2 +- .../ui/renderer/text/views/richtext.js | 4 +- src/package-lock.json | 6 +- src/package.json | 2 +- src/resources/i18n/de.json | 2 +- src/resources/i18n/en.json | 2 +- src/resources/i18n/tr.json | 2 +- 98 files changed, 658 insertions(+), 533 deletions(-) create mode 100644 src/imports/api/utils/filesystem/fileExists.js delete mode 100644 src/imports/contexts/tasks/results/TaskWorkingState.js create mode 100644 src/imports/contexts/tasks/state/TaskWorkingState.js create mode 100644 src/imports/contexts/tasks/state/methods/byLesson.js create mode 100644 src/imports/contexts/tasks/state/methods/getMyTaskWorkingState.js create mode 100644 src/imports/contexts/tasks/state/methods/saveTaskWorkingState.js delete mode 100644 src/imports/infrastructure/collection/ClientCollection.js rename src/imports/ui/editors/unit/views/material/helpers/{selectEntries.js => createSelectableMaterialEntriesQuery.js} (88%) rename src/imports/ui/forms/users/{users.html => userGroupSelect.html} (100%) rename src/imports/ui/forms/users/{users.js => userGroupSelect.js} (94%) rename src/imports/ui/forms/users/{users.scss => userGroupSelect.scss} (100%) delete mode 100644 src/imports/ui/pages/admin/adminSubsKey.js diff --git a/src/imports/api/csp/cspOptions.js b/src/imports/api/csp/cspOptions.js index a1af31e..8923905 100644 --- a/src/imports/api/csp/cspOptions.js +++ b/src/imports/api/csp/cspOptions.js @@ -44,11 +44,12 @@ export function cspOptions (externalHostUrls = []) { const opt = { crossOriginEmbedderPolicy: false, - // crossOriginOpenerPolicy: false, + crossOriginOpenerPolicy: { policy: 'unsafe-none' }, crossOriginResourcePolicy: { policy: 'cross-origin' }, contentSecurityPolicy: { blockAllMixedContent: true, directives: { + upgradeInSecureRequests: Meteor.isDevelopment ? null : [], defaultSrc: [self], scriptSrc: [ self, diff --git a/src/imports/api/utils/filesystem/fileExists.js b/src/imports/api/utils/filesystem/fileExists.js new file mode 100644 index 0000000..be2cb4b --- /dev/null +++ b/src/imports/api/utils/filesystem/fileExists.js @@ -0,0 +1,23 @@ +let fs + +/** + * Full async version using fs.stat + * @param path + * @return {Promise} + */ +export const fileExists = function exists (path) { + return new Promise((resolve, reject) => { + if (!fs) fs = require('fs') + fs.stat(path, (err, stats) => { + if (err) { + reject(err) + } + else if (!stats) { + reject(new Error()) + } + else { + resolve(stats) + } + }) + }) +} diff --git a/src/imports/contexts/classroom/lessons/runtime/removeDocuments.js b/src/imports/contexts/classroom/lessons/runtime/removeDocuments.js index ceeaa40..076db76 100644 --- a/src/imports/contexts/classroom/lessons/runtime/removeDocuments.js +++ b/src/imports/contexts/classroom/lessons/runtime/removeDocuments.js @@ -1,6 +1,6 @@ import { check } from 'meteor/check' import { TaskResults } from '../../../tasks/results/TaskResults' -import { TaskWorkingState } from '../../../tasks/results/TaskWorkingState' +import { TaskWorkingState } from '../../../tasks/state/TaskWorkingState' import { createRemoveDoc } from '../../../../api/utils/documentUtils' diff --git a/src/imports/contexts/classroom/lessons/tests/LessonRuntime.tests.js b/src/imports/contexts/classroom/lessons/tests/LessonRuntime.tests.js index 0cfbba9..b1e7575 100644 --- a/src/imports/contexts/classroom/lessons/tests/LessonRuntime.tests.js +++ b/src/imports/contexts/classroom/lessons/tests/LessonRuntime.tests.js @@ -7,7 +7,7 @@ import { TaskResults } from '../../../tasks/results/TaskResults' import { Random } from 'meteor/random' import { mockCollection } from '../../../../../tests/testutils/mockCollection' import { expect } from 'chai' -import { TaskWorkingState } from '../../../tasks/results/TaskWorkingState' +import { TaskWorkingState } from '../../../tasks/state/TaskWorkingState' import { Cluster } from '../../../tasks/responseProcessors/aggregate/cluster/Cluster' import { getCollection } from '../../../../api/utils/getCollection' import { ImageFiles } from '../../../files/image/ImageFiles' diff --git a/src/imports/contexts/files/document/converter/documentConverter.js b/src/imports/contexts/files/document/converter/documentConverter.js index 525fbd9..de9f9bb 100644 --- a/src/imports/contexts/files/document/converter/documentConverter.js +++ b/src/imports/contexts/files/document/converter/documentConverter.js @@ -1 +1,89 @@ -export const documentConverter = fileRef => fileRef +import { Meteor } from 'meteor/meteor' +import { createLog } from '../../../../api/log/createLog' +import { fileExists } from '../../../../api/utils/filesystem/fileExists' + +let fs +let im + +// convert -density 150 presentation.pdf[0] -quality 90 test.jpg +export const documentConverter = async function (fileRef, options){ + if (!fileRef.isPDF) { + return fileRef + } + + const filesCollection = this + const exists = await fileExists(fileRef.path) + + if (!exists) { + throw Meteor.Error('upload.convertError') + } + + if (!im) im = require('gm').subClass({imageMagick: true}) + + let document + const thumbnailPath = (filesCollection.storagePath(fileRef)) + '/thumbnail-' + fileRef._id + '.png' + + try { + document = im(fileRef.path) + .selectFrame(0) + .define('filter:support=2') + .define('png:compression-filter=5') + .define('png:compression-level=9') + .define('png:compression-strategy=1') + .define('png:exclude-chunk=all') + .noProfile() + .strip() + .dither(false) + .interlace('Line') + .filter('Triangle') + .resize('50%') + .interlace('Line') + } catch (imError) { + // if we catch an error here we skip the rest as we have nothing + // created neither on disk nor in the database + console.error(imError) + return fileRef + } + + try { + await gmexec(document, document.write, thumbnailPath) + } catch (imErr) { + console.error(imErr) + return fileRef + } + + const stat = await fileExists(thumbnailPath) + const thumbImage = im(thumbnailPath) + const imgInfo = await gmexec(thumbImage, thumbImage.size) + fileRef.versions.thumbnail = { + path: thumbnailPath, + size: stat.size, + type: fileRef.type, + extension: fileRef.extension, + meta: { + width: imgInfo.width, + height: imgInfo.height + } + } + + // update the files doc + const updateDoc = { $set: {} } + updateDoc.$set['versions.thumbnail'] = fileRef.versions.thumbnail + await filesCollection.collection.update(fileRef._id, updateDoc) + + return fileRef +} + +function gmexec (thisObj, fct, ...args) { + return new Promise((resolve, reject) => { + args.push((err, res) => { + if (err) { + reject(err) + } + else { + resolve(res) + } + }) + fct.call(thisObj, ...args) + }) +} \ No newline at end of file diff --git a/src/imports/contexts/files/document/renderer/list/documentFilesListRenderer.html b/src/imports/contexts/files/document/renderer/list/documentFilesListRenderer.html index 29d7e8a..df2d88a 100644 --- a/src/imports/contexts/files/document/renderer/list/documentFilesListRenderer.html +++ b/src/imports/contexts/files/document/renderer/list/documentFilesListRenderer.html @@ -1,5 +1,11 @@ \ No newline at end of file diff --git a/src/imports/ui/forms/unit/unitSelect.js b/src/imports/ui/forms/unit/unitSelect.js index 37ebc3a..6f32c99 100644 --- a/src/imports/ui/forms/unit/unitSelect.js +++ b/src/imports/ui/forms/unit/unitSelect.js @@ -94,9 +94,6 @@ Template.afUnitSelect.helpers({ availablePockets () { return Template.getState('availablePockets') }, - lesson (unitId) { - const { classId } = Template.currentData().atts - }, pocketCtx () { return Pocket }, @@ -140,6 +137,8 @@ Template.afUnitSelect.events({ } }) -const updateHidden = ({ templateInstance }) => templateInstance - .$('.hidden-input') - .val(templateInstance.state.get('selectedUnit')) +const updateHidden = ({ templateInstance }) => { + templateInstance + .$('input[data-unitselect-hidden=""]') + .val(templateInstance.state.get('selectedUnit')) +} diff --git a/src/imports/ui/forms/users/users.html b/src/imports/ui/forms/users/userGroupSelect.html similarity index 100% rename from src/imports/ui/forms/users/users.html rename to src/imports/ui/forms/users/userGroupSelect.html diff --git a/src/imports/ui/forms/users/users.js b/src/imports/ui/forms/users/userGroupSelect.js similarity index 94% rename from src/imports/ui/forms/users/users.js rename to src/imports/ui/forms/users/userGroupSelect.js index 9ddb96a..4f693f5 100644 --- a/src/imports/ui/forms/users/users.js +++ b/src/imports/ui/forms/users/userGroupSelect.js @@ -5,25 +5,18 @@ import { Schema } from '../../../api/schema/Schema' import { dataTarget } from '../../utils/dataTarget' import { formIsValid } from '../../components/forms/formUtils' import './autoform' -import './users.scss' -import './users.html' +import './userGroupSelect.scss' +import './userGroupSelect.html' -// TODO rename this file users.js to userGroupSelect - -const API = Template.afUserGroupSelect.setDependencies() +Template.afUserGroupSelect.setDependencies() Template.afUserGroupSelect.onCreated(function () { const instance = this instance.state.set('selectedUsers', []) - instance.autorun(() => { - const data = Template.currentData() - console.debug(data) - }) - - const { minCount, maxCount } = instance.data + // const { minCount, maxCount } = instance.data const { builder, allMaterial } = instance.data.atts - const { users = [], roles = [], material = [], maxUsers, maxGroups, materialForAllGroups } = builder + const { users = [], roles = [], material = [], maxUsers, materialForAllGroups } = builder const materialOptions = (allMaterial || []).filter(opt => material.includes(opt.value)) instance.builder = builder diff --git a/src/imports/ui/forms/users/users.scss b/src/imports/ui/forms/users/userGroupSelect.scss similarity index 100% rename from src/imports/ui/forms/users/users.scss rename to src/imports/ui/forms/users/userGroupSelect.scss diff --git a/src/imports/ui/generic/actionbutton/actionbutton.js b/src/imports/ui/generic/actionbutton/actionbutton.js index 125c61f..8422f67 100644 --- a/src/imports/ui/generic/actionbutton/actionbutton.js +++ b/src/imports/ui/generic/actionbutton/actionbutton.js @@ -16,10 +16,6 @@ Template.actionButton.helpers({ } }) return dataAtts - }, - useToolip () { - const data = Template.instance().data - return data.title && data.tooltip !== false } }) diff --git a/src/imports/ui/generic/disconnected/disconnected.js b/src/imports/ui/generic/disconnected/disconnected.js index ffb4564..00e8682 100644 --- a/src/imports/ui/generic/disconnected/disconnected.js +++ b/src/imports/ui/generic/disconnected/disconnected.js @@ -3,21 +3,23 @@ import { Template } from 'meteor/templating' import '../icon/icon' import './disconnected.html' +const API = Template.disconnected.setDependencies() + Template.disconnected.onRendered(function () { const instance = this instance.autorun(() => { const status = Meteor.status() if (!status.connected) { - console.info('[Connection]: disconnected, check in 1000ms if still disconnected') + API.log('[Connection]: disconnected, check in 1000ms if still disconnected') setTimeout(() => { if (Meteor.status().connected === false) { - console.info('[Connection]: still disconnected') + API.log('[Connection]: still disconnected') instance.$('#global-disconnected-modal').modal('show') } }, 1000) } else { - console.info('[Connection]: connected') + API.log('[Connection]: connected') setTimeout(() => { instance.$('#global-disconnected-modal').modal('hide') }, 1000) diff --git a/src/imports/ui/generic/fail/fail.js b/src/imports/ui/generic/fail/fail.js index 0f57285..521eb6e 100644 --- a/src/imports/ui/generic/fail/fail.js +++ b/src/imports/ui/generic/fail/fail.js @@ -3,7 +3,6 @@ import './fail.html' Template.fail.helpers({ title (err) { - console.info(err) return err.error || err.message } }) diff --git a/src/imports/ui/layout/submenu/submenu.js b/src/imports/ui/layout/submenu/submenu.js index f78e098..c622457 100644 --- a/src/imports/ui/layout/submenu/submenu.js +++ b/src/imports/ui/layout/submenu/submenu.js @@ -1,5 +1,4 @@ import { Template } from 'meteor/templating' -import { ReactiveDict } from 'meteor/reactive-dict' import { dataTarget } from '../../utils/dataTarget' import { UserUtils } from '../../../contexts/system/accounts/users/UserUtils' import './submenu.html' @@ -65,7 +64,6 @@ Template.submenu.helpers({ }, navAtts () { const { data } = Template.instance() - console.debug('navAtts', data) const justified = data.justified || data?.nav?.justified ? 'nav-justified' : '' diff --git a/src/imports/ui/pages/admin/admin.js b/src/imports/ui/pages/admin/admin.js index 2416d25..93791b1 100644 --- a/src/imports/ui/pages/admin/admin.js +++ b/src/imports/ui/pages/admin/admin.js @@ -1,3 +1,4 @@ +import { Meteor } from 'meteor/meteor' import { Template } from 'meteor/templating' import { AdminViewStates } from './adminViewStates' import { Form } from '../../components/forms/Form' diff --git a/src/imports/ui/pages/admin/adminSubsKey.js b/src/imports/ui/pages/admin/adminSubsKey.js deleted file mode 100644 index 884a9cd..0000000 --- a/src/imports/ui/pages/admin/adminSubsKey.js +++ /dev/null @@ -1 +0,0 @@ -export const adminSubsKey = 'adminSubsKey' diff --git a/src/imports/ui/pages/admin/views/users/users.js b/src/imports/ui/pages/admin/views/users/users.js index 00469ee..2185b86 100644 --- a/src/imports/ui/pages/admin/views/users/users.js +++ b/src/imports/ui/pages/admin/views/users/users.js @@ -1,5 +1,4 @@ import { Meteor } from 'meteor/meteor' -import { Mongo } from 'meteor/mongo' import { Template } from 'meteor/templating' import { Tracker } from 'meteor/tracker' import { Admin } from '../../../../../contexts/system/accounts/admin/Admin' @@ -86,7 +85,6 @@ Template.adminUsers.onCreated(function () { callbacks: { onError: API.fatal, onReady: () => { - console.debug(Meteor.users.find().fetch()) setTimeout(() => instance.state.set('loadUsers', false), 300) } } @@ -105,9 +103,7 @@ Template.adminUsers.helpers({ return Template.getState('loadUsers') }, getUsers (institution) { - const cursor = Meteor.users.find({ institution }) - console.debug(institution, Template.getState('showInst'), institution === Template.getState('showInst'), cursor.count()) - return cursor + return Meteor.users.find({ institution }) }, getUserEmail (user) { return user && user.emails ? user.emails[0].address : '' diff --git a/src/imports/ui/pages/lesson/lesson.js b/src/imports/ui/pages/lesson/lesson.js index 5357182..1c16eeb 100644 --- a/src/imports/ui/pages/lesson/lesson.js +++ b/src/imports/ui/pages/lesson/lesson.js @@ -8,7 +8,7 @@ import { LessonMaterial } from '../../controllers/LessonMaterial' import { Users } from '../../../contexts/system/accounts/users/User' import { TimeUnit } from '../../../contexts/curriculum/curriculum/types/TimeUnit' import { Group } from '../../../contexts/classroom/group/Group' -import { TaskWorkingState } from '../../../contexts/tasks/results/TaskWorkingState' +import { TaskWorkingState } from '../../../contexts/tasks/state/TaskWorkingState' import { LessonStates } from '../../../contexts/classroom/lessons/LessonStates' import { ProfileImages } from '../../../contexts/files/image/ProfileImages' import { Unit } from '../../../contexts/curriculum/curriculum/unit/Unit' diff --git a/src/imports/ui/pages/lesson/views/info/groupForms.js b/src/imports/ui/pages/lesson/views/info/groupForms.js index 03a61bd..e78ab67 100644 --- a/src/imports/ui/pages/lesson/views/info/groupForms.js +++ b/src/imports/ui/pages/lesson/views/info/groupForms.js @@ -47,7 +47,6 @@ export const createGroupForms = ({ translate, onError }) => { }) const toGroupViewDoc = ({ groupDoc, material }) => { - console.debug(groupDoc) const finalDoc = { ...groupDoc } finalDoc.users = groupDoc.users .map(({ userId, role }) => { diff --git a/src/imports/ui/pages/lesson/views/material/lessonMaterial.js b/src/imports/ui/pages/lesson/views/material/lessonMaterial.js index 12e4789..1c1616a 100644 --- a/src/imports/ui/pages/lesson/views/material/lessonMaterial.js +++ b/src/imports/ui/pages/lesson/views/material/lessonMaterial.js @@ -1,4 +1,3 @@ -import { Meteor } from 'meteor/meteor' import { Template } from 'meteor/templating' import { ReactiveDict } from 'meteor/reactive-dict' import { ReactiveVar } from 'meteor/reactive-var' @@ -448,9 +447,6 @@ Template.lessonMaterial.events({ }) }, 500) } - else { - - } }, // =========================================================================== @@ -508,8 +504,7 @@ Template.lessonMaterial.events({ args: { lessonId }, key: lessonSubKey, callbacks: { - onError: API.notify, - onReady: () => console.debug('results ready', getCollection(TaskResults.name).find().fetch()) + onError: API.notify } }) diff --git a/src/imports/ui/pages/lesson/views/material/taskResultTable/taskResultTable.js b/src/imports/ui/pages/lesson/views/material/taskResultTable/taskResultTable.js index d901268..b4a7b9f 100644 --- a/src/imports/ui/pages/lesson/views/material/taskResultTable/taskResultTable.js +++ b/src/imports/ui/pages/lesson/views/material/taskResultTable/taskResultTable.js @@ -9,9 +9,8 @@ import { $in } from '../../../../../../api/utils/query/inSelector' import { getCollection } from '../../../../../../api/utils/getCollection' import { getAllItemsInTask } from '../../../../../../contexts/tasks/getAllItemsInTask' import { dataTarget } from '../../../../../utils/dataTarget' -import './taskResulTable.html' -import { Group } from '../../../../../../contexts/classroom/group/Group' import { GroupMode } from '../../../../../../contexts/classroom/group/GroupMode' +import './taskResulTable.html' const API = Template.taskResultTable.setDependencies({}) Item.initialize().catch(API.notify) diff --git a/src/imports/ui/pages/lesson/views/progress/taskProgress.js b/src/imports/ui/pages/lesson/views/progress/taskProgress.js index c306203..fdf1090 100644 --- a/src/imports/ui/pages/lesson/views/progress/taskProgress.js +++ b/src/imports/ui/pages/lesson/views/progress/taskProgress.js @@ -1,11 +1,11 @@ import { Template } from 'meteor/templating' import { TaskResults } from '../../../../../contexts/tasks/results/TaskResults' -import { TaskWorkingState } from '../../../../../contexts/tasks/results/TaskWorkingState' +import { TaskWorkingState } from '../../../../../contexts/tasks/state/TaskWorkingState' import { getCollection } from '../../../../../api/utils/getCollection' import '../../../../components/profileImage/profileImage' import './taskProgress.html' -const API = Template.taskProgress.setDependencies({ +Template.taskProgress.setDependencies({ contexts: [TaskWorkingState, TaskResults] }) diff --git a/src/imports/ui/pages/lesson/views/safeguard/lessonSafeguard.js b/src/imports/ui/pages/lesson/views/safeguard/lessonSafeguard.js index b02efae..6d3f3d2 100644 --- a/src/imports/ui/pages/lesson/views/safeguard/lessonSafeguard.js +++ b/src/imports/ui/pages/lesson/views/safeguard/lessonSafeguard.js @@ -3,7 +3,7 @@ import { Task } from '../../../../../contexts/curriculum/curriculum/task/Task' import './lessonSafeguard.html' import { getCollection } from '../../../../../api/utils/getCollection' -const API = Template.lessonSafeguard.setDependencies({ +Template.lessonSafeguard.setDependencies({ contexts: [Task] }) const TaskCollection = getCollection(Task.name) diff --git a/src/imports/ui/pages/lessons/lessons.js b/src/imports/ui/pages/lessons/lessons.js index f34490e..e7f14e5 100644 --- a/src/imports/ui/pages/lessons/lessons.js +++ b/src/imports/ui/pages/lessons/lessons.js @@ -81,7 +81,6 @@ Template.lessons.onCreated(async function () { collection: UnitCollection, failure: API.fatal, success: unitDocs => { - console.debug(unitDocs) computation.stop() instance.state.set('unitsLoaded', true) } diff --git a/src/imports/ui/pages/prepare/prepare.js b/src/imports/ui/pages/prepare/prepare.js index 24707de..d5c51d0 100644 --- a/src/imports/ui/pages/prepare/prepare.js +++ b/src/imports/ui/pages/prepare/prepare.js @@ -10,11 +10,11 @@ import { setQueryParams } from '../../../api/routes/params/setQueryParams' import { getQueryParam } from '../../../api/routes/params/getQueryParam' import { loadIntoCollection } from '../../../infrastructure/loading/loadIntoCollection' import { getLocalCollection } from '../../../infrastructure/collection/getLocalCollection' +import { loadSelectableUnits } from '../../../contexts/curriculum/loadSelectableUnits' import prepareLanguage from './i18n/prepareLanguage' import '../../layout/submenu/submenu' import '../../generic/templateLoader/TemplateLoader' import './prepare.html' -import { loadSelectableUnits } from '../../../contexts/curriculum/loadSelectableUnits' const viewStates = Object.values(PrepareViewStates) const allContexts = [Pocket, Unit, Dimension] @@ -79,7 +79,7 @@ Template.prepare.helpers({ }, submenuData () { const instance = Template.instance() - console.debug('submenu data') + return { views: viewStates, queryParam: 'view', diff --git a/src/imports/ui/pages/prepare/views/create/create.js b/src/imports/ui/pages/prepare/views/create/create.js index ce31149..b64c2da 100644 --- a/src/imports/ui/pages/prepare/views/create/create.js +++ b/src/imports/ui/pages/prepare/views/create/create.js @@ -1,10 +1,8 @@ -/* global AutoForm */ import { Meteor } from 'meteor/meteor' import { Template } from 'meteor/templating' import { Wizard } from '../../../../../api/wizard/Wizard' import { SchoolClass } from '../../../../../contexts/classroom/schoolclass/SchoolClass' -import { Schema, ErrorTypes } from '../../../../../api/schema/Schema' import { Pocket } from '../../../../../contexts/curriculum/curriculum/pocket/Pocket' import { Unit } from '../../../../../contexts/curriculum/curriculum/unit/Unit' import { Dimension } from '../../../../../contexts/curriculum/curriculum/dimension/Dimension' @@ -15,7 +13,6 @@ import { i18n } from '../../../../../api/language/language' import { Material } from '../../../../../contexts/material/Material' import { LessonStates } from '../../../../../contexts/classroom/lessons/LessonStates' -import { formIsValid } from '../../../../components/forms/formUtils' import { cursor } from '../../../../../api/utils/cursor' import { dataTarget } from '../../../../utils/dataTarget' import { findUnassociatedMaterial } from '../../../../../api/utils/findUnassociatedMaterial' @@ -220,7 +217,6 @@ Template.createClass.helpers({ return LessonStates.isIdle(lessonDoc) }, unitsForPocket (pocketId) { - const userId = Meteor.userId() const disabledDimensions = Template.getState('disabledDimensions') const selectedUnit = Template.getState('selectedUnit') const query = { @@ -271,7 +267,6 @@ Template.createClass.helpers({ return Template.getState('unassociatedMaterial') }, reference (entry) { - console.debug({ entry }) const ctx = Material.get(entry.collection || entry.type) return { ...ctx, diff --git a/src/imports/ui/pages/present/present.js b/src/imports/ui/pages/present/present.js index 1c4d390..6575cfa 100644 --- a/src/imports/ui/pages/present/present.js +++ b/src/imports/ui/pages/present/present.js @@ -293,15 +293,19 @@ Template.present.onCreated(function () { } // TaskDoc = ref.document - // search in the task doc for the current item and skip where possible + // search in the task doc for the current item and skip, if possible const { itemId } = ref ref.document.pages.some(page => { - if (!page.content) return + if (!page.content) return false const entry = page.content.find(entry => entry.itemId === itemId) + if (entry) { - return referenceQueue.push({ ref, entry }) + referenceQueue.push({ ref, entry }) + return true } + + return false }) }) diff --git a/src/imports/ui/pages/profile/profile.js b/src/imports/ui/pages/profile/profile.js index f0971e4..b64dd78 100644 --- a/src/imports/ui/pages/profile/profile.js +++ b/src/imports/ui/pages/profile/profile.js @@ -281,7 +281,7 @@ Template.userProfile.events({ }, 'change .research-option-select' (event, templateInstance) { const form = templateInstance.$('#researchOptionsForm').get(0) - const formData = new FormData(form) + const formData = new window.FormData(form) const participateVisible = Array.from(formData.values()).every(val => !!val) templateInstance.state.set({ participateVisible }) }, diff --git a/src/imports/ui/pages/student/material/task/ItemHandlers.js b/src/imports/ui/pages/student/material/task/ItemHandlers.js index 63cd9bf..3d25f51 100644 --- a/src/imports/ui/pages/student/material/task/ItemHandlers.js +++ b/src/imports/ui/pages/student/material/task/ItemHandlers.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor' import { TaskResults } from '../../../../../contexts/tasks/results/TaskResults' -import { TaskWorkingState } from '../../../../../contexts/tasks/results/TaskWorkingState' +import { TaskWorkingState } from '../../../../../contexts/tasks/state/TaskWorkingState' import { fromResponse, toResponse diff --git a/src/imports/ui/pages/student/material/task/task.js b/src/imports/ui/pages/student/material/task/task.js index 22daae7..7bb7889 100644 --- a/src/imports/ui/pages/student/material/task/task.js +++ b/src/imports/ui/pages/student/material/task/task.js @@ -4,7 +4,7 @@ import { Unit } from '../../../../../contexts/curriculum/curriculum/unit/Unit' import { Lesson } from '../../../../../contexts/classroom/lessons/Lesson' import { Task } from '../../../../../contexts/curriculum/curriculum/task/Task' import { TaskResults } from '../../../../../contexts/tasks/results/TaskResults' -import { TaskWorkingState } from '../../../../../contexts/tasks/results/TaskWorkingState' +import { TaskWorkingState } from '../../../../../contexts/tasks/state/TaskWorkingState' import { TaskDefinitions } from '../../../../../contexts/tasks/definitions/TaskDefinitions' import { Group } from '../../../../../contexts/classroom/group/Group' import { Files } from '../../../../../contexts/files/Files' diff --git a/src/imports/ui/renderer/text/views/richtext.js b/src/imports/ui/renderer/text/views/richtext.js index 261294f..3e2509b 100644 --- a/src/imports/ui/renderer/text/views/richtext.js +++ b/src/imports/ui/renderer/text/views/richtext.js @@ -19,8 +19,6 @@ Template.textRendererrt.helpers({ .removeAttr('src') }) } - const appendedHtml = $('
').append($rt).html() - // console.log(appendedHtml) - return appendedHtml + return $('
').append($rt).html() } }) diff --git a/src/package-lock.json b/src/package-lock.json index d5d481e..f5ba128 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -2927,9 +2927,9 @@ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "helmet": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz", - "integrity": "sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg==" + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-6.0.0.tgz", + "integrity": "sha512-FO9RpR1wNJepH/GbLPQVtkE2eESglXL641p7SdyoT4LngHFJcZheHMoyUcjCZF4qpuMMO1u5q6RK0l9Ux8JBcg==" }, "hosted-git-info": { "version": "4.1.0", diff --git a/src/package.json b/src/package.json index f02959c..bda6e15 100644 --- a/src/package.json +++ b/src/package.json @@ -38,7 +38,7 @@ "file-type": "^16.5.4", "gm": "^1.23.1", "gridfs-stream": "^1.1.1", - "helmet": "^5.0.2", + "helmet": "6.0.0", "interactjs": "^1.10.11", "jquery": "^3.6.0", "lozad": "^1.16.0", diff --git a/src/resources/i18n/de.json b/src/resources/i18n/de.json index 56bd9df..8d6a15e 100644 --- a/src/resources/i18n/de.json +++ b/src/resources/i18n/de.json @@ -49,7 +49,7 @@ "inform": { "passwordReset": { "subject": "{{siteName}} info: Passwort wurde zurückgesetzt", - "text": "Hallo,\n\nfolgender User hat das Password zurückgesetzt bzw. neu gesetzt: {{name}}. Dies ist eine automatische Mail vom {{siteName}} system." + "text": "Hallo,\n\nfolgender User hat das Password zurückgesetzt bzw. neu gesetzt: {{name}} ({{institution}}). Dies ist eine automatische Mail vom {{siteName}} system." } } }, diff --git a/src/resources/i18n/en.json b/src/resources/i18n/en.json index aefe3f0..859fd4a 100644 --- a/src/resources/i18n/en.json +++ b/src/resources/i18n/en.json @@ -49,7 +49,7 @@ "inform": { "passwordReset": { "subject": "{{siteName}} password reset", - "text": "Hello,\n\n{{name}} has reset their password on {{siteName}}." + "text": "Hello,\n\n{{name}} ({{institution}}) has reset their password on {{siteName}}." } } }, diff --git a/src/resources/i18n/tr.json b/src/resources/i18n/tr.json index 27ccefb..139179c 100644 --- a/src/resources/i18n/tr.json +++ b/src/resources/i18n/tr.json @@ -49,7 +49,7 @@ "inform": { "passwordReset": { "subject": "{{siteName}} Bilgi: Şifreyi sıfırla", - "text": "Merhaba,\n\naşağıdaki kullanıcı şifreyi sıfırladı: {{name}}. Bu, {{siteName}} sisteminden gelen otomatik bir postadır." + "text": "Merhaba,\n\naşağıdaki kullanıcı şifreyi sıfırladı: {{name}} ({{institution}}). Bu, {{siteName}} sisteminden gelen otomatik bir postadır." } } }, From 93ba85ebd067b83bc344f3812aff006b90913ef9 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Mon, 21 Nov 2022 17:07:37 +0100 Subject: [PATCH 02/24] fix: lint fix --- .../accounts/emailTemplates/resetPassword.js | 6 +- .../accounts/emailTemplates/verifyEmail.js | 6 +- .../api/accounts/registration/UserFactory.js | 26 ++- .../accounts/registration/createUserSchema.js | 4 + src/imports/api/config/Features.js | 4 +- src/imports/api/log/createLog.js | 2 +- src/imports/api/utils/insertUpdate.js | 18 ++- src/imports/contexts/beamer/Beamer.js | 4 +- .../contexts/beamer/tests/Beamer.tests.js | 1 - src/imports/contexts/classroom/group/Group.js | 8 +- .../contexts/classroom/lessons/Lesson.js | 2 +- .../contexts/curriculum/utils/formfactory.js | 8 +- .../contexts/files/image/ImageFiles.js | 4 +- .../video/renderer/main/videoFileRenderer.js | 2 - .../contexts/material/materialPipeline.js | 2 - src/imports/contexts/sync/SyncPipeline.js | 40 ++--- .../contexts/system/accounts/users/User.js | 5 +- .../accounts/users/methods/setResearch.js | 1 - .../system/accounts/users/usersByClass.js | 1 + .../contexts/tasks/getAllItemsInTask.js | 6 + .../ResponseProcessorRegistry.js | 1 - .../audioList/audioResultsRenderer.js | 4 +- .../contexts/tasks/results/TaskResults.js | 149 +----------------- .../results/methods/getAllTaskByGroup.js | 30 ++++ .../methods/getAllTaskResultsByTask.js | 36 +++++ .../results/methods/getAllTasksByItem.js | 29 ++++ .../tasks/results/methods/saveTaskResult.js | 74 +++++++++ .../contexts/tasks/state/TaskWorkingState.js | 2 - 28 files changed, 268 insertions(+), 207 deletions(-) create mode 100644 src/imports/contexts/tasks/results/methods/getAllTaskByGroup.js create mode 100644 src/imports/contexts/tasks/results/methods/getAllTaskResultsByTask.js create mode 100644 src/imports/contexts/tasks/results/methods/getAllTasksByItem.js create mode 100644 src/imports/contexts/tasks/results/methods/saveTaskResult.js diff --git a/src/imports/api/accounts/emailTemplates/resetPassword.js b/src/imports/api/accounts/emailTemplates/resetPassword.js index 2ca6a44..54ae8aa 100644 --- a/src/imports/api/accounts/emailTemplates/resetPassword.js +++ b/src/imports/api/accounts/emailTemplates/resetPassword.js @@ -1,6 +1,9 @@ import { i18n } from '../../language/language' import { Meteor } from 'meteor/meteor' import { getCredentialsAsBuffer, getFullName } from './common' +import {createLog} from '../../log/createLog' + +const debug = createLog({ name: 'email/resetPassword', type: 'debug' }) export const getResetPasswordSubject = ({ siteName, defaultLocale }) => user => { const locale = user?.locale || defaultLocale @@ -21,8 +24,7 @@ export const getResetPasswordText = ({ expiration, defaultLocale, supportEmail } const text = i18n.get(locale, 'accounts.resetPassword.text', textOptions) if (Meteor.isDevelopment && !Meteor.isTest) { - console.debug(textOptions.url) - console.debug(text) + debug('body', textOptions.url, text) } return text diff --git a/src/imports/api/accounts/emailTemplates/verifyEmail.js b/src/imports/api/accounts/emailTemplates/verifyEmail.js index 52909b9..731117f 100644 --- a/src/imports/api/accounts/emailTemplates/verifyEmail.js +++ b/src/imports/api/accounts/emailTemplates/verifyEmail.js @@ -1,6 +1,9 @@ import { Meteor } from 'meteor/meteor' import { i18n } from '../../language/language' import { getFullName } from './common' +import {createLog} from '../../log/createLog' + +const debug = createLog({ name: 'email/verify', type: 'debug' }) export const getVerifyEmailSubject = ({ siteName, defaultLocale }) => user => { const locale = user?.locale || defaultLocale @@ -15,8 +18,7 @@ export const getVeryFyEmailText = ({ defaultLocale, supportEmail }) => (user, or const text = i18n.get(locale, 'accounts.verifyEmail.text', { name, url, supportEmail }) if (Meteor.isDevelopment && !Meteor.isTest) { - console.debug(url) - console.debug(text) + debug(url, text) } return text diff --git a/src/imports/api/accounts/registration/UserFactory.js b/src/imports/api/accounts/registration/UserFactory.js index 8d54cc2..651c8db 100644 --- a/src/imports/api/accounts/registration/UserFactory.js +++ b/src/imports/api/accounts/registration/UserFactory.js @@ -5,14 +5,36 @@ import { Schema } from '../../schema/Schema' import { Roles } from 'meteor/alanning:roles' import { rollbackAccount } from './rollbackAccount' import { userExists } from '../user/userExists' +import {createLog} from '../../log/createLog' -let createSchema +/** + * Creates new user accounts + * @namespace + */ export const UserFactory = {} UserFactory.name = 'UserFactory' +const debug = createLog({ name: UserFactory.name, type: 'debug'}) +let createSchema + +/** + * Creates a new user account by given options. Args are validated. + * @param email {string} + * @param password {string} + * @param role {string} + * @param firstName {string} + * @param lastName {string} + * @param institution {string} + * @param locale {string=} + * @returns {string} the new user's document _id + * @throws {Meteor.Error} if user exists by given Email + * @throws {Meteor.Error} if user has not been created + * @throws {Meteor.Error} if user has not successfully been assigned to given roles + */ UserFactory.create = function create ({ email, password, role, firstName, lastName, institution, locale }) { + debug('create new user', { email, institution, role }) if (!createSchema) { createSchema = Schema.create(createUserSchema) } @@ -64,7 +86,7 @@ UserFactory.create = function create ({ email, password, role, firstName, lastNa throw new Meteor.Error('createUser.failed', 'createUser.profileNotUpdated', email) } - console.debug('add user to roles', userId, [role], institution) + debug('add user to roles', userId, [role], institution) // adds the user to the given roles and scope Roles.addUsersToRoles(userId, [role], institution) if (!Roles.userIsInRole(userId, [role], institution)) { diff --git a/src/imports/api/accounts/registration/createUserSchema.js b/src/imports/api/accounts/registration/createUserSchema.js index e3d2778..3f22b93 100644 --- a/src/imports/api/accounts/registration/createUserSchema.js +++ b/src/imports/api/accounts/registration/createUserSchema.js @@ -9,8 +9,12 @@ import { password2Schema } from './registerUserSchema' +/** @private */ const passwordConfig = PasswordConfig.from(Meteor.settings.public.password) +/** + * Default schema to validate arguments for creating new users. + */ export const createUserSchema = { email: emailSchema(), firstName: firstNameSchema(), diff --git a/src/imports/api/config/Features.js b/src/imports/api/config/Features.js index 76ea85d..b4278ab 100644 --- a/src/imports/api/config/Features.js +++ b/src/imports/api/config/Features.js @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor' const features = Object.create(null) + Object.assign(features, Meteor.settings.public.features) /** @@ -10,8 +11,7 @@ export const Features = {} Features.get = (name) => { if (!name || !Object.hasOwnProperty.call(features, name)) { - console.debug(name, features, features[name]) - throw new Error(`Features has no feature by name ${name}`) + throw new Error(`Features have no feature by name ${name}`) } return features[name] } diff --git a/src/imports/api/log/createLog.js b/src/imports/api/log/createLog.js index 1b568a5..1e0992d 100644 --- a/src/imports/api/log/createLog.js +++ b/src/imports/api/log/createLog.js @@ -34,7 +34,7 @@ const internal = { * Creates a log for a given name and type. * Returns a no-op function if devOnly is true but app is in prod mode * @param name {string} - * @param type {string='log'} + * @param type {'log'|'info'|'debug'|'warn'|'error'} * @param devOnly {boolean=false} * @return {function} */ diff --git a/src/imports/api/utils/insertUpdate.js b/src/imports/api/utils/insertUpdate.js index e309f00..16146de 100644 --- a/src/imports/api/utils/insertUpdate.js +++ b/src/imports/api/utils/insertUpdate.js @@ -1,4 +1,7 @@ import crypto from 'crypto' +import {createLog} from '../log/createLog' + +const debugLog = createLog({ name: 'insertUpdate', type: 'debug' }) const sortedProps = (doc) => { let obj = '' @@ -19,10 +22,12 @@ const hash = (doc = {}) => { const areEqual = (doc1, doc2, debug) => { const h1 = hash(doc1) const h2 = hash(doc2) + if (debug && h1 !== h2) { - console.info(`Hash 1: ${h1}`) - console.info(`Hash 2: ${h2}`) + debugLog(`Hash 1: ${h1}`) + debugLog(`Hash 2: ${h2}`) } + return h1 === h2 } @@ -44,7 +49,7 @@ export const insertUpdate = function insertUpdate (collection, doc, hash, debug) if (!existingDoc) { const insertDocId = collection.insert(doc) if (debug) { - console.info(`[${collection._name}]: insert ${insertDocId}`) + debugLog(`[${collection._name}]: insert ${insertDocId}`) } return insertDocId } @@ -59,14 +64,11 @@ export const insertUpdate = function insertUpdate (collection, doc, hash, debug) delete doc._id const updated = collection.update(docId, { $set: doc }) if (debug) { - console.info(`[${collection._name}]: update ${docId} ${updated}`) + debugLog(`[${collection._name}]: update ${docId} ${updated}`) } return updated } catch (e) { - console.log(e.message) - console.log('collection: ' + collection._name) - console.log('document: ') - console.dir(doc) + console.error(e) } } diff --git a/src/imports/contexts/beamer/Beamer.js b/src/imports/contexts/beamer/Beamer.js index cec5033..f10a69e 100644 --- a/src/imports/contexts/beamer/Beamer.js +++ b/src/imports/contexts/beamer/Beamer.js @@ -185,14 +185,16 @@ Beamer.methods.insert = { timeInterval: 50000, run: onServer(function () { const BeamerCollection = getCollection(Beamer.name) - console.log(BeamerCollection.findOne({ createdBy: this.userId })) + if (BeamerCollection.findOne({ createdBy: this.userId })) { throw new Meteor.Error('errors.docAlreadyExists') } + const ui = { background: Beamer.defaultBackground, grid: Beamer.defaultGridlayout } + return BeamerCollection.insert({ createdBy: this.userId, references: [], ui }) }) } diff --git a/src/imports/contexts/beamer/tests/Beamer.tests.js b/src/imports/contexts/beamer/tests/Beamer.tests.js index 129e6e9..adac254 100644 --- a/src/imports/contexts/beamer/tests/Beamer.tests.js +++ b/src/imports/contexts/beamer/tests/Beamer.tests.js @@ -255,7 +255,6 @@ describe('Beamer', function () { // after update Beamer.doc.background(Beamer.ui.backgroundColors.dark.value, (err, color) => { - console.info(err, color) if (err) { console.error(err) return done(err) diff --git a/src/imports/contexts/classroom/group/Group.js b/src/imports/contexts/classroom/group/Group.js index 80fd117..78b566a 100644 --- a/src/imports/contexts/classroom/group/Group.js +++ b/src/imports/contexts/classroom/group/Group.js @@ -158,9 +158,7 @@ Group.publications.my = { query.$or.push(myGroups, iamMember) - const cursor = getCollection(Group.name).find(query, { fields: Group.publicFields }) - console.log(this.userId, JSON.stringify(query), cursor.count()) - return cursor + return getCollection(Group.name).find(query, { fields: Group.publicFields }) }) } @@ -174,9 +172,7 @@ Group.publications.single = { run: onServer(function ({ groupId }) { const { userId } = this const query = { _id: groupId, users: { $elemMatch: { userId } } } - const cursor = getCollection(Group.name).find(query, { fields: Group.publicFields }) - console.log(this.userId, JSON.stringify(query), cursor.count()) - return cursor + return getCollection(Group.name).find(query, { fields: Group.publicFields }) }) } diff --git a/src/imports/contexts/classroom/lessons/Lesson.js b/src/imports/contexts/classroom/lessons/Lesson.js index 35c4e86..e674699 100644 --- a/src/imports/contexts/classroom/lessons/Lesson.js +++ b/src/imports/contexts/classroom/lessons/Lesson.js @@ -842,7 +842,7 @@ Lesson.methods.toggle = { else { throw new Meteor.Error(LessonErrors.unexpectedMaterialIndex, index) } - console.debug('UPDATE LESSON DOC', _id, transform) + return !!updateLesson.call(this, _id, transform) } diff --git a/src/imports/contexts/curriculum/utils/formfactory.js b/src/imports/contexts/curriculum/utils/formfactory.js index 46ccdf8..bf9bef5 100644 --- a/src/imports/contexts/curriculum/utils/formfactory.js +++ b/src/imports/contexts/curriculum/utils/formfactory.js @@ -29,7 +29,6 @@ export const FormFactory = { // filter doc fields only by label/value references fields const data = collection.find(filter, transform).fetch() - // console.log("ger data from " + collectionName, data, filter, transform); if (!data || data.length === 0) return [] return groupKey @@ -68,20 +67,15 @@ export const FormFactory = { optionsFromDataWihtGroups (data, label, value, groupKey, collection, resolver) { const ret = [] const groupNames = this.getUniqueGroupNames(data, groupKey) - // console.log("******************************") - // console.log("optionsFromDataWihtGroups"); - // console.log(groupNames); for (let groupName of groupNames) { if (typeof groupName === 'undefined') groupName = { $exists: false } const query = {} query[groupKey] = groupName const filteredData = collection.find(query, { sort: { title: 1 } }).fetch() - // console.log(query, filteredData); ret.push(this.createOptionsGroup(groupName, filteredData, label, value, resolver)) } - // console.log(ret); - // console.log("-------------------------"); + return ret }, diff --git a/src/imports/contexts/files/image/ImageFiles.js b/src/imports/contexts/files/image/ImageFiles.js index 40b1545..6200de3 100644 --- a/src/imports/contexts/files/image/ImageFiles.js +++ b/src/imports/contexts/files/image/ImageFiles.js @@ -103,9 +103,7 @@ ImageFiles.material = { } } }, - onCreated: function (imageId, unitDoc) { - console.info(imageId, unitDoc) - }, + // onCreated: function (imageId, unitDoc) {}, async load () { await FilesTemplates.upload.load() await FilesTemplates.renderer.load() diff --git a/src/imports/contexts/files/video/renderer/main/videoFileRenderer.js b/src/imports/contexts/files/video/renderer/main/videoFileRenderer.js index ec44428..1ab3493 100644 --- a/src/imports/contexts/files/video/renderer/main/videoFileRenderer.js +++ b/src/imports/contexts/files/video/renderer/main/videoFileRenderer.js @@ -10,7 +10,6 @@ const API = Template.videoFileRenderer.setDependencies({ Template.videoFileRenderer.onCreated(function () { const instance = this - console.debug('video file renderer created') instance.state.setDefault('version', 'original') instance.deleteFile = createDeleteFile({ context: VideoFiles, @@ -21,7 +20,6 @@ Template.videoFileRenderer.onCreated(function () { Template.videoFileRenderer.helpers({ getLink (videoFile) { - console.debug(videoFile) return getFilesLink({ file: videoFile, name: VideoFiles.name, diff --git a/src/imports/contexts/material/materialPipeline.js b/src/imports/contexts/material/materialPipeline.js index 397d655..86d0c74 100644 --- a/src/imports/contexts/material/materialPipeline.js +++ b/src/imports/contexts/material/materialPipeline.js @@ -3,9 +3,7 @@ import { createPipeline } from '../../infrastructure/pipelines/createPipeline' import { createPreviewMethod } from '../../api/decorators/methods/createPreviewMethod' export const materialPipeline = createPipeline(Material.name, function (context) { - console.debug('MATERIAL PIPELINE') if (context.material.isPreviewable) { - console.debug('previewable', context) context.methods = context.methods || {} context.methods.preview = context.methods.preview || createPreviewMethod(context) } diff --git a/src/imports/contexts/sync/SyncPipeline.js b/src/imports/contexts/sync/SyncPipeline.js index 5252116..9608000 100644 --- a/src/imports/contexts/sync/SyncPipeline.js +++ b/src/imports/contexts/sync/SyncPipeline.js @@ -1,4 +1,9 @@ -const _events = { +import {createLog} from '../../api/log/createLog' + +export const SyncPipeline = {} + +const debug = createLog({ name: 'SyncPipeline', type: 'debug' }) +const events = { loggedin: 'loggedin', synced: 'synced', cleaned: 'cleaned', @@ -7,32 +12,29 @@ const _events = { chunks: 'chunks', filesCollections: 'filesCollections' } -const _eventKeys = Object.keys(_events) -const _callbacks = {} -_eventKeys.forEach(key => { - _callbacks[key] = [] +let debugActive = false +const eventKeys = Object.keys(events) +const callbacks = {} +eventKeys.forEach(key => { + callbacks[key] = [] }) -let debug = false - -function _debug (value) { - debug = !!value +SyncPipeline.events = events +SyncPipeline.on = (name, cb) => callbacks[name].push(cb) +SyncPipeline.debug = value => { + debugActive = !!value } -export const SyncPipeline = {} -SyncPipeline.events = _events -SyncPipeline.on = (name, cb) => _callbacks[name].push(cb) -SyncPipeline.debug = value => _debug(value) SyncPipeline.complete = (key, optionalArgs) => { - if (debug) { - console.info(`[SyncPipeline]: complete [${key}]`) + if (debugActive) { + debug(`complete [${key}]`) } - _callbacks[key].forEach(cb => cb(optionalArgs)) - _callbacks[key] = [] + callbacks[key].forEach(cb => cb(optionalArgs)) + callbacks[key] = [] } SyncPipeline.done = () => { - if (debug) { - console.info('[SyncPipeline]: all complete') + if (debugActive) { + debug('all complete') } } diff --git a/src/imports/contexts/system/accounts/users/User.js b/src/imports/contexts/system/accounts/users/User.js index 95b307c..b1a0fd2 100644 --- a/src/imports/contexts/system/accounts/users/User.js +++ b/src/imports/contexts/system/accounts/users/User.js @@ -327,7 +327,8 @@ Users.methods.byClass = { 'skip.$': String }, run: onServerExec(function () { - import { usersByClass } from './usersByClass' + const { usersByClass } = require('./usersByClass') + const run = usersByClass() return function ({ classId, skip }) { @@ -370,7 +371,7 @@ Users.publications.byClass = { classId: String }, run: onServerExecLazy(function () { - import { usersByClass } from './usersByClass' + const{ usersByClass } = require('./usersByClass') return usersByClass }) } diff --git a/src/imports/contexts/system/accounts/users/methods/setResearch.js b/src/imports/contexts/system/accounts/users/methods/setResearch.js index cc8cd75..038acb9 100644 --- a/src/imports/contexts/system/accounts/users/methods/setResearch.js +++ b/src/imports/contexts/system/accounts/users/methods/setResearch.js @@ -18,7 +18,6 @@ export const setResearch = function setResearch ({ participate }) { if (participate) { const user = Meteor.users.findOne(userId) - console.debug(user) const { firstName, lastName } = user const token = createResearchConfirmToken({ userId }) const email = user.emails[0].address diff --git a/src/imports/contexts/system/accounts/users/usersByClass.js b/src/imports/contexts/system/accounts/users/usersByClass.js index 946858a..3795a69 100644 --- a/src/imports/contexts/system/accounts/users/usersByClass.js +++ b/src/imports/contexts/system/accounts/users/usersByClass.js @@ -7,6 +7,7 @@ export const usersByClass = function () { // run phase return function usersByClass ({ classId, skip }) { const classDoc = getCollection(SchoolClass.name).findOne(classId) + if (!classDoc) { throw new Meteor.Error('usersByClass.failed', 'errors.docNotFound', classId) } diff --git a/src/imports/contexts/tasks/getAllItemsInTask.js b/src/imports/contexts/tasks/getAllItemsInTask.js index 90bfe80..bea26ed 100644 --- a/src/imports/contexts/tasks/getAllItemsInTask.js +++ b/src/imports/contexts/tasks/getAllItemsInTask.js @@ -3,6 +3,12 @@ import { getResponseProcessors } from '../../api/response/getResponseProcessors' import { getFileType } from '../../api/files/getFileType' import { Files } from '../files/Files' +/** + * Extracts all items from a given taskDoc + * + * @param taskDoc + * @returns {*[]} + */ export const getAllItemsInTask = taskDoc => { if (!Item.isInitialized() || !Files.isInitialized()) { throw new Error('Items and Files need to be initialized to get all items in task') diff --git a/src/imports/contexts/tasks/responseProcessors/ResponseProcessorRegistry.js b/src/imports/contexts/tasks/responseProcessors/ResponseProcessorRegistry.js index c00ae95..e99fd4a 100644 --- a/src/imports/contexts/tasks/responseProcessors/ResponseProcessorRegistry.js +++ b/src/imports/contexts/tasks/responseProcessors/ResponseProcessorRegistry.js @@ -138,7 +138,6 @@ ResponseProcessorRegistry.allForDataType = dataType => { : dataType const typeMap = dataTypeMap.get(dataTypeName) || { values: [] } - console.debug({ dataTypeMap, typeMap }) const contexts = new Set(typeMap.values.map(toContext)) contexts.add(RawResponse) diff --git a/src/imports/contexts/tasks/responseProcessors/aggregate/audioList/audioResultsRenderer.js b/src/imports/contexts/tasks/responseProcessors/aggregate/audioList/audioResultsRenderer.js index 5a88543..b964e97 100644 --- a/src/imports/contexts/tasks/responseProcessors/aggregate/audioList/audioResultsRenderer.js +++ b/src/imports/contexts/tasks/responseProcessors/aggregate/audioList/audioResultsRenderer.js @@ -19,7 +19,6 @@ Template.audioResultsRenderer.onCreated(function () { const { itemId } = instance.data instance.autorun(c => { - console.debug(AudioFiles) if (API.initComplete()) { API.subscribe({ name: AudioFiles.publications.byItem, @@ -69,6 +68,8 @@ Template.audioResultsRenderer.helpers({ } }) +/* +// dev-only, uncomment, if needed Template.audioResultsRenderer.events({ '* audio' (event) { console.info(event) @@ -80,3 +81,4 @@ Template.audioResultsRenderer.events({ console.info(event) } }) + */ diff --git a/src/imports/contexts/tasks/results/TaskResults.js b/src/imports/contexts/tasks/results/TaskResults.js index 2356763..925aa30 100644 --- a/src/imports/contexts/tasks/results/TaskResults.js +++ b/src/imports/contexts/tasks/results/TaskResults.js @@ -1,6 +1,4 @@ -import { Meteor } from 'meteor/meteor' import { UserUtils } from '../../system/accounts/users/UserUtils' -import { getCollection } from '../../../api/utils/getCollection' import { onServerExec } from '../../../api/utils/archUtils' import { Item } from '../definitions/items/Item' @@ -66,81 +64,9 @@ TaskResults.methods.saveTask = { response: TaskResults.schema.response, 'response.$': TaskResults.schema['response.$'] }, - run: onServerExec(function () { - import { LessonErrors } from '../../classroom/lessons/LessonErrors' - import { SchoolClass } from '../../classroom/schoolclass/SchoolClass' - import { LessonStates } from '../../classroom/lessons/LessonStates' - import { Lesson } from '../../classroom/lessons/Lesson' - import { createDocGetter } from '../../../api/utils/document/createDocGetter' - import { Task } from '../../curriculum/curriculum/task/Task' - import { Group } from '../../classroom/group/Group' - - const getLessonDoc = createDocGetter(Lesson) - const checkTask = createDocGetter(Task) - const getGroupDoc = createDocGetter(Group) - - /** - * Saves a response to an item of a given task - * @param lessonId the lesson of the task - * @param taskId the task - * @param itemId the item the response is related to - * @param response the response value(s) - * @return {undefined|String|Number} returns the doc id if inserted (undefined if failed) or the update number 1 if - * updated (0 if failed) - */ - - function saveTask ({ lessonId, taskId, itemId, groupId, groupMode, response }) { - const { userId } = this - if (!Lesson.helpers.isMemberOfLesson({ userId, lessonId })) { - throw new Meteor.Error('errors.permissionDenied', SchoolClass.errors.notMember) - } - - checkTask(taskId) - const lessonDoc = getLessonDoc(lessonId) - if (!LessonStates.isRunning(lessonDoc)) { - throw new Meteor.Error('errors.permissionDenied', LessonErrors.unexpectedState) - } - - // if groupId check group membership - let groupDoc - - if (groupId) { - groupDoc = getGroupDoc(groupId) - - if (!groupDoc.users.some(entry => entry.userId === userId)) { - throw new Meteor.Error('errors.permissionDenied', 'group.notAMember', { groupId, userId }) - } - } - - // check if we can even edit the task - if (!Lesson.helpers.taskIsEditable({ lessonDoc, taskId, groupDoc })) { - throw new Meteor.Error('errors.permissionDenied', TaskResults.errors.notEditable) - } - - - const createdBy = userId - const TaskResultCollection = getCollection(TaskResults.name) - const query = { lessonId, taskId, itemId, createdBy } - if (groupId) { - query.groupId = groupId - } - - const taskResultDoc = TaskResultCollection.findOne(query) - - if (!taskResultDoc) { - const insertDoc = { lessonId, taskId, itemId, response } - if (groupId) { - insertDoc.groupId = groupId - } - return TaskResultCollection.insert(insertDoc) - } - - else { - return TaskResultCollection.update(taskResultDoc._id, { $set: { response } }) - } - } - - return saveTask + run: onServerExec(() => { + import { saveTaskResult } from './methods/saveTaskResult' + return saveTaskResult }) } @@ -162,25 +88,8 @@ TaskResults.publications.allByItem = { }, roles: UserUtils.roles.teacher, run: onServerExec(function () { - import { SchoolClass } from '../../classroom/schoolclass/SchoolClass' - import { Lesson } from '../../classroom/lessons/Lesson' - import { userIsAdmin } from '../../../api/accounts/admin/userIsAdmin' - import { PermissionDeniedError } from '../../../api/errors/types/PermissionDeniedError' - - return function run ({ references }) { - const userId = this.userId - const query = { $or: [] } - - references.forEach(({ lessonId, taskId, itemId }) => { - if (!userIsAdmin(userId) && !Lesson.helpers.isTeacher({ userId, lessonId })) { - throw new PermissionDeniedError(SchoolClass.errors.notMember) - } - - query.$or.push({ lessonId, taskId, itemId }) - }) - - return getCollection(TaskResults.name).find(query) - } + import { getAllTasksByItem } from './methods/getAllTasksByItem' + return getAllTasksByItem }) } /** @@ -194,25 +103,7 @@ TaskResults.publications.byGroup = { itemId: String }, run: onServerExec(function () { - import { Group } from '../../classroom/group/Group' - import { createDocGetter } from '../../../api/utils/document/createDocGetter' - import { PermissionDeniedError } from '../../../api/errors/types/PermissionDeniedError' - const getGroupDoc = createDocGetter(Group) - - return function ({ groupId, itemId }) { - const { userId } = this - - // check if user is group member - const groupDoc = getGroupDoc(groupId) - const { users } = groupDoc - - if (!users || !users.length || !users.find(u => u && u.userId === userId)) { - throw new PermissionDeniedError('group.notAMember', { userId, groupId }) - } - - const query = { groupId, itemId } - return getCollection(TaskResults.name).find(query) - } + import { getAllTasksByGroupAndItem } from './methods/getAllTaskByGroup' }) } @@ -226,31 +117,7 @@ TaskResults.publications.byTask = { } }, run: onServerExec(function () { - import { SchoolClass } from '../../classroom/schoolclass/SchoolClass' - import { Lesson } from '../../classroom/lessons/Lesson' - import { PermissionDeniedError } from '../../../api/errors/types/PermissionDeniedError' - import { createDocGetter } from '../../../api/utils/document/createDocGetter' - - const getLessonDoc = createDocGetter(Lesson) - - return function run ({ lessonId, taskId }) { - const { userId } = this - const lessonDoc = getLessonDoc({ _id: lessonId }) - const isTeacher = lessonDoc.createdBy === userId - - if (!isTeacher && !Lesson.helpers.isMemberOfLesson({ userId, lessonId })) { - throw new PermissionDeniedError(SchoolClass.errors.notMember) - } - - const query = { lessonId } - if (!isTeacher) { - query.createdBy = userId - } - if (taskId) { - query.taskId = taskId - } - - return getCollection(TaskResults.name).find(query) - } + import { getAllTaskResultsByTask } from './methods/getAllTaskResultsByTask' + return getAllTaskResultsByTask }) } diff --git a/src/imports/contexts/tasks/results/methods/getAllTaskByGroup.js b/src/imports/contexts/tasks/results/methods/getAllTaskByGroup.js new file mode 100644 index 0000000..fc13d2b --- /dev/null +++ b/src/imports/contexts/tasks/results/methods/getAllTaskByGroup.js @@ -0,0 +1,30 @@ +import { Group } from '../../../classroom/group/Group' +import { createDocGetter } from '../../../../api/utils/document/createDocGetter' +import { PermissionDeniedError } from '../../../../api/errors/types/PermissionDeniedError' +import {getCollection} from '../../../../api/utils/getCollection' +import {TaskResults} from '../TaskResults' + +const getGroupDoc = createDocGetter(Group) + +/** + * Queries all task results for a given group by _id and item id. + * + * @param groupId {string} + * @param itemId {string} + * @returns {Mongo.Cursor} + * @throws {PermissionDeniedError} if user is not in group + */ +export const getAllTasksByGroupAndItem = function ({ groupId, itemId }) { + const { userId } = this + + // check if user is group member + const groupDoc = getGroupDoc(groupId) + const { users } = groupDoc + + if (!users || !users.length || !users.find(u => u && u.userId === userId)) { + throw new PermissionDeniedError('group.notAMember', { userId, groupId }) + } + + const query = { groupId, itemId } + return getCollection(TaskResults.name).find(query) +} diff --git a/src/imports/contexts/tasks/results/methods/getAllTaskResultsByTask.js b/src/imports/contexts/tasks/results/methods/getAllTaskResultsByTask.js new file mode 100644 index 0000000..ab53cf9 --- /dev/null +++ b/src/imports/contexts/tasks/results/methods/getAllTaskResultsByTask.js @@ -0,0 +1,36 @@ +import { SchoolClass } from '../../../classroom/schoolclass/SchoolClass' +import { Lesson } from '../../../classroom/lessons/Lesson' +import { PermissionDeniedError } from '../../../../api/errors/types/PermissionDeniedError' +import { createDocGetter } from '../../../../api/utils/document/createDocGetter' +import { getCollection } from '../../../../api/utils/getCollection' +import { TaskResults } from '../TaskResults' + +const getLessonDoc = createDocGetter(Lesson) + +/** + * Returns all task results for a given task (presumed, that the user is teacher/member of the lesson). + * @param lessonId + * @param taskId + * @returns {*} + */ +export const getAllTaskResultsByTask = function ({ lessonId, taskId }) { + const { userId } = this + const lessonDoc = getLessonDoc({ _id: lessonId }) + const isTeacher = lessonDoc.createdBy === userId + + if (!isTeacher && !Lesson.helpers.isMemberOfLesson({ userId, lessonId })) { + throw new PermissionDeniedError(SchoolClass.errors.notMember) + } + + const query = { lessonId } + + if (!isTeacher) { + query.createdBy = userId + } + + if (taskId) { + query.taskId = taskId + } + + return getCollection(TaskResults.name).find(query) +} diff --git a/src/imports/contexts/tasks/results/methods/getAllTasksByItem.js b/src/imports/contexts/tasks/results/methods/getAllTasksByItem.js new file mode 100644 index 0000000..0da7a9c --- /dev/null +++ b/src/imports/contexts/tasks/results/methods/getAllTasksByItem.js @@ -0,0 +1,29 @@ +import { SchoolClass } from '../../../classroom/schoolclass/SchoolClass' +import { Lesson } from '../../../classroom/lessons/Lesson' +import { TaskResults } from '../TaskResults' +import { PermissionDeniedError } from '../../../../api/errors/types/PermissionDeniedError' +import { userIsAdmin } from '../../../../api/accounts/admin/userIsAdmin' +import { getCollection } from '../../../../api/utils/getCollection' + +/** + * Creates a query for all given references that contain the combination of lessonId, taskId and itemId. + * @param references {object} + * @param references.lessonId {string} + * @param references.taskId {string} + * @param references.itemId {string} + * @returns {Mongo.Cursor} + */ +export const getAllTasksByItem = function run ({ references }) { + const userId = this.userId + const query = { $or: [] } + + references.forEach(({ lessonId, taskId, itemId }) => { + if (!userIsAdmin(userId) && !Lesson.helpers.isTeacher({ userId, lessonId })) { + throw new PermissionDeniedError(SchoolClass.errors.notMember) + } + + query.$or.push({ lessonId, taskId, itemId }) + }) + + return getCollection(TaskResults.name).find(query) +} diff --git a/src/imports/contexts/tasks/results/methods/saveTaskResult.js b/src/imports/contexts/tasks/results/methods/saveTaskResult.js new file mode 100644 index 0000000..15a8f35 --- /dev/null +++ b/src/imports/contexts/tasks/results/methods/saveTaskResult.js @@ -0,0 +1,74 @@ +import {Meteor} from 'meteor/meteor' +import { LessonErrors } from '../../../classroom/lessons/LessonErrors' +import { SchoolClass } from '../../../classroom/schoolclass/SchoolClass' +import { LessonStates } from '../../../classroom/lessons/LessonStates' +import { Lesson } from '../../../classroom/lessons/Lesson' +import { createDocGetter } from '../../../../api/utils/document/createDocGetter' +import { Task } from '../../../curriculum/curriculum/task/Task' +import { Group } from '../../../classroom/group/Group' +import { getCollection } from '../../../../api/utils/getCollection' +import { TaskResults } from '../TaskResults' + +const getLessonDoc = createDocGetter(Lesson) +const checkTask = createDocGetter(Task) +const getGroupDoc = createDocGetter(Group) + +/** + * Saves a response to an item of a given task + * @param lessonId the lesson of the task + * @param taskId the task + * @param itemId the item the response is related to + * @param response the response value(s) + * @return {undefined|String|Number} returns the doc id if inserted (undefined if failed) or the update number 1 if + * updated (0 if failed) + */ + +export const saveTaskResult = function ({ lessonId, taskId, itemId, groupId, groupMode, response }) { + const { userId } = this + if (!Lesson.helpers.isMemberOfLesson({ userId, lessonId })) { + throw new Meteor.Error('errors.permissionDenied', SchoolClass.errors.notMember) + } + + checkTask(taskId) + const lessonDoc = getLessonDoc(lessonId) + if (!LessonStates.isRunning(lessonDoc)) { + throw new Meteor.Error('errors.permissionDenied', LessonErrors.unexpectedState) + } + + // if groupId check group membership + let groupDoc + + if (groupId) { + groupDoc = getGroupDoc(groupId) + + if (!groupDoc.users.some(entry => entry.userId === userId)) { + throw new Meteor.Error('errors.permissionDenied', 'group.notAMember', { groupId, userId }) + } + } + + // check if we can even edit the task + if (!Lesson.helpers.taskIsEditable({ lessonDoc, taskId, groupDoc })) { + throw new Meteor.Error('errors.permissionDenied', TaskResults.errors.notEditable) + } + + const createdBy = userId + const TaskResultCollection = getCollection(TaskResults.name) + const query = { lessonId, taskId, itemId, createdBy } + if (groupId) { + query.groupId = groupId + } + + const taskResultDoc = TaskResultCollection.findOne(query) + + if (!taskResultDoc) { + const insertDoc = { lessonId, taskId, itemId, response } + if (groupId) { + insertDoc.groupId = groupId + } + return TaskResultCollection.insert(insertDoc) + } + + else { + return TaskResultCollection.update(taskResultDoc._id, { $set: { response } }) + } +} diff --git a/src/imports/contexts/tasks/state/TaskWorkingState.js b/src/imports/contexts/tasks/state/TaskWorkingState.js index 12b88e0..e0f26e1 100644 --- a/src/imports/contexts/tasks/state/TaskWorkingState.js +++ b/src/imports/contexts/tasks/state/TaskWorkingState.js @@ -1,7 +1,5 @@ -import { Meteor } from 'meteor/meteor' import { UserUtils } from '../../system/accounts/users/UserUtils' import { onServerExec } from '../../../api/utils/archUtils' -import { getMyTaskWorkingState } from './methods/getMyTaskWorkingState' export const TaskWorkingState = { name: 'taskWorkingState', From 2fdd92ab9ea52514c4092c1b21fb1a951645c176 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Mon, 21 Nov 2022 17:50:35 +0100 Subject: [PATCH 03/24] fix: lint fix --- .../api/accounts/emailTemplates/resetPassword.js | 2 +- .../api/accounts/emailTemplates/verifyEmail.js | 2 +- .../api/accounts/registration/UserFactory.js | 5 ++--- src/imports/api/decorators/methods/createClone.js | 2 +- src/imports/api/utils/insertUpdate.js | 2 +- .../classroom/invitations/CodeInvitations.js | 15 +++++++-------- .../classroom/invitations/methods/addToClass.js | 2 +- src/imports/contexts/classroom/lessons/Lesson.js | 10 ++++------ src/imports/contexts/files/Files.js | 3 +-- .../files/document/converter/documentConverter.js | 12 +++++++----- .../renderer/list/documentFilesListRenderer.js | 2 +- .../contexts/material/resolveMaterialReference.js | 2 +- src/imports/contexts/sync/SyncPipeline.js | 2 +- .../contexts/system/accounts/admin/Admin.js | 9 +++++---- .../contexts/system/accounts/users/User.js | 2 +- .../contexts/tasks/definitions/TaskDefinitions.js | 3 +-- .../definitions/forms/groupText/groupText.js | 2 +- .../contexts/tasks/definitions/items/Item.js | 5 +++-- src/imports/contexts/tasks/getTaskContexts.js | 2 +- .../ResponseProcessorRegistry.js | 2 +- .../aggregate/audioList/audioResultsRenderer.js | 2 +- .../aggregate/barChart/rpBarChart.js | 1 - .../documentList/documentResultsRenderer.js | 2 +- .../imageGallery/imageResultsRenderer.js | 2 +- .../aggregate/pieChart/rpPieChart.js | 2 +- .../contexts/tasks/results/TaskResultUtils.js | 2 +- src/imports/contexts/tasks/results/TaskResults.js | 2 +- .../tasks/results/methods/getAllTaskByGroup.js | 4 ++-- .../tasks/results/methods/saveTaskResult.js | 2 +- src/package.json | 1 + 30 files changed, 52 insertions(+), 54 deletions(-) diff --git a/src/imports/api/accounts/emailTemplates/resetPassword.js b/src/imports/api/accounts/emailTemplates/resetPassword.js index 54ae8aa..dc587dd 100644 --- a/src/imports/api/accounts/emailTemplates/resetPassword.js +++ b/src/imports/api/accounts/emailTemplates/resetPassword.js @@ -1,7 +1,7 @@ import { i18n } from '../../language/language' import { Meteor } from 'meteor/meteor' import { getCredentialsAsBuffer, getFullName } from './common' -import {createLog} from '../../log/createLog' +import { createLog } from '../../log/createLog' const debug = createLog({ name: 'email/resetPassword', type: 'debug' }) diff --git a/src/imports/api/accounts/emailTemplates/verifyEmail.js b/src/imports/api/accounts/emailTemplates/verifyEmail.js index 731117f..c9367a3 100644 --- a/src/imports/api/accounts/emailTemplates/verifyEmail.js +++ b/src/imports/api/accounts/emailTemplates/verifyEmail.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor' import { i18n } from '../../language/language' import { getFullName } from './common' -import {createLog} from '../../log/createLog' +import { createLog } from '../../log/createLog' const debug = createLog({ name: 'email/verify', type: 'debug' }) diff --git a/src/imports/api/accounts/registration/UserFactory.js b/src/imports/api/accounts/registration/UserFactory.js index 651c8db..8dda205 100644 --- a/src/imports/api/accounts/registration/UserFactory.js +++ b/src/imports/api/accounts/registration/UserFactory.js @@ -5,8 +5,7 @@ import { Schema } from '../../schema/Schema' import { Roles } from 'meteor/alanning:roles' import { rollbackAccount } from './rollbackAccount' import { userExists } from '../user/userExists' -import {createLog} from '../../log/createLog' - +import { createLog } from '../../log/createLog' /** * Creates new user accounts @@ -16,7 +15,7 @@ export const UserFactory = {} UserFactory.name = 'UserFactory' -const debug = createLog({ name: UserFactory.name, type: 'debug'}) +const debug = createLog({ name: UserFactory.name, type: 'debug' }) let createSchema /** diff --git a/src/imports/api/decorators/methods/createClone.js b/src/imports/api/decorators/methods/createClone.js index 749c858..6a1c6af 100644 --- a/src/imports/api/decorators/methods/createClone.js +++ b/src/imports/api/decorators/methods/createClone.js @@ -5,7 +5,7 @@ import { isCurriculumDoc } from './isCurriculumDoc' import { checkCurriculum } from './checkCurriculum' export const createClone = (collectionName, { owner, isCurriculum } = {}) => { - const info = createLog( { name: collectionName }) + const info = createLog({ name: collectionName }) let collection return function ({ _id }) { diff --git a/src/imports/api/utils/insertUpdate.js b/src/imports/api/utils/insertUpdate.js index 16146de..a9d3d1e 100644 --- a/src/imports/api/utils/insertUpdate.js +++ b/src/imports/api/utils/insertUpdate.js @@ -1,5 +1,5 @@ import crypto from 'crypto' -import {createLog} from '../log/createLog' +import { createLog } from '../log/createLog' const debugLog = createLog({ name: 'insertUpdate', type: 'debug' }) diff --git a/src/imports/contexts/classroom/invitations/CodeInvitations.js b/src/imports/contexts/classroom/invitations/CodeInvitations.js index 0fa4654..1c9fac7 100644 --- a/src/imports/contexts/classroom/invitations/CodeInvitations.js +++ b/src/imports/contexts/classroom/invitations/CodeInvitations.js @@ -463,13 +463,13 @@ CodeInvitation.helpers.isPending = function isPending (doc) { * @return {*} */ CodeInvitation.helpers.getStatus = function getStatus ({ - invalid, - createdAt, - expires, - registeredUsers, - maxUsers, - _id - }) { + invalid, + createdAt, + expires, + registeredUsers, + maxUsers, + _id +}) { const isExpired = CodeInvitation.helpers.isExpired({ invalid, createdAt, @@ -605,7 +605,6 @@ CodeInvitation.methods.verify = { }, isPublic: true, run: onServerExec(function () { - return function ({ code }) { const codeDoc = getCollection(CodeInvitation.name).findOne({ code }) diff --git a/src/imports/contexts/classroom/invitations/methods/addToClass.js b/src/imports/contexts/classroom/invitations/methods/addToClass.js index 7847e34..08b56b8 100644 --- a/src/imports/contexts/classroom/invitations/methods/addToClass.js +++ b/src/imports/contexts/classroom/invitations/methods/addToClass.js @@ -66,4 +66,4 @@ export const addToClass = function ({ code }) { // add CodeInvitation.helpers.addUserToInvitation.call(thisContext, code, userId) return true -} \ No newline at end of file +} diff --git a/src/imports/contexts/classroom/lessons/Lesson.js b/src/imports/contexts/classroom/lessons/Lesson.js index e674699..387cc94 100644 --- a/src/imports/contexts/classroom/lessons/Lesson.js +++ b/src/imports/contexts/classroom/lessons/Lesson.js @@ -352,7 +352,7 @@ Lesson.publications.my = { const { userId } = this const query = { $or: [ - { createdBy: userId}, + { createdBy: userId }, { teachers: userId } ] } @@ -360,8 +360,6 @@ Lesson.publications.my = { }) } - - /** * Publishes all Lessons, associated with a unit and which I have created * @roles teacher @@ -394,7 +392,7 @@ Lesson.publications.single = { }, run: onServerExec(function () { import { userIsAdmin } from '../../../api/accounts/admin/userIsAdmin' - + return function ({ _id }) { const { userId } = this const isMember = Lesson.helpers.isMemberOfLesson({ @@ -759,7 +757,7 @@ Lesson.methods.restart = { * @return {object} A boolean value, whether the operation has been successful */ function restartLesson ({ _id }) { - const {userId, log } = this + const { userId, log } = this const { lessonDoc } = Lesson.helpers.docsForTeacher({ userId, lessonId: _id @@ -907,7 +905,7 @@ Lesson.methods.material = { _id: String, groupId: { type: String, - optional: true, + optional: true }, skip: { type: Array, diff --git a/src/imports/contexts/files/Files.js b/src/imports/contexts/files/Files.js index b8f554b..85325a8 100644 --- a/src/imports/contexts/files/Files.js +++ b/src/imports/contexts/files/Files.js @@ -43,9 +43,8 @@ Files.getMaterialContexts = auto(function () { } }) - onClientExec(function () { - import { ITaskDefinition } from '../tasks/definitions/ITaskDefinition' + import { ITaskDefinition } from '../tasks/definitions/ITaskDefinition' import { FilesTemplates } from './FilesTemplates' /** @deprecated move into own module FilesTemplates **/ diff --git a/src/imports/contexts/files/document/converter/documentConverter.js b/src/imports/contexts/files/document/converter/documentConverter.js index de9f9bb..36850de 100644 --- a/src/imports/contexts/files/document/converter/documentConverter.js +++ b/src/imports/contexts/files/document/converter/documentConverter.js @@ -6,7 +6,7 @@ let fs let im // convert -density 150 presentation.pdf[0] -quality 90 test.jpg -export const documentConverter = async function (fileRef, options){ +export const documentConverter = async function (fileRef, options) { if (!fileRef.isPDF) { return fileRef } @@ -18,7 +18,7 @@ export const documentConverter = async function (fileRef, options){ throw Meteor.Error('upload.convertError') } - if (!im) im = require('gm').subClass({imageMagick: true}) + if (!im) im = require('gm').subClass({ imageMagick: true }) let document const thumbnailPath = (filesCollection.storagePath(fileRef)) + '/thumbnail-' + fileRef._id + '.png' @@ -38,7 +38,8 @@ export const documentConverter = async function (fileRef, options){ .filter('Triangle') .resize('50%') .interlace('Line') - } catch (imError) { + } + catch (imError) { // if we catch an error here we skip the rest as we have nothing // created neither on disk nor in the database console.error(imError) @@ -47,7 +48,8 @@ export const documentConverter = async function (fileRef, options){ try { await gmexec(document, document.write, thumbnailPath) - } catch (imErr) { + } + catch (imErr) { console.error(imErr) return fileRef } @@ -86,4 +88,4 @@ function gmexec (thisObj, fct, ...args) { }) fct.call(thisObj, ...args) }) -} \ No newline at end of file +} diff --git a/src/imports/contexts/files/document/renderer/list/documentFilesListRenderer.js b/src/imports/contexts/files/document/renderer/list/documentFilesListRenderer.js index 0ff813d..db7514f 100644 --- a/src/imports/contexts/files/document/renderer/list/documentFilesListRenderer.js +++ b/src/imports/contexts/files/document/renderer/list/documentFilesListRenderer.js @@ -9,7 +9,7 @@ Template.documentFilesListRenderer.helpers({ return DocumentFiles.name }, hasThumbnail (fileObj) { - return fileObj?.versions?.thumbnail; + return fileObj?.versions?.thumbnail }, getThumbnail (fileObj) { return getFilesLink({ diff --git a/src/imports/contexts/material/resolveMaterialReference.js b/src/imports/contexts/material/resolveMaterialReference.js index 73bf9f4..372db45 100644 --- a/src/imports/contexts/material/resolveMaterialReference.js +++ b/src/imports/contexts/material/resolveMaterialReference.js @@ -15,7 +15,7 @@ import { getLocalCollection } from '../../infrastructure/collection/getLocalColl * @param options.preferLocal {boolean=false} * @return {null|object} */ -export const resolveMaterialReference = (refObj, { preferLocal=false } = {}) => { +export const resolveMaterialReference = (refObj, { preferLocal = false } = {}) => { const { collection, document } = refObj if (!collection || !document) { diff --git a/src/imports/contexts/sync/SyncPipeline.js b/src/imports/contexts/sync/SyncPipeline.js index 9608000..21bca56 100644 --- a/src/imports/contexts/sync/SyncPipeline.js +++ b/src/imports/contexts/sync/SyncPipeline.js @@ -1,4 +1,4 @@ -import {createLog} from '../../api/log/createLog' +import { createLog } from '../../api/log/createLog' export const SyncPipeline = {} diff --git a/src/imports/contexts/system/accounts/admin/Admin.js b/src/imports/contexts/system/accounts/admin/Admin.js index 89b3ad4..fc2222c 100644 --- a/src/imports/contexts/system/accounts/admin/Admin.js +++ b/src/imports/contexts/system/accounts/admin/Admin.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor' import { auto, onClient, onServer, onServerExec } from '../../../../api/utils/archUtils' import { AdminErrors } from './AdminErrors' -import { UserUtils } from '../users/UserUtils' +import { UserUtils } from '../users/UserUtils' export const Admin = { name: 'admin', @@ -80,7 +80,7 @@ Admin.methods.createUser = { })(), run: onServerExec(function () { import { Accounts } from 'meteor/accounts-base' - import { UserFactory } from '../../../../api/accounts/registration/UserFactory' + import { UserFactory } from '../../../../api/accounts/registration/UserFactory' import { createAdmin } from '../../../../api/accounts/admin/createAdmin' import { userIsAdmin } from '../../../../api/accounts/admin/userIsAdmin' import { PermissionDeniedError } from '../../../../api/errors/types/PermissionDeniedError' @@ -107,7 +107,8 @@ Admin.methods.createUser = { lastName: correctName(lastName, options), institution: correctName(institution, options), email, - role }) + role + }) if (willBeAdmin) { createAdmin(newUserId) @@ -172,7 +173,7 @@ Admin.methods.updateRole = { })(), run: onServerExec(function () { import { Roles } from 'meteor/alanning:roles' - import { createAdmin } from '../../../../api/accounts/admin/createAdmin' + import { createAdmin } from '../../../../api/accounts/admin/createAdmin' import { removeAdmin } from '../../../../api/accounts/admin/removeAdmin' import { userExists } from '../../../../api/accounts/user/userExists' import { userIsAdmin } from '../../../../api/accounts/admin/userIsAdmin' diff --git a/src/imports/contexts/system/accounts/users/User.js b/src/imports/contexts/system/accounts/users/User.js index b1a0fd2..1afc784 100644 --- a/src/imports/contexts/system/accounts/users/User.js +++ b/src/imports/contexts/system/accounts/users/User.js @@ -371,7 +371,7 @@ Users.publications.byClass = { classId: String }, run: onServerExecLazy(function () { - const{ usersByClass } = require('./usersByClass') + const { usersByClass } = require('./usersByClass') return usersByClass }) } diff --git a/src/imports/contexts/tasks/definitions/TaskDefinitions.js b/src/imports/contexts/tasks/definitions/TaskDefinitions.js index 56c21db..95615f0 100644 --- a/src/imports/contexts/tasks/definitions/TaskDefinitions.js +++ b/src/imports/contexts/tasks/definitions/TaskDefinitions.js @@ -45,7 +45,6 @@ const log = createLog({ name: TaskDefinitions.name, type: 'debug' }) /// //////////////////////////////////////////////////////////////////////////// const init = new ReactiveVar() -let localeTracker let initializing = false TaskDefinitions.initialize = function () { @@ -76,7 +75,7 @@ async function initialize () { plugins.forEach(({ name, plugin }) => processPlugin(name, plugin)) // TODO merge localeTrackers into one Tracker in plugin registry - localeTracker = Tracker.autorun(() => { + Tracker.autorun(() => { const currentLocale = i18n.getLocale() TaskElementPlugins.onLanguageChange(currentLocale) .catch(e => console.error(e)) diff --git a/src/imports/contexts/tasks/definitions/forms/groupText/groupText.js b/src/imports/contexts/tasks/definitions/forms/groupText/groupText.js index 3ed6aba..312261a 100644 --- a/src/imports/contexts/tasks/definitions/forms/groupText/groupText.js +++ b/src/imports/contexts/tasks/definitions/forms/groupText/groupText.js @@ -1,4 +1,4 @@ -/* global AutoForm */ +/* global AutoForm $ */ import { Meteor } from 'meteor/meteor' import { Template } from 'meteor/templating' import { TaskResults } from '../../../results/TaskResults' diff --git a/src/imports/contexts/tasks/definitions/items/Item.js b/src/imports/contexts/tasks/definitions/items/Item.js index bd47ede..2888396 100644 --- a/src/imports/contexts/tasks/definitions/items/Item.js +++ b/src/imports/contexts/tasks/definitions/items/Item.js @@ -43,7 +43,6 @@ const debug = createLog({ name: Item.name, type: 'debug' }) /// ///////////////////////////////////////////////////////////////////////////////////////////// const initialized = new ReactiveVar(false) -let localeTracker /** * Allows to determine, whether this module has been initialized. @@ -85,7 +84,7 @@ Item.initialize = async function () { plugins.forEach(({ name, plugin }) => processPlugin(name, plugin)) // setup reactive language updates - localeTracker = Tracker.autorun(() => { + Tracker.autorun(() => { const currentLocale = i18n.getLocale() ItemPlugins.onLanguageChange(currentLocale) .catch(e => console.error(e)) @@ -243,6 +242,8 @@ Item.extract = function (itemId, document) { item = found return true } + + return false }) return item diff --git a/src/imports/contexts/tasks/getTaskContexts.js b/src/imports/contexts/tasks/getTaskContexts.js index 9f09531..f226eb4 100644 --- a/src/imports/contexts/tasks/getTaskContexts.js +++ b/src/imports/contexts/tasks/getTaskContexts.js @@ -6,7 +6,7 @@ export const getTaskContexts = () => { import { WebResources } from '../resources/web/WebResources' import { LinkedResource } from '../resources/web/linked/LinkedResource' import { EmbeddedResource } from '../resources/web/embedded/EmbeddedResource' - import { Literature } from '../resources/web/literature/Literature' + import { Literature } from '../resources/web/literature/Literature' // FILES import { ImageFiles } from '../files/image/ImageFiles' diff --git a/src/imports/contexts/tasks/responseProcessors/ResponseProcessorRegistry.js b/src/imports/contexts/tasks/responseProcessors/ResponseProcessorRegistry.js index e99fd4a..b7f2b42 100644 --- a/src/imports/contexts/tasks/responseProcessors/ResponseProcessorRegistry.js +++ b/src/imports/contexts/tasks/responseProcessors/ResponseProcessorRegistry.js @@ -49,7 +49,7 @@ const checkResponseProcessorContext = ({ name, label, icon, isResponseProcessor, * @return {any} */ ResponseProcessorRegistry.register = function (context) { - const { name, type, dataTypes, fileType, csp, renderer } = context + const { name, type, dataTypes, fileType /*, csp, renderer */ } = context debugLog('register', { context }) check(name, String) diff --git a/src/imports/contexts/tasks/responseProcessors/aggregate/audioList/audioResultsRenderer.js b/src/imports/contexts/tasks/responseProcessors/aggregate/audioList/audioResultsRenderer.js index b964e97..dc2e81f 100644 --- a/src/imports/contexts/tasks/responseProcessors/aggregate/audioList/audioResultsRenderer.js +++ b/src/imports/contexts/tasks/responseProcessors/aggregate/audioList/audioResultsRenderer.js @@ -31,7 +31,7 @@ Template.audioResultsRenderer.onCreated(function () { }, onReady: () => { API.debug('sub complete') - }, + } } }) c.stop() diff --git a/src/imports/contexts/tasks/responseProcessors/aggregate/barChart/rpBarChart.js b/src/imports/contexts/tasks/responseProcessors/aggregate/barChart/rpBarChart.js index c83272d..2c64e40 100644 --- a/src/imports/contexts/tasks/responseProcessors/aggregate/barChart/rpBarChart.js +++ b/src/imports/contexts/tasks/responseProcessors/aggregate/barChart/rpBarChart.js @@ -28,7 +28,6 @@ Template.rpBarChart.onRendered(function () { instance.autorun(() => { const data = Template.currentData() const { choices = [], results, api } = data - const item = api.item() const dataType = api.dataType() const plotData = { diff --git a/src/imports/contexts/tasks/responseProcessors/aggregate/documentList/documentResultsRenderer.js b/src/imports/contexts/tasks/responseProcessors/aggregate/documentList/documentResultsRenderer.js index 9b4da4e..e9ba31a 100644 --- a/src/imports/contexts/tasks/responseProcessors/aggregate/documentList/documentResultsRenderer.js +++ b/src/imports/contexts/tasks/responseProcessors/aggregate/documentList/documentResultsRenderer.js @@ -63,7 +63,7 @@ Template.documentResultsRenderer.helpers({ return DocumentFiles.name }, hasThumbnail (fileObj = {}) { - return fileObj.isPDF && fileObj.versions?.thumbnail; + return fileObj.isPDF && fileObj.versions?.thumbnail }, getThumbnail (fileObj) { return getFilesLink({ diff --git a/src/imports/contexts/tasks/responseProcessors/aggregate/imageGallery/imageResultsRenderer.js b/src/imports/contexts/tasks/responseProcessors/aggregate/imageGallery/imageResultsRenderer.js index d5feb18..46d5103 100644 --- a/src/imports/contexts/tasks/responseProcessors/aggregate/imageGallery/imageResultsRenderer.js +++ b/src/imports/contexts/tasks/responseProcessors/aggregate/imageGallery/imageResultsRenderer.js @@ -8,7 +8,7 @@ import '../shared/cssbox.scss' import './imageResultsRenderer.html' const API = Template.imageResultsRenderer.setDependencies({ - contexts: [ImageFiles], + contexts: [ImageFiles] }) const ImageFilesCollection = getFilesCollection(ImageFiles.name) diff --git a/src/imports/contexts/tasks/responseProcessors/aggregate/pieChart/rpPieChart.js b/src/imports/contexts/tasks/responseProcessors/aggregate/pieChart/rpPieChart.js index bb15dc8..78d8304 100644 --- a/src/imports/contexts/tasks/responseProcessors/aggregate/pieChart/rpPieChart.js +++ b/src/imports/contexts/tasks/responseProcessors/aggregate/pieChart/rpPieChart.js @@ -12,7 +12,7 @@ const baseLayout = { } } -const TemplateAPI = Template.rpPieChart.setDependencies({}) +Template.rpPieChart.setDependencies({}) Template.rpPieChart.onRendered(function () { const instance = this diff --git a/src/imports/contexts/tasks/results/TaskResultUtils.js b/src/imports/contexts/tasks/results/TaskResultUtils.js index 3e655c8..8cf7037 100644 --- a/src/imports/contexts/tasks/results/TaskResultUtils.js +++ b/src/imports/contexts/tasks/results/TaskResultUtils.js @@ -43,7 +43,7 @@ export const fromResponse = ({ taskId, itemId, taskDoc, page, response }) => { return dataType.from(response, item) } - if (!dataType in ResponseDataTypes) { + if (!(dataType in ResponseDataTypes)) { throw new Error(`Unexpected dataType "${dataType}".`) } diff --git a/src/imports/contexts/tasks/results/TaskResults.js b/src/imports/contexts/tasks/results/TaskResults.js index 925aa30..3603bd5 100644 --- a/src/imports/contexts/tasks/results/TaskResults.js +++ b/src/imports/contexts/tasks/results/TaskResults.js @@ -72,7 +72,6 @@ TaskResults.methods.saveTask = { TaskResults.publications = {} - /** * Reveal all results for a specific item, usable for instance in presentation mode */ @@ -104,6 +103,7 @@ TaskResults.publications.byGroup = { }, run: onServerExec(function () { import { getAllTasksByGroupAndItem } from './methods/getAllTaskByGroup' + return getAllTasksByGroupAndItem }) } diff --git a/src/imports/contexts/tasks/results/methods/getAllTaskByGroup.js b/src/imports/contexts/tasks/results/methods/getAllTaskByGroup.js index fc13d2b..5a7a29f 100644 --- a/src/imports/contexts/tasks/results/methods/getAllTaskByGroup.js +++ b/src/imports/contexts/tasks/results/methods/getAllTaskByGroup.js @@ -1,8 +1,8 @@ import { Group } from '../../../classroom/group/Group' import { createDocGetter } from '../../../../api/utils/document/createDocGetter' import { PermissionDeniedError } from '../../../../api/errors/types/PermissionDeniedError' -import {getCollection} from '../../../../api/utils/getCollection' -import {TaskResults} from '../TaskResults' +import { getCollection } from '../../../../api/utils/getCollection' +import { TaskResults } from '../TaskResults' const getGroupDoc = createDocGetter(Group) diff --git a/src/imports/contexts/tasks/results/methods/saveTaskResult.js b/src/imports/contexts/tasks/results/methods/saveTaskResult.js index 15a8f35..e864275 100644 --- a/src/imports/contexts/tasks/results/methods/saveTaskResult.js +++ b/src/imports/contexts/tasks/results/methods/saveTaskResult.js @@ -1,4 +1,4 @@ -import {Meteor} from 'meteor/meteor' +import { Meteor } from 'meteor/meteor' import { LessonErrors } from '../../../classroom/lessons/LessonErrors' import { SchoolClass } from '../../../classroom/schoolclass/SchoolClass' import { LessonStates } from '../../../classroom/lessons/LessonStates' diff --git a/src/package.json b/src/package.json index bda6e15..a5f4b0a 100644 --- a/src/package.json +++ b/src/package.json @@ -108,6 +108,7 @@ "AutoForm": "readonly" }, "rules": { + "import/no-duplicates":"off", "brace-style": [ "error", "stroustrup", From 7dfd1276a113e22323b9590d97a49afd25c4546b Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Mon, 21 Nov 2022 18:14:05 +0100 Subject: [PATCH 04/24] fix: client tests fixed --- src/.meteor/versions | 2 +- src/imports/api/utils/archUtils.js | 5 ++-- src/imports/contexts/files/Files.js | 6 +++-- .../system/accounts/users/UserUtils.js | 9 +++---- .../accounts/users/tests/UserUtils.tests.js | 24 +++++++++++++------ .../pipelines/client/buildPipeline.tests.js | 9 ++++++- 6 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/.meteor/versions b/src/.meteor/versions index 5eff68c..bd5ec09 100644 --- a/src/.meteor/versions +++ b/src/.meteor/versions @@ -90,7 +90,7 @@ mongo@1.15.0 mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 -muqube:autoform-nouislider@0.6.0 +muqube:autoform-nouislider@0.5.2 npm-mongo@4.3.1 observe-sequence@1.0.20 ordered-dict@1.1.0 diff --git a/src/imports/api/utils/archUtils.js b/src/imports/api/utils/archUtils.js index cb86d7e..62bef58 100644 --- a/src/imports/api/utils/archUtils.js +++ b/src/imports/api/utils/archUtils.js @@ -12,10 +12,11 @@ export const onClientExec = fct => Meteor.isClient ? fct() : undefined export const auto = fct => fct() export const isomporph = ({ client, server }) => { - if (client && Meteor.isClient) { + if (Meteor.isClient && client) { return client() } - if (server && Meteor.isServer) { + if (Meteor.isServer && server) { return server() } + return null } diff --git a/src/imports/contexts/files/Files.js b/src/imports/contexts/files/Files.js index 85325a8..3d1c19a 100644 --- a/src/imports/contexts/files/Files.js +++ b/src/imports/contexts/files/Files.js @@ -34,10 +34,12 @@ Files.helpers = {} Files.getMaterialContexts = auto(function () { import { isMaterial } from '../material/isMaterial' - return function () { + return function getMaterialContexts () { const contexts = [] filesMap.forEach(ctx => { - if (isMaterial(ctx)) contexts.push(ctx) + if (isMaterial(ctx)) { + contexts.push(ctx) + } }) return contexts } diff --git a/src/imports/contexts/system/accounts/users/UserUtils.js b/src/imports/contexts/system/accounts/users/UserUtils.js index de436b2..fdd4116 100644 --- a/src/imports/contexts/system/accounts/users/UserUtils.js +++ b/src/imports/contexts/system/accounts/users/UserUtils.js @@ -139,23 +139,24 @@ UserUtils.isCurriculum = function (userId = Meteor.userId(), scope) { } UserUtils.isAdmin = isomporph({ - client: onClient(function () { + client: function () { return function isAdmin (userId = Meteor.userId()) { if (!userId) return false const user = Meteor.users.findOne(userId) + if (!user) return false return Roles.userIsInRole(userId, UserUtils.roles.admin, user.institution) } - }), + }, - server: onServer(function () { + server: function () { import { userIsAdmin } from '../../../../api/accounts/admin/userIsAdmin' return function isAdmin (userId = Meteor.userId()) { if (!userId) return false return userIsAdmin(userId) } - }) + } }) const roleMap = mapFromObject(UserUtils.roles) diff --git a/src/imports/contexts/system/accounts/users/tests/UserUtils.tests.js b/src/imports/contexts/system/accounts/users/tests/UserUtils.tests.js index 13d6922..8dccd59 100644 --- a/src/imports/contexts/system/accounts/users/tests/UserUtils.tests.js +++ b/src/imports/contexts/system/accounts/users/tests/UserUtils.tests.js @@ -5,6 +5,7 @@ import { UserUtils } from '../UserUtils' import { assert } from 'chai' import { mockCollection } from '../../../../../../tests/testutils/mockCollection' import { Admin } from '../../admin/Admin' +import { onClientExec, onServer, onServerExec } from '../../../../../api/utils/archUtils' const userObj = () => ({ _id: Random.id(), @@ -39,15 +40,24 @@ describe('UserUtils', function () { assert.isFalse(UserUtils.isAdmin()) }) - it('returns false for a fake admin', function () { - stubUser(user, user._id, [UserUtils.roles.admin], user.institution) - assert.isFalse(UserUtils.isAdmin()) + onServerExec(function () { + it('returns false for a fake admin', function () { + stubUser(user, user._id, [UserUtils.roles.admin], user.institution) + assert.isFalse(UserUtils.isAdmin()) + }) + + it('returns true for a true admin', function () { + stubUser(user, user._id, [UserUtils.roles.admin], user.institution) + AdminCollection.insert({ userId: user._id }) + assert.isTrue(UserUtils.isAdmin()) + }) }) - it('returns true for a true admin', function () { - stubUser(user, user._id, [UserUtils.roles.admin], user.institution) - AdminCollection.insert({ userId: user._id }) - assert.isTrue(UserUtils.isAdmin()) + onClientExec(function () { + it('returns true for a roles-admin', function () { + stubUser(user, user._id, [UserUtils.roles.admin], user.institution) + assert.isTrue(UserUtils.isAdmin()) + }) }) }) diff --git a/src/imports/infrastructure/pipelines/client/buildPipeline.tests.js b/src/imports/infrastructure/pipelines/client/buildPipeline.tests.js index 1517306..42ce0cb 100644 --- a/src/imports/infrastructure/pipelines/client/buildPipeline.tests.js +++ b/src/imports/infrastructure/pipelines/client/buildPipeline.tests.js @@ -29,7 +29,14 @@ describe(buildPipeline.name, function () { const context = { name: Random.id(6), - isFilesCollection: true + isFilesCollection: true, + files: { + type: 'custom', + extensions: ['.abc'], + accept: 'custom/abc', + maxSize: 100000000, + converter: file => file + } } const products = buildPipeline(context, options) From 3816b86b9cc7f569b031440e90f15576640ef7b6 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Tue, 22 Nov 2022 10:03:37 +0100 Subject: [PATCH 05/24] fix: tests --- src/.settings-schema.js | 11 +- .../accounts/admin/tests/adminExists.tests.js | 20 +++- .../accounts/admin/tests/createAdmin.tests.js | 28 +++-- .../accounts/admin/tests/removeAdmin.tests.js | 23 +++- .../accounts/admin/tests/userIsAdmin.tests.js | 21 +++- .../api/accounts/registration/UserFactory.js | 13 +- .../registration/registerUserSchema.js | 70 +++++++---- .../registration/tests/UserFactory.tests.js | 90 ++++++++------ .../tests/rollbackAccount.tests.js | 21 +++- .../schoolclass/methods/removeClass.js | 7 ++ .../schoolclass/tests/SchoolClass.tests.js | 26 +++- .../results/tests/TaskWorkingState.tests.js | 13 +- .../state/methods/saveTaskWorkingState.js | 1 + .../pipelines/server/buildPipeline.tests.js | 6 +- src/scripts/test.sh | 2 +- src/settings.json | 1 + src/tests/settings.json | 112 ++++++++++++++++++ src/tests/testutils/collectPublication.js | 4 +- src/tests/testutils/mockCollection.js | 67 ++++++++--- 19 files changed, 411 insertions(+), 125 deletions(-) create mode 100644 src/tests/settings.json diff --git a/src/.settings-schema.js b/src/.settings-schema.js index b604fe7..ac84d47 100644 --- a/src/.settings-schema.js +++ b/src/.settings-schema.js @@ -12,7 +12,7 @@ const optionalBoolean = { const monitorSchema = schema({ constructView: optionalBoolean, - onCreated:optionalBoolean, + onCreated: optionalBoolean, onRendered: optionalBoolean, onDestroyed: optionalBoolean, registerHelper: optionalBoolean, @@ -90,12 +90,17 @@ module.exports = schema({ 'fixtures.teacher': optionalArray, 'fixtures.teacher.$': accountsFixtureSchema, 'fixtures.schoolAdmin': optionalArray, - 'fixtures.schoolAdmin.$': accountsFixtureSchema, + 'fixtures.schoolAdmin.$': accountsFixtureSchema }), patch: patchSchema, public: schema({ + logLevel: { + type: Number, + optional: true, + allowedValues: [0, 1, 2, 3, 4] + }, features: schema({ - groups:Boolean + groups: Boolean }), defaultLocale: String, templateMonitor: monitorSchema, diff --git a/src/imports/api/accounts/admin/tests/adminExists.tests.js b/src/imports/api/accounts/admin/tests/adminExists.tests.js index a21f7d0..d13d7e9 100644 --- a/src/imports/api/accounts/admin/tests/adminExists.tests.js +++ b/src/imports/api/accounts/admin/tests/adminExists.tests.js @@ -1,15 +1,27 @@ import { Meteor } from 'meteor/meteor' import { Admin } from '../../../../contexts/system/accounts/admin/Admin' import { adminExists } from '../adminExists' -import { mockCollection } from '../../../../../tests/testutils/mockCollection' +import { + clearCollections, + mockCollections, + restoreAllCollections +} from '../../../../../tests/testutils/mockCollection' import { Random } from 'meteor/random' import { expect } from 'chai' -const AdminCollection = mockCollection(Admin) +let AdminCollection describe(adminExists.name, function () { - beforeEach(function () { - AdminCollection.remove({}) + before(function () { + AdminCollection = mockCollections(Admin) + }) + + afterEach(function () { + clearCollections(Admin) + }) + + after(function () { + restoreAllCollections() }) it('returns false if no admin exists', function () { diff --git a/src/imports/api/accounts/admin/tests/createAdmin.tests.js b/src/imports/api/accounts/admin/tests/createAdmin.tests.js index 68f79aa..df6bec8 100644 --- a/src/imports/api/accounts/admin/tests/createAdmin.tests.js +++ b/src/imports/api/accounts/admin/tests/createAdmin.tests.js @@ -1,16 +1,28 @@ -import { Meteor } from 'meteor/meteor' import { Admin } from '../../../../contexts/system/accounts/admin/Admin' +import { Users } from '../../../../contexts/system/accounts/users/User' import { createAdmin } from '../createAdmin' -import { mockCollection } from '../../../../../tests/testutils/mockCollection' +import { + clearCollection, clearCollections, + mockCollections, + restoreAllCollections +} from '../../../../../tests/testutils/mockCollection' import { Random } from 'meteor/random' import { expect } from 'chai' -const AdminCollection = mockCollection(Admin) +let AdminCollection +let UsersCollection describe(createAdmin.name, function () { - beforeEach(function () { - Meteor.users.remove({}) - AdminCollection.remove({}) + before(function () { + [AdminCollection, UsersCollection] = mockCollections(Admin, Users) + }) + + afterEach(function () { + clearCollections(Admin, Users) + }) + + after(function () { + restoreAllCollections() }) it('throws if no userId is given', function () { @@ -21,12 +33,12 @@ describe(createAdmin.name, function () { expect(() => createAdmin(Random.id())).to.throw('userId is invalid in null insert') }) it('throws if the user is already an Admin', function () { - const userId = Meteor.users.insert({ username: Random.id() }) + const userId = UsersCollection.insert({ username: Random.id() }) AdminCollection.insert({ userId }) expect(() => createAdmin(userId)).to.throw('createAdmin.failed').with.property('reason', 'createAdmin.alreadyAdmin') }) it(`inserts a userId to the ${Admin.name} collections`, function () { - const userId = Meteor.users.insert({ username: Random.id() }) + const userId = UsersCollection.insert({ username: Random.id() }) const adminId = createAdmin(userId) const adminDoc = AdminCollection.findOne(adminId) diff --git a/src/imports/api/accounts/admin/tests/removeAdmin.tests.js b/src/imports/api/accounts/admin/tests/removeAdmin.tests.js index 3d8fe1c..27bcaf1 100644 --- a/src/imports/api/accounts/admin/tests/removeAdmin.tests.js +++ b/src/imports/api/accounts/admin/tests/removeAdmin.tests.js @@ -1,16 +1,29 @@ import { Meteor } from 'meteor/meteor' import { Admin } from '../../../../contexts/system/accounts/admin/Admin' import { removeAdmin } from '../removeAdmin' -import { mockCollection } from '../../../../../tests/testutils/mockCollection' +import { + mockCollections, + clearCollections, + restoreAllCollections +} from '../../../../../tests/testutils/mockCollection' import { Random } from 'meteor/random' import { expect } from 'chai' +import { Users } from '../../../../contexts/system/accounts/users/User' -const AdminCollection = mockCollection(Admin) +let AdminCollection +let UsersCollection describe(removeAdmin.name, function () { - beforeEach(function () { - Meteor.users.remove({}) - AdminCollection.remove({}) + before(function () { + [AdminCollection, UsersCollection] = mockCollections(Admin, Users) + }) + + afterEach(function () { + clearCollections(Admin, Users) + }) + + after(function () { + restoreAllCollections() }) it('throws if no userId is given', function () { diff --git a/src/imports/api/accounts/admin/tests/userIsAdmin.tests.js b/src/imports/api/accounts/admin/tests/userIsAdmin.tests.js index bd5b074..919631f 100644 --- a/src/imports/api/accounts/admin/tests/userIsAdmin.tests.js +++ b/src/imports/api/accounts/admin/tests/userIsAdmin.tests.js @@ -2,15 +2,26 @@ import { Meteor } from 'meteor/meteor' import { Random } from 'meteor/random' import { Admin } from '../../../../contexts/system/accounts/admin/Admin' import { userIsAdmin } from '../userIsAdmin' -import { mockCollection } from '../../../../../tests/testutils/mockCollection' +import { + clearCollections, + mockCollections, + restoreAllCollections +} from '../../../../../tests/testutils/mockCollection' import { expect } from 'chai' +import { Users } from '../../../../contexts/system/accounts/users/User' -const AdminCollection = mockCollection(Admin) +let AdminCollection +let UsersCollection describe(userIsAdmin.name, function () { - beforeEach(function () { - Meteor.users.remove({}) - AdminCollection.remove({}) + before(function () { + [AdminCollection, UsersCollection] = mockCollections(Admin, Users) + }) + afterEach(function () { + clearCollections(Admin, Users) + }) + after(function () { + restoreAllCollections() }) it('throws if no userId is given', function () { diff --git a/src/imports/api/accounts/registration/UserFactory.js b/src/imports/api/accounts/registration/UserFactory.js index 8dda205..9181299 100644 --- a/src/imports/api/accounts/registration/UserFactory.js +++ b/src/imports/api/accounts/registration/UserFactory.js @@ -64,9 +64,9 @@ UserFactory.create = function create ({ email, password, role, firstName, lastNa const profileDoc = { $set: { role, - firstName, - lastName, - institution + firstName: clean(firstName), + lastName: clean(lastName), + institution: clean(institution) } } @@ -95,3 +95,10 @@ UserFactory.create = function create ({ email, password, role, firstName, lastNa return userId } + +const clean = name => { + const cleaned = name.trim().replace(/\s+/g, ' ') + const first = cleaned.substring(0, 1).toUpperCase() + const rest = cleaned.substring(1, name.length) + return `${first}${rest}` +} \ No newline at end of file diff --git a/src/imports/api/accounts/registration/registerUserSchema.js b/src/imports/api/accounts/registration/registerUserSchema.js index 6b5415d..7cd5974 100644 --- a/src/imports/api/accounts/registration/registerUserSchema.js +++ b/src/imports/api/accounts/registration/registerUserSchema.js @@ -3,14 +3,15 @@ import { i18n } from '../../language/language' import { Schema } from '../../schema/Schema' import { onClient } from '../../utils/archUtils' -const nameRegex = /^[\w'\-,.][^0-9_!¡?÷?¿/\\+=@#$%ˆ&*(){}|~<>;:[\]]{2,}$/ +const nameRegex = /^[^!$%&/^()=?\\{[\]};<>|+#:\d]+$/ const passwordRegExp = '[a-z0-9A-Z_@\\-\\!\\?\\.]+' -export const firstNameSchema = ({ max = 50, autofocus, autocomplete, optional = false } = {}) => ({ +export const firstNameSchema = ({ label, max = 50, autofocus, autocomplete, optional = false } = {}) => ({ type: String, optional: optional, - label: onClient(i18n.reactive('userProfile.firstName')), + label: label || i18n.reactive('userProfile.firstName'), regEx: nameRegex, + min: 2, max: max, autoform: onClient({ type: 'text', @@ -19,10 +20,10 @@ export const firstNameSchema = ({ max = 50, autofocus, autocomplete, optional = }) }) -export const lastNameSchema = ({ max = 50, autofocus, autocomplete, optional = false } = {}) => ({ +export const lastNameSchema = ({ max = 50, label, autofocus, autocomplete, optional = false } = {}) => ({ type: String, optional: optional, - label: onClient(i18n.reactive('userProfile.lastName')), + label: label || i18n.reactive('userProfile.lastName'), regEx: nameRegex, max: max, autoform: onClient({ @@ -32,9 +33,9 @@ export const lastNameSchema = ({ max = 50, autofocus, autocomplete, optional = f }) }) -export const userNameSchema = ({ min = 4, max = 32, regExp = passwordRegExp } = {}) => ({ +export const userNameSchema = ({ label, min = 4, max = 32, regExp = passwordRegExp } = {}) => ({ type: String, - label: onClient(i18n.reactive('userProfile.username')), + label: label || i18n.reactive('userProfile.username'), regEx: regExp, min: min, max: max, @@ -43,9 +44,9 @@ export const userNameSchema = ({ min = 4, max = 32, regExp = passwordRegExp } = }) }) -export const codeSchema = ({ max = 20, label = i18n.reactive('codeRegister.code'), autofocus, autocomplete } = {}) => ({ +export const codeSchema = ({ max = 20, label, autofocus, autocomplete } = {}) => ({ type: String, - label: onClient(label), + label: label || i18n.reactive('codeRegister.code'), max: max, autoform: onClient({ autofocus: autofocus, @@ -55,14 +56,14 @@ export const codeSchema = ({ max = 20, label = i18n.reactive('codeRegister.code' let roleSchema -(function () { +;(function () { import { UserUtils } from '../../../contexts/system/accounts/users/UserUtils' const rolesList = Object.values(UserUtils.roles) let mappedRoles roleSchema = () => ({ type: String, - label: onClient(i18n.reactive('codeInvitation.role')), + label: i18n.reactive('codeInvitation.role'), allowedValues: rolesList, autoform: onClient({ firstOption: () => i18n.reactive('form.selectOne'), @@ -91,7 +92,7 @@ export { roleSchema } export const emailSchema = ({ hidden, label, classNames, autofocus, autocomplete, optional } = {}) => ({ type: String, optional: optional, - label: onClient(label || i18n.reactive('userProfile.email')), + label: label || i18n.reactive('userProfile.email'), max: 100, regEx: () => Schema.provider.RegEx.EmailWithTLD, autoform: onClient({ @@ -103,9 +104,16 @@ export const emailSchema = ({ hidden, label, classNames, autofocus, autocomplete }) }) -export const passwordSchemaClassic = ({ min = 8, max = 64, hint, autocomplete, regExp = passwordRegExp } = {}) => ({ +export const passwordSchemaClassic = ({ + label, + min = 8, + max = 64, + hint, + autocomplete, + regExp = passwordRegExp + } = {}) => ({ type: String, - label: onClient(i18n.reactive('userProfile.password')), + label: label || i18n.reactive('userProfile.password'), regEx: regExp && new RegExp(regExp), min: min, max: max, @@ -120,10 +128,23 @@ export const passwordSchemaClassic = ({ min = 8, max = 64, hint, autocomplete, r }) }) -export const password2Schema = ({ min = 8, label, max = 128, optional, autocomplete, rules, regExp, visibilityButton, visible, userIcon, css, autofocus } = {}) => ({ +export const password2Schema = ({ + min = 8, + label, + max = 128, + optional, + autocomplete, + rules, + regExp, + visibilityButton, + visible, + userIcon, + css, + autofocus + } = {}) => ({ type: String, optional: optional, - label: onClient(label || i18n.reactive('login.password.title')), + label: label || i18n.reactive('login.password.title'), min: min, max: max, autoform: onClient({ @@ -172,15 +193,16 @@ export const password2Schema = ({ min = 8, label, max = 128, optional, autocompl } }) -export const institutionSchema = ({ optional = true, max = 250 } = {}) => ({ +export const institutionSchema = ({ label, optional = false, max = 250 } = {}) => ({ type: String, - label: onClient(i18n.reactive('codeInvitation.institution')), - max: max + label: label || i18n.reactive('codeInvitation.institution'), + max: max, + optional: optional }) -export const confirmSchema = ({ formName, rules, visibilityButton, visible, userIcon, css } = {}) => ({ +export const confirmSchema = ({ label, rules, visibilityButton, visible, userIcon, css } = {}) => ({ type: String, - label: onClient(i18n.reactive('login.confirm')), + label: label || i18n.reactive('login.confirm'), autoform: onClient({ type: 'password2', rules: rules, @@ -200,13 +222,13 @@ export const confirmSchema = ({ formName, rules, visibilityButton, visible, user } }) -export const agreementSchema = () => ({ +export const agreementSchema = ({ termsOfServiceLabel, privacyLabel } = {}) => ({ termsOfService: { type: Boolean, - label: onClient(i18n.reactive('agreements.termsOfService.read')) + label: termsOfServiceLabel || i18n.reactive('agreements.termsOfService.read') }, privacyPolicy: { type: Boolean, - label: onClient(i18n.reactive('agreements.privacyPolicy.read')) + label: privacyLabel || i18n.reactive('agreements.privacyPolicy.read') } }) diff --git a/src/imports/api/accounts/registration/tests/UserFactory.tests.js b/src/imports/api/accounts/registration/tests/UserFactory.tests.js index 479d6f8..a1ecb5f 100644 --- a/src/imports/api/accounts/registration/tests/UserFactory.tests.js +++ b/src/imports/api/accounts/registration/tests/UserFactory.tests.js @@ -6,8 +6,14 @@ import { Accounts } from 'meteor/accounts-base' import { Roles } from 'meteor/alanning:roles' import { UserUtils } from '../../../../contexts/system/accounts/users/UserUtils' import { restoreAll, stub } from '../../../../../tests/testutils/stub' -import { mockCollection } from '../../../../../tests/testutils/mockCollection' +import { + clearCollections, + mockCollection, + mockCollections, + restoreAllCollections +} from '../../../../../tests/testutils/mockCollection' import { Admin } from '../../../../contexts/system/accounts/admin/Admin' +import { Users } from '../../../../contexts/system/accounts/users/User' const userDoc = ({ email, firstName, lastName, password, role, institution } = {}) => { const doc = { @@ -41,13 +47,19 @@ const loop = (times, fct) => { } describe(UserFactory.name, function () { - beforeEach(function () { - Meteor.users.remove({}) - }) + let AdminCollection + let UsersCollection + before(function () { + [AdminCollection, UsersCollection] = mockCollections(Admin, Users) + }) afterEach(function () { + clearCollections(Admin, Users) restoreAll() }) + after(function () { + restoreAllCollections() + }) describe('input validation', function () { it('throws on empty / missing input', function () { @@ -56,38 +68,37 @@ describe(UserFactory.name, function () { try { UserFactory.create({}) - } - catch (validationError) { + } catch (validationError) { expect(validationError.details).to.deep.equal([{ name: 'email', type: 'required', value: undefined, message: 'form.validation.required' }, - { - name: 'role', - type: 'required', - value: undefined, - message: 'form.validation.required' - }, - { - name: 'firstName', - type: 'required', - value: undefined, - message: 'form.validation.required' - }, - { - name: 'lastName', - type: 'required', - value: undefined, - message: 'form.validation.required' - }, - { - name: 'institution', - type: 'required', - value: undefined, - message: 'form.validation.required' - } + { + name: 'role', + type: 'required', + value: undefined, + message: 'form.validation.required' + }, + { + name: 'firstName', + type: 'required', + value: undefined, + message: 'form.validation.required' + }, + { + name: 'lastName', + type: 'required', + value: undefined, + message: 'form.validation.required' + }, + { + name: 'institution', + type: 'required', + value: undefined, + message: 'form.validation.required' + } ]) } }) @@ -129,7 +140,7 @@ describe(UserFactory.name, function () { loop(100, function () { const user = userDoc() const userId = UserFactory.create(user) - const createdUser = Meteor.users.findOne(userId) + const createdUser = UsersCollection.findOne(userId) expect(createdUser.emails[0].address).to.equal(user.email) }) }) @@ -137,7 +148,7 @@ describe(UserFactory.name, function () { loop(100, function () { const user = userDoc() const userId = UserFactory.create(user) - const createdUser = Meteor.users.findOne(userId) + const createdUser = UsersCollection.findOne(userId) expect(createdUser.firstName).to.equal(user.firstName) expect(createdUser.lastName).to.equal(user.lastName) expect(createdUser.institution).to.equal(user.institution) @@ -146,16 +157,16 @@ describe(UserFactory.name, function () { }) it('creates a user optionally with or without password', function () { const withoutPasswordUserId = UserFactory.create(userDoc()) - const withoutPasswordUser = Meteor.users.findOne(withoutPasswordUserId) + const withoutPasswordUser = UsersCollection.findOne(withoutPasswordUserId) expect(withoutPasswordUser.services.password).to.equal(undefined) const withPasswordUserId = UserFactory.create(userDoc({ password: Random.id() + '1' })) - const withPasswordUser = Meteor.users.findOne(withPasswordUserId) + const withPasswordUser = UsersCollection.findOne(withPasswordUserId) expect(withPasswordUser.services.password).to.be.an('object') }) it('strips any unnecessary whitespace from firstName, lastName and institution', function () { const user = userDoc({ - firstName: 'John the second ', + firstName: ' John the second ', lastName: 'doe ', institution: `where he @@ -164,7 +175,7 @@ describe(UserFactory.name, function () { }) const userId = UserFactory.create(user) - const createdUser = Meteor.users.findOne(userId) + const createdUser = UsersCollection.findOne(userId) expect(createdUser.firstName).to.equal('John the second') expect(createdUser.lastName).to.equal('Doe') expect(createdUser.institution).to.equal('Where he is working at') @@ -191,7 +202,7 @@ describe(UserFactory.name, function () { user = userDoc() assertRollback = () => { expect(Accounts.findUserByEmail(user.email)).to.equal(undefined) - expect(Meteor.users.find({ + expect(UsersCollection.find({ firstName: user.firstName, lastName: user.lastName, institution: user.institution @@ -199,14 +210,15 @@ describe(UserFactory.name, function () { } }) it('rolls back the account on Accounts.createUser failure', function () { - stub(Accounts, 'createUser', () => {}) + stub(Accounts, 'createUser', () => { + }) expect(() => UserFactory.create(user)).to.throw('createUser.failed') .with.property('reason', 'createUser.notCreated') assertRollback() }) it('rolls back the account on profile update failure', function () { - stub(Meteor.users, 'update', () => 0) + stub(UsersCollection, 'update', () => 0) expect(() => UserFactory.create(user)).to.throw('createUser.failed') .with.property('reason', 'createUser.profileNotUpdated') assertRollback() diff --git a/src/imports/api/accounts/registration/tests/rollbackAccount.tests.js b/src/imports/api/accounts/registration/tests/rollbackAccount.tests.js index 0304674..db4abbc 100644 --- a/src/imports/api/accounts/registration/tests/rollbackAccount.tests.js +++ b/src/imports/api/accounts/registration/tests/rollbackAccount.tests.js @@ -1,12 +1,29 @@ import { Random } from 'meteor/random' import { Admin } from '../../../../contexts/system/accounts/admin/Admin' +import { Users } from '../../../../contexts/system/accounts/users/User' import { rollbackAccount } from '../rollbackAccount' -import { mockCollection } from '../../../../../tests/testutils/mockCollection' +import { + clearCollections, + mockCollections, + restoreAllCollections +} from '../../../../../tests/testutils/mockCollection' import { expect } from 'chai' -const AdminCollection = mockCollection(Admin) + describe(rollbackAccount.name, function () { + let AdminCollection + let UsersCollection + + before(function () { + [AdminCollection, UsersCollection] = mockCollections(Admin, Users) + }) + afterEach(function () { + clearCollections(Admin, Users) + }) + after(function () { + restoreAllCollections() + }) it('removes a user from the account system', function () { const userId = Accounts.createUser({ username: Random.id() }) expect(Meteor.users.find(userId).count()).to.equal(1) diff --git a/src/imports/contexts/classroom/schoolclass/methods/removeClass.js b/src/imports/contexts/classroom/schoolclass/methods/removeClass.js index 689b217..c3580d9 100644 --- a/src/imports/contexts/classroom/schoolclass/methods/removeClass.js +++ b/src/imports/contexts/classroom/schoolclass/methods/removeClass.js @@ -7,6 +7,13 @@ import { removeLesson } from '../../lessons/methods/removeLesson' const getClassDoc = createGetDoc(SchoolClass) +/** + * Removes a class by given _id and userId. The user must be externally validated! + * @param classId {string} + * @param userId {string} + * @param log {function=} + * @return {number} + */ export const removeClass = function removeClass ({ classId, userId, log = () => {} }) { const { Lesson } = require('../../lessons/Lesson') const schoolClassDoc = getClassDoc.call({ userId }, classId) diff --git a/src/imports/contexts/classroom/schoolclass/tests/SchoolClass.tests.js b/src/imports/contexts/classroom/schoolclass/tests/SchoolClass.tests.js index 8459e0e..14096cb 100644 --- a/src/imports/contexts/classroom/schoolclass/tests/SchoolClass.tests.js +++ b/src/imports/contexts/classroom/schoolclass/tests/SchoolClass.tests.js @@ -2,7 +2,7 @@ import { Random } from 'meteor/random' import { SchoolClass } from '../SchoolClass' import { Lesson } from '../../lessons/Lesson' -import { mockCollection } from '../../../../../tests/testutils/mockCollection' +import { clearCollection, mockCollection, restoreAllCollections } from '../../../../../tests/testutils/mockCollection' import { DocNotFoundError } from '../../../../api/errors/types/DocNotFoundError' import { InvocationChecker } from '../../../../api/utils/InvocationChecker' import { onServerExec } from '../../../../api/utils/archUtils' @@ -11,8 +11,8 @@ import { restoreAll, stub } from '../../../../../tests/testutils/stub' import { stubMethod, unstubMethod } from '../../../../../tests/testutils/stubMethod' import { expect } from 'chai' -const SchoolClassCollection = mockCollection(SchoolClass) -const LessonCollection = mockCollection(Lesson) +let SchoolClassCollection +let LessonCollection const { isStudent } = SchoolClass.helpers const { isTeacher } = SchoolClass.helpers @@ -21,12 +21,22 @@ const { addStudent } = SchoolClass.helpers const { removeStudent } = SchoolClass.helpers describe(SchoolClass.name, function () { + + before(function () { + SchoolClassCollection = mockCollection(SchoolClass) + LessonCollection = mockCollection(Lesson) + }) + afterEach(function () { - SchoolClassCollection.remove({}) - LessonCollection.remove({}) + clearCollection(SchoolClass) + clearCollection(Lesson) restoreAll() }) + after(function () { + restoreAllCollections() + }) + describe('helpers', function () { describe(SchoolClass.helpers.isStudent.name, function () { it('throws if no classdoc is given', function () { @@ -249,7 +259,11 @@ describe(SchoolClass.name, function () { let otherLessons = [] otherLessons.length = Math.floor(1 + Math.random() * 10) otherLessons.fill(0) - otherLessons = otherLessons.map(() => LessonCollection.insert({ classId: otherClassId, title: Random.id(), createdBy: userId })) + otherLessons = otherLessons.map(() => LessonCollection.insert({ + classId: otherClassId, + title: Random.id(), + createdBy: userId + })) // before lessons.forEach(lessonId => expect(LessonCollection.find(lessonId).count()).to.equal(1)) diff --git a/src/imports/contexts/tasks/results/tests/TaskWorkingState.tests.js b/src/imports/contexts/tasks/results/tests/TaskWorkingState.tests.js index 78fb439..369cd8a 100644 --- a/src/imports/contexts/tasks/results/tests/TaskWorkingState.tests.js +++ b/src/imports/contexts/tasks/results/tests/TaskWorkingState.tests.js @@ -40,9 +40,12 @@ describe(TaskWorkingState.name, function () { const { lessonDoc, userId } = stubStudentDocs({ startedAt: new Date() }) const lessonId = lessonDoc._id const taskId = Random.id() - expect(() => saveState.call({ userId }, { lessonId, taskId })).to.throw('docNotFound') - expect(() => saveState.call({ userId }, { lessonId, taskId })).to.throw(taskId) + expect(() => saveState.call({ userId }, { lessonId, taskId })) + .to.throw('getDocument.docUndefined') + .with.property('details') + .with.property('query', taskId) }) + it('throws if a given groupdoc does not exist by griupd id') it('throws if the task is not editable', function () { const { lessonDoc, userId } = stubStudentDocs({ startedAt: new Date() }) const taskId = Random.id() @@ -53,8 +56,10 @@ describe(TaskWorkingState.name, function () { taskId: taskId } - expect(() => saveState.call({ userId }, insertDoc)).to.throw(TaskWorkingState.errors.taskNotEditable) - expect(() => saveState.call({ userId }, insertDoc)).to.throw(taskId) + expect(() => saveState.call({ userId }, insertDoc)) + .to.throw('taskWorkingState.notVisible') + .with.property('details') + .with.property('taskId', taskId) }) it('creates a new task progress document if none exists for the given task', function () { const taskId = Random.id() diff --git a/src/imports/contexts/tasks/state/methods/saveTaskWorkingState.js b/src/imports/contexts/tasks/state/methods/saveTaskWorkingState.js index 2bc36b9..47fbe0d 100644 --- a/src/imports/contexts/tasks/state/methods/saveTaskWorkingState.js +++ b/src/imports/contexts/tasks/state/methods/saveTaskWorkingState.js @@ -17,6 +17,7 @@ const getGroupDoc = createDocGetter({ name: Group.name, optional: false }) * Saves a current task working state. * @param lessonId * @param taskId + * @param groupId * @param complete * @param page * @param progress diff --git a/src/imports/infrastructure/pipelines/server/buildPipeline.tests.js b/src/imports/infrastructure/pipelines/server/buildPipeline.tests.js index 2b78176..33c09c3 100644 --- a/src/imports/infrastructure/pipelines/server/buildPipeline.tests.js +++ b/src/imports/infrastructure/pipelines/server/buildPipeline.tests.js @@ -1,5 +1,5 @@ /* eslint-env mocha */ -import { Mongo } from 'meteor/mongo' +import { Mongo, Cursor } from 'meteor/mongo' import { Random } from 'meteor/random' import { FilesCollection } from 'meteor/ostrio:files' import { expect } from 'chai' @@ -93,10 +93,10 @@ describe(buildPipeline.name, function () { const products = buildPipeline(context, options) const Collection = products.collection - const publication = products.publications[0] const docId = Collection.insert({ title }) - const docs = collectPublication(publication()) + const cursor = publication() + const docs = collectPublication(cursor) expect(docs.length).to.equal(1) expect(docs[0]._id).to.equal(docId) diff --git a/src/scripts/test.sh b/src/scripts/test.sh index 422c725..38d6022 100755 --- a/src/scripts/test.sh +++ b/src/scripts/test.sh @@ -116,5 +116,5 @@ METEOR_PACKAGE_DIRS=${T_PACKAGE_DIRS} \ meteor test \ ${T_RUN_ONCE} \ --driver-package=meteortesting:mocha \ - --settings=settings.json \ + --settings=tests/settings.json \ --port=${PORT} diff --git a/src/settings.json b/src/settings.json index 4ae87ab..d82dff9 100644 --- a/src/settings.json +++ b/src/settings.json @@ -1,5 +1,6 @@ { "public": { + "logLevel": 3, "features": { "groups": true }, diff --git a/src/tests/settings.json b/src/tests/settings.json new file mode 100644 index 0000000..d82dff9 --- /dev/null +++ b/src/tests/settings.json @@ -0,0 +1,112 @@ +{ + "public": { + "logLevel": 3, + "features": { + "groups": true + }, + "defaultLocale": "en", + "siteName": "CLAIRE", + "templateMonitor": { + "constructView": false, + "onCreated": false, + "onRendered": false, + "onDestroyed": false, + "helpers": false, + "events": false, + "registerHelper": false + }, + "classroom": { + "maxUsers": 100 + }, + "password": { + "min": { + "value": 8, + "rule": true + }, + "max": { + "value": 128, + "rule": false + }, + "allowedChars": { + "value": "^[a-z0-9A-Z_@&\\-\\+\/\\!\\?\\.]+$", + "message": "Buchstaben, Zahlen und Sonderzeichen @ & _ + - ! ? . /", + "rule": true + }, + "icon": "lock", + "confirm": true, + "blacklist": [ + "^\\d+$", + "^[a-zA-Z]+$", + "^.*p(a|@|4)(s|5)+w(o|0)r(d|t).*$", + "^.*h(e|3|a|4|@)llo.*$", + "^.*f(i|1)ck(e|3)n.*$", + "^.*1234.*$", + "^.*abcd.*$", + "^.*qwer.*$", + "^.*asdf.*$", + "^.*yxcv.*$" + ] + } + }, + "patch": { + "removeDeadReferences": true + }, + "defaultLocale": "en", + "files": { + "bucketName": "fs", + "images": { + "maxSize": 6144000 + } + }, + "curriculum": { + "sync": { + "enabled": false, + "username": "syncuser", + "password": "01234567890", + "url": "localhost:7070" + } + }, + "emailTemplates": { + "from": "no-reply@claireapp.cloud", + "siteName": "CLAIRE", + "textEncoding": "quoted-printable", + "supportEmail": "support@claireapp.cloud" + }, + "accounts": { + "admin": { + "firstName": "mister", + "lastName": "superadmin", + "email": "admin@claireapp.cloud", + "institution": "admins" + }, + "config": { + "forbidClientAccountCreation": true, + "ambiguousErrorMessages": true, + "sendVerificationEmail": false, + "loginExpirationInDays": 90, + "passwordResetTokenExpirationInDays": 1, + "passwordEnrollTokenExpirationInDays": 3 + }, + "inform": { + "passwordReset": "admin@claireapp.cloud" + }, + "fixtures": { + "schoolAdmin": [ + { + "firstName": "Simon", + "lastName": "Sleepy", + "email": "sleepy.simon@claireapp.cloud", + "institution": "Best School" + } + ], + "teacher": [ + { + "firstName": "Cindy", + "lastName": "Candy", + "email": "candy.cindy@claireapp.cloud", + "institution": "Best School" + } + ] + } + } +} \ No newline at end of file diff --git a/src/tests/testutils/collectPublication.js b/src/tests/testutils/collectPublication.js index 875bcf0..a8737da 100644 --- a/src/tests/testutils/collectPublication.js +++ b/src/tests/testutils/collectPublication.js @@ -1,8 +1,8 @@ -import { Mongo } from 'meteor/mongo' +import { LocalCollection } from 'meteor/minimongo' import { assert } from 'chai' export const collectPublication = cursor => { - if (!cursor || !(cursor instanceof Mongo.Cursor)) { + if (!cursor?.fetch) { assert.fail('expected cursor') } return cursor.fetch() diff --git a/src/tests/testutils/mockCollection.js b/src/tests/testutils/mockCollection.js index 4d49859..dce14d6 100644 --- a/src/tests/testutils/mockCollection.js +++ b/src/tests/testutils/mockCollection.js @@ -1,33 +1,68 @@ import { Mongo } from 'meteor/mongo' import { Schema } from '../../imports/api/schema/Schema' +import Collection2 from 'meteor/aldeed:collection2' -const _locals = {} +// XXX: backwards compat for pre 4.0 collection2 +if (Collection2 && 'function' === typeof Collection2.load) { + Collection2.load() +} -Mongo.Collection.get = name => _locals[name] +const originals = new Map() +Mongo.Collection.get = name => originals.get(name) -export const mockCollection = ({ name, schema } = {}, { noSchema = false, override = false } = {}) => { +export const mockCollection = ({ name, schema } = {}, { + noSchema = false, + noDefaults = false, + override = false +} = {}) => { let collection = Mongo.Collection.get(name) - if (collection) { - collection.remove({}) - - if (override) { - delete _locals[name] - } - - else { - return collection - } + if (collection && override) { + originals.delete(name) } - collection = new Mongo.Collection(null) + if (collection) { + return collection + } + else { + collection = new Mongo.Collection(null) + } if (!noSchema) { - const schemaInstance = Schema.withDefault(schema) + const schemaInstance = noDefaults + ? Schema.create(schema) + : Schema.withDefault(schema) collection.attachSchema(schemaInstance) } - _locals[name] = collection + originals.set(name, collection) return collection } + +export const mockCollections = (...collections) => { + return collections.map(c => { + return (Array.isArray(c)) + ? mockCollection(c[0], c[1]) + : mockCollection(c) + }) +} + +export const restoreCollection = ({ name }) => { + const collection = originals.get(name) + return collection && originals.delete(name) +} + +export const restoreAllCollections = () => { + originals.forEach(collection => collection.remove({})) + originals.clear() +} + +const clearCollection = ({ name }) => { + const collection = originals.get(name) + return collection && collection.remove({}) +} + +export const clearCollections = (...contexts) => { + return contexts.map(c => clearCollection(c)) +} From d73d90fd7c797566abeb59f8c6af5ce76428dadd Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Thu, 24 Nov 2022 14:35:48 +0100 Subject: [PATCH 06/24] fix: tests updated and issues fixed --- src/imports/api/accounts/admin/createAdmin.js | 18 +- src/imports/api/accounts/admin/removeAdmin.js | 6 +- .../accounts/admin/tests/adminExists.tests.js | 9 +- .../accounts/admin/tests/createAdmin.tests.js | 2 +- .../accounts/admin/tests/removeAdmin.tests.js | 12 +- .../accounts/admin/tests/userIsAdmin.tests.js | 2 +- src/imports/api/accounts/admin/userIsAdmin.js | 18 +- .../api/accounts/registration/UserFactory.js | 3 +- .../accounts/registration/rollbackAccount.js | 3 +- .../registration/tests/UserFactory.tests.js | 103 ++- .../tests/rollbackAccount.tests.js | 10 +- .../api/accounts/user/getUserByEmail.js | 5 + .../accounts/user/tests/userExists.tests.js | 16 +- src/imports/api/accounts/user/userExists.js | 9 +- src/imports/api/utils/InvocationChecker.js | 4 + .../api/utils/document/createDocCloner.js | 2 - .../api/utils/document/createDocGetter.js | 20 +- src/imports/api/utils/documentUtils.js | 4 +- src/imports/api/utils/getCollection.js | 20 +- src/imports/api/utils/getUsersCollection.js | 13 + .../contexts/beamer/tests/Beamer.tests.js | 23 +- .../classroom/invitations/CodeInvitations.js | 108 ++- .../invitations/tests/CodeInvitation.tests.js | 62 +- .../contexts/classroom/lessons/Lesson.js | 255 ++----- .../classroom/lessons/LessonHelpers.js | 124 ++++ .../classroom/lessons/methods/removeLesson.js | 84 ++- .../lessons/runtime/LessonRuntime.js | 19 +- .../lessons/runtime/isMemberOfLesson.js | 7 +- .../lessons/runtime/removeDocuments.js | 2 - .../classroom/lessons/runtime/resetBeamer.js | 1 + .../classroom/lessons/runtime/resetGroups.js | 21 +- .../classroom/lessons/tests/Lesson.tests.js | 661 ++++++++---------- .../lessons/tests/LessonHelpers.tests.js | 177 +++++ .../lessons/tests/LessonRuntime.tests.js | 107 ++- .../contexts/classroom/lessons/tests/index.js | 3 +- .../classroom/schoolclass/SchoolClass.js | 5 + .../schoolclass/helpers/isMemberOfClass.js | 19 +- .../schoolclass/methods/removeClass.js | 7 +- .../schoolclass/tests/SchoolClass.tests.js | 74 +- .../curriculum/curriculum/unit/Unit.js | 1 + .../curriculum/unit/tests/Unit.tests.js | 22 +- .../contexts/system/accounts/admin/Admin.js | 31 +- .../accounts/admin/tests/Admin.tests.js | 234 ++++--- .../contexts/system/accounts/users/User.js | 46 +- .../system/accounts/users/UserUtils.js | 24 +- .../system/accounts/users/methods/getUser.js | 3 +- .../users/methods/registerWithCode.js | 15 +- .../users/methods/resendVerificationEmail.js | 4 +- .../users/methods/sendResetPasswordEmail.js | 4 +- .../accounts/users/methods/updateProfile.js | 3 +- .../system/accounts/users/methods/updateUI.js | 5 +- .../accounts/users/methods/verifyToken.js | 33 +- .../accounts/users/tests/UserUtils.tests.js | 20 +- .../accounts/users/tests/Users.tests.js | 381 +++++----- .../system/accounts/users/usersByClass.js | 25 +- .../methods/getAllTaskResultsByTask.js | 3 +- .../results/methods/getAllTasksByItem.js | 4 +- .../tasks/results/methods/saveTaskResult.js | 5 +- .../tasks/results/tests/TaskResults.tests.js | 63 +- .../results/tests/TaskWorkingState.tests.js | 37 +- .../contexts/tasks/state/methods/byLesson.js | 4 +- .../state/methods/saveTaskWorkingState.js | 4 +- .../pipelines/server/buildPipeline.tests.js | 2 +- src/tests/main.js | 5 + src/tests/testutils/doc/createCodeDoc.js | 5 +- src/tests/testutils/doc/mockClassDoc.js | 18 + src/tests/testutils/doc/mockPhaseDoc.js | 37 + src/tests/testutils/doc/mockUnitDoc.js | 61 ++ src/tests/testutils/doc/stubDocs.js | 68 +- src/tests/testutils/exampleUser.js | 9 +- src/tests/testutils/mockCollection.js | 24 +- src/tests/testutils/stubUser.js | 30 +- 72 files changed, 2002 insertions(+), 1266 deletions(-) create mode 100644 src/imports/api/accounts/user/getUserByEmail.js create mode 100644 src/imports/api/utils/getUsersCollection.js create mode 100644 src/imports/contexts/classroom/lessons/LessonHelpers.js create mode 100644 src/imports/contexts/classroom/lessons/tests/LessonHelpers.tests.js create mode 100644 src/tests/testutils/doc/mockClassDoc.js create mode 100644 src/tests/testutils/doc/mockPhaseDoc.js create mode 100644 src/tests/testutils/doc/mockUnitDoc.js diff --git a/src/imports/api/accounts/admin/createAdmin.js b/src/imports/api/accounts/admin/createAdmin.js index 60871a7..3be25bc 100644 --- a/src/imports/api/accounts/admin/createAdmin.js +++ b/src/imports/api/accounts/admin/createAdmin.js @@ -4,24 +4,20 @@ import { getCollection } from '../../utils/getCollection' import { matchNonEmptyString } from '../../utils/check/matchNonEmptyString' import { Admin } from '../../../contexts/system/accounts/admin/Admin' -let AdminCollection - /** * Adds a user by user id to the Admins collection. - * @param userId + * @param newAdminId {string} the user _id of the user who will be new admin * @return {String} the doc id of the the user's entry in the admin collection */ -export const createAdmin = function (userId) { - check(userId, matchNonEmptyString) +export const createAdmin = function (newAdminId) { + check(newAdminId, matchNonEmptyString) - if (!AdminCollection) { - AdminCollection = getCollection(Admin.name) - } + const AdminCollection = getCollection(Admin.name) - if (AdminCollection.find({ userId }).count() > 0) { - throw new Meteor.Error('createAdmin.failed', 'createAdmin.alreadyAdmin', userId) + if (AdminCollection.find({ userId: newAdminId }).count() > 0) { + throw new Meteor.Error('createAdmin.failed', 'createAdmin.alreadyAdmin', { adminId: newAdminId }) } - return AdminCollection.insert({ userId }) + return AdminCollection.insert({ userId: newAdminId }) } diff --git a/src/imports/api/accounts/admin/removeAdmin.js b/src/imports/api/accounts/admin/removeAdmin.js index 61ee731..29116d1 100644 --- a/src/imports/api/accounts/admin/removeAdmin.js +++ b/src/imports/api/accounts/admin/removeAdmin.js @@ -5,8 +5,6 @@ import { matchNonEmptyString } from '../../utils/check/matchNonEmptyString' import { Admin } from '../../../contexts/system/accounts/admin/Admin' import { userExists } from '../user/userExists' -let AdminCollection - /** * Removes a user by user id from the Admins collection. * @param userId @@ -19,9 +17,7 @@ export const removeAdmin = function (userId) { throw new Meteor.Error('removeAdmin.failed', 'errors.userNotFound', userId) } - if (!AdminCollection) { - AdminCollection = getCollection(Admin.name) - } + const AdminCollection = getCollection(Admin.name) if (AdminCollection.find({ userId }).count() === 0) { throw new Meteor.Error('removeAdmin.failed', 'removeAdmin.notAdmin', userId) diff --git a/src/imports/api/accounts/admin/tests/adminExists.tests.js b/src/imports/api/accounts/admin/tests/adminExists.tests.js index d13d7e9..0230074 100644 --- a/src/imports/api/accounts/admin/tests/adminExists.tests.js +++ b/src/imports/api/accounts/admin/tests/adminExists.tests.js @@ -3,17 +3,18 @@ import { Admin } from '../../../../contexts/system/accounts/admin/Admin' import { adminExists } from '../adminExists' import { clearCollections, - mockCollections, - restoreAllCollections + restoreAllCollections, mockCollections } from '../../../../../tests/testutils/mockCollection' import { Random } from 'meteor/random' import { expect } from 'chai' +import { Users } from '../../../../contexts/system/accounts/users/User' let AdminCollection +let UsersCollection describe(adminExists.name, function () { before(function () { - AdminCollection = mockCollections(Admin) + [AdminCollection, UsersCollection] = mockCollections(Admin, Users) }) afterEach(function () { @@ -29,7 +30,7 @@ describe(adminExists.name, function () { }) it('returns true if an admin exists', function () { - const userId = Meteor.users.insert({ username: Random.id() }) + const userId = UsersCollection.insert({ username: Random.id() }) AdminCollection.insert({ userId }) expect(adminExists()).to.equal(true) }) diff --git a/src/imports/api/accounts/admin/tests/createAdmin.tests.js b/src/imports/api/accounts/admin/tests/createAdmin.tests.js index df6bec8..04cf1e4 100644 --- a/src/imports/api/accounts/admin/tests/createAdmin.tests.js +++ b/src/imports/api/accounts/admin/tests/createAdmin.tests.js @@ -30,7 +30,7 @@ describe(createAdmin.name, function () { expect(() => createAdmin('')).to.throw('Match error: Failed Match.Where validation') }) it('throws if there is no user found for the given userId', function () { - expect(() => createAdmin(Random.id())).to.throw('userId is invalid in null insert') + expect(() => createAdmin(Random.id())).to.throw('userId is invalid') }) it('throws if the user is already an Admin', function () { const userId = UsersCollection.insert({ username: Random.id() }) diff --git a/src/imports/api/accounts/admin/tests/removeAdmin.tests.js b/src/imports/api/accounts/admin/tests/removeAdmin.tests.js index 27bcaf1..9d058c2 100644 --- a/src/imports/api/accounts/admin/tests/removeAdmin.tests.js +++ b/src/imports/api/accounts/admin/tests/removeAdmin.tests.js @@ -32,16 +32,20 @@ describe(removeAdmin.name, function () { }) it('throws if there is no user found for the given userId', function () { - expect(() => removeAdmin(Random.id())).to.throw('removeAdmin.failed').with.property('reason', 'errors.userNotFound') + expect(() => removeAdmin(Random.id())) + .to.throw('removeAdmin.failed') + .with.property('reason', 'errors.userNotFound') }) it('throws if the given userId is not in Admins', function () { - const userId = Meteor.users.insert({ username: Random.id() }) - expect(() => removeAdmin(userId)).to.throw('removeAdmin.failed').with.property('reason', 'removeAdmin.notAdmin') + const userId = UsersCollection.insert({ username: Random.id() }) + expect(() => removeAdmin(userId)) + .to.throw('removeAdmin.failed') + .with.property('reason', 'removeAdmin.notAdmin') }) it('removes the userId from the Admins', function () { - const userId = Meteor.users.insert({ username: Random.id() }) + const userId = UsersCollection.insert({ username: Random.id() }) const adminId = AdminCollection.insert({ userId }) expect(removeAdmin(userId)).to.equal(1) expect(AdminCollection.find(adminId).count()).to.equal(0) diff --git a/src/imports/api/accounts/admin/tests/userIsAdmin.tests.js b/src/imports/api/accounts/admin/tests/userIsAdmin.tests.js index 919631f..c823848 100644 --- a/src/imports/api/accounts/admin/tests/userIsAdmin.tests.js +++ b/src/imports/api/accounts/admin/tests/userIsAdmin.tests.js @@ -36,7 +36,7 @@ describe(userIsAdmin.name, function () { }) it('returns true if the user is in Admins', function () { - const userId = Meteor.users.insert({ username: Random.id() }) + const userId = UsersCollection.insert({ username: Random.id() }) AdminCollection.insert({ userId }) expect(userIsAdmin(userId)).to.equal(true) }) diff --git a/src/imports/api/accounts/admin/userIsAdmin.js b/src/imports/api/accounts/admin/userIsAdmin.js index 04c68e5..3493d2f 100644 --- a/src/imports/api/accounts/admin/userIsAdmin.js +++ b/src/imports/api/accounts/admin/userIsAdmin.js @@ -1,26 +1,14 @@ import { check } from 'meteor/check' - +import { getCollection } from '../../utils/getCollection' import { matchNonEmptyString } from '../../utils/check/matchNonEmptyString' -let AdminCollection - /** * Determines, whether a given user (by id) is an Admin, independent from the assigned roles. * @param userId The _id of the user to check * @return {boolean} true if the given user is part of the admin collection, false if not */ - export const userIsAdmin = function (userId) { check(userId, matchNonEmptyString) - - if (!AdminCollection) { - (function () { - import { getCollection } from '../../utils/getCollection' - import { Admin } from '../../../contexts/system/accounts/admin/Admin' - - AdminCollection = getCollection(Admin.name) - })() - } - - return AdminCollection.find({ userId }).count() > 0 + const { Admin } = require('../../../contexts/system/accounts/admin/Admin') + return getCollection(Admin.name).find({ userId }).count() > 0 } diff --git a/src/imports/api/accounts/registration/UserFactory.js b/src/imports/api/accounts/registration/UserFactory.js index 9181299..6a6e258 100644 --- a/src/imports/api/accounts/registration/UserFactory.js +++ b/src/imports/api/accounts/registration/UserFactory.js @@ -6,6 +6,7 @@ import { Roles } from 'meteor/alanning:roles' import { rollbackAccount } from './rollbackAccount' import { userExists } from '../user/userExists' import { createLog } from '../../log/createLog' +import { getUsersCollection } from '../../utils/getUsersCollection' /** * Creates new user accounts @@ -78,7 +79,7 @@ UserFactory.create = function create ({ email, password, role, firstName, lastNa profileDoc.$set.locale = locale } - const profileUpdated = Meteor.users.update(userId, profileDoc) + const profileUpdated = getUsersCollection().update(userId, profileDoc) if (!profileUpdated) { rollbackAccount(userId) diff --git a/src/imports/api/accounts/registration/rollbackAccount.js b/src/imports/api/accounts/registration/rollbackAccount.js index 7d961ff..6c48e77 100644 --- a/src/imports/api/accounts/registration/rollbackAccount.js +++ b/src/imports/api/accounts/registration/rollbackAccount.js @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor' import { check } from 'meteor/check' import { Admin } from '../../../contexts/system/accounts/admin/Admin' import { getCollection } from '../../utils/getCollection' +import { getUsersCollection } from '../../utils/getUsersCollection' export const rollbackAccount = userId => { check(userId, String) @@ -10,7 +11,7 @@ export const rollbackAccount = userId => { const adminRemoved = AdminCollection.remove({ userId }) const rolesRemoved = Meteor.roleAssignment.remove({ 'user._id': userId }) - const userRemoved = Meteor.users.remove(userId) + const userRemoved = getUsersCollection().remove(userId) return { adminRemoved, rolesRemoved, userRemoved } } diff --git a/src/imports/api/accounts/registration/tests/UserFactory.tests.js b/src/imports/api/accounts/registration/tests/UserFactory.tests.js index a1ecb5f..163c5f6 100644 --- a/src/imports/api/accounts/registration/tests/UserFactory.tests.js +++ b/src/imports/api/accounts/registration/tests/UserFactory.tests.js @@ -8,12 +8,13 @@ import { UserUtils } from '../../../../contexts/system/accounts/users/UserUtils' import { restoreAll, stub } from '../../../../../tests/testutils/stub' import { clearCollections, - mockCollection, mockCollections, restoreAllCollections } from '../../../../../tests/testutils/mockCollection' import { Admin } from '../../../../contexts/system/accounts/admin/Admin' import { Users } from '../../../../contexts/system/accounts/users/User' +import { getUserByEmail } from '../../user/getUserByEmail' +import { Meteor } from 'meteor/meteor' const userDoc = ({ email, firstName, lastName, password, role, institution } = {}) => { const doc = { @@ -35,11 +36,6 @@ const userDoc = ({ email, firstName, lastName, password, role, institution } = { return doc } -const allRoles = Object.values(UserUtils.roles) -allRoles.forEach(role => Roles.createRole(role, { unlessExists: true })) - -const AdminCollection = mockCollection(Admin) - const loop = (times, fct) => { for (let i = 0; i < times; i++) { fct() @@ -62,73 +58,82 @@ describe(UserFactory.name, function () { }) describe('input validation', function () { + + + afterEach(function () { + restoreAll() + }) + it('throws on empty / missing input', function () { expect(() => UserFactory.create()).to.throw('Cannot destructure property') - expect(() => UserFactory.create({})).to.throw('form.validation.required') try { UserFactory.create({}) - } catch (validationError) { + } + catch (validationError) { expect(validationError.details).to.deep.equal([{ name: 'email', type: 'required', value: undefined, - message: 'form.validation.required' + message: 'Email is required.' }, { name: 'role', type: 'required', value: undefined, - message: 'form.validation.required' + message: 'Invitee\'s role is required.' }, { name: 'firstName', type: 'required', value: undefined, - message: 'form.validation.required' + message: 'First name is required.' }, { name: 'lastName', type: 'required', value: undefined, - message: 'form.validation.required' + message: 'Last name is required.' }, { name: 'institution', type: 'required', value: undefined, - message: 'form.validation.required' + message: 'Institution / Company is required.' } ]) } }) it('throws on an incorrect email', function () { loop(100, function () { - expect(() => UserFactory.create(userDoc({ email: Random.id() }))).to.throw('form.validation.EmailWithTLD') + expect(() => UserFactory.create(userDoc({ email: Random.id() }))) + .to.throw('form.validation.EmailWithTLD') }) }) it('throws on an incorrect firstName', function () { loop(100, function () { const firstName = Random.id(49) + '1' - expect(() => UserFactory.create(userDoc({ firstName }))).to.throw('form.validation.regEx') + expect(() => UserFactory.create(userDoc({ firstName }))) + .to.throw('form.validation.regEx') }) }) it('throws on an incorrect lastName', function () { loop(100, function () { const lastName = Random.id(49) + '1' - expect(() => UserFactory.create(userDoc({ lastName }))).to.throw('form.validation.regEx') + expect(() => UserFactory.create(userDoc({ lastName }))) + .to.throw('form.validation.regEx') }) }) it('throws on an incorrect role', function () { loop(100, function () { const role = Random.id() - expect(() => UserFactory.create(userDoc({ role }))).to.throw('form.validation.notAllowed') + expect(() => UserFactory.create(userDoc({ role }))) + .to.throw(`'${role}' is not allowed.`) }) }) it('throws if a user with the given email already exists', function () { const user = userDoc() - stub(Accounts, 'findUserByEmail', () => true) - + UsersCollection.insert({ emails: [{ address: user.email }] }) const expectThrown = expect(() => UserFactory.create(user)).to.throw('createUser.failed') expectThrown.with.property('reason', 'user.emailUsed') expectThrown.with.property('details', user.email) @@ -136,7 +141,27 @@ describe(UserFactory.name, function () { }) describe('creation', function () { + + afterEach(function () { + restoreAll() + }) + + const stubAccountCreation = ({ stubRole = true } = {}) => { + if (stubRole) { + stub(Roles, 'addUsersToRoles', () => true) + stub(Roles, 'userIsInRole', () => true) + } + stub(Accounts, 'createUser', (userDoc) => { + const { email, password, ...rest } = userDoc + const insertDoc = { emails: [{ address: email }], services: {}, ...rest } + if (password) { + insertDoc.services.password = {} + } + return UsersCollection.insert(insertDoc) + }) + } it('creates a new user for given email address', function () { + stubAccountCreation() loop(100, function () { const user = userDoc() const userId = UserFactory.create(user) @@ -145,6 +170,7 @@ describe(UserFactory.name, function () { }) }) it('updates the user profile with the minimal defaults', function () { + stubAccountCreation() loop(100, function () { const user = userDoc() const userId = UserFactory.create(user) @@ -156,6 +182,7 @@ describe(UserFactory.name, function () { }) }) it('creates a user optionally with or without password', function () { + stubAccountCreation() const withoutPasswordUserId = UserFactory.create(userDoc()) const withoutPasswordUser = UsersCollection.findOne(withoutPasswordUserId) expect(withoutPasswordUser.services.password).to.equal(undefined) @@ -165,6 +192,7 @@ describe(UserFactory.name, function () { expect(withPasswordUser.services.password).to.be.an('object') }) it('strips any unnecessary whitespace from firstName, lastName and institution', function () { + stubAccountCreation() const user = userDoc({ firstName: ' John the second ', lastName: 'doe ', @@ -181,13 +209,16 @@ describe(UserFactory.name, function () { expect(createdUser.institution).to.equal('Where he is working at') }) it('adds the user to the respective given role with institution scope', function () { - allRoles.forEach(role => { + stubAccountCreation({ stubRole: false }) + Object.values(UserUtils.roles).forEach(role => { + Roles.createRole(role, { unlessExists: true }) const user = userDoc({ role }) const userId = UserFactory.create(user) expect(Roles.userIsInRole(userId, role, user.institution)) }) }) it('does not make user a real Admin', function () { + stubAccountCreation() const user = userDoc({ role: UserUtils.roles.admin }) const userId = UserFactory.create(user) expect(AdminCollection.find({ userId }).count()).to.equal(0) @@ -200,8 +231,9 @@ describe(UserFactory.name, function () { beforeEach(function () { user = userDoc() + user._id = Random.id() assertRollback = () => { - expect(Accounts.findUserByEmail(user.email)).to.equal(undefined) + expect(getUserByEmail(user.email)).to.equal(undefined) expect(UsersCollection.find({ firstName: user.firstName, lastName: user.lastName, @@ -209,22 +241,47 @@ describe(UserFactory.name, function () { }).count()).to.equal(0) } }) + + afterEach(function () { + restoreAll() + }) + it('rolls back the account on Accounts.createUser failure', function () { - stub(Accounts, 'createUser', () => { - }) + stub(Accounts, 'createUser', () => {}) expect(() => UserFactory.create(user)).to.throw('createUser.failed') .with.property('reason', 'createUser.notCreated') assertRollback() }) it('rolls back the account on profile update failure', function () { + stub(Accounts, 'createUser', () => user._id) stub(UsersCollection, 'update', () => 0) + stub(AdminCollection, 'remove', ({ userId }) => { + expect(userId).to.equal(user._id) + }) + stub(Meteor.roleAssignment, 'remove', (query) => { + expect(query['user._id']).to.equal(user._id) + }) + stub(UsersCollection, 'remove', (userId) => { + expect(userId).to.equal(user._id) + }) expect(() => UserFactory.create(user)).to.throw('createUser.failed') .with.property('reason', 'createUser.profileNotUpdated') assertRollback() }) it('rolls back the account on Roles asisignment failure', function () { + stub(Accounts, 'createUser', () => user._id) + stub(UsersCollection, 'update', () => 1) + stub(AdminCollection, 'remove', ({ userId }) => { + expect(userId).to.equal(user._id) + }) + stub(Meteor.roleAssignment, 'remove', (query) => { + expect(query['user._id']).to.equal(user._id) + }) + stub(UsersCollection, 'remove', (userId) => { + expect(userId).to.equal(user._id) + }) stub(Roles, 'userIsInRole', () => false) expect(() => UserFactory.create(user)).to.throw('createUser.failed') .with.property('reason', 'createUser.rolesNotAdded') diff --git a/src/imports/api/accounts/registration/tests/rollbackAccount.tests.js b/src/imports/api/accounts/registration/tests/rollbackAccount.tests.js index db4abbc..d0b7971 100644 --- a/src/imports/api/accounts/registration/tests/rollbackAccount.tests.js +++ b/src/imports/api/accounts/registration/tests/rollbackAccount.tests.js @@ -25,16 +25,16 @@ describe(rollbackAccount.name, function () { restoreAllCollections() }) it('removes a user from the account system', function () { - const userId = Accounts.createUser({ username: Random.id() }) - expect(Meteor.users.find(userId).count()).to.equal(1) + const userId = UsersCollection.insert({ username: Random.id() }) + expect(UsersCollection.find(userId).count()).to.equal(1) const { userRemoved, adminRemoved, rolesRemoved } = rollbackAccount(userId) expect(userRemoved).to.equal(1) expect(adminRemoved).to.equal(0) expect(rolesRemoved).to.equal(0) - expect(Meteor.users.find(userId).count()).to.equal(0) + expect(UsersCollection.find(userId).count()).to.equal(0) }) it('removes all roles from the user', function () { - const userId = Accounts.createUser({ username: Random.id() }) + const userId =UsersCollection.insert({ username: Random.id() }) const role = Random.id() const scope = Random.id() @@ -56,7 +56,7 @@ describe(rollbackAccount.name, function () { expect(Roles.userIsInRole(userId, role, scope)).to.equal(false) }) it('removes Admin status', function () { - const userId = Accounts.createUser({ username: Random.id() }) + const userId = UsersCollection.insert({ username: Random.id() }) AdminCollection.insert({ userId }) const { userRemoved, rolesRemoved, adminRemoved } = rollbackAccount(userId) diff --git a/src/imports/api/accounts/user/getUserByEmail.js b/src/imports/api/accounts/user/getUserByEmail.js new file mode 100644 index 0000000..4f2cf42 --- /dev/null +++ b/src/imports/api/accounts/user/getUserByEmail.js @@ -0,0 +1,5 @@ +import { getUsersCollection } from '../../utils/getUsersCollection' + +export const getUserByEmail = email => { + return getUsersCollection().findOne({ emails: { address: email } }) +} diff --git a/src/imports/api/accounts/user/tests/userExists.tests.js b/src/imports/api/accounts/user/tests/userExists.tests.js index 9546f7b..c06be2c 100644 --- a/src/imports/api/accounts/user/tests/userExists.tests.js +++ b/src/imports/api/accounts/user/tests/userExists.tests.js @@ -1,11 +1,25 @@ /* eslint-env mocha */ -import { loggedIn } from '../loggedIn' import { stubUser, unstubUser } from '../../../../../tests/testutils/stubUser' import { expect } from 'chai' import { Random } from 'meteor/random' import { userExists } from '../userExists' +import { clearCollections, mockCollections, restoreAllCollections } from '../../../../../tests/testutils/mockCollection' +import { Admin } from '../../../../contexts/system/accounts/admin/Admin' +import { Users } from '../../../../contexts/system/accounts/users/User' describe(userExists.name, function () { + before(function () { + mockCollections(Admin, Users) + }) + + afterEach(function () { + clearCollections() + }) + + after(function () { + restoreAllCollections() + }) + it('returns false if no user exists for given id', function () { expect(userExists({})).to.equal(false) expect(userExists({ userId: '' })).to.equal(false) diff --git a/src/imports/api/accounts/user/userExists.js b/src/imports/api/accounts/user/userExists.js index 0202c0a..4ab9ba7 100644 --- a/src/imports/api/accounts/user/userExists.js +++ b/src/imports/api/accounts/user/userExists.js @@ -1,13 +1,14 @@ -import { Meteor } from 'meteor/meteor' -import { Accounts } from 'meteor/accounts-base' +import { Users } from '../../../contexts/system/accounts/users/User' +import { getCollection } from '../../utils/getCollection' +import { getUserByEmail } from './getUserByEmail' export const userExists = ({ userId, email } = {}) => { if (userId) { - return Meteor.users.find(userId).count() > 0 + return getCollection(Users.name).find(userId).count() > 0 } if (email) { - return !!Accounts.findUserByEmail(email) + return !!getUserByEmail(email) } return false diff --git a/src/imports/api/utils/InvocationChecker.js b/src/imports/api/utils/InvocationChecker.js index a01bc02..5bf46d4 100644 --- a/src/imports/api/utils/InvocationChecker.js +++ b/src/imports/api/utils/InvocationChecker.js @@ -1,6 +1,10 @@ import { DDP } from 'meteor/ddp-client' import { NotInMethodError } from '../errors/types/NotInMethodError' +/** + * Will be dropped, due to dropping fibers + * @deprecated + */ export const InvocationChecker = {} InvocationChecker.NotInMethodError = NotInMethodError diff --git a/src/imports/api/utils/document/createDocCloner.js b/src/imports/api/utils/document/createDocCloner.js index 4e25b02..b3f6a1a 100644 --- a/src/imports/api/utils/document/createDocCloner.js +++ b/src/imports/api/utils/document/createDocCloner.js @@ -24,7 +24,6 @@ export const createDocCloner = function getClone ({ name } = {}) { function cloneDoc (docId, { $set } = {}) { const Collection = getCollection(name) - const sourceDoc = Collection.findOne(docId) if (!sourceDoc) { @@ -44,7 +43,6 @@ export const createDocCloner = function getClone ({ name } = {}) { delete sourceDoc.updatedAt const insertDoc = $set ? Object.assign({}, sourceDoc, $set) : sourceDoc - // XXX: simple sanity check if we really have a new _id // Could be the case, when someone weirdly added the original _id to the $set const clonedDocId = Collection.insert(insertDoc) diff --git a/src/imports/api/utils/document/createDocGetter.js b/src/imports/api/utils/document/createDocGetter.js index b111ae2..98b057c 100644 --- a/src/imports/api/utils/document/createDocGetter.js +++ b/src/imports/api/utils/document/createDocGetter.js @@ -5,16 +5,18 @@ import { DocNotFoundError } from '../../errors/types/DocNotFoundError' /** * Factory function to create a document-getter that includes several safety-checks (collection exists, * document exists, ownership). - * @param name The name of the context, used to obtain the collection - * @param {boolean} optional use to skip doc not found errors + * @param options {object} + * @param options.name {string} The name of the context, used to obtain the collection + * @param {boolean=false} options.optional use to skip doc not found errors * @returns {function} A function to retrieve documents by query */ -export const createDocGetter = function ({ name, optional = false }) { - check(name, String) - check(optional, Match.Maybe(Boolean)) - - let collection = null +export const createDocGetter = function (options) { + check(options, Match.ObjectIncluding({ + name: String, + optional: Match.Maybe(Boolean) + })) + const { name, optional = false } = options /** * Returns a document by a given _id. @@ -30,9 +32,7 @@ export const createDocGetter = function ({ name, optional = false }) { throw new DocNotFoundError('getDocument.invalidQuery', { name, query }) } - collection = collection || getCollection(name) - - const document = collection.findOne(query) + const document = getCollection(name).findOne(query) if (!optional && !document) { throw new DocNotFoundError('getDocument.docUndefined', { name, query }) diff --git a/src/imports/api/utils/documentUtils.js b/src/imports/api/utils/documentUtils.js index 084a535..b64643e 100644 --- a/src/imports/api/utils/documentUtils.js +++ b/src/imports/api/utils/documentUtils.js @@ -168,6 +168,7 @@ export const createRemoveDoc = function createRemoveDoc ({ name, isFilesCollecti ? getFilesCollection(name) : getCollection(name) const cursor = Collection.find(query) + if (!cursor) { throw new DocNotFoundError(query, name) } @@ -185,9 +186,10 @@ export const createRemoveDoc = function createRemoveDoc ({ name, isFilesCollecti } let removed + if (isFilesCollection) { - Collection.remove(query) removed = cursor.count() + Collection.remove(query) } else { removed = Collection.remove(query) diff --git a/src/imports/api/utils/getCollection.js b/src/imports/api/utils/getCollection.js index dc35fee..5a978e6 100644 --- a/src/imports/api/utils/getCollection.js +++ b/src/imports/api/utils/getCollection.js @@ -1,26 +1,24 @@ import { Meteor } from 'meteor/meteor' import { Mongo } from 'meteor/mongo' -import { assignToWindow } from '../../utils/assignToWindow' - -const _cache = new Map() +import { onClientExec } from './archUtils' export const getCollection = function (contextOrName) { const name = typeof contextOrName === 'object' ? contextOrName.name : contextOrName - let collection = _cache.get(name) + const collection = Mongo.Collection.get(name) if (!collection) { - collection = Mongo.Collection.get(name) - - if (!collection) { - throw new Meteor.Error('errors.collectionNotFound', name) - } - _cache.set(name, collection) + throw new Meteor.Error('errors.collectionNotFound', 'getCollection.notFoundByName', { name }) } return collection } -assignToWindow({ getCollection }) +// TODO move into client startup somewhere +onClientExec(function () { + import { assignToWindow } from '../../utils/assignToWindow' + assignToWindow({ getCollection }) +}) + diff --git a/src/imports/api/utils/getUsersCollection.js b/src/imports/api/utils/getUsersCollection.js new file mode 100644 index 0000000..67a0834 --- /dev/null +++ b/src/imports/api/utils/getUsersCollection.js @@ -0,0 +1,13 @@ +import { getCollection } from './getCollection' + +/** + * This is a special case, since in Meteor the users collection is + * a property of the Meteor namespace object. + * However, this creates strong coupling to the Meteor namespace + * and also makes it much harder to stub users on tests. + * + * Since the collection name will not change we can safely hard-code + * it's value as convention. + * @return {Mongo.Collection} the Meteor.users collection + */ +export const getUsersCollection = () => getCollection('users') diff --git a/src/imports/contexts/beamer/tests/Beamer.tests.js b/src/imports/contexts/beamer/tests/Beamer.tests.js index adac254..15e203d 100644 --- a/src/imports/contexts/beamer/tests/Beamer.tests.js +++ b/src/imports/contexts/beamer/tests/Beamer.tests.js @@ -2,34 +2,47 @@ import { Random } from 'meteor/random' import { assert, expect } from 'chai' import { Beamer } from '../Beamer' - +import { Users } from '../../system/accounts/users/User' import { isContext } from '../../../../tests/testutils/isContext' import { onClientExec, onServerExec } from '../../../api/utils/archUtils' -import { mockCollection } from '../../../../tests/testutils/mockCollection' +import { + clearCollections, + mockCollections, + restoreAllCollections +} from '../../../../tests/testutils/mockCollection' import { stubUser, unstubUser } from '../../../../tests/testutils/stubUser' import { exampleUser } from '../../../../tests/testutils/exampleUser' import { stubMethod, unstubMethod } from '../../../../tests/testutils/stubMethod' import { DocNotFoundError } from '../../../api/errors/types/DocNotFoundError' -const BeamerCollection = mockCollection(Beamer) - describe('Beamer', function () { let user let userId let environment + let BeamerCollection + let UsersCollection + + before(function () { + [BeamerCollection, UsersCollection] = mockCollections(Beamer, Users) + }) beforeEach(function () { user = exampleUser() userId = user._id environment = { userId } stubUser(user) - BeamerCollection.remove({}) }) afterEach(function () { + clearCollections(Beamer) + clearCollections(Users) unstubUser(user, userId) }) + after(function () { + restoreAllCollections() + }) + it('is a context', function () { isContext(Beamer) }) diff --git a/src/imports/contexts/classroom/invitations/CodeInvitations.js b/src/imports/contexts/classroom/invitations/CodeInvitations.js index 1c9fac7..99f63f2 100644 --- a/src/imports/contexts/classroom/invitations/CodeInvitations.js +++ b/src/imports/contexts/classroom/invitations/CodeInvitations.js @@ -11,6 +11,7 @@ import { getCollection } from '../../../api/utils/getCollection' import { onClient, onServer, onServerExec } from '../../../api/utils/archUtils' import { getSchemaField } from '../../../ui/utils/form/getSchemaField' import { getLocalCollection } from '../../../infrastructure/collection/getLocalCollection' +import { getUsersCollection } from '../../../api/utils/getUsersCollection' const mappedRoles = Object.values(UserUtils.roles).map(role => ({ value: role, @@ -42,6 +43,10 @@ export const CodeInvitation = { MAX_EXPIRY: 7 } +/** + * Will soon be an own module + * @deprecated + */ CodeInvitation.status = { pending: { value: 'pending', @@ -63,6 +68,10 @@ CodeInvitation.status = { } } +/** + * Will soon be an own module + * @deprecated + */ CodeInvitation.errors = { expirationExceeded: 'codeInvitation.expirationExceeded', removeNoPermission: 'codeInvitation.removeNoPermission', @@ -330,11 +339,9 @@ CodeInvitation.publications.class = { } /** - * - * HELPERS - * + * Will soon be an own module + * @deprecated */ - CodeInvitation.helpers = {} /** @@ -402,17 +409,25 @@ CodeInvitation.helpers.timeLeft = function timeLeft (createdAt, expires) { /** * Checks, whether a code doc is expired. Checks for validity and expiration date. - * @param invalid - the invalid flag for force-expired docs - * @param createdAt - the creation date of the doc - * @param expires - the number of days until expiration + * @param codeDoc {object} + * @param codeDoc.invalid {boolean=} - the invalid flag for force-expired docs + * @param codeDoc.createdAt {Date} - the creation date of the doc + * @param codeDoc.expires {Number} - the number of days until expiration * @return {boolean} true if */ -CodeInvitation.helpers.isExpired = function isExpired ({ invalid, createdAt, expires }) { - check(invalid, Match.Maybe(Boolean)) - check(createdAt, Date) - check(expires, Number) - if (invalid) return true +CodeInvitation.helpers.isExpired = function isExpired (codeDoc) { + check(codeDoc, Match.ObjectIncluding({ + createdAt: Date, + expires: Number + })) + + const { invalid, createdAt, expires } = codeDoc + + if (invalid) { + return true + } + const now = new Date().getTime() const expirationDate = CodeInvitation.helpers.getOffset(new Date(createdAt), expires) return (now - expirationDate) >= 0 @@ -421,19 +436,22 @@ CodeInvitation.helpers.isExpired = function isExpired ({ invalid, createdAt, exp /** * Checks, whether a code document is considered complete. This is the case when all users have fulfilled their * invitation with a registration. It does not differentiate , whether a registration failed or succeeded. - * @param _id the _id of the current code doc - * @param registeredUsers the current registered users - * @param maxUsers the number of maxmimum allowed registrations + * @param codeDoc {object} + * @param codeDoc._id {string} the _id of the current code doc + * @param codeDoc.registeredUsers {[string]=} the current registered users + * @param codeDoc.maxUsers {Number} the number of maxmimum allowed registrations * @return {boolean} true if max users is exactly reached, otherwise false * @throws {Meteor.Error} if parameters are not contained or validated * @throws {Meteor.Error} in case the registered users amount has exceeded the max users */ -CodeInvitation.helpers.isComplete = function isComplete ({ _id, registeredUsers, maxUsers }) { - check(_id, String) - check(maxUsers, Number) - check(registeredUsers, Match.Maybe([String])) - - if (!registeredUsers || !registeredUsers.length) { +CodeInvitation.helpers.isComplete = function isComplete (codeDoc) { + check(codeDoc, Match.ObjectIncluding({ + _id: String, + maxUsers: Number, + })) + const { _id, registeredUsers, maxUsers } = codeDoc + + if (!Array.isArray(registeredUsers) || !registeredUsers.length) { return false } else if (registeredUsers.length > maxUsers) { @@ -545,25 +563,34 @@ CodeInvitation.methods = {} CodeInvitation.methods.create = { name: 'codeInvitations.methods.create', schema: CodeInvitation.createCodeSchema, - roles: [UserUtils.roles.admin, UserUtils.roles.schoolAdmin, UserUtils.roles.teacher], + roles: [UserUtils.roles.admin, UserUtils.roles.schoolAdmin, UserUtils.roles.curriculum, UserUtils.roles.teacher], run: onServerExec(function () { - import { createGetDoc } from '../../../api/utils/documentUtils' + import { createDocGetter } from '../../../api/utils/document/createDocGetter' + import { userIsAdmin } from '../../../api/accounts/admin/userIsAdmin' - const getClassDoc = createGetDoc(SchoolClass) + const getClassDoc = createDocGetter(SchoolClass) return function (createDoc) { const { userId } = this if (!UserUtils.canInvite(userId, createDoc.role)) { - throw new Meteor.Error(CodeInvitation.errors.insufficientRole) + throw new Meteor.Error( + 'codeInvitation.createFailed', + CodeInvitation.errors.insufficientRole, + { userId, role: createDoc.role } + ) } // check if institution matches - const user = Meteor.users.findOne(userId) + const user = getUsersCollection().findOne(userId) const { institution } = user - if (!UserUtils.isAdmin(userId) && institution !== createDoc.institution) { - throw new Meteor.Error(CodeInvitation.errors.institutionMismatch) + if (institution !== createDoc.institution && !userIsAdmin(userId)) { + throw new Meteor.Error( + 'codeInvitation.createFailed', + CodeInvitation.errors.institutionMismatch, + { institution: createDoc.institution, userId } + ) } const insertDoc = { @@ -581,7 +608,12 @@ CodeInvitation.methods.create = { // verify class ownership if (createDoc.role === UserUtils.roles.student) { - getClassDoc.call(this, insertDoc.classId) + const { classId } = insertDoc + const classDoc = getClassDoc(classId) + + if (!SchoolClass.helpers.isTeacher({ classDoc, userId })) { + throw new PermissionDeniedError('schoolClass.notTeacher', { classId, userId }) + } } // otherwise remove class entirely else { @@ -605,14 +637,24 @@ CodeInvitation.methods.verify = { }, isPublic: true, run: onServerExec(function () { + import { createDocGetter } from '../../../api/utils/document/createDocGetter' + + const getCodeDoc = createDocGetter({ name: CodeInvitation.name, optional: true }) + const getClassDoc = createDocGetter({ name: SchoolClass.name }) + return function ({ code }) { - const codeDoc = getCollection(CodeInvitation.name).findOne({ code }) + const codeDoc = getCodeDoc({ code }) if (!codeDoc || CodeInvitation.helpers.isExpired(codeDoc) || CodeInvitation.helpers.isComplete(codeDoc)) { throw new Meteor.Error(CodeInvitation.errors.invalidLink, CodeInvitation.errors.invalidLinkReason) } - const classDoc = getCollection(SchoolClass.name).findOne(codeDoc.classId) + let classDoc + + if (codeDoc.classId) { + classDoc = getClassDoc(codeDoc.classId) + } + return { firstName: codeDoc.firstName, lastName: codeDoc.lastName, @@ -685,7 +727,7 @@ CodeInvitation.methods.addToClass = { // 2nd validate user const userId = this.userId - const user = Meteor.users.findOne(userId) + const user = getUsersCollection().findOne(userId) if (!Users.helpers.verify(user)) { console.warn('warning adding unverified user', user._id) // throw new PermissionDeniedError('user.notVerified') @@ -724,7 +766,7 @@ CodeInvitation.methods.addToClass = { userId }) if (!added) throw new Meteor.Error(500) - Meteor.users.update(userId, { $set: { 'ui.classId': classId } }) + getUsersCollection().update(userId, { $set: { 'ui.classId': classId } }) } else { throw new PermissionDeniedError(SchoolClass.errors.invalidRole, role) diff --git a/src/imports/contexts/classroom/invitations/tests/CodeInvitation.tests.js b/src/imports/contexts/classroom/invitations/tests/CodeInvitation.tests.js index 09998be..8ee2c4a 100644 --- a/src/imports/contexts/classroom/invitations/tests/CodeInvitation.tests.js +++ b/src/imports/contexts/classroom/invitations/tests/CodeInvitation.tests.js @@ -1,5 +1,4 @@ /* global describe it beforeEach afterEach */ -import { Meteor } from 'meteor/meteor' import { Random } from 'meteor/random' import { expect } from 'chai' import { CodeInvitation } from '../CodeInvitations' @@ -10,6 +9,10 @@ import { createCodeDoc } from '../../../../../tests/testutils/doc/createCodeDoc' import { Users } from '../../../system/accounts/users/User' import { stub, restoreAll } from '../../../../../tests/testutils/stub' import { InvocationChecker } from '../../../../api/utils/InvocationChecker' +import { clearCollections, mockCollections, restoreAllCollections } from '../../../../../tests/testutils/mockCollection' +import { DocNotFoundError } from '../../../../api/errors/types/DocNotFoundError' +import { PermissionDeniedError } from '../../../../api/errors/types/PermissionDeniedError' +import { Admin } from '../../../system/accounts/admin/Admin' describe(CodeInvitation.name, function () { describe('helpers', function () { @@ -47,12 +50,12 @@ describe(CodeInvitation.name, function () { }) it('returns true for a doc with expired date', function () { - const expiredDoc = { expires: -3, createdAt: new Date() } + const expiredDoc = { expires: -3, createdAt: new Date(), invalid: false } expect(CodeInvitation.helpers.isExpired(expiredDoc)).to.equal(true) }) it('returns false for a valid doc with unexpired date', function () { - const expiredDoc = { expires: 3, createdAt: new Date() } + const expiredDoc = { expires: 3, createdAt: new Date(), invalid: false } expect(CodeInvitation.helpers.isExpired(expiredDoc)).to.equal(false) }) @@ -163,12 +166,13 @@ describe(CodeInvitation.name, function () { }) onServerExec(function () { - import { mockCollection } from '../../../../../tests/testutils/mockCollection' + import { mockCollections } from '../../../../../tests/testutils/mockCollection' import { exampleUser } from '../../../../../tests/testutils/exampleUser' import { unstubUser, stubUser } from '../../../../../tests/testutils/stubUser' - const CodeCollection = mockCollection(CodeInvitation) - const SchoolClassCollection = mockCollection(SchoolClass, { noSchema: true }) + let CodeCollection + let SchoolClassCollection + let UsersCollection let user let userId @@ -177,20 +181,26 @@ describe(CodeInvitation.name, function () { let classId describe('methods', function () { + before(function () { + [UsersCollection, CodeCollection, SchoolClassCollection] = mockCollections(Users, CodeInvitation, SchoolClass, Admin) + }) + beforeEach(function () { user = exampleUser() userId = user._id environment = { userId } - SchoolClassCollection.remove({}) classDoc = { createdBy: userId, title: Random.id() } classId = SchoolClassCollection.insert(classDoc) - Meteor.users.insert(user) }) afterEach(function () { unstubUser(user, userId) restoreAll() - Meteor.users.remove({}) + clearCollections(Users, CodeInvitation, SchoolClass) + }) + + after(function () { + restoreAllCollections() }) const createInvitation = (...args) => CodeInvitation.methods.create.run.call(environment, ...args) @@ -202,26 +212,33 @@ describe(CodeInvitation.name, function () { describe(CodeInvitation.methods.create.name, function () { it('throws, if the user cannot invite the given role', function () { // expected errors + const { admin, schoolAdmin, curriculum, teacher, student } = UserUtils.roles const errorPairs = [ - [UserUtils.roles.schoolAdmin, [UserUtils.roles.admin, UserUtils.roles.schoolAdmin]], - [UserUtils.roles.teacher, [UserUtils.roles.admin, UserUtils.roles.schoolAdmin, UserUtils.roles.teacher]], - [UserUtils.roles.student, [UserUtils.roles.admin, UserUtils.roles.schoolAdmin, UserUtils.roles.teacher, UserUtils.roles.student]] + [schoolAdmin, [admin, schoolAdmin]], + [curriculum, [admin, schoolAdmin, curriculum]], + [teacher, [admin, schoolAdmin, curriculum, teacher]], + [student, [admin, schoolAdmin, curriculum, teacher, student]] ] errorPairs.forEach(entry => { const role = entry[0] const targets = entry[1] - stubUser(user, userId, [role], user.institution) + const { institution } = user + stubUser(user, userId, [role], institution) targets.forEach(targetRole => { const createDoc = { maxUsers: 1, expires: 1, role: targetRole, - institution: user.institution, + institution, classId } - expect(() => createInvitation(createDoc)).to.throw(CodeInvitation.errors.insufficientRole) + + const thrown = expect(() => createInvitation.call(environment, createDoc)) + .to.throw('codeInvitation.createFailed') + thrown.with.property('reason', CodeInvitation.errors.insufficientRole) + thrown.with.deep.property('details', { userId, role: targetRole }) }) unstubUser(user, userId) @@ -238,11 +255,17 @@ describe(CodeInvitation.name, function () { } // case a: classdoc not found - expect(() => createInvitation(createDoc)).to.throw('docNotFound') + const notFound = expect(() => createInvitation.call(environment, createDoc)) + .to.throw(DocNotFoundError.name) + notFound.with.property('reason', 'getDocument.docUndefined') + notFound.with.deep.property('details', { name: SchoolClass.name, query: createDoc.classId }) // case b: not owner - createDoc.classId = SchoolClassCollection.insert({ title: Random.id() }) - expect(() => createInvitation(createDoc)).to.throw('permissionDenied') + createDoc.classId = SchoolClassCollection.insert({ title: Random.id(), createdBy: Random.id() }) + const notTeacher = expect(() => createInvitation(createDoc)) + .to.throw(PermissionDeniedError.name) + notTeacher.with.property('reason', 'schoolClass.notTeacher') + notTeacher.with.deep.property('details', { userId, classId: createDoc.classId }) }) it('throws if user is not admin and institutions mismatch', function () { const errorPairs = [ @@ -363,7 +386,8 @@ describe(CodeInvitation.name, function () { role: codeDoc.role, institution: codeDoc.institution, email: codeDoc.email, - classId: codeDoc.classId + classId: codeDoc.classId, + className: SchoolClassCollection.findOne(classId).title } expect(verifiedDoc).to.deep.equal(expectedDoc) }) diff --git a/src/imports/contexts/classroom/lessons/Lesson.js b/src/imports/contexts/classroom/lessons/Lesson.js index 387cc94..f79b97e 100644 --- a/src/imports/contexts/classroom/lessons/Lesson.js +++ b/src/imports/contexts/classroom/lessons/Lesson.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor' import { UserUtils } from '../../system/accounts/users/UserUtils' import { i18n } from '../../../api/language/language' import { PermissionDeniedError } from '../../../api/errors/types/PermissionDeniedError' -import { auto, onServer, onServerExec } from '../../../api/utils/archUtils' +import { onServer, onServerExec } from '../../../api/utils/archUtils' import { getCollection } from '../../../api/utils/getCollection' /** @@ -20,17 +20,6 @@ export const Lesson = { isClassroom: true } -/************************************************************** - * - * STATES - * - **************************************************************/ - -/** - * The states are reflecting the three main states of a real world lesson. - * @deprecated TODO extract into own module - */ - /************************************************************** * * SCHEMA @@ -191,151 +180,6 @@ Lesson.publicFields = { uploads: 1 } -/************************************************************** - * - * HELPERS - * - **************************************************************/ - -/** - * use external helpers - * @deprecated - */ -Lesson.helpers = {} - -auto(function () { - import { SchoolClass } from '../schoolclass/SchoolClass' - import { createGetDoc } from '../../../api/utils/documentUtils' - - const getLessonDoc = createGetDoc(Lesson, { checkOwner: false }) - const getClassDoc = createGetDoc(SchoolClass, { checkOwner: false }) - const checkUser = userId => { - if (!userId || !Meteor.users.findOne(userId)) { - throw new Meteor.Error('errors.docNotFound', 'user.notFound') - } - } - - /** - * @deprecated extract - * @param lessonDoc - * @param taskId - * @return {*|boolean} - */ - Lesson.helpers.taskIsEditable = function taskIsEditable ({ lessonDoc = {}, taskId, groupDoc = {} }) { - const isEditable = ref => ref._id === taskId - return (lessonDoc.visibleStudent || []).some(isEditable) || (groupDoc.visible || []).some(isEditable) - } - - /** - * Gets a classDoc, if given user is student - * @deprecated extract method - * @param classId The _id of classDoc, where the user should be member of - * @param userId the id of the user - * @returns {classDoc} - */ - - Lesson.helpers.getClassDocIfStudent = function getClassDocIfMember ({ userId, classId }) { - checkUser(userId) - const classDoc = getClassDoc(classId) - if (!classDoc.students || classDoc.students.indexOf(userId) === -1) { - throw new Meteor.Error('errors.permissionDenied', SchoolClass.errors.notMember) - } - return classDoc - } - - /** - * Checks if the given user is member of a given lesson - * use isMemberOfClass - * @deprecated - * @param userId - * @param lessonId - * @param returnDocs - * @return {boolean} - */ - Lesson.helpers.isMemberOfLesson = function isMemberOfLesson ({ userId, lessonId } = {}, { returnDocs = false } = {}) { - checkUser(userId) - const lessonDoc = getLessonDoc(lessonId) - const { classId } = lessonDoc - const classDoc = getClassDoc(classId) - const isMember = !!(classDoc.createdBy === userId || - (classDoc.teachers && classDoc.teachers.indexOf(userId) > -1) || - (classDoc.students && classDoc.students.indexOf(userId) > -1)) - return returnDocs - ? isMember && { lessonDoc, classDoc } - : isMember - } - - /** - * Checks if the given user is teacher of the lesson, or if not, being teacher of the class. - * @deprecated - * @param userId The user to be checked - * @param lessonId the id of the lesson document - * @return {boolean} true if creator of lesson or class or member of class teachers - */ - Lesson.helpers.isTeacher = function isTeacher ({ userId, lessonId }, { returnDocs = false } = {}) { - const lessonDoc = getLessonDoc(lessonId) - if (lessonDoc.createdBy === userId) return true - - const { classId } = lessonDoc - const classDoc = getClassDoc(classId) - const isTeacher = SchoolClass.helpers.isTeacher({ classDoc, userId }) - return returnDocs - ? isTeacher && { lessonDoc, classDoc } - : isTeacher - } - - /** - * @deprecated extract - * @param userId - * @param lessonId - * @param returnDocs - * @return {*} - */ - Lesson.helpers.isStudentOfLesson = function isStudentOfLesson ({ userId, lessonId }, { returnDocs = false } = {}) { - const lessonDoc = getLessonDoc(lessonId) - const { classId } = lessonDoc - const classDoc = getClassDoc(classId) - const isStudent = !!(userId && classDoc.students && classDoc.students.indexOf(userId) > -1) - return returnDocs - ? isStudent && { lessonDoc, classDoc } - : isStudent - } - - /** - * Gets lessonDoc and classDoc if the userId is a teacher - * @deprecated extract - * @param userId - * @param lessonId - * @return {{lessonDoc: *, classDoc: *}} - */ - - Lesson.helpers.docsForTeacher = function docsForTeacher ({ userId, lessonId }) { - const lessonDoc = getLessonDoc(lessonId) - const classDoc = getClassDoc(lessonDoc.classId) - if (!SchoolClass.helpers.isTeacher({ classDoc, userId })) { - throw new PermissionDeniedError(SchoolClass.errors.notTeacher) - } - return { lessonDoc, classDoc } - } - - /** - * Returns lessonDoc and classDoc if user is a student of the class - * @deprecated extract - * @param userId - * @param lessonId - * @return {{lessonDoc: *, classDoc: *}} - */ - - Lesson.helpers.docsForStudent = function docsForStudent ({ userId, lessonId }) { - const lessonDoc = getLessonDoc(lessonId) - const classDoc = getClassDoc(lessonDoc.classId) - if (!SchoolClass.helpers.isStudent({ classDoc, userId })) { - throw new PermissionDeniedError(SchoolClass.errors.notMember) - } - return { lessonDoc, classDoc } - } -}) - /************************************************************** * * PUBLICATIONS @@ -392,10 +236,11 @@ Lesson.publications.single = { }, run: onServerExec(function () { import { userIsAdmin } from '../../../api/accounts/admin/userIsAdmin' + import { LessonHelpers } from './LessonHelpers' return function ({ _id }) { const { userId } = this - const isMember = Lesson.helpers.isMemberOfLesson({ + const isMember = LessonHelpers.isMemberOfLesson({ userId, lessonId: _id }) @@ -450,10 +295,14 @@ Lesson.publications.byClassStudent = { schema: { classId: String }, - run: onServer(function ({ classId }) { - const userId = this.userId - const classDoc = Lesson.helpers.getClassDocIfStudent({ userId, classId }) - return getCollection(Lesson.name).find({ classId: classDoc && classDoc._id }) + run: onServerExec(function () { + import { LessonHelpers } from './LessonHelpers' + + return function ({ classId }) { + const userId = this.userId + const classDoc = LessonHelpers.getClassDocIfStudent({ userId, classId }) + return getCollection(Lesson.name).find({ classId: classDoc && classDoc._id }) + } }) } @@ -591,6 +440,7 @@ Lesson.methods.start = { run: onServerExec(function () { import { LessonStates } from './LessonStates' import { LessonErrors } from './LessonErrors' + import { LessonHelpers } from './LessonHelpers' import { createUpdateDoc } from '../../../api/utils/documentUtils' const updateLesson = createUpdateDoc(Lesson, { checkOwner: false }) @@ -605,7 +455,7 @@ Lesson.methods.start = { */ function startLesson ({ _id }) { const userId = this.userId - const { lessonDoc } = Lesson.helpers.docsForTeacher({ + const { lessonDoc } = LessonHelpers.docsForTeacher({ userId, lessonId: _id }) @@ -631,6 +481,7 @@ Lesson.methods.complete = { run: onServerExec(function () { import { createUpdateDoc } from '../../../api/utils/documentUtils' import { LessonStates } from './LessonStates' + import { LessonHelpers } from './LessonHelpers' const updateLesson = createUpdateDoc(Lesson, { checkOwner: false }) @@ -643,7 +494,7 @@ Lesson.methods.complete = { function completeLesson ({ _id }) { const userId = this.userId - const { lessonDoc } = Lesson.helpers.docsForTeacher({ + const { lessonDoc } = LessonHelpers.docsForTeacher({ userId, lessonId: _id }) @@ -676,6 +527,7 @@ Lesson.methods.stop = { run: onServerExec(function () { import { LessonErrors } from './LessonErrors' import { LessonStates } from './LessonStates' + import { LessonHelpers } from './LessonHelpers' import { createUpdateDoc } from '../../../api/utils/documentUtils' const updateLesson = createUpdateDoc(Lesson, { checkOwner: false }) @@ -689,11 +541,15 @@ Lesson.methods.stop = { function stopLesson ({ _id }) { const userId = this.userId - const { lessonDoc } = Lesson.helpers.docsForTeacher({ + const { lessonDoc } = LessonHelpers.docsForTeacher({ userId, lessonId: _id }) - if (!LessonStates.isRunning(lessonDoc)) throw new Meteor.Error(LessonErrors.unexpectedState) + + if (!LessonStates.isRunning(lessonDoc)) { + throw new Meteor.Error(LessonErrors.unexpectedState) + } + return updateLesson.call(this, lessonDoc._id, { $unset: { startedAt: 1 } }) } @@ -710,6 +566,7 @@ Lesson.methods.resume = { run: onServerExec(function () { import { LessonStates } from './LessonStates' import { LessonErrors } from './LessonErrors' + import { LessonHelpers } from './LessonHelpers' import { createUpdateDoc } from '../../../api/utils/documentUtils' const updateLesson = createUpdateDoc(Lesson, { checkOwner: false }) @@ -723,7 +580,7 @@ Lesson.methods.resume = { function resumeLesson ({ _id }) { const userId = this.userId - const { lessonDoc } = Lesson.helpers.docsForTeacher({ + const { lessonDoc } = LessonHelpers.docsForTeacher({ userId, lessonId: _id }) @@ -744,9 +601,10 @@ Lesson.methods.restart = { run: onServerExec(function () { import { LessonRuntime } from './runtime/LessonRuntime' import { LessonStates } from './LessonStates' + import { LessonHelpers } from './LessonHelpers' import { createUpdateDoc } from '../../../api/utils/documentUtils' - const updateLesson = createUpdateDoc(Lesson, { checkOwner: false }) + const updateLesson = createUpdateDoc(Lesson) /** * Restartes a lesson by _id and removes all data that has been generated during the lesson run @@ -758,20 +616,23 @@ Lesson.methods.restart = { */ function restartLesson ({ _id }) { const { userId, log } = this - const { lessonDoc } = Lesson.helpers.docsForTeacher({ + const { lessonDoc } = LessonHelpers.docsForTeacher({ userId, lessonId: _id }) if (!LessonStates.canRestart(lessonDoc)) { - throw new Meteor.Error('lesson.errors.unexpectedState', 'lesson.errors.expectedRestartable') + throw new Meteor.Error( + 'lesson.errors.unexpectedState', + 'lesson.errors.expectedRestartable', + { lessonId: _id, userId } + ) } - const runtimeOptions = { lessonId: _id, userId } - - const runtimeDocs = LessonRuntime.removeDocuments(runtimeOptions) - const groupDocs = LessonRuntime.resetGroups(runtimeOptions) - const beamerReset = LessonRuntime.resetBeamer(runtimeOptions) + const options = { lessonId: _id, userId } + const runtimeDocs = LessonRuntime.removeDocuments(options) + const groupDocs = LessonRuntime.resetGroups(options) + const beamerReset = LessonRuntime.resetBeamer(options) const lessonReset = !!updateLesson.call(this, _id, { $unset: { phase: 1, @@ -804,13 +665,15 @@ Lesson.methods.toggle = { run: onServerExec(function () { import { LessonStates } from './LessonStates' import { LessonErrors } from './LessonErrors' - import { createGetDoc, createUpdateDoc } from '../../../api/utils/documentUtils' + import { LessonHelpers } from './LessonHelpers' + import { createUpdateDoc } from '../../../api/utils/documentUtils' + import { createDocGetter } from '../../../api/utils/document/createDocGetter' const updateLesson = createUpdateDoc(Lesson) function toggleLessonMaterial ({ _id, referenceId, context }) { - const userId = this.userId - const { lessonDoc } = Lesson.helpers.docsForTeacher({ + const { userId } = this + const { lessonDoc } = LessonHelpers.docsForTeacher({ userId, lessonId: _id }) @@ -819,8 +682,8 @@ Lesson.methods.toggle = { throw new Meteor.Error('lesson.errors.unexpectedState', 'lesson.errors.expectedToggleAble') } - const checkRef = createGetDoc({ name: context }, { checkOwner: false }) - checkRef(referenceId) + // use doc getter to ensure reference doc exists + createDocGetter({ name: context })(referenceId) const index = (lessonDoc.visibleStudent || []).findIndex(reference => reference._id === referenceId) const transform = {} @@ -863,7 +726,7 @@ Lesson.methods.units = { import { SchoolClass } from '../schoolclass/SchoolClass' import { Unit } from '../../curriculum/curriculum/unit/Unit' import { $in } from '../../../api/utils/query/inSelector' - import { isMemberOfClass } from '../schoolclass/helpers/isMemberOfClass' + import { LessonHelpers } from './LessonHelpers' /** * Getss all associated units by a given set of lessons (via lesson ids) @@ -877,18 +740,31 @@ Lesson.methods.units = { const classIds = new Set() const unitsIds = new Set() const lessonDocs = getCollection(Lesson.name).find({ _id: $in(lessonIds) }) + lessonDocs.forEach(doc => { classIds.add(doc.classId) - unitsIds.add(doc.unit) }) - const classDocs = getCollection(SchoolClass.name).find({ _id: $in(classIds) }) - const validQuery = classDocs.fetch().every(classDoc => isMemberOfClass({ classDoc, userId })) + const classDocs = getCollection(SchoolClass.name).find({ _id: $in(classIds) }).fetch() + const validQuery = lessonDocs.fetch().every(lessonDoc => { + const { classId } = lessonDoc + const classDoc = classDocs.find(({ _id }) => _id === classId ) + + if (!classDoc) { + return false + } + + return LessonHelpers.isMemberOfClass({ classDoc, userId }) + }) if (!validQuery) { - throw new Meteor.Error('errors.permissionDenied', SchoolClass.errors.notMember) + throw new Meteor.Error('errors.permissionDenied', SchoolClass.errors.notMember, { userId }) } + lessonDocs.forEach(doc => { + unitsIds.add(doc.unit) + }) + return getCollection(Unit.name).find({ _id: $in(unitsIds) }).fetch() } }) @@ -918,12 +794,13 @@ Lesson.methods.material = { import { SchoolClass } from '../schoolclass/SchoolClass' import { LessonStates } from './LessonStates' import { LessonErrors } from './LessonErrors' - import { createGetDoc } from '../../../api/utils/documentUtils' - import { loadMaterial } from '../../material/loadMaterial' + import { LessonHelpers } from './LessonHelpers' import { createDocGetter } from '../../../api/utils/document/createDocGetter' + import { loadMaterial } from '../../material/loadMaterial' - const getClassDoc = createGetDoc(SchoolClass, { checkOwner: false }) + const getClassDoc = createDocGetter(SchoolClass) const getGroupDoc = createDocGetter({ name: Group.name }) + /** * Loads material relevant for a lesson. * Allows to skip already loaded material @@ -942,7 +819,7 @@ Lesson.methods.material = { const { userId, log } = this // first we need the lesson doc for any further steps - const { lessonDoc } = Lesson.helpers.docsForStudent({ + const { lessonDoc } = LessonHelpers.docsForStudent({ userId, lessonId: _id }) diff --git a/src/imports/contexts/classroom/lessons/LessonHelpers.js b/src/imports/contexts/classroom/lessons/LessonHelpers.js new file mode 100644 index 0000000..f0487d5 --- /dev/null +++ b/src/imports/contexts/classroom/lessons/LessonHelpers.js @@ -0,0 +1,124 @@ +import { Lesson } from './Lesson' +import { SchoolClass } from '../schoolclass/SchoolClass' +import { createDocGetter } from '../../../api/utils/document/createDocGetter' +import { PermissionDeniedError } from '../../../api/errors/types/PermissionDeniedError' +import { isMemberOfClass } from '../schoolclass/helpers/isMemberOfClass' + +const getLessonDoc = createDocGetter(Lesson) +const getClassDoc = createDocGetter(SchoolClass) + +/** + * Utility functions for common checks around lessons. + */ +export const LessonHelpers = {} + +/** + * @param lessonDoc + * @param taskId + * @return {*|boolean} + */ +LessonHelpers.taskIsEditable = function taskIsEditable ({ lessonDoc = {}, taskId, groupDoc = {} }) { + const isEditable = ref => ref._id === taskId + return (lessonDoc.visibleStudent || []).some(isEditable) || (groupDoc.visible || []).some(isEditable) +} + +/** + * Gets a classDoc, if given user is student + * @param classId The _id of classDoc, where the user should be member of + * @param userId the id of the user + * @returns {classDoc} + */ + +LessonHelpers.getClassDocIfStudent = function getClassDocIfStudent ({ userId, classId }) { + const classDoc = getClassDoc(classId) + + if (!isMemberOfClass({ classDoc, userId })) { + throw new PermissionDeniedError(SchoolClass.errors.notMember, { userId, classId }) + } + + return classDoc +} + +/** + * Checks if the given user is member of a given lesson + * use isMemberOfClass + * @param userId + * @param lessonId + * @param returnDocs + * @return {boolean} + */ +LessonHelpers.isMemberOfLesson = function isMemberOfLesson ({ userId, lessonId } = {}, { returnDocs = false } = {}) { + const lessonDoc = getLessonDoc(lessonId) + const { classId } = lessonDoc + const classDoc = classId && getClassDoc(classId) + return isMemberOfClass({ classDoc, userId }) +} + +LessonHelpers.isMemberOfClass = ({ classDoc, userId }) => isMemberOfClass({ classDoc, userId }) + +/** + * Checks if the given user is teacher of the lesson, or if not, being teacher of the class. + * @param userId The user to be checked + * @param lessonId the id of the lesson document + * @return {boolean} true if creator of lesson or class or member of class teachers + */ +LessonHelpers.isTeacher = function isTeacher ({ userId, lessonId }, { returnDocs = false } = {}) { + const lessonDoc = getLessonDoc(lessonId) + if (lessonDoc.createdBy === userId) return true + + const { classId } = lessonDoc + const classDoc = getClassDoc(classId) + const isTeacher = SchoolClass.helpers.isTeacher({ classDoc, userId }) + return returnDocs + ? isTeacher && { lessonDoc, classDoc } + : isTeacher +} + +/** + * @param userId + * @param lessonId + * @param returnDocs + * @return {*} + */ +LessonHelpers.isStudentOfLesson = function isStudentOfLesson ({ userId, lessonId }, { returnDocs = false } = {}) { + const lessonDoc = getLessonDoc(lessonId) + const { classId } = lessonDoc + const classDoc = getClassDoc(classId) + const isStudent = !!(userId && classDoc.students && classDoc.students.indexOf(userId) > -1) + return returnDocs + ? isStudent && { lessonDoc, classDoc } + : isStudent +} + +/** + * Gets lessonDoc and classDoc if the userId is a teacher + * @param userId + * @param lessonId + * @return {{lessonDoc: object, classDoc: object}} + */ + +LessonHelpers.docsForTeacher = function docsForTeacher ({ userId, lessonId }) { + const lessonDoc = getLessonDoc(lessonId) + const classDoc = getClassDoc(lessonDoc.classId) + + if (!SchoolClass.helpers.isTeacher({ classDoc, userId })) { + throw new PermissionDeniedError(SchoolClass.errors.notTeacher, { userId, lessonId }) + } + return { lessonDoc, classDoc } +} + +/** + * Returns lessonDoc and classDoc if user is a student of the class + * @param userId + * @param lessonId + * @return {{lessonDoc: *, classDoc: *}} + */ + +LessonHelpers.docsForStudent = function docsForStudent ({ userId, lessonId }) { + const lessonDoc = getLessonDoc(lessonId) + const classDoc = getClassDoc(lessonDoc.classId) + if (!SchoolClass.helpers.isStudent({ classDoc, userId })) { + throw new PermissionDeniedError(SchoolClass.errors.notMember) + } + return { lessonDoc, classDoc } +} \ No newline at end of file diff --git a/src/imports/contexts/classroom/lessons/methods/removeLesson.js b/src/imports/contexts/classroom/lessons/methods/removeLesson.js index f38c100..c40f4ef 100644 --- a/src/imports/contexts/classroom/lessons/methods/removeLesson.js +++ b/src/imports/contexts/classroom/lessons/methods/removeLesson.js @@ -1,32 +1,42 @@ -import { Meteor } from 'meteor/meteor' import { Lesson } from '../Lesson' import { LessonRuntime } from '../runtime/LessonRuntime' import { Unit } from '../../../curriculum/curriculum/unit/Unit' import { Phase } from '../../../curriculum/curriculum/phase/Phase' -import { createRemoveAllMaterial } from '../../../material/createRemoveAllMaterial' import { getCollection } from '../../../../api/utils/getCollection' - -const removeAllMaterial = createRemoveAllMaterial({ isCurriculum: false }) +import { LessonHelpers } from '../LessonHelpers' +import { DocNotFoundError } from '../../../../api/errors/types/DocNotFoundError' /** * Removes / deletes a lesson by a given _id. Removes all related documents, too. * - * @param {lessonId} the _id of the lesson to be deleted - * @param {lessonDoc} the lesson doc of the lesson to be deleted - * @param {userId} the user of which in behalf to call - * @return {{lessonRemoved: *, unitRemoved: *, phasesRemoved: *}} + * @param options {object} + * @param options.lessonId {string} the _id of the lesson to be deleted + * @param options.lessonDoc {object} the lesson doc of the lesson to be deleted + * @param options.userId {string} the user of which in behalf to call + * @param options.log {function=} optional log to be passed + * @return {{ + * lessonRemoved: number, + * unitRemoved: number, + * phasesRemoved: number, + * materialRemoved: number, + * runtimeDocsRemoved: number, + * beamerRemoved: number + * }} */ -export const removeLesson = function removeLesson ({ lessonId, lessonDoc, userId, log = () => {} }) { +export const removeLesson = (options) => { + const { userId, log } = options + let lessonId = options.lessonId + let lessonDoc = options.lessonDoc + if (!lessonDoc) { - lessonDoc = Lesson.helpers.docsForTeacher({ userId, lessonId }).lessonDoc + const docsForTeacher = LessonHelpers.docsForTeacher({ userId, lessonId }) + lessonDoc = docsForTeacher.lessonDoc } + // if still not existent.... if (!lessonDoc) { - throw new Meteor.Error('removeLesson.error', 'errors.docNotFound', { - lessonId, - userId - }) + throw new DocNotFoundError('removeLesson.noLessonById', { lessonId, userId }) } if (!lessonId && lessonDoc) { @@ -42,25 +52,20 @@ export const removeLesson = function removeLesson ({ lessonId, lessonDoc, userId beamerRemoved: 0 } - result.runtimeDocsRemoved = LessonRuntime.removeDocuments({ - lessonId, - userId - }) - - result.beamerRemoved = LessonRuntime.resetBeamer({ lessonId, userId }) + log('remove runtime docs') + const removeRuntimeArgs = { lessonId, userId } + result.runtimeDocsRemoved = LessonRuntime.removeDocuments(removeRuntimeArgs) + result.beamerRemoved = LessonRuntime.resetBeamer(removeRuntimeArgs) // XXX: there are cases where the unit doc is // removed and we need to remove the lesson but omit the unit doc // which is why it's optional const unitDoc = getCollection(Unit.name).findOne({ _id: lessonDoc.unit }) - log(unitDoc) + log('has unitdoc?', unitDoc) + if (unitDoc) { // removes all linked phases but not global phases - const phaseQuery = { - _master: { $exists: false }, - unit: unitDoc._id, - createdBy: userId - } + const phaseQuery = createPhaseQuery({ userId, unitId: unitDoc._id }) if (unitDoc.phases?.length) { phaseQuery._id = { $in: unitDoc.phases } @@ -68,19 +73,14 @@ export const removeLesson = function removeLesson ({ lessonId, lessonDoc, userId log('remove phase query', phaseQuery) result.phasesRemoved = getCollection(Phase.name).remove(phaseQuery) - result.materialRemoved = removeAllMaterial({ unitDoc, userId }) - result.unitRemoved = getCollection(Unit.name).remove({ _id: unitDoc._id }) + result.materialRemoved = LessonRuntime.removeAllMaterial({ unitDoc, userId }) + result.unitRemoved = getCollection(Unit.name).remove({ _id: unitDoc._id, _master: { $exists: false } }) } - // if the unit doc is not found we still try to remove phases and material + // If the unit doc is not found we still try to remove phases and material. + // Removes all linked phases but not global phases. else { - // removes all linked phases but not global phases - const phaseQuery = { - _master: { $exists: false }, - unit: lessonDoc.unit, - createdBy: userId - } - + const phaseQuery = createPhaseQuery({ userId, unitId: lessonDoc.unit }) log('remove phase query', phaseQuery) result.phasesRemoved = getCollection(Phase.name).remove(phaseQuery) } @@ -90,3 +90,15 @@ export const removeLesson = function removeLesson ({ lessonId, lessonDoc, userId return result } + +/** + * @private + * @param userId {string} + * @param unitId {string} + * @return {{unit, createdBy, _master: {$exists: boolean}}} + */ +const createPhaseQuery = ({ userId, unitId }) => ({ + _master: { $exists: false }, + unit: unitId, + createdBy: userId +}) diff --git a/src/imports/contexts/classroom/lessons/runtime/LessonRuntime.js b/src/imports/contexts/classroom/lessons/runtime/LessonRuntime.js index c04e82e..59eea46 100644 --- a/src/imports/contexts/classroom/lessons/runtime/LessonRuntime.js +++ b/src/imports/contexts/classroom/lessons/runtime/LessonRuntime.js @@ -1,10 +1,27 @@ import { resetBeamer } from './resetBeamer' import { removeDocuments } from './removeDocuments' import { resetGroups } from './resetGroups' +import { createRemoveAllMaterial } from '../../../material/createRemoveAllMaterial' + +/** + * @type {function({unitDoc: string, userId: string}): number} + */ +const removeAllMaterial = createRemoveAllMaterial({ isCurriculum: false }) export const LessonRuntime = { + name: 'lesson.runtime', + /** + * @type {string} + */ resetBeamer: resetBeamer, + /** + * @type {function} + */ removeDocuments: removeDocuments, - resetGroups: resetGroups + /** + * @type {function} + */ + resetGroups: resetGroups, + removeAllMaterial: removeAllMaterial } diff --git a/src/imports/contexts/classroom/lessons/runtime/isMemberOfLesson.js b/src/imports/contexts/classroom/lessons/runtime/isMemberOfLesson.js index df1077e..787eb00 100644 --- a/src/imports/contexts/classroom/lessons/runtime/isMemberOfLesson.js +++ b/src/imports/contexts/classroom/lessons/runtime/isMemberOfLesson.js @@ -1,4 +1,7 @@ +/** + * @deprecated + */ export const isMemberOfLesson = ({ userId, lessonId, lessonDoc }) => { - import { Lesson } from '../Lesson' - return Lesson.helpers.isMemberOfLesson({ userId, lessonId }) + import { LessonHelpers } from '../LessonHelpers' + return LessonHelpers.isMemberOfLesson({ userId, lessonId }) } diff --git a/src/imports/contexts/classroom/lessons/runtime/removeDocuments.js b/src/imports/contexts/classroom/lessons/runtime/removeDocuments.js index 076db76..77c7d0e 100644 --- a/src/imports/contexts/classroom/lessons/runtime/removeDocuments.js +++ b/src/imports/contexts/classroom/lessons/runtime/removeDocuments.js @@ -13,7 +13,6 @@ const removeImageFiles = createRemoveDoc(ImageFiles, { checkOwner: false, multip const removeAudioFiles = createRemoveDoc(AudioFiles, { checkOwner: false, multiple: true }) const removeDocumentFiles = createRemoveDoc(DocumentFiles, { checkOwner: false, multiple: true }) const removeVideoFiles = createRemoveDoc(VideoFiles, { checkOwner: false, multiple: true }) - const removeTaskResults = createRemoveDoc(TaskResults, { checkOwner: false, multiple: true }) const removeTaskWorkingState = createRemoveDoc(TaskWorkingState, { checkOwner: false, multiple: true }) @@ -29,7 +28,6 @@ export const removeDocuments = function ({ lessonId } = {}) { check(lessonId, String) const docQuery = { lessonId } const fileQuery = { 'meta.lessonId': lessonId } - // the context <-> count map const removed = {} diff --git a/src/imports/contexts/classroom/lessons/runtime/resetBeamer.js b/src/imports/contexts/classroom/lessons/runtime/resetBeamer.js index 5d7f7d8..4168e2f 100644 --- a/src/imports/contexts/classroom/lessons/runtime/resetBeamer.js +++ b/src/imports/contexts/classroom/lessons/runtime/resetBeamer.js @@ -6,6 +6,7 @@ import { getCollection } from '../../../../api/utils/getCollection' /** * Resets all beamer settings that are related to the given setting and user. + * @type {function} * @param lessonId the id of the lesson to which the beamer settings should be reset * @param userId the user to whom the respective beamer doc is associated * @return {Number} the amount of references, that have been reset diff --git a/src/imports/contexts/classroom/lessons/runtime/resetGroups.js b/src/imports/contexts/classroom/lessons/runtime/resetGroups.js index 956a3e3..87082b3 100644 --- a/src/imports/contexts/classroom/lessons/runtime/resetGroups.js +++ b/src/imports/contexts/classroom/lessons/runtime/resetGroups.js @@ -1,12 +1,21 @@ -import { check } from 'meteor/check' +import { check, Match } from 'meteor/check' import { getCollection } from '../../../../api/utils/getCollection' import { Group } from '../../group/Group' -export const resetGroups = ({ lessonId, userId }) => { - check(lessonId, String) - +/** + * Resets all groups of a given lesson. + * @param options {object} + * @param options.lessonId {string} + * @return {number} number of updated documents + */ +export const resetGroups = (options) => { + check(options, Match.ObjectIncluding({ + lessonId: String + })) + const { lessonId } = options const query = { lessonId } const transform = { $set: { visible: [] } } - const options = { multi: true } - return getCollection(Group.name).update(query, transform, options) + const updateOptions = { multi: true } + + return getCollection(Group.name).update(query, transform, updateOptions) } diff --git a/src/imports/contexts/classroom/lessons/tests/Lesson.tests.js b/src/imports/contexts/classroom/lessons/tests/Lesson.tests.js index 12a3eb2..5fc7ab3 100644 --- a/src/imports/contexts/classroom/lessons/tests/Lesson.tests.js +++ b/src/imports/contexts/classroom/lessons/tests/Lesson.tests.js @@ -1,7 +1,10 @@ /* eslint-env mocha */ -import { Meteor } from 'meteor/meteor' import { Random } from 'meteor/random' -import { mockCollection } from '../../../../../tests/testutils/mockCollection' +import { + clearCollections, + mockCollections, + restoreAllCollections +} from '../../../../../tests/testutils/mockCollection' import { Lesson } from '../Lesson' import { SchoolClass } from '../../schoolclass/SchoolClass' import { UserUtils } from '../../../system/accounts/users/UserUtils' @@ -25,170 +28,54 @@ import { LessonRuntime } from '../runtime/LessonRuntime' import { Unit } from '../../../curriculum/curriculum/unit/Unit' import { Task } from '../../../curriculum/curriculum/task/Task' import { Phase } from '../../../curriculum/curriculum/phase/Phase' +import { Users } from '../../../system/accounts/users/User' +import { LessonHelpers } from '../LessonHelpers' // require startup files to initialize Material import '../../../../startup/server/contexts' +import { mockUnitDoc } from '../../../../../tests/testutils/doc/mockUnitDoc' +import { mockPhaseDoc } from '../../../../../tests/testutils/doc/mockPhaseDoc' +import { mockClassDoc } from '../../../../../tests/testutils/doc/mockClassDoc' +import { Group } from '../../group/Group' +import { PermissionDeniedError } from '../../../../api/errors/types/PermissionDeniedError' -const LessonCollection = mockCollection(Lesson, { noSchema: true }) -const UnitCollection = mockCollection(Unit, { noSchema: true, override: true }) -const SchoolClassCollection = mockCollection(SchoolClass, { noSchema: true }) -const PhaseCollection = mockCollection(Phase, { noSchema: true }) -const TaskCollection = mockCollection(Task, { noSchema: true }) +const log = () => { +} describe(Lesson.name, function () { - describe('helpers', function () { - afterEach(function () { - restoreAll() - }) + let LessonCollection + let UnitCollection + let SchoolClassCollection + let PhaseCollection + let TaskCollection + let UsersCollection + + before(function () { + [LessonCollection, UnitCollection, SchoolClassCollection, PhaseCollection, TaskCollection, UsersCollection] = mockCollections( + [Lesson, { noSchema: false }], + Unit, + SchoolClass, + Phase, + Task, + Users, + Group + ) + }) - describe(Lesson.helpers.getClassDocIfStudent.name, function () { - const get = Lesson.helpers.getClassDocIfStudent + afterEach(function () { + clearCollections(Users, Lesson, Unit, SchoolClass, Phase, Task) + restoreAll() + }) - it('throws if user does not exists', function () { - const userId = Random.id() - expect(() => get({ userId })).to.throw('user.notFound') - expect(() => get({ userId })).to.throw(DocNotFoundError.name) - }) - it('throws if class does not exists', function () { - const userId = Random.id() - const classId = Random.id() - stub(Meteor.users, 'findOne', () => ({ _id: userId })) - expect(() => get({ userId, classId })).to.throw(DocNotFoundError.name) - expect(() => get({ userId, classId })).to.throw(classId) - }) - it('throws is user is not student', function () { - const userId = Random.id() - const classId = Random.id() - stubUserDoc({ userId }) - stubClassDoc({ _id: classId, students: [] }) - expect(() => get({ userId, classId })).to.throw('errors.permissionDenied') - expect(() => get({ userId, classId })).to.throw(SchoolClass.errors.notMember) - }) - it('returns the doc otherwise', function () { - const userId = Random.id() - const classId = Random.id() - const classDoc = { _id: classId, students: [userId] } - stubUserDoc({ userId }) - stubClassDoc(classDoc) - const actualClassDoc = get({ userId, classId }) - expect(actualClassDoc).to.deep.equal(classDoc) - }) - }) - describe(Lesson.helpers.isMemberOfLesson.name, function () { - const member = Lesson.helpers.isMemberOfLesson - it('throws if user does not exists', function () { - const userId = Random.id() - expect(() => member({ userId })).to.throw(DocNotFoundError.name, 'user.notFound') - }) - it('throws if lesson does not exists', function () { - const userId = Random.id() - const lessonId = Random.id() - stub(Meteor.users, 'findOne', () => ({ _id: userId })) - expect(() => member({ userId, lessonId })).to.throw(DocNotFoundError.name) - expect(() => member({ userId, lessonId })).to.throw(lessonId) - }) - it('throws class doc does not exists', function () { - const userId = Random.id() - const lessonId = Random.id() - const classId = Random.id() - stub(Meteor.users, 'findOne', () => ({ _id: userId })) - stub(LessonCollection, 'findOne', () => ({ _id: lessonId, classId })) - stub(SchoolClassCollection, 'findOne', () => undefined) - expect(() => member({ userId, lessonId })).to.throw(DocNotFoundError.name) - expect(() => member({ userId, lessonId })).to.throw(classId) - }) - it('returns true if the given user is student of the lesson / class', function () { - const userId = Random.id() - const lessonId = Random.id() - const classdoc = { _id: Random.id(), students: [userId] } - stub(Meteor.users, 'findOne', () => ({ _id: userId })) - stub(LessonCollection, 'findOne', () => ({ _id: lessonId })) - stub(SchoolClassCollection, 'findOne', () => classdoc) - expect(member({ userId, lessonId })).to.equal(true) - }) - it('returns true if the given user is teacher of the lesson / class', function () { - const userId = Random.id() - const lessonId = Random.id() - const classdoc = { _id: Random.id(), teachers: [userId] } - stub(Meteor.users, 'findOne', () => ({ _id: userId })) - stub(LessonCollection, 'findOne', () => ({ _id: lessonId })) - stub(SchoolClassCollection, 'findOne', () => classdoc) - expect(member({ userId, lessonId })).to.equal(true) - }) - it('returns true if the given user is owner of the class', function () { - const userId = Random.id() - const lessonId = Random.id() - const classdoc = { _id: Random.id(), createdBy: userId } - stub(Meteor.users, 'findOne', () => ({ _id: userId })) - stub(LessonCollection, 'findOne', () => ({ _id: lessonId })) - stub(SchoolClassCollection, 'findOne', () => classdoc) - expect(member({ userId, lessonId })).to.equal(true) - }) - it('returns false if the given user is not member of the lesson', function () { - const userId = Random.id() - const lessonId = Random.id() - const classdoc = { _id: Random.id() } - stub(Meteor.users, 'findOne', () => ({ _id: userId })) - stub(LessonCollection, 'findOne', () => ({ _id: lessonId })) - stub(SchoolClassCollection, 'findOne', () => classdoc) - expect(member({ userId, lessonId })).to.equal(false) - }) - }) - describe(Lesson.helpers.isTeacher.name, function () { - const isTeacher = Lesson.helpers.isTeacher - it('throws if there is no lessonDoc', function () { - const defDoc = { userId: Random.id(), lessonId: Random.id() } - expect(() => isTeacher(defDoc)).to.throw(DocNotFoundError.name, defDoc.lessonId, Lesson.name) - }) - it('throws if there is no linked classDoc', function () { - const userId = Random.id() - const classId = Random.id() - const lessonDocId = LessonCollection.insert({ createdBy: Random.id(), classId }) - const defDoc = { userId, lessonId: lessonDocId } - expect(() => isTeacher(defDoc)).to.throw(DocNotFoundError.name, classId, SchoolClass.name) - }) - it('returns true if the user creator of the lesson', function () { - const userId = Random.id() - const lessonId = LessonCollection.insert({ createdBy: userId, classId: Random.id() }) - const defDoc = { userId, lessonId } - expect(isTeacher(defDoc)).to.equal(true) - }) - it('returns true if the user is in teachers of the class', function () { - const userId = Random.id() - const classId = SchoolClassCollection.insert({ createdBy: Random.id(), title: Random.id(), teachers: [userId] }) - const lessonId = LessonCollection.insert({ createdBy: Random.id(), classId }) - const defDoc = { userId, lessonId } - expect(isTeacher(defDoc)).to.equal(true) - }) - it('returns true if the user is creator of the class', function () { - const userId = Random.id() - const classId = SchoolClassCollection.insert({ createdBy: userId, title: Random.id(), teachers: [Random.id()] }) - const lessonId = LessonCollection.insert({ createdBy: Random.id(), classId }) - const defDoc = { userId, lessonId } - expect(isTeacher(defDoc)).to.equal(true) - }) - it('returns false otherwise', function () { - const userId = Random.id() - const classId = SchoolClassCollection.insert({ - createdBy: Random.id(), - title: Random.id(), - teachers: [Random.id()] - }) - const lessonId = LessonCollection.insert({ createdBy: Random.id(), classId }) - const defDoc = { userId, lessonId } - expect(isTeacher(defDoc)).to.equal(false) - }) - }) + after(function () { + restoreAllCollections() }) onServerExec(() => { describe('methods', function () { - afterEach(function () { - LessonCollection.remove({}) - UnitCollection.remove({}) - restoreAll() - }) - + // ====================================================================== + // CREATE + // ====================================================================== const createLesson = Lesson.methods.create.run describe(Lesson.methods.create.name, function () { @@ -202,66 +89,59 @@ describe(Lesson.name, function () { stubUserDoc({ userId }) stubAdmin(false) - const docNotFound = expect(() => createLesson.call({ userId }, lessonCreateDoc)).to.throw(DocNotFoundError.name) - docNotFound.with.property('reason', 'createCloneDoc.sourceNotFound') - docNotFound.to.have.deep.property('details', { docId: unit, name: Unit.name }) + const expectError = expect(() => createLesson.call({ userId, log }, lessonCreateDoc)) + .to.throw(DocNotFoundError.name) + expectError.with.property('reason', 'getDocument.docUndefined') + expectError.to.have.deep.property('details', { query: unit, name: Unit.name }) }) it('throws if the given class does not exists', function () { const originalUnit = Random.id() const classId = Random.id() const lessonCreateDoc = { classId, unit: originalUnit } - stub(UnitCollection, 'findOne', () => ({ _id: originalUnit })) - const docNotFound = expect(() => createLesson(lessonCreateDoc)).to.throw(DocNotFoundError.name) - docNotFound.with.property('reason', 'getDocument.docUndefined') - docNotFound.to.have.deep.property('details', { name: SchoolClass.name, query: classId }) + mockUnitDoc({ _id: originalUnit }, UnitCollection) + + const expectError = expect(() => createLesson(lessonCreateDoc)).to.throw(DocNotFoundError.name) + expectError.with.property('reason', 'getDocument.docUndefined') + expectError.to.have.deep.property('details', { name: SchoolClass.name, query: classId }) }) it('creates a new lesson doc', function () { const userId = Random.id() const classId = Random.id() const originalUnit = Random.id() - stub(UnitCollection, 'findOne', (_id) => { - return { _id, createdBy: userId, title: Random.id() } - }) + mockUnitDoc({ _id: originalUnit }, UnitCollection) stub(SchoolClassCollection, 'findOne', () => ({ _id: classId, createdBy: userId })) stub(UserUtils, 'isAdmin', () => false) - const { lessonId, unitId } = createLesson.call({ userId }, { classId, unit: originalUnit }) + const { lessonId, unitId } = createLesson.call({ userId, log }, { classId, unit: originalUnit }) expect(unitId).to.not.equal(originalUnit) - const lessonDoc = LessonCollection.findOne(lessonId) expect(lessonDoc.unitOriginal).to.equal(originalUnit) expect(lessonDoc.classId).to.equal(classId) }) it('creates a copy of the given master unit', function () { - const unitOriginal = { _id: Random.id(), title: Random.id(), phases: [Random.id()] } - const classId = Random.id() const userId = Random.id() - const lessonCreateDoc = { classId, unit: unitOriginal._id, createdBy: userId } + const phaseDoc = mockPhaseDoc({}, PhaseCollection) + const unitOriginal = mockUnitDoc({ + createdBy: userId, + phases: [phaseDoc._id] + }, UnitCollection) - stub(SchoolClassCollection, 'findOne', () => Object.assign({}, { _id: classId, createdBy: userId })) - stub(PhaseCollection, 'findOne', (_id) => Object.assign({}, { _id, createdBy: userId })) - stub(UnitCollection, 'findOne', (_id) => { - return Object.assign({}, { - _id, - createdBy: userId, - title: _id === unitOriginal._id ? unitOriginal.title : Random.id(), - phases: _id === unitOriginal._id ? unitOriginal.phases : [Random.id()] - }) - }) + const classId = Random.id() + const lessonCreateDoc = { classId, unit: unitOriginal._id, createdBy: userId } stub(UserUtils, 'isAdmin', () => false) + stub(SchoolClassCollection, 'findOne', () => { + return Object.assign({}, { _id: classId, createdBy: userId }) + }) - const { lessonId, unitId } = createLesson.call({ userId }, lessonCreateDoc) + const { lessonId, unitId } = createLesson.call({ userId, log }, lessonCreateDoc) restore(UserUtils, 'isAdmin') restore(SchoolClassCollection, 'findOne') - restore(UnitCollection, 'findOne') - restore(PhaseCollection, 'findOne') const lessonDoc = LessonCollection.findOne(lessonId) - expect(lessonDoc.unitOriginal).to.equal(unitOriginal._id) expect(lessonDoc.unit).to.not.equal(unitOriginal._id) expect(lessonDoc.unit).to.equal(unitId) @@ -271,56 +151,33 @@ describe(Lesson.name, function () { expect(newUnit.phases.length).to.equal(unitOriginal.phases.length) }) it('creates copies of the given master phases', function () { - const unitOriginal = { _id: Random.id(), title: Random.id(), phases: [] } + const unitOriginal = mockUnitDoc({ + _id: Random.id(), + title: Random.id(), + phases: [] + }) + const originalPhases = [] for (let i = 0; i < 3; i++) { - const originalPhaseDoc = { + const originalPhaseDoc = mockPhaseDoc({ _id: Random.id(), title: Random.id(), unit: unitOriginal._id - } + }, PhaseCollection) originalPhases.push(originalPhaseDoc) } unitOriginal.phases = originalPhases.map(e => e._id) + UnitCollection.insert(unitOriginal) const classId = Random.id() const userId = Random.id() const lessonCreateDoc = { classId, unit: unitOriginal._id, createdBy: userId } + mockClassDoc({ _id: classId, createdBy: userId }, SchoolClassCollection) stub(UserUtils, 'isAdmin', () => false) - stub(SchoolClassCollection, 'findOne', () => Object.assign({}, { _id: classId, createdBy: userId })) - - stub(UnitCollection, 'findOne', (_id) => { - return Object.assign({}, { - _id, - createdBy: userId, - title: _id === unitOriginal._id ? unitOriginal.title : Random.id(), - phases: _id === unitOriginal._id ? unitOriginal.phases : unitOriginal.phases.map(x => Random.id()) - }) - }) - - stub(PhaseCollection, 'findOne', phaseId => { - const phaseDoc = originalPhases.find(entry => entry._id === phaseId) - if (phaseDoc) { - return Object.assign({}, phaseDoc) - } - else { - return { - _id: Random.id(), - createdBy: userId, - title: Random.id(), - unit: Random.id() - } - } - }) - - const { lessonId } = createLesson.call({ userId }, lessonCreateDoc) - - restore(SchoolClassCollection, 'findOne') - restore(UnitCollection, 'findOne') - restore(PhaseCollection, 'findOne') + const { lessonId } = createLesson.call({ userId, log }, lessonCreateDoc) const lessonDoc = LessonCollection.findOne(lessonId) const newUnit = UnitCollection.findOne(lessonDoc.unit) expect(newUnit.title).to.equal(unitOriginal.title) @@ -338,45 +195,59 @@ describe(Lesson.name, function () { }) }) + // ====================================================================== + // START + // ====================================================================== describe(Lesson.methods.start.name, function () { - const start = Lesson.methods.start.run + const startLesson = Lesson.methods.start.run - checkLesson(start, LessonStates.canStart) - checkClass(start) + checkLesson(startLesson, LessonStates.canStart) + checkClass(startLesson) it('updates the lesson state to running', function () { - const { userId, lessonDoc } = stubTeacherDocs() + const unit = Random.id() + const { userId, lessonDoc } = stubTeacherDocs({ unit }) LessonCollection.insert(lessonDoc) - const started = start.call({ userId }, lessonDoc) + + const started = startLesson.call({ userId, log }, lessonDoc) const updatedDoc = LessonCollection.findOne(lessonDoc._id) expect(started).to.equal(true) expect(LessonStates.isRunning(updatedDoc)) }) }) + // ====================================================================== + // TOGGLE + // ====================================================================== describe(Lesson.methods.toggle.name, function () { - const toggle = Lesson.methods.toggle.run + const toggleMaterial = Lesson.methods.toggle.run - checkLesson(toggle, LessonStates.canToggle) - checkClass(toggle, { userId: Random.id() }) + checkLesson(toggleMaterial, LessonStates.canToggle) + checkClass(toggleMaterial, { userId: Random.id() }) it('throws if the material is not existent', function () { - const { lessonDoc, userId } = stubTeacherDocs({ startedAt: new Date() }) - expect(() => toggle.call({ userId }, { + const unit = Random.id() + const name = Random.id(6) + const { lessonDoc, userId } = stubTeacherDocs({ startedAt: new Date(), unit }) + const thrown = expect(() => toggleMaterial.call({ userId, log }, { _id: lessonDoc._id, - context: Random.id() + referenceId: Random.id(), + context: name })).to.throw('collectionNotFound') + thrown.with.property('reason', 'getCollection.notFoundByName') + thrown.to.have.deep.property('details', { name }) }) it('pushes material to the list, if not visible', function () { const { lessonDoc, userId } = stubTeacherDocs({ startedAt: new Date() }) const taskId = Random.id() const taskDoc = { _id: Random.id() } + stubTaskDoc(taskDoc) const toggleDoc = { _id: lessonDoc._id, referenceId: taskId, context: Task.name } LessonCollection.insert(lessonDoc) - const toggled = toggle.call({ userId }, toggleDoc) + const toggled = toggleMaterial.call({ userId, log }, toggleDoc) expect(toggled).to.equal(true) restore(LessonCollection, 'findOne') @@ -395,7 +266,7 @@ describe(Lesson.name, function () { const toggleDoc = { _id: lessonDoc._id, referenceId: taskId, context: Task.name } LessonCollection.insert(lessonDoc) - const toggled = toggle.call({ userId }, toggleDoc) + const toggled = toggleMaterial.call({ userId, log }, toggleDoc) expect(toggled).to.equal(true) restore(LessonCollection, 'findOne') @@ -405,37 +276,43 @@ describe(Lesson.name, function () { }) }) + // ====================================================================== + // COMPLETE + // ====================================================================== describe(Lesson.methods.complete.name, function () { - const complete = Lesson.methods.complete.run + const completeLesson = Lesson.methods.complete.run - checkLesson(complete, LessonStates.canComplete) - checkClass(complete) + checkLesson(completeLesson, LessonStates.canComplete) + checkClass(completeLesson) it('updates the lesson state to completed', function () { const { userId, lessonDoc } = stubTeacherDocs() lessonDoc.startedAt = new Date() - LessonCollection.insert(lessonDoc) - const completed = complete.call({ userId }, lessonDoc) + + const completed = completeLesson.call({ userId, log }, lessonDoc) const updatedDoc = LessonCollection.findOne(lessonDoc._id) expect(completed).to.equal(true) expect(LessonStates.isCompleted(updatedDoc)) }) }) + // ====================================================================== + // RESTART + // ====================================================================== describe(Lesson.methods.restart.name, function () { - const restart = Lesson.methods.restart.run + const restartLesson = Lesson.methods.restart.run - checkLesson(restart, LessonStates.canRestart) - checkClass(restart) + checkLesson(restartLesson, LessonStates.canRestart) + checkClass(restartLesson) it('restarts the lesson', function () { const { userId, lessonDoc } = stubTeacherDocs() lessonDoc.startedAt = new Date() LessonCollection.insert(lessonDoc) - stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => {}) + stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => 0) stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => true) - const { lessonReset } = restart.call({ userId }, lessonDoc) + const { lessonReset } = restartLesson.call({ userId, log }, lessonDoc) const updatedDoc = LessonCollection.findOne(lessonDoc._id) expect(lessonReset).to.equal(true) @@ -445,142 +322,186 @@ describe(Lesson.name, function () { it('removes all visible references', function () { const { userId, lessonDoc } = stubTeacherDocs() lessonDoc.startedAt = new Date() - lessonDoc.visibleStudent = [Random.id()] + lessonDoc.visibleStudent = [{ _id: Random.id(), context: Random.id(5) }] lessonDoc.visibleBeamer = [Random.id()] + lessonDoc.phase = Random.id() LessonCollection.insert(lessonDoc) - stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => {}) - stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => true) - restart.call({ userId }, lessonDoc) + + stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => 123) + stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => 456) + const { runtimeDocs, beamerReset, lessonReset, groupDocs } = restartLesson.call({ userId, log }, lessonDoc) // otherwise it returns always the stubbed doc - restore(LessonCollection, 'findOne') + expect(runtimeDocs).to.equal(123) + expect(beamerReset).to.equal(456) + expect(lessonReset).to.equal(true) + expect(groupDocs).to.equal(0) + restore(LessonCollection, 'findOne') const updatedDoc = LessonCollection.findOne(lessonDoc._id) - + expect(updatedDoc.visibleStudent).to.equal(undefined) expect(updatedDoc.visibleStudent).to.equal(undefined) expect(updatedDoc.visibleBeamer).to.equal(undefined) }) + it('resets all groups, associated with this lesson') }) + // ====================================================================== + // REMOVE + // ====================================================================== describe(Lesson.methods.remove.name, function () { const removeLesson = Lesson.methods.remove.run checkLesson(removeLesson) checkClass(removeLesson, { userId: Random.id() }) - const errorCode = (_id, message) => `${_id} [${message}]` - - beforeEach(function () { - TaskCollection.remove({}) - LessonCollection.remove({}) - UnitCollection.remove({}) - PhaseCollection.remove({}) - }) - it('throws if the lesson does not exists', function () { - const _id = Random.id() - expect(() => removeLesson({ _id })).to.throw(errorCode(_id, DocNotFoundError.name)) - }) - it('throws if the linked unit does not exist', function () { - const { userId, lessonDoc } = stubTeacherDocs({ unit: Random.id() }) - expect(() => removeLesson.call({ userId }, { _id: lessonDoc._id })).to.throw(errorCode(lessonDoc.unit, DocNotFoundError.name)) + const lessonId = Random.id() + const userId = Random.id() + const env = { userId, log } + const args = { _id: lessonId } + const thrown = expect(() => removeLesson.call(env, args)).to.throw(DocNotFoundError.name) + thrown.with.property('reason', 'getDocument.docUndefined') + thrown.to.have.deep.property('details', { name: Lesson.name, query: lessonId }) }) + it('removes lesson', function () { - const { userId, lessonDoc } = stubTeacherDocs() - lessonDoc.unit = UnitCollection.insert({ createdBy: userId, title: Random.id() }) + const userId = Random.id() + const unitDoc = mockUnitDoc({ createdBy: userId }, UnitCollection) + const unitId = unitDoc._id + const { lessonDoc } = stubTeacherDocs({}, { userId, unit: unitId }) + LessonCollection.insert(lessonDoc) expect(LessonCollection.find(lessonDoc._id).count()).to.equal(1) - stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => {}) - stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => true) + stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => 123) + stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => 456) + stub(LessonRuntime, LessonRuntime.removeAllMaterial.name, () => 0) - const { lessonRemoved } = removeLesson.call({ userId }, { _id: lessonDoc._id }) + const { lessonRemoved, unitRemoved, runtimeDocsRemoved, beamerRemoved } = removeLesson.call({ + userId, + log + }, { _id: lessonDoc._id }) expect(lessonRemoved).to.equal(1) + expect(unitRemoved).to.equal(1) + expect(runtimeDocsRemoved).to.equal(123) + expect(beamerRemoved).to.equal(456) expect(LessonCollection.find(lessonDoc._id).count()).to.equal(0) + expect(UnitCollection.find(unitDoc._id).count()).to.equal(0) }) - it('removes cloned unit', function () { - const { userId, lessonDoc } = stubTeacherDocs() - const unitId = UnitCollection.insert({ createdBy: userId, title: Random.id() }) - expect(UnitCollection.find(unitId).count()).to.equal(1) - lessonDoc.unit = unitId + it('still removes lesson, even in case the linked unit does not exist', function () { + const userId = Random.id() + const unitId = Random.id() + const { lessonDoc } = stubTeacherDocs({}, { userId, unit: unitId }) + LessonCollection.insert(lessonDoc) + expect(LessonCollection.find(lessonDoc._id).count()).to.equal(1) - stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => {}) - stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => true) + stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => 123) + stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => 456) + stub(LessonRuntime, LessonRuntime.removeAllMaterial.name, () => 0) - const { unitRemoved } = removeLesson.call({ userId }, { _id: lessonDoc._id }) - expect(unitRemoved).to.equal(1) + const { lessonRemoved, unitRemoved, runtimeDocsRemoved, beamerRemoved } = removeLesson.call({ + userId, + log + }, { _id: lessonDoc._id }) + expect(lessonRemoved).to.equal(1) + expect(unitRemoved).to.equal(0) + expect(runtimeDocsRemoved).to.equal(123) + expect(beamerRemoved).to.equal(456) + expect(LessonCollection.find(lessonDoc._id).count()).to.equal(0) expect(UnitCollection.find(unitId).count()).to.equal(0) }) - it('removes cloned phases', function () { - const { userId, lessonDoc } = stubTeacherDocs() - const unitId = UnitCollection.insert({ createdBy: userId, title: Random.id() }) - lessonDoc.unit = unitId - const phaseId = PhaseCollection.insert({ createdBy: userId, unit: unitId, title: Random.id() }) - UnitCollection.update(unitId, { $set: { phases: [phaseId] } }) - - const unitDoc = UnitCollection.findOne(unitId) - expect(unitDoc.phases).to.deep.equal([phaseId]) + it('does not remove master unit', function () { + const userId = Random.id() + const unitDoc = mockUnitDoc({ _master: true, createdBy: userId }, UnitCollection) + const unitId = unitDoc._id + const { lessonDoc } = stubTeacherDocs({}, { userId, unit: unitId }) LessonCollection.insert(lessonDoc) + expect(LessonCollection.find(lessonDoc._id).count()).to.equal(1) - stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => {}) - stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => true) + stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => 1) + stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => 1) + stub(LessonRuntime, LessonRuntime.removeAllMaterial.name, () => 0) + + const { lessonRemoved, unitRemoved } = removeLesson.call({ userId, log }, { _id: lessonDoc._id }) + expect(lessonRemoved).to.equal(1) + expect(unitRemoved).to.equal(0) + expect(LessonCollection.find(lessonDoc._id).count()).to.equal(0) + expect(UnitCollection.find(unitDoc._id).count()).to.equal(1) + }) + + it('removes cloned phases', function () { + const userId = Random.id() + const phaseDoc = mockPhaseDoc({ createdBy: userId }) + const unitDoc = mockUnitDoc({ phases: [phaseDoc._id], createdBy: userId }, UnitCollection) + const unitId = unitDoc._id + phaseDoc.unit = unitId - const { phasesRemoved } = removeLesson.call({ userId }, { _id: lessonDoc._id }) + const { lessonDoc } = stubTeacherDocs({}, { unit: unitId, userId }) + const phaseId = PhaseCollection.insert(phaseDoc) + expect(unitDoc.phases).to.deep.equal([phaseDoc._id]) + + LessonCollection.insert(lessonDoc) + stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => 0) + stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => 0) + stub(LessonRuntime, LessonRuntime.removeAllMaterial.name, () => 0) + + const { phasesRemoved } = removeLesson.call({ userId, log }, { _id: lessonDoc._id }) expect(phasesRemoved).to.equal(1) expect(PhaseCollection.find(phaseId).count()).to.equal(0) }) + it('does not remove global phases or master phases', function () { - const { userId, lessonDoc } = stubTeacherDocs() - const unitId = UnitCollection.insert({ createdBy: userId, title: Random.id() }) - lessonDoc.unit = unitId + const userId = Random.id() + const phaseDoc = mockPhaseDoc({ createdBy: userId }) + let unitDoc = mockUnitDoc({ phases: [phaseDoc._id], createdBy: userId }, UnitCollection) + const unitId = unitDoc._id + phaseDoc.unit = unitId - const phaseId = PhaseCollection.insert({ createdBy: userId, unit: unitId, title: Random.id() }) - const othersPhaseId = PhaseCollection.insert({ createdBy: Random.id(), unit: unitId, title: Random.id() }) - const globalPhaseId = PhaseCollection.insert({ createdBy: userId, title: Random.id() }) - const masterPhaseId = PhaseCollection.insert({ _master: true, createdBy: userId, title: Random.id() }) - UnitCollection.update(unitId, { $set: { phases: [phaseId, othersPhaseId, globalPhaseId, masterPhaseId] } }) + const { lessonDoc } = stubTeacherDocs({}, { unit: unitId, userId }) + const phaseId = PhaseCollection.insert(phaseDoc) - const unitDoc = UnitCollection.findOne(unitId) + const othersPhaseId = mockPhaseDoc({ unit: unitId }, PhaseCollection)._id + const globalPhaseId = mockPhaseDoc({ createdBy: userId }, PhaseCollection)._id + const masterPhaseId = mockPhaseDoc({ _master: true }, PhaseCollection)._id + UnitCollection.update(unitId, { $set: { phases: [phaseId, othersPhaseId, globalPhaseId, masterPhaseId] } }) + unitDoc = UnitCollection.findOne(unitId) expect(unitDoc.phases).to.deep.equal([phaseId, othersPhaseId, globalPhaseId, masterPhaseId]) LessonCollection.insert(lessonDoc) + stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => 0) + stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => 0) + stub(LessonRuntime, LessonRuntime.removeAllMaterial.name, () => 0) - stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => {}) - stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => true) - - const { phasesRemoved } = removeLesson.call({ userId }, { _id: lessonDoc._id }) - expect(phasesRemoved).to.equal(1) + const { phasesRemoved } = removeLesson.call({ userId, log }, { _id: lessonDoc._id }) expect(PhaseCollection.find(phaseId).count()).to.equal(0) expect(PhaseCollection.find(othersPhaseId).count()).to.equal(1) expect(PhaseCollection.find(globalPhaseId).count()).to.equal(1) expect(PhaseCollection.find(masterPhaseId).count()).to.equal(1) + expect(phasesRemoved).to.equal(1) }) + it('removes cloned material', function () { - const { userId, lessonDoc } = stubTeacherDocs() - const unitId = UnitCollection.insert({ - createdBy: userId, - title: Random.id() - }) + const userId = Random.id() + let unitDoc = mockUnitDoc({ createdBy: userId }, UnitCollection) + const unitId = unitDoc._id + const { lessonDoc } = stubTeacherDocs({}, { userId, unit: unitId }) // connect task with unit and with lesson const taskId = TaskCollection.insert({ createdBy: userId, title: Random.id() }) UnitCollection.update(unitId, { $set: { tasks: [taskId] } }) - lessonDoc.unit = unitId - - // check unit integrit - const unitDoc = UnitCollection.findOne(unitId) + unitDoc = UnitCollection.findOne(unitId) expect(unitDoc.tasks).to.deep.equal([taskId]) LessonCollection.insert(lessonDoc) + stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => 0) + stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => 0) - stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => {}) - stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => true) - - const { materialRemoved } = removeLesson.call({ userId }, { _id: lessonDoc._id }) + const { materialRemoved } = removeLesson.call({ userId, log }, { _id: lessonDoc._id }) const entries = Object.entries(materialRemoved) expect(entries.length).to.equal(8) @@ -597,47 +518,54 @@ describe(Lesson.name, function () { }) it('does not remove master material', function () { - const { userId, lessonDoc } = stubTeacherDocs() - const unitId = UnitCollection.insert({ createdBy: userId, title: Random.id() }) - lessonDoc.unit = unitId + const userId = Random.id() + let unitDoc = mockUnitDoc({ createdBy: userId }, UnitCollection) + const unitId = unitDoc._id + const { lessonDoc } = stubTeacherDocs({}, { userId, unit: unitId }) + // connect task with unit and with lesson const taskId = TaskCollection.insert({ _master: true, createdBy: userId, title: Random.id() }) UnitCollection.update(unitId, { $set: { tasks: [taskId] } }) - - const unitDoc = UnitCollection.findOne(unitId) + unitDoc = UnitCollection.findOne(unitId) expect(unitDoc.tasks).to.deep.equal([taskId]) LessonCollection.insert(lessonDoc) + stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => 0) + stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => 0) - stub(LessonRuntime, LessonRuntime.removeDocuments.name, () => {}) - stub(LessonRuntime, LessonRuntime.resetBeamer.name, () => true) - - const result = removeLesson.call({ userId }, { _id: lessonDoc._id }) - const { materialRemoved } = result + const { materialRemoved } = removeLesson.call({ userId, log }, { _id: lessonDoc._id }) + const entries = Object.entries(materialRemoved) + expect(entries.length).to.equal(8) - Object.entries(materialRemoved).forEach(([context, removeCount]) => { + entries.forEach(([context, removeCount]) => { expect(removeCount).to.equal(0) }) expect(TaskCollection.find(taskId).count()).to.equal(1) }) + + it('removes custom material only if it\'s not used by other lessons') + it('removes groups, associated with this lesson') }) + // ====================================================================== + // MATERIAL + // ====================================================================== describe(Lesson.methods.material.name, function () { - const listLessonMaterial = Lesson.methods.material.run + const getLessonMaterial = Lesson.methods.material.run - checkLesson(listLessonMaterial, LessonStates.isRunning) - checkClass(listLessonMaterial, { isStudent: true, isTeacher: false }) + checkLesson(getLessonMaterial, LessonStates.isRunning) + checkClass(getLessonMaterial, { isStudent: true, isTeacher: false }) it('returns undefined if no material is considered visible', function () { const { lessonDoc, userId } = stubStudentDocs({ startedAt: new Date() }) - expect(listLessonMaterial.call({ userId }, lessonDoc)).to.equal(undefined) + expect(getLessonMaterial.call({ userId, log }, lessonDoc)).to.equal(undefined) }) it('throws if a collection is not found by context, referenced in the material', function () { const reference = { _id: Random.id(), context: Random.id() } const { lessonDoc, userId } = stubStudentDocs({ startedAt: new Date(), visibleStudent: [reference] }) - expect(() => listLessonMaterial.call({ userId }, lessonDoc)).to.throw('collectionNotFound') + expect(() => getLessonMaterial.call({ userId, log }, lessonDoc)).to.throw('collectionNotFound') }) it('returns the material, referenced by a lesson doc', function () { const taskId = Random.id() @@ -648,7 +576,7 @@ describe(Lesson.name, function () { const { lessonDoc, userId } = stubStudentDocs({ startedAt: new Date(), visibleStudent: [reference] }) TaskCollection.insert(taskDoc) - const materialDocs = listLessonMaterial.call({ userId }, lessonDoc) + const materialDocs = getLessonMaterial.call({ userId, log }, lessonDoc) expect(materialDocs).to.deep.equal({ [Task.name]: [taskDoc] }) }) it('indicates if there are docs not found, but referenced in the material', function () { @@ -656,7 +584,7 @@ describe(Lesson.name, function () { const reference = { _id: taskId, context: Task.name } const { lessonDoc, userId } = stubStudentDocs({ startedAt: new Date(), visibleStudent: [reference] }) - const materialDocs = listLessonMaterial.call({ userId }, lessonDoc) + const materialDocs = getLessonMaterial.call({ userId, log }, lessonDoc) expect(materialDocs).to.deep.equal({ [Task.name]: [], notFound: [{ context: Task.name, _id: taskId }] }) }) it('allows to skip material', function () { @@ -666,55 +594,30 @@ describe(Lesson.name, function () { const { lessonDoc, userId } = stubStudentDocs({ startedAt: new Date(), visibleStudent: [reference] }) TaskCollection.insert(taskDoc) - const materialDocs = listLessonMaterial.call({ userId }, { _id: lessonDoc._id, skip: [taskId] }) + const materialDocs = getLessonMaterial.call({ userId, log }, { _id: lessonDoc._id, skip: [taskId] }) expect(materialDocs).to.deep.equal({}) }) }) + // ====================================================================== + // UNIT + // ====================================================================== describe(Lesson.methods.units.name, function () { const getUnits = Lesson.methods.units.run - it('throws if any of the given lessons is not found by _id', function () { - const lessonId = Random.id() - const userId = Random.id() - stubUserDoc({ userId }) - expect(() => getUnits.call({ userId }, { lessonIds: [lessonId] })).to.throw(DocNotFoundError.name) - expect(() => getUnits.call({ userId }, { lessonIds: [lessonId] })).to.throw(lessonId) - }) - it('throws if any of the linked class docs is not found by _id', function () { - const lessonId = Random.id() - const classId = Random.id() - const userId = Random.id() - const lessonDoc = { _id: lessonId, classId } - stubLessonDoc(lessonDoc) - stubUserDoc({ userId }) - expect(() => getUnits.call({ userId }, { lessonIds: [lessonId] })).to.throw(DocNotFoundError.name) - expect(() => getUnits.call({ userId }, { lessonIds: [lessonId] })).to.throw(classId) - }) it('throws if the user is not member of any of the linked classes', function () { const lessonId = Random.id() const classId = Random.id() const userId = Random.id() - const lessonDoc = { _id: lessonId, classId } - const classDoc = { _id: classId } - stubLessonDoc(lessonDoc) - stubUserDoc({ userId }) - stubClassDoc(classDoc) - expect(() => getUnits.call({ userId }, { lessonIds: [lessonId] })).to.throw('permissionDenied') - expect(() => getUnits.call({ userId }, { lessonIds: [lessonId] })).to.throw(SchoolClass.errors.notMember) - }) - it('throws if any of the linked units does not exists', function () { - const lessonId = Random.id() - const classId = Random.id() - const userId = Random.id() - const unitId = Random.id() - const lessonDoc = { _id: lessonId, classId, unit: unitId } - const classDoc = { _id: classId } - stub(Lesson.helpers, 'isMemberOfLesson', () => ({ lessonDoc, classDoc })) + const classDoc = { _id: classId, title: Random.id(6), createdBy: Random.id() } + const lessonDoc = { _id: lessonId, classId, createdBy: Random.id(), unit: Random.id() } + SchoolClassCollection.insert(classDoc) + LessonCollection.insert(lessonDoc) - const thrown = expect(() => getUnits.call({ userId }, { lessonIds: [lessonId] })).to.throw(DocNotFoundError.name) - thrown.with.property('reason', 'getDocument.docUndefined') - thrown.to.have.deep.property('details', { name: Unit.name, query: unitId }) + const expectThrown = expect(() => getUnits.call({ userId, log }, { lessonIds: [lessonId] })) + .to.throw(PermissionDeniedError.name) + expectThrown.with.property('reason', SchoolClass.errors.notMember) + expectThrown.to.have.deep.property('details', { userId }) }) it('returns all units by given lesson ids', function () { const lessonIds = [Random.id(), Random.id()] @@ -722,24 +625,22 @@ describe(Lesson.name, function () { const userId = Random.id() const unitIds = [Random.id(), Random.id()] const unitDocs = unitIds.map(unitId => { - return { _id: unitId, title: Random.id() } + return { _id: unitId, title: Random.id(), pocket: Random.id(), index: 0, period: 10 } }) - stub(UnitCollection, 'findOne', id => unitDocs.find(doc => doc._id === id)) - const lessonDocs = lessonIds.map((lessonId, index) => ({ _id: lessonId, classId, unit: unitIds[index] })) - const classDoc = { _id: classId } + lessonDocs.forEach(doc => LessonCollection.insert(doc)) + unitDocs.forEach(doc => UnitCollection.insert(doc)) - stub(Lesson.helpers, 'isMemberOfLesson', ({ lessonId }) => { - return { - lessonDoc: lessonDocs.find(doc => doc._id === lessonId), - classDoc - } - }) + const classDoc = { _id: classId, title: Random.id() } + SchoolClassCollection.insert(classDoc) + + stub(LessonHelpers, 'isMemberOfClass', () => true) - const foundUnitDocs = getUnits.call({ userId }, { lessonIds }) + const foundUnitDocs = getUnits.call({ userId, log }, { lessonIds }) expect(foundUnitDocs).to.deep.equal(unitDocs) }) + it('skips units that are not found by _id') }) }) diff --git a/src/imports/contexts/classroom/lessons/tests/LessonHelpers.tests.js b/src/imports/contexts/classroom/lessons/tests/LessonHelpers.tests.js new file mode 100644 index 0000000..2a516dd --- /dev/null +++ b/src/imports/contexts/classroom/lessons/tests/LessonHelpers.tests.js @@ -0,0 +1,177 @@ +import { Random } from 'meteor/random' +import { restoreAll, stub } from '../../../../../tests/testutils/stub' +import { clearCollections, mockCollections, restoreAllCollections } from '../../../../../tests/testutils/mockCollection' +import { Users } from '../../../system/accounts/users/User' +import { Lesson } from '../Lesson' +import { LessonHelpers } from '../LessonHelpers' +import { Unit } from '../../../curriculum/curriculum/unit/Unit' +import { SchoolClass } from '../../schoolclass/SchoolClass' +import { Phase } from '../../../curriculum/curriculum/phase/Phase' +import { Task } from '../../../curriculum/curriculum/task/Task' +import { expect } from 'chai' +import { DocNotFoundError } from '../../../../api/errors/types/DocNotFoundError' +import { stubClassDoc } from '../../../../../tests/testutils/doc/stubDocs' +import { Meteor } from 'meteor/meteor' + +describe('LessonHelpers', function () { + let LessonCollection + let SchoolClassCollection + let UsersCollection + + beforeEach(function () { + [UsersCollection, LessonCollection, SchoolClassCollection] = mockCollections([Users, { noSchema: true }], [Lesson, { noSchema: true }], SchoolClass, Unit, Phase, Task) + }) + + afterEach(function () { + restoreAll() + clearCollections(Users, Lesson, Unit, SchoolClass, Phase, Task) + }) + + after(function () { + restoreAllCollections() + }) + + describe(LessonHelpers.getClassDocIfStudent.name, function () { + const { getClassDocIfStudent } = LessonHelpers + + it('throws if user does not exists', function () { + const userId = Random.id() + const classId = Random.id() + const classDoc = { _id: classId, students: [Random.id()], createdBy: Random.id() } + stubClassDoc(classDoc) + expect(() => getClassDocIfStudent({ userId, classId })) + .to.throw('schoolClass.notMember') + .with.property('details') + .with.property('userId', userId) + }) + it('throws if class does not exists', function () { + const userId = Random.id() + const classId = Random.id() + expect(() => getClassDocIfStudent({ userId, classId })) + .to.throw('getDocument.docUndefined') + .with.property('details') + .with.property('query', classId) + }) + it('throws is user is not student', function () { + const userId = Random.id() + const classId = Random.id() + stubClassDoc({ _id: classId, students: [] }) + expect(() => getClassDocIfStudent({ userId, classId })).to.throw(SchoolClass.errors.notMember) + }) + it('returns the doc otherwise', function () { + const userId = Random.id() + const classId = Random.id() + const classDoc = { _id: classId, students: [userId] } + stubClassDoc(classDoc) + const actualClassDoc = getClassDocIfStudent({ userId, classId }) + expect(actualClassDoc).to.deep.equal(classDoc) + }) + }) + describe(LessonHelpers.isMemberOfLesson.name, function () { + const { isMemberOfLesson } = LessonHelpers + + it('throws if user does not exists', function () { + const userId = Random.id() + expect(() => isMemberOfLesson({ userId })).to.throw(DocNotFoundError.name, 'user.notFound') + }) + it('throws if lesson does not exists', function () { + const userId = Random.id() + const lessonId = Random.id() + expect(() => isMemberOfLesson({ userId, lessonId })) + .to.throw('getDocument.docUndefined') + .with.property('details') + .with.property('query', lessonId) + }) + it('throws class doc does not exists', function () { + const userId = Random.id() + const lessonId = Random.id() + const classId = Random.id() + stub(LessonCollection, 'findOne', () => ({ _id: lessonId, classId })) + stub(SchoolClassCollection, 'findOne', () => undefined) + expect(() => isMemberOfLesson({ userId, lessonId })) + .to.throw('getDocument.docUndefined') + .with.property('details') + .with.property('query', classId) + }) + it('returns true if the given user is student of the lesson / class', function () { + const userId = Random.id() + const lessonId = Random.id() + const classId = Random.id() + const classDoc = { _id: classId, students: [userId] } + stub(LessonCollection, 'findOne', () => ({ _id: lessonId, classId })) + stub(SchoolClassCollection, 'findOne', () => classDoc) + expect(isMemberOfLesson({ userId, lessonId })).to.equal(true) + }) + it('returns true if the given user is teacher of the lesson / class', function () { + const userId = Random.id() + const lessonId = Random.id() + const classId = Random.id() + const classDoc = { _id: classId, teachers: [userId] } + stub(LessonCollection, 'findOne', () => ({ _id: lessonId, classId })) + stub(SchoolClassCollection, 'findOne', () => classDoc) + expect(isMemberOfLesson({ userId, lessonId })).to.equal(true) + }) + it('returns true if the given user is owner of the class', function () { + const userId = Random.id() + const lessonId = Random.id() + const classId = Random.id() + const classDoc = { _id: classId, createdBy: userId } + stub(LessonCollection, 'findOne', () => ({ _id: lessonId, classId })) + stub(SchoolClassCollection, 'findOne', () => classDoc) + expect(isMemberOfLesson({ userId, lessonId })).to.equal(true) + }) + it('returns false if the given user is not member of the lesson', function () { + const userId = Random.id() + const lessonId = Random.id() + const classdoc = { _id: Random.id() } + stub(LessonCollection, 'findOne', () => ({ _id: lessonId })) + stub(SchoolClassCollection, 'findOne', () => classdoc) + expect(isMemberOfLesson({ userId, lessonId })).to.equal(false) + }) + }) + describe(LessonHelpers.isTeacher.name, function () { + const { isTeacher } = LessonHelpers + it('throws if there is no lessonDoc', function () { + const defDoc = { userId: Random.id(), lessonId: Random.id() } + expect(() => isTeacher(defDoc)).to.throw(DocNotFoundError.name, defDoc.lessonId, Lesson.name) + }) + it('throws if there is no linked classDoc', function () { + const userId = Random.id() + const classId = Random.id() + const lessonDocId = LessonCollection.insert({ createdBy: Random.id(), classId }) + const defDoc = { userId, lessonId: lessonDocId } + expect(() => isTeacher(defDoc)).to.throw(DocNotFoundError.name, classId, SchoolClass.name) + }) + it('returns true if the user creator of the lesson', function () { + const userId = Random.id() + const lessonId = LessonCollection.insert({ createdBy: userId, classId: Random.id() }) + const defDoc = { userId, lessonId } + expect(isTeacher(defDoc)).to.equal(true) + }) + it('returns true if the user is in teachers of the class', function () { + const userId = Random.id() + const classId = SchoolClassCollection.insert({ createdBy: Random.id(), title: Random.id(), teachers: [userId] }) + const lessonId = LessonCollection.insert({ createdBy: Random.id(), classId }) + const defDoc = { userId, lessonId } + expect(isTeacher(defDoc)).to.equal(true) + }) + it('returns true if the user is creator of the class', function () { + const userId = Random.id() + const classId = SchoolClassCollection.insert({ createdBy: userId, title: Random.id(), teachers: [Random.id()] }) + const lessonId = LessonCollection.insert({ createdBy: Random.id(), classId }) + const defDoc = { userId, lessonId } + expect(isTeacher(defDoc)).to.equal(true) + }) + it('returns false otherwise', function () { + const userId = Random.id() + const classId = SchoolClassCollection.insert({ + createdBy: Random.id(), + title: Random.id(), + teachers: [Random.id()] + }) + const lessonId = LessonCollection.insert({ createdBy: Random.id(), classId }) + const defDoc = { userId, lessonId } + expect(isTeacher(defDoc)).to.equal(false) + }) + }) +}) diff --git a/src/imports/contexts/classroom/lessons/tests/LessonRuntime.tests.js b/src/imports/contexts/classroom/lessons/tests/LessonRuntime.tests.js index b1e7575..08f40ea 100644 --- a/src/imports/contexts/classroom/lessons/tests/LessonRuntime.tests.js +++ b/src/imports/contexts/classroom/lessons/tests/LessonRuntime.tests.js @@ -2,10 +2,12 @@ import { LessonRuntime } from '../runtime/LessonRuntime' import { Beamer } from '../../../beamer/Beamer' import { TaskResults } from '../../../tasks/results/TaskResults' -// import { Files } from '../../../imports/api/decorators/methods/files/Files' - import { Random } from 'meteor/random' -import { mockCollection } from '../../../../../tests/testutils/mockCollection' +import { + clearAllCollections, + mockCollections, + restoreAllCollections +} from '../../../../../tests/testutils/mockCollection' import { expect } from 'chai' import { TaskWorkingState } from '../../../tasks/state/TaskWorkingState' import { Cluster } from '../../../tasks/responseProcessors/aggregate/cluster/Cluster' @@ -13,29 +15,10 @@ import { getCollection } from '../../../../api/utils/getCollection' import { ImageFiles } from '../../../files/image/ImageFiles' import { AudioFiles } from '../../../files/audio/AudioFiles' import { DocumentFiles } from '../../../files/document/DocumentFiles' +import { Users } from '../../../system/accounts/users/User' +import { VideoFiles } from '../../../files/video/VideoFiles' +import { stub, restoreAll } from '../../../../../tests/testutils/stub' -// remove runtimedocs related - -// TODO 1.0 -// TODO we should have a function that we can call and -// TODO that returns all the context names for those context -// TODO that are related to the lesson runtime -// TODO why? -// TODO because in the furutre we want to have packages to be -// TODO added, that can augment lessons with custom items -// TODO and therefore custom artifacts, so they need to be -// TODO registered somewhere and retrievable somehow - -const noSchema = { noSchema: true } -const TaskResultCollection = mockCollection(TaskResults, noSchema) -const ImageFilesCollection = mockCollection(ImageFiles, noSchema) -const AudioFilesColection = mockCollection(AudioFiles, noSchema) -const DocumentFilesCollection = mockCollection(DocumentFiles, noSchema) -const TaskWorkingStateCollection = mockCollection(TaskWorkingState, noSchema) -const ClusterCollection = mockCollection(Cluster, noSchema) -// beamer related - -const BeamerCollection = mockCollection(Beamer) const randomReferences = (beamerDoc, lessonId) => { const rand = Math.floor(Math.random() * 53) let lessonRefs = 0 @@ -63,11 +46,52 @@ const randomReferences = (beamerDoc, lessonId) => { } describe(LessonRuntime.name, function () { - describe(LessonRuntime.resetBeamer.name, function () { - beforeEach(function () { - BeamerCollection.remove({}) - }) + // remove runtimedocs related +// TODO 1.0 +// TODO we should have a function that we can call and +// TODO that returns all the context names for those context +// TODO that are related to the lesson runtime +// TODO why? +// TODO because in the furutre we want to have packages to be +// TODO added, that can augment lessons with custom items +// TODO and therefore custom artifacts, so they need to be +// TODO registered somewhere and retrievable somehow + const noSchema = { noSchema: true } + const forFiles = { noSchema: true, isFilesCollection: true } + let TaskResultCollection + let ImageFilesCollection + let AudioFilesColection + let DocumentFilesCollection + let VideoFilesCollection + let TaskWorkingStateCollection + let ClusterCollection + let BeamerCollection + + before(function () { + [TaskResultCollection, ImageFilesCollection, AudioFilesColection, DocumentFilesCollection, VideoFilesCollection, TaskWorkingStateCollection, ClusterCollection, BeamerCollection] = mockCollections( + [TaskResults, noSchema], + [ImageFiles, forFiles], + [AudioFiles, forFiles], + [DocumentFiles, forFiles], + [VideoFiles, forFiles], + [TaskWorkingState, noSchema], + [Cluster, noSchema], + Beamer, + Users + ) + }) + + afterEach(function () { + restoreAll() + clearAllCollections() + }) + + after(function () { + restoreAllCollections() + }) + + describe(LessonRuntime.resetBeamer.name, function () { it('returns -1, if no beamer doc exists for given query', function () { const query = { lessonId: Random.id(), userId: Random.id() } expect(LessonRuntime.resetBeamer(query)).to.equal(-1) @@ -122,14 +146,6 @@ describe(LessonRuntime.name, function () { }) describe(LessonRuntime.removeDocuments.name, function () { - beforeEach(function () { - TaskResultCollection.remove({}) - TaskWorkingStateCollection.remove({}) - AudioFilesColection.remove({}) - ImageFilesCollection.remove({}) - DocumentFilesCollection.remove({}) - ClusterCollection.remove({}) - }) it('removes no documents if there are no docs for a given lesson', function () { const removed = LessonRuntime.removeDocuments({ lessonId: Random.id() }) Object.values(removed).forEach(removedCount => expect(removedCount).to.equal(0)) @@ -143,11 +159,26 @@ describe(LessonRuntime.name, function () { ImageFilesCollection.insert({ meta: { lessonId } }) AudioFilesColection.insert({ meta: { lessonId } }) DocumentFilesCollection.insert({ meta: { lessonId } }) + VideoFilesCollection.insert({ meta: { lessonId } }) + + stub(ImageFilesCollection.filesCollection, 'remove', (query) => { + return ImageFilesCollection.remove(query) + }) + stub(AudioFilesColection.filesCollection, 'remove', (query) => { + return AudioFilesColection.remove(query) + }) + stub(DocumentFilesCollection.filesCollection, 'remove', (query) => { + return DocumentFilesCollection.remove(query) + }) + stub(VideoFilesCollection.filesCollection, 'remove', (query) => { + return VideoFilesCollection.remove(query) + }) const removed = LessonRuntime.removeDocuments({ lessonId }) Object.entries(removed).forEach(([context, removedCount]) => { + const remainCount = getCollection(context).find().count() expect(removedCount).to.equal(1) - expect(getCollection(context).find().count()).to.equal(0) + expect(remainCount).to.equal(0) }) }) }) diff --git a/src/imports/contexts/classroom/lessons/tests/index.js b/src/imports/contexts/classroom/lessons/tests/index.js index f1f08e3..35cf8ff 100644 --- a/src/imports/contexts/classroom/lessons/tests/index.js +++ b/src/imports/contexts/classroom/lessons/tests/index.js @@ -1,6 +1,7 @@ /* eslint-env mocha */ describe('Lesson', function () { + import './LessonHelpers.tests' + import './LessonStates.tests' import './LessonRuntime.tests' import './Lesson.tests' - import './LessonStates.tests' }) diff --git a/src/imports/contexts/classroom/schoolclass/SchoolClass.js b/src/imports/contexts/classroom/schoolclass/SchoolClass.js index 871c9ed..d829eb0 100644 --- a/src/imports/contexts/classroom/schoolclass/SchoolClass.js +++ b/src/imports/contexts/classroom/schoolclass/SchoolClass.js @@ -20,6 +20,10 @@ export const SchoolClass = { students: 1 }, dependencies: [], + /** + * Extract into own namespace + * @deprecated + */ errors: { progressIncomplete: 'schoolClass.progressIncomplete', invalidSchoolYear: 'schoolClass.invalidSchoolYear', @@ -408,6 +412,7 @@ SchoolClass.methods.remove = { roles: UserUtils.roles.teacher, run: onServerExec(function () { import { removeClass } from './methods/removeClass' + return function ({ _id }) { const { userId, log } = this const classId = _id diff --git a/src/imports/contexts/classroom/schoolclass/helpers/isMemberOfClass.js b/src/imports/contexts/classroom/schoolclass/helpers/isMemberOfClass.js index c0707ce..751f0a2 100644 --- a/src/imports/contexts/classroom/schoolclass/helpers/isMemberOfClass.js +++ b/src/imports/contexts/classroom/schoolclass/helpers/isMemberOfClass.js @@ -6,7 +6,18 @@ * @param userId {string} the user's _id to use for the check * @return {boolean} */ -export const isMemberOfClass = ({ classDoc, userId }) => classDoc && userId && - !!(classDoc.createdBy === userId || - (classDoc.teachers && classDoc.teachers.indexOf(userId) > -1) || - (classDoc.students && classDoc.students.indexOf(userId) > -1)) +export const isMemberOfClass = ({ classDoc, userId }) => { + if (typeof classDoc !== 'object' || typeof userId !== 'string') { + return false + } + + if (classDoc.createdBy === userId) { + return true + } + + if (classDoc.teachers && classDoc.teachers.includes(userId)) { + return true + } + + return classDoc.students && classDoc.students.includes(userId) +} diff --git a/src/imports/contexts/classroom/schoolclass/methods/removeClass.js b/src/imports/contexts/classroom/schoolclass/methods/removeClass.js index c3580d9..98e88ff 100644 --- a/src/imports/contexts/classroom/schoolclass/methods/removeClass.js +++ b/src/imports/contexts/classroom/schoolclass/methods/removeClass.js @@ -2,10 +2,10 @@ import { SchoolClass } from '../SchoolClass' import { Meteor } from 'meteor/meteor' import { getCollection } from '../../../../api/utils/getCollection' import { userIsAdmin } from '../../../../api/accounts/admin/userIsAdmin' -import { createGetDoc } from '../../../../api/utils/documentUtils' import { removeLesson } from '../../lessons/methods/removeLesson' +import { createDocGetter } from '../../../../api/utils/document/createDocGetter' -const getClassDoc = createGetDoc(SchoolClass) +const getClassDoc = createDocGetter({ name: SchoolClass.name }) /** * Removes a class by given _id and userId. The user must be externally validated! @@ -16,10 +16,11 @@ const getClassDoc = createGetDoc(SchoolClass) */ export const removeClass = function removeClass ({ classId, userId, log = () => {} }) { const { Lesson } = require('../../lessons/Lesson') - const schoolClassDoc = getClassDoc.call({ userId }, classId) + const schoolClassDoc = getClassDoc(classId) // check if user is even allowed to delete const canDelete = userId === schoolClassDoc.createdBy || userIsAdmin(userId) + if (!canDelete) { throw new Error('errors.permissionDenied', 'errors.notOwnerOrAdmin') } diff --git a/src/imports/contexts/classroom/schoolclass/tests/SchoolClass.tests.js b/src/imports/contexts/classroom/schoolclass/tests/SchoolClass.tests.js index 14096cb..0908fdc 100644 --- a/src/imports/contexts/classroom/schoolclass/tests/SchoolClass.tests.js +++ b/src/imports/contexts/classroom/schoolclass/tests/SchoolClass.tests.js @@ -2,7 +2,12 @@ import { Random } from 'meteor/random' import { SchoolClass } from '../SchoolClass' import { Lesson } from '../../lessons/Lesson' -import { clearCollection, mockCollection, restoreAllCollections } from '../../../../../tests/testutils/mockCollection' +import { + clearAllCollections, + clearCollection, + mockCollections, + restoreAllCollections +} from '../../../../../tests/testutils/mockCollection' import { DocNotFoundError } from '../../../../api/errors/types/DocNotFoundError' import { InvocationChecker } from '../../../../api/utils/InvocationChecker' import { onServerExec } from '../../../../api/utils/archUtils' @@ -10,9 +15,12 @@ import { PermissionDeniedError } from '../../../../api/errors/types/PermissionDe import { restoreAll, stub } from '../../../../../tests/testutils/stub' import { stubMethod, unstubMethod } from '../../../../../tests/testutils/stubMethod' import { expect } from 'chai' +import { Users } from '../../../system/accounts/users/User' +import { LessonRuntime } from '../../lessons/runtime/LessonRuntime' +import { Unit } from '../../../curriculum/curriculum/unit/Unit' +import { Phase } from '../../../curriculum/curriculum/phase/Phase' + -let SchoolClassCollection -let LessonCollection const { isStudent } = SchoolClass.helpers const { isTeacher } = SchoolClass.helpers @@ -21,15 +29,17 @@ const { addStudent } = SchoolClass.helpers const { removeStudent } = SchoolClass.helpers describe(SchoolClass.name, function () { + let SchoolClassCollection + let LessonCollection + let UsersCollection before(function () { - SchoolClassCollection = mockCollection(SchoolClass) - LessonCollection = mockCollection(Lesson) + [SchoolClassCollection, LessonCollection, UsersCollection] = mockCollections(SchoolClass, Lesson, Users, Unit, Phase) + }) afterEach(function () { - clearCollection(SchoolClass) - clearCollection(Lesson) + clearAllCollections() restoreAll() }) @@ -202,34 +212,27 @@ describe(SchoolClass.name, function () { onServerExec(function () { describe('methods', function () { - const create = SchoolClass.methods.create.run - const remove = SchoolClass.methods.remove.run + const createClass = SchoolClass.methods.create.run + const removeClass = SchoolClass.methods.remove.run describe(SchoolClass.methods.create.name, function () { it('creates a new school class doc', function () { - const classDocDef = { title: Random.id(), students: [Random.id()], teachers: [Random.id()] } + const classDocDef = { title: Random.id() } const environment = { userId: Random.id() } - const classDocId = create.call(environment, Object.assign({}, classDocDef)) + const classDocId = createClass.call(environment, Object.assign({}, classDocDef)) const classDoc = SchoolClassCollection.findOne(classDocId) expect(classDoc.title).to.equal(classDocDef.title) - expect(classDoc.title).to.equal(classDocDef.title) - expect(classDoc.students).to.deep.equal(classDocDef.students) - expect(classDoc.teachers).to.deep.equal(classDocDef.teachers.concat([environment.userId])) + expect(classDoc.createdBy).to.equal(environment.userId) + + // there are no students invited so none should be added + // at the same time there is the only teacher the owner of the class + expect(classDoc.students).to.deep.equal([]) + expect(classDoc.teachers).to.deep.equal([environment.userId]) }) }) describe(SchoolClass.methods.remove.name, function () { - beforeEach(function () { - stubMethod(Lesson.methods.remove.name, function ({ _id }) { - return LessonCollection.remove(_id) - }) - }) - - afterEach(function () { - unstubMethod(Lesson.methods.remove.name) - }) - it('throws if the classDoc is not found', function () { - expect(() => remove({ _id: Random.id() })).to.throw(DocNotFoundError.name) + expect(() => removeClass({ _id: Random.id() })).to.throw(DocNotFoundError.name) }) it('removes the class (and only this class) by given _id', function () { const userId = Random.id() @@ -237,7 +240,7 @@ describe(SchoolClass.name, function () { const classId = SchoolClassCollection.insert(classDoc) const otherClassId = SchoolClassCollection.insert(classDoc) - remove.call({ userId }, { _id: classId }) + removeClass.call({ userId }, { _id: classId }) expect(SchoolClassCollection.find(classId).count()).to.equal(0) expect(SchoolClassCollection.find(otherClassId).count()).to.equal(1) }) @@ -253,7 +256,7 @@ describe(SchoolClass.name, function () { let lessons = [] lessons.length = Math.floor(1 + Math.random() * 10) lessons.fill(0) - lessons = lessons.map(() => LessonCollection.insert({ classId, title: Random.id(), createdBy: userId })) + lessons = lessons.map(() => LessonCollection.insert({ classId, title: Random.id(), createdBy: userId, unit: Random.id() })) // create other lessons let otherLessons = [] @@ -262,7 +265,8 @@ describe(SchoolClass.name, function () { otherLessons = otherLessons.map(() => LessonCollection.insert({ classId: otherClassId, title: Random.id(), - createdBy: userId + createdBy: userId, + unit: Random.id() })) // before @@ -270,7 +274,19 @@ describe(SchoolClass.name, function () { otherLessons.forEach(lessonId => expect(LessonCollection.find(lessonId).count()).to.equal(1)) expect(LessonCollection.find({ classId }).count()).to.equal(lessons.length) - remove.call({ userId }, { _id: classId }) + // stub lesson runtime, make sure we don't remove + // content from other lessons + stub(LessonRuntime, 'removeDocuments', ({ lessonId, userId: id }) => { + expect(id).to.equal(userId) + const byLessonId = id => id === lessonId + expect(lessons.some(byLessonId)).to.equal(true) + expect(otherLessons.some(byLessonId)).to.equal(false) + return 1 + }) + + stub(LessonRuntime, 'resetBeamer', () => 1) + + removeClass.call({ userId }, { _id: classId }) // after expect(LessonCollection.find({ classId }).count()).to.equal(0) diff --git a/src/imports/contexts/curriculum/curriculum/unit/Unit.js b/src/imports/contexts/curriculum/curriculum/unit/Unit.js index d34654e..28462de 100644 --- a/src/imports/contexts/curriculum/curriculum/unit/Unit.js +++ b/src/imports/contexts/curriculum/curriculum/unit/Unit.js @@ -445,6 +445,7 @@ Unit.methods.remove = { import { createRemoveAllMaterial } from '../../../material/createRemoveAllMaterial' import { checkOwnership } from '../../../../api/utils/document/checkOwnership' import { ensureDocumentExists } from '../../../../api/utils/document/ensureDocumentExists' + const removeAllMaterial = createRemoveAllMaterial({ isCurriculum: true }) return function ({ _id }) { diff --git a/src/imports/contexts/curriculum/curriculum/unit/tests/Unit.tests.js b/src/imports/contexts/curriculum/curriculum/unit/tests/Unit.tests.js index 58c7fa3..c28a543 100644 --- a/src/imports/contexts/curriculum/curriculum/unit/tests/Unit.tests.js +++ b/src/imports/contexts/curriculum/curriculum/unit/tests/Unit.tests.js @@ -2,13 +2,31 @@ import { Unit } from '../Unit' import { Random } from 'meteor/random' import { expect } from 'chai' -import { mockCollection } from '../../../../../../tests/testutils/mockCollection' +import { + clearAllCollections, + mockCollections, + restoreAllCollections +} from '../../../../../../tests/testutils/mockCollection' import { PermissionDeniedError } from '../../../../../api/errors/types/PermissionDeniedError' import { DocNotFoundError } from '../../../../../api/errors/types/DocNotFoundError' +import { Phase } from '../../phase/Phase' -const UnitCollection = mockCollection(Unit) describe(Unit.name, function () { + let UnitCollection + + before(function () { + [UnitCollection] = mockCollections(Unit, Phase) + }) + + afterEach(function () { + clearAllCollections() + }) + + after(function () { + restoreAllCollections() + }) + describe('methods', function () { describe(Unit.methods.byTaskId.name, function () { it('returns all units that reference a task by id if curriculum user') diff --git a/src/imports/contexts/system/accounts/admin/Admin.js b/src/imports/contexts/system/accounts/admin/Admin.js index fc2222c..28847f7 100644 --- a/src/imports/contexts/system/accounts/admin/Admin.js +++ b/src/imports/contexts/system/accounts/admin/Admin.js @@ -34,6 +34,9 @@ Admin.schema = { } } +/** + * @deprecated + */ Admin.errors = AdminErrors Admin.methods = {} @@ -88,7 +91,7 @@ Admin.methods.createUser = { return function ({ role, firstName, lastName, email, institution }) { const willBeAdmin = role === UserUtils.roles.admin - +console.debug(this.userId, { willBeAdmin }) // deny any attempt to create a new admin from a non-admin account if (willBeAdmin && !userIsAdmin(this.userId)) { throw new PermissionDeniedError('roles.notAdmin', { @@ -132,10 +135,11 @@ Admin.methods.removeUser = { import { userExists } from '../../../../api/accounts/user/userExists' import { userIsAdmin } from '../../../../api/accounts/admin/userIsAdmin' import { PermissionDeniedError } from '../../../../api/errors/types/PermissionDeniedError' + import { DocNotFoundError } from '../../../../api/errors/types/DocNotFoundError' return function ({ _id }) { if (!userExists({ userId: _id })) { - throw new Meteor.Error('user.notExist', undefined, _id) + throw new DocNotFoundError('user.notExist', { _id }) } // can't self delete in any case @@ -177,14 +181,16 @@ Admin.methods.updateRole = { import { removeAdmin } from '../../../../api/accounts/admin/removeAdmin' import { userExists } from '../../../../api/accounts/user/userExists' import { userIsAdmin } from '../../../../api/accounts/admin/userIsAdmin' + import { getCollection } from '../../../../api/utils/getCollection' + import { Users } from '../users/User' return function ({ userId, role, group }) { if (this.userId === userId) { - throw new Meteor.Error('admin.updateRoleFailed', 'admin.noOwnRolesChangeAllowed', userId) + throw new Meteor.Error('admin.updateRoleFailed', 'admin.noOwnRolesChangeAllowed', { userId }) } if (!userExists({ userId })) { - throw new Meteor.Error('admin.updateRoleFailed', Admin.errors.USER_NOT_FOUND, userId) + throw new Meteor.Error('admin.updateRoleFailed', Admin.errors.USER_NOT_FOUND, { userId }) } if (!UserUtils.roleExists(role)) { @@ -208,7 +214,7 @@ Admin.methods.updateRole = { throw new Meteor.Error('admin.updateRoleFailed', 'roles.notAssigned', { userId, role, group }) } - return Meteor.users.update(userId, { $set: { role } }) + return getCollection(Users.name).update(userId, { $set: { role } }) } }) } @@ -223,12 +229,17 @@ Admin.methods.users = { }, 'ids.$': String }, - run: onServer(function ({ ids }) { - const query = {} - if (ids?.length) { - query._id = { $in: ids } + run: onServerExec(function () { + import { Users } from '../users/User' + import { getCollection } from '../../../../api/utils/getCollection' + + return function ({ ids }) { + const query = {} + if (ids?.length) { + query._id = { $in: ids } + } + return getCollection(Users.name).find(query, { fields: { services: 0 } }).fetch() } - return Meteor.users.find(query, { fields: { services: 0 } }).fetch() }) } diff --git a/src/imports/contexts/system/accounts/admin/tests/Admin.tests.js b/src/imports/contexts/system/accounts/admin/tests/Admin.tests.js index e1f7d63..d83c765 100644 --- a/src/imports/contexts/system/accounts/admin/tests/Admin.tests.js +++ b/src/imports/contexts/system/accounts/admin/tests/Admin.tests.js @@ -6,24 +6,36 @@ import { Accounts } from 'meteor/accounts-base' import { onServerExec } from '../../../../../api/utils/archUtils' import { restoreAll, stub } from '../../../../../../tests/testutils/stub' import { UserUtils } from '../../users/UserUtils' -import { mockCollection } from '../../../../../../tests/testutils/mockCollection' +import { + clearAllCollections, + mockCollections, + restoreAllCollections +} from '../../../../../../tests/testutils/mockCollection' import { expect } from 'chai' import { UserFactory } from '../../../../../api/accounts/registration/UserFactory' - -const AdminCollection = mockCollection(Admin) +import { Users } from '../../users/User' +import { PermissionDeniedError } from '../../../../../api/errors/types/PermissionDeniedError' +import { DocNotFoundError } from '../../../../../api/errors/types/DocNotFoundError' describe(Admin.name, function () { - onServerExec(function () { - describe('methods', function () { - beforeEach(function () { - AdminCollection.remove({}) - Meteor.users.remove({}) - }) + let AdminCollection + let UsersCollection - afterEach(function () { - restoreAll() - }) + before(function () { + [AdminCollection, UsersCollection] = mockCollections(Admin, Users) + }) + + afterEach(function () { + restoreAll() + clearAllCollections() + }) + after(function () { + restoreAllCollections() + }) + + onServerExec(function () { + describe('methods', function () { describe(Admin.methods.createUser.name, function () { const createUser = Admin.methods.createUser.run @@ -36,156 +48,214 @@ describe(Admin.name, function () { expect(created).to.equal(userId) expect(AdminCollection.find({ userId }).count()).to.equal(0) }) - it('makes a user admin if the role is given', function () { - const userId = Random.id() - stub(UserFactory, 'create', () => userId) - stub(Accounts, 'sendEnrollmentEmail', () => userId) - stub(Meteor.users, 'find', () => ({ count: () => 1 })) + it('throws if the user is not admin but role is admin', function () { + const userId = UsersCollection.insert({ username: Random.id() }) + const env = { userId } + const args = { + firstName: Random.id(), + lastName: Random.id(), + email: Random.id(), + institution: Random.id(), + role: UserUtils.roles.admin + } + const expectThrow = expect(() => createUser.call(env, args)) + .to.throw(PermissionDeniedError.name) + expectThrow.with.property('reason', 'roles.notAdmin') + expectThrow.with.deep.property('details', { userId, ...args }) + }) + it('makes a user admin if the role is given and current user is admin', function () { + stub(UserFactory, 'create', () => newUserId) + stub(Accounts, 'sendEnrollmentEmail', () => newUserId) - const created = createUser({ role: UserUtils.roles.admin }) - expect(created).to.equal(userId) + const userId = UsersCollection.insert({ username: Random.id() }) + const newUserId = UsersCollection.insert({ username: Random.id() }) + AdminCollection.insert({ userId }) + expect(AdminCollection.find({ userId }).count()).to.equal(1) + + const env = { userId } + const created = createUser.call(env, { role: UserUtils.roles.admin }) + expect(created).to.equal(newUserId) expect(AdminCollection.find({ userId }).count()).to.equal(1) + expect(AdminCollection.find({ userId: newUserId }).count()).to.equal(1) }) }) + describe(Admin.methods.removeUser.name, function () { - const remove = Admin.methods.removeUser.run + const removeUser = Admin.methods.removeUser.run it('throws if the user does not exists', function () { - expect(() => remove({})).throw(Admin.errors.REMOVE_USER_NOT_EXISTS) - .with.property('details', undefined) - const _id = Random.id() - expect(() => remove({ _id })).throw(Admin.errors.REMOVE_USER_NOT_EXISTS) - .with.property('details', _id) + const expectThrow = expect(() => removeUser({ _id })) + .to.throw(DocNotFoundError.name) + expectThrow.with.property('reason', 'user.notExist') + expectThrow.with.deep.property('details', { _id }) }) - it('removes the user', function () { - const _id = Random.id() - stub(Meteor.users, 'find', () => ({ count: () => 1 })) - stub(Meteor.users, 'remove', () => 1) + it('throws if the users wants to delete themselves', function () { + const _id = UsersCollection.insert({ username: Random.id() }) + const env = { userId: _id } + const expectThrow = expect(() => removeUser.call(env, { _id })) + .to.throw(PermissionDeniedError.name) + expectThrow.with.property('reason', 'user.noSelfDelete') + expectThrow.with.deep.property('details', { userId: _id, _id }) + }) + + it('throws if the users is no admin but wants to remove an admin', function () { + const execUserId = UsersCollection.insert({ username: Random.id() }) + const adminUserId = UsersCollection.insert({ username: Random.id() }) + AdminCollection.insert({ userId: adminUserId }) + const env = { userId: execUserId } + const expectThrow = expect(() => removeUser.call(env, { _id: adminUserId })) + .to.throw(PermissionDeniedError.name) + expectThrow.with.property('reason', 'roles.notAdmin') + expectThrow.with.deep.property('details', { userId: execUserId, _id: adminUserId }) + }) - const { adminRemoved, rolesRemoved, userRemoved } = remove({ _id }) + it('removes the user', function () { + const _id = UsersCollection.insert({ username: Random.id() }) + const { adminRemoved, rolesRemoved, userRemoved } = removeUser({ _id }) expect(adminRemoved).to.equal(0) expect(rolesRemoved).to.equal(0) expect(userRemoved).to.equal(1) }) it('removes the roles', function () { - const _id = Random.id() - stub(Meteor.users, 'find', () => ({ count: () => 1 })) + const _id = UsersCollection.insert({ username: Random.id() }) stub(Meteor.roleAssignment, 'remove', () => 1) - const { adminRemoved, rolesRemoved, userRemoved } = remove({ _id }) + const { adminRemoved, rolesRemoved, userRemoved } = removeUser({ _id }) expect(adminRemoved).to.equal(0) expect(rolesRemoved).to.equal(1) - expect(userRemoved).to.equal(0) + expect(userRemoved).to.equal(1) }) it('removes the admin', function () { - const _id = Random.id() - stub(Meteor.users, 'find', () => ({ count: () => 1 })) - stub(AdminCollection, 'remove', () => 1) + const execUserId = UsersCollection.insert({ username: Random.id() }) + AdminCollection.insert({ userId: execUserId }) + + const adminUserId = UsersCollection.insert({ username: Random.id() }) + AdminCollection.insert({ userId: adminUserId }) + + stub(Meteor.roleAssignment, 'remove', () => 1) - const { adminRemoved, rolesRemoved, userRemoved } = remove({ _id }) + const env = { userId: execUserId } + const { adminRemoved, rolesRemoved, userRemoved } = removeUser.call(env, { _id: adminUserId }) expect(adminRemoved).to.equal(1) - expect(rolesRemoved).to.equal(0) - expect(userRemoved).to.equal(0) + expect(rolesRemoved).to.equal(1) + expect(userRemoved).to.equal(1) }) }) + describe(Admin.methods.reinvite.name, function () { - const reinvite = Admin.methods.reinvite.run + const reinviteUser = Admin.methods.reinvite.run it('throws if the user does not exist', function () { - const thrown = expect(() => reinvite({})).to.throw('errors.docNotFound') + const thrown = expect(() => reinviteUser({})).to.throw('errors.docNotFound') thrown.with.property('reason', 'errors.userNotExists') thrown.with.property('details', undefined) const userId = Random.id() - const thrownWithId = expect(() => reinvite({ userId })).to.throw('errors.docNotFound') + const thrownWithId = expect(() => reinviteUser({ userId })).to.throw('errors.docNotFound') thrownWithId.with.property('reason', 'errors.userNotExists') thrownWithId.with.property('details', userId) }) it('sends an enrollment email', function () { - const userId = Random.id() - stub(Meteor.users, 'find', () => ({ count: () => 1 })) + const userId = UsersCollection.insert({ username: Random.id() }) stub(Accounts, 'sendEnrollmentEmail', () => userId) - expect(reinvite({ userId })).to.equal(userId) + expect(reinviteUser({ userId })).to.equal(userId) }) }) + describe(Admin.methods.updateRole.name, function () { const updateRole = Admin.methods.updateRole.run it('throws if the user does not exist', function () { - const thrown = expect(() => updateRole({})).to.throw('admin.updateRoleFailed') + const userId = Random.id() + const env = { userId } + const thrown = expect(() => updateRole.call(env, {})).to.throw('admin.updateRoleFailed') thrown.with.property('reason', Admin.errors.USER_NOT_FOUND) - thrown.with.property('details', undefined) - + thrown.with.deep.property('details', { userId: undefined }) + }) + it('throws if the user wants to change their own role', function () { const userId = Random.id() - const thrownWithId = expect(() => updateRole({ userId })).to.throw('admin.updateRoleFailed') - thrownWithId.with.property('reason', Admin.errors.USER_NOT_FOUND) - thrownWithId.with.property('details', userId) + const env = { userId } + + const thrownWithId = expect(() => updateRole.call(env,env)).to.throw('admin.updateRoleFailed') + thrownWithId.with.property('reason', 'admin.noOwnRolesChangeAllowed') + thrownWithId.with.deep.property('details', { userId }) }) it('throws if the role does not exist', function () { - const userId = Random.id() + const userId = UsersCollection.insert({ username: Random.id() }) const role = Random.id() - stub(Meteor.users, 'find', () => ({ count: () => 1 })) - const thrownWithId = expect(() => updateRole({ userId, role })).to.throw('admin.updateRoleFailed') + const group = Random.id() + const thrownWithId = expect(() => updateRole({ userId, role, group })).to.throw('admin.updateRoleFailed') thrownWithId.with.property('reason', 'roles.unknownRole') - thrownWithId.with.property('details', role) + thrownWithId.with.deep.property('details', { userId, role, group }) }) it('updates the user\'s role', function () { - const userId = Random.id() + const userId = UsersCollection.insert({ username: Random.id() }) const role = Random.id() + const group = Random.id() - stub(Meteor.users, 'find', () => ({ count: () => 1 })) stub(Roles, 'setUserRoles', () => true) stub(Roles, 'userIsInRole', () => userId) stub(UserUtils, 'roleExists', () => true) - expect(updateRole({ userId, role })).to.equal(userId) + expect(updateRole({ userId, role, group })).to.equal(1) }) it('makes admin if not already admin and will be admin', function () { - const userId = Random.id() + const execUserId = UsersCollection.insert({ username: Random.id() }) + const env = { userId: execUserId } + AdminCollection.insert({ userId: execUserId }) + + const newAdminUserId = UsersCollection.insert({ username: Random.id() }) const role = UserUtils.roles.admin - stub(Meteor.users, 'find', () => ({ count: () => 1 })) - stub(Meteor.users, 'findOne', () => ({ _id: userId })) stub(Roles, 'setUserRoles', () => true) - stub(Roles, 'userIsInRole', () => userId) + stub(Roles, 'userIsInRole', () => newAdminUserId) stub(UserUtils, 'roleExists', () => true) - expect(AdminCollection.find().count()).to.equal(0) - expect(updateRole({ userId, role })).to.equal(userId) - expect(AdminCollection.find({ userId }).count()).to.equal(1) + expect(AdminCollection.find({ userId: newAdminUserId }).count()).to.equal(0) + expect(updateRole.call(env, { userId: newAdminUserId, role })).to.equal(1) + expect(AdminCollection.find({ userId: newAdminUserId }).count()).to.equal(1) }) - it('remoes admin if already admin and will be non-admin', function () { - const userId = Random.id() - const role = Random.id() + it('removes admin if already admin and will be non-admin', function () { + const execUserId = UsersCollection.insert({ username: Random.id() }) + const oldAdminUserId = UsersCollection.insert({ username: Random.id() }) + const env = { userId: execUserId } + AdminCollection.insert({ userId: execUserId }) + AdminCollection.insert({ userId: oldAdminUserId }) + + const role = UserUtils.roles.teacher - stub(Meteor.users, 'find', () => ({ count: () => 1 })) - stub(Meteor.users, 'findOne', () => ({ _id: userId })) stub(Roles, 'setUserRoles', () => true) - stub(Roles, 'userIsInRole', () => userId) + stub(Roles, 'userIsInRole', () => oldAdminUserId) stub(UserUtils, 'roleExists', () => true) - AdminCollection.insert({ userId }) - expect(AdminCollection.find().count()).to.equal(1) - expect(updateRole({ userId, role })).to.equal(userId) - expect(AdminCollection.find().count()).to.equal(0) + expect(AdminCollection.find({ userId: oldAdminUserId }).count()).to.equal(1) + expect(updateRole.call(env, { userId: oldAdminUserId, role })).to.equal(1) + expect(AdminCollection.find({ userId: oldAdminUserId }).count()).to.equal(0) }) }) describe(Admin.methods.users.name, function () { it('returns all users', function () { - const users = [{ _id: Random.id(), services: {} }, { _id: Random.id(), services: {} }, { - _id: Random.id(), - services: {} - }] + const users = [ + { + _id: Random.id(), services: {} + }, { + _id: Random.id(), services: {} + }, { + _id: Random.id(), services: {} + }] + users.forEach(entry => { - Meteor.users.insert(entry) + UsersCollection.insert(entry) }) - Admin.methods.users.run().forEach((userDoc, index) => { + const ids = users.map(({ _id }) => _id) + + Admin.methods.users.run({ ids }).forEach((userDoc, index) => { const expectedUser = users[index] expect(userDoc._id).to.equal(expectedUser._id) expect(userDoc.services).to.equal(undefined) diff --git a/src/imports/contexts/system/accounts/users/User.js b/src/imports/contexts/system/accounts/users/User.js index 1afc784..f8cce8c 100644 --- a/src/imports/contexts/system/accounts/users/User.js +++ b/src/imports/contexts/system/accounts/users/User.js @@ -28,7 +28,51 @@ Users.publicFields = { 'presence.status': 1 } -Users.schema = {} +Users.schema = { + username: { + type: String, + optional: true, + }, + profileImage: profileImageSchema({ optional: true }), + emails: { + type: Array, + optional: true + }, + 'emails.$': { + type: Object, + blackbox: true, + optional: true + }, + firstName: firstNameSchema({ optional: true }), + lastName: lastNameSchema({ optional: true }), + locale: { + type: String, + optional: true + }, + institution: { + type: String, + optional: true + }, + role: { + type: String, + optional: true + }, + services: { + type: Object, + blackbox: true, + optional: true + }, + ui: { + type: Object, + blackbox: true, + optional: true + }, + presence: { + type: Object, + blackbox: true, + optional: true + }, +} /** @deprecated **/ Users.roles = { diff --git a/src/imports/contexts/system/accounts/users/UserUtils.js b/src/imports/contexts/system/accounts/users/UserUtils.js index fdd4116..fbc78d9 100644 --- a/src/imports/contexts/system/accounts/users/UserUtils.js +++ b/src/imports/contexts/system/accounts/users/UserUtils.js @@ -3,6 +3,7 @@ import { Meteor } from 'meteor/meteor' import { Roles } from 'meteor/alanning:roles' import { mapFromObject } from '../../../../api/utils/mapFromObject' import { isomporph, onClient, onServer } from '../../../../api/utils/archUtils' +import { getUsersCollection } from '../../../../api/utils/getUsersCollection' const roleIndices = mapFromObject({ admin: 0, @@ -30,7 +31,7 @@ export const UserUtils = { let finalScope if (!scope) { - const user = Meteor.users.findOne(userId) + const user = getUsersCollection().findOne(userId) finalScope = user.institution } else { @@ -47,18 +48,23 @@ export const UserUtils = { /** * @deprecated TODO extract */ - canInvite (userId, role, scope) { + canInvite (userId, role, institution) { check(userId, String) check(role, String) - check(scope, Match.Maybe(String)) + check(institution, Match.Maybe(String)) let finalScope - if (!scope) { - const user = Meteor.users.findOne(userId) - finalScope = user.institution + + if (!institution) { + const user = getUsersCollection().findOne(userId) + finalScope = user?.institution } else { - finalScope = scope + finalScope = institution + } + + if (!finalScope) { + return false } switch (role) { @@ -121,7 +127,7 @@ export const UserUtils = { UserUtils.isCurriculum = function (userId = Meteor.userId(), scope) { let finalScope if (!scope) { - const user = Meteor.users.findOne(userId) + const user = getUsersCollection().findOne(userId) finalScope = user.institution } else { @@ -142,7 +148,7 @@ UserUtils.isAdmin = isomporph({ client: function () { return function isAdmin (userId = Meteor.userId()) { if (!userId) return false - const user = Meteor.users.findOne(userId) + const user = getUsersCollection().findOne(userId) if (!user) return false return Roles.userIsInRole(userId, UserUtils.roles.admin, user.institution) diff --git a/src/imports/contexts/system/accounts/users/methods/getUser.js b/src/imports/contexts/system/accounts/users/methods/getUser.js index 37e38f3..ab910b7 100644 --- a/src/imports/contexts/system/accounts/users/methods/getUser.js +++ b/src/imports/contexts/system/accounts/users/methods/getUser.js @@ -1,7 +1,8 @@ import { Meteor } from 'meteor/meteor' +import { getUsersCollection } from '../../../../../api/utils/getUsersCollection' export const getUser = function getUser ({ _id, userId }) { - const userDoc = Meteor.users.findOne(_id) + const userDoc = getUsersCollection().findOne(_id) if (!userDoc) { throw new Meteor.Error('user.invalidUser', 'user.notFound', _id) } diff --git a/src/imports/contexts/system/accounts/users/methods/registerWithCode.js b/src/imports/contexts/system/accounts/users/methods/registerWithCode.js index 87ecda9..de6f094 100644 --- a/src/imports/contexts/system/accounts/users/methods/registerWithCode.js +++ b/src/imports/contexts/system/accounts/users/methods/registerWithCode.js @@ -5,6 +5,7 @@ import { SchoolClass } from '../../../../classroom/schoolclass/SchoolClass' import { rollbackAccount } from '../../../../../api/accounts/registration/rollbackAccount' import { correctName } from '../../../../../api/utils/correctName' import { userExists } from '../../../../../api/accounts/user/userExists' +import { createDocGetter } from '../../../../../api/utils/document/createDocGetter' const errors = { codeInvalid: 'codeRegister.codeInvalid', @@ -14,6 +15,8 @@ const errors = { studentNotAdded: 'codeRegister.studentNotAdded' } +const getCodeDoc = createDocGetter({ name: CodeInvitation.name, optional: true }) + /** * Registers a new user with a given invitation code. * The code can only be obtained, from a teacher. @@ -27,7 +30,7 @@ const errors = { * @return {*} */ export const registerWithCode = function ({ code, email, firstName, lastName, password, institution, locale }) { - const codeDoc = CodeInvitation.helpers.getCodeDoc(code) + const codeDoc = getCodeDoc({ code }) // first we validate if the related code doc exists and is still valid if (!CodeInvitation.helpers.validate(codeDoc)) { @@ -46,9 +49,9 @@ export const registerWithCode = function ({ code, email, firstName, lastName, pa userId = UserFactory.create({ email: email || codeDoc.email, password: password, - firstName: correctName(firstName || codeDoc.firstName, options), - lastName: correctName(lastName || codeDoc.lastName, options), - institution: correctName(codeDoc.institution || institution, options), + firstName: correctName(firstName ?? codeDoc.firstName, options), + lastName: correctName(lastName ?? codeDoc.lastName, options), + institution: correctName(codeDoc.institution ?? institution, options), role: codeDoc.role, locale: locale }) @@ -72,10 +75,10 @@ export const registerWithCode = function ({ code, email, firstName, lastName, pa if (!studentAdded) { rollbackAccount(userId) - throw new Meteor.Error(errors.failed, errors.studentNotAdded, JSON.stringify({ + throw new Meteor.Error(errors.failed, errors.studentNotAdded, { classId, studentAdded - })) + }) } } diff --git a/src/imports/contexts/system/accounts/users/methods/resendVerificationEmail.js b/src/imports/contexts/system/accounts/users/methods/resendVerificationEmail.js index 3c650e2..2605f53 100644 --- a/src/imports/contexts/system/accounts/users/methods/resendVerificationEmail.js +++ b/src/imports/contexts/system/accounts/users/methods/resendVerificationEmail.js @@ -1,9 +1,9 @@ -import { Meteor } from 'meteor/meteor' import { Accounts } from 'meteor/accounts-base' import { Users } from '../User' +import { getUsersCollection } from '../../../../../api/utils/getUsersCollection' export const resendVerificationEmail = function resendVerificationEmail ({ userId }) { - const user = Meteor.users.findOne(userId) + const user = getUsersCollection().findOne(userId) if (!user) { // logError() diff --git a/src/imports/contexts/system/accounts/users/methods/sendResetPasswordEmail.js b/src/imports/contexts/system/accounts/users/methods/sendResetPasswordEmail.js index a43de37..f07c3fc 100644 --- a/src/imports/contexts/system/accounts/users/methods/sendResetPasswordEmail.js +++ b/src/imports/contexts/system/accounts/users/methods/sendResetPasswordEmail.js @@ -1,7 +1,9 @@ import { Accounts } from 'meteor/accounts-base' +import { getUserByEmail } from '../../../../../api/accounts/user/getUserByEmail' export const sendResetPasswordEmail = function sendResetPasswordEmail ({ email }) { - const user = Accounts.findUserByEmail(email) + const user = getUserByEmail(email) + if (user) { return Accounts.sendResetPasswordEmail(user._id) } diff --git a/src/imports/contexts/system/accounts/users/methods/updateProfile.js b/src/imports/contexts/system/accounts/users/methods/updateProfile.js index eaa2df5..ea0905e 100644 --- a/src/imports/contexts/system/accounts/users/methods/updateProfile.js +++ b/src/imports/contexts/system/accounts/users/methods/updateProfile.js @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor' import { userExists } from '../../../../../api/accounts/user/userExists' +import { getUsersCollection } from '../../../../../api/utils/getUsersCollection' /** * Updates the user's profile. Picks only the defined arguments for the update call. @@ -32,5 +33,5 @@ export const updateProfile = function updateProfile ({ userId, profileImage, fir updateDoc.locale = locale } - return Meteor.users.update(userId, { $set: updateDoc }) + return getUsersCollection().update(userId, { $set: updateDoc }) } diff --git a/src/imports/contexts/system/accounts/users/methods/updateUI.js b/src/imports/contexts/system/accounts/users/methods/updateUI.js index c31d39d..685b919 100644 --- a/src/imports/contexts/system/accounts/users/methods/updateUI.js +++ b/src/imports/contexts/system/accounts/users/methods/updateUI.js @@ -1,9 +1,10 @@ import { Meteor } from 'meteor/meteor' import { userExists } from '../../../../../api/accounts/user/userExists' +import { getUsersCollection } from '../../../../../api/utils/getUsersCollection' export const updateUI = function updateUI ({ userId, fluid, classId }) { if (!userExists({ userId })) { - throw new Meteor.Error('errors.403', 'user.updateUI', 'user.userNotFound') + throw new Meteor.Error('user.updateUI', 'user.userNotFound', { userId }) } const query = { ui: {} } @@ -11,5 +12,5 @@ export const updateUI = function updateUI ({ userId, fluid, classId }) { if (typeof fluid === 'boolean') query.ui.fluid = fluid if (typeof classId === 'string') query.ui.classId = classId - return Meteor.users.update(userId, { $set: query }) + return getUsersCollection().update(userId, { $set: query }) } diff --git a/src/imports/contexts/system/accounts/users/methods/verifyToken.js b/src/imports/contexts/system/accounts/users/methods/verifyToken.js index ee661a3..6455578 100644 --- a/src/imports/contexts/system/accounts/users/methods/verifyToken.js +++ b/src/imports/contexts/system/accounts/users/methods/verifyToken.js @@ -1,29 +1,40 @@ import { Meteor } from 'meteor/meteor' -import { Accounts } from 'meteor/accounts-base' import { getEnrollmentExpiration } from '../../../../../api/accounts/registration/getEnrollmentExpiration' - +import { getUserByEmail } from '../../../../../api/accounts/user/getUserByEmail' + +/** + * Verifies a given reset attempt, which consists of email, reason and token. + * All three must be valid in order to pass! + * + * @param email {string} + * @param token {srting} + * @param reason {string} + * @return {boolean} + * @throws {Meteor.Error} when one of the three is not valid + */ export const verifyToken = function verifyToken ({ email, token, reason }) { - const user = Accounts.findUserByEmail(email) + const user = getUserByEmail(email) if (!user) { - throw new Meteor.Error('errors.403', 'user.tokenInvalid', 'user.userNotFound') + throw new Meteor.Error('user.tokenInvalid', 'user.userNotFound', { email }) } - const service = (user?.services?.password?.[reason] || {}) + const service = user?.services?.password?.[reason] - if (service.token !== token) { - throw new Meteor.Error('errors.403', 'user.tokenInvalid', 'user.tokenInvalid') + if (!service || service.reason !== reason) { + throw new Meteor.Error('user.tokenInvalid', 'user.reasonInvalid', { reason }) } - if (service.reason !== reason) { - throw new Meteor.Error('errors.403', 'user.tokenInvalid', 'user.reasonInvalid') + if (service.token !== token) { + throw new Meteor.Error('user.tokenInvalid', 'user.tokenInvalid', { email }) } const now = Date.now() const when = getEnrollmentExpiration(new Date(service.when || 0)) + const expired = when - now - if ((when - now) < 0) { - throw new Meteor.Error('errors.403', 'user.tokenInvalid', 'user.tokenExpired') + if (expired < 0) { + throw new Meteor.Error('user.tokenInvalid', 'user.tokenExpired', { expired }) } return true diff --git a/src/imports/contexts/system/accounts/users/tests/UserUtils.tests.js b/src/imports/contexts/system/accounts/users/tests/UserUtils.tests.js index 8dccd59..1084402 100644 --- a/src/imports/contexts/system/accounts/users/tests/UserUtils.tests.js +++ b/src/imports/contexts/system/accounts/users/tests/UserUtils.tests.js @@ -3,9 +3,13 @@ import { Random } from 'meteor/random' import { stubUser, unstubUser } from '../../../../../../tests/testutils/stubUser' import { UserUtils } from '../UserUtils' import { assert } from 'chai' -import { mockCollection } from '../../../../../../tests/testutils/mockCollection' +import { + clearAllCollections, mockCollections, + restoreAllCollections +} from '../../../../../../tests/testutils/mockCollection' import { Admin } from '../../admin/Admin' -import { onClientExec, onServer, onServerExec } from '../../../../../api/utils/archUtils' +import { onClientExec, onServerExec } from '../../../../../api/utils/archUtils' +import { Users } from '../User' const userObj = () => ({ _id: Random.id(), @@ -15,18 +19,26 @@ const userObj = () => ({ custom: 'foo' }) -const AdminCollection = mockCollection(Admin) describe('UserUtils', function () { let user + let AdminCollection + + before(function () { + [AdminCollection] = mockCollections(Admin, Users) + }) beforeEach(function () { user = userObj() - AdminCollection.remove({}) }) afterEach(function () { unstubUser(true, true) + clearAllCollections() + }) + + after(function () { + restoreAllCollections() }) describe('isAdmin', function () { diff --git a/src/imports/contexts/system/accounts/users/tests/Users.tests.js b/src/imports/contexts/system/accounts/users/tests/Users.tests.js index 6d70726..052a06b 100644 --- a/src/imports/contexts/system/accounts/users/tests/Users.tests.js +++ b/src/imports/contexts/system/accounts/users/tests/Users.tests.js @@ -11,39 +11,66 @@ import { onServerExec } from '../../../../../api/utils/archUtils' import { createCodeDoc } from '../../../../../../tests/testutils/doc/createCodeDoc' import { expect } from 'chai' import { restoreAll, stub } from '../../../../../../tests/testutils/stub' -import { mockCollection } from '../../../../../../tests/testutils/mockCollection' +import { + clearAllCollections, + mockCollections, + restoreAllCollections +} from '../../../../../../tests/testutils/mockCollection' import { collectPublication } from '../../../../../../tests/testutils/collectPublication' import { InvocationChecker } from '../../../../../api/utils/InvocationChecker' - -// some methods depend on these collections -mockCollection(Admin) -const SchoolClassCollection = mockCollection(SchoolClass) -const CodeInvitationCollection = mockCollection(CodeInvitation) - -const registerUser = ({ code = Random.id(), email = `${Random.id()}@example.app`, firstName, lastName } = {}) => ({ - code, email, firstName, lastName +import { UserUtils } from '../UserUtils' +import { mockClassDoc } from '../../../../../../tests/testutils/doc/mockClassDoc' +import { PermissionDeniedError } from '../../../../../api/errors/types/PermissionDeniedError' +import { DocNotFoundError } from '../../../../../api/errors/types/DocNotFoundError' + +const createRegisterDoc = ({ + code = Random.id(), + email = `${Random.id()}@example.app`, + firstName, + lastName, + institution = Random.id() + } = {}) => ({ + code, email, firstName, lastName, institution }) describe('Users', function () { + let SchoolClassCollection + let CodeInvitationCollection + let AdminCollection + let UsersCollection + + before(function () { + [SchoolClassCollection, CodeInvitationCollection, AdminCollection, UsersCollection] = mockCollections(SchoolClass, CodeInvitation, Admin, Users) + }) + + afterEach(function () { + restoreAll() + clearAllCollections() + }) + + after(function () { + restoreAllCollections() + }) + onServerExec(function () { describe('helpers', function () { describe(Users.helpers.verify.name, function () { - const verify = Users.helpers.verify + const verifyUser = Users.helpers.verify it('throws if the user is not found', function () { - expect(() => verify()).to.throw('errors.userNotFound') + expect(() => verifyUser()).to.throw('errors.userNotFound') }) it('throws if the user has no email', function () { - expect(() => verify({})).to.throw('errors.noEmailFound') - expect(() => verify({ emails: [] })).to.throw('errors.noEmailFound') - expect(() => verify({ emails: [{}] })).to.throw('errors.noEmailFound') + expect(() => verifyUser({})).to.throw('errors.noEmailFound') + expect(() => verifyUser({ emails: [] })).to.throw('errors.noEmailFound') + expect(() => verifyUser({ emails: [{}] })).to.throw('errors.noEmailFound') }) it('returns false if the user is not verified', function () { - expect(verify({ emails: [{ address: Random.id() }] })).to.equal(false) - expect(verify({ emails: [{ address: Random.id(), verified: false }] })).to.equal(false) + expect(verifyUser({ emails: [{ address: Random.id() }] })).to.equal(false) + expect(verifyUser({ emails: [{ address: Random.id(), verified: false }] })).to.equal(false) }) it('returns true if the user is verified', function () { - expect(verify({ emails: [{ address: Random.id(), verified: true }] })).to.equal(true) + expect(verifyUser({ emails: [{ address: Random.id(), verified: true }] })).to.equal(true) }) }) }) @@ -51,129 +78,109 @@ describe('Users', function () { describe('methods', function () { const registerWithCode = (...args) => Users.methods.registerWithCode.run(...args) - afterEach(function () { - Meteor.users.remove({}) - CodeInvitationCollection.remove({}) - restoreAll() - }) - describe(Users.methods.registerWithCode.name, function () { it('throws on an invalid code', function () { - const registerDoc = registerUser() - expect(() => registerWithCode(registerDoc)) + const doc = createRegisterDoc() + expect(() => registerWithCode(doc)) .to.throw('codeRegister.failed') .with.property('reason', 'codeRegister.codeInvalid') }) it('throws if a user exists already by given email', function () { - const registerDoc = registerUser() - Accounts.createUser(registerDoc) + const registerDoc = createRegisterDoc() + UsersCollection.insert({ emails: [{ address: registerDoc.email }]}) + stub(CodeInvitation.helpers, CodeInvitation.helpers.validate.name, () => true) expect(() => registerWithCode(registerDoc)) .to.throw('codeRegister.failed') .with.property('reason', 'codeRegister.emailExists') }) it('throws if the account creation failed', function () { - const registerDoc = registerUser() - + const registerDoc = createRegisterDoc() + CodeInvitationCollection.insert({ + ...registerDoc, + expires: 2, + maxUsers: 2, + role: UserUtils.roles.teacher + }) stub(CodeInvitation.helpers, CodeInvitation.helpers.validate.name, () => true) - stub(CodeInvitation.helpers, CodeInvitation.helpers.getCodeDoc.name, () => {}) + stub(CodeInvitation.helpers, CodeInvitation.helpers.getCodeDoc.name, () => undefined) + stub(UserFactory, 'create', () => { + throw new Meteor.Error('error', 'expectedErrorReason') + }) stub(Accounts, 'createUser', () => null) expect(() => registerWithCode(registerDoc)) .to.throw('codeRegister.failed') - .with.property('reason', 'account') + .with.property('reason', 'expectedErrorReason') }) it('throws if adding user to the class fails', function () { const codeDoc = createCodeDoc() - const registerDoc = registerUser({ code: codeDoc._id, firstName: 'John', lastName: 'Doe' }) + CodeInvitationCollection.insert(codeDoc) + const registerDoc = createRegisterDoc({ code: codeDoc.code, firstName: 'John', lastName: 'Doe' }) stub(CodeInvitation.helpers, CodeInvitation.helpers.validate.name, () => true) - stub(CodeInvitation.helpers, CodeInvitation.helpers.getCodeDoc.name, () => codeDoc) stub(CodeInvitation.helpers, CodeInvitation.helpers.addUserToInvitation.name, () => 1) stub(UserFactory, UserFactory.create.name, () => Random.id()) - stub(SchoolClass.helpers, SchoolClass.helpers.addStudent.name, () => undefined) + stub(SchoolClass.helpers, SchoolClass.helpers.addStudent.name, () => false) - expect(() => registerWithCode(registerDoc)).to.throw('codeRegister.failed').with.property('reason', 'class') + const thrown = expect(() => registerWithCode(registerDoc)) + .to.throw('codeRegister.failed') + thrown.with.property('reason', 'codeRegister.studentNotAdded') + thrown.with.deep.property('details', { classId: codeDoc.classId, studentAdded: false }) }) - it('registers a user', function () { - const codeDoc = createCodeDoc() - delete codeDoc.classId - - const registerDoc = registerUser({ code: codeDoc._id, firstName: 'John', lastName: 'Doe' }) - stub(CodeInvitation.helpers, CodeInvitation.helpers.validate.name, () => true) - stub(CodeInvitation.helpers, CodeInvitation.helpers.getCodeDoc.name, () => codeDoc) - stub(CodeInvitation.helpers, CodeInvitation.helpers.addUserToInvitation.name, () => 1) - stub(Accounts, 'sendEnrollmentEmail', () => true) - stub(Accounts, 'sendVerificationEmail', () => true) - - const userId = registerWithCode(registerDoc) - expect(Meteor.users.find(userId).count()).to.equal(1) - }) - it('invalidates the codeDoc after registration', function () { - const classId = SchoolClassCollection.insert({ title: Random.id() }) - const codeDoc = createCodeDoc({ classId }) + const validRegistration = ({ codeDocArgs } = {}) => { + const codeDoc = createCodeDoc(codeDocArgs) + delete codeDoc.classId const codeDocId = CodeInvitationCollection.insert(codeDoc) - const registerDoc = registerUser({ code: codeDoc._id, firstName: 'John', lastName: 'Doe' }) + const registerDoc = createRegisterDoc({ code: codeDoc.code, firstName: 'John', lastName: 'Doe' }) stub(CodeInvitation.helpers, CodeInvitation.helpers.validate.name, () => true) stub(CodeInvitation.helpers, CodeInvitation.helpers.getCodeDoc.name, () => codeDoc) - + stub(Roles, 'addUsersToRoles', () => true) + stub(Roles, 'userIsInRole', () => true) + stub(Accounts, 'createUser', ({ email }) => { + return UsersCollection.insert({ emails: [{ address: email }] }) + }) stub(Accounts, 'sendEnrollmentEmail', () => true) stub(Accounts, 'sendVerificationEmail', () => true) - stub(InvocationChecker, 'ensureMethodInvocation', () => {}) - - expect(CodeInvitation.helpers.isComplete(codeDoc)).to.equal(false) const userId = registerWithCode(registerDoc) - expect(Meteor.users.find(userId).count()).to.equal(1) + expect(UsersCollection.find(userId).count()).to.equal(1) + return { codeDocId, userId, registerDoc } + } + + it('registers a user', function () { + validRegistration() + }) + it('invalidates the codeDoc after registration', function () { + const { codeDocId } = validRegistration() const afterRegisterCodeDoc = CodeInvitationCollection.findOne(codeDocId) expect(CodeInvitation.helpers.isComplete(afterRegisterCodeDoc)).to.equal(true) }) it('always uses role and institution from codeDoc', function () { - const codeDoc = createCodeDoc() - delete codeDoc.classId - - const registerDoc = registerUser({ - code: codeDoc._id, - firstName: 'John', - lastName: 'Doe', - role: Random.id(), - institution: 'other school' - }) - - stub(CodeInvitation.helpers, CodeInvitation.helpers.validate.name, () => true) - stub(CodeInvitation.helpers, CodeInvitation.helpers.getCodeDoc.name, () => codeDoc) - stub(CodeInvitation.helpers, CodeInvitation.helpers.addUserToInvitation.name, () => 1) - stub(Accounts, 'sendEnrollmentEmail', () => true) - stub(Accounts, 'sendVerificationEmail', () => true) + const codeDocArgs = { + role: UserUtils.roles.curriculum, + institution: 'Other school' + } + const { userId, codeDocId, registerDoc } = validRegistration({ codeDocArgs }) + const user = UsersCollection.findOne(userId) + const codeDoc = CodeInvitationCollection.findOne(codeDocId) - const userId = registerWithCode(registerDoc) - const user = Meteor.users.findOne({ _id: userId }) expect(user.role).to.equal(codeDoc.role) expect(user.role).to.not.equal(registerDoc.role) expect(user.institution).to.equal(codeDoc.institution) expect(user.institution).to.not.equal(registerDoc.institution) }) it('allows to use firstName and lastName in favour from user input', function () { - const codeDoc = createCodeDoc() - delete codeDoc.classId - - const registerDoc = registerUser({ - code: codeDoc._id, + const codeDocArgs = { firstName: 'Jane', lastName: 'Done' - }) - - stub(CodeInvitation.helpers, CodeInvitation.helpers.validate.name, () => true) - stub(CodeInvitation.helpers, CodeInvitation.helpers.getCodeDoc.name, () => codeDoc) - stub(CodeInvitation.helpers, CodeInvitation.helpers.addUserToInvitation.name, () => 1) - stub(Accounts, 'sendEnrollmentEmail', () => true) - stub(Accounts, 'sendVerificationEmail', () => true) - - const userId = registerWithCode(registerDoc) - const user = Meteor.users.findOne() + } + const { userId, codeDocId, registerDoc } = validRegistration({ codeDocArgs }) + const user = UsersCollection.findOne(userId) + const codeDoc = CodeInvitationCollection.findOne(codeDocId) expect(user.firstName).to.not.equal(codeDoc.firstName) expect(user.firstName).to.equal(registerDoc.firstName) expect(user.lastName).to.not.equal(codeDoc.lastName) @@ -183,96 +190,111 @@ describe('Users', function () { describe(Users.methods.checkResetpasswordToken.name, function () { const checkResetPasswordToken = Users.methods.checkResetpasswordToken.run - beforeEach(function () { - Meteor.users.remove({}) - }) - - afterEach(function () { - restoreAll() - }) - it('throws if the given user is not found', function () { - const throwed = expect(() => checkResetPasswordToken({ - email: Random.id(), - token: Random.id() + const email = Random.id() + const thrown = expect(() => checkResetPasswordToken({ + token: Random.id(), + email })) - .to.throw('403') + .to.throw('user.tokenInvalid') - throwed.with.property('reason', 'user.tokenInvalid') - throwed.with.property('details', 'user.userNotFound') + thrown.with.property('reason', 'user.userNotFound') + thrown.with.deep.property('details', { email }) }) it('throws if the token is the token is missing', function () { - const userDoc = { email: Random.id() } - const userId = Accounts.createUser(userDoc) - Accounts.setPassword(userId, Random.id()) + const email = Random.id() + const userDoc = { email } + UsersCollection.insert({ + emails: [{ address: email }], + services: { + password: { + reset: { reason: 'reset', token: Random.id() } + } + } + }) - const throwed = expect(() => checkResetPasswordToken({ + const thrown = expect(() => checkResetPasswordToken({ email: userDoc.email, - token: Random.id() - })).to.throw('403') + token: Random.id(), + reason: 'reset' + })).to.throw('user.tokenInvalid') - throwed.with.property('reason', 'user.tokenInvalid') - throwed.with.property('details', 'user.tokenInvalid') + thrown.with.property('reason', 'user.tokenInvalid') + thrown.with.deep.property('details', { email }) }) it('throws if the reason is not valid', function () { - const userDoc = { email: Random.id(), password: Random.id() } - const userId = Accounts.createUser(userDoc) + const email = 'me@example.com' + const userId = UsersCollection.insert({ emails: [{ address: email }], services: { password: {} } }) const tokenId = Random.id() - Meteor.users.update(userId, { - $set: { 'services.password.reset': { token: tokenId } } + UsersCollection.update(userId, { + $set: { 'services.password.reset': { token: tokenId, reason: 'reset' } } }) - const throwed = expect(() => checkResetPasswordToken({ - email: userDoc.email, + const reason = Random.id() + const thrown = expect(() => checkResetPasswordToken({ token: tokenId, - reason: Random.id() - })).to.throw('403') + email, + reason + })).to.throw('user.tokenInvalid') - throwed.with.property('reason', 'user.tokenInvalid') - throwed.with.property('details', 'user.reasonInvalid') + thrown.with.property('reason', 'user.reasonInvalid') + thrown.with.deep.property('details', { reason }) }) it('throws if the date is already expired', function () { - const userDoc = { email: Random.id(), password: Random.id() } - const userId = Accounts.createUser(userDoc) + const email = Random.id() + const userId = UsersCollection.insert({ emails: [{ address: email }], services: { password: {} } }) const tokenId = Random.id() - const reason = Random.id() - Meteor.users.update(userId, { - $set: { 'services.password.reset': { token: tokenId, reason } } + const reason = 'reset' + + UsersCollection.update(userId, { + $set: { + 'services.password.reset': { + token: tokenId, + reason, + when: new Date(Date.now() - 1000000000) + } + } }) - const throwed = expect(() => checkResetPasswordToken({ - email: userDoc.email, + const thrown = expect(() => checkResetPasswordToken({ + email, token: tokenId, reason: reason - })).to.throw('403') + })).to.throw('user.tokenInvalid') - throwed.with.property('reason', 'user.tokenInvalid') - throwed.with.property('details', 'user.tokenExpired') + thrown.with.property('reason', 'user.tokenExpired') }) it('returns true if the token is valid', function () { - const userDoc = { email: Random.id(), password: Random.id() } - const userId = Accounts.createUser(userDoc) + const email = Random.id() + const userId = UsersCollection.insert({ emails: [{ address: email }], services: { password: {} } }) const tokenId = Random.id() - const reason = Random.id() - const when = new Date(Date.now() - 1000000) - Meteor.users.update(userId, { - $set: { 'services.password.reset': { token: tokenId, reason, when } } + const reason = 'reset' + + UsersCollection.update(userId, { + $set: { + 'services.password.reset': { + token: tokenId, + reason, + when: new Date(Date.now() - 1000000) + } + } }) + expect(checkResetPasswordToken({ - email: userDoc.email, + email: email, token: tokenId, reason: reason })).to.equal(true) }) }) + describe(Users.methods.getUser.name, function () { const getUser = Users.methods.getUser.run const _id = Random.id() let user beforeEach(function () { - Meteor.users.remove({}) user = { _id: _id, emails: [{ address: `${Random.id()}@domain.tld` }], @@ -297,7 +319,7 @@ describe('Users', function () { thrown.with.property('details', undefined) }) it('returns a near full user for oneself', function () { - stub(Meteor.users, 'findOne', () => user) + UsersCollection.insert(user) const actualUser = getUser.call({ userId: user._id }, { _id }) expect(actualUser._id).to.equal(user._id) @@ -308,7 +330,7 @@ describe('Users', function () { expect(actualUser.presence).to.deep.equal({ online: true }) }) it('returns a limited user for others', function () { - stub(Meteor.users, 'findOne', () => user) + UsersCollection.insert(user) const actualUser = getUser.call({ userId: Random.id() }, { _id }) expect(actualUser._id).to.equal(user._id) @@ -319,6 +341,7 @@ describe('Users', function () { expect(actualUser.services).to.equal(undefined) }) }) + describe(Users.methods.resendVerificationMail.name, function () { const resend = Users.methods.resendVerificationMail.run @@ -334,7 +357,7 @@ describe('Users', function () { verified: true }] } - stub(Meteor.users, 'findOne', () => user) + UsersCollection.insert(user) const sent = resend({ userId: user._id }) expect(sent).to.equal(undefined) @@ -349,74 +372,73 @@ describe('Users', function () { } const mailId = Random.id() - stub(Meteor.users, 'findOne', () => user) + stub(UsersCollection, 'findOne', () => user) stub(Accounts, 'sendVerificationEmail', () => mailId) const sent = resend({ userId: user._id }) expect(sent).to.equal(mailId) }) }) + describe(Users.methods.sendResetPasswordEmail.name, function () { const send = Users.methods.sendResetPasswordEmail.run it('fails silent if the user not exists by email', function () { - stub(Accounts, 'findUserByEmail', () => undefined) const sent = send({ email: Random.id() }) expect(sent).to.equal(undefined) }) it('sends a password-reset mail to the given user', function () { - const userId = Random.id() - stub(Accounts, 'findUserByEmail', () => userId) + const email = Random.id() + const userId = UsersCollection.insert({ emails: [{ address: email }]}) stub(Accounts, 'sendResetPasswordEmail', () => userId) - const sent = send({ email: Random.id() }) + const sent = send({ email }) expect(sent).to.equal(userId) }) }) + describe(Users.methods.updateProfile.name, function () { const update = Users.methods.updateProfile.run it('updates the current user\'s profile', function () { - const user = { firstName: Random.id(), lastName: Random.id(), profileImage: Random.id() } - const userId = Meteor.users.insert(user) - + const userId = UsersCollection.insert({ firstName: 'John', lastName: 'Doe', profileImage: Random.id() }) + const userDoc = UsersCollection.findOne(userId) const updateDoc = { - firstName: Random.id(), - lastName: Random.id(), + firstName: 'Jane', + lastName: 'Done', profileImage: Random.id() } expect(update.call({ userId }, updateDoc)).to.equal(1) - const updatedUser = Meteor.users.findOne(userId) + const updatedUser = UsersCollection.findOne(userId) expect(updatedUser.firstName).to.equal(updateDoc.firstName) expect(updatedUser.lastName).to.equal(updateDoc.lastName) expect(updatedUser.profileImage).to.equal(updateDoc.profileImage) - expect(updatedUser.firstName).to.not.equal(user.firstName) - expect(updatedUser.lastName).to.not.equal(user.lastName) - expect(updatedUser.profileImage).to.not.equal(user.profileImage) + expect(updatedUser.firstName).to.not.equal(userDoc.firstName) + expect(updatedUser.lastName).to.not.equal(userDoc.lastName) + expect(updatedUser.profileImage).to.not.equal(userDoc.profileImage) }) }) + describe(Users.methods.updateUI.name, function () { const update = Users.methods.updateUI.run it('it updates the users ui', function () { const user = { ui: { fluid: undefined } } - const userId = Meteor.users.insert(user) + const userId = UsersCollection.insert(user) - const updateDoc = { - fluid: true - } + const updateDoc = { fluid: true } expect(update.call({ userId }, updateDoc)).to.equal(1) - const updatedUser = Meteor.users.findOne(userId) + const updatedUser = UsersCollection.findOne(userId) expect(updatedUser.ui.fluid).to.equal(updateDoc.fluid) expect(updatedUser.ui.fluid).to.not.equal(user.ui.fluid) }) it('creates a new ui namespace on the user if it does not exist', function () { const user = {} - const userId = Meteor.users.insert(user) + const userId = UsersCollection.insert(user) const updateDoc = { fluid: true @@ -424,17 +446,18 @@ describe('Users', function () { expect(update.call({ userId }, updateDoc)).to.equal(1) - const updatedUser = Meteor.users.findOne(userId) + const updatedUser = UsersCollection.findOne(userId) expect(updatedUser.ui.fluid).to.equal(true) }) }) + describe(Users.methods.userIsAvailable.name, function () { const userIsAvailable = Users.methods.userIsAvailable.run it('returns if a user exists by mail', function () { - const userDoc = { email: Random.id(), password: Random.id() } - Accounts.createUser(userDoc) - expect(userIsAvailable({ email: userDoc.email })).to.equal(false) + const email = Random.id() + UsersCollection.insert({ emails: [{ address: email }]}) + expect(userIsAvailable({ email })).to.equal(false) expect(userIsAvailable({ email: Random.id() })).to.equal(true) }) }) @@ -450,24 +473,24 @@ describe('Users', function () { it('throws if there is no class by given classId', function () { const classId = Random.id() - const thrown = expect(() => byClass({ classId })).to.throw('errors.publicationFailed') - thrown.with.property('reason', 'errors.docNotFound') - thrown.with.property('details', classId) + const thrown = expect(() => byClass({ classId })) + .to.throw(DocNotFoundError.name) + thrown.with.property('reason', 'getDocument.docUndefined') + thrown.with.deep.property('details', { name: SchoolClass.name, query: classId }) }) it('throws if the current user is not not owner and also not a member of the class', function () { - const classDoc = { _id: Random.id() } + const classDoc = mockClassDoc({}, SchoolClassCollection) const classId = classDoc._id const userId = Random.id() - - stub(SchoolClassCollection, 'findOne', () => classDoc) - const thrown = expect(() => byClass.call({ userId }, { classId })).to.throw('errors.publicationFailed') - thrown.with.property('reason', 'errors.permissionDenied') - thrown.with.property('details', classId) + const thrown = expect(() => byClass.call({ userId }, { classId })) + .to.throw(PermissionDeniedError.name) + thrown.with.property('reason', 'schoolClass.notMember') + thrown.with.deep.property('details', { userId, classId }) }) it('returns all users of a class if the user is owner', function () { const userId = Random.id() const studentDoc = { _id: Random.id(), username: Random.id(), presence: {}, services: {} } - Meteor.users.insert(studentDoc) + UsersCollection.insert(studentDoc) const classDoc = { _id: Random.id(), createdBy: userId, students: [studentDoc._id] } const classId = classDoc._id @@ -484,7 +507,7 @@ describe('Users', function () { it('returns all users of a class if the user is member', function () { const teacherId = Random.id() const studentDoc = { _id: Random.id(), username: Random.id(), presence: {}, services: {} } - Meteor.users.insert(studentDoc) + UsersCollection.insert(studentDoc) const classDoc = { _id: Random.id(), createdBy: teacherId, students: [studentDoc._id] } const classId = classDoc._id diff --git a/src/imports/contexts/system/accounts/users/usersByClass.js b/src/imports/contexts/system/accounts/users/usersByClass.js index 3795a69..9648ec4 100644 --- a/src/imports/contexts/system/accounts/users/usersByClass.js +++ b/src/imports/contexts/system/accounts/users/usersByClass.js @@ -1,29 +1,24 @@ -import { Meteor } from 'meteor/meteor' -import { getCollection } from '../../../../api/utils/getCollection' +import { getUsersCollection } from '../../../../api/utils/getUsersCollection' +import { createDocGetter } from '../../../../api/utils/document/createDocGetter' +import { PermissionDeniedError } from '../../../../api/errors/types/PermissionDeniedError' export const usersByClass = function () { import { SchoolClass } from '../../../classroom/schoolclass/SchoolClass' + const getClassDoc = createDocGetter({ name: SchoolClass.name }) + // run phase return function usersByClass ({ classId, skip }) { - const classDoc = getCollection(SchoolClass.name).findOne(classId) + const { userId } = this + const classDoc = getClassDoc(classId) - if (!classDoc) { - throw new Meteor.Error('usersByClass.failed', 'errors.docNotFound', classId) + if (!SchoolClass.helpers.isMember({ classDoc, userId })) { + throw new PermissionDeniedError(SchoolClass.errors.notMember, { userId, classId }) } const allStudents = (classDoc.students || []) const allTeachers = (classDoc.teachers || []) const allUsers = allStudents.concat(allTeachers) - - const { userId } = this - const isOwner = userId && (classDoc.createdBy === userId) - const isMember = !isOwner && allUsers.includes(userId) - - if (!isOwner && !isMember) { - throw new Meteor.Error('usersByClass.failed', 'errors.permissionDenied', classId) - } - const projection = { fields: { emails: 0, @@ -38,6 +33,6 @@ export const usersByClass = function () { query._id.$nin = skip } - return Meteor.users.find(query, projection) + return getUsersCollection().find(query, projection) } } diff --git a/src/imports/contexts/tasks/results/methods/getAllTaskResultsByTask.js b/src/imports/contexts/tasks/results/methods/getAllTaskResultsByTask.js index ab53cf9..fdea43a 100644 --- a/src/imports/contexts/tasks/results/methods/getAllTaskResultsByTask.js +++ b/src/imports/contexts/tasks/results/methods/getAllTaskResultsByTask.js @@ -4,6 +4,7 @@ import { PermissionDeniedError } from '../../../../api/errors/types/PermissionDe import { createDocGetter } from '../../../../api/utils/document/createDocGetter' import { getCollection } from '../../../../api/utils/getCollection' import { TaskResults } from '../TaskResults' +import { LessonHelpers } from '../../../classroom/lessons/LessonHelpers' const getLessonDoc = createDocGetter(Lesson) @@ -18,7 +19,7 @@ export const getAllTaskResultsByTask = function ({ lessonId, taskId }) { const lessonDoc = getLessonDoc({ _id: lessonId }) const isTeacher = lessonDoc.createdBy === userId - if (!isTeacher && !Lesson.helpers.isMemberOfLesson({ userId, lessonId })) { + if (!isTeacher && !LessonHelpers.isMemberOfLesson({ userId, lessonId })) { throw new PermissionDeniedError(SchoolClass.errors.notMember) } diff --git a/src/imports/contexts/tasks/results/methods/getAllTasksByItem.js b/src/imports/contexts/tasks/results/methods/getAllTasksByItem.js index 0da7a9c..0474bf7 100644 --- a/src/imports/contexts/tasks/results/methods/getAllTasksByItem.js +++ b/src/imports/contexts/tasks/results/methods/getAllTasksByItem.js @@ -1,9 +1,9 @@ import { SchoolClass } from '../../../classroom/schoolclass/SchoolClass' -import { Lesson } from '../../../classroom/lessons/Lesson' import { TaskResults } from '../TaskResults' import { PermissionDeniedError } from '../../../../api/errors/types/PermissionDeniedError' import { userIsAdmin } from '../../../../api/accounts/admin/userIsAdmin' import { getCollection } from '../../../../api/utils/getCollection' +import { LessonHelpers } from '../../../classroom/lessons/LessonHelpers' /** * Creates a query for all given references that contain the combination of lessonId, taskId and itemId. @@ -18,7 +18,7 @@ export const getAllTasksByItem = function run ({ references }) { const query = { $or: [] } references.forEach(({ lessonId, taskId, itemId }) => { - if (!userIsAdmin(userId) && !Lesson.helpers.isTeacher({ userId, lessonId })) { + if (!userIsAdmin(userId) && !LessonHelpers.isTeacher({ userId, lessonId })) { throw new PermissionDeniedError(SchoolClass.errors.notMember) } diff --git a/src/imports/contexts/tasks/results/methods/saveTaskResult.js b/src/imports/contexts/tasks/results/methods/saveTaskResult.js index e864275..6061911 100644 --- a/src/imports/contexts/tasks/results/methods/saveTaskResult.js +++ b/src/imports/contexts/tasks/results/methods/saveTaskResult.js @@ -8,6 +8,7 @@ import { Task } from '../../../curriculum/curriculum/task/Task' import { Group } from '../../../classroom/group/Group' import { getCollection } from '../../../../api/utils/getCollection' import { TaskResults } from '../TaskResults' +import { LessonHelpers } from '../../../classroom/lessons/LessonHelpers' const getLessonDoc = createDocGetter(Lesson) const checkTask = createDocGetter(Task) @@ -25,7 +26,7 @@ const getGroupDoc = createDocGetter(Group) export const saveTaskResult = function ({ lessonId, taskId, itemId, groupId, groupMode, response }) { const { userId } = this - if (!Lesson.helpers.isMemberOfLesson({ userId, lessonId })) { + if (!LessonHelpers.isMemberOfLesson({ userId, lessonId })) { throw new Meteor.Error('errors.permissionDenied', SchoolClass.errors.notMember) } @@ -47,7 +48,7 @@ export const saveTaskResult = function ({ lessonId, taskId, itemId, groupId, gro } // check if we can even edit the task - if (!Lesson.helpers.taskIsEditable({ lessonDoc, taskId, groupDoc })) { + if (!LessonHelpers.taskIsEditable({ lessonDoc, taskId, groupDoc })) { throw new Meteor.Error('errors.permissionDenied', TaskResults.errors.notEditable) } diff --git a/src/imports/contexts/tasks/results/tests/TaskResults.tests.js b/src/imports/contexts/tasks/results/tests/TaskResults.tests.js index d8a3b88..1f4b140 100644 --- a/src/imports/contexts/tasks/results/tests/TaskResults.tests.js +++ b/src/imports/contexts/tasks/results/tests/TaskResults.tests.js @@ -4,37 +4,59 @@ import { Random } from 'meteor/random' import { TaskResults } from '../TaskResults' import { Lesson } from '../../../classroom/lessons/Lesson' import { SchoolClass } from '../../../classroom/schoolclass/SchoolClass' -import { mockCollection } from '../../../../../tests/testutils/mockCollection' +import { + clearAllCollections, + mockCollections, + restoreAllCollections +} from '../../../../../tests/testutils/mockCollection' import { LessonStates } from '../../../classroom/lessons/LessonStates' import { DocNotFoundError } from '../../../../api/errors/types/DocNotFoundError' import { restoreAll, stub } from '../../../../../tests/testutils/stub' import { expect } from 'chai' import { Task } from '../../../curriculum/curriculum/task/Task' import { LessonErrors } from '../../../classroom/lessons/LessonErrors' - -const LessonCollection = mockCollection(Lesson) -const TaskCollection = mockCollection(Task, { noSchema: true }) -const TaskResultCollection = mockCollection(TaskResults) +import { LessonHelpers } from '../../../classroom/lessons/LessonHelpers' describe(TaskResults.name, function () { + let LessonCollection + let TaskCollection + let TaskResultCollection + + before(function () { + [LessonCollection, TaskCollection, TaskResultCollection] = mockCollections(Lesson, [Task, { noSchema: true }], TaskResults) + }) + + afterEach(function () { + restoreAll() + clearAllCollections() + }) + + after(function () { + restoreAllCollections() + }) + describe('methods', function () { describe(TaskResults.methods.saveTask.name, function () { - afterEach(function () { - TaskResultCollection.remove({}) - restoreAll() - }) const save = TaskResults.methods.saveTask.run + it('throws if the lesson does not exists', function () { const userId = Random.id() const createDoc = { lessonId: Random.id() } stub(Meteor.users, 'findOne', () => ({ _id: userId })) - expect(() => save.call({ userId }, createDoc)).to.throw(DocNotFoundError.name, createDoc.lessonId) + + const thrown = expect(() => save.call({ userId }, createDoc)) + .to.throw(DocNotFoundError.name) + thrown.with.property('reason', 'getDocument.docUndefined') + thrown.with.deep.property('details', { + name: Lesson.name, + query: createDoc.lessonId + }) }) it('throws if the lesson is not running', function () { const taskId = Random.id() const createDoc = { lessonId: Random.id(), taskId, itemId: Random.id(), response: [Random.id()] } - stub(Lesson.helpers, 'isMemberOfLesson', () => true) + stub(LessonHelpers, 'isMemberOfLesson', () => true) stub(LessonCollection, 'findOne', () => ({ _id: createDoc.lessonId, visibleStudent: [taskId] })) stub(TaskCollection, 'findOne', () => ({ _id: createDoc.taskId })) stub(LessonStates, 'isRunning', () => false) @@ -43,7 +65,7 @@ describe(TaskResults.name, function () { }) it('throws if the task is not editable', function () { const createDoc = { lessonId: Random.id(), taskId: Random.id(), itemId: Random.id(), response: [Random.id()] } - stub(Lesson.helpers, 'isMemberOfLesson', () => true) + stub(LessonHelpers, 'isMemberOfLesson', () => true) stub(LessonCollection, 'findOne', () => ({ _id: createDoc.lessonId })) stub(TaskCollection, 'findOne', () => ({ _id: createDoc.taskId })) stub(LessonStates, 'isRunning', () => false) @@ -52,22 +74,27 @@ describe(TaskResults.name, function () { }) it('throws if the task does not exists', function () { const createDoc = { taskId: Random.id() } - stub(Lesson.helpers, 'isMemberOfLesson', () => true) + stub(LessonHelpers, 'isMemberOfLesson', () => true) expect(() => save(createDoc)).to.throw(DocNotFoundError.name, createDoc.taskId) }) it('throws if not member of the lesson', function () { - stub(Lesson.helpers, 'isMemberOfLesson', () => false) + stub(LessonHelpers, 'isMemberOfLesson', () => false) expect(() => save({})).to.throw('errors.permissionDenied', SchoolClass.errors.notMember) }) it('creates a new response document if none exists for the given item', function () { const createDoc = { lessonId: Random.id(), taskId: Random.id(), itemId: Random.id(), response: [Random.id()] } - stub(Lesson.helpers, 'isMemberOfLesson', () => true) - stub(Lesson.helpers, 'taskIsEditable', () => true) + stub(LessonHelpers, 'isMemberOfLesson', () => true) + stub(LessonHelpers, 'taskIsEditable', () => true) stub(TaskCollection, 'findOne', () => ({ _id: createDoc.taskId })) stub(LessonCollection, 'findOne', () => ({ _id: createDoc.lessonId })) stub(LessonStates, 'isRunning', () => true) + expect(TaskResultCollection.find().count()).to.equal(0) + const docId = save(createDoc) + expect(docId).to.be.a('string') + expect(TaskResultCollection.find().count()).to.equal(1) + const resultDoc = TaskResultCollection.findOne(docId) delete resultDoc._id @@ -75,11 +102,11 @@ describe(TaskResults.name, function () { }) it('updates the response document if one exists already for the given item', function () { const createDoc = { lessonId: Random.id(), taskId: Random.id(), itemId: Random.id(), response: [Random.id()] } - stub(Lesson.helpers, 'isMemberOfLesson', () => true) + stub(LessonHelpers, 'isMemberOfLesson', () => true) stub(TaskCollection, 'findOne', () => ({ _id: createDoc.taskId })) stub(LessonCollection, 'findOne', () => ({ _id: createDoc.lessonId })) stub(LessonStates, 'isRunning', () => true) - stub(Lesson.helpers, 'taskIsEditable', () => true) + stub(LessonHelpers, 'taskIsEditable', () => true) const docId = TaskResultCollection.insert(createDoc) const updateDoc = Object.assign({}, createDoc, { response: [Random.id()] }) diff --git a/src/imports/contexts/tasks/results/tests/TaskWorkingState.tests.js b/src/imports/contexts/tasks/results/tests/TaskWorkingState.tests.js index 369cd8a..6640fde 100644 --- a/src/imports/contexts/tasks/results/tests/TaskWorkingState.tests.js +++ b/src/imports/contexts/tasks/results/tests/TaskWorkingState.tests.js @@ -3,21 +3,37 @@ import { Random } from 'meteor/random' import { TaskWorkingState } from '../../state/TaskWorkingState' import { LessonStates } from '../../../classroom/lessons/LessonStates' import { restoreAll } from '../../../../../tests/testutils/stub' -import { mockCollection } from '../../../../../tests/testutils/mockCollection' +import { + clearAllCollections, + mockCollections, + restoreAllCollections +} from '../../../../../tests/testutils/mockCollection' import { checkClass, checkLesson, stubStudentDocs, stubTaskDoc } from '../../../../../tests/testutils/doc/stubDocs' import { expect } from 'chai' import { Task } from '../../../curriculum/curriculum/task/Task' import { LessonErrors } from '../../../classroom/lessons/LessonErrors' - -const TaskWorkingStateCollection = mockCollection(TaskWorkingState) +import { Lesson } from '../../../classroom/lessons/Lesson' +import { Users } from '../../../system/accounts/users/User' +import { SchoolClass } from '../../../classroom/schoolclass/SchoolClass' +import { DocNotFoundError } from '../../../../api/errors/types/DocNotFoundError' describe(TaskWorkingState.name, function () { - describe('methods', function () { - afterEach(function () { - restoreAll() - TaskWorkingStateCollection.remove({}) - }) + let TaskWorkingStateCollection + + before(function () { + [TaskWorkingStateCollection] = mockCollections(TaskWorkingState, Lesson, Users, SchoolClass, Task) + }) + afterEach(function () { + restoreAll() + clearAllCollections() + }) + + after(function () { + restoreAllCollections() + }) + + describe('methods', function () { describe(TaskWorkingState.methods.saveState.name, function () { const saveState = TaskWorkingState.methods.saveState.run @@ -26,8 +42,9 @@ describe(TaskWorkingState.name, function () { it('throws if the lesson does not exists', function () { const lessonId = Random.id() - expect(() => saveState({ lessonId })).to.throw('docNotFound') - expect(() => saveState({ lessonId })).to.throw(lessonId) + const thrown = expect(() => saveState({ lessonId })).to.throw(DocNotFoundError.name) + thrown.with.property('reason', 'getDocument.docUndefined') + thrown.with.deep.property('details', { name: Lesson.name, query: lessonId }) }) it('throws if the lesson is not running', function () { const { lessonDoc, userId } = stubStudentDocs() diff --git a/src/imports/contexts/tasks/state/methods/byLesson.js b/src/imports/contexts/tasks/state/methods/byLesson.js index a0f06bb..a44512d 100644 --- a/src/imports/contexts/tasks/state/methods/byLesson.js +++ b/src/imports/contexts/tasks/state/methods/byLesson.js @@ -1,11 +1,11 @@ -import { Lesson } from '../../../classroom/lessons/Lesson' import { getCollection } from '../../../../api/utils/getCollection' import { Meteor } from 'meteor/meteor' import { TaskWorkingState } from '../TaskWorkingState' +import { LessonHelpers } from '../../../classroom/lessons/LessonHelpers' export const taskWorkingStateByLesson = function ({ lessonId }) { const { userId } = this - if (!Lesson.helpers.isMemberOfLesson({ userId, lessonId })) { + if (!LessonHelpers.isMemberOfLesson({ userId, lessonId })) { throw new Meteor.Error('errors.noClassMember') } return getCollection(TaskWorkingState.name).find({ lessonId }) diff --git a/src/imports/contexts/tasks/state/methods/saveTaskWorkingState.js b/src/imports/contexts/tasks/state/methods/saveTaskWorkingState.js index 47fbe0d..308f435 100644 --- a/src/imports/contexts/tasks/state/methods/saveTaskWorkingState.js +++ b/src/imports/contexts/tasks/state/methods/saveTaskWorkingState.js @@ -2,13 +2,13 @@ import { Meteor } from 'meteor/meteor' import { TaskWorkingState } from '../TaskWorkingState' import { Task } from '../../../curriculum/curriculum/task/Task' import { LessonStates } from '../../../classroom/lessons/LessonStates' -import { Lesson } from '../../../classroom/lessons/Lesson' import { Group } from '../../../classroom/group/Group' import { Features } from '../../../../api/config/Features' import { LessonErrors } from '../../../classroom/lessons/LessonErrors' import { createDocGetter } from '../../../../api/utils/document/createDocGetter' import { ensureDocumentExists } from '../../../../api/utils/document/ensureDocumentExists' import { getCollection } from '../../../../api/utils/getCollection' +import { LessonHelpers } from '../../../classroom/lessons/LessonHelpers' const checkTaskDoc = createDocGetter({ name: Task.name, optional: false }) const getGroupDoc = createDocGetter({ name: Group.name, optional: false }) @@ -25,7 +25,7 @@ const getGroupDoc = createDocGetter({ name: Group.name, optional: false }) */ export const saveTaskWorkingState = function ({ lessonId, taskId, groupId, complete, page, progress }) { const { userId } = this - const { lessonDoc } = Lesson.helpers.docsForStudent({ userId, lessonId }) + const { lessonDoc } = LessonHelpers.docsForStudent({ userId, lessonId }) ensureDocumentExists({ document: lessonDoc, diff --git a/src/imports/infrastructure/pipelines/server/buildPipeline.tests.js b/src/imports/infrastructure/pipelines/server/buildPipeline.tests.js index 33c09c3..41251bf 100644 --- a/src/imports/infrastructure/pipelines/server/buildPipeline.tests.js +++ b/src/imports/infrastructure/pipelines/server/buildPipeline.tests.js @@ -63,7 +63,7 @@ describe(buildPipeline.name, function () { const method = products.methods[0] expect(method._execute({ userId }, { title })).to.equal(true) - expect(() => method._execute({ userId }, {})).to.throw('form.validation.required') + expect(() => method._execute({ userId }, {})).to.throw('Title is required.') }) it('it creates publications if defined', function () { const options = { diff --git a/src/tests/main.js b/src/tests/main.js index 952609a..c58d255 100644 --- a/src/tests/main.js +++ b/src/tests/main.js @@ -1,4 +1,9 @@ import { onClientExec, onServerExec } from '../imports/api/utils/archUtils' +import { initLanguage } from '../imports/api/language/initLanguage' + +before(async function () { + await initLanguage('en') +}) onClientExec(function () { import './client/main' diff --git a/src/tests/testutils/doc/createCodeDoc.js b/src/tests/testutils/doc/createCodeDoc.js index 5b801a3..16aaa13 100644 --- a/src/tests/testutils/doc/createCodeDoc.js +++ b/src/tests/testutils/doc/createCodeDoc.js @@ -1,7 +1,7 @@ import { UserUtils } from '../../../imports/contexts/system/accounts/users/UserUtils' import { Random } from 'meteor/random' -export const createCodeDoc = ({ maxUsers = 1, registeredUsers = [], institution = 'Super School', expires = 1, role = UserUtils.roles.student, firstName = 'John', lastName = 'Doe', email = `${Random.id()}@example.com`, classId = Random.id() } = {}) => ({ +export const createCodeDoc = ({ maxUsers = 1, registeredUsers = [], institution = 'Super School', expires = 1, role = UserUtils.roles.student, firstName = 'John', lastName = 'Doe', email = `${Random.id()}@example.com`, classId = Random.id(), invalid = false } = {}) => ({ _id: Random.id(), createdAt: new Date(), code: Random.id(4), @@ -13,5 +13,6 @@ export const createCodeDoc = ({ maxUsers = 1, registeredUsers = [], institution institution: institution, registeredUsers: registeredUsers, maxUsers: maxUsers, - classId: classId + classId: classId, + invalid: invalid }) diff --git a/src/tests/testutils/doc/mockClassDoc.js b/src/tests/testutils/doc/mockClassDoc.js new file mode 100644 index 0000000..a55a030 --- /dev/null +++ b/src/tests/testutils/doc/mockClassDoc.js @@ -0,0 +1,18 @@ +import { Random } from 'meteor/random' + +export const mockClassDoc = (options, collection) => { + const classDoc = { + _id: options._id ?? Random.id(), + title: options.title ?? Random.id(), + createdBy: options.createdBy, + timeFrame: options.timeFrame, + teachers: options.teacher, + students: options.students + } + + if (collection) { + collection.insert(classDoc) + } + + return classDoc +} diff --git a/src/tests/testutils/doc/mockPhaseDoc.js b/src/tests/testutils/doc/mockPhaseDoc.js new file mode 100644 index 0000000..0dc9e85 --- /dev/null +++ b/src/tests/testutils/doc/mockPhaseDoc.js @@ -0,0 +1,37 @@ +import { Random } from 'meteor/random' + +/** + * + * @param options {object} + * @param options._id {string=} + * @param options.title {string=} + * @param options.createdBy {string=} + * @param options.period {number=} + * @param options.unit {string} + * @param collection {Mongo.Collection=} + * @return {object} + */ +export const mockPhaseDoc = (options = {}, collection) => { + const phaseDoc = { + _id: options._id ?? Random.id(), + createdBy: options.createdBy, + title: options.title ?? Random.id(), + period: options.period ?? 5, + unit: options.unit, + plot: options.plot, + socialState: options.socialState, + method: options.method, + references: options.references, + notes: options.notes + } + + if (options._master) { + phaseDoc._master = options._master + } + + if (collection) { + collection.insert(phaseDoc) + } + + return phaseDoc +} \ No newline at end of file diff --git a/src/tests/testutils/doc/mockUnitDoc.js b/src/tests/testutils/doc/mockUnitDoc.js new file mode 100644 index 0000000..904d27b --- /dev/null +++ b/src/tests/testutils/doc/mockUnitDoc.js @@ -0,0 +1,61 @@ +import { Random } from 'meteor/random' + +/** + * + * @param options {object} + * @param options._master {boolean=} + * @param options._id {string=} + * @param options.title {string=} + * @param options.createdBy {string=} + * @param options.description {string=} + * @param options.pocket {string=} + * @param options.index {number=} + * @param options.period {string=} + * @param options.requirements {string=} + * @param options.dimensions {[string]=} + * @param options.objectives {[string]=} + * @param options.links {[string]=} + * @param options.embeds {[string]=} + * @param options.images {[string]=} + * @param options.audio {[string]=} + * @param options.documents {[string]=} + * @param options.videos {[string]=} + * @param options.literature {[string]=} + * @param options.tasks {[string]=} + * @param options.phases {[string]=} + * @param collection {Mongo.Collection=} + * @returns {object} + */ +export const mockUnitDoc = (options = {}, collection) => { + const unitDoc = { + _id: options._id ?? Random.id(), + title: options.title ?? Random.id(6), + createdBy: options.createdBy, + description: options.description, + pocket: options.pocket ?? Random.id(), + index: options.index ?? 0, + dimensions: options.dimensions, + period: options.period ?? 5, + objectives: options.objectives, + requirements: options.requirements, + links: options.links, + embeds: options.embeds, + images: options.images, + audio: options.audio, + documents: options.documents, + videos: options.videos, + tasks: options.tasks, + literature: options.literature, + phases: options.phases, + } + + if (options._master) { + unitDoc._master = options._master + } + + if (collection) { + collection.insert(unitDoc) + } + + return unitDoc +} diff --git a/src/tests/testutils/doc/stubDocs.js b/src/tests/testutils/doc/stubDocs.js index fa746fc..bdfa9e7 100644 --- a/src/tests/testutils/doc/stubDocs.js +++ b/src/tests/testutils/doc/stubDocs.js @@ -7,20 +7,18 @@ import { Lesson } from '../../../imports/contexts/classroom/lessons/Lesson' import { LessonStates } from '../../../imports/contexts/classroom/lessons/LessonStates' import { Unit } from '../../../imports/contexts/curriculum/curriculum/unit/Unit' import { Task } from '../../../imports/contexts/curriculum/curriculum/task/Task' -import { mockCollection } from '../mockCollection' import { stub } from '../stub' import { expect } from 'chai' +import { getCollection } from '../../../imports/api/utils/getCollection' +import { Users } from '../../../imports/contexts/system/accounts/users/User' +import { DocNotFoundError } from '../../../imports/api/errors/types/DocNotFoundError' +import { getUsersCollection } from '../../../imports/api/utils/getUsersCollection' -const LessonCollection = mockCollection(Lesson, { noSchema: true }) -const UnitCollection = mockCollection(Unit, { noSchema: true }) -const SchoolClassCollection = mockCollection(SchoolClass, { noSchema: true }) -const TaskCollection = mockCollection(Task, { noSchema: true }) - -export const stubClassDoc = classDoc => stub(SchoolClassCollection, 'findOne', () => classDoc) -export const stubLessonDoc = lessonDoc => stub(LessonCollection, 'findOne', () => lessonDoc) -export const stubUnitDoc = unitDoc => stub(UnitCollection, 'findOne', () => unitDoc) -export const stubUserDoc = ({ userId }) => stub(Meteor.users, 'findOne', () => ({ _id: userId })) -export const stubTaskDoc = taskDoc => stub(TaskCollection, 'findOne', () => taskDoc) +export const stubClassDoc = classDoc => stub(getCollection(SchoolClass.name), 'findOne', () => classDoc) +export const stubLessonDoc = lessonDoc => stub(getCollection(Lesson.name), 'findOne', () => lessonDoc) +export const stubUnitDoc = unitDoc => stub(getCollection(Unit.name), 'findOne', () => unitDoc) +export const stubUserDoc = ({ userId }) => stub(getCollection(Users.name), 'findOne', () => ({ _id: userId })) +export const stubTaskDoc = taskDoc => stub(getCollection(Task.name), 'findOne', () => taskDoc) export const stubAdmin = value => stub(UserUtils, 'isAdmin', () => value) export const checkLesson = (fct, stateFct, fields = { lessonId: '_id' }) => { @@ -30,18 +28,23 @@ export const checkLesson = (fct, stateFct, fields = { lessonId: '_id' }) => { it('throws if the given lesson does not exists', function () { const lessonId = Random.id() - expect(() => fct.call(environment, { [lessonIdField]: lessonId })).to.throw('docNotFound') - expect(() => fct.call(environment, { [lessonIdField]: lessonId })).to.throw(lessonId) + const expectLesson = expect(() => fct.call(environment, { [lessonIdField]: lessonId })) + .to.throw(DocNotFoundError.name) + expectLesson.with.property('reason', 'getDocument.docUndefined') + expectLesson + .with.property('details') + .with.property('query', lessonId) }) if (stateFct) { it(`throws if ${stateFct.name} is false`, function () { - const lessonId = Random.id() - const classId = Random.id() - const lessonDoc = { _id: lessonId, classId, createdBy: userId } - const classDoc = { _id: classId, createdBy: userId, teachers: [userId], students: [userId] } - stubUserDoc({ userId }) - stubLessonDoc(lessonDoc) - stubClassDoc(classDoc) + const classId = getCollection(SchoolClass.name).insert({ + title: Random.id(), + createdBy: userId, + teachers: [userId], + students: [userId] + }) + const lessonId = getCollection(Lesson.name).insert({ classId, createdBy: userId, unit: Random.id() }) + getUsersCollection().insert({ _id: userId, username: Random.id() }) stubAdmin(false) stub(LessonStates, stateFct.name, () => false) expect(() => fct.call(environment, { [lessonIdField]: lessonId })).to.throw('unexpectedState') @@ -60,8 +63,11 @@ export const checkClass = (fct, { isTeacher = true, isStudent = false } = {}, fi const lessonDoc = { _id: lessonId, classId } stubLessonDoc(lessonDoc) stubUserDoc(environment) - expect(() => fct.call(environment, { [lessonIdField]: lessonId })).to.throw('docNotFound') - expect(() => fct.call(environment, { [lessonIdField]: lessonId })).to.throw(classId) + const expectError = expect(() => fct.call(environment, { [lessonIdField]: lessonId })).to.throw(DocNotFoundError.name) + expectError.with.property('reason', 'getDocument.docUndefined') + expectError + .with.property('details') + .with.property('query', classId) }) if (isTeacher) { @@ -95,16 +101,20 @@ export const checkClass = (fct, { isTeacher = true, isStudent = false } = {}, fi } } -export const stubTeacherDocs = (lessonMutator) => { - const userId = Random.id() - const lessonId = Random.id() - const classId = Random.id() - const lessonDoc = Object.assign({}, { _id: lessonId, classId, createdBy: userId }, lessonMutator) - const classDoc = { _id: classId, createdBy: userId } +export const stubTeacherDocs = (lessonMutator = {}, { + classId = Random.id(), + userId = Random.id(), + lessonId = Random.id(), + isAdmin = false, + classTitle = Random.id(5), + unit = Random.id() +} = {}) => { + const lessonDoc = Object.assign({}, { _id: lessonId, classId, createdBy: userId, unit }, lessonMutator) + const classDoc = { _id: classId, createdBy: userId, title: classTitle } stubUserDoc({ userId }) stubLessonDoc(lessonDoc) stubClassDoc(classDoc) - stubAdmin(false) + stubAdmin(isAdmin) return { userId, lessonDoc, classDoc } } diff --git a/src/tests/testutils/exampleUser.js b/src/tests/testutils/exampleUser.js index f6b1cc2..2bd8f39 100644 --- a/src/tests/testutils/exampleUser.js +++ b/src/tests/testutils/exampleUser.js @@ -1,6 +1,13 @@ import { Random } from 'meteor/random' -export const exampleUser = ({ _id = Random.id(), username = Random.id(), email = `${Random.id()}@test.tld`, firstName, lastName, institution = Random.id() } = {}) => { +export const exampleUser = ({ + _id = Random.id(), + username = Random.id(), + email = `${Random.id()}@test.tld`, + firstName, + lastName, + institution = Random.id() +} = {}) => { return { _id, username, diff --git a/src/tests/testutils/mockCollection.js b/src/tests/testutils/mockCollection.js index dce14d6..f7de33d 100644 --- a/src/tests/testutils/mockCollection.js +++ b/src/tests/testutils/mockCollection.js @@ -1,6 +1,8 @@ import { Mongo } from 'meteor/mongo' import { Schema } from '../../imports/api/schema/Schema' import Collection2 from 'meteor/aldeed:collection2' +import { Random } from 'meteor/random' +import { FilesCollection } from 'meteor/ostrio:files' // XXX: backwards compat for pre 4.0 collection2 if (Collection2 && 'function' === typeof Collection2.load) { @@ -8,12 +10,16 @@ if (Collection2 && 'function' === typeof Collection2.load) { } const originals = new Map() -Mongo.Collection.get = name => originals.get(name) + +Mongo.Collection.get = (name) => { + return originals.get(name) +} export const mockCollection = ({ name, schema } = {}, { noSchema = false, noDefaults = false, - override = false + override = false, + isFilesCollection = false } = {}) => { let collection = Mongo.Collection.get(name) @@ -24,11 +30,17 @@ export const mockCollection = ({ name, schema } = {}, { if (collection) { return collection } + + else if (isFilesCollection) { + const filesCollection = new FilesCollection({ collectionName: Random.id() }) + collection = filesCollection.collection + } else { collection = new Mongo.Collection(null) + collection._name = `${name}-mocked` } - if (!noSchema) { + if (schema && noSchema !== true) { const schemaInstance = noDefaults ? Schema.create(schema) : Schema.withDefault(schema) @@ -54,7 +66,7 @@ export const restoreCollection = ({ name }) => { } export const restoreAllCollections = () => { - originals.forEach(collection => collection.remove({})) + clearAllCollections() originals.clear() } @@ -66,3 +78,7 @@ const clearCollection = ({ name }) => { export const clearCollections = (...contexts) => { return contexts.map(c => clearCollection(c)) } + +export const clearAllCollections = () => { + originals.forEach(collection => collection.remove({})) +} \ No newline at end of file diff --git a/src/tests/testutils/stubUser.js b/src/tests/testutils/stubUser.js index b6eed06..8625ac7 100644 --- a/src/tests/testutils/stubUser.js +++ b/src/tests/testutils/stubUser.js @@ -1,14 +1,14 @@ -/* global Roles */ -import { Meteor } from 'meteor/meteor' +import { Roles } from 'meteor/alanning:roles' import { stub, restore, isStubbed } from './stub' -import StubCollections from 'meteor/hwillson:stub-collections' +import { getUsersCollection } from '../../imports/api/utils/getUsersCollection' -export const stubUser = function (userObj, userId, roles, group) { - const userDefined = typeof userObj !== 'undefined' - - if (userDefined) { +export const stubUser = function (userObj, userId, roles, institution) { + const userIsDefined = typeof userObj !== 'undefined' + const UsersCollection = getUsersCollection() + + if (userIsDefined) { if (userObj !== null) { - Meteor.users.upsert({ _id: userObj._id }, userObj) + UsersCollection.upsert({ _id: userObj._id }, { $set: { ...userObj } }) stub(Meteor, 'userId', () => userObj._id) } @@ -19,20 +19,18 @@ export const stubUser = function (userObj, userId, roles, group) { stub(Meteor, 'user', () => userObj) } - if (!userDefined && typeof userId !== 'undefined') { + if (!userIsDefined && typeof userId !== 'undefined') { stub(Meteor, 'user', () => userObj || null) stub(Meteor, 'userId', () => userId) } if (typeof roles !== 'undefined') { - StubCollections.add([Meteor.roles]) - StubCollections.stub() stub(Roles, 'userIsInRole', (id, role, domain) => { if (userObj) { - return id === userObj._id && roles.includes(role) && domain === group + return id === userObj._id && roles.includes(role) && domain === institution } else { - return id === userId && roles.includes(role) && domain === group + return id === userId && roles.includes(role) && domain === institution } }) } @@ -52,10 +50,6 @@ export const unstubUser = (user, userId) => { restore(Meteor, 'userId') } - if (_id) { - Meteor.users.remove(_id) - } - - StubCollections.restore() + getUsersCollection().remove(_id) restore(Roles, 'userIsInRole') } From f9c09eafd69a94c36253f5d18428c0a677bcb548 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Thu, 24 Nov 2022 15:07:12 +0100 Subject: [PATCH 07/24] fix(ui): add to hrefs with target the rel=noreferrer noopener atts --- src/imports/ui/components/download/downloadButton.html | 3 ++- src/imports/ui/pages/register/code/codeRegister.html | 4 ++-- src/imports/ui/pages/register/enroll/enrollAccount.html | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/imports/ui/components/download/downloadButton.html b/src/imports/ui/components/download/downloadButton.html index c20639d..fae7e5e 100644 --- a/src/imports/ui/components/download/downloadButton.html +++ b/src/imports/ui/components/download/downloadButton.html @@ -1,5 +1,6 @@