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

#1851 - Enable File Upload for Public Institution User #1893

Merged
merged 13 commits into from
Apr 25, 2023
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { HttpStatus, INestApplication } from "@nestjs/common";
import * as request from "supertest";
import { DataSource, Repository } from "typeorm";
import {
authorizeUserTokenForLocation,
BEARER_AUTH_TYPE,
createTestingAppModule,
getAuthRelatedEntities,
getInstitutionToken,
InstitutionTokenTypes,
} from "../../../../testHelpers";
import {
createFakeInstitutionLocation,
createFakeApplication,
saveFakeStudent,
saveFakeStudentFileUpload,
} from "@sims/test-utils";
import {
Application,
FileOriginType,
Institution,
InstitutionLocation,
} from "@sims/sims-db";

describe("StudentInstitutionsController(e2e)-getStudentFileUploads", () => {
let app: INestApplication;
let appDataSource: DataSource;
let collegeF: Institution;
let collegeFLocation: InstitutionLocation;
let applicationRepo: Repository<Application>;

beforeAll(async () => {
const { nestApplication, dataSource } = await createTestingAppModule();
app = nestApplication;
appDataSource = dataSource;

const { institution } = await getAuthRelatedEntities(
appDataSource,
InstitutionTokenTypes.CollegeFUser,
);
collegeF = institution;
collegeFLocation = createFakeInstitutionLocation(collegeF);
await authorizeUserTokenForLocation(
appDataSource,
InstitutionTokenTypes.CollegeFUser,
collegeFLocation,
);
applicationRepo = appDataSource.getRepository(Application);
});

it("Should get the student file uploads when student has at least one application submitted for the institution.", async () => {
// Arrange.
// Student who has application submitted to institution.
Copy link
Collaborator

Choose a reason for hiding this comment

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

comment Arrange is missing.

const student = await saveFakeStudent(appDataSource);
const application = createFakeApplication({
location: collegeFLocation,
student,
});
await applicationRepo.save(application);

const institutionUserToken = await getInstitutionToken(
InstitutionTokenTypes.CollegeFUser,
);

// Save fake file upload for the student.
const studentUploadedFile = await saveFakeStudentFileUpload(appDataSource, {
student,
creator: student.user,
});

// Endpoint to test.
const endpoint = `/institutions/student/${student.id}/documents`;

// Act/Assert.
await request(app.getHttpServer())
.get(endpoint)
.auth(institutionUserToken, BEARER_AUTH_TYPE)
.expect(HttpStatus.OK)
.expect([
{
fileName: studentUploadedFile.fileName,
uniqueFileName: studentUploadedFile.uniqueFileName,
metadata: studentUploadedFile.metadata,
groupName: "Ministry communications",
updatedAt: studentUploadedFile.updatedAt.toISOString(),
fileOrigin: studentUploadedFile.fileOrigin,
},
]);
});

it("Should not get the student file uploads when student has at least one application submitted for the institution but the fileOrigin is set to Temporary", async () => {
// Arrange.
// Student who has application submitted to institution.
const student = await saveFakeStudent(appDataSource);
const application = createFakeApplication({
location: collegeFLocation,
student,
});
await applicationRepo.save(application);

const institutionUserToken = await getInstitutionToken(
InstitutionTokenTypes.CollegeFUser,
);

// Save fake file upload for the student.
await saveFakeStudentFileUpload(
appDataSource,
{
student,
creator: student.user,
},
{ fileOrigin: FileOriginType.Temporary },
);

// Endpoint to test.
const endpoint = `/institutions/student/${student.id}/documents`;

// Act/Assert.
await request(app.getHttpServer())
.get(endpoint)
.auth(institutionUserToken, BEARER_AUTH_TYPE)
.expect(HttpStatus.OK)
.expect([]);
});

afterAll(async () => {
await app?.close();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,9 @@ export class StudentUploadFileAPIOutDTO {
}

/**
* AEST user to view student uploaded documents.
* AEST / Institution user to view student uploaded documents.
*/
export class AESTStudentFileAPIOutDTO extends StudentUploadFileAPIOutDTO {
export class StudentFileDetailsAPIOutDTO extends StudentUploadFileAPIOutDTO {
metadata: StudentFileMetadataAPIOutDTO;
groupName: string;
updatedAt: Date;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { UserGroups } from "../../auth/user-groups.enum";
import BaseController from "../BaseController";
import {
AESTFileUploadToStudentAPIInDTO,
AESTStudentFileAPIOutDTO,
StudentFileDetailsAPIOutDTO,
AESTStudentProfileAPIOutDTO,
StudentSearchAPIInDTO,
ApplicationSummaryAPIOutDTO,
Expand Down Expand Up @@ -103,18 +103,10 @@ export class StudentAESTController extends BaseController {
@Get(":studentId/documents")
async getAESTStudentFiles(
@Param("studentId", ParseIntPipe) studentId: number,
): Promise<AESTStudentFileAPIOutDTO[]> {
const studentDocuments = await this.fileService.getStudentUploadedFiles(
studentId,
);
return studentDocuments.map((studentDocument) => ({
fileName: studentDocument.fileName,
uniqueFileName: studentDocument.uniqueFileName,
metadata: studentDocument.metadata,
groupName: studentDocument.groupName,
updatedAt: studentDocument.updatedAt,
fileOrigin: studentDocument.fileOrigin,
}));
): Promise<StudentFileDetailsAPIOutDTO[]> {
return this.studentControllerService.getStudentUploadedFiles(studentId, {
extendedDetails: true,
}) as Promise<StudentFileDetailsAPIOutDTO[]>;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
ApplicationSummaryAPIOutDTO,
SearchStudentAPIOutDTO,
StudentProfileAPIOutDTO,
StudentFileDetailsAPIOutDTO,
StudentUploadFileAPIOutDTO,
} from "./models/student.dto";
import { transformAddressDetailsForAddressBlockForm } from "../utils/address-utils";

Expand Down Expand Up @@ -135,6 +137,34 @@ export class StudentControllerService {
};
}

/**
* Get the student uploaded files.
* @param studentId student id to retrieve the data.
* @param options related to student file uploads
* - `extendedDetails` option to specify the additional properties to be returned (metadata, groupName, updatedAt) as a part of the studentDocuments.
* @returns student file details.
*/
async getStudentUploadedFiles(
Copy link
Collaborator

@dheepak-aot dheepak-aot Apr 21, 2023

Choose a reason for hiding this comment

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

The controller service can be used like this to adapt student file upload as well.

async getStudentUploadedFiles(
    studentId: number,
    options?: { extendedDetails: true },
  ): Promise<StudentFileDetailsAPIOutDTO[] | StudentUploadFileAPIOutDTO[]> {
    const studentDocuments = await this.fileService.getStudentUploadedFiles(
      studentId,
    );
    return studentDocuments.map((studentDocument) => ({
      fileName: studentDocument.fileName,
      uniqueFileName: studentDocument.uniqueFileName,
      fileOrigin: studentDocument.fileOrigin,
      metadata: options?.extendedDetails? studentDocument.metadata : undefined,
      groupName: options?.extendedDetails? studentDocument.groupName : undefined,
      updatedAt: options?.extendedDetails ? studentDocument.updatedAt : undefined,
    }));
  }

student.institutions.controller

@Get(":studentId/documents")
  async getInstitutionStudentFiles(
    @Param("studentId", ParseIntPipe) studentId: number,
  ): Promise<StudentFileDetailsAPIOutDTO[]> {
    return this.studentControllerService.getStudentUploadedFiles(studentId, {
      details: true,
    }) as Promise<StudentFileDetailsAPIOutDTO[]>;
  }

student.students.controller

@Get("documents")
  async getStudentFiles(
    @UserToken() studentUserToken: StudentUserToken,
  ): Promise<StudentUploadFileAPIOutDTO[]> {
    return this.studentControllerService.getStudentUploadedFiles(
      studentUserToken.studentId,
    ) as Promise<StudentUploadFileAPIOutDTO[]>;
  }

Copy link
Collaborator

Choose a reason for hiding this comment

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

I am not sure if I would be on the same page about this.

Copy link
Collaborator

@dheepak-aot dheepak-aot Apr 21, 2023

Choose a reason for hiding this comment

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

I am confused here on why. we can talk if needed.

Copy link
Collaborator

Choose a reason for hiding this comment

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

After our conversation, I got that the 3 endpoints are actually the same resource /documents accessed by the three different clients (Students, Ministry, Institution) hence I am ok if your suggestion @dheepak-aot.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I did the change.

Copy link
Collaborator

@andrewsignori-aot andrewsignori-aot Apr 24, 2023

Choose a reason for hiding this comment

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

I will not go back in the decision to have the method centralized but in the future if it comes with the price needing to force the cast at the controller (e.g. as Promise<StudentFileDetailsAPIOutDTO[]>) I would think twice about reusing the same method.
@dheepak-aot FYI

Copy link
Collaborator

Choose a reason for hiding this comment

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

So in that case, I assume we would not consider using a controller service method just to share the transformation code alone.

studentId: number,
options?: { extendedDetails: boolean },
): Promise<StudentFileDetailsAPIOutDTO[] | StudentUploadFileAPIOutDTO[]> {
const studentDocuments = await this.fileService.getStudentUploadedFiles(
studentId,
);
return studentDocuments.map((studentDocument) => ({
fileName: studentDocument.fileName,
uniqueFileName: studentDocument.uniqueFileName,
fileOrigin: studentDocument.fileOrigin,
metadata: options?.extendedDetails ? studentDocument.metadata : undefined,
groupName: options?.extendedDetails
? studentDocument.groupName
: undefined,
updatedAt: options?.extendedDetails
? studentDocument.updatedAt
: undefined,
}));
}

/**
* Get all the applications that belong to student.
* This API will be used by students.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
Post,
} from "@nestjs/common";
import { ApiNotFoundResponse, ApiTags } from "@nestjs/swagger";
import { StudentService } from "../../services";
import { StudentService, StudentFileService } from "../../services";
import { ClientTypeBaseRoute } from "../../types";
import { AuthorizedParties } from "../../auth/authorized-parties.enum";
import { AllowAuthorizedParty, UserToken } from "../../auth/decorators";
Expand All @@ -18,6 +18,7 @@ import {
StudentSearchAPIInDTO,
SearchStudentAPIOutDTO,
StudentProfileAPIOutDTO,
StudentFileDetailsAPIOutDTO,
} from "./models/student.dto";
import { IInstitutionUserToken } from "../../auth";
import { StudentControllerService } from "./student.controller.service";
Expand All @@ -31,6 +32,7 @@ import { StudentControllerService } from "./student.controller.service";
export class StudentInstitutionsController extends BaseController {
constructor(
private readonly studentService: StudentService,
private readonly fileService: StudentFileService,
private readonly studentControllerService: StudentControllerService,
) {
super();
Expand Down Expand Up @@ -73,4 +75,18 @@ export class StudentInstitutionsController extends BaseController {
): Promise<StudentProfileAPIOutDTO> {
return this.studentControllerService.getStudentProfile(studentId);
}

/**
* Get all the documents uploaded by student.
* @param studentId student id.
* @returns list of student documents.
*/
@Get(":studentId/documents")
async getInstitutionStudentFiles(
@Param("studentId", ParseIntPipe) studentId: number,
): Promise<StudentFileDetailsAPIOutDTO[]> {
return this.studentControllerService.getStudentUploadedFiles(studentId, {
extendedDetails: true,
}) as Promise<StudentFileDetailsAPIOutDTO[]>;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,14 +179,10 @@ export class StudentStudentsController extends BaseController {
async getStudentFiles(
@UserToken() studentUserToken: StudentUserToken,
): Promise<StudentUploadFileAPIOutDTO[]> {
const studentDocuments = await this.fileService.getStudentUploadedFiles(
return this.studentControllerService.getStudentUploadedFiles(
studentUserToken.studentId,
);
return studentDocuments.map((studentDocument) => ({
fileName: studentDocument.fileName,
uniqueFileName: studentDocument.uniqueFileName,
fileOrigin: studentDocument.fileOrigin,
}));
{ extendedDetails: false },
) as Promise<StudentUploadFileAPIOutDTO[]>;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as faker from "faker";
import { FileOriginType, Student, StudentFile, User } from "@sims/sims-db";
import { DataSource } from "typeorm";
import { createFakeStudent } from "./student";

/**
* Create fake student file upload object.
* @params relations entity relations
* - `student` related student relation.
* - `creator` related user relation.
* @param options related to StudentFile
* - `fileOrigin` option for specifying the fileOrigin
* @returns created studentFile object.
*/
export function createFakeStudentFileUpload(
relations?: {
student?: Student;
creator?: User;
},
options?: {
fileOrigin?: FileOriginType;
},
): StudentFile {
const studentFile = new StudentFile();
studentFile.fileName = faker.system.fileName();
studentFile.uniqueFileName =
studentFile.fileName + faker.random.uuid() + "." + faker.system.fileType();
studentFile.groupName = "Ministry communications";
studentFile.mimeType = faker.system.mimeType();
studentFile.fileContent = Buffer.from(faker.random.words(50), "utf-8");
studentFile.student = relations?.student ?? createFakeStudent();
studentFile.creator = relations?.creator;
studentFile.fileOrigin = options?.fileOrigin ?? FileOriginType.Ministry;
return studentFile;
}

/**
* Save fake student file upload.
* @param dataSource data source to persist studentFileUpload.
* @param relations entity relations.
* - `student` related student relation.
* - `creator` related user relation.
* @param options related to StudentFile
* - `fileOrigin` option for specifying the fileOrigin
* @returns persisted studentFile.
*/
export async function saveFakeStudentFileUpload(
dataSource: DataSource,
relations?: { student?: Student; creator?: User },
options?: { fileOrigin: FileOriginType },
): Promise<StudentFile> {
const studentFile = createFakeStudentFileUpload(relations, options);
const studentFileRepo = dataSource.getRepository(StudentFile);
return studentFileRepo.save(studentFile);
}
1 change: 1 addition & 0 deletions sources/packages/backend/libs/test-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from "./factories/application-exception-request";
export * from "./factories/student-scholastic-standing";
export * from "./factories/student-appeal";
export * from "./factories/student-appeal-request";
export * from "./factories/student-file-uploads";
export * from "./models/common.model";
export * from "./mocks/zeebe-client-mock";
export * from "./mocks/ssh-service-mock";
Expand Down
Loading