diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-type-guards.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-type-guards.spec.ts index 6c130dc77fa..7475c669032 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-type-guards.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-type-guards.spec.ts @@ -247,6 +247,76 @@ describe("Risk Insights Type Guards", () => { /Invalid OrganizationReportSummary/, ); }); + + // Backward compatibility tests - legacy encrypted data predates newApplications field + it("should accept OrganizationReportSummary without newApplications field (backward compatibility)", () => { + const validLegacyData = { + totalMemberCount: 10, + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + // newApplications field intentionally omitted - legacy data + }; + + expect(() => validateOrganizationReportSummary(validLegacyData)).not.toThrow(); + expect(validateOrganizationReportSummary(validLegacyData)).toEqual(validLegacyData); + }); + + it("should accept OrganizationReportSummary with undefined newApplications (backward compatibility)", () => { + const validData = { + totalMemberCount: 10, + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: undefined, + }; + + expect(() => validateOrganizationReportSummary(validData)).not.toThrow(); + }); + + it("should enforce array length limits on newApplications when present", () => { + const invalidData = { + totalMemberCount: 10, + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: new Array(50001).fill("app"), // exceeds MAX_ARRAY_LENGTH + }; + + expect(() => validateOrganizationReportSummary(invalidData)).toThrow( + /Invalid OrganizationReportSummary.*newApplications/, + ); + }); + + it("should enforce string length limits on newApplications when present", () => { + const invalidData = { + totalMemberCount: 10, + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: ["app-1", "a".repeat(1001)], // exceeds MAX_STRING_LENGTH + }; + + expect(() => validateOrganizationReportSummary(invalidData)).toThrow( + /Invalid OrganizationReportSummary.*newApplications/, + ); + }); }); describe("validateOrganizationReportApplicationArray", () => { @@ -616,6 +686,82 @@ describe("Risk Insights Type Guards", () => { }; expect(isOrganizationReportSummary(invalidData)).toBe(false); }); + + // Backward compatibility tests - legacy encrypted data predates newApplications field + it("should return true for OrganizationReportSummary without newApplications field (backward compatibility)", () => { + const validLegacyData = { + totalMemberCount: 10, + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + // newApplications field intentionally omitted - legacy data + }; + expect(isOrganizationReportSummary(validLegacyData)).toBe(true); + }); + + it("should return true for OrganizationReportSummary with undefined newApplications (backward compatibility)", () => { + const validData = { + totalMemberCount: 10, + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: undefined, + }; + expect(isOrganizationReportSummary(validData)).toBe(true); + }); + + it("should return false for empty string in newApplications when present", () => { + const invalidData = { + totalMemberCount: 10, + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: ["app-1", "", "app-3"], // empty string should be rejected + }; + expect(isOrganizationReportSummary(invalidData)).toBe(false); + }); + + it("should return false when newApplications exceeds array length limit", () => { + const invalidData = { + totalMemberCount: 10, + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: new Array(50001).fill("app"), // exceeds MAX_ARRAY_LENGTH + }; + expect(isOrganizationReportSummary(invalidData)).toBe(false); + }); + + it("should return false when newApplications contains strings exceeding length limit", () => { + const invalidData = { + totalMemberCount: 10, + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: ["app-1", "a".repeat(1001)], // exceeds MAX_STRING_LENGTH + }; + expect(isOrganizationReportSummary(invalidData)).toBe(false); + }); }); describe("isOrganizationReportApplication", () => {