@saooti/octopus-sdk 41.7.2 → 41.8.0
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/.claude/settings.local.json +2 -1
- package/CHANGELOG.md +28 -0
- package/index.ts +6 -2
- package/package.json +1 -1
- package/src/api/emissionApi.ts +44 -1
- package/src/api/podcastApi.ts +27 -5
- package/src/components/composable/route/useAdvancedParamInit.ts +4 -3
- package/src/components/composable/useRights.ts +196 -0
- package/src/components/composable/useSeasonsManagement.ts +43 -0
- package/src/components/display/live/RadioPlanning.vue +6 -4
- package/src/components/display/podcasts/PodcastFilterList.vue +100 -18
- package/src/components/display/podcasts/PodcastInlineListTemplate.vue +4 -1
- package/src/components/display/podcasts/PodcastList.vue +5 -1
- package/src/components/display/podcasts/PodcastModuleBox.vue +21 -4
- package/src/components/display/podcasts/PodcastSwiperList.vue +9 -3
- package/src/components/form/ClassicRadio.vue +13 -14
- package/src/components/misc/TopBar.vue +7 -1
- package/src/components/pages/EmissionPage.vue +22 -10
- package/src/locale/de.json +7 -1
- package/src/locale/en.json +7 -1
- package/src/locale/es.json +7 -1
- package/src/locale/fr.json +7 -1
- package/src/locale/it.json +7 -1
- package/src/locale/sl.json +7 -1
- package/src/stores/class/general/emission.ts +20 -0
- package/src/stores/class/general/podcast.ts +17 -0
- package/src/style/general.scss +7 -0
- package/tests/api/podcastApi.spec.ts +43 -0
- package/tests/components/composable/useAdvancedParamInit.spec.ts +90 -0
- package/tests/components/composable/useRights.spec.ts +265 -0
- package/tests/components/composable/useSeasonsManagement.spec.ts +35 -0
- package/tests/components/display/podcasts/PodcastFilterList.spec.ts +33 -0
- package/tests/components/display/podcasts/PodcastInlineListTemplate.spec.ts +23 -0
- package/tests/components/display/podcasts/PodcastModuleBox.spec.ts +49 -22
- package/tests/components/pages/EmissionPage.spec.ts +86 -0
- package/tests/utils.ts +12 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('../../src/api/classicApi', () => ({
|
|
4
|
+
default: {
|
|
5
|
+
fetchData: vi.fn()
|
|
6
|
+
}
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import classicApi from '../../src/api/classicApi';
|
|
10
|
+
import { podcastApi } from '../../src/api/podcastApi';
|
|
11
|
+
|
|
12
|
+
const baseOptions = { organisationId: ['org-1'] };
|
|
13
|
+
|
|
14
|
+
describe('podcastApi', () => {
|
|
15
|
+
describe('count', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.mocked(classicApi.fetchData).mockResolvedValue({ count: 42, result: [], sort: null });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns the count from search results', async () => {
|
|
21
|
+
const result = await podcastApi.count(baseOptions);
|
|
22
|
+
expect(result).toBe(42);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('calls search with size 0', async () => {
|
|
26
|
+
await podcastApi.count(baseOptions);
|
|
27
|
+
expect(classicApi.fetchData).toHaveBeenCalledWith(
|
|
28
|
+
expect.objectContaining({
|
|
29
|
+
parameters: expect.objectContaining({ size: 0 })
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('forwards other options to search', async () => {
|
|
35
|
+
await podcastApi.count({ ...baseOptions, emissionId: 5 });
|
|
36
|
+
expect(classicApi.fetchData).toHaveBeenCalledWith(
|
|
37
|
+
expect.objectContaining({
|
|
38
|
+
parameters: expect.objectContaining({ organisationId: ['org-1'], emissionId: 5 })
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import '@tests/mocks/useRouter';
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
4
|
+
import { defineComponent, nextTick } from 'vue';
|
|
5
|
+
import { mount as _mount } from '@vue/test-utils';
|
|
6
|
+
|
|
7
|
+
import { useAdvancedParamInit } from '@/components/composable/route/useAdvancedParamInit';
|
|
8
|
+
import { state } from '@/stores/ParamSdkStore';
|
|
9
|
+
import { setupPinia, setupAuthStore } from '@tests/utils';
|
|
10
|
+
import type { RouteProps } from '@/components/composable/route/types';
|
|
11
|
+
|
|
12
|
+
vi.mock('@/api/groupsApi', () => ({
|
|
13
|
+
groupsApi: { getAllById: vi.fn().mockResolvedValue({}) }
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
async function setupComposable(
|
|
17
|
+
props: RouteProps,
|
|
18
|
+
isEmission: boolean,
|
|
19
|
+
patchStores?: () => void | Promise<void>
|
|
20
|
+
): Promise<ReturnType<typeof useAdvancedParamInit>> {
|
|
21
|
+
let result!: ReturnType<typeof useAdvancedParamInit>;
|
|
22
|
+
|
|
23
|
+
const pinia = setupPinia();
|
|
24
|
+
await patchStores?.();
|
|
25
|
+
|
|
26
|
+
_mount(defineComponent({
|
|
27
|
+
setup() { result = useAdvancedParamInit(props, isEmission); return {}; },
|
|
28
|
+
template: '<div/>'
|
|
29
|
+
}), { global: { plugins: [pinia] } });
|
|
30
|
+
|
|
31
|
+
await nextTick(); // allow onMounted
|
|
32
|
+
await nextTick(); // allow isInit via nextTick in initAdvancedParams
|
|
33
|
+
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('useAdvancedParamInit', () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
state.generalParameters.podcastmaker = false;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('initValidity', () => {
|
|
43
|
+
const propsWithHidden: RouteProps = { routeValidity: 'false', routeIncludeHidden: 'true' };
|
|
44
|
+
const withOrg = (roles: string[]) => setupAuthStore({ roles, organisationId: 'test-org-id' });
|
|
45
|
+
|
|
46
|
+
describe('forces validity to "true"', () => {
|
|
47
|
+
it('when isEmission is true', async () => {
|
|
48
|
+
const { validity } = await setupComposable(propsWithHidden, true, withOrg(['PRODUCTION']));
|
|
49
|
+
expect(validity.value).toBe('true');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('when isPodcastmaker is true', async () => {
|
|
53
|
+
state.generalParameters.podcastmaker = true;
|
|
54
|
+
const { validity } = await setupComposable(propsWithHidden, false, withOrg(['PRODUCTION']));
|
|
55
|
+
expect(validity.value).toBe('true');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('when includeHidden is false (no org)', async () => {
|
|
59
|
+
// authOrgaId starts undefined → filterOrgaId undefined → organisation undefined → includeHidden false
|
|
60
|
+
const { validity } = await setupComposable(propsWithHidden, false);
|
|
61
|
+
expect(validity.value).toBe('true');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('when routeIncludeHidden is "false"', async () => {
|
|
65
|
+
const { validity } = await setupComposable(
|
|
66
|
+
{ routeValidity: 'false', routeIncludeHidden: 'false' },
|
|
67
|
+
false,
|
|
68
|
+
withOrg(['PRODUCTION'])
|
|
69
|
+
);
|
|
70
|
+
expect(validity.value).toBe('true');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
(['PODCAST_CRUD', 'RESTRICTED_PRODUCTION', 'PLAYLISTS'] as const).forEach(role => {
|
|
74
|
+
it(`when role cannot validate (${role})`, async () => {
|
|
75
|
+
const { validity } = await setupComposable(propsWithHidden, false, withOrg([role]));
|
|
76
|
+
expect(validity.value).toBe('true');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('uses routeValidity when canValidatePodcast() is true', () => {
|
|
82
|
+
(['ADMIN', 'ORGANISATION', 'PRODUCTION', 'PODCAST_VALIDATION'] as const).forEach(role => {
|
|
83
|
+
it(`allows role ${role}`, async () => {
|
|
84
|
+
const { validity } = await setupComposable(propsWithHidden, false, withOrg([role]));
|
|
85
|
+
expect(validity.value).toBe('false');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { setupPinia, setupAuthStore } from '@tests/utils';
|
|
3
|
+
import { useAuthStore } from '@/stores/AuthStore';
|
|
4
|
+
import { useRights } from '@/components/composable/useRights';
|
|
5
|
+
import type { Organisation } from '@/stores/class/general/organisation';
|
|
6
|
+
import type { Emission } from '@/stores/class/general/emission';
|
|
7
|
+
import type { Podcast } from '@/stores/class/general/podcast';
|
|
8
|
+
|
|
9
|
+
async function setup(roles: string[], userId = 'test-user-123'): Promise<void> {
|
|
10
|
+
setupPinia();
|
|
11
|
+
await setupAuthStore({ roles })();
|
|
12
|
+
useAuthStore().$patch({ authProfile: { userId } });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('useRights', () => {
|
|
16
|
+
describe('Emission permissions', () => {
|
|
17
|
+
describe('canCreateEmission', () => {
|
|
18
|
+
['ADMIN', 'ORGANISATION', 'PRODUCTION', 'RESTRICTED_PRODUCTION'].forEach(role => {
|
|
19
|
+
it(`allows ${role}`, async () => {
|
|
20
|
+
await setup([role]);
|
|
21
|
+
expect(useRights().canCreateEmission()).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('denies unrelated roles', async () => {
|
|
26
|
+
await setup(['PLAYLISTS']);
|
|
27
|
+
expect(useRights().canCreateEmission()).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('canEditEmission', () => {
|
|
32
|
+
it('allows ADMIN to edit any emission', async () => {
|
|
33
|
+
await setup(['ADMIN']);
|
|
34
|
+
expect(useRights().canEditEmission({ emissionId: 1, createdByUserId: 'other-user' } as Emission)).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('allows RESTRICTED_PRODUCTION to edit own emission', async () => {
|
|
38
|
+
await setup(['RESTRICTED_PRODUCTION']);
|
|
39
|
+
expect(useRights().canEditEmission({ emissionId: 1, createdByUserId: 'test-user-123' } as Emission)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('denies RESTRICTED_PRODUCTION editing others\' emission', async () => {
|
|
43
|
+
await setup(['RESTRICTED_PRODUCTION']);
|
|
44
|
+
expect(useRights().canEditEmission({ emissionId: 1, createdByUserId: 'other-user' } as Emission)).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('allows editing new emissions if can create', async () => {
|
|
48
|
+
await setup(['PRODUCTION']);
|
|
49
|
+
const newEmission = {
|
|
50
|
+
emissionId: undefined,
|
|
51
|
+
beneficiaries: [],
|
|
52
|
+
description: '',
|
|
53
|
+
monetisable: false,
|
|
54
|
+
name: 'New Emission',
|
|
55
|
+
orga: {} as Organisation,
|
|
56
|
+
rubriqueIds: []
|
|
57
|
+
} as unknown as Emission;
|
|
58
|
+
expect(useRights().canEditEmission(newEmission)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('canDeleteEmission', () => {
|
|
63
|
+
['ADMIN', 'ORGANISATION', 'PRODUCTION', 'RESTRICTED_PRODUCTION'].forEach(role => {
|
|
64
|
+
it(`allows ${role}`, async () => {
|
|
65
|
+
await setup([role]);
|
|
66
|
+
expect(useRights().canDeleteEmission()).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('denies unrelated roles', async () => {
|
|
71
|
+
await setup(['PLAYLISTS']);
|
|
72
|
+
expect(useRights().canDeleteEmission()).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('Podcast permissions', () => {
|
|
78
|
+
describe('canCreatePodcast', () => {
|
|
79
|
+
['ADMIN', 'ORGANISATION', 'PRODUCTION', 'PODCAST_CRUD', 'RESTRICTED_PRODUCTION', 'RESTRICTED_ANIMATION'].forEach(role => {
|
|
80
|
+
it(`allows ${role}`, async () => {
|
|
81
|
+
await setup([role]);
|
|
82
|
+
expect(useRights().canCreatePodcast()).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('denies unrelated roles', async () => {
|
|
87
|
+
await setup(['PLAYLISTS']);
|
|
88
|
+
expect(useRights().canCreatePodcast()).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('canDuplicatePodcast', () => {
|
|
93
|
+
['ADMIN', 'ORGANISATION', 'PRODUCTION', 'RESTRICTED_PRODUCTION'].forEach(role => {
|
|
94
|
+
it(`allows ${role}`, async () => {
|
|
95
|
+
await setup([role]);
|
|
96
|
+
expect(useRights().canDuplicatePodcast()).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('denies unrelated roles', async () => {
|
|
101
|
+
await setup(['PLAYLISTS']);
|
|
102
|
+
expect(useRights().canDuplicatePodcast()).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('canEditPodcast', () => {
|
|
107
|
+
const ownPodcast = { podcastId: 1, createdByUserId: 'test-user-123', valid: true } as Podcast;
|
|
108
|
+
const otherPodcast = { podcastId: 1, createdByUserId: 'other-user', valid: true } as Podcast;
|
|
109
|
+
|
|
110
|
+
['ADMIN', 'ORGANISATION', 'PRODUCTION'].forEach(role => {
|
|
111
|
+
it(`allows ${role} to edit any podcast`, async () => {
|
|
112
|
+
await setup([role]);
|
|
113
|
+
expect(useRights().canEditPodcast(otherPodcast)).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('allows PODCAST_CRUD to edit own non-valid podcast', async () => {
|
|
118
|
+
await setup(['PODCAST_CRUD']);
|
|
119
|
+
const podcast = { podcastId: 1, valid: false, publisher: { userId: 'test-user-123' } } as Podcast;
|
|
120
|
+
expect(useRights().canEditPodcast(podcast)).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('denies PODCAST_CRUD editing own valid podcast', async () => {
|
|
124
|
+
await setup(['PODCAST_CRUD']);
|
|
125
|
+
const podcast = { podcastId: 1, valid: true, publisher: { userId: 'test-user-123' } } as Podcast;
|
|
126
|
+
expect(useRights().canEditPodcast(podcast)).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('denies PODCAST_CRUD editing others\' podcast', async () => {
|
|
130
|
+
await setup(['PODCAST_CRUD']);
|
|
131
|
+
const podcast = { podcastId: 1, valid: false, publisher: { userId: 'other-user' } } as Podcast;
|
|
132
|
+
expect(useRights().canEditPodcast(podcast)).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
['RESTRICTED_PRODUCTION', 'RESTRICTED_ANIMATION'].forEach(role => {
|
|
136
|
+
it(`allows ${role} to edit own podcast`, async () => {
|
|
137
|
+
await setup([role]);
|
|
138
|
+
expect(useRights().canEditPodcast(ownPodcast)).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it(`denies ${role} editing others' podcast`, async () => {
|
|
142
|
+
await setup([role]);
|
|
143
|
+
expect(useRights().canEditPodcast(otherPodcast)).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('denies unrelated roles', async () => {
|
|
148
|
+
await setup(['PLAYLISTS']);
|
|
149
|
+
expect(useRights().canEditPodcast(ownPodcast)).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Regression: RESTRICTED_* must take priority over PODCAST_CRUD when both roles are present.
|
|
153
|
+
['RESTRICTED_PRODUCTION', 'RESTRICTED_ANIMATION'].forEach(role => {
|
|
154
|
+
it(`allows ${role} + PODCAST_CRUD to edit own valid podcast`, async () => {
|
|
155
|
+
await setup([role, 'PODCAST_CRUD']);
|
|
156
|
+
const podcast = { podcastId: 1, createdByUserId: 'test-user-123', valid: true, publisher: { userId: 'test-user-123' } } as Podcast;
|
|
157
|
+
expect(useRights().canEditPodcast(podcast)).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('denies RESTRICTED_PRODUCTION + PODCAST_CRUD editing others\' valid podcast', async () => {
|
|
162
|
+
await setup(['RESTRICTED_PRODUCTION', 'PODCAST_CRUD']);
|
|
163
|
+
const podcast = { podcastId: 1, createdByUserId: 'other-user', valid: true, publisher: { userId: 'other-user' } } as Podcast;
|
|
164
|
+
expect(useRights().canEditPodcast(podcast)).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('canDeletePodcast', () => {
|
|
169
|
+
it('delegates to canEditPodcast', async () => {
|
|
170
|
+
await setup(['RESTRICTED_PRODUCTION']);
|
|
171
|
+
expect(useRights().canDeletePodcast({ podcastId: 1, createdByUserId: 'test-user-123' } as Podcast)).toBe(true);
|
|
172
|
+
expect(useRights().canDeletePodcast({ podcastId: 1, createdByUserId: 'other-user' } as Podcast)).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('canValidatePodcast', () => {
|
|
177
|
+
['ADMIN', 'ORGANISATION', 'PRODUCTION', 'PODCAST_VALIDATION'].forEach(role => {
|
|
178
|
+
it(`allows ${role}`, async () => {
|
|
179
|
+
await setup([role]);
|
|
180
|
+
expect(useRights().canValidatePodcast()).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
['PODCAST_CRUD', 'RESTRICTED_PRODUCTION', 'PLAYLISTS'].forEach(role => {
|
|
185
|
+
it(`denies ${role}`, async () => {
|
|
186
|
+
await setup([role]);
|
|
187
|
+
expect(useRights().canValidatePodcast()).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('Playlist permissions', () => {
|
|
194
|
+
['canCreatePlaylist', 'canEditPlaylist', 'canDeletePlaylist'].forEach(method => {
|
|
195
|
+
describe(method, () => {
|
|
196
|
+
['ADMIN', 'ORGANISATION', 'PLAYLISTS'].forEach(role => {
|
|
197
|
+
it(`allows ${role}`, async () => {
|
|
198
|
+
await setup([role]);
|
|
199
|
+
expect(useRights()[method as 'canCreatePlaylist']()).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('denies unrelated roles', async () => {
|
|
204
|
+
await setup(['PRODUCTION']);
|
|
205
|
+
expect(useRights()[method as 'canCreatePlaylist']()).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('Participant permissions', () => {
|
|
212
|
+
describe('canCreateParticipant', () => {
|
|
213
|
+
['ADMIN', 'ORGANISATION', 'PRODUCTION', 'RESTRICTED_PRODUCTION'].forEach(role => {
|
|
214
|
+
it(`allows ${role}`, async () => {
|
|
215
|
+
await setup([role]);
|
|
216
|
+
expect(useRights().canCreateParticipant()).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('denies unrelated roles', async () => {
|
|
221
|
+
await setup(['PLAYLISTS']);
|
|
222
|
+
expect(useRights().canCreateParticipant()).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
['canEditParticipant', 'canDeleteParticipant'].forEach(method => {
|
|
227
|
+
describe(method, () => {
|
|
228
|
+
['ADMIN', 'ORGANISATION', 'PRODUCTION', 'RESTRICTED_PRODUCTION'].forEach(role => {
|
|
229
|
+
it(`allows ${role}`, async () => {
|
|
230
|
+
await setup([role]);
|
|
231
|
+
expect(await useRights()[method as 'canEditParticipant'](123)).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('allows undefined id (new participant)', async () => {
|
|
236
|
+
await setup(['RESTRICTED_PRODUCTION']);
|
|
237
|
+
expect(await useRights()[method as 'canEditParticipant'](undefined)).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('denies unrelated roles for existing participant', async () => {
|
|
241
|
+
await setup(['PLAYLISTS']);
|
|
242
|
+
expect(await useRights()[method as 'canEditParticipant'](123)).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('Other permissions', () => {
|
|
249
|
+
describe('canEditCodeInsertPlayer', () => {
|
|
250
|
+
['ADMIN', 'ORGANISATION'].forEach(role => {
|
|
251
|
+
it(`allows ${role}`, async () => {
|
|
252
|
+
await setup([role]);
|
|
253
|
+
expect(useRights().canEditCodeInsertPlayer()).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
['PRODUCTION', 'PODCAST_CRUD', 'RESTRICTED_PRODUCTION', 'PLAYLISTS'].forEach(role => {
|
|
258
|
+
it(`denies ${role}`, async () => {
|
|
259
|
+
await setup([role]);
|
|
260
|
+
expect(useRights().canEditCodeInsertPlayer()).toBe(false);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { useSeasonsManagement } from '@/components/composable/useSeasonsManagement';
|
|
3
|
+
import { emptyEmissionData, SeasonMode } from '@/stores/class/general/emission';
|
|
4
|
+
import { emptyPodcastData } from '@/stores/class/general/podcast';
|
|
5
|
+
|
|
6
|
+
const { areSeasonsEnabled, formatSeason } = useSeasonsManagement();
|
|
7
|
+
|
|
8
|
+
describe('useSeasonsManagement', () => {
|
|
9
|
+
describe('areSeasonsEnabled', () => {
|
|
10
|
+
it.each([
|
|
11
|
+
SeasonMode.SEASON_WITH_PODCAST_NUMBERING,
|
|
12
|
+
SeasonMode.SEASON_WITHOUT_PODCAST_NUMBERING,
|
|
13
|
+
])('returns true for %s', (seasonMode) => {
|
|
14
|
+
expect(areSeasonsEnabled({ ...emptyEmissionData(), seasonMode })).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns false for NO_SEASON', () => {
|
|
18
|
+
expect(areSeasonsEnabled(emptyEmissionData())).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('formatSeason', () => {
|
|
23
|
+
it.each([
|
|
24
|
+
{ seasonMode: SeasonMode.NO_SEASON, seasonNumber: 1, seasonEpisodeNumber: 1, expected: null },
|
|
25
|
+
{ seasonMode: SeasonMode.SEASON_WITHOUT_PODCAST_NUMBERING, seasonNumber: 2, seasonEpisodeNumber: 1, expected: 'S2' },
|
|
26
|
+
{ seasonMode: SeasonMode.SEASON_WITH_PODCAST_NUMBERING, seasonNumber: 2, seasonEpisodeNumber: 5, expected: 'S2·E5' },
|
|
27
|
+
])('formats $seasonMode as $expected', ({ seasonMode, seasonNumber, seasonEpisodeNumber, expected }) => {
|
|
28
|
+
const podcast = emptyPodcastData();
|
|
29
|
+
podcast.seasonNumber = seasonNumber;
|
|
30
|
+
podcast.seasonEpisodeNumber = seasonEpisodeNumber;
|
|
31
|
+
podcast.emission.seasonMode = seasonMode;
|
|
32
|
+
expect(formatSeason(podcast)).toBe(expected);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import '@tests/mocks/i18n';
|
|
2
|
+
|
|
3
|
+
import PodcastFilterList from '@/components/display/podcasts/PodcastFilterList.vue';
|
|
4
|
+
import { emptyEmissionData, SeasonMode } from '@/stores/class/general/emission';
|
|
5
|
+
import { mount as testMount } from '@tests/utils';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const mount = (props: Record<string, unknown> = {}) =>
|
|
9
|
+
testMount(PodcastFilterList, { shallow: true, props });
|
|
10
|
+
|
|
11
|
+
describe('PodcastFilterList', () => {
|
|
12
|
+
describe('season display', () => {
|
|
13
|
+
it.each([
|
|
14
|
+
{ desc: 'no emission provided', props: {} },
|
|
15
|
+
{ desc: 'emission has NO_SEASON mode', props: { emission: { ...emptyEmissionData(), seasonMode: SeasonMode.NO_SEASON, seasonCount: 3 } } },
|
|
16
|
+
{ desc: 'emission has seasons but seasonCount is 0', props: { emission: { ...emptyEmissionData(), seasonMode: SeasonMode.SEASON_WITH_PODCAST_NUMBERING, seasonCount: 0 } } },
|
|
17
|
+
])('shows a plain list when $desc', async ({ props }) => {
|
|
18
|
+
const wrapper = await mount(props);
|
|
19
|
+
expect(wrapper.find('podcast-list-stub').exists()).toBe(true);
|
|
20
|
+
expect(wrapper.find('classic-nav-stub').exists()).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it.each([
|
|
24
|
+
SeasonMode.SEASON_WITH_PODCAST_NUMBERING,
|
|
25
|
+
SeasonMode.SEASON_WITHOUT_PODCAST_NUMBERING,
|
|
26
|
+
])('shows a season nav when seasonMode is %s', async (seasonMode) => {
|
|
27
|
+
const emission = { ...emptyEmissionData(), seasonMode, seasonCount: 3 };
|
|
28
|
+
const wrapper = await mount({ emission });
|
|
29
|
+
expect(wrapper.find('classic-nav-stub').exists()).toBe(true);
|
|
30
|
+
expect(wrapper.find('podcast-list-stub').exists()).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import '@tests/mocks/i18n';
|
|
2
|
+
import '@tests/mocks/useRouter';
|
|
3
|
+
|
|
4
|
+
import PodcastInlineListTemplate from '@/components/display/podcasts/PodcastInlineListTemplate.vue';
|
|
5
|
+
import { mount as testMount } from '@tests/utils';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const mount = (props: Record<string, unknown> = {}) =>
|
|
9
|
+
testMount(PodcastInlineListTemplate, { shallow: true, props });
|
|
10
|
+
|
|
11
|
+
describe('PodcastInlineListTemplate', () => {
|
|
12
|
+
describe('noSort prop', () => {
|
|
13
|
+
it('shows sort buttons by default', async () => {
|
|
14
|
+
const wrapper = await mount();
|
|
15
|
+
expect(wrapper.find('.btn-underline').exists()).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('hides sort buttons when noSort is true', async () => {
|
|
19
|
+
const wrapper = await mount({ noSort: true });
|
|
20
|
+
expect(wrapper.find('.btn-underline').exists()).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -2,47 +2,74 @@ import '@tests/mocks/i18n';
|
|
|
2
2
|
import '@tests/mocks/useRouter';
|
|
3
3
|
|
|
4
4
|
import PodcastModuleBox from '@/components/display/podcasts/PodcastModuleBox.vue';
|
|
5
|
+
import { SeasonMode } from '@/stores/class/general/emission';
|
|
5
6
|
import { emptyPodcastData, Podcast } from '@/stores/class/general/podcast';
|
|
6
|
-
import { mount, setupAuthStore } from '@tests/utils';
|
|
7
|
+
import { mount as testMount, setupAuthStore } from '@tests/utils';
|
|
7
8
|
import { describe, expect, it } from 'vitest';
|
|
8
9
|
import { initialize } from '@/stores/ParamSdkStore';
|
|
9
10
|
|
|
11
|
+
const mount = (podcast: Podcast) => testMount(PodcastModuleBox, {
|
|
12
|
+
props: { podcast },
|
|
13
|
+
stubs: ['ShareAnonymous', 'LikeSection'],
|
|
14
|
+
beforeMount: setupAuthStore()
|
|
15
|
+
});
|
|
16
|
+
|
|
10
17
|
describe('PodcastModuleBox', () => {
|
|
11
18
|
describe('date display', () => {
|
|
12
19
|
const podcast: Podcast = emptyPodcastData();
|
|
13
20
|
podcast.pubDate = '2025-12-01T10:21:31.000+00:00';
|
|
14
21
|
|
|
15
22
|
it('shows the date without time by default', async() => {
|
|
16
|
-
const wrapper = await mount(
|
|
17
|
-
props: { podcast },
|
|
18
|
-
stubs: ['ShareAnonymous', 'LikeSection'],
|
|
19
|
-
beforeMount: setupAuthStore()
|
|
20
|
-
});
|
|
23
|
+
const wrapper = await mount(podcast);
|
|
21
24
|
expect(wrapper.text()).toContain('1 December 2025');
|
|
22
25
|
expect(wrapper.text()).not.toContain('11:21');
|
|
23
26
|
});
|
|
24
27
|
|
|
25
|
-
it('shows the date
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
stubs: ['ShareAnonymous', 'LikeSection'],
|
|
29
|
-
beforeMount: setupAuthStore()
|
|
30
|
-
});
|
|
28
|
+
it('shows the date with time when enabled in SdkParams', async() => {
|
|
29
|
+
initialize({ generalParameters: { showTimeWithDates: true } });
|
|
30
|
+
const wrapper = await mount(podcast);
|
|
31
31
|
expect(wrapper.text()).toContain('1 December 2025');
|
|
32
|
-
expect(wrapper.text()).
|
|
32
|
+
expect(wrapper.text()).toContain('11:21');
|
|
33
33
|
});
|
|
34
|
+
});
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
describe('season info', () => {
|
|
37
|
+
function makePodcast(seasonMode = SeasonMode.NO_SEASON, overrides: Partial<Podcast> = {}) {
|
|
38
|
+
const podcast = emptyPodcastData();
|
|
39
|
+
podcast.emission.seasonMode = seasonMode;
|
|
40
|
+
return { ...podcast, ...overrides };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('season number', () => {
|
|
44
|
+
it('hidden when seasonNumber is not set', async () => {
|
|
45
|
+
const wrapper = await mount(makePodcast(SeasonMode.SEASON_WITH_PODCAST_NUMBERING));
|
|
46
|
+
expect(wrapper.text()).not.toContain('Podcast - Season');
|
|
38
47
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
|
|
49
|
+
it('hidden when emission has no season mode', async () => {
|
|
50
|
+
const wrapper = await mount(makePodcast(SeasonMode.NO_SEASON, { seasonNumber: 2 }));
|
|
51
|
+
expect(wrapper.text()).not.toContain('Podcast - Season');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it.each([
|
|
55
|
+
SeasonMode.SEASON_WITH_PODCAST_NUMBERING,
|
|
56
|
+
SeasonMode.SEASON_WITHOUT_PODCAST_NUMBERING,
|
|
57
|
+
])('shown for %s', async (seasonMode) => {
|
|
58
|
+
const wrapper = await mount(makePodcast(seasonMode, { seasonNumber: 2 }));
|
|
59
|
+
expect(wrapper.text()).toContain('Podcast - Season : 2');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('episode number', () => {
|
|
64
|
+
it('hidden for SEASON_WITHOUT_PODCAST_NUMBERING', async () => {
|
|
65
|
+
const wrapper = await mount(makePodcast(SeasonMode.SEASON_WITHOUT_PODCAST_NUMBERING, { seasonEpisodeNumber: 5 }));
|
|
66
|
+
expect(wrapper.text()).not.toContain('Podcast - Episode number');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('shown for SEASON_WITH_PODCAST_NUMBERING', async () => {
|
|
70
|
+
const wrapper = await mount(makePodcast(SeasonMode.SEASON_WITH_PODCAST_NUMBERING, { seasonEpisodeNumber: 5 }));
|
|
71
|
+
expect(wrapper.text()).toContain('Podcast - Episode number : 5');
|
|
43
72
|
});
|
|
44
|
-
expect(wrapper.text()).toContain('1 December 2025');
|
|
45
|
-
expect(wrapper.text()).toContain('11:21');
|
|
46
73
|
});
|
|
47
74
|
});
|
|
48
75
|
});
|