Files
overleaf-cep/services/web/test/acceptance/src/RegistrationTests.mjs
Jakob Ackermann 17e01526b4 [web] fix typo in i18n key (#31713)
* [web] fix typo in i18n key

* [web] sort locales

GitOrigin-RevId: 9736eae24cdbd7aabd995087de368c667b25b849
2026-02-24 09:07:18 +00:00

339 lines
10 KiB
JavaScript

import { expect } from 'chai'
import metrics from './helpers/metrics.mjs'
import User from './helpers/User.mjs'
import redis from './helpers/redis.mjs'
import Features from '../../../app/src/infrastructure/Features.mjs'
const UserPromises = User.promises
// Expectations
async function expectProjectAccess(user, projectId) {
// should have access to project
await user.openProject(projectId)
}
async function expectNoProjectAccess(user, projectId) {
// should not have access to project page
let error = null
try {
await user.openProject(projectId)
} catch (err) {
error = err
}
expect(error).to.be.instanceof(Error)
}
// Actions
async function tryLoginThroughRegistrationForm(user, email, password) {
await user.getCsrfToken()
return await user.doRequest('POST', {
url: '/register',
json: { email, password },
})
}
describe('Registration', function () {
describe('LoginRateLimit', function () {
let userA
beforeEach(function () {
userA = new UserPromises()
})
function loginRateLimited(line) {
return line.includes('rate_limit_hit') && line.includes('login')
}
async function getLoginRateLimitHitMetricValue() {
return await metrics.promises.getMetric(loginRateLimited)
}
let beforeCount
beforeEach('get baseline metric value', async function () {
beforeCount = await getLoginRateLimitHitMetricValue()
})
beforeEach('setup csrf token', async function () {
await userA.getCsrfToken()
})
describe('pushing an account just below the rate limit', function () {
async function doLoginAttempts(user, n, pushInto) {
while (n--) {
const { body } = await user.doRequest('POST', {
url: '/login',
json: {
email: user.email,
password: 'invalid-password',
'g-recaptcha-response': 'valid',
},
})
const message = body && body.message && body.message.key
pushInto.push(message)
}
}
let results = []
beforeEach('do 9 login attempts', async function () {
results = []
await doLoginAttempts(userA, 9, results)
})
it('should not record any rate limited requests', async function () {
const afterCount = await getLoginRateLimitHitMetricValue()
expect(afterCount).to.equal(beforeCount)
})
it('should produce the correct responses so far', function () {
expect(results.length).to.equal(9)
expect(results).to.deep.equal(
Array(9).fill('invalid-password-retry-or-reset')
)
})
describe('pushing the account past the limit', function () {
beforeEach('do 6 login attempts', async function () {
await doLoginAttempts(userA, 6, results)
})
it('should record 5 rate limited requests', async function () {
const afterCount = await getLoginRateLimitHitMetricValue()
expect(afterCount).to.equal(beforeCount + 5)
})
it('should produce the correct responses', function () {
expect(results.length).to.equal(15)
expect(results).to.deep.equal(
Array(10)
.fill('invalid-password-retry-or-reset')
.concat(Array(5).fill('too-many-login-requests-2-mins'))
)
})
describe('logging in with another user', function () {
let userB
beforeEach(function () {
userB = new UserPromises()
})
beforeEach('update baseline metric value', async function () {
beforeCount = await getLoginRateLimitHitMetricValue()
})
beforeEach('setup csrf token', async function () {
await userB.getCsrfToken()
})
let messages = []
beforeEach('do bad login', async function () {
messages = []
await doLoginAttempts(userB, 1, messages)
})
it('should not rate limit their request', function () {
expect(messages).to.deep.equal(['invalid-password-retry-or-reset'])
})
it('should not record any further rate limited requests', async function () {
const afterCount = await getLoginRateLimitHitMetricValue()
expect(afterCount).to.equal(beforeCount)
})
})
})
describe('performing a valid login for clearing the limit', function () {
beforeEach('do login', async function () {
await userA.login()
})
it('should log the user in', async function () {
const { response } = await userA.doRequest('GET', '/project')
expect(response.statusCode).to.equal(200)
})
it('should not record any rate limited requests', async function () {
const afterCount = await getLoginRateLimitHitMetricValue()
expect(afterCount).to.equal(beforeCount)
})
describe('logging out and performing more invalid login requests', function () {
beforeEach('logout', async function () {
await userA.logout()
})
beforeEach('fetch new csrf token', async function () {
await userA.getCsrfToken()
})
let results = []
beforeEach('do 9 login attempts', async function () {
results = []
await doLoginAttempts(userA, 9, results)
})
it('should not record any rate limited requests yet', async function () {
const afterCount = await getLoginRateLimitHitMetricValue()
expect(afterCount).to.equal(beforeCount)
})
it('should not emit any rate limited responses yet', function () {
expect(results.length).to.equal(9)
expect(results).to.deep.equal(
Array(9).fill('invalid-password-retry-or-reset')
)
})
})
})
})
})
describe('CSRF protection', function () {
before(function () {
if (!Features.hasFeature('registration')) {
this.skip()
}
})
beforeEach(function () {
this.user = new UserPromises()
this.email = `test+${Math.random()}@example.com`
this.password = 'password11'
})
afterEach(async function () {
await this.user.fullDeleteUser(this.email)
})
it('should register with the csrf token', async function () {
await this.user.request.get('/login')
await this.user.getCsrfToken()
const { response } = await this.user.doRequest('POST', {
url: '/register',
json: {
email: this.email,
password: this.password,
},
headers: {
'x-csrf-token': this.user.csrfToken,
},
})
expect(response.statusCode).to.equal(200)
})
it('should fail with no csrf token', async function () {
await this.user.request.get('/login')
await this.user.getCsrfToken()
const { response } = await this.user.doRequest('POST', {
url: '/register',
json: {
email: this.email,
password: this.password,
},
headers: {
'x-csrf-token': '',
},
})
expect(response.statusCode).to.equal(403)
})
it('should fail with a stale csrf token', async function () {
await this.user.request.get('/login')
await this.user.getCsrfToken()
const oldCsrfToken = this.user.csrfToken
await this.user.logout()
const { response } = await this.user.doRequest('POST', {
url: '/register',
json: {
email: this.email,
password: this.password,
},
headers: {
'x-csrf-token': oldCsrfToken,
},
})
expect(response.statusCode).to.equal(403)
})
})
describe('Register', function () {
before(function () {
if (!Features.hasFeature('registration')) {
this.skip()
}
})
beforeEach(function () {
this.user = new UserPromises()
})
it('Set emails attribute', async function () {
const user = await this.user.register()
expect(user.email).to.equal(this.user.email)
expect(user.emails).to.exist
expect(user.emails).to.be.a('array')
expect(user.emails.length).to.equal(1)
expect(user.emails[0].email).to.equal(this.user.email)
})
})
describe('LoginViaRegistration', function () {
beforeEach(async function () {
this.timeout(60000)
this.user1 = new UserPromises()
this.user2 = new UserPromises()
await this.user1.login()
await this.user1.logout()
await redis.clearUserSessions(this.user1)
await this.user2.login()
await this.user2.logout()
await redis.clearUserSessions(this.user2)
this.project_id = null
})
describe('[Security] Trying to register/login as another user', function () {
before(function () {
if (!Features.hasFeature('registration')) {
this.skip()
}
})
it('should not allow sign in with secondary email', async function () {
const secondaryEmail = 'acceptance-test-secondary@example.com'
await this.user1.addEmail(secondaryEmail)
let error = null
try {
await this.user1.loginWith(secondaryEmail)
} catch (err) {
error = err
}
expect(error).to.be.instanceOf(Error)
expect(error.message).to.match(/login failed: status=401/)
expect(error.info.body).to.deep.equal({
message: {
type: 'error',
key: 'invalid-password-retry-or-reset',
},
})
const isLoggedIn = await this.user1.isLoggedIn()
expect(isLoggedIn).to.equal(false)
})
it('should have user1 login and create a project, which user2 cannot access', async function () {
// user1 logs in and creates a project which only they can access
await this.user1.login()
const projectId = await this.user1.createProject('Private Project')
await expectProjectAccess(this.user1, projectId)
await expectNoProjectAccess(this.user2, projectId)
// should prevent user2 from login/register with user1 email address
const { body } = await tryLoginThroughRegistrationForm(
this.user2,
this.user1.email,
'totally_not_the_right_password'
)
expect(body.redir != null).to.equal(false)
expect(body.message != null).to.equal(true)
expect(body.message).to.have.all.keys('type', 'text')
expect(body.message.type).to.equal('error')
// check user still can't access the project
await expectNoProjectAccess(this.user2, projectId)
})
})
})
})