feat(fc3b): platforms + credentials Pinia stores + vitests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useApi } from '../composables/useApi.js'
|
||||
|
||||
export const useCredentialsStore = defineStore('credentials', () => {
|
||||
const api = useApi()
|
||||
|
||||
const byPlatform = ref(new Map())
|
||||
const extensionKey = ref(null)
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
async function loadAll() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const arr = await api.get('/api/credentials')
|
||||
const m = new Map()
|
||||
for (const c of arr) m.set(c.platform, c)
|
||||
byPlatform.value = m
|
||||
} catch (e) {
|
||||
error.value = e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function upload(platform, credential_type, data) {
|
||||
const rec = await api.post('/api/credentials', {
|
||||
body: { platform, credential_type, data },
|
||||
})
|
||||
byPlatform.value.delete(platform)
|
||||
return rec
|
||||
}
|
||||
|
||||
async function remove(platform) {
|
||||
await api.delete(`/api/credentials/${platform}`)
|
||||
byPlatform.value.delete(platform)
|
||||
}
|
||||
|
||||
async function loadKey() {
|
||||
const body = await api.get('/api/settings/extension_api_key')
|
||||
extensionKey.value = body.key
|
||||
}
|
||||
|
||||
async function rotateKey() {
|
||||
const body = await api.post('/api/settings/extension_api_key/rotate')
|
||||
extensionKey.value = body.key
|
||||
}
|
||||
|
||||
return {
|
||||
byPlatform, extensionKey, loading, error,
|
||||
loadAll, upload, remove, loadKey, rotateKey,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useApi } from '../composables/useApi.js'
|
||||
|
||||
export const usePlatformsStore = defineStore('platforms', () => {
|
||||
const api = useApi()
|
||||
|
||||
const byKey = ref(new Map())
|
||||
const loaded = ref(false)
|
||||
|
||||
const list = computed(() => Array.from(byKey.value.values()))
|
||||
|
||||
async function loadAll() {
|
||||
if (loaded.value) return
|
||||
const body = await api.get('/api/platforms')
|
||||
const m = new Map()
|
||||
for (const [k, v] of Object.entries(body.platforms || {})) {
|
||||
m.set(k, v)
|
||||
}
|
||||
byKey.value = m
|
||||
loaded.value = true
|
||||
}
|
||||
|
||||
return { byKey, loaded, list, loadAll }
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useCredentialsStore } from '../src/stores/credentials.js'
|
||||
|
||||
function stubFetch(handler) {
|
||||
globalThis.fetch = vi.fn(async (url, init) => {
|
||||
const { status, body } = handler(url, init)
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status, statusText: String(status),
|
||||
text: async () => (body == null ? '' : JSON.stringify(body)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('credentials store', () => {
|
||||
beforeEach(() => setActivePinia(createPinia()))
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
it('loadAll populates byPlatform map', async () => {
|
||||
const s = useCredentialsStore()
|
||||
stubFetch(() => ({
|
||||
status: 200,
|
||||
body: [{ platform: 'patreon', credential_type: 'cookies', captured_at: '2026-05-20T00:00:00+00:00', expires_at: null, last_verified: null }],
|
||||
}))
|
||||
await s.loadAll()
|
||||
expect(s.byPlatform.get('patreon').credential_type).toBe('cookies')
|
||||
})
|
||||
|
||||
it('upload invalidates the cache', async () => {
|
||||
const s = useCredentialsStore()
|
||||
s.byPlatform.set('patreon', { platform: 'patreon' })
|
||||
stubFetch(() => ({ status: 201, body: { platform: 'patreon', credential_type: 'cookies' } }))
|
||||
await s.upload('patreon', 'cookies', 'NETSCAPE_CONTENT')
|
||||
expect(s.byPlatform.has('patreon')).toBe(false)
|
||||
})
|
||||
|
||||
it('remove deletes and invalidates the cache', async () => {
|
||||
const s = useCredentialsStore()
|
||||
s.byPlatform.set('patreon', { platform: 'patreon' })
|
||||
stubFetch(() => ({ status: 204, body: null }))
|
||||
await s.remove('patreon')
|
||||
expect(s.byPlatform.has('patreon')).toBe(false)
|
||||
})
|
||||
|
||||
it('loadKey + rotateKey', async () => {
|
||||
const s = useCredentialsStore()
|
||||
const seen = []
|
||||
stubFetch((url) => {
|
||||
seen.push(url)
|
||||
if (url.endsWith('/rotate')) return { status: 200, body: { key: 'NEW' } }
|
||||
return { status: 200, body: { key: 'OLD' } }
|
||||
})
|
||||
await s.loadKey()
|
||||
expect(s.extensionKey).toBe('OLD')
|
||||
await s.rotateKey()
|
||||
expect(s.extensionKey).toBe('NEW')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { usePlatformsStore } from '../src/stores/platforms.js'
|
||||
|
||||
function stubFetch(handler) {
|
||||
globalThis.fetch = vi.fn(async (url, init) => {
|
||||
const { status, body } = handler(url, init)
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status, statusText: String(status),
|
||||
text: async () => (body == null ? '' : JSON.stringify(body)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('platforms store', () => {
|
||||
beforeEach(() => setActivePinia(createPinia()))
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
it('loadAll fetches /api/platforms and populates list+byKey', async () => {
|
||||
const s = usePlatformsStore()
|
||||
stubFetch(() => ({
|
||||
status: 200,
|
||||
body: { platforms: {
|
||||
patreon: { key: 'patreon', name: 'Patreon', auth_type: 'cookies' },
|
||||
discord: { key: 'discord', name: 'Discord', auth_type: 'token' },
|
||||
}},
|
||||
}))
|
||||
await s.loadAll()
|
||||
expect(s.list.map(p => p.key).sort()).toEqual(['discord', 'patreon'])
|
||||
expect(s.byKey.get('patreon').auth_type).toBe('cookies')
|
||||
})
|
||||
|
||||
it('loadAll is cached on subsequent calls', async () => {
|
||||
const s = usePlatformsStore()
|
||||
let n = 0
|
||||
stubFetch(() => { n++; return { status: 200, body: { platforms: {} } } })
|
||||
await s.loadAll()
|
||||
await s.loadAll()
|
||||
expect(n).toBe(1)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user