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