test/e2e/api/assets.e2e.spec.tsmodified
import {
    expect,
    app,
    http,
    Server,
    cleanMockSetup,
    createAuthToken,
    setupIAM,
    closeMockServer,
    setupE2EMockEnvVariables,
    Authorities,
    authoriseProjectIamAccess,
    chai,
    uuidv4,
} from "../common/e2e-imports";
import { setupProjectAndGetDetails, getAssetById } from "../util/db-helper";

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

    before(async () => {
        setupE2EMockEnvVariables();
        await cleanMockSetup();
        await setupIAM();
        server = http.createServer(app);
        server.listen();
        const projectDetails = await setupProjectAndGetDetails(uuidv4().substring(20));
        testProjectId = projectDetails.ProjectId;

        authToken = await createAuthToken("abc");
        await authoriseProjectIamAccess(testProjectId, authToken.token, [Authorities.PROJECT_EDIT]);

        // An asset requires an existing asset type (assetTypeId), which in turn needs a workflow.
        const wfRes = await chai
            .request(app)
            .post(`/api/v2/projects/${testProjectId}/commissioning/workflows`)
            .set("Authorization", `Bearer ${authToken.token}`)
            .send({ name: "Assets E2E Workflow" });
        const workflowId = wfRes.body.commissioningWorkflowId;

        const atRes = await chai
            .request(app)
            .post(`/api/v2/projects/${testProjectId}/asset-types`)
            .set("Authorization", `Bearer ${authToken.token}`)
            .send({ name: `Assets E2E AssetType ${uuidv4()}`, commissioningWorkflowId: workflowId });
        testAssetTypeId = atRes.body.assetTypeId;
    });

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

    describe("GET /api/v2/projects/:projectId/assets", () => {
        before(async () => {
            authToken = await createAuthToken("abc");
            await authoriseProjectIamAccess(testProjectId, authToken.token, [Authorities.PROJECT_VIEW]);
        });

        it("should return 200 with a paginated envelope", async () => {
            const res = await chai.request(app).get(`/api/v2/projects/${testProjectId}/assets`).set("Authorization", `Bearer ${authToken.token}`);

            expect(res.status).to.equal(200);
            expect(res.body).to.be.an("object");
            expect(res.body).to.have.property("records").that.is.an("array");
            expect(res.body).to.have.property("recordCount");
            expect(res.body).to.have.property("lastFetchedIndexId");
        });

        it("should advance to the next page using lastFetchedIndexId and size", async () => {
            const editToken = await createAuthToken("abc");
            await authoriseProjectIamAccess(testProjectId, editToken.token, [Authorities.PROJECT_EDIT]);

            // Ensure there are at least 2 records to paginate over
            await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/assets`)
                .set("Authorization", `Bearer ${editToken.token}`)
                .send({ name: `E2E Page Asset A ${uuidv4()}`, assetTypeId: testAssetTypeId });
            await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/assets`)
                .set("Authorization", `Bearer ${editToken.token}`)
                .send({ name: `E2E Page Asset B ${uuidv4()}`, assetTypeId: testAssetTypeId });

            const firstPage = await chai.request(app).get(`/api/v2/projects/${testProjectId}/assets?size=1`).set("Authorization", `Bearer ${authToken.token}`);

            expect(firstPage.status).to.equal(200);
            expect(firstPage.body.records).to.be.an("array");
            expect(firstPage.body.records.length).to.equal(1);
            expect(firstPage.body.lastFetchedIndexId).to.not.equal(-1);

            const firstRecordId = firstPage.body.records[0].assetId;

            const nextPage = await chai
                .request(app)
                .get(`/api/v2/projects/${testProjectId}/assets?size=1&lastFetchedIndexId=${firstPage.body.lastFetchedIndexId}`)
                .set("Authorization", `Bearer ${authToken.token}`);

            expect(nextPage.status).to.equal(200);
            expect(nextPage.body.records).to.be.an("array");
            if (nextPage.body.records.length > 0) {
                expect(nextPage.body.records[0].assetId).to.not.equal(firstRecordId);
            }
        });

        it("should return 400 when size is not a number", async () => {
            const res = await chai.request(app).get(`/api/v2/projects/${testProjectId}/assets?size=abc`).set("Authorization", `Bearer ${authToken.token}`);

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

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

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

    describe("POST /api/v2/projects/:projectId/assets", () => {
        before(async () => {
            authToken = await createAuthToken("abc");
            await authoriseProjectIamAccess(testProjectId, authToken.token, [Authorities.PROJECT_EDIT]);
        });

        it("should return 201 and create an asset with valid payload", async () => {
            const payload = { name: "E2E Chiller", assetTypeId: testAssetTypeId };

            const res = await chai.request(app).post(`/api/v2/projects/${testProjectId}/assets`).set("Authorization", `Bearer ${authToken.token}`).send(payload);

            expect(res.status).to.equal(201);
            expect(res.body).to.have.property("assetId");
            expect(res.body).to.have.property("name", "E2E Chiller");
            expect(res.body).to.have.property("assetTypeId", testAssetTypeId);
            expect(res.body).to.have.property("createdBy");
            expect(res.body).to.have.property("insertedOn");

            const dbRow = await getAssetById(testProjectId, res.body.assetId);
            expect(dbRow).to.exist;
            expect(dbRow.Name).to.equal("E2E Chiller");
        });

        it("should return 400 when name is missing", async () => {
            const res = await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/assets`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ assetTypeId: testAssetTypeId });

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

        it("should return 400 when assetTypeId is missing", async () => {
            const res = await chai.request(app).post(`/api/v2/projects/${testProjectId}/assets`).set("Authorization", `Bearer ${authToken.token}`).send({ name: "E2E Chiller" });

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

        it("should return 400 when assetTypeId is not a valid UUID", async () => {
            const res = await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/assets`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: "E2E Chiller", assetTypeId: "not-a-uuid" });

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

        it("should return 404 when assetTypeId does not exist", async () => {
            const res = await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/assets`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: "E2E Chiller", assetTypeId: uuidv4() });

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

        it("should return 400 when projectId is not a valid UUID", async () => {
            const res = await chai
                .request(app)
                .post("/api/v2/projects/not-a-uuid/assets")
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: "E2E Chiller", assetTypeId: testAssetTypeId });

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

    describe("GET /api/v2/projects/:projectId/assets/:assetId", () => {
        let createdAssetId: string;

        before(async () => {
            authToken = await createAuthToken("abc");
            await authoriseProjectIamAccess(testProjectId, authToken.token, [Authorities.PROJECT_EDIT]);
            const res = await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/assets`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: "E2E Get Asset Test", assetTypeId: testAssetTypeId });
            createdAssetId = res.body.assetId;
        });

        it("should return 200 with the asset", async () => {
            const res = await chai.request(app).get(`/api/v2/projects/${testProjectId}/assets/${createdAssetId}`).set("Authorization", `Bearer ${authToken.token}`);

            expect(res.status).to.equal(200);
            expect(res.body).to.have.property("assetId", createdAssetId);
            expect(res.body).to.have.property("name", "E2E Get Asset Test");
        });

        it("should return 404 when asset does not exist", async () => {
            const res = await chai.request(app).get(`/api/v2/projects/${testProjectId}/assets/${uuidv4()}`).set("Authorization", `Bearer ${authToken.token}`);

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

        it("should return 400 when assetId is not a valid UUID", async () => {
            const res = await chai.request(app).get(`/api/v2/projects/${testProjectId}/assets/not-a-uuid`).set("Authorization", `Bearer ${authToken.token}`);

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

    describe("PATCH /api/v2/projects/:projectId/assets/:assetId", () => {
        let createdAssetId: string;
        let secondAssetTypeId: string;
        let parentAssetId: string;

        before(async () => {
            authToken = await createAuthToken("abc");
            await authoriseProjectIamAccess(testProjectId, authToken.token, [Authorities.PROJECT_EDIT]);
            const res = await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/assets`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: "E2E Patch Asset Original", assetTypeId: testAssetTypeId });
            createdAssetId = res.body.assetId;

            // A second asset type to test patching the assetTypeId relational field.
            const wfRes = await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/commissioning/workflows`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: "Assets E2E Patch Workflow" });
            const atRes = await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/asset-types`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: `Assets E2E Patch AssetType ${uuidv4()}`, commissioningWorkflowId: wfRes.body.commissioningWorkflowId });
            secondAssetTypeId = atRes.body.assetTypeId;

            // A separate asset to use as the parent when patching parentAssetId.
            const parentRes = await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/assets`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: "E2E Patch Parent Asset", assetTypeId: testAssetTypeId });
            parentAssetId = parentRes.body.assetId;
        });

        it("should return 200 and update the asset name", async () => {
            const res = await chai
                .request(app)
                .patch(`/api/v2/projects/${testProjectId}/assets/${createdAssetId}`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: "E2E Patch Asset Updated" });

            expect(res.status).to.equal(200);
            expect(res.body).to.have.property("assetId", createdAssetId);
            expect(res.body).to.have.property("name", "E2E Patch Asset Updated");
            expect(res.body).to.have.property("lastModifiedBy");
            expect(res.body).to.have.property("lastModifiedOn");

            const dbRow = await getAssetById(testProjectId, createdAssetId);
            expect(dbRow.Name).to.equal("E2E Patch Asset Updated");
        });

        it("should leave the asset type unchanged after a name patch", async () => {
            const res = await chai
                .request(app)
                .patch(`/api/v2/projects/${testProjectId}/assets/${createdAssetId}`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: "E2E Patch Asset Renamed Again" });

            expect(res.status).to.equal(200);
            expect(res.body).to.have.property("assetTypeId", testAssetTypeId);
        });

        it("should return 200 and update the assetTypeId", async () => {
            const res = await chai
                .request(app)
                .patch(`/api/v2/projects/${testProjectId}/assets/${createdAssetId}`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ assetTypeId: secondAssetTypeId });

            expect(res.status).to.equal(200);
            expect(res.body).to.have.property("assetTypeId", secondAssetTypeId);

            const dbRow = await getAssetById(testProjectId, createdAssetId);
            expect(dbRow.AssetTypeId).to.equal(secondAssetTypeId);
        });

        it("should return 200 and update the parentAssetId", async () => {
            const res = await chai
                .request(app)
                .patch(`/api/v2/projects/${testProjectId}/assets/${createdAssetId}`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ parentAssetId });

            expect(res.status).to.equal(200);

            const dbRow = await getAssetById(testProjectId, createdAssetId);
            expect(dbRow.ParentAssetId).to.equal(parentAssetId);
        });

        it("should return 404 when patching to a non-existent assetTypeId", async () => {
            const res = await chai
                .request(app)
                .patch(`/api/v2/projects/${testProjectId}/assets/${createdAssetId}`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ assetTypeId: uuidv4() });

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

        it("should return 400 when assetTypeId is not a valid UUID", async () => {
            const res = await chai
                .request(app)
                .patch(`/api/v2/projects/${testProjectId}/assets/${createdAssetId}`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ assetTypeId: "not-a-uuid" });

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

        it("should return 400 when the body is empty", async () => {
            const res = await chai.request(app).patch(`/api/v2/projects/${testProjectId}/assets/${createdAssetId}`).set("Authorization", `Bearer ${authToken.token}`).send({});

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

        it("should return 400 when name is blank", async () => {
            const res = await chai
                .request(app)
                .patch(`/api/v2/projects/${testProjectId}/assets/${createdAssetId}`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: "   " });

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

        it("should return 406 when a non-patchable field is supplied", async () => {
            const res = await chai
                .request(app)
                .patch(`/api/v2/projects/${testProjectId}/assets/${createdAssetId}`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ createdBy: "someone@example.com" });

            expect(res.status).to.equal(406);
        });

        it("should return 404 when the asset does not exist", async () => {
            const res = await chai
                .request(app)
                .patch(`/api/v2/projects/${testProjectId}/assets/${uuidv4()}`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: "Does Not Exist" });

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

        it("should return 400 when assetId is not a valid UUID", async () => {
            const res = await chai
                .request(app)
                .patch(`/api/v2/projects/${testProjectId}/assets/not-a-uuid`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: "New Name" });

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

        it("should return 400 when projectId is not a valid UUID", async () => {
            const res = await chai
                .request(app)
                .patch(`/api/v2/projects/not-a-uuid/assets/${createdAssetId}`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: "New Name" });

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

    describe("systemId derivation on GET asset endpoints", () => {
        let mappedAssetTypeId: string;
        let mappedSystemTypeId: string;
        let mappedAssetId: string;
        let unmappedAssetId: string;

        before(async () => {
            authToken = await createAuthToken("abc");
            await authoriseProjectIamAccess(testProjectId, authToken.token, [Authorities.PROJECT_EDIT]);

            // A dedicated asset type + system type that we map together.
            const atWfRes = await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/commissioning/workflows`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: `Assets E2E systemId AT Workflow ${uuidv4()}` });
            const atRes = await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/asset-types`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: `Assets E2E systemId AssetType ${uuidv4()}`, commissioningWorkflowId: atWfRes.body.commissioningWorkflowId });
            mappedAssetTypeId = atRes.body.assetTypeId;

            const stWfRes = await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/commissioning/workflows`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: `Assets E2E systemId ST Workflow ${uuidv4()}` });
            const stRes = await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/system-types`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: `Assets E2E systemId SystemType ${uuidv4()}`, commissioningWorkflowId: stWfRes.body.commissioningWorkflowId });
            mappedSystemTypeId = stRes.body.systemTypeId;

            await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/asset-types/system-type-mappings`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ assetTypeId: mappedAssetTypeId, systemTypeId: mappedSystemTypeId });

            const mappedAsset = await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/assets`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: `E2E Mapped Asset ${uuidv4()}`, assetTypeId: mappedAssetTypeId });
            mappedAssetId = mappedAsset.body.assetId;

            // An asset whose asset type has no mapping.
            const unmappedAsset = await chai
                .request(app)
                .post(`/api/v2/projects/${testProjectId}/assets`)
                .set("Authorization", `Bearer ${authToken.token}`)
                .send({ name: `E2E Unmapped Asset ${uuidv4()}`, assetTypeId: testAssetTypeId });
            unmappedAssetId = unmappedAsset.body.assetId;

            authToken = await createAuthToken("abc");
            await authoriseProjectIamAccess(testProjectId, authToken.token, [Authorities.PROJECT_VIEW]);
        });

        it("should return the mapped systemId on GET by id", async () => {
            const res = await chai.request(app).get(`/api/v2/projects/${testProjectId}/assets/${mappedAssetId}`).set("Authorization", `Bearer ${authToken.token}`);

            expect(res.status).to.equal(200);
            expect(res.body).to.have.property("systemId", mappedSystemTypeId);
        });

        it("should return systemId null on GET by id when the asset type has no mapping", async () => {
            const res = await chai.request(app).get(`/api/v2/projects/${testProjectId}/assets/${unmappedAssetId}`).set("Authorization", `Bearer ${authToken.token}`);

            expect(res.status).to.equal(200);
            expect(res.body).to.have.property("systemId", null);
        });

        it("should include systemId on the list endpoint records", async () => {
            const res = await chai.request(app).get(`/api/v2/projects/${testProjectId}/assets`).set("Authorization", `Bearer ${authToken.token}`);

            expect(res.status).to.equal(200);
            const mapped = res.body.records.find((r: any) => r.assetId === mappedAssetId);
            expect(mapped).to.exist;
            expect(mapped).to.have.property("systemId", mappedSystemTypeId);
        });
    });
});