Migrate oauth store to pinia
This commit is contained in:
parent
a5cc7351ec
commit
216d318bb5
12 changed files with 663 additions and 145 deletions
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
Add a link
Reference in a new issue