diff --git a/.idea/misc.xml b/.idea/misc.xml index dd5e3ad..7ec6dce 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,7 +8,7 @@ - + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index f3e2172..e71109e 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,14 @@ MainKt + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + @@ -81,18 +89,17 @@ 5.10.0 test + + org.junit.jupiter + junit-jupiter-api + 5.10.0 + test + org.jetbrains.kotlin kotlin-stdlib 1.9.0 - - - io.rest-assured - rest-assured - 5.5.0 - test - org.junit.jupiter @@ -111,6 +118,12 @@ retrofit 2.11.0 + + + com.squareup.retrofit2 + converter-gson + 2.9.0 + com.squareup.okhttp3 logging-interceptor @@ -121,19 +134,11 @@ api-testing-automation-framework 1.0-SNAPSHOT - - - com.squareup.retrofit2 - converter-gson - 2.11.0 - - org.jetbrains.kotlinx kotlinx-coroutines-core 1.6.2 - org.apache.logging.log4j log4j-core @@ -144,6 +149,18 @@ log4j-api 2.19.0 + + + org.assertj + assertj-core + 3.26.3 + test + + + net.sf.jasperreports + jasperreports + 6.20.0 + \ No newline at end of file diff --git a/src/main/kotlin/api/BookerApi.kt b/src/main/kotlin/api/BookerApi.kt index 3fa771f..7d468a8 100644 --- a/src/main/kotlin/api/BookerApi.kt +++ b/src/main/kotlin/api/BookerApi.kt @@ -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( + object : X509TrustManager { + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array?, authType: String?) { + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array?, authType: String?) { + } + + override fun getAcceptedIssuers(): Array { + return arrayOfNulls(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) } diff --git a/src/main/kotlin/api/RestfulBookerApi.kt b/src/main/kotlin/api/RestfulBookerApi.kt index 738e267..3eaf812 100644 --- a/src/main/kotlin/api/RestfulBookerApi.kt +++ b/src/main/kotlin/api/RestfulBookerApi.kt @@ -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 @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> @GET("booking/{id}") @@ -26,7 +22,7 @@ interface RestfulBookerApi { @Path("id") id: String ): Response - @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 @DELETE("booking/{id}") suspend fun deleteBooking( - @Path("id") id: String + @Path("id") id: String, + @Header("Authorization") token: String ): Response } \ No newline at end of file diff --git a/src/main/kotlin/model/AuthRequest.kt b/src/main/kotlin/model/AuthRequest.kt new file mode 100644 index 0000000..0c5aa34 --- /dev/null +++ b/src/main/kotlin/model/AuthRequest.kt @@ -0,0 +1,6 @@ +package model + +data class AuthRequest( + val username: String, + val password: String +) diff --git a/src/main/kotlin/model/AuthResponse.kt b/src/main/kotlin/model/AuthResponse.kt index d8e0ac1..d8a5fa5 100644 --- a/src/main/kotlin/model/AuthResponse.kt +++ b/src/main/kotlin/model/AuthResponse.kt @@ -1,5 +1,5 @@ package model -class AuthResponse { - var token: String? = null -} +data class AuthResponse ( + val token: String +) diff --git a/src/main/kotlin/model/BookingIdResponse.kt b/src/main/kotlin/model/BookingIdResponse.kt index b1a2070..d56937e 100644 --- a/src/main/kotlin/model/BookingIdResponse.kt +++ b/src/main/kotlin/model/BookingIdResponse.kt @@ -1,5 +1,5 @@ package model -class BookingIdResponse { - var bookingid = 0 -} +data class BookingIdResponse( + val bookingid: Int +) diff --git a/src/main/kotlin/model/BookingRequest.kt b/src/main/kotlin/model/BookingRequest.kt index 9a5da1e..a721e6c 100644 --- a/src/main/kotlin/model/BookingRequest.kt +++ b/src/main/kotlin/model/BookingRequest.kt @@ -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, ) diff --git a/src/main/kotlin/model/BookingResponse.kt b/src/main/kotlin/model/BookingResponse.kt index 10082c5..424ae12 100644 --- a/src/main/kotlin/model/BookingResponse.kt +++ b/src/main/kotlin/model/BookingResponse.kt @@ -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 ) \ No newline at end of file diff --git a/src/main/kotlin/model/Bookingdates.kt b/src/main/kotlin/model/Bookingdates.kt index 6a1c1e7..efceaea 100644 --- a/src/main/kotlin/model/Bookingdates.kt +++ b/src/main/kotlin/model/Bookingdates.kt @@ -1,6 +1,6 @@ package model data class Bookingdates ( - var checkin: String? = null, - var checkout: String? = null, + var checkin: String, + var checkout: String, ) \ No newline at end of file diff --git a/src/main/kotlin/model/CreateBookingResponse.kt b/src/main/kotlin/model/CreateBookingResponse.kt index e280a85..3442f28 100644 --- a/src/main/kotlin/model/CreateBookingResponse.kt +++ b/src/main/kotlin/model/CreateBookingResponse.kt @@ -1,6 +1,6 @@ package model -class CreateBookingResponse { - var bookingid = 0 - var booking: BookingResponse? = null -} \ No newline at end of file +data class CreateBookingResponse( + var bookingid: Int, + var booking: BookingResponse +) \ No newline at end of file diff --git a/src/main/kotlin/model/UpdateBookingRequest.kt b/src/main/kotlin/model/UpdateBookingRequest.kt new file mode 100644 index 0000000..092e282 --- /dev/null +++ b/src/main/kotlin/model/UpdateBookingRequest.kt @@ -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, +) diff --git a/src/main/kotlin/storage/OrdersDatabase.kt b/src/main/kotlin/storage/OrdersDatabase.kt new file mode 100644 index 0000000..8b3ed62 --- /dev/null +++ b/src/main/kotlin/storage/OrdersDatabase.kt @@ -0,0 +1,92 @@ +package storage + +import model.BookingResponse +import model.Bookingdates + +class OrdersDatabase { + private val storage = mutableMapOf() + + // 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 { + 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) +} \ No newline at end of file diff --git a/src/main/resources/Log4j2.xml b/src/main/resources/Log4j2.xml new file mode 100644 index 0000000..3957a82 --- /dev/null +++ b/src/main/resources/Log4j2.xml @@ -0,0 +1,27 @@ + + + + C:\\logs + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/kotlin/NetworkTests.kt b/src/test/kotlin/NetworkTests.kt index f487b47..8f35e0e 100644 --- a/src/test/kotlin/NetworkTests.kt +++ b/src/test/kotlin/NetworkTests.kt @@ -4,6 +4,7 @@ import java.io.IOException abstract class NetworkTests { + // Call retrofit API and unwrap response or throw exception fun responseUnwrap( call: suspend () -> Response ): T { @@ -12,9 +13,11 @@ abstract class NetworkTests { if (response.isSuccessful) { return response.body()!! } else { - val error = response.errorBody()?.string() - - throw IOException(error ?: "Unable to handle end point") + val error = StringBuilder().append(response.code()).append(" : ") + .append(response.errorBody()?.string() ?: "Unable to handle end point").toString() + print(response.raw()) + throw IOException(error) } } + } \ No newline at end of file diff --git a/src/test/kotlin/Tests.kt b/src/test/kotlin/Tests.kt index e76947f..f32bf84 100644 --- a/src/test/kotlin/Tests.kt +++ b/src/test/kotlin/Tests.kt @@ -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("test1") - val bookingRequestTwo = fileReader.readJsonFileFromResources("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() } } \ No newline at end of file diff --git a/src/test/kotlin/utils/FileReader.kt b/src/test/kotlin/utils/FileReader.kt index bb64a91..00ad928 100644 --- a/src/test/kotlin/utils/FileReader.kt +++ b/src/test/kotlin/utils/FileReader.kt @@ -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 readJsonFileFromResources(fileName: String): T { - val iStream = this::class.java.getResourceAsStream("$fileName.json") + /** + * Read json files from resources and turn into object of type + */ + fun readJsonFileFromResources(fileName: String, clazz: Class): 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) - ?: throw IllegalStateException("Can not find class from generic argument") - - return gson.fromJson(data, genericType) + return gson.fromJson(data, clazz) } } \ No newline at end of file diff --git a/src/test/resources/reportTemplate.jrxml b/src/test/resources/reportTemplate.jrxml new file mode 100644 index 0000000..53be0d7 --- /dev/null +++ b/src/test/resources/reportTemplate.jrxml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/test3.json b/src/test/resources/test3.json new file mode 100644 index 0000000..8f86452 --- /dev/null +++ b/src/test/resources/test3.json @@ -0,0 +1,11 @@ +{ + "firstname" : "Jim", + "lastname" : "Brown", + "totalprice" : 500, + "depositpaid" : true, + "bookingdates" : { + "checkin" : "2025-01-01", + "checkout" : "2025-01-10" + }, + "additionalneeds" : "Lunch" +} \ No newline at end of file