Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#3738 - E2E Tests SIMS to SFAS #3847

Merged
merged 8 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import { DeepMocked } from "@golevelup/ts-jest";
import MockDate from "mockdate";
import * as faker from "faker";
import { INestApplication } from "@nestjs/common";
import { formatDate, QueueNames } from "@sims/utilities";
import {
createTestingAppModule,
describeProcessorRootTest,
mockBullJob,
} from "../../../../../test/helpers";
import {
E2EDataSources,
createE2EDataSources,
createFakeCASSupplier,
createFakeUser,
saveFakeApplicationDisbursements,
saveFakeStudent,
} from "@sims/test-utils";
import * as Client from "ssh2-sftp-client";
import { SIMSToSFASIntegrationScheduler } from "../sims-to-sfas-integration.scheduler";
import { OfferingIntensity, Student, SupplierStatus } from "@sims/sims-db";
import { getUploadedFile } from "@sims/test-utils/mocks";
import { addMilliSeconds, addYears } from "@sims/test-utils/utils";

describe(describeProcessorRootTest(QueueNames.SIMSToSFASIntegration), () => {
let app: INestApplication;
let processor: SIMSToSFASIntegrationScheduler;
let db: E2EDataSources;
let sftpClientMock: DeepMocked<Client>;
let latestBridgeFileDate: Date;
let simsDataUpdatedDate: Date;
let mockedCurrentDate: Date;
const DATE_FORMAT = "YYYYMMDD";

beforeAll(async () => {
const { nestApplication, dataSource, sshClientMock } =
await createTestingAppModule();
app = nestApplication;
db = createE2EDataSources(dataSource);
sftpClientMock = sshClientMock;
// Processor under test.
processor = app.get(SIMSToSFASIntegrationScheduler);
});

beforeEach(async () => {
// Reset all SFAS bridge logs.
await db.sfasBridgeLog.delete({});
// Set the start date and end date of the bridge file to be after 10 years
// to ensure that data produced by other tests will not affect the results of this test.
latestBridgeFileDate = addYears(10);
// Set the data updated date and mocked current date in milliseconds later than
// the latest bridge file date to ensure that it holds integrity only for the particular test scenario
// and not having to update the database for each test case
// which could potentially have various columns to be updated.
simsDataUpdatedDate = addMilliSeconds(10, latestBridgeFileDate);
mockedCurrentDate = addMilliSeconds(10, simsDataUpdatedDate);
});

afterEach(async () => {
// Reset the current date mock.
MockDate.reset();
});

it(
"Should generate a SIMS to SFAS bridge file when there is an update on student data " +
"between the most recent bridge file date and the current bridge file execution date.",
async () => {
// Arrange
// Create bridge file log.
await db.sfasBridgeLog.insert({
referenceDate: latestBridgeFileDate,
generatedFileName: "DummyFileName.TXT",
bidyashish marked this conversation as resolved.
Show resolved Hide resolved
});

// Student created with expected first name, last name and more importantly the updated date
// to fall between the most recent bridge file date and the mocked current date.
const student = await createStudentWithExpectedData(simsDataUpdatedDate);
// Student has submitted an application.
await saveFakeApplicationDisbursements(
db.dataSource,
{ student },
{ offeringIntensity: OfferingIntensity.partTime },
);

// Queued job.
const mockedJob = mockBullJob<void>();

// Mock the current date.
MockDate.set(mockedCurrentDate);

// Expected file name.
const expectedFileName = buildExpectedFileName(mockedCurrentDate);
bidyashish marked this conversation as resolved.
Show resolved Hide resolved

// Act
const processingResult = await processor.generateSFASBridgeFile(
mockedJob.job,
);

// Assert
// Assert process result.
expect(processingResult).toEqual([
"Process finalized with success.",
"Student records sent: 1.",
`Uploaded file name: ${expectedFileName}.`,
]);
expect(
mockedJob.containLogMessages([
`Processing data since ${latestBridgeFileDate} until ${mockedCurrentDate}.`,
"Found 1 student(s) with updates.",
`SIMS to SFAS file ${expectedFileName} has been uploaded successfully.`,
`SIMS to SFAS file log has been created with file name ${expectedFileName} and reference date ${mockedCurrentDate}.`,
]),
).toBe(true);
const uploadedFile = getUploadedFile(sftpClientMock);
const [header, studentRecord, footer] = uploadedFile.fileLines;
expect(header).toBe(buildHeader(mockedCurrentDate));
expect(footer).toBe("999000000001");
expect(studentRecord).toBe(buildStudentRecord(student));
// Check the database for creation of SFAS bridge log.
const uploadedFileLog = await db.sfasBridgeLog.existsBy({
generatedFileName: expectedFileName,
referenceDate: mockedCurrentDate,
});
expect(uploadedFileLog).toBe(true);
},
);

it(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we be consistent about where the white space goes? This description was added twice. I would recommend adding to the end of the lines only.

"Should not generate a SIMS to SFAS bridge file when there is an update on student data but the student " +
"does not have any submitted application.",
async () => {
// Arrange
// Create bridge file log.
await db.sfasBridgeLog.insert({
referenceDate: latestBridgeFileDate,
generatedFileName: "DummyFileName.TXT",
});

// Student created with expected first name, last name and more importantly the updated date
// to fall between the most recent bridge file date and the mocked current date.
await createStudentWithExpectedData(simsDataUpdatedDate);

// Queued job.
const mockedJob = mockBullJob<void>();

// Mock the current date.
MockDate.set(mockedCurrentDate);
bidyashish marked this conversation as resolved.
Show resolved Hide resolved

// Act
const processingResult = await processor.generateSFASBridgeFile(
mockedJob.job,
);

// Assert
// Assert process result.
expect(processingResult).toEqual([
"Process finalized with success.",
"Student records sent: 0.",
"Uploaded file name: none.",
]);
expect(
mockedJob.containLogMessages([
"There is no SIMS to SFAS updates to process.",
]),
).toBe(true);
},
);

/**
* Creates a student with expected first name, last name and updated date.
* @param expectedUpdatedDate expected updated date.
* @param options optional params.
* - `expectedCASDetails` expected CAS details.
* @returns created student.
*/
async function createStudentWithExpectedData(
andrewsignori-aot marked this conversation as resolved.
Show resolved Hide resolved
expectedUpdatedDate: Date,
options?: {
expectedCASDetails?: { supplierNumber: string; supplierSiteCode: string };
},
): Promise<Student> {
const user = createFakeUser();
// Name with fixed length will be easy to build the expected file data.
user.firstName = faker.random.alpha({ count: 15 });
user.lastName = faker.random.alpha({ count: 25 });
// Create student with expected first name, last name and updated date.
const student = await saveFakeStudent(db.dataSource, { user });
// Create CAS supplier.
const casSupplier = createFakeCASSupplier(
{
student,
auditUser: student.user,
},
{
supplierStatus: SupplierStatus.Verified,
},
);
// Update CAS details as expected.
casSupplier.supplierNumber =
options?.expectedCASDetails?.supplierNumber ?? null;
casSupplier.supplierAddress.supplierSiteCode =
options?.expectedCASDetails?.supplierSiteCode ?? null;
await db.casSupplier.save(casSupplier);

// Set the student profile updated date to fall between the most recent bridge file date and the mocked current date.
// Set the CAS supplier.
await db.student.update(
{ id: student.id },
{ casSupplier, updatedAt: expectedUpdatedDate },
);
return student;
}

/**
* Build expected file name.
* @param bridgeFileExtractedDate bridge file extracted date.
* @returns expected file name.
*/
function buildExpectedFileName(bridgeFileExtractedDate: Date) {
return `SIMS-TO-SFAS-${formatDate(
bridgeFileExtractedDate,
"YYYYMMDD-HHmmss",
)}.TXT`;
}

/**
* Build header.
* @param bridgeFileExtractedDate bridge file extracted date.
* @returns header.
*/
function buildHeader(bridgeFileExtractedDate: Date) {
const creationDate = formatDate(bridgeFileExtractedDate, "YYYYMMDDHHmmss");
return `100PSFSSIMS to SFAS BRIDGE ${creationDate}`;
}

/**
* Build student record.
* @param student student.
* @returns student record.
*/
function buildStudentRecord(student: Student): string {
return `200${student.id.toString().padStart(10, "0")}${
student.user.firstName
}${student.user.lastName}${formatDate(student.birthDate, DATE_FORMAT)}${
student.sinValidation.sin
}N N 000000000000000000000000000000`;
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class SIMSToSFASProcessingService {
}

processSummary.info(
`Found ${uniqueStudentIds.length} students with updates.`,
`Found ${uniqueStudentIds.length} student(s) with updates.`,
);
const studentDetails =
await this.simsToSFASService.getStudentRecordsByStudentIds(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
CASSupplier,
ApplicationRestrictionBypass,
BetaUsersAuthorizations,
SFASBridgeLog,
} from "@sims/sims-db";
import { DataSource, Repository } from "typeorm";

Expand Down Expand Up @@ -147,6 +148,7 @@ export function createE2EDataSources(dataSource: DataSource): E2EDataSources {
),
studentLoanBalance: dataSource.getRepository(StudentLoanBalance),
eCertFeedbackError: dataSource.getRepository(ECertFeedbackError),
sfasBridgeLog: dataSource.getRepository(SFASBridgeLog),
};
}

Expand Down Expand Up @@ -211,4 +213,5 @@ export interface E2EDataSources {
applicationOfferingChangeRequest: Repository<ApplicationOfferingChangeRequest>;
studentLoanBalance: Repository<StudentLoanBalance>;
eCertFeedbackError: Repository<ECertFeedbackError>;
sfasBridgeLog: Repository<SFASBridgeLog>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ export function createFakeStudent(
*/
export async function saveFakeStudent(
dataSource: DataSource,
relations?: { student?: Student; user?: User; sinValidation?: SINValidation },
relations?: {
student?: Student;
user?: User;
sinValidation?: SINValidation;
},
options?: {
initialValue?: Partial<Student>;
sinValidationInitialValue?: Partial<SINValidation>;
Expand Down
27 changes: 27 additions & 0 deletions sources/packages/backend/libs/test-utils/src/utils/date-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,30 @@ import * as dayjs from "dayjs";
export function isValidFileTimestamp(timestamp: string): boolean {
return dayjs(timestamp, "YYYYMMDD-HHmmssSSS").isValid();
}

/**
* Add years to a given date.
* @param yearsToAdd number of years to be added.
* @param date date.
* @returns a new date with years added.
*/
export const addYears = (yearsToAdd: number, date?: Date | string): Date => {
return dayjs(date ?? new Date())
.add(yearsToAdd, "year")
.toDate();
};

/**
* Add milliseconds to a given date.
* @param yearsToAdd number of years to be added.
* @param date date.
* @returns a new date with years added.
*/
export const addMilliSeconds = (
milliSecondsToAdd: number,
date?: Date | string,
): Date => {
return dayjs(date ?? new Date())
.add(milliSecondsToAdd, "millisecond")
.toDate();
};
7 changes: 7 additions & 0 deletions sources/packages/backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions sources/packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"faker": "^5.2.0",
"find-config": "^1.0.0",
"jest": "^29.6.4",
"mockdate": "^3.0.5",
"prettier": "^2.1.2",
"supertest": "^6.3.3",
"ts-jest": "^29.1.1",
Expand Down