diff --git a/backend/fixtures/0-software.json b/backend/fixtures/0-software.json new file mode 100644 index 000000000..ac6fe22c6 --- /dev/null +++ b/backend/fixtures/0-software.json @@ -0,0 +1 @@ +[{ "id": 1, "name": "cloudnetpy", "version": "1.0.4", "url": null }] diff --git a/backend/fixtures/2-regular_file.json b/backend/fixtures/2-regular_file.json index 4bab1bfe9..638940112 100644 --- a/backend/fixtures/2-regular_file.json +++ b/backend/fixtures/2-regular_file.json @@ -14,7 +14,8 @@ "format": "HDF5 (NetCDF4)", "site": "mace-head", "version": "123", - "errorLevel": null + "errorLevel": null, + "software": [{ "id": 1 }] }, { "uuid": "bde7a35f-03aa-4bff-acfb-b4974ea9f217", @@ -31,7 +32,8 @@ "format": "HDF5 (NetCDF4)", "site": "mace-head", "version": "123", - "errorLevel": null + "errorLevel": null, + "software": [{ "id": 1 }] }, { "uuid": "d21d6a9b-6804-4465-a026-74ec429fe17d", @@ -48,7 +50,8 @@ "format": "HDF5 (NetCDF4)", "site": "hyytiala", "version": "123", - "errorLevel": null + "errorLevel": null, + "software": [{ "id": 1 }] }, { "uuid": "22b32746-faf0-4057-9076-ed2e698dcc34", @@ -65,7 +68,8 @@ "format": "HDF5 (NetCDF4)", "site": "bucharest", "version": "123", - "errorLevel": null + "errorLevel": null, + "software": [{ "id": 1 }] }, { "uuid": "6cb32746-faf0-4057-9076-ed2e698dcf36", @@ -82,7 +86,8 @@ "format": "HDF5 (NetCDF4)", "site": "bucharest", "version": "123", - "errorLevel": null + "errorLevel": null, + "software": [{ "id": 1 }] }, { "uuid": "8bb32746-faf0-4057-9076-ed2e698dcf36", @@ -99,7 +104,8 @@ "format": "HDF5 (NetCDF4)", "site": "bucharest", "version": "123", - "errorLevel": null + "errorLevel": null, + "software": [{ "id": 1 }] }, { "uuid": "1bb32746-faf0-4057-9076-ed2e698dcf36", @@ -121,7 +127,8 @@ "a45a2e9a-e39d-4af2-9798-5ea0fadf041e" ], "version": "123", - "errorLevel": null + "errorLevel": null, + "software": [{ "id": 1 }] }, { "uuid": "2bb32746-faf0-4057-9076-ed2e698dcf36", @@ -139,7 +146,8 @@ "site": "bucharest", "sourceFileIds": ["1bb32746-faf0-4057-9076-ed2e698dcf36"], "version": "123", - "errorLevel": "pass" + "errorLevel": "pass", + "software": [{ "id": 1 }] }, { "uuid": "3bb32746-faf0-4057-9076-ed2e698dcf36", @@ -157,7 +165,8 @@ "format": "HDF5 (NetCDF4)", "site": "bucharest", "version": "123", - "errorLevel": null + "errorLevel": null, + "software": [{ "id": 1 }] }, { "uuid": "52b32746-faf0-4057-9076-ed2e698dcc34", @@ -175,7 +184,8 @@ "format": "HDF5 (NetCDF4)", "site": "bucharest", "version": "123", - "errorLevel": null + "errorLevel": null, + "software": [{ "id": 1 }] }, { "uuid": "62b32746-faf0-4057-9076-ed2e698dcc34", @@ -193,7 +203,8 @@ "format": "HDF5 (NetCDF4)", "site": "bucharest", "version": "123", - "errorLevel": "pass" + "errorLevel": "pass", + "software": [{ "id": 1 }] }, { "uuid": "72b32746-faf0-4057-9076-ed2e698dcc34", @@ -211,7 +222,8 @@ "format": "HDF5 (NetCDF4)", "site": "bucharest", "version": "123", - "errorLevel": null + "errorLevel": null, + "software": [{ "id": 1 }] }, { "uuid": "82b32746-faf0-4057-9076-ed2e698dcc34", @@ -229,7 +241,8 @@ "format": "HDF5 (NetCDF4)", "site": "bucharest", "version": "123", - "errorLevel": null + "errorLevel": null, + "software": [{ "id": 1 }] }, { "uuid": "acf78456-11b1-41a6-b2de-aa7590a75675", @@ -248,7 +261,8 @@ "site": "bucharest", "version": "123", "quality": "qc", - "errorLevel": "error" + "errorLevel": "error", + "software": [{ "id": 1 }] }, { "uuid": "b6de8cf4-8825-47b0-aaa9-4fd413bbb0d7", @@ -266,7 +280,8 @@ "site": "bucharest", "version": "123", "errorLevel": null, - "instrumentPid": "https://hdl.handle.net/123/bucharest_lidar" + "instrumentPid": "https://hdl.handle.net/123/bucharest_lidar", + "software": [{ "id": 1 }] }, { "uuid": "f036da43-c19c-4832-99f9-6cc88f3255c5", @@ -284,6 +299,7 @@ "site": "bucharest", "version": "123", "errorLevel": null, - "instrumentPid": "https://hdl.handle.net/123/bucharest_radar" + "instrumentPid": "https://hdl.handle.net/123/bucharest_radar", + "software": [{ "id": 1 }] } ] diff --git a/backend/src/entity/File.ts b/backend/src/entity/File.ts index 99086904f..e79ad132c 100644 --- a/backend/src/entity/File.ts +++ b/backend/src/entity/File.ts @@ -4,6 +4,8 @@ import { Column, Entity, Index, + JoinTable, + ManyToMany, ManyToOne, OneToMany, PrimaryColumn, @@ -17,6 +19,7 @@ import { basename } from "path"; import { Model } from "./Model"; import { ModelVisualization } from "./ModelVisualization"; import { ErrorLevel } from "./QualityReport"; +import { Software } from "./Software"; export enum Quality { NRT = "nrt", @@ -82,6 +85,10 @@ export abstract class File { @Column({ default: "" }) processingVersion!: string; + @ManyToMany(() => Software) + @JoinTable() + software!: Software[]; + get filename() { return basename(this.s3key); } diff --git a/backend/src/entity/Software.ts b/backend/src/entity/Software.ts new file mode 100644 index 000000000..e102b3b6b --- /dev/null +++ b/backend/src/entity/Software.ts @@ -0,0 +1,17 @@ +import { Column, Entity, Unique, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +@Unique(["name", "version"]) +export class Software { + @PrimaryGeneratedColumn() + id?: number; + + @Column() + name!: string; + + @Column() + version!: string; + + @Column({ type: "text", nullable: true }) + url!: string | null; +} diff --git a/backend/src/lib/index.ts b/backend/src/lib/index.ts index 6b487ccd5..26038bcb9 100644 --- a/backend/src/lib/index.ts +++ b/backend/src/lib/index.ts @@ -87,6 +87,9 @@ export const augmentFile = (includeS3path: boolean) => (file: RegularFile | Mode s3key: undefined, s3path: includeS3path ? getS3pathForFile(file) : undefined, model: "model" in file ? file.model : undefined, + software: file.software + ? file.software.map((software) => ({ name: software.name, version: software.version, url: software.url })) + : undefined, }); export const ssAuthString = () => diff --git a/backend/src/migration/1692622438290-AddSoftware.ts b/backend/src/migration/1692622438290-AddSoftware.ts new file mode 100644 index 000000000..6dc02e871 --- /dev/null +++ b/backend/src/migration/1692622438290-AddSoftware.ts @@ -0,0 +1,81 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddSoftware1692622438290 implements MigrationInterface { + name = "AddSoftware1692622438290"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "software" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, "version" character varying NOT NULL, "url" text, CONSTRAINT "PK_3ceec82cc90b32643b07e8d9841" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "file_software_software" ("fileUuid" uuid NOT NULL, "softwareId" integer NOT NULL, CONSTRAINT "PK_46858ee5fd23ddd01a7370e0d4c" PRIMARY KEY ("fileUuid", "softwareId"))` + ); + await queryRunner.query(`CREATE INDEX "IDX_d3e4502978abd25bdc51e42c60" ON "file_software_software" ("fileUuid") `); + await queryRunner.query( + `CREATE INDEX "IDX_94fcf91af85f113565d9d82ef0" ON "file_software_software" ("softwareId") ` + ); + await queryRunner.query( + `CREATE TABLE "regular_file_software_software" ("regularFileUuid" uuid NOT NULL, "softwareId" integer NOT NULL, CONSTRAINT "PK_beb1f28e62cd9e70183524086df" PRIMARY KEY ("regularFileUuid", "softwareId"))` + ); + await queryRunner.query( + `CREATE INDEX "IDX_80069d8fc68b7b591221745c1e" ON "regular_file_software_software" ("regularFileUuid") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_bcbd69960eebffb7658aec8b94" ON "regular_file_software_software" ("softwareId") ` + ); + await queryRunner.query( + `CREATE TABLE "model_file_software_software" ("modelFileUuid" uuid NOT NULL, "softwareId" integer NOT NULL, CONSTRAINT "PK_aac272d389dd8c007c948ce991a" PRIMARY KEY ("modelFileUuid", "softwareId"))` + ); + await queryRunner.query( + `CREATE INDEX "IDX_53d4543423792e10d0c9859b07" ON "model_file_software_software" ("modelFileUuid") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_64ecef712f34a271013c99768f" ON "model_file_software_software" ("softwareId") ` + ); + await queryRunner.query( + `ALTER TABLE "file_software_software" ADD CONSTRAINT "FK_d3e4502978abd25bdc51e42c604" FOREIGN KEY ("fileUuid") REFERENCES "file"("uuid") ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE "file_software_software" ADD CONSTRAINT "FK_94fcf91af85f113565d9d82ef02" FOREIGN KEY ("softwareId") REFERENCES "software"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE "regular_file_software_software" ADD CONSTRAINT "FK_80069d8fc68b7b591221745c1e4" FOREIGN KEY ("regularFileUuid") REFERENCES "regular_file"("uuid") ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE "regular_file_software_software" ADD CONSTRAINT "FK_bcbd69960eebffb7658aec8b94a" FOREIGN KEY ("softwareId") REFERENCES "software"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE "model_file_software_software" ADD CONSTRAINT "FK_53d4543423792e10d0c9859b077" FOREIGN KEY ("modelFileUuid") REFERENCES "model_file"("uuid") ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE "model_file_software_software" ADD CONSTRAINT "FK_64ecef712f34a271013c99768f3" FOREIGN KEY ("softwareId") REFERENCES "software"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "model_file_software_software" DROP CONSTRAINT "FK_64ecef712f34a271013c99768f3"` + ); + await queryRunner.query( + `ALTER TABLE "model_file_software_software" DROP CONSTRAINT "FK_53d4543423792e10d0c9859b077"` + ); + await queryRunner.query( + `ALTER TABLE "regular_file_software_software" DROP CONSTRAINT "FK_bcbd69960eebffb7658aec8b94a"` + ); + await queryRunner.query( + `ALTER TABLE "regular_file_software_software" DROP CONSTRAINT "FK_80069d8fc68b7b591221745c1e4"` + ); + await queryRunner.query(`ALTER TABLE "file_software_software" DROP CONSTRAINT "FK_94fcf91af85f113565d9d82ef02"`); + await queryRunner.query(`ALTER TABLE "file_software_software" DROP CONSTRAINT "FK_d3e4502978abd25bdc51e42c604"`); + await queryRunner.query(`DROP INDEX "public"."IDX_64ecef712f34a271013c99768f"`); + await queryRunner.query(`DROP INDEX "public"."IDX_53d4543423792e10d0c9859b07"`); + await queryRunner.query(`DROP TABLE "model_file_software_software"`); + await queryRunner.query(`DROP INDEX "public"."IDX_bcbd69960eebffb7658aec8b94"`); + await queryRunner.query(`DROP INDEX "public"."IDX_80069d8fc68b7b591221745c1e"`); + await queryRunner.query(`DROP TABLE "regular_file_software_software"`); + await queryRunner.query(`DROP INDEX "public"."IDX_94fcf91af85f113565d9d82ef0"`); + await queryRunner.query(`DROP INDEX "public"."IDX_d3e4502978abd25bdc51e42c60"`); + await queryRunner.query(`DROP TABLE "file_software_software"`); + await queryRunner.query(`DROP TABLE "software"`); + } +} diff --git a/backend/src/routes/file.ts b/backend/src/routes/file.ts index 27a137880..26ca8b2a4 100644 --- a/backend/src/routes/file.ts +++ b/backend/src/routes/file.ts @@ -23,6 +23,7 @@ import { SearchFileResponse } from "../entity/SearchFileResponse"; import { Visualization } from "../entity/Visualization"; import { ModelVisualization } from "../entity/ModelVisualization"; import { Product } from "../entity/Product"; +import { Software } from "../entity/Software"; export class FileRoutes { constructor(conn: Connection) { @@ -35,6 +36,7 @@ export class FileRoutes { this.modelVisualizationRepo = conn.getRepository("model_visualization"); this.productRepo = conn.getRepository("product"); this.fileQualityRepo = conn.getRepository("file_quality"); + this.softwareRepo = conn.getRepository("software"); } readonly conn: Connection; @@ -46,13 +48,15 @@ export class FileRoutes { readonly modelVisualizationRepo: Repository; readonly productRepo: Repository; readonly fileQualityRepo: Repository; + readonly softwareRepo: Repository; file: RequestHandler = async (req: Request, res: Response, next) => { const getFileByUuid = (repo: Repository, isModel: boolean | undefined) => { const qb = repo .createQueryBuilder("file") .leftJoinAndSelect("file.site", "site") - .leftJoinAndSelect("file.product", "product"); + .leftJoinAndSelect("file.product", "product") + .leftJoinAndSelect("file.software", "software"); if (isModel) qb.leftJoinAndSelect("file.model", "model"); qb.where("file.uuid = :uuid", req.params); return hideTestDataFromNormalUsers(qb, req).getOne(); @@ -127,6 +131,10 @@ export class FileRoutes { return next({ status: 400, errors: ["The specified file was not found in storage service"] }); } + if (file.software) { + file.software = await this.getSoftware(file.software); + } + try { const findFileByName = (model: boolean) => { const repo = model ? this.modelFileRepo : this.fileRepo; @@ -155,14 +163,14 @@ export class FileRoutes { } else { await transactionalEntityManager.insert(SearchFile, searchFile); } - await transactionalEntityManager.insert(FileClass, file); + await transactionalEntityManager.save(FileClass, file); }); res.sendStatus(201); } else if (existingFile.site.isTestSite || existingFile.volatile) { // Replace existing file.createdAt = existingFile.createdAt; await this.conn.transaction(async (transactionalEntityManager) => { - await transactionalEntityManager.update(FileClass, { uuid: file.uuid }, file); + await transactionalEntityManager.save(FileClass, file); await transactionalEntityManager.update(SearchFile, { uuid: file.uuid }, searchFile); }); res.sendStatus(200); @@ -171,7 +179,7 @@ export class FileRoutes { if (isModel) return next({ status: 501, errors: ["Versioning is not supported for model files."] }); await this.conn.transaction(async (transactionalEntityManager) => { file.createdAt = file.updatedAt; - await transactionalEntityManager.insert(FileClass, file); + await transactionalEntityManager.save(FileClass, file); if (!file.legacy) { // Don't display legacy files in search if cloudnet version is available await transactionalEntityManager.delete(SearchFile, { uuid: existingFile.uuid }); @@ -186,6 +194,8 @@ export class FileRoutes { }); } } catch (e) { + console.error("============== ERROR ================="); + console.error(e); next({ status: 500, errors: e }); } }; @@ -417,6 +427,18 @@ export class FileRoutes { .getRawMany(); return products.map((prod) => prod.id); } + + getSoftware(software: Record): Promise { + const promises = Object.entries(software).map(async ([name, version]) => { + const query = { name, version: version as string }; + let software = await this.softwareRepo.findOne(query); + if (!software) { + software = await this.softwareRepo.save(query); + } + return software; + }); + return Promise.all(promises); + } } function addCommonFilters(qb: SelectQueryBuilder, query: any) { diff --git a/backend/tests/data/file.json b/backend/tests/data/file.json index 0792190bd..1d8e5e973 100644 --- a/backend/tests/data/file.json +++ b/backend/tests/data/file.json @@ -4,7 +4,6 @@ "volatile": true, "measurementDate": "2018-11-15", "product": "radar", - "cloudnetpyVersion": "1.0.4", "createdAt": "2020-02-20T10:56:19.382Z", "updatedAt": "2020-02-20T10:56:19.382Z", "s3key": "20181115_mace-head_mira.nc", @@ -12,5 +11,8 @@ "size": 12200657, "format": "HDF5 (NetCDF4)", "site": "mace-head", - "version": "123" + "version": "123", + "software": { + "cloudnetpy": "1.0.4" + } } diff --git a/backend/tests/integration/parallel/__snapshots__/file.test.ts.snap b/backend/tests/integration/parallel/__snapshots__/file.test.ts.snap new file mode 100644 index 000000000..ca3daed71 --- /dev/null +++ b/backend/tests/integration/parallel/__snapshots__/file.test.ts.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`/api/files/:uuid request succeeds on instrument file 1`] = ` +Object { + "checksum": "298688b011a511f8f0e9353371cf73ee86f60c89b0e02c6931d8c05542c64cdb", + "cloudnetpyVersion": "1.0.4", + "createdAt": "2020-02-20T10:56:19.382Z", + "downloadUrl": "http://localhost:3000/api/download/product/38092c00-161d-4ca2-a29d-628cf8e960f6/20181115_mace-head_mira.nc", + "errorLevel": null, + "filename": "20181115_mace-head_mira.nc", + "format": "HDF5 (NetCDF4)", + "instrumentPid": null, + "legacy": false, + "measurementDate": "2018-11-15", + "pid": "", + "processingVersion": "", + "product": Object { + "experimental": false, + "humanReadableName": "Radar", + "id": "radar", + "level": "1b", + }, + "quality": "nrt", + "site": Object { + "actrisId": null, + "altitude": 16, + "country": "Ireland", + "countryCode": "IE", + "countrySubdivisionCode": null, + "dvasId": null, + "gaw": "MHD", + "humanReadableName": "Mace Head", + "id": "mace-head", + "latitude": 53.326, + "longitude": -9.9, + "type": Array [ + "cloudnet", + ], + }, + "size": "12200657", + "software": Array [ + Object { + "name": "cloudnetpy", + "url": null, + "version": "1.0.4", + }, + ], + "sourceFileIds": null, + "updatedAt": "2020-02-20T10:56:19.382Z", + "uuid": "38092c00-161d-4ca2-a29d-628cf8e960f6", + "version": "123", + "volatile": true, +} +`; + +exports[`/api/files/:uuid request succeeds on model file 1`] = ` +Object { + "checksum": "055bcd9deae26851992c3da6352844fb9443203c48ae4f4ad8f1aa50ef2ab26f", + "createdAt": "2020-02-20T10:52:59.073Z", + "downloadUrl": "http://localhost:3000/api/download/product/b5d1d5af-3667-41bc-b952-e684f627d91c/20141205_mace-head_ecmwf.nc", + "errorLevel": null, + "filename": "20141205_mace-head_ecmwf.nc", + "format": "NetCDF3", + "legacy": false, + "measurementDate": "2020-12-05", + "model": Object { + "humanReadableName": "ECMWF forecast", + "id": "ecmwf", + "optimumOrder": 0, + }, + "pid": "", + "processingVersion": "", + "product": Object { + "experimental": false, + "humanReadableName": "Model", + "id": "model", + "level": "1b", + }, + "quality": "nrt", + "site": Object { + "actrisId": 1337, + "altitude": 93, + "country": "Romania", + "countryCode": "RO", + "countrySubdivisionCode": null, + "dvasId": "INO", + "gaw": null, + "humanReadableName": "Bucharest", + "id": "bucharest", + "latitude": 44.348, + "longitude": 26.029, + "type": Array [ + "cloudnet", + ], + }, + "size": "500452", + "software": Array [], + "updatedAt": "2020-02-20T10:52:59.073Z", + "uuid": "b5d1d5af-3667-41bc-b952-e684f627d91c", + "version": "123", + "volatile": true, +} +`; diff --git a/backend/tests/integration/parallel/file.test.ts b/backend/tests/integration/parallel/file.test.ts index c7774b9ff..8935794fc 100644 --- a/backend/tests/integration/parallel/file.test.ts +++ b/backend/tests/integration/parallel/file.test.ts @@ -12,11 +12,13 @@ describe("/api/files/:uuid", () => { }; it("request succeeds on instrument file", async () => { - return expect(axios.get(`${url}a5d1d5af-3667-41bc-b952-e684f627d91c`)).resolves.toBeTruthy(); + const res = await axios.get(`${url}38092c00-161d-4ca2-a29d-628cf8e960f6`); + expect(res.data).toMatchSnapshot(); }); it("request succeeds on model file", async () => { - return expect(axios.get(`${url}b5d1d5af-3667-41bc-b952-e684f627d91c`)).resolves.toBeTruthy(); + const res = await axios.get(`${url}b5d1d5af-3667-41bc-b952-e684f627d91c`); + expect(res.data).toMatchSnapshot(); }); it("responds with a 404 on test file if in normal mode", async () => { diff --git a/backend/tests/integration/sequential/file.test.ts b/backend/tests/integration/sequential/file.test.ts index c183935a2..c6dad0ad0 100644 --- a/backend/tests/integration/sequential/file.test.ts +++ b/backend/tests/integration/sequential/file.test.ts @@ -12,6 +12,7 @@ import { initUsersAndPermissions } from "../../lib/userAccountAndPermissions"; const uuidGen = require("uuid"); const crypto = require("crypto"); import { readResources } from "../../../../shared/lib"; +import { Software } from "../../../src/entity/Software"; let conn: Connection; let fileRepo: Repository; @@ -21,6 +22,7 @@ let vizRepo: Repository; let modelVizRepo: Repository; let fileQualityRepo: Repository; let QualityReportRepo: Repository; +let softwareRepo: Repository; const volatileFile = JSON.parse(readFileSync("tests/data/file.json", "utf8")); const stableFile = { ...volatileFile, ...{ volatile: false, pid: "1234" } }; @@ -35,6 +37,7 @@ beforeAll(async () => { modelVizRepo = conn.getRepository("model_visualization"); fileQualityRepo = conn.getRepository("file_quality"); QualityReportRepo = conn.getRepository("quality_report"); + softwareRepo = conn.getRepository("software"); await initUsersAndPermissions(); const prefix = `${storageServiceUrl}cloudnet-product`; await axios.put(`${prefix}-volatile/${volatileFile.s3key}`, "content"); @@ -352,6 +355,22 @@ describe("PUT /files/:s3key", () => { tmpfile.site = "hyytiala"; await expect(putFile(tmpfile)).rejects.toMatchObject({ response: { status: 400 } }); }); + + it("inserts and updates software", async () => { + await expect(putFile(volatileFile)).resolves.toMatchObject({ status: 201 }); + const file1 = await fileRepo.findOneOrFail(volatileFile.uuid, { relations: ["software"] }); + expect(file1.software.length).toBe(1); + expect(file1.software[0].name).toBe("cloudnetpy"); + expect(file1.software[0].version).toBe("1.0.4"); + + await expect(putFile({ ...volatileFile, software: { cloudnetpy: "1.0.5" } })).resolves.toMatchObject({ + status: 200, + }); + const file2 = await fileRepo.findOneOrFail(volatileFile.uuid, { relations: ["software"] }); + expect(file2.software.length).toBe(1); + expect(file2.software[0].name).toBe("cloudnetpy"); + expect(file2.software[0].version).toBe("1.0.5"); + }); }); describe("POST /files/", () => { @@ -601,5 +620,5 @@ async function cleanRepos() { await searchFileRepo.delete({}); await fileQualityRepo.delete({}); await QualityReportRepo.delete({}); - return; + await softwareRepo.delete({}); }