diff --git a/db/channels.go b/db/channels.go index 999ba1c..00cd8f2 100644 --- a/db/channels.go +++ b/db/channels.go @@ -90,3 +90,19 @@ func (d *postgresDB) SubscribeUserToChannel(ctx context.Context, username string return nil } + +var unsubscribeUserFromChannelQuery = `DELETE FROM user_subscriptions WHERE username = $1 AND channel_id = $2` + +func (d *postgresDB) UnsubscribeUserFromChannel(ctx context.Context, username string, channelID string) error { + resp, err := d.db.ExecContext(ctx, unsubscribeUserFromChannelQuery, username, channelID) + if err != nil { + d.l.Log("level", "ERROR", "function", "db.SubscribeUserToChannel", "error", err) + return err + } + + if affected, err := resp.RowsAffected(); err != nil || affected != 1 { + return ErrChannelNotFound + } + + return nil +} diff --git a/db/db.go b/db/db.go index 41d8c14..c79f451 100644 --- a/db/db.go +++ b/db/db.go @@ -10,6 +10,7 @@ import ( var ( ErrChannelExists = errors.New("channel already exists") + ErrChannelNotFound = errors.New("no channel with that ID found") ErrAlreadySubscribed = errors.New("already subscribed to channel") ErrVideoExists = errors.New("video already exists") ErrUserExists = errors.New("user already exists") @@ -30,8 +31,10 @@ type DB interface { ListChannels(ctx context.Context) ([]models.Channel, error) // GetChannelSubscribers lists all channels from the database GetChannelSubscribers(ctx context.Context, channelID string) ([]string, error) - // SubscribeUserToChannel will start showing new videos for that channel to the user + // SubscribeUserToChannel will start adding new videos from that channel to the user SubscribeUserToChannel(ctx context.Context, username string, channelID string) error + // SubscribeUserToChannel will stop adding videos from that channel to the user + UnsubscribeUserFromChannel(ctx context.Context, username string, channelID string) error // GetNewVideos returns a list of unwatched videos from all subscribed channels GetNewVideos(ctx context.Context, username string) ([]models.Video, error) diff --git a/handler/channels.go b/handler/channels.go index eebcead..0b2b787 100644 --- a/handler/channels.go +++ b/handler/channels.go @@ -27,3 +27,7 @@ func (h *handler) SubscribeToChannel(ctx context.Context, username string, chann return h.db.SubscribeUserToChannel(ctx, username, channelID) } + +func (h *handler) UnsubscribeFromChannel(ctx context.Context, username string, channelID string) error { + return h.db.UnsubscribeUserFromChannel(ctx, username, channelID) +} diff --git a/handler/handler.go b/handler/handler.go index f8f82ae..8c95bf7 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -11,6 +11,7 @@ import ( type Handler interface { CreateUser(ctx context.Context, user models.User) error SubscribeToChannel(ctx context.Context, username string, channelID string) error + UnsubscribeFromChannel(ctx context.Context, username string, channelID string) error GetNewVideos(ctx context.Context, username string) ([]models.Video, error) GetWatchedVideos(ctx context.Context, username string) ([]models.Video, error) FetchVideos(ctx context.Context) error diff --git a/httpserver/ytrssil/channels.go b/httpserver/ytrssil/channels.go index 457677c..7864517 100644 --- a/httpserver/ytrssil/channels.go +++ b/httpserver/ytrssil/channels.go @@ -37,3 +37,26 @@ func (s *server) SubscribeToChannel(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"msg": "subscribed to channel successfully"}) } + +func (s *server) UnsubscribeFromChannel(c *gin.Context) { + var channel models.Channel + err := c.ShouldBindUri(&channel) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + username := c.GetString("username") + + err = s.handler.UnsubscribeFromChannel(c.Request.Context(), username, channel.ID) + if err != nil { + if errors.Is(err, db.ErrChannelNotFound) { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"msg": "unsubscribed from channel successfully"}) +} diff --git a/httpserver/ytrssil/server.go b/httpserver/ytrssil/server.go index 668a8c5..548d4e9 100644 --- a/httpserver/ytrssil/server.go +++ b/httpserver/ytrssil/server.go @@ -44,6 +44,7 @@ func SetupGinRouter(l log.Logger, handler handler.Handler, authMiddleware func(c api.Use(authMiddleware) { api.POST("channels/:channel_id/subscribe", srv.SubscribeToChannel) + api.POST("channels/:channel_id/unsubscribe", srv.UnsubscribeFromChannel) api.GET("videos/new", srv.GetNewVideos) api.GET("videos/watched", srv.GetWatchedVideos) api.POST("videos/:video_id/watch", srv.MarkVideoAsWatched) diff --git a/mocks/db/db.go b/mocks/db/db.go index 0bcbb02..4291a00 100644 --- a/mocks/db/db.go +++ b/mocks/db/db.go @@ -57,6 +57,9 @@ var _ db.DB = &DBMock{} // SubscribeUserToChannelFunc: func(ctx context.Context, username string, channelID string) error { // panic("mock out the SubscribeUserToChannel method") // }, +// UnsubscribeUserFromChannelFunc: func(ctx context.Context, username string, channelID string) error { +// panic("mock out the UnsubscribeUserFromChannel method") +// }, // } // // // use mockedDB in code that requires db.DB @@ -100,6 +103,9 @@ type DBMock struct { // SubscribeUserToChannelFunc mocks the SubscribeUserToChannel method. SubscribeUserToChannelFunc func(ctx context.Context, username string, channelID string) error + // UnsubscribeUserFromChannelFunc mocks the UnsubscribeUserFromChannel method. + UnsubscribeUserFromChannelFunc func(ctx context.Context, username string, channelID string) error + // calls tracks calls to the methods. calls struct { // AddVideo holds details about calls to the AddVideo method. @@ -194,19 +200,29 @@ type DBMock struct { // ChannelID is the channelID argument value. ChannelID string } + // UnsubscribeUserFromChannel holds details about calls to the UnsubscribeUserFromChannel method. + UnsubscribeUserFromChannel []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Username is the username argument value. + Username string + // ChannelID is the channelID argument value. + ChannelID string + } } - lockAddVideo sync.RWMutex - lockAddVideoToUser sync.RWMutex - lockAuthenticateUser sync.RWMutex - lockCreateChannel sync.RWMutex - lockCreateUser sync.RWMutex - lockDeleteUser sync.RWMutex - lockGetChannelSubscribers sync.RWMutex - lockGetNewVideos sync.RWMutex - lockGetWatchedVideos sync.RWMutex - lockListChannels sync.RWMutex - lockSetVideoWatchTime sync.RWMutex - lockSubscribeUserToChannel sync.RWMutex + lockAddVideo sync.RWMutex + lockAddVideoToUser sync.RWMutex + lockAuthenticateUser sync.RWMutex + lockCreateChannel sync.RWMutex + lockCreateUser sync.RWMutex + lockDeleteUser sync.RWMutex + lockGetChannelSubscribers sync.RWMutex + lockGetNewVideos sync.RWMutex + lockGetWatchedVideos sync.RWMutex + lockListChannels sync.RWMutex + lockSetVideoWatchTime sync.RWMutex + lockSubscribeUserToChannel sync.RWMutex + lockUnsubscribeUserFromChannel sync.RWMutex } // AddVideo calls AddVideoFunc. @@ -656,3 +672,43 @@ func (mock *DBMock) SubscribeUserToChannelCalls() []struct { mock.lockSubscribeUserToChannel.RUnlock() return calls } + +// UnsubscribeUserFromChannel calls UnsubscribeUserFromChannelFunc. +func (mock *DBMock) UnsubscribeUserFromChannel(ctx context.Context, username string, channelID string) error { + if mock.UnsubscribeUserFromChannelFunc == nil { + panic("DBMock.UnsubscribeUserFromChannelFunc: method is nil but DB.UnsubscribeUserFromChannel was just called") + } + callInfo := struct { + Ctx context.Context + Username string + ChannelID string + }{ + Ctx: ctx, + Username: username, + ChannelID: channelID, + } + mock.lockUnsubscribeUserFromChannel.Lock() + mock.calls.UnsubscribeUserFromChannel = append(mock.calls.UnsubscribeUserFromChannel, callInfo) + mock.lockUnsubscribeUserFromChannel.Unlock() + return mock.UnsubscribeUserFromChannelFunc(ctx, username, channelID) +} + +// UnsubscribeUserFromChannelCalls gets all the calls that were made to UnsubscribeUserFromChannel. +// Check the length with: +// +// len(mockedDB.UnsubscribeUserFromChannelCalls()) +func (mock *DBMock) UnsubscribeUserFromChannelCalls() []struct { + Ctx context.Context + Username string + ChannelID string +} { + var calls []struct { + Ctx context.Context + Username string + ChannelID string + } + mock.lockUnsubscribeUserFromChannel.RLock() + calls = mock.calls.UnsubscribeUserFromChannel + mock.lockUnsubscribeUserFromChannel.RUnlock() + return calls +}