diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..0ef9113 --- /dev/null +++ b/Gopkg.lock @@ -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 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..25c7579 --- /dev/null +++ b/Gopkg.toml @@ -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 diff --git a/api/handler.go b/api/handler.go new file mode 100644 index 0000000..0b46341 --- /dev/null +++ b/api/handler.go @@ -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 +} diff --git a/api/response.go b/api/response.go new file mode 100644 index 0000000..b68c9ee --- /dev/null +++ b/api/response.go @@ -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) + } +} diff --git a/api/serializer.go b/api/serializer.go new file mode 100644 index 0000000..9de02c7 --- /dev/null +++ b/api/serializer.go @@ -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{} +} diff --git a/api/user_http.go b/api/user_http.go new file mode 100644 index 0000000..e12d131 --- /dev/null +++ b/api/user_http.go @@ -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) +} diff --git a/api/user_http_test.go b/api/user_http_test.go new file mode 100644 index 0000000..161dde5 --- /dev/null +++ b/api/user_http_test.go @@ -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 +} diff --git a/helper/repo.go b/helper/repo.go new file mode 100644 index 0000000..1de978b --- /dev/null +++ b/helper/repo.go @@ -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 +} diff --git a/logic/login_logic.go b/logic/login_logic.go new file mode 100644 index 0000000..460ec84 --- /dev/null +++ b/logic/login_logic.go @@ -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) + +} diff --git a/logic/user_logic.go b/logic/user_logic.go new file mode 100644 index 0000000..ad365f5 --- /dev/null +++ b/logic/user_logic.go @@ -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 + +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..2e95990 --- /dev/null +++ b/main.go @@ -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) +} diff --git a/models/user_model.go b/models/user_model.go new file mode 100644 index 0000000..40a00b4 --- /dev/null +++ b/models/user_model.go @@ -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 +} diff --git a/repositories/login_repository.go b/repositories/login_repository.go new file mode 100644 index 0000000..ae56a8f --- /dev/null +++ b/repositories/login_repository.go @@ -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 +} diff --git a/repositories/mongodb/user_mongo_repo.go b/repositories/mongodb/user_mongo_repo.go new file mode 100644 index 0000000..c686438 --- /dev/null +++ b/repositories/mongodb/user_mongo_repo.go @@ -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 + +} diff --git a/repositories/redis/user_redis_repo.go b/repositories/redis/user_redis_repo.go new file mode 100644 index 0000000..f16977a --- /dev/null +++ b/repositories/redis/user_redis_repo.go @@ -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 + +} diff --git a/repositories/user_repository.go b/repositories/user_repository.go new file mode 100644 index 0000000..b52cca0 --- /dev/null +++ b/repositories/user_repository.go @@ -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 +} diff --git a/serializer/json/json_serializer.go b/serializer/json/json_serializer.go new file mode 100644 index 0000000..27cdde0 --- /dev/null +++ b/serializer/json/json_serializer.go @@ -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 +} diff --git a/serializer/msgpack/msgpack_serializer.go b/serializer/msgpack/msgpack_serializer.go new file mode 100644 index 0000000..398573a --- /dev/null +++ b/serializer/msgpack/msgpack_serializer.go @@ -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 +} diff --git a/serializer/user_serializer.go b/serializer/user_serializer.go new file mode 100644 index 0000000..f64c436 --- /dev/null +++ b/serializer/user_serializer.go @@ -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) +} diff --git a/services/login_service.go b/services/login_service.go new file mode 100644 index 0000000..96851c2 --- /dev/null +++ b/services/login_service.go @@ -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 +} diff --git a/services/user_service.go b/services/user_service.go new file mode 100644 index 0000000..8896eac --- /dev/null +++ b/services/user_service.go @@ -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 +} diff --git a/services/user_service_test.go b/services/user_service_test.go new file mode 100644 index 0000000..c24f656 --- /dev/null +++ b/services/user_service_test.go @@ -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'") + } + }) +}