Administration of users with pending - #877

This commit is contained in:
Laurent GAY
2025-09-12 23:04:12 +02:00
parent 3a3c452e92
commit e3f3ccd148
10 changed files with 434 additions and 79 deletions

View File

@@ -1,5 +1,89 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`UsersView > Show moderate list 1`] = `
"<div>
<breadcrumbs-nav links="[object Object],[object Object],[object Object]"></breadcrumbs-nav>
<section>
<h2 class="text-lg font-bold mb-3">Details</h2>
<div class="flex flex-col">
<div class="overflow-x-auto">
<div class="inline-block py-2 min-w-full sm:px-2">
<div class="overflow-hidden shadow-md sm:rounded-lg">
<table class="table w-full">
<tbody>
<tr class="border-b">
<td class="py-4 px-2 whitespace-nowrap align-middle">Email</td>
<td class="py-4 px-2 align-middle">truc@mobilizon.test</td>
<td class="py-4 px-2 whitespace-nowrap flex flex flex-col items-start gap-2">
<!--v-if-->
<o-button-stub tag="router-link" variant="text" size="small" iconleft="magnify" rounded="false" expanded="false" disabled="false" outlined="false" loading="false" inverted="false" nativetype="button" role="button" iconboth="false" to="[object Object]"></o-button-stub>
</td>
</tr>
<tr class="border-b">
<td class="py-4 px-2 whitespace-nowrap align-middle">Language</td>
<td class="py-4 px-2 align-middle">French</td>
<td></td>
</tr>
<tr class="border-b">
<td class="py-4 px-2 whitespace-nowrap align-middle">Role</td>
<td class="py-4 px-2 whitespace-nowrap"><span class="bg-orange-100 text-orange-800 text-sm font-medium mr-2 px-2.5 py-0.5 rounded">Pending</span></td>
<td class="py-4 px-2 whitespace-nowrap flex items-center">
<!--v-if-->
</td>
</tr>
<tr class="border-b">
<td class="py-4 px-2 whitespace-nowrap align-middle">Moderation</td>
<td class="py-4 px-2 align-middle">moderation text</td>
<td></td>
</tr>
<tr class="border-b">
<td class="py-4 px-2 whitespace-nowrap align-middle">Confirmed</td>
<td class="py-4 px-2 align-middle">Saturday, August 30, 2025 at 11:56 AM</td>
<td class="py-4 px-2 whitespace-nowrap flex items-center">
<!--v-if-->
</td>
</tr>
<tr class="border-b">
<td class="py-4 px-2 whitespace-nowrap align-middle">Last sign-in</td>
<td class="py-4 px-2 align-middle">Unknown</td>
<td></td>
</tr>
<tr class="border-b">
<td class="py-4 px-2 whitespace-nowrap align-middle">Last IP adress</td>
<td class="py-4 px-2 align-middle">Unknown</td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</section>
<!--v-if-->
<section class="my-4">
<h2 class="text-lg font-bold mb-3">Actions</h2>
<table>
<tr>
<td>
<div class="buttons">
<o-button-stub tag="button" variant="success" rounded="false" expanded="false" disabled="false" outlined="false" loading="false" inverted="false" nativetype="button" role="button" iconboth="false"></o-button-stub>
</div>
</td>
<td>
<div class="buttons">
<o-button-stub tag="button" variant="danger" rounded="false" expanded="false" disabled="false" outlined="false" loading="false" inverted="false" nativetype="button" role="button" iconboth="false"></o-button-stub>
</div>
</td>
</tr>
</table>
</section>
<o-modal-stub active="false" fullscreen="false" width="960" animation="zoom-out" cancelable="escape,x,outside,button" scroll="keep" trapfocus="true" ariarole="dialog" aria-label="Edit user email" destroyonhide="false" autofocus="true" closeicon="close" closeiconsize="medium" teleport="false" events="[object Object]" container="body" close-button-aria-label="Close" aria-modal=""></o-modal-stub>
<o-modal-stub active="false" fullscreen="false" width="960" animation="zoom-out" cancelable="escape,x,outside,button" scroll="keep" trapfocus="true" ariarole="dialog" aria-label="Edit user email" destroyonhide="false" autofocus="true" closeicon="close" closeiconsize="medium" teleport="false" events="[object Object]" container="body" has-modal-card="" close-button-aria-label="Close" aria-modal=""></o-modal-stub>
<o-modal-stub active="false" fullscreen="false" width="960" animation="zoom-out" cancelable="escape,x,outside,button" scroll="keep" trapfocus="true" ariarole="dialog" aria-label="Edit user email" destroyonhide="false" autofocus="true" closeicon="close" closeiconsize="medium" teleport="false" events="[object Object]" container="body" has-modal-card="" close-button-aria-label="Close" aria-modal=""></o-modal-stub>
</div>"
`;
exports[`UsersView > Show simple list 1`] = `
"<div>
<breadcrumbs-nav links="[object Object],[object Object],[object Object]"></breadcrumbs-nav>
@@ -78,9 +162,16 @@ exports[`UsersView > Show simple list 1`] = `
</section>
<section class="my-4">
<h2 class="text-lg font-bold mb-3">Actions</h2>
<div class="buttons">
<o-button-stub tag="button" variant="danger" rounded="false" expanded="false" disabled="false" outlined="false" loading="false" inverted="false" nativetype="button" role="button" iconboth="false"></o-button-stub>
</div>
<table>
<tr>
<!--v-if-->
<td>
<div class="buttons">
<o-button-stub tag="button" variant="danger" rounded="false" expanded="false" disabled="false" outlined="false" loading="false" inverted="false" nativetype="button" role="button" iconboth="false"></o-button-stub>
</div>
</td>
</tr>
</table>
</section>
<o-modal-stub active="false" fullscreen="false" width="960" animation="zoom-out" cancelable="escape,x,outside,button" scroll="keep" trapfocus="true" ariarole="dialog" aria-label="Edit user email" destroyonhide="false" autofocus="true" closeicon="close" closeiconsize="medium" teleport="false" events="[object Object]" container="body" close-button-aria-label="Close" aria-modal=""></o-modal-stub>
<o-modal-stub active="false" fullscreen="false" width="960" animation="zoom-out" cancelable="escape,x,outside,button" scroll="keep" trapfocus="true" ariarole="dialog" aria-label="Edit user email" destroyonhide="false" autofocus="true" closeicon="close" closeiconsize="medium" teleport="false" events="[object Object]" container="body" has-modal-card="" close-button-aria-label="Close" aria-modal=""></o-modal-stub>

View File

@@ -0,0 +1,114 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`UsersView > Show simple list 1`] = `
"<div data-v-01123d3b="">
<breadcrumbs-nav data-v-01123d3b="" links="[object Object],[object Object]"></breadcrumbs-nav>
<div data-v-01123d3b="">
<form data-v-01123d3b="">
<div data-v-01123d3b="" data-oruga="field" class="o-field mb-5">
<!---->
<div class="o-field__body">
<div class="o-field o-field--grouped-multiline o-field--grouped">
<div data-v-01123d3b="" data-oruga="field" class="o-field" expanded=""><label class="o-field__label" for="">Email</label>
<div class="o-field__body">
<div class="o-field o-field--addons">
<div data-v-01123d3b="" data-oruga="input" class="o-input__wrapper"><input trap-focus="" id="" data-oruga-input="text" type="text" class="o-input o-input--iconspace-left" autocomplete="off"><span class="o-icon o-input__icon-left" data-oruga="icon"><i class="mdi mdi-email mdi-24px"></i></span>
<!---->
<!---->
</div>
</div>
</div>
<!---->
</div>
<div data-v-01123d3b="" data-oruga="field" class="o-field" expanded=""><label class="o-field__label" for="">IP Address</label>
<div class="o-field__body">
<div class="o-field o-field--addons">
<div data-v-01123d3b="" data-oruga="input" class="o-input__wrapper"><input id="" data-oruga-input="text" type="text" class="o-input o-input--iconspace-left" autocomplete="off"><span class="o-icon o-input__icon-left" data-oruga="icon"><i class="mdi mdi-web mdi-24px"></i></span>
<!---->
<!---->
</div>
</div>
</div>
<!---->
</div>
<p data-v-01123d3b="" class="control self-end mb-0"><button data-v-01123d3b="" type="submit" class="o-btn o-btn--primary" role="button" data-oruga="button"><span class="o-btn__wrapper"><!----><span class="o-btn__label">Filter</span>
<!----></span>
</button></p>
</div>
</div>
<!---->
</div>
</form>
<div data-v-01123d3b="" class="o-table__root" data-oruga="table">
<div style="display: none;"><span data-v-01123d3b="" data-id="1" data-oruga="table-column"> <!----></span><span data-v-01123d3b="" data-id="2" data-oruga="table-column">Email <!----></span><span data-v-01123d3b="" data-id="3" data-oruga="table-column" centered="true">Last seen on <!----></span><span data-v-01123d3b="" data-id="4" data-oruga="table-column" centered="true">Language <!----></span></div>
<!---->
<!---->
<div class="o-table__wrapper">
<table class="o-table">
<!---->
<thead>
<tr>
<!---->
<!---->
<th class="o-table__th" style="width: 40px;" draggable="false"><span> <span class="o-table__th__sort-icon" style="display: none;"><span class="o-icon o-icon--small" data-oruga="icon"><i class="mdi mdi-arrow-up"></i></span></span></span></th>
<th class="o-table__th" draggable="false"><span>Email <span class="o-table__th__sort-icon" style="display: none;"><span class="o-icon o-icon--small" data-oruga="icon"><i class="mdi mdi-arrow-up"></i></span></span></span></th>
<th class="o-table__th" draggable="false"><span>Last seen on <span class="o-table__th__sort-icon" style="display: none;"><span class="o-icon o-icon--small" data-oruga="icon"><i class="mdi mdi-arrow-up"></i></span></span></span></th>
<th class="o-table__th" draggable="false"><span>Language <span class="o-table__th__sort-icon" style="display: none;"><span class="o-icon o-icon--small" data-oruga="icon"><i class="mdi mdi-arrow-up"></i></span></span></span></th>
<!---->
</tr>
<!---->
<!---->
</thead>
<tbody>
<tr class="" draggable="false">
<!---->
<!---->
<td class="o-table__td" style="width: 40px;">6</td>
<td class="o-table__td" data-label="Email"><a data-v-01123d3b="" href="/settings/admin/users/6" class="">truc@mobilizon.test</a></td>
<td class="o-table__td" data-label="Last seen on"><time data-v-01123d3b="">Thursday, January 1, 1970 at 1:00 AM</time></td>
<td class="o-table__td" data-label="Language">English</td>
<!---->
</tr>
<transition-stub name="slide" appear="false" persisted="false" css="true">
<!---->
</transition-stub>
<!---->
<tr class="" draggable="false">
<!---->
<!---->
<td class="o-table__td" style="width: 40px;">1</td>
<td class="o-table__td" data-label="Email"><a data-v-01123d3b="" href="/settings/admin/users/1" class="">admin@mobilizon.test</a></td>
<td class="o-table__td" data-label="Last seen on"><time data-v-01123d3b="" datetime="2025-09-11T16:10:03Z">Thursday, September 11, 2025 at 6:10 PM</time></td>
<td class="o-table__td" data-label="Language">English</td>
<!---->
</tr>
<transition-stub name="slide" appear="false" persisted="false" css="true">
<!---->
</transition-stub>
<!---->
<!---->
</tbody>
<!---->
</table>
<transition-stub name="fade" appear="false" persisted="false" css="true">
<!---->
</transition-stub>
</div>
<div class="o-table__pagination" per-page="10" total="2" rounded="false" size="small" simple="false" aria-next-label="Next page" aria-previous-label="Previous page" aria-page-label="Page" aria-current-label="Current page">
<div></div>
<div>
<nav class="o-pag o-pag--right o-pag--small" data-oruga="pagination"><button role="button" tabindex="0" class="o-pag__link o-pag__previous o-pag__link--disabled" aria-label="Previous page" aria-current="false"><span class="o-icon" data-oruga="icon" aria-hidden="true"><i class="mdi mdi-chevron-left mdi-24px"></i></span></button><button role="button" tabindex="0" class="o-pag__link o-pag__next o-pag__link--disabled" aria-label="Next page" aria-current="false"><span class="o-icon" data-oruga="icon" aria-hidden="true"><i class="mdi mdi-chevron-right mdi-24px"></i></span></button>
<ul class="o-pag__list">
<!---->
<!---->
<li class="o-pag__item"><button role="button" tabindex="0" class="o-pag__link o-pag__link--current" aria-label="Current page, Page 1." aria-current="true">1</button></li>
<!---->
<!---->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>"
`;

View File

@@ -11,12 +11,15 @@ import {
} from "mock-apollo-client";
import AdminUserProfile from "@/views/Admin/AdminUserProfile.vue";
import { GET_USER } from "@/graphql/user";
import { LANGUAGES_CODES } from "@/graphql/admin";
import { createRouter, createWebHistory, Router } from "vue-router";
import { routes } from "@/router";
import { ADMIN_UPDATE_USER, LANGUAGES_CODES } from "@/graphql/admin";
import { Oruga } from "@oruga-ui/oruga-next";
let router: Router;
import { nullMock } from "../../common";
import {
createRouterMock,
injectRouterMock,
VueRouterMock,
} from "vue-router-mock";
import { SettingsRouteName } from "@/router/settings";
let mockClient: MockApolloClient | null;
let requestHandlers: Record<string, RequestHandler>;
@@ -38,7 +41,7 @@ const languageCodeMock = {
},
};
const getUsersMock = {
const getUserMock = {
data: {
user: {
__typename: "User",
@@ -55,6 +58,7 @@ const getUsersMock = {
url: "http://mobilizon.test/@truc",
},
],
moderation: "moderation text",
confirmedAt: "2025-08-30T09:56:59Z",
currentSignInAt: null,
currentSignInIp: null,
@@ -74,50 +78,126 @@ const getUsersMock = {
},
};
config.global.plugins.push(Oruga);
const generateWrapper = () => {
mockClient = createMockClient({
cache,
resolvers: buildCurrentUserResolver(cache),
});
requestHandlers = {
languagecode: vi.fn().mockResolvedValue(languageCodeMock),
get_users: vi.fn().mockResolvedValue(getUsersMock),
};
mockClient.setRequestHandler(LANGUAGES_CODES, requestHandlers.languagecode);
mockClient.setRequestHandler(GET_USER, requestHandlers.get_users);
const wrapper = shallowMount(AdminUserProfile, {
props: { id: "1234" },
stubs: ["router-link", "router-view"],
global: {
provide: {
[DefaultApolloClient]: mockClient,
const getModerateMock = {
data: {
user: {
__typename: "User",
actors: [
{
__typename: "Person",
avatar: null,
domain: null,
id: "11371",
name: "Truc",
preferredUsername: "truc",
summary: null,
type: "PERSON",
url: "http://mobilizon.test/@truc",
},
],
moderation: "moderation text",
confirmedAt: "2025-08-30T09:56:59Z",
currentSignInAt: null,
currentSignInIp: null,
disabled: false,
email: "truc@mobilizon.test",
id: "1234",
lastSignInAt: "2025-08-28T12:33:03Z",
lastSignInIp: "176.171.166.30",
locale: "fr",
mediaSize: 7093555,
participations: {
__typename: "PaginatedParticipantList",
total: 14,
},
plugins: [router],
role: "PENDING",
},
});
return wrapper;
},
};
describe("UsersView", () => {
beforeEach(async () => {
router = createRouter({
history: createWebHistory(),
routes: routes,
});
config.global.plugins.push(Oruga);
config.plugins.VueWrapper.install(VueRouterMock);
// await router.isReady();
describe("UsersView", () => {
const router = createRouterMock({
spy: {
create: (fn) => vi.fn(fn),
reset: (spy) => spy.mockReset(),
},
});
beforeEach(async () => {
// await router.isReady();
injectRouterMock(router);
});
const generateWrapper = (currentUserMock = getUserMock) => {
mockClient = createMockClient({
cache,
resolvers: buildCurrentUserResolver(cache),
});
requestHandlers = {
languagecode: vi.fn().mockResolvedValue(languageCodeMock),
get_users: vi.fn().mockResolvedValue(currentUserMock),
update_user: vi.fn().mockResolvedValue(nullMock),
};
mockClient.setRequestHandler(LANGUAGES_CODES, requestHandlers.languagecode);
mockClient.setRequestHandler(GET_USER, requestHandlers.get_users);
mockClient.setRequestHandler(
ADMIN_UPDATE_USER,
requestHandlers.update_user
);
const wrapper = shallowMount(AdminUserProfile, {
props: { id: "1234" },
stubs: ["router-link", "router-view"],
global: {
provide: {
[DefaultApolloClient]: mockClient,
},
},
});
return wrapper;
};
it("Show simple list", async () => {
const wrapper = generateWrapper();
await wrapper.vm.$nextTick();
await flushPromises();
expect(wrapper.exists()).toBe(true);
expect(requestHandlers.languagecode).toHaveBeenCalled();
expect(requestHandlers.get_users).toHaveBeenCalled();
expect(wrapper.html()).toMatchSnapshot();
expect(requestHandlers.languagecode).toHaveBeenCalledTimes(1);
expect(requestHandlers.get_users).toHaveBeenCalledTimes(1);
expect(requestHandlers.update_user).toHaveBeenCalledTimes(0);
});
it("Show moderate list", async () => {
const wrapper = generateWrapper(getModerateMock);
expect(wrapper.router).toBe(router);
wrapper.router.push.mockReset();
await wrapper.vm.$nextTick();
await flushPromises();
expect(wrapper.exists()).toBe(true);
expect(wrapper.html()).toMatchSnapshot();
expect(requestHandlers.languagecode).toHaveBeenCalledTimes(0);
expect(requestHandlers.get_users).toHaveBeenCalledTimes(1);
expect(requestHandlers.update_user).toHaveBeenCalledTimes(0);
const btn = wrapper.find('o-button-stub[variant="success"]');
expect(btn.exists()).toBe(true);
btn.trigger("click");
await flushPromises();
expect(requestHandlers.languagecode).toHaveBeenCalledTimes(0);
expect(requestHandlers.get_users).toHaveBeenCalledTimes(1);
expect(requestHandlers.update_user).toHaveBeenCalledTimes(1);
expect(requestHandlers.update_user).toHaveBeenCalledWith({
id: "1234",
notify: true,
role: "USER",
});
await flushPromises();
await flushPromises();
expect(wrapper.router.push).toHaveBeenCalledWith({
name: SettingsRouteName.ADMIN_USER_PROFILE,
id: "1234",
});
});
});

View File

@@ -15,6 +15,7 @@ import { LANGUAGES_CODES } from "@/graphql/admin";
import { createRouter, createWebHistory, Router } from "vue-router";
import { routes } from "@/router";
import { Oruga } from "@oruga-ui/oruga-next";
import { htmlRemoveId } from "../../common";
let router: Router;
@@ -102,14 +103,14 @@ const listUsersMock = {
config.global.plugins.push(Oruga);
const generateWrapper = () => {
const generateWrapper = (currentUsersMock = listUsersMock) => {
mockClient = createMockClient({
cache,
resolvers: buildCurrentUserResolver(cache),
});
requestHandlers = {
languagecode: vi.fn().mockResolvedValue(languageCodeMock),
list_users: vi.fn().mockResolvedValue(listUsersMock),
list_users: vi.fn().mockResolvedValue(currentUsersMock),
};
mockClient.setRequestHandler(LANGUAGES_CODES, requestHandlers.languagecode);
mockClient.setRequestHandler(LIST_USERS, requestHandlers.list_users);
@@ -144,5 +145,6 @@ describe("UsersView", () => {
expect(wrapper.exists()).toBe(true);
expect(requestHandlers.languagecode).toHaveBeenCalled();
expect(requestHandlers.list_users).toHaveBeenCalled();
expect(htmlRemoveId(wrapper.html())).toMatchSnapshot();
});
});