1
0
Fork 0

chore(backend): Migrated from sanity to cloudflare images

This commit is contained in:
Joost De Cock 2023-08-08 06:51:56 +02:00
parent 550830310c
commit 6b8060e05c
11 changed files with 196 additions and 185 deletions

View file

@ -38,13 +38,16 @@ BACKEND_WEBSITE_SCHEME=https
##################################################################### #####################################################################
# For users # For users
#BACKEND_AVATAR_USER=https://freesewing.org/avatar.svg #BACKEND_AVATAR_USER=default-avatar
# For measurement sets # For measurement sets
#BACKEND_AVATAR_SET=https://freesewing.org/avatar.svg #BACKEND_AVATAR_SET=default-avatar
# For curated measurement sets
#BACKEND_AVATAR_CSET=default-avatar
# For patterns # For patterns
#BACKEND_AVATAR_PATTERN=https://freesewing.org/avatar.svg #BACKEND_AVATAR_PATTERN=default-avatar
##################################################################### #####################################################################
@ -110,38 +113,23 @@ BACKEND_AWS_SES_BCC='["FreeSewing records <records@freesewing.org>"]'
##################################################################### #####################################################################
# Sanity # # Clouldflare images #
# # # #
# We use Sanity to store the avatars of our users # # We use Cloudflare's images service to store images #
##################################################################### #####################################################################
# Set this to no to disable Sanity altogther # Set this to no to disable Cloudflare images altogther
# This will cause uploading images to not work # This will cause uploading images to not work
BACKEND_ENABLE_SANITY=no BACKEND_ENABLE_CLOUDFLARE_IMAGES=yes
# Sanity project ID # Cloudflare account ID
#SANITY_PROJECT BACKEND_CLOUDFLARE_ACCOUNT=yourAccountIdHere
# Sanity dataset # Cloudflare Image Delivery URL
#SANITY_DATASET BACKEND_CLOUDFLARE_IMAGE_URL=https://imagedelivery.net/ouSuR9yY1bHt-fuAokSA5Q
# Sanity access token # Cloudflare API token
#SANITY_TOKEN BACKEND_CLOUDFLARE_IMAGES_TOKEN=yourTokenHere
# Sanity API version
#SANITY_VERSION=v2022-10-31
#####################################################################
# Payments via Stripe #
# #
# We use Stripe as payments processor for patron contributions #
#####################################################################
# Set this to no to disable Stripe payments altogther
BACKEND_ENABLE_PAYMENTS=no
# Stripe API key with permissions to create a payment intent
#BACKEND_STRIPE_CREATE_INTENT_KEY=yourKeyHere
##################################################################### #####################################################################
# Github integration # # Github integration #
@ -206,8 +194,8 @@ BACKEND_TESTS_DOMAIN=freesewing.dev
# Only relevant if BACKEND_TEST_ALLOW is true # Only relevant if BACKEND_TEST_ALLOW is true
#BACKEND_ENABLE_TESTS_EMAIL=yes #BACKEND_ENABLE_TESTS_EMAIL=yes
# Whether toinclude the (slow) tests that involve reaching out to Sanity # Whether toinclude the (slow) tests that involve reaching out to cloudflare
#BACKEND_ENABLE_TESTS_SANITY=yes #BACKEND_ENABLE_TESTS_CLOUDFLARE_IMAGES=yes
##################################################################### #####################################################################

View file

@ -42,12 +42,12 @@ const baseConfig = {
github: envToBool(process.env.BACKEND_ENABLE_OAUTH_GITHUB), github: envToBool(process.env.BACKEND_ENABLE_OAUTH_GITHUB),
google: envToBool(process.env.BACKEND_ENABLE_OAUTH_GOOGLE), google: envToBool(process.env.BACKEND_ENABLE_OAUTH_GOOGLE),
}, },
sanity: envToBool(process.env.BACKEND_ENABLE_SANITY), cloudflareImages: envToBool(process.env.BACKEND_ENABLE_CLOUDFLARE_IMAGES),
ses: envToBool(process.env.BACKEND_ENABLE_AWS_SES), ses: envToBool(process.env.BACKEND_ENABLE_AWS_SES),
tests: { tests: {
base: envToBool(process.env.BACKEND_ENABLE_TESTS), base: envToBool(process.env.BACKEND_ENABLE_TESTS),
email: envToBool(process.env.BACKEND_ENABLE_TESTS_EMAIL), email: envToBool(process.env.BACKEND_ENABLE_TESTS_EMAIL),
sanity: envToBool(process.env.BACKEND_ENABLE_TESTS_SANITY), cloudflareImages: envToBool(process.env.BACKEND_ENABLE_TESTS_CLOUDFLARE_IMAGES),
}, },
import: envToBool(process.env.BACKEND_ENABLE_IMPORT), import: envToBool(process.env.BACKEND_ENABLE_IMPORT),
}, },
@ -58,9 +58,10 @@ const baseConfig = {
expiryMaxSeconds: 365 * 24 * 3600, expiryMaxSeconds: 365 * 24 * 3600,
}, },
avatars: { avatars: {
user: process.env.BACKEND_AVATAR_USER || 'https://freesewing.org/avatar.svg', user: process.env.BACKEND_AVATAR_USER || 'default-avatar',
set: process.env.BACKEND_AVATAR_SET || 'https://freesewing.org/avatar.svg', set: process.env.BACKEND_AVATAR_SET || 'default-avatar',
pattern: process.env.BACKEND_AVATAR_PATTERN || 'https://freesewing.org/avatar.svg', cset: process.env.BACKEND_AVATAR_CSET || 'default-avatar',
pattern: process.env.BACKEND_AVATAR_PATTERN || 'default-avatar',
}, },
db: { db: {
url: process.env.BACKEND_DB_URL || './db.sqlite', url: process.env.BACKEND_DB_URL || './db.sqlite',
@ -141,17 +142,15 @@ if (baseConfig.use.github)
}, },
} }
// Sanity config // Cloudflare Images config
if (baseConfig.use.sanity) if (baseConfig.use.cloudflareImages) {
baseConfig.sanity = { const account = process.env.BACKEND_CLOUDFLARE_ACCOUNT || 'fixmeSetCloudflareAccountId'
project: process.env.SANITY_PROJECT, baseConfig.cloudflareImages = {
dataset: process.env.SANITY_DATASET || 'site-content', account,
token: process.env.SANITY_TOKEN || 'fixmeSetSanityToken', api: `https://api.cloudflare.com/client/v4/accounts/${account}/images/v1`,
version: process.env.SANITY_VERSION || 'v2022-10-31', token: process.env.BACKEND_CLOUDFLARE_IMAGES_TOKEN || 'fixmeSetCloudflareToken',
api: `https://${process.env.SANITY_PROJECT || 'missing-project-id'}.api.sanity.io/${
process.env.SANITY_VERSION || 'v2022-10-31'
}`,
} }
}
// AWS SES config (for sending out emails) // AWS SES config (for sending out emails)
if (baseConfig.use.ses) if (baseConfig.use.ses)
@ -193,7 +192,7 @@ if (baseConfig.use.oauth?.google)
const config = postConfig(baseConfig) const config = postConfig(baseConfig)
// Exporting this stand-alone config // Exporting this stand-alone config
export const sanity = config.sanity || {} export const cloudflareImages = config.cloudflareImages || {}
export const website = config.website export const website = config.website
const vars = { const vars = {
@ -206,7 +205,7 @@ const vars = {
BACKEND_JWT_EXPIRY: 'optional', BACKEND_JWT_EXPIRY: 'optional',
// Feature flags // Feature flags
BACKEND_ENABLE_AWS_SES: 'optional', BACKEND_ENABLE_AWS_SES: 'optional',
BACKEND_ENABLE_SANITY: 'optional', BACKEND_ENABLE_CLOUDFLARE_IMAGES: 'optional',
BACKEND_ENABLE_GITHUB: 'optional', BACKEND_ENABLE_GITHUB: 'optional',
BACKEND_ENABLE_OAUTH_GITHUB: 'optional', BACKEND_ENABLE_OAUTH_GITHUB: 'optional',
BACKEND_ENABLE_OAUTH_GOOGLE: 'optional', BACKEND_ENABLE_OAUTH_GOOGLE: 'optional',
@ -227,12 +226,10 @@ if (envToBool(process.env.BACKEND_ENABLE_AWS_SES)) {
vars.BACKEND_AWS_SES_CC = 'optional' vars.BACKEND_AWS_SES_CC = 'optional'
vars.BACKEND_AWS_SES_BCC = 'optional' vars.BACKEND_AWS_SES_BCC = 'optional'
} }
// Vars for Sanity integration // Vars for Cloudflare Images integration
if (envToBool(process.env.BACKEND_USE_SANITY)) { if (envToBool(process.env.BACKEND_USE_CLOUDFLARE_IMAGES)) {
vars.SANITY_PROJECT = 'required' vars.BACKEND_CLOUDFLARE_IMAGES_TOKEN = 'requiredSecret'
vars.SANITY_TOKEN = 'requiredSecret' vars.BACKEND_TEST_CLOUDFLARE_IMAGES = 'optional'
vars.SANITY_VERSION = 'optional'
vars.BACKEND_TEST_SANITY = 'optional'
} }
// Vars for Github integration // Vars for Github integration
if (envToBool(process.env.BACKEND_ENABLE_GITHUB)) { if (envToBool(process.env.BACKEND_ENABLE_GITHUB)) {
@ -327,10 +324,13 @@ export function verifyConfig(silent = false) {
config.jwt.secretOrKey.slice(0, 4) + '**redacted**' + config.jwt.secretOrKey.slice(-4), config.jwt.secretOrKey.slice(0, 4) + '**redacted**' + config.jwt.secretOrKey.slice(-4),
}, },
} }
if (config.sanity) if (config.cloudflareImages)
dump.sanity = { dump.cloudflareImages = {
...config.sanity, ...config.cloudflareImages,
token: config.sanity.token.slice(0, 4) + '**redacted**' + config.sanity.token.slice(-4), token:
config.cloudflareImages.token.slice(0, 4) +
'**redacted**' +
config.cloudflareImages.token.slice(-4),
} }
console.log(chalk.cyan.bold('Dumping configuration:\n'), asJson(dump, null, 2)) console.log(chalk.cyan.bold('Dumping configuration:\n'), asJson(dump, null, 2))
} }

View file

@ -1,6 +1,6 @@
import { capitalize } from '../utils/index.mjs' import { capitalize } from '../utils/index.mjs'
import { log } from '../utils/log.mjs' import { log } from '../utils/log.mjs'
import { setSetAvatar } from '../utils/sanity.mjs' import { storeImage } from '../utils/cloudflare-images.mjs'
import yaml from 'js-yaml' import yaml from 'js-yaml'
export function CuratedSetModel(tools) { export function CuratedSetModel(tools) {
@ -28,7 +28,7 @@ CuratedSetModel.prototype.guardedCreate = async function ({ body, user }) {
} }
if (body.measies) data.measies = this.sanitizeMeasurements(body.measies) if (body.measies) data.measies = this.sanitizeMeasurements(body.measies)
else data.measies = {} else data.measies = {}
// Set this one initially as we need the ID to create a custom img via Sanity // Set this one initially as we need the ID to store an image on cloudflare
data.img = this.config.avatars.set data.img = this.config.avatars.set
// Create record // Create record
@ -36,10 +36,14 @@ CuratedSetModel.prototype.guardedCreate = async function ({ body, user }) {
// Update img? (now that we have the ID) // Update img? (now that we have the ID)
const img = const img =
this.config.use.sanity && this.config.use.cloudflareImages &&
typeof body.img === 'string' && typeof body.img === 'string' &&
(!body.test || (body.test && this.config.use.tests?.sanity)) (!body.test || (body.test && this.config.use.tests?.cloudflareImages))
? await setSetAvatar(this.record.id, body.img) ? await storeImage({
id: `cset-${this.record.id}`,
metadata: { user: user.uid },
b64: body.img,
})
: false : false
if (img) await this.unguardedUpdate({ img: img.url }) if (img) await this.unguardedUpdate({ img: img.url })
@ -235,7 +239,11 @@ CuratedSetModel.prototype.guardedUpdate = async function ({ params, body, user }
// Image (img) // Image (img)
if (typeof body.img === 'string') { if (typeof body.img === 'string') {
const img = await setSetAvatar(params.id, body.img) const img = await storeImage({
id: `cset-${this.record.id}`,
metadata: { user: this.user.uid },
b64: body.img,
})
data.img = img.url data.img = img.url
} }

View file

@ -1,6 +1,6 @@
import { log } from '../utils/log.mjs' import { log } from '../utils/log.mjs'
import { capitalize } from '../utils/index.mjs' import { capitalize } from '../utils/index.mjs'
import { setPatternAvatar } from '../utils/sanity.mjs' import { storeImage } from '../utils/cloudflare-images.mjs'
import yaml from 'js-yaml' import yaml from 'js-yaml'
import { SetModel } from './set.mjs' import { SetModel } from './set.mjs'
@ -75,7 +75,7 @@ PatternModel.prototype.guardedCreate = async function ({ body, user }) {
// Public // Public
if (body.public === true) data.public = true if (body.public === true) data.public = true
data.userId = user.uid data.userId = user.uid
// Set this one initially as we need the ID to create a custom img via Sanity // Set this one initially as we need the ID to store an image on cloudflare
data.img = this.config.avatars.pattern data.img = this.config.avatars.pattern
// Create record // Create record
@ -83,10 +83,14 @@ PatternModel.prototype.guardedCreate = async function ({ body, user }) {
// Update img? (now that we have the ID) // Update img? (now that we have the ID)
const img = const img =
this.config.use.sanity && this.config.use.cloudflareImages &&
typeof body.img === 'string' && typeof body.img === 'string' &&
(!body.test || (body.test && this.config.use.tests?.sanity)) (!body.test || (body.test && this.config.use.tests?.cloudflareImages))
? await setPatternAvatar(this.record.id, body.img) ? await storeImage({
id: `pattern-${this.record.id}`,
metadata: { user: user.uid },
b64: body.img,
})
: false : false
if (img) await this.unguardedUpdate(this.cloak({ img: img.url })) if (img) await this.unguardedUpdate(this.cloak({ img: img.url }))
@ -300,7 +304,11 @@ PatternModel.prototype.guardedUpdate = async function ({ params, body, user }) {
if (typeof body.settings === 'object') data.settings = body.settings if (typeof body.settings === 'object') data.settings = body.settings
// Image (img) // Image (img)
if (typeof body.img === 'string') { if (typeof body.img === 'string') {
const img = await setPatternAvatar(params.id, body.img) const img = await storeImage({
id: `pattern-${this.record.id}`,
metadata: { user: this.user.uid },
b64: body.img,
})
data.img = img.url data.img = img.url
} }

View file

@ -1,5 +1,5 @@
import { log } from '../utils/log.mjs' import { log } from '../utils/log.mjs'
import { setSetAvatar, downloadImage } from '../utils/sanity.mjs' import { storeImage } from '../utils/cloudflare-images.mjs'
import yaml from 'js-yaml' import yaml from 'js-yaml'
export function SetModel(tools) { export function SetModel(tools) {
@ -32,7 +32,7 @@ SetModel.prototype.guardedCreate = async function ({ body, user }) {
else data.measies = {} else data.measies = {}
data.imperial = body.imperial === true ? true : false data.imperial = body.imperial === true ? true : false
data.userId = user.uid data.userId = user.uid
// Set this one initially as we need the ID to create a custom img via Sanity // Set this one initially as we need the ID to store the image on cloudflare
data.img = this.config.avatars.set data.img = this.config.avatars.set
// Create record // Create record
@ -40,10 +40,18 @@ SetModel.prototype.guardedCreate = async function ({ body, user }) {
// Update img? (now that we have the ID) // Update img? (now that we have the ID)
const img = const img =
this.config.use.sanity && this.config.use.cloudflareImages &&
typeof body.img === 'string' && typeof body.img === 'string' &&
(!body.test || (body.test && this.config.use.tests?.sanity)) (!body.test || (body.test && this.config.use.tests?.cloudflareImages))
? await setSetAvatar(this.record.id, body.img, this.clear.name) ? await storeImage({
id: `set-${this.record.id}`,
metadata: {
user: user.uid,
name: this.clear.name,
},
b64: body.img,
requireSignedURLs: true,
})
: false : false
if (img) await this.unguardedUpdate(this.cloak({ img: img.url })) if (img) await this.unguardedUpdate(this.cloak({ img: img.url }))
@ -264,7 +272,15 @@ SetModel.prototype.guardedUpdate = async function ({ params, body, user }) {
// Image (img) // Image (img)
if (typeof body.img === 'string') { if (typeof body.img === 'string') {
const img = await setSetAvatar(params.id, body.img, data.name || this.clear.name) const img = await replaceImage({
id: `set-${this.record.id}`,
metadata: {
user: user.uid,
name: this.clear.name,
},
b64: body.img,
notPublic: true,
})
data.img = img.url data.img = img.url
} }

View file

@ -1,5 +1,4 @@
import { hash, randomString } from '../utils/crypto.mjs' import { hash, randomString } from '../utils/crypto.mjs'
import { setUserAvatar } from '../utils/sanity.mjs'
import { log } from '../utils/log.mjs' import { log } from '../utils/log.mjs'
import { clean, i18nUrl } from '../utils/index.mjs' import { clean, i18nUrl } from '../utils/index.mjs'

View file

@ -1,7 +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 { setUserAvatar, downloadImage } from '../utils/sanity.mjs' import { storeImage, replaceImage } from '../utils/cloudflare-images.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'
import { SetModel } from './set.mjs' import { SetModel } from './set.mjs'
@ -250,7 +250,7 @@ UserModel.prototype.guardedCreate = async function ({ body }) {
password: asJson(hashPassword(randomString())), // We'll change this later password: asJson(hashPassword(randomString())), // We'll change this later
github: this.encrypt(''), github: this.encrypt(''),
bio: this.encrypt(''), bio: this.encrypt(''),
// Set this one initially as we need the ID to create a custom img via Sanity // Set this one initially as we need the ID to store an image on Cloudflare
img: this.encrypt(this.config.avatars.user), img: this.encrypt(this.config.avatars.user),
} }
// During tests, users can set their own permission level so you can test admin stuff // During tests, users can set their own permission level so you can test admin stuff
@ -544,10 +544,15 @@ UserModel.prototype.guardedUpdate = async function ({ body, user }) {
} }
} }
// Image (img) // Image (img)
if (typeof body.img === 'string') { if (typeof body.img === 'string')
const img = await setUserAvatar(this.record.ihash, body.img, data.username) data.img = await replaceImage({
data.img = img.url id: `user-${this.record.ihash}`,
} metadata: {
user: user.uid,
ihash: this.record.ihash,
},
b64: body.img,
})
// Now update the record // Now update the record
await this.unguardedUpdate(this.cloak(data)) await this.unguardedUpdate(this.cloak(data))

View file

@ -0,0 +1,84 @@
import axios from 'axios'
import { cloudflareImages as config } from '../config.mjs'
// We'll use this a bunch
const headers = { Authorization: `Bearer ${config.token}` }
/*
* Method that does the actual image upload to cloudflare
* Use this for a new image that does not yet exist
*/
export async function storeImage(props) {
const form = getFormData(props)
let result
try {
result = await axios.post(config.api, form, { headers })
} catch (err) {
if (err.response.status == 409) {
/*
* Image already exists.
* Cloudflare does not support udating the image,
* so we need to delete it and upload again
*/
try {
log.warn(props.id, 'Called storeImage when replaceImage should have been used')
await axios.delete(`${config.api}/${props.id}`)
result = await axios.post(config.api, form, { headers })
} catch (err) {
console.log('Failed to replace image on cloudflare', err)
}
} else {
console.log('Failed to upload image to cloudflare', err)
}
}
return result.data?.result?.id ? result.data.result.id : false
}
/*
* Method that does the actual image upload to cloudflare
* Use this to replace an existing image
*/
export async function replaceImage(props) {
const form = getFormData(props)
// Ignore errors on delete, probably means the image does not exist
try {
await axios.delete(`${config.api}/${props.id}`)
} catch (err) {}
let result
try {
result = await axios.post(config.api, form, { headers })
} catch (err) {
console.log('Failed to replace image on cloudflare', err)
}
return result.data?.result?.id ? result.data.result.id : false
}
/*
* Helper method to construct the form data for cloudflare
*/
function getFormData({ id, metadata, url = false, b64 = false, blob = false, notPublic = false }) {
const form = new FormData()
form.append('id', id)
form.append('metadata', JSON.stringify(metadata))
// Handle base-64 encoded data
if (b64) form.append('file', b64ToBlob(b64), id)
// Handle binary data
else if (blob) form.append('file', blob)
// Handle URL-based upload
else if (url) form.append('url', url)
// Handle requireSignedURLs
if (notPublic) form.append('requireSignedURLs', true)
return form
}
/*
* Helper method to turn a data-uri into binary data
*/
function b64ToBlob(dataUri) {
const [start, data] = dataUri.split(';base64,')
return new Blob([new Buffer.from(data, 'base64')])
}

View file

@ -1,98 +0,0 @@
import axios from 'axios'
import { sanity as config } from '../config.mjs'
import { createClient } from '@sanity/client'
const sanity = createClient({
projectId: config.project,
dataset: config.dataset,
useCdn: true, // Set to false to bypass cache
apiVersion: config.version,
token: config.token,
})
// We'll use this a bunch
const headers = { Authorization: `Bearer ${config.token}` }
/*
* Turns a sanity asset _ref into an image URL
*/
function imageUrl(ref) {
return `https://cdn.sanity.io/images/${config.project}/${config.dataset}/${ref.slice(6)}`
}
/*
* Retrieval of avatar images from the Sanity API
*/
export const getUserAvatar = async (id) => getAvatar('user', id)
export const getSetAvatar = async (id) => getAvatar('set', id)
async function getAvatar(type, id) {
const url =
`${config.api}/data/query/${config.dataset}?query=` +
encodeURIComponent(`*[_type=='${type}img' && recordid==${id}]{ img }`)
const result = await axios.get(url, { headers })
if (result.data?.result && Array.isArray(result.data.result) && result.data.result.length === 1) {
return imageUrl(result.data.result[0].img.asset._ref)
}
return false
}
/*
* Uploads an image to sanity
*/
export const setUserAvatar = async (id, data, title) => setAvatar('user', id, data, title)
export const setSetAvatar = async (id, data, title) => setAvatar('set', id, data, title)
export const setPatternAvatar = async (id, data, title) => setAvatar('pattern', id, data, title)
export async function setAvatar(type, id, data, title) {
// Step 1: Upload the image as asset
const [contentType, binary] = Array.isArray(data) ? data : b64ToBinaryWithType(data)
if (!contentType) return ''
const imgDocument = await sanity.assets.upload('image', binary, {
contentType,
filename: `${type}.${contentType.split('/').pop()}`,
})
const document = await sanity.createOrReplace({
_id: `${type}-${id}`,
_type: `${type}img`,
title: title,
recordid: id,
img: {
asset: {
_ref: imgDocument._id,
_type: 'reference',
},
},
})
return document._id
}
/*
* Helper method to turn a data-uri into binary data + content type
*/
function b64ToBinaryWithType(dataUri) {
let type = false
const [start, data] = dataUri.split(';base64,')
if (start.includes('image/png')) type = 'image/png'
else if (start.includes('image/jpg')) type = 'image/jpeg'
else if (start.includes('image/jpeg')) type = 'image/jpeg'
return [type, new Buffer.from(data, 'base64')]
}
/*
* Helper method to download an image
* Used in import only, thus ok for removal post v3 release
*/
export const downloadImage = async (url) => {
let result
try {
result = await axios.get(url, { responseType: 'arraybuffer' })
} catch (err) {
console.log(`Could not download from ${url}`, err, result)
}
// Returning [contentType, data]
return [result.headers['content-type'], Buffer.from(result.data, 'binary')]
}

View file

@ -188,8 +188,11 @@ export const accountTests = async (chai, config, expect, store) => {
done() done()
}) })
}) })
/*
if (store.config.tests.includeSanity) { * Running this twice immeadiatly (jwt and key) will break because cloudflare api
* will not be ready yet
*/
if (store.config.use.tests.cloudflareImages && auth === 'jwt') {
it(`${store.icon('user', auth)} Should update the account img (${auth})`, (done) => { it(`${store.icon('user', auth)} Should update the account img (${auth})`, (done) => {
chai chai
.request(config.api) .request(config.api)

View file

@ -124,6 +124,4 @@ export const setup = async () => {
return { chai, config, expect, store } return { chai, config, expect, store }
} }
export const teardown = async function (/*store*/) { export const teardown = async function (/*store*/) {}
//console.log(store)
}