Skip to content

Commit

Permalink
Client function for new metrics endpoint (#268)
Browse files Browse the repository at this point in the history
  • Loading branch information
macfarlandian committed Dec 7, 2020
1 parent df00860 commit 6b5b8b6
Show file tree
Hide file tree
Showing 13 changed files with 220 additions and 3 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,16 @@ jobs:
defaults:
run:
working-directory: spotlight-client
env:
REACT_APP_API_URL: http://localhost:3002
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: "12.x"
- uses: c-hive/gha-yarn-cache@v1
- run: yarn install --frozen-lockfile
- run: yarn test --coverage
- run: yarn test --coverage --forceExit
- name: Coveralls
uses: coverallsapp/github-action@master
with:
Expand Down
11 changes: 11 additions & 0 deletions spotlight-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This package is a Node/Express server application that provides a thin API backe
If you have followed the [setup instructions](../README.md#getting-set-up) in the root directory, you should be ready to go. You should be able to test your development environment via:

`yarn lint`
`yarn test`

We suggest installing a linting package for your preferred code editor that hooks into [eslint](#yarn-lint). We recommend [linter-eslint](https://atom.io/packages/linter-eslint) if you're using Atom.

Expand Down Expand Up @@ -69,3 +70,13 @@ Besides the scripts mentioned above for running and deploying the app, you can a
Runs the eslint checks against the package to check for issues in code style.

Eslint rules are configurable in `.eslintrc.json`, which inherits from the root `../.eslintrc.json` and extends it with settings specific to this package. Any change to this file should be accompanied with an explanation for the change and why it should be merged.

### `yarn test`

Launches the test runner ([Jest](https://jestjs.io/)) in interactive watch mode.

### `yarn start-test-server`

Starts the server with settings that are expected by other packages that wish to run integration tests against a live server (e.g. listening on a particular port, using local data fixtures rather than fetching from GCP).

Packages that wish to use this test server will generally be responsible for provisioning it for themselves, so you shouldn't have to run this directly in order to run tests in other packages.
7 changes: 7 additions & 0 deletions spotlight-api/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ app.get("/api/:tenantId/race", api.race);
app.get("/api/:tenantId/sentencing", api.sentencing);
app.post("/api/:tenantId/public", express.json(), api.metricsByName);

// uptime check endpoint
app.get("/health", (req, res) => {
// eslint-disable-next-line no-console
console.log("Responding to uptime check ...");
res.sendStatus(200);
});

// An App Engine-specific API for handling warmup requests on new instance initialization
app.get("/_ah/warmup", () => {
// The server automatically launches initialization of the metric cache, so nothing is needed here
Expand Down
1 change: 1 addition & 0 deletions spotlight-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dev": "nodemon index.js",
"lint": "eslint .",
"start": "node index.js",
"start-test-server": "PORT=3002 IS_DEMO=true yarn start",
"test": "jest"
},
"dependencies": {
Expand Down
1 change: 1 addition & 0 deletions spotlight-client/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
REACT_APP_AUTH_ENABLED={FLAG}
REACT_APP_AUTH_ENV={ENV_NAME}
REACT_APP_API_URL=http://localhost:3001
5 changes: 4 additions & 1 deletion spotlight-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ We suggest installing a linting package for your preferred code editor that hook

#### Environment variables

Second and last, set up your environment variables. Copy the `.env.example` file and set variables accordingly per environment. The app can be deployed to both staging and production environments. Staging relies on environment variables stored in `.env.development` and production relies on variables in `.env.production`. Local relies on `.env.development.local`.
Second and last, set up your environment variables. Copy the `.env.example` file and set variables accordingly per environment. The app can be deployed to both staging and production environments. Staging relies on environment variables stored in `.env.development` and production relies on variables in `.env.production`. Local relies on `.env.development.local`. The test environment relies on `.env.test`.

Expected environment variables include:

- `REACT_APP_AUTH_ENABLED` - set to `true` or `false` to toggle Auth0 protection per environment. Currently only used in staging to make the entire site private. No need to enable this locally unless you are developing or testing something auth-related. If set to `true` then `REACT_APP_AUTH_ENV` **must** be set to a supported value.
- `REACT_APP_AUTH_ENV` - a string indicating the "auth environment" used to point to the correct Auth0 tenant. `development` (which also covers staging) is the only supported value, which **must** be set if `REACT_APP_AUTH_ENABLED` is `true`.
- `REACT_APP_API_URL` - the base URL of the backend API server. This should be set to http://localhost:3001 when running the server locally, and to http://localhost:3002 in the test environment (because some tests will make requests to this URL).

(Note that variables must be prefixed with `REACT_APP_` to be available inside the client application.)

Expand Down Expand Up @@ -54,6 +55,8 @@ Launches the test runner in the interactive watch mode.

We use [`@testing-library/react`](https://testing-library.com/docs/react-testing-library/intro) for React component tests.

Also worth noting is that some integration tests execute against a real API server from `/spotlight-api`. This server process is started in `./globalTestSetup.js` and killed in `./globalTestTeardown.js`.

See the [Create React App docs](https://facebook.github.io/create-react-app/docs/running-tests) for more information.

### `yarn lint`
Expand Down
10 changes: 10 additions & 0 deletions spotlight-client/globalTestSetup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { resolve } = require("path");
const { spawn } = require("child_process");

module.exports = async () => {
// start the API test server. save reference so we can kill it in teardown
global.TEST_SERVER = spawn("yarn", ["start-test-server"], {
cwd: resolve(__dirname, "../spotlight-api"),
});
};
4 changes: 4 additions & 0 deletions spotlight-client/globalTestTeardown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = async () => {
// kill the test server we spawned during global setup
global.TEST_SERVER.kill();
};
7 changes: 6 additions & 1 deletion spotlight-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"react-dom": "^16.13.1",
"react-scripts": "3.4.3",
"typescript": "^4.0.0",
"utility-types": "^3.10.0"
"utility-types": "^3.10.0",
"wait-for-localhost": "^3.3.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^4.2.4",
Expand Down Expand Up @@ -57,5 +58,9 @@
],
"**.{js,jsx}": "eslint --fix",
"**.json": "prettier --write"
},
"jest": {
"globalSetup": "./globalTestSetup.js",
"globalTeardown": "./globalTestTeardown.js"
}
}
80 changes: 80 additions & 0 deletions spotlight-client/src/fetchMetrics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2020 Recidiviz, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

import fetchMetrics from "./fetchMetrics";
import { waitForTestServer } from "./testUtils";

test("returns fetched metrics", async () => {
await waitForTestServer();
// these are arbitrarily chosen to spot-check the API, nothing special about them
const metricNames = [
"active_program_participation_by_region",
"supervision_success_by_month",
];

const tenantId = "US_ND";

const response = await fetchMetrics({
metricNames,
tenantId,
});

expect(Object.keys(response)).toEqual(metricNames);

// these are records chosen at random from the spotlight-api data fixtures
expect(response.active_program_participation_by_region).toEqual(
expect.arrayContaining([
{
supervision_type: "PAROLE",
participation_count: "21",
state_code: "US_ND",
region_id: "6",
race_or_ethnicity: "ALL",
},
])
);
expect(response.supervision_success_by_month).toEqual(
expect.arrayContaining([
{
state_code: "US_ND",
projected_year: "2019",
projected_month: "8",
district: "SOUTH_CENTRAL",
supervision_type: "PROBATION",
successful_termination_count: "43",
projected_completion_count: "95",
success_rate: 0.45263157894736844,
},
])
);
});

test("handles error responses", async () => {
expect.assertions(2);

await waitForTestServer();

try {
await fetchMetrics({
metricNames: ["this_file_does_not_exist"],
tenantId: "US_ND",
});
} catch (e) {
expect(e.message).toMatch("500");
expect(e.message).toMatch("not registered");
}
});
65 changes: 65 additions & 0 deletions spotlight-client/src/fetchMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2020 Recidiviz, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

import { TenantId } from "./contentApi/types";

/**
* All data comes back from the server as string values;
* it will be up to us to cast those strings to other types as needed
*/
type RawMetricData = Record<string, string>[];

type MetricsApiResponse = Record<string, RawMetricData | null>;
type ErrorAPIResponse = { error: string };

type FetchMetricOptions = {
metricNames: string[];
tenantId: TenantId;
};

/**
* Retrieves the metric data provided for this application in the `/spotlight-api` package.
*/
export default async function fetchMetrics({
metricNames,
tenantId,
}: FetchMetricOptions): Promise<MetricsApiResponse> {
const response = await fetch(
`${process.env.REACT_APP_API_URL}/api/${tenantId}/public`,
{
body: JSON.stringify({
metrics: metricNames,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
}
);

if (response.ok) {
const responseData: MetricsApiResponse = await response.json();
return responseData;
}

const errorResponse: ErrorAPIResponse = await response.json();
throw new Error(
`Metrics API responded with status ${response.status}. Error message: ${
errorResponse.error || "none"
}`
);
}
23 changes: 23 additions & 0 deletions spotlight-client/src/testUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2020 Recidiviz, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

import waitForLocalhost from "wait-for-localhost";

// eslint-disable-next-line import/prefer-default-export
export function waitForTestServer(): Promise<void> {
return waitForLocalhost({ path: "/health", port: 3002 });
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -13243,6 +13243,11 @@ wait-for-expect@^3.0.2:
resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-3.0.2.tgz#d2f14b2f7b778c9b82144109c8fa89ceaadaa463"
integrity sha512-cfS1+DZxuav1aBYbaO/kE06EOS8yRw7qOFoD3XtjTkYvCvh3zUvNST8DXK/nPaeqIzIv3P3kL3lRJn8iwOiSag==

wait-for-localhost@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/wait-for-localhost/-/wait-for-localhost-3.3.0.tgz#aa3df1aeb7067a29630683a42e06515d18006061"
integrity sha512-/9FDq1qaRXfFwVpRSDiybxHSt6TakGscBqyhO3tsPB1ToRVyjOXVSI2IvW1T04MqM2Mq3a+6J8PdzAlscrCQzg==

walker@^1.0.7, walker@~1.0.5:
version "1.0.7"
resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
Expand Down

0 comments on commit 6b5b8b6

Please sign in to comment.