Skip to content

Commit

Permalink
Add additional spec support in rest Java (#10118)
Browse files Browse the repository at this point in the history
* add support for contract spec tests
* add support for entity stake spec tests
* add support for history entities
* various bug fixes

---------

Signed-off-by: Jesse Nelson <[email protected]>
  • Loading branch information
jnels124 authored Jan 17, 2025
1 parent 1eb4ff0 commit a901bf9
Show file tree
Hide file tree
Showing 22 changed files with 399 additions and 138 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@
public class EntityIdFromStringConverter implements Converter<String, EntityId> {
@Override
public EntityId convert(String source) {
return StringUtils.hasText(source) ? EntityId.of(source) : null;
if (!StringUtils.hasText(source)) {
return null;
}

var parts = source.split("\\.");
if (parts.length == 3) {
return EntityId.of(source);
}

return EntityId.of(Long.parseLong(source));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

package com.hedera.mirror.restjava.converter;

import com.google.common.collect.BoundType;
import com.google.common.collect.Range;
import io.hypersistence.utils.hibernate.type.range.guava.PostgreSQLGuavaRangeType;
import jakarta.inject.Named;
import java.util.regex.Pattern;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
Expand All @@ -26,12 +26,8 @@

@Named
@ConfigurationPropertiesBinding
@SuppressWarnings("java:S5842") // Upper and lower bounds in regex may be empty and must still match.
public class RangeFromStringConverter implements Converter<String, Range<Long>> {
private static final String LOWER_CLOSED = "[";
private static final String UPPER_CLOSED = "]";

private static final String RANGE_REGEX = "^([\\[(])?(\\d*)?,(\\d*)?([])])$";
private static final String RANGE_REGEX = "^([\\[(])?(\\d*)?,\\s*(\\d*)?([])])$";
private static final Pattern RANGE_PATTERN = Pattern.compile(RANGE_REGEX);

@Override
Expand All @@ -40,28 +36,12 @@ public Range<Long> convert(String source) {
return null;
}

var matcher = RANGE_PATTERN.matcher(source);
if (!matcher.matches()) {
throw new IllegalArgumentException("Range string is not valid, '%s'".formatted(source));
}

var lowerValueStr = matcher.group(2);
var lowerValue = StringUtils.hasText(lowerValueStr) ? Long.parseLong(lowerValueStr) : null;

var upperValueStr = matcher.group(3);
var upperValue = StringUtils.hasText(upperValueStr) ? Long.parseLong(upperValueStr) : null;
var upperBoundType = UPPER_CLOSED.equals(matcher.group(4)) ? BoundType.CLOSED : BoundType.OPEN;
var cleanedSource = source.replaceAll("\\s", "");

Range<Long> range;
if (lowerValue != null) {
var lowerBoundType = LOWER_CLOSED.equals(matcher.group(1)) ? BoundType.CLOSED : BoundType.OPEN;
range = upperValue != null
? Range.range(lowerValue, lowerBoundType, upperValue, upperBoundType)
: Range.downTo(lowerValue, lowerBoundType);
} else {
range = upperValue != null ? Range.upTo(upperValue, upperBoundType) : Range.all();
if (!RANGE_PATTERN.matcher(cleanedSource).matches()) {
throw new IllegalArgumentException("Range string is not valid, '%s'".formatted(source));
}

return range;
return PostgreSQLGuavaRangeType.ofString(cleanedSource, Long::parseLong, Long.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2025 Hedera Hashgraph, LLC
*
* Licensed 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.
*/

package com.hedera.mirror.restjava.converter;

import com.hedera.mirror.common.domain.entity.EntityId;
import jakarta.inject.Named;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.StringUtils;

@Named
@ConfigurationPropertiesBinding
public class StringToLongConverter implements Converter<String, Long> {
@Override
public Long convert(String source) {
if (!StringUtils.hasText(source)) {
return null;
}


var parts = source.split("\\.");
if (parts.length == 3) {
return EntityId.of(source).getId();
}
return Long.parseLong(source);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2025 Hedera Hashgraph, LLC
*
* Licensed 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.
*/

package com.hedera.mirror.restjava.converter;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;

class StringToLongConverterTest {

@ParameterizedTest(name = "Convert \"{0}\" to Long")
@CsvSource(delimiter = ',', textBlock = """
1, 1
0, 0
0.0.2, 2
""")
void testConverter(String source, Long expected) {
var converter = new StringToLongConverter();
Long actual = converter.convert(source);
assertThat(actual).isEqualTo(expected);
}

@ParameterizedTest(name = "Convert \"{0}\" to Long")
@NullAndEmptySource
void testInvalidSource(String source) {
var converter = new StringToLongConverter();
assertThat(converter.convert(source)).isNull();
}

@ParameterizedTest(name = "Fail to convert \"{0}\" to Long")
@ValueSource(strings = {"bad", "1.557", "5444.0"})
void testConverterFailures(String source) {
var converter = new StringToLongConverter();
assertThrows(NumberFormatException.class, () -> converter.convert(source));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,27 @@
import com.hedera.mirror.restjava.spec.model.RestSpec;
import com.hedera.mirror.restjava.spec.model.RestSpecNormalized;
import com.hedera.mirror.restjava.spec.model.SpecTestNormalized;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.sql.DataSource;
import lombok.SneakyThrows;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.junit.jupiter.api.DynamicContainer;
import org.junit.jupiter.api.TestFactory;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletRegistrationBean;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatusCode;
Expand All @@ -56,48 +63,38 @@
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {"spring.main.allow-bean-definition-overriding=true"})
public class RestSpecTest extends RestJavaIntegrationTest {

private static final Pattern INCLUDED_SPEC_DIRS = Pattern.compile(
"^(accounts|accounts/\\{id}/allowances.*|accounts/\\{id}/rewards.*|blocks.*|contracts|network/exchangerate.*|network/fees.*|network/stake.*|topics/\\{id}/messages)$");
private static final String RESPONSE_HEADER_FILE = "responseHeaders.json";
private static final int JS_REST_API_CONTAINER_PORT = 5551;
private static final Path REST_BASE_PATH = Path.of("..", "hedera-mirror-rest", "__tests__", "specs");
private static final List<Path> SELECTED_SPECS = List.of(
REST_BASE_PATH.resolve("accounts/{id}/allowances/crypto/alias-not-found.json"),
REST_BASE_PATH.resolve("accounts/{id}/allowances/crypto/no-params.json"),
REST_BASE_PATH.resolve("accounts/{id}/allowances/crypto/all-params.json"),
REST_BASE_PATH.resolve("accounts/{id}/allowances/crypto/all-params.json"),
REST_BASE_PATH.resolve("accounts/{id}/allowances/tokens/empty.json"),
REST_BASE_PATH.resolve("accounts/{id}/allowances/tokens/no-params.json"),
REST_BASE_PATH.resolve("accounts/{id}/allowances/tokens/specific-spender-id.json"),
REST_BASE_PATH.resolve("accounts/{id}/allowances/tokens/spender-id-range-token-id-upper.json"),
REST_BASE_PATH.resolve("accounts/{id}/rewards/no-params.json"),
REST_BASE_PATH.resolve("accounts/{id}/rewards/no-rewards.json"),
REST_BASE_PATH.resolve("accounts/{id}/rewards/specific-timestamp.json"),
REST_BASE_PATH.resolve("accounts/specific-id.json"),
REST_BASE_PATH.resolve("blocks/no-records.json"),
REST_BASE_PATH.resolve("blocks/timestamp-param.json"),
REST_BASE_PATH.resolve("blocks/all-params-together.json"),
REST_BASE_PATH.resolve("blocks/limit-param.json"),
REST_BASE_PATH.resolve("blocks/no-records.json"),
REST_BASE_PATH.resolve("blocks/{id}/hash-64.json"),
REST_BASE_PATH.resolve("blocks/{id}/hash-96.json"),
REST_BASE_PATH.resolve("network/exchangerate/no-params.json"),
REST_BASE_PATH.resolve("network/exchangerate/timestamp-upper-bound.json"),
// Disable the following test cases since it fails in PR #9973 due to the fact that the PR fixes bug in
// network fees endpoint however the test class runs the hedera-mirror-rest:latest image without the fix
// REST_BASE_PATH.resolve("network/fees/no-params.json"),
// REST_BASE_PATH.resolve("network/fees/order.json"),
REST_BASE_PATH.resolve("network/fees/timestamp-not-found.json"),
REST_BASE_PATH.resolve("network/stake/no-params.json"),
REST_BASE_PATH.resolve("topics/{id}/messages/all-params.json"),
REST_BASE_PATH.resolve("topics/{id}/messages/encoding.json"),
REST_BASE_PATH.resolve("topics/{id}/messages/no-params.json"),
REST_BASE_PATH.resolve("topics/{id}/messages/order.json"));

private static final IOFileFilter SPEC_FILE_FILTER = new IOFileFilter() {
@Override
public boolean accept(File file) {
var directory = file.isDirectory() ? file : file.getParentFile();
var dirName = directory.getPath().replace(REST_BASE_PATH + "/", "");
return INCLUDED_SPEC_DIRS.matcher(dirName).matches() && !RESPONSE_HEADER_FILE.equals(file.getName());
}

@Override
public boolean accept(File dir, String name) {
return accept(dir);
}
};
private static final List<Path> SELECTED_SPECS =
FileUtils.listFiles(REST_BASE_PATH.toFile(), SPEC_FILE_FILTER, TrueFileFilter.INSTANCE).stream()
.map(File::toPath)
.toList();
private final ResourceDatabasePopulator databaseCleaner;
private final DataSource dataSource;
private final ObjectMapper objectMapper;
private final RestClient restClient;
private final SpecDomainBuilder specDomainBuilder;

@Autowired
private DispatcherServletRegistrationBean dispatcherServletRegistration;

RestSpecTest(
@Value("classpath:cleanup.sql") Resource cleanupSqlResource,
DataSource dataSource,
Expand Down Expand Up @@ -135,6 +132,12 @@ Stream<DynamicContainer> generateTestsFromSpecs() {
continue;
}

// Skip tests that require rest application config
if (normalizedSpec.setup().config() != null) {
log.info("Skipping spec file: {} (setup not yet supported)", specFilePath);
continue;
}

var normalizedSpecTests = normalizedSpec.tests();
for (var test : normalizedSpecTests) {
var testCases =
Expand Down Expand Up @@ -175,6 +178,9 @@ private void testSpecUrl(String url, SpecTestNormalized specTest, RestSpecNormal
.onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
// Override default handling of 4xx errors, and proceed to evaluate the response.
})
.onStatus(HttpStatusCode::is5xxServerError, (req, res) -> {
// Override default handling of 5xx errors, and proceed to evaluate the response.
})
.toEntity(String.class);

assertThat(response.getStatusCode().value()).isEqualTo(specTest.responseStatus());
Expand Down
Loading

0 comments on commit a901bf9

Please sign in to comment.