-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
206 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 60 additions & 0 deletions
60
src/test/java/io/weesvc/springboot/weesvc/api/APIContractTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package io.weesvc.springboot.weesvc.api; | ||
|
||
import io.weesvc.springboot.weesvc.WeesvcApplication; | ||
import io.weesvc.springboot.weesvc.domain.PlacesRepository; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.boot.test.context.SpringBootTest; | ||
import org.springframework.boot.test.web.server.LocalServerPort; | ||
import org.springframework.test.context.ActiveProfiles; | ||
import org.testcontainers.Testcontainers; | ||
import org.testcontainers.containers.output.WaitingConsumer; | ||
import org.testcontainers.k6.K6Container; | ||
import org.testcontainers.utility.MountableFile; | ||
|
||
import java.util.concurrent.TimeUnit; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
@SpringBootTest( | ||
classes = { WeesvcApplication.class, PlacesRestController.class, PlacesRepository.class }, | ||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, | ||
properties = { "spring.r2dbc.url=r2dbc:tc:postgresql:///testdb?TC_IMAGE_TAG=15.3-alpine" } | ||
) | ||
@ActiveProfiles("test") | ||
public class APIContractTests { | ||
|
||
@LocalServerPort | ||
private Integer port; | ||
|
||
@Test | ||
public void validateAPIContract() throws Exception { | ||
|
||
Testcontainers.exposeHostPorts(port); | ||
try ( | ||
K6Container container = new K6Container("grafana/k6:0.49.0") | ||
.withTestScript(MountableFile.forClasspathResource("api-compliance.js")) | ||
.withScriptVar("HOST", "host.testcontainers.internal") | ||
.withScriptVar("PORT", port.toString()) | ||
.withCmdOptions("--no-usage-report") | ||
) { | ||
container.start(); | ||
|
||
WaitingConsumer consumer = new WaitingConsumer(); | ||
container.followOutput(consumer); | ||
|
||
// Wait for test script results to be collected | ||
consumer.waitUntil( | ||
frame -> frame.getUtf8String().contains("iteration_duration"), | ||
1, | ||
TimeUnit.MINUTES | ||
); | ||
|
||
assertThat(container.getLogs()).doesNotContain("thresholds on metrics 'checks' have been crossed"); | ||
// Force a "passing" test to fail to see output results from k6 Test | ||
// assertThat(container.getLogs()).contains("thresholds on metrics 'checks' have been crossed"); | ||
|
||
} | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { check, fail, group } from 'k6'; | ||
import http from 'k6/http'; | ||
|
||
export const options = { | ||
vus: 1, | ||
thresholds: { | ||
// Ensure we have 100% compliance on API tests | ||
checks: [{ threshold: 'rate == 1.0', abortOnFail: true }], | ||
}, | ||
}; | ||
|
||
var targetProtocol = "http" | ||
if (__ENV.PROTOCOL !== undefined) { | ||
targetProtocol = __ENV.PROTOCOL | ||
} | ||
var targetHost = "localhost" | ||
if (__ENV.HOST !== undefined) { | ||
targetHost = __ENV.HOST | ||
} | ||
var targetPort = "80" | ||
if (__ENV.PORT !== undefined) { | ||
targetPort = __ENV.PORT | ||
} | ||
const BASE_URL = `${targetProtocol}://${targetHost}:${targetPort}`; | ||
|
||
export default () => { | ||
const params = { | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
}; | ||
|
||
let testId = -1; | ||
const testName = `k6-${Date.now()}`; | ||
const testDesc = 'API Compliance Test'; | ||
const testLat = 35.4183; | ||
const testLon = 76.5517; | ||
|
||
group('Initial listing check', function () { | ||
const placesRes = http.get(`${BASE_URL}/api/places`) | ||
check(placesRes, { | ||
'fetch returns appropriate status': (resp) => resp.status === 200, | ||
}); | ||
|
||
// Confirm we do not have a place having the testName | ||
let places = placesRes.json(); | ||
for (var i = 0; i < places.length; i++) { | ||
if (places[i].name === testName) { | ||
fail(`Test named "${testName}" already exists`); | ||
} | ||
} | ||
}); | ||
|
||
group('Create a new place', function () { | ||
const createRes = http.post(`${BASE_URL}/api/places`, JSON.stringify({ | ||
name: testName, | ||
description: testDesc, | ||
latitude: testLat, | ||
longitude: testLon, | ||
}), params); | ||
check(createRes, { | ||
'create returns appropriate status': (resp) => resp.status === 200, | ||
'and successfully creates a new place': (resp) => resp.json('id') !== '', | ||
}); | ||
testId = createRes.json('id'); | ||
}); | ||
|
||
group('Retrieving a place', function () { | ||
const placeRes = http.get(`${BASE_URL}/api/places/${testId}`); | ||
check(placeRes, { | ||
'retrieving by id is successful': (resp) => resp.status === 200, | ||
}); | ||
check(placeRes.json(), { | ||
'response provides attribute `id`': (place) => place.id === testId, | ||
'response provides attribute `name`': (place) => place.name === testName, | ||
'response provides attribute `description`': (place) => place.description === testDesc, | ||
'response provides attribute `latitude`': (place) => place.latitude === testLat, | ||
'response provides attribute `longitude`': (place) => place.longitude === testLon, | ||
'response provides attribute `created_at``': (place) => place.created_at !== undefined && place.created_at !== '', | ||
'response provides attribute `updated_at`': (place) => place.updated_at !== undefined && place.updated_at !== '', | ||
}); | ||
// console.log("POST CREATE"); | ||
// console.log(JSON.stringify(placeRes.body)); | ||
|
||
// Ensure the place is returned in the list | ||
const placesRes = http.get(`${BASE_URL}/api/places`) | ||
let places = placesRes.json(); | ||
let found = false; | ||
for (var i = 0; i < places.length; i++) { | ||
if (places[i].id === testId) { | ||
found = true; | ||
break; | ||
} | ||
} | ||
if (!found) { | ||
fail('Test place was not returned when retrieving all places'); | ||
} | ||
}); | ||
|
||
group('Update place by id', function () { | ||
const patchRes = http.patch(`${BASE_URL}/api/places/${testId}`, JSON.stringify({ | ||
description: testDesc + " Updated" | ||
}), params); | ||
check(patchRes, { | ||
'update returns appropriate status': (resp) => resp.status === 200, | ||
}); | ||
check(patchRes.json(), { | ||
'response provides attribute `id`': (place) => place.id === testId, | ||
'response provides attribute `name`': (place) => place.name === testName, | ||
'response provides modified attribute `description`': (place) => place.description === testDesc + " Updated", | ||
'response provides attribute `latitude`': (place) => place.latitude === testLat, | ||
'response provides attribute `longitude`': (place) => place.longitude === testLon, | ||
'response provides attribute `created_at``': (place) => place.created_at !== undefined && place.created_at !== '', | ||
'response provides attribute `updated_at`': (place) => place.updated_at !== undefined && place.updated_at !== '', | ||
'update changes modification date': (place) => place.updated_at !== place.created_at, | ||
}); | ||
// console.log("POST UPDATE"); | ||
// console.log(JSON.stringify(patchRes.body)); | ||
}); | ||
|
||
group('Delete place by id', function () { | ||
const deleteRes = http.del(`${BASE_URL}/api/places/${testId}`) | ||
check(deleteRes, { | ||
'delete returns appropriate status': (resp) => resp.status === 200, | ||
}); | ||
// Confirm that the place has been removed | ||
const placeRes = http.get(`${BASE_URL}/api/places/${testId}`) | ||
check(placeRes, { | ||
'deleted place no longer available': (resp) => resp.status === 404, | ||
}); | ||
}); | ||
|
||
} |