Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alert location parsing tests #1828

Open
wants to merge 25 commits into
base: api-interop-layer
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b23caa6
api interop layer
greg-does-weather Aug 13, 2024
fee7006
handle empty observations
greg-does-weather Sep 13, 2024
a965b74
data is partitioned by units
greg-does-weather Sep 13, 2024
5f73d47
make interop URL configurable
greg-does-weather Sep 13, 2024
8faaaf2
run interop tests in ci/cd
greg-does-weather Sep 13, 2024
9aee06a
top-level error handling for point forecast page
greg-does-weather Sep 13, 2024
dae0643
update copy
greg-does-weather Sep 13, 2024
07dd0a9
add tests for observations
greg-does-weather Sep 13, 2024
d94086b
new relic npm agent configuration
jamestranovich-noaa Sep 12, 2024
348daae
new relic metrics
jamestranovich-noaa Sep 13, 2024
b010602
fix: send metric payload correctly
jamestranovich-noaa Sep 16, 2024
dadb583
use new relic attributes instead of env
jamestranovich-noaa Sep 16, 2024
987818f
simplification
jamestranovich-noaa Sep 16, 2024
e9a9af4
minor fix
jamestranovich-noaa Sep 16, 2024
cc741b4
eslint all javascript files
jamestranovich-noaa Sep 24, 2024
b6c1e6c
more eslint fixes
jamestranovich-noaa Sep 24, 2024
34142d7
even more eslint fixes
jamestranovich-noaa Sep 24, 2024
e56fd14
eslint configuration tweaks
jamestranovich-noaa Sep 24, 2024
9a65274
install api-interop-layer dependencies
jamestranovich-noaa Sep 24, 2024
ab92b77
api interop layer: geometry tests
jamestranovich-noaa Sep 24, 2024
6f9db40
Merge pull request #1817 from weather-gov/jt/api-interop-test-alert-g…
jamestranovich-noaa Sep 26, 2024
e8ffa3b
Implementing tests from WeatherAlertParser.php
eric-gade Sep 26, 2024
2abe45a
lints
eric-gade Sep 26, 2024
b686bc3
Merge pull request #1827 from weather-gov/eg-1600-alert-description-p…
eric-gade Sep 26, 2024
dffd1bd
Adding tests and removing php test file
eric-gade Sep 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
25 changes: 21 additions & 4 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
module.exports = {
root: true,
extends: ["airbnb-base", "prettier"],
ignorePatterns: [
"tests/translations/**/*.js",
"uswds*.js"
],
ignorePatterns: ["tests/translations/**/*.js", "uswds*.js"],
rules: {
// For imports in the browser, file extensions are always required.
"import/extensions": ["error", "always"],
Expand Down Expand Up @@ -46,6 +44,25 @@ module.exports = {
"class-methods-use-this": 0,
},
},
{
files: ["api-interop-layer/**/*.test.js"],
extends: ["airbnb-base", "prettier"],
parserOptions: { ecmaVersion: 2024 },
rules: {
// This rule disallows using require() on files in devDependencies. But
// for test code, we'll rely on that heavily so we can disable the rule
// in here.
"import/no-extraneous-dependencies": [0],

// For imports in Node, file extensions are optional and discouraged as
// a matter of practice.
"import/extensions": ["error", "always"],

// chai provides "empty" expressions, such as `to.be.true`
"no-unused-expressions": "off"
},
env: { mocha: true },
},
{
files: [
"tests/**/*.js",
Expand Down
6 changes: 5 additions & 1 deletion .github/actions/javascript-lint/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ runs:
shell: bash
run: cd tests/api && npm ci

- name: install api-interop-layer dependencies
shell: bash
run: cd api-interop-layer && npm ci

- name: add problem matcher
shell: bash
run: echo "::add-matcher::${{ github.workspace }}/.github/workflows/problem-matcher-eslint.json"

- name: run eslint
shell: bash
run: npm run js-lint
run: npm run js-lint
8 changes: 8 additions & 0 deletions .github/actions/setup-site/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,11 @@ runs:
- name: clear the Drupal cache
shell: bash
run: make cc

# Restart the interop layer once the database is loaded. It immediately
# tries to run some queries, but if the database isn't ready, it'll crash
# and die.
- name: restart the interop layer
shell: bash
run: |
docker compose restart api-interop-layer
22 changes: 22 additions & 0 deletions .github/workflows/code-standards.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,28 @@ jobs:
if: needs.should-test.outputs.yes == 'true'
uses: ./.github/actions/style-lint

interop-layer-unit-tests:
name: interop layer unit tests
runs-on: ubuntu-latest
needs: [should-test]
defaults:
run:
working-directory: ./api-interop-layer/

steps:
- name: checkout
if: needs.should-test.outputs.yes == 'true'
uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: install dependencies
run: npm ci
- name: test
run: npm test

build-drupal-image:
name: build Drupal image
runs-on: ubuntu-latest
Expand Down
6 changes: 6 additions & 0 deletions api-interop-layer/.c8rc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"all": true,
"reporter": ["html"],
"exclude": ["node_modules/**", "coverage", "**/*.test.js"],
"require": ["mocha.js"]
}
2 changes: 2 additions & 0 deletions api-interop-layer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.nyc_output
coverage
11 changes: 11 additions & 0 deletions api-interop-layer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM node:20

RUN mkdir /app
WORKDIR /app

ADD ./package.json .
ADD ./package-lock.json .

RUN npm ci

CMD npm start
72 changes: 72 additions & 0 deletions api-interop-layer/data/alerts/geometry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const unwindGeometryCollection = (geojson, parentIsCollection = false) => {
if (geojson.type === "GeometryCollection") {
const geometries = geojson.geometries.flatMap((geometry) =>
unwindGeometryCollection(geometry, true),
);
if (parentIsCollection) {
return geometries;
}

geojson.geometries = geometries;
return geojson;
}

return geojson;
};

export const generateAlertGeometry = async (db, rawAlert) => {
// if the alert already has geometry, nothing to do
if (rawAlert.geometry) {
return unwindGeometryCollection(rawAlert.geometry);
}

// if we have affected zones, generate a geometry from zones
const zones = rawAlert.properties.affectedZones;
if (Array.isArray(zones) && zones.length > 0) {
const sql = `
SELECT ST_ASGEOJSON(
ST_SIMPLIFY(
ST_SRID(
ST_COLLECT(shape),
0
),
0.003
)
)
AS shape
FROM weathergov_geo_zones
WHERE id IN (${zones.map(() => "?").join(",")})`;
const [{ shape }] = await db.query(sql, zones);
if (shape) {
return unwindGeometryCollection(shape);
}
}

// if all geocodes are the same, generate a geometry from geocodes
const counties = rawAlert.properties.geocode?.SAME;
if (Array.isArray(counties) && counties.length > 0) {
const sql = `
SELECT ST_ASGEOJSON(
ST_SIMPLIFY(
ST_SRID(
ST_COLLECT(shape),
0
),
0.003
)
)
AS shape
FROM weathergov_geo_counties
WHERE countyFips IN (${counties.map((c) => `'${c.slice(1)}'`).join(",")})`;
const [{ shape }] = await db.query(sql);

if (shape) {
return unwindGeometryCollection(shape);
}
}

// we cannot generate a geometry.
return null;
}

export default { generateAlertGeometry };
100 changes: 100 additions & 0 deletions api-interop-layer/data/alerts/geometry.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import sinon from "sinon";
import { expect } from "chai";
import * as mariadb from "mariadb";
import { generateAlertGeometry } from "./geometry.js";

describe("alert geometries", () => {
const sandbox = sinon.createSandbox();
const db = {
query: sandbox.stub(),
end: () => Promise.resolve(),
};

// Do this before everything, so it'll happen before any describe blocks run,
// otherwise the connection creation won't be stubbed when the script is first
// imported below.
before(() => {
mariadb.default.createConnection.resolves(db);
});

beforeEach(() => {
sandbox.resetBehavior();
sandbox.resetHistory();
});

it("returns an alert with a geometry as-is", async () => {
const rawAlert = {
geometry: "existing geometry",
};

const geometry = await generateAlertGeometry(db, rawAlert);
expect(geometry).to.equal("existing geometry");
});

it("autogenerates a geometry from affected zones", async () => {
const affectedZones = ["zone 1", "zone 2", "zone 3"];
const rawAlert = {
geometry: false,
properties: {
affectedZones,
},
};
const query = `SELECT ST_ASGEOJSON(
ST_SIMPLIFY(
ST_SRID(
ST_COLLECT(shape),
0
),
0.003
)
)
AS shape
FROM weathergov_geo_zones
WHERE id IN (?,?,?)`;
db.query.withArgs(sinon.match(query), sinon.match.same(affectedZones))
.resolves([{ shape: { combined: "zones" } }]);

const geometry = await generateAlertGeometry(db, rawAlert);
expect(geometry).to.eql({ combined: "zones" });
});

it("autogenerates a geometry from same geocodes if no zones are present", async () => {
const rawAlert = {
geometry: false,
properties: {
geocode: {
// SAME code is FIPS code with a leading zero. The leading
// zero gets stripped out, so we need to include it here
// so we get what we expect later.
"SAME": ["0county 1", "0county 2", "0county 3"],
},
},
};
const query = `SELECT ST_ASGEOJSON(
ST_SIMPLIFY(
ST_SRID(
ST_COLLECT(shape),
0
),
0.003
)
)
AS shape
FROM weathergov_geo_counties
WHERE countyFips IN ('county 1','county 2','county 3')`;
db.query.withArgs(sinon.match(query)).resolves([{ shape: { combined: "county" } }]);

const geometry = await generateAlertGeometry(db, rawAlert);
expect(geometry).to.eql({ combined: "county" });
});

it("returns null geometry if no zones or different geocodes are present", async () => {
const rawAlert = {
geometry: false,
properties: {},
};

const geometry = await generateAlertGeometry(db, rawAlert);
expect(geometry).to.be.null;
});
});
Loading
Loading