diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts index 62eb0122dca..aa42cd8bb24 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts @@ -167,4 +167,32 @@ export enum DrawerType { OrgAtRiskApps = 3, } +export interface RiskInsightsReport { + organizationId: OrganizationId; + date: string; + reportData: string; + reportKey: string; +} + +export interface ReportInsightsReportData { + data: string; + key: string; +} + +export interface SaveRiskInsightsReportRequest { + data: RiskInsightsReport; +} + +export interface SaveRiskInsightsReportResponse { + id: string; +} + +export interface GetRiskInsightsReportResponse { + id: string; + organizationId: OrganizationId; + date: string; + reportData: string; + reportKey: string; +} + export type PasswordHealthReportApplicationId = Opaque; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts new file mode 100644 index 00000000000..da896cb3753 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts @@ -0,0 +1,173 @@ +import { mock } from "jest-mock-extended"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; + +import { SaveRiskInsightsReportRequest } from "../models/password-health"; + +import { RiskInsightsApiService } from "./risk-insights-api.service"; + +describe("RiskInsightsApiService", () => { + let service: RiskInsightsApiService; + const apiService = mock(); + + const orgId = "org1" as OrganizationId; + + const getRiskInsightsReportResponse = { + organizationId: orgId, + date: new Date().toISOString(), + reportData: "test", + reportKey: "test-key", + }; + + const saveRiskInsightsReportRequest: SaveRiskInsightsReportRequest = { + data: { + organizationId: orgId, + date: new Date().toISOString(), + reportData: "test", + reportKey: "test-key", + }, + }; + const saveRiskInsightsReportResponse = { + ...saveRiskInsightsReportRequest.data, + }; + + beforeEach(() => { + service = new RiskInsightsApiService(apiService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + it("should call apiService.send with correct parameters for saveRiskInsightsReport", (done) => { + apiService.send.mockReturnValue(Promise.resolve(saveRiskInsightsReportResponse)); + + service.saveRiskInsightsReport(saveRiskInsightsReportRequest).subscribe((result) => { + expect(result).toEqual(saveRiskInsightsReportResponse); + expect(apiService.send).toHaveBeenCalledWith( + "POST", + `/reports/organization-reports`, + saveRiskInsightsReportRequest.data, + true, + true, + ); + done(); + }); + }); + + it("should call apiService.send with correct parameters and return the response for saveRiskInsightsReport ", (done) => { + apiService.send.mockReturnValue(Promise.resolve(saveRiskInsightsReportResponse)); + + service.saveRiskInsightsReport(saveRiskInsightsReportRequest).subscribe((result) => { + expect(result).toEqual(saveRiskInsightsReportResponse); + expect(apiService.send).toHaveBeenCalledWith( + "POST", + `/reports/organization-reports`, + saveRiskInsightsReportRequest.data, + true, + true, + ); + done(); + }); + }); + + it("should propagate errors from apiService.send for saveRiskInsightsReport - 1", (done) => { + const error = { statusCode: 500, message: "Internal Server Error" }; + apiService.send.mockReturnValue(Promise.reject(error)); + + service.saveRiskInsightsReport(saveRiskInsightsReportRequest).subscribe({ + next: () => { + fail("Expected error to be thrown"); + }, + error: () => { + expect(apiService.send).toHaveBeenCalledWith( + "POST", + `/reports/organization-reports`, + saveRiskInsightsReportRequest.data, + true, + true, + ); + done(); + }, + complete: () => { + done(); + }, + }); + }); + + it("should propagate network errors from apiService.send for saveRiskInsightsReport - 2", (done) => { + const error = new Error("Network error"); + apiService.send.mockReturnValue(Promise.reject(error)); + + service.saveRiskInsightsReport(saveRiskInsightsReportRequest).subscribe({ + next: () => { + fail("Expected error to be thrown"); + }, + error: () => { + expect(apiService.send).toHaveBeenCalledWith( + "POST", + `/reports/organization-reports`, + saveRiskInsightsReportRequest.data, + true, + true, + ); + done(); + }, + complete: () => { + done(); + }, + }); + }); + + it("should call apiService.send with correct parameters and return the response for getRiskInsightsReport ", (done) => { + apiService.send.mockReturnValue(Promise.resolve(getRiskInsightsReportResponse)); + + service.getRiskInsightsReport(orgId).subscribe((result) => { + expect(result).toEqual(getRiskInsightsReportResponse); + expect(apiService.send).toHaveBeenCalledWith( + "GET", + `/reports/organization-reports/latest/${orgId.toString()}`, + null, + true, + true, + ); + done(); + }); + }); + + it("should return null if apiService.send rejects with 404 error for getRiskInsightsReport", (done) => { + const error = { statusCode: 404 }; + apiService.send.mockReturnValue(Promise.reject(error)); + + service.getRiskInsightsReport(orgId).subscribe((result) => { + expect(result).toBeNull(); + done(); + }); + }); + + it("should throw error if apiService.send rejects with non-404 error for getRiskInsightsReport", (done) => { + const error = { statusCode: 500, message: "Server error" }; + apiService.send.mockReturnValue(Promise.reject(error)); + + service.getRiskInsightsReport(orgId).subscribe({ + next: () => { + // Should not reach here + fail("Expected error to be thrown"); + }, + error: () => { + expect(apiService.send).toHaveBeenCalledWith( + "GET", + `/reports/organization-reports/latest/${orgId.toString()}`, + null, + true, + true, + ); + done(); + }, + complete: () => { + done(); + }, + }); + }); +}); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts new file mode 100644 index 00000000000..07b5062eec5 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts @@ -0,0 +1,41 @@ +import { from, Observable } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; + +import { + GetRiskInsightsReportResponse, + SaveRiskInsightsReportRequest, + SaveRiskInsightsReportResponse, +} from "../models/password-health"; + +export class RiskInsightsApiService { + constructor(private apiService: ApiService) {} + + saveRiskInsightsReport( + request: SaveRiskInsightsReportRequest, + ): Observable { + const dbResponse = this.apiService.send( + "POST", + `/reports/organization-reports`, + request.data, + true, + true, + ); + + return from(dbResponse as Promise); + } + + getRiskInsightsReport(orgId: OrganizationId): Observable { + const dbResponse = this.apiService + .send("GET", `/reports/organization-reports/latest/${orgId.toString()}`, null, true, true) + .catch((error: any): any => { + if (error.statusCode === 404) { + return null; // Handle 404 by returning null or an appropriate default value + } + throw error; // Re-throw other errors + }); + + return from(dbResponse as Promise); + } +}