1
0
Fork 0

wip(backend): more progress on account endpoints and tests

This commit is contained in:
joostdecock 2022-11-11 18:02:28 +01:00
parent f8e1fed09a
commit bccf4a35ee
7 changed files with 395 additions and 74 deletions

View file

@ -8,6 +8,13 @@ dotenv.config()
export const port = process.env.API_PORT || 3000 export const port = process.env.API_PORT || 3000
export const api = process.env.API_URL || `http://localhost:${port}` export const api = process.env.API_URL || `http://localhost:${port}`
// All environment variables are strings
// This is a helper method to turn them into a boolean
const envToBool = (input = 'no') => {
if (['yes', '1', 'true'].includes(input.toLowerCase())) return true
return false
}
// Construct config object // Construct config object
const config = { const config = {
api, api,
@ -20,9 +27,10 @@ const config = {
url: process.env.API_DB_URL, url: process.env.API_DB_URL,
}, },
tests: { tests: {
allow: process.env.ALLOW_UNITTESTS || false, allow: envToBool(process.env.ALLOW_UNITTESTS),
domain: process.env.TESTDOMAIN || 'freesewing.dev', domain: process.env.TESTDOMAIN || 'freesewing.dev',
sendEmail: process.env.SEND_UNITTEST_EMAILS || false, sendEmail: envToBool(process.env.SEND_UNITTEST_EMAILS),
includeSanity: envToBool(process.env.INCLUDE_SANITY_TESTS),
}, },
static: process.env.API_STATIC, static: process.env.API_STATIC,
storage: process.env.API_STORAGE, storage: process.env.API_STORAGE,

View file

@ -1,6 +1,7 @@
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import { log } from '../utils/log.mjs' import { log } from '../utils/log.mjs'
import { hash, hashPassword, randomString, verifyPassword } from '../utils/crypto.mjs' import { hash, hashPassword, randomString, verifyPassword } from '../utils/crypto.mjs'
import { setPersonAvatar } from '../utils/sanity.mjs'
import { clean, asJson, i18nUrl } from '../utils/index.mjs' import { clean, asJson, i18nUrl } from '../utils/index.mjs'
import { ConfirmationModel } from './confirmation.mjs' import { ConfirmationModel } from './confirmation.mjs'
@ -343,45 +344,31 @@ UserModel.prototype.unsafeUpdate = async function (body) {
notes.push('usernameChangeRejected') notes.push('usernameChangeRejected')
} }
} }
// Image (img)
if (typeof body.img === 'string') {
const img = await setPersonAvatar(this.record.id, body.img)
data.img = img.url
}
// Now update the record // Now update the record
await this.safeUpdate(this.cloak(data)) await this.safeUpdate(this.cloak(data))
// Email change requires confirmation const isUnitTest = this.isUnitTest(body)
if (typeof body.email === 'string' && this.clear.email !== clean(body.email)) { if (typeof body.email === 'string' && this.clear.email !== clean(body.email)) {
if (typeof body.confirmation === 'string') { // Email change (requires confirmation)
// Retrieve confirmation record this.confirmation = await this.Confirmation.create({
await this.Confirmation.read({ id: body.confirmation }) type: 'emailchange',
data: {
if (!this.Confirmation.exists) { language: this.record.language,
log.warn(err, `Could not find confirmation id ${params.id}`) email: {
return this.setResponse(404, 'failedToFindConfirmationId') current: this.clear.email,
} new: body.email,
if (this.Confirmation.record.type !== 'emailchange') {
log.warn(err, `Confirmation mismatch; ${params.id} is not an emailchange id`)
return this.setResponse(404, 'confirmationIdTypeMismatch')
}
const data = this.Confirmation.clear.data
if (data.email.current === this.clear.email && typeof data.email.new === 'string') {
await this.saveUpdate({
email: this.encrypt(data.email),
})
}
} else {
// Create confirmation for email change
this.confirmation = await this.Confirmation.create({
type: 'emailchange',
data: {
language: this.record.language,
email: {
current: this.clear.email,
new: body.email,
},
}, },
userId: this.record.id, },
}) userId: this.record.id,
})
console.log(this.config.tests)
if (!isUnitTest || this.config.tests.sendEmail) {
// Send confirmation email // Send confirmation email
await this.mailer.send({ await this.mailer.send({
template: 'emailchange', template: 'emailchange',
@ -395,12 +382,36 @@ UserModel.prototype.unsafeUpdate = async function (body) {
}, },
}) })
} }
} else if (typeof body.confirmation === 'string' && body.confirm === 'emailchange') {
// Handle email change confirmation
await this.Confirmation.read({ id: body.confirmation })
if (!this.Confirmation.exists) {
log.warn(err, `Could not find confirmation id ${params.id}`)
return this.setResponse(404, 'failedToFindConfirmationId')
}
if (this.Confirmation.record.type !== 'emailchange') {
log.warn(err, `Confirmation mismatch; ${params.id} is not an emailchange id`)
return this.setResponse(404, 'confirmationIdTypeMismatch')
}
const data = this.Confirmation.clear.data
if (data.email.current === this.clear.email && typeof data.email.new === 'string') {
await this.safeUpdate({
email: this.encrypt(data.email.new),
ehash: hash(clean(data.email.new)),
})
}
} }
return this.setResponse(200, false, { const returnData = {
result: 'success', result: 'success',
account: this.asAccount(), account: this.asAccount(),
}) }
if (isUnitTest) returnData.confirmation = this.Confirmation.record.id
return this.setResponse(200, false, returnData)
} }
/* /*
@ -415,6 +426,7 @@ UserModel.prototype.asAccount = function () {
data: this.clear.data, data: this.clear.data,
email: this.clear.email, email: this.clear.email,
github: this.clear.github, github: this.clear.github,
img: this.record.img,
imperial: this.record.imperial, imperial: this.record.imperial,
initial: this.clear.initial, initial: this.clear.initial,
language: this.record.language, language: this.record.language,
@ -481,7 +493,11 @@ UserModel.prototype.sendResponse = async function (res) {
* part of a unit test * part of a unit test
*/ */
UserModel.prototype.isUnitTest = function (body) { UserModel.prototype.isUnitTest = function (body) {
return body.unittest && this.clear.email.split('@').pop() === this.config.tests.domain if (!body.unittest) return false
if (!this.clear.email.split('@').pop() === this.config.tests.domain) return false
if (body.email && !body.email.split('@').pop() === this.config.tests.domain) return false
return true
} }
/* /*
@ -519,7 +535,6 @@ UserModel.prototype.loginOk = function () {
*/ */
UserModel.prototype.isLusernameAvailable = async function (lusername) { UserModel.prototype.isLusernameAvailable = async function (lusername) {
if (lusername.length < 2) return false if (lusername.length < 2) return false
if (lusername.slice(0, 5) === 'user-') return false
let found let found
try { try {
found = await this.prisma.user.findUnique({ where: { lusername } }) found = await this.prisma.user.findUnique({ where: { lusername } })

View file

@ -24,7 +24,7 @@ export function userRoutes(tools) {
// Update account // Update account
app.put('/account/jwt', passport.authenticate(...jwt), (req, res) => User.update(req, res, tools)) app.put('/account/jwt', passport.authenticate(...jwt), (req, res) => User.update(req, res, tools))
app.put('/account/key', passport.authenticate(...jwt), (req, res) => User.update(req, res, tools)) app.put('/account/key', passport.authenticate(...bsc), (req, res) => User.update(req, res, tools))
/* /*

View file

@ -91,5 +91,5 @@ function b64ToBinaryWithType(dataUri) {
else if (start.includes('image/jpg')) type = 'image/jpeg' else if (start.includes('image/jpg')) type = 'image/jpeg'
else if (start.includes('image/jpeg')) type = 'image/jpeg' else if (start.includes('image/jpeg')) type = 'image/jpeg'
return [type, new Buffer(data, 'base64')] return [type, new Buffer.from(data, 'base64')]
} }

View file

@ -1,36 +1,331 @@
export const accountTests = async (chai, config, expect, store) => { import { cat } from './cat.mjs'
/*
consent Int @default(0)
data String @default("{}")
ehash String @unique
email String
newsletter Boolean @default(false)
password String
username String
lusername String @unique
*/
describe(`${store.icon('user')} Update account data`, async function () { export const accountTests = async (chai, config, expect, store) => {
it(`${store.icon('user')} Should update consent to 3 (jwt)`, (done) => { const data = {
chai jwt: {
.request(config.api) bio: "I know it sounds funny but I just can't stand the pain",
.put('/account/jwt') consent: 1,
.set('Authorization', 'Bearer ' + store.account.token) github: 'sorchanidhubhghaill',
.send({ imperial: true,
consent: 3, language: 'es',
data: { newsletter: true,
banana: 'Sure', },
}, key: {
newsletter: true, bio: "It's a long way to the top, if you wanna rock & roll",
password: 'Something new', consent: 2,
username: 'new', github: 'joostdecock',
}) imperial: true,
.end((err, res) => { language: 'de',
expect(err === null).to.equal(true) newsletter: true,
expect(res.status).to.equal(200) },
expect(res.body.result).to.equal(`success`) }
done()
for (const auth in data) {
describe(`${store.icon('user', auth)} Update account data (${auth})`, async function () {
for (const [field, val] of Object.entries(data[auth])) {
it(`${store.icon('user', auth)} Should update ${field} (${auth})`, (done) => {
const body = {}
body[field] = val
chai
.request(config.api)
.put(`/account/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(
`${store.account.apikey.key}:${store.account.apikey.secret}`
).toString('base64')
)
.send(body)
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(res.body.account[field]).to.equal(val)
done()
})
}) })
}
// Update password - Check with login
const password = store.randomString()
it(`${store.icon('user', auth)} Should update the password (${auth})`, (done) => {
const body = {}
chai
.request(config.api)
.put(`/account/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.send({ password })
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
done()
})
})
it(`${store.icon(
'user',
auth
)} Should be able to login with the updated password (${auth})`, (done) => {
const body = {}
chai
.request(config.api)
.post(`/login`)
.send({
username: store.account.username,
password,
})
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
done()
})
})
it(`${store.icon('user', auth)} Better restore the original password (${auth})`, (done) => {
const body = {}
chai
.request(config.api)
.put(`/account/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.send({ password: store.account.password })
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
done()
})
})
it(`${store.icon(
'user',
auth
)} Should be able to login with the original password (${auth})`, (done) => {
const body = {}
chai
.request(config.api)
.post(`/login`)
.send({
username: store.account.username,
password: store.account.password,
})
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
done()
})
})
// Update username - Should also update lusername
const username = store.randomString().toUpperCase()
it(`${store.icon('user', auth)} Update username (${auth})`, (done) => {
chai
.request(config.api)
.put(`/account/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.send({ username })
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(res.body.account.username).to.equal(username)
expect(res.body.account.lusername).to.equal(username.toLowerCase())
done()
})
})
it(`${store.icon('user', auth)} Restore original username (${auth})`, (done) => {
chai
.request(config.api)
.put(`/account/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.send({ username: store.account.username })
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(res.body.account.username).to.equal(store.account.username)
expect(res.body.account.lusername).to.equal(store.account.username.toLowerCase())
done()
})
})
if (store.config.tests.includeSanity) {
it(`${store.icon('user', auth)} Should update the account img (${auth})`, (done) => {
chai
.request(config.api)
.put(`/account/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(
`${store.account.apikey.key}:${store.account.apikey.secret}`
).toString('base64')
)
.send({ img: cat })
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(typeof res.body.account.img).to.equal('string')
done()
})
}).timeout(5000)
}
let confirmation
step(
`${store.icon('user', auth)} Should update the account email address (${auth})`,
(done) => {
chai
.request(config.api)
.put(`/account/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(
`${store.account.apikey.key}:${store.account.apikey.secret}`
).toString('base64')
)
.send({
email: `updating_${store.randomString()}@${store.config.tests.domain}`,
unittest: true,
})
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(typeof res.body.account.img).to.equal('string')
confirmation = res.body.confirmation
done()
})
}
)
step(`${store.icon('user', auth)} Should confirm the email change (${auth})`, (done) => {
chai
.request(config.api)
.put(`/account/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.send({
confirm: 'emailchange',
confirmation,
})
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(typeof res.body.account.img).to.equal('string')
confirmation = res.body.confirmation
done()
})
})
step(`${store.icon('user', auth)} Restore email address (${auth})`, (done) => {
chai
.request(config.api)
.put(`/account/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.send({
email: store.account.email,
unittest: true,
})
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(typeof res.body.account.img).to.equal('string')
confirmation = res.body.confirmation
done()
})
})
step(
`${store.icon('user', auth)} Should confirm the (restore) email change (${auth})`,
(done) => {
chai
.request(config.api)
.put(`/account/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(
`${store.account.apikey.key}:${store.account.apikey.secret}`
).toString('base64')
)
.send({
confirm: 'emailchange',
confirmation,
})
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(typeof res.body.account.img).to.equal('string')
confirmation = res.body.confirmation
done()
})
}
)
}) })
}) }
} }

File diff suppressed because one or more lines are too long

View file

@ -25,8 +25,9 @@ export const setup = async () => {
icons: { icons: {
user: '🧑 ', user: '🧑 ',
jwt: '🎫 ', jwt: '🎫 ',
key: '🎟️ ', key: '🎟️ ',
}, },
randomString,
} }
store.icon = (icon1, icon2 = false) => store.icons[icon1] + (icon2 ? store.icons[icon2] : '') store.icon = (icon1, icon2 = false) => store.icons[icon1] + (icon2 ? store.icons[icon2] : '')