Skip to content

Commit

Permalink
Merge pull request #6 from systemli/ticker-users-api
Browse files Browse the repository at this point in the history
add endpoints to interact with users at the ticker entity
  • Loading branch information
0x46616c6b authored Jan 9, 2019
2 parents 0adb5ab + 10f0d45 commit 7f046aa
Show file tree
Hide file tree
Showing 14 changed files with 466 additions and 38 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ require (
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/labstack/echo v3.3.5+incompatible // indirect
github.com/labstack/gommon v0.2.8 // indirect
github.com/labstack/gommon v0.2.8
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mitchellh/gox v0.4.0 // indirect
github.com/mitchellh/iochan v1.0.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ func API() *gin.Engine {
admin.PUT(`/tickers/:tickerID/twitter`, PutTickerTwitterHandler)
admin.DELETE(`/tickers/:tickerID`, DeleteTickerHandler)
admin.PUT(`/tickers/:tickerID/reset`, ResetTickerHandler)
admin.GET(`/tickers/:tickerID/users`, GetTickerUsersHandler)
admin.PUT(`/tickers/:tickerID/users`, PutTickerUsersHandler)
admin.DELETE(`/tickers/:tickerID/users/:userID`, DeleteTickerUserHandler)

admin.GET(`/tickers/:tickerID/messages`, GetMessagesHandler)
admin.GET(`/tickers/:tickerID/messages/:messageID`, GetMessageHandler)
Expand Down
4 changes: 2 additions & 2 deletions internal/api/messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ func TestPostMessageHandler(t *testing.T) {
r := setup()

ticker := model.Ticker{
ID: 1,
Active: true,
ID: 1,
Active: true,
Hashtags: []string{`#hashtag`},
}

Expand Down
135 changes: 135 additions & 0 deletions internal/api/tickers.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,40 @@ func GetTickerHandler(c *gin.Context) {
c.JSON(http.StatusOK, NewJSONSuccessResponse("ticker", NewTickerResponse(&ticker)))
}

//GetTickerUsersHandler returns Users for the given ticker
func GetTickerUsersHandler(c *gin.Context) {
me, err := Me(c)
if err != nil {
c.JSON(http.StatusNotFound, NewJSONErrorResponse(ErrorCodeDefault, ErrorUserNotFound))
return
}

tickerID, err := strconv.Atoi(c.Param("tickerID"))
if err != nil {
c.JSON(http.StatusBadRequest, NewJSONErrorResponse(ErrorCodeDefault, err.Error()))
return
}

var ticker Ticker
err = DB.One("ID", tickerID, &ticker)
if err != nil {
c.JSON(http.StatusNotFound, NewJSONErrorResponse(ErrorCodeNotFound, err.Error()))
return
}

if !me.IsSuperAdmin {
if !contains(me.Tickers, tickerID) {
c.JSON(http.StatusForbidden, NewJSONErrorResponse(ErrorCodeInsufficientPermissions, ErrorInsufficientPermissions))
return
}
}

//TODO: Discuss need of Pagination
users, _ := FindUsersByTicker(ticker)

c.JSON(http.StatusOK, NewJSONSuccessResponse("users", NewUsersResponse(users)))
}

//PostTickerHandler creates and returns a new Ticker
func PostTickerHandler(c *gin.Context) {
if !IsAdmin(c) {
Expand Down Expand Up @@ -138,6 +172,55 @@ func PutTickerHandler(c *gin.Context) {
c.JSON(http.StatusOK, NewJSONSuccessResponse("ticker", NewTickerResponse(&ticker)))
}

//PutTickerUsersHandler changes the allowed users for a ticker
func PutTickerUsersHandler(c *gin.Context) {
me, err := Me(c)
if err != nil {
c.JSON(http.StatusNotFound, NewJSONErrorResponse(ErrorCodeDefault, ErrorUserNotFound))
return
}

tickerID, err := strconv.Atoi(c.Param("tickerID"))
if err != nil {
c.JSON(http.StatusBadRequest, NewJSONErrorResponse(ErrorCodeDefault, err.Error()))
return
}

var ticker Ticker
err = DB.One("ID", tickerID, &ticker)
if err != nil {
c.JSON(http.StatusNotFound, NewJSONErrorResponse(ErrorCodeNotFound, err.Error()))
return
}

if !me.IsSuperAdmin {
if !contains(me.Tickers, tickerID) {
c.JSON(http.StatusForbidden, NewJSONErrorResponse(ErrorCodeInsufficientPermissions, ErrorInsufficientPermissions))
return
}
}

var body struct {
Users []int `json:"users" binding:"required"`
}

err = c.Bind(&body)
if err != nil {
c.JSON(http.StatusBadRequest, NewJSONErrorResponse(ErrorCodeDefault, err.Error()))
return
}

err = AddUsersToTicker(ticker, body.Users)
if err != nil {
c.JSON(http.StatusInternalServerError, NewJSONErrorResponse(ErrorCodeDefault, err.Error()))
return
}

users, _ := FindUsersByTicker(ticker)

c.JSON(http.StatusOK, NewJSONSuccessResponse("users", NewUsersResponse(users)))
}

//
func PutTickerTwitterHandler(c *gin.Context) {
me, err := Me(c)
Expand Down Expand Up @@ -240,6 +323,58 @@ func DeleteTickerHandler(c *gin.Context) {
})
}

//DeleteTickerUserHandler removes ticker credentials for a user
func DeleteTickerUserHandler(c *gin.Context) {
me, err := Me(c)
if err != nil {
c.JSON(http.StatusNotFound, NewJSONErrorResponse(ErrorCodeDefault, ErrorUserNotFound))
return
}

tickerID, err := strconv.Atoi(c.Param("tickerID"))
if err != nil {
c.JSON(http.StatusBadRequest, NewJSONErrorResponse(ErrorCodeDefault, err.Error()))
return
}

var ticker Ticker
err = DB.One("ID", tickerID, &ticker)
if err != nil {
c.JSON(http.StatusNotFound, NewJSONErrorResponse(ErrorCodeNotFound, err.Error()))
return
}

if !me.IsSuperAdmin {
if !contains(me.Tickers, tickerID) {
c.JSON(http.StatusForbidden, NewJSONErrorResponse(ErrorCodeInsufficientPermissions, ErrorInsufficientPermissions))
return
}
}

userID, err := strconv.Atoi(c.Param("userID"))
if err != nil {
c.JSON(http.StatusBadRequest, NewJSONErrorResponse(ErrorCodeDefault, err.Error()))
return
}

var user User
err = DB.One("ID", userID, &user)
if err != nil {
c.JSON(http.StatusNotFound, NewJSONErrorResponse(ErrorCodeNotFound, err.Error()))
return
}

err = RemoveTickerFromUser(ticker, user)
if err != nil {
c.JSON(http.StatusInternalServerError, NewJSONErrorResponse(ErrorCodeDefault, err.Error()))
return
}

users, _ := FindUsersByTicker(ticker)

c.JSON(http.StatusOK, NewJSONSuccessResponse("users", NewUsersResponse(users)))
}

func ResetTickerHandler(c *gin.Context) {
if !IsAdmin(c) {
c.JSON(http.StatusForbidden, NewJSONErrorResponse(ErrorCodeInsufficientPermissions, ErrorInsufficientPermissions))
Expand Down
168 changes: 168 additions & 0 deletions internal/api/tickers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,174 @@ func TestResetTickerHandler(t *testing.T) {
})
}

func TestGetTickerUsersHandler(t *testing.T) {
r := setup()

r.GET("/v1/admin/tickers/2/users").
SetHeader(map[string]string{"Authorization": "Bearer " + AdminToken}).
Run(api.API(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
assert.Equal(t, 404, r.Code)
})

ticker := model.Ticker{
ID: 1,
Active: true,
}

storage.DB.Save(&ticker)

r.GET("/v1/admin/tickers/1/users").
SetHeader(map[string]string{"Authorization": "Bearer " + AdminToken}).
Run(api.API(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
assert.Equal(t, 200, r.Code)
})

r.GET("/v1/admin/tickers/1/users").
SetHeader(map[string]string{"Authorization": "Bearer " + UserToken}).
Run(api.API(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
assert.Equal(t, 403, r.Code)
})

user, _ := model.NewUser("user@systemli.org", "password")
user.Tickers = []int{ticker.ID}

storage.DB.Save(user)

r.GET("/v1/admin/tickers/1/users").
SetHeader(map[string]string{"Authorization": "Bearer " + AdminToken}).
Run(api.API(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
assert.Equal(t, 200, r.Code)

type jsonResp struct {
Data map[string][]model.UserResponse `json:"data"`
Status string `json:"status"`
Error interface{} `json:"error"`
}

var response jsonResp

err := json.Unmarshal(r.Body.Bytes(), &response)
if err != nil {
t.Fatal(err)
}

assert.Equal(t, model.ResponseSuccess, response.Status)
assert.Equal(t, nil, response.Error)
assert.Equal(t, 1, len(response.Data))

assert.Equal(t, 1, len(response.Data["users"]))
assert.Equal(t, "user@systemli.org", response.Data["users"][0].Email)
})
}

func TestPutTickerUsersHandler(t *testing.T) {
r := setup()

r.PUT("/v1/admin/tickers/2/users").
SetHeader(map[string]string{"Authorization": "Bearer " + AdminToken}).
Run(api.API(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
assert.Equal(t, 404, r.Code)
})

ticker := model.Ticker{
ID: 1,
Active: true,
}

storage.DB.Save(&ticker)

r.PUT("/v1/admin/tickers/1/users").
SetHeader(map[string]string{"Authorization": "Bearer " + UserToken}).
Run(api.API(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
assert.Equal(t, 403, r.Code)
})

body := `{"users": [2]}`

r.PUT("/v1/admin/tickers/1/users").
SetHeader(map[string]string{"Authorization": "Bearer " + AdminToken}).
SetBody(body).
Run(api.API(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
assert.Equal(t, 200, r.Code)

type jsonResp struct {
Data map[string][]model.UserResponse `json:"data"`
Status string `json:"status"`
Error interface{} `json:"error"`
}

var response jsonResp

err := json.Unmarshal(r.Body.Bytes(), &response)
if err != nil {
t.Fatal(err)
}

assert.Equal(t, model.ResponseSuccess, response.Status)
assert.Equal(t, nil, response.Error)
assert.Equal(t, 1, len(response.Data))

assert.Equal(t, 1, len(response.Data["users"]))
assert.Equal(t, "louis@systemli.org", response.Data["users"][0].Email)
})
}

func TestDeleteTickerUserHandler(t *testing.T) {
r := setup()

r.DELETE("/v1/admin/tickers/1/users/1").
SetHeader(map[string]string{"Authorization": "Bearer " + AdminToken}).
Run(api.API(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
assert.Equal(t, 404, r.Code)
})

ticker := model.Ticker{
ID: 1,
Active: true,
}

storage.DB.Save(&ticker)

r.DELETE("/v1/admin/tickers/1/users/10").
SetHeader(map[string]string{"Authorization": "Bearer " + AdminToken}).
Run(api.API(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
assert.Equal(t, 404, r.Code)
})

user := model.User{
ID: 10,
Email: "user_10@systemli.org",
Tickers: []int{1},
}

storage.DB.Save(&user)

r.DELETE("/v1/admin/tickers/1/users/10").
SetHeader(map[string]string{"Authorization": "Bearer " + AdminToken}).
Run(api.API(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
assert.Equal(t, 200, r.Code)

type jsonResp struct {
Data map[string][]model.UserResponse `json:"data"`
Status string `json:"status"`
Error interface{} `json:"error"`
}

var response jsonResp

err := json.Unmarshal(r.Body.Bytes(), &response)
if err != nil {
t.Fatal(err)
}

assert.Equal(t, model.ResponseSuccess, response.Status)
assert.Equal(t, nil, response.Error)
assert.Equal(t, 1, len(response.Data))
assert.Equal(t, 0, len(response.Data["users"]))
})

}

func setup() *gofight.RequestConfig {
gin.SetMode(gin.TestMode)

Expand Down
5 changes: 1 addition & 4 deletions internal/api/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"net/http"
"strconv"

"github.com/asdine/storm"
"github.com/gin-gonic/gin"

. "github.com/systemli/ticker/internal/model"
Expand All @@ -18,10 +17,8 @@ func GetUsersHandler(c *gin.Context) {
return
}

var users []User

//TODO: Discuss need of Pagination
err := DB.All(&users, storm.Reverse())
users, err := FindUsers()
if err != nil {
c.JSON(http.StatusNotFound, NewJSONErrorResponse(ErrorCodeDefault, err.Error()))
return
Expand Down
4 changes: 1 addition & 3 deletions internal/bridge/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"github.com/dghubble/go-twitter/twitter"
"github.com/dghubble/oauth1"
"github.com/systemli/ticker/internal/model"
"github.com/systemli/ticker/internal/util"

"strconv"
)

Expand Down Expand Up @@ -34,7 +32,7 @@ func (tb *TwitterBridge) Initialized() bool {
func (tb *TwitterBridge) Update(ticker model.Ticker, message model.Message) (*twitter.Tweet, error) {
client := tb.client(ticker.Twitter.Token, ticker.Twitter.Secret)

tweet, _, err := client.Statuses.Update(util.PrepareTweet(&ticker, &message), nil)
tweet, _, err := client.Statuses.Update(message.PrepareTweet(&ticker), nil)
if err != nil {
return tweet, err
}
Expand Down
Loading

0 comments on commit 7f046aa

Please sign in to comment.