Finished implementing AuthService (Unit/Integration tests, documentation and code)
This commit is contained in:
@@ -1,11 +1,24 @@
|
||||
// Request schemas
|
||||
export interface OtpPleSendCodeReq {
|
||||
unameMailPhone: string;
|
||||
export interface PleSendCodeReq {
|
||||
phoneMail: string;
|
||||
type: number;
|
||||
}
|
||||
|
||||
export interface OtpPleVerifyCodeReq {
|
||||
unameMailPhone: string;
|
||||
export interface OtpSendCodeReq {
|
||||
usernamePhoneMail: string;
|
||||
type: number;
|
||||
}
|
||||
|
||||
export interface PleVerifyCodeReq {
|
||||
phoneMail: string;
|
||||
type: number;
|
||||
code: number;
|
||||
os: string | null;
|
||||
language: string | null;
|
||||
}
|
||||
|
||||
export interface OtpVerifyCodeReq {
|
||||
usernamePhoneMail: string;
|
||||
type: number;
|
||||
code: number;
|
||||
os: string | null;
|
||||
@@ -79,6 +92,10 @@ export interface SignInSuccessResp {
|
||||
userid: string;
|
||||
}
|
||||
|
||||
export interface OtpPleCodeSendTestingResp {
|
||||
code: number | null;
|
||||
}
|
||||
|
||||
// Types
|
||||
export interface AuthMethods {
|
||||
password: boolean;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import {AuthMethods} from "../../domain/authService.schema";
|
||||
import {
|
||||
AuthMethods, FinishPleAccountReq, LoginPasswordAuthReq,
|
||||
OtpPleCodeSendTestingResp,
|
||||
OtpSendCodeReq,
|
||||
OtpVerifyCodeReq, PleVerifyCodeReq, PleVerifyCodeResp, RegisterReq, SignInSuccessResp,
|
||||
UserDataValidationResp, VerifyPasswordResetReq
|
||||
} from "../../domain/authService.schema";
|
||||
import {GenericSuccessBody} from "../../domain/http.schema";
|
||||
|
||||
export const authHandlers = [
|
||||
http.get('*/user/authOptions', () => {
|
||||
@@ -9,4 +16,84 @@ export const authHandlers = [
|
||||
sms: false,
|
||||
})
|
||||
}),
|
||||
|
||||
http.post('*/v2/user/otpSendCode', () => {
|
||||
return HttpResponse.json(<OtpPleCodeSendTestingResp>{
|
||||
code: 5000
|
||||
})
|
||||
}),
|
||||
|
||||
http.post('*/v2/user/otpVerifyCode', async ({request}) => {
|
||||
const body = await request.json() as OtpVerifyCodeReq;
|
||||
return HttpResponse.json(<SignInSuccessResp>{
|
||||
username: "bob",
|
||||
displayName: "Bob",
|
||||
token: "token"
|
||||
}, {status: body.code == 5000 ? 200 : 401})
|
||||
}),
|
||||
|
||||
http.post('*/v2/user/loginPasswordAuth', async ({request}) => {
|
||||
const body = await request.json() as LoginPasswordAuthReq;
|
||||
return HttpResponse.json(<GenericSuccessBody>{
|
||||
response: "success"
|
||||
}, {status: body.password == "password" ? 200 : 401})
|
||||
}),
|
||||
|
||||
http.get('*/v2/user/unameUsage', async ({request}) => {
|
||||
const url = new URL(request.url);
|
||||
return HttpResponse.json(<UserDataValidationResp>{
|
||||
used: url.searchParams.get('username') == "username"
|
||||
})
|
||||
}),
|
||||
|
||||
http.get('*/v2/user/emailUsage', async ({request}) => {
|
||||
const url = new URL(request.url);
|
||||
return HttpResponse.json(<UserDataValidationResp>{
|
||||
used: url.searchParams.get('email') == "taken@example.com"
|
||||
})
|
||||
}),
|
||||
|
||||
http.post('*/v2/user/pleSendVCode', () => {
|
||||
return HttpResponse.json(<OtpPleCodeSendTestingResp>{
|
||||
code: 5000
|
||||
})
|
||||
}),
|
||||
|
||||
http.post('*/v2/user/pleVerifyCode', async ({request}) => {
|
||||
const body = await request.json() as PleVerifyCodeReq;
|
||||
return HttpResponse.json(<GenericSuccessBody>{
|
||||
response: "success"
|
||||
}, {status: body.code == 5000 ? 200 : 401})
|
||||
}),
|
||||
|
||||
http.post('*/v2/user/finishPLEAccount', async ({request}) => {
|
||||
const body = await request.json() as FinishPleAccountReq;
|
||||
return HttpResponse.json(<SignInSuccessResp>{
|
||||
username: body.username,
|
||||
displayName: body.displayName,
|
||||
token: "token"
|
||||
}, {status: body.authCode == "goodAuthCode" ? 200 : 401})
|
||||
}),
|
||||
|
||||
http.post('*/v2/user/register', async ({request}) => {
|
||||
const body = await request.json() as RegisterReq;
|
||||
return HttpResponse.json(<SignInSuccessResp>{
|
||||
username: body.username,
|
||||
displayName: body.displayName,
|
||||
token: "token"
|
||||
}, {status: 200})
|
||||
}),
|
||||
|
||||
http.post('*/user/resetPassword', () => {
|
||||
return HttpResponse.json(<OtpPleCodeSendTestingResp>{
|
||||
code: 5000
|
||||
})
|
||||
}),
|
||||
|
||||
http.post('*/user/verifyResetCode', async ({request}) => {
|
||||
const body = await request.json() as VerifyPasswordResetReq;
|
||||
return HttpResponse.json(<GenericSuccessBody>{
|
||||
response: "success"
|
||||
}, {status: body.vCode == 5000 ? 200 : 401})
|
||||
})
|
||||
]
|
||||
@@ -1,5 +1,7 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {AuthService} from "./authService";
|
||||
import {VerificationTypeEmail} from "../domain/authService.schema";
|
||||
import {faker} from "@faker-js/faker/locale/en";
|
||||
|
||||
describe("AuthService", () => {
|
||||
it("should return authMethods", async () => {
|
||||
@@ -9,4 +11,69 @@ describe("AuthService", () => {
|
||||
expect(methods.email).toBeTruthy()
|
||||
expect(methods.password).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should send OTP code", async () => {
|
||||
const service = new AuthService();
|
||||
const code = await service.otpSendCode("bob@example.com", VerificationTypeEmail)
|
||||
expect(code).toBe(5000)
|
||||
})
|
||||
|
||||
it("should send password reset code", async () => {
|
||||
const service = new AuthService();
|
||||
const code = await service.resetPassword("bob@example.com")
|
||||
expect(code).toBe(5000)
|
||||
})
|
||||
|
||||
it("should send PLE code", async () => {
|
||||
const service = new AuthService();
|
||||
const code = await service.pleSendVCode("bob@example.com", VerificationTypeEmail)
|
||||
expect(code).toBe(5000)
|
||||
})
|
||||
|
||||
it("should throw on wrong OTP code", async () => {
|
||||
const service = new AuthService();
|
||||
await expect(service.otpVerifyCode("bob@example.com", VerificationTypeEmail, 9999)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("should throw on wrong password reset code", async () => {
|
||||
const service = new AuthService();
|
||||
await expect(service.verifyPasswordReset("bob@example.com", 9999, "newPassword")).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("should throw on wrong PLE code", async () => {
|
||||
const service = new AuthService();
|
||||
await expect(service.pleVerifyCode("bob@example.com", VerificationTypeEmail, 9999)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("should throw on wrong password", async () => {
|
||||
const service = new AuthService();
|
||||
await expect(service.loginPasswordAuth("bob@example.com", "wrongPasswd")).rejects.toThrow();
|
||||
})
|
||||
|
||||
it("should detect taken username", async () => {
|
||||
const service = new AuthService();
|
||||
const taken = await service.isUsernameUsed("username")
|
||||
expect(taken).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should detect taken e-mail address", async () => {
|
||||
const service = new AuthService();
|
||||
const taken = await service.isEmailUsed("taken@example.com")
|
||||
expect(taken).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should register passwordless account", async () => {
|
||||
const service = new AuthService();
|
||||
const username = faker.internet.username()
|
||||
const displayName = faker.internet.displayName()
|
||||
const userData = await service.finishPLEAccount(
|
||||
"bob@example.com",
|
||||
"goodAuthCode",
|
||||
username,
|
||||
displayName,
|
||||
)
|
||||
expect(userData.username).toBe(username)
|
||||
expect(userData.displayName).toBe(displayName)
|
||||
expect(userData.token).toBe("token")
|
||||
})
|
||||
})
|
||||
@@ -1,15 +1,20 @@
|
||||
import {getClient} from "../core/http";
|
||||
import {
|
||||
AuthMethods, FinishPleAccountReq,
|
||||
LoginPasswordAuthReq, LoginWithApple, LoginWithGoogleReq,
|
||||
OtpPleSendCodeReq,
|
||||
OtpPleVerifyCodeReq, PleVerifyCodeResp, RegisterReq, ResetPasswordReq, ResetPasswordResp,
|
||||
LoginPasswordAuthReq, LoginWithApple, LoginWithGoogleReq, OtpPleCodeSendTestingResp,
|
||||
OtpSendCodeReq,
|
||||
OtpVerifyCodeReq, PleSendCodeReq,
|
||||
PleVerifyCodeReq, PleVerifyCodeResp, RegisterReq, ResetPasswordReq, ResetPasswordResp,
|
||||
SignInSuccessResp, UserDataValidationResp, VerifyPasswordResetReq
|
||||
} from "../domain/authService.schema";
|
||||
import {isAxiosError} from "axios";
|
||||
import {GenericErrorBody, GenericSuccessBody} from "../domain/http.schema";
|
||||
|
||||
export class AuthService {
|
||||
/**
|
||||
* Fetches the available authentication methods for a user
|
||||
* @param unameMailPhone Username, e-mail address or phone number in international format
|
||||
*/
|
||||
async getAuthMethods(unameMailPhone: string): Promise<AuthMethods> {
|
||||
try {
|
||||
const resp = await getClient().get<AuthMethods>(`user/authOptions?unameMailPhone=${unameMailPhone}`);
|
||||
@@ -22,13 +27,23 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async otpSendCode(unameMailPhone: string, type: number): Promise<void> {
|
||||
/**
|
||||
* Starts the one-time-password sign-in procedure by sending a code to the preferred destination
|
||||
* @param unameMailPhone Username, e-mail address or phone number in international format
|
||||
* @param type Whether to send the code via e-mail or SMS
|
||||
* @returns void in production. Number (the code) is only returned when unit testing with the API in testing mode
|
||||
*/
|
||||
async otpSendCode(unameMailPhone: string, type: number): Promise<void|number> {
|
||||
try {
|
||||
await getClient().post<GenericSuccessBody>("v2/user/otpSendCode", <OtpPleSendCodeReq>{
|
||||
unameMailPhone: unameMailPhone,
|
||||
const resp = await getClient().post<OtpPleCodeSendTestingResp>("v2/user/otpSendCode", <OtpSendCodeReq>{
|
||||
usernamePhoneMail: unameMailPhone,
|
||||
type: type
|
||||
});
|
||||
return
|
||||
if (resp.data.code != null) {
|
||||
return resp.data.code
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
@@ -37,14 +52,20 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async otpVerifyCode(unameMailPhone: string, type: number, code: number): Promise<void> {
|
||||
/**
|
||||
* With the provided code, this registers a new session and returns user data and auth token
|
||||
* @param unameMailPhone Username, e-mail address or phone number in international format
|
||||
* @param type Whether to send the code via e-mail or SMS
|
||||
* @param code The verification code
|
||||
*/
|
||||
async otpVerifyCode(unameMailPhone: string, type: number, code: number): Promise<SignInSuccessResp> {
|
||||
try {
|
||||
await getClient().post<GenericSuccessBody>("v2/user/otpVerifyCode", <OtpPleVerifyCodeReq>{
|
||||
unameMailPhone: unameMailPhone,
|
||||
const resp = await getClient().post<SignInSuccessResp>("v2/user/otpVerifyCode", <OtpVerifyCodeReq>{
|
||||
usernamePhoneMail: unameMailPhone,
|
||||
type: type,
|
||||
code: code
|
||||
});
|
||||
return
|
||||
return resp.data
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
@@ -53,6 +74,11 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs into a Chatenium account via password
|
||||
* @param usernamePhoneMail Username, e-mail address or phone number in international format
|
||||
* @param password The password associated with the account
|
||||
*/
|
||||
async loginPasswordAuth(usernamePhoneMail: string, password: string): Promise<SignInSuccessResp> {
|
||||
try {
|
||||
const resp = await getClient().post<SignInSuccessResp>("v2/user/loginPasswordAuth", <LoginPasswordAuthReq>{
|
||||
@@ -68,6 +94,9 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the provided username is already taken
|
||||
*/
|
||||
async isUsernameUsed(username: string): Promise<Boolean> {
|
||||
try {
|
||||
const resp = await getClient().get<UserDataValidationResp>(`v2/user/unameUsage?username=${username}`);
|
||||
@@ -80,6 +109,9 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the provided e-mail address is already taken
|
||||
*/
|
||||
async isEmailUsed(email: string): Promise<Boolean> {
|
||||
try {
|
||||
const resp = await getClient().get<UserDataValidationResp>(`v2/user/emailUsage?email=${email}`);
|
||||
@@ -92,13 +124,23 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async pleSendVCode(phoneMail: string, type: number): Promise<void> {
|
||||
/**
|
||||
* Starts the passwordless register procedure by sending a code to the preferred destination
|
||||
* @param phoneMail E-mail address or phone number in international format
|
||||
* @param type Whether to send the code via e-mail or SMS
|
||||
* @returns void in production. Number (the code) is only returned when unit testing with the API in testing mode
|
||||
*/
|
||||
async pleSendVCode(phoneMail: string, type: number): Promise<void|number> {
|
||||
try {
|
||||
await getClient().post<UserDataValidationResp>("v2/user/pleSendVCode", <OtpPleSendCodeReq>{
|
||||
unameMailPhone: phoneMail,
|
||||
const resp = await getClient().post<OtpPleCodeSendTestingResp>("v2/user/pleSendVCode", <PleSendCodeReq>{
|
||||
phoneMail: phoneMail,
|
||||
type: type,
|
||||
});
|
||||
return
|
||||
if (resp.data.code == null) {
|
||||
return
|
||||
} else {
|
||||
return resp.data.code
|
||||
}
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
@@ -107,10 +149,16 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if the provided code is valid to continue the procedure
|
||||
* @param phoneMail E-mail address or phone number in international format
|
||||
* @param type Whether to send the code via e-mail or SMS
|
||||
* @param code The code that was sent out
|
||||
*/
|
||||
async pleVerifyCode(phoneMail: string, type: number, code: number): Promise<string> {
|
||||
try {
|
||||
const resp = await getClient().post<PleVerifyCodeResp>("v2/user/pleVerifyCode", <OtpPleVerifyCodeReq>{
|
||||
unameMailPhone: phoneMail,
|
||||
const resp = await getClient().post<PleVerifyCodeResp>("v2/user/pleVerifyCode", <PleVerifyCodeReq>{
|
||||
phoneMail: phoneMail,
|
||||
type: type,
|
||||
code: code
|
||||
});
|
||||
@@ -123,7 +171,14 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async finishPLEAccount(phoneMail: string, type: number, authCode: string, username: string, displayName: string): Promise<SignInSuccessResp> {
|
||||
/**
|
||||
* Finishes the procedure by permanently registering the new account in the database
|
||||
* @param phoneMail E-mail address or phone number in international format
|
||||
* @param authCode Provided in pleVerifyCode
|
||||
* @param username Must be provided by the user
|
||||
* @param displayName Let the user optionally provide it
|
||||
*/
|
||||
async finishPLEAccount(phoneMail: string, authCode: string, username: string, displayName: string|null): Promise<SignInSuccessResp> {
|
||||
try {
|
||||
const resp = await getClient().post<SignInSuccessResp>("v2/user/finishPLEAccount", <FinishPleAccountReq>{
|
||||
phoneMail: phoneMail,
|
||||
@@ -140,6 +195,10 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A social login method via Google
|
||||
* @param code Provided by the Google JavaScript API
|
||||
*/
|
||||
async loginWithGoogle(code: string): Promise<SignInSuccessResp> {
|
||||
try {
|
||||
const resp = await getClient().post<SignInSuccessResp>("user/loginWithGoogle", <LoginWithGoogleReq>{
|
||||
@@ -154,6 +213,10 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A social login method via Apple
|
||||
* @param code Provided by AppleJS
|
||||
*/
|
||||
async loginWithApple(code: string): Promise<SignInSuccessResp> {
|
||||
try {
|
||||
const resp = await getClient().post<SignInSuccessResp>("user/loginWithGoogle", <LoginWithApple>{
|
||||
@@ -169,7 +232,13 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async register(username: string, displayName: string, password: string): Promise<SignInSuccessResp> {
|
||||
/**
|
||||
* A quick way to register a new Chatenium account
|
||||
* @param username Must be provided by the user
|
||||
* @param password Must be provided by the user
|
||||
* @param displayName Let the user optionally provide it
|
||||
*/
|
||||
async register(username: string, password: string, displayName: string|null): Promise<SignInSuccessResp> {
|
||||
try {
|
||||
const resp = await getClient().post<SignInSuccessResp>("v2/user/register", <RegisterReq>{
|
||||
username: username,
|
||||
@@ -185,12 +254,22 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async resetPassword(unameMailPhone: string): Promise<string> {
|
||||
|
||||
/**
|
||||
* Starts the password reset procedure by sending out a code via phone (or via e-mail if SMS is not available)
|
||||
* @param unameMailPhone Username, e-mail address or phone number
|
||||
* @returns void in production. Number (the code) is only returned when unit testing with the API in testing mode
|
||||
*/
|
||||
async resetPassword(unameMailPhone: string): Promise<void|number> {
|
||||
try {
|
||||
const resp = await getClient().post<ResetPasswordResp>("user/resetPassword", <ResetPasswordReq>{
|
||||
const resp = await getClient().post<OtpPleCodeSendTestingResp>("user/resetPassword", <ResetPasswordReq>{
|
||||
unameMailPhone: unameMailPhone,
|
||||
});
|
||||
return resp.data.userid
|
||||
if (resp.data.code != null) {
|
||||
return resp.data.code
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
@@ -199,9 +278,15 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishes the password reset procedure by verifying the code and providing a new password
|
||||
* @param unameMailPhone Username, e-mail address or phone number
|
||||
* @param code The code that was sent out
|
||||
* @param newPassword Must be provided by the user
|
||||
*/
|
||||
async verifyPasswordReset(unameMailPhone: string, code: number, newPassword: string): Promise<void> {
|
||||
try {
|
||||
await getClient().post<GenericSuccessBody>("user/resetPassword", <VerifyPasswordResetReq>{
|
||||
await getClient().post<GenericSuccessBody>("user/verifyResetCode", <VerifyPasswordResetReq>{
|
||||
unameMailPhone: unameMailPhone,
|
||||
vCode: code,
|
||||
newPassword: newPassword,
|
||||
|
||||
6
src/vitest.setup.unit.ts
Normal file
6
src/vitest.setup.unit.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import {beforeAll, afterEach, afterAll, vi} from 'vitest';
|
||||
import {mockServer} from "./mocks/node";
|
||||
|
||||
beforeAll(() => mockServer.listen({onUnhandledRequest: 'error'}));
|
||||
afterEach(() => mockServer.resetHandlers());
|
||||
afterAll(() => mockServer.close());
|
||||
Reference in New Issue
Block a user