diff --git a/.env b/.env new file mode 100644 index 000000000..a679bd58f --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +DB_URI=postgres://jbavnilybmiowp:d2651d1455e17c89d721f110c0574591de722b6155d06c378dcb2560336c721c@ec2-44-197-128-108.compute-1.amazonaws.com:5432/d9bg7rlhmeks7s +dbDriver=postgres \ No newline at end of file diff --git a/Dockerfile.production b/Dockerfile.production new file mode 100644 index 000000000..5da72956a --- /dev/null +++ b/Dockerfile.production @@ -0,0 +1,25 @@ +# Dockerfile.production + +FROM registry.semaphoreci.com/golang:1.18 as builder + +ENV APP_HOME /go/src/togo + +WORKDIR "$APP_HOME" +COPY src/ . + +RUN go mod download +RUN go mod verify +RUN go build -o togo + +FROM registry.semaphoreci.com/golang:1.18 + +ENV APP_HOME /go/src/togo +RUN mkdir -p "$APP_HOME" +WORKDIR "$APP_HOME" + +COPY src/conf/ conf/ +COPY src/views/ views/ +COPY --from=builder "$APP_HOME"/togo $APP_HOME + +EXPOSE 8080 +CMD ["./togo"] \ No newline at end of file diff --git a/README.md b/README.md index 8df9d4d3a..49da97666 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,170 @@ -### Requirements +## Todo API -- Implement one single API which accepts a todo task and records it - - There is a maximum **limit of N tasks per user** that can be added **per day**. - - Different users can have **different** maximum daily limit. -- Write integration (functional) tests -- Write unit tests -- Choose a suitable architecture to make your code simple, organizable, and maintainable -- Write a concise README - - How to run your code locally? - - A sample “curl” command to call your API - - How to run your unit tests locally? - - What do you love about your solution? - - What else do you want us to know about however you do not have enough time to complete? +- Link API document: https://basalt-leech-fa6.notion.site/Todo-API-document-fcfd66100f6743b2b585eeb1cf84f49a +- Link API online: https://todo-api-version1.herokuapp.com/ +- Link API after install local: localhost:8000 -### Notes +### :old_key: Prerequisites +Before you start, ensure you meet the following requirements: -- We're using Golang at Manabie. **However**, we encourage you to use the programming language that you are most comfortable with because we want you to **shine** with all your skills and knowledge. + - You have installed Golang version 1.18.1 + - You have installed the Visual Studio Code. + - You have a basic understand of Golang, CLI. + +### :page_with_curl: Guide -### How to submit your solution? +#### How to run this project locally -- Fork this repo and show us your development progress via a PR +Open Git bash +Paste folllowing command: -### Interesting facts about Manabie +``` +git clone https://github.com/huynhhuuloc129/togo.git +``` -- Monthly there are about 2 million lines of code changes (inserted/updated/deleted) committed into our GitHub repositories. To avoid **regression bugs**, we write different kinds of **automated tests** (unit/integration (functionality)/end2end) as parts of the definition of done of our assigned tasks. -- We nurture the cultural values: **knowledge sharing** and **good communication**, therefore good written documents and readable, organizable, and maintainable code are in our blood when we build any features to grow our products. -- We have **collaborative** culture at Manabie. Feel free to ask trieu@manabie.com any questions. We are very happy to answer all of them. +#### How to call API using curl -Thank you for spending time to read and attempt our take-home assessment. We are looking forward to your submission. +#### Install +Open the folder todo with VSCode and using this command to get all the library necessary: +``` +go mod tidy +``` + +#### Usage +After installing enviroment, using those commands on VSCode to use +* Note: ***REPLACE ALL THE TEXT IN <> WITH YOUR INFO*** + +##### :one: Start server +* The server must be keep open all the time you use the app +``` +go run server.go +``` +##### :two: Register +Open another CLI to call API: + * To use the API you need to register account first: +``` +curl --location --request POST 'localhost:8000/auth/register' \ +--data-raw '{ + "username": "", + "password": "" +}' +``` + - The response will be something like this: +```json +{ + "Username":"", + "Password":"", + "LimitTask":10 +} +``` + +##### :three: Login + * After that you can login using the same password as register to get the token of that account: +``` +curl --location --request POST 'localhost:8000/auth/login' \ +--data-raw '{ + "username": "", + "password": "" +}' +``` + - The response will be something like this: +```json +{ + "Message": "login success", + "Token": "" +} +``` + +##### :four: Using task +* You can check your info at: +``` +curl --location --request GET 'localhost:8000/users/info' \ +--header 'token: ' +``` + +###### Use the token response to you after login to: +* Get all task +``` +curl --location --request GET 'localhost:8000/tasks' \ +--header 'token: ' +``` +* Get one task by task id +``` +curl --location --request GET 'localhost:8000/tasks/' \ +--header 'token: ' +``` +* Create new task +``` +curl --location --request POST 'localhost:8000/tasks' \ +--header 'token: ' \ +--data-raw '{ + "Content": "" +}' +``` +* Update an existing task +``` +curl --location --request PUT 'localhost:8000/tasks/' \ +--header 'token: ' \ +--data-raw '{ + "Content": "" +}' +``` +* Delete one task by task id +``` +curl --location --request DELETE 'localhost:8000/tasks/' \ +--header 'token: ' + +``` +***Beside that you can run your all of tasks and also users command under admin account*** +* Login with +``` +curl -X POST -H "Content-Type: application/json" -d '{"username": "admin", "password": "admin"}' "localhost:8000/auth/login" +``` +* After logging in, you can modify users and your task: +* Get all users +``` +curl --location --request GET 'http://127.0.0.1:8000/users' \ +--header 'token: ' +``` +* Get one task by user id +``` +curl --location --request GET 'http://127.0.0.1:8000/users/' \ +--header 'token: ' +``` +* Create new user +``` +curl --location --request POST 'http://127.0.0.1:8000/users' \ +--header 'token: ' \ +--data-raw '{ + "username": "", + "password": "" +}' +``` +* Update an existing user +``` +curl --location --request PUT 'http://127.0.0.1:8000/users/' \ +--header 'token: ' \ +--data-raw '{ + "username": "", + "password": "" +}' +``` +* Delete one user by user id +``` +curl --location --request DELETE 'http://127.0.0.1:8000/users/' \ +--header 'token: ' +``` +#### How to run test locally +* Using this command to test all test in project (include unit test and intergration test): +``` +go test -v -cover ./... +``` +* If you want to test some folder only, you can go to the folder and using command: +``` +cd +go test . +``` +#### What special about this solution: +* This project using all dependencies in clouds, include database, even the project it self has been host through heroku for using convenient. +* It has an admin account for full admin control over users but can't seen task to keep it private for each user. +* ***Note: Because this project use a cloud database so some action may take longer than usual*** diff --git a/controllers/authController.go b/controllers/authController.go new file mode 100644 index 000000000..cd5874cb8 --- /dev/null +++ b/controllers/authController.go @@ -0,0 +1,94 @@ +package controllers + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/huynhhuuloc129/todo/jwt" + "github.com/huynhhuuloc129/todo/models" +) + +type BaseHandler struct { + BaseCtrl *models.DbConn +} + +// NewBaseHandler returns a new BaseHandler +func NewBaseHandler(BC *models.DbConn) *BaseHandler { + return &BaseHandler{ + BaseCtrl: BC, + } +} + +//response token +type ResponseToken struct { + Message string + Token string +} + +// Handle register with method post +func (h *BaseHandler) Register(w http.ResponseWriter, r *http.Request) { + var user, user1 models.NewUser + _ = json.NewDecoder(r.Body).Decode(&user) + user1 = models.NewUser{ + Username: user.Username, + Password: user.Password, + } + ok := models.CheckUserInput(user1) + if !ok { + http.Error(w, "registered failed", http.StatusBadRequest) + return + } + + if strings.ToLower(user1.Username) != "admin" { + user1.LimitTask = 10 + } else { + user1.LimitTask = 0 + } + if err := h.BaseCtrl.InsertUser(user1); err != nil { // insert new user to database + http.Error(w, "insert user failed, err: "+err.Error(), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(user1); err != nil { // response message and token back to view + http.Error(w, "encode failed, err: "+err.Error(), http.StatusCreated) + return + } +} + +// handle login with method post +func (h *BaseHandler) Login(w http.ResponseWriter, r *http.Request) { + var user models.NewUser + + _ = json.NewDecoder(r.Body).Decode(&user) + user1, ok := h.BaseCtrl.CheckUserNameExist(user.Username) + if !ok { // check username exist or not + http.Error(w, "account doesn't exist", http.StatusNotFound) + return + } + + if ok := models.CheckUserInput(user); !ok { // check if user input valid or not + http.Error(w, "account input invalid", http.StatusNotFound) + return + } + if err := models.CheckPasswordHash(user1.Password, user.Password); err != nil { // check password correct or not + http.Error(w, "password incorrect, err: "+err.Error(), http.StatusUnauthorized) + return + } + token, err := jwt.Create(user.Username, int(user1.Id)) // Create token + if err != nil { + http.Error(w, "internal server error, err: "+err.Error(), 500) + return + } + + w.Header().Set("Content-Type", "application/json") + resToken := ResponseToken{ + Message: "login success", + Token: token, + } + + if err = json.NewEncoder(w).Encode(resToken); err != nil { // response token back to client + http.Error(w, "encode failed, err: " + err.Error(), http.StatusFailedDependency) + } +} diff --git a/controllers/authController_test.go b/controllers/authController_test.go new file mode 100644 index 000000000..93eb7121f --- /dev/null +++ b/controllers/authController_test.go @@ -0,0 +1,91 @@ +package controllers + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/huynhhuuloc129/todo/models" + "github.com/stretchr/testify/assert" +) + +// create a mock database +func CreateMockingDB() (sqlmock.Sqlmock, *BaseHandler) { + db, mock := models.NewMock() + dbConn := models.NewdbConn(db) + h := NewBaseHandler(dbConn) + return mock, h +} + +// test controller register +func TestRegister(t *testing.T) { + mock, h := CreateMockingDB() + + newUser := models.RandomNewUser() + newUserJSON, err := json.Marshal(newUser) + if err != nil { + t.Errorf("Can't marshal user, err: " + err.Error()) + } + + mock.ExpectExec(regexp.QuoteMeta(models.InsertUserText)).WithArgs(newUser.Username, newUser.Password, newUser.LimitTask).WillReturnResult(sqlmock.NewResult(1, 1)) + + w := httptest.NewRecorder() + req := httptest.NewRequest("POST", "localhost:8000/register", bytes.NewReader(newUserJSON)) + h.Register(w, req) + + resp := w.Result() + var user models.NewUser + err = json.NewDecoder(resp.Body).Decode(&user) + if err != nil { + t.Fatal("decode failed, err: " + err.Error()) + } + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, user.Username, newUser.Username) + assert.Equal(t, user.Password, newUser.Password) + assert.Equal(t, user.LimitTask, newUser.LimitTask) +} + +// test controller login +func TestLogin(t *testing.T) { + mock, h := CreateMockingDB() + + user := models.RandomUser() + passworhHashed, _ := models.Hash(user.Password) + newUser := models.NewUser{ + Username: user.Username, + Password: user.Password, + LimitTask: user.LimitTask, + } + newUserJSON, err := json.Marshal(newUser) + if err != nil { + t.Errorf("Can't marshal user, err: " + err.Error()) + } + + rows := sqlmock.NewRows([]string{"id", "username", "password", "limittask"}) + rows.AddRow(user.Id, user.Username, passworhHashed, user.LimitTask) + + mock.ExpectQuery(regexp.QuoteMeta(models.QueryAllUsernameText)).WithArgs(newUser.Username).WillReturnRows(rows) + + w := httptest.NewRecorder() + req := httptest.NewRequest("POST", "localhost:8000/login", bytes.NewReader(newUserJSON)) + h.Login(w, req) + + resp := w.Result() + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("Can't read body response") + } + + var resToken ResponseToken + err = json.Unmarshal(respBody, &resToken) + if err != nil { + t.Errorf(err.Error()) + } + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, resToken.Message, "login success") +} diff --git a/controllers/taskController.go b/controllers/taskController.go new file mode 100644 index 000000000..6528c72b9 --- /dev/null +++ b/controllers/taskController.go @@ -0,0 +1,134 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gorilla/context" + "github.com/huynhhuuloc129/todo/models" +) + +// Get all task from database +func (bh *BaseHandler) ResponseAllTask(w http.ResponseWriter, r *http.Request) { + userid, _ := strconv.Atoi(fmt.Sprintf("%v", context.Get(r, "userid"))) // get userid from login + + tasks, err := bh.BaseCtrl.GetAllTasks(userid) + if err != nil { + http.Error(w, "get all task failed", http.StatusFailedDependency) + return + } + + // tasks = ChangeStatusAllTasksAfterDay(tasks) + w.Header().Set("Content-Type", "application/json") + if err = json.NewEncoder(w).Encode(tasks); err != nil { + http.Error(w, "encode tasks failed, err: "+err.Error(), 500) + return + } +} + +// Get one task from database +func (bh *BaseHandler) ResponseOneTask(w http.ResponseWriter, r *http.Request) { + userid, _ := strconv.Atoi(fmt.Sprintf("%v", context.Get(r, "userid"))) // get userid from login + id, _ := strconv.Atoi(fmt.Sprintf("%v", context.Get(r, "id"))) // get id from url + + task, ok := bh.BaseCtrl.FindTaskByID(id, userid) + if !ok { + http.Error(w, "id invalid", http.StatusBadRequest) + return + } + + // task = ChangeStatusOneTaskAfterDay(task) + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(task); err != nil { + http.Error(w, "encode task failed, err: "+err.Error(), http.StatusFailedDependency) + return + } +} + +// Create a new task +func (bh *BaseHandler) CreateTask(w http.ResponseWriter, r *http.Request) { + userid, _ := strconv.Atoi(fmt.Sprintf("%v", context.Get(r, "userid"))) // get userid from login + + var task models.NewTask + if err := json.NewDecoder(r.Body).Decode(&task); err != nil { + http.Error(w, "decode failed, err: "+err.Error(), http.StatusFailedDependency) + return + } + + task.UserId = userid + task.Status = "pending" + task.TimeDone = time.Date(0001, 01, 01, 0, 0, 0, 0, time.Local).Round(0) + year, month, day := task.Time.Date() + if year != time.Now().Year() || month != time.Now().Month() || day != time.Now().Day() { + task.Time = time.Now() + } + if ok := models.CheckTaskInput(task); !ok { + http.Error(w, "task field invalid", http.StatusBadRequest) + } + if err := bh.BaseCtrl.InsertTask(task); err != nil { + http.Error(w, "insert task failed, err: "+err.Error(), http.StatusFailedDependency) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(task); err != nil { + http.Error(w, "encode failed, err: "+err.Error(), http.StatusCreated) + return + } +} + +// Delete one task from database +func (bh *BaseHandler) DeleteFromTask(w http.ResponseWriter, r *http.Request) { + userid, _ := strconv.Atoi(fmt.Sprintf("%v", context.Get(r, "userid"))) // get userid from login + id, _ := strconv.Atoi(fmt.Sprintf("%v", context.Get(r, "id"))) // get id from url + + if _, ok := bh.BaseCtrl.FindTaskByID(id, userid); !ok { // Check task id exist or not and return that task + http.Error(w, "Id invalid", http.StatusBadRequest) + return + } + + if err := bh.BaseCtrl.DeleteTask(id, userid); err != nil { + http.Error(w, err.Error(), http.StatusFailedDependency) + return + } + w.Write([]byte("message: delete success")) +} + +// Update one task already exist in database +func (bh *BaseHandler) UpdateToTask(w http.ResponseWriter, r *http.Request) { + userid, _ := strconv.Atoi(fmt.Sprintf("%v", context.Get(r, "userid"))) // get userid from login + id, _ := strconv.Atoi(fmt.Sprintf("%v", context.Get(r, "id"))) // get id from url + + oldTask, ok := bh.BaseCtrl.FindTaskByID(id, userid) // Check task id exist or not and return that task + if !ok { + http.Error(w, "Id invalid", http.StatusBadRequest) + return + } + if err := json.NewDecoder(r.Body).Decode(&oldTask); err != nil { // write on old task + http.Error(w, "decode failed, input invalid, err: " +err.Error(), http.StatusBadRequest) + return + } + + if strings.ToLower(oldTask.Status) == "done" { // check status and insert time to it + oldTask.TimeDone = time.Now() + } else if strings.ToLower(oldTask.Status) == "pending" { + oldTask.TimeDone = time.Date(0001, 01, 01, 0, 0, 0, 0, time.Local).Round(0) + } else { + http.Error(w, "status can only be done or pending", http.StatusBadRequest) + return + } + + if err := bh.BaseCtrl.UpdateTask(oldTask, id, userid); err != nil { + http.Error(w, "update task failed, err: "+ err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(oldTask); err != nil { + http.Error(w, "encode failed, err: "+err.Error(), http.StatusBadRequest) + return + } +} diff --git a/controllers/taskController_test.go b/controllers/taskController_test.go new file mode 100644 index 000000000..bf51c12a2 --- /dev/null +++ b/controllers/taskController_test.go @@ -0,0 +1,180 @@ +package controllers + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/gorilla/context" + "github.com/huynhhuuloc129/todo/models" + "github.com/huynhhuuloc129/todo/util" + "github.com/stretchr/testify/assert" +) + +// test controller response all task +func TestResponseAllTask(t *testing.T) { + mock, h := CreateMockingDB() + userId := util.RandomInt(0, 100) + + rows := sqlmock.NewRows([]string{"id", "content", "status", "time", "timedone", "userid"}) + for i := 0; i < 10; i++ { + task := models.RandomTask() + rows.AddRow(task.Id, task.Content, task.Status, task.Time, task.TimeDone, userId) + } + + //query + mock.ExpectQuery(regexp.QuoteMeta(models.QueryAllTaskText)).WithArgs(userId).WillReturnRows(rows) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "localhost:8000/tasks", nil) + context.Set(req, "userid", userId) + h.ResponseAllTask(w, req) + + resp := w.Result() + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("Can't read body response") + } + + var tasks []models.Task + err = json.Unmarshal(respBody, &tasks) + if err != nil { + t.Errorf(err.Error()) + } + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.NotEmpty(t, respBody) + assert.Len(t, tasks, 10) +} + +// test controller response one task +func TestResponseOneTask(t *testing.T) { + mock, h := CreateMockingDB() + task := models.RandomTask() + + rows := sqlmock.NewRows([]string{"id", "content", "status", "time", "timedone", "userid"}) + rows.AddRow(task.Id, task.Content, task.Status, task.Time, task.TimeDone, task.UserId) + + //query + mock.ExpectQuery(regexp.QuoteMeta(models.FindTaskByIDText)).WithArgs(task.Id, task.UserId).WillReturnRows(rows) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "localhost:8000/tasks/"+fmt.Sprintf("%v", task.UserId), nil) + context.Set(req, "id", task.Id) + context.Set(req, "userid", task.UserId) + h.ResponseOneTask(w, req) + + resp := w.Result() + var taskfromdb models.Task + if err := json.NewDecoder(resp.Body).Decode(&taskfromdb); err != nil { + http.Error(w, "decode failed, err: "+err.Error(), http.StatusFailedDependency) + return + } + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, taskfromdb.Content, task.Content) + assert.Equal(t, taskfromdb.Status, task.Status) + assert.Equal(t, taskfromdb.UserId, task.UserId) +} + +// test controller create task +func TestCreateTask(t *testing.T) { + mock, h := CreateMockingDB() + task := models.RandomTask() + taskJSON, err := json.Marshal(task) + if err != nil { + t.Errorf("Can't marshal task, err: " + err.Error()) + } + + //exec + mock.ExpectExec(regexp.QuoteMeta(models.InsertTaskText)).WithArgs(task.Content, task.Status, task.Time, task.TimeDone, task.UserId).WillReturnResult(sqlmock.NewResult(1, 1)) + + w := httptest.NewRecorder() // set custom writer and response + req := httptest.NewRequest("POST", "localhost:8000/users", bytes.NewReader(taskJSON)) + context.Set(req, "userid", task.UserId) + h.CreateTask(w, req) + + resp := w.Result() + var taskfromdb models.Task + if err := json.NewDecoder(resp.Body).Decode(&taskfromdb); err != nil { + http.Error(w, "decode failed, err: "+err.Error(), http.StatusFailedDependency) + return + } + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.NotEmpty(t, taskfromdb) + assert.Equal(t, taskfromdb.Content, task.Content) + assert.Equal(t, taskfromdb.Status, task.Status) + assert.Equal(t, taskfromdb.UserId, task.UserId) +} + + +// test controller delete from task +func TestDeleteFromTask(t *testing.T) { + mock, h := CreateMockingDB() + task := models.RandomTask() + + rows := sqlmock.NewRows([]string{"id", "content", "status", "time", "timedone", "userid"}) + rows.AddRow(task.Id, task.Content, task.Status, task.Time, task.TimeDone, task.UserId) + + mock.ExpectQuery(regexp.QuoteMeta(models.FindTaskByIDText)).WithArgs(task.Id, task.UserId).WillReturnRows(rows) + mock.ExpectExec(regexp.QuoteMeta(models.DeleteTaskText)).WithArgs(task.Id, task.UserId).WillReturnResult(sqlmock.NewResult(1, 1)) + + w := httptest.NewRecorder() + req := httptest.NewRequest("DELETE", fmt.Sprintf("localhost:8000/%v", task.Id), nil) + context.Set(req, "userid", task.UserId) + context.Set(req, "id", task.Id) + h.DeleteFromTask(w, req) + + resp := w.Result() + respBody, err := ioutil.ReadAll(resp.Body) + respBodyString := string(respBody) + if err != nil { + t.Errorf("Can't read body response") + } + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, respBodyString, "message: delete success") +} + + +//test controller update to task +func TestUpdateToTask(t *testing.T) { + mock, h := CreateMockingDB() + + task := models.RandomTask() + newTask := models.RandomNewTask() + newTaskJSON, err := json.Marshal(newTask) + if err != nil { + t.Errorf("Can't marshal task, err: " + err.Error()) + } + + rows := sqlmock.NewRows([]string{"id", "content", "status", "time", "timedone", "userid"}) + rows.AddRow(task.Id, task.Content, task.Status, task.Time, task.TimeDone, task.UserId) + + mock.ExpectQuery(regexp.QuoteMeta(models.FindTaskByIDText)).WithArgs(task.Id, task.UserId).WillReturnRows(rows) + mock.ExpectExec(regexp.QuoteMeta(models.UpdateTaskText)).WithArgs(newTask.Content, newTask.Status, newTask.TimeDone, task.Id, task.UserId).WillReturnResult(sqlmock.NewResult(1, 1)) + + w := httptest.NewRecorder() + req := httptest.NewRequest("PUT", fmt.Sprintf("localhost:8000/%v", task.Id), bytes.NewReader(newTaskJSON)) + context.Set(req, "id", task.Id) + context.Set(req, "userid", task.UserId) + h.UpdateToTask(w, req) + + resp := w.Result() + var taskfromdb models.Task + if err := json.NewDecoder(resp.Body).Decode(&taskfromdb); err != nil { + http.Error(w, "decode failed, err: "+err.Error(), http.StatusFailedDependency) + return + } + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, taskfromdb.Content, newTask.Content) + assert.Equal(t, taskfromdb.Status, newTask.Status) + assert.Equal(t, taskfromdb.UserId, newTask.UserId) +} diff --git a/controllers/userController.go b/controllers/userController.go new file mode 100644 index 000000000..dbbde2c79 --- /dev/null +++ b/controllers/userController.go @@ -0,0 +1,115 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/gorilla/context" + "github.com/huynhhuuloc129/todo/models" +) + +// Get all user from database +func (bh *BaseHandler) ResponseAllUser(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + users, err := bh.BaseCtrl.GetAllUser() + if err != nil { + http.Error(w, "get all user failed, err: "+err.Error(), http.StatusFailedDependency) + return + } + + if err = json.NewEncoder(w).Encode(users); err != nil { + http.Error(w, "encode failed, err: "+err.Error(), 500) + return + } +} + +// Get one user from database +func (bh *BaseHandler) ResponseOneUser(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(fmt.Sprintf("%v", context.Get(r, "id"))) // get id from url + user, ok := bh.BaseCtrl.FindUserByID(id) + if !ok { + http.Error(w, "id invalid", http.StatusFailedDependency) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(user); err != nil { + http.Error(w, "encode failed, err:" + err.Error(), http.StatusFailedDependency) + return + } +} + +// Create a new user +func (bh *BaseHandler) CreateUser(w http.ResponseWriter, r *http.Request) { + var user models.NewUser + if err := json.NewDecoder(r.Body).Decode(&user); err != nil { + http.Error(w, "decode failed, err: "+err.Error(), http.StatusFailedDependency) + return + } + + if strings.ToLower(user.Username) != "admin" { // check admin or not + user.LimitTask = 10 + } else { + user.LimitTask = 0 + } + + if err := bh.BaseCtrl.InsertUser(user); err != nil { // insert user to database + http.Error(w, "insert user failed, err: "+err.Error(), http.StatusFailedDependency) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(user); err != nil { // return response + http.Error(w, "encode failed, err: "+err.Error(), http.StatusCreated) + return + } +} + +// Delete user from database +func (bh *BaseHandler) DeleteFromUser(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(fmt.Sprintf("%v", context.Get(r, "id"))) // get id from url + if _, ok := bh.BaseCtrl.FindUserByID(id); !ok { + http.Error(w, "Id invalid", http.StatusBadRequest) + return + } + if err := bh.BaseCtrl.DeleteAllTaskFromUser(id); err != nil { + http.Error(w, "delete all task of user failed, err: " +err.Error() , http.StatusFailedDependency) + return + } + if err := bh.BaseCtrl.DeleteUser(id); err != nil { + http.Error(w, "delete user failed, err: "+err.Error(), http.StatusFailedDependency) + return + } + w.Write([]byte("message: delete success")) +} + +// Update one user already exist in database +func (bh *BaseHandler) UpdateToUser(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(fmt.Sprintf("%v", context.Get(r, "id"))) // get id from url + + var newUser models.NewUser + user, ok := bh.BaseCtrl.FindUserByID(id) + newUser.Username = user.Username + newUser.Password = user.Password + newUser.LimitTask = user.LimitTask + + if !ok { + http.Error(w, "Id invalid", http.StatusBadRequest) + return + } + if err := json.NewDecoder(r.Body).Decode(&newUser); err != nil { + http.Error(w, "decode failed, input invalid, err: "+err.Error(), http.StatusBadRequest) + return + } + if err := bh.BaseCtrl.UpdateUser(newUser, id); err != nil { + http.Error(w, "update user failed, err:"+err.Error(), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(newUser); err != nil { + http.Error(w, "encode failed, err: "+err.Error(), http.StatusBadRequest) + return + } +} diff --git a/controllers/userController_test.go b/controllers/userController_test.go new file mode 100644 index 000000000..b6f3851c6 --- /dev/null +++ b/controllers/userController_test.go @@ -0,0 +1,171 @@ +package controllers + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/gorilla/context" + "github.com/huynhhuuloc129/todo/models" + "github.com/stretchr/testify/assert" +) + +// test controller response all user +func TestResponseAllUser(t *testing.T) { + mock, h := CreateMockingDB() + + rows := sqlmock.NewRows([]string{"id", "username", "password", "limittask"}) + for i := 0; i < 10; i++ { + user := models.RandomUser() + rows.AddRow(user.Id, user.Username, user.Password, user.LimitTask) + } + + mock.ExpectQuery(regexp.QuoteMeta(models.QueryAllUserText)).WillReturnRows(rows) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "localhost:8000/users", nil) + h.ResponseAllUser(w, req) + + resp := w.Result() + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("Can't read body response") + } + + var users []models.User + err = json.Unmarshal(respBody, &users) + if err != nil { + t.Errorf(err.Error()) + } + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.NotEmpty(t, respBody) + assert.Len(t, users, 10) +} + +// test controller response one user +func TestResponseOneUser(t *testing.T) { + mock, h := CreateMockingDB() + + user := models.RandomUser() + rows := sqlmock.NewRows([]string{"id", "username", "password", "limittask"}) + rows.AddRow(user.Id, user.Username, user.Password, user.LimitTask) + + mock.ExpectQuery(regexp.QuoteMeta(models.FindUserByIDText)).WithArgs(user.Id).WillReturnRows(rows) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "localhost:8000/users/"+fmt.Sprintf("%v", user.Id), nil) + context.Set(req, "id", user.Id) + h.ResponseOneUser(w, req) + + var userfromdb models.User + resp := w.Result() + err := json.NewDecoder(resp.Body).Decode(&userfromdb) + if err != nil { + t.Fatal("decode failed, err: " +err.Error()) + } + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.NotEmpty(t, userfromdb) + assert.Equal(t, userfromdb.Username, user.Username) + assert.Equal(t, userfromdb.Password, user.Password) + assert.Equal(t, userfromdb.LimitTask, user.LimitTask) +} + +// test controller create user +func TestCreateUser(t *testing.T) { + mock, h := CreateMockingDB() + + user := models.RandomUser() + userJSON, err := json.Marshal(user) + if err != nil { + t.Errorf("Can't marshal user, err: " + err.Error()) + } + + //exec + mock.ExpectExec(regexp.QuoteMeta(models.InsertUserText)).WithArgs(user.Username, user.Password, 10).WillReturnResult(sqlmock.NewResult(1, 1)) + + w := httptest.NewRecorder() // set custom writer and response + req := httptest.NewRequest("POST", "localhost:8000/users", bytes.NewReader(userJSON)) + h.CreateUser(w, req) + + var userfromdb models.User + resp := w.Result() + err = json.NewDecoder(resp.Body).Decode(&userfromdb) + if err != nil { + t.Fatal("decode failed, err: " +err.Error()) + } + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.NotEmpty(t, userfromdb) + assert.Equal(t, userfromdb.Username, user.Username) +} + +// test controller delete user +func TestDeleteFromUser(t *testing.T) { + mock, h := CreateMockingDB() + + user := models.RandomUser() + + rows := sqlmock.NewRows([]string{"id", "username", "password", "limittask"}) + rows.AddRow(user.Id, user.Username, user.Password, user.LimitTask) + + mock.ExpectQuery(regexp.QuoteMeta(models.FindUserByIDText)).WithArgs(user.Id).WillReturnRows(rows) + mock.ExpectExec(regexp.QuoteMeta(models.DeleteAllTaskText)).WithArgs(user.Id).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectExec(regexp.QuoteMeta(models.DeleteUserText)).WithArgs(user.Id).WillReturnResult(sqlmock.NewResult(1, 1)) + + w := httptest.NewRecorder() + req := httptest.NewRequest("DELETE", fmt.Sprintf("localhost:8000/%v", user.Id), nil) + context.Set(req, "id", user.Id) + h.DeleteFromUser(w, req) + + resp := w.Result() + respBody, err := ioutil.ReadAll(resp.Body) + respBodyString := string(respBody) + if err != nil { + t.Errorf("Can't read body response") + } + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, respBodyString, "message: delete success") +} + +// test controller update to user +func TestUpdateToUser(t *testing.T) { + mock, h := CreateMockingDB() + + user := models.RandomUser() + newUser := models.RandomNewUser() + newUserJSON, err := json.Marshal(newUser) + if err != nil { + t.Errorf("Can't marshal user, err: " + err.Error()) + } + + rows := sqlmock.NewRows([]string{"id", "username", "password", "limittask"}) + rows.AddRow(user.Id, user.Username, user.Password, user.LimitTask) + + mock.ExpectQuery(regexp.QuoteMeta(models.FindUserByIDText)).WithArgs(user.Id).WillReturnRows(rows) + mock.ExpectExec(regexp.QuoteMeta(models.UpdateUserText)).WithArgs(newUser.Username, newUser.Password, newUser.LimitTask, user.Id).WillReturnResult(sqlmock.NewResult(1, 1)) + + w := httptest.NewRecorder() + req := httptest.NewRequest("PUT", fmt.Sprintf("localhost:8000/%v", user.Id), bytes.NewReader(newUserJSON)) + context.Set(req, "id", user.Id) + h.UpdateToUser(w, req) + + var userfromdb models.User + resp := w.Result() + err = json.NewDecoder(resp.Body).Decode(&userfromdb) + if err != nil { + t.Fatal("decode failed, err: " +err.Error()) + } + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, userfromdb.Username, newUser.Username) + assert.Equal(t, userfromdb.Password, newUser.Password) + assert.Equal(t, userfromdb.LimitTask, newUser.LimitTask) +} diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..69edecf5c --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module github.com/huynhhuuloc129/todo + +go 1.18 + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/gorilla/context v1.1.1 + github.com/joho/godotenv v1.4.0 + github.com/lib/pq v1.10.6 + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e +) + +require ( + github.com/DATA-DOG/go-sqlmock v1.5.0 + github.com/gorilla/mux v1.8.0 + github.com/stretchr/testify v1.7.5 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..c51f5f8d0 --- /dev/null +++ b/go.sum @@ -0,0 +1,29 @@ +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= +github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file diff --git a/intergrationTest/intergrationAuth_test.go b/intergrationTest/intergrationAuth_test.go new file mode 100644 index 000000000..18ca0aeb6 --- /dev/null +++ b/intergrationTest/intergrationAuth_test.go @@ -0,0 +1,110 @@ +package intergrationTest + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/gorilla/mux" + "github.com/huynhhuuloc129/todo/controllers" + "github.com/huynhhuuloc129/todo/middlewares" + "github.com/huynhhuuloc129/todo/models" + "github.com/joho/godotenv" +) + +func TestRegisterHandle(t *testing.T) { + err := godotenv.Load("../.env") + if err != nil { + t.Fatal("can't load env variable") + } + dbConn := models.Connect(os.Getenv("DB_URI")) + bh := controllers.NewBaseHandler(dbConn) + defer dbConn.DB.Close() + + w := httptest.NewRecorder() + req, err := http.NewRequest("POST", "/register", nil) + if err != nil { + t.Fatal(err) + } + + user := models.RandomNewUser() + newRequestBody, err := json.Marshal(user) + if err != nil { + http.Error(w, "marshal request body failed, err: "+err.Error(), http.StatusBadRequest) + return + } + req.Body = ioutil.NopCloser(bytes.NewBuffer(newRequestBody)) + + r := mux.NewRouter() + r.HandleFunc("/register", middlewares.ValidUsernameAndHashPassword(bh, bh.Register)).Methods("POST") + r.ServeHTTP(w, req) + + // Test that the status code is correct. + if status := w.Code; status != http.StatusOK { + t.Errorf("Status code is invalid. Expected %d. Got %d instead", http.StatusOK, status) + } + users, _ := bh.BaseCtrl.GetAllUser() + // Read the response body. + var returnedUsers models.NewUser + if err := json.NewDecoder(w.Body).Decode(&returnedUsers); err != nil { + t.Errorf("Returned user is invalid JSON. Got: %s", returnedUsers.Username) + } + + bh.BaseCtrl.DeleteUser(int(users[len(users)-1].Id)) + if strings.Compare(returnedUsers.Username, users[len(users)-1].Username) != 0 { + t.Fatal("Get return wrong user expected: " + users[len(users)-1].Username + ", got: " + returnedUsers.Username) + } +} + +func TestLoginHandle(t *testing.T) { + err := godotenv.Load("../.env") + if err != nil { + t.Fatal("can't load env variable") + } + dbConn := models.Connect(os.Getenv("DB_URI")) + bh := controllers.NewBaseHandler(dbConn) + defer dbConn.DB.Close() + + w := httptest.NewRecorder() + req, err := http.NewRequest("POST", "/register", nil) + if err != nil { + t.Fatal(err) + } + + user := models.NewUser{ + Username: "admin", + Password: "admin", + } + newRequestBody, err := json.Marshal(user) + if err != nil { + http.Error(w, "marshal request body failed, err: "+err.Error(), http.StatusBadRequest) + return + } + req.Body = ioutil.NopCloser(bytes.NewBuffer(newRequestBody)) + + r := mux.NewRouter() + r.HandleFunc("/register", bh.Login).Methods("POST") + r.ServeHTTP(w, req) + + // Test that the status code is correct. + if status := w.Code; status != http.StatusOK { + t.Errorf("Status code is invalid. Expected %d. Got %d instead", http.StatusOK, status) + } + // Read the response body. + var data controllers.ResponseToken + + if err = json.NewDecoder(w.Body).Decode(&data); err != nil { + t.Fatal("read body failed") + } + users, _ := bh.BaseCtrl.GetAllUser() + bh.BaseCtrl.DeleteUser(int(users[len(users)-1].Id)) + + if strings.Compare(data.Message, "login success") !=0 { + t.Fatal("Login test failed, expected success got:"+data.Message) + } +} \ No newline at end of file diff --git a/intergrationTest/intergrationTask_test.go b/intergrationTest/intergrationTask_test.go new file mode 100644 index 000000000..6bcb36a8b --- /dev/null +++ b/intergrationTest/intergrationTask_test.go @@ -0,0 +1,275 @@ +package intergrationTest + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/gorilla/context" + "github.com/gorilla/mux" + "github.com/huynhhuuloc129/todo/controllers" + "github.com/huynhhuuloc129/todo/middlewares" + "github.com/huynhhuuloc129/todo/models" + "github.com/joho/godotenv" + _ "github.com/lib/pq" +) + +func TestGetAllTasksHandle(t *testing.T) { + err := godotenv.Load("../.env") + if err != nil { + t.Fatal("can't load env variable") + } + dbConn := models.Connect(os.Getenv("DB_URI")) + bh := controllers.NewBaseHandler(dbConn) + defer dbConn.DB.Close() + + var tasks []models.NewTask + tasks = append(tasks, models.RandomNewTask(), models.RandomNewTask()) + for _, task := range tasks { + task.UserId = AdminID + err := bh.BaseCtrl.InsertTask(task) + if err != nil { + t.Fatal("insert task failed") + } + } + + w := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/tasks", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("token", AdminToken) + context.Set(req, "userid", AdminID) + + r := mux.NewRouter() + r.Use(middlewares.LoggingVerified) + r.HandleFunc("/tasks", bh.ResponseAllTask).Methods("GET") + r.ServeHTTP(w, req) + + // Test that the status code is correct. + if status := w.Code; status != http.StatusOK { + t.Errorf("Status code is invalid. Expected %d. Got %d instead", http.StatusOK, status) + } + + // Read the response body. + var returnTasks []models.Task + if err := json.NewDecoder(w.Body).Decode(&returnTasks); err != nil { + t.Errorf("Returned user list is invalid JSON.") + } + // delete after insert to check + bh.BaseCtrl.DeleteTask(int(returnTasks[len(returnTasks)-1].Id), AdminID) + bh.BaseCtrl.DeleteTask(int(returnTasks[len(returnTasks)-2].Id), AdminID) + + if len(returnTasks) < len(tasks) { + t.Errorf("Returned user list is an invalid length. Expected %d. Got %d instead", len(tasks), len(returnTasks)) + } + count := 0 + for _, returnTask := range returnTasks { + if returnTask.Content == tasks[0].Content || returnTask.Content == tasks[1].Content { + count++ + } + } + if count != len(tasks) { + t.Fatal("Returned task list is different") + } +} + +func TestGetOneTaskHandle(t *testing.T) { + err := godotenv.Load("../.env") + if err != nil { + t.Fatal("can't load env variable") + } + dbConn := models.Connect(os.Getenv("DB_URI")) + bh := controllers.NewBaseHandler(dbConn) + defer dbConn.DB.Close() + + task := models.RandomNewTask() + task.UserId = AdminID + bh.BaseCtrl.InsertTask(task) + tasks, _ := bh.BaseCtrl.GetAllTasks(AdminID) + + w := httptest.NewRecorder() + req, err := http.NewRequest("GET", fmt.Sprintf("/tasks/%v", tasks[len(tasks)-1].Id), nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("token", AdminToken) + context.Set(req, "userid", AdminID) + context.Set(req, "id", tasks[len(tasks)-1].Id) + + r := mux.NewRouter() + r.Use(middlewares.LoggingVerified, middlewares.MiddlewareID) + r.HandleFunc("/tasks/{id}", bh.ResponseOneTask).Methods("GET") + r.ServeHTTP(w, req) + + // Test that the status code is correct. + if status := w.Code; status != http.StatusOK { + t.Errorf("Status code is invalid. Expected %d. Got %d instead", http.StatusOK, status) + } + + // Read the response body. + var returnTask models.Task + if err := json.NewDecoder(w.Body).Decode(&returnTask); err != nil { + t.Errorf("Returned task is invalid JSON.") + } + + bh.BaseCtrl.DeleteTask(tasks[len(tasks)-1].Id, AdminID) + if strings.Compare(returnTask.Content, task.Content) != 0{ + t.Fatal("wrong task return") + } +} + +func TestDeleteTaskHandle(t *testing.T) { + err := godotenv.Load("../.env") + if err != nil { + t.Fatal("can't load env variable") + } + dbConn := models.Connect(os.Getenv("DB_URI")) + bh := controllers.NewBaseHandler(dbConn) + defer dbConn.DB.Close() + + task := models.RandomNewTask() + task.UserId = AdminID + bh.BaseCtrl.InsertTask(task) + + tasks, err := bh.BaseCtrl.GetAllTasks(AdminID) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + req, err := http.NewRequest("DELETE",fmt.Sprintf("/tasks/%v", tasks[len(tasks)-1].Id), nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("token", AdminToken) + context.Set(req, "id", tasks[len(tasks)-1].Id) + context.Set(req, "userid", AdminID) + + r := mux.NewRouter() + r.Use(middlewares.AdminVerified, middlewares.MiddlewareID) + r.HandleFunc("/tasks/{id}", bh.DeleteFromTask).Methods("DELETE") + r.ServeHTTP(w, req) + + // Test that the status code is correct. + if status := w.Code; status != http.StatusOK { + t.Errorf("Status code is invalid. Expected %d. Got %d instead", http.StatusOK, status) + } + + // Read the response body. + data, err := ioutil.ReadAll(w.Result().Body) + if err != nil { + t.Fatal(err) + } + if strings.Compare(string(data), "message: delete success") != 0 { + t.Error("Expected message: delete success, got " + string(data)) + } +} + + +func TestUpdateTaskHandle(t *testing.T) { + err := godotenv.Load("../.env") + if err != nil { + t.Fatal("can't load env variable") + } + dbConn := models.Connect(os.Getenv("DB_URI")) + bh := controllers.NewBaseHandler(dbConn) + defer dbConn.DB.Close() + + task := models.RandomNewTask() + tasks, err := bh.BaseCtrl.GetAllTasks(AdminID) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + req, err := http.NewRequest("PUT",fmt.Sprintf("/tasks/%v", tasks[len(tasks)-1].Id), nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("token", AdminToken) + context.Set(req, "id", tasks[len(tasks)-1].Id) + context.Set(req, "userid", AdminID) + + newRequestBody, err := json.Marshal(task) + if err != nil { + http.Error(w, "marshal request body failed, err: "+err.Error(), http.StatusBadRequest) + return + } + req.Body = ioutil.NopCloser(bytes.NewBuffer(newRequestBody)) + + r := mux.NewRouter() + r.Use(middlewares.AdminVerified, middlewares.MiddlewareID) + r.HandleFunc("/tasks/{id}", bh.UpdateToTask).Methods("PUT") + r.ServeHTTP(w, req) + + // Test that the status code is correct. + if status := w.Code; status != http.StatusOK { + t.Errorf("Status code is invalid. Expected %d. Got %d instead", http.StatusOK, status) + } + tasks, _ = bh.BaseCtrl.GetAllTasks(AdminID) + // Read the response body. + var returnTasks models.NewTask + if err := json.NewDecoder(w.Body).Decode(&returnTasks); err != nil { + t.Errorf("Returned task is invalid JSON. Got: %s", returnTasks.Content) + } + + if strings.Compare(returnTasks.Content, tasks[len(tasks)-1].Content) != 0{ + t.Fatal("Get return wrong task expected: "+tasks[len(tasks)-1].Content+", got: " + returnTasks.Content) + } +} + +func TestCreateTaskHandle(t *testing.T) { + err := godotenv.Load("../.env") + if err != nil { + t.Fatal("can't load env variable") + } + dbConn := models.Connect(os.Getenv("DB_URI")) + bh := controllers.NewBaseHandler(dbConn) + defer dbConn.DB.Close() + + w := httptest.NewRecorder() + req, err := http.NewRequest("POST", "/tasks", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("token", AdminToken) + context.Set(req, "userid", AdminID) + + task := models.RandomNewTask() + task.UserId =AdminID + newRequestBody, err := json.Marshal(task) + if err != nil { + http.Error(w, "marshal request body failed, err: "+err.Error(), http.StatusBadRequest) + return + } + req.Body = ioutil.NopCloser(bytes.NewBuffer(newRequestBody)) + + r := mux.NewRouter() + r.Use(middlewares.LoggingVerified) + r.HandleFunc("/tasks", bh.CreateTask).Methods("POST") + r.ServeHTTP(w, req) + + // Test that the status code is correct. + if status := w.Code; status != http.StatusOK { + t.Errorf("Status code is invalid. Expected %d. Got %d instead", http.StatusOK, status) + } + users, _ := bh.BaseCtrl.GetAllTasks(AdminID) + // Read the response body. + var returnedTasks models.NewTask + if err := json.NewDecoder(w.Body).Decode(&returnedTasks); err != nil { + t.Errorf("Returned user is invalid JSON. Got: %s", returnedTasks.Content) + } + + bh.BaseCtrl.DeleteTask(int(users[len(users)-1].Id), AdminID) + + if strings.Compare(returnedTasks.Content, users[len(users)-1].Content) != 0 { + t.Fatal("Get return wrong user expected: " + users[len(users)-1].Content + ", got: " + returnedTasks.Content) + } +} diff --git a/intergrationTest/intergrationUser_test.go b/intergrationTest/intergrationUser_test.go new file mode 100644 index 000000000..644aa1eca --- /dev/null +++ b/intergrationTest/intergrationUser_test.go @@ -0,0 +1,265 @@ +package intergrationTest + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/gorilla/context" + "github.com/gorilla/mux" + "github.com/huynhhuuloc129/todo/controllers" + "github.com/huynhhuuloc129/todo/middlewares" + "github.com/huynhhuuloc129/todo/models" + "github.com/joho/godotenv" + _ "github.com/lib/pq" +) + +const ( + AdminToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2NTY0MzQ0NzgsImlkIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0.fprKS6TBv8L95_ZqD_jwbGRblm9hnWKi5vQVdGQEtqM" + AdminID = 1 +) + +func TestGetAllUsersHandle(t *testing.T) { + err := godotenv.Load("../.env") + if err != nil { + t.Fatal("can't load env variable") + } + dbConn := models.Connect(os.Getenv("DB_URI")) + bh := controllers.NewBaseHandler(dbConn) + defer dbConn.DB.Close() + + var users []models.NewUser + users = append(users, models.RandomNewUser(), models.RandomNewUser()) + + for _, user := range users { + bh.BaseCtrl.InsertUser(user) + } + + w := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/users", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("token", AdminToken) + + r := mux.NewRouter() + r.Use(middlewares.AdminVerified) + r.HandleFunc("/users", bh.ResponseAllUser).Methods("GET") + r.ServeHTTP(w, req) + + // Test that the status code is correct. + if status := w.Code; status != http.StatusOK { + t.Errorf("Status code is invalid. Expected %d. Got %d instead", http.StatusOK, status) + } + + // Read the response body. + data, err := ioutil.ReadAll(w.Result().Body) + if err != nil { + t.Fatal(err) + } + returnedUsers := []models.User{} + if err := json.Unmarshal(data, &returnedUsers); err != nil { + t.Errorf("Returned user list is invalid JSON. Got: %s", data) + } + // delete after insert to check + bh.BaseCtrl.DeleteUser(int(returnedUsers[len(returnedUsers)-1].Id)) + bh.BaseCtrl.DeleteUser(int(returnedUsers[len(returnedUsers)-2].Id)) + + if len(returnedUsers) < len(users) { + t.Errorf("Returned user list is an invalid length. Expected %d. Got %d instead", len(users), len(returnedUsers)) + } + count := 0 + for _, returnUser := range returnedUsers { + if returnUser.Username == users[0].Username || returnUser.Username == users[1].Username { + count++ + } + } + if count != len(users) { + t.Fatal("Returned user list is different") + } +} + +func TestGetOneUserHandle(t *testing.T) { + err := godotenv.Load("../.env") + if err != nil { + t.Fatal("can't load env variable") + } + dbConn := models.Connect(os.Getenv("DB_URI")) + bh := controllers.NewBaseHandler(dbConn) + defer dbConn.DB.Close() + + w := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/users/1", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("token", AdminToken) + context.Set(req, "id", 1) + + r := mux.NewRouter() + r.Use(middlewares.AdminVerified, middlewares.MiddlewareID) + r.HandleFunc("/users/{id}", bh.ResponseOneUser).Methods("GET") + r.ServeHTTP(w, req) + + // Test that the status code is correct. + if status := w.Code; status != http.StatusOK { + t.Errorf("Status code is invalid. Expected %d. Got %d instead", http.StatusOK, status) + } + + // Read the response body. + var returnedUsers models.NewUser + if err := json.NewDecoder(w.Body).Decode(&returnedUsers); err != nil { + t.Errorf("Returned user is invalid JSON. Got: %s", returnedUsers.Username) + } + + if returnedUsers.Username != "admin" { + t.Fatal("Get return wrong user expected: admin, got: " + returnedUsers.Username) + } +} + +func TestDeleteUserHandle(t *testing.T) { + err := godotenv.Load("../.env") + if err != nil { + t.Fatal("can't load env variable") + } + dbConn := models.Connect(os.Getenv("DB_URI")) + bh := controllers.NewBaseHandler(dbConn) + defer dbConn.DB.Close() + + user := models.RandomNewUser() + bh.BaseCtrl.InsertUser(user) + users, err := bh.BaseCtrl.GetAllUser() + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + req, err := http.NewRequest("DELETE", fmt.Sprintf("/users/%v", users[len(users)-1].Id), nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("token", AdminToken) + context.Set(req, "id", users[len(users)-1].Id) + + r := mux.NewRouter() + r.Use(middlewares.AdminVerified, middlewares.MiddlewareID) + r.HandleFunc("/users/{id}", bh.DeleteFromUser).Methods("DELETE") + r.ServeHTTP(w, req) + + // Test that the status code is correct. + if status := w.Code; status != http.StatusOK { + t.Errorf("Status code is invalid. Expected %d. Got %d instead", http.StatusOK, status) + } + + // Read the response body. + data, err := ioutil.ReadAll(w.Result().Body) + if err != nil { + t.Fatal(err) + } + if strings.Compare(string(data), "message: delete success") != 0 { + t.Error("Expected message: delete success, got " + string(data)) + } +} + +func TestUpdateUserHandle(t *testing.T) { + err := godotenv.Load("../.env") + if err != nil { + t.Fatal("can't load env variable") + } + dbConn := models.Connect(os.Getenv("DB_URI")) + bh := controllers.NewBaseHandler(dbConn) + defer dbConn.DB.Close() + + user := models.RandomNewUser() + users, err := bh.BaseCtrl.GetAllUser() + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + req, err := http.NewRequest("PUT", fmt.Sprintf("/users/%v", users[len(users)-1].Id), nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("token", AdminToken) + context.Set(req, "id", users[len(users)-1].Id) + newRequestBody, err := json.Marshal(user) + if err != nil { + http.Error(w, "marshal request body failed, err: "+err.Error(), http.StatusBadRequest) + return + } + req.Body = ioutil.NopCloser(bytes.NewBuffer(newRequestBody)) + + r := mux.NewRouter() + r.Use(middlewares.AdminVerified, middlewares.MiddlewareID) + r.HandleFunc("/users/{id}", bh.UpdateToUser).Methods("PUT") + r.ServeHTTP(w, req) + + // Test that the status code is correct. + if status := w.Code; status != http.StatusOK { + t.Errorf("Status code is invalid. Expected %d. Got %d instead", http.StatusOK, status) + } + users, _ = bh.BaseCtrl.GetAllUser() + // Read the response body. + var returnedUsers models.NewUser + if err := json.NewDecoder(w.Body).Decode(&returnedUsers); err != nil { + t.Errorf("Returned user is invalid JSON. Got: %s", returnedUsers.Username) + } + + if strings.Compare(returnedUsers.Username, users[len(users)-1].Username) != 0 { + t.Fatal("Get return wrong user expected: " + users[len(users)-1].Username + ", got: " + returnedUsers.Username) + } +} + +func TestCreateUserHandle(t *testing.T) { + err := godotenv.Load("../.env") + if err != nil { + t.Fatal("can't load env variable") + } + dbConn := models.Connect(os.Getenv("DB_URI")) + bh := controllers.NewBaseHandler(dbConn) + defer dbConn.DB.Close() + + w := httptest.NewRecorder() + req, err := http.NewRequest("POST", "/users", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("token", AdminToken) + + user := models.RandomNewUser() + newRequestBody, err := json.Marshal(user) + if err != nil { + http.Error(w, "marshal request body failed, err: "+err.Error(), http.StatusBadRequest) + return + } + req.Body = ioutil.NopCloser(bytes.NewBuffer(newRequestBody)) + + r := mux.NewRouter() + r.Use(middlewares.AdminVerified) + r.HandleFunc("/users", bh.CreateUser).Methods("POST") + r.ServeHTTP(w, req) + + // Test that the status code is correct. + if status := w.Code; status != http.StatusOK { + t.Errorf("Status code is invalid. Expected %d. Got %d instead", http.StatusOK, status) + } + users, _ := bh.BaseCtrl.GetAllUser() + // Read the response body. + var returnedUsers models.NewUser + if err := json.NewDecoder(w.Body).Decode(&returnedUsers); err != nil { + t.Errorf("Returned user is invalid JSON. Got: %s", returnedUsers.Username) + } + + bh.BaseCtrl.DeleteUser(int(users[len(users)-1].Id)) + + if strings.Compare(returnedUsers.Username, users[len(users)-1].Username) != 0 { + t.Fatal("Get return wrong user expected: " + users[len(users)-1].Username + ", got: " + returnedUsers.Username) + } +} diff --git a/jwt/jwt.go b/jwt/jwt.go new file mode 100644 index 000000000..59850b314 --- /dev/null +++ b/jwt/jwt.go @@ -0,0 +1,38 @@ +package jwt + +import ( + "fmt" + "net/http" + "os" + "strconv" + "time" + + "github.com/dgrijalva/jwt-go" +) + +func Create(username string, id int) (string, error) { + claims := jwt.MapClaims{} + claims["authorized"] = true + claims["username"] = username + claims["id"] = id + claims["exp"] = time.Now().Add(time.Hour * 12).Unix() //Token expired after 12 hours + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(os.Getenv("SECRET_JWT"))) +} + +func ParseToken(tokenString string) jwt.MapClaims { + claims := jwt.MapClaims{} + _, _ = jwt.ParseWithClaims(tokenString, claims, nil) + return claims +} + +func CheckToken(w http.ResponseWriter, r *http.Request) (string, int, bool) { + token := r.Header.Get("token") + result := ParseToken(token) + username := fmt.Sprintf("%v", result["username"]) + id, err := strconv.Atoi(fmt.Sprintf("%v", result["id"])) + if result["username"] == nil || err != nil { + return username, id, false + } + return username, id, true +} diff --git a/jwt/jwt_test.go b/jwt/jwt_test.go new file mode 100644 index 000000000..0e73e7958 --- /dev/null +++ b/jwt/jwt_test.go @@ -0,0 +1,48 @@ +package jwt + +import ( + "fmt" + "log" + "net/http/httptest" + "strings" + "testing" +) + +const ( + Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2NTYxMDUzNDEsImlkIjozLCJ1c2VybmFtZSI6Imh1dWxvYyJ9.MqHypeN42fopG5jHWEjk6bu9m7wSENqLewBGq9VC3sA" + TokenUsername = "huuloc" + TokenId = "3" +) + +// test function token parse +func TestTokenParse(t *testing.T) { + info := ParseToken(Token) + username := fmt.Sprintf("%v", info["username"]) + id := fmt.Sprintf("%v", info["id"]) + + if username != TokenUsername || id != TokenId { + log.Fatal("Token parse failed") + } +} + +// test function create token +func TestCreateToken(t *testing.T) { + newToken, err := Create(TokenUsername, 3) + if err != nil { + log.Fatal("Create token failed") + } + if len(newToken) < 32 { + log.Fatal("Invalid token created") + } +} + + +func TestCheckToken(t *testing.T){ + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "localhost:8000/tasks", nil) + req.Header.Set("token", Token) + username, id, check := CheckToken(w, req) + if username != TokenUsername || strings.Compare(fmt.Sprintf("%v",id), TokenId) != 0 || check==false{ + t.Fatal("Token check failed") + } +} \ No newline at end of file diff --git a/middlewares/middlewares.go b/middlewares/middlewares.go new file mode 100644 index 000000000..99f02da9e --- /dev/null +++ b/middlewares/middlewares.go @@ -0,0 +1,106 @@ +package middlewares + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strconv" + "strings" + + "github.com/gorilla/context" + "github.com/gorilla/mux" + "github.com/huynhhuuloc129/todo/controllers" + "github.com/huynhhuuloc129/todo/jwt" + "github.com/huynhhuuloc129/todo/models" +) + +// check if logging as admin or not +func AdminVerified(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username, userid, ok := jwt.CheckToken(w, r) + if !ok || strings.ToLower(username) != "admin" { + http.Error(w, "you need to login as ADMIN first to perform this action", http.StatusUnauthorized) + return + } + context.Set(r, "userid", userid) + next.ServeHTTP(w, r) + }) +} + +// check if logging or not +func LoggingVerified(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, userid, ok := jwt.CheckToken(w, r) + if !ok { + http.Error(w, "you need to login first to perform this action", http.StatusUnauthorized) + return + } + context.Set(r, "userid", userid) + context.Set(r, "id", userid) + + next.ServeHTTP(w, r) + }) +} + +// check ID is a number or not +func MiddlewareID(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id, err := strconv.Atoi(params["id"]) + if err != nil { + http.Error(w, "id url need to be a number", http.StatusBadRequest) + return + } + context.Set(r, "id", id) + next.ServeHTTP(w, r) + }) +} + +// check username duplicate/valid or not and hash password incoming +func ValidUsernameAndHashPassword(bh *controllers.BaseHandler, next http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var bodyJSON models.NewUser + if err := json.NewDecoder(r.Body).Decode(&bodyJSON); err != nil { + http.Error(w, "decode failed", http.StatusFailedDependency) + return + } + + context.Set(r, "password", bodyJSON.Password) + newpassword, err := models.Hash(bodyJSON.Password) + bodyJSON.Password = newpassword + + if err != nil { + http.Error(w, "hash password failed, err: "+err.Error(), http.StatusBadRequest) + return + } + newRequestBody, err := json.Marshal(bodyJSON) + if err != nil { + http.Error(w, "marshal request body failed, err: "+err.Error(), http.StatusBadRequest) + return + } + if _, ok := bh.BaseCtrl.CheckUserNameExist(bodyJSON.Username); ok { // Check username exist or not + http.Error(w, "this username already exist", http.StatusNotAcceptable) + return + } + + r.Body = ioutil.NopCloser(bytes.NewBuffer(newRequestBody)) + next.ServeHTTP(w, r) + }) +} + +func CheckLimitTaskUserMiddleware(bh *controllers.BaseHandler, next http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userid, _ := strconv.Atoi(fmt.Sprintf("%v", context.Get(r, "userid"))) // get userid from login + if ok, err := bh.BaseCtrl.CheckLimitTaskUser(userid); !ok { + if err != nil { + http.Error(w, err.Error(), http.StatusFailedDependency) + return + } + http.Error(w, "The limit of today is full", http.StatusFailedDependency) + return + } + next.ServeHTTP(w, r) + }) +} diff --git a/middlewares/middlewares_test.go b/middlewares/middlewares_test.go new file mode 100644 index 000000000..11e058f64 --- /dev/null +++ b/middlewares/middlewares_test.go @@ -0,0 +1,127 @@ +package middlewares + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "regexp" + "strings" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/gorilla/context" + "github.com/gorilla/mux" + "github.com/huynhhuuloc129/todo/controllers" + "github.com/huynhhuuloc129/todo/models" + "github.com/stretchr/testify/assert" +) + +const ( + AdminToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2NTY0MzQ0NzgsImlkIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0.fprKS6TBv8L95_ZqD_jwbGRblm9hnWKi5vQVdGQEtqM" + Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2NTYxMDUzNDEsImlkIjozLCJ1c2VybmFtZSI6Imh1dWxvYyJ9.MqHypeN42fopG5jHWEjk6bu9m7wSENqLewBGq9VC3sA" +) + +type httptestHandler struct { + w http.ResponseWriter + r *http.Request +} + +// test middleware Logging +func TestLoggingVerified(t *testing.T) { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "localhost:8000/users/", nil) + req.Header.Add("token", Token) // add token to header + Handler := httptestHandler{ + w: w, + r: req, + } + + logging := LoggingVerified(Handler) + logging.ServeHTTP(Handler.w, Handler.r) + + userid := context.Get(Handler.r, "userid") + id := context.Get(Handler.r, "id") + if id != 3 && userid != 3 { + t.Fatal("logging test failed") + } +} + + +// test middleware AdminVerified +func TestAdminVerified(t *testing.T) { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "localhost:8000/users/", nil) + req.Header.Add("token", AdminToken) // add token to header + Handler := httptestHandler{ + w: w, + r: req, + } + + adminverified := AdminVerified(Handler) + adminverified.ServeHTTP(Handler.w, Handler.r) + + userid := context.Get(Handler.r, "userid") + if userid != 1 { + t.Fatal("logging test failed") + } +} + +// test middleware MiddlewareID +func TestMiddlewareID(t *testing.T) { + w := httptest.NewRecorder() + id := "random text" + req := httptest.NewRequest(http.MethodGet, "localhost:8000/users", nil) + vars := map[string]string{ + "id": id, + } + req = mux.SetURLVars(req, vars) // set url variable + Handler := httptestHandler{ + w: w, + r: req, + } + + idHandler := MiddlewareID(Handler) + idHandler.ServeHTTP(Handler.w, Handler.r) + // newid := context.Get(Handler.r, "id") + res := w.Result() + resbody, _ := ioutil.ReadAll(res.Body) + if strings.Compare(string(resbody), "id url need to be a number") != 1 { + t.Fatal("test id failed") + } +} + +// test middleware ValidUsernameAndHashPassword +func TestValidUsernameAndHashPassword(t *testing.T) { + db, mock := models.NewMock() + dbConn := models.NewdbConn(db) + bh := controllers.NewBaseHandler(dbConn) + + newUser := models.RandomNewUser() // create a new random user + mock.ExpectQuery(regexp.QuoteMeta(models.QueryAllUsernameText)).WithArgs(newUser.Username).WillReturnRows(sqlmock.NewRows([]string{"id", "username", "password", "limittask"})) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "localhost:8000/users", nil) + requestBody, err := json.Marshal(newUser) + if err != nil { + t.Fatal("make body request failed") + } + req.Body = ioutil.NopCloser(bytes.NewBuffer(requestBody)) // send newuser to body of the request to the middlewares + + Handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + valid := ValidUsernameAndHashPassword(bh, Handler) + + valid.ServeHTTP(w, req) + var bodyJSON models.NewUser + if err := json.NewDecoder(req.Body).Decode(&bodyJSON); err != nil { // get info out after middleware process + http.Error(w, "decode failed", http.StatusFailedDependency) + return + } + assert.Equal(t, bodyJSON.Username, newUser.Username) + assert.NotEqual(t, bodyJSON.Password, newUser.Password) // after hash password can't be the same + assert.Equal(t, bodyJSON.LimitTask, newUser.LimitTask) +} + +func (l httptestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // function to implement interface +} diff --git a/models/db.go b/models/db.go new file mode 100644 index 000000000..ba2117bc4 --- /dev/null +++ b/models/db.go @@ -0,0 +1,52 @@ +package models + +import ( + "database/sql" + "log" + + "golang.org/x/crypto/bcrypt" +) + +// Create a Db connection +type DbConn struct { + DB *sql.DB +} +type BaseHandler struct { + BaseCtrl *DbConn +} + +// newdbConn returns a new DbConn +func NewdbConn(db *sql.DB) *DbConn { + return &DbConn{ + DB: db, + } +} + +// NewBaseHandler returns a new BaseHandler +func NewBaseHandler(BC *DbConn) *BaseHandler { + return &BaseHandler{ + BaseCtrl: BC, + } +} + +// connect to database +func Connect(DB_URI string) *DbConn{ + db, err := sql.Open("postgres", DB_URI) + dbconn := NewdbConn(db) + if err != nil { + log.Fatal("Connect to database failed, err: "+err.Error()) + } + return dbconn +} + +// Hash password into a crypt text +func Hash(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + return string(bytes), err +} + + // check a crypted text and a password user enter +func CheckPasswordHash(hashedPassword, password string) error { + return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) +} + diff --git a/models/db_test.go b/models/db_test.go new file mode 100644 index 000000000..93b7d6e4f --- /dev/null +++ b/models/db_test.go @@ -0,0 +1,22 @@ +package models + +import ( + "log" + "os" + "testing" + + "github.com/joho/godotenv" + _ "github.com/lib/pq" +) + +func TestDB(t *testing.T) { + if err := godotenv.Load("../.env"); err != nil { + log.Fatal(err) + } + DB_URI := os.Getenv("DB_URI") + db := Connect(DB_URI) + if db == nil { + t.Fatal("Error connect database") + } +} + diff --git a/models/dbquerytext.go b/models/dbquerytext.go new file mode 100644 index 000000000..85e19fcc4 --- /dev/null +++ b/models/dbquerytext.go @@ -0,0 +1,22 @@ +package models + +// All query text use for a whole project to change database + + +const ( // query for task + QueryAllTaskText = "SELECT * FROM tasks WHERE userid = $1" + FindTaskByIDText = "SELECT * FROM tasks WHERE id = $1 AND userid = $2" + InsertTaskText = "INSERT INTO tasks(content, status, time, timedone, userid) VALUES ($1, $2, $3, $4, $5)" + DeleteTaskText = "DELETE FROM tasks WHERE id = $1 AND userid = $2" + DeleteAllTaskText = "DELETE FROM tasks WHERE userid = $1" + UpdateTaskText = "UPDATE tasks SET content =COALESCE($1, content), status = COALESCE($2, status), timedone = COALESCE($3, timedone) WHERE id = $4 AND userid = $5" +) + +const ( // query for user + QueryAllUserText = "SELECT * FROM users" + QueryAllUsernameText = "SELECT * FROM users WHERE username = $1" + FindUserByIDText = "SELECT * FROM users WHERE id = $1" + InsertUserText = "INSERT INTO users(username, password, limittask) VALUES ($1, $2, $3)" + DeleteUserText = "DELETE FROM users WHERE id = $1" + UpdateUserText = "UPDATE users SET username = COALESCE($1, username), password = COALESCE($2, password), limittask = COALESCE($3, limittask) WHERE id = $4" +) diff --git a/models/export.go b/models/export.go new file mode 100644 index 000000000..7b554e4e7 --- /dev/null +++ b/models/export.go @@ -0,0 +1,71 @@ +package models + +import ( + "database/sql" + "log" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/huynhhuuloc129/todo/util" +) + +func CreateMockingDB() (sqlmock.Sqlmock, *BaseHandler) { + db, mock := NewMock() + dbConn := NewdbConn(db) + h := NewBaseHandler(dbConn) + return mock, h +} + +func NewMock() (*sql.DB, sqlmock.Sqlmock) { + db, mock, err := sqlmock.New() + if err != nil { + log.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + return db, mock +} + +// create random user +func RandomUser() User { + user := User{ + Id: util.RandomId(), + Username: util.RandomUsername(), + Password: util.RandomPassword(), + LimitTask: 10, + } + return user +} + +// create random new user +func RandomNewUser() NewUser { + newUser := NewUser{ + Username: util.RandomUsername(), + Password: util.RandomPassword(), + LimitTask: 10, + } + return newUser +} + +// fucntion create a random task +func RandomTask() Task { + task := Task{ + Id: int(util.RandomId()), + Content: util.RandomContent(), + Status: "pending", + Time: time.Now().Round(0), + TimeDone: time.Date(0001, 01, 01, 0, 0, 0, 0, time.Local).Round(0), + UserId: int(util.RandomUserid()), + } + return task +} + +//function create a random new task +func RandomNewTask() NewTask { + task := NewTask{ + Content: util.RandomContent(), + Status: "pending", + Time: time.Now().Round(0), + TimeDone: time.Date(0001, 01, 01, 0, 0, 0, 0, time.Local).Round(0), + UserId: int(util.RandomUserid()), + } + return task +} diff --git a/models/tasks.go b/models/tasks.go new file mode 100644 index 000000000..165470d56 --- /dev/null +++ b/models/tasks.go @@ -0,0 +1,104 @@ +package models + +import ( + "database/sql" + "errors" + "strings" + "time" +) + +type Task struct { + Id int + Content string + Status string + Time time.Time + TimeDone time.Time + UserId int +} +type NewTask struct { + Content string + Status string + Time time.Time + TimeDone time.Time + UserId int +} + +// Get all task from the database with user id +func (Conn *DbConn) GetAllTasks(userId int) ([]Task, error) { + rows, err := Conn.DB.Query(QueryAllTaskText, userId) + + var tasks []Task + if err != nil { + return tasks, err + } + for rows.Next() { + var task Task + if err = rows.Scan(&task.Id, &task.Content, &task.Status, &task.Time, &task.TimeDone, &task.UserId); err != nil { + return tasks, err + } + tasks = append(tasks, task) + } + return tasks, nil +} + +// Insert one task to the database +func (Conn *DbConn) InsertTask(task NewTask) error { + _, err := Conn.DB.Exec(InsertTaskText, task.Content, task.Status, task.Time, task.TimeDone, task.UserId) + return err +} + +// Delete task from database +func (Conn *DbConn) DeleteTask(id int, userid int) error { + _, err := Conn.DB.Exec(DeleteTaskText, id, userid) + return err +} + +// Delete task from database +func (Conn *DbConn) DeleteAllTaskFromUser(userid int) error { + _, err := Conn.DB.Exec(DeleteAllTaskText, userid) + return err +} + +// Update one task already exist in database +func (Conn *DbConn) UpdateTask(newTask Task, id int, userid int) error { + _, err := Conn.DB.Exec(UpdateTaskText, newTask.Content, newTask.Status, newTask.TimeDone, id, userid) + return err +} + +// Check ID task is valid or not +func (Conn *DbConn) FindTaskByID(id int, userId int) (Task, bool) { + task := Task{} + row := Conn.DB.QueryRow(FindTaskByIDText, id, userId) + err := row.Scan(&task.Id, &task.Content, &task.Status, &task.Time, &task.TimeDone, &task.UserId) + if err != nil { + if err != sql.ErrNoRows { + return task, false + } + return task, false + } + return task, true +} + +// check task per day pass limit of user or not +func (Conn *DbConn) CheckLimitTaskUser(userid int) (bool, error) { + user, ok := Conn.FindUserByID(userid) + if !ok { + return false, errors.New("userid Wrong") + } + tasks, err := Conn.GetAllTasks(userid) + if err != nil || !ok { + return false, err + } + if strings.ToLower(user.Username) == "admin" { // if admin then don't need to check + return true, nil + } + + countLimit := 0 + for _, task := range tasks { + year, month, day := task.Time.Date() + if year == time.Now().Year() && month == time.Now().Month() && day == time.Now().Day() { + countLimit++ + } + } + return countLimit < user.LimitTask, nil +} diff --git a/models/tasks_test.go b/models/tasks_test.go new file mode 100644 index 000000000..d4a558155 --- /dev/null +++ b/models/tasks_test.go @@ -0,0 +1,122 @@ +package models + +import ( + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/huynhhuuloc129/todo/util" + "github.com/stretchr/testify/assert" +) + +// unit test for get all task +func TestGetAllTask(t *testing.T) { + mock, h := CreateMockingDB() + + rows := sqlmock.NewRows([]string{"id", "content", "status", "time", "timedone", "userid"}) + for i := 0; i < 10; i++ { // create 10 random new task and add row + task := RandomTask() + rows.AddRow(task.Id, task.Content, task.Status, task.Time, task.TimeDone, 1) + } + + query := regexp.QuoteMeta(QueryAllTaskText) + mock.ExpectQuery(query).WithArgs(1).WillReturnRows(rows) + + tasks, err := h.BaseCtrl.GetAllTasks(1) + assert.NotEmpty(t, tasks) + assert.NoError(t, err) + assert.Len(t, tasks, 10) +} + +// unit test for get task by id +func TestFindTaskById(t *testing.T) { + mock, h := CreateMockingDB() + + task := RandomTask() + rows := sqlmock.NewRows([]string{"id", "content", "status", "time", "timedone", "userid"}).AddRow(task.Id, task.Content, task.Status, task.Time, task.TimeDone, task.UserId) + + query := regexp.QuoteMeta(FindTaskByIDText) + mock.ExpectQuery(query).WithArgs(task.Id, task.UserId).WillReturnRows(rows) + newTask, valid := h.BaseCtrl.FindTaskByID(int(task.Id), task.UserId) + + assert.Equal(t, newTask.Content, task.Content) + assert.Equal(t, newTask.UserId, task.UserId) + assert.NotNil(t, task) + assert.NotEqual(t, false, valid) +} + +// unit test for delete task +func TestDeleteTask(t *testing.T) { + mock, h := CreateMockingDB() + task := RandomTask() + query := regexp.QuoteMeta(DeleteTaskText) + mock.ExpectExec(query).WithArgs(task.Id, task.UserId).WillReturnResult(sqlmock.NewResult(0, 1)) + + err := h.BaseCtrl.DeleteTask(task.Id, task.UserId) + assert.NoError(t, err) +} + +// unit test for delete task +func TestDeleteAllTaskFromUser(t *testing.T) { + mock, h := CreateMockingDB() + + userid := util.RandomInt(1, 100) + rows := sqlmock.NewRows([]string{"id", "content", "status", "time", "timedone", "userid"}) + for i := 0; i < 10; i++ { // create 10 random new task and add row + task := RandomTask() + rows.AddRow(task.Id, task.Content, task.Status, task.Time, task.TimeDone, userid) + } + + query := regexp.QuoteMeta(DeleteAllTaskText) + mock.ExpectExec(query).WithArgs(userid).WillReturnResult(sqlmock.NewResult(0, 1)) + + err := h.BaseCtrl.DeleteAllTaskFromUser(int(userid)) + assert.NoError(t, err) +} + +// unit test for insert task +func TestInsertTask(t *testing.T) { + mock, h := CreateMockingDB() + + newTask := RandomNewTask() + mock.ExpectExec(regexp.QuoteMeta(InsertTaskText)).WithArgs(newTask.Content, newTask.Status, newTask.Time, newTask.TimeDone, newTask.UserId).WillReturnResult(sqlmock.NewResult(0, 1)) + + err := h.BaseCtrl.InsertTask(newTask) + assert.NoError(t, err) +} + +// unit test for update task +func TestUpdateTask(t *testing.T) { + mock, h := CreateMockingDB() + + query := regexp.QuoteMeta(UpdateTaskText) + + task := RandomTask() + mock.ExpectExec(query).WithArgs(task.Content, task.Status, task.TimeDone, task.Id, task.UserId).WillReturnResult(sqlmock.NewResult(0, 1)) + + err := h.BaseCtrl.UpdateTask(task, task.Id, task.UserId) + assert.NoError(t, err) +} + +// unit test for check limit task user +func TestCheckLimitTaskUser(t *testing.T) { + mock, h := CreateMockingDB() + + user := RandomUser() + rows1 := sqlmock.NewRows([]string{"id", "username", "password", "limittask"}) + rows1.AddRow(user.Id, user.Username, user.Password, user.LimitTask) + + rows2 := sqlmock.NewRows([]string{"id", "content", "status", "time", "timedone", "userid"}) + for i := 0; i < user.LimitTask; i++ { // create user.limit random new task and add row + task := RandomTask() + rows2.AddRow(task.Id, task.Content, task.Status, task.Time, task.TimeDone, user.Id) + } + + mock.ExpectQuery(regexp.QuoteMeta(FindUserByIDText)).WithArgs(user.Id).WillReturnRows(rows1) + mock.ExpectQuery(regexp.QuoteMeta(QueryAllTaskText)).WithArgs(user.Id).WillReturnRows(rows2) + + valid, err := h.BaseCtrl.CheckLimitTaskUser(int(user.Id)) + + assert.NoError(t, err) + assert.Equal(t, false, valid) // should return false because limittask >= user.limittask +} diff --git a/models/users.go b/models/users.go new file mode 100644 index 000000000..d1d2d57fb --- /dev/null +++ b/models/users.go @@ -0,0 +1,95 @@ +package models + +import ( + "database/sql" + "errors" + "log" +) + +type User struct { + Id int64 + Username string + Password string + LimitTask int +} +type NewUser struct { + Username string + Password string + LimitTask int +} + +// Get all user from the database +func (Conn *DbConn) GetAllUser() ([]User, error) { + rows, err := Conn.DB.Query(QueryAllUserText) + var users []User + if err != nil { + return users, err + } + + for rows.Next() { + var user User + if err = rows.Scan(&user.Id, &user.Username, &user.Password, &user.LimitTask); err != nil { + return users, err + } + users = append(users, user) + } + return users, nil +} + +// Insert one user to the database +func (Conn *DbConn) InsertUser(user NewUser) error { + if !CheckUserInput(user) { + return errors.New("decode failed") + } + _, err := Conn.DB.Exec(InsertUserText, user.Username, user.Password, user.LimitTask) + return err +} + +// delete 1 user +func (Conn *DbConn) DeleteUser(id int) error { + if id == 1 { + return errors.New("can't delete admin") + } + _, err := Conn.DB.Exec(DeleteUserText, id) + return err +} + +// Update one user already exist in database +func (Conn *DbConn) UpdateUser(newUser NewUser, id int) error { + if id ==1 { + return errors.New("can't update admin account") + } + if !CheckUserInput(newUser) { + return errors.New("user input invalid") + } + _, err := Conn.DB.Exec(UpdateUserText, newUser.Username, newUser.Password, newUser.LimitTask, id) + return err +} + +// Check ID is valid or not +func (Conn *DbConn) FindUserByID(id int) (User, bool) { + user := User{} + row := Conn.DB.QueryRow(FindUserByIDText, id) + err := row.Scan(&user.Id, &user.Username, &user.Password, &user.LimitTask) + if err != nil { + if err != sql.ErrNoRows { + log.Fatal(err) + } + return user, false + } + return user, true +} + +// Check if username already exist or not +func (Conn *DbConn) CheckUserNameExist(username string) (User, bool) { + user := User{} + row := Conn.DB.QueryRow(QueryAllUsernameText, username) + err := row.Scan(&user.Id, &user.Username, &user.Password, &user.LimitTask) + if err != nil { + if err != sql.ErrNoRows { + log.Fatal("Error checking if row exist, err: " + err.Error()) + } + return user, false + } + return user, true +} diff --git a/models/users_test.go b/models/users_test.go new file mode 100644 index 000000000..bc3673154 --- /dev/null +++ b/models/users_test.go @@ -0,0 +1,99 @@ +package models + +import ( + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +// unit test for get all user +func TestGetAllUser(t *testing.T) { + mock, h := CreateMockingDB() + + rows := sqlmock.NewRows([]string{"id", "username", "password", "limittask"}) + for i := 0; i < 10; i++ { + user := RandomUser() + rows.AddRow(user.Id, user.Username, user.Password, user.LimitTask) + } + + query := regexp.QuoteMeta(QueryAllUserText) + mock.ExpectQuery(query).WillReturnRows(rows) + + users, err := h.BaseCtrl.GetAllUser() + assert.NotEmpty(t, users) + assert.NoError(t, err) + assert.Len(t, users, 10) +} + +// unit test for get user by id +func TestFindUserById(t *testing.T) { + mock, h := CreateMockingDB() + + user := RandomUser() + rows := sqlmock.NewRows([]string{"id", "username", "password", "limittask"}).AddRow(user.Id, user.Username, user.Password, user.LimitTask) + + query := regexp.QuoteMeta(FindUserByIDText) + + mock.ExpectQuery(query).WithArgs(user.Id).WillReturnRows(rows) + newUser, valid := h.BaseCtrl.FindUserByID(int(user.Id)) + + assert.Equal(t, newUser.Username, user.Username) + assert.Equal(t, newUser.Password, user.Password) + assert.NotNil(t, user) + assert.NotEqual(t, false, valid) +} + +// unit test check username exist +func TestCheckUserNameExist(t *testing.T) { + mock, h := CreateMockingDB() + + user := RandomUser() + rows := sqlmock.NewRows([]string{"id", "username", "password", "limittask"}).AddRow(user.Id, user.Username, user.Password, user.LimitTask) + query := regexp.QuoteMeta(QueryAllUsernameText) + mock.ExpectQuery(query).WithArgs(user.Username).WillReturnRows(rows) + + user, valid := h.BaseCtrl.CheckUserNameExist(user.Username) + assert.NotNil(t, user) + assert.NotEqual(t, false, valid) +} + +// unit test for delete user +func TestDeleteUser(t *testing.T) { + mock, h := CreateMockingDB() + + user := RandomUser() + query := regexp.QuoteMeta(DeleteUserText) + mock.ExpectExec(query).WithArgs(user.Id).WillReturnResult(sqlmock.NewResult(0, 1)) + + err := h.BaseCtrl.DeleteUser(int(user.Id)) + assert.NoError(t, err) +} + +// unit test for insert user +func TestInsertUser(t *testing.T) { + mock, h := CreateMockingDB() + + query := regexp.QuoteMeta(InsertUserText) + + newUser := RandomNewUser() + mock.ExpectExec(query).WithArgs(newUser.Username, newUser.Password, newUser.LimitTask).WillReturnResult(sqlmock.NewResult(0, 1)) + + err := h.BaseCtrl.InsertUser(newUser) + assert.NoError(t, err) +} + +// unit test for delete user +func TestUpdateUser(t *testing.T) { + mock, h := CreateMockingDB() + + query := regexp.QuoteMeta(UpdateUserText) + + user := RandomUser() + newUser := RandomNewUser() + mock.ExpectExec(query).WithArgs(newUser.Username, newUser.Password, newUser.LimitTask, user.Id).WillReturnResult(sqlmock.NewResult(0, 1)) + + err := h.BaseCtrl.UpdateUser(newUser, int(user.Id)) + assert.NoError(t, err) +} diff --git a/models/validateInput.go b/models/validateInput.go new file mode 100644 index 000000000..5058d7372 --- /dev/null +++ b/models/validateInput.go @@ -0,0 +1,27 @@ +package models + +import ( + "strings" +) + +// Check user input is valid or not +func CheckUserInput(user NewUser) bool { + password := strings.TrimSpace(user.Password) + username := strings.TrimSpace(user.Username) + if password == "" || username == "" { + return false + } + return true +} + +// Check task input value is valid or not +func CheckTaskInput(task NewTask) bool { + var Content string + if task.Content != "" { + Content = strings.TrimSpace(task.Content) + } + if Content == "" { + return false + } + return true +} diff --git a/models/validateInput_test.go b/models/validateInput_test.go new file mode 100644 index 000000000..eb38c0ebe --- /dev/null +++ b/models/validateInput_test.go @@ -0,0 +1,76 @@ +package models + +import ( + "log" + "testing" + "time" +) + +// test function check user input +func TestCheckUserInput(t *testing.T) { + user1 := NewUser{ + Username: "", + Password: "", + } + user2 := NewUser{ + Username: "", + Password: "asdfasf", + } + user3 := NewUser{ + Username: "asdfasf", + Password: "", + } + user4 := NewUser{ + Username: "asdfasfa", + Password: "asdfsafsf", + } + result1 := CheckUserInput(user1) + result2 := CheckUserInput(user2) + result3 := CheckUserInput(user3) + result4 := CheckUserInput(user4) + + if result1 || result2 || result3 || !result4 { + log.Fatal("Check user input failed") + } + +} + +// test function check task input +func TestCheckTaskInput(t *testing.T) { + task1 := NewTask{ + Content: "sadfsaf", + Status: "pending", + Time: time.Now(), + TimeDone: time.Now(), + UserId: 1, + } + task2 := NewTask{ + Content: "", + Status: "pending", + Time: time.Now(), + TimeDone: time.Now(), + UserId: 1, + } + task3 := NewTask{ + Content: "sadfsaf", + Status: "pending", + Time: time.Now(), + TimeDone: time.Now(), + UserId: 1, + } + task4 := NewTask{ + Content: "admin Task", + Status: "pending", + Time: time.Now(), + TimeDone: time.Now(), + UserId: 1, + } + result1 := CheckTaskInput(task1) + result2 := CheckTaskInput(task2) + result3 := CheckTaskInput(task3) + result4 := CheckTaskInput(task4) + + if !result1 || result2 || !result3 || !result4 { + t.Fatal("Check user input failed") + } +} diff --git a/routers/routers.go b/routers/routers.go new file mode 100644 index 000000000..68dd441fc --- /dev/null +++ b/routers/routers.go @@ -0,0 +1,29 @@ +package routers + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/huynhhuuloc129/todo/controllers" + "github.com/huynhhuuloc129/todo/middlewares" +) + +func Routing(r *mux.Router, bh *controllers.BaseHandler) { + userRouter(r, bh) + taskRouter(r, bh) + authRoutingLogin := r.PathPrefix("/auth/login").Subrouter() + authRoutingLogin.HandleFunc("", bh.Login).Methods("POST") + + authRoutingRegister := r.PathPrefix("/auth/register").Subrouter() + authRoutingRegister.HandleFunc("", middlewares.ValidUsernameAndHashPassword(bh, bh.Register)).Methods("POST") + r.NotFoundHandler = http.HandlerFunc(notFound) + r.MethodNotAllowedHandler = http.HandlerFunc(methodNotAllowed) +} + +func notFound(w http.ResponseWriter, r *http.Request){ + http.Error(w, "Page not found", http.StatusNotFound) +} + +func methodNotAllowed(w http.ResponseWriter, r *http.Request){ + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) +} \ No newline at end of file diff --git a/routers/taskRouter.go b/routers/taskRouter.go new file mode 100644 index 000000000..dd82413b7 --- /dev/null +++ b/routers/taskRouter.go @@ -0,0 +1,21 @@ +package routers + +import ( + "github.com/gorilla/mux" + "github.com/huynhhuuloc129/todo/controllers" + "github.com/huynhhuuloc129/todo/middlewares" +) + +func taskRouter(r *mux.Router, bh *controllers.BaseHandler) { + taskRouting := r.PathPrefix("/tasks").Subrouter() + taskRouting.Use(middlewares.LoggingVerified) // only logging can check task + taskRouting.HandleFunc("", bh.ResponseAllTask).Methods("GET") + taskRouting.HandleFunc("", middlewares.CheckLimitTaskUserMiddleware(bh, bh.CreateTask)).Methods("POST") + + taskRoutingid := r.PathPrefix("/tasks/{id}").Subrouter() + taskRoutingid.Use(middlewares.LoggingVerified, middlewares.MiddlewareID) // only logging and ID valid can check task + taskRoutingid.HandleFunc("", bh.DeleteFromTask).Methods("DELETE") + taskRoutingid.HandleFunc("", bh.ResponseOneTask).Methods("GET") + taskRoutingid.HandleFunc("", bh.UpdateToTask).Methods("PUT") + taskRoutingid.HandleFunc("", bh.UpdateToTask).Methods("PATCH") +} diff --git a/routers/userRouter.go b/routers/userRouter.go new file mode 100644 index 000000000..3ebe3b608 --- /dev/null +++ b/routers/userRouter.go @@ -0,0 +1,25 @@ +package routers + +import ( + "github.com/gorilla/mux" + "github.com/huynhhuuloc129/todo/controllers" + "github.com/huynhhuuloc129/todo/middlewares" +) + +func userRouter(r *mux.Router, bh *controllers.BaseHandler) { + UserRouting := r.PathPrefix("/users").Subrouter() + UserRouting.Use(middlewares.AdminVerified) // middleware admin, only admin can be modified user + UserRouting.HandleFunc("", bh.ResponseAllUser).Methods("GET") + UserRouting.HandleFunc("", middlewares.ValidUsernameAndHashPassword(bh, bh.CreateUser)).Methods("POST") + + userRoutingGetme := r.PathPrefix("/users/info").Subrouter() + userRoutingGetme.Use(middlewares.LoggingVerified) + userRoutingGetme.HandleFunc("", bh.ResponseOneUser).Methods("GET") + + userRoutingid := r.PathPrefix("/users/{id}").Subrouter() + userRoutingid.Use(middlewares.AdminVerified, middlewares.MiddlewareID)// middleware admin, only admin can be modified user and check ID + userRoutingid.HandleFunc("", bh.ResponseOneUser).Methods("GET") + userRoutingid.HandleFunc("", bh.DeleteFromUser).Methods("DELETE") + userRoutingid.HandleFunc("", middlewares.ValidUsernameAndHashPassword(bh, bh.UpdateToUser)).Methods("PUT") + userRoutingid.HandleFunc("", middlewares.ValidUsernameAndHashPassword(bh, bh.UpdateToUser)).Methods("PATCH") +} diff --git a/server.go b/server.go new file mode 100644 index 000000000..bfe1dc1c5 --- /dev/null +++ b/server.go @@ -0,0 +1,34 @@ +package main + +import ( + "log" + "net/http" + "os" + + "github.com/gorilla/mux" + "github.com/huynhhuuloc129/todo/controllers" + "github.com/huynhhuuloc129/todo/models" + "github.com/huynhhuuloc129/todo/routers" + "github.com/joho/godotenv" + _ "github.com/lib/pq" +) + +const defaultPort = "8000" + +func main() { + if err := godotenv.Load(); err != nil { + log.Fatal(err) + } + port := os.Getenv("PORT") // Load PORT from env file (if it had) + if port == "" { + port = defaultPort + } + DB_URI := os.Getenv("DB_URI") + + db := models.Connect(DB_URI) // connect to database + Repo := controllers.NewBaseHandler(db) + + r := mux.NewRouter().StrictSlash(true) + routers.Routing(r, Repo) + log.Fatal(http.ListenAndServe(":"+port, r)) +} diff --git a/todo b/todo new file mode 100755 index 000000000..e937c3ff0 Binary files /dev/null and b/todo differ diff --git a/util/random.go b/util/random.go new file mode 100644 index 000000000..0cf188436 --- /dev/null +++ b/util/random.go @@ -0,0 +1,59 @@ +package util + +import ( + "math/rand" + "strings" + "time" +) + +const alphabet = "abcdefghijklmnoprstuwxyz" + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// RandomInt generates a random interger between min and max +func RandomInt(min, max int64) int64 { + return min + rand.Int63n(max-min+1) +} + +// RandomString generates a random string with length of n +func RandomString(n int) string { + var sb strings.Builder + k := len(alphabet) + + for i := 0; i < n; i++ { + c := alphabet[rand.Intn(k)] + sb.WriteByte(c) + } + return sb.String() +} + +// RandomUsername generates a random username +func RandomUsername() string{ + return RandomString(15) +} + +// RandomPassword generates a random password +func RandomPassword() string{ + return RandomString(17) +} + +// RandomContent generates a random content of a task +func RandomContent() string{ + return RandomString(17) +} + +// RandomUserid generates a random number of userid +func RandomUserid() int64{ + return RandomInt(0, 100) +} + +// RandomId generates a random number of id +func RandomId() int64{ + return RandomInt(0, 100) +} + +func RandomLimittask() int64 { + return RandomInt(0, 15) +} \ No newline at end of file