Skip to content

Commit

Permalink
break out okta endpoint from health check (#7891)
Browse files Browse the repository at this point in the history
* add in separate okta component

* add in okta messages

* add in tests

* add in okta message

* undo hardcode for test branch

* fix frontend test

* fix frontend test

* make status check reflect api shape for okta

* make status check reflect api shape for okta

* make tests more readable and handle NPE in health check

* make smoke test script include a check for okta

* woops

* weave in config values into endpoint

Co-authored-by: Merethe Hansen <[email protected]>

* add in old api exception

* status check reflect subapi endpoint (sorry merethe)

* dedupe application.yaml

---------

Co-authored-by: Merethe Hansen <[email protected]>
  • Loading branch information
fzhao99 and mehansen committed Jul 9, 2024
1 parent 5a67de2 commit d4967cb
Show file tree
Hide file tree
Showing 12 changed files with 266 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package gov.cdc.usds.simplereport.api.heathcheck;

import com.okta.sdk.resource.client.ApiException;
import gov.cdc.usds.simplereport.db.repository.FeatureFlagRepository;
import gov.cdc.usds.simplereport.idp.repository.OktaRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.JDBCConnectionException;
Expand All @@ -15,28 +13,16 @@
@RequiredArgsConstructor
public class BackendAndDatabaseHealthIndicator implements HealthIndicator {
private final FeatureFlagRepository _ffRepo;
private final OktaRepository _oktaRepo;
public static final String ACTIVE_LITERAL = "ACTIVE";

@Override
public Health health() {
try {
_ffRepo.findAll();
String oktaStatus = _oktaRepo.getApplicationStatusForHealthCheck();

if (!ACTIVE_LITERAL.equals(oktaStatus)) {
log.info("Okta status didn't return ACTIVE, instead returned " + oktaStatus);
return Health.down().build();
}
return Health.up().build();

// reach into the ff repository returned a bad value or db connection issue respectively
} catch (IllegalArgumentException | JDBCConnectionException e) {
return Health.down().build();
} catch (ApiException e) {
// Okta API call errored
log.info(e.getMessage());
return Health.down().build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package gov.cdc.usds.simplereport.api.heathcheck;

import com.okta.sdk.resource.client.ApiException;
import gov.cdc.usds.simplereport.idp.repository.OktaRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
@Slf4j
@RequiredArgsConstructor
public class OktaHealthIndicator implements HealthIndicator {
private final OktaRepository _oktaRepo;
public static final String ACTIVE_LITERAL = "ACTIVE";

@Override
public Health health() {
Health.Builder oktaDegradedWarning = Health.status("OKTA_DEGRADED");
try {
String oktaStatus = _oktaRepo.getApplicationStatusForHealthCheck();
if (!ACTIVE_LITERAL.equals(oktaStatus)) {
log.info("Okta status didn't return ACTIVE, instead returned " + oktaStatus);
return oktaDegradedWarning.build();
}
} catch (NullPointerException e) {
log.info("Call to Okta repository status returned null");
return oktaDegradedWarning.build();
} catch (ApiException e) {
// Okta API call errored
log.info("Okta status call raised an exception: " + e.getMessage());
return oktaDegradedWarning.build();
}

return Health.up().build();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package gov.cdc.usds.simplereport.idp.repository;

import static gov.cdc.usds.simplereport.api.heathcheck.BackendAndDatabaseHealthIndicator.ACTIVE_LITERAL;
import static gov.cdc.usds.simplereport.api.heathcheck.OktaHealthIndicator.ACTIVE_LITERAL;

import com.okta.sdk.resource.model.UserStatus;
import gov.cdc.usds.simplereport.api.CurrentTenantDataAccessContextHolder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
Expand Down Expand Up @@ -690,7 +691,7 @@ public PartialOktaUser findUser(String username) {

@Override
public String getApplicationStatusForHealthCheck() {
return app.getStatus().toString();
return Objects.requireNonNull(app.getStatus()).toString();
}

private Optional<OrganizationRoleClaims> getOrganizationRoleClaimsFromAuthorities(
Expand Down
12 changes: 9 additions & 3 deletions backend/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,16 @@ server:
error:
include-stacktrace: never
management:
endpoint.health.probes.enabled: true
endpoint.info.enabled: true
endpoints.web.exposure.include: health, info
endpoint.health.show-components: always
endpoint:
info:
enabled: true
health:
show-components: always
status:
http-mapping:
okta_degraded: 204
probes.enabled: true
okta:
oauth2:
issuer: https://hhs-prime.okta.com/oauth2/default
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package gov.cdc.usds.simplereport.api.healthcheck;

import static gov.cdc.usds.simplereport.api.heathcheck.BackendAndDatabaseHealthIndicator.ACTIVE_LITERAL;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

import gov.cdc.usds.simplereport.api.heathcheck.BackendAndDatabaseHealthIndicator;
import gov.cdc.usds.simplereport.db.repository.BaseRepositoryTest;
import gov.cdc.usds.simplereport.db.repository.FeatureFlagRepository;
import gov.cdc.usds.simplereport.idp.repository.OktaRepository;
import java.sql.SQLException;
import java.util.List;
import lombok.RequiredArgsConstructor;
Expand All @@ -23,14 +21,12 @@
class BackendAndDatabaseHealthIndicatorTest extends BaseRepositoryTest {

@SpyBean private FeatureFlagRepository mockFeatureFlagRepo;
@SpyBean private OktaRepository mockOktaRepo;

@Autowired private BackendAndDatabaseHealthIndicator indicator;

@Test
void health_succeedsWhenReposDoesntThrow() {
when(mockFeatureFlagRepo.findAll()).thenReturn(List.of());
when(mockOktaRepo.getApplicationStatusForHealthCheck()).thenReturn(ACTIVE_LITERAL);

assertThat(indicator.health()).isEqualTo(Health.up().build());
}
Expand All @@ -51,10 +47,4 @@ void health_failsWhenFeatureFlagRepoThrows() {
when(mockFeatureFlagRepo.findAll()).thenThrow(dbConnectionException);
assertThat(indicator.health()).isEqualTo(Health.down().build());
}

@Test
void health_failsWhenOktaRepoDoesntReturnActive() {
when(mockOktaRepo.getApplicationStatusForHealthCheck()).thenReturn("INACTIVE");
assertThat(indicator.health()).isEqualTo(Health.down().build());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package gov.cdc.usds.simplereport.api.healthcheck;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

import com.okta.sdk.resource.client.ApiException;
import gov.cdc.usds.simplereport.api.heathcheck.OktaHealthIndicator;
import gov.cdc.usds.simplereport.db.repository.BaseRepositoryTest;
import gov.cdc.usds.simplereport.idp.repository.OktaRepository;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.mock.mockito.SpyBean;

@RequiredArgsConstructor
@EnableConfigurationProperties
class OktaHealthIndicatorTest extends BaseRepositoryTest {

@SpyBean private OktaRepository mockOktaRepo;

@Autowired private OktaHealthIndicator indicator;

@Test
void health_SucceedsWhenOktaRepoReturnsActive() {
when(mockOktaRepo.getApplicationStatusForHealthCheck()).thenReturn("ACTIVE");
assertThat(indicator.health()).isEqualTo(Health.up().build());
}

@Test
void health_FailsWhenOktaApiThrowsErrors() {
when(mockOktaRepo.getApplicationStatusForHealthCheck())
.thenThrow(new ApiException("some api error"));
Health.Builder oktaDegradedWarning = Health.status("OKTA_DEGRADED");
assertThat(indicator.health()).isEqualTo(oktaDegradedWarning.build());
}

@Test
void health_FailsWhenOktaApiThrowsNPE() {
when(mockOktaRepo.getApplicationStatusForHealthCheck()).thenThrow(new NullPointerException());
Health.Builder oktaDegradedWarning = Health.status("OKTA_DEGRADED");
assertThat(indicator.health()).isEqualTo(oktaDegradedWarning.build());
}

@Test
void health_failsWhenOktaRepoDoesntReturnActive() {
when(mockOktaRepo.getApplicationStatusForHealthCheck()).thenReturn("INACTIVE");
Health.Builder oktaDegradedWarning = Health.status("OKTA_DEGRADED");

assertThat(indicator.health()).isEqualTo(oktaDegradedWarning.build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import gov.cdc.usds.simplereport.api.CurrentTenantDataAccessContextHolder;
import gov.cdc.usds.simplereport.api.WebhookContextHolder;
import gov.cdc.usds.simplereport.api.heathcheck.BackendAndDatabaseHealthIndicator;
import gov.cdc.usds.simplereport.api.heathcheck.OktaHealthIndicator;
import gov.cdc.usds.simplereport.api.pxp.CurrentPatientContextHolder;
import gov.cdc.usds.simplereport.config.AuditingConfig;
import gov.cdc.usds.simplereport.config.AuthorizationProperties;
Expand Down Expand Up @@ -105,6 +106,7 @@
TenantDataAccessService.class,
PatientSelfRegistrationLinkService.class,
BackendAndDatabaseHealthIndicator.class,
OktaHealthIndicator.class,
EmailService.class,
SendGridDisabledConfiguration.class,
})
Expand Down
24 changes: 21 additions & 3 deletions frontend/deploy-smoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// endpoint which does a simple ping to a non-sensitive DB table to verify
// all the connections are good.
// https://github.com/CDCgov/prime-simplereport/pull/7057

require("dotenv").config();
let { Builder } = require("selenium-webdriver");
const Chrome = require("selenium-webdriver/chrome");
Expand Down Expand Up @@ -33,16 +32,35 @@ driver
return value;
})
.then((value) => {
if (value.includes("success")) {
let appStatusSuccess, oktaStatusSuccess;
if (value.includes("App status returned success")) {
appStatusSuccess = true;
}
if (value.includes("Okta status returned success")) {
oktaStatusSuccess = true;
}
if (value.includes("App status returned failure")) {
appStatusSuccess = false;
}
if (value.includes("Okta status returned failure")) {
oktaStatusSuccess = false;
}

if (appStatusSuccess && oktaStatusSuccess) {
console.log(`Smoke test returned success status for ${appUrl}`);
process.exitCode = 0;
return;
}
if (value.includes("failure")) {

if (appStatusSuccess === false || oktaStatusSuccess === false) {
console.log(`Smoke test returned failure status for ${appUrl}`);
console.log(
`App health returned ${appStatusSuccess}, okta health returned ${oktaStatusSuccess}`
);
process.exitCode = 1;
return;
}

console.log("Smoke test encountered unknown failure.");
console.log(`Root element value was: ${value}`);
process.exitCode = 1;
Expand Down
36 changes: 28 additions & 8 deletions frontend/src/app/DeploySmokeTest.test.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,50 @@
import { render, screen, waitFor } from "@testing-library/react";
import { FetchMock } from "jest-fetch-mock";

import DeploySmokeTest from "./DeploySmokeTest";
import DeploySmokeTest, {
APP_STATUS_FAILURE,
APP_STATUS_LOADING,
APP_STATUS_SUCCESS,
OKTA_STATUS_LOADING,
OKTA_STATUS_SUCCESS,
} from "./DeploySmokeTest";
import { generateBackendApiHealthResponse } from "./deploySmokeTestTestConstants";

describe("DeploySmokeTest", () => {
beforeEach(() => {
(fetch as FetchMock).resetMocks();
});

it("renders success when returned from the API endpoint", async () => {
it("renders success when returned from the backend API smoke test endpoint", async () => {
(fetch as FetchMock).mockResponseOnce(JSON.stringify({ status: "UP" }));
(fetch as FetchMock).mockResponseOnce(JSON.stringify({ status: "DOWN" }));

render(<DeploySmokeTest />);
await waitFor(() =>
expect(screen.queryByText("Status loading...")).not.toBeInTheDocument()
expect(screen.queryByText(APP_STATUS_LOADING)).not.toBeInTheDocument()
);
expect(screen.getByText("Status returned success :)"));
expect(screen.getByText(APP_STATUS_SUCCESS));
});

it("renders failure when returned from the API endpoint", async () => {
(fetch as FetchMock).mockResponseOnce(JSON.stringify({ status: "DOWN" }));
it("renders failure when returned from the backend API smoke test endpoint", async () => {
(fetch as FetchMock).mockResponse(JSON.stringify({ status: "DOWN" }));

render(<DeploySmokeTest />);
await waitFor(() =>
expect(screen.queryByText(APP_STATUS_LOADING)).not.toBeInTheDocument()
);
expect(screen.getByText(APP_STATUS_FAILURE));
});

it("renders Okta success when returned from the backend API health endpoint", async () => {
(fetch as FetchMock).mockResponse(
JSON.stringify(generateBackendApiHealthResponse())
);

render(<DeploySmokeTest />);
await waitFor(() =>
expect(screen.queryByText("Status loading...")).not.toBeInTheDocument()
expect(screen.queryByText(OKTA_STATUS_LOADING)).not.toBeInTheDocument()
);
expect(screen.getByText("Status returned failure :("));
expect(screen.getByText(OKTA_STATUS_SUCCESS));
});
});
Loading

0 comments on commit d4967cb

Please sign in to comment.