test/e2e/api/projectfiles.e2e.spec.tsmodified
import {
    expect,
    app,
    http,
    Server,
    cleanMockSetup,
    closeMockServer,
    createAuthToken,
    setupIAM,
    setupE2EMockEnvVariables,
    createAzureContainerClient,
    authoriseProjectIamAccess,
    Authorities,
    TestProjects,
    chai,
    uuidv4,
} from "../common/e2e-imports";

const FILE_RESOURCE = `${__dirname}/../../resources/upload-file.txt`;
const PROJECT_ID = TestProjects.ProjectId1;
const EMPTY_PROJECT_ID = TestProjects.ProjectId6;

/* eslint-disable @typescript-eslint/no-explicit-any */
describe("Project Files API E2E tests", () => {
    let server: Server;
    let authToken: any;

    before(async () => {
        setupE2EMockEnvVariables();
        await cleanMockSetup();
        await setupIAM();
        server = http.createServer(app);
        server.listen();
        authToken = await createAuthToken("abc");
        await createAzureContainerClient();
    });

    after(() => {
        closeMockServer(server);
    });

    // -------------------------------------------------------------------------
    describe("POST /api/v2/projects/:projectId/files - Upload file", () => {
        beforeEach(async () => {
            await cleanMockSetup();
            await setupIAM();
        });

        it("Should return 201 and fileReferenceId when a single-chunk file is uploaded successfully", async () => {
            await authoriseProjectIamAccess(PROJECT_ID, authToken.token, [Authorities.IMAGE_UPLOAD]);

            const response = await chai
                .request(app)
                .post(`/api/v2/projects/${PROJECT_ID}/files`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .set("content-type", "multipart/form-data")
                .field("fileName", "upload-file.txt")
                .field("xyzDisplayName", "Test File")
                .field("fileType", "Report")
                .field("chunkIndex", "0")
                .field("totalChunks", "1")
                .attach("file", FILE_RESOURCE, "upload-file.txt");

            expect(response.status).to.equal(201);
            expect(response.body).to.have.property("fileReferenceId");
            expect(response.body.fileReferenceId).to.be.a("string");
            expect(response.body).to.have.property("message", "Project file uploaded successfully");
        });

        it("Should return 200 on intermediate chunk and 201 on final chunk for multi-chunk upload", async () => {
            await authoriseProjectIamAccess(PROJECT_ID, authToken.token, [Authorities.IMAGE_UPLOAD]);

            const chunk1 = await chai
                .request(app)
                .post(`/api/v2/projects/${PROJECT_ID}/files`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .set("content-type", "multipart/form-data")
                .field("fileName", "upload-file.txt")
                .field("xyzDisplayName", "Chunked File")
                .field("fileType", "Dashboard")
                .field("chunkIndex", "0")
                .field("totalChunks", "2")
                .attach("file", FILE_RESOURCE, "upload-file.txt");

            expect(chunk1.status).to.equal(200);
            expect(chunk1.body).to.have.property("fileReferenceId");
            expect(chunk1.body).to.have.property("message", "Project file chunk uploaded successfully");

            const fileReferenceId = chunk1.body.fileReferenceId;

            await authoriseProjectIamAccess(PROJECT_ID, authToken.token, [Authorities.IMAGE_UPLOAD]);

            const chunk2 = await chai
                .request(app)
                .post(`/api/v2/projects/${PROJECT_ID}/files`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .set("content-type", "multipart/form-data")
                .field("fileName", "upload-file.txt")
                .field("xyzDisplayName", "Chunked File")
                .field("fileType", "Dashboard")
                .field("chunkIndex", "1")
                .field("totalChunks", "2")
                .field("fileReferenceId", fileReferenceId)
                .attach("file", FILE_RESOURCE, "upload-file.txt");

            expect(chunk2.status).to.equal(201);
            expect(chunk2.body).to.have.property("fileReferenceId", fileReferenceId);
            expect(chunk2.body).to.have.property("message", "Project file uploaded successfully");
        });

        it("Should return 400 when no file is attached", async () => {
            await authoriseProjectIamAccess(PROJECT_ID, authToken.token, [Authorities.IMAGE_UPLOAD]);

            const response = await chai
                .request(app)
                .post(`/api/v2/projects/${PROJECT_ID}/files`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .set("content-type", "multipart/form-data")
                .field("fileName", "upload-file.txt")
                .field("xyzDisplayName", "Test File")
                .field("fileType", "Report")
                .field("chunkIndex", "0")
                .field("totalChunks", "1");

            expect(response.status).to.equal(400);
            expect(response.body).to.have.property("code", "InvalidRequest");
        });

        it("Should return 400 when fileName is missing", async () => {
            await authoriseProjectIamAccess(PROJECT_ID, authToken.token, [Authorities.IMAGE_UPLOAD]);

            const response = await chai
                .request(app)
                .post(`/api/v2/projects/${PROJECT_ID}/files`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .set("content-type", "multipart/form-data")
                .field("xyzDisplayName", "Test File")
                .field("fileType", "Report")
                .field("chunkIndex", "0")
                .field("totalChunks", "1")
                .attach("file", FILE_RESOURCE, "upload-file.txt");

            expect(response.status).to.equal(400);
            expect(response.body).to.have.property("code", "InvalidRequest");
            expect(response.body.message).to.include("fileName");
        });

        it("Should return 400 when xyzDisplayName is missing", async () => {
            await authoriseProjectIamAccess(PROJECT_ID, authToken.token, [Authorities.IMAGE_UPLOAD]);

            const response = await chai
                .request(app)
                .post(`/api/v2/projects/${PROJECT_ID}/files`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .set("content-type", "multipart/form-data")
                .field("fileName", "upload-file.txt")
                .field("fileType", "Report")
                .field("chunkIndex", "0")
                .field("totalChunks", "1")
                .attach("file", FILE_RESOURCE, "upload-file.txt");

            expect(response.status).to.equal(400);
            expect(response.body).to.have.property("code", "InvalidRequest");
            expect(response.body.message).to.include("xyzDisplayName");
        });

        it("Should return 400 when chunkIndex > 0 and fileReferenceId is not provided", async () => {
            await authoriseProjectIamAccess(PROJECT_ID, authToken.token, [Authorities.IMAGE_UPLOAD]);

            const response = await chai
                .request(app)
                .post(`/api/v2/projects/${PROJECT_ID}/files`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .set("content-type", "multipart/form-data")
                .field("fileName", "upload-file.txt")
                .field("xyzDisplayName", "Test File")
                .field("fileType", "Report")
                .field("chunkIndex", "1")
                .field("totalChunks", "3")
                .attach("file", FILE_RESOURCE, "upload-file.txt");

            expect(response.status).to.equal(400);
            expect(response.body).to.have.property("code", "InvalidRequest");
        });

        it("Should return 401 when Authorization header is missing", async () => {
            const response = await chai
                .request(app)
                .post(`/api/v2/projects/${PROJECT_ID}/files`)
                .set("content-type", "multipart/form-data")
                .field("fileName", "upload-file.txt")
                .field("xyzDisplayName", "Test File")
                .field("chunkIndex", "0")
                .field("totalChunks", "1")
                .attach("file", FILE_RESOURCE, "upload-file.txt");

            expect(response.status).to.equal(401);
        });

        it("Should return 403 when caller lacks IMAGE_UPLOAD authority", async () => {
            const response = await chai
                .request(app)
                .post(`/api/v2/projects/${PROJECT_ID}/files`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .set("content-type", "multipart/form-data")
                .field("fileName", "upload-file.txt")
                .field("xyzDisplayName", "Test File")
                .field("fileType", "Report")
                .field("chunkIndex", "0")
                .field("totalChunks", "1")
                .attach("file", FILE_RESOURCE, "upload-file.txt");

            expect(response.status).to.equal(403);
        });
    });

    // -------------------------------------------------------------------------
    describe("GET /api/v2/projects/:projectId/files - List files", () => {
        let uploadedFileReferenceId: string;

        before(async () => {
            await cleanMockSetup();
            await setupIAM();
            await authoriseProjectIamAccess(PROJECT_ID, authToken.token, [Authorities.IMAGE_UPLOAD]);

            const upload = await chai
                .request(app)
                .post(`/api/v2/projects/${PROJECT_ID}/files`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .set("content-type", "multipart/form-data")
                .field("fileName", "upload-file.txt")
                .field("xyzDisplayName", "Listed File")
                .field("chunkIndex", "0")
                .field("totalChunks", "1")
                .attach("file", FILE_RESOURCE, "upload-file.txt");

            expect(upload.status).to.equal(201);
            uploadedFileReferenceId = upload.body.fileReferenceId;
        });

        beforeEach(async () => {
            await cleanMockSetup();
            await setupIAM();
        });

        it("Should return 200 with an array containing the uploaded file metadata", async () => {
            await authoriseProjectIamAccess(PROJECT_ID, authToken.token, [Authorities.IMAGE_VIEW]);

            const response = await chai
                .request(app)
                .get(`/api/v2/projects/${PROJECT_ID}/files`)
                .set("Authorization", `Bearer ${authToken.token}`);

            expect(response.status).to.equal(200);
            expect(response.body).to.be.an("array");
            expect(response.body.length).to.be.at.least(1);

            const file = response.body.find((f: any) => f.fileReferenceId === uploadedFileReferenceId);
            expect(file).to.not.be.undefined;
            expect(file).to.have.property("fileReferenceId", uploadedFileReferenceId);
            expect(file).to.have.property("fileName", "upload-file.txt");
            expect(file).to.have.property("fileExtension", "txt");
            expect(file).to.have.property("xyzDisplayName", "Listed File");
            expect(file).to.have.property("insertedOn");
            expect(file).to.not.have.property("downloadUrl");
        });

        it("Should return 200 with an empty array for a project that has no files", async () => {
            await authoriseProjectIamAccess(EMPTY_PROJECT_ID, authToken.token, [Authorities.IMAGE_VIEW]);

            const response = await chai
                .request(app)
                .get(`/api/v2/projects/${EMPTY_PROJECT_ID}/files`)
                .set("Authorization", `Bearer ${authToken.token}`);

            expect(response.status).to.equal(200);
            console.log(response.body);
            expect(response.body).to.be.an("array").that.is.empty;
        });

        it("Should return 401 when Authorization header is missing", async () => {
            const response = await chai
                .request(app)
                .get(`/api/v2/projects/${PROJECT_ID}/files`);

            expect(response.status).to.equal(401);
        });

        it("Should return 403 when caller lacks IMAGE_VIEW authority", async () => {
            const response = await chai
                .request(app)
                .get(`/api/v2/projects/${PROJECT_ID}/files`)
                .set("Authorization", `Bearer ${authToken.token}`);

            expect(response.status).to.equal(403);
        });
    });

    // -------------------------------------------------------------------------
    describe("GET /api/v2/projects/:projectId/files/:fileReferenceId - Get file", () => {
        let uploadedFileReferenceId: string;

        before(async () => {
            await cleanMockSetup();
            await setupIAM();
            await authoriseProjectIamAccess(PROJECT_ID, authToken.token, [Authorities.IMAGE_UPLOAD]);

            const upload = await chai
                .request(app)
                .post(`/api/v2/projects/${PROJECT_ID}/files`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .set("content-type", "multipart/form-data")
                .field("fileName", "upload-file.txt")
                .field("xyzDisplayName", "Single File")
                .field("chunkIndex", "0")
                .field("totalChunks", "1")
                .attach("file", FILE_RESOURCE, "upload-file.txt");

            expect(upload.status).to.equal(201);
            uploadedFileReferenceId = upload.body.fileReferenceId;
        });

        beforeEach(async () => {
            await cleanMockSetup();
            await setupIAM();
        });

        it("Should return 200 with file details and a downloadUrl", async () => {
            await authoriseProjectIamAccess(PROJECT_ID, authToken.token, [Authorities.IMAGE_VIEW]);

            const response = await chai
                .request(app)
                .get(`/api/v2/projects/${PROJECT_ID}/files/${uploadedFileReferenceId}`)
                .set("Authorization", `Bearer ${authToken.token}`);

            expect(response.status).to.equal(200);
            expect(response.body).to.have.property("fileReferenceId", uploadedFileReferenceId);
            expect(response.body).to.have.property("fileName", "upload-file.txt");
            expect(response.body).to.have.property("fileExtension", "txt");
            expect(response.body).to.have.property("xyzDisplayName", "Single File");
            expect(response.body).to.have.property("fileSizeBytes").that.is.a("number");
            expect(response.body).to.have.property("createdBy");
            expect(response.body).to.have.property("insertedOn");
            expect(response.body).to.have.property("downloadUrl").that.is.a("string");
        });

        it("Should return 404 when the fileReferenceId does not exist", async () => {
            await authoriseProjectIamAccess(PROJECT_ID, authToken.token, [Authorities.IMAGE_VIEW]);

            const response = await chai
                .request(app)
                .get(`/api/v2/projects/${PROJECT_ID}/files/${uuidv4()}`)
                .set("Authorization", `Bearer ${authToken.token}`);

            expect(response.status).to.equal(404);
        });

        it("Should return 400 when fileReferenceId is not a valid UUID", async () => {
            await authoriseProjectIamAccess(PROJECT_ID, authToken.token, [Authorities.IMAGE_VIEW]);

            const response = await chai
                .request(app)
                .get(`/api/v2/projects/${PROJECT_ID}/files/not-a-uuid`)
                .set("Authorization", `Bearer ${authToken.token}`);

            expect(response.status).to.equal(400);
        });

        it("Should return 401 when Authorization header is missing", async () => {
            const response = await chai
                .request(app)
                .get(`/api/v2/projects/${PROJECT_ID}/files/${uploadedFileReferenceId}`);

            expect(response.status).to.equal(401);
        });

        it("Should return 403 when caller lacks IMAGE_VIEW authority", async () => {
            const response = await chai
                .request(app)
                .get(`/api/v2/projects/${PROJECT_ID}/files/${uploadedFileReferenceId}`)
                .set("Authorization", `Bearer ${authToken.token}`);

            expect(response.status).to.equal(403);
        });
    });

    // -------------------------------------------------------------------------
    describe("GET /api/v2/projects/:projectId/files/biggest - Biggest file", () => {
        before(async () => {
            await cleanMockSetup();
            await setupIAM();
            await authoriseProjectIamAccess(PROJECT_ID, authToken.token, [Authorities.IMAGE_UPLOAD]);

            const upload = await chai
                .request(app)
                .post(`/api/v2/projects/${PROJECT_ID}/files`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .set("content-type", "multipart/form-data")
                .field("fileName", "upload-file.txt")
                .field("xyzDisplayName", "Biggest Candidate")
                .field("chunkIndex", "0")
                .field("totalChunks", "1")
                .attach("file", FILE_RESOURCE, "upload-file.txt");

            expect(upload.status).to.equal(201);
        });

        beforeEach(async () => {
            await cleanMockSetup();
            await setupIAM();
        });

        it("Should return 200 with the biggest file's metadata for a project that has files", async () => {
            await authoriseProjectIamAccess(PROJECT_ID, authToken.token, [Authorities.IMAGE_VIEW]);

            const response = await chai
                .request(app)
                .get(`/api/v2/projects/${PROJECT_ID}/files/biggest`)
                .set("Authorization", `Bearer ${authToken.token}`);

            expect(response.status).to.equal(200);
            expect(response.body).to.be.an("object");
            expect(response.body).to.have.property("fileReferenceId").that.is.a("string");
            expect(response.body).to.have.property("fileName");
            expect(response.body).to.have.property("fileExtension");
            expect(response.body).to.have.property("xyzDisplayName");
            expect(response.body).to.have.property("fileSizeBytes").that.is.a("number");
            expect(response.body).to.have.property("insertedOn");
            expect(response.body).to.not.have.property("downloadUrl");
        });

        it("Should return 200 with a null body for a project that has no files", async () => {
            await authoriseProjectIamAccess(EMPTY_PROJECT_ID, authToken.token, [Authorities.IMAGE_VIEW]);

            const response = await chai
                .request(app)
                .get(`/api/v2/projects/${EMPTY_PROJECT_ID}/files/biggest`)
                .set("Authorization", `Bearer ${authToken.token}`);

            expect(response.status).to.equal(200);
            expect(response.body).to.be.null;
        });

        it("Should return 404 when the project does not exist", async () => {
            const missingProjectId = uuidv4();
            await authoriseProjectIamAccess(missingProjectId, authToken.token, [Authorities.IMAGE_VIEW]);

            const response = await chai
                .request(app)
                .get(`/api/v2/projects/${missingProjectId}/files/biggest`)
                .set("Authorization", `Bearer ${authToken.token}`);

            expect(response.status).to.equal(404);
        });

        it("Should return 400 when projectId is not a valid UUID", async () => {
            const response = await chai
                .request(app)
                .get(`/api/v2/projects/not-a-uuid/files/biggest`)
                .set("Authorization", `Bearer ${authToken.token}`);

            expect(response.status).to.equal(400);
        });

        it("Should return 401 when Authorization header is missing", async () => {
            const response = await chai
                .request(app)
                .get(`/api/v2/projects/${PROJECT_ID}/files/biggest`);

            expect(response.status).to.equal(401);
        });

        it("Should return 403 when caller lacks IMAGE_VIEW authority", async () => {
            const response = await chai
                .request(app)
                .get(`/api/v2/projects/${PROJECT_ID}/files/biggest`)
                .set("Authorization", `Bearer ${authToken.token}`);

            expect(response.status).to.equal(403);
        });
    });
});