Unverified Commit 5d46462b authored by Martin Lowe's avatar Martin Lowe 🇨🇦 Committed by GitHub
Browse files

Add unit tests for various parts of API (#126)

* Adding tests for OIDC and membership form

* Add JSON schema generation script

* Fix existing membership tests, add additional endpoint tests

Also includes formal upgrade to snapshot 0.2 of Commons API. Fixes a few
issues discovered while testing where API results diverge from expected
output from openAPI spec.

* Fix broken service, wrong inject imported

* Add CORS + SameSite security to API

* Fix break from org service not binding properly in testing, api base

* Fix docker build for JVM to use new 1.13 standard dockerfile contents

* Add test assertions to ensure Accept and ContentType formats are
followed

* Remove hardcoded user IDs from dataloader
parent 736a3a04
......@@ -77,3 +77,6 @@ docker.secret.properties
#environment variables
.env
#Test resources
src/test/resources/schemas
\ No newline at end of file
......@@ -9,6 +9,7 @@ clean:;
install-react:;
yarn --cwd src/main/www install --frozen-lockfile
validate-spec: install-react;
yarn --cwd src/main/www generate-json-schema
yarn --cwd src/main/www test-spec
generate-cert:;
rm -rf certs && mkdir -p certs
......
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.eclipsefoundation</groupId>
<artifactId>react-container</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<eclipse-api-version>0.1-SNAPSHOT</eclipse-api-version>
<eclipse-api-version>0.2-SNAPSHOT</eclipse-api-version>
<surefire-plugin.version>2.22.1</surefire-plugin.version>
<maven.compiler.target>11</maven.compiler.target>
<quarkus.platform.version>1.9.1.Final</quarkus.platform.version>
<quarkus.platform.version>1.13.7.Final</quarkus.platform.version>
<maven.compiler.source>11</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.parameters>true</maven.compiler.parameters>
<quarkus-plugin.version>1.9.1.Final</quarkus-plugin.version>
<quarkus-plugin.version>1.13.7.Final</quarkus-plugin.version>
<compiler-plugin.version>3.8.1</compiler-plugin.version>
<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
</properties>
......@@ -93,6 +92,26 @@
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-oidc-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
......@@ -118,6 +137,9 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<includes>
<include>**/*Test.java</include>
</includes>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
......
......@@ -3,31 +3,29 @@
#
# Before building the container image run:
#
# mvn package
# ./mvnw package
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/react-container-jvm .
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/code-with-quarkus-jvm .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/react-container-jvm
# docker run -i --rm -p 8080:8080 quarkus/code-with-quarkus-jvm
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5050
#
# Then run the container using :
# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005
#
# docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/react-container-jvm
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/code-with-quarkus-jvm
#
###
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3
ARG JAVA_PACKAGE=java-11-openjdk-headless
ARG RUN_JAVA_VERSION=1.3.8
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
# Install java and the run-java script
# Also set up permissions for user `1001`
RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
......@@ -40,15 +38,17 @@ RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
&& curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
&& chown 1001 /deployments/run-java.sh \
&& chmod 540 /deployments/run-java.sh \
&& echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security
&& echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security
# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
COPY target/lib/* /deployments/lib/
COPY target/*-runner.jar /deployments/app.jar
# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/
COPY --chown=1001 target/quarkus-app/*.jar /deployments/
COPY --chown=1001 target/quarkus-app/app/ /deployments/app/
COPY --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8090
USER 1001
ENTRYPOINT [ "/deployments/run-java.sh" ]
\ No newline at end of file
ENTRYPOINT [ "/deployments/run-java.sh" ]
......@@ -80,7 +80,7 @@ public class DataLoader {
List<MembershipForm> forms = new ArrayList<>(config.getFormCount());
for (int i = 0; i < config.getFormCount(); i++) {
MembershipForm mf = new MembershipForm();
String userID = config.getUserIDs().get(r.nextInt(config.getUserIDs().size()));
String userID = config.getUserIds().get(r.nextInt(config.getUserIds().size()));
mf.setUserID(userID);
mf.setMembershipLevel(config.getMembershipLevels().get(r.nextInt(config.getMembershipLevels().size())));
......
......@@ -13,8 +13,10 @@ package org.eclipsefoundation.react.bootstrap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import io.quarkus.arc.config.ConfigProperties;
/**
......@@ -29,7 +31,7 @@ public class DataLoaderConfig {
private boolean enabled = false;
private List<String> dataLoaderProfiles = Arrays.asList("dev", "staging");
private Integer formCount = 25;
private List<String> userIDs = Arrays.asList("malowe", "cguindon", "epoirier", "zhoufang");
private List<String> userIds = Collections.emptyList();
private List<String> workingGroups = Arrays.asList("internet-things-iot", "jakarta-ee", "cloud-tools-development");
private List<String> membershipLevels = Arrays.asList("strategic", "contributing", "associate", "committer");
private List<String> participationLevels = Arrays.asList("platinum", "gold", "silver", "associate");
......@@ -79,15 +81,15 @@ public class DataLoaderConfig {
/**
* @return the userIDs
*/
public List<String> getUserIDs() {
return new ArrayList<>(userIDs);
public List<String> getUserIds() {
return new ArrayList<>(userIds);
}
/**
* @param userIDs the userIDs to set
*/
public void setUserIDs(List<String> userIDs) {
this.userIDs = new ArrayList<>(userIDs);
public void setUserIds(List<String> userIDs) {
this.userIds = new ArrayList<>(userIDs);
}
/**
......
......@@ -15,6 +15,7 @@ import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.json.bind.annotation.JsonbProperty;
import javax.json.bind.annotation.JsonbTransient;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
......@@ -44,6 +45,7 @@ public class FormOrganization extends BareNode implements TargetedClone<FormOrga
@GenericGenerator(name = "system-uuid", strategy = "uuid")
private String id;
private String legalName;
@JsonbProperty("twitter")
private String twitterHandle;
// form entity
......
......@@ -102,9 +102,11 @@ public class ContactsResource extends AbstractRESTResource {
List<Contact> results = dao.get(new RDBMSQuery<>(wrap, filters.get(Contact.class), params));
if (results == null) {
return Response.serverError().build();
} else if (results.isEmpty()) {
return Response.status(404).build();
}
// return the results as a response
return Response.ok(results).build();
return Response.ok(results.get(0)).build();
}
@PUT
......
......@@ -112,9 +112,11 @@ public class FormOrganizationsResource extends AbstractRESTResource {
List<FormOrganization> results = dao.get(new RDBMSQuery<>(wrap, filters.get(FormOrganization.class), params));
if (results == null) {
return Response.serverError().build();
} else if (results.isEmpty()) {
return Response.status(404).build();
}
// return the results as a response
return Response.ok(results).build();
return Response.ok(results.get(0)).build();
}
@PUT
......
......@@ -47,7 +47,6 @@ import io.quarkus.security.Authenticated;
@Consumes(MediaType.APPLICATION_JSON)
public class MembershipFormResource extends AbstractRESTResource {
@GET
public Response getAll(@HeaderParam(value = CSRFHelper.CSRF_HEADER_NAME) String csrf) {
// ensure csrf
......@@ -83,9 +82,11 @@ public class MembershipFormResource extends AbstractRESTResource {
List<MembershipForm> results = dao.get(new RDBMSQuery<>(wrap, filters.get(MembershipForm.class), params));
if (results == null) {
return Response.serverError().build();
} else if (results.isEmpty()) {
return Response.status(404).build();
}
// return the results as a response
return Response.ok(results).build();
return Response.ok(results.get(0)).build();
}
@POST
......@@ -98,6 +99,10 @@ public class MembershipFormResource extends AbstractRESTResource {
@PUT
@Path("{id}")
public Response update(@PathParam("id") String formID, MembershipForm mem) {
// make sure we have something to put
if (mem == null) {
return Response.status(500).build();
}
// check if user is allowed to modify these resources
Response r = checkAccess(formID);
if (r != null) {
......@@ -106,7 +111,8 @@ public class MembershipFormResource extends AbstractRESTResource {
mem.setUserID(ident.getPrincipal().getName());
// need to fetch ref to use attached entity
MembershipForm ref = mem.cloneTo(dao.getReference(formID, MembershipForm.class));
return Response.ok(dao.add(new RDBMSQuery<>(wrap, filters.get(MembershipForm.class)), Arrays.asList(ref))).build();
return Response.ok(dao.add(new RDBMSQuery<>(wrap, filters.get(MembershipForm.class)), Arrays.asList(ref)))
.build();
}
@DELETE
......
......@@ -15,11 +15,14 @@ import java.net.URI;
import java.net.URISyntaxException;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.eclipsefoundation.core.helper.CSRFHelper;
import io.quarkus.security.Authenticated;
import io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipal;
......@@ -44,7 +47,8 @@ public class OIDCResource extends AbstractRESTResource {
}
/**
* While OIDC plugin takes care of actual logout, a route is needed to properly reroute anon user to home page.
* While OIDC plugin takes care of actual logout, a route is needed to properly
* reroute anon user to home page.
*
* @throws URISyntaxException
*/
......@@ -57,7 +61,9 @@ public class OIDCResource extends AbstractRESTResource {
@GET
@Path("userinfo")
@Produces(MediaType.APPLICATION_JSON)
public Response getUserInfo() {
public Response getUserInfo(@HeaderParam(value = CSRFHelper.CSRF_HEADER_NAME) String csrf) {
// require CSRF to protect user info (could contain PII)
csrfHelper.compareCSRF(aud, csrf);
if (!ident.isAnonymous()) {
// cast the principal to a JWT token (which is the type for OIDC)
DefaultJWTCallerPrincipal defaultPrin = (DefaultJWTCallerPrincipal) ident.getPrincipal();
......@@ -68,20 +74,14 @@ public class OIDCResource extends AbstractRESTResource {
uiw.familyName = defaultPrin.getClaim("family_name");
return Response.ok(uiw).build();
} else {
return Response.ok().build();
return Response.noContent().build();
}
}
@GET
@Path("csrf")
public Response generateCSRF() {
if (!ident.isAnonymous()) {
aud.setCsrf(csrfHelper.getNewCSRFToken());
wrap.setHeader("csrf", aud.getCsrf());
return Response.ok().build();
} else {
return Response.status(403).build();
}
return Response.ok().build();
}
private Response redirect(String location) throws URISyntaxException {
......
......@@ -113,9 +113,12 @@ public class WorkingGroupsResource extends AbstractRESTResource {
List<FormWorkingGroup> results = dao.get(new RDBMSQuery<>(wrap, filters.get(FormWorkingGroup.class), params));
if (results == null) {
return Response.serverError().build();
} else if (results.isEmpty()) {
return Response.status(404).build();
}
// return the results as a response
return Response.ok(results).build();
return Response.ok(results.get(0)).build();
}
@PUT
......
......@@ -11,10 +11,8 @@
*/
package org.eclipsefoundation.react.service.impl;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
......@@ -33,8 +31,6 @@ import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.quarkus.runtime.Startup;
/**
* Builds a list of working group definitions from an embedded list of working
* group definitions. This is an interim solution to accelerate this project and
......@@ -42,7 +38,6 @@ import io.quarkus.runtime.Startup;
*
* @author Martin Lowe
*/
@Startup
@ApplicationScoped
public class DefaultOrganizationsService implements OrganizationsService {
public static final Logger LOGGER = LoggerFactory.getLogger(DefaultOrganizationsService.class);
......@@ -54,7 +49,7 @@ public class DefaultOrganizationsService implements OrganizationsService {
CachingService cache;
@PostConstruct
void init() throws IOException {
void init() {
LOGGER.info("Starting init of cached organizations");
Optional<List<Organization>> orgs = cache.get("all", new MultivaluedMapImpl<>(), Organization.class, () -> getAll(null));
if (orgs.isEmpty()) {
......@@ -79,13 +74,13 @@ public class DefaultOrganizationsService implements OrganizationsService {
private List<Organization> getAll(String workingGroup) {
String actualWG = workingGroup == null ? "" : workingGroup;
List<Organization> orgs = new LinkedList<>();
Set<Organization> tmp = Collections.emptySet();
Set<Organization> tmp;
int count = 1;
do {
tmp = orgAPI.organizations(actualWG, count);
orgs.addAll(tmp);
count++;
} while(!tmp.isEmpty() && tmp != null);
} while(!tmp.isEmpty());
return orgs;
}
}
......@@ -11,13 +11,9 @@
*/
package org.eclipsefoundation.react.service.impl;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
......@@ -28,8 +24,6 @@ import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.json.bind.Jsonb;
import com.google.inject.Singleton;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipsefoundation.react.model.WorkingGroup;
import org.eclipsefoundation.react.model.WorkingGroupList;
......
quarkus.log.level=INFO
quarkus.http.port=8090
quarkus.http.cors=true
quarkus.http.cors.origins=https://membership.eclipse.org,https://membership-staging.eclipse.org
quarkus.http.cors.exposed-headers=x-csrf-token
quarkus.http.cors.headers=x-csrf-token
## EXTERNAL API CLIENT CONFIG
fdn-api/mp-rest/url=https://api.eclipse.org/public
......@@ -28,13 +32,17 @@ quarkus.oidc.authentication.redirect-path=/api/login
quarkus.oidc.logout.post-logout-path=/
quarkus.oidc.logout.path=/api/logout
quarkus.resteasy.path=/api
security.csrf.enabled=true
## Recreate DB profile (easy to trigger in remote envs)
%dbfresh.quarkus.hibernate-orm.database.generation=drop-and-create
## DEV SETTINGS
%dev.quarkus.http.port=8090
%dev.quarkus.oidc.credentials.client-secret.value=4d596003-2cfe-49ba-a7cb-ea3d40bf5538
%dev.security.csrf.enabled = false
# %dev.quarkus.hibernate-orm.database.generation=drop-and-create
%dev.eclipse.dataloader.enabled=false
%dev.quarkus.http.cors.origins=https://membership.eclipse.org,https://membership-staging.eclipse.org,http://localhost:8091,http://api.rem.docker,https://www.rem.docker
%dev.quarkus.hibernate-orm.database.generation=drop-and-create
%dev.eclipse.dataloader.enabled=true
%dev.eclipse.dataloader.user-ids=malowe, cguindon, epoirier, zhoufang
%dev.eclipse.app.base-url=http://localhost:8090/
%dev.quarkus.log.category."org.eclipsefoundation".level=DEBUG
......@@ -32,7 +32,8 @@
"test": "react-scripts test",
"eject": "react-scripts eject",
"test-spec": "swagger-repo validate -b ../../../spec",
"start-spec": "swagger-repo serve -b ../../../spec -p 8999"
"start-spec": "swagger-repo serve -b ../../../spec -p 8999",
"generate-json-schema": "node src/openapi2schema.js -s ../../../spec/openapi.yaml -t ../../test/resources"
},
"eslintConfig": {
"extends": "react-app"
......@@ -51,8 +52,13 @@
]
},
"devDependencies": {
"@openapi-contrib/openapi-schema-to-json-schema": "^3.1.1",
"@stoplight/json-ref-resolver": "^3.1.2",
"decamelize": "^5.0.0",
"js-yaml": "^4.1.0",
"react-datepicker": "^3.2.2",
"react-select": "^4.1.0",
"yargs": "^17.0.1",
"yup": "^0.32.8"
},
"prettier": {
......
/**
* Copyright (c) 2021 Eclipse
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* Author: Martin Lowe <martin.lowe@eclipsefoundation.org>
*
* SPDX-License-Identifier: EPL-2.0
*/
const toJsonSchema = require('@openapi-contrib/openapi-schema-to-json-schema');
const Resolver = require('@stoplight/json-ref-resolver');
const yaml = require('js-yaml');
const fs = require('fs');
const decamelize = require('decamelize');
const args = require('yargs')
.option('s', {
alias: 'src',
desc: 'The fully qualified path to the YAML spec.',
})
.option('t', {
alias: 'target',
desc: 'The fully qualified path to write the JSON schema to',
}).argv;
if (!args.s || !args.t) {
process.exit(1);
}
run();
/**
* Generates JSON schema files for consumption of the Java tests.
*/
async function run() {
try {
// load in the openapi yaml spec as an object
const doc = yaml.load(fs.readFileSync(args.s, 'utf8'));
// resolve $refs in openapi spec
let resolvedInp = await new Resolver.Resolver().resolve(doc);
const out = toJsonSchema(resolvedInp.result);
// if folder doesn't exist, create it
if (!fs.existsSync(`${args.t}/schemas`)) {
fs.mkdirSync(`${args.t}/schemas`);
}
// for each of the schemas, generate a JSON schema file
for (let schemaName in out.components.schemas) {
fs.writeFileSync(`${args.t}/schemas/${decamelize(schemaName, { separator: '-' })}-schema.json`, JSON.stringify(out.components.schemas[schemaName]));
}
} catch (e) {
console.log(e);
}
}
......@@ -1655,6 +1655,13 @@
mkdirp "^1.0.4"
rimraf "^3.0.2"
 
"@openapi-contrib/openapi-schema-to-json-schema@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@openapi-contrib/openapi-schema-to-json-schema/-/openapi-schema-to-json-schema-3.1.1.tgz#e43b09680e652bf1b9e135db3f8648e979b76c07"
integrity sha512-FMvdhv9Jr9tULjJAQaQzhCmNYYj2vQFVnl7CGlLAImZvJal71oedXMGszpPaZTLftAk5TCHqjnirig+P6LZxug==
dependencies:
fast-deep-equal "^3.1.3"
"@pmmmwh/react-refresh-webpack-plugin@0.4.3":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.4.3.tgz#1eec460596d200c0236bf195b078a5d1df89b766"
......@@ -1714,6 +1721,60 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
 
"@stoplight/json-ref-resolver@^3.1.2":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.2.tgz#ae218cf6062d6be490c135603478415e700b52ba"
integrity sha512-SwSvhQ1jNDDLrmKSc17cEx2zZxjzF1tB4nmzaD5fn/r0oI77CNL0Rts4tAVodXVFKrWnaNWZuZmlla6x8y7eFA==
dependencies:
"@stoplight/json" "^3.10.2"
"@stoplight/path" "^1.3.2"
"@stoplight/types" "^11.9.0"
"@types/urijs" "^1.19.14"
dependency-graph "~0.10.0"
fast-memoize "^2.5.2"
immer "^8.0.1"
lodash.get "^4.4.2"
lodash.set "^4.3.2"
tslib "^2.1.0"
urijs "^1.19.5"
"@stoplight/json@^3.10.2":
version "3.15.0"
resolved "https://registry.yarnpkg.com/@stoplight/json/-/json-3.15.0.tgz#e7c2919aaa12dc4dafb43d4da71e29d101106f34"
integrity sha512-FxdmBaZyt6FZVN8F/GaGzevLxjkW1gLHC5cPeb4slMM8BIXCxKluIkGLzmb4bnkk2+4gPaYj75V28U6s0WNrbQ==
dependencies:
"@stoplight/ordered-object-literal" "^1.0.1"
"@stoplight/types" "^12.2.0"
jsonc-parser "~2.2.1"
lodash "^4.17.15"
safe-stable-stringify "^1.1"
"@stoplight/ordered-object-literal@^1.0.1":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.2.tgz#2a88a5ebc8b68b54837ac9a9ae7b779cdd862062"
integrity sha512-0ZMS/9sNU3kVo/6RF3eAv7MK9DY8WLjiVJB/tVyfF2lhr2R4kqh534jZ0PlrFB9CRXrdndzn1DbX6ihKZXft2w==
"@stoplight/path@^1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@stoplight/path/-/path-1.3.2.tgz#96e591496b72fde0f0cdae01a61d64f065bd9ede"
integrity sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==
"@stoplight/types@^11.9.0":
version "11.10.0"
resolved "https://registry.yarnpkg.com/@stoplight/types/-/types-11.10.0.tgz#60bbee770526f4de9cd1c613c7ab84c4e4674126"
integrity sha512-ffHD9i4UHS8Gsg7Ar8pKjI9B4kq7MRekE7tCROFGcFDzQhfRx9T92AduoLAtP/010XNYq23yDKpLBRPIEk8+xg==
dependencies: