diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aca15f5ad..c25910c935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,13 +10,17 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added - Added AVD-KSV-0014 to trivy ignore - Added tooltips on functionalities that are unauthorized or unavailable -- +- Added concept #521 revoked notification handling +- Added eclipse trace-x matrix channel to README.md and CONTRIBUTING.md + ### Changed - Updated Irs Library from 1.4.1-SNAPSHOT to 1.5.1-SNAPSHOT - Changed some java implementations according to security findings ( business logic unchanged ) - Adjusted sync logic to create jobs only for related BomLifecycles - Spring core updated from 6.0.14 to 6.0.16 - Springboot updated from 3.1.6 to 3.1.7 +- Implemented asset publisher component functionality +- Updated postgres to version 15.4 ### Removed @@ -40,7 +44,6 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Added GET /policies endpoint to retrieve accepted policies - Added POST assets/publish endpoint to publish transient assets - ### Changed - Fixed security findings - Rework GET alerts and investigations endpoint to POST to send a request body @@ -65,6 +68,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Updated github/codeql-action from 2 to 3 - Updated actions/download-artifact from 3 to 4actions/download-artifact from 3 to 4 - Updated com.nimbusds:nimbus-jose-jwt from 9.37.1 to 9.37.3 +- Changed some java implementations according to security findings ( business logic unchanged ) +- Updated createIrsPolicyIfMissing() method to validate policies based on rightOperand values rather than policyIDs ### Removed - Shell descriptor entity with underlying logic diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index efa7d8d664..ace02fa04d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -178,8 +178,12 @@ Attributes in Angular template should be properly ordered by groups: `java -jar scripts/download/org.eclipse.dash.licenses-0.0.1-SNAPSHOT.jar yarn.lock -review -token -project automotive.tractusx` -## Contact +## Contact -Contact the project developers via the project's "dev" list. +Contact the Eclipse Tractus-X developers via the developer mailing list. -- https://accounts.eclipse.org/mailing-list/tractusx-dev +* https://accounts.eclipse.org/mailing-list/tractusx-dev + +Contact the project developers via eclipse matrix chat. + +* Eclipse Matrix Chat https://chat.eclipse.org/#/room/#tractusx-trace-x:matrix.eclipse.org diff --git a/README.md b/README.md index ec4ad0eb48..d030644b38 100644 --- a/README.md +++ b/README.md @@ -180,3 +180,13 @@ DockerHub Frontend: https://hub.docker.com/r/tractusx/traceability-foss-frontend As with all Docker images, these likely also contain other software which may be under other licenses (such as Bash, etc from the base distribution, along with any direct or indirect dependencies of the primary software being contained). As for any pre-built image usage, it is the image user's responsibility to ensure that any use of this image complies with any relevant licenses for all software contained within. + +## Contact + +Contact the Eclipse Tractus-X developers via the developer mailing list. + +* https://accounts.eclipse.org/mailing-list/tractusx-dev + +Contact the project developers via eclipse matrix chat. + +* Eclipse Matrix Chat https://chat.eclipse.org/#/room/#tractusx-trace-x:matrix.eclipse.org diff --git a/charts/traceability-foss/Chart.yaml b/charts/traceability-foss/Chart.yaml index 93d75a3487..07ce1a93dc 100644 --- a/charts/traceability-foss/Chart.yaml +++ b/charts/traceability-foss/Chart.yaml @@ -34,11 +34,11 @@ dependencies: version: 1.3.27 - name: postgresql repository: https://charts.bitnami.com/bitnami - version: 12.1.6 + version: 12.12.10 condition: postgresql.enabled - name: pgadmin4 repository: https://helm.runix.net - version: 1.13.6 + version: 1.23.1 condition: pgadmin4.enabled - name: irs-helm repository: https://eclipse-tractusx.github.io/item-relationship-service @@ -51,5 +51,5 @@ dependencies: - name: postgresql alias: edc-postgresql repository: https://charts.bitnami.com/bitnami - version: 12.1.6 + version: 12.12.10 condition: edc-postgresql.enabled diff --git a/charts/traceability-foss/charts/backend/Chart.yaml b/charts/traceability-foss/charts/backend/Chart.yaml index a0404a17c9..c15e4eda68 100644 --- a/charts/traceability-foss/charts/backend/Chart.yaml +++ b/charts/traceability-foss/charts/backend/Chart.yaml @@ -25,11 +25,11 @@ appVersion: "10.2.1" dependencies: - name: postgresql repository: https://charts.bitnami.com/bitnami - version: 12.1.6 + version: 12.12.10 condition: postgresql.enabled - name: pgadmin4 repository: https://helm.runix.net - version: 1.13.6 + version: 1.23.1 condition: pgadmin4.enabled - name: irs-helm repository: https://eclipse-tractusx.github.io/item-relationship-service diff --git a/docs/concept/#521-revoked-notification-handling/#521-revoked-notification-handling.md b/docs/concept/#521-revoked-notification-handling/#521-revoked-notification-handling.md new file mode 100644 index 0000000000..f4c4572650 --- /dev/null +++ b/docs/concept/#521-revoked-notification-handling/#521-revoked-notification-handling.md @@ -0,0 +1,111 @@ +# Concept #521: Revoked notification handling + +| Key | Value | +|---------------|-----------------------------------------------------------------------| +| Author | ds-crehm | +| Creation date | 24.01.2024 | +| Ticket Id | #521 https://github.com/eclipse-tractusx/traceability-foss/issues/521 | +| State | DRAFT | + +# Table of Contents +1. [Overview](#overview) +2. [Summary](#summary) +3. [Requirements](#requirements) +4. [Assumptions](#assumptions) +5. [Concept](#concept) +6. [References](#references) + +# Overview + +After a notification is created and approved, relevant policies must be verified before it can be sent. +There are three possibilities: +1. All constraints are fulfilled. The notification may be sent. +2. The policy is expired (validUntil DateTime < current DateTime). The notification is not permitted to be sent. +3. One or more policy-based constraints are not fulfilled. The notification is not permitted to be sent. + +Policies are stored in the IRS' policy store. The IRS provides a policy store API to create, fetch and verify policies. + +# Summary + +If the notification may not be sent after the policy verification, the user must be notified properly. +The status of the notification must be updated accordingly. + +# Requirements + +- During policy check, throw separate Exceptions based on the type of failure. + - If policy is not valid -> UsagePolicyExpiredException + - If policy is valid but notification not permitted -> UsagePolicyPermissionException +- Transient quality investigation & alert status: "FAILED" + - Notification set to this status, when it could not be sent due to the policy exceptions +- Error toast message informing the user of the exception +- Detailed status information stored in the message history of the notification +- User must be able to resend the notification + +# Assumptions + +- The message history can show the current notification status persistently and accurately (https://github.com/eclipse-tractusx/traceability-foss/issues/423) +- IRS library response for verification has enough details to show the user + - If not, Trace-X might have to add additional information to the thrown exceptions + +# Concept + +### Backend + +Instead of only having one UsagePolicyException, there must be two different exceptions: +- UsagePolicyPermissionException (thrown when permission validation fails; contains information from the IRS policy checker response) +- UsagePolicyExpiredException (thrown when policy validUntil DateTime < current DateTime) + +When either of these is thrown, the notification will be set to the transient status "FAILED" and a message is stored in the message history, containing information about the exception. +The standard notification status flow must **not** be changed. The "FAILED" status will only extend this standard within Trace-X. +After the notification is successfully resent, the status will be set to "SENT". Alternatively, the user can cancel the notification flow, which will set the status to "CANCELLED"/"CLOSED". + +Notification status flow: +![Notification-Status-Flow.png](Notification-Status-Flow.png) + +Creating and sending notifications: +```mermaid +sequenceDiagram + participant TraceX + participant IRSLib + participant IRSPolicyStore + TraceX->>TraceX: Create notification + TraceX->>IRSLib: Get policy + activate IRSLib + IRSLib->>IRSPolicyStore: Get policy + activate IRSPolicyStore + IRSPolicyStore-->>IRSLib: Policy A(id,validUntil,permissions,...) + deactivate IRSPolicyStore + IRSLib-->>TraceX: Policy A (id,validUntil,permissions,...) + deactivate IRSLib + TraceX->>IRSLib: Verify notification with policy A + activate IRSLib + alt Success + IRSLib-->>TraceX: Success + TraceX->>TraceX: Send notification + else Exception + alt Notification rejected + IRSLib-->>TraceX: UsagePolicyPermissionException + else policy.validUntil>TraceX: UsagePolicyExpiredException + end + deactivate IRSLib + TraceX->>TraceX: Notification status = FAILED + TraceX->>TraceX: Create error message + end +``` + +### Frontend + +After creating and approving the notification and one of the exceptions is thrown: +1. An error toast message must be shown to the user +2. The notification status must be changed to "FAILED" +3. A new message must be created and shown in the message history including the error description + +UsagePolicyPermissionException: +![UsagePolicyPermissionException-Mockup.png](UsagePolicyPermissionException-Mockup.png) +UsagePolicyExpiredException: +![UsagePolicyExpiredException-Mockup.png](UsagePolicyExpiredException-Mockup.png) + +# References + +- Current notification status flow: https://eclipse-tractusx.github.io/traceability-foss/docs/user/user-manual.html diff --git a/docs/concept/#521-revoked-notification-handling/Notification-Status-Flow.png b/docs/concept/#521-revoked-notification-handling/Notification-Status-Flow.png new file mode 100644 index 0000000000..2ae7f7db0b Binary files /dev/null and b/docs/concept/#521-revoked-notification-handling/Notification-Status-Flow.png differ diff --git a/docs/concept/#521-revoked-notification-handling/UsagePolicyExpiredException-Mockup.png b/docs/concept/#521-revoked-notification-handling/UsagePolicyExpiredException-Mockup.png new file mode 100644 index 0000000000..f069edbc6b Binary files /dev/null and b/docs/concept/#521-revoked-notification-handling/UsagePolicyExpiredException-Mockup.png differ diff --git a/docs/concept/#521-revoked-notification-handling/UsagePolicyPermissionException-Mockup.png b/docs/concept/#521-revoked-notification-handling/UsagePolicyPermissionException-Mockup.png new file mode 100644 index 0000000000..2617a704be Binary files /dev/null and b/docs/concept/#521-revoked-notification-handling/UsagePolicyPermissionException-Mockup.png differ diff --git a/docs/src/docs/user/user-manual.adoc b/docs/src/docs/user/user-manual.adoc index cd5f306bb2..50a4a3a492 100644 --- a/docs/src/docs/user/user-manual.adoc +++ b/docs/src/docs/user/user-manual.adoc @@ -148,11 +148,45 @@ Parts that are in a quality alert are highlighted yellow. === Parts selection -> Create Quality alert Select one or multiple child components/parts/batches that are build into your part. -Selection will enable you to create a quality alert (notification) to your customers. +Selection will enable you to create a quality alert (notification) to your customers. For this action, a button appears at the top right of the table as soon as an asset has been selected The quality alert will be added to a queue (queued & requested inbox) and not directly sent to the customers. Once the quality alert is created you will get a pop-up and can directly navigate to the inbox for further action. +=== Parts selection -> Publish Assets + +Select one or multiple parts that are in the AsBuilt lifecycle. A button will appear on the right of the lifecycle view selection: + +image::https://raw.githubusercontent.com/eclipse-tractusx/traceability-foss/main/docs/src/images/arc42/user-guide/publish_assets_button.png[] + +Selection will enable you to publish assets with the goal to persist them (import state "persistent"). +With a click on the button a window will be opened, where the selected assets are displayed and a required policy must be selected: + +image::https://raw.githubusercontent.com/eclipse-tractusx/traceability-foss/main/docs/src/images/arc42/user-guide/publish_assets_view.png[] + +The following table explains the different import state an asset can have: + +[cols="1,1"] +|=== +|transient +|Asset is uploaded but not synchronized with the Item Relationship Service (IRS). + +|in_synchronization +|Asset is in the process of synchronizing with the IRS. + +|persistent +|Asset is successfully synchronized with the IRS. + +|unset +|The import state of the asset was not set + +|error +|Along the import state transition and error occurred. +|=== + + + + === Parts table column settings On the right upper site of a table there is a settings icon in which you can set the table columns to a desired view. @@ -198,6 +232,10 @@ Zooming in/out can be done with the corresponding control buttons. image:https://raw.githubusercontent.com/eclipse-tractusx/traceability-foss/main/docs/src/images/arc42/user-guide/open-new-tab.png[] Open part tree in new tab to zoom, scroll and focus in a larger view. A minimap on the bottom right provides an overview of the current position on the part tree. +==== Asset State + +Information about the import process and state of the part. + ==== Manufacturer data Detailed information on the IDs for the manufactured part/batch. diff --git a/docs/src/images/arc42/user-guide/parts-list-detailed-view.png b/docs/src/images/arc42/user-guide/parts-list-detailed-view.png index acf604d491..f98da69000 100644 Binary files a/docs/src/images/arc42/user-guide/parts-list-detailed-view.png and b/docs/src/images/arc42/user-guide/parts-list-detailed-view.png differ diff --git a/docs/src/images/arc42/user-guide/publish_assets_button.png b/docs/src/images/arc42/user-guide/publish_assets_button.png new file mode 100644 index 0000000000..90a7c4b386 Binary files /dev/null and b/docs/src/images/arc42/user-guide/publish_assets_button.png differ diff --git a/docs/src/images/arc42/user-guide/publish_assets_view.png b/docs/src/images/arc42/user-guide/publish_assets_view.png new file mode 100644 index 0000000000..4184c3c2b3 Binary files /dev/null and b/docs/src/images/arc42/user-guide/publish_assets_view.png differ diff --git a/frontend/src/app/modules/page/parts/presentation/parts.component.html b/frontend/src/app/modules/page/parts/presentation/parts.component.html index d99d9c3778..5621bb1ac1 100644 --- a/frontend/src/app/modules/page/parts/presentation/parts.component.html +++ b/frontend/src/app/modules/page/parts/presentation/parts.component.html @@ -47,7 +47,7 @@ matTooltipPosition="above" [class.mdc-tooltip--multiline]="true" [matTooltipShowDelay]="1000" - [matTooltipDisabled]="roleService.hasAccess(['wip'])" + [matTooltipDisabled]="roleService.hasAccess(['admin'])" > diff --git a/frontend/src/app/modules/page/parts/presentation/parts.component.spec.ts b/frontend/src/app/modules/page/parts/presentation/parts.component.spec.ts index 10ce4eb5e2..56b0dc8c0e 100644 --- a/frontend/src/app/modules/page/parts/presentation/parts.component.spec.ts +++ b/frontend/src/app/modules/page/parts/presentation/parts.component.spec.ts @@ -19,19 +19,19 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -import { LayoutModule } from '@layout/layout.module'; -import { SidenavComponent } from '@layout/sidenav/sidenav.component'; -import { SidenavService } from '@layout/sidenav/sidenav.service'; -import { OtherPartsModule } from '@page/other-parts/other-parts.module'; -import { AssetAsBuiltFilter, AssetAsPlannedFilter } from '@page/parts/model/parts.model'; -import { PartsComponent } from '@page/parts/presentation/parts.component'; -import { TableHeaderSort } from '@shared/components/table/table.model'; -import { toAssetFilter, toGlobalSearchAssetFilter } from '@shared/helper/filter-helper'; -import { PartDetailsFacade } from '@shared/modules/part-details/core/partDetails.facade'; -import { SharedModule } from '@shared/shared.module'; -import { screen, waitFor } from '@testing-library/angular'; -import { renderComponent } from '@tests/test-render.utils'; -import { PartsModule } from '../parts.module'; +import {LayoutModule} from '@layout/layout.module'; +import {SidenavComponent} from '@layout/sidenav/sidenav.component'; +import {SidenavService} from '@layout/sidenav/sidenav.service'; +import {OtherPartsModule} from '@page/other-parts/other-parts.module'; +import {AssetAsBuiltFilter, AssetAsPlannedFilter} from '@page/parts/model/parts.model'; +import {PartsComponent} from '@page/parts/presentation/parts.component'; +import {TableHeaderSort} from '@shared/components/table/table.model'; +import {toAssetFilter, toGlobalSearchAssetFilter} from '@shared/helper/filter-helper'; +import {PartDetailsFacade} from '@shared/modules/part-details/core/partDetails.facade'; +import {SharedModule} from '@shared/shared.module'; +import {screen, waitFor} from '@testing-library/angular'; +import {renderComponent} from '@tests/test-render.utils'; +import {PartsModule} from '../parts.module'; describe('Parts', () => { @@ -315,4 +315,46 @@ describe('Parts', () => { expect(partsFacadeSpy).toHaveBeenCalledWith(); }); + it('shouldRefreshPartsOnPublishAction', async function() { + const { fixture } = await renderParts(); + const { componentInstance } = fixture; + + + + }); + + it('should show success toast and refresh parts on successful publish', async() => { + const { fixture } = await renderParts(); + const { componentInstance } = fixture; + const partsFacade = (componentInstance as any)['partsFacade']; + const toastService = componentInstance.toastService; + spyOn(toastService, 'success'); + spyOn(partsFacade, 'setPartsAsBuilt'); + spyOn(partsFacade, 'setPartsAsPlanned'); + + componentInstance.refreshPartsOnPublish(''); + + expect(toastService.success).toHaveBeenCalledWith('requestPublishAssets.success'); + expect(partsFacade.setPartsAsBuilt).toHaveBeenCalled(); + expect(partsFacade.setPartsAsPlanned).toHaveBeenCalled(); + }); + + it('should show error toast and not refresh parts on failed publish', async () => { + const { fixture } = await renderParts(); + const { componentInstance } = fixture; + const partsFacade = (componentInstance as any)['partsFacade']; + const toastService = componentInstance.toastService; + spyOn(toastService, 'error'); + spyOn(partsFacade, 'setPartsAsBuilt'); + spyOn(partsFacade, 'setPartsAsPlanned'); + + componentInstance.refreshPartsOnPublish('Error message'); + + expect(toastService.error).toHaveBeenCalledWith('Error message'); + expect(partsFacade.setPartsAsBuilt).not.toHaveBeenCalled(); + expect(partsFacade.setPartsAsPlanned).not.toHaveBeenCalled(); + }); + + + }); diff --git a/frontend/src/app/modules/page/parts/presentation/parts.component.ts b/frontend/src/app/modules/page/parts/presentation/parts.component.ts index ebfc8184fa..e15ea887b7 100644 --- a/frontend/src/app/modules/page/parts/presentation/parts.component.ts +++ b/frontend/src/app/modules/page/parts/presentation/parts.component.ts @@ -22,6 +22,7 @@ import {AfterViewInit, Component, OnDestroy, OnInit, QueryList, ViewChildren} from '@angular/core'; import {FormControl, FormGroup} from '@angular/forms'; import {Pagination} from '@core/model/pagination.model'; +import {RoleService} from '@core/user/role.service'; import {PartsFacade} from '@page/parts/core/parts.facade'; import {resetMultiSelectionAutoCompleteComponent} from '@page/parts/core/parts.helper'; import {MainAspectType} from '@page/parts/model/mainAspectType.enum'; @@ -39,7 +40,6 @@ import {BomLifecycleSettingsService, UserSettingView} from '@shared/service/bom- import {StaticIdService} from '@shared/service/staticId.service'; import {BehaviorSubject, combineLatest, Observable, Subject} from 'rxjs'; import {map} from 'rxjs/operators'; -import {RoleService} from "@core/user/role.service"; @Component({ @@ -71,7 +71,7 @@ export class PartsComponent implements OnInit, OnDestroy, AfterViewInit { public DEFAULT_PAGE_SIZE = 50; public ctrlKeyState = false; - isPublisherOpen$ = new BehaviorSubject(false); + isPublisherOpen$ = new Subject(); @ViewChildren(PartsTableComponent) partsTableComponents: QueryList; @@ -136,6 +136,17 @@ export class PartsComponent implements OnInit, OnDestroy, AfterViewInit { } + refreshPartsOnPublish(message: string) { + if(message) { + this.toastService.error(message); + } else { + this.toastService.success("requestPublishAssets.success") + this.partsFacade.setPartsAsBuilt(); + this.partsFacade.setPartsAsPlanned(); + this.partsTableComponents.map(component => component.clearAllRows()) + } + } + private resetFilterAndShowToast() { let filterIsSet = resetMultiSelectionAutoCompleteComponent(this.partsTableComponents, false); if (filterIsSet) { diff --git a/frontend/src/app/modules/page/policies/model/policy.model.ts b/frontend/src/app/modules/page/policies/model/policy.model.ts new file mode 100644 index 0000000000..b54a6af0cc --- /dev/null +++ b/frontend/src/app/modules/page/policies/model/policy.model.ts @@ -0,0 +1,39 @@ +// TODO: Decide if long term a Policy state, facade, ResponseType and Assembler is needed +export interface Policy { + policyId: string; + createdOn: string; + validUntil: string; + permissions?: PolicyPermission[]; +} + +export interface PolicyPermission { + action: PolicyType; + constraints?: Constraint[]; +} + +export enum PolicyType { + ACCESS="ACCESS", + USE="USE" +} + +export interface Constraint { + leftOperand: string; + operator: OperatorType; + rightOperand: string[]; +} + +export enum OperatorType { + EQ = 'eq', + NEQ = 'neq', + LT = 'lt', + GT = 'gt', + IN = 'in', + LTEQ = 'lteq', + GTEQ = 'gteq', + ISA = 'isA', + HASPART = 'hasPart', + ISPARTOF = 'isPartOf', + ISONEOF = 'isOneOf', + ISALLOF = 'isAllOf', + ISNONEOF = 'isNoneOf', +} diff --git a/frontend/src/app/modules/shared/components/asset-publisher/asset-publisher.component.html b/frontend/src/app/modules/shared/components/asset-publisher/asset-publisher.component.html index 739aafacdf..83fd99528e 100644 --- a/frontend/src/app/modules/shared/components/asset-publisher/asset-publisher.component.html +++ b/frontend/src/app/modules/shared/components/asset-publisher/asset-publisher.component.html @@ -3,10 +3,10 @@

{{'actions.publishAssets' | i18n}}

{{'publisher.selectedAssets' | i18n}}:

- + - {{item.nameAtManufacturer}} - {{item.id}} + {{asset.nameAtManufacturer}} + {{asset.id}} @@ -17,21 +17,30 @@

{{'publisher.policyToApply' | i18n}}:

{{'publisher.selectPolicyLabel' | i18n}} - - {{policy.name}} + + {{policy.policyId}} {{'publisher.selectPolicyError' | i18n}} -
+
+
{{'publisher.publish' | i18n}} +
diff --git a/frontend/src/app/modules/shared/components/asset-publisher/asset-publisher.component.spec.ts b/frontend/src/app/modules/shared/components/asset-publisher/asset-publisher.component.spec.ts index 07ace2da15..179dec1717 100644 --- a/frontend/src/app/modules/shared/components/asset-publisher/asset-publisher.component.spec.ts +++ b/frontend/src/app/modules/shared/components/asset-publisher/asset-publisher.component.spec.ts @@ -1,14 +1,20 @@ -import { Policy } from '@shared/components/asset-publisher/policy.model'; +import { Policy } from '@page/policies/model/policy.model'; +import { PolicyService } from '@shared/service/policy.service'; import { renderComponent } from '@tests/test-render.utils'; +import { BehaviorSubject, of } from 'rxjs'; import { AssetPublisherComponent } from './asset-publisher.component'; describe('AssetPublisherComponent', () => { - + const policyServiceSpy = jasmine.createSpyObj('PolicyService', ['getPolicies', 'publishAssets']); + const isOpenSubject = new BehaviorSubject(true); const renderAssetPublisherComponent = () => { return renderComponent(AssetPublisherComponent, { - + providers: [ { provide: PolicyService, useValue: policyServiceSpy }], + componentInputs: { + isOpen: isOpenSubject.asObservable(), + } }) } @@ -17,21 +23,54 @@ describe('AssetPublisherComponent', () => { expect(fixture).toBeTruthy(); }); - it('should publish assets and emit submitted event', async () => { - const {fixture} = await renderAssetPublisherComponent(); - const {componentInstance} = fixture - const publishSpy = spyOn(componentInstance.assetPublisherService, 'publishAssets'); + it('should publish assets and emit submitted event', async function() { + const { fixture } = await renderAssetPublisherComponent(); + const { componentInstance } = fixture; + + const dummyPolicy: Policy = { policyId: 'id-1', createdOn: 'testdate', validUntil: 'testdate' }; + + policyServiceSpy.publishAssets.and.returnValue(of({})); + policyServiceSpy.getPolicies.and.returnValue(of([dummyPolicy])); + const submittedSpy = spyOn(componentInstance.submitted, 'emit'); - const dummyPolicy: Policy = { id: 'id-1', name: 'myPolicy' }; - componentInstance.policyFormControl.setValue(dummyPolicy.id); + componentInstance.policyFormControl.setValue(dummyPolicy.policyId); + fixture.detectChanges(); componentInstance.publish(); - expect(publishSpy).toHaveBeenCalledWith(dummyPolicy.id); + fixture.whenStable().then(() => { + expect(policyServiceSpy.publishAssets).toHaveBeenCalledWith([], dummyPolicy.policyId); + expect(componentInstance.policyFormControl.value).toBeNull(); + expect(submittedSpy).toHaveBeenCalled(); + }); + }); + + it('should set policies when requesting policies', async function() { + const { fixture } = await renderAssetPublisherComponent(); + const { componentInstance } = fixture; + const dummyPolicy: Policy = { policyId: 'id-1', createdOn: 'testdate', validUntil: 'testdate' }; + + const submittedSpy = spyOn(componentInstance.submitted, 'emit'); + + + + policyServiceSpy.publishAssets.and.returnValue(of({})); + policyServiceSpy.getPolicies.and.returnValue(of([dummyPolicy])) + + + componentInstance.policyFormControl.setValue(dummyPolicy.policyId); + fixture.detectChanges(); + + componentInstance.publish(); - expect(componentInstance.policyFormControl.value).toBeNull(); - expect(submittedSpy).toHaveBeenCalled(); + fixture.whenStable().then(() => { + expect(policyServiceSpy.publishAssets).toHaveBeenCalledWith([], dummyPolicy.policyId); + expect(policyServiceSpy.getPolicies).toHaveBeenCalled(); + expect(componentInstance.policiesList).toEqual([dummyPolicy]); + expect(componentInstance.policyFormControl.value).toBeNull(); + expect(submittedSpy).toHaveBeenCalled(); + }); }); }); diff --git a/frontend/src/app/modules/shared/components/asset-publisher/asset-publisher.component.ts b/frontend/src/app/modules/shared/components/asset-publisher/asset-publisher.component.ts index 3530b0080b..62d75b9f3c 100644 --- a/frontend/src/app/modules/shared/components/asset-publisher/asset-publisher.component.ts +++ b/frontend/src/app/modules/shared/components/asset-publisher/asset-publisher.component.ts @@ -1,8 +1,9 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { FormControl, Validators } from '@angular/forms'; -import { Part } from '@page/parts/model/parts.model'; -import { Policy } from '@shared/components/asset-publisher/policy.model'; -import { AssetPublisherService } from '@shared/service/asset-publisher.service'; +import { ImportState, Part } from '@page/parts/model/parts.model'; +import { Policy } from '@page/policies/model/policy.model'; +import { PolicyService } from '@shared/service/policy.service'; +import { Observable, Subscription } from 'rxjs'; @Component({ selector: 'app-asset-publisher', @@ -12,23 +13,54 @@ import { AssetPublisherService } from '@shared/service/asset-publisher.service'; export class AssetPublisherComponent { @Input() selectedAssets: Part[] = []; + @Input() isOpen: Observable; + isOpenSubscription: Subscription; - @Output() submitted = new EventEmitter(); + @Output() submitted = new EventEmitter(); - policies: Policy[]; + policiesSubscription: Subscription; + policiesList: Policy[] = []; policyFormControl = new FormControl('', [Validators.required]) - constructor(readonly assetPublisherService: AssetPublisherService) {} + constructor(readonly policyService: PolicyService) {} ngOnInit(): void { - this.policies = this.assetPublisherService.getPolicies(); + this.isOpenSubscription = this.isOpen.subscribe(next => { + if(!next) { + this.policyFormControl.reset(); + return; + } + this.getPolicies(); + }) + } + + ngOnDestroy():void { + if (this.isOpenSubscription) { + this.isOpenSubscription.unsubscribe(); + } + if(this.policiesSubscription) { + this.policiesSubscription.unsubscribe(); + } } - publish() { - this.assetPublisherService.publishAssets(this.policyFormControl.value) + publish(): void { + const selectedAssetIds = this.selectedAssets.map(part => part.id); + this.policyService.publishAssets(selectedAssetIds, this.policyFormControl.value).subscribe({ + next: data => {this.submitted.emit(null);}, + error: error => {this.submitted.emit(error);} + }); this.policyFormControl.reset(); - this.submitted.emit(); - console.log("IMPORT") + this.selectedAssets = []; + } + + private getPolicies() { + this.policiesSubscription = this.policyService.getPolicies().subscribe(data => { + this.policiesList = data; + }) + } + + checkForNonTransientPart(): boolean { + return this.selectedAssets.some(part => part.importState !== ImportState.TRANSIENT) } } diff --git a/frontend/src/app/modules/shared/components/asset-publisher/policy.model.ts b/frontend/src/app/modules/shared/components/asset-publisher/policy.model.ts deleted file mode 100644 index e59b64b502..0000000000 --- a/frontend/src/app/modules/shared/components/asset-publisher/policy.model.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Policy { - id: string; - name: string; -} diff --git a/frontend/src/app/modules/shared/components/chip/chip.component.scss b/frontend/src/app/modules/shared/components/chip/chip.component.scss index b4fe293c79..9a50021b93 100644 --- a/frontend/src/app/modules/shared/components/chip/chip.component.scss +++ b/frontend/src/app/modules/shared/components/chip/chip.component.scss @@ -1,6 +1,6 @@ .chip-container { padding: 0.25rem 0.75rem 0.25rem 0.75rem; - width: 148px; + width: 161px; background-color: currentColor; display: flex; justify-content: center; diff --git a/frontend/src/app/modules/shared/components/parts-table/parts-table.component.html b/frontend/src/app/modules/shared/components/parts-table/parts-table.component.html index 9433105531..807057769b 100644 --- a/frontend/src/app/modules/shared/components/parts-table/parts-table.component.html +++ b/frontend/src/app/modules/shared/components/parts-table/parts-table.component.html @@ -29,7 +29,7 @@

{{ selectedPartsInfoLabel | i18n : {count: selection?.selected?.length || 0} }}

{ - let service: AssetPublisherService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(AssetPublisherService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/modules/shared/service/asset-publisher.service.ts b/frontend/src/app/modules/shared/service/asset-publisher.service.ts deleted file mode 100644 index 1acc0cebaa..0000000000 --- a/frontend/src/app/modules/shared/service/asset-publisher.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {Injectable} from '@angular/core'; -import {Policy} from '@shared/components/asset-publisher/policy.model'; - -@Injectable({ - providedIn: 'root' -}) -export class AssetPublisherService { - - POLICIES: Policy[] = [ - { - id: 'policy_id1', - name: 'EDC Policy' - }, - { - id: 'policy_id2', - name: 'Assets Policy', - }, - { - id: 'policy_id3', - name: 'Investigations Policy' - }, - { - id: 'policy_id4', - name: 'Alerts Policy', - }, - ] - - getPolicies(): Policy[] { - return this.POLICIES; - } - - publishAssets(policyId: string) { - return "successfully published assets under policy with id " + policyId; - } -} diff --git a/frontend/src/app/modules/shared/service/policy.service.spec.ts b/frontend/src/app/modules/shared/service/policy.service.spec.ts new file mode 100644 index 0000000000..c91603f210 --- /dev/null +++ b/frontend/src/app/modules/shared/service/policy.service.spec.ts @@ -0,0 +1,31 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { ApiService } from '@core/api/api.service'; +import { AuthService } from '@core/auth/auth.service'; +import { KeycloakService } from 'keycloak-angular'; + +import { PolicyService } from './policy.service'; + +describe('AssetPublisherService', () => { + let service: PolicyService; + let httpTestingController: HttpTestingController; + let authService: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [PolicyService, ApiService, KeycloakService, AuthService], + }); + service = TestBed.inject(PolicyService); + httpTestingController = TestBed.inject(HttpTestingController); + authService = TestBed.inject(AuthService); + }); + + afterEach(() => { + httpTestingController.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/modules/shared/service/policy.service.ts b/frontend/src/app/modules/shared/service/policy.service.ts new file mode 100644 index 0000000000..2154cb5f92 --- /dev/null +++ b/frontend/src/app/modules/shared/service/policy.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { ApiService } from '@core/api/api.service'; +import { environment } from '@env'; +import { Policy } from '@page/policies/model/policy.model'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class PolicyService { + private readonly url = environment.apiUrl; + constructor(private readonly apiService: ApiService) {} + + getPolicies(): Observable { + return this.apiService + .getBy(`${this.url}/policies`); + } + + publishAssets(assetIds: string[],policyId: string) { + return this.apiService.post(`${this.url}/assets/publish`, {assetIds, policyId}); + } +} diff --git a/frontend/src/assets/locales/de/common.json b/frontend/src/assets/locales/de/common.json index d6b75dd673..d7d404dc42 100644 --- a/frontend/src/assets/locales/de/common.json +++ b/frontend/src/assets/locales/de/common.json @@ -16,7 +16,8 @@ "unauthorized": "Die Funktion ist aufgrund einer fehlenden Rolle deaktiviert. Bitten Sie Ihren Administrator, die erforderliche Rolle für die Funktion bereitzustellen.", "notAllowedForAsPlanned": "Diese Funktion ist für Produkte im Lebenszyklus \"AsPlanned\" nicht verfügbar.", "noChildPartsForInvestigation": "Diese Funktion ist für Produkte ohne Bauteile nicht verfügbar.", - "noCustomerAsPlannedParts": "Produkte von Kunden im Lebenszyklus \"AsPlanned\" sind nicht verfügbar." + "noCustomerAsPlannedParts": "Produkte von Kunden im Lebenszyklus \"AsPlanned\" sind nicht verfügbar.", + "nonTransientPart": "Ein oder mehrere ausgewählte Produkte befinden sich nicht im erforderlichen Importstatus \"TRANSIENT\"." }, "pageTitle": { "dashboard": "Dashboard", @@ -196,6 +197,10 @@ "severity": "Gefahrenstufe", "severityDescription": "Wählen Sie die Gefahrenstufe für das vorliegende Problem" }, + "requestPublishAssets": { + "success": "Anfrage zur Veröffentlichung der Produkte war erfolgreich", + "error": "" + }, "pagination": { "itemsPerPageLabel": "Artikel pro Seite:", "nextPageLabel": "Nächsten Seite", diff --git a/frontend/src/assets/locales/en/common.json b/frontend/src/assets/locales/en/common.json index 785289c0b2..a97306e724 100644 --- a/frontend/src/assets/locales/en/common.json +++ b/frontend/src/assets/locales/en/common.json @@ -14,9 +14,10 @@ "adminBpn": "BPN - EDC configuration", "adminImport": "Data provisioning", "unauthorized": "Functionality is disabled because of missing role. Ask your administrator to provide the required role for the functionality.", - "notAllowedForAsPlanned": "This function is not available for Parts in the Lifecycle \"AsPlanned\". ", + "notAllowedForAsPlanned": "This function is not available for Parts in the Lifecycle \"AsPlanned\".", "noChildPartsForInvestigation": "This function is not available for Parts without components.", - "noCustomerAsPlannedParts": "Customer parts in lifecycle \"AsPlanned\" are not available." + "noCustomerAsPlannedParts": "Customer parts in lifecycle \"AsPlanned\" are not available.", + "nonTransientPart": "One or more selected parts are not in the required import state \"TRANSIENT\"." }, "pageTitle": { "dashboard": "Dashboard", @@ -198,6 +199,10 @@ "severity": "Severity", "severityDescription": "Select the severity for the problem at hand" }, + "requestPublishAssets": { + "success": "Request to publish assets was successful", + "error": "" + }, "pagination": { "itemsPerPageLabel": "Items per page:", "nextPageLabel": "Next page", diff --git a/pom.xml b/pom.xml index 487a6cb3d7..a5dbe77919 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ SPDX-License-Identifier: Apache-2.0 3.1.1 0.8.8 3.3.0 - 8.4.0 + 9.0.9 4.8.3.0 3.1.3 3.4.5 diff --git a/tx-backend/docker/docker-compose.yml b/tx-backend/docker/docker-compose.yml index 9a21432254..409e99fcde 100644 --- a/tx-backend/docker/docker-compose.yml +++ b/tx-backend/docker/docker-compose.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2023 Contributors to the Eclipse Foundation +# Copyright (c) 2023, 2024 Contributors to the Eclipse Foundation # # See the NOTICE file(s) distributed with this work for additional # information regarding copyright ownership. @@ -18,7 +18,7 @@ name: trace-foss services: postgres: - image: "postgres:14.4" + image: "postgres:15.4" volumes: - postgres-data:/var/lib/postgresql/data - ./db-init:/docker-entrypoint-initdb.d diff --git a/tx-backend/src/main/java/org/eclipse/tractusx/traceability/assets/infrastructure/base/irs/IrsService.java b/tx-backend/src/main/java/org/eclipse/tractusx/traceability/assets/infrastructure/base/irs/IrsService.java index 60aa11e0fa..9d7f57dfaa 100644 --- a/tx-backend/src/main/java/org/eclipse/tractusx/traceability/assets/infrastructure/base/irs/IrsService.java +++ b/tx-backend/src/main/java/org/eclipse/tractusx/traceability/assets/infrastructure/base/irs/IrsService.java @@ -1,7 +1,7 @@ /******************************************************************************** * Copyright (c) 2022, 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) * Copyright (c) 2022, 2023 ZF Friedrichshafen AG - * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2023, 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -39,6 +39,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -143,43 +144,53 @@ void saveOrUpdateAssets(AssetCallbackRepository repository, AssetBase asset) { @Override public void createIrsPolicyIfMissing() { log.info("Check if irs policy exists"); - List irsPolicies = irsApiClient.getPolicies(adminApiKey); + List irsPolicies = Objects.requireNonNullElse(irsApiClient.getPolicies(adminApiKey), Collections.emptyList()); log.info("Irs has following policies: {}", irsPolicies); - log.info("Required constraints from application yaml are : {}", traceabilityProperties.getRightOperand()); - //update existing policies - irsPolicies.stream().filter( - irsPolicy -> traceabilityProperties.getRightOperand().equals(irsPolicy.policyId())) - .forEach(existingPolicy -> checkAndUpdatePolicy(irsPolicies)); - - - //create missing policies - boolean missingPolicy = irsPolicies.stream().noneMatch(irsPolicy -> irsPolicy.policyId().equals(traceabilityProperties.getRightOperand())); - if (missingPolicy) { - createPolicy(); + PolicyResponse matchingIrsPolicy = irsPolicies.stream() + .filter(irsPolicy -> irsPolicy.permissions().stream() + .flatMap(permission -> permission.getConstraints().stream()) + .anyMatch(constraint -> + constraint.getOr().stream().anyMatch(rightO -> + rightO.getRightOperand().stream().anyMatch(value -> + value.equals(traceabilityProperties.getRightOperand()))) + || + constraint.getAnd().stream().allMatch(rightO -> + rightO.getRightOperand().stream().allMatch(value -> + value.equals(traceabilityProperties.getRightOperand()))) + )) + .findFirst() + .orElse(null); + + if (matchingIrsPolicy == null) { + createMissingPolicies(); + } else { + checkAndUpdateExpiredPolicies((matchingIrsPolicy)); } } - private void createPolicy() { + private void createMissingPolicies() { log.info("Irs policy does not exist creating {}", traceabilityProperties.getRightOperand()); irsApiClient.registerPolicy(adminApiKey, RegisterPolicyRequest.from(traceabilityProperties.getLeftOperand(), OperatorType.fromValue(traceabilityProperties.getOperatorType()), traceabilityProperties.getRightOperand(), traceabilityProperties.getValidUntil())); } - private void checkAndUpdatePolicy(List requiredPolicies) { - Optional requiredPolicy = requiredPolicies.stream().filter(policyItem -> policyItem.policyId().equals(traceabilityProperties.getRightOperand())).findFirst(); - if (requiredPolicy.isPresent() && - traceabilityProperties.getValidUntil().isAfter(requiredPolicy.get().validUntil()) - ) { - log.info("IRS Policy {} has outdated validity updating new ttl {}", traceabilityProperties.getRightOperand(), requiredPolicy); + private void checkAndUpdateExpiredPolicies(PolicyResponse matchingIrsPolicy) { + if (isPolicyExpired(matchingIrsPolicy)) { + log.info("IRS Policy {} has outdated validity updating new ttl {}", traceabilityProperties.getRightOperand(), matchingIrsPolicy); irsApiClient.deletePolicy(adminApiKey, traceabilityProperties.getRightOperand()); irsApiClient.registerPolicy(adminApiKey, RegisterPolicyRequest.from(traceabilityProperties.getLeftOperand(), OperatorType.fromValue(traceabilityProperties.getOperatorType()), traceabilityProperties.getRightOperand(), traceabilityProperties.getValidUntil())); } } + private boolean isPolicyExpired(PolicyResponse requiredPolicy) { + return traceabilityProperties.getValidUntil().isAfter(requiredPolicy.validUntil()); + } + public List getPolicies() { return irsApiClient.getPolicies(adminApiKey); } + } diff --git a/tx-backend/src/main/java/org/eclipse/tractusx/traceability/assets/infrastructure/base/irs/model/request/RegisterPolicyRequest.java b/tx-backend/src/main/java/org/eclipse/tractusx/traceability/assets/infrastructure/base/irs/model/request/RegisterPolicyRequest.java index 07dbec0da9..549d785f20 100644 --- a/tx-backend/src/main/java/org/eclipse/tractusx/traceability/assets/infrastructure/base/irs/model/request/RegisterPolicyRequest.java +++ b/tx-backend/src/main/java/org/eclipse/tractusx/traceability/assets/infrastructure/base/irs/model/request/RegisterPolicyRequest.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2023,2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -28,7 +28,6 @@ import java.time.Instant; import java.time.OffsetDateTime; import java.util.List; -import java.util.UUID; public record RegisterPolicyRequest( String policyId, @@ -37,7 +36,7 @@ public record RegisterPolicyRequest( ) { public static RegisterPolicyRequest from(String leftOperand, OperatorType operatorType, String rightOperand, OffsetDateTime ttl) { return new RegisterPolicyRequest( - UUID.randomUUID().toString(), + rightOperand, ttl.toInstant(), List.of(new Permission( PolicyType.USE, diff --git a/tx-backend/src/test/java/org/eclipse/tractusx/traceability/assets/infrastructure/repository/rest/irs/IrsServiceTest.java b/tx-backend/src/test/java/org/eclipse/tractusx/traceability/assets/infrastructure/repository/rest/irs/IrsServiceTest.java index 0679e76997..3520727fc0 100644 --- a/tx-backend/src/test/java/org/eclipse/tractusx/traceability/assets/infrastructure/repository/rest/irs/IrsServiceTest.java +++ b/tx-backend/src/test/java/org/eclipse/tractusx/traceability/assets/infrastructure/repository/rest/irs/IrsServiceTest.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2023, 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -34,7 +34,6 @@ import org.eclipse.tractusx.traceability.assets.infrastructure.base.irs.model.response.JobStatus; import org.eclipse.tractusx.traceability.assets.infrastructure.base.irs.model.response.Parameter; import org.eclipse.tractusx.traceability.assets.infrastructure.base.irs.model.response.PolicyResponse; -import org.eclipse.tractusx.traceability.assets.infrastructure.base.irs.model.response.RegisterJobResponse; import org.eclipse.tractusx.traceability.assets.infrastructure.base.irs.model.response.Shell; import org.eclipse.tractusx.traceability.assets.infrastructure.base.irs.model.response.relationship.Aspect; import org.eclipse.tractusx.traceability.assets.infrastructure.base.irs.model.response.relationship.LinkedItem; @@ -124,7 +123,7 @@ void givenNoPolicyExist_whenCreateIrsPolicyIfMissing_thenCreateIt() { @Test void givenPolicyExist_whenCreateIrsPolicyIfMissing_thenDoNotCreateIt() { // given - final PolicyResponse existingPolicy = new PolicyResponse("test", OffsetDateTime.parse("2023-07-03T16:01:05.309Z"), OffsetDateTime.now(), List.of()); + final PolicyResponse existingPolicy = new PolicyResponse("test", OffsetDateTime.parse("2023-07-03T16:01:05.309Z"), OffsetDateTime.now(), List.of(new Permission(PolicyType.USE, List.of(new Constraints(List.of(), List.of(new Constraint("leftOperand1", OperatorType.EQ, List.of("test")))))))); when(irsClient.getPolicies(anyString())).thenReturn(List.of(existingPolicy)); when(traceabilityProperties.getRightOperand()).thenReturn("test"); when(traceabilityProperties.getValidUntil()).thenReturn(OffsetDateTime.parse("2023-07-02T16:01:05.309Z")); @@ -139,7 +138,7 @@ void givenPolicyExist_whenCreateIrsPolicyIfMissing_thenDoNotCreateIt() { @Test void givenOutdatedPolicyExist_whenCreateIrsPolicyIfMissing_thenUpdateIt() { // given - final PolicyResponse existingPolicy = new PolicyResponse("test", OffsetDateTime.parse("2023-07-03T16:01:05.309Z"), OffsetDateTime.parse("2023-07-03T16:01:05.309Z"), List.of()); + final PolicyResponse existingPolicy = new PolicyResponse("test", OffsetDateTime.parse("2023-07-03T16:01:05.309Z"), OffsetDateTime.parse("2023-07-03T16:01:05.309Z"), List.of(new Permission(PolicyType.USE, List.of(new Constraints(List.of(), List.of(new Constraint("leftOperand1", OperatorType.EQ, List.of("test")))))))); when(irsClient.getPolicies(anyString())).thenReturn(List.of(existingPolicy)); when(traceabilityProperties.getRightOperand()).thenReturn("test"); when(traceabilityProperties.getOperatorType()).thenReturn("eq"); diff --git a/tx-backend/src/test/java/org/eclipse/tractusx/traceability/integration/common/config/PostgreSQLConfig.java b/tx-backend/src/test/java/org/eclipse/tractusx/traceability/integration/common/config/PostgreSQLConfig.java index d9585294c4..182499d9de 100644 --- a/tx-backend/src/test/java/org/eclipse/tractusx/traceability/integration/common/config/PostgreSQLConfig.java +++ b/tx-backend/src/test/java/org/eclipse/tractusx/traceability/integration/common/config/PostgreSQLConfig.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2023,2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -31,7 +31,7 @@ public class PostgreSQLConfig { // will be started before and stopped after each test method @Container - private static final PostgreSQLContainer postgresqlContainer = new PostgreSQLContainer("postgres:14.4") + private static final PostgreSQLContainer postgresqlContainer = new PostgreSQLContainer("postgres:15.4") .withDatabaseName("trace") .withUsername("foo") .withPassword("secret");