diff --git a/frontend/website/cypress/e2e/confirm-email.cy.ts b/frontend/website/cypress/e2e/confirm-email.cy.ts new file mode 100644 index 0000000..075909d --- /dev/null +++ b/frontend/website/cypress/e2e/confirm-email.cy.ts @@ -0,0 +1,77 @@ +const authenticatedUser = { + id: 7, + email: 'user@example.com', +} + +describe('email confirmation', () => { + beforeEach(() => { + cy.intercept('GET', '**/api/me', { + statusCode: 401, + body: { error: 'unauthenticated' }, + }).as('me') + }) + + it('chooses a password, confirms the account, and opens the dashboard', () => { + cy.intercept('POST', '**/api/confirm-email', (request) => { + expect(request.headers.accept).to.equal('application/json') + expect(request.body).to.deep.equal({ + token: 'confirmation-token', + password: 'password123', + }) + request.reply({ + statusCode: 200, + body: { user: authenticatedUser }, + }) + }).as('confirmEmail') + + cy.visit('/confirm-email?token=confirmation-token') + cy.get('#confirm-email-password').type('password123') + cy.get('#confirm-email-password-confirmation').type('password123') + cy.get('form').submit() + cy.wait('@confirmEmail') + + cy.location('pathname').should('equal', '/dashboard') + cy.get('h1').should('have.text', 'Your next step starts here.') + }) + + it('validates password length and confirmation before submitting', () => { + cy.intercept('POST', '**/api/confirm-email').as('confirmEmail') + + cy.visit('/confirm-email?token=confirmation-token') + cy.get('#confirm-email-password').type('short') + cy.get('#confirm-email-password-confirmation').type('different') + cy.get('form').submit() + + cy.get('#confirm-email-password-error') + .should('have.text', 'Password must be at least 8 characters.') + .and('be.visible') + cy.get('#confirm-email-password-confirmation-error') + .should('have.text', 'Passwords do not match.') + .and('be.visible') + cy.get('@confirmEmail.all').should('have.length', 0) + }) + + it('shows confirmation errors from the backend', () => { + cy.intercept('POST', '**/api/confirm-email', { + statusCode: 409, + body: { error: 'token expired' }, + }).as('confirmEmail') + + cy.visit('/confirm-email?token=expired-token') + cy.get('#confirm-email-password').type('password123') + cy.get('#confirm-email-password-confirmation').type('password123') + cy.get('form').submit() + cy.wait('@confirmEmail') + + cy.get('[role="alert"]') + .should('have.text', 'token expired') + .and('be.visible') + cy.location('pathname').should('equal', '/confirm-email') + }) + + it('redirects a confirmation route without a token to signup', () => { + cy.visit('/confirm-email') + + cy.location('pathname').should('equal', '/signup') + }) +}) diff --git a/frontend/website/cypress/e2e/guest-auth.cy.ts b/frontend/website/cypress/e2e/guest-auth.cy.ts index 2dea54b..7c0e66c 100644 --- a/frontend/website/cypress/e2e/guest-auth.cy.ts +++ b/frontend/website/cypress/e2e/guest-auth.cy.ts @@ -42,28 +42,18 @@ describe('guest authentication pages', () => { cy.visit('/signup') cy.get('h1').should('have.text', 'Start your journey') - cy.get('label[for="signup-name"]').should('have.text', 'Full name') - cy.get('#signup-name').should('have.attr', 'autocomplete', 'name') cy.get('label[for="signup-email"]').should('have.text', 'Email address') cy.get('#signup-email').should('have.attr', 'autocomplete', 'email') - cy.get('label[for="signup-password"]').should('have.text', 'Password') - cy.get('#signup-password').should('have.attr', 'autocomplete', 'new-password') - cy.get('label[for="signup-password-confirmation"]').should( - 'have.text', - 'Confirm password', - ) - cy.get('#signup-password-confirmation').should( - 'have.attr', - 'autocomplete', - 'new-password', - ) - cy.get('button[type="submit"]').should('have.text', 'Create account') + cy.get('#signup-name').should('not.exist') + cy.get('#signup-password').should('not.exist') + cy.get('#signup-password-confirmation').should('not.exist') + cy.get('button[type="submit"]').should('have.text', 'Continue with email') cy.contains('a', 'Log in').click() cy.location('pathname').should('equal', '/login') }) - it('keeps UI-only form submissions on their current route', () => { + it('keeps invalid form submissions on their current route', () => { cy.visit('/login') cy.get('form').submit() cy.location('pathname').should('equal', '/login') diff --git a/frontend/website/cypress/e2e/signup.cy.ts b/frontend/website/cypress/e2e/signup.cy.ts new file mode 100644 index 0000000..6496499 --- /dev/null +++ b/frontend/website/cypress/e2e/signup.cy.ts @@ -0,0 +1,63 @@ +describe('email signup', () => { + beforeEach(() => { + cy.intercept('GET', '**/api/me', { + statusCode: 401, + body: { error: 'unauthenticated' }, + }).as('me') + }) + + it('requests a confirmation email and shows the check-email page', () => { + cy.intercept('POST', '**/api/signup', (request) => { + expect(request.headers.accept).to.equal('application/json') + expect(request.body).to.deep.equal({ email: 'user@example.com' }) + request.reply({ statusCode: 201 }) + }).as('signup') + + cy.visit('/signup') + cy.get('#signup-email').type(' user@example.com ') + cy.get('form').submit() + cy.wait('@signup') + + cy.location('pathname').should('equal', '/check-email') + cy.get('h1').should('have.text', 'Check your email') + cy.contains('We sent you a link to confirm your signup.').should('be.visible') + }) + + it('validates the email before submitting', () => { + cy.intercept('POST', '**/api/signup').as('signup') + + cy.visit('/signup') + cy.get('#signup-email').type('not-an-email') + cy.get('form').submit() + + cy.get('#signup-email-error') + .should('have.text', 'Enter a valid email address.') + .and('be.visible') + cy.get('#signup-email').should('have.attr', 'aria-invalid', 'true') + cy.get('@signup.all').should('have.length', 0) + cy.location('pathname').should('equal', '/signup') + }) + + it('shows backend signup errors', () => { + cy.intercept('POST', '**/api/signup', { + statusCode: 409, + body: { error: 'user@example.com already has an account' }, + }).as('signup') + + cy.visit('/signup') + cy.get('#signup-email').type('user@example.com') + cy.get('form').submit() + cy.wait('@signup') + + cy.get('[role="alert"]') + .should('have.text', 'user@example.com already has an account') + .and('be.visible') + cy.location('pathname').should('equal', '/signup') + }) + + it('redirects direct check-email visits back to signup', () => { + cy.visit('/check-email') + + cy.location('pathname').should('equal', '/signup') + }) +})