2021-03-12 05:23:46 -05:00
|
|
|
import { expect } from 'chai'
|
|
|
|
import sinon from 'sinon'
|
|
|
|
import React from 'react'
|
|
|
|
import {
|
|
|
|
screen,
|
|
|
|
fireEvent,
|
2021-03-31 06:44:20 -04:00
|
|
|
waitFor,
|
2021-04-27 03:52:58 -04:00
|
|
|
waitForElementToBeRemoved,
|
2021-03-12 05:23:46 -05:00
|
|
|
} from '@testing-library/react'
|
|
|
|
import fetchMock from 'fetch-mock'
|
2021-05-28 05:04:32 -04:00
|
|
|
import { get } from 'lodash'
|
2021-06-03 09:44:23 -04:00
|
|
|
|
2021-03-12 05:23:46 -05:00
|
|
|
import ShareProjectModal from '../../../../../frontend/js/features/share-project-modal/components/share-project-modal'
|
2021-06-03 09:44:23 -04:00
|
|
|
import {
|
|
|
|
renderWithEditorContext,
|
|
|
|
cleanUpContext,
|
|
|
|
} from '../../../helpers/render-with-context'
|
2021-03-12 05:23:46 -05:00
|
|
|
import * as locationModule from '../../../../../frontend/js/features/share-project-modal/utils/location'
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
describe('<ShareProjectModal/>', function () {
|
2021-03-12 05:23:46 -05:00
|
|
|
const project = {
|
|
|
|
_id: 'test-project',
|
|
|
|
name: 'Test Project',
|
|
|
|
features: {
|
2021-04-27 03:52:58 -04:00
|
|
|
collaborators: 10,
|
2021-03-12 05:23:46 -05:00
|
|
|
},
|
|
|
|
owner: {
|
2021-04-27 03:52:58 -04:00
|
|
|
email: 'project-owner@example.com',
|
2021-03-12 05:23:46 -05:00
|
|
|
},
|
|
|
|
members: [],
|
2021-04-27 03:52:58 -04:00
|
|
|
invites: [],
|
2021-03-12 05:23:46 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
const contacts = [
|
|
|
|
// user with edited name
|
|
|
|
{
|
|
|
|
type: 'user',
|
|
|
|
email: 'test-user@example.com',
|
|
|
|
first_name: 'Test',
|
|
|
|
last_name: 'User',
|
2021-04-27 03:52:58 -04:00
|
|
|
name: 'Test User',
|
2021-03-12 05:23:46 -05:00
|
|
|
},
|
|
|
|
// user with default name (email prefix)
|
|
|
|
{
|
|
|
|
type: 'user',
|
|
|
|
email: 'test@example.com',
|
2021-04-27 03:52:58 -04:00
|
|
|
first_name: 'test',
|
2021-03-12 05:23:46 -05:00
|
|
|
},
|
|
|
|
// no last name
|
|
|
|
{
|
|
|
|
type: 'user',
|
|
|
|
first_name: 'Eratosthenes',
|
2021-04-27 03:52:58 -04:00
|
|
|
email: 'eratosthenes@example.com',
|
2021-03-12 05:23:46 -05:00
|
|
|
},
|
|
|
|
// more users
|
|
|
|
{
|
|
|
|
type: 'user',
|
|
|
|
first_name: 'Claudius',
|
|
|
|
last_name: 'Ptolemy',
|
2021-04-27 03:52:58 -04:00
|
|
|
email: 'ptolemy@example.com',
|
2021-03-12 05:23:46 -05:00
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'user',
|
|
|
|
first_name: 'Abd al-Rahman',
|
|
|
|
last_name: 'Al-Sufi',
|
2021-04-27 03:52:58 -04:00
|
|
|
email: 'al-sufi@example.com',
|
2021-03-12 05:23:46 -05:00
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'user',
|
|
|
|
first_name: 'Nicolaus',
|
|
|
|
last_name: 'Copernicus',
|
2021-04-27 03:52:58 -04:00
|
|
|
email: 'copernicus@example.com',
|
|
|
|
},
|
2021-03-12 05:23:46 -05:00
|
|
|
]
|
|
|
|
|
2021-03-31 06:44:20 -04:00
|
|
|
const ideWithProject = project => {
|
2021-05-28 05:04:32 -04:00
|
|
|
const scope = { project }
|
|
|
|
|
2021-03-31 06:44:20 -04:00
|
|
|
return {
|
|
|
|
$scope: {
|
2021-05-28 05:04:32 -04:00
|
|
|
$watch: (path, callback) => {
|
|
|
|
callback(get(scope, path))
|
|
|
|
return () => null
|
|
|
|
},
|
2021-03-31 06:44:20 -04:00
|
|
|
$applyAsync: () => {},
|
2021-05-28 05:04:32 -04:00
|
|
|
...scope,
|
2021-04-27 03:52:58 -04:00
|
|
|
},
|
2021-03-31 06:44:20 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-12 05:23:46 -05:00
|
|
|
const modalProps = {
|
2021-03-31 06:44:20 -04:00
|
|
|
ide: ideWithProject(project),
|
2021-03-12 05:23:46 -05:00
|
|
|
show: true,
|
|
|
|
isAdmin: true,
|
2021-04-27 03:52:58 -04:00
|
|
|
handleHide: sinon.stub(),
|
2021-03-12 05:23:46 -05:00
|
|
|
}
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
beforeEach(function () {
|
2021-03-12 05:23:46 -05:00
|
|
|
fetchMock.get('/user/contacts', { contacts })
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
afterEach(function () {
|
2021-03-12 05:23:46 -05:00
|
|
|
fetchMock.restore()
|
2021-06-03 09:44:23 -04:00
|
|
|
cleanUpContext()
|
2021-03-12 05:23:46 -05:00
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('renders the modal', async function () {
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(<ShareProjectModal {...modalProps} />)
|
2021-03-12 05:23:46 -05:00
|
|
|
|
|
|
|
await screen.findByText('Share Project')
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('calls handleHide when a Close button is pressed', async function () {
|
2021-03-12 05:23:46 -05:00
|
|
|
const handleHide = sinon.stub()
|
|
|
|
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
|
|
|
<ShareProjectModal {...modalProps} handleHide={handleHide} />
|
|
|
|
)
|
2021-03-12 05:23:46 -05:00
|
|
|
|
|
|
|
const [
|
|
|
|
headerCloseButton,
|
2021-04-27 03:52:58 -04:00
|
|
|
footerCloseButton,
|
2021-03-12 05:23:46 -05:00
|
|
|
] = await screen.findAllByRole('button', { name: 'Close' })
|
|
|
|
|
|
|
|
fireEvent.click(headerCloseButton)
|
|
|
|
fireEvent.click(footerCloseButton)
|
|
|
|
|
|
|
|
expect(handleHide.callCount).to.equal(2)
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('handles access level "private"', async function () {
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
2021-03-12 05:23:46 -05:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({ ...project, publicAccesLevel: 'private' })}
|
2021-03-12 05:23:46 -05:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
await screen.findByText(
|
|
|
|
'Link sharing is off, only invited users can view this project.'
|
|
|
|
)
|
|
|
|
await screen.findByRole('button', { name: 'Turn on link sharing' })
|
|
|
|
|
|
|
|
expect(screen.queryByText('Anyone with this link can view this project')).to
|
|
|
|
.be.null
|
|
|
|
expect(screen.queryByText('Anyone with this link can edit this project')).to
|
|
|
|
.be.null
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('handles access level "tokenBased"', async function () {
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
2021-03-12 05:23:46 -05:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({ ...project, publicAccesLevel: 'tokenBased' })}
|
2021-03-12 05:23:46 -05:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
await screen.findByText('Link sharing is on')
|
|
|
|
await screen.findByRole('button', { name: 'Turn off link sharing' })
|
|
|
|
|
|
|
|
expect(screen.queryByText('Anyone with this link can view this project'))
|
|
|
|
.not.to.be.null
|
|
|
|
expect(screen.queryByText('Anyone with this link can edit this project'))
|
|
|
|
.not.to.be.null
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('handles legacy access level "readAndWrite"', async function () {
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
2021-03-12 05:23:46 -05:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({ ...project, publicAccesLevel: 'readAndWrite' })}
|
2021-03-12 05:23:46 -05:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
await screen.findByText(
|
|
|
|
'This project is public and can be edited by anyone with the URL.'
|
|
|
|
)
|
|
|
|
await screen.findByRole('button', { name: 'Make Private' })
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('handles legacy access level "readOnly"', async function () {
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
2021-03-12 05:23:46 -05:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({ ...project, publicAccesLevel: 'readOnly' })}
|
2021-03-12 05:23:46 -05:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
await screen.findByText(
|
|
|
|
'This project is public and can be viewed but not edited by anyone with the URL'
|
|
|
|
)
|
|
|
|
await screen.findByRole('button', { name: 'Make Private' })
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('hides actions from non-admins', async function () {
|
2021-03-12 05:23:46 -05:00
|
|
|
const invites = [
|
|
|
|
{
|
|
|
|
_id: 'invited-author',
|
|
|
|
email: 'invited-author@example.com',
|
2021-04-27 03:52:58 -04:00
|
|
|
privileges: 'readAndWrite',
|
|
|
|
},
|
2021-03-12 05:23:46 -05:00
|
|
|
]
|
|
|
|
|
|
|
|
// render as admin: actions should be present
|
2021-06-03 09:44:23 -04:00
|
|
|
const { rerender } = renderWithEditorContext(
|
2021-03-12 05:23:46 -05:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({
|
|
|
|
...project,
|
|
|
|
invites,
|
2021-04-27 03:52:58 -04:00
|
|
|
publicAccesLevel: 'tokenBased',
|
2021-03-31 06:44:20 -04:00
|
|
|
})}
|
2021-03-12 05:23:46 -05:00
|
|
|
isAdmin
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
await screen.findByRole('button', { name: 'Turn off link sharing' })
|
|
|
|
await screen.findByRole('button', { name: 'Resend' })
|
|
|
|
|
2021-06-03 09:44:23 -04:00
|
|
|
// render as non-admin (non-owner), link sharing on: actions should be missing and message should be present
|
2021-03-12 05:23:46 -05:00
|
|
|
rerender(
|
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({
|
|
|
|
...project,
|
|
|
|
invites,
|
2021-04-27 03:52:58 -04:00
|
|
|
publicAccesLevel: 'tokenBased',
|
2021-03-31 06:44:20 -04:00
|
|
|
})}
|
2021-03-12 05:23:46 -05:00
|
|
|
isAdmin={false}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
await screen.findByText(
|
|
|
|
'To change access permissions, please ask the project owner'
|
|
|
|
)
|
|
|
|
|
|
|
|
expect(screen.queryByRole('button', { name: 'Turn off link sharing' })).to
|
|
|
|
.be.null
|
|
|
|
expect(screen.queryByRole('button', { name: 'Turn on link sharing' })).to.be
|
|
|
|
.null
|
|
|
|
expect(screen.queryByRole('button', { name: 'Resend' })).to.be.null
|
|
|
|
|
2021-03-31 06:44:20 -04:00
|
|
|
// render as non-admin (non-owner), link sharing off: actions should be missing and message should be present
|
2021-03-12 05:23:46 -05:00
|
|
|
rerender(
|
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({
|
|
|
|
...project,
|
|
|
|
invites,
|
2021-04-27 03:52:58 -04:00
|
|
|
publicAccesLevel: 'private',
|
2021-03-31 06:44:20 -04:00
|
|
|
})}
|
2021-03-12 05:23:46 -05:00
|
|
|
isAdmin={false}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
await screen.findByText(
|
2021-05-28 05:04:32 -04:00
|
|
|
'To add more collaborators or turn on link sharing, please ask the project owner'
|
2021-03-12 05:23:46 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
expect(screen.queryByRole('button', { name: 'Turn off link sharing' })).to
|
|
|
|
.be.null
|
|
|
|
expect(screen.queryByRole('button', { name: 'Turn on link sharing' })).to.be
|
|
|
|
.null
|
|
|
|
expect(screen.queryByRole('button', { name: 'Resend' })).to.be.null
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('only shows read-only token link to restricted token members', async function () {
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
2021-03-12 05:23:46 -05:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({ ...project, publicAccesLevel: 'tokenBased' })}
|
2021-06-03 09:44:23 -04:00
|
|
|
/>,
|
|
|
|
{ isRestrictedTokenMember: true }
|
2021-03-12 05:23:46 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
// no buttons
|
|
|
|
expect(screen.queryByRole('button', { name: 'Turn on link sharing' })).to.be
|
|
|
|
.null
|
|
|
|
expect(screen.queryByRole('button', { name: 'Turn off link sharing' })).to
|
|
|
|
.be.null
|
|
|
|
|
|
|
|
// only read-only token link
|
|
|
|
await screen.findByText('Anyone with this link can view this project')
|
|
|
|
expect(screen.queryByText('Anyone with this link can edit this project')).to
|
|
|
|
.be.null
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('displays project members and invites', async function () {
|
2021-03-12 05:23:46 -05:00
|
|
|
const members = [
|
|
|
|
{
|
|
|
|
_id: 'member-author',
|
|
|
|
email: 'member-author@example.com',
|
2021-04-27 03:52:58 -04:00
|
|
|
privileges: 'readAndWrite',
|
2021-03-12 05:23:46 -05:00
|
|
|
},
|
|
|
|
{
|
|
|
|
_id: 'member-viewer',
|
|
|
|
email: 'member-viewer@example.com',
|
2021-04-27 03:52:58 -04:00
|
|
|
privileges: 'readOnly',
|
|
|
|
},
|
2021-03-12 05:23:46 -05:00
|
|
|
]
|
|
|
|
|
|
|
|
const invites = [
|
|
|
|
{
|
|
|
|
_id: 'invited-author',
|
|
|
|
email: 'invited-author@example.com',
|
2021-04-27 03:52:58 -04:00
|
|
|
privileges: 'readAndWrite',
|
2021-03-12 05:23:46 -05:00
|
|
|
},
|
|
|
|
{
|
|
|
|
_id: 'invited-viewer',
|
|
|
|
email: 'invited-viewer@example.com',
|
2021-04-27 03:52:58 -04:00
|
|
|
privileges: 'readOnly',
|
|
|
|
},
|
2021-03-12 05:23:46 -05:00
|
|
|
]
|
|
|
|
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
2021-03-12 05:23:46 -05:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({
|
2021-03-12 05:23:46 -05:00
|
|
|
...project,
|
|
|
|
members,
|
|
|
|
invites,
|
2021-04-27 03:52:58 -04:00
|
|
|
publicAccesLevel: 'tokenBased',
|
2021-03-31 06:44:20 -04:00
|
|
|
})}
|
2021-03-12 05:23:46 -05:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
expect(screen.queryAllByText('project-owner@example.com')).to.have.length(1)
|
|
|
|
expect(screen.queryAllByText('member-author@example.com')).to.have.length(1)
|
|
|
|
expect(screen.queryAllByText('member-viewer@example.com')).to.have.length(1)
|
|
|
|
expect(screen.queryAllByText('invited-author@example.com')).to.have.length(
|
|
|
|
1
|
|
|
|
)
|
|
|
|
expect(screen.queryAllByText('invited-viewer@example.com')).to.have.length(
|
|
|
|
1
|
|
|
|
)
|
|
|
|
|
|
|
|
expect(screen.queryAllByText('Invite not yet accepted.')).to.have.length(
|
|
|
|
invites.length
|
|
|
|
)
|
|
|
|
expect(screen.queryAllByRole('button', { name: 'Resend' })).to.have.length(
|
|
|
|
invites.length
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('resends an invite', async function () {
|
2021-03-12 05:23:46 -05:00
|
|
|
fetchMock.postOnce(
|
|
|
|
'express:/project/:projectId/invite/:inviteId/resend',
|
|
|
|
204
|
|
|
|
)
|
|
|
|
|
|
|
|
const invites = [
|
|
|
|
{
|
|
|
|
_id: 'invited-author',
|
|
|
|
email: 'invited-author@example.com',
|
2021-04-27 03:52:58 -04:00
|
|
|
privileges: 'readAndWrite',
|
|
|
|
},
|
2021-03-12 05:23:46 -05:00
|
|
|
]
|
|
|
|
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
2021-03-12 05:23:46 -05:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({
|
2021-03-12 05:23:46 -05:00
|
|
|
...project,
|
|
|
|
invites,
|
2021-04-27 03:52:58 -04:00
|
|
|
publicAccesLevel: 'tokenBased',
|
2021-03-31 06:44:20 -04:00
|
|
|
})}
|
2021-03-12 05:23:46 -05:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
const [, closeButton] = screen.getAllByRole('button', {
|
2021-04-27 03:52:58 -04:00
|
|
|
name: 'Close',
|
2021-03-12 05:23:46 -05:00
|
|
|
})
|
|
|
|
|
|
|
|
const resendButton = screen.getByRole('button', { name: 'Resend' })
|
2021-03-31 06:44:20 -04:00
|
|
|
fireEvent.click(resendButton)
|
2021-03-12 05:23:46 -05:00
|
|
|
|
2021-03-31 06:44:20 -04:00
|
|
|
await waitFor(() => expect(closeButton.disabled).to.be.true)
|
2021-03-12 05:23:46 -05:00
|
|
|
|
|
|
|
expect(fetchMock.done()).to.be.true
|
|
|
|
expect(closeButton.disabled).to.be.false
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('revokes an invite', async function () {
|
2021-03-12 05:23:46 -05:00
|
|
|
fetchMock.deleteOnce('express:/project/:projectId/invite/:inviteId', 204)
|
|
|
|
|
|
|
|
const invites = [
|
|
|
|
{
|
|
|
|
_id: 'invited-author',
|
|
|
|
email: 'invited-author@example.com',
|
2021-04-27 03:52:58 -04:00
|
|
|
privileges: 'readAndWrite',
|
|
|
|
},
|
2021-03-12 05:23:46 -05:00
|
|
|
]
|
|
|
|
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
2021-03-12 05:23:46 -05:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({
|
2021-03-12 05:23:46 -05:00
|
|
|
...project,
|
|
|
|
invites,
|
2021-04-27 03:52:58 -04:00
|
|
|
publicAccesLevel: 'tokenBased',
|
2021-03-31 06:44:20 -04:00
|
|
|
})}
|
2021-03-12 05:23:46 -05:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
const [, closeButton] = screen.getAllByRole('button', {
|
2021-04-27 03:52:58 -04:00
|
|
|
name: 'Close',
|
2021-03-12 05:23:46 -05:00
|
|
|
})
|
|
|
|
|
|
|
|
const revokeButton = screen.getByRole('button', { name: 'Revoke' })
|
2021-03-31 06:44:20 -04:00
|
|
|
fireEvent.click(revokeButton)
|
|
|
|
await waitFor(() => expect(closeButton.disabled).to.be.true)
|
2021-03-12 05:23:46 -05:00
|
|
|
|
|
|
|
expect(fetchMock.done()).to.be.true
|
|
|
|
expect(closeButton.disabled).to.be.false
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('changes member privileges to read + write', async function () {
|
2021-03-12 05:23:46 -05:00
|
|
|
fetchMock.putOnce('express:/project/:projectId/users/:userId', 204)
|
|
|
|
|
|
|
|
const members = [
|
|
|
|
{
|
|
|
|
_id: 'member-viewer',
|
|
|
|
email: 'member-viewer@example.com',
|
2021-04-27 03:52:58 -04:00
|
|
|
privileges: 'readOnly',
|
|
|
|
},
|
2021-03-12 05:23:46 -05:00
|
|
|
]
|
|
|
|
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
2021-03-12 05:23:46 -05:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({
|
2021-03-12 05:23:46 -05:00
|
|
|
...project,
|
|
|
|
members,
|
2021-04-27 03:52:58 -04:00
|
|
|
publicAccesLevel: 'tokenBased',
|
2021-03-31 06:44:20 -04:00
|
|
|
})}
|
2021-03-12 05:23:46 -05:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
const [, closeButton] = await screen.getAllByRole('button', {
|
2021-04-27 03:52:58 -04:00
|
|
|
name: 'Close',
|
2021-03-12 05:23:46 -05:00
|
|
|
})
|
|
|
|
|
|
|
|
expect(screen.queryAllByText('member-viewer@example.com')).to.have.length(1)
|
|
|
|
|
|
|
|
const select = screen.getByDisplayValue('Read Only')
|
|
|
|
await fireEvent.change(select, { target: { value: 'readAndWrite' } })
|
|
|
|
|
|
|
|
const changeButton = screen.getByRole('button', { name: 'Change' })
|
|
|
|
|
2021-03-31 06:44:20 -04:00
|
|
|
fireEvent.click(changeButton)
|
|
|
|
await waitFor(() => expect(closeButton.disabled).to.be.true)
|
2021-03-12 05:23:46 -05:00
|
|
|
|
2021-03-31 06:44:20 -04:00
|
|
|
const { body } = fetchMock.lastOptions()
|
|
|
|
expect(JSON.parse(body)).to.deep.equal({ privilegeLevel: 'readAndWrite' })
|
2021-03-12 05:23:46 -05:00
|
|
|
|
|
|
|
expect(fetchMock.done()).to.be.true
|
|
|
|
expect(closeButton.disabled).to.be.false
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('removes a member from the project', async function () {
|
2021-03-12 05:23:46 -05:00
|
|
|
fetchMock.deleteOnce('express:/project/:projectId/users/:userId', 204)
|
|
|
|
|
|
|
|
const members = [
|
|
|
|
{
|
|
|
|
_id: 'member-viewer',
|
|
|
|
email: 'member-viewer@example.com',
|
2021-04-27 03:52:58 -04:00
|
|
|
privileges: 'readOnly',
|
|
|
|
},
|
2021-03-12 05:23:46 -05:00
|
|
|
]
|
|
|
|
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
2021-03-12 05:23:46 -05:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({
|
2021-03-12 05:23:46 -05:00
|
|
|
...project,
|
|
|
|
members,
|
2021-04-27 03:52:58 -04:00
|
|
|
publicAccesLevel: 'tokenBased',
|
2021-03-31 06:44:20 -04:00
|
|
|
})}
|
2021-03-12 05:23:46 -05:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
expect(screen.queryAllByText('member-viewer@example.com')).to.have.length(1)
|
|
|
|
|
|
|
|
const removeButton = screen.getByRole('button', {
|
2021-05-12 06:28:03 -04:00
|
|
|
name: 'Remove collaborator',
|
2021-03-12 05:23:46 -05:00
|
|
|
})
|
|
|
|
|
2021-03-31 06:44:20 -04:00
|
|
|
fireEvent.click(removeButton)
|
|
|
|
|
|
|
|
const url = fetchMock.lastUrl()
|
|
|
|
expect(url).to.equal('/project/test-project/users/member-viewer')
|
2021-03-12 05:23:46 -05:00
|
|
|
|
|
|
|
expect(fetchMock.done()).to.be.true
|
|
|
|
|
2021-03-31 06:44:20 -04:00
|
|
|
await waitForElementToBeRemoved(() =>
|
|
|
|
screen.queryByText('member-viewer@example.com')
|
|
|
|
)
|
2021-03-12 05:23:46 -05:00
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('changes member privileges to owner with confirmation', async function () {
|
2021-03-12 05:23:46 -05:00
|
|
|
fetchMock.postOnce('express:/project/:projectId/transfer-ownership', 204)
|
|
|
|
|
|
|
|
const members = [
|
|
|
|
{
|
|
|
|
_id: 'member-viewer',
|
|
|
|
email: 'member-viewer@example.com',
|
2021-04-27 03:52:58 -04:00
|
|
|
privileges: 'readOnly',
|
|
|
|
},
|
2021-03-12 05:23:46 -05:00
|
|
|
]
|
|
|
|
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
2021-03-12 05:23:46 -05:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({
|
2021-03-12 05:23:46 -05:00
|
|
|
...project,
|
|
|
|
members,
|
2021-04-27 03:52:58 -04:00
|
|
|
publicAccesLevel: 'tokenBased',
|
2021-03-31 06:44:20 -04:00
|
|
|
})}
|
2021-03-12 05:23:46 -05:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
expect(screen.queryAllByText('member-viewer@example.com')).to.have.length(1)
|
|
|
|
|
|
|
|
const select = screen.getByDisplayValue('Read Only')
|
|
|
|
fireEvent.change(select, { target: { value: 'owner' } })
|
|
|
|
|
|
|
|
const changeButton = screen.getByRole('button', { name: 'Change' })
|
|
|
|
await fireEvent.click(changeButton)
|
|
|
|
|
|
|
|
screen.getByText((_, node) => {
|
|
|
|
return (
|
|
|
|
node.textContent ===
|
|
|
|
'Are you sure you want to make member-viewer@example.com the owner of Test Project?'
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
const reloadStub = sinon.stub(locationModule, 'reload')
|
|
|
|
|
2021-03-31 06:44:20 -04:00
|
|
|
const confirmButton = screen.getByRole('button', {
|
2021-04-27 03:52:58 -04:00
|
|
|
name: 'Change owner',
|
2021-03-12 05:23:46 -05:00
|
|
|
})
|
2021-03-31 06:44:20 -04:00
|
|
|
fireEvent.click(confirmButton)
|
|
|
|
await waitFor(() => expect(confirmButton.disabled).to.be.true)
|
|
|
|
|
|
|
|
const { body } = fetchMock.lastOptions()
|
|
|
|
expect(JSON.parse(body)).to.deep.equal({ user_id: 'member-viewer' })
|
2021-03-12 05:23:46 -05:00
|
|
|
|
|
|
|
expect(fetchMock.done()).to.be.true
|
|
|
|
expect(reloadStub.calledOnce).to.be.true
|
|
|
|
reloadStub.restore()
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('sends invites to input email addresses', async function () {
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
2021-03-12 05:23:46 -05:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({
|
2021-03-12 05:23:46 -05:00
|
|
|
...project,
|
2021-04-27 03:52:58 -04:00
|
|
|
publicAccesLevel: 'tokenBased',
|
2021-03-31 06:44:20 -04:00
|
|
|
})}
|
2021-03-12 05:23:46 -05:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
const [inputElement] = await screen.findAllByLabelText(
|
|
|
|
'Share with your collaborators'
|
|
|
|
)
|
|
|
|
|
|
|
|
// loading contacts
|
|
|
|
await waitFor(() => {
|
|
|
|
expect(fetchMock.called('express:/user/contacts')).to.be.true
|
|
|
|
})
|
|
|
|
|
|
|
|
// displaying a list of matching contacts
|
|
|
|
inputElement.focus()
|
|
|
|
fireEvent.change(inputElement, { target: { value: 'ptolemy' } })
|
|
|
|
|
|
|
|
await screen.findByText(/ptolemy@example.com/)
|
|
|
|
|
|
|
|
// sending invitations
|
|
|
|
|
|
|
|
fetchMock.post('express:/project/:projectId/invite', (url, req) => {
|
|
|
|
const data = JSON.parse(req.body)
|
|
|
|
|
|
|
|
if (data.email === 'a@b.c') {
|
|
|
|
return {
|
|
|
|
status: 400,
|
2021-04-27 03:52:58 -04:00
|
|
|
body: { errorReason: 'invalid_email' },
|
2021-03-12 05:23:46 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
status: 200,
|
|
|
|
body: {
|
|
|
|
invite: {
|
|
|
|
...data,
|
2021-04-27 03:52:58 -04:00
|
|
|
_id: data.email,
|
|
|
|
},
|
|
|
|
},
|
2021-03-12 05:23:46 -05:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
fireEvent.paste(inputElement, {
|
|
|
|
clipboardData: {
|
|
|
|
getData: () =>
|
2021-04-27 03:52:58 -04:00
|
|
|
'test@example.com, foo@example.com, bar@example.com, a@b.c',
|
|
|
|
},
|
2021-03-12 05:23:46 -05:00
|
|
|
})
|
|
|
|
|
|
|
|
const privilegesElement = screen.getByDisplayValue('Can Edit')
|
|
|
|
fireEvent.change(privilegesElement, { target: { value: 'readOnly' } })
|
|
|
|
|
|
|
|
const submitButton = screen.getByRole('button', { name: 'Share' })
|
|
|
|
submitButton.click()
|
|
|
|
|
|
|
|
let calls
|
|
|
|
await waitFor(
|
|
|
|
() => {
|
|
|
|
calls = fetchMock.calls('express:/project/:projectId/invite')
|
|
|
|
expect(calls).to.have.length(4)
|
|
|
|
},
|
|
|
|
{ timeout: 5000 } // allow time for delay between each request
|
|
|
|
)
|
|
|
|
|
|
|
|
expect(calls[0][1].body).to.equal(
|
|
|
|
JSON.stringify({ email: 'test@example.com', privileges: 'readOnly' })
|
|
|
|
)
|
|
|
|
expect(calls[1][1].body).to.equal(
|
|
|
|
JSON.stringify({ email: 'foo@example.com', privileges: 'readOnly' })
|
|
|
|
)
|
|
|
|
expect(calls[2][1].body).to.equal(
|
|
|
|
JSON.stringify({ email: 'bar@example.com', privileges: 'readOnly' })
|
|
|
|
)
|
|
|
|
expect(calls[3][1].body).to.equal(
|
|
|
|
JSON.stringify({ email: 'a@b.c', privileges: 'readOnly' })
|
|
|
|
)
|
|
|
|
|
|
|
|
// error from the last invite
|
|
|
|
screen.getByText('An email address is invalid')
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('displays a message when the collaborator limit is reached', async function () {
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
2021-03-12 05:23:46 -05:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({
|
2021-03-12 05:23:46 -05:00
|
|
|
...project,
|
|
|
|
publicAccesLevel: 'tokenBased',
|
|
|
|
features: {
|
2021-04-27 03:52:58 -04:00
|
|
|
collaborators: 0,
|
|
|
|
},
|
2021-03-31 06:44:20 -04:00
|
|
|
})}
|
2021-06-03 09:44:23 -04:00
|
|
|
/>,
|
|
|
|
{
|
|
|
|
user: {
|
|
|
|
id: '123abd',
|
|
|
|
allowedFreeTrial: true,
|
|
|
|
},
|
|
|
|
}
|
2021-03-12 05:23:46 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
expect(screen.queryByLabelText('Share with your collaborators')).to.be.null
|
|
|
|
|
|
|
|
screen.getByText(
|
|
|
|
/You need to upgrade your account to add more collaborators/
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('handles server error responses', async function () {
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
2021-03-12 05:23:46 -05:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
2021-03-31 06:44:20 -04:00
|
|
|
ide={ideWithProject({
|
2021-03-12 05:23:46 -05:00
|
|
|
...project,
|
2021-04-27 03:52:58 -04:00
|
|
|
publicAccesLevel: 'tokenBased',
|
2021-03-31 06:44:20 -04:00
|
|
|
})}
|
2021-03-12 05:23:46 -05:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
// loading contacts
|
|
|
|
await waitFor(() => {
|
|
|
|
expect(fetchMock.called('express:/user/contacts')).to.be.true
|
|
|
|
})
|
|
|
|
|
|
|
|
const [inputElement] = await screen.findAllByLabelText(
|
|
|
|
'Share with your collaborators'
|
|
|
|
)
|
|
|
|
|
|
|
|
const submitButton = screen.getByRole('button', { name: 'Share' })
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
const respondWithError = async function (errorReason) {
|
2021-03-31 06:44:20 -04:00
|
|
|
inputElement.focus()
|
|
|
|
fireEvent.change(inputElement, {
|
2021-04-27 03:52:58 -04:00
|
|
|
target: { value: 'invited-author-1@example.com' },
|
2021-03-12 05:23:46 -05:00
|
|
|
})
|
2021-03-31 06:44:20 -04:00
|
|
|
inputElement.blur()
|
2021-03-12 05:23:46 -05:00
|
|
|
|
|
|
|
fetchMock.postOnce(
|
|
|
|
'express:/project/:projectId/invite',
|
|
|
|
{
|
|
|
|
status: 400,
|
2021-04-27 03:52:58 -04:00
|
|
|
body: { errorReason },
|
2021-03-12 05:23:46 -05:00
|
|
|
},
|
|
|
|
{ overwriteRoutes: true }
|
|
|
|
)
|
|
|
|
|
|
|
|
expect(submitButton.disabled).to.be.false
|
|
|
|
submitButton.click()
|
|
|
|
await fetchMock.flush(true)
|
|
|
|
expect(fetchMock.done()).to.be.true
|
|
|
|
}
|
|
|
|
|
|
|
|
await respondWithError('cannot_invite_non_user')
|
|
|
|
await screen.findByText(
|
|
|
|
`Can't send invite. Recipient must already have a Overleaf account`
|
|
|
|
)
|
|
|
|
|
|
|
|
await respondWithError('cannot_verify_user_not_robot')
|
|
|
|
await screen.findByText(
|
|
|
|
`Sorry, we could not verify that you are not a robot. Please check that Google reCAPTCHA is not being blocked by an ad blocker or firewall.`
|
|
|
|
)
|
|
|
|
|
|
|
|
await respondWithError('cannot_invite_self')
|
|
|
|
await screen.findByText(`Can't send invite to yourself`)
|
|
|
|
|
|
|
|
await respondWithError('invalid_email')
|
|
|
|
await screen.findByText(`An email address is invalid`)
|
|
|
|
|
|
|
|
await respondWithError('too_many_requests')
|
|
|
|
await screen.findByText(
|
|
|
|
`Too many requests were received in a short space of time. Please wait for a few moments and try again.`
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('handles switching between access levels', async function () {
|
2021-03-31 06:44:20 -04:00
|
|
|
fetchMock.post('express:/project/:projectId/settings/admin', 204)
|
|
|
|
|
2021-05-05 09:05:04 -04:00
|
|
|
const watchCallbacks = {}
|
2021-04-15 05:04:58 -04:00
|
|
|
|
|
|
|
const ideWithProject = project => {
|
|
|
|
return {
|
|
|
|
$scope: {
|
|
|
|
$watch: (path, callback, deep) => {
|
|
|
|
watchCallbacks[path] = callback
|
|
|
|
return () => {}
|
|
|
|
},
|
|
|
|
$applyAsync: () => {},
|
2021-04-27 03:52:58 -04:00
|
|
|
project,
|
|
|
|
},
|
2021-04-15 05:04:58 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(
|
2021-03-31 06:44:20 -04:00
|
|
|
<ShareProjectModal
|
|
|
|
{...modalProps}
|
|
|
|
ide={ideWithProject({ ...project, publicAccesLevel: 'private' })}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
|
|
|
|
await screen.findByText(
|
|
|
|
'Link sharing is off, only invited users can view this project.'
|
|
|
|
)
|
|
|
|
|
|
|
|
const enableButton = await screen.findByRole('button', {
|
2021-04-27 03:52:58 -04:00
|
|
|
name: 'Turn on link sharing',
|
2021-03-31 06:44:20 -04:00
|
|
|
})
|
|
|
|
fireEvent.click(enableButton)
|
|
|
|
await waitFor(() => expect(enableButton.disabled).to.be.true)
|
|
|
|
|
|
|
|
const { body: tokenBody } = fetchMock.lastOptions()
|
|
|
|
expect(JSON.parse(tokenBody)).to.deep.equal({
|
2021-04-27 03:52:58 -04:00
|
|
|
publicAccessLevel: 'tokenBased',
|
2021-03-31 06:44:20 -04:00
|
|
|
})
|
|
|
|
|
2021-04-15 05:04:58 -04:00
|
|
|
// NOTE: updating the scoped project data manually,
|
|
|
|
// as the project data is usually updated via the websocket connection
|
|
|
|
watchCallbacks.project({ ...project, publicAccesLevel: 'tokenBased' })
|
|
|
|
|
2021-03-31 06:44:20 -04:00
|
|
|
await screen.findByText('Link sharing is on')
|
|
|
|
const disableButton = await screen.findByRole('button', {
|
2021-04-27 03:52:58 -04:00
|
|
|
name: 'Turn off link sharing',
|
2021-03-31 06:44:20 -04:00
|
|
|
})
|
|
|
|
fireEvent.click(disableButton)
|
|
|
|
await waitFor(() => expect(disableButton.disabled).to.be.true)
|
|
|
|
|
|
|
|
const { body: privateBody } = fetchMock.lastOptions()
|
|
|
|
expect(JSON.parse(privateBody)).to.deep.equal({
|
2021-04-27 03:52:58 -04:00
|
|
|
publicAccessLevel: 'private',
|
2021-03-31 06:44:20 -04:00
|
|
|
})
|
|
|
|
|
2021-04-15 05:04:58 -04:00
|
|
|
// NOTE: updating the scoped project data manually,
|
|
|
|
// as the project data is usually updated via the websocket connection
|
|
|
|
watchCallbacks.project({ ...project, publicAccesLevel: 'private' })
|
|
|
|
|
2021-03-31 06:44:20 -04:00
|
|
|
await screen.findByText(
|
|
|
|
'Link sharing is off, only invited users can view this project.'
|
|
|
|
)
|
|
|
|
})
|
2021-03-31 06:58:17 -04:00
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
it('avoids selecting unmatched contact', async function () {
|
2021-06-03 09:44:23 -04:00
|
|
|
renderWithEditorContext(<ShareProjectModal {...modalProps} />)
|
2021-03-31 06:58:17 -04:00
|
|
|
|
|
|
|
const [inputElement] = await screen.findAllByLabelText(
|
|
|
|
'Share with your collaborators'
|
|
|
|
)
|
|
|
|
|
|
|
|
// Wait for contacts to load
|
|
|
|
await waitFor(() => {
|
|
|
|
expect(fetchMock.called('express:/user/contacts')).to.be.true
|
|
|
|
})
|
|
|
|
|
|
|
|
// Enter a prefix that matches a contact
|
|
|
|
inputElement.focus()
|
|
|
|
fireEvent.change(inputElement, { target: { value: 'ptolemy' } })
|
|
|
|
|
|
|
|
// The matching contact should now be present and selected
|
|
|
|
await screen.findByRole('option', {
|
|
|
|
name: `Claudius Ptolemy <ptolemy@example.com>`,
|
2021-04-27 03:52:58 -04:00
|
|
|
selected: true,
|
2021-03-31 06:58:17 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
// Keep entering text so the contact no longer matches
|
|
|
|
fireEvent.change(inputElement, {
|
2021-04-27 03:52:58 -04:00
|
|
|
target: { value: 'ptolemy.new@example.com' },
|
2021-03-31 06:58:17 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
// The matching contact should no longer be present
|
|
|
|
expect(
|
|
|
|
screen.queryByRole('option', {
|
2021-04-27 03:52:58 -04:00
|
|
|
name: `Claudius Ptolemy <ptolemy@example.com>`,
|
2021-03-31 06:58:17 -04:00
|
|
|
})
|
|
|
|
).to.be.null
|
|
|
|
|
|
|
|
// No items should be added yet
|
|
|
|
expect(screen.queryByRole('button', { name: 'Remove' })).to.be.null
|
|
|
|
|
|
|
|
// Pressing Tab should add the entered item
|
|
|
|
fireEvent.keyDown(inputElement, { key: 'Tab', code: 'Tab' })
|
|
|
|
await waitFor(() => {
|
|
|
|
expect(screen.getAllByRole('button', { name: 'Remove' })).to.have.length(
|
|
|
|
1
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
// Blurring the input should not add another contact
|
|
|
|
fireEvent.blur(inputElement)
|
|
|
|
await waitFor(() => {
|
|
|
|
expect(screen.getAllByRole('button', { name: 'Remove' })).to.have.length(
|
|
|
|
1
|
|
|
|
)
|
|
|
|
})
|
|
|
|
})
|
2021-03-12 05:23:46 -05:00
|
|
|
})
|