1
0
Fork 0

wip(backend): More work on backend/tests

This commit is contained in:
Joost De Cock 2022-11-16 15:07:24 +01:00
parent 2297b61d20
commit b9bb96d837
22 changed files with 905 additions and 101 deletions

View file

@ -7,7 +7,6 @@
"author": "Joost De Cock <joost@joost.at> (https://github.com/joostdecock)",
"homepage": "https://freesewing.org/",
"repository": "github:freesewing/freesewing",
"license": "MIT",
"bugs": {
"url": "https://github.com/freesewing/freesewing/issues"
},
@ -30,10 +29,13 @@
"crypto": "^1.0.1",
"express": "4.18.2",
"mustache": "^4.2.0",
"passport": "^0.6.0",
"passport-http": "^0.3.0",
"passport-jwt": "^4.0.0",
"pino": "^8.7.0"
},
"devDependencies": {
"chai-http": "^4.3.0",
"mocha": "^10.1.0",
"mocha-steps": "^1.3.0",
"prisma": "4.5.0"

View file

@ -73,9 +73,12 @@ model Pattern {
data String
design String
img String?
name String @default("")
notes String
person Person? @relation(fields: [personId], references: [id])
personId Int?
notes String
public Boolean @default(false)
settings String
user User @relation(fields: [userId], references: [id])
userId Int
updatedAt DateTime @updatedAt

Binary file not shown.

View file

@ -4,7 +4,7 @@ import { log } from '../utils/log.mjs'
import { ApikeyModel } from '../models/apikey.mjs'
import { UserModel } from '../models/user.mjs'
export function ApikeyController() {}
export function ApikeysController() {}
/*
* Create API key
@ -12,7 +12,7 @@ export function ApikeyController() {}
* This is the endpoint that handles creation of API keys/tokens
* See: https://freesewing.dev/reference/backend/api/apikey
*/
ApikeyController.prototype.create = async (req, res, tools) => {
ApikeysController.prototype.create = async (req, res, tools) => {
const Apikey = new ApikeyModel(tools)
await Apikey.create(req)
@ -25,7 +25,7 @@ ApikeyController.prototype.create = async (req, res, tools) => {
* This is the endpoint that handles creation of API keys/tokens
* See: https://freesewing.dev/reference/backend/api/apikey
*/
ApikeyController.prototype.read = async (req, res, tools) => {
ApikeysController.prototype.read = async (req, res, tools) => {
const Apikey = new ApikeyModel(tools)
await Apikey.guardedRead(req)
@ -39,7 +39,7 @@ ApikeyController.prototype.read = async (req, res, tools) => {
* request
* See: https://freesewing.dev/reference/backend/api/apikey
*/
ApikeyController.prototype.whoami = async (req, res, tools) => {
ApikeysController.prototype.whoami = async (req, res, tools) => {
const User = new UserModel(tools)
const Apikey = new ApikeyModel(tools)
@ -69,7 +69,7 @@ ApikeyController.prototype.whoami = async (req, res, tools) => {
* This is the endpoint that handles removal of API keys/tokens
* See: https://freesewing.dev/reference/backend/api/apikey
*/
ApikeyController.prototype.delete = async (req, res, tools) => {
ApikeysController.prototype.delete = async (req, res, tools) => {
const Apikey = new ApikeyModel(tools)
await Apikey.guardedDelete(req)

View file

@ -0,0 +1,58 @@
import { PatternModel } from '../models/pattern.mjs'
export function PatternsController() {}
/*
* Create a pattern
* See: https://freesewing.dev/reference/backend/api
*/
PatternsController.prototype.create = async (req, res, tools) => {
const Pattern = new PatternModel(tools)
await Pattern.guardedCreate(req)
return Pattern.sendResponse(res)
}
/*
* Read a pattern
* See: https://freesewing.dev/reference/backend/api
*/
PatternsController.prototype.read = async (req, res, tools) => {
const Pattern = new PatternModel(tools)
await Pattern.guardedRead(req)
return Pattern.sendResponse(res)
}
/*
* Update a pattern
* See: https://freesewing.dev/reference/backend/api
*/
PatternsController.prototype.update = async (req, res, tools) => {
const Pattern = new PatternModel(tools)
await Pattern.guardedUpdate(req)
return Pattern.sendResponse(res)
}
/*
* Remove a pattern
* See: https://freesewing.dev/reference/backend/api
*/
PatternsController.prototype.delete = async (req, res, tools) => {
const Pattern = new PatternModel(tools)
await Pattern.guardedDelete(req)
return Pattern.sendResponse(res)
}
/*
* Clone a pattern
* See: https://freesewing.dev/reference/backend/api
*/
PatternsController.prototype.clone = async (req, res, tools) => {
const Pattern = new PatternModel(tools)
await Pattern.guardedClone(req)
return Pattern.sendResponse(res)
}

View file

@ -1,12 +1,12 @@
import { PersonModel } from '../models/person.mjs'
export function PersonController() {}
export function PeopleController() {}
/*
* Create a person for the authenticated user
* See: https://freesewing.dev/reference/backend/api
*/
PersonController.prototype.create = async (req, res, tools) => {
PeopleController.prototype.create = async (req, res, tools) => {
const Person = new PersonModel(tools)
await Person.guardedCreate(req)
@ -17,7 +17,7 @@ PersonController.prototype.create = async (req, res, tools) => {
* Read a person
* See: https://freesewing.dev/reference/backend/api
*/
PersonController.prototype.read = async (req, res, tools) => {
PeopleController.prototype.read = async (req, res, tools) => {
const Person = new PersonModel(tools)
await Person.guardedRead(req)
@ -28,7 +28,7 @@ PersonController.prototype.read = async (req, res, tools) => {
* Update a person
* See: https://freesewing.dev/reference/backend/api
*/
PersonController.prototype.update = async (req, res, tools) => {
PeopleController.prototype.update = async (req, res, tools) => {
const Person = new PersonModel(tools)
await Person.guardedUpdate(req)
@ -39,7 +39,7 @@ PersonController.prototype.update = async (req, res, tools) => {
* Remove a person
* See: https://freesewing.dev/reference/backend/api
*/
PersonController.prototype.delete = async (req, res, tools) => {
PeopleController.prototype.delete = async (req, res, tools) => {
const Person = new PersonModel(tools)
await Person.guardedDelete(req)
@ -50,7 +50,7 @@ PersonController.prototype.delete = async (req, res, tools) => {
* Clone a person
* See: https://freesewing.dev/reference/backend/api
*/
PersonController.prototype.clone = async (req, res, tools) => {
PeopleController.prototype.clone = async (req, res, tools) => {
const Person = new PersonModel(tools)
await Person.guardedClone(req)

View file

@ -1,6 +1,6 @@
import { UserModel } from '../models/user.mjs'
export function UserController() {}
export function UsersController() {}
/*
* Signup
@ -8,7 +8,7 @@ export function UserController() {}
* This is the endpoint that handles account signups
* See: https://freesewing.dev/reference/backend/api
*/
UserController.prototype.signup = async (req, res, tools) => {
UsersController.prototype.signup = async (req, res, tools) => {
const User = new UserModel(tools)
await User.guardedCreate(req)
@ -21,7 +21,7 @@ UserController.prototype.signup = async (req, res, tools) => {
* This is the endpoint that fully unlocks the account if the user gives their consent
* See: https://freesewing.dev/reference/backend/api
*/
UserController.prototype.confirm = async (req, res, tools) => {
UsersController.prototype.confirm = async (req, res, tools) => {
const User = new UserModel(tools)
await User.confirm(req)
@ -34,7 +34,7 @@ UserController.prototype.confirm = async (req, res, tools) => {
* This is the endpoint that provides traditional username/password login
* See: https://freesewing.dev/reference/backend/api
*/
UserController.prototype.login = async function (req, res, tools) {
UsersController.prototype.login = async function (req, res, tools) {
const User = new UserModel(tools)
await User.passwordLogin(req)
@ -46,7 +46,7 @@ UserController.prototype.login = async function (req, res, tools) {
*
* See: https://freesewing.dev/reference/backend/api
*/
UserController.prototype.whoami = async (req, res, tools) => {
UsersController.prototype.whoami = async (req, res, tools) => {
const User = new UserModel(tools)
await User.guardedRead({ id: req.user.uid }, req)
@ -58,7 +58,7 @@ UserController.prototype.whoami = async (req, res, tools) => {
*
* See: https://freesewing.dev/reference/backend/api
*/
UserController.prototype.update = async (req, res, tools) => {
UsersController.prototype.update = async (req, res, tools) => {
const User = new UserModel(tools)
await User.guardedRead({ id: req.user.uid }, req)
await User.guardedUpdate(req)

View file

@ -0,0 +1,317 @@
import { log } from '../utils/log.mjs'
import { setPatternAvatar } from '../utils/sanity.mjs'
export function PatternModel(tools) {
this.config = tools.config
this.prisma = tools.prisma
this.decrypt = tools.decrypt
this.encrypt = tools.encrypt
this.encryptedFields = ['data', 'img', 'name', 'notes', 'settings']
this.clear = {} // For holding decrypted data
return this
}
/*
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
data String
design String
img String?
person Person? @relation(fields: [personId], references: [id])
personId Int?
name String @default("")
notes String
public
settings String
user User @relation(fields: [userId], references: [id])
userId Int
updatedAt DateTime @updatedAt
*/
PatternModel.prototype.guardedCreate = async function ({ body, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel')
if (Object.keys(body) < 2) return this.setResponse(400, 'postBodyMissing')
if (!body.person) return this.setResponse(400, 'personMissing')
if (typeof body.person !== 'number') return this.setResponse(400, 'personNotNumeric')
if (typeof body.settings !== 'object') return this.setResponse(400, 'settingsNotAnObject')
if (body.data && typeof body.data !== 'object') return this.setResponse(400, 'dataNotAnObject')
if (!body.design && !body.data?.design) return this.setResponse(400, 'designMissing')
if (typeof body.design !== 'string') return this.setResponse(400, 'designNotStringy')
// Prepare data
const data = {
design: body.design,
personId: body.person,
settings: body.settings,
}
// Data (will be encrypted, so always set _some_ value)
if (typeof body.data === 'object') data.data = body.data
else data.data = {}
// Name (will be encrypted, so always set _some_ value)
if (typeof body.name === 'string' && body.name.length > 0) data.name = body.name
else data.name = '--'
// Notes (will be encrypted, so always set _some_ value)
if (typeof body.notes === 'string' && body.notes.length > 0) data.notes = body.notes
else data.notes = '--'
// Public
if (body.public === true) data.public = true
data.userId = user.uid
// Set this one initially as we need the ID to create a custom img via Sanity
data.img = this.config.avatars.pattern
// Create record
await this.unguardedCreate(data)
// Update img? (now that we have the ID)
const img =
this.config.use.sanity &&
typeof body.img === 'string' &&
(!body.unittest || (body.unittest && this.config.use.tests?.sanity))
? await setPatternAvatar(this.record.id, body.img)
: false
if (img) await this.unguardedUpdate(this.cloak({ img: img.url }))
else await this.read({ id: this.record.id })
return this.setResponse(201, 'created', { pattern: this.asPattern() })
}
PatternModel.prototype.unguardedCreate = async function (data) {
try {
this.record = await this.prisma.pattern.create({ data: this.cloak(data) })
} catch (err) {
log.warn(err, 'Could not create pattern')
return this.setResponse(500, 'createPatternFailed')
}
return this
}
/*
* Loads a pattern from the database based on the where clause you pass it
*
* Stores result in this.record
*/
PatternModel.prototype.read = async function (where) {
try {
this.record = await this.prisma.pattern.findUnique({ where })
} catch (err) {
log.warn({ err, where }, 'Could not read pattern')
}
this.reveal()
return this.setExists()
}
/*
* Loads a pattern from the database based on the where clause you pass it
* In addition prepares it for returning the pattern data
*
* Stores result in this.record
*/
PatternModel.prototype.guardedRead = async function ({ params, user }) {
if (user.level < 1) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && user.level < 5) {
return this.setResponse(403, 'insufficientAccessLevel')
}
return this.setResponse(200, false, {
result: 'success',
pattern: this.asPattern(),
})
}
/*
* Clones a pattern
* In addition prepares it for returning the pattern data
*
* Stores result in this.record
*/
PatternModel.prototype.guardedClone = async function ({ params, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && !this.record.public && user.level < 5) {
return this.setResponse(403, 'insufficientAccessLevel')
}
// Clone pattern
const data = this.asPattern()
delete data.id
data.name += ` (cloned from #${this.record.id})`
data.notes += ` (Note: This pattern was cloned from pattern #${this.record.id})`
await this.unguardedCreate(data)
// Update unencrypted data
this.reveal()
return this.setResponse(200, false, {
result: 'success',
pattern: this.asPattern(),
})
}
/*
* Helper method to decrypt at-rest data
*/
PatternModel.prototype.reveal = async function () {
this.clear = {}
if (this.record) {
for (const field of this.encryptedFields) {
this.clear[field] = this.decrypt(this.record[field])
}
}
return this
}
/*
* Helper method to encrypt at-rest data
*/
PatternModel.prototype.cloak = function (data) {
for (const field of this.encryptedFields) {
if (typeof data[field] !== 'undefined') {
data[field] = this.encrypt(data[field])
}
}
return data
}
/*
* Checks this.record and sets a boolean to indicate whether
* the pattern exists or not
*
* Stores result in this.exists
*/
PatternModel.prototype.setExists = function () {
this.exists = this.record ? true : false
return this
}
/*
* Updates the pattern data - Used when we create the data ourselves
* so we know it's safe
*/
PatternModel.prototype.unguardedUpdate = async function (data) {
try {
this.record = await this.prisma.pattern.update({
where: { id: this.record.id },
data,
})
} catch (err) {
log.warn(err, 'Could not update pattern record')
process.exit()
return this.setResponse(500, 'updatePatternFailed')
}
await this.reveal()
return this.setResponse(200)
}
/*
* Updates the pattern data - Used when we pass through user-provided data
* so we can't be certain it's safe
*/
PatternModel.prototype.guardedUpdate = async function ({ params, body, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && user.level < 8) {
return this.setResponse(403, 'insufficientAccessLevel')
}
const data = {}
// Name
if (typeof body.name === 'string') data.name = body.name
// Notes
if (typeof body.notes === 'string') data.notes = body.notes
// Public
if (body.public === true || body.public === false) data.public = body.public
// Data
if (typeof body.data === 'object') data.data = body.data
// Settings
if (typeof body.settings === 'object') data.settings = body.settings
// Image (img)
if (typeof body.img === 'string') {
const img = await setPatternAvatar(params.id, body.img)
data.img = img.url
}
// Now update the record
await this.unguardedUpdate(this.cloak(data))
return this.setResponse(200, false, { pattern: this.asPattern() })
}
/*
* Removes the pattern - No questions asked
*/
PatternModel.prototype.unguardedDelete = async function () {
await this.prisma.pattern.delete({ here: { id: this.record.id } })
this.record = null
this.clear = null
return this.setExists()
}
/*
* Removes the pattern - Checks permissions
*/
PatternModel.prototype.guardedDelete = async function ({ params, body, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && user.level < 8) {
return this.setResponse(403, 'insufficientAccessLevel')
}
await this.unguardedDelete()
return this.setResponse(204, false)
}
/*
* Returns record data
*/
PatternModel.prototype.asPattern = function () {
return {
...this.record,
...this.clear,
}
}
/*
* Helper method to set the response code, result, and body
*
* Will be used by this.sendResponse()
*/
PatternModel.prototype.setResponse = function (status = 200, error = false, data = {}) {
this.response = {
status,
body: {
result: 'success',
...data,
},
}
if (status > 201) {
this.response.body.error = error
this.response.body.result = 'error'
this.error = true
} else this.error = false
return this.setExists()
}
/*
* Helper method to send response
*/
PatternModel.prototype.sendResponse = async function (res) {
return res.status(this.response.status).send(this.response.body)
}

View file

@ -19,7 +19,12 @@ PersonModel.prototype.guardedCreate = async function ({ body, user }) {
// Prepare data
const data = { name: body.name }
// Name (will be encrypted, so always set _some_ value)
if (typeof body.name === 'string') data.name = body.name
else data.name = '--'
// Notes (will be encrypted, so always set _some_ value)
if (body.notes || typeof body.notes === 'string') data.notes = body.notes
else data.notes = '--'
if (body.public === true) data.public = true
if (body.measies) data.measies = this.sanitizeMeasurements(body.measies)
data.imperial = body.imperial === true ? true : false

View file

@ -223,9 +223,9 @@ UserModel.prototype.guardedCreate = async function ({ body }) {
* Login based on username + password
*/
UserModel.prototype.passwordLogin = async function (req) {
if (Object.keys(req.body) < 1) return this.setReponse(400, 'postBodyMissing')
if (!req.body.username) return this.setReponse(400, 'usernameMissing')
if (!req.body.password) return this.setReponse(400, 'passwordMissing')
if (Object.keys(req.body) < 1) return this.setResponse(400, 'postBodyMissing')
if (!req.body.username) return this.setResponse(400, 'usernameMissing')
if (!req.body.password) return this.setResponse(400, 'passwordMissing')
await this.find(req.body)
if (!this.exists) {
@ -255,7 +255,7 @@ UserModel.prototype.passwordLogin = async function (req) {
* Confirms a user account
*/
UserModel.prototype.confirm = async function ({ body, params }) {
if (!params.id) return this.setReponse(404, 'missingConfirmationId')
if (!params.id) return this.setResponse(404, 'missingConfirmationId')
if (Object.keys(body) < 1) return this.setResponse(400, 'postBodyMissing')
if (!body.consent || typeof body.consent !== 'number' || body.consent < 1)
return this.setResponse(400, 'consentRequired')

View file

@ -1,38 +0,0 @@
import { ApikeyController } from '../controllers/apikey.mjs'
const Apikey = new ApikeyController()
const jwt = ['jwt', { session: false }]
const bsc = ['basic', { session: false }]
export function apikeyRoutes(tools) {
const { app, passport } = tools
// Create Apikey
app.post('/apikey/jwt', passport.authenticate(...jwt), (req, res) =>
Apikey.create(req, res, tools)
)
app.post('/apikey/key', passport.authenticate(...bsc), (req, res) =>
Apikey.create(req, res, tools)
)
// Read Apikey
app.get('/apikey/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Apikey.read(req, res, tools)
)
app.get('/apikey/:id/key', passport.authenticate(...bsc), (req, res) =>
Apikey.read(req, res, tools)
)
// Read current Apikey
app.get('/whoami/key', passport.authenticate(...bsc), (req, res) =>
Apikey.whoami(req, res, tools)
)
// Remove Apikey
app.delete('/apikey/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Apikey.delete(req, res, tools)
)
app.delete('/apikey/:id/key', passport.authenticate(...bsc), (req, res) =>
Apikey.delete(req, res, tools)
)
}

View file

@ -0,0 +1,38 @@
import { ApikeysController } from '../controllers/apikeys.mjs'
const Apikeys = new ApikeysController()
const jwt = ['jwt', { session: false }]
const bsc = ['basic', { session: false }]
export function apikeysRoutes(tools) {
const { app, passport } = tools
// Create Apikey
app.post('/apikeys/jwt', passport.authenticate(...jwt), (req, res) =>
Apikeys.create(req, res, tools)
)
app.post('/apikeys/key', passport.authenticate(...bsc), (req, res) =>
Apikeys.create(req, res, tools)
)
// Read Apikey
app.get('/apikeys/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Apikeys.read(req, res, tools)
)
app.get('/apikeys/:id/key', passport.authenticate(...bsc), (req, res) =>
Apikeys.read(req, res, tools)
)
// Read current Apikey
app.get('/whoami/key', passport.authenticate(...bsc), (req, res) =>
Apikeys.whoami(req, res, tools)
)
// Remove Apikey
app.delete('/apikeys/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Apikeys.delete(req, res, tools)
)
app.delete('/apikeys/:id/key', passport.authenticate(...bsc), (req, res) =>
Apikeys.delete(req, res, tools)
)
}

View file

@ -1,9 +1,11 @@
import { apikeyRoutes } from './apikey.mjs'
import { userRoutes } from './user.mjs'
import { personRoutes } from './person.mjs'
import { apikeysRoutes } from './apikeys.mjs'
import { usersRoutes } from './users.mjs'
import { peopleRoutes } from './people.mjs'
import { patternsRoutes } from './patterns.mjs'
export const routes = {
apikeyRoutes,
userRoutes,
personRoutes,
apikeysRoutes,
usersRoutes,
peopleRoutes,
patternsRoutes,
}

View file

@ -0,0 +1,49 @@
import { PatternsController } from '../controllers/patterns.mjs'
const Patterns = new PatternsController()
const jwt = ['jwt', { session: false }]
const bsc = ['basic', { session: false }]
export function patternsRoutes(tools) {
const { app, passport } = tools
// Create pattern
app.post('/patterns/jwt', passport.authenticate(...jwt), (req, res) =>
Patterns.create(req, res, tools)
)
app.post('/patterns/key', passport.authenticate(...bsc), (req, res) =>
Patterns.create(req, res, tools)
)
// Clone pattern
app.post('/patterns/:id/clone/jwt', passport.authenticate(...jwt), (req, res) =>
Patterns.clone(req, res, tools)
)
app.post('/patterns/:id/clone/key', passport.authenticate(...bsc), (req, res) =>
Patterns.clone(req, res, tools)
)
// Read pattern
app.get('/patterns/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Patterns.read(req, res, tools)
)
app.get('/patterns/:id/key', passport.authenticate(...bsc), (req, res) =>
Patterns.read(req, res, tools)
)
// Update pattern
app.put('/patterns/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Patterns.update(req, res, tools)
)
app.put('/patterns/:id/key', passport.authenticate(...bsc), (req, res) =>
Patterns.update(req, res, tools)
)
// Delete pattern
app.delete('/patterns/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Patterns.delete(req, res, tools)
)
app.delete('/patterns/:id/key', passport.authenticate(...bsc), (req, res) =>
Patterns.delete(req, res, tools)
)
}

View file

@ -1,49 +1,49 @@
import { PersonController } from '../controllers/person.mjs'
import { PeopleController } from '../controllers/people.mjs'
const Person = new PersonController()
const People = new PeopleController()
const jwt = ['jwt', { session: false }]
const bsc = ['basic', { session: false }]
export function personRoutes(tools) {
export function peopleRoutes(tools) {
const { app, passport } = tools
// Create person
app.post('/people/jwt', passport.authenticate(...jwt), (req, res) =>
Person.create(req, res, tools)
People.create(req, res, tools)
)
app.post('/people/key', passport.authenticate(...bsc), (req, res) =>
Person.create(req, res, tools)
People.create(req, res, tools)
)
// Clone person
app.post('/people/:id/clone/jwt', passport.authenticate(...jwt), (req, res) =>
Person.clone(req, res, tools)
People.clone(req, res, tools)
)
app.post('/people/:id/clone/key', passport.authenticate(...bsc), (req, res) =>
Person.clone(req, res, tools)
People.clone(req, res, tools)
)
// Read person
app.get('/people/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Person.read(req, res, tools)
People.read(req, res, tools)
)
app.get('/people/:id/key', passport.authenticate(...bsc), (req, res) =>
Person.read(req, res, tools)
People.read(req, res, tools)
)
// Update person
app.put('/people/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Person.update(req, res, tools)
People.update(req, res, tools)
)
app.put('/people/:id/key', passport.authenticate(...bsc), (req, res) =>
Person.update(req, res, tools)
People.update(req, res, tools)
)
// Delete person
app.delete('/people/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Person.delete(req, res, tools)
People.delete(req, res, tools)
)
app.delete('/people/:id/key', passport.authenticate(...bsc), (req, res) =>
Person.delete(req, res, tools)
People.delete(req, res, tools)
)
}

View file

@ -1,30 +1,38 @@
import { UserController } from '../controllers/user.mjs'
import { UsersController } from '../controllers/users.mjs'
const User = new UserController()
const Users = new UsersController()
const jwt = ['jwt', { session: false }]
const bsc = ['basic', { session: false }]
export function userRoutes(tools) {
export function usersRoutes(tools) {
const { app, passport } = tools
// Sign up
app.post('/signup', (req, res) => User.signup(req, res, tools))
app.post('/signup', (req, res) => Users.signup(req, res, tools))
// Confirm account
app.post('/confirm/signup/:id', (req, res) => User.confirm(req, res, tools))
app.post('/confirm/signup/:id', (req, res) => Users.confirm(req, res, tools))
// Login
app.post('/login', (req, res) => User.login(req, res, tools))
app.post('/login', (req, res) => Users.login(req, res, tools))
// Read current jwt
app.get('/whoami/jwt', passport.authenticate(...jwt), (req, res) => User.whoami(req, res, tools))
app.get('/account/jwt', passport.authenticate(...jwt), (req, res) => User.whoami(req, res, tools))
app.get('/account/key', passport.authenticate(...bsc), (req, res) => User.whoami(req, res, tools))
app.get('/whoami/jwt', passport.authenticate(...jwt), (req, res) => Users.whoami(req, res, tools))
app.get('/account/jwt', passport.authenticate(...jwt), (req, res) =>
Users.whoami(req, res, tools)
)
app.get('/account/key', passport.authenticate(...bsc), (req, res) =>
Users.whoami(req, res, tools)
)
// Update account
app.put('/account/jwt', passport.authenticate(...jwt), (req, res) => User.update(req, res, tools))
app.put('/account/key', passport.authenticate(...bsc), (req, res) => User.update(req, res, tools))
app.put('/account/jwt', passport.authenticate(...jwt), (req, res) =>
Users.update(req, res, tools)
)
app.put('/account/key', passport.authenticate(...bsc), (req, res) =>
Users.update(req, res, tools)
)
/*

View file

@ -33,6 +33,7 @@ async function getAvatar(type, id) {
*/
export const setUserAvatar = async (id, data) => setAvatar('user', id, data)
export const setPersonAvatar = async (id, data) => setAvatar('person', id, data)
export const setPatternAvatar = async (id, data) => setAvatar('pattern', id, data)
export async function setAvatar(type, id, data) {
// Step 1: Upload the image as asset
const [contentType, binary] = b64ToBinaryWithType(data)

View file

@ -3,7 +3,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
step(`${store.icon('key', 'jwt')} Create API Key (jwt)`, (done) => {
chai
.request(config.api)
.post('/apikey/jwt')
.post('/apikeys/jwt')
.set('Authorization', 'Bearer ' + store.account.token)
.send({
name: 'Test API key',
@ -27,7 +27,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
step(`${store.icon('key', 'key')} Create API Key (key)`, (done) => {
chai
.request(config.api)
.post('/apikey/key')
.post('/apikeys/key')
.auth(store.apikey1.key, store.apikey1.secret)
.send({
name: 'Test API key with key',
@ -67,7 +67,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
step(`${store.icon('key', 'key')} Read API key (key)`, (done) => {
chai
.request(config.api)
.get(`/apikey/${store.apikey1.key}/key`)
.get(`/apikeys/${store.apikey1.key}/key`)
.auth(store.apikey2.key, store.apikey2.secret)
.end((err, res) => {
expect(res.status).to.equal(200)
@ -83,7 +83,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
step(`${store.icon('key', 'jwt')} Read API key (jwt)`, (done) => {
chai
.request(config.api)
.get(`/apikey/${store.apikey2.key}/jwt`)
.get(`/apikeys/${store.apikey2.key}/jwt`)
.set('Authorization', 'Bearer ' + store.account.token)
.end((err, res) => {
expect(res.status).to.equal(200)
@ -99,7 +99,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
step(`${store.icon('key', 'key')} Remove API key (key)`, (done) => {
chai
.request(config.api)
.delete(`/apikey/${store.apikey2.key}/key`)
.delete(`/apikeys/${store.apikey2.key}/key`)
.auth(store.apikey2.key, store.apikey2.secret)
.end((err, res) => {
expect(res.status).to.equal(204)
@ -110,7 +110,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
step(`${store.icon('key', 'jwt')} Remove API key (jwt)`, (done) => {
chai
.request(config.api)
.delete(`/apikey/${store.apikey1.key}/jwt`)
.delete(`/apikeys/${store.apikey1.key}/jwt`)
.set('Authorization', 'Bearer ' + store.account.token)
.end((err, res) => {
expect(res.status).to.equal(204)

View file

@ -2,6 +2,7 @@ import { userTests } from './user.mjs'
import { accountTests } from './account.mjs'
import { apikeyTests } from './apikey.mjs'
import { personTests } from './person.mjs'
import { patternTests } from './pattern.mjs'
import { setup } from './shared.mjs'
const runTests = async (...params) => {
@ -9,6 +10,7 @@ const runTests = async (...params) => {
await apikeyTests(...params)
await accountTests(...params)
await personTests(...params)
await patternTests(...params)
}
// Load initial data required for tests

View file

@ -0,0 +1,326 @@
import { cat } from './cat.mjs'
export const patternTests = async (chai, config, expect, store) => {
store.account.patterns = {}
for (const auth of ['jwt', 'key']) {
describe(`${store.icon('pattern', auth)} Pattern tests (${auth})`, () => {
it(`${store.icon('pattern', auth)} Should create a new pattern (${auth})`, (done) => {
chai
.request(config.api)
.post(`/patterns/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.send({
design: 'aaron',
settings: {},
person: store.account.people.her.id,
})
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(201)
expect(res.body.result).to.equal(`success`)
expect(typeof res.body.pattern?.id).to.equal('number')
expect(res.body.pattern.userId).to.equal(store.account.id)
expect(res.body.pattern.personId).to.equal(store.account.people.her.id)
expect(res.body.pattern.design).to.equal('aaron')
expect(res.body.pattern.public).to.equal(false)
store.account.patterns[auth] = res.body.pattern
done()
})
}).timeout(5000)
for (const field of ['name', 'notes']) {
it(`${store.icon('pattern', auth)} Should update the ${field} field (${auth})`, (done) => {
const data = {}
const val = store.account.patterns[auth][field] + '_updated'
data[field] = val
chai
.request(config.api)
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(
`${store.account.apikey.key}:${store.account.apikey.secret}`
).toString('base64')
)
.send(data)
.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.pattern[field]).to.equal('--_updated')
done()
})
})
}
it(`${store.icon('person', auth)} Should update the public field (${auth})`, (done) => {
chai
.request(config.api)
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.send({ public: true })
.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.pattern.public).to.equal(true)
done()
})
})
it(`${store.icon('person', auth)} Should not update the design field (${auth})`, (done) => {
chai
.request(config.api)
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.send({ design: 'updated' })
.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.pattern.design).to.equal('aaron')
done()
})
})
it(`${store.icon('person', auth)} Should not update the person field (${auth})`, (done) => {
chai
.request(config.api)
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.send({ person: 1 })
.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.pattern.personId).to.equal(store.account.people.her.id)
done()
})
})
for (const field of ['data', 'settings']) {
it(`${store.icon('person', auth)} Should update the ${field} field (${auth})`, (done) => {
const data = {}
data[field] = { test: { value: 'hello' } }
chai
.request(config.api)
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(
`${store.account.apikey.key}:${store.account.apikey.secret}`
).toString('base64')
)
.send(data)
.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.pattern[field].test.value).to.equal('hello')
done()
})
})
}
it(`${store.icon('pattern', auth)} Should read a pattern (${auth})`, (done) => {
chai
.request(config.api)
.get(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.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.pattern.data.test.value).to.equal('hello')
done()
})
})
it(`${store.icon(
'person',
auth
)} Should not allow reading another user's pattern (${auth})`, (done) => {
chai
.request(config.api)
.get(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.altaccount.token
: 'Basic ' +
new Buffer(
`${store.altaccount.apikey.key}:${store.altaccount.apikey.secret}`
).toString('base64')
)
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(403)
expect(res.body.result).to.equal(`error`)
expect(res.body.error).to.equal(`insufficientAccessLevel`)
done()
})
})
it(`${store.icon(
'person',
auth
)} Should not allow updating another user's pattern (${auth})`, (done) => {
chai
.request(config.api)
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.altaccount.token
: 'Basic ' +
new Buffer(
`${store.altaccount.apikey.key}:${store.altaccount.apikey.secret}`
).toString('base64')
)
.send({
name: 'I have been taken over',
})
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(403)
expect(res.body.result).to.equal(`error`)
expect(res.body.error).to.equal(`insufficientAccessLevel`)
done()
})
})
it(`${store.icon(
'person',
auth
)} Should not allow removing another user's pattern (${auth})`, (done) => {
chai
.request(config.api)
.delete(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.altaccount.token
: 'Basic ' +
new Buffer(
`${store.altaccount.apikey.key}:${store.altaccount.apikey.secret}`
).toString('base64')
)
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(403)
expect(res.body.result).to.equal(`error`)
expect(res.body.error).to.equal(`insufficientAccessLevel`)
done()
})
})
/*
it(`${store.icon('person', auth)} Should clone a person (${auth})`, (done) => {
chai
.request(config.api)
.post(`/people/${store.person[auth].id}/clone/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.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.error).to.equal(`undefined`)
expect(typeof res.body.person.id).to.equal(`number`)
done()
})
})
it(`${store.icon(
'person',
auth
)} Should (not) clone a public person across accounts (${auth})`, (done) => {
chai
.request(config.api)
.post(`/people/${store.person[auth].id}/clone/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.altaccount.token
: 'Basic ' +
new Buffer(
`${store.altaccount.apikey.key}:${store.altaccount.apikey.secret}`
).toString('base64')
)
.end((err, res) => {
if (store.person[auth].public) {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(typeof res.body.error).to.equal(`undefined`)
expect(typeof res.body.person.id).to.equal(`number`)
} else {
expect(err === null).to.equal(true)
expect(res.status).to.equal(403)
expect(res.body.result).to.equal(`error`)
expect(res.body.error).to.equal(`insufficientAccessLevel`)
}
done()
})
})
// TODO:
// - Clone person
// - Clone person accross accounts of they are public
*/
})
}
}

View file

@ -4,12 +4,17 @@ import chai from 'chai'
import http from 'chai-http'
import { verifyConfig } from '../src/config.mjs'
import { randomString } from '../src/utils/crypto.mjs'
import {
cisFemaleAdult34 as her,
cisMaleAdult42 as him,
} from '../../../packages/models/src/index.mjs'
dotenv.config()
const config = verifyConfig(true)
const expect = chai.expect
chai.use(http)
const people = { her, him }
export const setup = async () => {
// Initial store contents
@ -21,17 +26,20 @@ export const setup = async () => {
email: `test_${randomString()}@${config.tests.domain}`,
language: 'en',
password: randomString(),
people: {},
},
altaccount: {
email: `test_${randomString()}@${config.tests.domain}`,
language: 'en',
password: randomString(),
people: {},
},
icons: {
user: '🧑 ',
jwt: '🎫 ',
key: '🎟️ ',
person: '🧕 ',
pattern: '👕 ',
},
randomString,
}
@ -63,12 +71,12 @@ export const setup = async () => {
}
store[acc].token = result.data.token
store[acc].username = result.data.account.username
store[acc].userid = result.data.account.id
store[acc].id = result.data.account.id
// Create API key
try {
result = await axios.post(
`${store.config.api}/apikey/jwt`,
`${store.config.api}/apikeys/jwt`,
{
name: 'Test API key',
level: 4,
@ -85,6 +93,29 @@ export const setup = async () => {
process.exit()
}
store[acc].apikey = result.data.apikey
// Create people key
for (const name in people) {
try {
result = await axios.post(
`${store.config.api}/people/jwt`,
{
name: `This is ${name} name`,
name: `These are ${name} notes`,
measies: people[name],
},
{
headers: {
authorization: `Bearer ${store[acc].token}`,
},
}
)
} catch (err) {
console.log('Failed at API key creation request', err)
process.exit()
}
store[acc].people[name] = result.data.person
}
}
return { chai, config, expect, store }

View file

@ -184,12 +184,12 @@ export const userTests = async (chai, config, expect, store) => {
})
})
step(`${store.icon('user')} Should login with userid and password`, (done) => {
step(`${store.icon('user')} Should login with id and password`, (done) => {
chai
.request(config.api)
.post('/login')
.send({
username: store.account.userid,
username: store.account.id,
password: store.account.password,
})
.end((err, res) => {