1.1.0 - In this version, the account logout method and the latest changes published by Tuitio were implemented.

master
Tudor Stanciu 2023-03-18 02:24:30 +02:00
parent 67035fbc7e
commit 8e200a1d14
15 changed files with 2656 additions and 2159 deletions

View File

@ -36,3 +36,4 @@ All tests in the package can be executed by running: `npm test`.
1.0.0 - Package initialization
1.0.1 - Added useTuitioClient default options
1.1.0 - In this version, the account logout method and the latest changes published by Tuitio were implemented.

4409
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@flare/tuitio-client-react",
"version": "1.0.1",
"version": "1.1.0",
"description": "Tuitio client react is an npm package written in typescript that facilitates the integration of a react application with Tuitio.",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
@ -48,7 +48,7 @@
"README.md"
],
"dependencies": {
"@flare/tuitio-client": "^1.0.4"
"@flare/tuitio-client": "^1.1.0"
},
"peerDependencies": {
"react": "^16.14.0"

View File

@ -1,35 +1,42 @@
import { useContext } from "react";
import { TuitioContext, TuitioDispatchContext } from "../contexts";
import { TuitioClient, invalidate } from "@flare/tuitio-client";
import type { TuitioAuthenticationResult } from "@flare/tuitio-client";
import { TuitioClient } from "@flare/tuitio-client";
import { TuitioLoginResponse, TuitioLogoutResponse } from "@flare/tuitio-client";
export type TuitioClientHookOptions = {
onLoginSuccess?: (result: TuitioAuthenticationResult, userName: string) => void;
onLoginFailed?: (result: TuitioAuthenticationResult, userName: string) => void;
onLoginSuccess?: (userName: string, token: string, expiresIn: number, validUntil: Date) => void;
onLoginFailed?: (error: string) => void;
onLoginError?: (error: any) => void;
onLogoutSuccess?: (userName: string, logoutDate: Date) => void;
onLogoutFailed?: (error: string) => void;
onLogoutError?: (error: any) => void;
};
const defaultOptions: TuitioClientHookOptions = {
onLoginSuccess: undefined,
onLoginFailed: undefined,
onLoginError: undefined
onLoginError: undefined,
onLogoutSuccess: undefined,
onLogoutFailed: undefined,
onLogoutError: undefined
};
const useTuitioClient = (options: TuitioClientHookOptions = defaultOptions) => {
const state = useContext(TuitioContext);
const dispatchActions = useContext(TuitioDispatchContext);
const { onLoginSuccess, onLoginFailed, onLoginError, onLogoutSuccess, onLogoutFailed, onLogoutError } = options;
const { onLoginSuccess, onLoginFailed, onLoginError } = options;
const login = async (userName: string, password: string) => {
const login = async (userName: string, password: string): Promise<TuitioLoginResponse> => {
try {
const client = new TuitioClient(state.configuration.tuitioUrl);
const response = await client.authenticate(userName, password);
if (response.token) {
dispatchActions.onLoginSuccess(response.token, userName);
onLoginSuccess && onLoginSuccess(response, userName);
const response = await client.login(userName, password);
if (!response.error) {
if (!response.result) throw new Error("Unexpected error: Login result and error are both null.");
dispatchActions.onLoginSuccess(response.result.token, response.result.validUntil, userName);
onLoginSuccess &&
onLoginSuccess(userName, response.result.token, response.result.expiresIn, response.result.validUntil);
} else {
onLoginFailed && onLoginFailed(response, userName);
onLoginFailed && onLoginFailed(response.error);
}
return response;
} catch (error) {
@ -38,9 +45,22 @@ const useTuitioClient = (options: TuitioClientHookOptions = defaultOptions) => {
}
};
const logout = (): void => {
invalidate();
dispatchActions.onLogout();
const logout = async (): Promise<TuitioLogoutResponse> => {
try {
const client = new TuitioClient(state.configuration.tuitioUrl);
const response = await client.logout();
if (!response.error) {
if (!response.result) throw new Error("Unexpected error: Logout result and error are both null.");
dispatchActions.onLogoutSuccess();
onLogoutSuccess && onLogoutSuccess(response.result.userName, response.result.logoutDate);
} else {
onLogoutFailed && onLogoutFailed(response.error);
}
return response;
} catch (error: any) {
onLogoutError && onLogoutError(error);
throw error;
}
};
return { login, logout };

View File

@ -6,12 +6,11 @@ const useTuitioToken = () => {
const token = state.token;
const validate = (): boolean => {
const token = state.token;
if (!token) {
const validUntil = state.validUntil;
if (!validUntil) {
return false;
}
const valid = new Date(token.validUntil) >= new Date();
const valid = new Date(validUntil) >= new Date();
return valid;
};

View File

@ -1,11 +1,12 @@
import { useContext } from "react";
import { TuitioContext } from "../contexts";
// vezi poate il combini cu token: useTuitioAuth
const useTuitioUser = () => {
const state = useContext(TuitioContext);
const userName = state.userName;
const lastLoginDate = state.token?.validFrom;
return { userName, lastLoginDate };
return { userName };
};
export { useTuitioUser };

View File

@ -1,15 +1,17 @@
import { fetch } from "@flare/tuitio-client";
import type { TuitioReactState, TuitioDispatchActions } from "./types";
const { userName, token } = fetch();
const { token, validUntil, userName } = fetch();
export const initialState: TuitioReactState = {
// pune intr-un obiect auth
userName,
token,
validUntil,
configuration: { tuitioUrl: null }
};
export const initialDispatchActions: TuitioDispatchActions = {
onLoginSuccess: () => undefined,
onLogout: () => undefined
onLogoutSuccess: () => undefined
};

View File

@ -1,18 +1,19 @@
import { initialState } from "./initialState";
import type { TuitioReactState, TuitioDispatchActions } from "./types";
import type { TuitioToken } from "@flare/tuitio-client";
import { Dispatch } from "react";
export const reducer = (state: TuitioReactState = initialState, action: any): TuitioReactState => {
switch (action.type) {
case "onLoginSuccess": {
const { token, validUntil, userName } = action;
return {
...state,
token: action.token,
userName: action.userName
token,
validUntil,
userName
};
}
case "onLogout": {
case "onLogoutSuccess": {
const { configuration } = state;
return {
...initialState,
@ -26,6 +27,7 @@ export const reducer = (state: TuitioReactState = initialState, action: any): Tu
};
export const dispatchActions = (dispatch: Dispatch<any>): TuitioDispatchActions => ({
onLoginSuccess: (token: TuitioToken, userName: string): void => dispatch({ type: "onLoginSuccess", token, userName }),
onLogout: (): void => dispatch({ type: "onLogout" })
onLoginSuccess: (token: string, validUntil: Date, userName: string): void =>
dispatch({ type: "onLoginSuccess", token, validUntil, userName }),
onLogoutSuccess: (): void => dispatch({ type: "onLogoutSuccess" })
});

View File

@ -1,8 +1,11 @@
import type { TuitioToken } from "@flare/tuitio-client";
export type TuitioConfiguration = { tuitioUrl: string | null };
export type TuitioReactState = { userName: string; token: TuitioToken | null; configuration: TuitioConfiguration };
export type TuitioDispatchActions = {
onLoginSuccess: (token: TuitioToken, userName: string) => void;
onLogout: () => void;
export type TuitioReactState = {
userName: string | null;
token: string | null;
validUntil: Date | null;
configuration: TuitioConfiguration;
};
export type TuitioDispatchActions = {
onLoginSuccess: (token: string, validUntil: Date, userName: string) => void;
onLogoutSuccess: () => void;
};

View File

@ -1,63 +1,69 @@
import { renderHook, act } from "@testing-library/react-hooks";
import { TuitioClient, invalidate } from "@flare/tuitio-client";
import { TuitioClient } from "@flare/tuitio-client";
import { useTuitioClient } from "../../src/hooks";
const userName = "tuitio.user";
const password = "password";
const token = "mocked-token";
const expiresIn = 600000;
const validUntil = new Date();
jest.mock("@flare/tuitio-client", () => ({
TuitioClient: jest.fn().mockImplementation(() => ({
authenticate: jest.fn().mockResolvedValue({
token: {
raw: "mocked-token",
validFrom: new Date(),
validUntil: new Date()
login: jest.fn().mockResolvedValue({
result: {
token,
expiresIn,
validUntil
},
status: "success"
error: null
}),
logout: jest.fn().mockResolvedValue({
result: {
userId: 0,
userName,
logoutDate: new Date()
},
error: null
})
})),
invalidate: jest.fn(),
fetch: jest.fn().mockResolvedValue({
token: {
raw: "mocked-token",
validFrom: new Date(),
validUntil: new Date()
},
userName: "user"
token,
validUntil,
userName
})
}));
describe("useTuitioClient", () => {
it("should call onLoginSuccess when login is successful", async () => {
it("should call onLoginSuccess when login is successful and should call onLogoutSuccess when logout is called", async () => {
const onLoginSuccess = jest.fn();
const onLogoutSuccess = jest.fn();
const { result } = renderHook(() =>
useTuitioClient({
onLoginSuccess,
onLoginFailed: jest.fn(),
onLoginError: jest.fn()
onLoginError: jest.fn(),
onLogoutSuccess
})
);
await act(async () => {
await result.current.login("user", "password");
await result.current.login(userName, password);
await result.current.logout();
});
expect(onLoginSuccess).toHaveBeenCalledWith(
{
token: {
raw: "mocked-token",
validFrom: expect.any(Date),
validUntil: expect.any(Date)
},
status: "success"
},
"user"
);
expect(onLoginSuccess).toHaveBeenCalledWith(userName, token, expiresIn, validUntil);
expect(onLogoutSuccess).toHaveBeenCalled();
});
it("should call onLoginFailed when login is unsuccessful", async () => {
const error = "error";
const onLoginFailed = jest.fn();
(TuitioClient as jest.Mocked<any>).mockImplementation(() => ({
authenticate: jest.fn().mockResolvedValue({
token: null,
status: "failed"
login: jest.fn().mockResolvedValue({
result: null,
error
})
}));
@ -70,21 +76,15 @@ describe("useTuitioClient", () => {
);
await act(async () => {
await result.current.login("user", "password");
await result.current.login(userName, password);
});
expect(onLoginFailed).toHaveBeenCalledWith(
{
token: null,
status: "failed"
},
"user"
);
expect(onLoginFailed).toHaveBeenCalledWith(error);
});
it("should call onLoginError with error if authentication fails", async () => {
(TuitioClient as jest.Mocked<any>).mockImplementation(() => ({
authenticate: jest.fn().mockRejectedValue(new Error("Login failed"))
login: jest.fn().mockRejectedValue(new Error("Login failed"))
}));
const onLoginError = jest.fn();
@ -94,20 +94,78 @@ describe("useTuitioClient", () => {
await act(async () => {
try {
await result.current.login("user", "password");
await result.current.login(userName, password);
} catch (error) {
expect(onLoginError).toHaveBeenCalledWith(error);
}
});
});
it("should call invalidate when logout is called", () => {
it("should return error if logout returns error", async () => {
(TuitioClient as jest.Mocked<any>).mockImplementation(() => ({
logout: jest.fn().mockResolvedValue({
result: null,
error: "error"
})
}));
const onLogoutFailed = jest.fn();
const { result } = renderHook(() =>
useTuitioClient({
onLogoutFailed
})
);
const response = await result.current.logout();
expect(response.result).toBeNull();
expect(response.error).toBe("error");
expect(onLogoutFailed).toHaveBeenCalledWith(response.error);
});
it("should call onLogoutError with error if logout fails", async () => {
(TuitioClient as jest.Mocked<any>).mockImplementation(() => ({
logout: jest.fn().mockRejectedValue(new Error("Login failed"))
}));
const onLogoutError = jest.fn();
const { result } = renderHook(() => useTuitioClient({ onLogoutError }));
await act(async () => {
try {
await result.current.logout();
} catch (error) {
expect(onLogoutError).toHaveBeenCalledWith(error);
}
});
});
it("should throw exception if bots result and error are null", async () => {
(TuitioClient as jest.Mocked<any>).mockImplementation(() => ({
login: jest.fn().mockResolvedValue({
result: null,
error: null
}),
logout: jest.fn().mockResolvedValue({
result: null,
error: null
})
}));
const { result } = renderHook(() => useTuitioClient());
act(() => {
result.current.logout();
});
await act(async () => {
try {
await result.current.login(userName, password);
} catch (error) {
expect(error).toBeDefined();
}
expect(invalidate).toHaveBeenCalled();
try {
await result.current.logout();
} catch (error) {
expect(error).toBeDefined();
}
});
});
});

View File

@ -2,51 +2,47 @@ import React from "react";
import { renderHook } from "@testing-library/react-hooks";
import { TuitioContext, TuitioDispatchContext } from "../../src/contexts";
import { useTuitioClient } from "../../src/hooks";
import { TuitioToken } from "@flare/tuitio-client";
const userName = "tuitio.user";
const token = "mocked-token";
const expiresIn = 600000;
jest.mock("@flare/tuitio-client", () => ({
TuitioClient: jest.fn().mockImplementation(() => ({
authenticate: jest.fn().mockResolvedValue({
token: {
raw: "mocked-token",
validFrom: new Date(),
validUntil: new Date()
},
status: "success"
login: jest.fn().mockResolvedValue({
result: { token, expiresIn, validUntil: new Date() },
error: null
}),
logout: jest.fn().mockResolvedValue({
result: { userId: 0, userName, logoutDate: new Date() },
error: null
})
})),
invalidate: jest.fn(),
fetch: jest.fn().mockResolvedValue({
token: {
raw: "mocked-token",
validFrom: new Date(),
validUntil: new Date()
},
userName: "user"
token,
validUntil: new Date(),
userName
})
}));
describe("useTuitioClient: dispatchActions", () => {
let spyOnLoginSuccess: (token: TuitioToken, userName: string) => void;
let spyOnLogout: () => void;
let spyOnLoginSuccess: (token: string, validUntil: Date, userName: string) => void;
let spyOnLogoutSuccess: () => void;
beforeEach(() => {
spyOnLoginSuccess = jest.fn();
spyOnLogout = jest.fn();
spyOnLogoutSuccess = jest.fn();
});
afterEach(() => {
(spyOnLoginSuccess as jest.Mock).mockReset();
(spyOnLogout as jest.Mock).mockReset();
(spyOnLogoutSuccess as jest.Mock).mockReset();
});
const tuitioContextState = {
userName: "",
token: {
raw: "mocked-token",
validFrom: new Date(),
validUntil: new Date()
},
token: "mocked-token",
validUntil: new Date(),
configuration: { tuitioUrl: null }
};
@ -56,7 +52,9 @@ describe("useTuitioClient: dispatchActions", () => {
const wrapper = ({ children }: Props) => (
<TuitioContext.Provider value={tuitioContextState}>
<TuitioDispatchContext.Provider value={{ onLoginSuccess: spyOnLoginSuccess, onLogout: spyOnLogout }}>
<TuitioDispatchContext.Provider
value={{ onLoginSuccess: spyOnLoginSuccess, onLogoutSuccess: spyOnLogoutSuccess }}
>
{children}
</TuitioDispatchContext.Provider>
</TuitioContext.Provider>
@ -75,12 +73,12 @@ describe("useTuitioClient: dispatchActions", () => {
const password = "password";
const response = await result.current.login(userName, password);
expect(spyOnLoginSuccess).toHaveBeenCalledWith(response.token, userName);
expect(spyOnLoginSuccess).toHaveBeenCalledWith(response.result?.token, response.result?.validUntil, userName);
});
it("onLogout should be called when user logs out", () => {
it("onLogoutSuccess should be called when user logs out", async () => {
const { result } = renderHook(() => useTuitioClient(optionMock), { wrapper });
result.current.logout();
expect(spyOnLogout).toHaveBeenCalled();
await result.current.logout();
expect(spyOnLogoutSuccess).toHaveBeenCalled();
});
});

View File

@ -6,30 +6,22 @@ import { useTuitioToken } from "../../src/hooks";
describe("useTuitioToken: positive flow", () => {
const testState = {
userName: "test-user",
token: {
raw: "mocked-token",
validFrom: new Date(),
validUntil: new Date()
},
token: "mocked-token",
validUntil: new Date(),
configuration: {
tuitioUrl: null
}
};
testState.token.validFrom.setHours(testState.token.validFrom.getHours() - 1);
testState.token.validUntil.setHours(testState.token.validUntil.getHours() + 1);
testState.validUntil.setHours(testState.validUntil.getHours() + 1);
const wrapper = ({ children }: { children?: React.ReactNode }) => (
<TuitioContext.Provider value={testState}>{children}</TuitioContext.Provider>
);
it("should return the correct token object", () => {
it("should return the correct token", () => {
const { result } = renderHook(() => useTuitioToken(), { wrapper });
expect(result.current.token).toEqual({
raw: "mocked-token",
validFrom: expect.any(Date),
validUntil: expect.any(Date)
});
expect(result.current.token).toEqual("mocked-token");
});
it("should return false value for valid", () => {
@ -48,6 +40,7 @@ describe("useTuitioToken: negative flow", () => {
const testState = {
userName: "test-user",
token: null,
validUntil: null,
configuration: {
tuitioUrl: null
}

View File

@ -4,14 +4,11 @@ import { TuitioContext } from "../../src/contexts";
import { useTuitioUser } from "../../src/hooks";
describe("useTuitioUser: positive flow", () => {
it("should return the userName and lastLoginDate from the TuitioContext", () => {
it("should return the userName from the TuitioContext", () => {
const testState = {
userName: "test-user",
token: {
raw: "mocked-token",
validFrom: new Date(2023, 1, 1),
validUntil: new Date(2023, 12, 31)
},
token: "mocked-token",
validUntil: new Date(2023, 12, 31),
configuration: {
tuitioUrl: null
}
@ -23,26 +20,5 @@ describe("useTuitioUser: positive flow", () => {
const { result } = renderHook(() => useTuitioUser(), { wrapper });
expect(result.current.userName).toEqual("test-user");
expect(result.current.lastLoginDate).toEqual(new Date(2023, 1, 1));
});
});
describe("useTuitioUser: negative flow", () => {
it("should return the userName and lastLoginDate from the TuitioContext", () => {
const testState = {
userName: "test-user",
token: null,
configuration: {
tuitioUrl: null
}
};
const wrapper = ({ children }: { children?: React.ReactNode }) => (
<TuitioContext.Provider value={testState}>{children}</TuitioContext.Provider>
);
const { result } = renderHook(() => useTuitioUser(), { wrapper });
expect(result.current.userName).toEqual("test-user");
expect(result.current.lastLoginDate).toBe(undefined);
});
});

View File

@ -1,28 +1,24 @@
import { initialDispatchActions } from "../src/initialState";
import { TuitioToken } from "@flare/tuitio-client";
describe("Initial dispatch actions", () => {
it("should have a property `onLoginSuccess` that is a function", () => {
expect(initialDispatchActions.onLoginSuccess).toBeInstanceOf(Function);
});
it("should have a property `onLogout` that is a function", () => {
expect(initialDispatchActions.onLogout).toBeInstanceOf(Function);
it("should have a property `onLogoutSuccess` that is a function", () => {
expect(initialDispatchActions.onLogoutSuccess).toBeInstanceOf(Function);
});
it("onLoginSuccess function must return undefined", () => {
const mockToken: TuitioToken = {
raw: "abc123",
validFrom: new Date("2023-01-01T00:00:00.000Z"),
validUntil: new Date("2023-12-31T23:59:59.999Z")
};
const mockToken = "abc123";
const validUntil = new Date("2023-12-31T23:59:59.999Z");
const result = initialDispatchActions.onLoginSuccess(mockToken, "user");
const result = initialDispatchActions.onLoginSuccess(mockToken, validUntil, "user");
expect(result).toBe(undefined);
});
it("onLogout function must return undefined", () => {
const result = initialDispatchActions.onLogout();
it("onLogoutSuccess function must return undefined", () => {
const result = initialDispatchActions.onLogoutSuccess();
expect(result).toBe(undefined);
});
});

View File

@ -1,9 +1,12 @@
import { reducer, dispatchActions } from "../src/reducer";
import { TuitioToken } from "@flare/tuitio-client";
import { TuitioReactState, TuitioDispatchActions } from "../src/types";
import { initialState } from "../src/initialState";
import { Dispatch } from "react";
const userName = "tuitio.user";
const token = "abc123";
const validUntil = new Date();
describe("reducer", () => {
it("should return the initial state by default", () => {
const result = reducer(undefined, { type: "UNKNOWN_ACTION" });
@ -11,14 +14,14 @@ describe("reducer", () => {
});
it('should handle the "onLoginSuccess" action', () => {
const token: TuitioToken = { raw: "abc123", validFrom: new Date(), validUntil: new Date() };
const result = reducer(initialState, { type: "onLoginSuccess", token, userName: "user" });
expect(result).toEqual({ ...initialState, token, userName: "user" });
const result = reducer(initialState, { type: "onLoginSuccess", token, validUntil, userName });
const expected = { ...initialState, token, validUntil, userName };
expect(result).toEqual(expected);
});
it('should handle the "onLogout" action', () => {
it('should handle the "onLogoutSuccess" action', () => {
const state: TuitioReactState = { ...initialState, configuration: { tuitioUrl: "https://test.com/api" } };
const result = reducer(state, { type: "onLogout" });
const result = reducer(state, { type: "onLogoutSuccess" });
expect(result).toEqual({ ...initialState, configuration: state.configuration });
});
});
@ -33,14 +36,12 @@ describe("dispatchActions", () => {
});
it("should dispatch an action with type 'onLoginSuccess' and payload when 'onLoginSuccess' is called", () => {
const token: TuitioToken = { raw: "test", validFrom: new Date(), validUntil: new Date() };
const userName = "test";
actions.onLoginSuccess(token, userName);
expect(dispatch).toHaveBeenCalledWith({ type: "onLoginSuccess", token, userName });
actions.onLoginSuccess(token, validUntil, userName);
expect(dispatch).toHaveBeenCalledWith({ type: "onLoginSuccess", token, validUntil, userName });
});
it("should dispatch an action with type 'onLogout' when 'onLogout' is called", () => {
actions.onLogout();
expect(dispatch).toHaveBeenCalledWith({ type: "onLogout" });
it("should dispatch an action with type 'onLogoutSuccess' when 'onLogoutSuccess' is called", () => {
actions.onLogoutSuccess();
expect(dispatch).toHaveBeenCalledWith({ type: "onLogoutSuccess" });
});
});