Repository: Ptt-official-app/Ptt-backend
Pull request #80 (2021-02-18 14:27:15)

實作 #53 Get popular board list
<!-- 原則上不接受沒有 Issue ID 的 PR。 -->
<!-- We don't accept PRs which has no Issue ID. -->

## 📝 相關的 issue / Related Issues
- 主 issue #51
- 實作 issue #53

## ⛏ 變更內容 / Details of Changes
<!-- 簡要陳列您的修改 -->
<!-- List down your changes concisely -->
- 實作 delivery/http/route_boards.go 中的 getPopularBoardList
- 實作 usecase/board.go 中的 GetPopularBoards
- 增加 delivery/http/route_boards_test.go 中的 TestGetPopularBoardList
- TODO: 依 BoardRecord 中的 number_of_user 遞減排序

因為之前再寫的時候 ptt-backend 是蠻舊版本的，所以就重新 merge、增加了簡單的測試並重發 pr。


Patch:
diff --git a/internal/delivery/http/route.go b/internal/delivery/http/route.go
index 166effa..0e44425 100644
--- a/internal/delivery/http/route.go
+++ b/internal/delivery/http/route.go
@@ -9,6 +9,7 @@ func (delivery *httpDelivery) buildRoute(mux *http.ServeMux) {
 	mux.HandleFunc("/v1/token", delivery.routeToken)
 	mux.HandleFunc("/v1/boards", delivery.routeBoards)
 	mux.HandleFunc("/v1/boards/", delivery.routeBoards)
+	mux.HandleFunc("/v1/popular-boards", delivery.routePopularBoards)
 	mux.HandleFunc("/v1/classes/", delivery.routeClasses)
 	mux.HandleFunc("/v1/users/", delivery.routeUsers)
 }
@@ -38,6 +39,15 @@ func (delivery *httpDelivery) routeBoards(w http.ResponseWriter, r *http.Request
 	}
 }
 
+
+func (delivery *httpDelivery) routePopularBoards(w http.ResponseWriter, r *http.Request) {
+	// TODO: Check IP Flowspeed
+	switch r.Method {
+	case http.MethodGet:
+		delivery.getPopularBoardList(w, r)
+	}
+}
+
 // routeClasses is the handler for `/v1/classes`
 func (delivery *httpDelivery) routeClasses(w http.ResponseWriter, r *http.Request) {
 	// TODO: Check IP Flowspeed
diff --git a/internal/delivery/http/route_boards.go b/internal/delivery/http/route_boards.go
index 253084c..1515b49 100644
--- a/internal/delivery/http/route_boards.go
+++ b/internal/delivery/http/route_boards.go
@@ -41,6 +41,38 @@ func (delivery *httpDelivery) getBoardList(w http.ResponseWriter, r *http.Reques
 	w.Write(b)
 }
 
+func (delivery *httpDelivery) getPopularBoardList(w http.ResponseWriter, r *http.Request) {
+	delivery.logger.Debugf("getPopularBoardList: %v", r)
+
+	boards, err := delivery.usecase.GetPopularBoards(context.Background())
+	if err != nil {
+		// TODO: record error
+		delivery.logger.Errorf("find popular board failed: %v", err)
+		m := map[string]string{
+			"error":             "find_popular_board_error",
+			"error_description": "get popular board failed",
+		}
+		b, _ := json.MarshalIndent(m, "", "  ")
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write(b)
+		return
+	}
+
+	dataList := make([]interface{}, 0, len(boards))
+	for _, board := range boards {
+		dataList = append(dataList, marshalBoardHeader(board))
+	}
+
+	responseMap := map[string]interface{}{
+		"data": struct {
+			Items []interface{} `json:"items"`
+		}{ dataList },
+	}
+
+	b, _ := json.MarshalIndent(responseMap, "", "  ")
+	w.Write(b)
+}
+
 func (delivery *httpDelivery) getBoardInformation(w http.ResponseWriter, r *http.Request, boardId string) {
 	delivery.logger.Debugf("getBoardInformation: %v", r)
 	token := delivery.getTokenFromRequest(r)
diff --git a/internal/usecase/board.go b/internal/usecase/board.go
index 128550c..36397da 100644
--- a/internal/usecase/board.go
+++ b/internal/usecase/board.go
@@ -46,6 +46,19 @@ func (usecase *usecase) GetBoards(ctx context.Context, userID string) []bbs.Boar
 	return boards
 }
 
+func (usecase *usecase) GetPopularBoards(ctx context.Context) ([]bbs.BoardRecord, error) {
+	boards := usecase.repo.GetBoards(ctx)
+	// TODO: Sort boards in descending order by number_of_user
+
+	// sort.Slice(boards, func(i, j int) bool {
+	// 	return boards[i].UserNum > boards[j].UserNum
+	// })
+	if len(boards) < 100 {
+		return boards, nil
+	}
+	return boards[:100], nil
+}
+
 func (usecase *usecase) GetClasses(ctx context.Context, userID, classID string) []bbs.BoardRecord {
 	boards := make([]bbs.BoardRecord, 0)
 	for _, board := range usecase.repo.GetBoards(ctx) {
diff --git a/internal/usecase/usecase.go b/internal/usecase/usecase.go
index 0cd6e64..5250beb 100644
--- a/internal/usecase/usecase.go
+++ b/internal/usecase/usecase.go
@@ -24,6 +24,8 @@ type Usecase interface {
 	GetBoardByID(ctx context.Context, boardID string) (bbs.BoardRecord, error)
 	// GetBoards returns all board records
 	GetBoards(ctx context.Context, userID string) []bbs.BoardRecord
+	// GetPopularBoards returns top 100 popular board records
+	GetPopularBoards(ctx context.Context) ([]bbs.BoardRecord, error)
 	// GetClasses returns board records in a class
 	GetClasses(ctx context.Context, userID, classID string) []bbs.BoardRecord
 	// GetBoardArticles returns articles of a board


Test patch:
diff --git a/internal/delivery/http/route_boards_mock_test.go b/internal/delivery/http/route_boards_mock_test.go
index 592842a..43bf749 100644
--- a/internal/delivery/http/route_boards_mock_test.go
+++ b/internal/delivery/http/route_boards_mock_test.go
@@ -14,6 +14,13 @@ func (usecase *MockUsecase) GetBoards(ctx context.Context, userID string) []bbs.
 	panic("Not implemented")
 }
 
+func (usecase *MockUsecase) GetPopularBoards(ctx context.Context) ([]bbs.BoardRecord, error) {
+	result := make([]bbs.BoardRecord, 0)
+	result = append(result, NewMockBoardRecord("SYSOP", "", "嘰哩 ◎站長好!", true))
+	result = append(result, NewMockBoardRecord("junk", "TEST", "發電 ◎雜七雜八的垃圾", false))
+	return result, nil
+}
+
 func (usecase *MockUsecase) GetBoardArticles(ctx context.Context, boardID string, cond *usecase.ArticleSearchCond) []interface{} {
 	panic("Not implemented")
 }
@@ -25,3 +32,20 @@ func (usecase *MockUsecase) GetBoardArticle(ctx context.Context, boardID, filena
 func (usecase *MockUsecase) GetBoardTreasures(ctx context.Context, boardID string, treasuresID []string) []interface{} {
 	panic("Not implemented")
 }
+
+type MockBoardRecord struct {
+	boardID string
+	title   string
+	isClass bool
+	classID string
+}
+
+func NewMockBoardRecord(classID, boardID, title string, isClass bool) *MockBoardRecord {
+	return &MockBoardRecord{boardID: boardID, title: title, isClass: isClass, classID: classID}
+}
+
+func (b *MockBoardRecord) BoardId() string { return b.boardID }
+func (b *MockBoardRecord) Title() string { return b.title }
+func (b *MockBoardRecord) IsClass() bool { return b.isClass }
+func (b *MockBoardRecord) ClassId() string { return b.classID }
+func (b *MockBoardRecord) BM() []string { return make([]string, 0) }
diff --git a/internal/delivery/http/route_boards_test.go b/internal/delivery/http/route_boards_test.go
new file mode 100644
index 0000000..36c1b41
--- /dev/null
+++ b/internal/delivery/http/route_boards_test.go
@@ -0,0 +1,51 @@
+package http
+
+import (
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"strconv"
+)
+
+func TestGetPopularBoardList(t *testing.T) {
+
+	usecase := NewMockUsecase()
+	delivery := NewHTTPDelivery(usecase)
+	req, err := http.NewRequest("GET", "/v1/popular-boards", nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	rr := httptest.NewRecorder()
+	r := http.NewServeMux()
+	r.HandleFunc("/v1/popular-boards", delivery.routePopularBoards)
+	r.ServeHTTP(rr, req)
+
+	if status := rr.Code; status != http.StatusOK {
+		t.Errorf("handler returned wrong status code: got %v want %v",
+			status, http.StatusOK)
+	}
+
+	responseMap := map[string]interface{}{}
+	json.Unmarshal(rr.Body.Bytes(), &responseMap)
+	t.Logf("got response %v", rr.Body.String())
+	responseData := responseMap["data"]
+	popularBoards := responseData.(map[string]interface{})["items"].([]interface{})
+
+	var prevNum int
+	for i := range popularBoards {
+		curr := popularBoards[i].(map[string]interface{})["number_of_user"]
+		currNum, err := strconv.Atoi(curr.(string))
+		if err != nil {
+			t.Fatalf("handler returned unexpected body, invalid number_of_user: got %v",
+				currNum)
+		}
+		
+		if i > 0 && prevNum < currNum {
+			t.Fatalf("handler returned unexpected body, invalid order: got %v before %v",
+				prevNum, currNum)
+		}
+		prevNum = currNum
+	}
+}