@saooti/octopus-sdk 41.7.1 → 41.7.3
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/CHANGELOG.md +17 -1
- package/index.ts +3 -0
- package/package.json +1 -1
- package/src/components/composable/route/useAdvancedParamInit.ts +4 -3
- package/src/components/composable/useRights.ts +196 -0
- package/src/components/display/live/RadioPlanning.vue +6 -4
- package/src/components/display/podcasts/PodcastPresentationList.vue +7 -1
- package/tests/components/composable/useAdvancedParamInit.spec.ts +90 -0
- package/tests/components/composable/useRights.spec.ts +265 -0
- package/tests/utils.ts +12 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
-
## 41.7.
|
|
3
|
+
## 41.7.3 (11/03/2026)
|
|
4
|
+
|
|
5
|
+
**Features**
|
|
6
|
+
|
|
7
|
+
- Intégration du composable de vérification des droits
|
|
8
|
+
|
|
9
|
+
**Fix**
|
|
10
|
+
|
|
11
|
+
- Correction affichage épisodes à valider pour `PODCAST_VALIDATION`.
|
|
12
|
+
|
|
13
|
+
## 41.7.2 (10/03/2026)
|
|
14
|
+
|
|
15
|
+
**Fix**
|
|
16
|
+
|
|
17
|
+
- Correction tri épisodes dans `PodcastPresentationList`
|
|
18
|
+
|
|
19
|
+
## 41.7.1 (10/03/2026)
|
|
4
20
|
|
|
5
21
|
**Fix**
|
|
6
22
|
|
package/index.ts
CHANGED
|
@@ -118,6 +118,7 @@ export const getClassicTagInput = () => import("./src/components/form/ClassicTag
|
|
|
118
118
|
export const getClassicWysiwyg = () => import("./src/components/form/ClassicWysiwyg.vue");
|
|
119
119
|
|
|
120
120
|
//Composable
|
|
121
|
+
import { useRights, EditRight } from "./src/components/composable/useRights.ts";
|
|
121
122
|
import {useResizePhone} from "./src/components/composable/useResizePhone";
|
|
122
123
|
import {useTagOf} from "./src/components/composable/useTagOf.ts";
|
|
123
124
|
import {useSelenium} from "./src/components/composable/useSelenium.ts";
|
|
@@ -202,6 +203,8 @@ import { ROUTE_PARAMS } from "./src/components/composable/route/types";
|
|
|
202
203
|
import { defineAsyncComponent } from "vue";
|
|
203
204
|
|
|
204
205
|
export {
|
|
206
|
+
useRights,
|
|
207
|
+
EditRight,
|
|
205
208
|
useResizePhone,
|
|
206
209
|
useTagOf,
|
|
207
210
|
useSelenium,
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import dayjs from "dayjs";
|
|
|
9
9
|
|
|
10
10
|
import { RouteProps } from "./types";
|
|
11
11
|
import { EmissionGroup, groupsApi } from "../../../api/groupsApi";
|
|
12
|
+
import { useRights } from "../useRights";
|
|
12
13
|
|
|
13
14
|
export const useAdvancedParamInit = (props: RouteProps, isEmission: boolean) => {
|
|
14
15
|
|
|
@@ -18,7 +19,7 @@ export const useAdvancedParamInit = (props: RouteProps, isEmission: boolean) =>
|
|
|
18
19
|
|
|
19
20
|
const filterStore = useFilterStore();
|
|
20
21
|
const authStore = useAuthStore();
|
|
21
|
-
|
|
22
|
+
const { canValidatePodcast } = useRights();
|
|
22
23
|
|
|
23
24
|
const isInit = ref(false);
|
|
24
25
|
const monetisable = ref("UNDEFINED");// UNDEFINED, YES, NO
|
|
@@ -125,8 +126,8 @@ export const useAdvancedParamInit = (props: RouteProps, isEmission: boolean) =>
|
|
|
125
126
|
includeHidden.value = undefined !== organisation.value && organisationRight.value && "false"!==props.routeIncludeHidden;
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
function initValidity(){
|
|
129
|
-
const cantDisplay = isPodcastmaker.value || isEmission || !includeHidden.value || !
|
|
129
|
+
function initValidity() {
|
|
130
|
+
const cantDisplay = isPodcastmaker.value || isEmission || !includeHidden.value || !canValidatePodcast();
|
|
130
131
|
if(cantDisplay){
|
|
131
132
|
validity.value = "true";
|
|
132
133
|
}else{
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { useAuthStore } from "../../stores/AuthStore";
|
|
2
|
+
import type { Emission } from "../../stores/class/general/emission";
|
|
3
|
+
import type { Podcast } from "../../stores/class/general/podcast";
|
|
4
|
+
|
|
5
|
+
type Role =
|
|
6
|
+
'ADMIN'|'ORGANISATION'|
|
|
7
|
+
'PRODUCTION'|'RESTRICTED_PRODUCTION'|'PODCAST_CRUD'|'PODCAST_VALIDATION'|
|
|
8
|
+
'PLAYLISTS'|'RESTRICTED_ANIMATION';
|
|
9
|
+
|
|
10
|
+
export enum EditRight {
|
|
11
|
+
None, // User cannot edit
|
|
12
|
+
Restricted, // User cannot edit because element is used elsewhere
|
|
13
|
+
Full // User can edit
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Composable to manage rights.
|
|
17
|
+
* Based on AuthStore, but converts roles to easily usable tests for various
|
|
18
|
+
* actions.
|
|
19
|
+
*/
|
|
20
|
+
export const useRights = () => {
|
|
21
|
+
const authStore = useAuthStore();
|
|
22
|
+
|
|
23
|
+
function roleContainsAny(...roles: Role[]): boolean {
|
|
24
|
+
return (authStore.authRole as Role[]).findIndex((r: Role) => roles.includes(r)) > -1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Creation is limited by roles
|
|
28
|
+
function canCreateEmission(): boolean {
|
|
29
|
+
return roleContainsAny('ADMIN', 'ORGANISATION', 'PRODUCTION', 'RESTRICTED_PRODUCTION');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function canEditEmission(emission: Emission): boolean {
|
|
33
|
+
if (
|
|
34
|
+
// Can edit new emissions
|
|
35
|
+
(!emission.emissionId && canCreateEmission()) ||
|
|
36
|
+
// Can edit when with sufficient rights
|
|
37
|
+
roleContainsAny('ADMIN', 'ORGANISATION', 'PRODUCTION')
|
|
38
|
+
) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Can only edit if has created the emission
|
|
43
|
+
return (roleContainsAny('RESTRICTED_PRODUCTION') && emission.createdByUserId === authStore.authProfile?.userId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function canDeleteEmission(): boolean {
|
|
47
|
+
// In case of restricted production, it will only delete podcasts
|
|
48
|
+
// created by user, and delete the emission only if empty afterwards
|
|
49
|
+
return roleContainsAny('ADMIN', 'ORGANISATION', 'PRODUCTION', 'RESTRICTED_PRODUCTION');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function canEditCommentsConfigEmission(): boolean {
|
|
53
|
+
return roleContainsAny('ADMIN', 'ORGANISATION', 'PRODUCTION');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function canCreatePodcast(): boolean {
|
|
57
|
+
// All roles that can create podcasts
|
|
58
|
+
return roleContainsAny(
|
|
59
|
+
'ADMIN',
|
|
60
|
+
'ORGANISATION',
|
|
61
|
+
'PRODUCTION',
|
|
62
|
+
'PODCAST_CRUD',
|
|
63
|
+
'RESTRICTED_PRODUCTION',
|
|
64
|
+
'RESTRICTED_ANIMATION'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function canDuplicatePodcast(): boolean {
|
|
69
|
+
// Same as creationm but notably without PODCAST_CRUD and
|
|
70
|
+
// RESTRICTED_ANIMATION
|
|
71
|
+
return roleContainsAny(
|
|
72
|
+
'ADMIN',
|
|
73
|
+
'ORGANISATION',
|
|
74
|
+
'PRODUCTION',
|
|
75
|
+
'RESTRICTED_PRODUCTION',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function canEditPodcast(podcast: Podcast): boolean {
|
|
80
|
+
// Full rights users can edit any podcast
|
|
81
|
+
if (roleContainsAny('ADMIN', 'ORGANISATION', 'PRODUCTION')) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// RESTRICTED users can only edit their own podcasts
|
|
86
|
+
if (roleContainsAny('RESTRICTED_PRODUCTION', 'RESTRICTED_ANIMATION')) {
|
|
87
|
+
return podcast.createdByUserId === authStore.authProfile?.userId;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// PODCAST_CRUD can only edit their own non-valid podcasts
|
|
91
|
+
if (roleContainsAny('PODCAST_CRUD')) {
|
|
92
|
+
return podcast.valid === false &&
|
|
93
|
+
podcast.publisher?.userId === authStore.authProfile?.userId;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function canDeletePodcast(podcast: Podcast): boolean {
|
|
100
|
+
// Same permissions as editing
|
|
101
|
+
return canEditPodcast(podcast);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function canValidatePodcast(): boolean {
|
|
105
|
+
return roleContainsAny('ADMIN', 'ORGANISATION', 'PRODUCTION', 'PODCAST_VALIDATION');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function canEditCommentsConfigPodcast(): boolean {
|
|
109
|
+
return roleContainsAny('ADMIN', 'ORGANISATION', 'PRODUCTION');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function canCreatePlaylist(): boolean {
|
|
113
|
+
return roleContainsAny('ADMIN', 'ORGANISATION', 'PLAYLISTS');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function canEditPlaylist(): boolean {
|
|
117
|
+
return roleContainsAny('ADMIN', 'ORGANISATION', 'PLAYLISTS');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function canDeletePlaylist(): boolean {
|
|
121
|
+
return roleContainsAny('ADMIN', 'ORGANISATION', 'PLAYLISTS');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function canCreateParticipant(): boolean {
|
|
125
|
+
return roleContainsAny('ADMIN', 'ORGANISATION', 'PRODUCTION', 'RESTRICTED_PRODUCTION');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function getParticipantEditRight(participantId: number|undefined): Promise<EditRight> {
|
|
129
|
+
// New participants can be edited, and also with sufficient rights
|
|
130
|
+
if(!participantId || roleContainsAny('ADMIN', 'ORGANISATION', 'PRODUCTION', 'RESTRICTED_PRODUCTION')) {
|
|
131
|
+
return EditRight.Full;
|
|
132
|
+
} else {
|
|
133
|
+
return EditRight.None;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function canEditParticipant(participantId: number|undefined): Promise<boolean> {
|
|
138
|
+
const editRight = await getParticipantEditRight(participantId);
|
|
139
|
+
return editRight === EditRight.Full;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async function canDeleteParticipant(participantId: number|undefined): Promise<boolean> {
|
|
144
|
+
const editRight = await getParticipantEditRight(participantId);
|
|
145
|
+
return editRight === EditRight.Full;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function canEditCodeInsertPlayer(): boolean {
|
|
149
|
+
return roleContainsAny('ADMIN', 'ORGANISATION');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function canEditTranscript(): boolean {
|
|
153
|
+
return roleContainsAny('ADMIN', 'ORGANISATION', 'PRODUCTION');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function canSeeHistory(): boolean {
|
|
157
|
+
return roleContainsAny('ADMIN', 'ORGANISATION');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isRestrictedProduction(): boolean {
|
|
161
|
+
return roleContainsAny('RESTRICTED_PRODUCTION') && !roleContainsAny('ADMIN', 'ORGANISATION', 'PRODUCTION');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
// Emissions
|
|
166
|
+
canCreateEmission,
|
|
167
|
+
canEditEmission,
|
|
168
|
+
canDeleteEmission,
|
|
169
|
+
canEditCommentsConfigEmission,
|
|
170
|
+
|
|
171
|
+
// Podcasts
|
|
172
|
+
canCreatePodcast,
|
|
173
|
+
canDuplicatePodcast,
|
|
174
|
+
canEditPodcast,
|
|
175
|
+
canDeletePodcast,
|
|
176
|
+
canValidatePodcast,
|
|
177
|
+
canEditCommentsConfigPodcast,
|
|
178
|
+
|
|
179
|
+
// Playlists
|
|
180
|
+
canCreatePlaylist,
|
|
181
|
+
canEditPlaylist,
|
|
182
|
+
canDeletePlaylist,
|
|
183
|
+
|
|
184
|
+
// Participants
|
|
185
|
+
canCreateParticipant,
|
|
186
|
+
getParticipantEditRight,
|
|
187
|
+
canEditParticipant,
|
|
188
|
+
canDeleteParticipant,
|
|
189
|
+
|
|
190
|
+
// Other
|
|
191
|
+
canEditCodeInsertPlayer,
|
|
192
|
+
canEditTranscript,
|
|
193
|
+
canSeeHistory,
|
|
194
|
+
isRestrictedProduction
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -254,9 +254,7 @@ function createArrayDays() {
|
|
|
254
254
|
});
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
|
-
async function fetchOccurrencesAndLives(): Promise<
|
|
258
|
-
Array<PlanningOccurrence | PlanningLive>
|
|
259
|
-
> {
|
|
257
|
+
async function fetchOccurrencesAndLives(): Promise<Array<PlanningOccurrence|PlanningLive>> {
|
|
260
258
|
const params = {
|
|
261
259
|
canalId: props.radio?.id,
|
|
262
260
|
from:startOfDay.value,
|
|
@@ -275,6 +273,7 @@ async function fetchOccurrencesAndLives(): Promise<
|
|
|
275
273
|
path: "live/list",
|
|
276
274
|
parameters: params,
|
|
277
275
|
});
|
|
276
|
+
|
|
278
277
|
if (lives.length) {
|
|
279
278
|
occurrences = occurrences.concat(lives);
|
|
280
279
|
occurrences.sort((a, b) => {
|
|
@@ -286,6 +285,7 @@ async function fetchOccurrencesAndLives(): Promise<
|
|
|
286
285
|
}
|
|
287
286
|
return occurrences;
|
|
288
287
|
}
|
|
288
|
+
|
|
289
289
|
async function fetchOccurrences(): Promise<void> {
|
|
290
290
|
if (planning.value[daySelected.value]) {
|
|
291
291
|
return;
|
|
@@ -310,6 +310,7 @@ async function fetchOccurrences(): Promise<void> {
|
|
|
310
310
|
) {
|
|
311
311
|
periodDayIndex += 1;
|
|
312
312
|
}
|
|
313
|
+
|
|
313
314
|
switch (periodOfDay.value[periodDayIndex].id) {
|
|
314
315
|
case "morning":
|
|
315
316
|
planning.value[daySelected.value].morning.push(occ);
|
|
@@ -325,8 +326,9 @@ async function fetchOccurrences(): Promise<void> {
|
|
|
325
326
|
}
|
|
326
327
|
planningLength.value[daySelected.value] += 1;
|
|
327
328
|
}
|
|
328
|
-
} catch {
|
|
329
|
+
} catch(e) {
|
|
329
330
|
error.value = true;
|
|
331
|
+
console.error(e);
|
|
330
332
|
}
|
|
331
333
|
loading.value = false;
|
|
332
334
|
}
|
|
@@ -116,9 +116,15 @@ async function fetchNext(): Promise<void> {
|
|
|
116
116
|
return simplifiedToFull(p, emission.orga, emission);
|
|
117
117
|
})
|
|
118
118
|
);
|
|
119
|
+
|
|
120
|
+
// Sort podcasts by pub date so that the most recent one is focused
|
|
121
|
+
podcasts.value.sort((p1, p2) => {
|
|
122
|
+
return new Date(p2.pubDate).getTime() - new Date(p1.pubDate).getTime();
|
|
123
|
+
});
|
|
124
|
+
|
|
119
125
|
loading.value = false;
|
|
120
126
|
} catch (errorWs) {
|
|
121
|
-
console.
|
|
127
|
+
console.error(errorWs);
|
|
122
128
|
handle403(errorWs as AxiosError);
|
|
123
129
|
error.value = true;
|
|
124
130
|
}
|
|
@@ -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
|
+
});
|
package/tests/utils.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Component } from 'vue';
|
|
2
2
|
import { vi } from 'vitest';
|
|
3
3
|
import { mount as _mount, VueWrapper } from '@vue/test-utils';
|
|
4
|
-
import { type Pinia, setActivePinia } from 'pinia';
|
|
4
|
+
import { type Pinia, setActivePinia, createPinia } from 'pinia';
|
|
5
5
|
import { createTestingPinia } from '@pinia/testing';
|
|
6
6
|
|
|
7
7
|
import { useAuthStore } from '../src/stores/AuthStore';
|
|
@@ -159,3 +159,14 @@ export function combineStoreSetups(...setups: Array<() => void>) {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
export { VueWrapper };
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Creates a Pinia instance without mounting a component.
|
|
165
|
+
* Use this for testing composables or APIs directly.
|
|
166
|
+
*/
|
|
167
|
+
export function setupPinia(setupFn?: () => void | Promise<void>): Pinia {
|
|
168
|
+
const pinia = createPinia();
|
|
169
|
+
setActivePinia(pinia);
|
|
170
|
+
if (setupFn) { setupFn(); }
|
|
171
|
+
return pinia;
|
|
172
|
+
}
|