@nextsparkjs/theme-default 0.1.0-beta.92 → 0.1.0-beta.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/blocks/hero/fields.ts +1 -1
- package/blocks/hero-with-form/fields.ts +1 -1
- package/blocks/jumbotron/fields.ts +1 -1
- package/blocks/logo-cloud/fields.ts +1 -1
- package/blocks/split-content/fields.ts +1 -1
- package/blocks/testimonials/fields.ts +1 -1
- package/blocks/video-hero/fields.ts +1 -1
- package/config/app.config.ts +17 -0
- package/messages/en/navigation.json +2 -1
- package/messages/es/navigation.json +2 -1
- package/package.json +2 -2
- package/styles/globals.css +14 -0
- package/tests/cypress/e2e/api/entities/media/media-crud.cy.ts +600 -0
- package/tests/cypress/src/controllers/MediaAPIController.js +231 -0
- package/LICENSE +0 -21
package/blocks/hero/fields.ts
CHANGED
|
@@ -21,7 +21,7 @@ const heroDesignFields: FieldDefinition[] = [
|
|
|
21
21
|
{
|
|
22
22
|
name: 'backgroundImage',
|
|
23
23
|
label: 'Background Image',
|
|
24
|
-
type: '
|
|
24
|
+
type: 'media-library',
|
|
25
25
|
tab: 'design',
|
|
26
26
|
required: false,
|
|
27
27
|
helpText: 'Optional background image (recommended: 1920x1080px minimum)',
|
|
@@ -34,7 +34,7 @@ const heroWithFormContentFields: FieldDefinition[] = [
|
|
|
34
34
|
{
|
|
35
35
|
name: 'backgroundImage',
|
|
36
36
|
label: 'Background Image',
|
|
37
|
-
type: '
|
|
37
|
+
type: 'media-library',
|
|
38
38
|
tab: 'content',
|
|
39
39
|
required: true,
|
|
40
40
|
helpText: 'Full-width background image (recommended: 1920x1080px minimum)',
|
|
@@ -148,7 +148,7 @@ const jumbotronDesignFields: FieldDefinition[] = [
|
|
|
148
148
|
{
|
|
149
149
|
name: 'backgroundImage',
|
|
150
150
|
label: 'Background Image',
|
|
151
|
-
type: '
|
|
151
|
+
type: 'media-library',
|
|
152
152
|
tab: 'design',
|
|
153
153
|
required: false,
|
|
154
154
|
helpText: 'Optional background image (recommended: 1920x1080px minimum)',
|
|
@@ -28,7 +28,7 @@ const videoHeroContentFields: FieldDefinition[] = [
|
|
|
28
28
|
{
|
|
29
29
|
name: 'videoThumbnail',
|
|
30
30
|
label: 'Custom Thumbnail',
|
|
31
|
-
type: '
|
|
31
|
+
type: 'media-library',
|
|
32
32
|
tab: 'content',
|
|
33
33
|
required: false,
|
|
34
34
|
helpText: 'Optional custom thumbnail shown before video plays (recommended: 1920x1080px)',
|
package/config/app.config.ts
CHANGED
|
@@ -333,6 +333,23 @@ export const APP_CONFIG_OVERRIDES = {
|
|
|
333
333
|
},
|
|
334
334
|
},
|
|
335
335
|
|
|
336
|
+
// =============================================================================
|
|
337
|
+
// MEDIA LIBRARY OVERRIDES
|
|
338
|
+
// =============================================================================
|
|
339
|
+
// Uncomment and modify to customize media upload limits and accepted file types.
|
|
340
|
+
//
|
|
341
|
+
// media: {
|
|
342
|
+
// maxSizeMB: 10, // General fallback max size
|
|
343
|
+
// maxSizeImageMB: 10, // Max size for image/* files (overrides maxSizeMB)
|
|
344
|
+
// maxSizeVideoMB: 50, // Max size for video/* files (overrides maxSizeMB)
|
|
345
|
+
// acceptedTypes: ['image/*', 'video/*', 'application/pdf'],
|
|
346
|
+
// allowedMimeTypes: [
|
|
347
|
+
// 'image/jpeg', 'image/png', 'image/webp',
|
|
348
|
+
// 'video/mp4', 'video/webm',
|
|
349
|
+
// 'application/pdf',
|
|
350
|
+
// ],
|
|
351
|
+
// },
|
|
352
|
+
|
|
336
353
|
// =============================================================================
|
|
337
354
|
// DEV KEYRING - MOVED TO dev.config.ts
|
|
338
355
|
// =============================================================================
|
package/package.json
CHANGED
package/styles/globals.css
CHANGED
|
@@ -12,6 +12,20 @@
|
|
|
12
12
|
IMPORTS
|
|
13
13
|
============================================= */
|
|
14
14
|
@import "tailwindcss";
|
|
15
|
+
|
|
16
|
+
/* =============================================
|
|
17
|
+
TAILWIND v4 DARK MODE CONFIGURATION
|
|
18
|
+
|
|
19
|
+
Por defecto Tailwind v4 usa @media (prefers-color-scheme: dark)
|
|
20
|
+
para las variantes dark:. Esto causa problemas porque:
|
|
21
|
+
- next-themes usa la clase .dark en el HTML
|
|
22
|
+
- El sistema operativo puede tener dark mode aunque la app use light
|
|
23
|
+
|
|
24
|
+
Esta configuración hace que Tailwind use el selector de clase .dark
|
|
25
|
+
en lugar de la media query, sincronizándose con next-themes.
|
|
26
|
+
============================================= */
|
|
27
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
28
|
+
|
|
15
29
|
@plugin "@tailwindcss/container-queries";
|
|
16
30
|
@import "@nextsparkjs/core/styles/ui.css";
|
|
17
31
|
@import "@nextsparkjs/core/styles/utilities.css";
|
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media API - CRUD Tests
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive test suite for Media API endpoints.
|
|
5
|
+
* Tests GET, PATCH, DELETE operations and upload functionality.
|
|
6
|
+
*
|
|
7
|
+
* Entity characteristics:
|
|
8
|
+
* - No team context required (global media library)
|
|
9
|
+
* - Soft delete (status → deleted)
|
|
10
|
+
* - Supports filtering by type, search by filename
|
|
11
|
+
* - Pagination via limit/offset
|
|
12
|
+
* - Sorting via orderBy/orderDir
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/// <reference types="cypress" />
|
|
16
|
+
|
|
17
|
+
import * as allure from 'allure-cypress'
|
|
18
|
+
|
|
19
|
+
const MediaAPIController = require('../../../../src/controllers/MediaAPIController.js')
|
|
20
|
+
|
|
21
|
+
describe('Media API - CRUD Operations', {
|
|
22
|
+
tags: ['@api', '@feat-media-library', '@crud', '@regression']
|
|
23
|
+
}, () => {
|
|
24
|
+
// Test constants
|
|
25
|
+
const SUPERADMIN_API_KEY = 'test_api_key_for_testing_purposes_only_not_a_real_secret_key_abc123'
|
|
26
|
+
const BASE_URL = Cypress.config('baseUrl') || 'http://localhost:5173'
|
|
27
|
+
|
|
28
|
+
// Controller instance
|
|
29
|
+
let mediaAPI: InstanceType<typeof MediaAPIController>
|
|
30
|
+
|
|
31
|
+
// Track created media for cleanup
|
|
32
|
+
let createdMedia: any[] = []
|
|
33
|
+
|
|
34
|
+
before(() => {
|
|
35
|
+
// Initialize controller with superadmin credentials
|
|
36
|
+
mediaAPI = new MediaAPIController(BASE_URL, SUPERADMIN_API_KEY)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
allure.epic('API')
|
|
41
|
+
allure.feature('Media Library')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
// Cleanup created media after each test
|
|
46
|
+
createdMedia.forEach((media) => {
|
|
47
|
+
if (media?.id) {
|
|
48
|
+
mediaAPI.deleteMedia(media.id)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
createdMedia = []
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// ============================================
|
|
55
|
+
// GET /api/v1/media - List Media
|
|
56
|
+
// ============================================
|
|
57
|
+
describe('GET /api/v1/media - List Media', () => {
|
|
58
|
+
it('MEDIA_API_001: Should list media with valid API key', { tags: '@smoke' }, () => {
|
|
59
|
+
allure.story('CRUD Operations')
|
|
60
|
+
allure.severity('critical')
|
|
61
|
+
|
|
62
|
+
mediaAPI.getMedia().then((response: any) => {
|
|
63
|
+
mediaAPI.validateSuccessResponse(response, 200)
|
|
64
|
+
expect(response.body.data).to.be.an('array')
|
|
65
|
+
expect(response.body.info).to.have.property('limit')
|
|
66
|
+
expect(response.body.info).to.have.property('offset')
|
|
67
|
+
expect(response.body.info).to.have.property('total')
|
|
68
|
+
|
|
69
|
+
cy.log(`Found ${response.body.data.length} media items`)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('MEDIA_API_002: Should list media with pagination (limit/offset)', () => {
|
|
74
|
+
mediaAPI.getMedia({ limit: 5, offset: 0 }).then((response: any) => {
|
|
75
|
+
mediaAPI.validateSuccessResponse(response, 200)
|
|
76
|
+
expect(response.body.info.limit).to.eq(5)
|
|
77
|
+
expect(response.body.info.offset).to.eq(0)
|
|
78
|
+
expect(response.body.data.length).to.be.at.most(5)
|
|
79
|
+
|
|
80
|
+
cy.log(`Page 1 with limit 5: ${response.body.data.length} media items`)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('MEDIA_API_003: Should filter media by type=image', () => {
|
|
85
|
+
mediaAPI.getMedia({ type: 'image' }).then((response: any) => {
|
|
86
|
+
mediaAPI.validateSuccessResponse(response, 200)
|
|
87
|
+
expect(response.body.data).to.be.an('array')
|
|
88
|
+
|
|
89
|
+
// All returned items should have type=image
|
|
90
|
+
response.body.data.forEach((media: any) => {
|
|
91
|
+
expect(media.type).to.eq('image')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
cy.log(`Found ${response.body.data.length} images`)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('MEDIA_API_004: Should filter media by type=video', () => {
|
|
99
|
+
mediaAPI.getMedia({ type: 'video' }).then((response: any) => {
|
|
100
|
+
mediaAPI.validateSuccessResponse(response, 200)
|
|
101
|
+
expect(response.body.data).to.be.an('array')
|
|
102
|
+
|
|
103
|
+
// All returned items should have type=video
|
|
104
|
+
response.body.data.forEach((media: any) => {
|
|
105
|
+
expect(media.type).to.eq('video')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
cy.log(`Found ${response.body.data.length} videos`)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('MEDIA_API_005: Should search media by filename', () => {
|
|
113
|
+
// This test assumes there's existing media with searchable filenames
|
|
114
|
+
// If no media exists, search returns empty array (still valid)
|
|
115
|
+
const searchTerm = 'test'
|
|
116
|
+
|
|
117
|
+
mediaAPI.getMedia({ search: searchTerm }).then((response: any) => {
|
|
118
|
+
mediaAPI.validateSuccessResponse(response, 200)
|
|
119
|
+
expect(response.body.data).to.be.an('array')
|
|
120
|
+
|
|
121
|
+
cy.log(`Search '${searchTerm}' found ${response.body.data.length} results`)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('MEDIA_API_006: Should sort media by filename (ascending)', () => {
|
|
126
|
+
mediaAPI.getMedia({ orderBy: 'filename', orderDir: 'asc', limit: 10 }).then((response: any) => {
|
|
127
|
+
mediaAPI.validateSuccessResponse(response, 200)
|
|
128
|
+
expect(response.body.data).to.be.an('array')
|
|
129
|
+
|
|
130
|
+
if (response.body.data.length > 1) {
|
|
131
|
+
// Verify ascending order
|
|
132
|
+
const filenames = response.body.data.map((m: any) => m.filename)
|
|
133
|
+
const sorted = [...filenames].sort()
|
|
134
|
+
expect(filenames).to.deep.eq(sorted)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
cy.log(`Sorted ${response.body.data.length} items by filename (asc)`)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('MEDIA_API_007: Should sort media by uploadedAt (descending)', () => {
|
|
142
|
+
mediaAPI.getMedia({ orderBy: 'uploadedAt', orderDir: 'desc', limit: 10 }).then((response: any) => {
|
|
143
|
+
mediaAPI.validateSuccessResponse(response, 200)
|
|
144
|
+
expect(response.body.data).to.be.an('array')
|
|
145
|
+
|
|
146
|
+
if (response.body.data.length > 1) {
|
|
147
|
+
// Verify descending order by uploadedAt
|
|
148
|
+
const timestamps = response.body.data.map((m: any) => new Date(m.uploadedAt).getTime())
|
|
149
|
+
for (let i = 0; i < timestamps.length - 1; i++) {
|
|
150
|
+
expect(timestamps[i]).to.be.at.least(timestamps[i + 1])
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
cy.log(`Sorted ${response.body.data.length} items by uploadedAt (desc)`)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('MEDIA_API_008: Should return empty array for non-matching search', () => {
|
|
159
|
+
const nonExistentTerm = 'NonExistentMediaFilename123456789'
|
|
160
|
+
|
|
161
|
+
mediaAPI.getMedia({ search: nonExistentTerm }).then((response: any) => {
|
|
162
|
+
mediaAPI.validateSuccessResponse(response, 200)
|
|
163
|
+
expect(response.body.data).to.be.an('array')
|
|
164
|
+
expect(response.body.data.length).to.eq(0)
|
|
165
|
+
|
|
166
|
+
cy.log('Search with non-matching term returns empty array')
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('MEDIA_API_009: Should reject request without API key', () => {
|
|
171
|
+
const noAuthAPI = new MediaAPIController(BASE_URL, null)
|
|
172
|
+
|
|
173
|
+
noAuthAPI.getMedia().then((response: any) => {
|
|
174
|
+
expect(response.status).to.eq(401)
|
|
175
|
+
expect(response.body).to.have.property('success', false)
|
|
176
|
+
|
|
177
|
+
cy.log('Request without API key rejected with 401')
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// ============================================
|
|
183
|
+
// GET /api/v1/media/{id} - Get Media by ID
|
|
184
|
+
// ============================================
|
|
185
|
+
describe('GET /api/v1/media/{id} - Get Media by ID', () => {
|
|
186
|
+
let testMediaId: string
|
|
187
|
+
|
|
188
|
+
beforeEach(() => {
|
|
189
|
+
// Get first media item from list for testing
|
|
190
|
+
mediaAPI.getMedia({ limit: 1 }).then((response: any) => {
|
|
191
|
+
if (response.body.data.length > 0) {
|
|
192
|
+
testMediaId = response.body.data[0].id
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('MEDIA_API_010: Should get media by valid ID', () => {
|
|
198
|
+
cy.then(() => {
|
|
199
|
+
if (!testMediaId) {
|
|
200
|
+
cy.log('No media items available to test')
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
mediaAPI.getMediaById(testMediaId).then((response: any) => {
|
|
205
|
+
mediaAPI.validateSuccessResponse(response, 200)
|
|
206
|
+
|
|
207
|
+
const media = response.body.data
|
|
208
|
+
mediaAPI.validateMediaObject(media)
|
|
209
|
+
expect(media.id).to.eq(testMediaId)
|
|
210
|
+
|
|
211
|
+
cy.log(`Retrieved media: ${media.filename}`)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('MEDIA_API_011: Should return 404 for non-existent media', () => {
|
|
217
|
+
const fakeId = 'non-existent-media-id-12345'
|
|
218
|
+
|
|
219
|
+
mediaAPI.getMediaById(fakeId).then((response: any) => {
|
|
220
|
+
expect(response.status).to.eq(404)
|
|
221
|
+
expect(response.body).to.have.property('success', false)
|
|
222
|
+
|
|
223
|
+
cy.log('Non-existent media returns 404')
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// ============================================
|
|
229
|
+
// PATCH /api/v1/media/{id} - Update Media Metadata
|
|
230
|
+
// ============================================
|
|
231
|
+
describe('PATCH /api/v1/media/{id} - Update Media Metadata', () => {
|
|
232
|
+
let testMediaId: string
|
|
233
|
+
|
|
234
|
+
beforeEach(() => {
|
|
235
|
+
// Get first media item for testing
|
|
236
|
+
mediaAPI.getMedia({ limit: 1 }).then((response: any) => {
|
|
237
|
+
if (response.body.data.length > 0) {
|
|
238
|
+
testMediaId = response.body.data[0].id
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('MEDIA_API_020: Should update media alt text', () => {
|
|
244
|
+
cy.then(() => {
|
|
245
|
+
if (!testMediaId) {
|
|
246
|
+
cy.log('No media items available to test')
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const updateData = {
|
|
251
|
+
alt: 'Updated alt text for testing'
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
mediaAPI.updateMedia(testMediaId, updateData).then((response: any) => {
|
|
255
|
+
mediaAPI.validateSuccessResponse(response, 200)
|
|
256
|
+
|
|
257
|
+
const media = response.body.data
|
|
258
|
+
expect(media.alt).to.eq(updateData.alt)
|
|
259
|
+
|
|
260
|
+
cy.log(`Updated media alt: ${media.alt}`)
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('MEDIA_API_021: Should update media caption', () => {
|
|
266
|
+
cy.then(() => {
|
|
267
|
+
if (!testMediaId) {
|
|
268
|
+
cy.log('No media items available to test')
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const updateData = {
|
|
273
|
+
caption: 'Updated caption for testing'
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
mediaAPI.updateMedia(testMediaId, updateData).then((response: any) => {
|
|
277
|
+
mediaAPI.validateSuccessResponse(response, 200)
|
|
278
|
+
|
|
279
|
+
const media = response.body.data
|
|
280
|
+
expect(media.caption).to.eq(updateData.caption)
|
|
281
|
+
|
|
282
|
+
cy.log(`Updated media caption: ${media.caption}`)
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('MEDIA_API_022: Should update both alt and caption', () => {
|
|
288
|
+
cy.then(() => {
|
|
289
|
+
if (!testMediaId) {
|
|
290
|
+
cy.log('No media items available to test')
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const updateData = {
|
|
295
|
+
alt: 'Updated alt text',
|
|
296
|
+
caption: 'Updated caption'
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
mediaAPI.updateMedia(testMediaId, updateData).then((response: any) => {
|
|
300
|
+
mediaAPI.validateSuccessResponse(response, 200)
|
|
301
|
+
|
|
302
|
+
const media = response.body.data
|
|
303
|
+
expect(media.alt).to.eq(updateData.alt)
|
|
304
|
+
expect(media.caption).to.eq(updateData.caption)
|
|
305
|
+
|
|
306
|
+
cy.log(`Updated both alt and caption`)
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('MEDIA_API_023: Should return 404 for non-existent media', () => {
|
|
312
|
+
const fakeId = 'non-existent-media-id-12345'
|
|
313
|
+
|
|
314
|
+
mediaAPI.updateMedia(fakeId, { alt: 'New Alt' }).then((response: any) => {
|
|
315
|
+
expect(response.status).to.eq(404)
|
|
316
|
+
expect(response.body).to.have.property('success', false)
|
|
317
|
+
|
|
318
|
+
cy.log('Update non-existent media returns 404')
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('MEDIA_API_024: Should reject empty update body', () => {
|
|
323
|
+
cy.then(() => {
|
|
324
|
+
if (!testMediaId) {
|
|
325
|
+
cy.log('No media items available to test')
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
mediaAPI.updateMedia(testMediaId, {}).then((response: any) => {
|
|
330
|
+
expect(response.status).to.eq(400)
|
|
331
|
+
expect(response.body).to.have.property('success', false)
|
|
332
|
+
|
|
333
|
+
cy.log('Empty update body rejected')
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('MEDIA_API_025: Should reject invalid update fields (only alt/caption allowed)', () => {
|
|
339
|
+
cy.then(() => {
|
|
340
|
+
if (!testMediaId) {
|
|
341
|
+
cy.log('No media items available to test')
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Try to update filename (should be rejected)
|
|
346
|
+
const invalidUpdate = {
|
|
347
|
+
filename: 'hacked-filename.jpg'
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
mediaAPI.updateMedia(testMediaId, invalidUpdate).then((response: any) => {
|
|
351
|
+
// Should return 400 (validation error) or ignore the invalid field
|
|
352
|
+
// Backend should only allow alt/caption updates
|
|
353
|
+
expect(response.status).to.be.oneOf([200, 400])
|
|
354
|
+
|
|
355
|
+
if (response.status === 200) {
|
|
356
|
+
// If 200, filename should NOT be changed
|
|
357
|
+
expect(response.body.data.filename).to.not.eq(invalidUpdate.filename)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
cy.log('Invalid field update handled correctly')
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
// ============================================
|
|
367
|
+
// DELETE /api/v1/media/{id} - Soft Delete Media
|
|
368
|
+
// ============================================
|
|
369
|
+
describe('DELETE /api/v1/media/{id} - Soft Delete Media', () => {
|
|
370
|
+
let testMediaId: string
|
|
371
|
+
|
|
372
|
+
beforeEach(() => {
|
|
373
|
+
// Get a media item that can be deleted
|
|
374
|
+
mediaAPI.getMedia({ limit: 1, type: 'image' }).then((response: any) => {
|
|
375
|
+
if (response.body.data.length > 0) {
|
|
376
|
+
testMediaId = response.body.data[0].id
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('MEDIA_API_030: Should soft delete media by valid ID', () => {
|
|
382
|
+
cy.then(() => {
|
|
383
|
+
if (!testMediaId) {
|
|
384
|
+
cy.log('No media items available to test deletion')
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Delete the media
|
|
389
|
+
mediaAPI.deleteMedia(testMediaId).then((response: any) => {
|
|
390
|
+
mediaAPI.validateSuccessResponse(response, 200)
|
|
391
|
+
expect(response.body.data).to.have.property('success', true)
|
|
392
|
+
expect(response.body.data).to.have.property('id', testMediaId)
|
|
393
|
+
|
|
394
|
+
cy.log(`Soft deleted media: ${testMediaId}`)
|
|
395
|
+
|
|
396
|
+
// Verify soft delete: item should return 404 or status=deleted
|
|
397
|
+
mediaAPI.getMediaById(testMediaId).then((getResponse: any) => {
|
|
398
|
+
// After soft delete, GET should return 404 (filtered out)
|
|
399
|
+
expect(getResponse.status).to.eq(404)
|
|
400
|
+
|
|
401
|
+
cy.log('Verified soft deletion - media no longer accessible')
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
it('MEDIA_API_031: Should return 404 for non-existent media', () => {
|
|
408
|
+
const fakeId = 'non-existent-media-id-12345'
|
|
409
|
+
|
|
410
|
+
mediaAPI.deleteMedia(fakeId).then((response: any) => {
|
|
411
|
+
expect(response.status).to.eq(404)
|
|
412
|
+
expect(response.body).to.have.property('success', false)
|
|
413
|
+
|
|
414
|
+
cy.log('Delete non-existent media returns 404')
|
|
415
|
+
})
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('MEDIA_API_032: Should return 404 when deleting already deleted media', () => {
|
|
419
|
+
cy.then(() => {
|
|
420
|
+
if (!testMediaId) {
|
|
421
|
+
cy.log('No media items available to test')
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Delete once
|
|
426
|
+
mediaAPI.deleteMedia(testMediaId).then((deleteResponse: any) => {
|
|
427
|
+
expect(deleteResponse.status).to.eq(200)
|
|
428
|
+
|
|
429
|
+
// Try to delete again
|
|
430
|
+
mediaAPI.deleteMedia(testMediaId).then((secondDeleteResponse: any) => {
|
|
431
|
+
expect(secondDeleteResponse.status).to.eq(404)
|
|
432
|
+
expect(secondDeleteResponse.body).to.have.property('success', false)
|
|
433
|
+
|
|
434
|
+
cy.log('Second delete attempt correctly returns 404')
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
// ============================================
|
|
442
|
+
// Authentication Tests
|
|
443
|
+
// ============================================
|
|
444
|
+
describe('Authentication', () => {
|
|
445
|
+
it('MEDIA_API_040: Should accept session authentication', () => {
|
|
446
|
+
// Login as superadmin and use session cookies
|
|
447
|
+
cy.login('superadmin@tmt.dev', 'Test1234')
|
|
448
|
+
|
|
449
|
+
cy.request({
|
|
450
|
+
method: 'GET',
|
|
451
|
+
url: `${BASE_URL}/api/v1/media`,
|
|
452
|
+
failOnStatusCode: false
|
|
453
|
+
}).then((response) => {
|
|
454
|
+
expect(response.status).to.eq(200)
|
|
455
|
+
expect(response.body.success).to.be.true
|
|
456
|
+
|
|
457
|
+
cy.log('Session authentication successful')
|
|
458
|
+
})
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
it('MEDIA_API_041: Should accept API key authentication', () => {
|
|
462
|
+
cy.request({
|
|
463
|
+
method: 'GET',
|
|
464
|
+
url: `${BASE_URL}/api/v1/media`,
|
|
465
|
+
headers: {
|
|
466
|
+
'Authorization': `Bearer ${SUPERADMIN_API_KEY}`
|
|
467
|
+
},
|
|
468
|
+
failOnStatusCode: false
|
|
469
|
+
}).then((response) => {
|
|
470
|
+
expect(response.status).to.eq(200)
|
|
471
|
+
expect(response.body.success).to.be.true
|
|
472
|
+
|
|
473
|
+
cy.log('API key authentication successful')
|
|
474
|
+
})
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
it('MEDIA_API_042: Should return 401 without authentication', () => {
|
|
478
|
+
cy.request({
|
|
479
|
+
method: 'GET',
|
|
480
|
+
url: `${BASE_URL}/api/v1/media`,
|
|
481
|
+
failOnStatusCode: false
|
|
482
|
+
}).then((response) => {
|
|
483
|
+
expect(response.status).to.eq(401)
|
|
484
|
+
expect(response.body.success).to.be.false
|
|
485
|
+
|
|
486
|
+
cy.log('No authentication returns 401')
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
// ============================================
|
|
492
|
+
// Edge Cases & Error Handling
|
|
493
|
+
// ============================================
|
|
494
|
+
describe('Edge Cases & Error Handling', () => {
|
|
495
|
+
it('MEDIA_API_050: Should handle invalid limit parameter', () => {
|
|
496
|
+
mediaAPI.getMedia({ limit: -1 }).then((response: any) => {
|
|
497
|
+
// Backend should either reject with 400 or use default limit
|
|
498
|
+
expect(response.status).to.be.oneOf([200, 400])
|
|
499
|
+
|
|
500
|
+
if (response.status === 200) {
|
|
501
|
+
// If accepted, should use default limit (not negative)
|
|
502
|
+
expect(response.body.info.limit).to.be.greaterThan(0)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
cy.log('Invalid limit handled correctly')
|
|
506
|
+
})
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
it('MEDIA_API_051: Should handle invalid offset parameter', () => {
|
|
510
|
+
mediaAPI.getMedia({ offset: -1 }).then((response: any) => {
|
|
511
|
+
// Backend should either reject with 400 or use default offset (0)
|
|
512
|
+
expect(response.status).to.be.oneOf([200, 400])
|
|
513
|
+
|
|
514
|
+
if (response.status === 200) {
|
|
515
|
+
expect(response.body.info.offset).to.be.at.least(0)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
cy.log('Invalid offset handled correctly')
|
|
519
|
+
})
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
it('MEDIA_API_052: Should handle invalid type filter', () => {
|
|
523
|
+
mediaAPI.getMedia({ type: 'invalid-type' }).then((response: any) => {
|
|
524
|
+
// Backend should either reject with 400 or return empty results
|
|
525
|
+
expect(response.status).to.be.oneOf([200, 400])
|
|
526
|
+
|
|
527
|
+
if (response.status === 200) {
|
|
528
|
+
// Should return empty array for invalid type
|
|
529
|
+
expect(response.body.data).to.be.an('array')
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
cy.log('Invalid type filter handled correctly')
|
|
533
|
+
})
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
it('MEDIA_API_053: Should handle invalid orderBy field', () => {
|
|
537
|
+
mediaAPI.getMedia({ orderBy: 'invalid-field' }).then((response: any) => {
|
|
538
|
+
// Backend should either reject with 400 or ignore invalid orderBy
|
|
539
|
+
expect(response.status).to.be.oneOf([200, 400])
|
|
540
|
+
|
|
541
|
+
cy.log('Invalid orderBy handled correctly')
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
// ============================================
|
|
547
|
+
// Integration - Read -> Update -> Delete Lifecycle
|
|
548
|
+
// ============================================
|
|
549
|
+
describe('Integration - Read -> Update -> Delete Lifecycle', () => {
|
|
550
|
+
it('MEDIA_API_100: Should complete lifecycle: Read -> Update -> Delete', () => {
|
|
551
|
+
// Get first available media item
|
|
552
|
+
mediaAPI.getMedia({ limit: 1 }).then((listResponse: any) => {
|
|
553
|
+
if (listResponse.body.data.length === 0) {
|
|
554
|
+
cy.log('No media items available for lifecycle test')
|
|
555
|
+
return
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const mediaId = listResponse.body.data[0].id
|
|
559
|
+
|
|
560
|
+
// 1. READ
|
|
561
|
+
mediaAPI.getMediaById(mediaId).then((readResponse: any) => {
|
|
562
|
+
mediaAPI.validateSuccessResponse(readResponse, 200)
|
|
563
|
+
const originalFilename = readResponse.body.data.filename
|
|
564
|
+
|
|
565
|
+
cy.log(`1. Read media: ${originalFilename}`)
|
|
566
|
+
|
|
567
|
+
// 2. UPDATE
|
|
568
|
+
const updateData = {
|
|
569
|
+
alt: 'Lifecycle Test Alt',
|
|
570
|
+
caption: 'Lifecycle Test Caption'
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
mediaAPI.updateMedia(mediaId, updateData).then((updateResponse: any) => {
|
|
574
|
+
mediaAPI.validateSuccessResponse(updateResponse, 200)
|
|
575
|
+
expect(updateResponse.body.data.alt).to.eq(updateData.alt)
|
|
576
|
+
expect(updateResponse.body.data.caption).to.eq(updateData.caption)
|
|
577
|
+
|
|
578
|
+
cy.log(`2. Updated media metadata`)
|
|
579
|
+
|
|
580
|
+
// 3. DELETE
|
|
581
|
+
mediaAPI.deleteMedia(mediaId).then((deleteResponse: any) => {
|
|
582
|
+
mediaAPI.validateSuccessResponse(deleteResponse, 200)
|
|
583
|
+
expect(deleteResponse.body.data).to.have.property('success', true)
|
|
584
|
+
|
|
585
|
+
cy.log(`3. Deleted media: ${mediaId}`)
|
|
586
|
+
|
|
587
|
+
// 4. VERIFY DELETION
|
|
588
|
+
mediaAPI.getMediaById(mediaId).then((verifyResponse: any) => {
|
|
589
|
+
expect(verifyResponse.status).to.eq(404)
|
|
590
|
+
|
|
591
|
+
cy.log('4. Verified deletion - media no longer accessible')
|
|
592
|
+
cy.log('Full lifecycle completed successfully!')
|
|
593
|
+
})
|
|
594
|
+
})
|
|
595
|
+
})
|
|
596
|
+
})
|
|
597
|
+
})
|
|
598
|
+
})
|
|
599
|
+
})
|
|
600
|
+
})
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MediaAPIController - Controller for interacting with the Media API
|
|
3
|
+
* Encapsulates all CRUD operations for /api/v1/media endpoints
|
|
4
|
+
*
|
|
5
|
+
* Requires:
|
|
6
|
+
* - API Key with media:read, media:write, media:delete scopes (or superadmin with *)
|
|
7
|
+
*/
|
|
8
|
+
const BaseAPIController = require('./BaseAPIController')
|
|
9
|
+
|
|
10
|
+
class MediaAPIController extends BaseAPIController {
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} baseUrl - Base URL for API requests
|
|
13
|
+
* @param {string|null} apiKey - API key for authentication
|
|
14
|
+
*/
|
|
15
|
+
constructor(baseUrl = 'http://localhost:5173', apiKey = null) {
|
|
16
|
+
// Media does not require x-team-id (no team context)
|
|
17
|
+
super(baseUrl, apiKey, null, { slug: 'media' })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ============================================================
|
|
21
|
+
// SEMANTIC ALIASES (for backward compatibility)
|
|
22
|
+
// ============================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* GET /api/v1/media - Get list of media items
|
|
26
|
+
* @param {Object} options - Query options
|
|
27
|
+
* @param {number} [options.limit] - Results per page (default: 20)
|
|
28
|
+
* @param {number} [options.offset] - Offset for pagination (default: 0)
|
|
29
|
+
* @param {string} [options.type] - Filter by type (image, video, document, audio)
|
|
30
|
+
* @param {string} [options.search] - Search in filename
|
|
31
|
+
* @param {string} [options.orderBy] - Sort field (filename, uploadedAt, size, etc.)
|
|
32
|
+
* @param {string} [options.orderDir] - Sort direction (asc, desc)
|
|
33
|
+
* @param {Object} [options.headers] - Additional headers
|
|
34
|
+
* @returns {Cypress.Chainable} Cypress response
|
|
35
|
+
*/
|
|
36
|
+
getMedia(options = {}) {
|
|
37
|
+
return this.list(options)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* GET /api/v1/media/{id} - Get specific media item by ID
|
|
42
|
+
* @param {string} id - Media ID
|
|
43
|
+
* @param {Object} options - Additional options
|
|
44
|
+
* @returns {Cypress.Chainable} Cypress response
|
|
45
|
+
*/
|
|
46
|
+
getMediaById(id, options = {}) {
|
|
47
|
+
return this.getById(id, options)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* PATCH /api/v1/media/{id} - Update media metadata
|
|
52
|
+
* @param {string} id - Media ID
|
|
53
|
+
* @param {Object} updateData - Data to update
|
|
54
|
+
* @param {string} [updateData.alt] - Alt text for image
|
|
55
|
+
* @param {string} [updateData.caption] - Caption for media
|
|
56
|
+
* @param {Object} options - Additional options
|
|
57
|
+
* @returns {Cypress.Chainable} Cypress response
|
|
58
|
+
*/
|
|
59
|
+
updateMedia(id, updateData, options = {}) {
|
|
60
|
+
return this.update(id, updateData, options)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* DELETE /api/v1/media/{id} - Soft delete media item (status -> deleted)
|
|
65
|
+
* @param {string} id - Media ID
|
|
66
|
+
* @param {Object} options - Additional options
|
|
67
|
+
* @returns {Cypress.Chainable} Cypress response
|
|
68
|
+
*/
|
|
69
|
+
deleteMedia(id, options = {}) {
|
|
70
|
+
return this.delete(id, options)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* POST /api/v1/media/upload - Upload file(s)
|
|
75
|
+
* @param {File|File[]} files - File or array of files to upload
|
|
76
|
+
* @param {Object} options - Additional options
|
|
77
|
+
* @returns {Cypress.Chainable} Cypress response
|
|
78
|
+
*/
|
|
79
|
+
uploadMedia(files, options = {}) {
|
|
80
|
+
const { headers = {} } = options
|
|
81
|
+
const formData = new FormData()
|
|
82
|
+
|
|
83
|
+
// Handle single file or array of files
|
|
84
|
+
const fileArray = Array.isArray(files) ? files : [files]
|
|
85
|
+
fileArray.forEach((file) => {
|
|
86
|
+
formData.append('files', file)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Note: For FormData, Content-Type should be auto-set by browser/Cypress
|
|
90
|
+
const uploadHeaders = { ...this.getHeaders(headers) }
|
|
91
|
+
delete uploadHeaders['Content-Type'] // Let Cypress set multipart boundary
|
|
92
|
+
|
|
93
|
+
return cy.request({
|
|
94
|
+
method: 'POST',
|
|
95
|
+
url: `${this.baseUrl}/api/v1/media/upload`,
|
|
96
|
+
headers: uploadHeaders,
|
|
97
|
+
body: formData,
|
|
98
|
+
failOnStatusCode: false
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================================
|
|
103
|
+
// DATA GENERATORS
|
|
104
|
+
// ============================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generate random media data for testing
|
|
108
|
+
* @param {Object} overrides - Specific data to override
|
|
109
|
+
* @returns {Object} Generated media data
|
|
110
|
+
*/
|
|
111
|
+
generateRandomMediaData(overrides = {}) {
|
|
112
|
+
const timestamp = Date.now()
|
|
113
|
+
const randomId = Math.random().toString(36).substring(2, 8)
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
filename: `test-image-${randomId}-${timestamp}.jpg`,
|
|
117
|
+
alt: `Alt text for test image ${randomId}`,
|
|
118
|
+
caption: `Caption for test image ${randomId}`,
|
|
119
|
+
type: 'image',
|
|
120
|
+
size: Math.floor(100000 + Math.random() * 900000), // Random size
|
|
121
|
+
mimeType: 'image/jpeg',
|
|
122
|
+
...overrides
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a test media item and return its data
|
|
128
|
+
* Note: This creates a DB record without actual file upload (for testing)
|
|
129
|
+
* @param {Object} mediaData - Media data (optional)
|
|
130
|
+
* @returns {Cypress.Chainable} Promise resolving with created media data
|
|
131
|
+
*/
|
|
132
|
+
createTestMedia(mediaData = {}) {
|
|
133
|
+
const data = this.generateRandomMediaData(mediaData)
|
|
134
|
+
return this.create(data).then((response) => {
|
|
135
|
+
if (response.status === 201) {
|
|
136
|
+
return response.body.data
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`Failed to create test media: ${response.status}`)
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Clean up a test media item (delete it)
|
|
144
|
+
* @param {string} id - Media ID
|
|
145
|
+
* @returns {Cypress.Chainable} Delete response
|
|
146
|
+
*/
|
|
147
|
+
cleanupTestMedia(id) {
|
|
148
|
+
return this.deleteMedia(id)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ============================================================
|
|
152
|
+
// ENTITY-SPECIFIC VALIDATORS
|
|
153
|
+
// ============================================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Validate media object structure
|
|
157
|
+
* @param {Object} media - Media object
|
|
158
|
+
*/
|
|
159
|
+
validateMediaObject(media) {
|
|
160
|
+
// Base fields
|
|
161
|
+
this.validateBaseEntityFields(media)
|
|
162
|
+
|
|
163
|
+
// Required media fields
|
|
164
|
+
expect(media).to.have.property('filename')
|
|
165
|
+
expect(media.filename).to.be.a('string')
|
|
166
|
+
|
|
167
|
+
expect(media).to.have.property('url')
|
|
168
|
+
expect(media.url).to.be.a('string')
|
|
169
|
+
|
|
170
|
+
expect(media).to.have.property('type')
|
|
171
|
+
expect(media.type).to.be.oneOf(['image', 'video', 'document', 'audio'])
|
|
172
|
+
|
|
173
|
+
expect(media).to.have.property('size')
|
|
174
|
+
expect(media.size).to.be.a('number')
|
|
175
|
+
|
|
176
|
+
expect(media).to.have.property('mimeType')
|
|
177
|
+
expect(media.mimeType).to.be.a('string')
|
|
178
|
+
|
|
179
|
+
expect(media).to.have.property('status')
|
|
180
|
+
expect(media.status).to.be.oneOf(['active', 'deleted'])
|
|
181
|
+
|
|
182
|
+
// Optional fields
|
|
183
|
+
if (media.alt !== null && media.alt !== undefined) {
|
|
184
|
+
expect(media.alt).to.be.a('string')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (media.caption !== null && media.caption !== undefined) {
|
|
188
|
+
expect(media.caption).to.be.a('string')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (media.thumbnailUrl !== null && media.thumbnailUrl !== undefined) {
|
|
192
|
+
expect(media.thumbnailUrl).to.be.a('string')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Timestamps
|
|
196
|
+
expect(media).to.have.property('uploadedAt')
|
|
197
|
+
if (media.uploadedAt) {
|
|
198
|
+
expect(media.uploadedAt).to.be.a('string')
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Validate upload response structure
|
|
204
|
+
* @param {Object} response - Upload response body
|
|
205
|
+
*/
|
|
206
|
+
validateUploadResponse(response) {
|
|
207
|
+
expect(response).to.have.property('success', true)
|
|
208
|
+
expect(response).to.have.property('urls')
|
|
209
|
+
expect(response.urls).to.be.an('array')
|
|
210
|
+
|
|
211
|
+
expect(response).to.have.property('media')
|
|
212
|
+
expect(response.media).to.be.an('array')
|
|
213
|
+
|
|
214
|
+
expect(response).to.have.property('count')
|
|
215
|
+
expect(response.count).to.be.a('number')
|
|
216
|
+
expect(response.count).to.eq(response.urls.length)
|
|
217
|
+
expect(response.count).to.eq(response.media.length)
|
|
218
|
+
|
|
219
|
+
expect(response).to.have.property('storage')
|
|
220
|
+
expect(response.storage).to.have.property('provider')
|
|
221
|
+
expect(response.storage).to.have.property('bucket')
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Export class for use in tests
|
|
226
|
+
module.exports = MediaAPIController
|
|
227
|
+
|
|
228
|
+
// For global use in Cypress
|
|
229
|
+
if (typeof window !== 'undefined') {
|
|
230
|
+
window.MediaAPIController = MediaAPIController
|
|
231
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 NextSpark
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|