1
0
Fork 0

feat(backend): Added curatedSets data type

This commit is contained in:
joostdecock 2023-05-07 12:00:43 +02:00
parent 19a81a0aed
commit e4f2aab9d0
15 changed files with 1006 additions and 48 deletions

View file

@ -0,0 +1,177 @@
import path from 'path'
import fs from 'fs'
import { PrismaClient } from '@prisma/client'
import { hash, encryption } from '../src/utils/crypto.mjs'
import { clean } from '../src/utils/index.mjs'
import { verifyConfig } from '../src/config.mjs'
import {
cisFemaleAdult,
cisMaleAdult,
cisFemaleDoll,
cisMaleDoll,
cisFemaleGiant,
cisMaleGiant,
} from '../../../packages/models/src/index.mjs'
const prisma = new PrismaClient()
const config = verifyConfig()
const { encrypt, decrypt } = encryption(config.encryption.key)
/*
* Note: This will import the FreeSewing (v2) sizing table into the
* FreeSewing curator account.
*
* The curator account is just a regular user account, but one that
* we use to store the 'official' measurement sets.
*
* Unless you are a very involved contributor, there is probably
* no reason for you to run this script.
*/
// Holds the sets to create
const sets = []
// Helper method to create the set data
const createSetData = ({ name, measies, imperial }) => ({
imperial,
name,
measies,
userId: config.curator.id,
public: true,
})
// CIS Female Adult
sets.push(
...Object.keys(cisFemaleAdult).map((size) =>
createSetData({
name: `Metric Cis Female Adult - Size ${size} (EU)`,
measies: cisFemaleAdult[size],
imperial: false,
})
)
)
sets.push(
...Object.keys(cisFemaleAdult).map((size) =>
createSetData({
name: `Imperial Cis Female Adult - Size ${size} (EU)`,
measies: cisFemaleAdult[size],
imperial: true,
})
)
)
// CIS Male Adult
sets.push(
...Object.keys(cisMaleAdult).map((size) =>
createSetData({
name: `Metric Cis Male Adult - Size ${size} (EU)`,
measies: cisMaleAdult[size],
imperial: false,
})
)
)
sets.push(
...Object.keys(cisMaleAdult).map((size) =>
createSetData({
name: `Imperial Cis Male Adult - Size ${size} (EU)`,
measies: cisMaleAdult[size],
imperial: true,
})
)
)
// CIS Female Doll
sets.push(
...Object.keys(cisFemaleDoll).map((size) =>
createSetData({
name: `Metric Cis Female Doll - ${size}%`,
measies: cisFemaleDoll[size],
imperial: false,
})
)
)
sets.push(
...Object.keys(cisFemaleDoll).map((size) =>
createSetData({
name: `Imperial Cis Female Doll - ${size}%`,
measies: cisFemaleDoll[size],
imperial: true,
})
)
)
// CIS Male Doll
sets.push(
...Object.keys(cisMaleDoll).map((size) =>
createSetData({
name: `Metric Cis Male Doll - ${size}%`,
measies: cisMaleDoll[size],
imperial: false,
})
)
)
sets.push(
...Object.keys(cisMaleDoll).map((size) =>
createSetData({
name: `Imperial Cis Male Doll - ${size}%`,
measies: cisMaleDoll[size],
imperial: true,
})
)
)
// CIS Female Giant
sets.push(
...Object.keys(cisFemaleGiant).map((size) =>
createSetData({
name: `Metric Cis Female Giant - Size ${size}%`,
measies: cisFemaleGiant[size],
imperial: false,
})
)
)
sets.push(
...Object.keys(cisFemaleGiant).map((size) =>
createSetData({
name: `Imperial Cis Female Giant - Size ${size}%`,
measies: cisFemaleGiant[size],
imperial: true,
})
)
)
// CIS Male Giant
sets.push(
...Object.keys(cisMaleGiant).map((size) =>
createSetData({
name: `Metric Cis Male Giant - Size ${size}%`,
measies: cisMaleGiant[size],
imperial: false,
})
)
)
sets.push(
...Object.keys(cisMaleGiant).map((size) =>
createSetData({
name: `Imperial Cis Male Giant - Size ${size}%`,
measies: cisMaleGiant[size],
imperial: true,
})
)
)
importSets(sets)
async function createSet(set) {
try {
record = await prisma.user.create({ data: set })
} catch (err) {
console.log(err)
}
}
async function importSets(sets) {
for (const set of sets) {
console.log(`Importing ${set.name}`)
await createSet(set)
}
}

View file

@ -27,6 +27,8 @@ const envToBool = (input = 'no') => {
// Construct config object
const baseConfig = {
// Environment
env: process.env.NODE_ENV || 'development',
// Feature flags
use: {
github: envToBool(process.env.BACKEND_ENABLE_GITHUB),
@ -87,6 +89,7 @@ const baseConfig = {
},
tests: {
domain: process.env.BACKEND_TEST_DOMAIN || 'freesewing.dev',
production: envToBool(process.env.BACKEND_ALLOW_TESTS_IN_PRODUCTION),
},
website: {
domain: process.env.BACKEND_WEBSITE_DOMAIN || 'freesewing.org',
@ -202,6 +205,7 @@ const vars = {
BACKEND_ENABLE_OAUTH_GITHUB: 'optional',
BACKEND_ENABLE_OAUTH_GOOGLE: 'optional',
BACKEND_ENABLE_TESTS: 'optional',
BACKEND_ALLOW_TESTS_IN_PRODUCTION: 'optional',
BACKEND_ENABLE_DUMP_CONFIG_AT_STARTUP: 'optional',
}
@ -241,7 +245,7 @@ if (envToBool(process.env.BACKEND_ENABLE_OAUTH_GOOGLE)) {
vars.BACKEND_OAUTH_GOOGLE_CLIENT_ID = 'required'
vars.BACKEND_OAUTH_GOOGLE_CLIENT_SECRET = 'requiredSecret'
}
// Vars for unit tests
// Vars for (unit) tests
if (envToBool(process.env.BACKEND_ENABLE_TESTS)) {
vars.BACKEND_TEST_DOMAIN = 'optional'
vars.BACKEND_ENABLE_TESTS_EMAIL = 'optional'

View file

@ -0,0 +1,79 @@
import { CuratedSetModel } from '../models/curated-set.mjs'
import { SetModel } from '../models/set.mjs'
export function CuratedSetsController() {}
/*
* Create a curated measurements set
* See: https://freesewing.dev/reference/backend/api
*/
CuratedSetsController.prototype.create = async (req, res, tools) => {
const CuratedSet = new CuratedSetModel(tools)
await CuratedSet.guardedCreate(req)
return CuratedSet.sendResponse(res)
}
/*
* Read a curated measurements set
* See: https://freesewing.dev/reference/backend/api
*/
CuratedSetsController.prototype.read = async (req, res, tools, format = false) => {
const CuratedSet = new CuratedSetModel(tools)
await CuratedSet.guardedRead(req, format)
return format === 'yaml' ? CuratedSet.sendYamlResponse(res) : CuratedSet.sendResponse(res)
}
/*
* Get a list of curated measurements sets
* See: https://freesewing.dev/reference/backend/api
*/
CuratedSetsController.prototype.list = async (req, res, tools, format = false) => {
const CuratedSet = new CuratedSetModel(tools)
const curatedSets = await CuratedSet.allCuratedSets()
if (curatedSets) {
if (!format) CuratedSet.setResponse(200, 'success', { curatedSets })
else CuratedSet.setResponse(200, 'success', curatedSets, true)
} else CuratedSet.setResponse(404, 'notFound')
return format === 'yaml' && curatedSets
? CuratedSet.sendYamlResponse(res)
: CuratedSet.sendResponse(res)
}
/*
* Update a curated measurements set
* See: https://freesewing.dev/reference/backend/api
*/
CuratedSetsController.prototype.update = async (req, res, tools) => {
const CuratedSet = new CuratedSetModel(tools)
await CuratedSet.guardedUpdate(req)
return CuratedSet.sendResponse(res)
}
/*
* Remove a curated measurements set
* See: https://freesewing.dev/reference/backend/api
*/
CuratedSetsController.prototype.delete = async (req, res, tools) => {
const CuratedSet = new CuratedSetModel(tools)
await CuratedSet.guardedDelete(req)
return Set.sendResponse(res)
}
/*
* Clone a curated measurements set
* See: https://freesewing.dev/reference/backend/api
*/
CuratedSetsController.prototype.clone = async (req, res, tools) => {
const CuratedSet = new CuratedSetModel(tools)
const Set = new SetModel(tools)
await CuratedSet.guardedClone(req, Set)
// Note: Sending the set back
return Set.sendResponse(res)
}

View file

@ -0,0 +1,322 @@
import { capitalize } from '../utils/index.mjs'
import { log } from '../utils/log.mjs'
import { setSetAvatar } from '../utils/sanity.mjs'
import yaml from 'js-yaml'
export function CuratedSetModel(tools) {
this.config = tools.config
this.prisma = tools.prisma
this.rbac = tools.rbac
return this
}
CuratedSetModel.prototype.guardedCreate = async function ({ body, user }) {
if (!this.rbac.curator(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (Object.keys(body).length < 1) return this.setResponse(400, 'postBodyMissing')
if (!body.nameEn || typeof body.nameEn !== 'string') return this.setResponse(400, 'nameEnMissing')
// Prepare data
const data = {}
for (const lang of this.config.languages) {
for (const field of ['name', 'notes']) {
const key = field + capitalize(lang)
if (body[key] && typeof body[key] === 'string') data[key] = body[key]
}
}
if (body.measies) data.measies = this.sanitizeMeasurements(body.measies)
else data.measies = {}
// Set this one initially as we need the ID to create a custom img via Sanity
data.img = this.config.avatars.set
// 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.test || (body.test && this.config.use.tests?.sanity))
? await setSetAvatar(this.record.id, body.img)
: false
if (img) await this.unguardedUpdate({ img: img.url })
else await this.read({ id: this.record.id })
return this.setResponse(201, 'created', { curatedSet: this.asCuratedSet() })
}
CuratedSetModel.prototype.unguardedCreate = async function (data) {
// FIXME: See https://github.com/prisma/prisma/issues/3786
if (data.measies && typeof data.measies === 'object') data.measies = JSON.stringify(data.measies)
try {
this.record = await this.prisma.curatedSet.create({ data })
} catch (err) {
log.warn(err, 'Could not create set')
return this.setResponse(500, 'createSetFailed')
}
return this
}
/*
* Loads a measurements set from the database based on the where clause you pass it
*
* Stores result in this.record
*/
CuratedSetModel.prototype.read = async function (where) {
try {
this.record = await this.prisma.curatedSet.findUnique({ where })
} catch (err) {
log.warn({ err, where }, 'Could not read measurements set')
}
// FIXME: Convert JSON to object. See https://github.com/prisma/prisma/issues/3786
this.record.measies = JSON.parse(this.record.measies)
return this.curatedSetExists()
}
/*
* Loads a measurements set from the database based on the where clause you pass it
* In addition prepares it for returning the set data
*
* Stores result in this.record
*/
CuratedSetModel.prototype.guardedRead = async function ({ params }, format = false) {
await this.read({ id: parseInt(params.id) })
if (!format)
return this.setResponse(200, false, {
result: 'success',
curatedSet: this.asCuratedSet(),
})
return this.setResponse(200, false, this.asData(), true)
}
/*
* Returns a list of all curated sets
*/
CuratedSetModel.prototype.allCuratedSets = async function () {
let curatedSets
try {
curatedSets = await this.prisma.curatedSet.findMany()
} catch (err) {
log.warn(`Failed to search curated sets: ${err}`)
}
const list = []
for (const curatedSet of curatedSets) list.push(curatedSet)
return list
}
/*
* Clones a curated measurements set (into a regular set)
* In addition prepares it for returning the set data
*
* Stores result in this.record
*/
CuratedSetModel.prototype.guardedClone = async function ({ params, user, body }, Set) {
if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
if (!body.language || !this.config.languages.includes(body.language))
return this.setResponse(403, 'languageMissing')
await this.read({ id: parseInt(params.id) })
// Clone curated set
const data = {}
const lang = capitalize(body.language.toLowerCase())
data.name = this.record[`name${lang}`]
data.notes = this.record[`notes${lang}`]
data.measies = this.record.measies
await Set.guardedCreate({ params, user, body: data })
return
}
/*
* Checks this.record and sets a boolean to indicate whether
* the curated set exists or not
*
* Stores result in this.exists
*/
CuratedSetModel.prototype.curatedSetExists = function () {
this.exists = this.record ? true : false
return this
}
/*
* Updates the set data - Used when we create the data ourselves
* so we know it's safe
*/
CuratedSetModel.prototype.unguardedUpdate = async function (data) {
// FIXME: Convert object to JSON. See https://github.com/prisma/prisma/issues/3786
if (data.measies && typeof data.measies === 'object') data.measies = JSON.stringify(data.measies)
try {
this.record = await this.prisma.curatedSet.update({
where: { id: this.record.id },
data,
})
// FIXME: Convert JSON to object. See https://github.com/prisma/prisma/issues/3786
this.record.measies = JSON.parse(this.record.measies)
} catch (err) {
log.warn(err, 'Could not update set record')
process.exit()
return this.setResponse(500, 'updateSetFailed')
}
return this.setResponse(200)
}
/*
* Updates the set data - Used when we pass through user-provided data
* so we can't be certain it's safe
*/
CuratedSetModel.prototype.guardedUpdate = async function ({ params, body, user }) {
if (!this.rbac.curator(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) })
// Prepare data
const data = {}
for (const lang of this.config.languages) {
for (const field of ['name', 'notes']) {
const key = field + capitalize(lang)
if (body[key] && typeof body[key] === 'string') data[key] = body[key]
}
}
// Measurements
const measies = {}
if (typeof body.measies === 'object') {
const remove = []
for (const [key, val] of Object.entries(body.measies)) {
if (this.config.measies.includes(key)) {
if (val === null) remove.push(key)
else if (typeof val == 'number' && val > 0) measies[key] = val
}
}
data.measies = { ...this.record.measies, ...measies }
for (const key of remove) delete data.measies[key]
}
// Image (img)
if (typeof body.img === 'string') {
const img = await setSetAvatar(params.id, body.img)
data.img = img.url
}
// Now update the record
await this.unguardedUpdate(data)
return this.setResponse(200, false, { set: this.asCuratedSet() })
}
/*
* Removes the set - No questions asked
*/
CuratedSetModel.prototype.unguardedDelete = async function () {
await this.prisma.curatedSet.delete({ where: { id: this.record.id } })
this.record = null
this.clear = null
return this.curatedSetExists()
}
/*
* Removes the set - Checks permissions
*/
CuratedSetModel.prototype.guardedDelete = async function ({ params, user }) {
if (!this.rbac.curator(user)) 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 fit for public publishing
*/
CuratedSetModel.prototype.asCuratedSet = function () {
return { ...this.record }
}
/*
* Returns record data fit for public publishing
*/
CuratedSetModel.prototype.asData = function () {
const data = {
author: 'FreeSewing.org',
type: 'curatedMeasurementsSet',
about: 'Contains measurements in mm as well as metadata',
...this.asCuratedSet(),
}
data.measurements = data.measies
delete data.measies
return data
}
/*
* Helper method to set the response code, result, and body
*
* Will be used by this.sendResponse()
*/
CuratedSetModel.prototype.setResponse = function (
status = 200,
error = false,
data = {},
rawData = false
) {
this.response = {
status,
body: rawData
? data
: {
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.curatedSetExists()
}
/*
* Helper method to send response (as JSON)
*/
CuratedSetModel.prototype.sendResponse = async function (res) {
return res.status(this.response.status).send(this.response.body)
}
/*
* Helper method to send response as YAML
*/
CuratedSetModel.prototype.sendYamlResponse = async function (res) {
return res.status(this.response.status).type('yaml').send(yaml.dump(this.response.body))
}
/* Helper method to parse user-supplied measurements */
CuratedSetModel.prototype.sanitizeMeasurements = function (input) {
const measies = {}
if (typeof input !== 'object') return measies
for (const [m, val] of Object.entries(input)) {
if (this.config.measies.includes(m) && typeof val === 'number' && val > 0) measies[m] = val
}
return measies
}

View file

@ -51,7 +51,7 @@ PatternModel.prototype.guardedCreate = async function ({ body, user }) {
const img =
this.config.use.sanity &&
typeof body.img === 'string' &&
(!body.unittest || (body.unittest && this.config.use.tests?.sanity))
(!body.test || (body.test && this.config.use.tests?.sanity))
? await setPatternAvatar(this.record.id, body.img)
: false

View file

@ -42,7 +42,7 @@ SetModel.prototype.guardedCreate = async function ({ body, user }) {
const img =
this.config.use.sanity &&
typeof body.img === 'string' &&
(!body.unittest || (body.unittest && this.config.use.tests?.sanity))
(!body.test || (body.test && this.config.use.tests?.sanity))
? await setSetAvatar(this.record.id, body.img)
: false
@ -393,10 +393,10 @@ SetModel.prototype.sendYamlResponse = async function (res) {
/*
* Update method to determine whether this request is
* part of a unit test
* part of a test
*/
//UserModel.prototype.isUnitTest = function (body) {
// if (!body.unittest) return false
//UserModel.prototype.isTest = function (body) {
// if (!body.test) 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
//

View file

@ -150,6 +150,10 @@ UserModel.prototype.guardedCreate = async function ({ body }) {
const ehash = hash(clean(body.email))
const check = randomString()
await this.read({ ehash })
// Check for unit tests only once
const isTest = this.isTest(body)
if (this.exists) {
/*
* User already exists. However, if we return an error, then baddies can
@ -183,7 +187,8 @@ UserModel.prototype.guardedCreate = async function ({ body }) {
userId: this.record.id,
})
}
// Always send email
// Send email unless it's a test and we don't want to send test emails
if (!isTest || this.config.tests.sendEmail)
await this.mailer.send({
template: type,
language: body.language,
@ -225,6 +230,8 @@ UserModel.prototype.guardedCreate = async function ({ body }) {
// Set this one initially as we need the ID to create a custom img via Sanity
img: this.encrypt(this.config.avatars.user),
}
// During tests, users can set their own permission level so you can test admin stuff
if (isTest && body.role) data.role = body.role
this.record = await this.prisma.user.create({ data })
} catch (err) {
log.warn(err, 'Could not create user record')
@ -256,7 +263,7 @@ UserModel.prototype.guardedCreate = async function ({ body }) {
})
// Send signup email
if (!this.isUnitTest(body) || this.config.tests.sendEmail)
if (!this.isTest(body) || this.config.tests.sendEmail)
await this.mailer.send({
template: 'signup',
language: this.language,
@ -271,7 +278,7 @@ UserModel.prototype.guardedCreate = async function ({ body }) {
},
})
return this.isUnitTest(body)
return this.isTest(body)
? this.setResponse(201, false, {
email: this.clear.email,
confirmation: this.confirmation.record.id,
@ -390,8 +397,8 @@ UserModel.prototype.sendSigninlink = async function (req) {
},
userId: this.record.id,
})
const isUnitTest = this.isUnitTest(req.body)
if (!isUnitTest) {
const isTest = this.isTest(req.body)
if (!isTest) {
// Send sign-in link email
await this.mailer.send({
template: 'signinlink',
@ -522,7 +529,7 @@ UserModel.prototype.guardedUpdate = async function ({ body, user }) {
// Now update the record
await this.unguardedUpdate(this.cloak(data))
const isUnitTest = this.isUnitTest(body)
const isTest = this.isTest(body)
if (typeof body.email === 'string' && this.clear.email !== clean(body.email)) {
// Email change (requires confirmation)
const check = randomString()
@ -538,7 +545,7 @@ UserModel.prototype.guardedUpdate = async function ({ body, user }) {
},
userId: this.record.id,
})
if (!isUnitTest || this.config.tests.sendEmail) {
if (!isTest || this.config.tests.sendEmail) {
// Send confirmation email
await this.mailer.send({
template: 'emailchange',
@ -590,8 +597,7 @@ UserModel.prototype.guardedUpdate = async function ({ body, user }) {
result: 'success',
account: this.asAccount(),
}
if (isUnitTest && this.Confirmation.record?.id)
returnData.confirmation = this.Confirmation.record.id
if (isTest && this.Confirmation.record?.id) returnData.confirmation = this.Confirmation.record.id
return this.setResponse(200, false, returnData)
}
@ -755,11 +761,14 @@ UserModel.prototype.sendResponse = async function (res) {
/*
* Update method to determine whether this request is
* part of a unit test
* part of a (unit) test
*/
UserModel.prototype.isUnitTest = function (body) {
if (!body.unittest) return false
if (!this.clear.email.split('@').pop() === this.config.tests.domain) return false
UserModel.prototype.isTest = function (body) {
// Disalowing tests in prodution is hard-coded to protect people from
if (this.config.env === 'production' && !this.config.tests.production) return false
if (!body.test) return false
if (this.clear?.email && !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

View file

@ -0,0 +1,51 @@
import { CuratedSetsController } from '../controllers/curated-sets.mjs'
const CuratedSets = new CuratedSetsController()
const jwt = ['jwt', { session: false }]
const bsc = ['basic', { session: false }]
export function curatedSetsRoutes(tools) {
const { app, passport } = tools
// Read a curated measurements set (no need to authenticate for this)
app.get('/curated-sets/:id.json', (req, res) => CuratedSets.read(req, res, tools, 'json'))
app.get('/curated-sets/:id.yaml', (req, res) => CuratedSets.read(req, res, tools, 'yaml'))
app.get('/curated-sets/:id', (req, res) => CuratedSets.read(req, res, tools))
// Get a list of all curated measurments sets (no need to authenticate for this)
app.get('/curated-sets.json', (req, res) => CuratedSets.list(req, res, tools, 'json'))
app.get('/curated-sets.yaml', (req, res) => CuratedSets.list(req, res, tools, 'yaml'))
app.get('/curated-sets', (req, res) => CuratedSets.list(req, res, tools))
// Create a curated measurements set
app.post('/curated-sets/jwt', passport.authenticate(...jwt), (req, res) =>
CuratedSets.create(req, res, tools)
)
app.post('/curated-sets/key', passport.authenticate(...bsc), (req, res) =>
CuratedSets.create(req, res, tools)
)
// Clone a curated measurements set
app.post('/curated-sets/:id/clone/jwt', passport.authenticate(...jwt), (req, res) =>
CuratedSets.clone(req, res, tools)
)
app.post('/curated-sets/:id/clone/key', passport.authenticate(...bsc), (req, res) =>
CuratedSets.clone(req, res, tools)
)
// Update a curated measurements set
app.patch('/curated-sets/:id/jwt', passport.authenticate(...jwt), (req, res) =>
CuratedSets.update(req, res, tools)
)
app.patch('/curated-sets/:id/key', passport.authenticate(...bsc), (req, res) =>
CuratedSets.update(req, res, tools)
)
// Delete a curated measurements set
app.delete('/curated-sets/:id/jwt', passport.authenticate(...jwt), (req, res) =>
CuratedSets.delete(req, res, tools)
)
app.delete('/curated-sets/:id/key', passport.authenticate(...bsc), (req, res) =>
CuratedSets.delete(req, res, tools)
)
}

View file

@ -3,7 +3,7 @@ import { usersRoutes } from './users.mjs'
import { setsRoutes } from './sets.mjs'
import { patternsRoutes } from './patterns.mjs'
import { confirmationsRoutes } from './confirmations.mjs'
//import { curatedSetsRoutes } from './curated-sets.mjs'
import { curatedSetsRoutes } from './curated-sets.mjs'
export const routes = {
apikeysRoutes,
@ -11,5 +11,5 @@ export const routes = {
setsRoutes,
patternsRoutes,
confirmationsRoutes,
//curatedSetsRoutes,
curatedSetsRoutes,
}

View file

@ -1,5 +1,10 @@
import { website } from '../config.mjs'
/*
* Capitalizes a string
*/
export const capitalize = (string) => string.charAt(0).toUpperCase() + string.slice(1)
/*
* Cleans a string (typically email) for hashing
*/

View file

@ -232,7 +232,7 @@ export const accountTests = async (chai, config, expect, store) => {
)
.send({
email: `updating_${store.randomString()}@${store.config.tests.domain}`,
unittest: true,
test: true,
})
.end((err, res) => {
expect(err === null).to.equal(true)
@ -287,7 +287,7 @@ export const accountTests = async (chai, config, expect, store) => {
)
.send({
email: store.account.email,
unittest: true,
test: true,
})
.end((err, res) => {
expect(err === null).to.equal(true)

View file

@ -0,0 +1,309 @@
import { cat } from './cat.mjs'
import { capitalize } from '../src/utils/index.mjs'
export const curatedSetTests = async (chai, config, expect, store) => {
const data = {
jwt: {
nameDe: 'Beispielmessungen A',
nameEn: 'Example measurements A',
nameEs: 'Medidas de ejemplo A',
nameFr: 'Mesures exemple A',
nameNl: 'Voorbeel maten A',
notesDe: 'Das sind die Notizen A',
notesEn: 'These are the notes A',
notesEs: 'Estas son las notas A',
notesFr: 'Ce sont les notes A',
notesNl: 'Dit zijn de notities A',
measies: {
chest: 1000,
neck: 420,
},
},
key: {
nameDe: 'Beispielmessungen B',
nameEn: 'Example measurements B',
nameEs: 'Medidas de ejemplo B',
nameFr: 'Mesures exemple B',
nameNl: 'Voorbeel maten B',
notesDe: 'Das sind die Notizen B',
notesEn: 'These are the notes B',
notesEs: 'Estas son las notas B',
notesFr: 'Ce sont les notes B',
notesNl: 'Dit zijn de notities B',
measies: {
chest: 930,
neck: 360,
},
img: cat,
},
}
store.curatedSet = {
jwt: {},
key: {},
}
store.altset = {
jwt: {},
key: {},
}
for (const auth of ['jwt', 'key']) {
describe(`${store.icon('set', auth)} Curated set tests (${auth})`, () => {
it(`${store.icon('set', auth)} Should create a new curated set (${auth})`, (done) => {
chai
.request(config.api)
.post(`/curated-sets/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.send(data[auth])
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(201)
expect(res.body.result).to.equal(`success`)
for (const [key, val] of Object.entries(data[auth])) {
if (!['measies', 'img'].includes(key)) expect(res.body.curatedSet[key]).to.equal(val)
}
store.curatedSet[auth] = res.body.curatedSet
done()
})
}).timeout(5000)
for (const field of ['name', 'notes']) {
for (const lang of config.languages) {
const langField = field + capitalize(lang)
it(`${store.icon(
'set',
auth
)} Should update the ${langField} field (${auth})`, (done) => {
const data = {}
const val = store.curatedSet[auth][langField] + '_updated'
data[langField] = val
chai
.request(config.api)
.patch(`/curated-sets/${store.curatedSet[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.set[langField]).to.equal(val)
done()
})
})
}
}
for (const field of ['chest', 'neck', 'ankle']) {
it(`${store.icon(
'set',
auth
)} Should update the ${field} measurement (${auth})`, (done) => {
const data = { measies: {} }
const val = Math.ceil(Math.random() * 1000)
data.measies[field] = val
chai
.request(config.api)
.patch(`/curated-sets/${store.curatedSet[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.set.measies[field]).to.equal(val)
done()
})
})
}
it(`${store.icon(
'set',
auth
)} Should not set an non-existing measurement (${auth})`, (done) => {
chai
.request(config.api)
.patch(`/curated-sets/${store.curatedSet[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({
measies: {
ankle: 320,
potatoe: 12,
},
})
.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.set.measies.ankle).to.equal(320)
expect(typeof res.body.set.measies.potatoe).to.equal('undefined')
done()
})
})
it(`${store.icon('set', auth)} Should clear a measurement (${auth})`, (done) => {
chai
.request(config.api)
.patch(`/curated-sets/${store.curatedSet[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({
measies: {
chest: null,
},
})
.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.set.measies.chest).to.equal('undefined')
done()
})
})
it(`${store.icon('set', auth)} Should clone a set (${auth})`, (done) => {
chai
.request(config.api)
.post(`/curated-sets/${store.curatedSet[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'
)
)
.send({ language: 'nl' })
.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.error).to.equal(`undefined`)
expect(typeof res.body.set.id).to.equal(`number`)
expect(res.body.set.name).to.equal(store.curatedSet[auth].nameNl + '_updated')
expect(res.body.set.notes).to.equal(store.curatedSet[auth].notesNl + '_updated')
done()
})
})
})
}
// Unauthenticated tests
describe(`${store.icon('set')} Curated set tests (unauthenticated)`, () => {
for (const auth of ['jwt', 'key']) {
it(`${store.icon('set')} Should read a curated set created with ${auth}`, (done) => {
chai
.request(config.api)
.get(`/curated-sets/${store.curatedSet[auth].id}`)
.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.curatedSet.measies).to.equal('object')
done()
})
})
it(`${store.icon('set')} Should read a curated set created with ${auth} as JSON`, (done) => {
chai
.request(config.api)
.get(`/curated-sets/${store.curatedSet[auth].id}.json`)
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(typeof res.body.measurements).to.equal('object')
done()
})
})
it(`${store.icon('set')} Should read a curated set created with ${auth} as YAML`, (done) => {
chai
.request(config.api)
.get(`/curated-sets/${store.curatedSet[auth].id}.yaml`)
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.text).to.include('FreeSewing')
done()
})
})
it(`${store.icon('set')} Should retrieve a list of curated sets`, (done) => {
chai
.request(config.api)
.get(`/curated-sets`)
.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('set')} Should retrieve a list of curated sets as JSON`, (done) => {
chai
.request(config.api)
.get(`/curated-sets.json`)
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
done()
})
})
it(`${store.icon('set')} Should retrieve a list of curated sets as YAML`, (done) => {
chai
.request(config.api)
.get(`/curated-sets.yaml`)
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
done()
})
})
}
})
//console.log(`/curated-sets/${store.curatedSet[auth].id}/${auth}`)
//console.log({body: res.body, status: res.status})
// TODO:
// - Delete Curated set
}

View file

@ -3,16 +3,18 @@ import { mfaTests } from './mfa.mjs'
import { accountTests } from './account.mjs'
import { apikeyTests } from './apikey.mjs'
import { setTests } from './set.mjs'
import { curatedSetTests } from './curated-set.mjs'
import { patternTests } from './pattern.mjs'
import { setup } from './shared.mjs'
const runTests = async (...params) => {
await userTests(...params)
await mfaTests(...params)
await apikeyTests(...params)
await accountTests(...params)
await setTests(...params)
await patternTests(...params)
//await userTests(...params)
//await mfaTests(...params)
//await apikeyTests(...params)
//await accountTests(...params)
//await setTests(...params)
await curatedSetTests(...params)
//await patternTests(...params)
}
// Load initial data required for tests

View file

@ -10,7 +10,7 @@ export const setTests = async (chai, config, expect, store) => {
neck: 420,
},
public: true,
unittest: true,
test: true,
imperial: true,
},
key: {
@ -22,7 +22,7 @@ export const setTests = async (chai, config, expect, store) => {
},
public: false,
img: cat,
unittest: true,
test: true,
imperial: false,
},
}
@ -56,8 +56,7 @@ export const setTests = async (chai, config, expect, store) => {
expect(res.status).to.equal(201)
expect(res.body.result).to.equal(`success`)
for (const [key, val] of Object.entries(data[auth])) {
if (!['measies', 'img', 'unittest'].includes(key))
expect(res.body.set[key]).to.equal(val)
if (!['measies', 'img', 'test'].includes(key)) expect(res.body.set[key]).to.equal(val)
}
store.set[auth] = res.body.set
done()

View file

@ -53,7 +53,8 @@ export const setup = async () => {
result = await axios.post(`${store.config.api}/signup`, {
email: store[acc].email,
language: store[acc].language,
unittest: true,
test: true,
role: 'curator',
})
} catch (err) {
console.log('Failed at first setup request', err)
@ -80,7 +81,7 @@ export const setup = async () => {
`${store.config.api}/apikeys/jwt`,
{
name: 'Test API key',
level: 4,
level: 5,
expiresIn: 60,
},
{