diff --git a/src/graphql/user.ts b/src/graphql/user.ts index 22e476b1b..9de34aa8e 100644 --- a/src/graphql/user.ts +++ b/src/graphql/user.ts @@ -2,8 +2,18 @@ import gql from "graphql-tag"; import { ACTOR_FRAGMENT } from "./actor"; export const CREATE_USER = gql` - mutation CreateUser($email: String!, $password: String!, $locale: String) { - createUser(email: $email, password: $password, locale: $locale) { + mutation CreateUser( + $email: String! + $password: String! + $moderation: String! + $locale: String + ) { + createUser( + email: $email + password: $password + moderation: $moderation + locale: $locale + ) { email confirmationSentAt } diff --git a/src/i18n/en_US.json b/src/i18n/en_US.json index f6ae1ba5f..3ab645f21 100644 --- a/src/i18n/en_US.json +++ b/src/i18n/en_US.json @@ -976,6 +976,7 @@ "Registration is closed.": "Registration is closed.", "Registration is currently closed.": "Registration is currently closed.", "Registration is moderated, new user must be validated.": "Registration is moderated, new user must be validated.", + "Registration is subject to moderation, indicate your motivation.": "Registration is subject to moderation, indicate your motivation.", "Registrations": "Registrations", "Registrations are restricted by allowlisting.": "Registrations are restricted by allowlisting.", "Reject": "Reject", diff --git a/src/views/User/RegisterView.vue b/src/views/User/RegisterView.vue index f8136f7e8..42d9ddfd6 100644 --- a/src/views/User/RegisterView.vue +++ b/src/views/User/RegisterView.vue @@ -107,7 +107,7 @@ @@ -123,6 +123,28 @@ /> + + + +
{{ t("Login") }}({ email: typeof route.query.email === "string" ? route.query.email : "", password: typeof route.query.password === "string" ? route.query.password : "", + moderation: + typeof route.query.moderation === "string" ? route.query.moderation : "", locale: "en", }); const emailErrors = ref([]); const passwordErrors = ref([]); +const moderationError = ref([]); const sendingForm = ref(false); @@ -298,6 +329,12 @@ onError((error) => { message: message[0] as string, }); break; + case "moderation": + moderationError.value.push({ + type: "danger" as errorType, + message: message[0] as string, + }); + break; default: } } @@ -311,6 +348,7 @@ const submit = async (): Promise => { try { emailErrors.value = []; passwordErrors.value = []; + moderationError.value = []; mutate(credentials); } catch (error: any) { @@ -343,11 +381,14 @@ const maxErrorType = (errors: errorMessage[]): errorType | undefined => { const errorEmailType = computed((): errorType | undefined => { return maxErrorType(emailErrors.value); }); - const errorPasswordType = computed((): errorType | undefined => { return maxErrorType(passwordErrors.value); }); +const errorModerationType = computed((): errorType | undefined => { + return maxErrorType(moderationError.value); +}); + const errorEmailMessage = computed((): string => { return emailErrors.value.map(({ message }) => message).join(" "); }); @@ -355,4 +396,8 @@ const errorEmailMessage = computed((): string => { const errorPasswordMessage = computed((): string => { return passwordErrors.value?.map(({ message }) => message).join(" "); }); + +const errorModerationMessage = computed((): string => { + return moderationError.value?.map(({ message }) => message).join(" "); +}); diff --git a/tests/unit/specs/components/User/RegisterView.spec.ts b/tests/unit/specs/components/User/RegisterView.spec.ts new file mode 100644 index 000000000..6eb618369 --- /dev/null +++ b/tests/unit/specs/components/User/RegisterView.spec.ts @@ -0,0 +1,185 @@ +import { config, mount } from "@vue/test-utils"; +import RegisterView from "@/views/User/RegisterView.vue"; +import { createMockClient, RequestHandler } from "mock-apollo-client"; +import flushPromises from "flush-promises"; +import { configMock } from "../../mocks/config"; +import { CONFIG } from "@/graphql/config"; +import { CREATE_USER } from "@/graphql/user"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { DefaultApolloClient } from "@vue/apollo-composable"; +import Oruga from "@oruga-ui/oruga-next"; +import { + VueRouterMock, + createRouterMock, + injectRouterMock, +} from "vue-router-mock"; +import { nullMock } from "../../common"; + +config.global.plugins.push(Oruga); +config.plugins.VueWrapper.install(VueRouterMock); + +let requestHandlers: Record; + +const generateWrapper = ( + customRegModeration: boolean = false, + customRequestHandlers: Record = {} +) => { + const mockClient = createMockClient(); + + const config_value = { + ...configMock, + }; + if (customRegModeration) { + config_value.data.config.registrationsModeration = true; + } + + requestHandlers = { + configQueryHandler: vi.fn().mockResolvedValue(config_value), + createUserHandler: vi.fn().mockResolvedValue(nullMock), + ...customRequestHandlers, + }; + + mockClient.setRequestHandler(CONFIG, requestHandlers.configQueryHandler); + mockClient.setRequestHandler(CREATE_USER, requestHandlers.createUserHandler); + + return mount(RegisterView, { + global: { + stubs: ["router-link", "router-view"], + provide: { + [DefaultApolloClient]: mockClient, + }, + }, + }); +}; + +describe("Register page", () => { + const router = createRouterMock({ + spy: { + create: (fn) => vi.fn(fn), + reset: (spy) => spy.mockReset(), + }, + }); + beforeEach(() => { + // inject it globally to ensure `useRoute()`, `$route`, etc work + // properly and give you access to test specific functions + injectRouterMock(router); + }); + + it("register without moderation", async () => { + const wrapper = generateWrapper(); + expect(wrapper.router).toBe(router); + await flushPromises(); + expect(wrapper.html()).toMatchSnapshot(); + expect(wrapper.find("form").exists()).toBe(true); + wrapper.find('form input[type="email"]').setValue("some@email.tld"); + wrapper.find('form input[type="password"]').setValue("somepassword"); + wrapper.find("form").trigger("submit"); + await wrapper.vm.$nextTick(); + expect(requestHandlers.createUserHandler).toHaveBeenCalledWith({ + email: "some@email.tld", + locale: "en_US", + moderation: "", + password: "somepassword", + }); + await flushPromises(); + expect(wrapper.find("form").exists()).toBe(false); + }); + + it("shows error without moderation email", async () => { + const wrapper = generateWrapper(false, { + createUserHandler: vi.fn().mockResolvedValue({ + errors: [{ field: "email", message: ["Bad email."] }], + }), + }); + expect(wrapper.find("form").exists()).toBe(true); + wrapper + .findAll('input[type="password"') + .forEach((inputField) => inputField.setValue("my password")); + wrapper.find("form").trigger("submit"); + await wrapper.vm.$nextTick(); + expect(requestHandlers.createUserHandler).toBeCalledTimes(1); + expect(requestHandlers.createUserHandler).toHaveBeenCalledWith({ + email: "", + locale: "en_US", + moderation: "", + password: "my password", + }); + await flushPromises(); + expect(wrapper.find("form").exists()).toBe(true); + expect(wrapper.find(".o-field__message-danger").text()).toContain( + "Bad email." + ); + }); + + it("shows error without moderation password", async () => { + const wrapper = generateWrapper(false, { + createUserHandler: vi.fn().mockResolvedValue({ + errors: [{ field: "password", message: ["Bad password."] }], + }), + }); + expect(wrapper.find("form").exists()).toBe(true); + wrapper + .findAll('input[type="password"') + .forEach((inputField) => inputField.setValue("my password")); + wrapper.find("form").trigger("submit"); + await wrapper.vm.$nextTick(); + expect(requestHandlers.createUserHandler).toBeCalledTimes(1); + expect(requestHandlers.createUserHandler).toHaveBeenCalledWith({ + email: "", + locale: "en_US", + moderation: "", + password: "my password", + }); + await flushPromises(); + expect(wrapper.find("form").exists()).toBe(true); + expect(wrapper.find(".o-field__message-danger").text()).toContain( + "Bad password." + ); + }); + + it("register with moderation", async () => { + const wrapper = generateWrapper(true); + expect(wrapper.router).toBe(router); + await flushPromises(); + expect(wrapper.html()).toMatchSnapshot(); + expect(wrapper.find("form").exists()).toBe(true); + wrapper.find('form input[type="email"]').setValue("some@email.tld"); + wrapper.find('form input[type="password"]').setValue("somepassword"); + wrapper.find("form").trigger("submit"); + await wrapper.vm.$nextTick(); + expect(requestHandlers.createUserHandler).toHaveBeenCalledWith({ + email: "some@email.tld", + locale: "en_US", + moderation: "", + password: "somepassword", + }); + await flushPromises(); + expect(wrapper.find("form").exists()).toBe(false); + }); + + it("shows error with moderation", async () => { + const wrapper = generateWrapper(true, { + createUserHandler: vi.fn().mockResolvedValue({ + errors: [{ field: "moderation", message: ["Bad moderation."] }], + }), + }); + expect(wrapper.find("form").exists()).toBe(true); + wrapper + .findAll('input[type="password"') + .forEach((inputField) => inputField.setValue("my password")); + wrapper.find("form").trigger("submit"); + await wrapper.vm.$nextTick(); + expect(requestHandlers.createUserHandler).toBeCalledTimes(1); + expect(requestHandlers.createUserHandler).toHaveBeenCalledWith({ + email: "", + locale: "en_US", + moderation: "", + password: "my password", + }); + await flushPromises(); + expect(wrapper.find("form").exists()).toBe(true); + expect(wrapper.find(".o-field__message-danger").text()).toContain( + "Bad moderation." + ); + }); +}); diff --git a/tests/unit/specs/components/User/__snapshots__/RegisterView.spec.ts.snap b/tests/unit/specs/components/User/__snapshots__/RegisterView.spec.ts.snap new file mode 100644 index 000000000..067399f39 --- /dev/null +++ b/tests/unit/specs/components/User/__snapshots__/RegisterView.spec.ts.snap @@ -0,0 +1,158 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Register page > register with moderation 1`] = ` +"
+
+

Register an account on Mobilizon!

+

Mobilizon is an instance of the Mobilizon software.

+
+
+
+
+

Why create an account?

+
+
    +
  • To create and manage your events
  • +
  • To create and manage multiples identities from a same account
  • +
  • To register for an event by choosing one of your identities
  • +
  • To create or join an group and start organizing with other people
  • +
  • To follow groups and be informed of their latest events
  • +
+
+
+ +
+
+

About Mobilizon

+
Mobilizon.fr est l'instance Mobilizon de Framasoft.
+

Please read the published by Mobilizon's administrators.

+
+
+
+ +
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+

+

+ + +

+
+ +
+
+
+
" +`; + +exports[`Register page > register without moderation 1`] = ` +"
+
+

Register an account on Mobilizon!

+

Mobilizon is an instance of the Mobilizon software.

+
+
+
+
+

Why create an account?

+
+
    +
  • To create and manage your events
  • +
  • To create and manage multiples identities from a same account
  • +
  • To register for an event by choosing one of your identities
  • +
  • To create or join an group and start organizing with other people
  • +
  • To follow groups and be informed of their latest events
  • +
+
+
+ +
+
+

About Mobilizon

+
Mobilizon.fr est l'instance Mobilizon de Framasoft.
+

Please read the published by Mobilizon's administrators.

+
+
+
+ +
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+ + +
+
+
+ +
+ +
+
+
+

+

+ + +

+
+ +
+
+
+
" +`;