mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 18:34:13 +00:00
Video Feature Implementation: - Add Videos and VideoCategories collections with multi-tenant support - Extend VideoBlock with library/upload/embed sources and playback options - Add featuredVideo group to Posts collection with processed embed URLs Hooks & Validation: - Add processFeaturedVideo hook for URL parsing and privacy mode embedding - Add createSlugValidationHook for tenant-scoped slug uniqueness - Add video-utils library (parseVideoUrl, generateEmbedUrl, formatDuration) Testing: - Add 84 unit tests for video-utils (URL parsing, duration, embed generation) - Add 14 integration tests for Videos collection CRUD and slug validation Database: - Migration for videos, video_categories tables with locales - Migration for Posts featuredVideo processed fields - Update payload internal tables for new collections 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
298 lines
7.8 KiB
TypeScript
298 lines
7.8 KiB
TypeScript
import { getPayload, Payload } from 'payload'
|
|
import config from '@/payload.config'
|
|
|
|
import { describe, it, beforeAll, afterAll, expect } from 'vitest'
|
|
|
|
let payload: Payload
|
|
let testTenantId: number
|
|
let testVideoId: number
|
|
let testCategoryId: number
|
|
|
|
describe('Videos Collection API', () => {
|
|
beforeAll(async () => {
|
|
const payloadConfig = await config
|
|
payload = await getPayload({ config: payloadConfig })
|
|
|
|
// Find or use existing tenant for testing
|
|
const tenants = await payload.find({
|
|
collection: 'tenants',
|
|
limit: 1,
|
|
})
|
|
|
|
if (tenants.docs.length > 0) {
|
|
testTenantId = tenants.docs[0].id as number
|
|
} else {
|
|
// Create a test tenant if none exists
|
|
const tenant = await payload.create({
|
|
collection: 'tenants',
|
|
data: {
|
|
name: 'Test Tenant for Videos',
|
|
slug: 'test-videos-tenant',
|
|
domains: [{ domain: 'test-videos.local' }],
|
|
},
|
|
})
|
|
testTenantId = tenant.id as number
|
|
}
|
|
})
|
|
|
|
afterAll(async () => {
|
|
// Cleanup: Delete test video and category if created
|
|
if (testVideoId) {
|
|
try {
|
|
await payload.delete({
|
|
collection: 'videos',
|
|
id: testVideoId,
|
|
})
|
|
} catch {
|
|
// Ignore if already deleted
|
|
}
|
|
}
|
|
if (testCategoryId) {
|
|
try {
|
|
await payload.delete({
|
|
collection: 'video-categories',
|
|
id: testCategoryId,
|
|
})
|
|
} catch {
|
|
// Ignore if already deleted
|
|
}
|
|
}
|
|
})
|
|
|
|
describe('VideoCategories CRUD', () => {
|
|
it('creates a video category', async () => {
|
|
const category = await payload.create({
|
|
collection: 'video-categories',
|
|
data: {
|
|
name: 'Test Category',
|
|
slug: 'test-category-' + Date.now(),
|
|
tenant: testTenantId,
|
|
isActive: true,
|
|
},
|
|
})
|
|
|
|
expect(category).toBeDefined()
|
|
expect(category.id).toBeDefined()
|
|
expect(category.name).toBe('Test Category')
|
|
testCategoryId = category.id as number
|
|
})
|
|
|
|
it('finds video categories', async () => {
|
|
const categories = await payload.find({
|
|
collection: 'video-categories',
|
|
where: {
|
|
tenant: { equals: testTenantId },
|
|
},
|
|
})
|
|
|
|
expect(categories).toBeDefined()
|
|
expect(categories.docs).toBeInstanceOf(Array)
|
|
expect(categories.docs.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('updates a video category', async () => {
|
|
const updated = await payload.update({
|
|
collection: 'video-categories',
|
|
id: testCategoryId,
|
|
data: {
|
|
name: 'Updated Category Name',
|
|
},
|
|
})
|
|
|
|
expect(updated.name).toBe('Updated Category Name')
|
|
})
|
|
})
|
|
|
|
describe('Videos CRUD', () => {
|
|
it('creates a video with YouTube embed', async () => {
|
|
const video = await payload.create({
|
|
collection: 'videos',
|
|
data: {
|
|
title: 'Test Video',
|
|
slug: 'test-video-' + Date.now(),
|
|
tenant: testTenantId,
|
|
source: 'youtube',
|
|
embedUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
|
status: 'draft',
|
|
},
|
|
})
|
|
|
|
expect(video).toBeDefined()
|
|
expect(video.id).toBeDefined()
|
|
expect(video.title).toBe('Test Video')
|
|
expect(video.source).toBe('youtube')
|
|
// Check that videoId was extracted by hook
|
|
expect(video.videoId).toBe('dQw4w9WgXcQ')
|
|
testVideoId = video.id as number
|
|
})
|
|
|
|
it('creates a video with Vimeo embed', async () => {
|
|
const video = await payload.create({
|
|
collection: 'videos',
|
|
data: {
|
|
title: 'Test Vimeo Video',
|
|
slug: 'test-vimeo-video-' + Date.now(),
|
|
tenant: testTenantId,
|
|
source: 'vimeo',
|
|
embedUrl: 'https://vimeo.com/76979871',
|
|
status: 'draft',
|
|
},
|
|
})
|
|
|
|
expect(video).toBeDefined()
|
|
expect(video.videoId).toBe('76979871')
|
|
|
|
// Cleanup this extra video
|
|
await payload.delete({
|
|
collection: 'videos',
|
|
id: video.id,
|
|
})
|
|
})
|
|
|
|
it('finds videos by tenant', async () => {
|
|
const videos = await payload.find({
|
|
collection: 'videos',
|
|
where: {
|
|
tenant: { equals: testTenantId },
|
|
},
|
|
})
|
|
|
|
expect(videos).toBeDefined()
|
|
expect(videos.docs).toBeInstanceOf(Array)
|
|
expect(videos.docs.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('finds videos by status', async () => {
|
|
const videos = await payload.find({
|
|
collection: 'videos',
|
|
where: {
|
|
and: [{ tenant: { equals: testTenantId } }, { status: { equals: 'draft' } }],
|
|
},
|
|
})
|
|
|
|
expect(videos).toBeDefined()
|
|
expect(videos.docs.every((v) => v.status === 'draft')).toBe(true)
|
|
})
|
|
|
|
it('updates a video', async () => {
|
|
const updated = await payload.update({
|
|
collection: 'videos',
|
|
id: testVideoId,
|
|
data: {
|
|
title: 'Updated Video Title',
|
|
status: 'published',
|
|
},
|
|
})
|
|
|
|
expect(updated.title).toBe('Updated Video Title')
|
|
expect(updated.status).toBe('published')
|
|
})
|
|
|
|
it('associates video with category', async () => {
|
|
const updated = await payload.update({
|
|
collection: 'videos',
|
|
id: testVideoId,
|
|
data: {
|
|
category: testCategoryId,
|
|
},
|
|
})
|
|
|
|
expect(updated.category).toBeDefined()
|
|
})
|
|
|
|
it('finds video by slug', async () => {
|
|
// First get the video to know its slug
|
|
const video = await payload.findByID({
|
|
collection: 'videos',
|
|
id: testVideoId,
|
|
})
|
|
|
|
const found = await payload.find({
|
|
collection: 'videos',
|
|
where: {
|
|
and: [{ tenant: { equals: testTenantId } }, { slug: { equals: video.slug } }],
|
|
},
|
|
})
|
|
|
|
expect(found.docs.length).toBe(1)
|
|
expect(found.docs[0].id).toBe(testVideoId)
|
|
})
|
|
})
|
|
|
|
describe('Slug Validation', () => {
|
|
it('prevents duplicate slugs within same tenant', async () => {
|
|
// Get the existing video's slug
|
|
const existingVideo = await payload.findByID({
|
|
collection: 'videos',
|
|
id: testVideoId,
|
|
})
|
|
|
|
// Try to create another video with the same slug
|
|
await expect(
|
|
payload.create({
|
|
collection: 'videos',
|
|
data: {
|
|
title: 'Duplicate Slug Video',
|
|
slug: existingVideo.slug,
|
|
tenant: testTenantId,
|
|
source: 'youtube',
|
|
embedUrl: 'https://www.youtube.com/watch?v=abc123',
|
|
status: 'draft',
|
|
},
|
|
})
|
|
).rejects.toThrow()
|
|
})
|
|
|
|
it('prevents duplicate category slugs within same tenant', async () => {
|
|
// Get the existing category's slug
|
|
const existingCategory = await payload.findByID({
|
|
collection: 'video-categories',
|
|
id: testCategoryId,
|
|
})
|
|
|
|
// Try to create another category with the same slug
|
|
await expect(
|
|
payload.create({
|
|
collection: 'video-categories',
|
|
data: {
|
|
name: 'Duplicate Category',
|
|
slug: existingCategory.slug,
|
|
tenant: testTenantId,
|
|
},
|
|
})
|
|
).rejects.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('Video Deletion', () => {
|
|
it('deletes a video', async () => {
|
|
const deleted = await payload.delete({
|
|
collection: 'videos',
|
|
id: testVideoId,
|
|
})
|
|
|
|
expect(deleted.id).toBe(testVideoId)
|
|
|
|
// Verify it's gone
|
|
const found = await payload.find({
|
|
collection: 'videos',
|
|
where: {
|
|
id: { equals: testVideoId },
|
|
},
|
|
})
|
|
|
|
expect(found.docs.length).toBe(0)
|
|
testVideoId = 0 // Mark as deleted so afterAll doesn't try again
|
|
})
|
|
|
|
it('deletes a video category', async () => {
|
|
const deleted = await payload.delete({
|
|
collection: 'video-categories',
|
|
id: testCategoryId,
|
|
})
|
|
|
|
expect(deleted.id).toBe(testCategoryId)
|
|
testCategoryId = 0 // Mark as deleted
|
|
})
|
|
})
|
|
})
|