unit test + some refactoring
This commit is contained in:
parent
9c00610d00
commit
8c59bad3c2
@ -10,7 +10,7 @@ library.add(
|
|||||||
faTimes
|
faTimes
|
||||||
)
|
)
|
||||||
|
|
||||||
const CURRENT_UPDATE_COUNTER = 1
|
export const CURRENT_UPDATE_COUNTER = 1
|
||||||
|
|
||||||
const UpdateNotification = {
|
const UpdateNotification = {
|
||||||
data () {
|
data () {
|
||||||
@ -40,13 +40,13 @@ const UpdateNotification = {
|
|||||||
},
|
},
|
||||||
neverShowAgain () {
|
neverShowAgain () {
|
||||||
this.toggleShow()
|
this.toggleShow()
|
||||||
// this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER })
|
this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER })
|
||||||
// this.$store.commit('setFlag', { flag: 'dontShowUpdateNotifs', value: 1 })
|
this.$store.commit('setFlag', { flag: 'dontShowUpdateNotifs', value: 1 })
|
||||||
// this.$store.dispatch('pushServerSideStorage')
|
this.$store.dispatch('pushServerSideStorage')
|
||||||
},
|
},
|
||||||
dismiss () {
|
dismiss () {
|
||||||
this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER })
|
this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER })
|
||||||
// this.$store.dispatch('pushServerSideStorage')
|
this.$store.dispatch('pushServerSideStorage')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { toRaw } from 'vue'
|
import { toRaw } from 'vue'
|
||||||
import { isEqual } from 'lodash'
|
import { isEqual, cloneDeep } from 'lodash'
|
||||||
|
import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js'
|
||||||
|
|
||||||
export const VERSION = 1
|
export const VERSION = 1
|
||||||
export const NEW_USER_DATE = new Date('04-08-2022') // date of writing this, basically
|
export const NEW_USER_DATE = new Date('2022-08-04') // date of writing this, basically
|
||||||
|
|
||||||
export const COMMAND_TRIM_FLAGS = 1000
|
export const COMMAND_TRIM_FLAGS = 1000
|
||||||
export const COMMAND_TRIM_FLAGS_AND_RESET = 1001
|
export const COMMAND_TRIM_FLAGS_AND_RESET = 1001
|
||||||
|
|
||||||
const defaultState = {
|
export const defaultState = {
|
||||||
// do we need to update data on server?
|
// do we need to update data on server?
|
||||||
dirty: false,
|
dirty: false,
|
||||||
// storage of flags - stuff that can only be set and incremented
|
// storage of flags - stuff that can only be set and incremented
|
||||||
@ -27,9 +28,9 @@ const defaultState = {
|
|||||||
cache: null
|
cache: null
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUserFlags = {
|
export const newUserFlags = {
|
||||||
...defaultState.flagStorage,
|
...defaultState.flagStorage,
|
||||||
updateCounter: 1 // new users don't need to see update notification
|
updateCounter: CURRENT_UPDATE_COUNTER // new users don't need to see update notification
|
||||||
}
|
}
|
||||||
|
|
||||||
const _wrapData = (data) => ({
|
const _wrapData = (data) => ({
|
||||||
@ -38,24 +39,23 @@ const _wrapData = (data) => ({
|
|||||||
_version: VERSION
|
_version: VERSION
|
||||||
})
|
})
|
||||||
|
|
||||||
export const _checkValidity = (data) => data._timestamp > 0 && data._version > 0
|
const _checkValidity = (data) => data._timestamp > 0 && data._version > 0
|
||||||
|
|
||||||
export const _getRecentData = (cache, live) => {
|
export const _getRecentData = (cache, live) => {
|
||||||
const result = { recent: null, stale: null, needUpload: false }
|
const result = { recent: null, stale: null, needUpload: false }
|
||||||
const cacheValid = _checkValidity(cache || {})
|
const cacheValid = _checkValidity(cache || {})
|
||||||
const liveValid = _checkValidity(live || {})
|
const liveValid = _checkValidity(live || {})
|
||||||
if (!liveValid) {
|
if (!liveValid && cacheValid) {
|
||||||
result.needUpload = true
|
result.needUpload = true
|
||||||
console.debug('Nothing valid stored on server, assuming cache to be source of truth')
|
console.debug('Nothing valid stored on server, assuming cache to be source of truth')
|
||||||
result.recent = cache
|
result.recent = cache
|
||||||
result.stale = live
|
result.stale = live
|
||||||
} else if (!cacheValid) {
|
} else if (!cacheValid && liveValid) {
|
||||||
console.debug('Valid storage on server found, no local cache found, using live as source of truth')
|
console.debug('Valid storage on server found, no local cache found, using live as source of truth')
|
||||||
result.recent = live
|
result.recent = live
|
||||||
result.stale = cache
|
result.stale = cache
|
||||||
} else {
|
} else if (cacheValid && liveValid) {
|
||||||
console.debug('Both sources have valid data, figuring things out...')
|
console.debug('Both sources have valid data, figuring things out...')
|
||||||
console.log(live._timestamp, cache._timestamp)
|
|
||||||
if (live._timestamp === cache._timestamp && live._version === cache._version) {
|
if (live._timestamp === cache._timestamp && live._version === cache._version) {
|
||||||
console.debug('Same version/timestamp on both source, source of truth irrelevant')
|
console.debug('Same version/timestamp on both source, source of truth irrelevant')
|
||||||
result.recent = cache
|
result.recent = cache
|
||||||
@ -70,14 +70,17 @@ export const _getRecentData = (cache, live) => {
|
|||||||
result.stale = cache
|
result.stale = cache
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.debug('Both sources are invalid, start from scratch')
|
||||||
|
result.needUpload = true
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const _getAllFlags = (recent = {}, stale = {}) => {
|
export const _getAllFlags = (recent, stale) => {
|
||||||
return Array.from(new Set([
|
return Array.from(new Set([
|
||||||
...Object.keys(toRaw(recent.flagStorage || {})),
|
...Object.keys(toRaw((recent || {}).flagStorage || {})),
|
||||||
...Object.keys(toRaw(stale.flagStorage || {}))
|
...Object.keys(toRaw((stale || {}).flagStorage || {}))
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,37 +89,43 @@ export const _mergeFlags = (recent, stale, allFlagKeys) => {
|
|||||||
const recentFlag = recent.flagStorage[flag]
|
const recentFlag = recent.flagStorage[flag]
|
||||||
const staleFlag = stale.flagStorage[flag]
|
const staleFlag = stale.flagStorage[flag]
|
||||||
// use flag that is of higher value
|
// use flag that is of higher value
|
||||||
return [flag, recentFlag > staleFlag ? recentFlag : staleFlag]
|
return [flag, Number((recentFlag > staleFlag ? recentFlag : staleFlag) || 0)]
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const _resetFlags = (totalFlags, allFlagKeys) => {
|
export const _resetFlags = (totalFlags, knownKeys = defaultState.flagStorage) => {
|
||||||
|
let result = { ...totalFlags }
|
||||||
|
const allFlagKeys = Object.keys(totalFlags)
|
||||||
// flag reset functionality
|
// flag reset functionality
|
||||||
if (totalFlags.reset >= COMMAND_TRIM_FLAGS && totalFlags.reset <= COMMAND_TRIM_FLAGS_AND_RESET) {
|
if (totalFlags.reset >= COMMAND_TRIM_FLAGS && totalFlags.reset <= COMMAND_TRIM_FLAGS_AND_RESET) {
|
||||||
console.debug('Received command to trim the flags')
|
console.debug('Received command to trim the flags')
|
||||||
const knownKeys = new Set(Object.keys(defaultState.flagStorage))
|
const knownKeysSet = new Set(Object.keys(knownKeys))
|
||||||
|
|
||||||
|
// Trim
|
||||||
|
result = {}
|
||||||
allFlagKeys.forEach(flag => {
|
allFlagKeys.forEach(flag => {
|
||||||
if (!knownKeys.has(flag)) {
|
if (knownKeysSet.has(flag)) {
|
||||||
delete totalFlags[flag]
|
result[flag] = totalFlags[flag]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Reset
|
||||||
if (totalFlags.reset === COMMAND_TRIM_FLAGS_AND_RESET) {
|
if (totalFlags.reset === COMMAND_TRIM_FLAGS_AND_RESET) {
|
||||||
// 1001 - and reset everything to 0
|
// 1001 - and reset everything to 0
|
||||||
console.debug('Received command to reset the flags')
|
console.debug('Received command to reset the flags')
|
||||||
allFlagKeys.forEach(flag => { totalFlags[flag] = 0 })
|
Object.keys(knownKeys).forEach(flag => { result[flag] = 0 })
|
||||||
} else {
|
|
||||||
// reset the reset 0
|
|
||||||
totalFlags.reset = 0
|
|
||||||
}
|
}
|
||||||
} else if (totalFlags.reset > 0 && totalFlags.reset < 9000) {
|
} else if (totalFlags.reset > 0 && totalFlags.reset < 9000) {
|
||||||
console.debug('Received command to reset the flags')
|
console.debug('Received command to reset the flags')
|
||||||
allFlagKeys.forEach(flag => { totalFlags[flag] = 0 })
|
allFlagKeys.forEach(flag => { result[flag] = 0 })
|
||||||
// for good luck
|
|
||||||
totalFlags.reset = 0
|
|
||||||
}
|
}
|
||||||
|
result.reset = 0
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const _doMigrations = (cache) => {
|
export const _doMigrations = (cache) => {
|
||||||
|
if (!cache) return cache
|
||||||
|
|
||||||
if (cache._version < VERSION) {
|
if (cache._version < VERSION) {
|
||||||
console.debug('Local cached data has older version, seeing if there any migrations that can be applied')
|
console.debug('Local cached data has older version, seeing if there any migrations that can be applied')
|
||||||
|
|
||||||
@ -139,11 +148,7 @@ export const _doMigrations = (cache) => {
|
|||||||
return cache
|
return cache
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverSideStorage = {
|
export const mutations = {
|
||||||
state: {
|
|
||||||
...defaultState
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
setServerSideStorage (state, userData) {
|
setServerSideStorage (state, userData) {
|
||||||
const live = userData.storage
|
const live = userData.storage
|
||||||
state.raw = live
|
state.raw = live
|
||||||
@ -154,7 +159,7 @@ const serverSideStorage = {
|
|||||||
let { recent, stale, needsUpload } = _getRecentData(cache, live)
|
let { recent, stale, needsUpload } = _getRecentData(cache, live)
|
||||||
|
|
||||||
const userNew = userData.created_at > NEW_USER_DATE
|
const userNew = userData.created_at > NEW_USER_DATE
|
||||||
const flagsTemplate = userNew ? newUserFlags : defaultState.defaultState
|
const flagsTemplate = userNew ? newUserFlags : defaultState.flagStorage
|
||||||
let dirty = false
|
let dirty = false
|
||||||
|
|
||||||
if (recent === null) {
|
if (recent === null) {
|
||||||
@ -183,21 +188,29 @@ const serverSideStorage = {
|
|||||||
totalFlags = recent.flagStorage
|
totalFlags = recent.flagStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
// This does side effects on totalFlags !!!
|
totalFlags = _resetFlags(totalFlags)
|
||||||
// only resets if needed (checks are inside)
|
|
||||||
_resetFlags(totalFlags, allFlagKeys)
|
|
||||||
|
|
||||||
recent.flagStorage = totalFlags
|
recent.flagStorage = totalFlags
|
||||||
|
|
||||||
state.dirty = dirty || needsUpload
|
state.dirty = dirty || needsUpload
|
||||||
state.cache = recent
|
state.cache = recent
|
||||||
|
// set local timestamp to smaller one if we don't have any changes
|
||||||
|
if (stale && recent && !state.dirty) {
|
||||||
|
state.cache._timestamp = Math.min(stale._timestamp, recent._timestamp)
|
||||||
|
}
|
||||||
state.flagStorage = state.cache.flagStorage
|
state.flagStorage = state.cache.flagStorage
|
||||||
},
|
},
|
||||||
setFlag (state, { flag, value }) {
|
setFlag (state, { flag, value }) {
|
||||||
state.flagStorage[flag] = value
|
state.flagStorage[flag] = value
|
||||||
state.dirty = true
|
state.dirty = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverSideStorage = {
|
||||||
|
state: {
|
||||||
|
...cloneDeep(defaultState)
|
||||||
},
|
},
|
||||||
|
mutations,
|
||||||
actions: {
|
actions: {
|
||||||
pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) {
|
pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) {
|
||||||
const needPush = state.dirty || force
|
const needPush = state.dirty || force
|
||||||
|
178
test/unit/specs/modules/serverSideStorage.spec.js
Normal file
178
test/unit/specs/modules/serverSideStorage.spec.js
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { cloneDeep } from 'lodash'
|
||||||
|
|
||||||
|
import {
|
||||||
|
VERSION,
|
||||||
|
COMMAND_TRIM_FLAGS,
|
||||||
|
COMMAND_TRIM_FLAGS_AND_RESET,
|
||||||
|
_getRecentData,
|
||||||
|
_getAllFlags,
|
||||||
|
_mergeFlags,
|
||||||
|
_resetFlags,
|
||||||
|
mutations,
|
||||||
|
defaultState,
|
||||||
|
newUserFlags
|
||||||
|
} from 'src/modules/serverSideStorage.js'
|
||||||
|
|
||||||
|
describe('The serverSideStorage module', () => {
|
||||||
|
describe('mutations', () => {
|
||||||
|
describe('setServerSideStorage', () => {
|
||||||
|
const { setServerSideStorage } = mutations
|
||||||
|
const user = {
|
||||||
|
created_at: new Date('1999-02-09'),
|
||||||
|
storage: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should initialize storage if none present', () => {
|
||||||
|
const state = cloneDeep(defaultState)
|
||||||
|
setServerSideStorage(state, user)
|
||||||
|
expect(state.cache._version).to.eql(VERSION)
|
||||||
|
expect(state.cache._timestamp).to.be.a('number')
|
||||||
|
expect(state.cache.flagStorage).to.eql(defaultState.flagStorage)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should initialize storage with proper flags for new users if none present', () => {
|
||||||
|
const state = cloneDeep(defaultState)
|
||||||
|
setServerSideStorage(state, { ...user, created_at: new Date() })
|
||||||
|
expect(state.cache._version).to.eql(VERSION)
|
||||||
|
expect(state.cache._timestamp).to.be.a('number')
|
||||||
|
expect(state.cache.flagStorage).to.eql(newUserFlags)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should merge flags even if remote timestamp is older', () => {
|
||||||
|
const state = {
|
||||||
|
...cloneDeep(defaultState),
|
||||||
|
cache: {
|
||||||
|
_timestamp: Date.now(),
|
||||||
|
_version: VERSION,
|
||||||
|
...cloneDeep(defaultState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setServerSideStorage(
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
...user,
|
||||||
|
storage: {
|
||||||
|
_timestamp: 123,
|
||||||
|
_version: VERSION,
|
||||||
|
flagStorage: {
|
||||||
|
...defaultState.flagStorage,
|
||||||
|
updateCounter: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(state.cache.flagStorage).to.eql({
|
||||||
|
...defaultState.flagStorage,
|
||||||
|
updateCounter: 1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reset local timestamp to remote if contents are the same', () => {
|
||||||
|
const state = {
|
||||||
|
...cloneDeep(defaultState),
|
||||||
|
cache: null
|
||||||
|
}
|
||||||
|
setServerSideStorage(
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
...user,
|
||||||
|
storage: {
|
||||||
|
_timestamp: 123,
|
||||||
|
_version: VERSION,
|
||||||
|
flagStorage: {
|
||||||
|
...defaultState.flagStorage,
|
||||||
|
updateCounter: 999
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(state.cache._timestamp).to.eql(123)
|
||||||
|
expect(state.flagStorage.updateCounter).to.eql(999)
|
||||||
|
expect(state.cache.flagStorage.updateCounter).to.eql(999)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remote version if local missing', () => {
|
||||||
|
const state = cloneDeep(defaultState)
|
||||||
|
setServerSideStorage(state, user)
|
||||||
|
expect(state.cache._version).to.eql(VERSION)
|
||||||
|
expect(state.cache._timestamp).to.be.a('number')
|
||||||
|
expect(state.cache.flagStorage).to.eql(defaultState.flagStorage)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('helper functions', () => {
|
||||||
|
describe('_getRecentData', () => {
|
||||||
|
it('should handle nulls correctly', () => {
|
||||||
|
expect(_getRecentData(null, null)).to.eql({ recent: null, stale: null, needUpload: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('doesn\'t choke on invalid data', () => {
|
||||||
|
expect(_getRecentData({ a: 1 }, { b: 2 })).to.eql({ recent: null, stale: null, needUpload: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prefer the valid non-null correctly, needUpload works properly', () => {
|
||||||
|
const nonNull = { _version: VERSION, _timestamp: 1 }
|
||||||
|
expect(_getRecentData(nonNull, null)).to.eql({ recent: nonNull, stale: null, needUpload: true })
|
||||||
|
expect(_getRecentData(null, nonNull)).to.eql({ recent: nonNull, stale: null, needUpload: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prefer the one with higher timestamp', () => {
|
||||||
|
const a = { _version: VERSION, _timestamp: 1 }
|
||||||
|
const b = { _version: VERSION, _timestamp: 2 }
|
||||||
|
|
||||||
|
expect(_getRecentData(a, b)).to.eql({ recent: b, stale: a, needUpload: false })
|
||||||
|
expect(_getRecentData(b, a)).to.eql({ recent: b, stale: a, needUpload: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('case where both are same', () => {
|
||||||
|
const a = { _version: VERSION, _timestamp: 3 }
|
||||||
|
const b = { _version: VERSION, _timestamp: 3 }
|
||||||
|
|
||||||
|
expect(_getRecentData(a, b)).to.eql({ recent: b, stale: a, needUpload: false })
|
||||||
|
expect(_getRecentData(b, a)).to.eql({ recent: b, stale: a, needUpload: false })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_getAllFlags', () => {
|
||||||
|
it('should handle nulls properly', () => {
|
||||||
|
expect(_getAllFlags(null, null)).to.eql([])
|
||||||
|
})
|
||||||
|
it('should output list of keys if passed single object', () => {
|
||||||
|
expect(_getAllFlags({ flagStorage: { a: 1, b: 1, c: 1 } }, null)).to.eql(['a', 'b', 'c'])
|
||||||
|
})
|
||||||
|
it('should union keys of both objects', () => {
|
||||||
|
expect(_getAllFlags({ flagStorage: { a: 1, b: 1, c: 1 } }, { flagStorage: { c: 1, d: 1 } })).to.eql(['a', 'b', 'c', 'd'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_mergeFlags', () => {
|
||||||
|
it('should handle merge two flag sets correctly picking higher numbers', () => {
|
||||||
|
expect(
|
||||||
|
_mergeFlags(
|
||||||
|
{ flagStorage: { a: 0, b: 3 } },
|
||||||
|
{ flagStorage: { b: 1, c: 4, d: 9 } },
|
||||||
|
['a', 'b', 'c', 'd'])
|
||||||
|
).to.eql({ a: 0, b: 3, c: 4, d: 9 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_resetFlags', () => {
|
||||||
|
it('should reset all known flags to 0 when reset flag is set to > 0 and < 9000', () => {
|
||||||
|
const totalFlags = { a: 0, b: 3, reset: 1 }
|
||||||
|
|
||||||
|
expect(_resetFlags(totalFlags)).to.eql({ a: 0, b: 0, reset: 0 })
|
||||||
|
})
|
||||||
|
it('should trim all flags to known when reset is set to 1000', () => {
|
||||||
|
const totalFlags = { a: 0, b: 3, c: 33, reset: COMMAND_TRIM_FLAGS }
|
||||||
|
|
||||||
|
expect(_resetFlags(totalFlags, { a: 0, b: 0, reset: 0 })).to.eql({ a: 0, b: 3, reset: 0 })
|
||||||
|
})
|
||||||
|
it('should trim all flags to known and reset when reset is set to 1001', () => {
|
||||||
|
const totalFlags = { a: 0, b: 3, c: 33, reset: COMMAND_TRIM_FLAGS_AND_RESET }
|
||||||
|
|
||||||
|
expect(_resetFlags(totalFlags, { a: 0, b: 0, reset: 0 })).to.eql({ a: 0, b: 0, reset: 0 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user