Migrate oauth store to pinia
This commit is contained in:
parent
a5cc7351ec
commit
216d318bb5
12 changed files with 663 additions and 145 deletions
1
changelog.d/oauth-store-to-pinia.change
Normal file
1
changelog.d/oauth-store-to-pinia.change
Normal file
|
@ -0,0 +1 @@
|
|||
Internal: Migrate OAuth store to pinia
|
|
@ -1,3 +1,4 @@
|
|||
/* global process */
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
|
@ -16,6 +17,7 @@ import { applyConfig } from '../services/style_setter/style_setter.js'
|
|||
import FaviconService from '../services/favicon_service/favicon_service.js'
|
||||
import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
|
||||
|
||||
import { useOAuthStore } from 'src/stores/oauth'
|
||||
import { useI18nStore } from 'src/stores/i18n'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
|
@ -227,8 +229,9 @@ const getStickers = async ({ store }) => {
|
|||
}
|
||||
|
||||
const getAppSecret = async ({ store }) => {
|
||||
if (store.state.oauth.userToken) {
|
||||
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||
const oauth = useOAuthStore()
|
||||
if (oauth.userToken) {
|
||||
store.commit('setBackendInteractor', backendInteractorService(oauth.getToken))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -322,20 +325,65 @@ const setConfig = async ({ store }) => {
|
|||
const apiConfig = configInfos[0]
|
||||
const staticConfig = configInfos[1]
|
||||
|
||||
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
|
||||
getAppSecret({ store })
|
||||
await setSettings({ store, apiConfig, staticConfig })
|
||||
}
|
||||
|
||||
const checkOAuthToken = async ({ store }) => {
|
||||
if (store.getters.getUserToken()) {
|
||||
return store.dispatch('loginUser', store.getters.getUserToken())
|
||||
const oauth = useOAuthStore()
|
||||
if (oauth.getUserToken) {
|
||||
return store.dispatch('loginUser', oauth.getUserToken)
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
||||
const app = createApp(App)
|
||||
// Must have app use pinia before we do anything that touches the store
|
||||
// https://pinia.vuejs.org/core-concepts/plugins.html#Introduction
|
||||
// "Plugins are only applied to stores created after the plugins themselves, and after pinia is passed to the app, otherwise they won't be applied."
|
||||
app.use(pinia)
|
||||
|
||||
const waitForAllStoresToLoad = async () => {
|
||||
// the stores that do not persist technically do not need to be awaited here,
|
||||
// but that involves either hard-coding the stores in some place (prone to errors)
|
||||
// or writing another vite plugin to analyze which stores needs persisting (++load time)
|
||||
const allStores = import.meta.glob('../stores/*.js', { eager: true })
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// do some checks to avoid common errors
|
||||
if (!Object.keys(allStores).length) {
|
||||
throw new Error('No stores are available. Check the code in src/boot/after_store.js')
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
Object.entries(allStores)
|
||||
.map(async ([name, mod]) => {
|
||||
const isStoreName = name => name.startsWith('use')
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (Object.keys(mod).filter(isStoreName).length !== 1) {
|
||||
throw new Error('Each store file must export exactly one store as a named export. Check your code in src/stores/')
|
||||
}
|
||||
}
|
||||
const storeFuncName = Object.keys(mod).find(isStoreName)
|
||||
if (storeFuncName && typeof mod[storeFuncName] === 'function') {
|
||||
const p = mod[storeFuncName]().$persistLoaded
|
||||
if (!(p instanceof Promise)) {
|
||||
throw new Error(`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`)
|
||||
}
|
||||
await p
|
||||
} else {
|
||||
throw new Error(`Store module ${name} does not export a 'use...' function`)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
try {
|
||||
await waitForAllStoresToLoad()
|
||||
} catch (e) {
|
||||
console.error('Cannot load stores:', e)
|
||||
storageError = e
|
||||
}
|
||||
|
||||
if (storageError) {
|
||||
useInterfaceStore().pushGlobalNotice({ messageKey: 'errors.storage_unavailable', level: 'error' })
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import { mapStores } from 'pinia'
|
||||
import oauthApi from '../../services/new_api/oauth.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
|
@ -17,11 +19,11 @@ const LoginForm = {
|
|||
computed: {
|
||||
isPasswordAuth () { return this.requiredPassword },
|
||||
isTokenAuth () { return this.requiredToken },
|
||||
...mapStores(useOAuthStore),
|
||||
...mapState({
|
||||
registrationOpen: state => state.instance.registrationOpen,
|
||||
instance: state => state.instance,
|
||||
loggingIn: state => state.users.loggingIn,
|
||||
oauth: state => state.oauth
|
||||
}),
|
||||
...mapGetters(
|
||||
'authFlow', ['requiredPassword', 'requiredToken', 'requiredMFA']
|
||||
|
@ -41,37 +43,30 @@ const LoginForm = {
|
|||
|
||||
// NOTE: we do not really need the app token, but obtaining a token and
|
||||
// calling verify_credentials is the only way to ensure the app still works.
|
||||
this.$store.dispatch('ensureAppToken')
|
||||
this.oauthStore.ensureAppToken()
|
||||
.then(() => {
|
||||
const app = {
|
||||
clientId: this.oauth.clientId,
|
||||
clientSecret: this.oauth.clientSecret,
|
||||
clientId: this.oauthStore.clientId,
|
||||
clientSecret: this.oauthStore.clientSecret,
|
||||
}
|
||||
oauthApi.login({ ...app, ...data })
|
||||
})
|
||||
},
|
||||
submitPassword () {
|
||||
const { clientId } = this.oauth
|
||||
const data = {
|
||||
clientId,
|
||||
oauth: this.oauth,
|
||||
instance: this.instance.server,
|
||||
commit: this.$store.commit
|
||||
}
|
||||
this.error = false
|
||||
|
||||
// NOTE: we do not really need the app token, but obtaining a token and
|
||||
// calling verify_credentials is the only way to ensure the app still works.
|
||||
this.$store.dispatch('ensureAppToken').then(() => {
|
||||
this.oauthStore.ensureAppToken().then(() => {
|
||||
const app = {
|
||||
clientId: this.oauth.clientId,
|
||||
clientSecret: this.oauth.clientSecret,
|
||||
clientId: this.oauthStore.clientId,
|
||||
clientSecret: this.oauthStore.clientSecret,
|
||||
}
|
||||
|
||||
oauthApi.getTokenWithCredentials(
|
||||
{
|
||||
...app,
|
||||
instance: data.instance,
|
||||
instance: this.instance.server,
|
||||
username: this.user.username,
|
||||
password: this.user.password
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import oauth from '../../services/new_api/oauth.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
|
||||
const oac = {
|
||||
props: ['code'],
|
||||
mounted () {
|
||||
if (this.code) {
|
||||
const { clientId, clientSecret } = this.$store.state.oauth
|
||||
const oauthStore = useOAuthStore()
|
||||
const { clientId, clientSecret } = oauthStore
|
||||
|
||||
oauth.getToken({
|
||||
clientId,
|
||||
|
@ -12,7 +14,7 @@ const oac = {
|
|||
instance: this.$store.state.instance.server,
|
||||
code: this.code
|
||||
}).then((result) => {
|
||||
this.$store.commit('setToken', result.access_token)
|
||||
oauthStore.setToken(result.access_token)
|
||||
this.$store.dispatch('loginUser', result.access_token)
|
||||
this.$router.push({ name: 'friends' })
|
||||
})
|
||||
|
|
|
@ -94,3 +94,160 @@ export default function createPersistedState ({
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* This persists state for pinia, which falls back to read from the vuex state
|
||||
* if pinia persisted state does not exist.
|
||||
*
|
||||
* When you migrate a module from vuex to pinia, you have to keep the original name.
|
||||
* If the module was called `xxx`, the name of the store has to be `xxx` too.
|
||||
*
|
||||
* This adds one property to the store, $persistLoaded, which is a promise
|
||||
* that resolves when the initial state is loaded. If the plugin is not enabled,
|
||||
* $persistLoaded is a promise that resolves immediately.
|
||||
* If we are not able to get the stored state because storage.getItem() throws or
|
||||
* rejects, $persistLoaded will be a rejected promise with the thrown error.
|
||||
*
|
||||
* Call signature:
|
||||
*
|
||||
* defineStore(name, {
|
||||
* ...,
|
||||
* // setting the `persist` property enables this plugin
|
||||
* // IMPORTANT: by default it is disabled, you have to set `persist` to at least an empty object
|
||||
* persist: {
|
||||
* // set to list of individual paths, or undefined/unset to persist everything
|
||||
* paths: [],
|
||||
* // function to call after loading initial state
|
||||
* // if afterLoad is a function, it must return a state object that will be sent to `store.$patch`, or a promise to the state object
|
||||
* // by default afterLoad is undefined
|
||||
* afterLoad: (originalState) => {
|
||||
* // ...
|
||||
* return modifiedState
|
||||
* },
|
||||
* // if it exists, only persist state after these actions
|
||||
* // if it doesn't exist or is undefined, persist state after every mutation of the state
|
||||
* saveImmediatelyActions: [],
|
||||
* // what to do after successfully saving the state
|
||||
* onSaveSuccess: () => {},
|
||||
* // what to do after there is an error saving the state
|
||||
* onSaveError: () => {}
|
||||
* }
|
||||
* })
|
||||
*
|
||||
*/
|
||||
export const piniaPersistPlugin = ({
|
||||
vuexKey = 'vuex-lz',
|
||||
keyFunction = (id) => `pinia-local-${id}`,
|
||||
storage = defaultStorage,
|
||||
reducer = defaultReducer
|
||||
} = {}) => ({ store, options }) => {
|
||||
if (!options.persist) {
|
||||
return {
|
||||
$persistLoaded: Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
let resolveLoaded
|
||||
let rejectLoaded
|
||||
const loadedPromise = new Promise((resolve, reject) => {
|
||||
resolveLoaded = resolve
|
||||
rejectLoaded = reject
|
||||
})
|
||||
|
||||
const {
|
||||
afterLoad,
|
||||
paths = [],
|
||||
saveImmediatelyActions,
|
||||
onSaveSuccess = () => {},
|
||||
onSaveError = () => {}
|
||||
} = options.persist || {}
|
||||
|
||||
const loadedGuard = { loaded: false }
|
||||
const key = keyFunction(store.$id)
|
||||
const getState = async () => {
|
||||
const id = store.$id
|
||||
const value = await storage.getItem(key)
|
||||
if (value) {
|
||||
return value
|
||||
}
|
||||
|
||||
const fallbackValue = await storage.getItem(vuexKey)
|
||||
if (fallbackValue && fallbackValue[id]) {
|
||||
console.info(`Migrating ${id} store data from vuex to pinia`)
|
||||
const res = fallbackValue[id]
|
||||
await storage.setItem(key, res)
|
||||
return res
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
const setState = (state) => {
|
||||
if (!loadedGuard.loaded) {
|
||||
console.info('waiting for old state to be loaded...')
|
||||
return Promise.reject()
|
||||
} else {
|
||||
return storage.setItem(key, state)
|
||||
}
|
||||
}
|
||||
|
||||
const getMaybeAugmentedState = async () => {
|
||||
const savedRawState = await getState()
|
||||
if (typeof afterLoad === 'function') {
|
||||
try {
|
||||
return await afterLoad(savedRawState)
|
||||
} catch (e) {
|
||||
console.error('Error running afterLoad:', e)
|
||||
return savedRawState
|
||||
}
|
||||
} else {
|
||||
return savedRawState
|
||||
}
|
||||
}
|
||||
|
||||
const persistCurrentState = async (state) => {
|
||||
const stateClone = cloneDeep(state)
|
||||
const stateToPersist = reducer(stateClone, paths)
|
||||
try {
|
||||
const res = await setState(stateToPersist)
|
||||
onSaveSuccess(res)
|
||||
} catch (e) {
|
||||
console.error('Cannot persist state:', e)
|
||||
onSaveError(e)
|
||||
}
|
||||
}
|
||||
|
||||
getMaybeAugmentedState()
|
||||
.then(savedState => {
|
||||
if (savedState) {
|
||||
store.$patch(savedState)
|
||||
}
|
||||
|
||||
loadedGuard.loaded = true
|
||||
resolveLoaded()
|
||||
|
||||
// only subscribe after we have done setting the initial state
|
||||
if (!saveImmediatelyActions) {
|
||||
store.$subscribe(async (_mutation, state) => {
|
||||
await persistCurrentState(state)
|
||||
})
|
||||
} else {
|
||||
store.$onAction(({
|
||||
name,
|
||||
store,
|
||||
after,
|
||||
}) => {
|
||||
if (saveImmediatelyActions.includes(name)) {
|
||||
after(() => persistCurrentState(store.$state))
|
||||
}
|
||||
})
|
||||
}
|
||||
}, error => {
|
||||
console.error('Cannot load storage:', error)
|
||||
rejectLoaded(error)
|
||||
})
|
||||
|
||||
return {
|
||||
$persistLoaded: loadedPromise
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import vuexModules from './modules/index.js'
|
|||
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import createPersistedState from './lib/persisted_state.js'
|
||||
import createPersistedState, { piniaPersistPlugin } from './lib/persisted_state.js'
|
||||
import pushNotifications from './lib/push_notifications_plugin.js'
|
||||
|
||||
import messages from './i18n/messages.js'
|
||||
|
@ -71,6 +71,8 @@ const persistedStateOptions = {
|
|||
let storageError
|
||||
const plugins = [pushNotifications]
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPersistPlugin())
|
||||
|
||||
try {
|
||||
const persistedState = await createPersistedState(persistedStateOptions)
|
||||
plugins.push(persistedState)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
|
||||
const PASSWORD_STRATEGY = 'password'
|
||||
const TOKEN_STRATEGY = 'token'
|
||||
|
||||
|
@ -68,8 +70,8 @@ const mutations = {
|
|||
// actions
|
||||
const actions = {
|
||||
|
||||
async login ({ state, dispatch, commit }, { access_token: accessToken }) {
|
||||
commit('setToken', accessToken, { root: true })
|
||||
async login ({ state, dispatch }, { access_token: accessToken }) {
|
||||
useOAuthStore().setToken(accessToken)
|
||||
await dispatch('loginUser', accessToken, { root: true })
|
||||
resetState(state)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import config from './config.js'
|
|||
import profileConfig from './profileConfig.js'
|
||||
import serverSideStorage from './serverSideStorage.js'
|
||||
import adminSettings from './adminSettings.js'
|
||||
import oauth from './oauth.js'
|
||||
import authFlow from './auth_flow.js'
|
||||
import oauthTokens from './oauth_tokens.js'
|
||||
import drafts from './drafts.js'
|
||||
|
@ -23,7 +22,6 @@ export default {
|
|||
profileConfig,
|
||||
serverSideStorage,
|
||||
adminSettings,
|
||||
oauth,
|
||||
authFlow,
|
||||
oauthTokens,
|
||||
drafts,
|
||||
|
|
|
@ -5,6 +5,7 @@ import oauthApi from '../services/new_api/oauth.js'
|
|||
import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
|
||||
import { registerPushNotifications, unregisterPushNotifications } from '../services/sw/sw.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
|
||||
// TODO: Unify with mergeOrAdd in statuses.js
|
||||
export const mergeOrAdd = (arr, obj, item) => {
|
||||
|
@ -526,17 +527,18 @@ const users = {
|
|||
})
|
||||
},
|
||||
async signUp (store, userInfo) {
|
||||
const oauthStore = useOAuthStore()
|
||||
store.commit('signUpPending')
|
||||
|
||||
try {
|
||||
const token = await store.dispatch('ensureAppToken')
|
||||
const token = await oauthStore.ensureAppToken()
|
||||
const data = await apiService.register(
|
||||
{ credentials: token, params: { ...userInfo } }
|
||||
)
|
||||
|
||||
if (data.access_token) {
|
||||
store.commit('signUpSuccess')
|
||||
store.commit('setToken', data.access_token)
|
||||
oauthStore.setToken(data.access_token)
|
||||
store.dispatch('loginUser', data.access_token)
|
||||
return 'ok'
|
||||
} else { // Request succeeded, but user cannot login yet.
|
||||
|
@ -554,21 +556,16 @@ const users = {
|
|||
},
|
||||
|
||||
logout (store) {
|
||||
const { oauth, instance } = store.rootState
|
||||
|
||||
const data = {
|
||||
...oauth,
|
||||
commit: store.commit,
|
||||
instance: instance.server
|
||||
}
|
||||
const oauth = useOAuthStore()
|
||||
const { instance } = store.rootState
|
||||
|
||||
// NOTE: No need to verify the app still exists, because if it doesn't,
|
||||
// the token will be invalid too
|
||||
return store.dispatch('ensureApp')
|
||||
return oauth.ensureApp()
|
||||
.then((app) => {
|
||||
const params = {
|
||||
app,
|
||||
instance: data.instance,
|
||||
instance: instance.server,
|
||||
token: oauth.userToken
|
||||
}
|
||||
|
||||
|
@ -577,9 +574,9 @@ const users = {
|
|||
.then(() => {
|
||||
store.commit('clearCurrentUser')
|
||||
store.dispatch('disconnectFromSocket')
|
||||
store.commit('clearToken')
|
||||
oauth.clearToken()
|
||||
store.dispatch('stopFetchingTimeline', 'friends')
|
||||
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||
store.commit('setBackendInteractor', backendInteractorService(oauth.getToken))
|
||||
store.dispatch('stopFetchingNotifications')
|
||||
store.dispatch('stopFetchingLists')
|
||||
store.dispatch('stopFetchingBookmarkFolders')
|
||||
|
@ -674,7 +671,7 @@ const users = {
|
|||
|
||||
// remove authentication token on client/authentication errors
|
||||
if ([400, 401, 403, 422].includes(response.status)) {
|
||||
commit('clearToken')
|
||||
useOAuthStore().clearToken()
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { createApp, getClientToken, verifyAppToken } from 'src/services/new_api/oauth.js'
|
||||
|
||||
// status codes about verifyAppToken (GET /api/v1/apps/verify_credentials)
|
||||
|
@ -20,7 +21,7 @@ const isClientDataRejected = error => (
|
|||
error.statusCode === 400
|
||||
)
|
||||
|
||||
const oauth = {
|
||||
export const useOAuthStore = defineStore('oauth', {
|
||||
state: () => ({
|
||||
clientId: false,
|
||||
clientSecret: false,
|
||||
|
@ -34,82 +35,77 @@ const oauth = {
|
|||
*/
|
||||
userToken: false
|
||||
}),
|
||||
mutations: {
|
||||
setClientData (state, { clientId, clientSecret }) {
|
||||
state.clientId = clientId
|
||||
state.clientSecret = clientSecret
|
||||
},
|
||||
setAppToken (state, token) {
|
||||
state.appToken = token
|
||||
},
|
||||
setToken (state, token) {
|
||||
state.userToken = token
|
||||
},
|
||||
clearToken (state) {
|
||||
state.userToken = false
|
||||
// state.token is userToken with older name, coming from persistent state
|
||||
// let's clear it as well, since it is being used as a fallback of state.userToken
|
||||
delete state.token
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
getToken: state => () => {
|
||||
// state.token is userToken with older name, coming from persistent state
|
||||
// added here for smoother transition, otherwise user will be logged out
|
||||
return state.userToken || state.token || state.appToken
|
||||
getToken () {
|
||||
return this.userToken || this.appToken
|
||||
},
|
||||
getUserToken: state => () => {
|
||||
// state.token is userToken with older name, coming from persistent state
|
||||
// added here for smoother transition, otherwise user will be logged out
|
||||
return state.userToken || state.token
|
||||
getUserToken () {
|
||||
return this.userToken
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async createApp ({ rootState, commit }) {
|
||||
const instance = rootState.instance.server
|
||||
setClientData ({ clientId, clientSecret }) {
|
||||
this.clientId = clientId
|
||||
this.clientSecret = clientSecret
|
||||
},
|
||||
setAppToken (token) {
|
||||
this.appToken = token
|
||||
},
|
||||
setToken (token) {
|
||||
this.userToken = token
|
||||
},
|
||||
clearToken () {
|
||||
this.userToken = false
|
||||
},
|
||||
async createApp () {
|
||||
const { state } = window.vuex
|
||||
const instance = state.instance.server
|
||||
const app = await createApp(instance)
|
||||
commit('setClientData', app)
|
||||
this.setClientData(app)
|
||||
return app
|
||||
},
|
||||
/// Use this if you want to get the client id and secret but are not interested
|
||||
/// in whether they are valid.
|
||||
/// @return {{ clientId: string, clientSecret: string }} An object representing the app
|
||||
ensureApp ({ state, dispatch }) {
|
||||
if (state.clientId && state.clientSecret) {
|
||||
async ensureApp () {
|
||||
if (this.clientId && this.clientSecret) {
|
||||
return {
|
||||
clientId: state.clientId,
|
||||
clientSecret: state.clientSecret
|
||||
clientId: this.clientId,
|
||||
clientSecret: this.clientSecret
|
||||
}
|
||||
} else {
|
||||
return dispatch('createApp')
|
||||
return this.createApp()
|
||||
}
|
||||
},
|
||||
async getAppToken ({ state, rootState, commit }) {
|
||||
async getAppToken () {
|
||||
const { state } = window.vuex
|
||||
const instance = state.instance.server
|
||||
const res = await getClientToken({
|
||||
clientId: state.clientId,
|
||||
clientSecret: state.clientSecret,
|
||||
instance: rootState.instance.server
|
||||
clientId: this.clientId,
|
||||
clientSecret: this.clientSecret,
|
||||
instance
|
||||
})
|
||||
commit('setAppToken', res.access_token)
|
||||
this.setAppToken(res.access_token)
|
||||
return res.access_token
|
||||
},
|
||||
/// Use this if you want to ensure the app is still valid to use.
|
||||
/// @return {string} The access token to the app (not attached to any user)
|
||||
async ensureAppToken ({ state, rootState, dispatch, commit }) {
|
||||
if (state.appToken) {
|
||||
async ensureAppToken () {
|
||||
const { state } = window.vuex
|
||||
if (this.appToken) {
|
||||
try {
|
||||
await verifyAppToken({
|
||||
instance: rootState.instance.server,
|
||||
appToken: state.appToken
|
||||
instance: state.instance.server,
|
||||
appToken: this.appToken
|
||||
})
|
||||
return state.appToken
|
||||
return this.appToken
|
||||
} catch (e) {
|
||||
if (!isAppTokenRejected(e)) {
|
||||
// The server did not reject our token, but we encountered other problems. Maybe the server is down.
|
||||
throw e
|
||||
} else {
|
||||
// The app token is rejected, so it is no longer useful.
|
||||
commit('setAppToken', false)
|
||||
this.setAppToken(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +113,7 @@ const oauth = {
|
|||
// appToken is not available, or is rejected: try to get a new one.
|
||||
// First, make sure the client id and client secret are filled.
|
||||
try {
|
||||
await dispatch('ensureApp')
|
||||
await this.ensureApp()
|
||||
} catch (e) {
|
||||
console.error('Cannot create app', e)
|
||||
throw e
|
||||
|
@ -126,7 +122,7 @@ const oauth = {
|
|||
// Note that at this step, the client id and secret may be invalid
|
||||
// (because the backend may have already deleted the app due to no user login)
|
||||
try {
|
||||
return await dispatch('getAppToken')
|
||||
return await this.getAppToken()
|
||||
} catch (e) {
|
||||
if (!isClientDataRejected(e)) {
|
||||
// Non-credentials problem, fail fast
|
||||
|
@ -135,17 +131,27 @@ const oauth = {
|
|||
} else {
|
||||
// the client id and secret are invalid, so we should clear them
|
||||
// and re-create our app
|
||||
commit('setClientData', {
|
||||
this.setClientData({
|
||||
clientId: false,
|
||||
clientSecret: false
|
||||
})
|
||||
await dispatch('createApp')
|
||||
await this.createApp()
|
||||
// try once again to get the token
|
||||
return await dispatch('getAppToken')
|
||||
return await this.getAppToken()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
persist: {
|
||||
afterLoad (state) {
|
||||
// state.token is userToken with older name, coming from persistent state
|
||||
if (state.token && !state.userToken) {
|
||||
state.userToken = state.token
|
||||
}
|
||||
if ('token' in state) {
|
||||
delete state.token
|
||||
}
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default oauth
|
||||
})
|
307
test/unit/specs/lib/persisted_state.spec.js
Normal file
307
test/unit/specs/lib/persisted_state.spec.js
Normal file
|
@ -0,0 +1,307 @@
|
|||
import { setActivePinia, createPinia, defineStore } from 'pinia'
|
||||
import { createApp } from 'vue'
|
||||
import { flushPromises } from '@vue/test-utils'
|
||||
import { piniaPersistPlugin } from 'src/lib/persisted_state.js'
|
||||
|
||||
const app = createApp({})
|
||||
|
||||
const getMockStorage = () => {
|
||||
let state = {}
|
||||
|
||||
return {
|
||||
getItem: vi.fn(async key => {
|
||||
console.log('get:', key, state[key])
|
||||
return state[key]
|
||||
}),
|
||||
setItem: vi.fn(async (key, value) => {
|
||||
console.log('set:', key, value)
|
||||
state[key] = value
|
||||
}),
|
||||
_clear: () => {
|
||||
state = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mockStorage
|
||||
|
||||
beforeEach(() => {
|
||||
mockStorage = getMockStorage()
|
||||
const pinia = createPinia().use(piniaPersistPlugin({ storage: mockStorage }))
|
||||
app.use(pinia)
|
||||
setActivePinia(pinia)
|
||||
})
|
||||
|
||||
describe('piniaPersistPlugin', () => {
|
||||
describe('initial state', () => {
|
||||
test('it does not load anything if it is not enabled', async () => {
|
||||
await mockStorage.setItem('pinia-local-test', { a: 3 })
|
||||
|
||||
const useTestStore = defineStore('test', {
|
||||
state: () => ({ a: 1, b: 2 })
|
||||
})
|
||||
|
||||
const test = useTestStore()
|
||||
await test.$persistLoaded
|
||||
expect(test.a).to.eql(1)
|
||||
expect(test.b).to.eql(2)
|
||||
})
|
||||
|
||||
test('$persistLoaded rejects if getItem() throws', async () => {
|
||||
const error = new Error('unable to get storage')
|
||||
mockStorage.getItem = vi.fn(async () => {
|
||||
throw error
|
||||
})
|
||||
|
||||
const useTestStore = defineStore('test', {
|
||||
state: () => ({ a: 1, b: 2, c: { d: 4, e: 5 } }),
|
||||
persist: {}
|
||||
})
|
||||
|
||||
const test = useTestStore()
|
||||
await expect(test.$persistLoaded).rejects.toThrowError(error)
|
||||
})
|
||||
|
||||
test('it loads from pinia storage', async () => {
|
||||
await mockStorage.setItem('pinia-local-test', { a: 3, c: { d: 0 } })
|
||||
await mockStorage.setItem('vuex-lz', { test: { a: 4 } })
|
||||
|
||||
const useTestStore = defineStore('test', {
|
||||
state: () => ({ a: 1, b: 2, c: { d: 4, e: 5 } }),
|
||||
persist: {}
|
||||
})
|
||||
|
||||
const test = useTestStore()
|
||||
await test.$persistLoaded
|
||||
expect(test.a).to.eql(3)
|
||||
expect(test.b).to.eql(2)
|
||||
expect(test.c.d).to.eql(0)
|
||||
expect(test.c.e).to.eql(5)
|
||||
})
|
||||
|
||||
test('it loads from vuex storage as fallback', async () => {
|
||||
await mockStorage.setItem('vuex-lz', { test: { a: 4, c: { d: 0 } } })
|
||||
|
||||
const useTestStore = defineStore('test', {
|
||||
state: () => ({ a: 1, b: 2, c: { d: 4, e: 5 } }),
|
||||
persist: {}
|
||||
})
|
||||
|
||||
const test = useTestStore()
|
||||
await test.$persistLoaded
|
||||
expect(test.a).to.eql(4)
|
||||
expect(test.b).to.eql(2)
|
||||
expect(test.c.d).to.eql(0)
|
||||
expect(test.c.e).to.eql(5)
|
||||
})
|
||||
|
||||
test('it loads from vuex storage and writes it into pinia storage', async () => {
|
||||
await mockStorage.setItem('vuex-lz', { test: { a: 4, c: { d: 0 } } })
|
||||
|
||||
const useTestStore = defineStore('test', {
|
||||
state: () => ({ a: 1, b: 2, c: { d: 4, e: 5 } }),
|
||||
persist: {
|
||||
afterLoad (state) {
|
||||
return {
|
||||
...state,
|
||||
a: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const test = useTestStore()
|
||||
await test.$persistLoaded
|
||||
expect(await mockStorage.getItem('pinia-local-test')).to.eql({ a: 4, c: { d: 0 } })
|
||||
expect(test.a).to.eql(5)
|
||||
expect(test.b).to.eql(2)
|
||||
expect(test.c.d).to.eql(0)
|
||||
expect(test.c.e).to.eql(5)
|
||||
})
|
||||
|
||||
test('it does not modify state if there is nothing to load', async () => {
|
||||
await mockStorage.setItem('vuex-lz', { test2: { a: 4 } })
|
||||
|
||||
const useTestStore = defineStore('test', {
|
||||
state: () => ({ a: 1, b: 2 }),
|
||||
persist: {}
|
||||
})
|
||||
|
||||
const test = useTestStore()
|
||||
await test.$persistLoaded
|
||||
expect(test.a).to.eql(1)
|
||||
expect(test.b).to.eql(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('paths', () => {
|
||||
test('it saves everything if paths is unspecified', async () => {
|
||||
const useTestStore = defineStore('test', {
|
||||
state: () => ({ a: 1, b: 2 }),
|
||||
persist: {}
|
||||
})
|
||||
|
||||
const test = useTestStore()
|
||||
await test.$persistLoaded
|
||||
test.$patch({ a: 3 })
|
||||
await flushPromises()
|
||||
expect(await mockStorage.getItem('pinia-local-test')).to.eql({ a: 3, b: 2 })
|
||||
})
|
||||
|
||||
test('it saves only specified paths', async () => {
|
||||
const useTestStore = defineStore('test', {
|
||||
state: () => ({ a: 1, b: 2, c: { d: 4, e: 5 } }),
|
||||
persist: {
|
||||
paths: ['a', 'c.d']
|
||||
}
|
||||
})
|
||||
|
||||
const test = useTestStore()
|
||||
await test.$persistLoaded
|
||||
test.$patch({ a: 3 })
|
||||
await flushPromises()
|
||||
expect(await mockStorage.getItem('pinia-local-test')).to.eql({ a: 3, c: { d: 4 } })
|
||||
})
|
||||
})
|
||||
|
||||
test('it only saves after load', async () => {
|
||||
const onSaveError = vi.fn()
|
||||
const onSaveSuccess = vi.fn()
|
||||
const useTestStore = defineStore('test', {
|
||||
state: () => ({ a: 1, b: 2 }),
|
||||
persist: {
|
||||
onSaveSuccess,
|
||||
onSaveError
|
||||
}
|
||||
})
|
||||
|
||||
const test = useTestStore()
|
||||
test.$patch({ a: 3 })
|
||||
expect(await mockStorage.getItem('pinia-local-test')).to.eql(undefined)
|
||||
// NOTE: it should not even have tried to save, because the subscribe function
|
||||
// is called only after loading the initial state.
|
||||
expect(mockStorage.setItem).not.toHaveBeenCalled()
|
||||
// this asserts that it has not called setState() in persistCurrentState()
|
||||
expect(onSaveError).not.toHaveBeenCalled()
|
||||
expect(onSaveSuccess).not.toHaveBeenCalled()
|
||||
await test.$persistLoaded
|
||||
test.$patch({ a: 4 })
|
||||
expect(await mockStorage.getItem('pinia-local-test')).to.eql({ a: 4, b: 2 })
|
||||
})
|
||||
|
||||
describe('saveImmediatelyActions', () => {
|
||||
test('it should only persist state after specified actions', async () => {
|
||||
const useTestStore = defineStore('test', {
|
||||
state: () => ({ a: 1, b: 2 }),
|
||||
actions: {
|
||||
increaseA () {
|
||||
++this.a
|
||||
},
|
||||
increaseB () {
|
||||
++this.b
|
||||
}
|
||||
},
|
||||
persist: {
|
||||
saveImmediatelyActions: ['increaseA']
|
||||
}
|
||||
})
|
||||
|
||||
const test = useTestStore()
|
||||
await test.$persistLoaded
|
||||
await test.increaseA()
|
||||
expect(await mockStorage.getItem('pinia-local-test')).to.eql({ a: 2, b: 2 })
|
||||
await test.increaseB()
|
||||
expect(await mockStorage.getItem('pinia-local-test')).to.eql({ a: 2, b: 2 })
|
||||
await test.increaseA()
|
||||
expect(await mockStorage.getItem('pinia-local-test')).to.eql({ a: 3, b: 3 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('onSaveSuccess / onSaveError', () => {
|
||||
test('onSaveSuccess is called after setState', async () => {
|
||||
const onSaveSuccess = vi.fn()
|
||||
const onSaveError = vi.fn()
|
||||
const useTestStore = defineStore('test', {
|
||||
state: () => ({ a: 1, b: 2 }),
|
||||
persist: {
|
||||
onSaveSuccess,
|
||||
onSaveError
|
||||
}
|
||||
})
|
||||
|
||||
const test = useTestStore()
|
||||
await test.$persistLoaded
|
||||
test.$patch({ a: 3 })
|
||||
await flushPromises()
|
||||
expect(onSaveSuccess).toHaveBeenCalledTimes(1)
|
||||
expect(onSaveError).toHaveBeenCalledTimes(0)
|
||||
test.$patch({ a: 4 })
|
||||
await flushPromises()
|
||||
expect(onSaveSuccess).toHaveBeenCalledTimes(2)
|
||||
expect(onSaveError).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
test('onSaveError is called after setState fails', async () => {
|
||||
mockStorage.setItem = vi.fn(async () => {
|
||||
throw new Error('cannot save')
|
||||
})
|
||||
const onSaveSuccess = vi.fn()
|
||||
const onSaveError = vi.fn()
|
||||
const useTestStore = defineStore('test', {
|
||||
state: () => ({ a: 1, b: 2 }),
|
||||
persist: {
|
||||
onSaveSuccess,
|
||||
onSaveError
|
||||
}
|
||||
})
|
||||
|
||||
const test = useTestStore()
|
||||
await test.$persistLoaded
|
||||
await test.$patch({ a: 3 })
|
||||
expect(onSaveSuccess).toHaveBeenCalledTimes(0)
|
||||
expect(onSaveError).toHaveBeenCalledTimes(1)
|
||||
await test.$patch({ a: 4 })
|
||||
expect(onSaveSuccess).toHaveBeenCalledTimes(0)
|
||||
expect(onSaveError).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('afterLoad', () => {
|
||||
test('it is called with the saved state object', async () => {
|
||||
await mockStorage.setItem('pinia-local-test', { a: 2 })
|
||||
const afterLoad = vi.fn(async orig => {
|
||||
return { a: orig.a + 1 }
|
||||
})
|
||||
|
||||
const useTestStore = defineStore('test', {
|
||||
state: () => ({ a: 1, b: 2 }),
|
||||
persist: {
|
||||
afterLoad
|
||||
}
|
||||
})
|
||||
const test = useTestStore()
|
||||
await test.$persistLoaded
|
||||
expect(afterLoad).toHaveBeenCalledTimes(1)
|
||||
expect(afterLoad).toHaveBeenCalledWith({ a: 2 })
|
||||
expect(test.a).to.eql(3)
|
||||
})
|
||||
|
||||
test('it is called with empty object if there is no saved state', async () => {
|
||||
const afterLoad = vi.fn(async () => {
|
||||
return { a: 3 }
|
||||
})
|
||||
|
||||
const useTestStore = defineStore('test', {
|
||||
state: () => ({ a: 1, b: 2 }),
|
||||
persist: {
|
||||
afterLoad
|
||||
}
|
||||
})
|
||||
const test = useTestStore()
|
||||
await test.$persistLoaded
|
||||
expect(afterLoad).toHaveBeenCalledTimes(1)
|
||||
expect(afterLoad).toHaveBeenCalledWith({})
|
||||
expect(test.a).to.eql(3)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,38 +1,41 @@
|
|||
import { createApp } from 'vue'
|
||||
import { createStore } from 'vuex'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import oauth from 'src/modules/oauth.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { injectMswToTest, authApis, testServer } from '/test/fixtures/mock_api.js'
|
||||
|
||||
const test = injectMswToTest(authApis)
|
||||
|
||||
const getStore = (defaultStateInjection) => {
|
||||
const stateFunction = defaultStateInjection ? () => {
|
||||
return {
|
||||
...oauth.state(),
|
||||
...defaultStateInjection
|
||||
const vuexStore = createStore({
|
||||
modules: {
|
||||
instance: {
|
||||
state: () => ({ server: testServer })
|
||||
}
|
||||
} : oauth.state
|
||||
}
|
||||
})
|
||||
const app = createApp({})
|
||||
app.use(vuexStore)
|
||||
window.vuex = vuexStore
|
||||
|
||||
return createStore({
|
||||
modules: {
|
||||
instance: {
|
||||
state: () => ({ server: testServer })
|
||||
},
|
||||
oauth: {
|
||||
...oauth,
|
||||
state: stateFunction
|
||||
}
|
||||
const getStore = (defaultStateInjection) => {
|
||||
const pinia = createPinia().use(({ store }) => {
|
||||
if (store.$id === 'oauth') {
|
||||
store.$patch(defaultStateInjection)
|
||||
}
|
||||
})
|
||||
app.use(pinia)
|
||||
setActivePinia(pinia)
|
||||
return useOAuthStore()
|
||||
}
|
||||
|
||||
|
||||
describe('createApp', () => {
|
||||
test('it should use create an app and record client id and secret', async () => {
|
||||
const store = getStore()
|
||||
const app = await store.dispatch('createApp')
|
||||
expect(store.state.oauth.clientId).to.eql('test-id')
|
||||
expect(store.state.oauth.clientSecret).to.eql('test-secret')
|
||||
const app = await store.createApp()
|
||||
expect(store.clientId).to.eql('test-id')
|
||||
expect(store.clientSecret).to.eql('test-secret')
|
||||
expect(app.clientId).to.eql('test-id')
|
||||
expect(app.clientSecret).to.eql('test-secret')
|
||||
})
|
||||
|
@ -45,19 +48,19 @@ describe('createApp', () => {
|
|||
)
|
||||
|
||||
const store = getStore()
|
||||
const res = store.dispatch('createApp')
|
||||
const res = store.createApp()
|
||||
await expect(res).rejects.toThrowError('Throttled')
|
||||
expect(store.state.oauth.clientId).to.eql(false)
|
||||
expect(store.state.oauth.clientSecret).to.eql(false)
|
||||
expect(store.clientId).to.eql(false)
|
||||
expect(store.clientSecret).to.eql(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureApp', () => {
|
||||
test('it should create an app if it does not exist', async () => {
|
||||
const store = getStore()
|
||||
const app = await store.dispatch('ensureApp')
|
||||
expect(store.state.oauth.clientId).to.eql('test-id')
|
||||
expect(store.state.oauth.clientSecret).to.eql('test-secret')
|
||||
const app = await store.ensureApp()
|
||||
expect(store.clientId).to.eql('test-id')
|
||||
expect(store.clientSecret).to.eql('test-secret')
|
||||
expect(app.clientId).to.eql('test-id')
|
||||
expect(app.clientSecret).to.eql('test-secret')
|
||||
})
|
||||
|
@ -73,9 +76,9 @@ describe('ensureApp', () => {
|
|||
clientId: 'another-id',
|
||||
clientSecret: 'another-secret'
|
||||
})
|
||||
const app = await store.dispatch('ensureApp')
|
||||
expect(store.state.oauth.clientId).to.eql('another-id')
|
||||
expect(store.state.oauth.clientSecret).to.eql('another-secret')
|
||||
const app = await store.ensureApp()
|
||||
expect(store.clientId).to.eql('another-id')
|
||||
expect(store.clientSecret).to.eql('another-secret')
|
||||
expect(app.clientId).to.eql('another-id')
|
||||
expect(app.clientSecret).to.eql('another-secret')
|
||||
})
|
||||
|
@ -87,9 +90,9 @@ describe('getAppToken', () => {
|
|||
clientId: 'test-id',
|
||||
clientSecret: 'test-secret'
|
||||
})
|
||||
const token = await store.dispatch('getAppToken')
|
||||
const token = await store.getAppToken()
|
||||
expect(token).to.eql('test-app-token')
|
||||
expect(store.state.oauth.appToken).to.eql('test-app-token')
|
||||
expect(store.appToken).to.eql('test-app-token')
|
||||
})
|
||||
|
||||
test('it should throw and not set state if it cannot get app token', async () => {
|
||||
|
@ -97,26 +100,26 @@ describe('getAppToken', () => {
|
|||
clientId: 'bad-id',
|
||||
clientSecret: 'bad-secret'
|
||||
})
|
||||
await expect(store.dispatch('getAppToken')).rejects.toThrowError('400')
|
||||
expect(store.state.oauth.appToken).to.eql(false)
|
||||
await expect(store.getAppToken()).rejects.toThrowError('400')
|
||||
expect(store.appToken).to.eql(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureAppToken', () => {
|
||||
test('it should work if the state is empty', async () => {
|
||||
const store = getStore()
|
||||
const token = await store.dispatch('ensureAppToken')
|
||||
const token = await store.ensureAppToken()
|
||||
expect(token).to.eql('test-app-token')
|
||||
expect(store.state.oauth.appToken).to.eql('test-app-token')
|
||||
expect(store.appToken).to.eql('test-app-token')
|
||||
})
|
||||
|
||||
test('it should work if we already have a working token', async () => {
|
||||
const store = getStore({
|
||||
appToken: 'also-good-app-token'
|
||||
})
|
||||
const token = await store.dispatch('ensureAppToken')
|
||||
const token = await store.ensureAppToken()
|
||||
expect(token).to.eql('also-good-app-token')
|
||||
expect(store.state.oauth.appToken).to.eql('also-good-app-token')
|
||||
expect(store.appToken).to.eql('also-good-app-token')
|
||||
})
|
||||
|
||||
test('it should work if we have a bad token but good app credentials', async ({ worker }) => {
|
||||
|
@ -130,9 +133,9 @@ describe('ensureAppToken', () => {
|
|||
clientId: 'test-id',
|
||||
clientSecret: 'test-secret'
|
||||
})
|
||||
const token = await store.dispatch('ensureAppToken')
|
||||
const token = await store.ensureAppToken()
|
||||
expect(token).to.eql('test-app-token')
|
||||
expect(store.state.oauth.appToken).to.eql('test-app-token')
|
||||
expect(store.appToken).to.eql('test-app-token')
|
||||
})
|
||||
|
||||
test('it should work if we have no token but good app credentials', async ({ worker }) => {
|
||||
|
@ -145,9 +148,9 @@ describe('ensureAppToken', () => {
|
|||
clientId: 'test-id',
|
||||
clientSecret: 'test-secret'
|
||||
})
|
||||
const token = await store.dispatch('ensureAppToken')
|
||||
const token = await store.ensureAppToken()
|
||||
expect(token).to.eql('test-app-token')
|
||||
expect(store.state.oauth.appToken).to.eql('test-app-token')
|
||||
expect(store.appToken).to.eql('test-app-token')
|
||||
})
|
||||
|
||||
test('it should work if we have no token and bad app credentials', async () => {
|
||||
|
@ -155,11 +158,11 @@ describe('ensureAppToken', () => {
|
|||
clientId: 'bad-id',
|
||||
clientSecret: 'bad-secret'
|
||||
})
|
||||
const token = await store.dispatch('ensureAppToken')
|
||||
const token = await store.ensureAppToken()
|
||||
expect(token).to.eql('test-app-token')
|
||||
expect(store.state.oauth.appToken).to.eql('test-app-token')
|
||||
expect(store.state.oauth.clientId).to.eql('test-id')
|
||||
expect(store.state.oauth.clientSecret).to.eql('test-secret')
|
||||
expect(store.appToken).to.eql('test-app-token')
|
||||
expect(store.clientId).to.eql('test-id')
|
||||
expect(store.clientSecret).to.eql('test-secret')
|
||||
})
|
||||
|
||||
test('it should work if we have bad token and bad app credentials', async () => {
|
||||
|
@ -168,11 +171,11 @@ describe('ensureAppToken', () => {
|
|||
clientId: 'bad-id',
|
||||
clientSecret: 'bad-secret'
|
||||
})
|
||||
const token = await store.dispatch('ensureAppToken')
|
||||
const token = await store.ensureAppToken()
|
||||
expect(token).to.eql('test-app-token')
|
||||
expect(store.state.oauth.appToken).to.eql('test-app-token')
|
||||
expect(store.state.oauth.clientId).to.eql('test-id')
|
||||
expect(store.state.oauth.clientSecret).to.eql('test-secret')
|
||||
expect(store.appToken).to.eql('test-app-token')
|
||||
expect(store.clientId).to.eql('test-id')
|
||||
expect(store.clientSecret).to.eql('test-secret')
|
||||
})
|
||||
|
||||
test('it should throw if we cannot create an app', async ({ worker }) => {
|
||||
|
@ -183,7 +186,7 @@ describe('ensureAppToken', () => {
|
|||
)
|
||||
|
||||
const store = getStore()
|
||||
await expect(store.dispatch('ensureAppToken')).rejects.toThrowError('Throttled')
|
||||
await expect(store.ensureAppToken()).rejects.toThrowError('Throttled')
|
||||
})
|
||||
|
||||
test('it should throw if we cannot obtain app token', async ({ worker }) => {
|
||||
|
@ -194,6 +197,6 @@ describe('ensureAppToken', () => {
|
|||
)
|
||||
|
||||
const store = getStore()
|
||||
await expect(store.dispatch('ensureAppToken')).rejects.toThrowError('Throttled')
|
||||
await expect(store.ensureAppToken()).rejects.toThrowError('Throttled')
|
||||
})
|
||||
})
|
Loading…
Add table
Reference in a new issue