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

Add API endpoint for accessing repo topics #7963

Merged
merged 35 commits into from
Sep 3, 2019
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ded028c
Create API endpoints for repo topics.
davidsvantesson Aug 24, 2019
6965aa8
Generate swagger
davidsvantesson Aug 24, 2019
5a87bab
Add documentation to functions
davidsvantesson Aug 24, 2019
0839b96
Grammar fix
davidsvantesson Aug 24, 2019
244cbd3
Fix function comment
davidsvantesson Aug 24, 2019
7c721f9
Merge branch 'master' into api-repo-topics
davidsvantesson Aug 24, 2019
f0f49bd
Can't use FindTopics when looking for a single repo topic, as it does…
davidsvantesson Aug 24, 2019
6ec5a0c
Add PUT ​/repos​/{owner}​/{repo}​/topics and remove GET ​/repos​/{own…
davidsvantesson Aug 25, 2019
8daf641
Ignore if topic is sent twice in same request, refactoring.
davidsvantesson Aug 25, 2019
9fdab25
Fix topic dropdown with api changes.
davidsvantesson Aug 25, 2019
1cb206c
Style fix
davidsvantesson Aug 25, 2019
c13a297
Update API documentation
davidsvantesson Aug 25, 2019
4a53681
Better way to handle duplicate topics in slice
davidsvantesson Aug 26, 2019
3705219
Make response element TopicName an array of strings, instead of using…
davidsvantesson Aug 26, 2019
ac93677
Add test cases for API Repo Topics.
davidsvantesson Aug 26, 2019
8bc836c
Fix format of tests
davidsvantesson Aug 26, 2019
3af43e2
Fix comments
davidsvantesson Aug 26, 2019
0e671f0
Merge branch 'master' into api-repo-topics
davidsvantesson Aug 26, 2019
5cdbbbc
Fix unit tests after adding some more topics to the test fixture.
davidsvantesson Aug 26, 2019
db3f6f3
Update models/topic.go
davidsvantesson Aug 27, 2019
2ef6904
Engine as first parameter in function
davidsvantesson Aug 27, 2019
99eb479
Replace magic numbers with http status code constants.
davidsvantesson Aug 27, 2019
ed7604a
Fix variable scope
davidsvantesson Aug 27, 2019
1088eac
Test one read with login and one with token
davidsvantesson Aug 27, 2019
098af67
Add some more tests
davidsvantesson Aug 27, 2019
41b6276
Apply suggestions from code review
davidsvantesson Aug 27, 2019
b654ebf
Add test case to check access for user with write access
davidsvantesson Aug 28, 2019
48989c3
Fix access, repo admin required to change topics
davidsvantesson Aug 28, 2019
db48d14
Correct first test to be without token
davidsvantesson Aug 28, 2019
fa8aa5d
Any repo reader should be able to access topics.
davidsvantesson Aug 29, 2019
1a6a8fc
Merge branch 'master' into api-repo-topics
davidsvantesson Aug 29, 2019
9d9f8dc
No need for string pointer
davidsvantesson Aug 29, 2019
6d9152f
Merge branch 'master' into api-repo-topics
davidsvantesson Aug 29, 2019
189d900
Merge branch 'master' into api-repo-topics
lafriks Aug 30, 2019
0b8409a
Merge branch 'master' into api-repo-topics
sapk Sep 3, 2019
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
124 changes: 124 additions & 0 deletions integrations/api_repo_topic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package integrations

import (
"fmt"
"net/http"
"testing"

"code.gitea.io/gitea/models"
api "code.gitea.io/gitea/modules/structs"

"github.com/stretchr/testify/assert"
)

func TestAPIRepoTopic(t *testing.T) {
prepareTestEnv(t)
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of repo2
user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of repo3
user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // write access to repo 3
repo2 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 2}).(*models.Repository)
repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository)

// Get user2's token
session := loginUser(t, user2.Name)
token2 := getTokenForLoggedInUser(t, session)

// Test read topics using login
url := fmt.Sprintf("/api/v1/repos/%s/%s/topics", user2.Name, repo2.Name)
req := NewRequest(t, "GET", url)
res := session.MakeRequest(t, req, http.StatusOK)
var topics *api.TopicName
DecodeJSON(t, res, &topics)
assert.ElementsMatch(t, []string{"topicname1", "topicname2"}, topics.TopicNames)

// Log out user2
session = emptyTestSession(t)
url = fmt.Sprintf("/api/v1/repos/%s/%s/topics?token=%s", user2.Name, repo2.Name, token2)

// Test delete a topic
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/topics/%s?token=%s", user2.Name, repo2.Name, "Topicname1", token2)
res = session.MakeRequest(t, req, http.StatusNoContent)

// Test add an existing topic
req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s?token=%s", user2.Name, repo2.Name, "Golang", token2)
res = session.MakeRequest(t, req, http.StatusNoContent)

// Test add a topic
req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s?token=%s", user2.Name, repo2.Name, "topicName3", token2)
res = session.MakeRequest(t, req, http.StatusNoContent)

// Test read topics using token
req = NewRequest(t, "GET", url)
res = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, res, &topics)
assert.ElementsMatch(t, []string{"topicname2", "golang", "topicname3"}, topics.TopicNames)

// Test replace topics
newTopics := []string{" windows ", " ", "MAC "}
req = NewRequestWithJSON(t, "PUT", url, &api.RepoTopicOptions{
Topics: newTopics,
})
res = session.MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "GET", url)
res = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, res, &topics)
assert.ElementsMatch(t, []string{"windows", "mac"}, topics.TopicNames)

// Test replace topics with something invalid
newTopics = []string{"topicname1", "topicname2", "topicname!"}
req = NewRequestWithJSON(t, "PUT", url, &api.RepoTopicOptions{
Topics: newTopics,
})
res = session.MakeRequest(t, req, http.StatusUnprocessableEntity)
req = NewRequest(t, "GET", url)
res = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, res, &topics)
assert.ElementsMatch(t, []string{"windows", "mac"}, topics.TopicNames)

// Test with some topics multiple times, less than 25 unique
newTopics = []string{"t1", "t2", "t1", "t3", "t4", "t5", "t6", "t7", "t8", "t9", "t10", "t11", "t12", "t13", "t14", "t15", "t16", "17", "t18", "t19", "t20", "t21", "t22", "t23", "t24", "t25"}
req = NewRequestWithJSON(t, "PUT", url, &api.RepoTopicOptions{
Topics: newTopics,
})
res = session.MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "GET", url)
res = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, res, &topics)
assert.Equal(t, 25, len(topics.TopicNames))

// Test writing more topics than allowed
newTopics = append(newTopics, "t26")
req = NewRequestWithJSON(t, "PUT", url, &api.RepoTopicOptions{
Topics: newTopics,
})
res = session.MakeRequest(t, req, http.StatusUnprocessableEntity)

// Test add a topic when there is already maximum
req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s?token=%s", user2.Name, repo2.Name, "t26", token2)
res = session.MakeRequest(t, req, http.StatusUnprocessableEntity)

// Test delete a topic that repo doesn't have
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/topics/%s?token=%s", user2.Name, repo2.Name, "Topicname1", token2)
res = session.MakeRequest(t, req, http.StatusNotFound)

// Get user4's token
session = loginUser(t, user4.Name)
token4 := getTokenForLoggedInUser(t, session)
session = emptyTestSession(t)

// Test read topics with write access
url = fmt.Sprintf("/api/v1/repos/%s/%s/topics?token=%s", user3.Name, repo3.Name, token4)
req = NewRequest(t, "GET", url)
res = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, res, &topics)
assert.Equal(t, 0, len(topics.TopicNames))

// Test add a topic to repo with write access (requires repo admin access)
req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s?token=%s", user3.Name, repo3.Name, "topicName", token4)
res = session.MakeRequest(t, req, http.StatusForbidden)

}
8 changes: 8 additions & 0 deletions models/fixtures/repo_topic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@
-
repo_id: 33
topic_id: 4

-
repo_id: 2
topic_id: 5

-
repo_id: 2
topic_id: 6
8 changes: 8 additions & 0 deletions models/fixtures/topic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@
- id: 4
name: graphql
repo_count: 1

- id: 5
name: topicname1
repo_count: 1

- id: 6
name: topicname2
repo_count: 2
154 changes: 124 additions & 30 deletions models/topic.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,38 @@ func (err ErrTopicNotExist) Error() string {
return fmt.Sprintf("topic is not exist [name: %s]", err.Name)
}

// ValidateTopic checks topics by length and match pattern rules
// ValidateTopic checks a topic by length and match pattern rules
func ValidateTopic(topic string) bool {
return len(topic) <= 35 && topicPattern.MatchString(topic)
}

// SanitizeAndValidateTopics sanitizes and checks an array or topics
func SanitizeAndValidateTopics(topics []string) (validTopics []string, invalidTopics []string) {
validTopics = make([]string, 0)
mValidTopics := make(map[string]struct{})
invalidTopics = make([]string, 0)

for _, topic := range topics {
topic = strings.TrimSpace(strings.ToLower(topic))
// ignore empty string
if len(topic) == 0 {
continue
}
// ignore same topic twice
if _, ok := mValidTopics[topic]; ok {
continue
}
if ValidateTopic(topic) {
validTopics = append(validTopics, topic)
mValidTopics[topic] = struct{}{}
} else {
invalidTopics = append(invalidTopics, topic)
}
}

return validTopics, invalidTopics
}

// GetTopicByName retrieves topic by name
func GetTopicByName(name string) (*Topic, error) {
var topic Topic
Expand All @@ -70,6 +97,54 @@ func GetTopicByName(name string) (*Topic, error) {
return &topic, nil
}

// addTopicByNameToRepo adds a topic name to a repo and increments the topic count.
// Returns topic after the addition
func addTopicByNameToRepo(e Engine, repoID int64, topicName string) (*Topic, error) {
var topic Topic
has, err := e.Where("name = ?", topicName).Get(&topic)
if err != nil {
return nil, err
}
if !has {
topic.Name = topicName
topic.RepoCount = 1
if _, err := e.Insert(&topic); err != nil {
return nil, err
}
} else {
topic.RepoCount++
if _, err := e.ID(topic.ID).Cols("repo_count").Update(&topic); err != nil {
return nil, err
}
}

if _, err := e.Insert(&RepoTopic{
RepoID: repoID,
TopicID: topic.ID,
}); err != nil {
return nil, err
}

return &topic, nil
}

// removeTopicFromRepo remove a topic from a repo and decrements the topic repo count
func removeTopicFromRepo(repoID int64, topic *Topic, e Engine) error {
topic.RepoCount--
if _, err := e.ID(topic.ID).Cols("repo_count").Update(topic); err != nil {
return err
}

if _, err := e.Delete(&RepoTopic{
RepoID: repoID,
TopicID: topic.ID,
}); err != nil {
return err
}

return nil
}

// FindTopicOptions represents the options when fdin topics
type FindTopicOptions struct {
RepoID int64
Expand Down Expand Up @@ -103,6 +178,50 @@ func FindTopics(opts *FindTopicOptions) (topics []*Topic, err error) {
return topics, sess.Desc("topic.repo_count").Find(&topics)
}

// GetRepoTopicByName retrives topic from name for a repo if it exist
func GetRepoTopicByName(repoID int64, topicName string) (*Topic, error) {
var cond = builder.NewCond()
var topic Topic
cond = cond.And(builder.Eq{"repo_topic.repo_id": repoID}).And(builder.Eq{"topic.name": topicName})
sess := x.Table("topic").Where(cond)
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
has, err := sess.Get(&topic)
if has {
return &topic, err
}
return nil, err
}

// AddTopic adds a topic name to a repository (if it does not already have it)
func AddTopic(repoID int64, topicName string) (*Topic, error) {
topic, err := GetRepoTopicByName(repoID, topicName)
if err != nil {
return nil, err
}
if topic != nil {
// Repo already have topic
return topic, nil
}

return addTopicByNameToRepo(x, repoID, topicName)
}

// DeleteTopic removes a topic name from a repository (if it has it)
func DeleteTopic(repoID int64, topicName string) (*Topic, error) {
topic, err := GetRepoTopicByName(repoID, topicName)
if err != nil {
return nil, err
}
if topic == nil {
// Repo doesn't have topic, can't be removed
return nil, nil
}

err = removeTopicFromRepo(repoID, topic, x)

return topic, err
}

// SaveTopics save topics to a repository
func SaveTopics(repoID int64, topicNames ...string) error {
topics, err := FindTopics(&FindTopicOptions{
Expand Down Expand Up @@ -152,40 +271,15 @@ func SaveTopics(repoID int64, topicNames ...string) error {
}

for _, topicName := range addedTopicNames {
var topic Topic
if has, err := sess.Where("name = ?", topicName).Get(&topic); err != nil {
return err
} else if !has {
topic.Name = topicName
topic.RepoCount = 1
if _, err := sess.Insert(&topic); err != nil {
return err
}
} else {
topic.RepoCount++
if _, err := sess.ID(topic.ID).Cols("repo_count").Update(&topic); err != nil {
return err
}
}

if _, err := sess.Insert(&RepoTopic{
RepoID: repoID,
TopicID: topic.ID,
}); err != nil {
_, err := addTopicByNameToRepo(sess, repoID, topicName)
if err != nil {
return err
}
}

for _, topic := range removeTopics {
topic.RepoCount--
if _, err := sess.ID(topic.ID).Cols("repo_count").Update(topic); err != nil {
return err
}

if _, err := sess.Delete(&RepoTopic{
RepoID: repoID,
TopicID: topic.ID,
}); err != nil {
err := removeTopicFromRepo(repoID, topic, sess)
if err != nil {
return err
}
}
Expand Down
19 changes: 13 additions & 6 deletions models/topic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ import (
)

func TestAddTopic(t *testing.T) {
totalNrOfTopics := 6
repo1NrOfTopics := 3
repo2NrOfTopics := 2

assert.NoError(t, PrepareTestDatabase())

topics, err := FindTopics(&FindTopicOptions{})
assert.NoError(t, err)
assert.EqualValues(t, 4, len(topics))
assert.EqualValues(t, totalNrOfTopics, len(topics))

topics, err = FindTopics(&FindTopicOptions{
Limit: 2,
Expand All @@ -27,33 +31,36 @@ func TestAddTopic(t *testing.T) {
RepoID: 1,
})
assert.NoError(t, err)
assert.EqualValues(t, 3, len(topics))
assert.EqualValues(t, repo1NrOfTopics, len(topics))

assert.NoError(t, SaveTopics(2, "golang"))
repo2NrOfTopics = 1
topics, err = FindTopics(&FindTopicOptions{})
assert.NoError(t, err)
assert.EqualValues(t, 4, len(topics))
assert.EqualValues(t, totalNrOfTopics, len(topics))

topics, err = FindTopics(&FindTopicOptions{
RepoID: 2,
})
assert.NoError(t, err)
assert.EqualValues(t, 1, len(topics))
assert.EqualValues(t, repo2NrOfTopics, len(topics))

assert.NoError(t, SaveTopics(2, "golang", "gitea"))
repo2NrOfTopics = 2
totalNrOfTopics++
topic, err := GetTopicByName("gitea")
assert.NoError(t, err)
assert.EqualValues(t, 1, topic.RepoCount)

topics, err = FindTopics(&FindTopicOptions{})
assert.NoError(t, err)
assert.EqualValues(t, 5, len(topics))
assert.EqualValues(t, totalNrOfTopics, len(topics))

topics, err = FindTopics(&FindTopicOptions{
RepoID: 2,
})
assert.NoError(t, err)
assert.EqualValues(t, 2, len(topics))
assert.EqualValues(t, repo2NrOfTopics, len(topics))
}

func TestTopicValidator(t *testing.T) {
Expand Down
Loading