View Code? Open in Web Editor
NEW
realworld app implementation using kotlin, http4k and exposed
Home Page: https://github.com/gothinkster/realworld
Kotlin 99.65%
Shell 0.35%
kotlin-http4k-realworld-example-app's Issues
Swagger description:
"post" : {
"summary" : " Create a comment for an article" ,
"description" : " Create a comment for an article. Auth is required" ,
"tags" : [
" Comments"
],
"security" : [
{
"Token" : []
}
],
"operationId" : " CreateArticleComment" ,
"parameters" : [
{
"name" : " slug" ,
"in" : " path" ,
"required" : true ,
"description" : " Slug of the article that you want to create a comment for" ,
"type" : " string"
},
{
"name" : " comment" ,
"in" : " body" ,
"required" : true ,
"description" : " Comment you want to create" ,
"schema" : {
"$ref" : " #/definitions/NewCommentRequest"
}
}
],
"responses" : {
"200" : {
"description" : " OK" ,
"schema" : {
"$ref" : " #/definitions/SingleCommentResponse"
}
},
"401" : {
"description" : " Unauthorized"
},
"422" : {
"description" : " Unexpected error" ,
"schema" : {
"$ref" : " #/definitions/GenericErrorModel"
}
}
}
}
},
Currently, handler classed does not contain the logic of the application. Instead, almost all the logic is implemented in ConduitRepository
. I think after using exposed having a separate class to hold all queries don't make sense anymore. The better abstraction would be having a (mockable) class which holds the database connection and transaction function.
The application uses time in different parts of the code. For example, when creating article. In order to be able to properly test handlers Clock
should be a dependency of the handler.
Currently, the token generation logic hard-codes the signing key and expiration. But these values should be configurable and provided by AppConfig
. Signing key should be provided as an environment variable.
Swagger description:
"/tags" : {
"get" : {
"summary" : " Get tags" ,
"description" : " Get tags. Auth not required" ,
"responses" : {
"200" : {
"description" : " OK" ,
"schema" : {
"$ref" : " #/definitions/TagsResponse"
}
},
"422" : {
"description" : " Unexpected error" ,
"schema" : {
"$ref" : " #/definitions/GenericErrorModel"
}
}
}
}
}
},
Swagger description:
"post" : {
"summary" : " Create an article" ,
"description" : " Create an article. Auth is required" ,
"tags" : [
" Articles"
],
"security" : [
{
"Token" : []
}
],
"operationId" : " CreateArticle" ,
"parameters" : [
{
"name" : " article" ,
"in" : " body" ,
"required" : true ,
"description" : " Article to create" ,
"schema" : {
"$ref" : " #/definitions/NewArticleRequest"
}
}
],
"responses" : {
"201" : {
"description" : " OK" ,
"schema" : {
"$ref" : " #/definitions/SingleArticleResponse"
}
},
"401" : {
"description" : " Unauthorized"
},
"422" : {
"description" : " Unexpected error" ,
"schema" : {
"$ref" : " #/definitions/GenericErrorModel"
}
}
}
}
},
$ javac -version
javac 11.0.6
$ ./gradlew run
...
[INFO ] o.e.j.s.Server - Started @3084ms
[INFO ] main - Server started on port 9000
curl http://localhost:9000/
(nothing)
Used Chrome: No webpage was found for the web address: http://localhost:9000/
HTTP ERROR 404
Looked with netstat. A server is listening on 9000:
$ netstat -ant|grep 9000
TCP 0.0.0.0:9000 0.0.0.0:0 LISTENING InHost
TCP [::]:9000 [::]:0 LISTENING InHost
TCP [::1]:9000 [::1]:59033 TIME_WAIT InHost
TCP [::1]:9000 [::1]:59034 TIME_WAIT InHost
There is only one class containing all the routes and their handlers. Having separated route handlers categorized by top-level endpoint name (e.g. tags, profiles etc) makes sense.
Swagger description:
"put" : {
"summary" : " Update an article" ,
"description" : " Update an article. Auth is required" ,
"tags" : [
" Articles"
],
"security" : [
{
"Token" : []
}
],
"operationId" : " UpdateArticle" ,
"parameters" : [
{
"name" : " slug" ,
"in" : " path" ,
"required" : true ,
"description" : " Slug of the article to update" ,
"type" : " string"
},
{
"name" : " article" ,
"in" : " body" ,
"required" : true ,
"description" : " Article to update" ,
"schema" : {
"$ref" : " #/definitions/UpdateArticleRequest"
}
}
],
"responses" : {
"200" : {
"description" : " OK" ,
"schema" : {
"$ref" : " #/definitions/SingleArticleResponse"
}
},
"401" : {
"description" : " Unauthorized"
},
"422" : {
"description" : " Unexpected error" ,
"schema" : {
"$ref" : " #/definitions/GenericErrorModel"
}
}
}
},
Swagger description:
"/articles/{slug}/comments/{id}" : {
"delete" : {
"summary" : " Delete a comment for an article" ,
"description" : " Delete a comment for an article. Auth is required" ,
"tags" : [
" Comments"
],
"security" : [
{
"Token" : []
}
],
"operationId" : " DeleteArticleComment" ,
"parameters" : [
{
"name" : " slug" ,
"in" : " path" ,
"required" : true ,
"description" : " Slug of the article that you want to delete a comment for" ,
"type" : " string"
},
{
"name" : " id" ,
"in" : " path" ,
"required" : true ,
"description" : " ID of the comment you want to delete" ,
"type" : " integer"
}
],
"responses" : {
"200" : {
"description" : " OK"
},
"401" : {
"description" : " Unauthorized"
},
"422" : {
"description" : " Unexpected error" ,
"schema" : {
"$ref" : " #/definitions/GenericErrorModel"
}
}
}
}
},
Suggested test cases:
Should throw proper exception when article not found.
Should call insertComment
of repository with proper values.
Should return the created comment
Swagger description:
"/articles/{slug}/favorite" : {
"post" : {
"summary" : " Favorite an article" ,
"description" : " Favorite an article. Auth is required" ,
"tags" : [
" Favorites"
],
"security" : [
{
"Token" : []
}
],
"operationId" : " CreateArticleFavorite" ,
"parameters" : [
{
"name" : " slug" ,
"in" : " path" ,
"required" : true ,
"description" : " Slug of the article that you want to favorite" ,
"type" : " string"
}
],
"responses" : {
"200" : {
"description" : " OK" ,
"schema" : {
"$ref" : " #/definitions/SingleArticleResponse"
}
},
"401" : {
"description" : " Unauthorized"
},
"422" : {
"description" : " Unexpected error" ,
"schema" : {
"$ref" : " #/definitions/GenericErrorModel"
}
}
}
},
Suggested test cases:
The user should be able to only delete comment created by themselves.
It should call deleteArticleComment
from repository.
After upgrading to Kotlin 1.3 it is possible to use inline classes for opaque types.
Swagger docs:
"delete" : {
"summary" : " Unfollow a user" ,
"description" : " Unfollow a user by username" ,
"tags" : [
" Profile"
],
"security" : [
{
"Token" : []
}
],
"operationId" : " UnfollowUserByUsername" ,
"parameters" : [
{
"name" : " username" ,
"in" : " path" ,
"description" : " Username of the profile you want to unfollow" ,
"required" : true ,
"type" : " string"
}
],
"responses" : {
"200" : {
"description" : " OK" ,
"schema" : {
"$ref" : " #/definitions/ProfileResponse"
}
},
"401" : {
"description" : " Unauthorized"
},
"422" : {
"description" : " Unexpected error" ,
"schema" : {
"$ref" : " #/definitions/GenericErrorModel"
}
}
}
}
},
http4k is so nicely designed and it helps to write integration tests much easier. One idea could be to remove some of the unit tests (or replace unit tests with some general non-related-to-business tests) and test the whole business of the application based on the postman test file using integration tests.
Swagger description:
"/articles/feed" : {
"get" : {
"summary" : " Get recent articles from users you follow" ,
"description" : " Get most recent articles from users you follow. Use query parameters to limit. Auth is required" ,
"tags" : [
" Articles"
],
"security" : [
{
"Token" : []
}
],
"operationId" : " GetArticlesFeed" ,
"parameters" : [
{
"name" : " limit" ,
"in" : " query" ,
"description" : " Limit number of articles returned (default is 20)" ,
"required" : false ,
"default" : 20 ,
"type" : " integer"
},
{
"name" : " offset" ,
"in" : " query" ,
"description" : " Offset/skip number of articles (default is 0)" ,
"required" : false ,
"default" : 0 ,
"type" : " integer"
}
],
"responses" : {
"200" : {
"description" : " OK" ,
"schema" : {
"$ref" : " #/definitions/MultipleArticlesResponse"
}
},
"401" : {
"description" : " Unauthorized"
},
"422" : {
"description" : " Unexpected error" ,
"schema" : {
"$ref" : " #/definitions/GenericErrorModel"
}
}
}
}
},
Suggested test cases:
It should throw exception if the article does not belong to the current user.
It should call deleteArticle
from repository.
it should throw exception when the article doesn't exist.
Swagger description:
"/articles/{slug}/comments" : {
"get" : {
"summary" : " Get comments for an article" ,
"description" : " Get the comments for an article. Auth is optional" ,
"tags" : [
" Comments"
],
"operationId" : " GetArticleComments" ,
"parameters" : [
{
"name" : " slug" ,
"in" : " path" ,
"required" : true ,
"description" : " Slug of the article that you want to get comments for" ,
"type" : " string"
}
],
"responses" : {
"200" : {
"description" : " OK" ,
"schema" : {
"$ref" : " #/definitions/MultipleCommentsResponse"
}
},
"401" : {
"description" : " Unauthorized"
},
"422" : {
"description" : " Unexpected error" ,
"schema" : {
"$ref" : " #/definitions/GenericErrorModel"
}
}
}
},
Suggested test cases:
It should throw proper exception when article does not exist.
If the user has not been favorited the article before, the handler should not throw exceptions.
It should throw exception when the article author not found.
It should return proper values for the return object.
Suggested test cases:
Article slug should properly being generated from title.
It should call insertArticle
from repository.
It should return the created article with proper initial values.
Swagger description:
"/articles" : {
"get" : {
"summary" : " Get recent articles globally" ,
"description" : " Get most recent articles globally. Use query parameters to filter results. Auth is optional" ,
"tags" : [
" Articles"
],
"operationId" : " GetArticles" ,
"parameters" : [
{
"name" : " tag" ,
"in" : " query" ,
"description" : " Filter by tag" ,
"required" : false ,
"type" : " string"
},
{
"name" : " author" ,
"in" : " query" ,
"description" : " Filter by author (username)" ,
"required" : false ,
"type" : " string"
},
{
"name" : " favorited" ,
"in" : " query" ,
"description" : " Filter by favorites of a user (username)" ,
"required" : false ,
"type" : " string"
},
{
"name" : " limit" ,
"in" : " query" ,
"description" : " Limit number of articles returned (default is 20)" ,
"required" : false ,
"default" : 20 ,
"type" : " integer"
},
{
"name" : " offset" ,
"in" : " query" ,
"description" : " Offset/skip number of articles (default is 0)" ,
"required" : false ,
"default" : 0 ,
"type" : " integer"
}
],
"responses" : {
"200" : {
"description" : " OK" ,
"schema" : {
"$ref" : " #/definitions/MultipleArticlesResponse"
}
},
"401" : {
"description" : " Unauthorized"
},
"422" : {
"description" : " Unexpected error" ,
"schema" : {
"$ref" : " #/definitions/GenericErrorModel"
}
}
}
},
Swagger description:
"delete" : {
"summary" : " Unfavorite an article" ,
"description" : " Unfavorite an article. Auth is required" ,
"tags" : [
" Favorites"
],
"security" : [
{
"Token" : []
}
],
"operationId" : " DeleteArticleFavorite" ,
"parameters" : [
{
"name" : " slug" ,
"in" : " path" ,
"required" : true ,
"description" : " Slug of the article that you want to unfavorite" ,
"type" : " string"
}
],
"responses" : {
"200" : {
"description" : " OK" ,
"schema" : {
"$ref" : " #/definitions/SingleArticleResponse"
}
},
"401" : {
"description" : " Unauthorized"
},
"422" : {
"description" : " Unexpected error" ,
"schema" : {
"$ref" : " #/definitions/GenericErrorModel"
}
}
}
}
},
Suggested test cases:
Should throw proper exception when article not found.
If it is not a favorited article, it should call insertFavorite
of repository.
Should throw proper exception when article doesn't have an author.
The favoritesCount
in the return value object should has been increased by one.
Suggested test cases:
Article slug should properly being generated from title.
It should call insertArticle
from repository.
It should return the created article with proper initial values.
Swagger description:
"/articles/{slug}" : {
"get" : {
"summary" : " Get an article" ,
"description" : " Get an article. Auth not required" ,
"tags" : [
" Articles"
],
"operationId" : " GetArticle" ,
"parameters" : [
{
"name" : " slug" ,
"in" : " path" ,
"required" : true ,
"description" : " Slug of the article to get" ,
"type" : " string"
}
],
"responses" : {
"200" : {
"description" : " OK" ,
"schema" : {
"$ref" : " #/definitions/SingleArticleResponse"
}
},
"422" : {
"description" : " Unexpected error" ,
"schema" : {
"$ref" : " #/definitions/GenericErrorModel"
}
}
}
},
Swagger description:
"delete" : {
"summary" : " Delete an article" ,
"description" : " Delete an article. Auth is required" ,
"tags" : [
" Articles"
],
"security" : [
{
"Token" : []
}
],
"operationId" : " DeleteArticle" ,
"parameters" : [
{
"name" : " slug" ,
"in" : " path" ,
"required" : true ,
"description" : " Slug of the article to delete" ,
"type" : " string"
}
],
"responses" : {
"200" : {
"description" : " OK"
},
"401" : {
"description" : " Unauthorized"
},
"422" : {
"description" : " Unexpected error" ,
"schema" : {
"$ref" : " #/definitions/GenericErrorModel"
}
}
}
}
},
model related classes are scattered everywhere in the project. They should belong to the right package and right place.