mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
[web] add endpoint for resolving a project for writes from tpds (#35408)
* [web] add endpoint for resolving a project for writes from tpds * [web] return historyId from new endpoint for resolving projects * [web] add zod validation to new endpoint for resolving projects GitOrigin-RevId: 0e607e56442117a4150a08373169c44b53dd4fdf
This commit is contained in:
@@ -146,7 +146,7 @@ const ProjectGetter = {
|
||||
async findUsersProjectsByName(userId, projectName) {
|
||||
const allProjects = await ProjectGetter.findAllUsersProjects(
|
||||
userId,
|
||||
'name archived trashed'
|
||||
'name archived trashed overleaf'
|
||||
)
|
||||
|
||||
const { owned, readAndWrite } = allProjects
|
||||
|
||||
@@ -11,6 +11,7 @@ import ProjectCreationHandler from '../Project/ProjectCreationHandler.mjs'
|
||||
import ProjectDetailsHandler from '../Project/ProjectDetailsHandler.mjs'
|
||||
import HttpErrorHandler from '../Errors/HttpErrorHandler.mjs'
|
||||
import TpdsQueueManager from './TpdsQueueManager.mjs'
|
||||
import { parseReq, z, zz } from '@overleaf/validation-tools'
|
||||
|
||||
async function createProject(req, res) {
|
||||
const { user_id: userId } = req.params
|
||||
@@ -30,6 +31,46 @@ async function createProject(req, res) {
|
||||
})
|
||||
}
|
||||
|
||||
const resolveProjectSchema = z.object({
|
||||
params: z.object({
|
||||
user_id: zz.objectId(),
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
projectId: zz.objectId(),
|
||||
})
|
||||
.or(
|
||||
z.object({
|
||||
projectName: z.string().min(1),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
// Resolve a project name (or id) to a project id, using the same
|
||||
// get-or-create semantics as mergeUpdate: a blank project is created when no
|
||||
// project matches the name, and duplicate names trigger the duplicate-name
|
||||
// handling before rejecting the request.
|
||||
async function resolveProject(req, res) {
|
||||
const {
|
||||
params: { user_id: userId },
|
||||
body: { projectId, projectName },
|
||||
} = parseReq(req, resolveProjectSchema)
|
||||
const project = await TpdsUpdateHandler.promises.getOrCreateProject(
|
||||
userId,
|
||||
projectId,
|
||||
projectName
|
||||
)
|
||||
if (project == null) {
|
||||
return res.json({ status: 'rejected' })
|
||||
}
|
||||
res.json({
|
||||
status: 'success',
|
||||
projectId: project._id.toString(),
|
||||
historyId: project.overleaf?.history?.id,
|
||||
otMigrationStage: project.overleaf?.history?.otMigrationStage ?? 0,
|
||||
})
|
||||
}
|
||||
|
||||
// mergeUpdate and deleteUpdate are used by Dropbox, where the project is only
|
||||
// passed as the name, as the first part of the file path. They have to check
|
||||
// the project exists, find it, and create it if not. They also ignore 'noisy'
|
||||
@@ -207,6 +248,7 @@ function splitPath(projectId, path) {
|
||||
|
||||
export default {
|
||||
createProject: expressify(createProject),
|
||||
resolveProject: expressify(resolveProject),
|
||||
mergeUpdate: expressify(mergeUpdate),
|
||||
deleteUpdate: expressify(deleteUpdate),
|
||||
updateFolder: expressify(updateFolder),
|
||||
|
||||
@@ -94,7 +94,7 @@ async function getOrCreateProject(userId, projectId, projectName) {
|
||||
async function findProjectByIdWithRWAccess(userId, projectId) {
|
||||
const allProjects = await ProjectGetter.promises.findAllUsersProjects(
|
||||
userId,
|
||||
'name archived trashed'
|
||||
'name archived trashed overleaf'
|
||||
)
|
||||
for (const projects of [allProjects.owned, allProjects.readAndWrite]) {
|
||||
for (const project of projects) {
|
||||
@@ -191,5 +191,6 @@ export default {
|
||||
newUpdate,
|
||||
deleteUpdate,
|
||||
createFolder,
|
||||
getOrCreateProject,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -980,6 +980,11 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
||||
AuthenticationController.requirePrivateApiAuth(),
|
||||
TpdsController.createProject
|
||||
)
|
||||
privateApiRouter.post(
|
||||
'/user/:user_id/project/resolve',
|
||||
AuthenticationController.requirePrivateApiAuth(),
|
||||
TpdsController.resolveProject
|
||||
)
|
||||
privateApiRouter.post(
|
||||
'/tpds/folder-update',
|
||||
AuthenticationController.requirePrivateApiAuth(),
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import ProjectGetter from '../../../app/src/Features/Project/ProjectGetter.mjs'
|
||||
import ProjectOptionsHandler from '../../../app/src/Features/Project/ProjectOptionsHandler.mjs'
|
||||
import ProjectRootDocManager from '../../../app/src/Features/Project/ProjectRootDocManager.mjs'
|
||||
import request from './helpers/request.js'
|
||||
import User from './helpers/User.mjs'
|
||||
|
||||
describe('TpdsUpdateTests', function () {
|
||||
beforeEach(function () {
|
||||
// Creating a project via TPDS schedules a root doc reset on a 30s timer,
|
||||
// which graceful shutdown waits for, exceeding the "after all" hook
|
||||
// timeout when stopping the app.
|
||||
this.setRootDocStub = sinon.stub(
|
||||
ProjectRootDocManager,
|
||||
'setRootDocAutomaticallyInBackground'
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
this.setRootDocStub.restore()
|
||||
})
|
||||
|
||||
beforeEach(function (done) {
|
||||
this.owner = new User()
|
||||
this.owner.login(error => {
|
||||
@@ -139,6 +156,261 @@ describe('TpdsUpdateTests', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolving a project by name', function () {
|
||||
it('should return the id of the matching project', function (done) {
|
||||
request(
|
||||
{
|
||||
method: 'POST',
|
||||
url: `/user/${this.owner._id}/project/resolve`,
|
||||
auth: {
|
||||
username: 'overleaf',
|
||||
password: 'password',
|
||||
sendImmediately: true,
|
||||
},
|
||||
json: { projectName: 'test-project' },
|
||||
},
|
||||
(error, response, body) => {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
expect(response.statusCode).to.equal(200)
|
||||
expect(body).to.deep.equal({
|
||||
status: 'success',
|
||||
projectId: this.projectId,
|
||||
historyId: 1,
|
||||
otMigrationStage: 0,
|
||||
})
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should create a project when the name does not match', function (done) {
|
||||
request(
|
||||
{
|
||||
method: 'POST',
|
||||
url: `/user/${this.owner._id}/project/resolve`,
|
||||
auth: {
|
||||
username: 'overleaf',
|
||||
password: 'password',
|
||||
sendImmediately: true,
|
||||
},
|
||||
json: { projectName: 'brand-new-project' },
|
||||
},
|
||||
(error, response, body) => {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
expect(response.statusCode).to.equal(200)
|
||||
expect(body.status).to.equal('success')
|
||||
expect(body.projectId).to.exist
|
||||
expect(body.projectId).to.not.equal(this.projectId)
|
||||
expect(body.otMigrationStage).to.equal(0)
|
||||
ProjectGetter.findAllUsersProjects(
|
||||
this.owner._id,
|
||||
'name',
|
||||
(err, projects) => {
|
||||
expect(err).to.not.exist
|
||||
const created = projects.owned.find(
|
||||
project => project._id.toString() === body.projectId
|
||||
)
|
||||
expect(created).to.exist
|
||||
expect(created.name).to.equal('brand-new-project')
|
||||
done()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the otMigrationStage of the matching project', function (done) {
|
||||
ProjectOptionsHandler.promises
|
||||
.setOTMigrationStage(this.projectId, 1)
|
||||
.then(() => {
|
||||
request(
|
||||
{
|
||||
method: 'POST',
|
||||
url: `/user/${this.owner._id}/project/resolve`,
|
||||
auth: {
|
||||
username: 'overleaf',
|
||||
password: 'password',
|
||||
sendImmediately: true,
|
||||
},
|
||||
json: { projectName: 'test-project' },
|
||||
},
|
||||
(error, response, body) => {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
expect(response.statusCode).to.equal(200)
|
||||
expect(body).to.deep.equal({
|
||||
status: 'success',
|
||||
projectId: this.projectId,
|
||||
historyId: 1,
|
||||
otMigrationStage: 1,
|
||||
})
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
.catch(done)
|
||||
})
|
||||
|
||||
it('should create a project when the user only has read-only access to the matching project', function (done) {
|
||||
this.reader = new User()
|
||||
this.reader.login(error => {
|
||||
expect(error).to.not.exist
|
||||
this.owner.addUserToProject(
|
||||
this.projectId,
|
||||
this.reader,
|
||||
'readOnly',
|
||||
error => {
|
||||
expect(error).to.not.exist
|
||||
request(
|
||||
{
|
||||
method: 'POST',
|
||||
url: `/user/${this.reader._id}/project/resolve`,
|
||||
auth: {
|
||||
username: 'overleaf',
|
||||
password: 'password',
|
||||
sendImmediately: true,
|
||||
},
|
||||
json: { projectName: 'test-project' },
|
||||
},
|
||||
(error, response, body) => {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
expect(response.statusCode).to.equal(200)
|
||||
expect(body.status).to.equal('success')
|
||||
expect(body.projectId).to.exist
|
||||
expect(body.projectId).to.not.equal(this.projectId)
|
||||
ProjectGetter.findAllUsersProjects(
|
||||
this.reader._id,
|
||||
'name',
|
||||
(err, projects) => {
|
||||
expect(err).to.not.exist
|
||||
const created = projects.owned.find(
|
||||
project => project._id.toString() === body.projectId
|
||||
)
|
||||
expect(created).to.exist
|
||||
expect(created.name).to.equal('test-project')
|
||||
done()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should reject when the project is archived', function (done) {
|
||||
this.owner.request(
|
||||
{
|
||||
url: `/Project/${this.projectId}/archive`,
|
||||
method: 'post',
|
||||
},
|
||||
(err, response, body) => {
|
||||
expect(err).to.not.exist
|
||||
request(
|
||||
{
|
||||
method: 'POST',
|
||||
url: `/user/${this.owner._id}/project/resolve`,
|
||||
auth: {
|
||||
username: 'overleaf',
|
||||
password: 'password',
|
||||
sendImmediately: true,
|
||||
},
|
||||
json: { projectName: 'test-project' },
|
||||
},
|
||||
(error, response, body) => {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
expect(response.statusCode).to.equal(200)
|
||||
expect(body).to.deep.equal({ status: 'rejected' })
|
||||
ProjectGetter.findAllUsersProjects(
|
||||
this.owner._id,
|
||||
'name',
|
||||
(err, projects) => {
|
||||
expect(err).to.not.exist
|
||||
expect(projects.owned.length).to.equal(1)
|
||||
done()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolving a project by id', function () {
|
||||
it('should return the id of the matching project', function (done) {
|
||||
request(
|
||||
{
|
||||
method: 'POST',
|
||||
url: `/user/${this.owner._id}/project/resolve`,
|
||||
auth: {
|
||||
username: 'overleaf',
|
||||
password: 'password',
|
||||
sendImmediately: true,
|
||||
},
|
||||
json: { projectId: this.projectId },
|
||||
},
|
||||
(error, response, body) => {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
expect(response.statusCode).to.equal(200)
|
||||
expect(body).to.deep.equal({
|
||||
status: 'success',
|
||||
projectId: this.projectId,
|
||||
historyId: 1,
|
||||
otMigrationStage: 0,
|
||||
})
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should reject when the user only has read-only access', function (done) {
|
||||
this.reader = new User()
|
||||
this.reader.login(error => {
|
||||
expect(error).to.not.exist
|
||||
this.owner.addUserToProject(
|
||||
this.projectId,
|
||||
this.reader,
|
||||
'readOnly',
|
||||
error => {
|
||||
expect(error).to.not.exist
|
||||
request(
|
||||
{
|
||||
method: 'POST',
|
||||
url: `/user/${this.reader._id}/project/resolve`,
|
||||
auth: {
|
||||
username: 'overleaf',
|
||||
password: 'password',
|
||||
sendImmediately: true,
|
||||
},
|
||||
json: { projectId: this.projectId },
|
||||
},
|
||||
(error, response, body) => {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
expect(response.statusCode).to.equal(200)
|
||||
expect(body).to.deep.equal({ status: 'rejected' })
|
||||
done()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('update when the project is archived', function () {
|
||||
beforeEach(function (done) {
|
||||
this.owner.request(
|
||||
|
||||
@@ -4,6 +4,7 @@ import sinon from 'sinon'
|
||||
import Errors from '../../../../app/src/Features/Errors/Errors.js'
|
||||
import MockResponse from '../helpers/MockResponse.mjs'
|
||||
import MockRequest from '../helpers/MockRequest.mjs'
|
||||
import { asZodError } from '@overleaf/validation-tools/testUtils.js'
|
||||
|
||||
const ObjectId = mongodb.ObjectId
|
||||
|
||||
@@ -19,11 +20,16 @@ describe('TpdsController', function () {
|
||||
entityType: 'doc',
|
||||
rev: 2,
|
||||
}
|
||||
ctx.resolvedProject = {
|
||||
_id: new ObjectId(),
|
||||
overleaf: { history: { id: 42, otMigrationStage: 1 } },
|
||||
}
|
||||
ctx.TpdsUpdateHandler = {
|
||||
promises: {
|
||||
newUpdate: sinon.stub().resolves(ctx.metadata),
|
||||
deleteUpdate: sinon.stub().resolves(ctx.metadata.entityId),
|
||||
createFolder: sinon.stub().resolves(),
|
||||
getOrCreateProject: sinon.stub().resolves(ctx.resolvedProject),
|
||||
},
|
||||
}
|
||||
ctx.UpdateMerger = {
|
||||
@@ -118,7 +124,7 @@ describe('TpdsController', function () {
|
||||
|
||||
ctx.TpdsController = (await import(MODULE_PATH)).default
|
||||
|
||||
ctx.user_id = 'dsad29jlkjas'
|
||||
ctx.user_id = new ObjectId().toString()
|
||||
})
|
||||
|
||||
describe('creating a project', function () {
|
||||
@@ -151,6 +157,129 @@ describe('TpdsController', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolving a project', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.req = {
|
||||
params: { user_id: ctx.user_id },
|
||||
body: {},
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw without any input', async function (ctx) {
|
||||
const next = sinon.stub()
|
||||
await ctx.TpdsController.resolveProject(ctx.req, {}, next)
|
||||
expect(next).to.have.been.calledWithMatch({
|
||||
name: 'InvalidRequestError',
|
||||
zodError: asZodError({
|
||||
code: 'invalid_union',
|
||||
errors: [
|
||||
[
|
||||
{
|
||||
expected: 'string',
|
||||
code: 'invalid_type',
|
||||
path: ['projectId'],
|
||||
message: 'Invalid input: expected string, received undefined',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
expected: 'string',
|
||||
code: 'invalid_type',
|
||||
path: ['projectName'],
|
||||
message: 'Invalid input: expected string, received undefined',
|
||||
},
|
||||
],
|
||||
],
|
||||
path: ['body'],
|
||||
message: 'Invalid input',
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
it('should resolve by name', async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.req.body = { projectName: 'projectName' }
|
||||
const res = {
|
||||
json: payload => {
|
||||
expect(payload).to.deep.equal({
|
||||
status: 'success',
|
||||
projectId: ctx.resolvedProject._id.toString(),
|
||||
historyId: 42,
|
||||
otMigrationStage: 1,
|
||||
})
|
||||
ctx.TpdsUpdateHandler.promises.getOrCreateProject.should.have.been.calledWith(
|
||||
ctx.user_id,
|
||||
undefined,
|
||||
'projectName'
|
||||
)
|
||||
resolve()
|
||||
},
|
||||
}
|
||||
ctx.TpdsController.resolveProject(ctx.req, res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should resolve by id', async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
const projectId = ctx.resolvedProject._id.toString()
|
||||
ctx.req.body = { projectId }
|
||||
const res = {
|
||||
json: payload => {
|
||||
expect(payload).to.deep.equal({
|
||||
status: 'success',
|
||||
projectId: ctx.resolvedProject._id.toString(),
|
||||
historyId: 42,
|
||||
otMigrationStage: 1,
|
||||
})
|
||||
ctx.TpdsUpdateHandler.promises.getOrCreateProject.should.have.been.calledWith(
|
||||
ctx.user_id,
|
||||
projectId,
|
||||
undefined
|
||||
)
|
||||
resolve()
|
||||
},
|
||||
}
|
||||
ctx.TpdsController.resolveProject(ctx.req, res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should default the otMigrationStage to zero when unset', async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.TpdsUpdateHandler.promises.getOrCreateProject.resolves({
|
||||
_id: ctx.resolvedProject._id,
|
||||
overleaf: { history: { id: 1337 } },
|
||||
})
|
||||
ctx.req.body = { projectName: 'projectName' }
|
||||
const res = {
|
||||
json: payload => {
|
||||
expect(payload).to.deep.equal({
|
||||
status: 'success',
|
||||
projectId: ctx.resolvedProject._id.toString(),
|
||||
historyId: 1337,
|
||||
otMigrationStage: 0,
|
||||
})
|
||||
resolve()
|
||||
},
|
||||
}
|
||||
ctx.TpdsController.resolveProject(ctx.req, res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should indicate in the response when the project could not be resolved', async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.TpdsUpdateHandler.promises.getOrCreateProject.resolves(null)
|
||||
ctx.req.body = { projectName: 'projectName' }
|
||||
const res = {
|
||||
json: payload => {
|
||||
expect(payload).to.deep.equal({ status: 'rejected' })
|
||||
resolve()
|
||||
},
|
||||
}
|
||||
ctx.TpdsController.resolveProject(ctx.req, res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getting an update', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.projectName = 'projectName'
|
||||
|
||||
@@ -424,6 +424,84 @@ describe('TpdsUpdateHandler', function () {
|
||||
expectDropboxUnlinked()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getting or creating a project', function () {
|
||||
describe('byId', function () {
|
||||
describe('with no matching project', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.projectId = new ObjectId().toString()
|
||||
})
|
||||
resolveProjectById()
|
||||
expectProjectNotCreated()
|
||||
expectNoProjectResolved()
|
||||
})
|
||||
|
||||
describe('with one matching active project', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.projectId = ctx.projects.active1._id.toString()
|
||||
})
|
||||
resolveProjectById()
|
||||
expectProjectNotCreated()
|
||||
expectProjectResolved()
|
||||
})
|
||||
|
||||
describe('with one matching archived project', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.projectId = ctx.projects.archived1._id.toString()
|
||||
})
|
||||
resolveProjectById()
|
||||
expectProjectNotCreated()
|
||||
expectNoProjectResolved()
|
||||
})
|
||||
|
||||
describe('with one matching trashed project', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.projectId = ctx.projects.trashed._id.toString()
|
||||
})
|
||||
resolveProjectById()
|
||||
expectProjectNotCreated()
|
||||
expectNoProjectResolved()
|
||||
})
|
||||
})
|
||||
|
||||
describe('with no matching project', function () {
|
||||
setupMatchingProjects([])
|
||||
resolveProjectByName()
|
||||
expectProjectCreated()
|
||||
expectProjectResolved()
|
||||
})
|
||||
|
||||
describe('with one matching active project', function () {
|
||||
setupMatchingProjects(['active1'])
|
||||
resolveProjectByName()
|
||||
expectProjectNotCreated()
|
||||
expectProjectResolved()
|
||||
})
|
||||
|
||||
describe('with one matching archived project', function () {
|
||||
setupMatchingProjects(['archived1'])
|
||||
resolveProjectByName()
|
||||
expectProjectNotCreated()
|
||||
expectNoProjectResolved()
|
||||
expectDropboxNotUnlinked()
|
||||
})
|
||||
|
||||
describe('with two matching active projects', function () {
|
||||
setupMatchingProjects(['active1', 'active2'])
|
||||
resolveProjectByName()
|
||||
expectProjectNotCreated()
|
||||
expectNoProjectResolved()
|
||||
expectDropboxUnlinked()
|
||||
})
|
||||
|
||||
describe('with one matching active and one matching archived project', function () {
|
||||
setupMatchingProjects(['active1', 'archived1'])
|
||||
resolveProjectByName()
|
||||
expectProjectNotCreated()
|
||||
expectNoProjectResolved()
|
||||
expectDropboxUnlinked()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* Setup helpers */
|
||||
@@ -512,6 +590,28 @@ function receiveFolderUpdate() {
|
||||
})
|
||||
}
|
||||
|
||||
function resolveProjectByName() {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.resolvedProject =
|
||||
await ctx.TpdsUpdateHandler.promises.getOrCreateProject(
|
||||
ctx.userId,
|
||||
null, // projectId
|
||||
ctx.projectName
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function resolveProjectById() {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.resolvedProject =
|
||||
await ctx.TpdsUpdateHandler.promises.getOrCreateProject(
|
||||
ctx.userId,
|
||||
ctx.projectId,
|
||||
'' // projectName
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/* Expectations */
|
||||
|
||||
function expectProjectCreated() {
|
||||
@@ -619,6 +719,18 @@ function expectDeleteNotProcessed() {
|
||||
})
|
||||
}
|
||||
|
||||
function expectProjectResolved() {
|
||||
it('resolves the project', function (ctx) {
|
||||
expect(ctx.resolvedProject._id).to.equal(ctx.projects.active1._id)
|
||||
})
|
||||
}
|
||||
|
||||
function expectNoProjectResolved() {
|
||||
it('does not resolve a project', function (ctx) {
|
||||
expect(ctx.resolvedProject).to.not.exist
|
||||
})
|
||||
}
|
||||
|
||||
function expectProjectDeleted() {
|
||||
it('deletes the project', function (ctx) {
|
||||
expect(
|
||||
|
||||
Reference in New Issue
Block a user