diff --git a/jest.config.ts b/jest.config.ts index 08fec57..9607fea 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -11,6 +11,7 @@ const config: Config = { ], watchPathIgnorePatterns: ["/.testing/", "/testing/"], collectCoverageFrom: ["lib/**/*.{js,jsx,ts,tsx}"], + restoreMocks: true, }; export default config; diff --git a/lib/src/index.ts b/lib/src/index.ts index f556eb3..3c0414d 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -6,7 +6,7 @@ import { pull } from "./commands/pull"; import { quit } from "./utils/quit"; import { version } from "../../package.json"; import logger from "./utils/logger"; -import { initAPIToken } from "./services/apiToken"; +import initAPIToken from "./services/apiToken/initAPIToken"; import { initProjectConfig } from "./services/projectConfig"; import appContext from "./utils/appContext"; diff --git a/lib/src/services/apiToken.test.ts b/lib/src/services/apiToken.test.ts deleted file mode 100644 index 0e01f3e..0000000 --- a/lib/src/services/apiToken.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { jest, describe, it, expect, beforeEach } from "@jest/globals"; -import { _test as apiTokenTest } from "./apiToken"; - -const { getURLHostname } = apiTokenTest; - -describe("apiToken", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe("getURLHostname", () => { - it("should return hostname when URL contains protocol", () => { - const expectedHostName = "example.com"; - - const result = getURLHostname("https://example.com"); - - expect(result).toBe(expectedHostName); - }); - - it("should return host as is when no protocol present", () => { - const expectedHostName = "example.com"; - - const result = getURLHostname("example.com"); - expect(result).toBe("example.com"); - }); - }); -}); diff --git a/lib/src/services/apiToken.ts b/lib/src/services/apiToken.ts deleted file mode 100644 index 2a79370..0000000 --- a/lib/src/services/apiToken.ts +++ /dev/null @@ -1,149 +0,0 @@ -import appContext from "../utils/appContext"; -import fs from "fs"; -import URL from "url"; -import * as configService from "./globalConfig"; -import checkToken from "../http/checkToken"; -import logger from "../utils/logger"; -import { quit } from "../utils/quit"; -import * as Sentry from "@sentry/node"; -import { prompt } from "enquirer"; - -/** - * Initializes the API token - * @param token The token to initialize the API token with. If not provided, the token will be fetched from the global config file. - * @param configFile The path to the global config file - * @param host The host to initialize the API token for - * @returns The initialized API token - */ -export async function initAPIToken( - token: string | undefined = appContext.apiToken, - configFile: string = appContext.configFile, - host: string = appContext.apiHost -) { - if (token) { - return await validateToken(token); - } - - if (!fs.existsSync(configFile)) { - return await collectAndSaveToken(); - } - - const configData = configService.readGlobalConfigData(configFile); - const sanitizedHost = getURLHostname(host); - - if ( - !configData[sanitizedHost] || - !configData[sanitizedHost][0] || - configData[sanitizedHost][0].token === "" - ) { - return await collectAndSaveToken(sanitizedHost); - } - - return await validateToken(configData[sanitizedHost][0].token); -} - -/** - * Collects a token from the user and saves it to the global config file - * @param host The host to save the token for - * @returns The collected token - */ -async function collectAndSaveToken(host: string = appContext.apiHost) { - try { - const token = await collectToken(); - logger.writeLine( - `Thanks for authenticating. We'll save the key to: ${logger.info( - appContext.configFile - )}\n` - ); - const sanitizedHost = getURLHostname(host); - configService.saveToken(appContext.configFile, sanitizedHost, token); - appContext.setApiToken(token); - return token; - } catch (error) { - // https://github.com/enquirer/enquirer/issues/225#issue-516043136 - // Empty string corresponds to the user hitting Ctrl + C - if (error === "") { - await quit("", 0); - return ""; - } - - const eventId = Sentry.captureException(error); - const eventStr = `\n\nError ID: ${logger.info(eventId)}`; - - await quit( - logger.errorText( - "Something went wrong. Please contact support or try again later." - ) + eventStr - ); - return ""; - } -} - -/** - * Outputs instructions to the user and collects an API token - * @returns The collected token - */ -async function collectToken() { - const apiUrl = logger.url("https://app.dittowords.com/account/devtools"); - const breadcrumbs = logger.bold(logger.info("API Keys")); - const tokenDescription = `To get started, you'll need your Ditto API key. You can find this at: ${apiUrl} under "${breadcrumbs}".`; - - logger.writeLine(tokenDescription); - - const response = await promptForApiToken(); - return response.token; -} - -/** - * Prompt the user for an API token - * @returns The collected token - */ -async function promptForApiToken() { - const response = await prompt<{ token: string }>({ - type: "input", - name: "token", - message: "What is your API key?", - // @ts-expect-error - Enquirer types are not updated for the validate function - validate: async (token) => { - console.log("token", token); - const result = await checkToken(token); - if (!result.success) { - return result.output?.join("\n") || "Invalid API key"; - } - return true; - }, - }); - - return response; -} - -/** - * Get the hostname from a URL string - * @param hostString - * @returns - */ -function getURLHostname(hostString: string) { - if (!hostString.includes("://")) return hostString; - return URL.parse(hostString).hostname || ""; -} - -/** - * Validate a token - * @param token The token to validate - * @returns The newly validated token - */ -async function validateToken(token: string) { - const response = await checkToken(token); - if (!response.success) { - return await collectAndSaveToken(); - } - - return token; -} - -export const _test = { - collectToken, - validateToken, - getURLHostname, - promptForApiToken, -}; diff --git a/lib/src/services/apiToken/collectAndSaveToken.test.ts b/lib/src/services/apiToken/collectAndSaveToken.test.ts new file mode 100644 index 0000000..fc61de5 --- /dev/null +++ b/lib/src/services/apiToken/collectAndSaveToken.test.ts @@ -0,0 +1,105 @@ +import * as CollectToken from "./collectToken"; +import * as GetURLHostname from "./getURLHostname"; +import * as configService from "../globalConfig"; +import appContext from "../../utils/appContext"; +import * as utils from "../../utils/quit"; +import collectAndSaveToken from "./collectAndSaveToken"; + +describe("collectAndSaveToken", () => { + let priorToken: string | undefined; + let priorHost: string; + + let collectTokenSpy: jest.SpiedFunction; + let getURLHostnameSpy: jest.SpiedFunction; + let saveTokenSpy: jest.SpiedFunction; + let quitSpy: jest.SpiedFunction; + + const token = "token"; + const host = "host"; + const apiHost = "apiHost"; + const sanitizedHost = "hostname"; + + beforeEach(() => { + priorToken = appContext.apiToken; + priorHost = appContext.apiHost; + appContext.setApiToken(""); + appContext.apiHost = apiHost; + collectTokenSpy = jest.spyOn(CollectToken, "default"); + getURLHostnameSpy = jest + .spyOn(GetURLHostname, "default") + .mockReturnValue(sanitizedHost); + saveTokenSpy = jest + .spyOn(configService, "saveToken") + .mockImplementation(() => Promise.resolve()); + quitSpy = jest + .spyOn(utils, "quit") + .mockImplementation(() => Promise.resolve()); + }); + + afterEach(() => { + appContext.setApiToken(priorToken); + appContext.apiHost = priorHost; + jest.restoreAllMocks(); + }); + + it("collects, saves and returns a token", async () => { + collectTokenSpy.mockResolvedValue(token); + + expect(appContext.apiToken).toBe(""); + const result = await collectAndSaveToken(); + + expect(collectTokenSpy).toHaveBeenCalled(); + expect(getURLHostnameSpy).toHaveBeenCalledWith(appContext.apiHost); + expect(saveTokenSpy).toHaveBeenCalledWith( + appContext.configFile, + sanitizedHost, + token + ); + expect(result).toBe(token); + expect(appContext.apiToken).toBe(token); + }); + + it("uses the host if provided", async () => { + collectTokenSpy.mockResolvedValue(token); + + expect(appContext.apiToken).toBe(""); + + const result = await collectAndSaveToken(host); + expect(collectTokenSpy).toHaveBeenCalled(); + expect(getURLHostnameSpy).toHaveBeenCalledWith(host); + expect(saveTokenSpy).toHaveBeenCalledWith( + appContext.configFile, + sanitizedHost, + token + ); + expect(result).toBe(token); + }); + + it("handles empty string error", async () => { + collectTokenSpy.mockImplementation(() => Promise.reject("")); + + expect(appContext.apiToken).toBe(""); + const response = await collectAndSaveToken(); + + expect(collectTokenSpy).toHaveBeenCalled(); + expect(quitSpy).toHaveBeenCalledWith("", 0); + expect(appContext.apiToken).toBe(""); + expect(response).toBe(""); + expect(getURLHostnameSpy).not.toHaveBeenCalled(); + expect(saveTokenSpy).not.toHaveBeenCalled(); + }); + + it("handles other errors", async () => { + collectTokenSpy.mockImplementation(() => Promise.reject("some error")); + + expect(appContext.apiToken).toBe(""); + const response = await collectAndSaveToken(); + + expect(collectTokenSpy).toHaveBeenCalled(); + expect(quitSpy).toHaveBeenCalledWith(expect.stringContaining("Error ID:")); + expect(appContext.apiToken).toBe(""); + expect(response).toBe(""); + expect(getURLHostnameSpy).not.toHaveBeenCalled(); + expect(saveTokenSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/lib/src/services/apiToken/collectAndSaveToken.ts b/lib/src/services/apiToken/collectAndSaveToken.ts new file mode 100644 index 0000000..08ba021 --- /dev/null +++ b/lib/src/services/apiToken/collectAndSaveToken.ts @@ -0,0 +1,46 @@ +import appContext from "../../utils/appContext"; +import * as configService from "../globalConfig"; +import logger from "../../utils/logger"; +import { quit } from "../../utils/quit"; +import * as Sentry from "@sentry/node"; +import collectToken from "./collectToken"; +import getURLHostname from "./getURLHostname"; + +/** + * Collects a token from the user and saves it to the global config file + * @param host The host to save the token for + * @returns The collected token + */ +export default async function collectAndSaveToken( + host: string = appContext.apiHost +) { + try { + const token = await collectToken(); + logger.writeLine( + `Thanks for authenticating. We'll save the key to: ${logger.info( + appContext.configFile + )}\n` + ); + const sanitizedHost = getURLHostname(host); + configService.saveToken(appContext.configFile, sanitizedHost, token); + appContext.setApiToken(token); + return token; + } catch (error) { + // https://github.com/enquirer/enquirer/issues/225#issue-516043136 + // Empty string corresponds to the user hitting Ctrl + C + if (error === "") { + await quit("", 0); + return ""; + } + + const eventId = Sentry.captureException(error); + const eventStr = `\n\nError ID: ${logger.info(eventId)}`; + + await quit( + logger.errorText( + "Something went wrong. Please contact support or try again later." + ) + eventStr + ); + return ""; + } +} diff --git a/lib/src/services/apiToken/collectToken.test.ts b/lib/src/services/apiToken/collectToken.test.ts new file mode 100644 index 0000000..4fa270c --- /dev/null +++ b/lib/src/services/apiToken/collectToken.test.ts @@ -0,0 +1,36 @@ +import collectToken from "./collectToken"; +import * as PromptForApiToken from "./promptForApiToken"; +import logger from "../../utils/logger"; + +describe("collectToken", () => { + let promptForApiTokenSpy: jest.SpiedFunction< + typeof PromptForApiToken.default + >; + + const token = "token"; + + beforeEach(() => { + logger.url = jest.fn((msg: string) => msg); + logger.bold = jest.fn((msg: string) => msg); + logger.info = jest.fn((msg: string) => msg); + logger.writeLine = jest.fn((msg: string) => {}); + + promptForApiTokenSpy = jest + .spyOn(PromptForApiToken, "default") + .mockResolvedValue({ token }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("prompts for API token and returns it", async () => { + const response = await collectToken(); + expect(response).toBe(token); + expect(logger.url).toHaveBeenCalled(); + expect(logger.bold).toHaveBeenCalled(); + expect(logger.info).toHaveBeenCalled(); + expect(logger.writeLine).toHaveBeenCalled(); + expect(promptForApiTokenSpy).toHaveBeenCalled(); + }); +}); diff --git a/lib/src/services/apiToken/collectToken.ts b/lib/src/services/apiToken/collectToken.ts new file mode 100644 index 0000000..eb7b717 --- /dev/null +++ b/lib/src/services/apiToken/collectToken.ts @@ -0,0 +1,17 @@ +import logger from "../../utils/logger"; +import promptForApiToken from "./promptForApiToken"; + +/** + * Outputs instructions to the user and collects an API token + * @returns The collected token + */ +export default async function collectToken() { + const apiUrl = logger.url("https://app.dittowords.com/account/devtools"); + const breadcrumbs = logger.bold(logger.info("API Keys")); + const tokenDescription = `To get started, you'll need your Ditto API key. You can find this at: ${apiUrl} under "${breadcrumbs}".`; + + logger.writeLine(tokenDescription); + + const response = await promptForApiToken(); + return response.token; +} diff --git a/lib/src/services/apiToken/getURLHostname.test.ts b/lib/src/services/apiToken/getURLHostname.test.ts new file mode 100644 index 0000000..05d218a --- /dev/null +++ b/lib/src/services/apiToken/getURLHostname.test.ts @@ -0,0 +1,17 @@ +import getURLHostname from "./getURLHostname"; + +describe("getURLHostname", () => { + it("should return hostname when URL contains protocol", () => { + const expectedHostName = "example.com"; + + const result = getURLHostname("https://example.com"); + expect(result).toBe(expectedHostName); + }); + + it("should return host as is when no protocol present", () => { + const expectedHostName = "example.com"; + + const result = getURLHostname(expectedHostName); + expect(result).toBe(expectedHostName); + }); +}); diff --git a/lib/src/services/apiToken/getURLHostname.ts b/lib/src/services/apiToken/getURLHostname.ts new file mode 100644 index 0000000..a14c31e --- /dev/null +++ b/lib/src/services/apiToken/getURLHostname.ts @@ -0,0 +1,11 @@ +import URL from "url"; + +/** + * Get the hostname from a URL string + * @param hostString + * @returns + */ +export default function getURLHostname(hostString: string) { + if (!hostString.includes("://")) return hostString; + return URL.parse(hostString).hostname || ""; +} diff --git a/lib/src/services/apiToken/initAPIToken.test.ts b/lib/src/services/apiToken/initAPIToken.test.ts new file mode 100644 index 0000000..57aaca0 --- /dev/null +++ b/lib/src/services/apiToken/initAPIToken.test.ts @@ -0,0 +1,121 @@ +import fs from "fs"; +import * as ConfigService from "../globalConfig"; +import * as ValidateToken from "./validateToken"; +import * as CollectAndSaveToken from "./collectAndSaveToken"; +import * as GetURLHostname from "./getURLHostname"; +import initAPIToken from "./initAPIToken"; +import appContext from "../../utils/appContext"; + +describe("initAPIToken", () => { + let validateTokenSpy: jest.SpiedFunction; + let collectAndSaveTokenSpy: jest.SpiedFunction< + typeof CollectAndSaveToken.default + >; + let existsSyncSpy: jest.SpyInstance; + let readGlobalConfigDataSpy: jest.SpiedFunction< + typeof ConfigService.readGlobalConfigData + >; + let getURLHostnameSpy: jest.SpiedFunction; + let priorToken: string | undefined; + + beforeEach(() => { + priorToken = appContext.apiToken; + appContext.setApiToken(""); + + validateTokenSpy = jest + .spyOn(ValidateToken, "default") + .mockImplementation((token: string) => Promise.resolve(token)); + collectAndSaveTokenSpy = jest + .spyOn(CollectAndSaveToken, "default") + .mockImplementation((host?: string) => { + if (host) { + return Promise.resolve("tokenWithHost"); + } else { + return Promise.resolve("newToken"); + } + }); + existsSyncSpy = jest.spyOn(fs, "existsSync"); + readGlobalConfigDataSpy = jest.spyOn(ConfigService, "readGlobalConfigData"); + getURLHostnameSpy = jest + .spyOn(GetURLHostname, "default") + .mockReturnValue("urlHostname"); + }); + + afterEach(() => { + appContext.setApiToken(priorToken); + jest.restoreAllMocks(); + }); + + it("should validate and return the token if provided", async () => { + appContext.setApiToken("validToken"); + const response = await initAPIToken(); + expect(response).toBe("validToken"); + expect(validateTokenSpy).toHaveBeenCalledWith("validToken"); + expect(collectAndSaveTokenSpy).not.toHaveBeenCalled(); + expect(readGlobalConfigDataSpy).not.toHaveBeenCalled(); + expect(getURLHostnameSpy).not.toHaveBeenCalled(); + }); + + it("should call collectAndSaveToken if no token is provided and config file does not exist", async () => { + existsSyncSpy.mockReturnValue(false); + const response = await initAPIToken(); + expect(response).toBe("newToken"); + expect(validateTokenSpy).not.toHaveBeenCalled(); + expect(collectAndSaveTokenSpy).toHaveBeenCalled(); + expect(existsSyncSpy).toHaveBeenCalledWith(appContext.configFile); + expect(readGlobalConfigDataSpy).not.toHaveBeenCalled(); + expect(getURLHostnameSpy).not.toHaveBeenCalled(); + }); + + describe("should collect and save token based on config if config does not have a token", () => { + const expectCollectsFromConfig = () => { + expect(validateTokenSpy).not.toHaveBeenCalled(); + expect(existsSyncSpy).toHaveBeenCalledWith(appContext.configFile); + expect(readGlobalConfigDataSpy).toHaveBeenCalledWith( + appContext.configFile + ); + expect(getURLHostnameSpy).toHaveBeenCalledWith(appContext.apiHost); + expect(collectAndSaveTokenSpy).toHaveBeenCalledWith("urlHostname"); + }; + + it("config[host] does not exist", async () => { + existsSyncSpy.mockReturnValue(true); + const configData = {}; + readGlobalConfigDataSpy.mockReturnValue(configData); + const response = await initAPIToken(); + expect(response).toBe("tokenWithHost"); + expectCollectsFromConfig(); + }); + + it("config[host][0] does not exist", async () => { + existsSyncSpy.mockReturnValue(true); + const configData = { urlHostname: [] }; + readGlobalConfigDataSpy.mockReturnValue(configData); + const response = await initAPIToken(); + expect(response).toBe("tokenWithHost"); + expectCollectsFromConfig(); + }); + + it("config[host][0].token is empty string", async () => { + existsSyncSpy.mockReturnValue(true); + const configData = { urlHostname: [{ token: "" }] }; + readGlobalConfigDataSpy.mockReturnValue(configData); + const response = await initAPIToken(); + expect(response).toBe("tokenWithHost"); + expectCollectsFromConfig(); + }); + }); + + it("should validate and return the token from the config file", async () => { + existsSyncSpy.mockReturnValue(true); + const configData = { urlHostname: [{ token: "myToken" }] }; + readGlobalConfigDataSpy.mockReturnValue(configData); + const response = await initAPIToken(); + expect(response).toBe("myToken"); + expect(validateTokenSpy).toHaveBeenCalledWith("myToken"); + expect(existsSyncSpy).toHaveBeenCalledWith(appContext.configFile); + expect(readGlobalConfigDataSpy).toHaveBeenCalledWith(appContext.configFile); + expect(getURLHostnameSpy).toHaveBeenCalledWith(appContext.apiHost); + expect(collectAndSaveTokenSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/lib/src/services/apiToken/initAPIToken.ts b/lib/src/services/apiToken/initAPIToken.ts new file mode 100644 index 0000000..30a7638 --- /dev/null +++ b/lib/src/services/apiToken/initAPIToken.ts @@ -0,0 +1,33 @@ +import appContext from "../../utils/appContext"; +import fs from "fs"; +import * as configService from "../globalConfig"; +import collectAndSaveToken from "./collectAndSaveToken"; +import validateToken from "./validateToken"; +import getURLHostname from "./getURLHostname"; + +/** + * Initializes the API token based on the appContext and config file. + * @returns The initialized API token + */ +export default async function initAPIToken() { + if (appContext.apiToken) { + return await validateToken(appContext.apiToken); + } + + if (!fs.existsSync(appContext.configFile)) { + return await collectAndSaveToken(); + } + + const configData = configService.readGlobalConfigData(appContext.configFile); + const sanitizedHost = getURLHostname(appContext.apiHost); + + if ( + !configData[sanitizedHost] || + !configData[sanitizedHost][0] || + configData[sanitizedHost][0].token === "" + ) { + return await collectAndSaveToken(sanitizedHost); + } + + return await validateToken(configData[sanitizedHost][0].token); +} diff --git a/lib/src/services/apiToken/promptForAPIToken.test.ts b/lib/src/services/apiToken/promptForAPIToken.test.ts new file mode 100644 index 0000000..7e6bedf --- /dev/null +++ b/lib/src/services/apiToken/promptForAPIToken.test.ts @@ -0,0 +1,66 @@ +import Enquirer from "enquirer"; +import * as CheckToken from "../../http/checkToken"; +import promptForApiToken, { validate } from "./promptForApiToken"; + +describe("promptForApiToken", () => { + const mockResponse = { token: "mockToken" }; + let promptSpy: jest.SpiedFunction; + + beforeEach(() => { + promptSpy = jest.spyOn(Enquirer, "prompt").mockResolvedValue(mockResponse); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("should prompt for API token and return it", async () => { + const response = await promptForApiToken(); + expect(response).toEqual(mockResponse); + expect(promptSpy).toHaveBeenCalledWith({ + type: "input", + name: "token", + message: "What is your API key?", + validate: expect.any(Function), + }); + }); + + describe("validate", () => { + let checkTokenSpy: jest.SpiedFunction; + + beforeEach(() => { + checkTokenSpy = jest + .spyOn(CheckToken, "default") + .mockImplementation((token: string) => { + if (token === "good") { + return Promise.resolve({ success: true }); + } else if (token === "output") { + return Promise.resolve({ + success: false, + output: ["error", "message"], + }); + } else { + return Promise.resolve({ success: false }); + } + }); + }); + + it("should return true for valid token", async () => { + const result = await validate("good"); + expect(result).toBe(true); + expect(checkTokenSpy).toHaveBeenCalledWith("good"); + }); + + it("should return error message for invalid token", async () => { + const result = await validate("bad"); + expect(result).toBe("Invalid API key"); + expect(checkTokenSpy).toHaveBeenCalledWith("bad"); + }); + + it("should return output message for invalid token with output", async () => { + const result = await validate("output"); + expect(result).toBe("error\nmessage"); + expect(checkTokenSpy).toHaveBeenCalledWith("output"); + }); + }); +}); diff --git a/lib/src/services/apiToken/promptForApiToken.ts b/lib/src/services/apiToken/promptForApiToken.ts new file mode 100644 index 0000000..91da9b6 --- /dev/null +++ b/lib/src/services/apiToken/promptForApiToken.ts @@ -0,0 +1,26 @@ +import checkToken from "../../http/checkToken"; +import { prompt } from "enquirer"; + +export const validate = async (token: string) => { + const result = await checkToken(token); + if (!result.success) { + return result.output?.join("\n") || "Invalid API key"; + } + return true; +}; + +/** + * Prompt the user for an API token + * @returns The collected token + */ +export default async function promptForApiToken() { + const response = await prompt<{ token: string }>({ + type: "input", + name: "token", + message: "What is your API key?", + // @ts-expect-error - Enquirer types are not updated for the validate function + validate, + }); + + return response; +} diff --git a/lib/src/services/apiToken/validateToken.test.ts b/lib/src/services/apiToken/validateToken.test.ts new file mode 100644 index 0000000..d1045ee --- /dev/null +++ b/lib/src/services/apiToken/validateToken.test.ts @@ -0,0 +1,44 @@ +import * as CheckToken from "../../http/checkToken"; +import * as CollectAndSaveToken from "./collectAndSaveToken"; +import validateToken from "./validateToken"; + +describe("validateToken", () => { + let checkTokenSpy: jest.SpiedFunction; + let collectAndSaveTokenSpy: jest.SpiedFunction< + typeof CollectAndSaveToken.default + >; + + beforeEach(() => { + checkTokenSpy = jest + .spyOn(CheckToken, "default") + .mockImplementation((token: string) => { + if (token === "good") { + return Promise.resolve({ success: true }); + } else { + return Promise.resolve({ success: false }); + } + }); + collectAndSaveTokenSpy = jest + .spyOn(CollectAndSaveToken, "default") + .mockImplementation(() => Promise.resolve("newToken")); + }); + + afterEach(() => { + checkTokenSpy.mockRestore(); + collectAndSaveTokenSpy.mockRestore(); + }); + + it("should return to provided token if valid", async () => { + const response = await validateToken("good"); + expect(response).toBe("good"); + expect(checkTokenSpy).toHaveBeenCalledWith("good"); + expect(collectAndSaveTokenSpy).not.toHaveBeenCalled(); + }); + + it("should call collectAndSaveToken if token is invalid, and return its result", async () => { + const response = await validateToken("bad"); + expect(response).toBe("newToken"); + expect(checkTokenSpy).toHaveBeenCalledWith("bad"); + expect(collectAndSaveTokenSpy).toHaveBeenCalled(); + }); +}); diff --git a/lib/src/services/apiToken/validateToken.ts b/lib/src/services/apiToken/validateToken.ts new file mode 100644 index 0000000..11ec110 --- /dev/null +++ b/lib/src/services/apiToken/validateToken.ts @@ -0,0 +1,16 @@ +import checkToken from "../../http/checkToken"; +import collectAndSaveToken from "./collectAndSaveToken"; + +/** + * Validate a token + * @param token The token to validate + * @returns The newly validated token + */ +export default async function validateToken(token: string) { + const response = await checkToken(token); + if (!response.success) { + return await collectAndSaveToken(); + } + + return token; +}