Loading src/test/java/org/etsi/osl/controllers/capif/invoker/ResourceRepoServiceHealthTest.java 0 → 100644 +344 −0 Original line number Diff line number Diff line package org.etsi.osl.controllers.capif.invoker; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.HashMap; import java.util.Map; import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationRef; import org.etsi.osl.tmf.ri639.model.Characteristic; import org.etsi.osl.tmf.ri639.model.Resource; import org.etsi.osl.tmf.ri639.model.ResourceAdministrativeStateType; import org.etsi.osl.tmf.ri639.model.ResourceHealth; import org.etsi.osl.tmf.ri639.model.ResourceOperationalStateType; import org.etsi.osl.tmf.ri639.model.ResourceStatusType; import org.etsi.osl.tmf.ri639.model.ResourceUpdate; import org.etsi.osl.tmf.ri639.model.ResourceUsageStateType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.test.util.ReflectionTestUtils; /** * Pins the ITU-T X.731 health this controller reports on every path that writes a resource back to * the TMF catalog. * * <p>OSOM decides a supporting service's lifecycle from {@code Resource.deriveHealth()}, which * folds {@code operationalState} and {@code administrativeState} into a verdict. * {@code resourceStatus} takes no part in it. A path that reports neither attribute derives * {@code PENDING}, which neither promotes nor demotes the service: the order hangs with no * exception and nothing in any log, which is exactly the failure this suite exists to prevent * recurring. Every assertion below therefore states the verdict the way OSOM reads it, by folding * what was written through {@code deriveHealth()}. */ class ResourceRepoServiceHealthTest { private static final String RESOURCE_ID = "res-1"; private static final String INVOKER_NAME = "myInvoker"; private ResourceRepoService repo; private CatalogClient catalogClient; private InvokerManagement invokerManagement; @BeforeEach void setUp() { repo = new ResourceRepoService(); catalogClient = mock(CatalogClient.class); invokerManagement = mock(InvokerManagement.class); repo.aCatalogClient = catalogClient; repo.invokerManagement = invokerManagement; repo.simpleResourceMapper = mock(SimpleResourceMapper.class); ReflectionTestUtils.setField(repo, "compcategory", "CAPIF"); ReflectionTestUtils.setField(repo, "compversion", "1.0.0"); when(catalogClient.updateResourceById(anyString(), any(ResourceUpdate.class))) .thenReturn(new Resource()); } // ---------------------------------------------------------------- create / update @Test @DisplayName("an onboarded invoker is reported UP, so the supporting service can reach ACTIVE") void onboardedInvokerIsReportedUp() throws Exception { when(invokerManagement.onboardInvokerByName(INVOKER_NAME)).thenReturn(anInvoker()); repo.updateResource(headers(), anUpdateFor(INVOKER_NAME)); ResourceUpdate written = captureWrite(); assertThat(written.getOperationalState()).isEqualTo(ResourceOperationalStateType.ENABLE); assertThat(written.getAdministrativeState()).isEqualTo(ResourceAdministrativeStateType.UNLOCKED); assertThat(written.getUsageState()).isEqualTo(ResourceUsageStateType.ACTIVE); assertThat(healthOf(written)).isEqualTo(ResourceHealth.UP); assertThat(written.getResourceStatus()).isEqualTo(ResourceStatusType.AVAILABLE); assertThat(lastValueOf(written, "status.Health")).isEqualTo("UP"); assertThat(lastValueOf(written, "status.infoMessage")).isEqualTo("onboarded to CAPIF service"); } @Test @DisplayName("an onboarded invoker is remembered, so its bearer token gets refreshed later") void onboardedInvokerIsRemembered() throws Exception { when(invokerManagement.onboardInvokerByName(INVOKER_NAME)).thenReturn(anInvoker()); repo.updateResource(headers(), anUpdateFor(INVOKER_NAME)); assertThat(repo.resourceToInvokerMap).containsKey(RESOURCE_ID); } @Test @DisplayName("an invoker name this controller does not know is reported DOWN, never left unreported") void unknownInvokerNameIsReportedDown() throws Exception { // onboardInvokerByName returns null for a name that is not in the configured invokers. This // branch used to fall through with resourceStatus=UNKNOWN and no health at all, which reads as // PENDING: the service would wait for an invoker that is never coming. when(invokerManagement.onboardInvokerByName("nosuch")).thenReturn(null); repo.updateResource(headers(), anUpdateFor("nosuch")); ResourceUpdate written = captureWrite(); assertThat(healthOf(written)).isEqualTo(ResourceHealth.DOWN); assertThat(written.getUsageState()).isEqualTo(ResourceUsageStateType.IDLE); assertThat(written.getResourceStatus()).isEqualTo(ResourceStatusType.ALARM); assertThat(lastValueOf(written, "status.state")).isEqualTo(CapifInvokerState.MISSING.name()); assertThat(lastValueOf(written, "status.infoMessage")).contains("nosuch"); assertThat(lastValueOf(written, "status.Health")).isEqualTo("DOWN"); assertThat(repo.resourceToInvokerMap).doesNotContainKey(RESOURCE_ID); } @Test @DisplayName("a failed onboarding is reported DOWN with the reason, which demotes rather than hangs") void failedOnboardingIsReportedDown() throws Exception { when(invokerManagement.onboardInvokerByName(INVOKER_NAME)) .thenThrow(new IllegalStateException("CAPIF refused the certificate")); repo.updateResource(headers(), anUpdateFor(INVOKER_NAME)); ResourceUpdate written = captureWrite(); assertThat(healthOf(written)).isEqualTo(ResourceHealth.DOWN); // DISABLE while still UNLOCKED: not working, but not torn down either. assertThat(written.getAdministrativeState()).isEqualTo(ResourceAdministrativeStateType.UNLOCKED); assertThat(written.getResourceStatus()).isEqualTo(ResourceStatusType.ALARM); assertThat(lastValueOf(written, "status.state")).isEqualTo(CapifInvokerState.ERROR.name()); assertThat(lastValueOf(written, "status.infoMessage")).contains("CAPIF refused the certificate"); assertThat(lastValueOf(written, "status.Health")).isEqualTo("DOWN"); } // ---------------------------------------------------------------- delete @Test @DisplayName("a deleted resource is reported GONE, which is what tears the service down") void deletedResourceIsReportedGone() { repo.resourceToInvokerMap.put(RESOURCE_ID, aCapifInvokerResource()); repo.deleteResource(headers(), new ResourceUpdate()); ResourceUpdate written = captureWrite(); assertThat(written.getOperationalState()).isEqualTo(ResourceOperationalStateType.DISABLE); // SHUTDOWN is the teardown tombstone; resourceStatus=UNKNOWN on its own moves nothing. assertThat(written.getAdministrativeState()).isEqualTo(ResourceAdministrativeStateType.SHUTDOWN); assertThat(written.getUsageState()).isEqualTo(ResourceUsageStateType.IDLE); assertThat(healthOf(written)).isEqualTo(ResourceHealth.GONE); assertThat(written.getResourceStatus()).isEqualTo(ResourceStatusType.UNKNOWN); assertThat(lastValueOf(written, "status.Health")).isEqualTo("GONE"); assertThat(repo.resourceToInvokerMap).doesNotContainKey(RESOURCE_ID); } // ---------------------------------------------------------------- bearer refresh heartbeat @Test @DisplayName("a refreshed bearer token is reported UP with the new token") void refreshedBearerIsReportedUp() throws Exception { repo.resourceToInvokerMap.put(RESOURCE_ID, aCapifInvokerResource()); CapifInvoker refreshed = anInvoker(); refreshed.setBearerAccessToken("fresh-token"); when(invokerManagement.refreshBearer(any(CapifInvoker.class))).thenReturn(refreshed); repo.processBearerRefreshTokens(); ResourceUpdate written = captureWrite(); assertThat(healthOf(written)).isEqualTo(ResourceHealth.UP); assertThat(written.getUsageState()).isEqualTo(ResourceUsageStateType.ACTIVE); // The sweeper says nothing about pool disposition: the update leaves resourceStatus unset, and // the TMF inventory applies only the fields an update carries, so a reservation survives it. assertThat(written.getResourceStatus()).isNull(); assertThat(lastValueOf(written, "bearerAccessToken")).isEqualTo("fresh-token"); assertThat(lastValueOf(written, "status.infoMessage")).startsWith("Bearer token refreshed"); } @Test @DisplayName("a bearer refresh that failed is reported DOWN, not as a refresh that succeeded") void failedBearerRefreshIsReportedDown() throws Exception { // refreshBearer used to swallow its exception and hand the stale invoker back, so this loop // wrote "Bearer token refreshed" onto a resource whose token was quietly expiring. repo.resourceToInvokerMap.put(RESOURCE_ID, aCapifInvokerResource()); when(invokerManagement.refreshBearer(any(CapifInvoker.class))) .thenThrow(new IllegalStateException("CAPIF unreachable")); repo.processBearerRefreshTokens(); ResourceUpdate written = captureWrite(); assertThat(healthOf(written)).isEqualTo(ResourceHealth.DOWN); // The bad news travels as health. Writing ALARM from here would clear a reservation the // sweeper knows nothing about, and DOWN already demotes the supporting service. assertThat(written.getResourceStatus()).isNull(); assertThat(lastValueOf(written, "status.state")).isEqualTo(CapifInvokerState.ERROR.name()); assertThat(lastValueOf(written, "status.infoMessage")) .contains("Bearer token refresh failed") .contains("CAPIF unreachable"); assertThat(lastValueOf(written, "status.Health")).isEqualTo("DOWN"); } @Test @DisplayName("one unwritable resource does not starve the rest of the refresh loop") void oneUnwritableResourceDoesNotStarveTheRest() throws Exception { repo.resourceToInvokerMap.put("res-bad", aCapifInvokerResource()); repo.resourceToInvokerMap.put("res-good", aCapifInvokerResource()); when(invokerManagement.refreshBearer(any(CapifInvoker.class))).thenReturn(anInvoker()); when(catalogClient.updateResourceById(eq("res-bad"), any(ResourceUpdate.class))) .thenThrow(new RuntimeException("catalog unreachable")); repo.processBearerRefreshTokens(); verify(catalogClient).updateResourceById(eq("res-good"), any(ResourceUpdate.class)); } // ------------------------------------------------- allocation someone else made @Test @DisplayName("onboarding does not release a reservation this controller did not make") void onboardingLeavesAReservationAlone() throws Exception { // The ResourceUpdate is built from the live catalog entry, so an orchestrator's RESERVED // arrives inside it. Reporting AVAILABLE here would put the resource back in the free pool // while the order that reserved it is still using it. when(invokerManagement.onboardInvokerByName(INVOKER_NAME)).thenReturn(anInvoker()); ResourceUpdate reserved = anUpdateFor(INVOKER_NAME); reserved.setResourceStatus(ResourceStatusType.RESERVED); repo.updateResource(headers(), reserved); ResourceUpdate written = captureWrite(); assertThat(written.getResourceStatus()).isEqualTo(ResourceStatusType.RESERVED); // ...and the health that actually promotes the service is unaffected. assertThat(healthOf(written)).isEqualTo(ResourceHealth.UP); } @Test @DisplayName("a failed onboarding does not release a reservation either") void failedOnboardingLeavesAReservationAlone() throws Exception { when(invokerManagement.onboardInvokerByName(INVOKER_NAME)) .thenThrow(new IllegalStateException("CAPIF refused the certificate")); ResourceUpdate reserved = anUpdateFor(INVOKER_NAME); reserved.setResourceStatus(ResourceStatusType.RESERVED); repo.updateResource(headers(), reserved); ResourceUpdate written = captureWrite(); assertThat(written.getResourceStatus()).isEqualTo(ResourceStatusType.RESERVED); assertThat(healthOf(written)).isEqualTo(ResourceHealth.DOWN); } @Test @DisplayName("teardown does end the allocation, because the resource it was made against is going") void deleteClearsTheAllocation() { ResourceUpdate reserved = new ResourceUpdate(); reserved.setResourceStatus(ResourceStatusType.RESERVED); repo.deleteResource(headers(), reserved); assertThat(captureWrite().getResourceStatus()).isEqualTo(ResourceStatusType.UNKNOWN); } @Test @DisplayName("a stale ALARM is superseded by a successful onboarding: a condition is only an observation") void onboardingSupersedesAStaleAlarm() throws Exception { when(invokerManagement.onboardInvokerByName(INVOKER_NAME)).thenReturn(anInvoker()); ResourceUpdate alarmed = anUpdateFor(INVOKER_NAME); alarmed.setResourceStatus(ResourceStatusType.ALARM); repo.updateResource(headers(), alarmed); assertThat(captureWrite().getResourceStatus()).isEqualTo(ResourceStatusType.AVAILABLE); } // ---------------------------------------------------------------- helpers private Map<String, Object> headers() { Map<String, Object> headers = new HashMap<>(); headers.put("org.etsi.osl.resourceId", RESOURCE_ID); return headers; } private ResourceUpdate anUpdateFor(String specName) { ResourceUpdate update = new ResourceUpdate(); ResourceSpecificationRef ref = new ResourceSpecificationRef(); ref.setName(specName); update.setResourceSpecification(ref); return update; } private CapifInvoker anInvoker() { CapifInvoker invoker = new CapifInvoker(); invoker.setName(INVOKER_NAME); invoker.setDescription("a CAPIF invoker"); invoker.setBearerAccessToken("a-token"); invoker.setDiscoveredServiceAPIs("[]"); invoker.setCapifInvokerState(CapifInvokerState.ONBOARDED); return invoker; } private CapifInvokerResource aCapifInvokerResource() { return new CapifInvokerResource(INVOKER_NAME, "1.0.0", "a CAPIF invoker", "CAPIF", anInvoker()); } private ResourceUpdate captureWrite() { ArgumentCaptor<ResourceUpdate> captor = ArgumentCaptor.forClass(ResourceUpdate.class); verify(catalogClient).updateResourceById(anyString(), captor.capture()); return captor.getValue(); } /** Folds what was written the way OSOM folds it, rather than restating the attribute pair. */ private ResourceHealth healthOf(ResourceUpdate update) { Resource asRead = new Resource(); asRead.setOperationalState(update.getOperationalState()); asRead.setAdministrativeState(update.getAdministrativeState()); return asRead.deriveHealth(); } /** * {@code addResourceCharacteristicItemShort} appends rather than replaces, so a name written * twice appears twice. A consumer applying them in order ends up with the last one. */ private String lastValueOf(ResourceUpdate update, String name) { String value = null; for (Characteristic c : update.getResourceCharacteristic()) { if (name.equals(c.getName()) && c.getValue() != null) { value = String.valueOf(c.getValue().getValue()); } } return value; } } Loading
src/test/java/org/etsi/osl/controllers/capif/invoker/ResourceRepoServiceHealthTest.java 0 → 100644 +344 −0 Original line number Diff line number Diff line package org.etsi.osl.controllers.capif.invoker; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.HashMap; import java.util.Map; import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationRef; import org.etsi.osl.tmf.ri639.model.Characteristic; import org.etsi.osl.tmf.ri639.model.Resource; import org.etsi.osl.tmf.ri639.model.ResourceAdministrativeStateType; import org.etsi.osl.tmf.ri639.model.ResourceHealth; import org.etsi.osl.tmf.ri639.model.ResourceOperationalStateType; import org.etsi.osl.tmf.ri639.model.ResourceStatusType; import org.etsi.osl.tmf.ri639.model.ResourceUpdate; import org.etsi.osl.tmf.ri639.model.ResourceUsageStateType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.test.util.ReflectionTestUtils; /** * Pins the ITU-T X.731 health this controller reports on every path that writes a resource back to * the TMF catalog. * * <p>OSOM decides a supporting service's lifecycle from {@code Resource.deriveHealth()}, which * folds {@code operationalState} and {@code administrativeState} into a verdict. * {@code resourceStatus} takes no part in it. A path that reports neither attribute derives * {@code PENDING}, which neither promotes nor demotes the service: the order hangs with no * exception and nothing in any log, which is exactly the failure this suite exists to prevent * recurring. Every assertion below therefore states the verdict the way OSOM reads it, by folding * what was written through {@code deriveHealth()}. */ class ResourceRepoServiceHealthTest { private static final String RESOURCE_ID = "res-1"; private static final String INVOKER_NAME = "myInvoker"; private ResourceRepoService repo; private CatalogClient catalogClient; private InvokerManagement invokerManagement; @BeforeEach void setUp() { repo = new ResourceRepoService(); catalogClient = mock(CatalogClient.class); invokerManagement = mock(InvokerManagement.class); repo.aCatalogClient = catalogClient; repo.invokerManagement = invokerManagement; repo.simpleResourceMapper = mock(SimpleResourceMapper.class); ReflectionTestUtils.setField(repo, "compcategory", "CAPIF"); ReflectionTestUtils.setField(repo, "compversion", "1.0.0"); when(catalogClient.updateResourceById(anyString(), any(ResourceUpdate.class))) .thenReturn(new Resource()); } // ---------------------------------------------------------------- create / update @Test @DisplayName("an onboarded invoker is reported UP, so the supporting service can reach ACTIVE") void onboardedInvokerIsReportedUp() throws Exception { when(invokerManagement.onboardInvokerByName(INVOKER_NAME)).thenReturn(anInvoker()); repo.updateResource(headers(), anUpdateFor(INVOKER_NAME)); ResourceUpdate written = captureWrite(); assertThat(written.getOperationalState()).isEqualTo(ResourceOperationalStateType.ENABLE); assertThat(written.getAdministrativeState()).isEqualTo(ResourceAdministrativeStateType.UNLOCKED); assertThat(written.getUsageState()).isEqualTo(ResourceUsageStateType.ACTIVE); assertThat(healthOf(written)).isEqualTo(ResourceHealth.UP); assertThat(written.getResourceStatus()).isEqualTo(ResourceStatusType.AVAILABLE); assertThat(lastValueOf(written, "status.Health")).isEqualTo("UP"); assertThat(lastValueOf(written, "status.infoMessage")).isEqualTo("onboarded to CAPIF service"); } @Test @DisplayName("an onboarded invoker is remembered, so its bearer token gets refreshed later") void onboardedInvokerIsRemembered() throws Exception { when(invokerManagement.onboardInvokerByName(INVOKER_NAME)).thenReturn(anInvoker()); repo.updateResource(headers(), anUpdateFor(INVOKER_NAME)); assertThat(repo.resourceToInvokerMap).containsKey(RESOURCE_ID); } @Test @DisplayName("an invoker name this controller does not know is reported DOWN, never left unreported") void unknownInvokerNameIsReportedDown() throws Exception { // onboardInvokerByName returns null for a name that is not in the configured invokers. This // branch used to fall through with resourceStatus=UNKNOWN and no health at all, which reads as // PENDING: the service would wait for an invoker that is never coming. when(invokerManagement.onboardInvokerByName("nosuch")).thenReturn(null); repo.updateResource(headers(), anUpdateFor("nosuch")); ResourceUpdate written = captureWrite(); assertThat(healthOf(written)).isEqualTo(ResourceHealth.DOWN); assertThat(written.getUsageState()).isEqualTo(ResourceUsageStateType.IDLE); assertThat(written.getResourceStatus()).isEqualTo(ResourceStatusType.ALARM); assertThat(lastValueOf(written, "status.state")).isEqualTo(CapifInvokerState.MISSING.name()); assertThat(lastValueOf(written, "status.infoMessage")).contains("nosuch"); assertThat(lastValueOf(written, "status.Health")).isEqualTo("DOWN"); assertThat(repo.resourceToInvokerMap).doesNotContainKey(RESOURCE_ID); } @Test @DisplayName("a failed onboarding is reported DOWN with the reason, which demotes rather than hangs") void failedOnboardingIsReportedDown() throws Exception { when(invokerManagement.onboardInvokerByName(INVOKER_NAME)) .thenThrow(new IllegalStateException("CAPIF refused the certificate")); repo.updateResource(headers(), anUpdateFor(INVOKER_NAME)); ResourceUpdate written = captureWrite(); assertThat(healthOf(written)).isEqualTo(ResourceHealth.DOWN); // DISABLE while still UNLOCKED: not working, but not torn down either. assertThat(written.getAdministrativeState()).isEqualTo(ResourceAdministrativeStateType.UNLOCKED); assertThat(written.getResourceStatus()).isEqualTo(ResourceStatusType.ALARM); assertThat(lastValueOf(written, "status.state")).isEqualTo(CapifInvokerState.ERROR.name()); assertThat(lastValueOf(written, "status.infoMessage")).contains("CAPIF refused the certificate"); assertThat(lastValueOf(written, "status.Health")).isEqualTo("DOWN"); } // ---------------------------------------------------------------- delete @Test @DisplayName("a deleted resource is reported GONE, which is what tears the service down") void deletedResourceIsReportedGone() { repo.resourceToInvokerMap.put(RESOURCE_ID, aCapifInvokerResource()); repo.deleteResource(headers(), new ResourceUpdate()); ResourceUpdate written = captureWrite(); assertThat(written.getOperationalState()).isEqualTo(ResourceOperationalStateType.DISABLE); // SHUTDOWN is the teardown tombstone; resourceStatus=UNKNOWN on its own moves nothing. assertThat(written.getAdministrativeState()).isEqualTo(ResourceAdministrativeStateType.SHUTDOWN); assertThat(written.getUsageState()).isEqualTo(ResourceUsageStateType.IDLE); assertThat(healthOf(written)).isEqualTo(ResourceHealth.GONE); assertThat(written.getResourceStatus()).isEqualTo(ResourceStatusType.UNKNOWN); assertThat(lastValueOf(written, "status.Health")).isEqualTo("GONE"); assertThat(repo.resourceToInvokerMap).doesNotContainKey(RESOURCE_ID); } // ---------------------------------------------------------------- bearer refresh heartbeat @Test @DisplayName("a refreshed bearer token is reported UP with the new token") void refreshedBearerIsReportedUp() throws Exception { repo.resourceToInvokerMap.put(RESOURCE_ID, aCapifInvokerResource()); CapifInvoker refreshed = anInvoker(); refreshed.setBearerAccessToken("fresh-token"); when(invokerManagement.refreshBearer(any(CapifInvoker.class))).thenReturn(refreshed); repo.processBearerRefreshTokens(); ResourceUpdate written = captureWrite(); assertThat(healthOf(written)).isEqualTo(ResourceHealth.UP); assertThat(written.getUsageState()).isEqualTo(ResourceUsageStateType.ACTIVE); // The sweeper says nothing about pool disposition: the update leaves resourceStatus unset, and // the TMF inventory applies only the fields an update carries, so a reservation survives it. assertThat(written.getResourceStatus()).isNull(); assertThat(lastValueOf(written, "bearerAccessToken")).isEqualTo("fresh-token"); assertThat(lastValueOf(written, "status.infoMessage")).startsWith("Bearer token refreshed"); } @Test @DisplayName("a bearer refresh that failed is reported DOWN, not as a refresh that succeeded") void failedBearerRefreshIsReportedDown() throws Exception { // refreshBearer used to swallow its exception and hand the stale invoker back, so this loop // wrote "Bearer token refreshed" onto a resource whose token was quietly expiring. repo.resourceToInvokerMap.put(RESOURCE_ID, aCapifInvokerResource()); when(invokerManagement.refreshBearer(any(CapifInvoker.class))) .thenThrow(new IllegalStateException("CAPIF unreachable")); repo.processBearerRefreshTokens(); ResourceUpdate written = captureWrite(); assertThat(healthOf(written)).isEqualTo(ResourceHealth.DOWN); // The bad news travels as health. Writing ALARM from here would clear a reservation the // sweeper knows nothing about, and DOWN already demotes the supporting service. assertThat(written.getResourceStatus()).isNull(); assertThat(lastValueOf(written, "status.state")).isEqualTo(CapifInvokerState.ERROR.name()); assertThat(lastValueOf(written, "status.infoMessage")) .contains("Bearer token refresh failed") .contains("CAPIF unreachable"); assertThat(lastValueOf(written, "status.Health")).isEqualTo("DOWN"); } @Test @DisplayName("one unwritable resource does not starve the rest of the refresh loop") void oneUnwritableResourceDoesNotStarveTheRest() throws Exception { repo.resourceToInvokerMap.put("res-bad", aCapifInvokerResource()); repo.resourceToInvokerMap.put("res-good", aCapifInvokerResource()); when(invokerManagement.refreshBearer(any(CapifInvoker.class))).thenReturn(anInvoker()); when(catalogClient.updateResourceById(eq("res-bad"), any(ResourceUpdate.class))) .thenThrow(new RuntimeException("catalog unreachable")); repo.processBearerRefreshTokens(); verify(catalogClient).updateResourceById(eq("res-good"), any(ResourceUpdate.class)); } // ------------------------------------------------- allocation someone else made @Test @DisplayName("onboarding does not release a reservation this controller did not make") void onboardingLeavesAReservationAlone() throws Exception { // The ResourceUpdate is built from the live catalog entry, so an orchestrator's RESERVED // arrives inside it. Reporting AVAILABLE here would put the resource back in the free pool // while the order that reserved it is still using it. when(invokerManagement.onboardInvokerByName(INVOKER_NAME)).thenReturn(anInvoker()); ResourceUpdate reserved = anUpdateFor(INVOKER_NAME); reserved.setResourceStatus(ResourceStatusType.RESERVED); repo.updateResource(headers(), reserved); ResourceUpdate written = captureWrite(); assertThat(written.getResourceStatus()).isEqualTo(ResourceStatusType.RESERVED); // ...and the health that actually promotes the service is unaffected. assertThat(healthOf(written)).isEqualTo(ResourceHealth.UP); } @Test @DisplayName("a failed onboarding does not release a reservation either") void failedOnboardingLeavesAReservationAlone() throws Exception { when(invokerManagement.onboardInvokerByName(INVOKER_NAME)) .thenThrow(new IllegalStateException("CAPIF refused the certificate")); ResourceUpdate reserved = anUpdateFor(INVOKER_NAME); reserved.setResourceStatus(ResourceStatusType.RESERVED); repo.updateResource(headers(), reserved); ResourceUpdate written = captureWrite(); assertThat(written.getResourceStatus()).isEqualTo(ResourceStatusType.RESERVED); assertThat(healthOf(written)).isEqualTo(ResourceHealth.DOWN); } @Test @DisplayName("teardown does end the allocation, because the resource it was made against is going") void deleteClearsTheAllocation() { ResourceUpdate reserved = new ResourceUpdate(); reserved.setResourceStatus(ResourceStatusType.RESERVED); repo.deleteResource(headers(), reserved); assertThat(captureWrite().getResourceStatus()).isEqualTo(ResourceStatusType.UNKNOWN); } @Test @DisplayName("a stale ALARM is superseded by a successful onboarding: a condition is only an observation") void onboardingSupersedesAStaleAlarm() throws Exception { when(invokerManagement.onboardInvokerByName(INVOKER_NAME)).thenReturn(anInvoker()); ResourceUpdate alarmed = anUpdateFor(INVOKER_NAME); alarmed.setResourceStatus(ResourceStatusType.ALARM); repo.updateResource(headers(), alarmed); assertThat(captureWrite().getResourceStatus()).isEqualTo(ResourceStatusType.AVAILABLE); } // ---------------------------------------------------------------- helpers private Map<String, Object> headers() { Map<String, Object> headers = new HashMap<>(); headers.put("org.etsi.osl.resourceId", RESOURCE_ID); return headers; } private ResourceUpdate anUpdateFor(String specName) { ResourceUpdate update = new ResourceUpdate(); ResourceSpecificationRef ref = new ResourceSpecificationRef(); ref.setName(specName); update.setResourceSpecification(ref); return update; } private CapifInvoker anInvoker() { CapifInvoker invoker = new CapifInvoker(); invoker.setName(INVOKER_NAME); invoker.setDescription("a CAPIF invoker"); invoker.setBearerAccessToken("a-token"); invoker.setDiscoveredServiceAPIs("[]"); invoker.setCapifInvokerState(CapifInvokerState.ONBOARDED); return invoker; } private CapifInvokerResource aCapifInvokerResource() { return new CapifInvokerResource(INVOKER_NAME, "1.0.0", "a CAPIF invoker", "CAPIF", anInvoker()); } private ResourceUpdate captureWrite() { ArgumentCaptor<ResourceUpdate> captor = ArgumentCaptor.forClass(ResourceUpdate.class); verify(catalogClient).updateResourceById(anyString(), captor.capture()); return captor.getValue(); } /** Folds what was written the way OSOM folds it, rather than restating the attribute pair. */ private ResourceHealth healthOf(ResourceUpdate update) { Resource asRead = new Resource(); asRead.setOperationalState(update.getOperationalState()); asRead.setAdministrativeState(update.getAdministrativeState()); return asRead.deriveHealth(); } /** * {@code addResourceCharacteristicItemShort} appends rather than replaces, so a name written * twice appears twice. A consumer applying them in order ends up with the last one. */ private String lastValueOf(ResourceUpdate update, String name) { String value = null; for (Characteristic c : update.getResourceCharacteristic()) { if (name.equals(c.getName()) && c.getValue() != null) { value = String.valueOf(c.getValue().getValue()); } } return value; } }