Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Extract Websites from Ticker #353

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ func API(config config.Config, store storage.Storage, log *logrus.Logger) *gin.E
admin.GET(`/tickers/:tickerID`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.GetTicker)
admin.POST(`/tickers`, user.NeedAdmin(), handler.PostTicker)
admin.PUT(`/tickers/:tickerID`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.PutTicker)
admin.DELETE(`/tickers/:tickerID/websites/:domain`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.DeleteTickerWebsite)
admin.POST(`/tickers/:tickerID/websites`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.PostTickerWebsite)
admin.PUT(`/tickers/:tickerID/telegram`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.PutTickerTelegram)
admin.DELETE(`/tickers/:tickerID/telegram`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.DeleteTickerTelegram)
admin.PUT(`/tickers/:tickerID/mastodon`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.PutTickerMastodon)
Expand Down
2 changes: 1 addition & 1 deletion internal/api/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (s *InitTestSuite) TestGetInit() {

s.Equal(http.StatusOK, s.w.Code)
s.Equal(`{"data":{"settings":{"refreshInterval":10000},"ticker":null},"status":"success","error":{}}`, s.w.Body.String())
s.store.AssertNotCalled(s.T(), "FindTickerByDomain", mock.AnythingOfType("string"), mock.Anything)
s.store.AssertNotCalled(s.T(), "FindTickerByDomain", "", mock.Anything)
s.store.AssertExpectations(s.T())
})

Expand Down
2 changes: 0 additions & 2 deletions internal/api/response/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
type InitTicker struct {
ID int `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Domain string `json:"domain"`
Title string `json:"title"`
Description string `json:"description"`
Information InitTickerInformation `json:"information"`
Expand All @@ -30,7 +29,6 @@ func InitTickerResponse(ticker storage.Ticker) InitTicker {
return InitTicker{
ID: ticker.ID,
CreatedAt: ticker.CreatedAt,
Domain: ticker.Domain,
Title: ticker.Title,
Description: ticker.Description,
Information: InitTickerInformation{
Expand Down
1 change: 0 additions & 1 deletion internal/api/response/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ func (s *InitTickerResponseTestSuite) TestInitTickerResponse() {

s.Equal(ticker.ID, response.ID)
s.Equal(ticker.CreatedAt, response.CreatedAt)
s.Equal(ticker.Domain, response.Domain)
s.Equal(ticker.Title, response.Title)
s.Equal(ticker.Description, response.Description)
s.Equal(ticker.Information.Author, response.Information.Author)
Expand Down
19 changes: 17 additions & 2 deletions internal/api/response/ticker.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import (
type Ticker struct {
ID int `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Domain string `json:"domain"`
Title string `json:"title"`
Description string `json:"description"`
Active bool `json:"active"`
Information Information `json:"information"`
Websites []Website `json:"websites"`
Telegram Telegram `json:"telegram"`
Mastodon Mastodon `json:"mastodon"`
Bluesky Bluesky `json:"bluesky"`
Expand All @@ -33,6 +33,12 @@ type Information struct {
Bluesky string `json:"bluesky"`
}

type Website struct {
ID int `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Domain string `json:"domain"`
}

type Telegram struct {
Active bool `json:"active"`
Connected bool `json:"connected"`
Expand Down Expand Up @@ -69,10 +75,18 @@ type Location struct {
}

func TickerResponse(t storage.Ticker, config config.Config) Ticker {
websites := make([]Website, 0)
for _, website := range t.Websites {
websites = append(websites, Website{
ID: website.ID,
CreatedAt: website.CreatedAt,
Domain: website.Domain,
})
}

return Ticker{
ID: t.ID,
CreatedAt: t.CreatedAt,
Domain: t.Domain,
Title: t.Title,
Description: t.Description,
Active: t.Active,
Expand All @@ -86,6 +100,7 @@ func TickerResponse(t storage.Ticker, config config.Config) Ticker {
Mastodon: t.Information.Mastodon,
Bluesky: t.Information.Bluesky,
},
Websites: websites,
Telegram: Telegram{
Active: t.Telegram.Active,
Connected: t.Telegram.Connected(),
Expand Down
8 changes: 7 additions & 1 deletion internal/api/response/ticker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ func (s *TickersResponseTestSuite) TestTickersResponse() {
Mastodon: "https://systemli.social/@example",
Bluesky: "https://example.com",
},
Websites: []storage.TickerWebsite{
{
Domain: "example.com",
},
},
Telegram: storage.TickerTelegram{
Active: true,
ChannelName: "example",
Expand Down Expand Up @@ -69,7 +74,6 @@ func (s *TickersResponseTestSuite) TestTickersResponse() {
s.Equal(1, len(tickerResponse))
s.Equal(ticker.ID, tickerResponse[0].ID)
s.Equal(ticker.CreatedAt, tickerResponse[0].CreatedAt)
s.Equal(ticker.Domain, tickerResponse[0].Domain)
s.Equal(ticker.Title, tickerResponse[0].Title)
s.Equal(ticker.Description, tickerResponse[0].Description)
s.Equal(ticker.Active, tickerResponse[0].Active)
Expand All @@ -81,6 +85,8 @@ func (s *TickersResponseTestSuite) TestTickersResponse() {
s.Equal(ticker.Information.Telegram, tickerResponse[0].Information.Telegram)
s.Equal(ticker.Information.Mastodon, tickerResponse[0].Information.Mastodon)
s.Equal(ticker.Information.Bluesky, tickerResponse[0].Information.Bluesky)
s.Equal(1, len(ticker.Websites))
s.Equal(ticker.Websites[0].Domain, tickerResponse[0].Websites[0].Domain)
s.Equal(ticker.Telegram.Active, tickerResponse[0].Telegram.Active)
s.Equal(ticker.Telegram.Connected(), tickerResponse[0].Telegram.Connected)
s.Equal(config.Telegram.User.UserName, tickerResponse[0].Telegram.BotUsername)
Expand Down
58 changes: 51 additions & 7 deletions internal/api/tickers.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,53 @@ func (h *handler) PutTickerUsers(c *gin.Context) {
c.JSON(http.StatusOK, response.SuccessResponse(map[string]interface{}{"users": response.UsersResponse(ticker.Users)}))
}

func (h *handler) PostTickerWebsite(c *gin.Context) {
ticker, err := helper.Ticker(c)
if err != nil {
c.JSON(http.StatusNotFound, response.ErrorResponse(response.CodeDefault, response.TickerNotFound))
return
}

var body struct {
Domain string `json:"domain" binding:"required"`
}

err = c.Bind(&body)
if err != nil {
c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.FormError))
return
}

err = h.storage.SaveTickerWebsite(&ticker, body.Domain)
if err != nil {
c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.StorageError))
return
}

h.ClearTickerCache(&ticker)

c.JSON(http.StatusOK, response.SuccessResponse(map[string]interface{}{"ticker": response.TickerResponse(ticker, h.config)}))
}

func (h *handler) DeleteTickerWebsite(c *gin.Context) {
ticker, err := helper.Ticker(c)
if err != nil {
c.JSON(http.StatusNotFound, response.ErrorResponse(response.CodeDefault, response.TickerNotFound))
return
}

domain := c.Param("domain")
err = h.storage.DeleteTickerWebsite(&ticker, domain)
if err != nil {
c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.StorageError))
return
}

h.ClearTickerCache(&ticker)

c.JSON(http.StatusOK, response.SuccessResponse(map[string]interface{}{"ticker": response.TickerResponse(ticker, h.config)}))
}

func (h *handler) PutTickerTelegram(c *gin.Context) {
ticker, err := helper.Ticker(c)
if err != nil {
Expand Down Expand Up @@ -450,16 +497,17 @@ func (h *handler) ResetTicker(c *gin.Context) {
// ClearTickerCache clears the cache for the init endpoint of a ticker
func (h *handler) ClearTickerCache(ticker *storage.Ticker) {
h.cache.Range(func(key, value interface{}) bool {
if strings.HasPrefix(key.(string), fmt.Sprintf("response:%s:/v1/init", ticker.Domain)) {
h.cache.Delete(key)
for _, website := range ticker.Websites {
if strings.HasPrefix(key.(string), fmt.Sprintf("response:%s:/v1/init", website.Domain)) {
h.cache.Delete(key)
}
}
return true
})
}

func updateTicker(t *storage.Ticker, c *gin.Context) error {
var body struct {
Domain string `json:"domain" binding:"required"`
Title string `json:"title" binding:"required"`
Description string `json:"description"`
Active bool `json:"active"`
Expand All @@ -484,10 +532,6 @@ func updateTicker(t *storage.Ticker, c *gin.Context) error {
return err
}

me, _ := helper.Me(c)
if me.IsSuperAdmin {
t.Domain = body.Domain
}
t.Title = body.Title
t.Description = body.Description
t.Active = body.Active
Expand Down
126 changes: 102 additions & 24 deletions internal/api/tickers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func (s *TickerTestSuite) TestPutTicker() {

s.Run("when storage returns error", func() {
s.ctx.Set("ticker", storage.Ticker{})
body := `{"domain":"localhost","title":"title","description":"description"}`
body := `{"title":"title","description":"description"}`
s.ctx.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1", strings.NewReader(body))
s.ctx.Request.Header.Add("Content-Type", "application/json")
s.store.On("SaveTicker", mock.Anything).Return(errors.New("storage error")).Once()
Expand All @@ -188,28 +188,11 @@ func (s *TickerTestSuite) TestPutTicker() {
s.store.AssertExpectations(s.T())
})

s.Run("user tries to update the domain", func() {
s.ctx.Set("ticker", storage.Ticker{Domain: "localhost"})
s.cache.Set("response:localhost:/v1/init", true, time.Minute)
s.ctx.Set("me", storage.User{IsSuperAdmin: false})
body := `{"domain":"new_domain","title":"title","description":"description"}`
s.ctx.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1", strings.NewReader(body))
s.ctx.Request.Header.Add("Content-Type", "application/json")
ticker := &storage.Ticker{Domain: "localhost", Title: "title", Description: "description"}
s.store.On("SaveTicker", ticker).Return(nil).Once()
h := s.handler()
h.PutTicker(s.ctx)

s.Equal(http.StatusOK, s.w.Code)
s.Nil(s.cache.Get("response:localhost:/v1/init"))
s.store.AssertExpectations(s.T())
})

s.Run("happy path", func() {
s.ctx.Set("ticker", storage.Ticker{})
s.ctx.Set("ticker", storage.Ticker{Websites: []storage.TickerWebsite{{Domain: "localhost"}}})
s.ctx.Set("me", storage.User{IsSuperAdmin: true})
s.cache.Set("response:localhost:/v1/init", true, time.Minute)
body := `{"domain":"localhost","title":"title","description":"description"}`
body := `{"title":"title","description":"description"}`
s.ctx.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1", strings.NewReader(body))
s.ctx.Request.Header.Add("Content-Type", "application/json")
s.store.On("SaveTicker", mock.Anything).Return(nil).Once()
Expand Down Expand Up @@ -269,6 +252,97 @@ func (s *TickerTestSuite) TestPutTickerUsers() {
})
}

func (s *TickerTestSuite) TestPostTickerWebsite() {
s.Run("when ticker not found", func() {
h := s.handler()
h.PostTickerWebsite(s.ctx)

s.Equal(http.StatusNotFound, s.w.Code)
s.store.AssertExpectations(s.T())
})

s.Run("when body is invalid", func() {
s.ctx.Set("ticker", storage.Ticker{})
s.ctx.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/tickers/1/websites", nil)
s.ctx.Request.Header.Add("Content-Type", "application/json")
h := s.handler()
h.PostTickerWebsite(s.ctx)

s.Equal(http.StatusBadRequest, s.w.Code)
s.store.AssertExpectations(s.T())
})

s.Run("when storage returns error", func() {
s.ctx.Set("ticker", storage.Ticker{})
body := `{"domain":"example.org"}`
s.ctx.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/tickers/1/websites", strings.NewReader(body))
s.ctx.Request.Header.Add("Content-Type", "application/json")
s.store.On("SaveTickerWebsite", mock.Anything, "example.org").Return(errors.New("storage error")).Once()

h := s.handler()
h.PostTickerWebsite(s.ctx)

s.Equal(http.StatusBadRequest, s.w.Code)
s.store.AssertExpectations(s.T())
})

s.Run("when storage returns ticker", func() {
s.cache.Set("response:example.org:/v1/init", true, time.Minute)
s.ctx.Set("ticker", storage.Ticker{Websites: []storage.TickerWebsite{{Domain: "example.org"}}})
body := `{"domain":"example.org"}`
s.ctx.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/tickers/1/websites", strings.NewReader(body))
s.ctx.Request.Header.Add("Content-Type", "application/json")
s.store.On("SaveTickerWebsite", mock.Anything, "example.org").Return(nil).Once()

h := s.handler()
h.PostTickerWebsite(s.ctx)

s.Equal(http.StatusOK, s.w.Code)
s.store.AssertExpectations(s.T())
s.Nil(s.cache.Get("response:example.org:/v1/init"))
})
}

func (s *TickerTestSuite) TestDeleteTickerWebsite() {
s.Run("when ticker not found", func() {
h := s.handler()
h.DeleteTickerWebsite(s.ctx)

s.Equal(http.StatusNotFound, s.w.Code)
s.store.AssertExpectations(s.T())
})

s.Run("when storage returns error", func() {
s.ctx.Set("ticker", storage.Ticker{})
s.ctx.Params = gin.Params{{Key: "domain", Value: "example.org"}}
s.ctx.Request = httptest.NewRequest(http.MethodDelete, "/v1/admin/tickers/1/websites/example.org", nil)
s.ctx.Request.Header.Add("Content-Type", "application/json")
s.store.On("DeleteTickerWebsite", mock.Anything, "example.org").Return(errors.New("storage error")).Once()

h := s.handler()
h.DeleteTickerWebsite(s.ctx)

s.Equal(http.StatusBadRequest, s.w.Code)
s.store.AssertExpectations(s.T())
})

s.Run("when storage returns ticker", func() {
s.cache.Set("response:example.org:/v1/init", true, time.Minute)
s.ctx.Set("ticker", storage.Ticker{Websites: []storage.TickerWebsite{{Domain: "example.org"}}})
s.ctx.Params = gin.Params{{Key: "domain", Value: "example.org"}}
s.ctx.Request = httptest.NewRequest(http.MethodDelete, "/v1/admin/tickers/1/websites/example.org", nil)
s.ctx.Request.Header.Add("Content-Type", "application/json")
s.store.On("DeleteTickerWebsite", mock.Anything, "example.org").Return(nil).Once()

h := s.handler()
h.DeleteTickerWebsite(s.ctx)

s.Equal(http.StatusOK, s.w.Code)
s.store.AssertExpectations(s.T())
s.Nil(s.cache.Get("response:example.org:/v1/init"))
})
}

func (s *TickerTestSuite) TestPutTickerTelegram() {
s.Run("when ticker not found", func() {
h := s.handler()
Expand Down Expand Up @@ -1166,13 +1240,15 @@ func (s *TickerTestSuite) TestDeleteTicker() {

s.Run("happy path", func() {
s.cache.Set("response:localhost:/v1/init", true, time.Minute)
s.ctx.Set("ticker", storage.Ticker{Domain: "localhost"})
s.ctx.Set("ticker", storage.Ticker{Websites: []storage.TickerWebsite{{Domain: "localhost"}}})
s.store.On("DeleteTicker", mock.Anything).Return(nil)
h := s.handler()
h.DeleteTicker(s.ctx)

s.Equal(http.StatusOK, s.w.Code)
s.Nil(s.cache.Get("response:localhost:/v1/init"))
item, found := s.cache.Get("response:localhost:/v1/init")
s.Nil(item)
s.False(found)
s.store.AssertExpectations(s.T())
})
}
Expand Down Expand Up @@ -1252,13 +1328,15 @@ func (s *TickerTestSuite) TestResetTicker() {

s.Run("happy path", func() {
s.cache.Set("response:localhost:/v1/init", true, time.Minute)
s.ctx.Set("ticker", storage.Ticker{Domain: "localhost"})
s.ctx.Set("ticker", storage.Ticker{Websites: []storage.TickerWebsite{{Domain: "localhost"}}})
s.store.On("ResetTicker", mock.Anything).Return(nil).Once()
h := s.handler()
h.ResetTicker(s.ctx)

s.Equal(http.StatusOK, s.w.Code)
s.Nil(s.cache.Get("response:localhost:/v1/init"))
item, found := s.cache.Get("response:localhost:/v1/init")
s.Nil(item)
s.False(found)
s.store.AssertExpectations(s.T())
})
}
Expand Down
Loading
Loading