Skip to content
This repository was archived by the owner on Jan 6, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 36 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,50 @@
### Requirements

- 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.
- 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?
- 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?

### Notes
#### How to run

- 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.
`Simply run the main.exe in build folder`

### How to submit your solution?
#### Test user

- Fork this repo and show us your development progress via a PR
Username: `dung`

### Interesting facts about Manabie
Password: `dung1234`

- 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.
#### Sample CURL:

Thank you for spending time to read and attempt our take-home assessment. We are looking forward to your submission.
`curl --location --request POST 'localhost:8800/api/togo/v1/task' \
--header 'Content-Type: application/json' \
--data-raw '{
"content": "Task 1",
"user_id": 1,
"date": "11-12-2022"
}'`

#### "date" must in type dd-mm-yyyy

#### How to run test

simple change dir to api_test folder and edit some date to make sure if it not duplicated and then

`go test -v -run TestCreateTask `

or

`go test`

#### My solution is using gorm with support a lot of types database, so it is much easier to get data which relation

#### A lot of things to do to enhance this (like cache database to avoid data reading time, doing pagination)
130 changes: 130 additions & 0 deletions api_test/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package api_test

import (
"bytes"
"encoding/json"
"fmt"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"io/ioutil"
"net/http"
"strconv"
"testing"
"togo-thdung002/entities"
)

var (
baseURL string = "http://localhost:8800/api/togo/v1"
)

func TestCreateUser(t *testing.T) {
user := entities.User{
Username: "dung",
Password: "dung123",
Limit: 15,
}
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(user)
if err != nil {
t.Log(err)
t.FailNow()
}

request, _ := http.NewRequest(http.MethodPost, baseURL+"/user/register", &buf)
request.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
resp, err := http.DefaultClient.Do(request)
if err != nil {
fmt.Println(err)
t.FailNow()
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
t.FailNow()
}
response := new(interface{})
err = json.Unmarshal(body, response)
if err != nil {
t.Fail()
}
t.Log(*response)

assert.Equal(t, 200, resp.StatusCode)

}

func TestCreateTask(t *testing.T) {
task := entities.Task{
Content: "Task number 1",
UserID: 1,
Date: "25-06-2022",
}
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(task)
if err != nil {
t.Log(err)
t.FailNow()
}

request, _ := http.NewRequest(http.MethodPost, baseURL+"/task", &buf)
request.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
resp, err := http.DefaultClient.Do(request)
if err != nil {
fmt.Println(err)
t.FailNow()
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
t.FailNow()
}
response := new(interface{})
err = json.Unmarshal(body, response)
if err != nil {
t.Fail()
}
t.Log(*response)

assert.Equal(t, 200, resp.StatusCode)

}

func TestCreateBatchTask(t *testing.T) {
//create batch task to get error
for i := 1; i <= 10; i++ {
task := entities.Task{
Content: "Task number " + strconv.Itoa(i),
UserID: 1,
Date: "15-07-2022",
}
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(task)
if err != nil {
t.Log(err)
t.FailNow()
}
request, _ := http.NewRequest(http.MethodPost, baseURL+"/task", &buf)
request.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
resp, err := http.DefaultClient.Do(request)
if err != nil {
fmt.Println(err)
t.FailNow()
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
t.FailNow()
}
response := new(interface{})
err = json.Unmarshal(body, response)
if err != nil {
t.Fail()
}
t.Log(*response)
assert.Equal(t, 200, resp.StatusCode)

}

}
47 changes: 47 additions & 0 deletions build/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package config

import (
"encoding/json"
log "github.com/sirupsen/logrus"
"io/ioutil"
)

type Config struct {
API struct {
Listen string `json:"listen"`
DBAddress string `json:"db"`
} `json:"api"`
}

func getConfigFromFile(filename string) ([]byte, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return data, nil
}

func decodeLocalConf(data []byte) (*Config, error) {
config := &Config{}

// try to decode json first and yaml in the next step
if err := json.Unmarshal(data, &config); err != nil {
return nil, err
}
// validate config
//config.Prefix = strings.TrimSuffix(config.Prefix, "/")
return config, nil
}

func LoadConfig(filename string) (*Config, error) {

log.Info("Loading configuration from ", filename)

confData, jerr := getConfigFromFile(filename)
if jerr != nil {
log.Fatal("Failed to load configuration: ", jerr)
return nil, jerr
}

return decodeLocalConf(confData)
}
6 changes: 6 additions & 0 deletions build/config/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"api": {
"listen": ":8800",
"db": "togo.db"
}
}
Binary file added build/togo.db
Binary file not shown.
47 changes: 47 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package config

import (
"encoding/json"
log "github.com/sirupsen/logrus"
"io/ioutil"
)

type Config struct {
API struct {
Listen string `json:"listen"`
DBAddress string `json:"db"`
} `json:"api"`
}

func getConfigFromFile(filename string) ([]byte, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return data, nil
}

func decodeLocalConf(data []byte) (*Config, error) {
config := &Config{}

// try to decode json first and yaml in the next step
if err := json.Unmarshal(data, &config); err != nil {
return nil, err
}
// validate config
//config.Prefix = strings.TrimSuffix(config.Prefix, "/")
return config, nil
}

func LoadConfig(filename string) (*Config, error) {

log.Info("Loading configuration from ", filename)

confData, jerr := getConfigFromFile(filename)
if jerr != nil {
log.Fatal("Failed to load configuration: ", jerr)
return nil, jerr
}

return decodeLocalConf(confData)
}
6 changes: 6 additions & 0 deletions config/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"api": {
"listen": ":8800",
"db": "togo.db"
}
}
12 changes: 12 additions & 0 deletions const/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package _const

import "errors"

var (
ErrOnCreateUser = errors.New("Create user fail")
ErrOnCreateTask = errors.New("Create task fail")
ErrLimitedTask = errors.New("Reach limit task")
ErrUserNotFound = errors.New("User not found")
ErrDateType = errors.New("Date type not right")
ErrValidate = errors.New("Validate fail")
)
37 changes: 37 additions & 0 deletions controllers/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package controllers

import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
)

func (ctrl *Controller) loadMux() {
e := echo.New()

e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(1000)))
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
Skipper: middleware.DefaultSkipper,
AllowOrigins: []string{"*"},
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
AllowCredentials: true,
}))

apiv1 := e.Group("/api")
togov1 := apiv1.Group("/togo/v1")
{
task := togov1.Group("/task")
{
task.GET("", apiGetTask(ctrl))
task.POST("", apiPostTask(ctrl))
}
}
{
user := togov1.Group("/user")
{
user.POST("/register", apiPostUser(ctrl))
}
}
ctrl.e = e

}
Loading