const authenticatedUser = { id: 7, email: 'user@example.com', } function fillLoginForm(): void { cy.get('#login-email').type('user@example.com') cy.get('#login-password').type('correct-password') } describe('password login', () => { beforeEach(() => { cy.intercept('GET', '**/api/me', { statusCode: 401, body: { error: 'unauthenticated' }, }).as('me') }) it('logs in and redirects to the dashboard', () => { cy.intercept('POST', '**/api/login', (request) => { expect(request.body).to.deep.equal({ email: 'user@example.com', password: 'correct-password', }) request.reply({ statusCode: 200, body: { user: authenticatedUser }, }) }).as('login') cy.visit('/login') fillLoginForm() cy.get('form').submit() cy.wait('@login') cy.location('pathname').should('equal', '/dashboard') }) it('returns to a safe internal redirect after login', () => { cy.intercept('POST', '**/api/login', { statusCode: 200, body: { user: authenticatedUser }, }).as('login') cy.visit('/login?redirect=%2Fdashboard%3Ffocus%3Dtoday') fillLoginForm() cy.get('form').submit() cy.wait('@login') cy.location('pathname').should('equal', '/dashboard') cy.location('search').should('equal', '?focus=today') }) it('ignores an unsafe redirect after login', () => { cy.intercept('POST', '**/api/login', { statusCode: 200, body: { user: authenticatedUser }, }).as('login') cy.visit('/login?redirect=https%3A%2F%2Fexample.com') fillLoginForm() cy.get('form').submit() cy.wait('@login') cy.location('pathname').should('equal', '/dashboard') }) it('validates required login fields before submitting', () => { cy.visit('/login') cy.get('form').submit() cy.get('#login-email-error') .should('have.text', 'Enter a valid email address.') .and('be.visible') cy.get('#login-email').should('have.attr', 'aria-invalid', 'true') cy.get('#login-password-error') .should('have.text', 'Enter your password.') .and('be.visible') cy.get('#login-password').should('have.attr', 'aria-invalid', 'true') cy.location('pathname').should('equal', '/login') }) it('shows a generic invalid-credentials error', () => { cy.intercept('POST', '**/api/login', { statusCode: 401, body: { error: 'invalid_credentials' }, }).as('login') cy.visit('/login') fillLoginForm() cy.get('form').submit() cy.wait('@login') cy.get('[role="alert"]') .should('have.text', 'Email or password is incorrect.') .and('be.visible') cy.location('pathname').should('equal', '/login') }) it('shows backend field validation errors', () => { cy.intercept('POST', '**/api/login', { statusCode: 422, body: { message: 'The email field must be a valid email address.', errors: { email: ['The email field must be a valid email address.'], }, }, }).as('login') cy.visit('/login') fillLoginForm() cy.get('form').submit() cy.wait('@login') cy.get('#login-email-error') .should( 'have.text', 'The email field must be a valid email address.', ) .and('be.visible') }) it('shows throttling and malformed-response errors', () => { cy.intercept('POST', '**/api/login', { statusCode: 429, body: { message: 'Too Many Attempts.' }, }).as('throttledLogin') cy.visit('/login') fillLoginForm() cy.get('form').submit() cy.wait('@throttledLogin') cy.get('[role="alert"]').should( 'have.text', 'Too many login attempts. Try again in a minute.', ) cy.intercept('POST', '**/api/login', { statusCode: 200, body: { user: { id: 7 } }, }).as('malformedLogin') cy.get('form').submit() cy.wait('@malformedLogin') cy.get('[role="alert"]').should( 'have.text', 'Unable to log in. Please try again.', ) }) it('disables the form while login is pending', () => { cy.intercept('POST', '**/api/login', { delay: 500, statusCode: 200, body: { user: authenticatedUser }, }).as('login') cy.visit('/login') fillLoginForm() cy.get('form').submit() cy.get('button[type="submit"]') .should('be.disabled') .and('have.text', 'Logging in...') cy.get('#login-email').should('be.disabled') cy.get('#login-password').should('be.disabled') cy.wait('@login') }) })