Skip to content

Commit

Permalink
Merge branch 'main' into timeout-faq
Browse files Browse the repository at this point in the history
  • Loading branch information
tbolt authored Jan 24, 2025
2 parents 99896ee + 0db7d48 commit 5959c18
Show file tree
Hide file tree
Showing 30 changed files with 1,357 additions and 143 deletions.
Binary file modified bun.lockb
Binary file not shown.
6 changes: 6 additions & 0 deletions lib/config/deployment-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ describe("DeploymentConfig", () => {
domainCertificateArn: "domainCertificateArn",
domainName: "domainName",
emailAddressLookupSecretName: "emailAddressLookupSecretName", // pragma: allowlist secret
notificationSecretName: "notificationSecretName", // pragma: allowlist secret
notificationSecretArn: "notificationSecretArn", // pragma: allowlist secret
googleAnalyticsDisable: "true",
googleAnalyticsGTag: "googleAnalyticsGTag",
idmAuthzApiEndpoint: "idmAuthzApiEndpoint",
Expand Down Expand Up @@ -81,6 +83,8 @@ describe("DeploymentConfig", () => {
domainCertificateArn: "domainCertificateArn",
domainName: "stage-domainName", // Overridden by stage secret
emailAddressLookupSecretName: "emailAddressLookupSecretName", // pragma: allowlist secret
notificationSecretName: "notificationSecretName", // pragma: allowlist secret
notificationSecretArn: "notificationSecretArn", // pragma: allowlist secret
googleAnalyticsDisable: false, // Converted to boolean and overridden by stage secret
googleAnalyticsGTag: "googleAnalyticsGTag",
idmAuthzApiEndpoint: "idmAuthzApiEndpoint",
Expand Down Expand Up @@ -145,6 +149,8 @@ describe("DeploymentConfig", () => {
domainCertificateArn: "domainCertificateArn",
domainName: "domainName",
emailAddressLookupSecretName: "emailAddressLookupSecretName", // pragma: allowlist secret
notificationSecretName: "notificationSecretName", // pragma: allowlist secret
notificationSecretArn: "notificationSecretArn", // pragma: allowlist secret
googleAnalyticsDisable: true,
googleAnalyticsGTag: "googleAnalyticsGTag",
idmAuthzApiEndpoint: "idmAuthzApiEndpoint",
Expand Down
4 changes: 4 additions & 0 deletions lib/config/deployment-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type InjectedConfigProperties = {
domainCertificateArn: string;
domainName: string;
emailAddressLookupSecretName: string;
notificationSecretName: string;
notificationSecretArn: string;
googleAnalyticsDisable: boolean;
googleAnalyticsGTag: string;
iamPath: string;
Expand Down Expand Up @@ -117,6 +119,8 @@ export class DeploymentConfig {
typeof config.domainCertificateArn == "string" &&
typeof config.domainName === "string" &&
typeof config.emailAddressLookupSecretName === "string" && // pragma: allowlist secret
typeof config.notificationSecretName === "string" && // pragma: allowlist secret
typeof config.notificationSecretArn === "string" && // pragma: allowlist secret
typeof config.googleAnalyticsDisable == "boolean" &&
typeof config.googleAnalyticsGTag === "string" &&
typeof config.iamPermissionsBoundary === "string" &&
Expand Down
50 changes: 50 additions & 0 deletions lib/lambda/getSystemNotifs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { getSystemNotifs } from "./getSystemNotifs";
import * as util from "shared-utils";

vi.mock("shared-utils", () => ({
getExport: vi.fn(),
getSecret: vi.fn(),
}));

describe("notif handler", () => {
beforeEach(() => {
vi.resetAllMocks();
});

it("returns 200 and notifs if secret exists", async () => {
vi.stubEnv("notificationSecretArn", "test_secret");
vi.spyOn(util, "getSecret").mockImplementation(async () => "[]");
const result = await getSystemNotifs();
expect(result.statusCode).toBe(200);
expect(result.body).toBe("[]");
});

it("returns 200 and empty array if no notifs", async () => {
vi.stubEnv("notificationSecretArn", "test_secret");
vi.spyOn(util, "getSecret").mockImplementation(async () => null as unknown as string);
const result = await getSystemNotifs();
expect(result.statusCode).toBe(200);
expect(result.body).toBe("[]");
});

it("returns 502 with specific error", async () => {
vi.stubEnv("notificationSecretArn", "error");
vi.spyOn(util, "getSecret").mockImplementation(async () => {
throw new Error("test error");
});
const result = await getSystemNotifs();
expect(result.statusCode).toBe(502);
expect(JSON.parse(result.body).error).toBe("test error");
});

it("returns 502 with generic error", async () => {
vi.stubEnv("notificationSecretArn", undefined);
vi.spyOn(util, "getSecret").mockImplementation(async () => {
throw new Error();
});
const result = await getSystemNotifs();
expect(result.statusCode).toBe(502);
expect(JSON.parse(result.body).error).toBe("Internal server error");
});
});
23 changes: 23 additions & 0 deletions lib/lambda/getSystemNotifs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getSecret } from "shared-utils";
import { response } from "libs/handler-lib";

export const getSystemNotifs = async () => {
try {
const notifs = await getSecret(process.env.notificationSecretArn!);

return response({
statusCode: 200,
body: JSON.parse(notifs) || [],
});
} catch (error: any) {
console.error("Error:", error);
return response({
statusCode: 502,
body: {
error: error.message ? error.message : "Internal server error",
},
});
}
};

export const handler = getSystemNotifs;
5 changes: 3 additions & 2 deletions lib/lambda/sinkMainProcessors.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { describe, expect, it, vi, afterEach } from "vitest";
import { startOfDay } from "date-fns";
import { UTCDate } from "@date-fns/utc";
import {
insertNewSeatoolRecordsFromKafkaIntoMako,
insertOneMacRecordsFromKafkaIntoMako,
syncSeatoolRecordDatesFromKafkaWithMako,
} from "./sinkMainProcessors";
import { seatool } from "shared-types/opensearch/main";
import { offsetToUtc } from "shared-utils";
import { SEATOOL_STATUS, statusToDisplayToCmsUser, statusToDisplayToStateUser } from "shared-types";
import * as sink from "libs/sink-lib";
import * as os from "libs/opensearch-lib";
Expand Down Expand Up @@ -208,7 +209,7 @@ describe("insertOneMacRecordsFromKafkaIntoMako", () => {
stateStatus: expectation.stateStatus || statusToDisplayToStateUser[seatoolStatus],
changedDate: ISO_DATETIME,
makoChangedDate: ISO_DATETIME,
statusDate: offsetToUtc(new Date(TIMESTAMP)).toISOString(),
statusDate: startOfDay(new UTCDate(TIMESTAMP)).toISOString(),
submissionDate: ISO_DATETIME,
state: "VA",
origin: "OneMAC",
Expand Down
1 change: 1 addition & 0 deletions lib/packages/shared-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from "./guides";
export * from "./inputs";
export * from "./issue";
export * from "./lambda-events";
export * from "./notification";
export * as opensearch from "./opensearch";
export * from "./states";
export * from "./statusHelper";
Expand Down
9 changes: 9 additions & 0 deletions lib/packages/shared-types/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface BannerNotification {
notifId: string;
header: string;
body: string;
buttonText?: string;
buttonLink?: string;
pubDate: string;
expDate?: string;
}
4 changes: 3 additions & 1 deletion lib/packages/shared-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"devDependencies": {},
"dependencies": {
"@18f/us-federal-holidays": "^4.0.0",
"moment-timezone": "^0.5.45",
"@date-fns/tz": "1.2.0",
"@date-fns/utc": "2.1.0",
"date-fns": "4.1.0",
"shared-types": "*",
"eslint-config-custom-server": "*",
"eslint-config-custom": "*"
Expand Down
66 changes: 25 additions & 41 deletions lib/packages/shared-utils/seatool-date-helper.test.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,67 @@
import { it, describe, expect } from "vitest";
import {
formatSeatoolDate,
getNextBusinessDayTimestamp,
offsetFromUtc,
offsetToUtc,
seaToolFriendlyTimestamp,
} from ".";
import { formatSeatoolDate, getNextBusinessDayTimestamp, seaToolFriendlyTimestamp } from ".";
import { format } from "date-fns";

describe("offsetToUtc", () => {
it("offsets given date to UTC", () => {
const originalDate = new Date("January 1, 2000 12:00:00");
const timezoneOffset = originalDate.getTimezoneOffset() * 60000; // in milliseconds
const expectedDate = new Date(originalDate.getTime() - timezoneOffset);
console.debug("originalDate: ", originalDate, "expectedDate: ", expectedDate);
expect(offsetToUtc(originalDate)).toEqual(expectedDate);
describe("seaToolFriendlyTimestamp", () => {
it("should convert date to a timestamp representing the date at midnight UTC time", () => {
const localDate = new Date("2025-01-23T17:01:42.000Z");
const expectedTimestamp = Date.parse("2025-01-23T00:00:00.000Z");
expect(seaToolFriendlyTimestamp(localDate)).toEqual(expectedTimestamp);
});
});

describe("offsetFromUtc", () => {
it("offsets UTC date to user's timezone", () => {
const originalDate = new Date("2000-01-01T12:00:00.000Z");
const timezoneOffset = originalDate.getTimezoneOffset() * 60000; // in milliseconds
const expectedDate = new Date(originalDate.getTime() + timezoneOffset);
console.debug("originalDate: ", originalDate, "expectedDate: ", expectedDate);
expect(offsetFromUtc(originalDate)).toEqual(expectedDate);
});
});

describe("seaToolFriendlyTimestamp", () => {
it("converts given date to a time string representing the given date", () => {
const originalDate = new Date("January 1, 2000 12:00:00");
const timezoneOffset = originalDate.getTimezoneOffset() * 60000; // in milliseconds
const expectedDate = new Date(originalDate.getTime() - timezoneOffset);
expect(seaToolFriendlyTimestamp(originalDate)).toEqual(expectedDate.getTime());
it("should return timestamp representing today at midnight UTC time", () => {
const todayDateStr = format(new Date(), "yyyy-MM-dd");
const expectedTimestamp = Date.parse(`${todayDateStr}T00:00:00.000Z`);
expect(seaToolFriendlyTimestamp()).toEqual(expectedTimestamp);
});
});

describe("formatSeatoolDate", () => {
it("formats a SEATool date to a user-friendly format", () => {
it("should format a SEATool date to a user-friendly format", () => {
const originalDate = new Date("2000-01-01T00:00:00.000Z");
expect(formatSeatoolDate(originalDate.toISOString())).toEqual("01/01/2000");
});

it("should convert time to UTC to handle timezone differences", () => {
const originalDate = new Date("Fri Dec 31 1999 19:00:00 GMT-0500 (Eastern Standard Time)");
expect(formatSeatoolDate(originalDate.toISOString())).toEqual("01/01/2000");
});
});

describe("getNextBusinessDayTimestamp", () => {
it("identifies weekenends", () => {
const testDate = new Date(2024, 0, 27, 12, 0, 0); // Saturday, noon, utc
it("identifies weekends", () => {
const testDate = new Date(Date.UTC(2024, 0, 27, 12, 0, 0)); // Saturday, noon, utc
const nextDate = getNextBusinessDayTimestamp(testDate);
expect(nextDate).toEqual(Date.UTC(2024, 0, 29)); // Monday, midnight, utc
});

it("identifies holidays", () => {
const testDate = new Date(2024, 0, 15, 12, 0, 0); // MLK Day, a Monday
const testDate = new Date(Date.UTC(2024, 0, 15, 12, 0, 0)); // MLK Day, a Monday
const nextDate = getNextBusinessDayTimestamp(testDate);
expect(nextDate).toEqual(Date.UTC(2024, 0, 16)); // Tuesday, midnight, utc
});

it("identifies submissions after 5pm eastern", () => {
const testDate = new Date(2024, 0, 17, 23, 0, 0); // Wednesday 11pm utc, Wednesday 6pm eastern
const testDate = new Date(Date.UTC(2024, 0, 17, 23, 0, 0)); // Wednesday 11pm utc, Wednesday 6pm eastern
const nextDate = getNextBusinessDayTimestamp(testDate);
expect(nextDate).toEqual(Date.UTC(2024, 0, 18)); // Thursday, midnight, utc
});

it("identifies submissions before 5pm eastern", () => {
const testDate = new Date(2024, 0, 17, 10, 0, 0); // Wednesday 10am utc, Wednesday 5am eastern
const testDate = new Date(Date.UTC(2024, 0, 17, 10, 0, 0)); // Wednesday 10am utc, Wednesday 5am eastern
const nextDate = getNextBusinessDayTimestamp(testDate);
expect(nextDate).toEqual(Date.UTC(2024, 0, 17)); // Wednesday, midnight, utc
});

it("handles combinations of rule violations", () => {
const testDate = new Date(2024, 0, 12, 23, 0, 0); // Friday 11pm utc, Friday 6pm eastern
const testDate = new Date(Date.UTC(2024, 0, 12, 23, 0, 0)); // Friday 11pm utc, Friday 6pm eastern
const nextDate = getNextBusinessDayTimestamp(testDate);
// Submission is after 5pm, Saturday is a weekend, Sunday is a weekend, and Monday is MLK Day
expect(nextDate).toEqual(Date.UTC(2024, 0, 16)); // Tuesday, midnight utc
});

// TODO: I dont know if its my time zone but this always fails for me in the MST
it.skip("identifies valid business days", () => {
const testDate = new Date(2024, 0, 9, 15, 0, 0); // Tuesday 3pm utc, Tuesday 8am eastern
it("identifies valid business days", () => {
const testDate = new Date(Date.UTC(2024, 0, 9, 15, 0, 0)); // Tuesday 3pm utc, Tuesday 8am eastern
const nextDate = getNextBusinessDayTimestamp(testDate);
expect(nextDate).toEqual(Date.UTC(2024, 0, 9)); // Tuesday, midnight utc
});
Expand Down
89 changes: 45 additions & 44 deletions lib/packages/shared-utils/seatool-date-helper.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,54 @@
import moment from "moment-timezone";
import * as fedHolidays from "@18f/us-federal-holidays";
import { TZDate } from "@date-fns/tz";
import { UTCDate } from "@date-fns/utc";
import { format, startOfDay, isWeekend, addDays } from "date-fns";
import { isAHoliday } from "@18f/us-federal-holidays";

// Takes a local epoch for a moment in time, and returns the UTC epoch for that same moment
export const offsetToUtc = (date: Date): Date => {
return new Date(date.getTime() - date.getTimezoneOffset() * 60000);
/**
* Returns the epoch timestamp for midnight UTC time for the date provided.
* If no date is provided, it returns the timestamp of midnight UTC time today.
*
* @param date the date object or date string to return the timestamp for
* @returns epoch timestamp for midnight UTC of the date or today, if none provided
*/
export const seaToolFriendlyTimestamp = (date?: Date | string): number => {
const utcDate = date ? new UTCDate(date) : new UTCDate();
return startOfDay(utcDate).getTime();
};

// Takes a UTC epoch for a moment in time, and returns the local epoch for that same moment
export const offsetFromUtc = (date: Date): Date => {
return new Date(date.getTime() + date.getTimezoneOffset() * 60000);
};

// This creates a Date for midnight today, then accounts for timezone offset.
export const seaToolFriendlyTimestamp = (date?: Date): number => {
// If you don't pass a date, we assume you want today the timestamp for today, midnight, utc.
if (!date) {
date = new Date();
date.setHours(0, 0, 0, 0);
}
return offsetToUtc(date).getTime();
};

// This takes an epoch string and converts it to a standard format for display
export const formatSeatoolDate = (date: string): string => {
return moment(date).tz("UTC").format("MM/DD/yyyy");
/**
* Returns the formatted date string of the UTC timezone for the date provided.
* If no date is provided, it returns an empty string.
*
* @param date the date object or date string to return the formatted time of
* @returns the `MM/dd/yyyy` formatted date string for the UTC time of the date provided
* or an empty string if no date was provided
*/
export const formatSeatoolDate = (date?: Date | string): string => {
if (!date) return "";
return format(new UTCDate(date), "MM/dd/yyyy");
};

/**
* Returns the epoch timestamp for midnight UTC time for the date provided.
* If no date is provided, it returns the timestamp of midnight UTC time today.
* If the date is after 5pm Eastern time, it returns midnight UTC the next day.
* If the date is on a federal holiday or weekend, it returns midnight UTC of the
* next business day.
*
* @param date the date object to return the timestamp for
* @returns epoch timestamp for midnight UTC of the date or today, if none provided
*/
export const getNextBusinessDayTimestamp = (date: Date = new Date()): number => {
const localeStringDate = date.toLocaleString("en-US", {
timeZone: "America/New_York",
dateStyle: "short",
});
const localeStringHours24 = date.toLocaleString("en-US", {
timeZone: "America/New_York",
hour: "numeric",
hour12: false,
});
const localeDate = new Date(localeStringDate);
const after5pmEST = parseInt(localeStringHours24, 10) >= 17;
const isHoliday = fedHolidays.isAHoliday(localeDate);
const isWeekend = !(localeDate.getDay() % 6);
if (after5pmEST || isHoliday || isWeekend) {
const nextDate = localeDate;
nextDate.setDate(nextDate.getDate() + 1);
nextDate.setHours(12, 0, 0, 0);
return getNextBusinessDayTimestamp(nextDate);
// Get the date in Eastern time
const nyDateTime = new TZDate(date.toISOString(), "America/New_York");

// Check if the time is after 5pm Eastern time or if the day is not a business day.
// If any of those are true, check again for the next day.
if (nyDateTime.getHours() >= 17 || isAHoliday(nyDateTime) || isWeekend(nyDateTime)) {
return getNextBusinessDayTimestamp(startOfDay(addDays(nyDateTime, 1)));
}

// Return the next business day's epoch for midnight UTC
const ret = offsetToUtc(localeDate).getTime();
return ret;
// If the date is a business day before 5pm Eastern time, return the timestamp
// of midnight UTC on that day
return startOfDay(new UTCDate(date)).getTime();
};
Loading

0 comments on commit 5959c18

Please sign in to comment.