Add initial project files

This commit is contained in:
2023-02-07 04:44:07 +00:00
commit af7291815d
27 changed files with 1415 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# IDE files
/.idea
/*.iml
# PDF's
data/*.pdf

10
.idea/.gitignore generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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 havent touched any webservice yet.
Like with everything: Dont stress yourself too much. If you notice, that you spend too much time
or cant get a grip around a task, theres 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 DeeSees 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 superheros 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
*

7
go.mod Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"))
}
}