diff --git a/assets/js/lib/test-utils/factories/advisoryErrata.js b/assets/js/lib/test-utils/factories/advisoryErrata.js index ce184270fc..8a01df2190 100644 --- a/assets/js/lib/test-utils/factories/advisoryErrata.js +++ b/assets/js/lib/test-utils/factories/advisoryErrata.js @@ -2,6 +2,18 @@ import { faker } from '@faker-js/faker'; import { Factory } from 'fishery'; import { advisoryType } from './relevantPatches'; +const affectedPackageFactory = Factory.define(({ sequence }) => ({ + name: `${faker.animal.cat().toLowerCase()}${sequence}`, + arch_label: faker.helpers.arrayElement(['x86_64', 'i586', 'aarch64']), + version: faker.system.semver(), + release: `${faker.number.int({ min: 0, max: 100 })}`, + epoch: `${faker.number.int({ min: 0, max: 50 })}`, +})); + +const affectedSystemFactory = Factory.define(({ sequence }) => ({ + name: `${faker.string.uuid()}-${sequence}`, +})); + const fixMapFactory = Factory.define(({ transientParams }) => { const { length = 1 } = transientParams; @@ -31,6 +43,8 @@ export const advisoryErrataFactory = Factory.define(({ params }) => ({ { transient: { length: faker.number.int({ min: 1, max: 10 }) } } ), cves: cveFactory.buildList(10), + affected_packages: affectedPackageFactory.buildList(10), + affected_systems: affectedSystemFactory.buildList(10), errata_details: { id: faker.number.int({ min: 1, max: 65536 }), issue_date: faker.date.recent({ days: 30 }), diff --git a/assets/js/pages/AdvisoryDetails/AdvisoryDetails.jsx b/assets/js/pages/AdvisoryDetails/AdvisoryDetails.jsx index 19924828ee..1640f1a418 100644 --- a/assets/js/pages/AdvisoryDetails/AdvisoryDetails.jsx +++ b/assets/js/pages/AdvisoryDetails/AdvisoryDetails.jsx @@ -5,6 +5,9 @@ import PageHeader from '@common/PageHeader'; import ListView from '@common/ListView'; import AdvisoryIcon from '@common/AdvisoryIcon'; +const formatPackage = ({ name, version, epoch, release, arch_label }) => + `${name}-${version}-${epoch}.${release}-${arch_label}`; + function EmptyData() { return

No data available

; } @@ -12,8 +15,7 @@ function EmptyData() { function AdvisoryDetails({ advisoryName, errata, - packages, - affectsPackageMaintanaceStack, + affectsPackageMaintenanceStack, }) { const { issue_date: issueDate, @@ -25,7 +27,12 @@ function AdvisoryDetails({ reboot_suggested: rebootSuggested, } = errata.errata_details; - const { fixes, cves } = errata; + const { + fixes, + cves, + affected_packages: affectedPackages, + affected_systems: affectedSystems, + } = errata; return (
@@ -60,8 +67,8 @@ function AdvisoryDetails({ content: rebootSuggested ? 'Yes' : 'No', }, { - title: 'Affects Package Maintanace Stack', - content: affectsPackageMaintanaceStack ? 'Yes' : 'No', + title: 'Affects Package Maintenance Stack', + content: affectsPackageMaintenanceStack ? 'Yes' : 'No', }, ]} /> @@ -118,10 +125,24 @@ function AdvisoryDetails({

Affected Packages

- {packages && packages.length ? ( + {affectedPackages && affectedPackages.length ? ( +
    + {affectedPackages.map((pkg) => ( +
  • {formatPackage(pkg)}
  • + ))} +
+ ) : ( + + )} +
+
+
+

Affected Systems

+
+ {affectedSystems && affectedSystems.length ? (
    - {packages.map((pkg) => ( -
  • {pkg}
  • + {affectedSystems.map(({ name }) => ( +
  • {name}
  • ))}
) : ( diff --git a/assets/js/pages/AdvisoryDetails/AdvisoryDetails.stories.jsx b/assets/js/pages/AdvisoryDetails/AdvisoryDetails.stories.jsx index 18ee771edf..eca6b4176b 100644 --- a/assets/js/pages/AdvisoryDetails/AdvisoryDetails.stories.jsx +++ b/assets/js/pages/AdvisoryDetails/AdvisoryDetails.stories.jsx @@ -28,8 +28,45 @@ However, the post didn't come by today, and I am starting to wonder, if my Geeko 4815162342: 'Geekos unexpectedly eating quiches', }, cves: ['CVE-2024-35938'], + affected_packages: [ + { + name: 'libprocps7', + version: '3.3.15', + release: '7.34.1', + epoch: '150000', + arch_label: 'x86_64', + }, + ], + affected_systems: [ + { + name: 'vmdrbddev01', + }, + ], }, - packages: undefined, - affectsPackageMaintanaceStack: false, + affectsPackageMaintenanceStack: false, + }, +}; + +export const Empty = { + args: { + advisoryName: 'SUSE-15-SP4-2023-3369', + errata: { + errata_details: { + issue_date: Date.now(), + update_date: Date.now(), + synopsis: 'I think my Geekos ate my quiche 🦎🦎', + advisory_status: 'stable', + type: 'security_advisory', + description: `My Geekos really love the cakes I order from the crab bakery. +Yesterday, I left before the post arrived. Normally, the post just delivers my packages the next day. +However, the post didn't come by today, and I am starting to wonder, if my Geekos ate my quiche. AITA? 😟`, + reboot_suggested: true, + }, + fixes: {}, + cves: [], + affected_packages: [], + affected_systems: [], + }, + affectsPackageMaintenanceStack: false, }, }; diff --git a/assets/js/pages/AdvisoryDetails/AdvisoryDetails.test.jsx b/assets/js/pages/AdvisoryDetails/AdvisoryDetails.test.jsx index c336562c57..862a7bbbce 100644 --- a/assets/js/pages/AdvisoryDetails/AdvisoryDetails.test.jsx +++ b/assets/js/pages/AdvisoryDetails/AdvisoryDetails.test.jsx @@ -11,13 +11,18 @@ import AdvisoryDetails from './AdvisoryDetails'; describe('AdvisoryDetails', () => { it('displays a message, when the CVE, packages or fixes section is empty', () => { - const errata = advisoryErrataFactory.build({ cves: [], fixes: {} }); + const errata = advisoryErrataFactory.build({ + cves: [], + fixes: {}, + affected_packages: [], + affected_systems: [], + }); render( ); - expect(screen.getAllByText('No data available').length).toBe(3); + expect(screen.getAllByText('No data available').length).toBe(4); }); it('displays relevant errata data', () => { @@ -34,22 +39,27 @@ describe('AdvisoryDetails', () => { expect(screen.getByText(errata.errata_details.description)).toBeVisible(); }); - it('displays packages', () => { + it('displays affected packages', () => { const errata = advisoryErrataFactory.build(); const advisoryName = faker.lorem.word(); - const packages = faker.word.words(2).split(' '); + render(); - render( - - ); + errata.affected_packages.forEach(({ name }) => { + const el = screen.getByText(name, { exact: false }); + expect(el).toBeVisible(); + }); + }); + + it('displays affected systems', () => { + const errata = advisoryErrataFactory.build(); + const advisoryName = faker.lorem.word(); - packages.forEach((expectedWord) => { - expect(screen.getByText(expectedWord)).toBeVisible(); + render(); + + errata.affected_systems.forEach(({ name }) => { + const el = screen.getByText(name); + expect(el).toBeVisible(); }); }); diff --git a/lib/trento_web/controllers/fallback_controller.ex b/lib/trento_web/controllers/fallback_controller.ex index 25cfd95abc..5a3eed9717 100644 --- a/lib/trento_web/controllers/fallback_controller.ex +++ b/lib/trento_web/controllers/fallback_controller.ex @@ -138,6 +138,20 @@ defmodule TrentoWeb.FallbackController do |> render(:"422", reason: "Unable to retrieve Bugzilla fixes for this advisory.") end + def call(conn, {:error, :error_getting_affected_packages}) do + conn + |> put_status(:unprocessable_entity) + |> put_view(ErrorView) + |> render(:"422", reason: "Unable to retrieve affected packages for this advisory.") + end + + def call(conn, {:error, :error_getting_affected_systems}) do + conn + |> put_status(:unprocessable_entity) + |> put_view(ErrorView) + |> render(:"422", reason: "Unable to retrieve affected systems for this advisory.") + end + def call(conn, {:error, :connection_test_failed}) do conn |> put_status(:unprocessable_entity) diff --git a/lib/trento_web/controllers/v1/suse_manager_controller.ex b/lib/trento_web/controllers/v1/suse_manager_controller.ex index 69cc4f3da2..68db614285 100644 --- a/lib/trento_web/controllers/v1/suse_manager_controller.ex +++ b/lib/trento_web/controllers/v1/suse_manager_controller.ex @@ -96,8 +96,16 @@ defmodule TrentoWeb.V1.SUSEManagerController do def errata_details(conn, %{advisory_name: advisory_name}) do with {:ok, errata_details} <- Discovery.get_errata_details(advisory_name), {:ok, cves} <- Discovery.get_cves(advisory_name), - {:ok, fixes} <- Discovery.get_bugzilla_fixes(advisory_name) do - render(conn, %{errata_details: errata_details, cves: cves, fixes: fixes}) + {:ok, fixes} <- Discovery.get_bugzilla_fixes(advisory_name), + {:ok, affected_packages} <- Discovery.get_affected_packages(advisory_name), + {:ok, affected_systems} <- Discovery.get_affected_systems(advisory_name) do + render(conn, %{ + errata_details: errata_details, + cves: cves, + fixes: fixes, + affected_packages: affected_packages, + affected_systems: affected_systems + }) end end end diff --git a/lib/trento_web/openapi/v1/schema/available_software_updates.ex b/lib/trento_web/openapi/v1/schema/available_software_updates.ex index e5e2f3ce76..0fb84c6b87 100644 --- a/lib/trento_web/openapi/v1/schema/available_software_updates.ex +++ b/lib/trento_web/openapi/v1/schema/available_software_updates.ex @@ -197,6 +197,64 @@ defmodule TrentoWeb.OpenApi.V1.Schema.AvailableSoftwareUpdates do }) end + defmodule AffectedPackages do + @moduledoc false + OpenApiSpex.schema(%{ + title: "AffectedPackages", + description: "Response returned from the get affected packages endpoint", + type: :array, + additionalProperties: false, + items: %Schema{ + title: "AffectedPackage", + description: "Metadata for a package effected by an advisory", + type: :object, + properties: %{ + name: %Schema{ + type: :string, + description: "Package name" + }, + arch_label: %Schema{ + type: :string, + description: "Package architecture" + }, + version: %Schema{ + type: :string, + description: "Package upstream version" + }, + release: %Schema{ + type: :string, + description: "Package RPM release number" + }, + epoch: %Schema{ + type: :string, + description: "Package epoch number" + } + } + } + }) + end + + defmodule AffectedSystems do + @moduledoc false + OpenApiSpex.schema(%{ + title: "AffectedSystems", + description: "Response returned from the get affected systems endpoint", + type: :array, + additionalProperties: false, + items: %Schema{ + title: "AffectedSystem", + description: "Metadata for a system effected by an advisory", + type: :object, + properties: %{ + name: %Schema{ + type: :string, + description: "System name" + } + } + } + }) + end + defmodule ErrataDetailsResponse do @moduledoc false OpenApiSpex.schema(%{ @@ -207,7 +265,9 @@ defmodule TrentoWeb.OpenApi.V1.Schema.AvailableSoftwareUpdates do properties: %{ errata_details: ErrataDetails, cves: CVEs, - fixes: AdvisoryFixes + fixes: AdvisoryFixes, + affected_packages: AffectedPackages, + affected_systems: AffectedSystems } }) end diff --git a/lib/trento_web/views/v1/suse_manager_view.ex b/lib/trento_web/views/v1/suse_manager_view.ex index 8c26911e9b..f8213e4fca 100644 --- a/lib/trento_web/views/v1/suse_manager_view.ex +++ b/lib/trento_web/views/v1/suse_manager_view.ex @@ -66,7 +66,9 @@ defmodule TrentoWeb.V1.SUSEManagerView do def render("errata_details.json", %{ errata_details: errata_details = %{errataFrom: errataFrom}, cves: cves, - fixes: fixes + fixes: fixes, + affected_packages: affected_packages, + affected_systems: affected_systems }), do: %{ errata_details: @@ -74,6 +76,8 @@ defmodule TrentoWeb.V1.SUSEManagerView do |> Map.drop([:errataFrom]) |> Map.put(:errata_from, errataFrom), cves: cves, - fixes: fixes + fixes: fixes, + affected_packages: affected_packages, + affected_systems: affected_systems } end diff --git a/test/trento_web/controllers/v1/suse_manager_controller_test.exs b/test/trento_web/controllers/v1/suse_manager_controller_test.exs index d5b24a4804..e26eae7ae9 100644 --- a/test/trento_web/controllers/v1/suse_manager_controller_test.exs +++ b/test/trento_web/controllers/v1/suse_manager_controller_test.exs @@ -184,6 +184,18 @@ defmodule TrentoWeb.V1.SUSEManagerControllerTest do {:ok, fixes} end) + affected_packages = build_list(10, :affected_package) + + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_affected_packages, 1, fn _ -> + {:ok, affected_packages} + end) + + affected_systems = build_list(10, :affected_system) + + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_affected_systems, 1, fn _ -> + {:ok, affected_systems} + end) + json = conn |> get("/api/v1/software_updates/errata_details/#{advisory_name}") @@ -218,7 +230,9 @@ defmodule TrentoWeb.V1.SUSEManagerControllerTest do reboot_suggested: ^reboot_suggested, restart_suggested: ^restart_suggested }, - cves: ^cves + cves: ^cves, + affected_packages: ^affected_packages, + affected_systems: ^affected_systems } = result end @@ -240,6 +254,14 @@ defmodule TrentoWeb.V1.SUSEManagerControllerTest do {:ok, build(:bugzilla_fix)} end) + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_affected_packages, 1, fn _ -> + {:ok, build_list(10, :affected_package)} + end) + + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_affected_systems, 1, fn _ -> + {:ok, build_list(10, :affected_system)} + end) + advisory_name = Faker.Pokemon.name() conn @@ -266,6 +288,14 @@ defmodule TrentoWeb.V1.SUSEManagerControllerTest do {:ok, build(:bugzilla_fix)} end) + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_affected_packages, 1, fn _ -> + {:ok, build_list(10, :affected_package)} + end) + + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_affected_systems, 1, fn _ -> + {:ok, build_list(10, :affected_system)} + end) + advisory_name = Faker.Pokemon.name() conn @@ -292,6 +322,82 @@ defmodule TrentoWeb.V1.SUSEManagerControllerTest do {:error, :error_getting_fixes} end) + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_affected_packages, 1, fn _ -> + {:ok, build_list(10, :affected_package)} + end) + + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_affected_systems, 1, fn _ -> + {:ok, build_list(10, :affected_system)} + end) + + advisory_name = Faker.Pokemon.name() + + conn + |> get("/api/v1/software_updates/errata_details/#{advisory_name}") + |> json_response(:unprocessable_entity) + |> assert_schema("UnprocessableEntity", api_spec) + end + + test "should return 422 when advisory affected packages are not found", %{ + conn: conn, + api_spec: api_spec + } do + insert_software_updates_settings() + + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_errata_details, 1, fn _ -> + {:ok, build(:errata_details)} + end) + + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_cves, 1, fn _ -> + {:ok, build_list(10, :cve)} + end) + + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_bugzilla_fixes, 1, fn _ -> + {:ok, build(:bugzilla_fix)} + end) + + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_affected_packages, 1, fn _ -> + {:error, :error_getting_affected_packages} + end) + + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_affected_systems, 1, fn _ -> + {:ok, build_list(10, :affected_system)} + end) + + advisory_name = Faker.Pokemon.name() + + conn + |> get("/api/v1/software_updates/errata_details/#{advisory_name}") + |> json_response(:unprocessable_entity) + |> assert_schema("UnprocessableEntity", api_spec) + end + + test "should return 422 when advisory affected systems are not found", %{ + conn: conn, + api_spec: api_spec + } do + insert_software_updates_settings() + + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_errata_details, 1, fn _ -> + {:ok, build(:errata_details)} + end) + + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_cves, 1, fn _ -> + {:ok, build_list(10, :cve)} + end) + + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_bugzilla_fixes, 1, fn _ -> + {:ok, build(:bugzilla_fix)} + end) + + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_affected_packages, 1, fn _ -> + {:ok, build_list(10, :affected_package)} + end) + + expect(Trento.SoftwareUpdates.Discovery.Mock, :get_affected_systems, 1, fn _ -> + {:error, :error_getting_affected_systems} + end) + advisory_name = Faker.Pokemon.name() conn diff --git a/test/trento_web/views/v1/suse_manager_view_test.exs b/test/trento_web/views/v1/suse_manager_view_test.exs index 03c70fe697..a459485bad 100644 --- a/test/trento_web/views/v1/suse_manager_view_test.exs +++ b/test/trento_web/views/v1/suse_manager_view_test.exs @@ -33,16 +33,24 @@ defmodule TrentoWeb.V1.SUSEManagerViewTest do fixes = build(:bugzilla_fix) + affected_packages = build_list(10, :affected_package) + + affected_systems = build_list(10, :affected_system) + assert %{ errata_details: ^expected_errata_details, cves: ^cves, - fixes: ^fixes + fixes: ^fixes, + affected_packages: ^affected_packages, + affected_systems: ^affected_systems } = render(SUSEManagerView, "errata_details.json", %{ errata_details: Map.put(errata_details_sans_errata_from, :errataFrom, errata_from), cves: cves, - fixes: fixes + fixes: fixes, + affected_packages: affected_packages, + affected_systems: affected_systems }) end end