Skip to content

Commit 2b1ac2a

Browse files
author
Walter
committedMay 8, 2021
New project structure and tests
1 parent 323017c commit 2b1ac2a

22 files changed

+334
-20
lines changed
 

‎Dockerfile

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM openjdk:14-alpine AS build
2+
COPY . /sentency-server
3+
WORKDIR /sentency-server
4+
RUN ./gradlew assemble
5+
6+
FROM openjdk:14-alpine
7+
EXPOSE 7000
8+
RUN mkdir /app
9+
COPY --from=build /sentency-server/build/libs/*.jar /app/sentency-server.jar
10+
RUN mkdir /resources
11+
COPY --from=build /sentency-server/resources/*.env /resources/
12+
ENTRYPOINT ["java","-jar","/app/sentency-server.jar"]

‎LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 Walter
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎README.md

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Sentency Server
2+
3+
The server will be done using Javalin, a simple web framework for Java and Kotlin.
4+
5+
## Third-Party Libraries
6+
7+
Javalin come with built-in libraries but it's mostly a "plug what you need" type of framework, because of that we had to
8+
add some third party libraries to fill some gaps:
9+
10+
* [Ktor](https://ktor.io/): Ktor is an asynchronous framework for creating microservices, web applications, and more.
11+
* [Exposed](https://github.com/JetBrains/Exposed): an ORM framework for Kotlin created by JetBrains
12+
13+
## Docker
14+
15+
The project is configured to be deployed using docker. To build the docker image go to the root of the project and run
16+
the command:
17+
18+
```bash
19+
docker build -t sentency-server .
20+
```
21+
22+
After the image is build to run locally you can execute:
23+
24+
```bash
25+
docker run -p 5000:7000 sentency-server:latest
26+
```
27+
28+
Open the address **http://localhost:5000/docs** on your browser and you will see this project Swagger with
29+
APIs.
30+
31+
## Environment variables
32+
33+
The project use .env libraries to load environment variables. To change any parameter change the value inside
34+
the file **development.env** that's inside the resource folder.
35+
36+
If it's necessary to create a new .env file (production file for example), the file should comply with the following
37+
pattern:
38+
39+
```bash
40+
ENVIRONMENT = development
41+
PREFIX = server
42+
```
43+
44+
Inside the **Application.kt** the *loadEnvVariables()* function need to be update to match the new file
45+
name and folder parameters

‎build.gradle.kts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
val ktor_version: String by project
2+
val kotlin_version: String by project
3+
val logback_version: String by project
4+
5+
plugins {
6+
application
7+
kotlin("jvm") version "1.5.0"
8+
kotlin("plugin.serialization") version "1.5.0"
9+
}
10+
11+
group = "org.wcode"
12+
version = "0.0.1"
13+
application {
14+
mainClass.set("org.wcode.ApplicationKt")
15+
}
16+
17+
repositories {
18+
mavenCentral()
19+
}
20+
21+
dependencies {
22+
implementation("io.ktor:ktor-server-core:$ktor_version")
23+
implementation("io.ktor:ktor-auth:$ktor_version")
24+
implementation("io.ktor:ktor-serialization:$ktor_version")
25+
implementation("io.ktor:ktor-locations:$ktor_version")
26+
implementation("io.ktor:ktor-client-core:$ktor_version")
27+
implementation("io.ktor:ktor-client-core-jvm:$ktor_version")
28+
implementation("io.ktor:ktor-client-apache:$ktor_version")
29+
implementation("io.ktor:ktor-server-jetty:$ktor_version")
30+
implementation("ch.qos.logback:logback-classic:$logback_version")
31+
testImplementation("io.ktor:ktor-server-tests:$ktor_version")
32+
}

‎gradle.properties

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ktor_version=1.5.4
2+
kotlin_version=1.5.0
3+
logback_version=1.2.3
4+
kotlin.code.style=official

‎gradle/wrapper/gradle-wrapper.jar

293 Bytes
Binary file not shown.
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip
44
zipStoreBase=GRADLE_USER_HOME
55
zipStorePath=wrapper/dists

‎gradlew

+1-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ fi
130130
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131131
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132132
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133-
133+
134134
JAVACMD=`cygpath --unix "$JAVACMD"`
135135

136136
# We build the pattern for arguments to be converted via cygpath

‎gradlew.bat

+3-18
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
4040

4141
set JAVA_EXE=java.exe
4242
%JAVA_EXE% -version >NUL 2>&1
43-
if "%ERRORLEVEL%" == "0" goto init
43+
if "%ERRORLEVEL%" == "0" goto execute
4444

4545
echo.
4646
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -54,7 +54,7 @@ goto fail
5454
set JAVA_HOME=%JAVA_HOME:"=%
5555
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
5656

57-
if exist "%JAVA_EXE%" goto init
57+
if exist "%JAVA_EXE%" goto execute
5858

5959
echo.
6060
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -64,29 +64,14 @@ echo location of your Java installation.
6464

6565
goto fail
6666

67-
:init
68-
@rem Get command-line arguments, handling Windows variants
69-
70-
if not "%OS%" == "Windows_NT" goto win9xME_args
71-
72-
:win9xME_args
73-
@rem Slurp the command line arguments.
74-
set CMD_LINE_ARGS=
75-
set _SKIP=2
76-
77-
:win9xME_args_slurp
78-
if "x%~1" == "x" goto execute
79-
80-
set CMD_LINE_ARGS=%*
81-
8267
:execute
8368
@rem Setup the command line
8469

8570
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
8671

8772

8873
@rem Execute Gradle
89-
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
74+
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
9075

9176
:end
9277
@rem End local scope for the variables with windows NT shell

‎resources/development.env

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ENVIRONMENT = development
2+
DB_USER = user
3+
DB_NAME = sentency_db
4+
DB_PASSWORD = eYPpJe8XBdqMaZ8j4C7QnqVcSSVctQ
5+
JWT_SECRET_KEY = SOMESECURESECRETKEY

‎resources/production.env

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ENVIRONMENT = production
2+
DB_USER = user
3+
DB_NAME = sentency_db
4+
DB_PASSWORD = eYPpJe8XBdqMaZ8j4C7QnqVcSSVctQ
5+
JWT_SECRET_KEY = SOMESECURESECRETKEY

‎sentency-server.iml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<module external.linked.project.id="sentency-server" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" type="JAVA_MODULE" version="4">
3+
<component name="NewModuleRootManager" inherit-compiler-output="true">
4+
<exclude-output />
5+
<content url="file://$MODULE_DIR$" />
6+
<orderEntry type="sourceFolder" forTests="false" />
7+
</component>
8+
</module>

‎settings.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = "sentency-server"
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.wcode
2+
3+
import io.ktor.application.*
4+
import io.ktor.server.engine.*
5+
import io.ktor.server.jetty.*
6+
import org.wcode.plugins.*
7+
8+
fun main() {
9+
embeddedServer(Jetty, port = 8080, host = "0.0.0.0") {
10+
setupApplication()
11+
}.start(wait = true)
12+
}
13+
14+
fun Application.setupApplication(){
15+
configureModules()
16+
configureRouting()
17+
configureHTTP()
18+
}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.wcode.models
2+
3+
import kotlinx.serialization.Serializable
4+
import kotlinx.serialization.encodeToString
5+
import kotlinx.serialization.json.Json
6+
import java.util.*
7+
8+
@Serializable
9+
data class Quote(val id: String = UUID.randomUUID().toString(), val message: String, val authorId: String) {
10+
11+
fun toJson(): String {
12+
return Json.encodeToString(this)
13+
}
14+
}
15+
16+
val quoteStorage = mutableListOf<Quote>()
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.wcode.plugins
2+
3+
import io.ktor.http.*
4+
import io.ktor.application.*
5+
import io.ktor.features.*
6+
import io.ktor.response.*
7+
import io.ktor.request.*
8+
9+
fun Application.configureHTTP() {
10+
install(CORS) {
11+
method(HttpMethod.Options)
12+
method(HttpMethod.Put)
13+
method(HttpMethod.Delete)
14+
method(HttpMethod.Patch)
15+
header(HttpHeaders.Authorization)
16+
header("MyCustomHeader")
17+
allowCredentials = true
18+
anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.wcode.plugins
2+
3+
import io.ktor.application.*
4+
import io.ktor.features.*
5+
import io.ktor.serialization.*
6+
7+
fun Application.configureModules() {
8+
install(ContentNegotiation) {
9+
json()
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.wcode.plugins
2+
3+
import io.ktor.routing.*
4+
import io.ktor.application.*
5+
import io.ktor.response.*
6+
import org.wcode.routes.quoteRouting
7+
8+
fun Application.configureRouting() {
9+
routing {
10+
get("/") {
11+
call.respondText("Hello World!")
12+
}
13+
quoteRouting()
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.wcode.routes
2+
3+
import io.ktor.application.*
4+
import io.ktor.http.*
5+
import io.ktor.request.*
6+
import io.ktor.response.*
7+
import io.ktor.routing.*
8+
import org.wcode.models.Quote
9+
import org.wcode.models.quoteStorage
10+
import java.util.*
11+
12+
fun Route.quoteRouting(){
13+
route("/quotes") {
14+
get {
15+
if (quoteStorage.isNotEmpty()) {
16+
call.respond(quoteStorage)
17+
} else {
18+
call.respondText("No quotes found", status = HttpStatusCode.NotFound)
19+
}
20+
}
21+
get("{id}") {
22+
val id = call.parameters["id"] ?: return@get call.respondText(
23+
"Missing or malformed id",
24+
status = HttpStatusCode.BadRequest
25+
)
26+
val quote =
27+
quoteStorage.find { it.id == id } ?: return@get call.respondText(
28+
"No quote with id $id",
29+
status = HttpStatusCode.NotFound
30+
)
31+
call.respond(quote)
32+
}
33+
post {
34+
val quote = call.receive<Quote>()
35+
quoteStorage.add(quote)
36+
call.respondText("Quote stored correctly", status = HttpStatusCode.Created)
37+
}
38+
delete("{id}") {
39+
val id = call.parameters["id"] ?: return@delete call.respond(HttpStatusCode.BadRequest)
40+
if (quoteStorage.removeIf { it.id == id }) {
41+
call.respondText("Quote removed correctly", status = HttpStatusCode.Accepted)
42+
} else {
43+
call.respondText("Not Found", status = HttpStatusCode.NotFound)
44+
}
45+
}
46+
}
47+
}

‎src/main/resources/logback.xml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<configuration>
2+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
3+
<encoder>
4+
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
5+
</encoder>
6+
</appender>
7+
<root level="trace">
8+
<appender-ref ref="STDOUT"/>
9+
</root>
10+
<logger name="org.eclipse.jetty" level="INFO"/>
11+
<logger name="io.netty" level="INFO"/>
12+
</configuration>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.wcode
2+
3+
import io.ktor.http.*
4+
import kotlin.test.*
5+
import io.ktor.server.testing.*
6+
import org.wcode.plugins.configureRouting
7+
8+
class ApplicationTest {
9+
@Test
10+
fun testRoot() {
11+
withTestApplication({ configureRouting() }) {
12+
handleRequest(HttpMethod.Get, "/").apply {
13+
assertEquals(HttpStatusCode.OK, response.status())
14+
assertEquals("Hello World!", response.content)
15+
}
16+
}
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.wcode.routes
2+
3+
import io.ktor.http.*
4+
import io.ktor.server.testing.*
5+
import org.junit.Test
6+
import org.wcode.models.Quote
7+
import org.wcode.setupApplication
8+
import kotlin.test.assertEquals
9+
10+
class QuoteRoutesTest {
11+
12+
@Test
13+
fun `Add Customer with ID`() {
14+
val quote = Quote(id = "Teste", message = "Test", authorId = "Test")
15+
withTestApplication({ setupApplication() }) {
16+
handleRequest(HttpMethod.Post, "/quotes") {
17+
addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
18+
setBody(quote.toJson())
19+
}.apply {
20+
assertEquals(HttpStatusCode.Created, response.status())
21+
assertEquals("Quote stored correctly", response.content)
22+
}
23+
}
24+
}
25+
26+
@Test
27+
fun `Add Customer without ID`() {
28+
val quote = Quote( message = "Test", authorId = "Test")
29+
withTestApplication({ setupApplication() }) {
30+
handleRequest(HttpMethod.Post, "/quotes") {
31+
addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
32+
setBody(quote.toJson())
33+
}.apply {
34+
assertEquals(HttpStatusCode.Created, response.status())
35+
assertEquals("Quote stored correctly", response.content)
36+
}
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)
Please sign in to comment.