diff --git a/client/config/jest.config.ts b/client/config/jest.config.ts index 6d683a602..4c33cd30c 100644 --- a/client/config/jest.config.ts +++ b/client/config/jest.config.ts @@ -30,6 +30,7 @@ const config: JestConfigWithTsJest = { "@app/(.*)": "/src/app/$1", "@assets/(.*)": "../node_modules/@patternfly/react-core/dist/styles/assets/$1", + "^@mocks/(.*)$": "/src/mocks/$1", }, // A list of paths to directories that Jest should use to search for files diff --git a/client/src/app/data/mock-questionnaire.ts b/client/src/app/data/mock-questionnaire.ts deleted file mode 100644 index 58b1f0ef2..000000000 --- a/client/src/app/data/mock-questionnaire.ts +++ /dev/null @@ -1,466 +0,0 @@ -export const mockQuestionnaire = { - id: 1, - name: "Q1", - description: "Questionnaire 1 ", - revision: 1, - questions: 42, - rating: "5% Red, 25% Yellow", - dateImported: "8 Aug. 2023, 10:20 AM EST", - required: false, - builtin: true, - sections: [ - { - name: "Application technologies 1", - questions: [ - { - formulation: "What is the main technology in your application?", - explanation: - "What would you describe as the main framework used to build your application.", - answers: [ - { - choice: "Unknown", - rationale: "This is a problem because of the uncertainty.", - mitigation: "Gathering more information about this is required.", - risk: "unknown", - }, - { - choice: "Quarkus", - risk: "green", - autoanswer_if_tags_present: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - autotag: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - }, - { - choice: "Spring Boot", - risk: "green", - autoanswer_if_tags_present: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - autotag: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - }, - { - choice: "Java EE", - rationale: - "This might not be the most cloud friendly technology.", - mitigation: - "Maybe start thinking about migrating to Quarkus or Jakarta EE.", - risk: "yellow", - autoanswer_if_tags_present: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - autotag: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - }, - { - choice: "J2EE", - rationale: "This is obsolete.", - mitigation: - "Maybe start thinking about migrating to Quarkus or Jakarta EE.", - risk: "red", - autoanswer_if_tags_present: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - autotag: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - }, - ], - }, - { - formulation: "What version of Java EE does the application use?", - explanation: - "What version of the Java EE specification is your application using?", - answers: [ - { - choice: "Below 5.", - rationale: "This technology stack is obsolete.", - mitigation: "Consider migrating to at least Java EE 7.", - risk: "red", - }, - { - choice: "5 or 6", - rationale: "This is a mostly outdated stack.", - mitigation: "Consider migrating to at least Java EE 7.", - risk: "yellow", - }, - { - choice: "7", - risk: "green", - }, - ], - include_if_tags_present: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - }, - { - formulation: "Does your application use any caching mechanism?", - answers: [ - { - choice: "Yes", - rationale: - "This could be problematic in containers and Kubernetes.", - mitigation: - "Review the clustering mechanism to check compatibility and support for container environments.", - risk: "yellow", - autoanswer_if_tags_present: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - }, - { - choice: "No", - risk: "green", - }, - { - choice: "Unknown", - rationale: "This is a problem because of the uncertainty.", - mitigation: "Gathering more information about this is required.", - risk: "unknown", - }, - ], - }, - { - formulation: - "What implementation of JAX-WS does your application use?", - answers: [ - { - choice: "Apache Axis", - rationale: "This version is obsolete", - mitigation: "Consider migrating to Apache CXF", - risk: "red", - }, - { - choice: "Apache CXF", - risk: "green", - }, - { - choice: "Unknown", - rationale: "This is a problem because of the uncertainty.", - mitigation: "Gathering more information about this is required.", - risk: "unknown", - }, - ], - skip_if_tags_present: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - }, - ], - }, - { - name: "Application technologies", - questions: [ - { - formulation: "What is the main technology in your application?", - explanation: - "What would you describe as the main framework used to build your application.", - answers: [ - { - choice: "Unknown", - rationale: "This is a problem because of the uncertainty.", - mitigation: "Gathering more information about this is required.", - risk: "unknown", - }, - { - choice: "Quarkus", - risk: "green", - autoanswer_if_tags_present: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - autotag: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - }, - { - choice: "Spring Boot", - risk: "green", - autoanswer_if_tags_present: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - autotag: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - }, - { - choice: "Java EE", - rationale: - "This might not be the most cloud friendly technology.", - mitigation: - "Maybe start thinking about migrating to Quarkus or Jakarta EE.", - risk: "yellow", - autoanswer_if_tags_present: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - autotag: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - }, - { - choice: "J2EE", - rationale: "This is obsolete.", - mitigation: - "Maybe start thinking about migrating to Quarkus or Jakarta EE.", - risk: "red", - autoanswer_if_tags_present: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - autotag: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - }, - ], - }, - { - formulation: "What version of Java EE does the application use?", - explanation: - "What version of the Java EE specification is your application using?", - answers: [ - { - choice: "Below 5.", - rationale: "This technology stack is obsolete.", - mitigation: "Consider migrating to at least Java EE 7.", - risk: "red", - }, - { - choice: "5 or 6", - rationale: "This is a mostly outdated stack.", - mitigation: "Consider migrating to at least Java EE 7.", - risk: "yellow", - }, - { - choice: "7", - risk: "green", - }, - ], - include_if_tags_present: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - }, - { - formulation: "Does your application use any caching mechanism?", - answers: [ - { - choice: "Yes", - rationale: - "This could be problematic in containers and Kubernetes.", - mitigation: - "Review the clustering mechanism to check compatibility and support for container environments.", - risk: "yellow", - autoanswer_if_tags_present: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - ], - }, - { - choice: "No", - risk: "green", - }, - { - choice: "Unknown", - rationale: "This is a problem because of the uncertainty.", - mitigation: "Gathering more information about this is required.", - risk: "unknown", - }, - ], - }, - { - formulation: - "What implementation of JAX-WS does your application use?", - answers: [ - { - choice: "Apache Axis", - rationale: "This version is obsolete", - mitigation: "Consider migrating to Apache CXF", - risk: "red", - }, - { - choice: "Apache CXF", - risk: "green", - }, - { - choice: "Unknown", - rationale: "This is a problem because of the uncertainty.", - mitigation: "Gathering more information about this is required.", - risk: "unknown", - }, - ], - skip_if_tags_present: [ - { - category: { - name: "Cat 1", - id: 23, - }, - id: 34, - name: "Tag 1", - }, - { - category: { - name: "Cat 2", - id: 23, - }, - id: 34, - name: "Tag 2", - }, - ], - }, - ], - }, - ], - thresholds: { red: "5", yellow: "25", unknown: "70" }, - riskMessages: { - green: "Low Risk", - red: "High Risk", - yellow: "Medium Risk", - unknown: "Low Risk", - }, -}; diff --git a/client/src/app/hooks/useAssessmentStatus.ts b/client/src/app/hooks/useAssessmentStatus.ts new file mode 100644 index 000000000..df1eea447 --- /dev/null +++ b/client/src/app/hooks/useAssessmentStatus.ts @@ -0,0 +1,80 @@ +// hooks/useAssessmentStatus.js +import { Assessment, Archetype, Application } from "@app/api/models"; +import { useFetchArchetypes } from "@app/queries/archetypes"; +import { useFetchAssessments } from "@app/queries/assessments"; +import { useMemo } from "react"; + +export const useAssessmentStatus = (application: Application) => { + const { assessments } = useFetchAssessments(); + const { archetypes } = useFetchArchetypes(); + + const isDirectlyAssessed = + (application.assessed && (application.assessments?.length ?? 0) > 0) ?? + false; + + return useMemo(() => { + const applicationAssessments = + assessments?.filter( + (assessment: Assessment) => + assessment.application?.id === application.id + ) ?? []; + const inheritedArchetypes = + archetypes?.filter( + (archetype: Archetype) => + archetype.applications?.map((app) => app.id).includes(application.id) + ) ?? []; + + const assessmentsWithArchetypes = inheritedArchetypes.map( + (inheritedArchetype) => ({ + inheritedArchetype, + assessments: assessments.filter( + (assessment) => assessment.archetype?.id === inheritedArchetype.id + ), + }) + ); + + const allArchetypesAssessed = + assessmentsWithArchetypes.length > 0 && + assessmentsWithArchetypes.every(({ inheritedArchetype, assessments }) => { + const requiredAssessments = assessments.filter( + (assessment) => assessment.required + ); + return ( + inheritedArchetype.assessed && + assessments.length > 0 && + requiredAssessments.length > 0 && + requiredAssessments.every( + (assessment) => assessment.status === "complete" + ) + ); + }); + + const assessmentsFromArchetypesCount = assessmentsWithArchetypes.filter( + ({ assessments }) => assessments.some((assessment) => assessment.required) + ).length; + + const assessedArchetypesCount = assessmentsWithArchetypes.filter( + ({ assessments, inheritedArchetype }) => + assessments.some( + (assessment) => + assessment.required && + assessment.status === "complete" && + inheritedArchetype.assessed + ) + ).length; + + const hasApplicationAssessmentInProgress = applicationAssessments?.some( + (assessment: Assessment) => + assessment.status === "started" || + assessment.status === "empty" || + assessment.status === "complete" + ); + return { + allArchetypesAssessed, + countOfFullyAssessedArchetypes: assessedArchetypesCount, + countOfArchetypesWithRequiredAssessments: assessmentsFromArchetypesCount, + hasApplicationAssessmentInProgress, + isApplicationDirectlyAssessed: isDirectlyAssessed, + }; + }, [assessments, archetypes, application.id, isDirectlyAssessed]); +}; diff --git a/client/src/app/pages/applications/applications-table/applications-table.tsx b/client/src/app/pages/applications/applications-table/applications-table.tsx index f64899b4b..4e9ca1cb0 100644 --- a/client/src/app/pages/applications/applications-table/applications-table.tsx +++ b/client/src/app/pages/applications/applications-table/applications-table.tsx @@ -920,8 +920,6 @@ export const ApplicationsTable: React.FC = () => { > = ({ application, assessments, archetypes, isLoading }) => { - const { t } = useTranslation(); - - const applicationAssessments = assessments?.filter( - (assessment: Assessment) => assessment.application?.id === application.id - ); - const inheritedArchetypes = archetypes?.filter( - (archetype: Archetype) => - archetype.applications?.map((app) => app.id).includes(application.id) - ); - const assessmentStatusInfo = React.useMemo(() => { - const assessmentsWithArchetypes = inheritedArchetypes.map( - (inheritedArchetype) => ({ - inheritedArchetype, - assessments: assessments.filter( - (assessment) => assessment.archetype?.id === inheritedArchetype.id - ), - }) - ); - - const someArchetypesAssessed = assessmentsWithArchetypes.some( - ({ assessments }) => assessments.length > 0 - ); - - const allArchetypesAssessed = - assessmentsWithArchetypes.length > 0 && - assessmentsWithArchetypes.every(({ inheritedArchetype, assessments }) => { - const requiredAssessments = assessments.filter( - (assessment) => assessment.required - ); - return ( - inheritedArchetype.assessed && - assessments.length > 0 && - requiredAssessments.length > 0 && - requiredAssessments.every( - (assessment) => assessment.status === "complete" - ) - ); - }); - - const hasInProgressOrNotStartedRequiredAssessments = - assessmentsWithArchetypes.some(({ assessments }) => - assessments.some( - (assessment) => - assessment.required && - ["empty", "started"].includes(assessment.status) - ) - ); - - const assessedArchetypesWithARequiredAssessment = - assessmentsWithArchetypes.filter(({ assessments, inheritedArchetype }) => - assessments.some( - (assessment) => - assessment.required && - assessment.status === "complete" && - inheritedArchetype.assessed - ) - ); - const assessedArchetypeCount = - inheritedArchetypes?.filter( - (inheritedArchetype) => - inheritedArchetype?.assessments?.length ?? - (0 > 0 && inheritedArchetype.assessed) - ).length || 0; - - return { - assessmentsWithArchetypes, - someArchetypesAssessed, - allArchetypesAssessed, - hasInProgressOrNotStartedRequiredAssessments, - assessedArchetypesWithARequiredAssessment, - assessedArchetypeCount, - }; - }, [inheritedArchetypes, assessments]); +> = ({ application, isLoading }) => { + const assessmentStatusInfo = useAssessmentStatus(application); if (isLoading) { return ; @@ -94,28 +20,23 @@ export const ApplicationAssessmentStatus: React.FC< let statusPreset: IconedStatusPreset = "NotStarted"; // Default status let tooltipCount: number = 0; - const isDirectlyAssessed = - application.assessed && (application.assessments?.length ?? 0) > 0; - const { allArchetypesAssessed, - assessedArchetypesWithARequiredAssessment, - hasInProgressOrNotStartedRequiredAssessments, + countOfFullyAssessedArchetypes, + countOfArchetypesWithRequiredAssessments, + hasApplicationAssessmentInProgress, + isApplicationDirectlyAssessed, } = assessmentStatusInfo; - if (isDirectlyAssessed) { + if (isApplicationDirectlyAssessed) { statusPreset = "Completed"; } else if (allArchetypesAssessed) { statusPreset = "InheritedAssessments"; - tooltipCount = assessedArchetypesWithARequiredAssessment.length; - } else if (hasInProgressOrNotStartedRequiredAssessments) { + tooltipCount = countOfFullyAssessedArchetypes; + } else if (countOfArchetypesWithRequiredAssessments > 0) { statusPreset = "InProgressInheritedAssessments"; - tooltipCount = assessedArchetypesWithARequiredAssessment.length; - } else if ( - applicationAssessments?.some( - (assessment) => assessment.status === "started" - ) - ) { + tooltipCount = countOfArchetypesWithRequiredAssessments; + } else if (hasApplicationAssessmentInProgress) { statusPreset = "InProgress"; } return ; diff --git a/client/src/app/pages/applications/components/application-assessment-status/tests/application-assessment-status.test.tsx b/client/src/app/pages/applications/components/application-assessment-status/tests/application-assessment-status.test.tsx new file mode 100644 index 000000000..e410dcac3 --- /dev/null +++ b/client/src/app/pages/applications/components/application-assessment-status/tests/application-assessment-status.test.tsx @@ -0,0 +1,416 @@ +import "@testing-library/jest-dom"; +import { useAssessmentStatus } from "@app/hooks/useAssessmentStatus"; +import { + createMockApplication, + createMockArchetype, + createMockAssessment, + renderHook, + waitFor, +} from "@app/test-config/test-utils"; +import { rest } from "msw"; +import { server } from "@mocks/server"; + +describe("useAssessmentStatus", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + server.resetHandlers(); + }); + + it("Correctly calculates status given one started assessment and one complete assessment for an application", async () => { + server.use( + rest.get("/hub/assessments", (req, res, ctx) => { + return res( + ctx.json([ + createMockAssessment({ + id: 1, + application: { id: 1, name: "app1" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "started", + sections: [], + }), + createMockAssessment({ + id: 2, + application: { id: 1, name: "app1" }, + questionnaire: { id: 2, name: "questionnaire2" }, + status: "complete", + sections: [], + }), + ]) + ); + }), + + rest.get("/hub/archetypes", (req, res, ctx) => { + return res( + ctx.json([ + createMockArchetype({ + id: 1, + name: "archetype1", + applications: [{ id: 1, name: "app1" }], + assessed: false, + assessments: [], + }), + ]) + ); + }) + ); + const { result, waitForNextUpdate } = renderHook(() => + useAssessmentStatus(createMockApplication({ id: 1, name: "app1" })) + ); + await waitForNextUpdate(); + await waitFor(() => { + expect(result.current).toEqual({ + allArchetypesAssessed: false, + countOfFullyAssessedArchetypes: 0, + countOfArchetypesWithRequiredAssessments: 0, + hasApplicationAssessmentInProgress: true, + isApplicationDirectlyAssessed: false, + }); + }); + }); + + it("Correctly calculates status given two complete assessments for an application", async () => { + const mockAssessments = [ + createMockAssessment({ + id: 1, + application: { id: 1, name: "app1" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "complete", + sections: [], + }), + createMockAssessment({ + id: 2, + application: { id: 1, name: "app1" }, + questionnaire: { id: 2, name: "questionnaire2" }, + status: "complete", + sections: [], + }), + ]; + server.use( + rest.get("/hub/assessments", (req, res, ctx) => { + return res(ctx.json(mockAssessments)); + }) + ); + server.use( + rest.get("/hub/archetypes", (req, res, ctx) => { + return res(ctx.json([])); + }) + ); + + const mockApplication = createMockApplication({ + id: 1, + name: "app1", + assessed: true, + assessments: mockAssessments, + }); + + const { result, waitForNextUpdate } = renderHook(() => + useAssessmentStatus(mockApplication) + ); + + await waitForNextUpdate(); + await waitFor(() => { + expect(result.current).toEqual({ + allArchetypesAssessed: false, + countOfFullyAssessedArchetypes: 0, + countOfArchetypesWithRequiredAssessments: 0, + hasApplicationAssessmentInProgress: true, + isApplicationDirectlyAssessed: true, + }); + }); + }); + + it("Correctly calculates status given two inherited archetype; One with a complete state and one with started state.", async () => { + const arch1Assessments = [ + createMockAssessment({ + id: 1, + archetype: { id: 1, name: "archetype1" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "complete", + sections: [], + }), + ]; + const arch2Assessments = [ + createMockAssessment({ + id: 2, + archetype: { id: 2, name: "archetype2" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "started", + sections: [], + }), + ]; + const mockAssessments = [...arch1Assessments, ...arch2Assessments]; + const mockArchetypes = [ + createMockArchetype({ + id: 1, + name: "archetype1", + applications: [{ id: 1, name: "app1" }], + assessments: arch1Assessments, + assessed: true, + }), + createMockArchetype({ + id: 2, + name: "archetype2", + applications: [{ id: 1, name: "app1" }], + assessments: arch2Assessments, + assessed: false, + }), + ]; + server.use( + rest.get("/hub/assessments", (req, res, ctx) => { + return res(ctx.json(mockAssessments)); + }), + + rest.get("/hub/archetypes", (req, res, ctx) => { + return res(ctx.json(mockArchetypes)); + }) + ); + const mockApplication = createMockApplication({ + id: 1, + name: "app1", + archetypes: [ + { id: 1, name: "archetype1" }, + { id: 2, name: "archetype2" }, + ], + assessed: false, + }); + const { result, waitForNextUpdate } = renderHook(() => + useAssessmentStatus(mockApplication) + ); + await waitForNextUpdate(); + await waitFor(() => { + expect(result.current).toEqual({ + allArchetypesAssessed: false, + countOfFullyAssessedArchetypes: 1, + countOfArchetypesWithRequiredAssessments: 2, + hasApplicationAssessmentInProgress: false, + isApplicationDirectlyAssessed: false, + }); + }); + }); + + it("Correctly calculates status given a single inherited archetype with a complete state.", async () => { + const mockAssessments = [ + createMockAssessment({ + id: 1, + archetype: { id: 1, name: "archetype1" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "complete", + sections: [], + }), + ]; + + const mockArchetypes = [ + createMockArchetype({ + id: 1, + name: "archetype1", + applications: [{ id: 1, name: "app1" }], + assessments: mockAssessments, + assessed: true, + }), + ]; + + const mockApplication = createMockApplication({ + id: 1, + name: "app1", + archetypes: [{ id: 1, name: "archetype1" }], + assessed: false, + }); + + server.use( + rest.get("/hub/assessments", (req, res, ctx) => { + return res(ctx.json(mockAssessments)); + }), + + rest.get("/hub/archetypes", (req, res, ctx) => { + return res(ctx.json(mockArchetypes)); + }) + ); + + const { result, waitForNextUpdate } = renderHook(() => + useAssessmentStatus(mockApplication) + ); + await waitForNextUpdate(); + await waitFor(() => { + expect(result.current).toEqual({ + allArchetypesAssessed: true, + countOfFullyAssessedArchetypes: 1, + countOfArchetypesWithRequiredAssessments: 1, + hasApplicationAssessmentInProgress: false, + isApplicationDirectlyAssessed: false, + }); + }); + }); + + it("Correctly calculates status given 1 started assessment for an applications only archetype.", async () => { + const mockAssessments = [ + createMockAssessment({ + id: 1, + archetype: { id: 1, name: "archetype1" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "started", + sections: [], + }), + ]; + + const mockArchetypes = [ + createMockArchetype({ + id: 1, + name: "archetype1", + applications: [{ id: 1, name: "app1" }], + assessments: [...mockAssessments], + }), + ]; + + const mockApplication = createMockApplication({ + id: 1, + name: "app1", + archetypes: [{ id: 1, name: "archetype1" }], + assessed: false, + }); + server.use( + rest.get("/hub/assessments", (req, res, ctx) => { + return res(ctx.json(mockAssessments)); + }), + + rest.get("/hub/archetypes", (req, res, ctx) => { + return res(ctx.json(mockArchetypes)); + }) + ); + + const { result, waitForNextUpdate } = renderHook(() => + useAssessmentStatus(mockApplication) + ); + await waitForNextUpdate(); + await waitFor(() => { + expect(result.current).toEqual({ + allArchetypesAssessed: false, + countOfFullyAssessedArchetypes: 0, + countOfArchetypesWithRequiredAssessments: 1, + hasApplicationAssessmentInProgress: false, + isApplicationDirectlyAssessed: false, + }); + }); + }); + it("Correctly calculates status given one complete assessment for an application's inherited archetype with no direct assessment", async () => { + const mockAssessments = [ + createMockAssessment({ + id: 1, + archetype: { id: 1, name: "archetype1" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "complete", + sections: [], + }), + ]; + + const mockArchetypes = [ + createMockArchetype({ + id: 1, + name: "archetype1", + applications: [{ id: 1, name: "app1" }], + assessments: mockAssessments, + assessed: true, + }), + ]; + + const mockApplication = createMockApplication({ + id: 1, + name: "app1", + archetypes: [{ id: 1, name: "archetype1" }], + assessed: false, + }); + + server.use( + rest.get("/hub/assessments", (req, res, ctx) => { + return res(ctx.json(mockAssessments)); + }), + + rest.get("/hub/archetypes", (req, res, ctx) => { + return res(ctx.json(mockArchetypes)); + }) + ); + + const { result, waitForNextUpdate } = renderHook(() => + useAssessmentStatus(mockApplication) + ); + await waitForNextUpdate(); + await waitFor(() => { + expect(result.current).toEqual({ + allArchetypesAssessed: true, + countOfFullyAssessedArchetypes: 1, + countOfArchetypesWithRequiredAssessments: 1, + hasApplicationAssessmentInProgress: false, + isApplicationDirectlyAssessed: false, + }); + }); + }); + + it("Correctly calculates status given one complete assessment for an application's inherited archetype with a direct assessment", async () => { + const archetypeAssessments = [ + createMockAssessment({ + id: 1, + archetype: { id: 1, name: "archetype1" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "complete", + sections: [], + }), + ]; + + const mockArchetypes = [ + createMockArchetype({ + id: 1, + name: "archetype1", + applications: [{ id: 1, name: "app1" }], + assessments: archetypeAssessments, + assessed: true, + }), + ]; + const applicationAssessments = [ + createMockAssessment({ + id: 2, + application: { id: 1, name: "app1" }, + questionnaire: { id: 2, name: "questionnaire2" }, + status: "complete", + sections: [], + }), + ]; + + const mockApplication = createMockApplication({ + id: 1, + name: "app1", + archetypes: [{ id: 1, name: "archetype1" }], + assessed: true, + assessments: applicationAssessments, + }); + + const mockAssessments = [ + ...archetypeAssessments, + ...applicationAssessments, + ]; + server.use( + rest.get("/hub/assessments", (req, res, ctx) => { + return res(ctx.json(mockAssessments)); + }), + + rest.get("/hub/archetypes", (req, res, ctx) => { + return res(ctx.json(mockArchetypes)); + }) + ); + + const { result, waitForNextUpdate } = renderHook(() => + useAssessmentStatus(mockApplication) + ); + await waitForNextUpdate(); + + await waitFor(() => { + expect(result.current).toEqual({ + allArchetypesAssessed: true, + countOfFullyAssessedArchetypes: 1, + countOfArchetypesWithRequiredAssessments: 1, + hasApplicationAssessmentInProgress: true, + isApplicationDirectlyAssessed: true, + }); + }); + }); +}); diff --git a/client/src/app/test-config/setupTests.ts b/client/src/app/test-config/setupTests.ts index 0618b987c..cf3fab95c 100644 --- a/client/src/app/test-config/setupTests.ts +++ b/client/src/app/test-config/setupTests.ts @@ -1,4 +1,5 @@ import "@testing-library/jest-dom"; +import { server } from "@mocks/server"; const mockInitialized = false; @@ -16,3 +17,21 @@ jest.mock("react-router-dom", () => ({ pathname: "localhost:3000/example/path", }), })); + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +// Uncomment the following to see request logging in the console: + +// server.events.on("request:start", (req) => { +// console.log(`Handling a request to ${req.url.href}`); +// }); + +// server.events.on("request:match", (req) => { +// console.log(`Request to ${req.url.href} was matched with a handler`); +// }); + +// server.events.on("request:unhandled", (req) => { +// console.warn(`Request to ${req.url.href} was not handled`); +// }); diff --git a/client/src/app/test-config/test-utils.tsx b/client/src/app/test-config/test-utils.tsx index a65555d63..e7e6d0546 100644 --- a/client/src/app/test-config/test-utils.tsx +++ b/client/src/app/test-config/test-utils.tsx @@ -1,9 +1,18 @@ import React, { FC, ReactElement } from "react"; import { render, RenderOptions } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { Application, Archetype, Assessment } from "@app/api/models"; +import { RenderHookOptions, renderHook } from "@testing-library/react-hooks"; const AllTheProviders: FC<{ children: React.ReactNode }> = ({ children }) => { - const queryClient = new QueryClient(); + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + cacheTime: 1000, + }, + }, + }); return ( {children} ); @@ -14,8 +23,50 @@ const customRender = ( options?: Omit ) => render(ui, { wrapper: AllTheProviders, ...options }); +const customRenderHook = ( + callback: (props: TProps) => TResult, + options?: Omit, "wrapper"> +) => { + const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + {children} + ); + + return renderHook(callback, { wrapper: Wrapper as React.FC, ...options }); +}; + // re-export everything export * from "@testing-library/react"; // override render method export { customRender as render }; +export { customRenderHook as renderHook }; + +export const createMockAssessment = ( + overrides: Partial = {} +): Assessment => { + return { + id: Math.random(), + name: "Default name", + description: "Default description", + required: true, + ...overrides, + } as Assessment; +}; + +export const createMockApplication = (overrides: Partial = {}) => { + return { + id: Math.random(), + name: "Default name", + description: "Default description", + ...overrides, + } as Application; +}; + +export const createMockArchetype = (overrides: Partial = {}) => { + return { + id: Math.random(), + name: "Default name", + description: "Default description", + ...overrides, + } as Archetype; +}; diff --git a/client/src/mocks/server.ts b/client/src/mocks/server.ts index e69de29bb..6402a75f2 100644 --- a/client/src/mocks/server.ts +++ b/client/src/mocks/server.ts @@ -0,0 +1,14 @@ +// src/mocks/server.js +import { rest } from "msw"; +import { setupServer } from "msw/node"; + +const localeHandler = rest.get( + "http://localhost/locales/en/translation.json", + (req, res, ctx) => { + return res(ctx.json({})); + } +); + +const handlers = [localeHandler].filter(Boolean); + +export const server = setupServer(...handlers); diff --git a/client/tsconfig.json b/client/tsconfig.json index b2fb8689e..26f410a79 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -10,7 +10,8 @@ "@app/*": ["src/app/*"], "@assets/*": [ "../node_modules/@patternfly/react-core/dist/styles/assets/*" - ] + ], + "@mocks/*": ["src/mocks/*"] }, "typeRoots": ["types/", "node_modules/@types", "../node_modules/@types"],