Tests pass but logging is still required

This commit is contained in:
2024-07-19 17:21:27 +01:00
parent 982b4e8d5d
commit 5bb6ccb789
20 changed files with 468 additions and 104 deletions

2
.idea/misc.xml generated
View File

@@ -8,7 +8,7 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_20" default="true" project-jdk-name="20" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17 (2)" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

47
pom.xml
View File

@@ -65,6 +65,14 @@
<mainClass>MainKt</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
@@ -81,18 +89,17 @@
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>1.9.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.rest-assured/rest-assured -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
<!-- jUnit -->
<dependency>
<groupId>org.junit.jupiter</groupId>
@@ -111,6 +118,12 @@
<artifactId>retrofit</artifactId>
<version>2.11.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.squareup.retrofit2/converter-gson -->
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-gson</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>logging-interceptor</artifactId>
@@ -121,19 +134,11 @@
<artifactId>api-testing-automation-framework</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.squareup.retrofit2/converter-gson -->
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-gson</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
@@ -144,6 +149,18 @@
<artifactId>log4j-api</artifactId>
<version>2.19.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.assertj/assertj-core -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>6.20.0</version>
</dependency>
</dependencies>
</project>

View File

@@ -1,26 +1,54 @@
package api
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.security.SecureRandom
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class BookerApi {
private val baseUrl = "https://restful-booker.herokuapp.com/"
private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
var trustAllCerts = arrayOf<TrustManager>(
object : X509TrustManager {
@Throws(CertificateException::class)
override fun checkClientTrusted(chain: Array<X509Certificate?>?, authType: String?) {
}
@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array<X509Certificate?>?, authType: String?) {
}
override fun getAcceptedIssuers(): Array<X509Certificate?> {
return arrayOfNulls<X509Certificate>(0)
}
}
)
var gson = GsonBuilder()
.setLenient()
.create()
private fun buildOkHttpClient(timeoutSeconds: Long = 30L): OkHttpClient {
val builder = OkHttpClient.Builder()
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, SecureRandom())
builder
.addInterceptor(loggingInterceptor)
.connectTimeout(timeoutSeconds, TimeUnit.SECONDS)
.writeTimeout(timeoutSeconds, TimeUnit.SECONDS)
.readTimeout(timeoutSeconds, TimeUnit.SECONDS)
.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier { _, _ -> true }
return builder.build()
}
@@ -30,7 +58,7 @@ class BookerApi {
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(RestfulBookerApi::class.java)
}

View File

@@ -6,19 +6,15 @@ import retrofit2.http.*
interface RestfulBookerApi {
@Headers("Content-Type:application/json")
@Headers("Content-Type:application/json", "Accept: application/json")
@POST("auth")
suspend fun createAuthToken(
@Field("username") username: String,
@Field("password") password: String,
@Body authRequest: AuthRequest
): Response<AuthResponse>
@GET("booking")
suspend fun getBookingIds(
@Field("firstname") firstname: String? = null,
@Field("lastname") lastname: String? = null,
@Field("checkin") checkin: String? = null,
@Field("checkout") checkout: String? = null,
): Response<List<BookingIdResponse>>
@GET("booking/{id}")
@@ -26,7 +22,7 @@ interface RestfulBookerApi {
@Path("id") id: String
): Response<BookingResponse>
@Headers("Content-Type:application/json")
@Headers("Content-Type:application/json", "Accept: application/json")
@POST("booking")
suspend fun createBooking(
@Body booking: BookingRequest,
@@ -45,17 +41,12 @@ interface RestfulBookerApi {
suspend fun partialUpdateBooking(
@Path("id") id: String,
@Header("Authorization") token: String,
@Field("firstname") firstname: String? = null,
@Field("lastname") lastname: String? = null,
@Field("totalprice") totalprice: Float? = null,
@Field("depositpaid") depositpaid: Boolean? = null,
@Field("checkin") checkin: String? = null,
@Field("checkout") checkout: String? = null,
@Field("additionalneeds") additionalneeds: String? = null
@Body update: UpdateBookingRequest
): Response<BookingResponse>
@DELETE("booking/{id}")
suspend fun deleteBooking(
@Path("id") id: String
@Path("id") id: String,
@Header("Authorization") token: String
): Response<Any>
}

View File

@@ -0,0 +1,6 @@
package model
data class AuthRequest(
val username: String,
val password: String
)

View File

@@ -1,5 +1,5 @@
package model
class AuthResponse {
var token: String? = null
}
data class AuthResponse (
val token: String
)

View File

@@ -1,5 +1,5 @@
package model
class BookingIdResponse {
var bookingid = 0
}
data class BookingIdResponse(
val bookingid: Int
)

View File

@@ -1,12 +1,12 @@
package model
data class BookingRequest (
var firstname: String? = null,
var lastname: String? = null,
var totalprice: Int = 0,
var depositpaid: Boolean = false,
var bookingdates: Bookingdates? = null,
var additionalneeds: String? = null,
var firstname: String,
var lastname: String,
var totalprice: Int,
var depositpaid: Boolean,
var bookingdates: Bookingdates,
var additionalneeds: String,
)

View File

@@ -1,11 +1,11 @@
package model
data class BookingResponse (
var firstname: String? = null,
var lastname: String? = null,
var totalprice: Int = 0,
var depositpaid: Boolean = false,
var bookingdates: Bookingdates? = null,
var additionalneeds: String? = null
data class BookingResponse(
var firstname: String,
var lastname: String,
var totalprice: Int,
var depositpaid: Boolean,
var bookingdates: Bookingdates,
var additionalneeds: String
)

View File

@@ -1,6 +1,6 @@
package model
data class Bookingdates (
var checkin: String? = null,
var checkout: String? = null,
var checkin: String,
var checkout: String,
)

View File

@@ -1,6 +1,6 @@
package model
class CreateBookingResponse {
var bookingid = 0
var booking: BookingResponse? = null
}
data class CreateBookingResponse(
var bookingid: Int,
var booking: BookingResponse
)

View File

@@ -0,0 +1,10 @@
package model
data class UpdateBookingRequest(
var firstname: String? = null,
var lastname: String? = null,
var totalprice: Int? = null,
var depositpaid: Boolean? = null,
var bookingdates: Bookingdates? = null,
var additionalneeds: String? = null,
)

View File

@@ -0,0 +1,92 @@
package storage
import model.BookingResponse
import model.Bookingdates
class OrdersDatabase {
private val storage = mutableMapOf<Int, BookingResponse>()
// Create
fun insertBooking(id: Int, booking: BookingResponse) {
if (storage.contains(id)) {
storage.replace(id, booking)
} else {
storage[id] = booking
}
}
// Read
fun getIdsOfBookingsAvailable() = storage.keys.toList()
fun getBookingsAvailable() = storage.values.toList()
fun getIdsAndBookings() = storage.toMap()
fun getIdsOfOrderBasedOnValues(
firstname: String? = null,
lastname: String? = null,
totalprice: Int? = null,
depositpaid: Boolean? = null,
checkin: String? = null,
checkout: String? = null,
additionalneeds: String? = null
): List<Int> {
return storage.filterValues {
firstname?.let { f -> f == it.firstname } ?: true &&
lastname?.let { f -> f == it.lastname } ?: true &&
totalprice?.let { f -> f == it.totalprice } ?: true &&
depositpaid?.let { f -> f == it.depositpaid } ?: true &&
checkin?.let { f -> f == it.bookingdates.checkin } ?: true &&
checkout?.let { f -> f == it.bookingdates.checkout } ?: true &&
additionalneeds?.let { f -> f == it.additionalneeds } ?: true
}.keys.toList()
}
fun getIdsOfOrderBasedOnValues(
id: Int
): BookingResponse? {
return storage[id]
}
// Update
fun updateCompleteOrder(id: Int, newBookingResponse: BookingResponse) {
insertBooking(id, newBookingResponse)
}
fun updateOrderPartial(
id: Int,
firstname: String? = null,
lastname: String? = null,
totalprice: Int? = null,
depositpaid: Boolean? = null,
checkin: String? = null,
checkout: String? = null,
additionalneeds: String? = null
) {
if (storage.keys.remove(id)) {
storage.compute(id) { k, v ->
val mFirstName = firstname ?: v!!.firstname
val mlastname = lastname ?: v!!.lastname
val mTotalprice = totalprice ?: v!!.totalprice
val mDepositpaid = depositpaid ?: v!!.depositpaid
val mCheckin = checkin ?: v!!.bookingdates.checkin
val mCheckout = checkout ?: v!!.bookingdates.checkout
val mAdditionalneeds = additionalneeds ?: v!!.additionalneeds
BookingResponse(
firstname = mFirstName,
lastname = mlastname,
totalprice = mTotalprice,
depositpaid = mDepositpaid,
bookingdates = Bookingdates(
checkin = mCheckin,
checkout = mCheckout
),
additionalneeds = mAdditionalneeds
)
}
}
}
// Delete
fun clearAllData() = storage.clear()
fun deleteSingleEntry(id: Int) = storage.remove(id)
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn">
<Properties>
<Property name="basePath">C:\\logs</Property>
</Properties>
<Appenders>
<RollingFile name="fileLogger" fileName="${basePath}/app-info.html"
filePattern="${basePath}/app-info-%d{yyyy-MM-dd}.html">
<HTMLLayout charset="UTF-8" title="Howtodoinjava Info Logs" locationInfo="true" />
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
<SizeBasedTriggeringPolicy size="10 MB" />
</Policies>
</RollingFile>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n" />
</Console>
</Appenders>
<Loggers>
<Logger name="com.howtodoinjava" level="debug" additivity="false">
<appender-ref ref="fileLogger" level="debug" />
</Logger>
<Root level="debug" additivity="false">
<appender-ref ref="console" />
</Root>
</Loggers>
</Configuration>

View File

@@ -4,6 +4,7 @@ import java.io.IOException
abstract class NetworkTests {
// Call retrofit API and unwrap response or throw exception
fun <T : Any> responseUnwrap(
call: suspend () -> Response<T>
): T {
@@ -12,9 +13,11 @@ abstract class NetworkTests {
if (response.isSuccessful) {
return response.body()!!
} else {
val error = response.errorBody()?.string()
val error = StringBuilder().append(response.code()).append(" : ")
.append(response.errorBody()?.string() ?: "Unable to handle end point").toString()
print(response.raw())
throw IOException(error)
}
}
throw IOException(error ?: "Unable to handle end point")
}
}
}

View File

@@ -1,35 +1,56 @@
import api.BookerApi
import api.RestfulBookerApi
import io.restassured.RestAssured.given
import kotlinx.coroutines.runBlocking
import model.AuthRequest
import model.BookingRequest
import model.Bookingdates
import model.UpdateBookingRequest
import net.sf.jasperreports.engine.JasperCompileManager
import net.sf.jasperreports.engine.JasperFillManager
import net.sf.jasperreports.engine.JasperReport
import net.sf.jasperreports.engine.export.HtmlExporter
import net.sf.jasperreports.engine.util.JRSaver
import net.sf.jasperreports.export.SimpleHtmlExporterOutput
import org.apache.logging.log4j.LogManager
import org.junit.FixMethodOrder
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.runners.MethodSorters
import org.apache.logging.log4j.message.MessageFormatMessage
import org.assertj.core.api.AssertionsForClassTypes.assertThat
import org.junit.jupiter.api.*
import storage.OrdersDatabase
import utils.FileReader
import java.io.InputStream
import java.util.*
@FixMethodOrder(MethodSorters.DEFAULT)
class Tests : NetworkTests(){
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class Tests : NetworkTests() {
companion object {
private lateinit var bookerApi: RestfulBookerApi
private lateinit var fileReader: FileReader
private val logger = LogManager.getLogger(Tests::class.java)
private val fileReader = FileReader()
private val logger = LogManager.getLogger("Test")
val bookingsReportStream: InputStream = javaClass.getResourceAsStream("/bookingsReport.jrxml")
val jasperReport: JasperReport = JasperCompileManager.compileReport(bookingsReportStream)
private val storage = OrdersDatabase()
private lateinit var bookingRequestTestOne: BookingRequest
private lateinit var bookingRequestTestTwo: BookingRequest
@BeforeAll
@JvmStatic
internal fun beforeAll() {
bookerApi = BookerApi().invoke()
bookingRequestTestOne = fileReader.readJsonFileFromResources("test1", BookingRequest::class.java)
bookingRequestTestTwo = fileReader.readJsonFileFromResources("test2", BookingRequest::class.java)
}
@AfterAll
@JvmStatic
internal fun afterAll() {
storage.clearAllData()
JRSaver.saveObject(jasperReport, "bookingReport.jasper");
}
}
@@ -40,10 +61,11 @@ class Tests : NetworkTests(){
* o Above added 3 new booking details
*/
@Test()
@Order(1)
fun testScenarioOne() {
// Given
val bookingRequestOne = fileReader.readJsonFileFromResources<BookingRequest>("test1")
val bookingRequestTwo = fileReader.readJsonFileFromResources<BookingRequest>("test2")
/*
* Given
*/
val bookingRequestThree = BookingRequest(
firstname = "Mark",
lastname = "Wahlberg",
@@ -56,17 +78,36 @@ class Tests : NetworkTests(){
additionalneeds = "Breakfast"
)
// When
val createBookingOneResponse = responseUnwrap { bookerApi.createBooking(bookingRequestOne) }
val createBookingTwoResponse = responseUnwrap { bookerApi.createBooking(bookingRequestTwo) }
/*
* When
*/
val createBookingOneResponse = responseUnwrap { bookerApi.createBooking(bookingRequestTestOne) }
val createBookingTwoResponse = responseUnwrap { bookerApi.createBooking(bookingRequestTestTwo) }
val createBookingThreeResponse = responseUnwrap { bookerApi.createBooking(bookingRequestThree) }
val bookingResponses = listOf(createBookingOneResponse, createBookingTwoResponse, createBookingThreeResponse)
// Then
/*
* Then
*/
val bookingIds = responseUnwrap { bookerApi.getBookingIds() }
assertThat(bookingIds.size)
.withFailMessage("Did not find 3 bookings")
.isGreaterThanOrEqualTo(3)
JasperFillManager.
logger.trace("Available booking IDs: ${bookingIds.joinToString()}")
bookingIds.forEach {
val currentBookingResponse = responseUnwrap { bookerApi.getSingleBooking("$it") }
logger.trace(currentBookingResponse)
// Add the booking details and idea for later
bookingResponses.forEach { response ->
storage.insertBooking(response.bookingid, response.booking)
logger.trace(
MessageFormatMessage(
"Booking with ID: {0} has been added: {1}",
response.bookingid,
response.booking
)
)
}
}
@@ -77,29 +118,133 @@ class Tests : NetworkTests(){
*
*/
@Test()
@Order(2)
fun testScenarioTwo() {
// Given
/*
* Given
*/
// Find my booking ids
val orderIdTestOne = storage.getIdsOfOrderBasedOnValues(
firstname = bookingRequestTestOne.firstname,
lastname = bookingRequestTestOne.lastname,
checkin = bookingRequestTestOne.bookingdates.checkin,
checkout = bookingRequestTestOne.bookingdates.checkout
).first()
val orderIdTestTwo = storage.getIdsOfOrderBasedOnValues(
firstname = bookingRequestTestTwo.firstname,
lastname = bookingRequestTestTwo.lastname,
checkin = bookingRequestTestTwo.bookingdates.checkin,
checkout = bookingRequestTestTwo.bookingdates.checkout
).first()
// When
/*
* When
*/
val auth = responseUnwrap {
bookerApi.createAuthToken(
AuthRequest(
UUID.randomUUID().toString(),
UUID.randomUUID().toString()
)
)
}
val tokenBuilder =
StringBuilder("Basic ").append(Base64.getEncoder().encodeToString("admin:password123".toByteArray()))
.toString()
val updateTestOneResponse = responseUnwrap {
bookerApi.partialUpdateBooking(
id = orderIdTestOne.toString(),
token = tokenBuilder,
update = UpdateBookingRequest(
totalprice = 1000
)
)
}.also {
storage.updateCompleteOrder(orderIdTestOne, it)
}
val updateTestTwoResponse = responseUnwrap {
bookerApi.partialUpdateBooking(
id = orderIdTestTwo.toString(),
token = tokenBuilder,
update = UpdateBookingRequest(
totalprice = 1500
)
)
}.also {
storage.updateCompleteOrder(orderIdTestTwo, it)
}
val updateResponseList = listOf(updateTestOneResponse, updateTestTwoResponse)
// Then
/*
* Then
*/
logger.trace(
MessageFormatMessage(
"Booking with ID: {0} has been updated to the following: {1}",
orderIdTestOne,
updateTestOneResponse
)
)
logger.trace(
MessageFormatMessage(
"Booking with ID: {0} has been updated to the following: {1}",
orderIdTestTwo,
updateTestTwoResponse
)
)
}
@Test()
@Order(3)
fun testScenarioThree() {
// Given
/*
* Given
*/
val idOfAny = storage.getIdsOfBookingsAvailable().random()
val tokenBuilder =
StringBuilder("Basic ").append(Base64.getEncoder().encodeToString("admin:password123".toByteArray()))
.toString()
// When
/*
* When
*/
val deleteResponse = responseUnwrap {
bookerApi.deleteBooking(id = idOfAny.toString(), tokenBuilder)
}.also { storage.deleteSingleEntry(id = idOfAny) }
// Then
/*
* Then
*/
logger.trace(
MessageFormatMessage(
"Booking with ID: {0} has been delete and given the following response: {1}",
idOfAny,
deleteResponse
)
)
}
@Test()
@Order(4)
fun testScenarioFour() {
// Given
/*
* Given
*/
// When
/*
* When
*/
// Then
/*
* Then
*/
val exporter = HtmlExporter()
// Set input ...
// Set input ...
exporter.exporterOutput = SimpleHtmlExporterOutput("bookingsReport.html")
exporter.exportReport()
}
}

View File

@@ -3,19 +3,19 @@ package utils
import com.google.gson.Gson
import java.io.BufferedReader
import java.lang.reflect.ParameterizedType
import kotlin.reflect.KClass
class FileReader {
private val gson by lazy { Gson() }
fun <T : Any?> readJsonFileFromResources(fileName: String): T {
val iStream = this::class.java.getResourceAsStream("$fileName.json")
/**
* Read json files from resources and turn into object of type <T>
*/
fun <T : Any?> readJsonFileFromResources(fileName: String, clazz: Class<T>): T {
val iStream = this::class.java.getResourceAsStream("/$fileName.json")
?: throw IllegalStateException("Unable to read the file requested")
val data = iStream.bufferedReader().use(BufferedReader::readText)
val genericType = ((javaClass.genericSuperclass as? ParameterizedType)
?.actualTypeArguments?.getOrNull(0) as? Class<T>)
?: throw IllegalStateException("Can not find class from generic argument")
return gson.fromJson(data, genericType)
return gson.fromJson(data, clazz)
}
}

View File

@@ -0,0 +1,28 @@
<jasperReport>
<field name="FIRST_NAME" class="java.lang.String"/>
<field name="LAST_NAME" class="java.lang.String"/>
<field name="SALARY" class="java.lang.Double"/>
<field name="ID" class="java.lang.Integer"/>
<detail>
<band height="51" splitType="Stretch">
<textField>
<reportElement x="0" y="0" width="100" height="20"/>
<textElement/>
<textFieldExpression class="java.lang.String">
<![CDATA[$F{FIRST_NAME}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="100" y="0" width="100" height="20"/>
<textElement/>
<textFieldExpression class="java.lang.String">
<![CDATA[$F{LAST_NAME}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="200" y="0" width="100" height="20"/>
<textElement/>
<textFieldExpression class="java.lang.String">
<![CDATA[$F{SALARY}]]></textFieldExpression>
</textField>
</band>
</detail>
</jasperReport>

View File

@@ -0,0 +1,11 @@
{
"firstname" : "Jim",
"lastname" : "Brown",
"totalprice" : 500,
"depositpaid" : true,
"bookingdates" : {
"checkin" : "2025-01-01",
"checkout" : "2025-01-10"
},
"additionalneeds" : "Lunch"
}