mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-29 00:01:42 +08:00
e55ee53496
Centralize request audit recording, preserve immutable download-task history, and derive hourly statistics and backfills from the same authoritative sources. Add durable user registration facts so admin deletion no longer destroys signup history.
266 lines
9.1 KiB
TypeScript
266 lines
9.1 KiB
TypeScript
import { env } from 'cloudflare:workers'
|
|
import { nanoid } from 'nanoid'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { DirType } from '../../shared/constants'
|
|
import type { CreateShareInput } from '../../shared/schemas/share'
|
|
import { S3Service } from '../adapters/gateways/s3'
|
|
import { createAuditRepo } from '../adapters/repos/audit'
|
|
import { createMatterRepo } from '../adapters/repos/matter'
|
|
import { createQuotaRepo } from '../adapters/repos/quota'
|
|
import { createShareRepo } from '../adapters/repos/share'
|
|
import { createStorageRepo } from '../adapters/repos/storage'
|
|
import { createStorageUsageRepo } from '../adapters/repos/storage-usage'
|
|
import { matters, orgQuotaEntitlements, orgQuotas } from '../db/schema'
|
|
import { createCloudflarePlatform } from '../platform/cloudflare'
|
|
import type { Database } from '../platform/interface'
|
|
import { type SaveShareInput, type SaveToDriveDeps, saveShareToDrive as saveShareToDriveUseCase } from './object'
|
|
|
|
function buildDb() {
|
|
return createCloudflarePlatform(env).db
|
|
}
|
|
|
|
const createShare = (db: Database, input: CreateShareInput) => createShareRepo(db).create(input)
|
|
const resolveShareByToken = (db: Database, token: string) => createShareRepo(db).resolveByToken(token)
|
|
const revokeShareByToken = (db: Database, token: string, creatorId: string) =>
|
|
createShareRepo(db).revokeByToken(token, creatorId)
|
|
|
|
function saveToDriveDeps(db: Database): SaveToDriveDeps {
|
|
return {
|
|
s3: new S3Service(),
|
|
storages: createStorageRepo(db),
|
|
storageUsage: createStorageUsageRepo(db),
|
|
quota: createQuotaRepo(db),
|
|
audit: createAuditRepo(db),
|
|
share: createShareRepo(db),
|
|
matter: createMatterRepo(db),
|
|
}
|
|
}
|
|
const saveShareToDrive = (db: Database, input: SaveShareInput) => saveShareToDriveUseCase(saveToDriveDeps(db), input)
|
|
|
|
async function seedStorage(db: ReturnType<typeof buildDb>, id: string) {
|
|
await db.run(
|
|
`INSERT OR IGNORE INTO storages (id, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
|
|
VALUES ('${id}', 'cf-bucket', 'https://s3.amazonaws.com', 'us-east-1', 'AKIA...', 'secret...', '', '', 0, 0, 'active', ${Date.now()}, ${Date.now()})`,
|
|
)
|
|
}
|
|
|
|
async function seedStorageQuota(db: ReturnType<typeof buildDb>, orgId: string, bytes = 10_000_000) {
|
|
const now = new Date()
|
|
await db.insert(orgQuotas).values({ id: nanoid(), orgId, quota: bytes })
|
|
await db.insert(orgQuotaEntitlements).values({
|
|
id: nanoid(),
|
|
orgId,
|
|
resourceType: 'storage',
|
|
entitlementType: 'plan',
|
|
source: 'free_plan',
|
|
sourceId: `free_plan:${orgId}`,
|
|
bytes,
|
|
startsAt: now,
|
|
expiresAt: null,
|
|
status: 'active',
|
|
metadata: JSON.stringify({ packageName: 'Free', source: 'free_plan' }),
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
})
|
|
}
|
|
|
|
async function seedMatter(db: ReturnType<typeof buildDb>, orgId: string, dirtype = DirType.FILE) {
|
|
const now = new Date()
|
|
const matter = {
|
|
id: nanoid(),
|
|
orgId,
|
|
alias: nanoid(10),
|
|
name: `cf-file-${nanoid(6)}.pdf`,
|
|
type: dirtype !== DirType.FILE ? 'folder' : 'application/pdf',
|
|
size: 1024,
|
|
dirtype,
|
|
parent: '',
|
|
object: dirtype !== DirType.FILE ? '' : `objects/${nanoid()}.pdf`,
|
|
storageId: 'cf-storage-1',
|
|
status: 'active',
|
|
trashedAt: null,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
}
|
|
await db.insert(matters).values(matter)
|
|
return matter
|
|
}
|
|
|
|
// ─── resolveShareByToken on D1 ────────────────────────────────────────────────
|
|
|
|
describe('[CF] resolveShareByToken', () => {
|
|
it('returns ok for an active landing share', async () => {
|
|
const db = buildDb()
|
|
const orgId = `org-${nanoid(6)}`
|
|
await seedStorage(db, 'cf-storage-1')
|
|
const matter = await seedMatter(db, orgId)
|
|
|
|
const share = await createShare(db, {
|
|
matterId: matter.id,
|
|
orgId,
|
|
creatorId: 'cf-user-1',
|
|
kind: 'landing',
|
|
})
|
|
|
|
const result = await resolveShareByToken(db, share.token)
|
|
expect(result.status).toBe('ok')
|
|
if (result.status !== 'ok') return
|
|
expect(result.share.id).toBe(share.id)
|
|
})
|
|
|
|
it('returns revoked when share is revoked', async () => {
|
|
const db = buildDb()
|
|
const orgId = `org-${nanoid(6)}`
|
|
await seedStorage(db, 'cf-storage-1')
|
|
const matter = await seedMatter(db, orgId)
|
|
|
|
const share = await createShare(db, { matterId: matter.id, orgId, creatorId: 'cf-user-2', kind: 'landing' })
|
|
await revokeShareByToken(db, share.token, 'cf-user-2')
|
|
|
|
const result = await resolveShareByToken(db, share.token)
|
|
expect(result.status).toBe('revoked')
|
|
})
|
|
|
|
it('returns matter_trashed when matter is trashed', async () => {
|
|
const db = buildDb()
|
|
const orgId = `org-${nanoid(6)}`
|
|
await seedStorage(db, 'cf-storage-1')
|
|
const matter = await seedMatter(db, orgId)
|
|
|
|
const share = await createShare(db, { matterId: matter.id, orgId, creatorId: 'cf-user-3', kind: 'landing' })
|
|
// Trash = active row with trashedAt set (no 'trashed' status).
|
|
await db.run(`UPDATE matters SET trashed_at = ${Date.now()} WHERE id = '${matter.id}'`)
|
|
|
|
const result = await resolveShareByToken(db, share.token)
|
|
expect(result.status).toBe('matter_trashed')
|
|
})
|
|
})
|
|
|
|
// ─── saveShareToDrive on D1 ───────────────────────────────────────────────────
|
|
|
|
describe('[CF] saveShareToDrive — stream copy via D1', () => {
|
|
beforeEach(() => {
|
|
vi.spyOn(S3Service.prototype, 'copyObject').mockResolvedValue(undefined)
|
|
vi.spyOn(S3Service.prototype, 'streamCopy').mockResolvedValue(undefined)
|
|
})
|
|
|
|
it('saves a single file to the target org on D1', async () => {
|
|
const db = buildDb()
|
|
const srcOrgId = `src-${nanoid(6)}`
|
|
const dstOrgId = `dst-${nanoid(6)}`
|
|
await seedStorage(db, 'cf-storage-1')
|
|
await seedStorageQuota(db, dstOrgId)
|
|
|
|
const matter = await seedMatter(db, srcOrgId)
|
|
const share = await createShare(db, { matterId: matter.id, orgId: srcOrgId, creatorId: 'cf-u1', kind: 'landing' })
|
|
|
|
if (share.status === 'revoked') throw new Error('test setup failed')
|
|
|
|
const result = await saveShareToDrive(db, {
|
|
share,
|
|
matter,
|
|
currentUserId: 'cf-u2',
|
|
targetOrgId: dstOrgId,
|
|
targetParent: '',
|
|
})
|
|
|
|
expect(result.saved).toHaveLength(1)
|
|
expect(result.skipped).toHaveLength(0)
|
|
expect(result.saved[0].orgId).toBe(dstOrgId)
|
|
expect(result.saved[0].status).toBe('active')
|
|
})
|
|
|
|
it('does not increment downloads counter after save', async () => {
|
|
const db = buildDb()
|
|
const srcOrgId = `src-${nanoid(6)}`
|
|
const dstOrgId = `dst-${nanoid(6)}`
|
|
await seedStorage(db, 'cf-storage-1')
|
|
await seedStorageQuota(db, dstOrgId)
|
|
|
|
const matter = await seedMatter(db, srcOrgId)
|
|
const share = await createShare(db, { matterId: matter.id, orgId: srcOrgId, creatorId: 'cf-u3', kind: 'landing' })
|
|
|
|
if (share.status === 'revoked') throw new Error('test setup failed')
|
|
const downloadsBefore = share.downloads
|
|
|
|
await saveShareToDrive(db, {
|
|
share,
|
|
matter,
|
|
currentUserId: 'cf-u4',
|
|
targetOrgId: dstOrgId,
|
|
targetParent: '',
|
|
})
|
|
|
|
const _rows = await db.select().from(matters).where(
|
|
// Check that the original share downloads didn't change
|
|
)
|
|
// Verify by re-querying the share
|
|
const updatedShareRows = await db.all<{ downloads: number }>(
|
|
`SELECT downloads FROM shares WHERE id = '${share.id}'`,
|
|
)
|
|
expect(updatedShareRows[0]?.downloads).toBe(downloadsBefore)
|
|
})
|
|
|
|
it('recursively saves a folder tree on D1', async () => {
|
|
const db = buildDb()
|
|
const srcOrgId = `src-${nanoid(6)}`
|
|
const dstOrgId = `dst-${nanoid(6)}`
|
|
await seedStorage(db, 'cf-storage-1')
|
|
await seedStorageQuota(db, dstOrgId)
|
|
|
|
// Create folder structure
|
|
const now = new Date()
|
|
const folder = {
|
|
id: nanoid(),
|
|
orgId: srcOrgId,
|
|
alias: nanoid(10),
|
|
name: 'cf-album',
|
|
type: 'folder',
|
|
size: 0,
|
|
dirtype: DirType.USER_FOLDER,
|
|
parent: '',
|
|
object: '',
|
|
storageId: 'cf-storage-1',
|
|
status: 'active',
|
|
trashedAt: null,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
}
|
|
await db.insert(matters).values(folder)
|
|
|
|
const file1 = {
|
|
id: nanoid(),
|
|
orgId: srcOrgId,
|
|
alias: nanoid(10),
|
|
name: 'photo1.jpg',
|
|
type: 'image/jpeg',
|
|
size: 500,
|
|
dirtype: DirType.FILE,
|
|
parent: 'cf-album',
|
|
object: `objects/${nanoid()}.jpg`,
|
|
storageId: 'cf-storage-1',
|
|
status: 'active',
|
|
trashedAt: null,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
}
|
|
await db.insert(matters).values(file1)
|
|
|
|
const share = await createShare(db, { matterId: folder.id, orgId: srcOrgId, creatorId: 'cf-u5', kind: 'landing' })
|
|
|
|
if (share.status === 'revoked') throw new Error('test setup failed')
|
|
|
|
const result = await saveShareToDrive(db, {
|
|
share,
|
|
matter: folder,
|
|
currentUserId: 'cf-u6',
|
|
targetOrgId: dstOrgId,
|
|
targetParent: '',
|
|
})
|
|
|
|
// 1 root folder + 1 file = 2
|
|
expect(result.saved).toHaveLength(2)
|
|
expect(result.skipped).toHaveLength(0)
|
|
})
|
|
})
|