-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: bring health checks to release early
- Loading branch information
Showing
22 changed files
with
768 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { healthCheckRouterFactory } from "@api/healthCheckRouter"; | ||
import type { HealthCheckMethod } from "@domain/types"; | ||
import { setupExpress } from "@libs/express"; | ||
import type { Express } from "express"; | ||
import { ReasonPhrases, StatusCodes } from "http-status-codes"; | ||
import request from "supertest"; | ||
|
||
const mockOKHealthCheckMethod: HealthCheckMethod = async () => { | ||
return new Promise((resolve) => { | ||
resolve({ | ||
success: true, | ||
data: { | ||
message: ReasonPhrases.OK, | ||
}, | ||
}); | ||
}); | ||
}; | ||
|
||
const mockErrorHealthCheckMethod: HealthCheckMethod = async () => { | ||
return new Promise((resolve) => { | ||
resolve({ | ||
success: false, | ||
error: { | ||
message: ReasonPhrases.BAD_GATEWAY, | ||
serverResponseBody: "", | ||
serverResponseCode: 404, | ||
timeout: false, | ||
}, | ||
}); | ||
}); | ||
}; | ||
|
||
const mockTimeoutHealthCheckMethod: HealthCheckMethod = async () => { | ||
return new Promise((resolve) => { | ||
resolve({ | ||
success: false, | ||
error: { | ||
message: ReasonPhrases.GATEWAY_TIMEOUT, | ||
timeout: true, | ||
}, | ||
}); | ||
}); | ||
}; | ||
|
||
describe("healthCheckRouter", () => { | ||
let app: Express; | ||
|
||
beforeEach(async () => { | ||
app = setupExpress(false); | ||
app.use( | ||
healthCheckRouterFactory(new Map<string, HealthCheckMethod>([["mockOK", mockOKHealthCheckMethod]])), | ||
healthCheckRouterFactory(new Map<string, HealthCheckMethod>([["mockErr", mockErrorHealthCheckMethod]])), | ||
healthCheckRouterFactory( | ||
new Map<string, HealthCheckMethod>([["mockTimeout", mockTimeoutHealthCheckMethod]]) | ||
) | ||
); | ||
}); | ||
|
||
afterAll(async () => { | ||
await new Promise((resolve) => { | ||
return setTimeout(resolve, 500); | ||
}); | ||
}); | ||
|
||
describe("GET /", () => { | ||
it("should return a successful response for the server's self check", async () => { | ||
const response = await request(app).get("/"); | ||
expect(response.status).toBe(StatusCodes.OK); | ||
expect(response.body.success).toBe(true); | ||
expect(response.body.data.message).toEqual("OK"); | ||
}); | ||
}); | ||
|
||
describe("GET /mockOK", () => { | ||
it("should return a successful response for a mocked health check", async () => { | ||
const response = await request(app).get("/mockOK"); | ||
expect(response.status).toBe(StatusCodes.OK); | ||
expect(response.body.success).toBe(true); | ||
expect(response.body.data.message).toEqual("OK"); | ||
}); | ||
}); | ||
|
||
describe("GET /mockErr", () => { | ||
it("should return an unsuccessful response for a errored health check", async () => { | ||
const response = await request(app).get("/mockErr"); | ||
expect(response.status).toBe(StatusCodes.BAD_GATEWAY); | ||
expect(response.body.success).toBe(false); | ||
expect(response.body.error.message).toEqual(ReasonPhrases.BAD_GATEWAY); | ||
}); | ||
}); | ||
|
||
describe("GET /mockTimeout", () => { | ||
it("should return a successful response for a mocked health check", async () => { | ||
const response = await request(app).get("/mockTimeout"); | ||
expect(response.status).toBe(StatusCodes.GATEWAY_TIMEOUT); | ||
expect(response.body.success).toBe(false); | ||
expect(response.body.error.message).toEqual(ReasonPhrases.GATEWAY_TIMEOUT); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import type { HealthCheckMetadata, HealthCheckMethod, SuccessfulHealthCheckResponse } from "@domain/types"; | ||
import { Router } from "express"; | ||
import { ReasonPhrases, StatusCodes } from "http-status-codes"; | ||
|
||
const requestTimestamp = (): number => Math.round(Date.now() / 1000); | ||
|
||
const healthCheckResponseStatus = (response: HealthCheckMetadata): StatusCodes => { | ||
let statusCode = StatusCodes.OK; | ||
|
||
if (!response.success && response.error) { | ||
statusCode = response.error.timeout ? StatusCodes.GATEWAY_TIMEOUT : StatusCodes.BAD_GATEWAY; | ||
} | ||
|
||
return statusCode; | ||
}; | ||
|
||
type HealthCheckEndpoint = string; | ||
type HealthChecks = Map<HealthCheckEndpoint, HealthCheckMethod>; | ||
|
||
export const healthCheckRouterFactory = (clients: HealthChecks): Router => { | ||
const router = Router(); | ||
|
||
router.get("/", (_req, res) => { | ||
res.json({ | ||
timestamp: requestTimestamp(), | ||
success: true, | ||
data: { | ||
message: ReasonPhrases.OK, | ||
}, | ||
} as SuccessfulHealthCheckResponse); | ||
}); | ||
|
||
for (const [endpoint, client] of clients) { | ||
router.get(`/${endpoint}`, async (_req, res) => { | ||
const timestamp = requestTimestamp(); | ||
const healthCheckResult = await client(); | ||
const statusCode = healthCheckResponseStatus(healthCheckResult); | ||
|
||
res.status(statusCode).json({ | ||
timestamp, | ||
...healthCheckResult, | ||
}); | ||
}); | ||
} | ||
|
||
return router; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
api/src/client/dynamics/elevator-safety/DynamicsElevatorSafetyHealthCheckClient.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { DynamicsElevatorSafetyHealthCheckClient } from "@client/dynamics/elevator-safety/DynamicsElevatorSafetyHealthCheckClient"; | ||
import { AccessTokenClient } from "@client/dynamics/types"; | ||
import { HealthCheckMethod } from "@domain/types"; | ||
import { LogWriter, LogWriterType } from "@libs/logWriter"; | ||
import axios from "axios"; | ||
|
||
jest.mock("axios"); | ||
jest.mock("winston"); | ||
const mockAxios = axios as jest.Mocked<typeof axios>; | ||
|
||
describe("DynamicsElevatorSafetyHealthCheckClient", () => { | ||
let client: HealthCheckMethod; | ||
let logger: LogWriterType; | ||
let stubAccessTokenClient: jest.Mocked<AccessTokenClient>; | ||
|
||
const ORG_URL = "www.test-org-url.com"; | ||
|
||
beforeEach(() => { | ||
logger = LogWriter("NavigatorWebService", "ApiLogs", "us-test-1"); | ||
stubAccessTokenClient = { | ||
getAccessToken: jest.fn(), | ||
}; | ||
client = DynamicsElevatorSafetyHealthCheckClient(logger, { | ||
accessTokenClient: stubAccessTokenClient, | ||
orgUrl: ORG_URL, | ||
}); | ||
}); | ||
|
||
it("returns a passing health check if data can be retrieved sucessfully", async () => { | ||
mockAxios.get.mockResolvedValue({}); | ||
expect(await client()).toEqual({ success: true, data: { message: "OK" } }); | ||
}); | ||
|
||
it("returns a failing health check if unexpected data is retrieved", async () => { | ||
mockAxios.get.mockRejectedValue({ response: { status: 404 }, message: "" }); | ||
expect(await client()).toEqual({ | ||
success: false, | ||
error: { | ||
message: "Bad Gateway", | ||
serverResponseBody: "", | ||
serverResponseCode: 404, | ||
timeout: false, | ||
}, | ||
}); | ||
}); | ||
|
||
it("returns a failing health check if axios request times out", async () => { | ||
mockAxios.get.mockRejectedValue({}); | ||
expect(await client()).toEqual({ | ||
success: false, | ||
error: { | ||
message: "Gateway Timeout", | ||
timeout: true, | ||
}, | ||
}); | ||
}); | ||
}); |
55 changes: 55 additions & 0 deletions
55
api/src/client/dynamics/elevator-safety/DynamicsElevatorSafetyHealthCheckClient.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import type { AccessTokenClient } from "@client/dynamics/types"; | ||
import type { HealthCheckMetadata, HealthCheckMethod } from "@domain/types"; | ||
import type { LogWriterType } from "@libs/logWriter"; | ||
import axios, { type AxiosError } from "axios"; | ||
import { ReasonPhrases } from "http-status-codes"; | ||
|
||
type Config = { | ||
accessTokenClient: AccessTokenClient; | ||
orgUrl: string; | ||
}; | ||
|
||
export const DynamicsElevatorSafetyHealthCheckClient = ( | ||
logWriter: LogWriterType, | ||
config: Config | ||
): HealthCheckMethod => { | ||
return async (): Promise<HealthCheckMetadata> => { | ||
const logId = logWriter.GetId(); | ||
const accessToken = await config.accessTokenClient.getAccessToken(); | ||
return axios | ||
.get(`${config.orgUrl}/api/data/v9.2/ultra_elsainspections?$top=1`, { | ||
headers: { | ||
Authorization: `Bearer ${accessToken}`, | ||
}, | ||
}) | ||
.then(() => { | ||
return { | ||
success: true, | ||
data: { | ||
message: ReasonPhrases.OK, | ||
}, | ||
} as HealthCheckMetadata; | ||
}) | ||
.catch((error: AxiosError) => { | ||
logWriter.LogError(`Dynamics Elevator Safety Health Check Failed - Id:${logId} - Error:`, error); | ||
if (error.response) { | ||
return { | ||
success: false, | ||
error: { | ||
serverResponseBody: error.message, | ||
serverResponseCode: error.response.status, | ||
message: ReasonPhrases.BAD_GATEWAY, | ||
timeout: false, | ||
}, | ||
} as HealthCheckMetadata; | ||
} | ||
return { | ||
success: false, | ||
error: { | ||
message: ReasonPhrases.GATEWAY_TIMEOUT, | ||
timeout: true, | ||
}, | ||
} as HealthCheckMetadata; | ||
}); | ||
}; | ||
}; |
Oops, something went wrong.