This is an automated email from the ASF dual-hosted git repository. zrhoffman pushed a commit to branch 6.0.x in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
commit 4381e9c88d615afc8fceb7efff5d9fa1d6db4bce Author: ocket8888 <[email protected]> AuthorDate: Fri Aug 20 11:15:17 2021 -0600 TP tests alert logging (#6120) * Move `hasProperty` to its own module and provide type-specific property checking * Add a data model for global testing configuration that can define log levels * Make API take a configuration parameter and export an instance globally * Add common logging of Alerts to all axios requests in the API class * Update API usage to use new global export * Fix implicit 'any' type detection in old TS versions * Put TO logs in artifacts, clear up console output This also fixes assumptions that asynchronous background jobs will have reached a certain point by the time the foreground reaches a certain point, and adds debug output to the TO logs. Plus uses `npm` scripts instead of raw commands. * Move Riak logs into artifacts, out of console * Fix not restoring directory state after starting TP, moving open file descriptors * upload artifacts on all failures, not just test failures * Fix docker logs leaking stderr * log full URL * Wait for TO to start before continuing * fix passing API requests through TP instead of straight to TO * fix extra path separator in url building * Fix incorrect info log, remove commented-out code * Simplify checking property existence and type with new hasProperty call signatures * Remove redundant option * change debug logs to stdout (cherry picked from commit 30c0feb6904de74517dd927df4c721f342105e57) --- .github/actions/tp-integration-tests/cdn.json | 10 +- .github/actions/tp-integration-tests/entrypoint.sh | 90 ++++----- traffic_portal/test/integration/CommonUtils/API.ts | 211 ++++++++++++++------- .../test/integration/CommonUtils/index.ts | 21 ++ .../test/integration/CommonUtils/utils.ts | 133 +++++++++++++ traffic_portal/test/integration/config.model.ts | 165 ++++++++++++++++ traffic_portal/test/integration/config.ts | 21 +- traffic_portal/test/integration/specs/ASNs.spec.ts | 3 +- .../test/integration/specs/Coordinates.spec.ts | 3 +- .../integration/specs/DeliveryServices.spec.ts | 5 +- .../test/integration/specs/Divisions.spec.ts | 4 +- traffic_portal/test/integration/specs/Jobs.spec.ts | 3 +- .../test/integration/specs/Origins.spec.ts | 3 +- .../test/integration/specs/Parameters.spec.ts | 5 +- .../test/integration/specs/PhysLocations.spec.ts | 3 +- .../test/integration/specs/Profiles.spec.ts | 3 +- .../test/integration/specs/Regions.spec.ts | 3 +- .../specs/ServerServerCapabilities.spec.ts | 3 +- .../test/integration/specs/Servers.spec.ts | 3 +- .../integration/specs/ServiceCategories.spec.ts | 3 +- .../test/integration/specs/Statuses.spec.ts | 3 +- .../test/integration/specs/Topologies.spec.ts | 6 +- .../test/integration/specs/Types.spec.ts | 3 +- 23 files changed, 554 insertions(+), 153 deletions(-) diff --git a/.github/actions/tp-integration-tests/cdn.json b/.github/actions/tp-integration-tests/cdn.json index d91f8be..8f1d100 100644 --- a/.github/actions/tp-integration-tests/cdn.json +++ b/.github/actions/tp-integration-tests/cdn.json @@ -13,11 +13,11 @@ "traffic_ops_golang": { "insecure": true, "port": "6443", - "log_location_error": "error.log", - "log_location_warning": "warning.log", - "log_location_info": "info.log", - "log_location_debug": null, - "log_location_event": "event.log", + "log_location_error": "stderr", + "log_location_warning": "stderr", + "log_location_info": "stdout", + "log_location_debug": "stdout", + "log_location_event": "stdout", "max_db_connections": 20, "db_conn_max_lifetime_seconds": 60, "db_query_timeout_seconds": 20, diff --git a/.github/actions/tp-integration-tests/entrypoint.sh b/.github/actions/tp-integration-tests/entrypoint.sh index afd3b8f..6e6ca2d 100755 --- a/.github/actions/tp-integration-tests/entrypoint.sh +++ b/.github/actions/tp-integration-tests/entrypoint.sh @@ -15,7 +15,35 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -trap 'echo "Error on line ${LINENO} of ${0}"; exit 1' ERR + +onFail() { + echo "Error on line ${1} of ${2}" >&2; + if ! [[ -d Reports ]]; then + mkdir Reports; + fi + if [[ -f tv.log ]]; then + cp tv.log Reports/traffic_vault.docker.log; + fi + docker logs "$trafficvault" > Reports/traffic_vault.log 2>&1; + if [[ -f tp.log ]]; then + mv tp.log Reports/forever.log + fi + if [[ -f access.log ]]; then + mv access.log Reports/tp-access.log + fi + if [[ -f out.log ]]; then + mv out.log Reports/node.log + fi + docker logs $CHROMIUM_CONTAINER > Reports/chromium.log 2>&1; + docker logs $HUB_CONTAINER > Reports/hub.log 2>&1; + if [[ -f "${REPO_DIR}/traffic_ops/traffic_ops_golang" ]]; then + cp "${REPO_DIR}/traffic_ops/traffic_ops_golang" Reports/to.log; + fi + echo "Detailed logs produced info Reports artifact" + exit 1 +} + +trap 'onFail "${LINENO}" "${0}"' ERR set -o errexit -o nounset -o pipefail hub_fqdn="http://localhost:4444/wd/hub/status" @@ -23,7 +51,7 @@ to_fqdn="https://localhost:6443" tp_fqdn="https://172.18.0.1:8443" if ! curl -Lvsk "${hub_fqdn}" >/dev/null 2>&1; then - echo "Selenium not started on ${hub_fqdn}" + echo "Selenium not started on ${hub_fqdn}" >&2; exit 1 fi @@ -81,19 +109,6 @@ QUERY sudo useradd trafops -gray_bg="$(printf '%s%s' $'\x1B' '[100m')"; -red_bg="$(printf '%s%s' $'\x1B' '[41m')"; -yellow_bg="$(printf '%s%s' $'\x1B' '[43m')"; -black_fg="$(printf '%s%s' $'\x1B' '[30m')"; -color_and_prefix() { - color="$1"; - shift; - prefix="$1"; - normal_bg="$(printf '%s%s' $'\x1B' '[49m')"; - normal_fg="$(printf '%s%s' $'\x1B' '[39m')"; - sed "s/^/${color}${black_fg}${prefix}: /" | sed "s/$/${normal_bg}${normal_fg}/"; -} - ciab_dir="${GITHUB_WORKSPACE}/infrastructure/cdn-in-a-box"; trafficvault=trafficvault; start_traffic_vault() { @@ -127,9 +142,9 @@ start_traffic_vault() { --publish=8087:8087 \ --rm \ "$trafficvault" \ - /usr/lib/riak/riak-cluster.sh; + /usr/lib/riak/riak-cluster.sh } -start_traffic_vault & +start_traffic_vault >tv.log 2>&1 & sudo apt-get install -y --no-install-recommends gettext \ ruby ruby-dev libc-dev curl \ @@ -154,7 +169,7 @@ if [[ ! -e "$REPO_DIR" ]]; then fi to_build() { - cd "${REPO_DIR}/traffic_ops/traffic_ops_golang" + pushd "${REPO_DIR}/traffic_ops/traffic_ops_golang" if [[ ! -d "${GITHUB_WORKSPACE}/vendor/golang.org" ]]; then go mod vendor fi @@ -167,56 +182,42 @@ to_build() { export $(<"${ciab_dir}/variables.env" sed '/^#/d') # defines TV_ADMIN_USER/PASSWORD envsubst <"${resources}/riak.json" >riak.conf - truncate --size=0 warning.log error.log event.log info.log + truncate -s0 out.log - ./traffic_ops_golang --cfg ./cdn.conf --dbcfg ./database.conf -riakcfg riak.conf & - tail -f warning.log 2>&1 | color_and_prefix "${yellow_bg}" 'Traffic Ops WARN' & - tail -f error.log 2>&1 | color_and_prefix "${red_bg}" 'Traffic Ops ERR' & - tail -f event.log 2>&1 | color_and_prefix "${gray_bg}" 'Traffic Ops EVT' & + ./traffic_ops_golang --cfg ./cdn.conf --dbcfg ./database.conf -riakcfg riak.conf >out.log 2>&1 & + popd } tp_build() { - cd "${REPO_DIR}/traffic_portal" + pushd "${REPO_DIR}/traffic_portal" npm ci bower install grunt dist cp "${resources}/config.js" ./conf/ touch tp.log access.log out.log err.log - sudo forever --minUptime 5000 --spinSleepTime 2000 -f -o out.log start server.js & - tail -f err.log 2>&1 | color_and_prefix "${red_bg}" "Node Err" & + sudo forever --minUptime 5000 --spinSleepTime 2000 -f start server.js >out.log 2>&1 & + popd } -(to_build) & -(tp_build) & - -onFail() { - docker logs "$trafficvault" > Reports/traffic_vault.log - mv tp.log Reports/forever.log - mv access.log Reports/tp-access.log - mv out.log Reports/node.log - docker logs $CHROMIUM_CONTAINER > Reports/chromium.log - docker logs $HUB_CONTAINER > Reports/hub.log - echo "Detailed logs produced info Reports artifact" - exit 1 -} +to_build +tp_build cd "${REPO_DIR}/traffic_portal/test/integration" npm ci -PATH=$(pwd)/node_modules/.bin/:$PATH -webdriver-manager update --gecko false --versions.chrome "LATEST_RELEASE_$CHROMIUM_VER" +./node_modules/.bin/webdriver-manager update --gecko false --versions.chrome "LATEST_RELEASE_$CHROMIUM_VER" jq " .capabilities.chromeOptions.args = [ \"--headless\", \"--no-sandbox\", \"--disable-gpu\", \"--ignore-certificate-errors\" - ] | .params.apiUrl = \"${tp_fqdn}/api/4.0\" | .params.baseUrl =\"${tp_fqdn}\" + ] | .params.apiUrl = \"${to_fqdn}/api/4.0\" | .params.baseUrl =\"${tp_fqdn}\" | .capabilities[\"goog:chromeOptions\"].w3c = false | .capabilities.chromeOptions.w3c = false" \ config.json > config.json.tmp && mv config.json.tmp config.json -tsc +npm run build # Wait for tp/to build timeout 5m bash <<TMOUT @@ -226,5 +227,4 @@ timeout 5m bash <<TMOUT done TMOUT -trap - ERR -protractor ./GeneratedCode/config.js --params.baseUrl="${tp_fqdn}" --params.apiUrl="${to_fqdn}/api/4.0" || onFail +npm test -- --params.baseUrl="${tp_fqdn}" --params.apiUrl="${to_fqdn}/api/4.0" diff --git a/traffic_portal/test/integration/CommonUtils/API.ts b/traffic_portal/test/integration/CommonUtils/API.ts index 9b18768..2a626a3 100644 --- a/traffic_portal/test/integration/CommonUtils/API.ts +++ b/traffic_portal/test/integration/CommonUtils/API.ts @@ -21,10 +21,12 @@ import { Agent } from "https"; import axios from 'axios'; -import type {AxiosResponse} from "axios"; +import type {AxiosResponse, AxiosError} from "axios"; import randomIpv6 from "random-ipv6"; -import { config, randomize } from '../config'; +import { hasProperty } from "./utils"; +import { randomize } from '../config'; +import { AlertLevel, isAlert, logAlert, TestingConfig } from "../config.model"; interface GetRequest { queryKey: string; @@ -50,33 +52,61 @@ export interface APIData { } /** - * hasProperty checks, generically, whether some variable passed as `o` has the - * property `k`. + * Checks if an object is an AxiosError, usually useful in `try`/`catch` blocks + * around axios calls. * - * @example - * hasProperty({}, "id"); // returns false - * hasProperty({id: 8}); // returns true - * hasProperty({id: undefined}); // returns true - * - * @param o The object to check. - * @param k The key for which to check in the object. - * @returns Whether or not `o` has the property `k`. - * @throws {Error} when the type check fails. + * @param e The object to check. + * @returns Whether or not `e` is an AxiosError. */ - export function hasProperty<T extends object, K extends PropertyKey, S>(o: T, k: K): o is T & Record<K, S | unknown> { - return Object.prototype.hasOwnProperty.call(o, k); +function isAxiosError(e: unknown): e is AxiosError { + if (typeof(e) !== "object" || e === null) { + return false; + } + if (!hasProperty(e, "isAxiosError", "boolean")) { + return false; + } + return e.isAxiosError; } export class API { private cookie = ""; - private readonly config = config; - constructor() { + /** + * This controls the alert levels that get logged - levels not in this set + * are not logged + */ + private readonly alertLevels = new Set<AlertLevel>(["warning", "error", "info"]); + /** + * Stores login information for the admin-level user. + */ + private readonly loginInfo: { + password: string; + username: string; + }; + /** + * The URL base used for the Traffic Ops API. + * + * Trailing `/` is guaranteed. + * + * @example + * "https://localhost:6443/api/4.0/" + */ + private readonly apiURL: string; + + /** + * @param cfg The testing configuration. + */ + constructor(cfg: TestingConfig) { axios.defaults.headers.common['Accept'] = 'application/json' axios.defaults.headers.common['Authorization'] = 'No-Auth' axios.defaults.headers.common['Content-Type'] = 'application/json' axios.defaults.httpsAgent = new Agent({ rejectUnauthorized: false }) + if (cfg.alertLevels) { + this.alertLevels = new Set(cfg.alertLevels); + } + this.loginInfo = cfg.login; + this.apiURL = cfg.apiUrl.endsWith("/") ? cfg.apiUrl : `${cfg.apiUrl}/`; } /** @@ -86,18 +116,90 @@ export class API { * @throws {Error} when login fails, or when Traffic Ops doesn't return a cookie. */ public async Login(): Promise<AxiosResponse<unknown>> { - const response = await axios({ - method: 'post', - url: this.config.params.apiUrl + '/user/login', - data: { - u: this.config.params.login.username, - p: this.config.params.login.password - } - }); - this.cookie = await response.headers["set-cookie"][0]; + const data = { + p: this.loginInfo.password, + u: this.loginInfo.username, + } + const response = await this.getResponse("post", "/user/login", data); + const h = response.headers as object; + if (!hasProperty(h, "set-cookie", "Array") || h["set-cookie"].length < 1) { + throw new Error("Traffic Ops response did not set a cookie"); + } + const cookie = await h["set-cookie"][0]; + if (typeof(cookie) !== "string") { + throw new Error(`non-string cookie: ${cookie}`); + } + this.cookie = cookie; return response } + /** + * Retrieves a response from the API. + * + * Alerts will be logged if they are found - even if an error occurs and is + * thrown. + * + * @param method The request method to use. + * @param path The path to request, relative to the configured TO API URL. + * @returns The server's response. + * @throws {unknown} when the request fails for any reason. If an error + * response was returned from the API, it was logged, so there's no need to + * dig into the properties of these errors, really. + */ + private async getResponse(method: "get" | "delete", path: string): Promise<AxiosResponse>; + /** + * Retrieves a response from the API. + * + * Alerts will be logged if they are found - even if an error occurs and is + * thrown. + * + * @param method The request method to use. + * @param path The path to request, relative to the configured TO API URL. + * @param data Data to send in the body of the POST request. + * @returns The server's response. + * @throws {unknown} when the request fails for any reason. If an error + * response was returned from the API, it was logged, so there's no need to + * dig into the properties of these errors, really. + */ + private async getResponse(method: "post", path: string, data: unknown): Promise<AxiosResponse>; + private async getResponse(method: "post" | "get" | "delete", path: string, data?: unknown): Promise<AxiosResponse> { + if (method === "post" && data === undefined) { + throw new TypeError("request body must be given for POST requests"); + } + + const url = `${this.apiURL}${path.replace(/^\/+/g, "")}`; + const conf = { + method, + url, + headers: { Cookie: this.cookie }, + data + } + + let throwable; + let resp: AxiosResponse<unknown>; + try { + resp = await axios(conf); + } catch(e) { + if (!isAxiosError(e) || !e.response) { + console.debug("non-axios error or axios error with no response thrown"); + throw e; + } + resp = e.response; + throwable = e; + } + if (typeof(resp.data) === "object" && resp.data !== null && hasProperty(resp.data, "alerts", "Array")) { + for (const a of resp.data.alerts) { + if (isAlert(a) && this.alertLevels.has(a.level)) { + logAlert(a, `${method.toUpperCase()} ${url} (${resp.status} ${resp.statusText}):`); + } + } + } + if (throwable) { + throw throwable; + } + return resp; + } + public async SendRequest<T extends IDData>(route: string, method: string, data: T): Promise<void> { let response this.Randomize(data) @@ -117,19 +219,10 @@ export class API { switch (method) { case "post": - response = await axios({ - method: method, - url: this.config.params.apiUrl + route, - headers: { Cookie: this.cookie}, - data: data - }); + response = await this.getResponse("post", route, data); break; case "get": - response = await axios({ - method: method, - url: this.config.params.apiUrl + route, - headers: { Cookie: this.cookie}, - }); + response = await this.getResponse("get", route); break; case "delete": if (!data.route) { @@ -147,11 +240,7 @@ export class API { if((data.route).includes('/service_categories/')){ data.route = data.route + randomize } - response = await axios({ - method: method, - url: this.config.params.apiUrl + data.route, - headers: { Cookie: this.cookie}, - }); + response = await this.getResponse("delete", data.route); break; default: throw new Error(`unrecognized request method: '${method}'`); @@ -171,11 +260,7 @@ export class API { } for (const request of data.getRequest) { const query = `?${encodeURIComponent(request.queryKey)}=${encodeURIComponent(request.queryValue)}${randomize}`; - const response = await axios({ - method: 'get', - url: this.config.params.apiUrl + request.route + query, - headers: { Cookie: this.cookie}, - }); + const response = await this.getResponse("get", request.route + query) if (response.status == 200) { if(request.hasOwnProperty('isArray')){ @@ -193,7 +278,7 @@ export class API { return null } - public Randomize(data: object): void { + public Randomize(data: object): void { if (hasProperty(data, "fullName")) { if (hasProperty(data, "email")) { data.email = data.fullName + randomize + data.email; @@ -231,23 +316,19 @@ export class API { if(hasProperty(data, 'domainName')) { data.domainName = data.domainName + randomize; } - if(hasProperty(data, 'nodes')){ - if (data.nodes instanceof Array) { - data.nodes.map(i => { - if (typeof(i) === "object" && i !== null && hasProperty(i, "cachegroup")) { - i.cachegroup = i.cachegroup + randomize; - } - }); - } + if(hasProperty(data, 'nodes', "Array")){ + data.nodes.map(i => { + if (typeof(i) === "object" && i !== null && hasProperty(i, "cachegroup")) { + i.cachegroup = i.cachegroup + randomize; + } + }); } - if(hasProperty(data, 'interfaces')){ - if (data.interfaces instanceof Array) { - const ipv6 = randomIpv6(); - for (const inf of data.interfaces) { - if (typeof(inf) === "object" && inf !== null && hasProperty(inf, "ipAddresses") && inf.ipAddresses instanceof Array) { - for (const ip of inf.ipAddresses) { - ip.address = ipv6.toString(); - } + if(hasProperty(data, 'interfaces', "Array")){ + const ipv6 = randomIpv6(); + for (const inf of data.interfaces) { + if (typeof(inf) === "object" && inf !== null && hasProperty(inf, "ipAddresses", "Array")) { + for (const ip of inf.ipAddresses) { + (ip as Record<"address", string>).address = ipv6.toString(); } } } @@ -273,7 +354,7 @@ export class API { } } } else if (response.status == undefined) { - throw new Error(`Error requesting ${this.config.params.apiUrl}: ${response}`); + throw new Error(`Error requesting ${this.apiURL}: ${response}`); } else { throw new Error(`Login failed: Response Status: '${response.statusText}'' Response Data: '${response.data}'`); } diff --git a/traffic_portal/test/integration/CommonUtils/index.ts b/traffic_portal/test/integration/CommonUtils/index.ts new file mode 100644 index 0000000..1cd793c --- /dev/null +++ b/traffic_portal/test/integration/CommonUtils/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from "./API"; +export * from "./utils"; diff --git a/traffic_portal/test/integration/CommonUtils/utils.ts b/traffic_portal/test/integration/CommonUtils/utils.ts new file mode 100644 index 0000000..5b3b6ac --- /dev/null +++ b/traffic_portal/test/integration/CommonUtils/utils.ts @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * hasProperty checks whether some variable passed as `o` has the string + * property `k`. + * + * @example + * hasProperty({}, "id", "string"); // returns false + * hasProperty({id: 8}, "id", "string"); // returns false + * hasProperty({id: undefined}, "id", "string"); // returns false + * + * @param o The object to check. + * @param k The key for which to check in the object. + * @param type Specifies that `o.k` should be a string. + * @returns Whether or not `o` has the string property `k`. + */ +export function hasProperty<T extends object, K extends PropertyKey>(o: T, k: K, type: "string"): o is T & Record<K, string>; +/** + * hasProperty checks whether some variable passed as `o` has the number + * property `k`. + * + * @example + * hasProperty({}, "id", "number"); // returns false + * hasProperty({id: 8}, "id", "number"); // returns true + * hasProperty({id: undefined}, "id", "number"); // returns false + * + * @param o The object to check. + * @param k The key for which to check in the object. + * @param type Specifies that `o.k` should be a number. + * @returns Whether or not `o` has the number property `k`. + */ +export function hasProperty<T extends object, K extends PropertyKey>(o: T, k: K, type: "number"): o is T & Record<K, number>; +/** + * hasProperty checks whether some variable passed as `o` has the boolean + * property `k`. + * + * @example + * hasProperty({}, "id", "boolean"); // returns false + * hasProperty({id: 8}, "id", "boolean"); // returns false + * hasProperty({id: undefined}, "id", "boolean"); // returns false + * hasProperty({id: true}, "id", "boolean"); // returns true + * + * @param o The object to check. + * @param k The key for which to check in the object. + * @param type Specifies that `o.k` should be a boolean. + * @returns Whether or not `o` has the boolean property `k`. + */ +export function hasProperty<T extends object, K extends PropertyKey>(o: T, k: K, type: "boolean"): o is T & Record<K, boolean>; +/** + * hasProperty checks whether some variable passed as `o` has the Array property + * `k`. + * + * @example + * hasProperty({}, "id", "Array"); // returns false + * hasProperty({id: 8}, "id", "Array"); // returns false + * hasProperty({id: undefined}, "id", "Array"); // returns false + * hasProperty({id: []}, "id", "Array"); // returns true + * hasProperty({id: [undefined, null, -7, true]}, "id", "Array"); // returns true + * + * @param o The object to check. + * @param k The key for which to check in the object. + * @param type Specifies that `o.k` should be a (potentially non-homogenous) + * Array. + * @returns Whether or not `o` has the Array property `k`. + */ +export function hasProperty<T extends object, K extends PropertyKey>(o: T, k: K, type: "Array"): o is T & Record<K, Array<unknown>>; +/** + * hasProperty checks, generically, whether some variable passed as `o` has the + * property `k`. + * + * @example + * hasProperty({}, "id"); // returns false + * hasProperty({id: 8}, "id"); // returns true + * hasProperty({id: undefined}, "id"); // returns true + * + * @param o The object to check. + * @param k The key for which to check in the object. + * @returns Whether or not `o` has the property `k`. + */ +export function hasProperty<T extends object, K extends PropertyKey>(o: T, k: K): o is T & Record<K, unknown>; +/** + * hasProperty checks, generically, whether some variable passed as `o` has the + * property `k`. + * + * @example + * hasProperty({}, "id"); // returns false + * hasProperty({id: 8}, "id"); // returns true + * hasProperty({id: undefined}, "id"); // returns true + * hasProperty({id: 8}, "id", "number"); // returns true + * hasProperty({id: undefined}, "id", "number"); // returns false + * hasProperty({id: 8}, "id", "string"); // returns false + * + * @param o The object to check. + * @param k The key for which to check in the object. + * @param type Optionally specify a type to check for. + * @returns Whether or not `o` has the property `k`. + */ +export function hasProperty<T extends object, K extends PropertyKey, S>(o: T, k: K, type?: "string" | "number" | "boolean" | "Array"): o is T & Record<K, S> { + if (!Object.prototype.hasOwnProperty.call(o, k)) { + return false; + } + if (!type) { + return true; + } + const val = (o as Record<K, unknown>)[k]; + switch (type) { + case "string": + return typeof(val) === "string"; + case "number": + return typeof(val) === "number"; + case "boolean": + return typeof(val) === "boolean"; + case "Array": + return val instanceof Array; + } +} diff --git a/traffic_portal/test/integration/config.model.ts b/traffic_portal/test/integration/config.model.ts new file mode 100644 index 0000000..9895820 --- /dev/null +++ b/traffic_portal/test/integration/config.model.ts @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { hasProperty } from "./CommonUtils/utils"; + +/** Possible levels of TO Alerts */ +export type AlertLevel = "success" | "info" | "warning" | "error"; + +/** + * Checks whether an object is a valid Alert level. + * + * @param s The object to check. + * @returns Whether or not `s` is an AlertLevel. + */ +export function isAlertLevel(s: unknown): s is AlertLevel { + if (typeof(s) !== "string") { + return false; + } + switch (s) { + case "success": + case "info": + case "warning": + case "error": + return true; + } + return false; +} + +/** TO API Alerts */ +export interface Alert { + level: AlertLevel; + text: string; +} + +/** + * Checks whether an object is an Alert like the ones TO normally returns. + * + * @param a The object to check. + * @returns Whether or not `a` is an Alert. + */ +export function isAlert(a: unknown): a is Alert { + if (typeof(a) !== "object" || a === null) { + return false; + } + if (!hasProperty(a, "level") || !hasProperty(a, "text", "string")) { + return false; + } + return isAlertLevel(a.level); +} + +/** + * Logs an alert to the appropriate console stream based on its `level`. + * + * @param a The Alert to log. + * @param prefix Optional prefix for the log message + */ +export function logAlert(a: Alert, prefix?: string): void { + let logfunc; + let pre = (prefix ?? "").trimStart(); + switch (a.level) { + case "success": + logfunc = console.log; + pre = `SUCCESS: ${pre}`; + break; + case "info": + logfunc = console.info + pre = `INFO: ${pre}`; + break; + case "warning": + logfunc = console.warn + pre = `WARN: ${pre}`; + break; + case "error": + logfunc = console.error + pre = `ERROR: ${pre}`; + break; + } + logfunc(pre, a.text); +} + +/** TestingConfig is the type of a testing configuration. */ +export interface TestingConfig { + /** This is login information for a user with admin-level permissions. */ + readonly login: { + readonly password: string; + readonly username: string; + }; + /** The URL at which the Traffic Ops API can be accessed. */ + readonly apiUrl: string; + /** The URL at which Traffic Portal is served - root path. */ + readonly baseUrl: string; + /** Logging alert levels that are enabled. */ + readonly alertLevels?: Array<AlertLevel>; +} + +/** + * Checks if a passed object is a valid testing configuration. + * + * @param c The object to check. + * @returns `true`, always. When the check fails, it throws an error that + * explains why. + */ +export function isTestingConfig(c: unknown): c is TestingConfig { + if (typeof(c) !== "object") { + throw new Error(`testing configuration must be an object, not a '${typeof(c)}'`); + } + if (c === null) { + throw new Error("testing configuration must be an object, not 'null'"); + } + + if (!hasProperty(c, "login") || typeof(c.login) !== "object" || c.login === null) { + throw new Error("missing or invalid 'login' property"); + } + if (!hasProperty(c.login, "password", "string") || !hasProperty(c.login, "username", "string")) { + throw new Error("'login' property has missing and/or invalid 'password' and/or 'username' property(ies)"); + } + if (c.login.username === "" || c.login.password === "") { + throw new Error("neither 'login.username' nor 'login.password' may be blank"); + } + if (!hasProperty(c, "apiUrl", "string")) { + throw new Error("missing or invalid 'apiUrl' property"); + } + try { + new URL(c.apiUrl); + } catch (e) { + throw new Error(`'apiUrl' is not a valid URL: ${c.apiUrl}`); + } + let baseURL; + if (!hasProperty(c, "baseUrl", "string")) { + throw new Error("missing or invalid 'baseUrl' property"); + } + try { + baseURL = new URL(c.baseUrl); + } catch (e) { + throw new Error(`'baseUrl' is not a valid URL: ${c.baseUrl}`); + } + if (baseURL.pathname !== "/") { + throw new Error("'baseUrl' must be a root path"); + } + if (!hasProperty(c, "alertLevels")) { + return true; + } + if (!(c.alertLevels instanceof Array)) { + throw new Error("'alertLevels' must be an array"); + } + if (!c.alertLevels.every(isAlertLevel)) { + throw new Error(`invalid alert levels: ${c.alertLevels}`); + } + return true; +} diff --git a/traffic_portal/test/integration/config.ts b/traffic_portal/test/integration/config.ts index 96ccc55..388d14b 100644 --- a/traffic_portal/test/integration/config.ts +++ b/traffic_portal/test/integration/config.ts @@ -23,9 +23,10 @@ import { Config, browser } from 'protractor'; import { JUnitXmlReporter } from 'jasmine-reporters'; import HtmlReporter from "protractor-beautiful-reporter"; -import { API } from './CommonUtils/API'; +import { API } from './CommonUtils'; import * as conf from "./config.json" import { prerequisites } from "./Data"; +import { isTestingConfig } from "./config.model"; const downloadsPath = resolve('Downloads'); export const randomize = Math.random().toString(36).substring(3, 7); @@ -37,6 +38,23 @@ if (config.capabilities) { } else { config.capabilities = {chromeOptions: {prefs: {download: {default_directory: downloadsPath}}}}; } + +if (!config.params) { + throw new Error("no testing parameters provided - cannot proceed"); +} + +try { + if (!isTestingConfig(config.params)) { + throw new Error(); + } +} catch (e) { + const msg = e instanceof Error ? e.message : String(e); + throw new Error(`invalid testing params: ${msg}`); +} + +export const testingConfig = config.params; +export const api = new API(testingConfig); + config.onPrepare = async function () { await browser.waitForAngularEnabled(true); await browser.driver.manage().window().maximize(); @@ -66,6 +84,5 @@ config.onPrepare = async function () { }).getJasmine2Reporter()); } - const api = new API(); await api.UseAPI(prerequisites); } diff --git a/traffic_portal/test/integration/specs/ASNs.spec.ts b/traffic_portal/test/integration/specs/ASNs.spec.ts index a3761c8..3359931 100644 --- a/traffic_portal/test/integration/specs/ASNs.spec.ts +++ b/traffic_portal/test/integration/specs/ASNs.spec.ts @@ -20,11 +20,10 @@ import { browser } from 'protractor'; import { LoginPage } from '../PageObjects/LoginPage.po'; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { ASNsPage } from '../PageObjects/ASNs.po'; import { ASNs } from "../Data"; -const api = new API(); const loginPage = new LoginPage(); const topNavigation = new TopNavigationPage(); const asnsPage = new ASNsPage(); diff --git a/traffic_portal/test/integration/specs/Coordinates.spec.ts b/traffic_portal/test/integration/specs/Coordinates.spec.ts index cb6c924..a617130 100644 --- a/traffic_portal/test/integration/specs/Coordinates.spec.ts +++ b/traffic_portal/test/integration/specs/Coordinates.spec.ts @@ -20,11 +20,10 @@ import { browser } from 'protractor'; import { LoginPage } from '../PageObjects/LoginPage.po' import { CoordinatesPage } from '../PageObjects/CoordinatesPage.po'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; import { coordinates } from "../Data"; -const api = new API(); const loginPage = new LoginPage(); const topNavigation = new TopNavigationPage(); const coordinatesPage = new CoordinatesPage(); diff --git a/traffic_portal/test/integration/specs/DeliveryServices.spec.ts b/traffic_portal/test/integration/specs/DeliveryServices.spec.ts index 2ba4a69..7c92742 100644 --- a/traffic_portal/test/integration/specs/DeliveryServices.spec.ts +++ b/traffic_portal/test/integration/specs/DeliveryServices.spec.ts @@ -21,10 +21,9 @@ import { browser } from 'protractor'; import { LoginPage } from '../PageObjects/LoginPage.po' import { DeliveryServicePage } from '../PageObjects/DeliveryServicePage.po'; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { deliveryservices } from "../Data/deliveryservices"; -const api = new API(); const topNavigation = new TopNavigationPage(); const loginPage = new LoginPage(); const deliveryservicesPage = new DeliveryServicePage(); @@ -66,7 +65,7 @@ deliveryservices.tests.forEach(async deliveryservicesData => { expect(await deliveryservicesPage.AssignServerToDeliveryService(assignserver)).toBe(true); await deliveryservicesPage.OpenDeliveryServicePage(); } - + ) }) deliveryservicesData.assignrequiredcapabilities.forEach(assignrc => { diff --git a/traffic_portal/test/integration/specs/Divisions.spec.ts b/traffic_portal/test/integration/specs/Divisions.spec.ts index 5028c4e..27f5342 100644 --- a/traffic_portal/test/integration/specs/Divisions.spec.ts +++ b/traffic_portal/test/integration/specs/Divisions.spec.ts @@ -20,11 +20,11 @@ import { browser } from 'protractor'; import { LoginPage } from '../PageObjects/LoginPage.po'; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { DivisionsPage } from '../PageObjects/Divisions.po'; import { divisions } from "../Data"; -const api = new API(); + const loginPage = new LoginPage(); const topNavigation = new TopNavigationPage(); const divisionsPage = new DivisionsPage(); diff --git a/traffic_portal/test/integration/specs/Jobs.spec.ts b/traffic_portal/test/integration/specs/Jobs.spec.ts index ba7b8c1..9a0973a 100644 --- a/traffic_portal/test/integration/specs/Jobs.spec.ts +++ b/traffic_portal/test/integration/specs/Jobs.spec.ts @@ -18,13 +18,12 @@ */ import { browser } from 'protractor'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { LoginPage } from '../PageObjects/LoginPage.po'; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; import { JobsPage } from '../PageObjects/Jobs.po' import { jobs } from '../Data/jobs'; -const api = new API(); const loginPage = new LoginPage(); const topNavigation = new TopNavigationPage(); const jobsPage = new JobsPage(); diff --git a/traffic_portal/test/integration/specs/Origins.spec.ts b/traffic_portal/test/integration/specs/Origins.spec.ts index 20ead35..974be82 100644 --- a/traffic_portal/test/integration/specs/Origins.spec.ts +++ b/traffic_portal/test/integration/specs/Origins.spec.ts @@ -21,10 +21,9 @@ import { browser } from 'protractor'; import { LoginPage } from '../PageObjects/LoginPage.po' import { OriginsPage } from '../PageObjects/OriginsPage.po'; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { origins } from "../Data"; -const api = new API(); const loginPage = new LoginPage(); const topNavigation = new TopNavigationPage(); const originsPage = new OriginsPage(); diff --git a/traffic_portal/test/integration/specs/Parameters.spec.ts b/traffic_portal/test/integration/specs/Parameters.spec.ts index 95b8c59..04c6c6c 100644 --- a/traffic_portal/test/integration/specs/Parameters.spec.ts +++ b/traffic_portal/test/integration/specs/Parameters.spec.ts @@ -20,11 +20,10 @@ import { browser } from 'protractor'; import { LoginPage } from '../PageObjects/LoginPage.po' import { ParametersPage } from '../PageObjects/ParametersPage.po'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; import { parameters } from "../Data"; -const api = new API(); const loginPage = new LoginPage(); const topNavigation = new TopNavigationPage(); const parametersPage = new ParametersPage(); @@ -63,7 +62,7 @@ parameters.tests.forEach(async parametersData => { expect(await parametersPage.ToggleTableColumn(toggle.Name)).toBe(true); await parametersPage.OpenParametersPage(); } - + }); }) parametersData.add.forEach(add => { diff --git a/traffic_portal/test/integration/specs/PhysLocations.spec.ts b/traffic_portal/test/integration/specs/PhysLocations.spec.ts index 24bed71..c221fb8 100644 --- a/traffic_portal/test/integration/specs/PhysLocations.spec.ts +++ b/traffic_portal/test/integration/specs/PhysLocations.spec.ts @@ -20,11 +20,10 @@ import { browser } from 'protractor'; import { LoginPage } from '../PageObjects/LoginPage.po' import { PhysLocationsPage } from '../PageObjects/PhysLocationsPage.po'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; import { physLocations } from "../Data"; -const api = new API(); const loginPage = new LoginPage(); const topNavigation = new TopNavigationPage(); const physlocationsPage = new PhysLocationsPage(); diff --git a/traffic_portal/test/integration/specs/Profiles.spec.ts b/traffic_portal/test/integration/specs/Profiles.spec.ts index 5878b85..f415db0 100644 --- a/traffic_portal/test/integration/specs/Profiles.spec.ts +++ b/traffic_portal/test/integration/specs/Profiles.spec.ts @@ -21,10 +21,9 @@ import { browser } from 'protractor' import { LoginPage } from '../PageObjects/LoginPage.po' import { ProfilesPage } from '../PageObjects/ProfilesPage.po'; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { profiles } from "../Data"; -const api = new API(); const loginPage = new LoginPage(); const topNavigation = new TopNavigationPage(); const profilesPage = new ProfilesPage(); diff --git a/traffic_portal/test/integration/specs/Regions.spec.ts b/traffic_portal/test/integration/specs/Regions.spec.ts index 4380c7a..82b768e 100644 --- a/traffic_portal/test/integration/specs/Regions.spec.ts +++ b/traffic_portal/test/integration/specs/Regions.spec.ts @@ -20,11 +20,10 @@ import { browser } from 'protractor'; import { LoginPage } from '../PageObjects/LoginPage.po'; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { RegionsPage } from '../PageObjects/RegionsPage.po'; import { regions } from "../Data"; -const api = new API(); const loginPage = new LoginPage(); const topNavigation = new TopNavigationPage(); const regionsPage = new RegionsPage(); diff --git a/traffic_portal/test/integration/specs/ServerServerCapabilities.spec.ts b/traffic_portal/test/integration/specs/ServerServerCapabilities.spec.ts index c3efb2f..d21c6d9 100644 --- a/traffic_portal/test/integration/specs/ServerServerCapabilities.spec.ts +++ b/traffic_portal/test/integration/specs/ServerServerCapabilities.spec.ts @@ -22,10 +22,9 @@ import { LoginPage } from '../PageObjects/LoginPage.po' import { ServerCapabilitiesPage } from '../PageObjects/ServerCapabilitiesPage.po'; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; import { ServersPage } from '../PageObjects/ServersPage.po'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { serverServerCapabilities } from "../Data"; -const api = new API(); const loginPage = new LoginPage(); const topNavigation = new TopNavigationPage(); const serverCapabilitiesPage = new ServerCapabilitiesPage(); diff --git a/traffic_portal/test/integration/specs/Servers.spec.ts b/traffic_portal/test/integration/specs/Servers.spec.ts index 536a166..867397e 100644 --- a/traffic_portal/test/integration/specs/Servers.spec.ts +++ b/traffic_portal/test/integration/specs/Servers.spec.ts @@ -20,11 +20,10 @@ import { browser } from 'protractor'; import { LoginPage } from '../PageObjects/LoginPage.po' import { ServersPage } from '../PageObjects/ServersPage.po'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; import { servers } from "../Data"; -const api = new API(); const loginPage = new LoginPage(); const topNavigation = new TopNavigationPage(); const serversPage = new ServersPage(); diff --git a/traffic_portal/test/integration/specs/ServiceCategories.spec.ts b/traffic_portal/test/integration/specs/ServiceCategories.spec.ts index 131658d..fdbcd6b 100644 --- a/traffic_portal/test/integration/specs/ServiceCategories.spec.ts +++ b/traffic_portal/test/integration/specs/ServiceCategories.spec.ts @@ -20,11 +20,10 @@ import { browser } from 'protractor'; import { LoginPage } from '../PageObjects/LoginPage.po'; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { ServiceCategoriesPage } from '../PageObjects/ServiceCategories.po'; import { serviceCategories } from "../Data"; -const api = new API(); const loginPage = new LoginPage(); const topNavigation = new TopNavigationPage(); const serviceCategoriesPage = new ServiceCategoriesPage(); diff --git a/traffic_portal/test/integration/specs/Statuses.spec.ts b/traffic_portal/test/integration/specs/Statuses.spec.ts index bb3ac1b..5f070c3 100644 --- a/traffic_portal/test/integration/specs/Statuses.spec.ts +++ b/traffic_portal/test/integration/specs/Statuses.spec.ts @@ -20,11 +20,10 @@ import { browser } from 'protractor'; import { LoginPage } from '../PageObjects/LoginPage.po'; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { StatusesPage } from '../PageObjects/Statuses.po' import { statuses } from "../Data"; -const api = new API(); const loginPage = new LoginPage(); const topNavigation = new TopNavigationPage(); const statusesPage = new StatusesPage(); diff --git a/traffic_portal/test/integration/specs/Topologies.spec.ts b/traffic_portal/test/integration/specs/Topologies.spec.ts index b9d9ccb..89678ab 100644 --- a/traffic_portal/test/integration/specs/Topologies.spec.ts +++ b/traffic_portal/test/integration/specs/Topologies.spec.ts @@ -20,11 +20,10 @@ import { browser } from 'protractor'; import { LoginPage } from '../PageObjects/LoginPage.po'; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { TopologiesPage } from '../PageObjects/TopologiesPage.po'; import { topologies } from "../Data/topologies"; -const api = new API(); const loginPage = new LoginPage(); const topologiesPage = new TopologiesPage(); const topNavigation = new TopNavigationPage(); @@ -56,7 +55,7 @@ topologies.tests.forEach(async topologiesData =>{ it('can logout', async function(){ expect(await topNavigation.Logout()).toBeTruthy(); }) - + }) }) }) @@ -66,4 +65,3 @@ describe('Clean Up API for Topologies Test', () => { await api.UseAPI(topologies.cleanup); }); }); - diff --git a/traffic_portal/test/integration/specs/Types.spec.ts b/traffic_portal/test/integration/specs/Types.spec.ts index 580635d..b9fb5d6 100644 --- a/traffic_portal/test/integration/specs/Types.spec.ts +++ b/traffic_portal/test/integration/specs/Types.spec.ts @@ -20,11 +20,10 @@ import { browser } from 'protractor'; import { LoginPage } from '../PageObjects/LoginPage.po'; import { TopNavigationPage } from '../PageObjects/TopNavigationPage.po'; -import { API } from '../CommonUtils/API'; +import { api } from "../config"; import { TypesPage } from '../PageObjects/Types.po' import { types } from "../Data"; -const api = new API(); const loginPage = new LoginPage(); const topNavigation = new TopNavigationPage(); const typesPage = new TypesPage();
