Skip to content

Commit dedde2d

Browse files
committed
test(cats): Testing one example created against cats
1 parent d3c81ef commit dedde2d

File tree

62 files changed

+1144
-1435
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+1144
-1435
lines changed

.cursor/agents/robot-micronaut-coder.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Apply guidance from these Skills when relevant:
3434
- `@521-frameworks-micronaut-testing-unit-tests`: Micronaut unit testing
3535
- `@522-frameworks-micronaut-testing-integration-tests`: Micronaut integration testing
3636
- `@523-frameworks-micronaut-testing-acceptance-tests`: Micronaut acceptance testing
37+
- `@702-technologies-wiremock`: Improve tests with Wiremock
3738

3839
### Workflow
3940

.cursor/agents/robot-quarkus-coder.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Apply guidance from these Skills when relevant:
3333
- `@421-frameworks-quarkus-testing-unit-tests`: Quarkus Unit Testing
3434
- `@422-frameworks-quarkus-testing-integration-tests`: Quarkus integration testing
3535
- `@423-frameworks-quarkus-testing-acceptance-tests`: Quarkus acceptance testing
36+
- `@702-technologies-wiremock`: Improve tests with Wiremock
3637

3738
### Workflow
3839

.cursor/agents/robot-spring-boot-coder.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Apply guidance from these Skills when relevant:
3737
- `@321-frameworks-spring-boot-testing-unit-tests`: Spring Boot unit testing
3838
- `@322-frameworks-spring-boot-testing-integration-tests`: Spring Boot integration testing
3939
- `@323-frameworks-spring-boot-testing-acceptance-tests`: Spring Boot acceptance testing
40+
- `@702-technologies-wiremock`: Improve tests with Wiremock
4041

4142
### Workflow
4243

examples/requirements-examples/problem1/implementation/pom.xml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@
2323
<groupId>org.springframework.boot</groupId>
2424
<artifactId>spring-boot-starter-webmvc</artifactId>
2525
</dependency>
26+
<dependency>
27+
<groupId>org.springframework.boot</groupId>
28+
<artifactId>spring-boot-starter-validation</artifactId>
29+
</dependency>
30+
<dependency>
31+
<groupId>org.springframework.boot</groupId>
32+
<artifactId>spring-boot-starter-json</artifactId>
33+
</dependency>
34+
<dependency>
35+
<groupId>com.fasterxml.jackson.core</groupId>
36+
<artifactId>jackson-databind</artifactId>
37+
</dependency>
2638
<dependency>
2739
<groupId>org.springframework.boot</groupId>
2840
<artifactId>spring-boot-starter-webmvc-test</artifactId>
@@ -37,6 +49,41 @@
3749
</dependencies>
3850
<build>
3951
<plugins>
52+
<plugin>
53+
<groupId>org.openapitools</groupId>
54+
<artifactId>openapi-generator-maven-plugin</artifactId>
55+
<version>7.15.0</version>
56+
<executions>
57+
<execution>
58+
<id>generate-api-boundary</id>
59+
<phase>generate-sources</phase>
60+
<goals>
61+
<goal>generate</goal>
62+
</goals>
63+
<configuration>
64+
<inputSpec>${project.basedir}/../requirements/agile/US-001-god-analysis-api.openapi.yaml</inputSpec>
65+
<generatorName>spring</generatorName>
66+
<apiPackage>info.jab.ms.api</apiPackage>
67+
<modelPackage>info.jab.ms.api.model</modelPackage>
68+
<generateApiTests>false</generateApiTests>
69+
<generateModelTests>false</generateModelTests>
70+
<generateApiDocumentation>false</generateApiDocumentation>
71+
<generateModelDocumentation>false</generateModelDocumentation>
72+
<configOptions>
73+
<dateLibrary>java8</dateLibrary>
74+
<useJakartaEe>true</useJakartaEe>
75+
<interfaceOnly>true</interfaceOnly>
76+
<useSpringBoot3>true</useSpringBoot3>
77+
<useTags>true</useTags>
78+
<skipDefaultInterface>true</skipDefaultInterface>
79+
<openApiNullable>false</openApiNullable>
80+
<annotationLibrary>none</annotationLibrary>
81+
<documentationProvider>none</documentationProvider>
82+
</configOptions>
83+
</configuration>
84+
</execution>
85+
</executions>
86+
</plugin>
4087
<plugin>
4188
<groupId>org.apache.maven.plugins</groupId>
4289
<artifactId>maven-surefire-plugin</artifactId>
Lines changed: 9 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,16 @@
11
package info.jab.ms.algorithm;
22

33
import java.math.BigInteger;
4+
import java.util.Objects;
5+
import org.springframework.stereotype.Component;
46

5-
/**
6-
* Concatenates decimal digits of each Unicode code point in a name, parses to {@link BigInteger},
7-
* and sums matching names. First-code-point filter uses case-insensitive comparison so the pinned
8-
* acceptance sum aligns with upstream data (e.g. {@code filter=n} matches {@code Nike}).
9-
*/
7+
@Component
108
public final class UnicodeAggregator {
119

12-
private UnicodeAggregator() {}
13-
14-
public static BigInteger sumFiltered(String filter, Iterable<String> names) {
15-
if (filter == null || filter.isEmpty()) {
16-
return BigInteger.ZERO;
17-
}
18-
int filterCp = filter.codePointAt(0);
19-
BigInteger total = BigInteger.ZERO;
20-
for (String name : names) {
21-
if (name == null || name.isEmpty()) {
22-
continue;
23-
}
24-
int firstCp = name.codePointAt(0);
25-
if (!firstCodePointMatchesFilter(firstCp, filterCp)) {
26-
continue;
27-
}
28-
total = total.add(nameToBigInteger(name));
29-
}
30-
return total;
31-
}
32-
33-
static boolean firstCodePointMatchesFilter(int nameFirstCodePoint, int filterCodePoint) {
34-
return Character.toLowerCase(nameFirstCodePoint) == Character.toLowerCase(filterCodePoint);
35-
}
36-
37-
static BigInteger nameToBigInteger(String name) {
38-
StringBuilder digits = new StringBuilder();
39-
name.codePoints().forEach(cp -> digits.append(Integer.toString(cp)));
40-
return new BigInteger(digits.toString());
41-
}
10+
public BigInteger toBigInteger(String value) {
11+
Objects.requireNonNull(value, "value must not be null");
12+
return value.codePoints()
13+
.mapToObj(BigInteger::valueOf)
14+
.reduce(BigInteger.ZERO, BigInteger::add);
15+
}
4216
}
Lines changed: 46 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,58 @@
11
package info.jab.ms.client;
22

3+
import info.jab.ms.algorithm.UnicodeAggregator;
4+
import info.jab.ms.api.model.PantheonSource;
35
import info.jab.ms.config.GodOutboundProperties;
6+
import info.jab.ms.service.GodData;
7+
import info.jab.ms.service.PantheonDataSource;
48
import java.util.List;
5-
import org.slf4j.Logger;
6-
import org.slf4j.LoggerFactory;
79
import org.springframework.beans.factory.annotation.Qualifier;
10+
import org.springframework.http.MediaType;
811
import org.springframework.core.ParameterizedTypeReference;
912
import org.springframework.stereotype.Component;
1013
import org.springframework.web.client.RestClient;
1114
import org.springframework.web.client.RestClientException;
1215

1316
@Component
14-
public class GodDataClient {
15-
16-
private static final Logger log = LoggerFactory.getLogger(GodDataClient.class);
17-
18-
private static final ParameterizedTypeReference<List<String>> STRING_LIST =
19-
new ParameterizedTypeReference<>() {};
20-
21-
private final RestClient restClient;
22-
private final GodOutboundProperties properties;
23-
24-
public GodDataClient(
25-
@Qualifier("godRestClient") RestClient restClient, GodOutboundProperties properties) {
26-
this.restClient = restClient;
27-
this.properties = properties;
28-
}
29-
30-
/**
31-
* Single GET to the configured URL for the pantheon. On connect/read failure or non-2xx, returns
32-
* an empty list (no retries).
33-
*/
34-
public List<String> fetchNames(String pantheonKey) {
35-
String url = urlFor(pantheonKey);
36-
if (url == null || url.isBlank()) {
37-
log.warn("god.outbound.fetch.skipped source={} reason=no_url", pantheonKey);
38-
return List.of();
39-
}
40-
log.debug("god.outbound.fetch.start source={} url={}", pantheonKey, url);
41-
try {
42-
List<String> names =
43-
restClient.get().uri(url).retrieve().body(STRING_LIST);
44-
if (names == null) {
45-
log.warn("god.outbound.fetch.empty_body source={}", pantheonKey);
46-
return List.of();
47-
}
48-
log.info(
49-
"god.outbound.fetch.ok source={} url={} nameCount={}",
50-
pantheonKey,
51-
url,
52-
names.size());
53-
return names;
54-
} catch (RestClientException ex) {
55-
log.warn(
56-
"god.outbound.fetch.failed source={} url={} error={}",
57-
pantheonKey,
58-
url,
59-
ex.toString());
60-
return List.of();
61-
}
62-
}
63-
64-
private String urlFor(String pantheonKey) {
65-
if (properties.urls() == null) {
66-
return null;
67-
}
68-
return switch (pantheonKey) {
69-
case "greek" -> properties.urls().greek();
70-
case "roman" -> properties.urls().roman();
71-
case "nordic" -> properties.urls().nordic();
72-
default -> null;
73-
};
74-
}
17+
public class GodDataClient implements PantheonDataSource {
18+
19+
private static final ParameterizedTypeReference<List<String>> GOD_NAMES_TYPE = new ParameterizedTypeReference<>() {
20+
};
21+
22+
private final RestClient restClient;
23+
private final GodOutboundProperties properties;
24+
private final UnicodeAggregator unicodeAggregator;
25+
private final OutboundCallObserver outboundCallObserver;
26+
27+
public GodDataClient(
28+
@Qualifier("godOutboundRestClient") RestClient restClient,
29+
GodOutboundProperties properties,
30+
UnicodeAggregator unicodeAggregator,
31+
OutboundCallObserver outboundCallObserver
32+
) {
33+
this.restClient = restClient;
34+
this.properties = properties;
35+
this.unicodeAggregator = unicodeAggregator;
36+
this.outboundCallObserver = outboundCallObserver;
37+
}
38+
39+
@Override
40+
public List<GodData> fetch(PantheonSource source) {
41+
outboundCallObserver.onStart(source);
42+
try {
43+
var names = restClient.get()
44+
.uri(properties.getUrlFor(source))
45+
.accept(MediaType.APPLICATION_JSON)
46+
.retrieve()
47+
.body(GOD_NAMES_TYPE);
48+
var result = names.stream()
49+
.map(name -> new GodData(name, unicodeAggregator.toBigInteger(name)))
50+
.toList();
51+
outboundCallObserver.onSuccess(source, result.size());
52+
return result;
53+
} catch (RestClientException exception) {
54+
outboundCallObserver.onFailure(source, exception);
55+
throw new OutboundSourceException(source, exception);
56+
}
57+
}
7558
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package info.jab.ms.client;
2+
3+
import info.jab.ms.api.model.PantheonSource;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
import org.springframework.stereotype.Component;
7+
8+
@Component
9+
final class LoggingOutboundCallObserver implements OutboundCallObserver {
10+
11+
private static final Logger log = LoggerFactory.getLogger(LoggingOutboundCallObserver.class);
12+
13+
@Override
14+
public void onStart(PantheonSource source) {
15+
log.info("outbound_call_started source={}", source.getValue());
16+
}
17+
18+
@Override
19+
public void onSuccess(PantheonSource source, int payloadSize) {
20+
log.info("outbound_call_completed source={} size={}", source.getValue(), payloadSize);
21+
}
22+
23+
@Override
24+
public void onFailure(PantheonSource source, Exception exception) {
25+
log.warn("outbound_call_failed source={} reason={}", source.getValue(), exception.getMessage());
26+
}
27+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package info.jab.ms.client;
2+
3+
import info.jab.ms.api.model.PantheonSource;
4+
5+
public interface OutboundCallObserver {
6+
7+
void onStart(PantheonSource source);
8+
9+
void onSuccess(PantheonSource source, int payloadSize);
10+
11+
void onFailure(PantheonSource source, Exception exception);
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package info.jab.ms.client;
2+
3+
import info.jab.ms.api.model.PantheonSource;
4+
5+
public final class OutboundSourceException extends RuntimeException {
6+
7+
public OutboundSourceException(PantheonSource source, Throwable cause) {
8+
super("Outbound source fetch failed for source: " + source.getValue(), cause);
9+
}
10+
}
Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
11
package info.jab.ms.config;
22

3+
import info.jab.ms.api.model.PantheonSource;
4+
import jakarta.validation.constraints.NotEmpty;
5+
import jakarta.validation.constraints.NotNull;
6+
import java.net.URI;
37
import java.time.Duration;
8+
import java.util.Map;
49
import org.springframework.boot.context.properties.ConfigurationProperties;
5-
import org.springframework.boot.context.properties.bind.DefaultValue;
10+
import org.springframework.validation.annotation.Validated;
611

12+
@Validated
713
@ConfigurationProperties(prefix = "god.outbound")
814
public record GodOutboundProperties(
9-
@DefaultValue("5s") Duration connectTimeout,
10-
@DefaultValue("5s") Duration readTimeout,
11-
Urls urls) {
15+
@NotNull Duration connectTimeout,
16+
@NotNull Duration readTimeout,
17+
@NotEmpty Map<String, URI> urls
18+
) {
1219

13-
public record Urls(String greek, String roman, String nordic) {}
20+
public URI getUrlFor(PantheonSource source) {
21+
var endpoint = urls.get(source.getValue());
22+
if (endpoint == null) {
23+
throw new IllegalStateException("Missing outbound URL for source: " + source.getValue());
24+
}
25+
return endpoint;
26+
}
1427
}

0 commit comments

Comments
 (0)