Skip to content

Commit cb0a6c6

Browse files
authored
Validation Open API definition using real responses (#272)
1 parent e14e743 commit cb0a6c6

File tree

9 files changed

+264
-3
lines changed

9 files changed

+264
-3
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
distribution: 'temurin'
2323
cache: 'maven'
2424
- name: Build
25-
run: ./mvnw -B package -Pcli
25+
run: ./mvnw -B package -Pcli --projects spotify-web-api-open-api --also-make
2626
- name: Patch Open API
2727
run: ./scripts/patch-open-api.sh
2828
- name: Check for generator results

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jobs:
5757
- name: Update version in code
5858
run: ./mvnw -B versions:set -DnewVersion="${{ needs.create-version.outputs.new-version }}" -DgenerateBackupPoms=false
5959
- name: Build
60-
run: ./mvnw -B package -Pcli
60+
run: ./mvnw -B package -Pcli --projects spotify-web-api-open-api --also-make
6161
- name: Update Open API
6262
run: ./scripts/patch-open-api.sh
6363
- name: Commit and tag

.github/workflows/update-openapi.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
distribution: 'temurin'
1919
cache: 'maven'
2020
- name: Build tools
21-
run: ./mvnw -B package -Pcli
21+
run: ./mvnw -B package -Pcli --projects spotify-web-api-open-api --also-make
2222
- name: Download Spotify's OpenAPI
2323
run: wget -O official-spotify-open-api.yml https://developer.spotify.com/_data/documentation/web-api/reference/open-api-schema.yml
2424
- name: Patch Spotify's OpenAPI

.github/workflows/validate.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Validation Open API
2+
on:
3+
push:
4+
branches: [ main ]
5+
pull_request:
6+
branches: [ main ]
7+
8+
jobs:
9+
validate:
10+
runs-on: ubuntu-latest
11+
name: Validation Open API
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v3
15+
- name: Setup java
16+
uses: actions/setup-java@v3
17+
with:
18+
java-version: 17
19+
distribution: 'temurin'
20+
cache: 'maven'
21+
- name: Validate
22+
env:
23+
SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }}
24+
SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }}
25+
run: ./mvnw -B test --projects open-api-response-validator --also-make

open-api-response-validator/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Open API Response Validator
2+
3+
Validates real responses from the Spotify Web API against the Open API definition.

open-api-response-validator/pom.xml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>de.sonallux.spotify</groupId>
9+
<artifactId>spotify-web-api-parent</artifactId>
10+
<version>2023.6.7</version>
11+
<relativePath>../pom.xml</relativePath>
12+
</parent>
13+
14+
<artifactId>open-api-response-validator</artifactId>
15+
<version>2023.6.7</version>
16+
<packaging>jar</packaging>
17+
18+
<name>open-api-response-validator</name>
19+
<description>Validate the Spotify Open API against real responses from the Spotify Web API</description>
20+
<url>https://github.com/sonallux/spotify-web-api</url>
21+
22+
<properties>
23+
<spring.version>6.0.9</spring.version>
24+
<swagger-request-validator.version>2.35.1</swagger-request-validator.version>
25+
26+
<moditect.module.name>de.sonallux.spotify.validator</moditect.module.name>
27+
</properties>
28+
29+
<dependencies>
30+
<dependency>
31+
<groupId>org.springframework</groupId>
32+
<artifactId>spring-web</artifactId>
33+
<version>${spring.version}</version>
34+
</dependency>
35+
<dependency>
36+
<groupId>com.atlassian.oai</groupId>
37+
<artifactId>swagger-request-validator-spring-web-client</artifactId>
38+
<version>${swagger-request-validator.version}</version>
39+
</dependency>
40+
<dependency>
41+
<groupId>org.projectlombok</groupId>
42+
<artifactId>lombok</artifactId>
43+
<version>${lombok.version}</version>
44+
<scope>provided</scope>
45+
</dependency>
46+
<dependency>
47+
<groupId>org.slf4j</groupId>
48+
<artifactId>slf4j-simple</artifactId>
49+
<version>${slf4j.version}</version>
50+
<scope>runtime</scope>
51+
</dependency>
52+
<dependency>
53+
<groupId>org.junit.jupiter</groupId>
54+
<artifactId>junit-jupiter</artifactId>
55+
<version>${junit.version}</version>
56+
<scope>test</scope>
57+
</dependency>
58+
</dependencies>
59+
60+
<build>
61+
<plugins>
62+
<plugin>
63+
<groupId>org.apache.maven.plugins</groupId>
64+
<artifactId>maven-compiler-plugin</artifactId>
65+
<version>${maven-compiler-plugin.version}</version>
66+
<configuration>
67+
<annotationProcessorPaths>
68+
<path>
69+
<groupId>org.projectlombok</groupId>
70+
<artifactId>lombok</artifactId>
71+
<version>${lombok.version}</version>
72+
</path>
73+
</annotationProcessorPaths>
74+
</configuration>
75+
</plugin>
76+
</plugins>
77+
</build>
78+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package de.sonallux.spotify.validator;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.springframework.http.*;
5+
import org.springframework.http.client.ClientHttpRequestExecution;
6+
import org.springframework.http.client.ClientHttpRequestInterceptor;
7+
import org.springframework.http.client.ClientHttpResponse;
8+
import org.springframework.util.LinkedMultiValueMap;
9+
import org.springframework.web.client.RestClientException;
10+
import org.springframework.web.client.RestTemplate;
11+
12+
import java.io.IOException;
13+
14+
import static java.util.Objects.requireNonNull;
15+
16+
@Slf4j
17+
public class SpotifyClientCredentialsAuthInterceptor implements ClientHttpRequestInterceptor {
18+
19+
private final String spotifyClientId;
20+
private final String spotifyClientSecret;
21+
private final RestTemplate restTemplate;
22+
private AccessToken accessToken = null;
23+
24+
public SpotifyClientCredentialsAuthInterceptor() {
25+
this.spotifyClientId = requireNonNull(System.getenv("SPOTIFY_CLIENT_ID"), "Missing SPOTIFY_CLIENT_ID environment variable");
26+
this.spotifyClientSecret = requireNonNull(System.getenv("SPOTIFY_CLIENT_SECRET"), "Missing SPOTIFY_CLIENT_SECRET environment variable");
27+
this.restTemplate = new RestTemplate();
28+
}
29+
30+
@Override
31+
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
32+
if (request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
33+
return execution.execute(request, body);
34+
}
35+
36+
if (accessToken == null) {
37+
retrieveNewAccessToken();
38+
}
39+
40+
if (accessToken != null) {
41+
request.getHeaders().add(HttpHeaders.AUTHORIZATION, accessToken.asHeaderValue());
42+
}
43+
44+
return execution.execute(request, body);
45+
}
46+
47+
private void retrieveNewAccessToken() {
48+
var headers = new HttpHeaders();
49+
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
50+
headers.setBasicAuth(spotifyClientId, spotifyClientSecret);
51+
52+
var body = new LinkedMultiValueMap<String, String>();
53+
body.add("grant_type", "client_credentials");
54+
55+
var httpEntity = new HttpEntity<>(body, headers);
56+
57+
try {
58+
var response = restTemplate.exchange("https://accounts.spotify.com/api/token", HttpMethod.POST, httpEntity, AccessToken.class);
59+
if (response.getStatusCode() == HttpStatus.OK) {
60+
this.accessToken = response.getBody();
61+
} else {
62+
log.warn("Failed to retrieve access token: " + response.getStatusCode());
63+
}
64+
} catch (RestClientException e) {
65+
log.warn("Failed to retrieve access token", e);
66+
}
67+
}
68+
69+
private record AccessToken(String token_type, String access_token) {
70+
public String asHeaderValue() {
71+
return "%s %s".formatted(token_type, access_token);
72+
}
73+
}
74+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package de.sonallux.spotify.validator;
2+
3+
import com.atlassian.oai.validator.OpenApiInteractionValidator;
4+
import com.atlassian.oai.validator.springweb.client.OpenApiValidationClientHttpRequestInterceptor;
5+
import org.junit.jupiter.api.BeforeAll;
6+
import org.junit.jupiter.api.Disabled;
7+
import org.junit.jupiter.api.Test;
8+
import org.springframework.http.HttpStatus;
9+
import org.springframework.web.client.RestTemplate;
10+
import org.springframework.web.util.DefaultUriBuilderFactory;
11+
12+
import java.util.List;
13+
14+
import static org.junit.jupiter.api.Assertions.assertEquals;
15+
16+
class ArtistsApiValidationTest {
17+
private static RestTemplate restTemplate;
18+
19+
@BeforeAll
20+
static void setupRestTemplate() {
21+
var spotifyAuthInterceptor = new SpotifyClientCredentialsAuthInterceptor();
22+
var validationInterceptor = new OpenApiValidationClientHttpRequestInterceptor(
23+
OpenApiInteractionValidator.createForSpecificationUrl("../fixed-spotify-open-api.yml")
24+
// https://bitbucket.org/atlassian/swagger-request-validator/src/30f00b42a4bcc6bad7a68fe0c7491dd4aa5c3a67/docs/FAQ.md
25+
.withResolveCombinators(true)
26+
.build());
27+
28+
restTemplate = new RestTemplate();
29+
restTemplate.setInterceptors(List.of(spotifyAuthInterceptor, validationInterceptor));
30+
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("https://api.spotify.com/v1"));
31+
}
32+
33+
@Test
34+
void validateGetArtist() {
35+
var response = restTemplate.getForEntity("/artists/{id}", String.class, "4lDiJcOJ2GLCK6p9q5BgfK");
36+
assertEquals(response.getStatusCode(), HttpStatus.OK);
37+
}
38+
39+
@Test
40+
void validateGetSeveralArtist() {
41+
var response = restTemplate.getForEntity("/artists?ids={ids}", String.class, "0Dvx6p8JDyzeOPGmaCIH1L,5Y5TRrQiqgUO4S36tzjIRZ");
42+
assertEquals(response.getStatusCode(), HttpStatus.OK);
43+
}
44+
45+
@Test
46+
void validateGetArtistsAlbums() {
47+
var artistId = "6XyY86QOPPrYVGvF9ch6wz";
48+
49+
var responseFirstPage = restTemplate.getForEntity("/artists/{id}/albums?limit=5", String.class, artistId);
50+
assertEquals(responseFirstPage.getStatusCode(), HttpStatus.OK);
51+
52+
var responseMiddlePage = restTemplate.getForEntity("/artists/{id}/albums?limit=5&offset=50", String.class, artistId);
53+
assertEquals(responseMiddlePage.getStatusCode(), HttpStatus.OK);
54+
55+
var responseLastPage = restTemplate.getForEntity("/artists/{id}/albums?limit=20&offset=360", String.class, artistId);
56+
assertEquals(responseLastPage.getStatusCode(), HttpStatus.OK);
57+
58+
var responseEmptyPage = restTemplate.getForEntity("/artists/{id}/albums?limit=20&offset=380", String.class, artistId);
59+
assertEquals(responseEmptyPage.getStatusCode(), HttpStatus.OK);
60+
}
61+
62+
/*
63+
* disabled because of errors
64+
* - [Path '/tracks/0/album'] Object instance has properties which are not allowed by the schema: [\"is_playable\"]
65+
* - [Path '/tracks/0/album'] Object has missing required properties ([\"available_markets\"])
66+
*/
67+
@Disabled
68+
@Test
69+
void validateGetArtistsTopTracks() {
70+
var response = restTemplate.getForEntity("/artists/{id}/top-tracks?market=DE", String.class, "0Dvx6p8JDyzeOPGmaCIH1L");
71+
assertEquals(response.getStatusCode(), HttpStatus.OK);
72+
}
73+
74+
@Test
75+
void validateGetArtistsRelatedArtists() {
76+
var response = restTemplate.getForEntity("/artists/{id}/related-artists", String.class, "0Dvx6p8JDyzeOPGmaCIH1L");
77+
assertEquals(response.getStatusCode(), HttpStatus.OK);
78+
}
79+
80+
}

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<modules>
1717
<module>json-utils</module>
1818
<module>spotify-web-api-open-api</module>
19+
<module>open-api-response-validator</module>
1920
</modules>
2021

2122
<licenses>

0 commit comments

Comments
 (0)