Skip to content

Commit

Permalink
Merge pull request #1229 from SciCatProject/improve-published-dataset…
Browse files Browse the repository at this point in the history
…-resync

Improve published dataset resync
  • Loading branch information
nitrosx authored Jun 3, 2024
2 parents 3fae222 + cbf6812 commit 9cc4a9d
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 38 deletions.
71 changes: 34 additions & 37 deletions src/published-data/published-data.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ import { FilterQuery, QueryOptions } from "mongoose";
import { DatasetsService } from "src/datasets/datasets.service";
import { ProposalsService } from "src/proposals/proposals.service";
import { AttachmentsService } from "src/attachments/attachments.service";
import { existsSync, readFileSync } from "fs";
import { HttpService } from "@nestjs/axios";
import { ConfigService } from "@nestjs/config";
import { firstValueFrom } from "rxjs";
Expand All @@ -59,8 +58,6 @@ import { DatasetClass } from "src/datasets/schemas/dataset.schema";
@ApiTags("published data")
@Controller("publisheddata")
export class PublishedDataController {
private doiConfigPath = "./src/config/doiconfig.local.json";

constructor(
private readonly attachmentsService: AttachmentsService,
private readonly configService: ConfigService,
Expand Down Expand Up @@ -434,6 +431,28 @@ export class PublishedDataController {
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Update, PublishedData),
)
@ApiOperation({
summary: "Edits published data.",
description:
"It edits published data and resyncs with OAI Provider if it is defined.",
})
@ApiParam({
name: "id",
description: "The DOI of the published data.",
type: String,
})
@ApiParam({
name: "data",
description:
"The edited data that will be updated in the database and with OAI Provider if defined.",
type: UpdatePublishedDataDto,
})
@ApiResponse({
status: HttpStatus.OK,
isArray: false,
description:
"Return the result of resync with OAI Provider if defined, or null.",
})
@Post("/:id/resync")
async resync(
@Param("id") id: string,
Expand All @@ -443,47 +462,25 @@ export class PublishedDataController {

const OAIServerUri = this.configService.get<string>("oaiProviderRoute");

let doiProviderCredentials = {
username: "removed",
password: "removed",
};

if (existsSync(this.doiConfigPath)) {
doiProviderCredentials = JSON.parse(
readFileSync(this.doiConfigPath).toString(),
let returnValue = null;
if (OAIServerUri) {
returnValue = await this.publishedDataService.resyncOAIPublication(
id,
publishedData,
OAIServerUri,
);
}

const resyncOAIPublication = {
method: "PUT",
body: publishedData,
json: true,
uri: OAIServerUri + "/" + encodeURIComponent(encodeURIComponent(id)),
headers: {
"content-type": "application/json;charset=UTF-8",
},
auth: doiProviderCredentials,
};

let res;
try {
res = await firstValueFrom(
this.httpService.request({
...resyncOAIPublication,
method: "PUT",
}),
);
} catch (error) {
handleAxiosRequestError(error, "PublishedDataController.resync");
}

try {
await this.publishedDataService.update({ doi: id }, publishedData);
} catch (error) {
console.error(error);
} catch (error: any) {
throw new HttpException(
`Error occurred: ${error}`,
error.response?.status || HttpStatus.FAILED_DEPENDENCY,
);
}

return res ? res.data : null;
return returnValue;
}
}

Expand Down
80 changes: 80 additions & 0 deletions src/published-data/published-data.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { HttpException, Logger } from "@nestjs/common";
import { getModelToken } from "@nestjs/mongoose";
import { Test, TestingModule } from "@nestjs/testing";
import { Model } from "mongoose";
import { PublishedDataService } from "./published-data.service";
import { PublishedData } from "./schemas/published-data.schema";
import { HttpModule, HttpService } from "@nestjs/axios";
import { AxiosInstance } from "axios";
import fs from "fs";
import { of } from "rxjs";
import { AxiosResponse } from "axios";

const mockPublishedData: PublishedData = {
doi: "100.10/random-test-uuid-string",
Expand Down Expand Up @@ -30,13 +36,21 @@ const mockPublishedData: PublishedData = {
updatedAt: new Date("2022-02-15T13:00:00"),
};

const mockAxiosResponse: Partial<AxiosResponse> = {
data: "success",
status: 200,
statusText: "OK",
};

describe("PublishedDataService", () => {
let service: PublishedDataService;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let model: Model<PublishedData>;
let httpService: HttpService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [HttpModule],
providers: [
PublishedDataService,
{
Expand All @@ -49,14 +63,80 @@ describe("PublishedDataService", () => {
exec: jest.fn(),
},
},
{
provide: "AXIOS_INSTANCE_TOKEN",
useValue: {} as AxiosInstance,
},
],
}).compile();

service = await module.resolve<PublishedDataService>(PublishedDataService);
model = module.get<Model<PublishedData>>(getModelToken("PublishedData"));
httpService = module.get<HttpService>(HttpService);
Logger.error = jest.fn();
});

it("should be defined", () => {
expect(service).toBeDefined();
});

describe("resyncOAIPublication", () => {
const id = "test-id";
const OAIServerUri = "https://oaimockserver.com";
const doiCredentials = {
username: "the_username",
password: "the_password",
};

it("should throw HttpException if doiConfigPath file does not exist", async () => {
jest.mock("fs");
jest.spyOn(fs, "existsSync").mockReturnValue(false);

await expect(
service.resyncOAIPublication(id, mockPublishedData, OAIServerUri),
).rejects.toThrowError(HttpException);
});

it("should call httpService.request with correct payload", async () => {
jest.mock("fs");
jest.spyOn(fs, "existsSync").mockReturnValueOnce(true);
jest
.spyOn(fs, "readFileSync")
.mockReturnValueOnce(JSON.stringify(doiCredentials));
jest
.spyOn(httpService, "request")
.mockReturnValueOnce(of(mockAxiosResponse as AxiosResponse));
await service.resyncOAIPublication(id, mockPublishedData, OAIServerUri);

expect(httpService.request).toHaveBeenCalledTimes(1);

expect(httpService.request).toHaveBeenCalledWith({
method: "PUT",
json: true,
body: mockPublishedData,
auth: doiCredentials,
uri: OAIServerUri + "/" + id,
headers: {
"content-type": "application/json;charset=UTF-8",
},
});
});

it("should throw HttpException if request to oaiProvider raise exception", async () => {
jest.mock("fs");
jest.spyOn(fs, "existsSync").mockReturnValueOnce(true);
jest
.spyOn(fs, "readFileSync")
.mockReturnValueOnce(JSON.stringify(doiCredentials));
jest.spyOn(httpService, "request").mockImplementation(() => {
throw new Error();
});

await expect(
service.resyncOAIPublication(id, mockPublishedData, OAIServerUri),
).rejects.toThrowError(HttpException);

expect(Logger.error).toBeCalled();
});
});
});
65 changes: 64 additions & 1 deletion src/published-data/published-data.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Inject, Injectable, Scope } from "@nestjs/common";
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
Inject,
Injectable,
Scope,
HttpException,
HttpStatus,
} from "@nestjs/common";
import { REQUEST } from "@nestjs/core";
import { Request } from "express";
import { InjectModel } from "@nestjs/mongoose";
Expand All @@ -20,12 +27,21 @@ import {
PublishedDataDocument,
} from "./schemas/published-data.schema";
import { JWTUser } from "src/auth/interfaces/jwt-user.interface";
import { HttpService } from "@nestjs/axios";
import { UpdatePublishedDataDto } from "./dto/update-published-data.dto";
import { IRegister } from "./interfaces/published-data.interface";
import { existsSync, readFileSync } from "fs";
import { firstValueFrom } from "rxjs";
import { handleAxiosRequestError } from "src/common/utils";

@Injectable({ scope: Scope.REQUEST })
export class PublishedDataService {
private doiConfigPath = "./src/config/doiconfig.local.json";

constructor(
@InjectModel(PublishedData.name)
private publishedDataModel: Model<PublishedDataDocument>,
private readonly httpService: HttpService,
@Inject(REQUEST)
private request: Request,
) {}
Expand Down Expand Up @@ -101,4 +117,51 @@ export class PublishedDataService {
async remove(filter: FilterQuery<PublishedDataDocument>): Promise<unknown> {
return this.publishedDataModel.findOneAndDelete(filter).exec();
}

async resyncOAIPublication(
id: string,
publishedData: UpdatePublishedDataDto,
OAIServerUri: string,
): Promise<IRegister | null> {
let doiProviderCredentials;

// this can be improved on by validating doiProviderCredentials
if (existsSync(this.doiConfigPath)) {
doiProviderCredentials = JSON.parse(
readFileSync(this.doiConfigPath).toString(),
);
} else {
throw new HttpException(
"doiConfigPath file not found",
HttpStatus.INTERNAL_SERVER_ERROR,
);
}

const resyncOAIPublication = {
method: "PUT",
body: publishedData,
json: true,
uri: OAIServerUri + "/" + encodeURIComponent(encodeURIComponent(id)),
headers: {
"content-type": "application/json;charset=UTF-8",
},
auth: doiProviderCredentials,
};

try {
const res = await firstValueFrom(
this.httpService.request({
...resyncOAIPublication,
method: "PUT",
}),
);
return res ? res.data : null;
} catch (error: any) {
handleAxiosRequestError(error, "PublishedDataController.resync");
throw new HttpException(
`Error occurred: ${error}`,
error.response?.status || HttpStatus.FAILED_DEPENDENCY,
);
}
}
}

0 comments on commit 9cc4a9d

Please sign in to comment.