1.1.0 - In this version, the account logout method and the latest changes published by Tuitio were implemented.
parent
e788ecbe63
commit
3443fbe685
|
@ -14,15 +14,15 @@ The package installation can be done in two ways:
|
||||||
## How to use the package
|
## How to use the package
|
||||||
|
|
||||||
```javascript!
|
```javascript!
|
||||||
const { TuitioClient, fetch, invalidate } = require("@flare/tuitio-client");
|
const { TuitioClient, fetch } = require("@flare/tuitio-client");
|
||||||
const TuitioClient = require("@flare/tuitio-client");
|
const TuitioClient = require("@flare/tuitio-client");
|
||||||
const type { TuitioToken, TuitioAuthenticationResult, TuitioState } = require("@flare/tuitio-client");
|
const type { TuitioLoginResult, TuitioLogoutResult, TuitioState } = require("@flare/tuitio-client");
|
||||||
```
|
```
|
||||||
|
|
||||||
```javascript!
|
```javascript!
|
||||||
import { TuitioClient, fetch, invalidate } from "@flare/tuitio-client";
|
import { TuitioClient, fetch } from "@flare/tuitio-client";
|
||||||
import TuitioClient from "@flare/tuitio-client";
|
import TuitioClient from "@flare/tuitio-client";
|
||||||
import type { TuitioToken, TuitioAuthenticationResult, TuitioState } from "@flare/tuitio-client";
|
import type { TuitioLoginResult, TuitioLogoutResult, TuitioState } from "@flare/tuitio-client";
|
||||||
```
|
```
|
||||||
|
|
||||||
## Unit testing
|
## Unit testing
|
||||||
|
@ -38,3 +38,4 @@ All tests in the package can be executed by running: `npm test`.
|
||||||
1.0.2 - Validate that Tuitio's URL parameter is a valid URL
|
1.0.2 - Validate that Tuitio's URL parameter is a valid URL
|
||||||
1.0.3 - Added LICENSE file
|
1.0.3 - Added LICENSE file
|
||||||
1.0.4 - TuitioState's token property can be null
|
1.0.4 - TuitioState's token property can be null
|
||||||
|
1.1.0 - In this version, the account logout method and the latest changes published by Tuitio were implemented.
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "@flare/tuitio-client",
|
"name": "@flare/tuitio-client",
|
||||||
"version": "1.0.4",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@flare/tuitio-client",
|
"name": "@flare/tuitio-client",
|
||||||
"version": "1.0.4",
|
"version": "1.1.0",
|
||||||
"license": "ISC",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@flare/js-utils": "^1.0.3",
|
"@flare/js-utils": "^1.0.3",
|
||||||
"axios": "^1.3.2"
|
"axios": "^1.3.2"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@flare/tuitio-client",
|
"name": "@flare/tuitio-client",
|
||||||
"version": "1.0.4",
|
"version": "1.1.0",
|
||||||
"description": "Tuitio client is an npm package written in typescript that facilitates the integration of a javascript application with Tuitio.",
|
"description": "Tuitio client is an npm package written in typescript that facilitates the integration of a javascript application with Tuitio.",
|
||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
|
|
|
@ -2,19 +2,24 @@
|
||||||
|
|
||||||
import { getUrlTemplates } from "../config";
|
import { getUrlTemplates } from "../config";
|
||||||
import { TuitioClient } from "../client";
|
import { TuitioClient } from "../client";
|
||||||
import type { TuitioAuthenticationResult, TuitioToken } from "../client";
|
import type { TuitioLoginResult } from "../client";
|
||||||
|
|
||||||
test("Get url templates", () => {
|
test("Get url templates", () => {
|
||||||
const result = getUrlTemplates("https://test.com/api");
|
const result = getUrlTemplates("https://test.com/api");
|
||||||
expect(result).toHaveProperty("authentication");
|
expect(result).toHaveProperty("login");
|
||||||
expect(result.authentication).toContain("https://test.com/api");
|
expect(result).toHaveProperty("logout");
|
||||||
expect(result.authentication).toContain("{username}");
|
expect(result.login).toContain("https://test.com/api");
|
||||||
expect(result.authentication).toContain("{password}");
|
expect(result.login).toContain("{username}");
|
||||||
|
expect(result.login).toContain("{password}");
|
||||||
|
expect(result.logout).toContain("{token}");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Tuitio client initialization", () => {
|
test("Tuitio client initialization", () => {
|
||||||
const client = new TuitioClient("https://test.com/api");
|
const tuitioUrl = "https://test.com/api";
|
||||||
expect(client.tuitioUrl).toBe("https://test.com/api");
|
const client = new TuitioClient(tuitioUrl);
|
||||||
|
expect(client.tuitioUrl).toBe(tuitioUrl);
|
||||||
|
expect(client.urlTemplates.login.startsWith(tuitioUrl)).toBe(true);
|
||||||
|
expect(client.urlTemplates.logout.startsWith(tuitioUrl)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Tuitio client initialization with null URL", () => {
|
test("Tuitio client initialization with null URL", () => {
|
||||||
|
@ -35,18 +40,17 @@ test("Tuitio client initialization with invalid URL", () => {
|
||||||
|
|
||||||
test("Tuitio client fake authentication", async () => {
|
test("Tuitio client fake authentication", async () => {
|
||||||
const client = new TuitioClient("https://test.com/api");
|
const client = new TuitioClient("https://test.com/api");
|
||||||
const spy = jest.spyOn(client, "authenticate").mockImplementation(async (userName, password) => {
|
const spy = jest.spyOn(client, "login").mockImplementation(async (userName, password) => {
|
||||||
const token = <TuitioToken>{ raw: `mock-${userName}-${password}`, validFrom: new Date("10/02/2023"), validUntil: new Date("11/02/2023") };
|
const token = `mock-${userName}-${password}`;
|
||||||
const authResult = <TuitioAuthenticationResult>{ token, status: "_MOCK_" };
|
const authResult = <TuitioLoginResult>{ result: { token, expiresIn: 600000 }, error: null };
|
||||||
return authResult;
|
return authResult;
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await client.authenticate("user", "pass");
|
const loginResult = await client.login("user", "pass");
|
||||||
|
|
||||||
expect(result.token.raw).toBe("mock-user-pass");
|
expect(loginResult.result.token).toBe("mock-user-pass");
|
||||||
expect(result.token.validFrom).toStrictEqual(new Date("10/02/2023"));
|
expect(loginResult.result.expiresIn).toBe(600000);
|
||||||
expect(result.token.validUntil).toStrictEqual(new Date("11/02/2023"));
|
expect(loginResult.error).toBeNull;
|
||||||
expect(result.status).toBe("_MOCK_");
|
|
||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,34 +5,32 @@
|
||||||
// Copyright (c) 2023 Tudor Stanciu
|
// Copyright (c) 2023 Tudor Stanciu
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { TuitioClient, fetch, invalidate } from "../client";
|
import { TuitioClient, fetch } from "../client";
|
||||||
|
|
||||||
jest.mock("axios");
|
jest.mock("axios");
|
||||||
|
|
||||||
test("Tuitio client authentication", async () => {
|
test("Tuitio client: successfully account login", async () => {
|
||||||
(axios.request as jest.Mock).mockResolvedValue({
|
(axios.request as jest.Mock).mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
token: { raw: "mock-user-pass", validFrom: new Date("10/02/2023"), validUntil: new Date("11/02/2023") },
|
result: {
|
||||||
status: "_MOCK_"
|
token: "token-mock",
|
||||||
|
expiresIn: 600000
|
||||||
|
},
|
||||||
|
error: null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = new TuitioClient("https://test.com/api");
|
const client = new TuitioClient("https://test.com/api");
|
||||||
const result = await client.authenticate("user", "pass");
|
const loginResult = await client.login("user", "pass");
|
||||||
|
|
||||||
expect(result.token.raw).toBe("mock-user-pass");
|
expect(loginResult.result.token).toBe("token-mock");
|
||||||
expect(result.token.validFrom).toStrictEqual(new Date("10/02/2023"));
|
expect(loginResult.result.expiresIn).toBe(600000);
|
||||||
expect(result.token.validUntil).toStrictEqual(new Date("11/02/2023"));
|
expect(loginResult.error).toBeNull();
|
||||||
expect(result.status).toBe("_MOCK_");
|
|
||||||
|
|
||||||
const storage = fetch();
|
const storage = fetch();
|
||||||
expect(storage.token?.raw).toBe("mock-user-pass");
|
expect(storage).toBeDefined();
|
||||||
expect(storage.userName).toBe("user");
|
expect(storage?.token).toBe("token-mock");
|
||||||
|
expect(storage?.userName).toBe("user");
|
||||||
invalidate();
|
|
||||||
const emptyStorage = fetch();
|
|
||||||
expect(emptyStorage.token).toBeNull();
|
|
||||||
expect(emptyStorage.userName).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Tuitio client failed authentication", async () => {
|
test("Tuitio client failed authentication", async () => {
|
||||||
|
@ -46,7 +44,7 @@ test("Tuitio client failed authentication", async () => {
|
||||||
const client = new TuitioClient("https://test.com/api");
|
const client = new TuitioClient("https://test.com/api");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.authenticate("user", "pass");
|
await client.login("user", "pass");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
expect(error.title).toBe("Internal server error");
|
expect(error.title).toBe("Internal server error");
|
||||||
expect(error.message).toBe("Internal server error");
|
expect(error.message).toBe("Internal server error");
|
||||||
|
@ -64,7 +62,7 @@ test("Tuitio client failed authentication with unhandled exception", async () =>
|
||||||
const client = new TuitioClient("https://test.com/api");
|
const client = new TuitioClient("https://test.com/api");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.authenticate("user", "pass");
|
await client.login("user", "pass");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
expect(error.response.status).toBe(500);
|
expect(error.response.status).toBe(500);
|
||||||
expect(error.response.error.message).toBe("Internal server error");
|
expect(error.response.error.message).toBe("Internal server error");
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Copyright (c) 2023 Tudor Stanciu
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
import { TuitioClient, fetch } from "../client";
|
||||||
|
|
||||||
|
jest.mock("axios");
|
||||||
|
|
||||||
|
test("Tuitio client: successfully account logout", async () => {
|
||||||
|
(axios.request as jest.Mock).mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
result: { userId: 0, userName: "tuitio.user", logoutDate: new Date() },
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new TuitioClient("https://test.com/api");
|
||||||
|
const logoutResult = await client.logout("token-mock");
|
||||||
|
|
||||||
|
expect(logoutResult.result.userId).toBe(0);
|
||||||
|
expect(logoutResult.result.userName).toBe("tuitio.user");
|
||||||
|
expect(logoutResult.error).toBeNull();
|
||||||
|
|
||||||
|
const storage = fetch();
|
||||||
|
expect(storage).toBeDefined();
|
||||||
|
expect(storage.token).toBeNull();
|
||||||
|
expect(storage.validUntil).toBeNull();
|
||||||
|
expect(storage.userName).toBeNull();
|
||||||
|
});
|
|
@ -4,14 +4,11 @@
|
||||||
|
|
||||||
// Copyright (c) 2023 Tudor Stanciu
|
// Copyright (c) 2023 Tudor Stanciu
|
||||||
|
|
||||||
import { fetch, invalidate } from "../client";
|
import { fetch } from "../client";
|
||||||
|
|
||||||
test("Tuitio empty storage", () => {
|
test("Tuitio empty storage", () => {
|
||||||
const result = fetch();
|
const result = fetch();
|
||||||
expect(result.token).toBeNull();
|
expect(result.token).toBeNull();
|
||||||
|
expect(result.validUntil).toBeNull();
|
||||||
expect(result.userName).toBeNull();
|
expect(result.userName).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Tuitio storage invalidation", () => {
|
|
||||||
expect(invalidate()).toBe(void 0);
|
|
||||||
});
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { localStorage } from "@flare/js-utils";
|
||||||
import { storageKeys } from "./constants";
|
import { storageKeys } from "./constants";
|
||||||
import { getUrlTemplates } from "./config";
|
import { getUrlTemplates } from "./config";
|
||||||
import { isValidURL } from "./utils";
|
import { isValidURL } from "./utils";
|
||||||
|
import type { UrlTemplates } from "./config";
|
||||||
|
|
||||||
const { setItem, getItem, removeItem } = localStorage;
|
const { setItem, getItem, removeItem } = localStorage;
|
||||||
|
|
||||||
|
@ -24,11 +25,12 @@ async function request(url: string, method: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TuitioToken = { raw: string; validFrom: Date; validUntil: Date };
|
export type TuitioLoginResult = { result: { token: string; expiresIn: number }; error: string | null };
|
||||||
export type TuitioAuthenticationResult = { token: TuitioToken; status: string };
|
export type TuitioLogoutResult = { result: { userId: number; userName: string; logoutDate: Date }; error: string };
|
||||||
|
|
||||||
class TuitioClient {
|
class TuitioClient {
|
||||||
tuitioUrl: string;
|
tuitioUrl: string;
|
||||||
|
urlTemplates: UrlTemplates;
|
||||||
|
|
||||||
constructor(tuitioUrl: string | null) {
|
constructor(tuitioUrl: string | null) {
|
||||||
if (tuitioUrl === null || tuitioUrl === "" || isValidURL(tuitioUrl) === false) {
|
if (tuitioUrl === null || tuitioUrl === "" || isValidURL(tuitioUrl) === false) {
|
||||||
|
@ -36,40 +38,48 @@ class TuitioClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tuitioUrl = tuitioUrl;
|
this.tuitioUrl = tuitioUrl;
|
||||||
|
this.urlTemplates = getUrlTemplates(this.tuitioUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
async authenticate(userName: string, password: string): Promise<TuitioAuthenticationResult> {
|
async login(userName: string, password: string): Promise<TuitioLoginResult> {
|
||||||
const templates = getUrlTemplates(this.tuitioUrl);
|
const url = this.urlTemplates.login.replace("{username}", userName).replace("{password}", password);
|
||||||
const url = templates.authentication.replace("{username}", userName).replace("{password}", password);
|
|
||||||
|
|
||||||
const response = await request(url, "post");
|
const response = await request(url, "post");
|
||||||
if (response.token) {
|
if (!response.error) {
|
||||||
setItem(storageKeys.TOKEN, response.token);
|
const currentDate = new Date();
|
||||||
setItem(storageKeys.USER, userName);
|
const expiresIn = response.result.expiresIn - 10000; //10 seconds of safety margin
|
||||||
|
const validUntil = new Date(currentDate.getTime() + expiresIn);
|
||||||
|
const authData = { token: response.result.token, validUntil, userName };
|
||||||
|
setItem(storageKeys.AUTH_DATA, authData);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async logout(token: string): Promise<TuitioLogoutResult> {
|
||||||
|
const url = this.urlTemplates.logout.replace("{token}", token);
|
||||||
|
const response = await request(url, "post");
|
||||||
|
if (!response.error) {
|
||||||
|
removeItem(storageKeys.AUTH_DATA);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const invalidate = (): void => {
|
export type TuitioState = { token: string | null; validUntil: Date | null; userName: string | null };
|
||||||
const token = getItem(storageKeys.TOKEN);
|
const defaultTuitioState: TuitioState = { token: null, validUntil: null, userName: null };
|
||||||
if (token) {
|
|
||||||
removeItem(storageKeys.TOKEN);
|
|
||||||
removeItem(storageKeys.USER);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TuitioState = { token: TuitioToken | null; userName: string };
|
|
||||||
|
|
||||||
const fetch = (): TuitioState => {
|
const fetch = (): TuitioState => {
|
||||||
const data = {
|
const authData = getItem(storageKeys.AUTH_DATA);
|
||||||
token: getItem(storageKeys.TOKEN),
|
if (!authData) return defaultTuitioState;
|
||||||
userName: getItem(storageKeys.USER)
|
const { token, validUntil, userName } = authData;
|
||||||
|
const data: TuitioState = {
|
||||||
|
token,
|
||||||
|
validUntil,
|
||||||
|
userName
|
||||||
};
|
};
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { TuitioClient, invalidate, fetch };
|
export { TuitioClient, fetch };
|
||||||
export default TuitioClient;
|
export default TuitioClient;
|
||||||
|
|
|
@ -2,12 +2,14 @@
|
||||||
|
|
||||||
import { combineUrls } from "./utils";
|
import { combineUrls } from "./utils";
|
||||||
|
|
||||||
type UrlTemplates = {
|
export type UrlTemplates = {
|
||||||
authentication: string;
|
login: string;
|
||||||
|
logout: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUrlTemplates = (tuitioUrl: string): UrlTemplates => ({
|
const getUrlTemplates = (tuitioUrl: string): UrlTemplates => ({
|
||||||
authentication: combineUrls(tuitioUrl, "/identity/authenticate?UserName={username}&Password={password}")
|
login: combineUrls(tuitioUrl, "/account/login?UserName={username}&Password={password}"),
|
||||||
|
logout: combineUrls(tuitioUrl, "/account/logout?Token={token}")
|
||||||
});
|
});
|
||||||
|
|
||||||
export { getUrlTemplates };
|
export { getUrlTemplates };
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
// Copyright (c) 2023 Tudor Stanciu
|
// Copyright (c) 2023 Tudor Stanciu
|
||||||
|
|
||||||
export const storageKeys: { TOKEN: string; USER: string } = {
|
export const storageKeys: { AUTH_DATA: string } = {
|
||||||
TOKEN: "TUITIO_AUTHORIZATION_TOKEN",
|
AUTH_DATA: "tuitio.auth.data"
|
||||||
USER: "TUITIO_USER_NAME"
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// Copyright (c) 2023 Tudor Stanciu
|
// Copyright (c) 2023 Tudor Stanciu
|
||||||
|
|
||||||
import { TuitioClient, invalidate, fetch } from "./client";
|
import { TuitioClient, fetch } from "./client";
|
||||||
import type { TuitioToken, TuitioAuthenticationResult, TuitioState } from "./client";
|
import type { TuitioLoginResult, TuitioLogoutResult, TuitioState } from "./client";
|
||||||
|
|
||||||
export { TuitioClient, invalidate, fetch };
|
export { TuitioClient, fetch };
|
||||||
export type { TuitioToken, TuitioAuthenticationResult, TuitioState };
|
export type { TuitioLoginResult, TuitioLogoutResult, TuitioState };
|
||||||
export default TuitioClient;
|
export default TuitioClient;
|
||||||
|
|
Loading…
Reference in New Issue