Repository: opossum-tool/OpossumUI (ts)

Issue:
Add package description and homepage into comment field
**Is your feature request related to a problem? Please describe.**
As a user, I want to see a package description and URLs to the package's homepage, if they exist, in the "comment" field of an attribution when I select a package suggestion via the "+" button of one of the package comboxes.

**Describe the solution you'd like**
Deps.dev sends this information already in the response payload to the version endpoint. Extract it from there and write it into the comment field.

**Additional context**
#2333 


Pull request:
feat: add package description and homepage into comment field
### Summary of changes

- Open Source Insights (deps.dev) provides information such as description and links to homepage and origins which we add to the "comment" field of an attribution when the user selects a suggestion from one of the package comboboxes via the "+" button

![image](https://github.com/opossum-tool/OpossumUI/assets/46091730/4ffd4f51-3bab-44d8-b864-76b7080b2dc1)

### Context and reason for change

Closes #2439 

### How can the changes be tested

Only works for packages (not projects).

Patch:
diff --git a/src/Frontend/util/package-search-api.ts b/src/Frontend/util/package-search-api.ts
index 5bc656b0..3e35704d 100644
--- a/src/Frontend/util/package-search-api.ts
+++ b/src/Frontend/util/package-search-api.ts
@@ -80,8 +80,9 @@ export interface Links {
 
 export interface WebVersionResponse {
   version: {
-    licenses: Array<string>;
-    links: Links;
+    description?: string;
+    licenses?: Array<string>;
+    links?: Links;
   };
 }
 
@@ -470,18 +471,33 @@ export class PackageSearchApi {
     }
 
     const {
-      version: { links, licenses },
+      version: { links, licenses, description },
     }: WebVersionResponse = await response.json();
 
     return this.enrichPackageInfoViaRepoUrl({
       ...packageInfo,
+      comments: packageInfo.comments || [
+        [
+          ...(description
+            ? [`${text.attributionColumn.description}: ${description}`]
+            : []),
+          ...(links?.homepage
+            ? [`${text.attributionColumn.homepage}: ${links.homepage}`]
+            : []),
+          ...(links?.origins?.length
+            ? links.origins.map(
+                (origin, index, origins) =>
+                  `${text.attributionColumn.origin}${
+                    origins.length > 1 ? ` #${index + 1}` : ''
+                  }: ${origin}`,
+              )
+            : []),
+        ].join('\n'),
+      ],
       licenseName:
         packageInfo.licenseName ||
-        licenses
-          .filter((license) => license !== DEPS_DEV_NON_STANDARD_LICENSE_MARKER)
-          .join(' AND '),
-      url:
-        packageInfo.url || links.repo || links.homepage || links.origins?.[0],
+        licenses?.filter((license) => license !== DEPS_DEV_NON_STANDARD_LICENSE_MARKER).join(' AND '),
+      url: packageInfo.url || links?.repo,
     });
   }
 
diff --git a/src/shared/Faker.ts b/src/shared/Faker.ts
index 61d5d9cd..11fff13b 100644
--- a/src/shared/Faker.ts
+++ b/src/shared/Faker.ts
@@ -479,6 +479,9 @@ class PackageSearchModule {
   public static links(props: Partial<Links> = {}): Links {
     return {
       origins: faker.helpers.multiple(faker.internet.url),
+      homepage: faker.internet.url(),
+      repo: faker.internet.url(),
+      issues: faker.internet.url(),
       ...props,
     };
   }
diff --git a/src/shared/text.ts b/src/shared/text.ts
index 2aedb610..56df1ac8 100644
--- a/src/shared/text.ts
+++ b/src/shared/text.ts
@@ -33,6 +33,9 @@ export const text = {
       'Adopt all coordinates and legal information from suggestion',
     originallyFrom: 'Originally from ',
     invalidPurl: 'INVALID PURL',
+    description: 'Description',
+    homepage: 'Homepage',
+    origin: 'Origin',
   },
   changePreferredStatusGloballyPopup: {
     markAsPreferred: 'Do you really want to prefer the attribution globally?',


Test patch:
diff --git a/src/Frontend/util/__tests__/package-search-api.test.ts b/src/Frontend/util/__tests__/package-search-api.test.ts
index 17ab966d..220c42f1 100644
--- a/src/Frontend/util/__tests__/package-search-api.test.ts
+++ b/src/Frontend/util/__tests__/package-search-api.test.ts
@@ -441,15 +441,12 @@ describe('PackageSearchApi', () => {
         url: undefined,
         licenseName: undefined,
       });
-      const repoUrl = faker.internet.url();
+      const links = faker.packageSearch.links({ origins: [] });
       const license1 = faker.commerce.productName();
       const license2 = faker.commerce.productName();
       const httpClient = faker.httpClient(
         faker.packageSearch.webVersionResponse({
-          version: {
-            links: faker.packageSearch.links({ repo: repoUrl }),
-            licenses: [license1, license2],
-          },
+          version: { links, licenses: [license1, license2] },
         }),
       );
       const packageSearchApi = new PackageSearchApi(httpClient);
@@ -459,57 +456,57 @@ describe('PackageSearchApi', () => {
       expect(httpClient.request).toHaveBeenCalledTimes(1);
       expect(enriched).toEqual(
         expect.objectContaining<Partial<DisplayPackageInfo>>({
-          url: repoUrl,
+          url: links.repo,
           licenseName: `${license1} AND ${license2}`,
+          comments: [`${text.attributionColumn.homepage}: ${links.homepage!}`],
         }),
       );
     });
 
-    it('falls back to homepage URL when repo URL not available', async () => {
+    it('gets license based on package default version if no package version provided', async () => {
       const packageInfo = faker.opossum.displayPackageInfo({
         packageType: faker.packageSearch.packageSystem(),
         packageNamespace: faker.internet.domainWord(),
+        packageVersion: undefined,
         url: undefined,
         licenseName: undefined,
+        copyright: undefined,
       });
-      const homepageUrl = faker.internet.url();
+      const defaultVersion = faker.system.semver();
       const httpClient = faker.httpClient(
-        faker.packageSearch.webVersionResponse({
-          version: {
-            links: faker.packageSearch.links({ homepage: homepageUrl }),
-            licenses: [],
-          },
-        }),
+        faker.packageSearch.defaultVersionResponse({ defaultVersion }),
+        faker.packageSearch.webVersionResponse(),
       );
       const packageSearchApi = new PackageSearchApi(httpClient);
 
-      const enriched = await packageSearchApi.enrichPackageInfo(packageInfo);
+      await packageSearchApi.enrichPackageInfo(packageInfo);
 
-      expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(enriched).toEqual(
-        expect.objectContaining<Partial<DisplayPackageInfo>>({
-          url: homepageUrl,
-          licenseName: '',
+      expect(httpClient.request).toHaveBeenCalledTimes(2);
+      expect(httpClient.request).toHaveBeenNthCalledWith(
+        2,
+        expect.objectContaining<Partial<RequestProps>>({
+          path: expect.stringContaining(`/v/${defaultVersion}`),
         }),
       );
     });
 
-    it('falls back to first origin URL when neither repo URL nor homepage URL are available', async () => {
+    it('provides repo URL, license, and copyright for a GitHub project', async () => {
+      const name = faker.internet.domainWord();
+      const namespace = faker.internet.domainWord();
       const packageInfo = faker.opossum.displayPackageInfo({
-        packageType: faker.packageSearch.packageSystem(),
-        packageNamespace: faker.internet.domainWord(),
+        packageType: 'github',
+        packageName: name,
+        packageNamespace: namespace,
         url: undefined,
         licenseName: undefined,
+        copyright: undefined,
       });
-      const originUrl = faker.internet.url();
+      const license = faker.commerce.productName();
+      const copyright = faker.opossum.copyright();
       const httpClient = faker.httpClient(
-        faker.packageSearch.webVersionResponse({
-          version: {
-            links: faker.packageSearch.links({
-              origins: [originUrl, faker.internet.url()],
-            }),
-            licenses: [],
-          },
+        faker.packageSearch.gitHubLicenseResponse({
+          license: { name: license },
+          content: faker.packageSearch.licenseTextWithCopyright(copyright),
         }),
       );
       const packageSearchApi = new PackageSearchApi(httpClient);
@@ -519,136 +516,61 @@ describe('PackageSearchApi', () => {
       expect(httpClient.request).toHaveBeenCalledTimes(1);
       expect(enriched).toEqual(
         expect.objectContaining<Partial<DisplayPackageInfo>>({
-          url: originUrl,
-          licenseName: '',
+          url: `https://github.com/${namespace}/${name}`,
+          licenseName: license,
+          packageVersion: packageInfo.packageVersion,
+          copyright,
         }),
       );
     });
-  });
-
-  it('does not enrich package info if URL, copyright, and license name are already known', async () => {
-    const packageInfo = faker.opossum.displayPackageInfo({
-      packageType: faker.packageSearch.packageSystem(),
-      packageNamespace: faker.internet.domainWord(),
-    });
-    const httpClient = faker.httpClient();
-    const packageSearchApi = new PackageSearchApi(httpClient);
 
-    await packageSearchApi.enrichPackageInfo(packageInfo);
+    it('provides repo URL, license, and copyright for a GitLab project', async () => {
+      const name = faker.internet.domainWord();
+      const namespace = faker.internet.domainWord();
+      const packageInfo = faker.opossum.displayPackageInfo({
+        packageType: 'gitlab',
+        packageName: name,
+        packageNamespace: namespace,
+        url: undefined,
+        licenseName: undefined,
+        copyright: undefined,
+      });
+      const license = faker.commerce.productName();
+      const copyright = faker.opossum.copyright();
+      const httpClient = faker.httpClient(
+        faker.packageSearch.gitLabProjectResponse({
+          license: { name: license },
+        }),
+        faker.packageSearch.gitLabLicenseResponse({
+          content: faker.packageSearch.licenseTextWithCopyright(copyright),
+        }),
+      );
+      const packageSearchApi = new PackageSearchApi(httpClient);
 
-    expect(httpClient.request).not.toHaveBeenCalled();
-  });
+      const enriched = await packageSearchApi.enrichPackageInfo(packageInfo);
 
-  it('gets license based on package default version if no package version provided', async () => {
-    const packageInfo = faker.opossum.displayPackageInfo({
-      packageType: faker.packageSearch.packageSystem(),
-      packageNamespace: faker.internet.domainWord(),
-      url: undefined,
-      licenseName: undefined,
-      copyright: undefined,
-      packageVersion: undefined,
+      expect(httpClient.request).toHaveBeenCalledTimes(2);
+      expect(enriched).toEqual(
+        expect.objectContaining<Partial<DisplayPackageInfo>>({
+          url: `https://gitlab.com/${namespace}/${name}`,
+          licenseName: license,
+          packageVersion: packageInfo.packageVersion,
+          copyright,
+        }),
+      );
     });
-    const defaultVersion = faker.system.semver();
-    const httpClient = faker.httpClient(
-      faker.packageSearch.defaultVersionResponse({ defaultVersion }),
-      faker.packageSearch.webVersionResponse(),
-    );
-    const packageSearchApi = new PackageSearchApi(httpClient);
-
-    await packageSearchApi.enrichPackageInfo(packageInfo);
-
-    expect(httpClient.request).toHaveBeenCalledTimes(2);
-    expect(httpClient.request).toHaveBeenNthCalledWith(
-      2,
-      expect.objectContaining<Partial<RequestProps>>({
-        path: expect.stringContaining(`/v/${defaultVersion}`),
-      }),
-    );
-  });
 
-  it('provides repo URL, license, and copyright for a GitHub project', async () => {
-    const name = faker.internet.domainWord();
-    const namespace = faker.internet.domainWord();
-    const packageInfo = faker.opossum.displayPackageInfo({
-      packageType: 'github',
-      packageName: name,
-      packageNamespace: namespace,
-      url: undefined,
-      licenseName: undefined,
-      copyright: undefined,
-    });
-    const license = faker.commerce.productName();
-    const copyright = faker.opossum.copyright();
-    const httpClient = faker.httpClient(
-      faker.packageSearch.gitHubLicenseResponse({
-        license: { name: license },
-        content: faker.packageSearch.licenseTextWithCopyright(copyright),
-      }),
-    );
-    const packageSearchApi = new PackageSearchApi(httpClient);
-
-    const enriched = await packageSearchApi.enrichPackageInfo(packageInfo);
-
-    expect(httpClient.request).toHaveBeenCalledTimes(1);
-    expect(enriched).toEqual(
-      expect.objectContaining<Partial<DisplayPackageInfo>>({
-        url: `https://github.com/${namespace}/${name}`,
-        licenseName: license,
-        packageVersion: packageInfo.packageVersion,
-        copyright,
-      }),
-    );
-  });
+    it('does not enrich package info if URL, copyright, and license name are already known', async () => {
+      const packageInfo = faker.opossum.displayPackageInfo({
+        packageType: faker.packageSearch.packageSystem(),
+        packageNamespace: faker.internet.domainWord(),
+      });
+      const httpClient = faker.httpClient();
+      const packageSearchApi = new PackageSearchApi(httpClient);
 
-  it('provides repo URL, license, and copyright for a GitLab project', async () => {
-    const name = faker.internet.domainWord();
-    const namespace = faker.internet.domainWord();
-    const packageInfo = faker.opossum.displayPackageInfo({
-      packageType: 'gitlab',
-      packageName: name,
-      packageNamespace: namespace,
-      url: undefined,
-      licenseName: undefined,
-      copyright: undefined,
-    });
-    const license = faker.commerce.productName();
-    const copyright = faker.opossum.copyright();
-    const httpClient = faker.httpClient(
-      faker.packageSearch.gitLabProjectResponse({
-        license: { name: license },
-      }),
-      faker.packageSearch.gitLabLicenseResponse({
-        content: faker.packageSearch.licenseTextWithCopyright(copyright),
-      }),
-    );
-    const packageSearchApi = new PackageSearchApi(httpClient);
-
-    const enriched = await packageSearchApi.enrichPackageInfo(packageInfo);
-
-    expect(httpClient.request).toHaveBeenCalledTimes(2);
-    expect(enriched).toEqual(
-      expect.objectContaining<Partial<DisplayPackageInfo>>({
-        url: `https://gitlab.com/${namespace}/${name}`,
-        licenseName: license,
-        packageVersion: packageInfo.packageVersion,
-        copyright,
-      }),
-    );
-  });
+      await packageSearchApi.enrichPackageInfo(packageInfo);
 
-  it('does not make any request if URL, license name, and copyright of a project are already known', async () => {
-    const packageInfo = faker.opossum.displayPackageInfo({
-      packageType: faker.packageSearch.projectType(),
-      packageNamespace: faker.internet.domainWord(),
-      url: faker.internet.url(),
-      licenseName: faker.commerce.productName(),
-      copyright: faker.opossum.copyright(),
+      expect(httpClient.request).not.toHaveBeenCalled();
     });
-    const httpClient = faker.httpClient();
-    const packageSearchApi = new PackageSearchApi(httpClient);
-
-    await packageSearchApi.enrichPackageInfo(packageInfo);
-
-    expect(httpClient.request).not.toHaveBeenCalled();
   });
 });