Add initial project files
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# IDE files
|
||||||
|
/.idea
|
||||||
|
/*.iml
|
||||||
|
|
||||||
|
# PDF's
|
||||||
|
data/*.pdf
|
||||||
|
|
||||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# Zeppelin ignored files
|
||||||
|
/ZeppelinRemoteNotebooks/
|
||||||
9
.idea/markdown.xml
generated
Normal file
9
.idea/markdown.xml
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MarkdownSettings">
|
||||||
|
<enabledExtensions>
|
||||||
|
<entry key="MermaidLanguageExtension" value="true" />
|
||||||
|
<entry key="PlantUMLLanguageExtension" value="true" />
|
||||||
|
</enabledExtensions>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager">
|
||||||
|
<output url="file://$PROJECT_DIR$/../superhero/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/superherohub.iml" filepath="$PROJECT_DIR$/superherohub.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/php.xml
generated
Normal file
12
.idea/php.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MessDetectorOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCSFixerOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCodeSnifferOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal 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>
|
||||||
138
CHALLENGE.md
Normal file
138
CHALLENGE.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# Introduction: AOE Backend Challenge
|
||||||
|
|
||||||
|
This challenge is intended to get an idea of the knowledge and experience you have as a
|
||||||
|
developer. We would like to give you the opportunity to showcase your knowledge and skills
|
||||||
|
based on different requirements. Please read through this document carefully before starting
|
||||||
|
and have fun solving our backend challenge!
|
||||||
|
|
||||||
|
> Please implement the exercise using one of: java, kotlin, scala, go, php
|
||||||
|
|
||||||
|
## Challenge Overview
|
||||||
|
|
||||||
|
Our coding challenge is divided into 3 tasks which will be explained in more detail in chapter 2. To
|
||||||
|
give you an first impression, here are the tasks that will be required to be implemented:
|
||||||
|
|
||||||
|
* String encryption
|
||||||
|
* Domain modelling (with filter logic)
|
||||||
|
* Webservice combining the domain model, filter logic and encryption
|
||||||
|
|
||||||
|
Depending on your knowledge, experience and your available time you could consider skipping
|
||||||
|
the third challenge, i.e. if you apply as a working student and haven’t touched any webservice yet.
|
||||||
|
Like with everything: Don’t stress yourself too much. If you notice, that you spend too much time
|
||||||
|
or can’t get a grip around a task, there’s no shame in skipping an exercise. Just let us know in the
|
||||||
|
[readme](README.md) why you might have decided not to implement it.
|
||||||
|
|
||||||
|
## Evaluation Criteria
|
||||||
|
|
||||||
|
As this challenge is used to assess your experience and skill level, we want to be transparent
|
||||||
|
about our evaluation and give you some hints about what is important to us. The following points
|
||||||
|
in no particular order are some of the criteria we will analyse:
|
||||||
|
|
||||||
|
* Understandable code (naming, structure, readability)
|
||||||
|
* Project structure (package structure, reusability)
|
||||||
|
* Meaningful unit test coverage
|
||||||
|
|
||||||
|
## Submission
|
||||||
|
|
||||||
|
Please provide your implementation either as an archive via mail or preferably via github (private
|
||||||
|
repository, shared with: [code-review-backend@aoe.com](mailto:code-review-backend@aoe.com)).
|
||||||
|
Ensure to include the following things in your submission:
|
||||||
|
|
||||||
|
* Source code
|
||||||
|
* Readme
|
||||||
|
* Short explanation of how to run your code
|
||||||
|
* Optional: Notes on specific tasks if you want to clarify or explain something
|
||||||
|
|
||||||
|
Please provide all of your text documents as either txt, md or pdf files.
|
||||||
|
|
||||||
|
# Coding Challenge
|
||||||
|
|
||||||
|
Since the number of superheroes in our world is growing bigger and bigger, a company called
|
||||||
|
“DeeSee Comics” has instructed us to build a superhero distribution platform to keep track of
|
||||||
|
their employed superheroes. The first feature of the new platform will be a webservice that
|
||||||
|
provides a way to store and retrieve superheroes. Superheroes can be filtered by their powers and
|
||||||
|
their true identity can be encrypted to further provide protection for the ones enrolled in the
|
||||||
|
platform.
|
||||||
|
|
||||||
|
> **Note**: If you decide not to implement the webservice (2.3), please provide an executable code
|
||||||
|
> fragment, in which you showcase your implementation of 2.1 and 2.2. Therefor we recommend to
|
||||||
|
> just create a few superheroes, filter them by their superpowers and print the resulting superheroes
|
||||||
|
> with encrypted identities.
|
||||||
|
|
||||||
|
## Identity Encryption
|
||||||
|
|
||||||
|
Beginning with the most critical requirement: DeeSee requires the encryption of the true identity
|
||||||
|
of their superheroes. Therefore, it is essential to provide a function that takes an identity and
|
||||||
|
returns an encrypted identity. Contrary to our recommendation, DeeSee insists on using a
|
||||||
|
proprietary encryption called the “DeeSee Chiffre”.
|
||||||
|
|
||||||
|
To encrypt a string using the “DeeSee Chiffre”, you will need to shift each letter of the identity by a
|
||||||
|
key n. For the sake of this exercise, you can assume that identities only contain lowercase
|
||||||
|
characters and spaces. The following example might help you understand the algorithm:
|
||||||
|
|
||||||
|
| Input | Key | Output |
|
||||||
|
|----------------|-----|----------------|
|
||||||
|
| clark | 5 | hqfwp |
|
||||||
|
| cherry blossom | 3 | fkhuub eorvvrp |
|
||||||
|
|
||||||
|
> **Note**: Please make sure to implement the algorithm by yourself without using available solutions
|
||||||
|
> from the internet / libraries.
|
||||||
|
|
||||||
|
# Domain Model
|
||||||
|
|
||||||
|
Before continuing to the webservice, we will need a domain model to efficiently work with our
|
||||||
|
stored superheroes. The following json contains an example of a superhero which can be used to
|
||||||
|
infer the domain model.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "superman",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "clark",
|
||||||
|
"lastName": "kent"
|
||||||
|
},
|
||||||
|
"birthday": "1977-04-18",
|
||||||
|
"superpowers": [
|
||||||
|
"flight",
|
||||||
|
"strength",
|
||||||
|
"invulnerability"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To keep the system as simple as possible, DeeSee has agreed to only accept superheroes with
|
||||||
|
the following superpowers: `strength`, `speed`, `flight`, `invulnerability`, `healing`
|
||||||
|
Backend Coding Challenge
|
||||||
|
|
||||||
|
## Superhero
|
||||||
|
|
||||||
|
For the core of the new distribution platform, we will need a webservice that will handle all the
|
||||||
|
superheroes via an RESTful API. The microservice needs to provide possibilities to allow DeeSee
|
||||||
|
to store and retrieve their superheroes.
|
||||||
|
It is up to you whether you implement the webservice by hand or use a framework of your choice.
|
||||||
|
|
||||||
|
> Hint: Using a database for storing the data is not required nor encouraged. Keep it as simple as
|
||||||
|
> possible for this use-case. Data storage is not part of the evaluation.
|
||||||
|
|
||||||
|
> **Hint**: Test data should be loaded on startup. Therefor we have provided you a json file containing
|
||||||
|
> some sample data you can use. How you use the json is up to you. Chose the easiest solution and
|
||||||
|
> avoid wasting time for this as it is not part of the evaluation either.
|
||||||
|
|
||||||
|
### Superhero Retrieval
|
||||||
|
|
||||||
|
To allow DeeSee to quickly find matching superheroes for their crime prevention system, the
|
||||||
|
webservice is required to provide an interface to quickly retrieve superheroes using HTTP.
|
||||||
|
Based on DeeSee’s requirements, the service must be able to provide all superheroes at once as
|
||||||
|
well as provide all matching superheroes given a required superpower. In addition, our customer
|
||||||
|
wants to have the possibility to tell the service to have all returned superheroes’ identity
|
||||||
|
encrypted (using 2.1).
|
||||||
|
|
||||||
|
Instead of returning a superhero’s identity as the composed object described in 2.2, DeeSee has
|
||||||
|
requested to return the identity as a string in the form of “`$firstName $lastName`”.
|
||||||
|
|
||||||
|
The following list shows some use-cases that should be supported:
|
||||||
|
|
||||||
|
* Retrieve all superheroes
|
||||||
|
* Retrieve all superheroes with encrypted identities
|
||||||
|
* Retrieve superheroes that match given superpower(s)
|
||||||
|
* Retrieve superheroes that match given superpower(s) with encrypted identities
|
||||||
37
Makefile
Normal file
37
Makefile
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
DIST_NAME=superhero
|
||||||
|
DIST_PATH=dist
|
||||||
|
PACKAGE_PATH=github.com/eslider/superherohub
|
||||||
|
BINARY_PATH=${DIST_PATH}/${DIST_NAME}
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
build:
|
||||||
|
go build -o ${BINARY_PATH} ${PACKAGE_PATH}
|
||||||
|
|
||||||
|
|
||||||
|
# Run the project
|
||||||
|
run:
|
||||||
|
go build -o ${BINARY_PATH} ${PACKAGE_PATH}
|
||||||
|
${BINARY_PATH}
|
||||||
|
|
||||||
|
|
||||||
|
# Test and build the project
|
||||||
|
test:
|
||||||
|
go test -v -cover ${PACKAGE_PATH}/pkg/deesee
|
||||||
|
go test -v -cover ${PACKAGE_PATH}/pkg/people
|
||||||
|
go test -v -cover -count 10 ${PACKAGE_PATH}
|
||||||
|
|
||||||
|
# This test ork only by developer
|
||||||
|
# Please ignore this
|
||||||
|
test-store:
|
||||||
|
curl -v -X PUT \
|
||||||
|
-H 'Accept: */*' \
|
||||||
|
-H 'Connection: keep-alive' \
|
||||||
|
-H 'Cache-Control: no-cache' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"supermans","identity":{"firstName":"Kent","lastName":"Clark"},"superpowers":["flight","strength","invulnerability"],"birthday":"1977-04-18"}' \
|
||||||
|
http://127.0.0.1:8080/superheroes
|
||||||
|
|
||||||
|
# Clean the project
|
||||||
|
clean:
|
||||||
|
go clean
|
||||||
|
rm ${BINARY_PATH}
|
||||||
86
README.md
Normal file
86
README.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# **DeeSee Comics** Superhero Distribution Platform
|
||||||
|
|
||||||
|
Distribution platform to keep track of employed superheroes as a web service.
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This project is a solution to the AOE Backend Challenge.
|
||||||
|
It is a simple REST API that allows to only retrieve superheroes.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Web service that allows to retrieve superheroes.
|
||||||
|
* Superheroes can be [filtered by their powers](#api-documentation) and their true identity can be encrypted
|
||||||
|
to further provide protection for the ones enrolled in the platform.
|
||||||
|
* Encryption is done using a proprietary encryption called the “DeeSee Chiffre”.
|
||||||
|
* The superheroes are stored in a [json file](data/heros.json).
|
||||||
|
* The application can be configured via [environment variables](#configuration).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
* Golang 1.19
|
||||||
|
* Make 4.3
|
||||||
|
* Bash 5.1
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The application can be configured via environment variables:
|
||||||
|
|
||||||
|
* `PORT` - the port the application will listen on (default: 8080)
|
||||||
|
* `KEY` - the key used for the encryption (default: 5)
|
||||||
|
* `DATA_FILE` - the path to the json file containing the superheroes (default: data/superheroes.json)
|
||||||
|
|
||||||
|
## How to run
|
||||||
|
|
||||||
|
* `make run` to run the application
|
||||||
|
* `make build` to build the application
|
||||||
|
* `make clean` to clean the application
|
||||||
|
* `make test` to run the tests
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
### GET /superheroes
|
||||||
|
|
||||||
|
Returns a list of superheroes.
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
* (optional) `superpowers` - a comma separated list of superpowers to filter the superheroes by.
|
||||||
|
* (optional) `encrypted` - if set to `true`, the identity will be encrypted.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
`GET /superheroes?superpowers=strength,flight&encrypted=true`
|
||||||
|
|
||||||
|
#### Result
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "superman",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "hqfwp",
|
||||||
|
"lastName": "kent"
|
||||||
|
},
|
||||||
|
"birthday": "1977-04-18",
|
||||||
|
"superpowers": [
|
||||||
|
"flight",
|
||||||
|
"strength",
|
||||||
|
"invulnerability"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "batman",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "hqfwp",
|
||||||
|
"lastName": "kent"
|
||||||
|
},
|
||||||
|
"birthday": "1977-04-18",
|
||||||
|
"superpowers": [
|
||||||
|
"flight",
|
||||||
|
"strength",
|
||||||
|
"invulnerability"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
173
api/router.go
Normal file
173
api/router.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/eslider/superherohub/pkg/deesee"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Router is a custom router that encapsulates the mux.Router
|
||||||
|
type Router struct {
|
||||||
|
Heros []*deesee.Superhero // The Heros where superheroes are stored
|
||||||
|
router *mux.Router // Encapsulated mux router
|
||||||
|
logger *log.Logger // The logger to use for this router
|
||||||
|
key int // The key to use for DeeSee encryption
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRouter creates a new encapsulated router.
|
||||||
|
//
|
||||||
|
// The pattern I use is to define a custom router structure
|
||||||
|
// that has a `mux.Router` as the field and also encapsulates things
|
||||||
|
// like the JSON Heros to load, encryption-keys, database connection,
|
||||||
|
// application configuration, and so on.
|
||||||
|
//
|
||||||
|
// This makes it easy to update routes as they
|
||||||
|
// require different resources and development progresses.
|
||||||
|
//
|
||||||
|
// And handlers shouldn't have prefix like "handle", course they are methods of the Router
|
||||||
|
func NewRouter(heros []*deesee.Superhero, key int, logger *log.Logger) *Router {
|
||||||
|
// Set the logger
|
||||||
|
if logger == nil {
|
||||||
|
logger = log.Default()
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &Router{
|
||||||
|
Heros: heros,
|
||||||
|
key: key,
|
||||||
|
logger: logger,
|
||||||
|
router: mux.NewRouter(),
|
||||||
|
}
|
||||||
|
|
||||||
|
r.router.Use(r.loggingMiddleware)
|
||||||
|
|
||||||
|
// Set a custom 404 handler
|
||||||
|
r.router.NotFoundHandler = http.HandlerFunc(r.HandleNotFound)
|
||||||
|
|
||||||
|
// Set `getSuperHeroes` as the handler for the route
|
||||||
|
r.router.HandleFunc("/superheroes", r.GetSuperHeroes).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
// Set `putSuperHero` as the handler for the route
|
||||||
|
r.router.HandleFunc("/superheroes", r.StoreSuperHero).Methods(http.MethodPut)
|
||||||
|
|
||||||
|
// Enable CORS by uncommenting the following line
|
||||||
|
// router.Use(mux.CORSMethodMiddleware(router))
|
||||||
|
|
||||||
|
// Maybe better to use /api prefix for all routes?
|
||||||
|
// subrouter := router.PathPrefix("/api").Subrouter()
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHandler returns the encapsulated router
|
||||||
|
func (r *Router) GetHandler() http.Handler {
|
||||||
|
return r.router
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSuperHeroes returns a list of superheroes
|
||||||
|
// with optional filtering and encryption of identities.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// - Retrieve all superheroes
|
||||||
|
//
|
||||||
|
// GET /superheroes
|
||||||
|
//
|
||||||
|
// - Retrieve all superheroes with encrypted identities:
|
||||||
|
//
|
||||||
|
// GET /superheroes?encode=deesee
|
||||||
|
//
|
||||||
|
// - Retrieve superheroes that match given superpower(s)
|
||||||
|
//
|
||||||
|
// GET /superheroes?powers=flight,super-strength
|
||||||
|
//
|
||||||
|
// - Retrieve superheroes that match given superpower(s) with encrypted identities
|
||||||
|
//
|
||||||
|
// GET /superheroes?powers=flight,super-strength&encode=deesee
|
||||||
|
func (r *Router) GetSuperHeroes(w http.ResponseWriter, req *http.Request) {
|
||||||
|
var (
|
||||||
|
// Resulting
|
||||||
|
heroes = r.Heros
|
||||||
|
// Get query parameter values
|
||||||
|
params = req.URL.Query()
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter superheroes by superpowers?
|
||||||
|
// The name of the power should be long enough to search for, at least 3 chars.
|
||||||
|
if powerFilter := params.Get("superpowers"); len(powerFilter) > 2 {
|
||||||
|
|
||||||
|
// Here we use the `strings` package to split comma-separated power names.
|
||||||
|
// Maybe it's better to prevent send special chars by remove with regular expression.
|
||||||
|
powers := strings.Split(powerFilter, ",")
|
||||||
|
heroes = deesee.SearchByPowers(heroes, powers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt identities with DeeSee encryption algorithm?
|
||||||
|
if strings.EqualFold(params.Get("encrypted"), "true") {
|
||||||
|
// Encrypt identities
|
||||||
|
heroes = deesee.EncryptHerosIdentities(heroes, r.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
// Return JSON
|
||||||
|
if err = json.NewEncoder(w).Encode(heroes); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreSuperHero saves one superhero
|
||||||
|
func (r *Router) StoreSuperHero(w http.ResponseWriter, req *http.Request) {
|
||||||
|
var err error
|
||||||
|
hero := deesee.NewSuperhero()
|
||||||
|
if err = json.NewDecoder(req.Body).Decode(hero); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if superhero superpower is acceptable
|
||||||
|
if !hero.IsAcceptable() {
|
||||||
|
http.Error(w, fmt.Sprintf("Hero power is not acceptable: %s", hero.Name), http.StatusExpectationFailed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent duplicate superheroes
|
||||||
|
if deesee.FindByName(r.Heros, strings.TrimSpace(hero.Name)) != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Hero is already exists: %s", hero.Name), http.StatusConflict)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Heros = append(r.Heros, hero)
|
||||||
|
|
||||||
|
// Return JSON
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_, err = w.Write([]byte(`{"status": "ok"}`))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleNotFound is a custom 404 handler
|
||||||
|
func (r *Router) HandleNotFound(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
_, err := w.Write([]byte(`{"error": "not found"}`))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loggingMiddleware is a middleware that logs all requests
|
||||||
|
func (r *Router) loggingMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
r.logger.Printf("Request: %s %s", req.Method, req.RequestURI)
|
||||||
|
next.ServeHTTP(w, req)
|
||||||
|
})
|
||||||
|
}
|
||||||
111
data/alt-heros.json
Normal file
111
data/alt-heros.json
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Wonder Woman",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "Diana",
|
||||||
|
"lastName": "Prince"
|
||||||
|
},
|
||||||
|
"birthday": "1941-12-01",
|
||||||
|
"superpowers": [
|
||||||
|
"super strength",
|
||||||
|
"flight",
|
||||||
|
"combat skills"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Green Lantern",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "Hal",
|
||||||
|
"lastName": "Jordan"
|
||||||
|
},
|
||||||
|
"birthday": "1940-07-22",
|
||||||
|
"superpowers": [
|
||||||
|
"energy projection",
|
||||||
|
"flight"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Flash",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "Barry",
|
||||||
|
"lastName": "Allen"
|
||||||
|
},
|
||||||
|
"birthday": "1940-03-14",
|
||||||
|
"superpowers": [
|
||||||
|
"super speed",
|
||||||
|
"time travel"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Aquaman",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "Arthur",
|
||||||
|
"lastName": "Curry"
|
||||||
|
},
|
||||||
|
"birthday": "1941-11-14",
|
||||||
|
"superpowers": [
|
||||||
|
"super strength",
|
||||||
|
"aquatic adaptation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cyborg",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "Victor",
|
||||||
|
"lastName": "Stone"
|
||||||
|
},
|
||||||
|
"birthday": "1980-05-14",
|
||||||
|
"superpowers": [
|
||||||
|
"super strength",
|
||||||
|
"cybernetic enhancements"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Martian Manhunter",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "J'onn",
|
||||||
|
"lastName": "J'onzz"
|
||||||
|
},
|
||||||
|
"birthday": "1961-11-18",
|
||||||
|
"superpowers": [
|
||||||
|
"shape-shifting",
|
||||||
|
"telepathy"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Green Arrow",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "Oliver",
|
||||||
|
"lastName": "Queen"
|
||||||
|
},
|
||||||
|
"birthday": "1941-11-01",
|
||||||
|
"superpowers": [
|
||||||
|
"archery",
|
||||||
|
"wealth"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Batwoman",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "Katherine",
|
||||||
|
"lastName": "Kane"
|
||||||
|
},
|
||||||
|
"birthday": "1956-07-25",
|
||||||
|
"superpowers": [
|
||||||
|
"martial arts",
|
||||||
|
"detective skills"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hawkman",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "Carter",
|
||||||
|
"lastName": "Hall"
|
||||||
|
},
|
||||||
|
"birthday": "1940-06-25",
|
||||||
|
"superpowers": [
|
||||||
|
"flight",
|
||||||
|
"super strength"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
61
data/heros.json
Normal file
61
data/heros.json
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "superman",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "clark",
|
||||||
|
"lastName": "kent"
|
||||||
|
},
|
||||||
|
"birthday": "1977-04-18",
|
||||||
|
"superpowers": [
|
||||||
|
"flight",
|
||||||
|
"strength",
|
||||||
|
"invulnerability"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deadpool",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "wade",
|
||||||
|
"lastName": "wilson"
|
||||||
|
},
|
||||||
|
"birthday": "1973-11-22",
|
||||||
|
"superpowers": [
|
||||||
|
"healing"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "batman",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "bruce",
|
||||||
|
"lastName": "wayne"
|
||||||
|
},
|
||||||
|
"birthday": "1915-04-17",
|
||||||
|
"superpowers": [
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aquaman",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "arthur",
|
||||||
|
"lastName": "curry"
|
||||||
|
},
|
||||||
|
"birthday": "1986-01-29",
|
||||||
|
"superpowers": [
|
||||||
|
"flight",
|
||||||
|
"healing",
|
||||||
|
"strength"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "flash",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "barry",
|
||||||
|
"lastName": "allen"
|
||||||
|
},
|
||||||
|
"birthday": "1992-09-30",
|
||||||
|
"superpowers": [
|
||||||
|
"speed",
|
||||||
|
"healing"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
1
dist/.gitignore
vendored
Normal file
1
dist/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*
|
||||||
7
go.mod
Normal file
7
go.mod
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module github.com/eslider/superherohub
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/mux v1.8.0
|
||||||
|
)
|
||||||
84
main.go
Normal file
84
main.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/eslider/superherohub/api"
|
||||||
|
"github.com/eslider/superherohub/pkg/deesee"
|
||||||
|
"github.com/eslider/superherohub/pkg/file"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dataPath = "data/heros.json" // Heros to JSON file
|
||||||
|
port = 8080 // Port to serve
|
||||||
|
)
|
||||||
|
|
||||||
|
// main is the entry point of the application
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
key = 5 // DeeSee encryption key
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create logger
|
||||||
|
logger := log.Default()
|
||||||
|
|
||||||
|
// Get port from environment variable
|
||||||
|
if os.Getenv("PORT") != "" {
|
||||||
|
port, err = strconv.Atoi(os.Getenv("PORT"))
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("Invalid port number: %s", err.Error())
|
||||||
|
}
|
||||||
|
if port < 1 || port > 65535 {
|
||||||
|
logger.Print("Port number must be between 1 and 65535")
|
||||||
|
logger.Fatalf("Invalid port number : %d", port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get key from environment variable
|
||||||
|
if os.Getenv("KEY") != "" {
|
||||||
|
key, err = strconv.Atoi(os.Getenv("KEY"))
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("Invalid key: %s", err.Error())
|
||||||
|
}
|
||||||
|
//if key < 1 || key > 25 {
|
||||||
|
// logger.Print("Key number should be between 1 and 25")
|
||||||
|
// logger.Fatalf("Invalid key number : %d", key)
|
||||||
|
//}
|
||||||
|
} else {
|
||||||
|
logger.Fatalf("Key is not set. Please set KEY environment variable.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get data path from environment variable
|
||||||
|
if os.Getenv("DATA_PATH") != "" {
|
||||||
|
dataPath = os.Getenv("DATA_PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if data path exists
|
||||||
|
if !file.IsExist(dataPath) {
|
||||||
|
logger.Fatalf("Invalid data path: %s", dataPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
herous, err := deesee.Load(dataPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("Invalid data format: %s", dataPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new server
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", port), // Define port and address to serve
|
||||||
|
Handler: api.NewRouter(herous, key, logger).GetHandler(),
|
||||||
|
ErrorLog: logger,
|
||||||
|
ReadTimeout: 240 * time.Second, // Good practice: enforcing timeouts for serving.
|
||||||
|
WriteTimeout: 240 * time.Second, // Good practice: enforcing timeouts for serving.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve and handle error
|
||||||
|
if err := srv.ListenAndServe(); err != nil {
|
||||||
|
logger.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
75
pkg/deesee/codec.go
Normal file
75
pkg/deesee/codec.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// Package deesee contains functions for encrypting and decrypting text.
|
||||||
|
//
|
||||||
|
// Important:
|
||||||
|
// - The encryption is not secure at all, it's just a simple substitution cipher.
|
||||||
|
// - Only the English language is supported.
|
||||||
|
//
|
||||||
|
// Ideas:
|
||||||
|
// - write an encoder and construct like NewEncoder(key).Encode(any), but this is not required at this stage of the project.
|
||||||
|
// - improve deesee-encoder to work with annotations, something like `deesee:"encode"` to prevent encoding for not authorized properties
|
||||||
|
package deesee
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const (
|
||||||
|
a = 97 // ASCII code for a
|
||||||
|
z = 122 // ASCII code for z
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encrypt text and returns an encrypted using the "DeeSee Chiffre" encryption.
|
||||||
|
func Encrypt(text string, n int) string {
|
||||||
|
return chiffre(text, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt text and returns a decrypted using the "DeeSee Chiffre" encryption.
|
||||||
|
func Decrypt(text string, n int) string {
|
||||||
|
return chiffre(text, -n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// chiffre function takes a string and a key as input and returns the DeeSee encoded string
|
||||||
|
func chiffre(text string, key int) string {
|
||||||
|
var (
|
||||||
|
output strings.Builder // String builder to store the encoded string
|
||||||
|
ascii int // ASCII value of the current character
|
||||||
|
)
|
||||||
|
//
|
||||||
|
// Convert the text string to lowercase
|
||||||
|
text = strings.ToLower(text)
|
||||||
|
|
||||||
|
// Let the key be in the range of letters
|
||||||
|
key %= z - a - 1
|
||||||
|
|
||||||
|
// Iterate through each character in the text string
|
||||||
|
for _, char := range text {
|
||||||
|
// Convert the character to its ASCII value
|
||||||
|
ascii = int(char)
|
||||||
|
|
||||||
|
// Encode only if character is a letter
|
||||||
|
if isLetter(ascii) {
|
||||||
|
// Shift the character by the key
|
||||||
|
ascii += key
|
||||||
|
|
||||||
|
// Which direction are we shifting?
|
||||||
|
if key > 0 {
|
||||||
|
// If the character is now out of bounds
|
||||||
|
if ascii > z {
|
||||||
|
ascii = a + key - 2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the character is now out of bounds
|
||||||
|
if ascii < a {
|
||||||
|
ascii = z + key + 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the encoded character to the output string
|
||||||
|
output.WriteRune(rune(ascii))
|
||||||
|
}
|
||||||
|
return output.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// isLetter returns true if the given character is inside of ASCII letter diapason.
|
||||||
|
func isLetter(char int) bool {
|
||||||
|
return char >= a && char <= z
|
||||||
|
}
|
||||||
36
pkg/deesee/codec_test.go
Normal file
36
pkg/deesee/codec_test.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package deesee
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// TestEncrypt tests the Encrypt and Decrypt function.
|
||||||
|
func TestCryptDecrypt(t *testing.T) {
|
||||||
|
|
||||||
|
// Case is a struct to store test cases.
|
||||||
|
type Case struct {
|
||||||
|
name string
|
||||||
|
key int
|
||||||
|
A string
|
||||||
|
B string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the test cases.
|
||||||
|
for _, tt := range []Case{
|
||||||
|
{"Test encrypt-decrypt: simple text",
|
||||||
|
5, "clark", "hqfwp"},
|
||||||
|
{"Test encrypt-decrypt: text with shifted outbounds chars",
|
||||||
|
3, "cherry blossom", "fkhuub eorvvrp"},
|
||||||
|
{"Test encrypt-decrypt: text with shifted outbounds chars and non-letter chars",
|
||||||
|
3, "cherry123 blossom!", "fkhuub123 eorvvrp!"},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
enc := Encrypt(tt.A, tt.key)
|
||||||
|
if enc != tt.B {
|
||||||
|
t.Errorf("Encrypt() = %v, want %v", enc, tt.B)
|
||||||
|
}
|
||||||
|
dec := Decrypt(enc, tt.key)
|
||||||
|
if dec != tt.A {
|
||||||
|
t.Errorf("Decrypt() = %v, want %v", dec, tt.A)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
39
pkg/deesee/filter_test.go
Normal file
39
pkg/deesee/filter_test.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package deesee
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/eslider/superherohub/pkg/file"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unit Test Coverage
|
||||||
|
func TestFilterBySuperPowers(t *testing.T) {
|
||||||
|
rootPath, err := file.GetModRootPath()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected GetModuleRootPath to return module root path, got error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
persons, err := Load(rootPath + "/data/heros.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected Load to return persons, got error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test count loaded persons
|
||||||
|
if len(persons) < 1 {
|
||||||
|
t.Errorf("Expexted at least one person, got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with valid
|
||||||
|
if len(SearchByPowers(persons, []string{"healing"})) < 1 {
|
||||||
|
t.Errorf("Expected at least one person with healing superpower, got none")
|
||||||
|
}
|
||||||
|
// Test with valid and invalid
|
||||||
|
if len(SearchByPowers(persons, []string{"healing", "slowpoking"})) < 1 {
|
||||||
|
t.Errorf("Expected at least one person with healing superpower, got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with invalid
|
||||||
|
if len(SearchByPowers(persons, []string{"programming"})) > 1 {
|
||||||
|
t.Errorf("Expected no one person with programming superpower, got some")
|
||||||
|
}
|
||||||
|
}
|
||||||
124
pkg/deesee/model.go
Normal file
124
pkg/deesee/model.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package deesee
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/eslider/superherohub/pkg/people"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// superPower is a type for superpowers.
|
||||||
|
type superPower string
|
||||||
|
|
||||||
|
// List of allowed superpowers.
|
||||||
|
const (
|
||||||
|
strength superPower = "strength"
|
||||||
|
speed superPower = "speed"
|
||||||
|
flight superPower = "flight"
|
||||||
|
invulnerability superPower = "invulnerability"
|
||||||
|
healing superPower = "healing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// To keep the system as simple as possible,
|
||||||
|
// DeeSee has agreed to only accept superheroes with the following superpowers.
|
||||||
|
var allowedSuperPowers = [5]superPower{strength, speed, flight, invulnerability, healing}
|
||||||
|
|
||||||
|
// Superhero DeeSee person type.
|
||||||
|
type Superhero people.Person
|
||||||
|
|
||||||
|
// IsAcceptable checks if the superhero is acceptable.
|
||||||
|
func (s *Superhero) IsAcceptable() (r bool) {
|
||||||
|
// Check if the person has superpowers.
|
||||||
|
if *s.SuperPowers == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Check if the person has at least one acceptable superpower.
|
||||||
|
for _, p := range *s.SuperPowers {
|
||||||
|
if IsAcceptable(p) {
|
||||||
|
r = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSuperHero checks if the superhero is acceptable.
|
||||||
|
func (s *Superhero) IsSuperHero() bool {
|
||||||
|
return s.IsAcceptable()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has checks if the superhero has the given power.
|
||||||
|
func (s *Superhero) Has(power string) bool {
|
||||||
|
return s.SuperPowers.Contains(power)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAcceptable checks if the given power is allowed.
|
||||||
|
func IsAcceptable(power string) bool {
|
||||||
|
for _, p := range allowedSuperPowers {
|
||||||
|
if string(p) == power {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load JSON file and decode to `[]*Person` list
|
||||||
|
func Load(path string) (l []*Superhero, err error) {
|
||||||
|
var f *os.File
|
||||||
|
if f, err = os.Open(path); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Parse the request body
|
||||||
|
err = json.NewDecoder(f).Decode(&l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptHerosIdentities encrypts all persons identities with DeeSee encryption algorithm
|
||||||
|
func EncryptHerosIdentities(heros []*Superhero, key int) (r []*Superhero) {
|
||||||
|
for _, person := range heros {
|
||||||
|
p := &Superhero{
|
||||||
|
Name: person.Name,
|
||||||
|
Identity: EncryptIdentity(person.Identity, key),
|
||||||
|
SuperPowers: person.SuperPowers,
|
||||||
|
Birthday: person.Birthday,
|
||||||
|
}
|
||||||
|
r = append(r, p)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptIdentity encrypts person identity with DeeSee encryption algorithm
|
||||||
|
func EncryptIdentity(identity *people.Identity, key int) *people.Identity {
|
||||||
|
return &people.Identity{
|
||||||
|
FirstName: Encrypt(identity.FirstName, key),
|
||||||
|
LastName: Encrypt(identity.LastName, key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchByPowers from persons list
|
||||||
|
func SearchByPowers(heros []*Superhero, powers []string) (r []*Superhero) {
|
||||||
|
for _, hero := range heros {
|
||||||
|
for _, power := range powers {
|
||||||
|
if hero.IsSuperHero() && hero.Has(power) {
|
||||||
|
r = append(r, hero)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSuperhero creates a new superhero
|
||||||
|
func NewSuperhero() *Superhero {
|
||||||
|
return &Superhero{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByName from superheros list
|
||||||
|
func FindByName(heros []*Superhero, name string) *Superhero {
|
||||||
|
for _, hero := range heros {
|
||||||
|
if strings.EqualFold(name, hero.Name) {
|
||||||
|
return hero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
45
pkg/file/utils.go
Normal file
45
pkg/file/utils.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
// This package makes it easier to implement tests to load data from a files.
|
||||||
|
//
|
||||||
|
// NOTE: Although it was not explicitly mentioned in the task,
|
||||||
|
// I decided to make the GetModRootPath() function return path and an error if `go.mod` was not found.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetModRootPath get the root path of the module
|
||||||
|
func GetModRootPath() (string, error) {
|
||||||
|
// Get caller filePath
|
||||||
|
_, path, _, _ := runtime.Caller(0)
|
||||||
|
// Get base directory by file info
|
||||||
|
var prevPath string
|
||||||
|
for {
|
||||||
|
path = filepath.Dir(path)
|
||||||
|
|
||||||
|
if IsExist(path + "/go.mod") {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
// Break if we reach root directory: '/'
|
||||||
|
// Break if prevPath is same, maybe on Windows, something like C:// or c:\\
|
||||||
|
if prevPath == path || len(path) < 2 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
prevPath = path
|
||||||
|
}
|
||||||
|
return "", errors.New("go.mod not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExist the path and isn't directory
|
||||||
|
func IsExist(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !info.IsDir()
|
||||||
|
}
|
||||||
37
pkg/people/birthday.go
Normal file
37
pkg/people/birthday.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package people
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Declaring birthday layout constant
|
||||||
|
const birthdayLayout = "2006-01-02" // YYYY-MM-DD - is default birthday layout
|
||||||
|
|
||||||
|
// Birthday is a custom encapsulation type for time.Time to handle birthdays
|
||||||
|
// by decoding and encoding from and to JSON format
|
||||||
|
type Birthday struct {
|
||||||
|
time.Time // Embedding time.Time
|
||||||
|
err error // Time parsing error
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON decodes a date text to time.Time object
|
||||||
|
func (t *Birthday) UnmarshalJSON(bytes []byte) (err error) {
|
||||||
|
// 1. Decode the bytes into an int64
|
||||||
|
// 2. Parse the unix timestamp
|
||||||
|
|
||||||
|
// Is value quoted?
|
||||||
|
if rune(bytes[0]) == '"' || rune(bytes[0]) == '\'' {
|
||||||
|
bytes = bytes[1 : len(bytes)-1] // remove quotes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse string to time.Time
|
||||||
|
t.Time, t.err = time.Parse(birthdayLayout, string(bytes))
|
||||||
|
t.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON encodes a time.Time object to date JSON text
|
||||||
|
func (t Birthday) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(t.Time.Format(birthdayLayout))
|
||||||
|
}
|
||||||
7
pkg/people/identity.go
Normal file
7
pkg/people/identity.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package people
|
||||||
|
|
||||||
|
// Identity represents the true identity of a person
|
||||||
|
type Identity struct {
|
||||||
|
FirstName string `json:"firstName" deesee:"encode"`
|
||||||
|
LastName string `json:"lastName" deesee:"encode"`
|
||||||
|
}
|
||||||
76
pkg/people/person.go
Normal file
76
pkg/people/person.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package people
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Person data representation.
|
||||||
|
//
|
||||||
|
// NOTE:
|
||||||
|
//
|
||||||
|
// The reason the people package was created is reusability.
|
||||||
|
//
|
||||||
|
// Personalities are used more often and are more general
|
||||||
|
// in applications than superheroes.
|
||||||
|
//
|
||||||
|
// # Some additional reflection:
|
||||||
|
//
|
||||||
|
// * Superheroes are a special case of people.
|
||||||
|
// * Normal persons are not superheros at all, but:
|
||||||
|
// - An ordinary person, over time, can gain superpowers
|
||||||
|
// - Do we need to handle them as no superhero?
|
||||||
|
// - A superhero can lose his superpowers.
|
||||||
|
// - But he was already using them, so does that mean he's no longer a superhero?
|
||||||
|
// - Batman, as example has, no superpowers, so he is not a DeeSee "superhero".
|
||||||
|
//
|
||||||
|
// Additional to think about, is that People, a man,
|
||||||
|
// can have many personalities(natural, juridical, fantasy, artistic, fake),
|
||||||
|
// also superheros can become transformation(magically) to another person(becomes additional personalities),
|
||||||
|
// so the person becomes more superpowers? (rhetorical question)
|
||||||
|
//
|
||||||
|
// .
|
||||||
|
// Anyway, to work anything about, there is a `Person` a struct which has a Method `IsSuperHero() bool.
|
||||||
|
type Person struct {
|
||||||
|
// Name of a Person
|
||||||
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
// Identity of a Person.
|
||||||
|
// Sure, people can have many identities, fakes too.
|
||||||
|
// But at the time we handle only one-to-one person to identity relation
|
||||||
|
Identity *Identity `json:"identity"`
|
||||||
|
|
||||||
|
// SuperPowers list
|
||||||
|
SuperPowers *SuperPowers `json:"superpowers"`
|
||||||
|
|
||||||
|
// Birthday formatted as `YYYY-MM-DD` text string, there is no time zone and time.
|
||||||
|
// The format-Layout is defined in internal `birthdayLayout` constant
|
||||||
|
Birthday *Birthday `json:"birthday"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSuperHero the person?
|
||||||
|
func (p *Person) IsSuperHero() bool {
|
||||||
|
return p.SuperPowers != nil && len(*p.SuperPowers) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has person the superpower?
|
||||||
|
// In other words: is the person a superhero at all?
|
||||||
|
func (p *Person) Has(power string) bool {
|
||||||
|
// The excessive use of the IsSuperHero() method is intentional, but for clarity,
|
||||||
|
// because it better describes what's going on.
|
||||||
|
//
|
||||||
|
// In the real world we deal with optimization of methods
|
||||||
|
// and of course it's better not to do so...
|
||||||
|
return p.IsSuperHero() && p.SuperPowers.Contains(power)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal marshals the person to JSON
|
||||||
|
func (p *Person) Marshal() ([]byte, error) {
|
||||||
|
return json.Marshal(p)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unmarshal(js string) (p *Person, err error) {
|
||||||
|
p = &Person{}
|
||||||
|
err = json.Unmarshal([]byte(js), p)
|
||||||
|
return
|
||||||
|
}
|
||||||
48
pkg/people/person_test.go
Normal file
48
pkg/people/person_test.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package people
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestPersonModel tests the Unmarshalling and properties.
|
||||||
|
func TestPersonModel(t *testing.T) {
|
||||||
|
p, err := Unmarshal(`{"name": "batman","identity": {"firstName": "bruce","lastName": "wayne"},"birthday": "1915-04-17"}`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unmarshal() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Name != "batman" ||
|
||||||
|
p.Identity.FirstName != "bruce" ||
|
||||||
|
p.Identity.LastName != "wayne" ||
|
||||||
|
p.Birthday.Year() != 1915 {
|
||||||
|
t.Fatalf("Unmarshal() error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.IsSuperHero() {
|
||||||
|
t.Fatalf("Batman has no superpowers")
|
||||||
|
}
|
||||||
|
|
||||||
|
js, err := p.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(string(js), `"birthday":"1915-04-17"`) {
|
||||||
|
t.Fatalf("Marshal() birthday error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batman with superpowers
|
||||||
|
p, err = Unmarshal(`{"name": "batman","identity": {"firstName": "bruce","lastName": "wayne"},"birthday": "1915-04-17","superpowers": ["money","intelligence"]}`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unmarshal() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.IsSuperHero() || !p.Has("money") || !p.Has("intelligence") {
|
||||||
|
t.Fatalf("Batman has some superpowers as well")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Has("flight") {
|
||||||
|
t.Fatalf("Batman has no superpowers")
|
||||||
|
}
|
||||||
|
}
|
||||||
19
pkg/people/power.go
Normal file
19
pkg/people/power.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package people
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// SuperPowers list of an SuperHero.
|
||||||
|
// Normal person has no super-power?
|
||||||
|
// Perhaps we just don't know enough to distinguish the qualities in each individual person. Sad but true.
|
||||||
|
type SuperPowers []string
|
||||||
|
|
||||||
|
// Contains superpower list the strength?
|
||||||
|
// Not go idiomatic? Okay, we can discuss it.
|
||||||
|
func (s *SuperPowers) Contains(power string) bool {
|
||||||
|
for _, p := range *s {
|
||||||
|
if strings.EqualFold(p, power) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
153
router_test.go
Normal file
153
router_test.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/eslider/superherohub/api"
|
||||||
|
"github.com/eslider/superherohub/pkg/deesee"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestWebserviceGetSuperHeros tests the HandleWebserviceRequest function
|
||||||
|
func TestWebserviceGetSuperHeros(t *testing.T) {
|
||||||
|
// Load heros
|
||||||
|
heros, err := deesee.Load(dataPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error loading heros: %heros", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create router
|
||||||
|
router := api.NewRouter(heros, 5, nil)
|
||||||
|
|
||||||
|
// Test cases
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string // Test name
|
||||||
|
url string // URL to test
|
||||||
|
encrypted bool // Encrypted identities
|
||||||
|
filterBy *[]string // Superpowers to filter by
|
||||||
|
}{
|
||||||
|
{"Test retrieve all superheroes", "/superheroes", false, nil},
|
||||||
|
{"Test retrieve all encrypted identities", "/superheroes?encrypted=true", true, nil},
|
||||||
|
{"Test retrieve superheroes that match given superpower(s)", "/superheroes?superpowers=healing", false, &[]string{"healing"}},
|
||||||
|
{"Test retrieve superheroes that match given superpower(s) with encrypted identities", "/superheroes?superpowers=healing&encrypted=true", true, &[]string{"healing"}},
|
||||||
|
} {
|
||||||
|
// Test with valid domain model
|
||||||
|
req, err := http.NewRequest(http.MethodGet, tt.url, strings.NewReader(""))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Error creating request: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create response recorder
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.GetSuperHeroes(rec, req)
|
||||||
|
res := rec.Result()
|
||||||
|
|
||||||
|
// Test status code
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Expected status code %d, got %d", http.StatusOK, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test content type
|
||||||
|
if res.Header.Get("Content-Type") != "application/json" {
|
||||||
|
t.Errorf("Expected content type %s, got %s", "application/json", res.Header.Get("Content-Type"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test decode response
|
||||||
|
heros := &[]*deesee.Superhero{}
|
||||||
|
if err = json.NewDecoder(res.Body).Decode(heros); err != nil {
|
||||||
|
t.Fatalf("Error decoding response: %heros", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test non-nil response
|
||||||
|
if heros == nil {
|
||||||
|
t.Fatalf("Expected non-nil response")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test length of response
|
||||||
|
if len(*heros) < 1 {
|
||||||
|
t.Fatalf("Expected non-empty response")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test heros entries
|
||||||
|
for _, hero := range *heros {
|
||||||
|
|
||||||
|
// Test heros identities
|
||||||
|
if hero.Identity == nil {
|
||||||
|
t.Errorf("Expected identity to be %s, got %s", "unknown", hero.Identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test encrypted identities
|
||||||
|
if tt.encrypted {
|
||||||
|
if hero.Name == "superman" && (hero.Identity.FirstName != "hqfwp" || hero.Identity.LastName != "pjsy") {
|
||||||
|
t.Errorf("Expected encoded superman identity")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test superpowers
|
||||||
|
if hero.SuperPowers != nil {
|
||||||
|
for _, gotPower := range *hero.SuperPowers {
|
||||||
|
if !deesee.IsAcceptable(gotPower) {
|
||||||
|
t.Errorf("Expected superpower %s to be acceptable", gotPower)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test superpowers filter
|
||||||
|
if tt.filterBy != nil {
|
||||||
|
for _, wantPower := range *tt.filterBy {
|
||||||
|
if !hero.Has(wantPower) {
|
||||||
|
t.Errorf("Expected superpower %s to be in %s", wantPower, hero.SuperPowers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWebserviceStoreSuperhero tests the HandleWebserviceRequest function
|
||||||
|
// TODO: Test invalid domain models
|
||||||
|
func TestWebserviceStoreSuperhero(t *testing.T) {
|
||||||
|
// Load heros
|
||||||
|
heros, err := deesee.Load(dataPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error loading heros: %heros", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create router
|
||||||
|
router := api.NewRouter(heros, 5, nil)
|
||||||
|
|
||||||
|
// Test with valid domain model
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "/superheroes", strings.NewReader(`{
|
||||||
|
"name": "ironman",
|
||||||
|
"identity": {
|
||||||
|
"firstName": "tony",
|
||||||
|
"lastName": "stark"
|
||||||
|
},
|
||||||
|
"birthday": "1970-05-29",
|
||||||
|
"superpowers": [
|
||||||
|
"intelligence",
|
||||||
|
"flight"
|
||||||
|
]
|
||||||
|
}`))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Error creating request: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create response recorder
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.StoreSuperHero(rec, req)
|
||||||
|
res := rec.Result()
|
||||||
|
|
||||||
|
// Test status code
|
||||||
|
if res.StatusCode != http.StatusCreated {
|
||||||
|
t.Errorf("Expected status code %d, got %d", http.StatusCreated, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test content type
|
||||||
|
if res.Header.Get("Content-Type") != "application/json" {
|
||||||
|
t.Errorf("Expected content type %s, got %s", "application/json", res.Header.Get("Content-Type"))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user