adding initial commit for CRUD user

This commit is contained in:
rinosukmandityo
2020-02-04 08:18:02 +07:00
parent b7f55adbd0
commit 60d040afd6
22 changed files with 1508 additions and 0 deletions

250
Gopkg.lock generated Normal file
View File

@@ -0,0 +1,250 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:938f79d09131c6204cee2c363a6e9066a369bc1977f09028ca549f44a836b734"
name = "github.com/DataDog/zstd"
packages = ["."]
pruneopts = "UT"
revision = "8fdb579680349497546a04291cedfff98a7829a0"
version = "v1.4.4"
[[projects]]
digest = "1:10d6aa505e0698b315d19aa9fe91bcf8e7cb3fa93a7acca41f1b41c6e669050b"
name = "github.com/go-chi/chi"
packages = [
".",
"middleware",
]
pruneopts = "UT"
revision = "2db61557f36b2f4caa20ce6ef50e98bac0f7fce5"
version = "v4.0.3"
[[projects]]
digest = "1:e17320223c7c866dfbec405efe8abc8e383ccad131fb76ca7ff70580dafb9067"
name = "github.com/go-redis/redis"
packages = [
".",
"internal",
"internal/consistenthash",
"internal/hashtag",
"internal/pool",
"internal/proto",
"internal/util",
]
pruneopts = "UT"
revision = "99cd690a7019656b0b67c6664453f27a6d8a658e"
version = "v6.15.7"
[[projects]]
digest = "1:586ea76dbd0374d6fb649a91d70d652b7fe0ccffb8910a77468e7702e7901f3d"
name = "github.com/go-stack/stack"
packages = ["."]
pruneopts = "UT"
revision = "2fee6af1a9795aafbe0253a0cfbdf668e1fb8a9a"
version = "v1.8.0"
[[projects]]
digest = "1:d1e35b720b5f5156502ffdd174000c81295919293735342da78582e4dc7a8bcd"
name = "github.com/golang/protobuf"
packages = ["proto"]
pruneopts = "UT"
revision = "d23c5127dc24889085f8ccea5c9d560a57a879d8"
version = "v1.3.3"
[[projects]]
branch = "master"
digest = "1:700b1e527116332847452edf9febc0c252c026785a8c3cfa3f68a8eeffb09ee8"
name = "github.com/golang/snappy"
packages = ["."]
pruneopts = "UT"
revision = "ff6b7dc882cf4cfba7ee0b9f7dcc1ac096c554aa"
[[projects]]
digest = "1:803b281410ec195645c782ddb9cb029d43c2855d4b7824ea31104985b507a458"
name = "github.com/leodido/go-urn"
packages = ["."]
pruneopts = "UT"
revision = "a0f5013415294bb94553821ace21a1a74c0298cc"
version = "v1.2.0"
[[projects]]
digest = "1:9e1d37b58d17113ec3cb5608ac0382313c5b59470b94ed97d0976e69c7022314"
name = "github.com/pkg/errors"
packages = ["."]
pruneopts = "UT"
revision = "614d223910a179a466c1767a985424175c39b465"
version = "v0.9.1"
[[projects]]
digest = "1:c08f5d8cc28ed4dccb098121d5f1f19f708bf04e3bbd4b772ce6461de6ef1ca2"
name = "github.com/teris-io/shortid"
packages = ["."]
pruneopts = "UT"
revision = "6c56cef5189ca1b3d5ef01dc07f4d611dfc0bb33"
version = "v1.0"
[[projects]]
digest = "1:bdbeee77ecd82dc2e9bdc6deab3739fb968d953f8660b5577c74585a119db40c"
name = "github.com/vmihailenco/msgpack"
packages = [
".",
"codes",
]
pruneopts = "UT"
revision = "cd92a145e6d2ce09e792f838dc0b669b680aea29"
version = "v4.1.1"
[[projects]]
digest = "1:a6c5ab44d09b463220e70b89e20e55e182ba4f7d15144697fe8dfc449d553e73"
name = "github.com/vmihailenco/tagparser"
packages = [
".",
"internal",
"internal/parser",
]
pruneopts = "UT"
revision = "e4b6c8cf4f3d7bb572551d5a531a9f2068b2fd6e"
version = "v0.1.1"
[[projects]]
branch = "master"
digest = "1:40fdfd6ab85ca32b6935853bbba35935dcb1d796c8135efd85947566c76e662e"
name = "github.com/xdg/scram"
packages = ["."]
pruneopts = "UT"
revision = "7eeb5667e42c09cb51bf7b7c28aea8c56767da90"
[[projects]]
branch = "master"
digest = "1:f5c1d04bc09c644c592b45b9f0bad4030521b1a7d11c7dadbb272d9439fa6e8e"
name = "github.com/xdg/stringprep"
packages = ["."]
pruneopts = "UT"
revision = "73f8eece6fdcd902c185bf651de50f3828bed5ed"
[[projects]]
digest = "1:f44e6646a34c7d251bcde34fe679235317340d0374a2d9f992a5fbf52d921aa0"
name = "go.mongodb.org/mongo-driver"
packages = [
"bson",
"bson/bsoncodec",
"bson/bsonoptions",
"bson/bsonrw",
"bson/bsontype",
"bson/primitive",
"event",
"internal",
"mongo",
"mongo/options",
"mongo/readconcern",
"mongo/readpref",
"mongo/writeconcern",
"tag",
"version",
"x/bsonx",
"x/bsonx/bsoncore",
"x/mongo/driver",
"x/mongo/driver/address",
"x/mongo/driver/auth",
"x/mongo/driver/auth/internal/gssapi",
"x/mongo/driver/connstring",
"x/mongo/driver/description",
"x/mongo/driver/dns",
"x/mongo/driver/mongocrypt",
"x/mongo/driver/mongocrypt/options",
"x/mongo/driver/operation",
"x/mongo/driver/session",
"x/mongo/driver/topology",
"x/mongo/driver/uuid",
"x/mongo/driver/wiremessage",
]
pruneopts = "UT"
revision = "55b507b3e45a2ef5dc1d5a4cba3333c45dce443c"
version = "v1.2.1"
[[projects]]
branch = "master"
digest = "1:f92f6956e4059f6a3efc14924d2dd58ba90da25cc57fe07ae3779ef2f5e0c5f2"
name = "golang.org/x/crypto"
packages = ["pbkdf2"]
pruneopts = "UT"
revision = "69ecbb4d6d5dab05e49161c6e77ea40a030884e1"
[[projects]]
branch = "master"
digest = "1:76ee51c3f468493aff39dbacc401e8831fbb765104cbf613b89bef01cf4bad70"
name = "golang.org/x/net"
packages = ["context"]
pruneopts = "UT"
revision = "16171245cfb220d5317888b716d69c1fb4e7992b"
[[projects]]
branch = "master"
digest = "1:382bb5a7fb4034db3b6a2d19e5a4a6bcf52f4750530603c01ca18a172fa3089b"
name = "golang.org/x/sync"
packages = ["semaphore"]
pruneopts = "UT"
revision = "cd5d95a43a6e21273425c7ae415d3df9ea832eeb"
[[projects]]
digest = "1:1093f2eb4b344996604f7d8b29a16c5b22ab9e1b25652140d3fede39f640d5cd"
name = "golang.org/x/text"
packages = [
"internal/gen",
"internal/triegen",
"internal/ucd",
"transform",
"unicode/cldr",
"unicode/norm",
]
pruneopts = "UT"
revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475"
version = "v0.3.2"
[[projects]]
digest = "1:f4d6f1de81abede35943b5f9d8106c9a4579d3ea5a19d0991be3055e3383254b"
name = "google.golang.org/appengine"
packages = [
".",
"datastore",
"datastore/internal/cloudkey",
"datastore/internal/cloudpb",
"internal",
"internal/app_identity",
"internal/base",
"internal/datastore",
"internal/log",
"internal/modules",
"internal/remote_api",
]
pruneopts = "UT"
revision = "971852bfffca25b069c31162ae8f247a3dba083b"
version = "v1.6.5"
[[projects]]
digest = "1:cde090148c51ba93f2ae5fafb626cfb210cf8fb45b56365da936055f1fd93101"
name = "gopkg.in/dealancer/validate.v2"
packages = ["."]
pruneopts = "UT"
revision = "2c9520160d3ebdb70e66ae17cbc5b3b9ef81f364"
version = "v2.1.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/go-chi/chi",
"github.com/go-chi/chi/middleware",
"github.com/go-redis/redis",
"github.com/pkg/errors",
"github.com/teris-io/shortid",
"github.com/vmihailenco/msgpack",
"go.mongodb.org/mongo-driver/bson",
"go.mongodb.org/mongo-driver/mongo",
"go.mongodb.org/mongo-driver/mongo/options",
"go.mongodb.org/mongo-driver/mongo/readpref",
"gopkg.in/dealancer/validate.v2",
]
solver-name = "gps-cdcl"
solver-version = 1

58
Gopkg.toml Normal file
View File

@@ -0,0 +1,58 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/go-chi/chi"
version = "4.0.3"
[[constraint]]
name = "github.com/go-redis/redis"
version = "6.15.7"
[[constraint]]
name = "github.com/pkg/errors"
version = "0.9.1"
[[constraint]]
name = "github.com/teris-io/shortid"
version = "1.0.0"
[[constraint]]
name = "github.com/vmihailenco/msgpack"
version = "4.1.1"
[[constraint]]
name = "go.mongodb.org/mongo-driver"
version = "1.2.1"
[[constraint]]
name = "gopkg.in/dealancer/validate.v2"
version = "2.1.0"
[prune]
go-tests = true
unused-packages = true

21
api/handler.go Normal file
View File

@@ -0,0 +1,21 @@
package api
import (
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
)
func RegisterHandler(handler UserHandler) *chi.Mux {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
// r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/{id}", handler.Get)
r.Post("/", handler.Post)
r.Post("/update", handler.Update)
r.Post("/delete", handler.Delete)
return r
}

14
api/response.go Normal file
View File

@@ -0,0 +1,14 @@
package api
import (
"log"
"net/http"
)
func SetupResponse(w http.ResponseWriter, contentType string, body []byte, statusCode int) {
w.Header().Set("Content-Type", contentType)
w.WriteHeader(statusCode)
if _, e := w.Write(body); e != nil {
log.Println(e)
}
}

19
api/serializer.go Normal file
View File

@@ -0,0 +1,19 @@
package api
import (
slz "github.com/rinosukmandityo/hexagonal-login/serializer"
js "github.com/rinosukmandityo/hexagonal-login/serializer/json"
ms "github.com/rinosukmandityo/hexagonal-login/serializer/msgpack"
)
var (
ContentTypeJson = "application/json"
ContentTypeMsgPack = "application/x-msgpack"
)
func GetSerializer(contentType string) slz.UserSerializer {
if contentType == ContentTypeMsgPack {
return &ms.User{}
}
return &js.User{}
}

116
api/user_http.go Normal file
View File

@@ -0,0 +1,116 @@
package api
import (
"io/ioutil"
"net/http"
"github.com/rinosukmandityo/hexagonal-login/logic"
svc "github.com/rinosukmandityo/hexagonal-login/services"
"github.com/go-chi/chi"
"github.com/pkg/errors"
)
type UserHandler interface {
Get(http.ResponseWriter, *http.Request)
Post(http.ResponseWriter, *http.Request)
Update(http.ResponseWriter, *http.Request)
Delete(http.ResponseWriter, *http.Request)
}
type userhandler struct {
userService svc.UserService
}
func NewUserHandler(userService svc.UserService) UserHandler {
return &userhandler{userService}
}
func (u *userhandler) Get(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
user, e := u.userService.GetById(id)
if e != nil {
if errors.Cause(e) == logic.ErrUserNotFound {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
contentType := r.Header.Get("Content-Type")
respBody, e := GetSerializer(contentType).Encode(user)
if e != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
SetupResponse(w, contentType, respBody, http.StatusFound)
}
func (u *userhandler) Post(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
requestBody, e := ioutil.ReadAll(r.Body)
if e != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
user, e := GetSerializer(contentType).Decode(requestBody)
if e != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if e = u.userService.Store(user); e != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
respBody, e := GetSerializer(contentType).Encode(user)
if e != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
SetupResponse(w, contentType, respBody, http.StatusCreated)
}
func (u *userhandler) Update(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
requestBody, e := ioutil.ReadAll(r.Body)
if e != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
user, e := GetSerializer(contentType).Decode(requestBody)
if e != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if e = u.userService.Update(user); e != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
respBody, e := GetSerializer(contentType).Encode(user)
if e != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
SetupResponse(w, contentType, respBody, http.StatusOK)
}
func (u *userhandler) Delete(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
requestBody, e := ioutil.ReadAll(r.Body)
if e != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
user, e := GetSerializer(contentType).Decode(requestBody)
if e != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if e = u.userService.Delete(user); e != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
respBody, e := GetSerializer(contentType).Encode(user)
SetupResponse(w, contentType, respBody, http.StatusOK)
}

258
api/user_http_test.go Normal file
View File

@@ -0,0 +1,258 @@
package api
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
h "github.com/rinosukmandityo/hexagonal-login/api"
"github.com/rinosukmandityo/hexagonal-login/helper"
"github.com/rinosukmandityo/hexagonal-login/logic"
m "github.com/rinosukmandityo/hexagonal-login/models"
repo "github.com/rinosukmandityo/hexagonal-login/repositories"
"github.com/go-chi/chi"
)
/*
==================
RUN FROM TERMINAL
==================
set mongo_url=mongodb://localhost:27017/local
set mongo_timeout=10
set mongo_db=local
set url_db=mongo
*/
var (
userRepo repo.UserRepository
r *chi.Mux
)
func UserTestData() []m.User {
return []m.User{{
Name: "User 01",
Username: "username01",
Password: "Password.1",
ID: "userid01",
Email: "usermail01@gmail.com",
Address: "User Address 01",
IsActive: false,
}, {
Name: "User 02",
Username: "username02",
ID: "userid02",
Password: "Password.1",
Email: "usermail02@gmail.com",
Address: "User Address 02",
IsActive: false,
}, {
Name: "User 03",
ID: "userid03",
Password: "Password.1",
Username: "username03",
Email: "usermail03@gmail.com",
Address: "User Address 03",
IsActive: false,
}}
}
func init() {
userRepo = helper.ChooseRepo()
userService := logic.NewUserService(userRepo)
handler := h.NewUserHandler(userService)
r = h.RegisterHandler(handler)
}
func readUserData(resp *http.Response) (*m.User, error) {
body, e := ioutil.ReadAll(resp.Body)
if e != nil {
return nil, e
}
defer resp.Body.Close()
user, _ := h.GetSerializer(h.ContentTypeJson).Decode(body)
return user, nil
}
func PostData(t *testing.T, ts *httptest.Server, url string, _data m.User) error {
dataBytes, e := getBytes(_data)
if e != nil {
return e
}
resp, _, e := makeRequest(t, ts, "POST", url, bytes.NewReader(dataBytes))
if e != nil {
return e
}
switch url {
case "/":
if resp.StatusCode != http.StatusCreated {
return errors.New("status should be 'Status Created' (201)")
}
default:
if resp.StatusCode != http.StatusOK {
return errors.New("status should be 'Status OK' (200)")
}
}
return nil
}
func GetData(t *testing.T, ts *httptest.Server, url, expected string) error {
resp, body, e := makeRequest(t, ts, "GET", url, nil)
if e != nil {
return e
}
if resp.StatusCode != http.StatusFound && strings.Contains(body, expected) {
return errors.New("status should be 'Status Found' (302)")
}
return nil
}
func getBytes(_data m.User) ([]byte, error) {
dataBytes, e := h.GetSerializer(h.ContentTypeJson).Encode(&_data)
if e != nil {
return dataBytes, e
}
return dataBytes, nil
}
func TestInsertUser(t *testing.T) {
// t.Skip()
userService := logic.NewUserService(userRepo)
testdata := UserTestData()
wg := sync.WaitGroup{}
ts := httptest.NewServer(r)
defer ts.Close()
t.Run("Case 1: Save data", func(t *testing.T) {
for _, data := range testdata {
wg.Add(1)
go func(_data m.User) {
if e := PostData(t, ts, "/", _data); e != nil {
t.Errorf("[ERROR] - Failed to save data %s ", e.Error())
}
wg.Done()
}(data)
}
wg.Wait()
for _, data := range testdata {
res, e := userService.GetById(data.ID)
if e != nil || res.ID == "" {
t.Errorf("[ERROR] - Failed to get data")
}
}
})
t.Run("Case 2: Negative Test", func(t *testing.T) {
t.Run("Case 2.1: Duplicate username", func(t *testing.T) {
_data := testdata[0]
_data.ID = "userid04"
if e := PostData(t, ts, "/", _data); e == nil {
t.Error("[ERROR] - duplicate validation username is not working")
}
})
t.Run("Case 2.2: Duplicate ID", func(t *testing.T) {
_data := testdata[0]
_data.Username = "username04"
if e := PostData(t, ts, "/", _data); e == nil {
t.Error("[ERROR] - duplicate validation ID is not working")
}
})
})
}
func TestUpdateUser(t *testing.T) {
// t.Skip()
testdata := UserTestData()
ts := httptest.NewServer(r)
t.Run("Case 1: Update data", func(t *testing.T) {
_data := testdata[0]
_data.Username = _data.Username + "UPDATED"
if e := PostData(t, ts, "/update", _data); e != nil {
t.Errorf("[ERROR] - Failed to update data %s ", e.Error())
}
})
t.Run("Case 2: Negative Test", func(t *testing.T) {
_data := m.User{ID: "ID DID NOT EXISTS"}
if e := PostData(t, ts, "/update", _data); e == nil {
t.Error("[ERROR] - It should be error 'User Not Found'")
}
})
}
func TestDeleteUser(t *testing.T) {
// t.Skip()
testdata := UserTestData()
ts := httptest.NewServer(r)
t.Run("Case 1: Delete data", func(t *testing.T) {
_data := testdata[1]
if e := PostData(t, ts, "/delete", _data); e != nil {
t.Errorf("[ERROR] - Failed to delete data %s ", e.Error())
}
})
t.Run("Case 2: Negative Test", func(t *testing.T) {
_data := testdata[1]
if e := PostData(t, ts, "/delete", _data); e == nil {
t.Error("[ERROR] - It should be error 'User Not Found'")
}
})
}
func TestGetDataById(t *testing.T) {
// t.Skip()
testdata := UserTestData()
ts := httptest.NewServer(r)
t.Run("Case 1: Get Data", func(t *testing.T) {
_data := testdata[0]
if e := GetData(t, ts, fmt.Sprintf("/%s", _data.ID), _data.ID); e != nil {
t.Errorf("[ERROR] - Failed to get data %s", e.Error())
}
})
t.Run("Case 2: Negative Test", func(t *testing.T) {
if e := GetData(t, ts, "/ID-DID-NOT-EXISTS", ""); e == nil {
t.Error("[ERROR] - It should be error 'Data Not Found'")
}
})
}
func makeRequest(t *testing.T, ts *httptest.Server, method, path string, body io.Reader) (*http.Response, string, error) {
req, e := http.NewRequest(method, ts.URL+path, body)
if e != nil {
return nil, "", e
}
req.Header.Set("Content-Type", h.ContentTypeJson)
var resp *http.Response
switch method {
case "GET":
resp, e = http.DefaultTransport.RoundTrip(req)
default:
resp, e = http.DefaultClient.Do(req)
}
if e != nil {
return nil, "", e
}
respBody, e := ioutil.ReadAll(resp.Body)
if e != nil {
return nil, "", e
}
defer resp.Body.Close()
return resp, string(respBody), nil
}

43
helper/repo.go Normal file
View File

@@ -0,0 +1,43 @@
package helper
import (
"log"
"os"
"strconv"
repo "github.com/rinosukmandityo/hexagonal-login/repositories"
mr "github.com/rinosukmandityo/hexagonal-login/repositories/mongodb"
rr "github.com/rinosukmandityo/hexagonal-login/repositories/redis"
)
func ChooseRepo() repo.UserRepository {
switch os.Getenv("url_db") {
case "redis":
redisURL := os.Getenv("redis_url")
repo, e := rr.NewUserRedisRepository(redisURL)
if e != nil {
log.Fatal(e)
}
return repo
default:
mongoURL := os.Getenv("mongo_url")
if mongoURL == "" {
mongoURL = "mongodb://localhost:27017/local"
}
mongoDB := os.Getenv("mongo_db")
if mongoDB == "" {
mongoDB = "local"
}
mongoTimeout, _ := strconv.Atoi(os.Getenv("mongo_timeout"))
if mongoTimeout == 0 {
mongoTimeout = 10
}
repo, e := mr.NewUserMongoRepository(mongoURL, mongoDB, mongoTimeout)
if e != nil {
log.Fatal(e)
}
return repo
}
return nil
}

30
logic/login_logic.go Normal file
View File

@@ -0,0 +1,30 @@
package logic
import (
m "github.com/rinosukmandityo/hexagonal-login/models"
repo "github.com/rinosukmandityo/hexagonal-login/repositories"
svc "github.com/rinosukmandityo/hexagonal-login/services"
)
type loginService struct {
loginRepo repo.LoginRepository
}
func NewLoginService(loginRepo repo.LoginRepository) svc.LoginService {
return &loginService{
loginRepo,
}
}
func (u *loginService) Authenticate(username, password string) (bool, *m.User, error) {
return u.loginRepo.Authenticate(username, password)
}
func (u *loginService) ChangePassword(user m.User, newpassword string) error {
return u.loginRepo.ChangePassword(user, newpassword)
}
func (u *loginService) UserActivation(activationkey string) error {
return u.loginRepo.UserActivation(activationkey)
}

71
logic/user_logic.go Normal file
View File

@@ -0,0 +1,71 @@
package logic
import (
"errors"
m "github.com/rinosukmandityo/hexagonal-login/models"
repo "github.com/rinosukmandityo/hexagonal-login/repositories"
svc "github.com/rinosukmandityo/hexagonal-login/services"
errs "github.com/pkg/errors"
"github.com/teris-io/shortid"
"gopkg.in/dealancer/validate.v2"
)
type userService struct {
userRepo repo.UserRepository
}
var (
ErrUserNotFound = errors.New("User Not Found")
ErrUserInvalid = errors.New("User Invalid")
ErrUserNameDuplicate = errors.New("User Name Already Exists")
)
func NewUserService(userRepo repo.UserRepository) svc.UserService {
return &userService{
userRepo,
}
}
func (u *userService) GetAll() ([]m.User, error) {
return u.userRepo.GetAll()
}
func (u *userService) GetById(id string) (*m.User, error) {
return u.userRepo.GetById(id)
}
func (u *userService) Store(user *m.User) error {
if e := validate.Validate(user); e != nil {
return errs.Wrap(ErrUserInvalid, "service.User.Store")
}
if user.ID == "" {
user.ID = shortid.MustGenerate()
}
if isFound, _, _ := u.userRepo.GetByUsername(user.Username); isFound {
return errs.Wrap(ErrUserNameDuplicate, "service.User.Store")
}
return u.userRepo.Store(user)
}
func (u *userService) Update(user *m.User) error {
if e := validate.Validate(user); e != nil {
return errs.Wrap(ErrUserInvalid, "service.User.Update")
}
if user.ID == "" {
user.ID = shortid.MustGenerate()
}
return u.userRepo.Update(user)
}
func (u *userService) Delete(user *m.User) error {
if user.ID == "" {
return errs.Wrap(ErrUserNotFound, "service.User.Delete")
}
if e := u.userRepo.Delete(user); e != nil {
return e
}
return nil
}

55
main.go Normal file
View File

@@ -0,0 +1,55 @@
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
h "github.com/rinosukmandityo/hexagonal-login/api"
"github.com/rinosukmandityo/hexagonal-login/helper"
"github.com/rinosukmandityo/hexagonal-login/logic"
)
/*
==================
RUN FROM TERMINAL
==================
set mongo_url=mongodb://localhost:27017/local
set mongo_timeout=30
set mongo_db=local
set url_db=mongo
*/
func main() {
userRepo := helper.ChooseRepo()
userService := logic.NewUserService(userRepo)
handler := h.NewUserHandler(userService)
r := h.RegisterHandler(handler)
errs := make(chan error, 2)
go func() {
log.Printf("Listening on port %s\n", httpPort())
errs <- http.ListenAndServe(httpPort(), r)
}()
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
log.Printf("Terminated %s", <-errs)
}
func httpPort() string {
port := "8000"
if os.Getenv("port") != "" {
port = os.Getenv("port")
}
return fmt.Sprintf(":%s", port)
}

30
models/user_model.go Normal file
View File

@@ -0,0 +1,30 @@
package models
type User struct {
ID string `json:"ID" bson:"_id" msgpack:"_id"`
Username string `json:"Username" bson:"Username" msgpack:"Username"`
Email string `json:"Email" bson:"Email" msgpack:"Email"`
Password string `json:"Password" bson:"Password" msgpack:"Password"`
Name string `json:"Name" bson:"Name" msgpack:"Name"`
Address string `json:"Address" bson:"Address" msgpack:"Address"`
IsActive bool `json:"IsActive" bson:"IsActive" msgpack:"IsActive"`
}
func NewUser() *User {
m := new(User)
m.IsActive = true
return m
}
func (m *User) TableName() string {
return "users"
}
func NewUserDefaultData() *User {
user := NewUser()
user.ID = "u001"
user.Username = "user001"
user.Password = "Password.1"
user.Name = "User 001"
return user
}

View File

@@ -0,0 +1,11 @@
package repositories
import (
m "github.com/rinosukmandityo/hexagonal-login/models"
)
type LoginRepository interface {
Authenticate(username, password string) (bool, *m.User, error)
ChangePassword(user m.User, newpassword string) error
UserActivation(activationkey string) error
}

View File

@@ -0,0 +1,132 @@
package mongo
import (
"context"
"time"
"github.com/rinosukmandityo/hexagonal-login/logic"
m "github.com/rinosukmandityo/hexagonal-login/models"
repo "github.com/rinosukmandityo/hexagonal-login/repositories"
"github.com/pkg/errors"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)
type userMongoRepository struct {
client *mongo.Client
database string
timeout time.Duration
}
func newUserMongoClient(mongoURL string, mongoTimeout int) (*mongo.Client, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(mongoTimeout)*time.Second)
defer cancel()
client, e := mongo.Connect(ctx, options.Client().ApplyURI(mongoURL))
if e != nil {
return nil, e
}
if e = client.Ping(ctx, readpref.Primary()); e != nil {
return nil, e
}
return client, e
}
func NewUserMongoRepository(mongoURL, mongoDB string, mongoTimeout int) (repo.UserRepository, error) {
repo := &userMongoRepository{
timeout: time.Duration(mongoTimeout) * time.Second,
database: mongoDB,
}
client, e := newUserMongoClient(mongoURL, mongoTimeout)
if e != nil {
return nil, errors.Wrap(e, "repository.NewUserMongoRepository")
}
repo.client = client
return repo, nil
}
func (r *userMongoRepository) GetAll() ([]m.User, error) {
ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
defer cancel()
res := []m.User{}
c := r.client.Database(r.database).Collection(new(m.User).TableName())
csr, e := c.Find(ctx, nil)
if e != nil {
return res, e
}
if e := csr.Decode(&res); e != nil {
return res, e
}
return res, nil
}
func (r *userMongoRepository) GetById(id string) (*m.User, error) {
ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
defer cancel()
user := new(m.User)
c := r.client.Database(r.database).Collection(user.TableName())
if e := c.FindOne(ctx, bson.M{"_id": id}).Decode(user); e != nil {
if e == mongo.ErrNoDocuments {
return nil, errors.Wrap(logic.ErrUserNotFound, "repository.User.GetById")
}
return user, errors.Wrap(e, "repository.User.GetById")
}
return user, nil
}
func (r *userMongoRepository) GetByUsername(username string) (bool, *m.User, error) {
ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
defer cancel()
user := new(m.User)
c := r.client.Database(r.database).Collection(user.TableName())
if e := c.FindOne(ctx, bson.M{"Username": username}).Decode(user); e != nil {
if e == mongo.ErrNoDocuments {
return false, nil, errors.Wrap(logic.ErrUserNotFound, "repository.User.GetById")
}
return false, user, errors.Wrap(e, "repository.User.GetById")
}
return true, user, nil
}
func (r *userMongoRepository) Store(user *m.User) error {
ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
defer cancel()
c := r.client.Database(r.database).Collection(new(m.User).TableName())
if _, e := c.InsertOne(ctx, user); e != nil {
return errors.Wrap(e, "repository.User.Store")
}
return nil
}
func (r *userMongoRepository) Update(user *m.User) error {
ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
defer cancel()
c := r.client.Database(r.database).Collection(new(m.User).TableName())
if res, e := c.UpdateOne(ctx, bson.M{"_id": user.ID}, bson.M{"$set": bson.M{"Username": user.Username}}, options.Update().SetUpsert(false)); e != nil {
return errors.Wrap(e, "repository.User.Update")
} else {
if res.MatchedCount == 0 && res.ModifiedCount == 0 {
return errors.Wrap(errors.New("User Not Found"), "repository.User.Update")
}
}
return nil
}
func (r *userMongoRepository) Delete(user *m.User) error {
ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
defer cancel()
c := r.client.Database(r.database).Collection(new(m.User).TableName())
if res, e := c.DeleteOne(ctx, user); e != nil {
return errors.Wrap(e, "repository.User.Delete")
} else {
if res.DeletedCount == 0 {
return errors.Wrap(errors.New("User Not Found"), "repository.User.Delete")
}
}
return nil
}

View File

@@ -0,0 +1,146 @@
package redis
import (
"fmt"
"strconv"
"github.com/rinosukmandityo/hexagonal-login/logic"
m "github.com/rinosukmandityo/hexagonal-login/models"
repo "github.com/rinosukmandityo/hexagonal-login/repositories"
"github.com/go-redis/redis"
"github.com/pkg/errors"
)
type userRedisRepository struct {
client *redis.Client
}
func newUserRedisClient(redisURL string) (*redis.Client, error) {
opt, e := redis.ParseURL(redisURL)
if e != nil {
return nil, e
}
client := redis.NewClient(opt)
if _, e = client.Ping().Result(); e != nil {
return nil, e
}
return client, e
}
func NewUserRedisRepository(redisURL string) (repo.UserRepository, error) {
repo := &userRedisRepository{}
client, e := newUserRedisClient(redisURL)
if e != nil {
return nil, errors.Wrap(e, "repository.NewUserRedisRepository")
}
repo.client = client
return repo, nil
}
func (r *userRedisRepository) generateKey(code string) string {
return fmt.Sprintf("login<>%s", code)
}
func (r *userRedisRepository) generateUsernameKey(code string) string {
return fmt.Sprintf("login<>username<>%s", code)
}
func (r *userRedisRepository) GetAll() ([]m.User, error) {
res := []m.User{}
return res, nil
}
func (r *userRedisRepository) GetById(id string) (*m.User, error) {
user := new(m.User)
key := r.generateKey(id)
data, e := r.client.HGetAll(key).Result()
if e != nil {
return user, errors.Wrap(e, "repository.Redis.GetById")
}
if len(data) == 0 {
return nil, errors.Wrap(logic.ErrUserNotFound, "repository.User.GetById")
}
user.ID = data["ID"]
user.Username = data["Username"]
user.Email = data["Email"]
user.Password = data["Password"]
user.Name = data["Name"]
user.Address = data["Address"]
user.IsActive, _ = strconv.ParseBool(data["IsActive"])
return user, nil
}
func (r *userRedisRepository) GetByUsername(username string) (bool, *m.User, error) {
user := new(m.User)
key := r.generateUsernameKey(username)
data, e := r.client.HGetAll(key).Result()
if e != nil {
return false, user, errors.Wrap(e, "repository.Redis.GetById")
}
if len(data) == 0 {
return false, nil, errors.Wrap(logic.ErrUserNotFound, "repository.User.GetById")
}
user.ID = data["ID"]
user.Username = data["Username"]
user.Email = data["Email"]
user.Password = data["Password"]
user.Name = data["Name"]
user.Address = data["Address"]
user.IsActive, _ = strconv.ParseBool(data["IsActive"])
return true, user, nil
}
func (r *userRedisRepository) Store(user *m.User) error {
key := r.generateKey(user.ID)
data := map[string]interface{}{
"ID": user.ID,
"Username": user.Username,
"Email": user.Email,
"Password": user.Password,
"Name": user.Name,
"Address": user.Address,
"IsActive": user.IsActive,
}
if _, e := r.client.HMSet(key, data).Result(); e != nil {
return errors.Wrap(e, "repository.User.Store")
}
keyUsername := r.generateUsernameKey((user.Username))
if _, e := r.client.HMSet(keyUsername, data).Result(); e != nil {
return errors.Wrap(e, "repository.User.Store")
}
return nil
}
func (r *userRedisRepository) Update(user *m.User) error {
key := r.generateKey(user.ID)
data := map[string]interface{}{
"ID": user.ID,
"Username": user.Username,
"Email": user.Email,
"Password": user.Password,
"Name": user.Name,
"Address": user.Address,
"IsActive": user.IsActive,
}
if _, e := r.client.HMSet(key, data).Result(); e != nil {
return errors.Wrap(e, "repository.User.Update")
}
keyUsername := r.generateUsernameKey((user.Username))
if _, e := r.client.HMSet(keyUsername, data).Result(); e != nil {
return errors.Wrap(e, "repository.User.Update")
}
return nil
}
func (r *userRedisRepository) Delete(user *m.User) error {
key := r.generateKey(user.ID)
if _, e := r.client.HDel(key).Result(); e != nil {
return errors.Wrap(e, "repository.User.Delete")
}
keyUsername := r.generateUsernameKey(user.Username)
if _, e := r.client.HDel(keyUsername).Result(); e != nil {
return errors.Wrap(e, "repository.User.Delete")
}
return nil
}

View File

@@ -0,0 +1,14 @@
package repositories
import (
m "github.com/rinosukmandityo/hexagonal-login/models"
)
type UserRepository interface {
GetAll() ([]m.User, error)
GetById(id string) (*m.User, error)
GetByUsername(username string) (bool, *m.User, error)
Store(user *m.User) error
Update(user *m.User) error
Delete(user *m.User) error
}

View File

@@ -0,0 +1,26 @@
package json
import (
m "github.com/rinosukmandityo/hexagonal-login/models"
"encoding/json"
"github.com/pkg/errors"
)
type User struct{}
func (u *User) Decode(input []byte) (*m.User, error) {
user := new(m.User)
if e := json.Unmarshal(input, user); e != nil {
return nil, errors.Wrap(e, "serializer.Logic.Decode")
}
return user, nil
}
func (u *User) Encode(input *m.User) ([]byte, error) {
rawMsg, e := json.Marshal(input)
if e != nil {
return nil, errors.Wrap(e, "serializer.logic.Encode")
}
return rawMsg, nil
}

View File

@@ -0,0 +1,26 @@
package msgpack
import (
m "github.com/rinosukmandityo/hexagonal-login/models"
"github.com/pkg/errors"
"github.com/vmihailenco/msgpack"
)
type User struct{}
func (u *User) Decode(input []byte) (*m.User, error) {
user := new(m.User)
if e := msgpack.Unmarshal(input, user); e != nil {
return nil, errors.Wrap(e, "serializer.Logic.Decode")
}
return user, nil
}
func (u *User) Encode(input *m.User) ([]byte, error) {
rawMsg, e := msgpack.Marshal(input)
if e != nil {
return nil, errors.Wrap(e, "serializer.logic.Encode")
}
return rawMsg, nil
}

View File

@@ -0,0 +1,10 @@
package serializer
import (
m "github.com/rinosukmandityo/hexagonal-login/models"
)
type UserSerializer interface {
Decode(input []byte) (*m.User, error)
Encode(input *m.User) ([]byte, error)
}

11
services/login_service.go Normal file
View File

@@ -0,0 +1,11 @@
package services
import (
m "github.com/rinosukmandityo/hexagonal-login/models"
)
type LoginService interface {
Authenticate(username, password string) (bool, *m.User, error)
ChangePassword(user m.User, newpassword string) error
UserActivation(activationkey string) error
}

13
services/user_service.go Normal file
View File

@@ -0,0 +1,13 @@
package services
import (
m "github.com/rinosukmandityo/hexagonal-login/models"
)
type UserService interface {
GetAll() ([]m.User, error)
GetById(id string) (*m.User, error)
Store(user *m.User) error
Update(user *m.User) error
Delete(user *m.User) error
}

View File

@@ -0,0 +1,154 @@
package services
import (
"sync"
"testing"
"github.com/rinosukmandityo/hexagonal-login/helper"
"github.com/rinosukmandityo/hexagonal-login/logic"
m "github.com/rinosukmandityo/hexagonal-login/models"
repo "github.com/rinosukmandityo/hexagonal-login/repositories"
)
/*
==================
RUN FROM TERMINAL
==================
set mongo_url=mongodb://localhost:27017/local
set mongo_timeout=10
set mongo_db=local
set url_db=mongo
*/
var (
userRepo repo.UserRepository
)
func UserTestData() []m.User {
return []m.User{{
Name: "User 01",
Username: "username01",
Password: "Password.1",
ID: "userid01",
Email: "usermail01@gmail.com",
Address: "User Address 01",
IsActive: false,
}, {
Name: "User 02",
Username: "username02",
ID: "userid02",
Password: "Password.1",
Email: "usermail02@gmail.com",
Address: "User Address 02",
IsActive: false,
}, {
Name: "User 03",
ID: "userid03",
Password: "Password.1",
Username: "username03",
Email: "usermail03@gmail.com",
Address: "User Address 03",
IsActive: false,
}}
}
func init() {
userRepo = helper.ChooseRepo()
}
func TestInsertUser(t *testing.T) {
userService := logic.NewUserService(userRepo)
testdata := UserTestData()
wg := sync.WaitGroup{}
t.Run("Case 1: Save data", func(t *testing.T) {
for _, data := range testdata {
wg.Add(1)
go func(_data m.User) {
if e := userService.Store(&_data); e != nil {
t.Errorf("[ERROR] - Failed to save data %s ", e.Error())
}
wg.Done()
}(data)
}
wg.Wait()
for _, data := range testdata {
res, e := userService.GetById(data.ID)
if e != nil || res.ID == "" {
t.Errorf("[ERROR] - Failed to get data")
}
}
})
t.Run("Case 2: Negative Test", func(t *testing.T) {
t.Run("Case 2.1: Duplicate username", func(t *testing.T) {
_data := testdata[0]
_data.ID = "userid04"
if e := userService.Store(&_data); e == nil {
t.Error("[ERROR] - duplicate validation username is not working")
}
})
t.Run("Case 2.2: Duplicate ID", func(t *testing.T) {
_data := testdata[0]
_data.Username = "username04"
if e := userService.Store(&_data); e == nil {
t.Error("[ERROR] - duplicate validation ID is not working")
}
})
})
}
func TestUpdateUser(t *testing.T) {
testdata := UserTestData()
userService := logic.NewUserService(userRepo)
t.Run("Case 1: Update data", func(t *testing.T) {
_data := testdata[0]
_data.Username = _data.Username + "UPDATED"
if e := userService.Update(&_data); e != nil {
t.Errorf("[ERROR] - Failed to update data %s ", e.Error())
}
})
t.Run("Case 2: Negative Test", func(t *testing.T) {
_data := m.User{ID: "ID DID NOT EXISTS"}
if e := userService.Update(&_data); e == nil {
t.Error("[ERROR] - It should be error 'User Not Found'")
}
})
}
func TestDeleteUser(t *testing.T) {
testdata := UserTestData()
userService := logic.NewUserService(userRepo)
t.Run("Case 1: Delete data", func(t *testing.T) {
_data := testdata[1]
if e := userService.Delete(&_data); e != nil {
t.Errorf("[ERROR] - Failed to delete data %s ", e.Error())
}
})
t.Run("Case 2: Negative Test", func(t *testing.T) {
_data := testdata[1]
if e := userService.Delete(&_data); e == nil {
t.Error("[ERROR] - It should be error 'User Not Found'")
}
})
}
func TestGetUser(t *testing.T) {
testdata := UserTestData()
userService := logic.NewUserService(userRepo)
t.Run("Case 1: Get data", func(t *testing.T) {
_data := testdata[0]
if _, e := userService.GetById(_data.ID); e != nil {
t.Errorf("[ERROR] - Failed to get data %s ", e.Error())
}
})
t.Run("Case 2: Negative Test", func(t *testing.T) {
if _, e := userService.GetById("ID DID NOT EXISTS"); e == nil {
t.Error("[ERROR] - It should be error 'User Not Found'")
}
})
}