Skip to content

Commit

Permalink
#260 - Merge Hotfix v1.14.1 into Release v1.15.0 (#3885)
Browse files Browse the repository at this point in the history
### The following are achieved in this hotfix:

- SIN Validation - Gender Field Limit
(#3839)
- SINF restriction bridge mapping and rename to SINR
(#3846)
- Virus Scanning - Not functioning PROD
(#3854)
- Modify process that reads SIN & CRA verification response files
(#3835)
- Hotfix: Bulk uploads: Unexpected error while uploading the file
displays when "Validate" or "Create now" are selected for valid bulk
upload
- Virus Scanning - Empty File Check
- Hotfix: Change file naming convention of the SIN validation request
file
- Hotfix: Fix for withdraw upload
- Hotfix: Name change for CRA Response file
- Parse the last 2 characters in NEB as decimal places
- Update PTMSFAA File Name

---------

Co-authored-by: Ashish <[email protected]>
Co-authored-by: Andre Pestana <[email protected]>
Co-authored-by: Lewis Chen <[email protected]>
Co-authored-by: Andrew Boni Signori <[email protected]>
Co-authored-by: Dheepak Ramanathan <[email protected]>
  • Loading branch information
6 people authored Nov 5, 2024
1 parent 20fcda4 commit a94ebce
Show file tree
Hide file tree
Showing 18 changed files with 108 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// One possible reason could be the ClamAV server being down.
export const CONNECTION_FAILED = "CONNECTION_FAILED";
export const FILE_NOT_FOUND = "FILE_NOT_FOUND";
export const EMPTY_FILE = "EMPTY_FILE";
export const FILE_SCANNING_FAILED = "FILE_SCANNING_FAILED";
export const SERVER_UNAVAILABLE = "SERVER_UNAVAILABLE";
export const UNKNOWN_ERROR = "UNKNOWN_ERROR";
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe(
getProcessDateFromMSFAARequestContent(createMSFAARequestContentMock);
const uploadedFile = getUploadedFile(sftpClientMock);
expect(uploadedFile.remoteFilePath).toBe(
`MSFT-Request\\DPBC.EDU.MSFA.SENT.PT.${processDateFormatted}.001`,
`MSFT-Request\\DPBC.EDU.MSFA.SENT.PT.${processDateFormatted}.010`,
);
// Assert process result.
expect(msfaaRequestResults).toStrictEqual([
Expand All @@ -106,7 +106,7 @@ describe(
] = uploadedFile.fileLines;
// Validate header.
expect(header).toBe(
`100BC MSFAA SENT ${processDateFormatted}${processTimeFormatted}000001 `,
`100BC MSFAA SENT ${processDateFormatted}${processTimeFormatted}000010 `,
);
// Validate records.
expect(msfaaPartTimeMarried).toBe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ describe(describeProcessorRootTest(QueueNames.SFASIntegration), () => {
ppdStatusDate: "1967-07-22",
msfaaNumber: "9876543210",
msfaaSignedDate: "2024-07-12",
neb: 5000,
neb: 50,
bcgg: 5000,
lfp: 7560.5,
pal: 7560.5,
Expand All @@ -376,7 +376,7 @@ describe(describeProcessorRootTest(QueueNames.SFASIntegration), () => {
ppdStatusDate: null,
msfaaNumber: "9876543211",
msfaaSignedDate: "2024-07-13",
neb: 5000,
neb: 50,
bcgg: 5000,
lfp: 11040,
pal: 11040,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,37 @@ import { VirusScanProcessor } from "../virus-scan.processor";
import { VirusScanStatus } from "@sims/sims-db";
import * as path from "path";
import { INFECTED_FILENAME_SUFFIX } from "../../../services";
import { ObjectStorageService } from "@sims/integrations/object-storage";
import {
createFakeGetObjectResponse,
resetObjectStorageServiceMock,
} from "@sims/test-utils/mocks";

describe(describeProcessorRootTest(QueueNames.FileVirusScanProcessor), () => {
let app: INestApplication;
let db: E2EDataSources;
let processor: VirusScanProcessor;
let clamAVServiceMock: ClamAVService;
let objectStorageService: ObjectStorageService;

beforeAll(async () => {
const {
nestApplication,
dataSource,
clamAVServiceMock: clamAVServiceFromAppModule,
objectStorageServiceMock,
} = await createTestingAppModule();
app = nestApplication;
// Processor under test.
processor = app.get(VirusScanProcessor);
clamAVServiceMock = clamAVServiceFromAppModule;
db = createE2EDataSources(dataSource);
objectStorageService = objectStorageServiceMock;
});

beforeEach(() => {
jest.clearAllMocks();
resetObjectStorageServiceMock(objectStorageService);
});

it("Should throw an error when the student file is not found during virus scanning.", async () => {
Expand Down Expand Up @@ -65,6 +74,32 @@ describe(describeProcessorRootTest(QueueNames.FileVirusScanProcessor), () => {
).toBe(true);
});

it("Should throw an error when the student file has no content.", async () => {
// Arrange
const studentFile = createFakeStudentFileUpload();
studentFile.virusScanStatus = VirusScanStatus.InProgress;
await db.studentFile.save(studentFile);
const mockedJob = mockBullJob<VirusScanQueueInDTO>({
uniqueFileName: studentFile.uniqueFileName,
fileName: studentFile.fileName,
});
// Mock an empty file.
objectStorageService.getObject = createFakeGetObjectResponse("");

// Act
const errorMessage = `File ${studentFile.uniqueFileName} has no content to be scanned.`;
await expect(
processor.performVirusScan(mockedJob.job),
).rejects.toStrictEqual(new Error(errorMessage));
expect(
mockedJob.containLogMessages([
"Log details",
"Starting virus scan.",
errorMessage,
]),
).toBe(true);
});

it("Should throw an error when the connection to the virus scan server failed.", async () => {
// Arrange
const studentFile = createFakeStudentFileUpload();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
ProcessSummary,
} from "@sims/utilities/logger";
import { logProcessSummaryToJobLogger } from "../../utilities";
import { FILE_NOT_FOUND } from "../../constants/error-code.constants";
import {
EMPTY_FILE,
FILE_NOT_FOUND,
} from "../../constants/error-code.constants";

@Processor(QueueNames.FileVirusScanProcessor)
export class VirusScanProcessor {
Expand All @@ -36,8 +39,9 @@ export class VirusScanProcessor {
} catch (error: unknown) {
if (error instanceof CustomNamedError) {
const errorMessage = error.message;
if (error.name === FILE_NOT_FOUND) {
// If the file is not present in the database, remove the file from the virus scan queue.
if ([FILE_NOT_FOUND, EMPTY_FILE].includes(error.name)) {
// If the file is not present in the database, or its content
// is empty then remove the file from the virus scan queue.
await job.discard();
}
processSummary.error(errorMessage);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ClamAVError, ClamAVService, SystemUsersService } from "@sims/services";
import * as path from "path";
import {
CONNECTION_FAILED,
EMPTY_FILE,
FILE_NOT_FOUND,
FILE_SCANNING_FAILED,
SERVER_UNAVAILABLE,
Expand Down Expand Up @@ -51,9 +52,16 @@ export class StudentFileService extends RecordDataModelService<StudentFile> {
}

// Retrieve the file from the object storage.
const { body } = await this.objectStorageService.getObject(
const { body, contentLength } = await this.objectStorageService.getObject(
studentFile.uniqueFileName,
);
if (contentLength === 0) {
// Empty files are not suitable for virus scanning using passthrough.
throw new CustomNamedError(
`File ${uniqueFileName} has no content to be scanned.`,
EMPTY_FILE,
);
}
let isInfected: boolean | null;
let errorName: string;
let errorMessage = `Unable to scan the file ${uniqueFileName} for viruses.`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import * as path from "path";
import { SshService } from "@sims/integrations/services";
import { FILE_DEFAULT_ENCODING } from "@sims/utilities";

const MOCKED_RESPONSE_FILES = [
"CCRA_RESPONSE_ABCSL00001.TXT",
"CCRA_RESPONSE_ABCSL00002.TXT",
];
const MOCKED_RESPONSE_FILES = ["ABCSA00001.TXT", "PBCSA00002.TXT"];

const SshServiceMock = new SshService();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,10 @@ export class CRAIncomeVerificationProcessingService {
async processResponses(): Promise<ProcessSftpResponseResult[]> {
const remoteFilePaths = await this.craService.getResponseFilesFullPath(
this.ftpResponseFolder,
/CCRA_RESPONSE_[\w]*\.txt/i,
new RegExp(
`${this.configService.craIntegration.environmentCode}BCSA\\d{5}\\.TXT`,
"i",
),
);
const processFiles: ProcessSftpResponseResult[] = [];
for (const remoteFilePath of remoteFilePaths) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { MSFAAIntegrationService } from "./msfaa.integration.service";
import { ESDCFileHandler } from "../esdc-file-handler";
import { ConfigService } from "@sims/utilities/config";
import { MSFAANumberService } from "@sims/integrations/services";
import { MSFAA_SEQUENCE_GAP } from "@sims/services/constants";

@Injectable()
export class MSFAARequestProcessingService extends ESDCFileHandler {
Expand Down Expand Up @@ -76,6 +77,10 @@ export class MSFAARequestProcessingService extends ESDCFileHandler {
)}`,
async (nextSequenceNumber: number, entityManager: EntityManager) => {
try {
this.logger.log(
`Applying MSFAA sequence gap to the sequence number. Current sequence gap ${MSFAA_SEQUENCE_GAP}.`,
);
nextSequenceNumber += MSFAA_SEQUENCE_GAP;
this.logger.log("Creating MSFAA request content...");
// Create the Request content for the MSFAA file by populating the
// header, footer and trailer content.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class SINValidationFileHeader implements FixedFormatFileLine {
getFixedFormat(): string {
const record = new StringBuilder();
record.append(this.recordTypeCode);
record.appendWithStartFiller(this.batchNumber, 8, NUMBER_FILLER);
record.appendWithStartFiller(this.batchNumber, 6, NUMBER_FILLER);
record.appendDate(this.processDate, DATE_FORMAT);
record.append(PROVINCE_CODE);
record.repeatAppend(SPACE_FILLER, 71);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ export class SINValidationIntegrationService extends SFTPIntegrationBase<SINVali
*/
createRequestFileName(sequenceNumber: number): CreateRequestFileNameResult {
const fileNameBuilder = new StringBuilder();
fileNameBuilder.append(this.esdcConfig.environmentCode);
fileNameBuilder.append(PROVINCE_CODE);
fileNameBuilder.appendWithStartFiller(sequenceNumber, 4, NUMBER_FILLER);
fileNameBuilder.append(".OS");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export class SFASIndividualRecord extends SFASRecordIdentification {
* Total Nurses Education Bursary (special_program_award.program_awd_cents, award_cde = "SP04").
*/
get neb(): number {
return +this.line.substring(131, 141);
return parseDecimal(this.line.substring(131, 141));
}
/**
* BC Completion Grant for Graduates (individual_award.award_dlr, award_cde = "BCGG").
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export const ESDC_SIN_VALIDATION_SEQUENCE_GROUP_NAME = "ESDC_SIN_VALIDATION";
*/
export const MSFAA_FULL_TIME_FILE_CODE = "PBC.EDU.MSFA.SENT.";
export const MSFAA_PART_TIME_FILE_CODE = "PBC.EDU.MSFA.SENT.PT.";
/**
* Sequence gap to be added to MSFAA sequence numbers to avoid conflicts
* with the MSFAA sequence numbers generated by the legacy system.
*/
export const MSFAA_SEQUENCE_GAP = 9;

/**
* Report the SFAS import progress every time that certain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,34 @@ export const S3_DEFAULT_MOCKED_FILE_CONTENT = "Some dummy file content.";
*/
export function createObjectStorageServiceMock(): ObjectStorageService {
const mockedObjectStorageService = {} as ObjectStorageService;
mockedObjectStorageService.putObject = jest.fn(() => Promise.resolve());
mockedObjectStorageService.getObject = jest.fn(() => {
const buffer = Buffer.from(S3_DEFAULT_MOCKED_FILE_CONTENT);
resetObjectStorageServiceMock(mockedObjectStorageService);
return mockedObjectStorageService;
}

/**
* Resets the mocked object storage service back to its original mocks.
* @param mock the mocked object storage service to be reset.
*/
export function resetObjectStorageServiceMock(
mock: ObjectStorageService,
): void {
mock.putObject = jest.fn(() => Promise.resolve());
mock.getObject = createFakeGetObjectResponse(S3_DEFAULT_MOCKED_FILE_CONTENT);
}

/**
* Creates a mock implementation for the getObject method of the {@linkcode ObjectStorageService}.
* @param fileContent the content of the file to be returned.
* @returns a jest mock function that resolves a promise.
*/
export function createFakeGetObjectResponse(fileContent: string): jest.Mock {
return jest.fn(() => {
const buffer = Buffer.from(fileContent);
const stream = Readable.from(buffer);
return Promise.resolve({
contentLength: buffer.length,
contentType: "text/html; charset=utf-8",
body: stream,
});
});
return mockedObjectStorageService;
}
13 changes: 6 additions & 7 deletions sources/packages/web/src/views/institution/OfferingsUpload.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
:clearable="true"
:accept="ACCEPTED_FILE_TYPE"
density="compact"
v-model="offeringFiles"
v-model="offeringFile"
label="Offering CSV file"
variant="outlined"
data-cy="fileUpload"
Expand Down Expand Up @@ -225,8 +225,8 @@ export default defineComponent({
const snackBar = useSnackBar();
const validationProcessing = ref(false);
const creationProcessing = ref(false);
// Only one will be used but the component allows multiple.
const offeringFiles = ref<InputFile[]>([]);
// If multiple prop is undefined or false for VFileInput the component returns now a File object.
const offeringFile = ref<File>();
// Possible errors and warnings received upon file upload.
const validationResults = ref([] as OfferingsUploadBulkInsert[]);
const uploadForm = ref({} as VForm);
Expand All @@ -252,10 +252,9 @@ export default defineComponent({
} else {
creationProcessing.value = true;
}
const [fileToUpload] = offeringFiles.value;
const uploadResults =
await EducationProgramOfferingService.shared.offeringBulkInsert(
fileToUpload,
offeringFile.value as Blob,
validationOnly,
(progressEvent: AxiosProgressEvent) => {
uploadProgress.value = progressEvent;
Expand Down Expand Up @@ -318,7 +317,7 @@ export default defineComponent({
const resetForm = () => {
validationResults.value = [];
offeringFiles.value = [];
offeringFile.value = undefined;
csvFileUploadKey.value++;
};
Expand All @@ -332,7 +331,7 @@ export default defineComponent({
creationProcessing,
DEFAULT_PAGE_LIMIT,
PAGINATION_LIST,
offeringFiles,
offeringFile,
uploadFile,
validationResults,
fileValidationRules,
Expand Down
13 changes: 6 additions & 7 deletions sources/packages/web/src/views/institution/WithdrawalUpload.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
:clearable="true"
:accept="ACCEPTED_FILE_TYPE"
density="compact"
v-model="withdrawalFiles"
v-model="withdrawalFile"
label="Withdrawal text file"
variant="outlined"
prepend-icon="fa:fa-solid fa-file-text"
Expand Down Expand Up @@ -181,8 +181,8 @@ export default defineComponent({
const validationProcessing = ref(false);
const creationProcessing = ref(false);
const { dateOnlyLongString, numberEmptyFiller } = useFormatters();
// Only one will be used but the component allows multiple.
const withdrawalFiles = ref<InputFile[]>([]);
// If multiple prop is undefined or false for VFileInput the component returns now a File object.
const withdrawalFile = ref<File>();
// Possible errors and warnings received upon file upload.
const validationResults = ref([] as ApplicationBulkWithdrawal[]);
const uploadForm = ref({} as VForm);
Expand Down Expand Up @@ -210,10 +210,9 @@ export default defineComponent({
} else {
creationProcessing.value = true;
}
const [fileToUpload] = withdrawalFiles.value;
const uploadResults =
await ScholasticStandingService.shared.applicationBulkWithdrawal(
fileToUpload,
withdrawalFile.value as Blob,
validationOnly,
(progressEvent: AxiosProgressEvent) => {
uploadProgress.value = progressEvent;
Expand Down Expand Up @@ -265,7 +264,7 @@ export default defineComponent({
const resetForm = () => {
validationResults.value = [];
withdrawalFiles.value = [];
withdrawalFile.value = undefined;
};
const loading = computed(
Expand All @@ -279,7 +278,7 @@ export default defineComponent({
DEFAULT_PAGE_LIMIT,
ITEMS_PER_PAGE,
PAGINATION_LIST,
withdrawalFiles,
withdrawalFile,
uploadFile,
validationResults,
fileValidationRules,
Expand Down

0 comments on commit a94ebce

Please sign in to comment.