diff --git a/client/modules/IDE/components/EditorAccessibility.unit.test.jsx b/client/modules/IDE/components/EditorAccessibility.unit.test.jsx index ca6dfd868..9e4e8463b 100644 --- a/client/modules/IDE/components/EditorAccessibility.unit.test.jsx +++ b/client/modules/IDE/components/EditorAccessibility.unit.test.jsx @@ -6,7 +6,7 @@ import EditorAccessibility from './EditorAccessibility'; describe('', () => { it('renders empty message with no lines', () => { - render(); + render(); expect( screen.getByRole('listitem', { @@ -21,11 +21,12 @@ describe('', () => { lintMessages={[ { severity: 'info', - line: '1', + line: 1, message: 'foo', - id: '1a2b3c' + id: 123 } ]} + currentLine={0} /> ); diff --git a/client/modules/IDE/components/ErrorModal.unit.test.jsx b/client/modules/IDE/components/ErrorModal.unit.test.jsx index 541276193..9eab7d674 100644 --- a/client/modules/IDE/components/ErrorModal.unit.test.jsx +++ b/client/modules/IDE/components/ErrorModal.unit.test.jsx @@ -8,20 +8,30 @@ jest.mock('../../../i18n'); describe('', () => { it('renders type forceAuthentication', () => { - render(); + render( + + ); expect(screen.getByText('Login')).toBeVisible(); expect(screen.getByText('Sign Up')).toBeVisible(); }); it('renders type staleSession', () => { - render(); + render( + + ); expect(screen.getByText('log in')).toBeVisible(); }); it('renders type staleProject', () => { - render(); + render( + + ); expect( screen.getByText( diff --git a/client/modules/IDE/components/FileNode.unit.test.jsx b/client/modules/IDE/components/FileNode.unit.test.jsx index 3f6706a2c..8676a817d 100644 --- a/client/modules/IDE/components/FileNode.unit.test.jsx +++ b/client/modules/IDE/components/FileNode.unit.test.jsx @@ -83,12 +83,18 @@ describe('', () => { }); it('can change to a different extension', async () => { + const mockConfirm = jest.fn(() => true); + window.confirm = mockConfirm; + const newName = 'newname.gif'; const props = renderFileNode('file'); changeName(newName); - await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled()); + expect(mockConfirm).toHaveBeenCalled(); + await waitFor(() => + expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName) + ); await expectFileNameToBe(props.name); }); diff --git a/client/modules/User/components/AccountForm.unit.test.jsx b/client/modules/User/components/AccountForm.unit.test.jsx new file mode 100644 index 000000000..653dce524 --- /dev/null +++ b/client/modules/User/components/AccountForm.unit.test.jsx @@ -0,0 +1,122 @@ +import React from 'react'; +import thunk from 'redux-thunk'; +import configureStore from 'redux-mock-store'; +import { + reduxRender, + screen, + fireEvent, + act, + waitFor +} from '../../../test-utils'; +import AccountForm from './AccountForm'; +import { initialTestState } from '../../../testData/testReduxStore'; +import * as actions from '../actions'; + +const mockStore = configureStore([thunk]); +const store = mockStore(initialTestState); + +jest.mock('../actions', () => ({ + ...jest.requireActual('../actions'), + updateSettings: jest.fn().mockReturnValue( + (dispatch) => + new Promise((resolve) => { + setTimeout(() => { + dispatch({ type: 'UPDATE_SETTINGS', payload: {} }); + resolve(); + }, 100); + }) + ) +})); + +const subject = () => { + reduxRender(, { + store + }); +}; + +describe('', () => { + it('renders form fields with initial values', () => { + subject(); + const emailElement = screen.getByRole('textbox', { + name: /email/i + }); + expect(emailElement).toBeInTheDocument(); + expect(emailElement).toHaveValue('happydog@example.com'); + + const userNameElement = screen.getByRole('textbox', { + name: /username/i + }); + expect(userNameElement).toBeInTheDocument(); + expect(userNameElement).toHaveValue('happydog'); + + const currentPasswordElement = screen.getByLabelText(/current password/i); + expect(currentPasswordElement).toBeInTheDocument(); + expect(currentPasswordElement).toHaveValue(''); + + const newPasswordElement = screen.getByLabelText(/new password/i); + expect(newPasswordElement).toBeInTheDocument(); + expect(newPasswordElement).toHaveValue(''); + }); + + it('handles form submission and calls updateSettings', async () => { + subject(); + + const saveAllSettingsButton = screen.getByRole('button', { + name: /save all settings/i + }); + + const currentPasswordElement = screen.getByLabelText(/current password/i); + const newPasswordElement = screen.getByLabelText(/new password/i); + + fireEvent.change(currentPasswordElement, { + target: { + value: 'currentPassword' + } + }); + + fireEvent.change(newPasswordElement, { + target: { + value: 'newPassword' + } + }); + + await act(async () => { + fireEvent.click(saveAllSettingsButton); + }); + + await waitFor(() => { + expect(actions.updateSettings).toHaveBeenCalledTimes(1); + }); + }); + + it('Save all setting button should get disabled while submitting and enable when not submitting', async () => { + subject(); + + const saveAllSettingsButton = screen.getByRole('button', { + name: /save all settings/i + }); + + const currentPasswordElement = screen.getByLabelText(/current password/i); + const newPasswordElement = screen.getByLabelText(/new password/i); + + fireEvent.change(currentPasswordElement, { + target: { + value: 'currentPassword' + } + }); + + fireEvent.change(newPasswordElement, { + target: { + value: 'newPassword' + } + }); + expect(saveAllSettingsButton).not.toHaveAttribute('disabled'); + + await act(async () => { + fireEvent.click(saveAllSettingsButton); + await waitFor(() => { + expect(saveAllSettingsButton).toHaveAttribute('disabled'); + }); + }); + }); +}); diff --git a/client/modules/User/components/LoginForm.unit.test.jsx b/client/modules/User/components/LoginForm.unit.test.jsx new file mode 100644 index 000000000..28fb9383c --- /dev/null +++ b/client/modules/User/components/LoginForm.unit.test.jsx @@ -0,0 +1,86 @@ +import React from 'react'; +import thunk from 'redux-thunk'; +import configureStore from 'redux-mock-store'; +import LoginForm from './LoginForm'; +import * as actions from '../actions'; +import { initialTestState } from '../../../testData/testReduxStore'; +import { reduxRender, screen, fireEvent, act } from '../../../test-utils'; + +const mockStore = configureStore([thunk]); +const store = mockStore(initialTestState); + +jest.mock('../actions', () => ({ + ...jest.requireActual('../actions'), + validateAndLoginUser: jest.fn().mockReturnValue( + (dispatch) => + new Promise((resolve) => { + setTimeout(() => { + dispatch({ type: 'AUTH_USER', payload: {} }); + dispatch({ type: 'SET_PREFERENCES', payload: {} }); + resolve(); + }, 100); + }) + ) +})); + +const subject = () => { + reduxRender(, { + store + }); +}; + +describe('', () => { + test('Renders form with the correct fields.', () => { + subject(); + const emailTextElement = screen.getByText(/email or username/i); + expect(emailTextElement).toBeInTheDocument(); + + const emailInputElement = screen.getByRole('textbox', { + name: /email or username/i + }); + expect(emailInputElement).toBeInTheDocument(); + + const passwordTextElement = screen.getByText(/password/i); + expect(passwordTextElement).toBeInTheDocument(); + + const passwordInputElement = screen.getByLabelText(/password/i); + expect(passwordInputElement).toBeInTheDocument(); + + const loginButtonElement = screen.getByRole('button', { + name: /log in/i + }); + expect(loginButtonElement).toBeInTheDocument(); + }); + test('Validate and login user is called with the correct values.', async () => { + subject(); + + const emailElement = screen.getByRole('textbox', { + name: /email or username/i + }); + fireEvent.change(emailElement, { + target: { + value: 'correctuser@gmail.com' + } + }); + + const passwordElement = screen.getByLabelText(/password/i); + + fireEvent.change(passwordElement, { + target: { + value: 'correctpassword' + } + }); + + const loginButton = screen.getByRole('button', { + name: /log in/i + }); + await act(async () => { + fireEvent.click(loginButton); + }); + + expect(actions.validateAndLoginUser).toHaveBeenCalledWith({ + email: 'correctuser@gmail.com', + password: 'correctpassword' + }); + }); +}); diff --git a/client/modules/User/components/NewPasswordForm.unit.test.jsx b/client/modules/User/components/NewPasswordForm.unit.test.jsx new file mode 100644 index 000000000..dbddf3c8c --- /dev/null +++ b/client/modules/User/components/NewPasswordForm.unit.test.jsx @@ -0,0 +1,84 @@ +import React from 'react'; +import thunk from 'redux-thunk'; +import configureStore from 'redux-mock-store'; +import { fireEvent } from '@storybook/testing-library'; +import { reduxRender, screen, act, waitFor } from '../../../test-utils'; +import { initialTestState } from '../../../testData/testReduxStore'; +import NewPasswordForm from './NewPasswordForm'; + +const mockStore = configureStore([thunk]); +const store = mockStore(initialTestState); + +const mockResetPasswordToken = 'mockResetToken'; +const subject = () => { + reduxRender(, { + store + }); +}; +const mockDispatch = jest.fn(); + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => mockDispatch +})); + +jest.mock('../../../utils/reduxFormUtils', () => ({ + validateNewPassword: jest.fn() +})); + +jest.mock('../actions', () => ({ + updatePassword: jest.fn().mockReturnValue( + new Promise((resolve) => { + resolve(); + }) + ) +})); + +describe('', () => { + beforeEach(() => { + mockDispatch.mockClear(); + jest.clearAllMocks(); + }); + test('renders form fields correctly', () => { + subject(); + + const passwordInputElements = screen.getAllByLabelText(/Password/i); + expect(passwordInputElements).toHaveLength(2); + + const passwordInputElement = passwordInputElements[0]; + expect(passwordInputElement).toBeInTheDocument(); + + const confirmPasswordInputElement = passwordInputElements[1]; + expect(confirmPasswordInputElement).toBeInTheDocument(); + + const submitElemement = screen.getByRole('button', { + name: /set new password/i + }); + expect(submitElemement).toBeInTheDocument(); + }); + + test('submits form with valid data', async () => { + subject(); + const passwordInputElements = screen.getAllByLabelText(/Password/i); + + const passwordInputElement = passwordInputElements[0]; + fireEvent.change(passwordInputElement, { + target: { value: 'password123' } + }); + + const confirmPasswordInputElement = passwordInputElements[1]; + fireEvent.change(confirmPasswordInputElement, { + target: { value: 'password123' } + }); + + const submitElemement = screen.getByRole('button', { + name: /set new password/i + }); + + await act(async () => { + fireEvent.click(submitElemement); + }); + + await waitFor(() => expect(mockDispatch).toHaveBeenCalled()); + }); +}); diff --git a/package-lock.json b/package-lock.json index 388e80ad9..de7ae82e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "p5.js-web-editor", - "version": "2.14.3", + "version": "2.14.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "p5.js-web-editor", - "version": "2.14.3", + "version": "2.14.5", "license": "LGPL-2.1", "dependencies": { "@auth0/s3": "^1.0.0", diff --git a/package.json b/package.json index 33f5e2ce2..c9f5edcab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "p5.js-web-editor", - "version": "2.14.3", + "version": "2.14.5", "description": "The web editor for p5.js.", "scripts": { "clean": "rimraf dist", diff --git a/server/controllers/project.controller/__test__/createProject.test.js b/server/controllers/project.controller/__test__/createProject.test.js index 8ab179dd3..8a7ea8f67 100644 --- a/server/controllers/project.controller/__test__/createProject.test.js +++ b/server/controllers/project.controller/__test__/createProject.test.js @@ -179,6 +179,10 @@ describe('project.controller', () => { }); it('fails if user does not have permission', async () => { + // We don't want to clog up the jest output with extra + // logs, so we turn off console warn for this one test. + jest.spyOn(console, 'warn').mockImplementation(() => {}); + request.user = { _id: 'abc123', username: 'alice' }; request.params = { username: 'dana' diff --git a/server/controllers/project.controller/createProject.js b/server/controllers/project.controller/createProject.js index 75e8ce84d..021fbb21f 100644 --- a/server/controllers/project.controller/createProject.js +++ b/server/controllers/project.controller/createProject.js @@ -59,7 +59,7 @@ export async function apiCreateProject(req, res) { const checkUserHasPermission = () => { if (req.user.username !== req.params.username) { - console.log('no permission'); + console.warn('no permission'); const error = new ProjectValidationError( `'${req.user.username}' does not have permission to create for '${req.params.username}'` ); diff --git a/server/previewServer.js b/server/previewServer.js index 754775645..3c07cd5d5 100644 --- a/server/previewServer.js +++ b/server/previewServer.js @@ -19,9 +19,13 @@ const mongoConnectionString = process.env.MONGO_URL; // Connect to MongoDB const connectToMongoDB = async () => { try { + mongoose.set('strictQuery', true); + await mongoose.connect(mongoConnectionString, { useNewUrlParser: true, - useUnifiedTopology: true + useUnifiedTopology: true, + serverSelectionTimeoutMS: 30000, // 30 seconds timeout + socketTimeoutMS: 45000 // 45 seconds timeout }); } catch (error) { console.error('Failed to connect to MongoDB: ', error); @@ -31,7 +35,6 @@ const connectToMongoDB = async () => { connectToMongoDB(); -mongoose.set('strictQuery', true); mongoose.connection.on('error', () => { console.error( 'MongoDB Connection Error. Please make sure that MongoDB is running.' diff --git a/server/server.js b/server/server.js index 64affcf50..8b4d19645 100644 --- a/server/server.js +++ b/server/server.js @@ -73,6 +73,18 @@ app.options('*', corsMiddleware); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); app.use(bodyParser.json({ limit: '50mb' })); app.use(cookieParser()); + +mongoose.set('strictQuery', true); + +const clientPromise = mongoose + .connect(mongoConnectionString, { + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 30000, // 30 seconds timeout + socketTimeoutMS: 45000 // 45 seconds timeout + }) + .then((m) => m.connection.getClient()); + app.use( session({ resave: true, @@ -85,7 +97,7 @@ app.use( secure: false }, store: new MongoStore({ - mongooseConnection: mongoose.connection, + clientPromise, autoReconnect: true }) }) @@ -151,29 +163,6 @@ app.use('/', passportRoutes); // configure passport require('./config/passport'); -// Connect to MongoDB -const connectToMongoDB = async () => { - try { - await mongoose.connect(mongoConnectionString, { - useNewUrlParser: true, - useUnifiedTopology: true - }); - } catch (error) { - console.error('Failed to connect to MongoDB: ', error); - process.exit(1); - } -}; - -connectToMongoDB(); - -mongoose.set('strictQuery', true); -mongoose.connection.on('error', () => { - console.error( - 'MongoDB Connection Error. Please make sure that MongoDB is running.' - ); - process.exit(1); -}); - app.get('/', (req, res) => { res.sendFile(renderIndex()); });