feat: remove hashed passwords feature spec and migrate to archive
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m6s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m6s
fix: update login token expiration to 62 days chore: bump version to 1.0.5RC1 test: add isParentPersistent to LoginButton.spec.ts refactor: rename Assign Tasks button to Assign Chores in ParentView.vue refactor: rename Assign Tasks to Assign Chores in TaskAssignView.vue feat: add stay in parent mode checkbox and badge in LoginButton.vue test: enhance LoginButton.spec.ts with persistent mode tests test: add authGuard.spec.ts for logoutParent and enforceParentExpiry feat: implement parent mode expiry logic in auth.ts test: add auth.expiry.spec.ts for parent mode expiry tests chore: create template for feature specs
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { isParentAuthenticated, isUserLoggedIn, loginUser, initAuthSync } from '../auth'
|
||||
import {
|
||||
isParentAuthenticated,
|
||||
isUserLoggedIn,
|
||||
loginUser,
|
||||
initAuthSync,
|
||||
authenticateParent,
|
||||
logoutParent,
|
||||
} from '../auth'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
// Stub window.location to prevent jsdom "navigation to another Document" warnings
|
||||
@@ -26,20 +33,26 @@ global.localStorage = {
|
||||
|
||||
describe('auth store - child mode on login', () => {
|
||||
beforeEach(() => {
|
||||
isParentAuthenticated.value = true
|
||||
localStorage.setItem('isParentAuthenticated', 'true')
|
||||
// Use authenticateParent() to set up parent-mode state
|
||||
authenticateParent(false)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
logoutParent()
|
||||
})
|
||||
|
||||
it('should clear isParentAuthenticated and localStorage on loginUser()', async () => {
|
||||
expect(isParentAuthenticated.value).toBe(true)
|
||||
loginUser()
|
||||
await nextTick() // flush Vue watcher
|
||||
await nextTick()
|
||||
expect(isParentAuthenticated.value).toBe(false)
|
||||
})
|
||||
|
||||
it('logs out on cross-tab storage logout event', async () => {
|
||||
initAuthSync()
|
||||
isUserLoggedIn.value = true
|
||||
isParentAuthenticated.value = true
|
||||
authenticateParent(false)
|
||||
expect(isParentAuthenticated.value).toBe(true)
|
||||
|
||||
const logoutEvent = new StorageEvent('storage', {
|
||||
key: 'authSyncEvent',
|
||||
@@ -51,4 +64,26 @@ describe('auth store - child mode on login', () => {
|
||||
expect(isUserLoggedIn.value).toBe(false)
|
||||
expect(isParentAuthenticated.value).toBe(false)
|
||||
})
|
||||
|
||||
it('exits parent mode on cross-tab parent_logout storage event', async () => {
|
||||
initAuthSync()
|
||||
authenticateParent(false)
|
||||
expect(isParentAuthenticated.value).toBe(true)
|
||||
|
||||
// Simulate being on a /parent route in this tab
|
||||
locationStub.pathname = '/parent'
|
||||
|
||||
const parentLogoutEvent = new StorageEvent('storage', {
|
||||
key: 'authSyncEvent',
|
||||
newValue: JSON.stringify({ type: 'parent_logout', at: Date.now() }),
|
||||
})
|
||||
window.dispatchEvent(parentLogoutEvent)
|
||||
|
||||
await nextTick()
|
||||
expect(isParentAuthenticated.value).toBe(false)
|
||||
expect(locationStub.href).toBe('/child')
|
||||
|
||||
// Reset for other tests
|
||||
locationStub.pathname = '/'
|
||||
})
|
||||
})
|
||||
|
||||
165
frontend/vue-app/src/stores/__tests__/auth.expiry.spec.ts
Normal file
165
frontend/vue-app/src/stores/__tests__/auth.expiry.spec.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import {
|
||||
isParentAuthenticated,
|
||||
isParentPersistent,
|
||||
parentAuthExpiresAt,
|
||||
authenticateParent,
|
||||
logoutParent,
|
||||
loginUser,
|
||||
} from '../auth'
|
||||
|
||||
// Stub window.location
|
||||
const locationStub = { href: '', pathname: '/', assign: vi.fn(), replace: vi.fn(), reload: vi.fn() }
|
||||
Object.defineProperty(window, 'location', { value: locationStub, writable: true })
|
||||
|
||||
// Build a stateful localStorage stub and register it via vi.stubGlobal so it is
|
||||
// visible to auth.ts's module scope (not just the test file's scope).
|
||||
function makeLocalStorageStub() {
|
||||
const store: Record<string, string> = {}
|
||||
return {
|
||||
getItem: (key: string) => store[key] ?? null,
|
||||
setItem: (key: string, value: string) => {
|
||||
store[key] = value
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
delete store[key]
|
||||
},
|
||||
clear: () => {
|
||||
for (const k of Object.keys(store)) delete store[k]
|
||||
},
|
||||
_store: store,
|
||||
}
|
||||
}
|
||||
|
||||
const localStorageStub = makeLocalStorageStub()
|
||||
vi.stubGlobal('localStorage', localStorageStub)
|
||||
|
||||
describe('auth store - parent mode expiry', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
localStorageStub.clear()
|
||||
logoutParent()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
logoutParent()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('non-persistent mode', () => {
|
||||
it('authenticateParent(false) sets isParentAuthenticated to true', () => {
|
||||
authenticateParent(false)
|
||||
expect(isParentAuthenticated.value).toBe(true)
|
||||
expect(isParentPersistent.value).toBe(false)
|
||||
})
|
||||
|
||||
it('non-persistent auth does not set isParentPersistent', () => {
|
||||
authenticateParent(false)
|
||||
expect(isParentPersistent.value).toBe(false)
|
||||
expect(parentAuthExpiresAt.value).not.toBeNull()
|
||||
// Confirm the expiry is ~1 minute, not 2 days
|
||||
expect(parentAuthExpiresAt.value!).toBeLessThan(Date.now() + 172_800_000)
|
||||
})
|
||||
|
||||
it('isParentAuthenticated becomes false after 1 minute (watcher fires)', () => {
|
||||
authenticateParent(false)
|
||||
expect(isParentAuthenticated.value).toBe(true)
|
||||
// Advance 60s: watcher fires every 15s, at t=60000 Date.now() >= expiresAt
|
||||
vi.advanceTimersByTime(60_001)
|
||||
expect(isParentAuthenticated.value).toBe(false)
|
||||
})
|
||||
|
||||
it('isParentAuthenticated is still true just before 1 minute', () => {
|
||||
authenticateParent(false)
|
||||
vi.advanceTimersByTime(59_999)
|
||||
// Watcher last fired at t=45000, next fires at t=60000 — hasn't expired yet
|
||||
expect(isParentAuthenticated.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('persistent mode', () => {
|
||||
it('authenticateParent(true) sets isParentAuthenticated to true', () => {
|
||||
authenticateParent(true)
|
||||
expect(isParentAuthenticated.value).toBe(true)
|
||||
expect(isParentPersistent.value).toBe(true)
|
||||
})
|
||||
|
||||
it('writes expiresAt to localStorage for persistent auth — parentAuthExpiresAt is set to ~2 days', () => {
|
||||
const before = Date.now()
|
||||
authenticateParent(true)
|
||||
const after = Date.now()
|
||||
// Verify the expiry ref is populated and within the 2-day window
|
||||
expect(parentAuthExpiresAt.value).not.toBeNull()
|
||||
expect(parentAuthExpiresAt.value!).toBeGreaterThanOrEqual(before + 172_800_000)
|
||||
expect(parentAuthExpiresAt.value!).toBeLessThanOrEqual(after + 172_800_000)
|
||||
})
|
||||
|
||||
it('isParentAuthenticated becomes false after 2 days (watcher fires)', () => {
|
||||
authenticateParent(true)
|
||||
expect(isParentAuthenticated.value).toBe(true)
|
||||
vi.advanceTimersByTime(172_800_001)
|
||||
expect(isParentAuthenticated.value).toBe(false)
|
||||
})
|
||||
|
||||
it('isParentAuthenticated is still true just before 2 days', () => {
|
||||
authenticateParent(true)
|
||||
// Advance to just before expiry; watcher last fired at t=172_785_000
|
||||
vi.advanceTimersByTime(172_784_999)
|
||||
expect(isParentAuthenticated.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('logoutParent()', () => {
|
||||
it('clears isParentAuthenticated and isParentPersistent', () => {
|
||||
authenticateParent(true)
|
||||
expect(isParentAuthenticated.value).toBe(true)
|
||||
logoutParent()
|
||||
expect(isParentAuthenticated.value).toBe(false)
|
||||
expect(isParentPersistent.value).toBe(false)
|
||||
})
|
||||
|
||||
it('removing auth clears expiresAt and persistent flag', () => {
|
||||
authenticateParent(true)
|
||||
expect(parentAuthExpiresAt.value).not.toBeNull()
|
||||
logoutParent()
|
||||
expect(parentAuthExpiresAt.value).toBeNull()
|
||||
expect(isParentPersistent.value).toBe(false)
|
||||
})
|
||||
|
||||
it('clears parentAuthExpiresAt', () => {
|
||||
authenticateParent(false)
|
||||
logoutParent()
|
||||
expect(parentAuthExpiresAt.value).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('loginUser()', () => {
|
||||
it('loginUser() clears parent auth entirely', () => {
|
||||
authenticateParent(true)
|
||||
expect(isParentAuthenticated.value).toBe(true)
|
||||
loginUser()
|
||||
expect(isParentAuthenticated.value).toBe(false)
|
||||
expect(isParentPersistent.value).toBe(false)
|
||||
expect(parentAuthExpiresAt.value).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('localStorage restore on init', () => {
|
||||
it('expired localStorage entry is cleaned up when checked', () => {
|
||||
// Simulate a stored entry that is already expired
|
||||
const expired = Date.now() - 1000
|
||||
localStorage.setItem('parentAuth', JSON.stringify({ expiresAt: expired }))
|
||||
|
||||
// Mirroring the init logic in auth.ts: read, check, remove if stale
|
||||
const stored = localStorage.getItem('parentAuth')
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored) as { expiresAt: number }
|
||||
if (!parsed.expiresAt || Date.now() >= parsed.expiresAt) {
|
||||
localStorage.removeItem('parentAuth')
|
||||
}
|
||||
}
|
||||
|
||||
expect(localStorage.getItem('parentAuth')).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user