diff --git a/_mocks/opencsg.com/csghub-server/builder/store/database/mock_TagStore.go b/_mocks/opencsg.com/csghub-server/builder/store/database/mock_TagStore.go index f2638d83..6a02617a 100644 --- a/_mocks/opencsg.com/csghub-server/builder/store/database/mock_TagStore.go +++ b/_mocks/opencsg.com/csghub-server/builder/store/database/mock_TagStore.go @@ -841,6 +841,53 @@ func (_c *MockTagStore_CreateTag_Call) RunAndReturn(run func(context.Context, st return _c } +// DeleteTagByID provides a mock function with given fields: ctx, id +func (_m *MockTagStore) DeleteTagByID(ctx context.Context, id int64) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DeleteTagByID") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockTagStore_DeleteTagByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteTagByID' +type MockTagStore_DeleteTagByID_Call struct { + *mock.Call +} + +// DeleteTagByID is a helper method to define mock.On call +// - ctx context.Context +// - id int64 +func (_e *MockTagStore_Expecter) DeleteTagByID(ctx interface{}, id interface{}) *MockTagStore_DeleteTagByID_Call { + return &MockTagStore_DeleteTagByID_Call{Call: _e.mock.On("DeleteTagByID", ctx, id)} +} + +func (_c *MockTagStore_DeleteTagByID_Call) Run(run func(ctx context.Context, id int64)) *MockTagStore_DeleteTagByID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *MockTagStore_DeleteTagByID_Call) Return(_a0 error) *MockTagStore_DeleteTagByID_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockTagStore_DeleteTagByID_Call) RunAndReturn(run func(context.Context, int64) error) *MockTagStore_DeleteTagByID_Call { + _c.Call.Return(run) + return _c +} + // FindOrCreate provides a mock function with given fields: ctx, tag func (_m *MockTagStore) FindOrCreate(ctx context.Context, tag database.Tag) (*database.Tag, error) { ret := _m.Called(ctx, tag) @@ -961,6 +1008,65 @@ func (_c *MockTagStore_FindTag_Call) RunAndReturn(run func(context.Context, stri return _c } +// FindTagByID provides a mock function with given fields: ctx, id +func (_m *MockTagStore) FindTagByID(ctx context.Context, id int64) (*database.Tag, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for FindTagByID") + } + + var r0 *database.Tag + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*database.Tag, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *database.Tag); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*database.Tag) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockTagStore_FindTagByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindTagByID' +type MockTagStore_FindTagByID_Call struct { + *mock.Call +} + +// FindTagByID is a helper method to define mock.On call +// - ctx context.Context +// - id int64 +func (_e *MockTagStore_Expecter) FindTagByID(ctx interface{}, id interface{}) *MockTagStore_FindTagByID_Call { + return &MockTagStore_FindTagByID_Call{Call: _e.mock.On("FindTagByID", ctx, id)} +} + +func (_c *MockTagStore_FindTagByID_Call) Run(run func(ctx context.Context, id int64)) *MockTagStore_FindTagByID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *MockTagStore_FindTagByID_Call) Return(_a0 *database.Tag, _a1 error) *MockTagStore_FindTagByID_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockTagStore_FindTagByID_Call) RunAndReturn(run func(context.Context, int64) (*database.Tag, error)) *MockTagStore_FindTagByID_Call { + _c.Call.Return(run) + return _c +} + // GetTagsByScopeAndCategories provides a mock function with given fields: ctx, scope, categories func (_m *MockTagStore) GetTagsByScopeAndCategories(ctx context.Context, scope database.TagScope, categories []string) ([]*database.Tag, error) { ret := _m.Called(ctx, scope, categories) @@ -1229,6 +1335,65 @@ func (_c *MockTagStore_SetMetaTags_Call) RunAndReturn(run func(context.Context, return _c } +// UpdateTagByID provides a mock function with given fields: ctx, tag +func (_m *MockTagStore) UpdateTagByID(ctx context.Context, tag *database.Tag) (*database.Tag, error) { + ret := _m.Called(ctx, tag) + + if len(ret) == 0 { + panic("no return value specified for UpdateTagByID") + } + + var r0 *database.Tag + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *database.Tag) (*database.Tag, error)); ok { + return rf(ctx, tag) + } + if rf, ok := ret.Get(0).(func(context.Context, *database.Tag) *database.Tag); ok { + r0 = rf(ctx, tag) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*database.Tag) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *database.Tag) error); ok { + r1 = rf(ctx, tag) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockTagStore_UpdateTagByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateTagByID' +type MockTagStore_UpdateTagByID_Call struct { + *mock.Call +} + +// UpdateTagByID is a helper method to define mock.On call +// - ctx context.Context +// - tag *database.Tag +func (_e *MockTagStore_Expecter) UpdateTagByID(ctx interface{}, tag interface{}) *MockTagStore_UpdateTagByID_Call { + return &MockTagStore_UpdateTagByID_Call{Call: _e.mock.On("UpdateTagByID", ctx, tag)} +} + +func (_c *MockTagStore_UpdateTagByID_Call) Run(run func(ctx context.Context, tag *database.Tag)) *MockTagStore_UpdateTagByID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*database.Tag)) + }) + return _c +} + +func (_c *MockTagStore_UpdateTagByID_Call) Return(_a0 *database.Tag, _a1 error) *MockTagStore_UpdateTagByID_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockTagStore_UpdateTagByID_Call) RunAndReturn(run func(context.Context, *database.Tag) (*database.Tag, error)) *MockTagStore_UpdateTagByID_Call { + _c.Call.Return(run) + return _c +} + // UpsertRepoTags provides a mock function with given fields: ctx, repoID, oldTagIDs, newTagIDs func (_m *MockTagStore) UpsertRepoTags(ctx context.Context, repoID int64, oldTagIDs []int64, newTagIDs []int64) error { ret := _m.Called(ctx, repoID, oldTagIDs, newTagIDs) diff --git a/_mocks/opencsg.com/csghub-server/component/mock_TagComponent.go b/_mocks/opencsg.com/csghub-server/component/mock_TagComponent.go index 05021f20..d6e7c76a 100644 --- a/_mocks/opencsg.com/csghub-server/component/mock_TagComponent.go +++ b/_mocks/opencsg.com/csghub-server/component/mock_TagComponent.go @@ -133,6 +133,174 @@ func (_c *MockTagComponent_ClearMetaTags_Call) RunAndReturn(run func(context.Con return _c } +// CreateTag provides a mock function with given fields: ctx, username, req +func (_m *MockTagComponent) CreateTag(ctx context.Context, username string, req types.CreateTag) (*database.Tag, error) { + ret := _m.Called(ctx, username, req) + + if len(ret) == 0 { + panic("no return value specified for CreateTag") + } + + var r0 *database.Tag + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, types.CreateTag) (*database.Tag, error)); ok { + return rf(ctx, username, req) + } + if rf, ok := ret.Get(0).(func(context.Context, string, types.CreateTag) *database.Tag); ok { + r0 = rf(ctx, username, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*database.Tag) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, types.CreateTag) error); ok { + r1 = rf(ctx, username, req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockTagComponent_CreateTag_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateTag' +type MockTagComponent_CreateTag_Call struct { + *mock.Call +} + +// CreateTag is a helper method to define mock.On call +// - ctx context.Context +// - username string +// - req types.CreateTag +func (_e *MockTagComponent_Expecter) CreateTag(ctx interface{}, username interface{}, req interface{}) *MockTagComponent_CreateTag_Call { + return &MockTagComponent_CreateTag_Call{Call: _e.mock.On("CreateTag", ctx, username, req)} +} + +func (_c *MockTagComponent_CreateTag_Call) Run(run func(ctx context.Context, username string, req types.CreateTag)) *MockTagComponent_CreateTag_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(types.CreateTag)) + }) + return _c +} + +func (_c *MockTagComponent_CreateTag_Call) Return(_a0 *database.Tag, _a1 error) *MockTagComponent_CreateTag_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockTagComponent_CreateTag_Call) RunAndReturn(run func(context.Context, string, types.CreateTag) (*database.Tag, error)) *MockTagComponent_CreateTag_Call { + _c.Call.Return(run) + return _c +} + +// DeleteTag provides a mock function with given fields: ctx, username, id +func (_m *MockTagComponent) DeleteTag(ctx context.Context, username string, id int64) error { + ret := _m.Called(ctx, username, id) + + if len(ret) == 0 { + panic("no return value specified for DeleteTag") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, int64) error); ok { + r0 = rf(ctx, username, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockTagComponent_DeleteTag_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteTag' +type MockTagComponent_DeleteTag_Call struct { + *mock.Call +} + +// DeleteTag is a helper method to define mock.On call +// - ctx context.Context +// - username string +// - id int64 +func (_e *MockTagComponent_Expecter) DeleteTag(ctx interface{}, username interface{}, id interface{}) *MockTagComponent_DeleteTag_Call { + return &MockTagComponent_DeleteTag_Call{Call: _e.mock.On("DeleteTag", ctx, username, id)} +} + +func (_c *MockTagComponent_DeleteTag_Call) Run(run func(ctx context.Context, username string, id int64)) *MockTagComponent_DeleteTag_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(int64)) + }) + return _c +} + +func (_c *MockTagComponent_DeleteTag_Call) Return(_a0 error) *MockTagComponent_DeleteTag_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockTagComponent_DeleteTag_Call) RunAndReturn(run func(context.Context, string, int64) error) *MockTagComponent_DeleteTag_Call { + _c.Call.Return(run) + return _c +} + +// GetTagByID provides a mock function with given fields: ctx, username, id +func (_m *MockTagComponent) GetTagByID(ctx context.Context, username string, id int64) (*database.Tag, error) { + ret := _m.Called(ctx, username, id) + + if len(ret) == 0 { + panic("no return value specified for GetTagByID") + } + + var r0 *database.Tag + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, int64) (*database.Tag, error)); ok { + return rf(ctx, username, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string, int64) *database.Tag); ok { + r0 = rf(ctx, username, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*database.Tag) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { + r1 = rf(ctx, username, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockTagComponent_GetTagByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTagByID' +type MockTagComponent_GetTagByID_Call struct { + *mock.Call +} + +// GetTagByID is a helper method to define mock.On call +// - ctx context.Context +// - username string +// - id int64 +func (_e *MockTagComponent_Expecter) GetTagByID(ctx interface{}, username interface{}, id interface{}) *MockTagComponent_GetTagByID_Call { + return &MockTagComponent_GetTagByID_Call{Call: _e.mock.On("GetTagByID", ctx, username, id)} +} + +func (_c *MockTagComponent_GetTagByID_Call) Run(run func(ctx context.Context, username string, id int64)) *MockTagComponent_GetTagByID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(int64)) + }) + return _c +} + +func (_c *MockTagComponent_GetTagByID_Call) Return(_a0 *database.Tag, _a1 error) *MockTagComponent_GetTagByID_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockTagComponent_GetTagByID_Call) RunAndReturn(run func(context.Context, string, int64) (*database.Tag, error)) *MockTagComponent_GetTagByID_Call { + _c.Call.Return(run) + return _c +} + // UpdateLibraryTags provides a mock function with given fields: ctx, tagScope, namespace, name, oldFilePath, newFilePath func (_m *MockTagComponent) UpdateLibraryTags(ctx context.Context, tagScope database.TagScope, namespace string, name string, oldFilePath string, newFilePath string) error { ret := _m.Called(ctx, tagScope, namespace, name, oldFilePath, newFilePath) @@ -296,6 +464,67 @@ func (_c *MockTagComponent_UpdateRepoTagsByCategory_Call) RunAndReturn(run func( return _c } +// UpdateTag provides a mock function with given fields: ctx, username, id, req +func (_m *MockTagComponent) UpdateTag(ctx context.Context, username string, id int64, req types.UpdateTag) (*database.Tag, error) { + ret := _m.Called(ctx, username, id, req) + + if len(ret) == 0 { + panic("no return value specified for UpdateTag") + } + + var r0 *database.Tag + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, int64, types.UpdateTag) (*database.Tag, error)); ok { + return rf(ctx, username, id, req) + } + if rf, ok := ret.Get(0).(func(context.Context, string, int64, types.UpdateTag) *database.Tag); ok { + r0 = rf(ctx, username, id, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*database.Tag) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, int64, types.UpdateTag) error); ok { + r1 = rf(ctx, username, id, req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockTagComponent_UpdateTag_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateTag' +type MockTagComponent_UpdateTag_Call struct { + *mock.Call +} + +// UpdateTag is a helper method to define mock.On call +// - ctx context.Context +// - username string +// - id int64 +// - req types.UpdateTag +func (_e *MockTagComponent_Expecter) UpdateTag(ctx interface{}, username interface{}, id interface{}, req interface{}) *MockTagComponent_UpdateTag_Call { + return &MockTagComponent_UpdateTag_Call{Call: _e.mock.On("UpdateTag", ctx, username, id, req)} +} + +func (_c *MockTagComponent_UpdateTag_Call) Run(run func(ctx context.Context, username string, id int64, req types.UpdateTag)) *MockTagComponent_UpdateTag_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(int64), args[3].(types.UpdateTag)) + }) + return _c +} + +func (_c *MockTagComponent_UpdateTag_Call) Return(_a0 *database.Tag, _a1 error) *MockTagComponent_UpdateTag_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockTagComponent_UpdateTag_Call) RunAndReturn(run func(context.Context, string, int64, types.UpdateTag) (*database.Tag, error)) *MockTagComponent_UpdateTag_Call { + _c.Call.Return(run) + return _c +} + // NewMockTagComponent creates a new instance of MockTagComponent. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockTagComponent(t interface { diff --git a/api/handler/tag.go b/api/handler/tag.go index e4e62ca2..53a10ebc 100644 --- a/api/handler/tag.go +++ b/api/handler/tag.go @@ -3,10 +3,12 @@ package handler import ( "log/slog" "net/http" + "strconv" "github.com/gin-gonic/gin" "opencsg.com/csghub-server/api/httpbase" "opencsg.com/csghub-server/common/config" + "opencsg.com/csghub-server/common/types" "opencsg.com/csghub-server/component" ) @@ -27,13 +29,14 @@ type TagsHandler struct { // GetAllTags godoc // @Security ApiKey // @Summary Get all tags -// @Description get all tags +// @Description Get all tags // @Tags Tag // @Accept json // @Produce json // @Param category query string false "category name" // @Param scope query string false "scope name" Enums(model, dataset) -// @Success 200 {object} types.ResponseWithTotal{data=[]database.Tag,total=int} "tags" +// @Success 200 {object} types.ResponseWithTotal{data=[]database.Tag} "tags" +// @Failure 400 {object} types.APIBadRequest "Bad request" // @Failure 500 {object} types.APIInternalServerError "Internal server error" // @Router /tags [get] func (t *TagsHandler) AllTags(ctx *gin.Context) { @@ -42,14 +45,151 @@ func (t *TagsHandler) AllTags(ctx *gin.Context) { scope := ctx.Query("scope") tags, err := t.tc.AllTagsByScopeAndCategory(ctx, scope, category) if err != nil { - slog.Error("Failed to load tags", "error", err) + slog.Error("Failed to load tags", slog.Any("category", category), slog.Any("scope", scope), slog.Any("error", err)) httpbase.ServerError(ctx, err) return } respData := gin.H{ "data": tags, } - - slog.Info("Tags loaded successfully", "count", len(tags)) ctx.JSON(http.StatusOK, respData) } + +// CreateTag godoc +// @Security ApiKey +// @Summary Create new tag +// @Description Create new tag +// @Tags Tag +// @Accept json +// @Produce json +// @Param body body types.CreateTag true "body" +// @Success 200 {object} types.Response{database.Tag} "OK" +// @Failure 400 {object} types.APIBadRequest "Bad request" +// @Failure 500 {object} types.APIInternalServerError "Internal server error" +// @Router /tags [post] +func (t *TagsHandler) CreateTag(ctx *gin.Context) { + userName := httpbase.GetCurrentUser(ctx) + if userName == "" { + httpbase.UnauthorizedError(ctx, component.ErrUserNotFound) + return + } + var req types.CreateTag + if err := ctx.ShouldBindJSON(&req); err != nil { + slog.Error("Bad request format", slog.Any("error", err)) + httpbase.BadRequest(ctx, err.Error()) + return + } + tag, err := t.tc.CreateTag(ctx, userName, req) + if err != nil { + slog.Error("Failed to create tag", slog.Any("req", req), slog.Any("error", err)) + httpbase.ServerError(ctx, err) + return + } + ctx.JSON(http.StatusOK, gin.H{"data": tag}) +} + +// GetTag godoc +// @Security ApiKey +// @Summary Get a tag by id +// @Description Get a tag by id +// @Tags Tag +// @Accept json +// @Produce json +// @Param id path string true "id of the tag" +// @Success 200 {object} types.Response{database.Tag} "OK" +// @Failure 400 {object} types.APIBadRequest "Bad request" +// @Failure 500 {object} types.APIInternalServerError "Internal server error" +// @Router /tag/{id} [get] +func (t *TagsHandler) GetTagByID(ctx *gin.Context) { + userName := httpbase.GetCurrentUser(ctx) + if userName == "" { + httpbase.UnauthorizedError(ctx, component.ErrUserNotFound) + return + } + id, err := strconv.ParseInt(ctx.Param("id"), 10, 64) + if err != nil { + slog.Error("Bad request format", slog.Any("error", err)) + httpbase.BadRequest(ctx, err.Error()) + return + } + tag, err := t.tc.GetTagByID(ctx, userName, id) + if err != nil { + slog.Error("Failed to get tag", slog.Int64("id", id), slog.Any("error", err)) + httpbase.ServerError(ctx, err) + return + } + ctx.JSON(http.StatusOK, gin.H{"data": tag}) +} + +// UpdateTag godoc +// @Security ApiKey +// @Summary Update a tag by id +// @Description Update a tag by id +// @Tags Tag +// @Accept json +// @Produce json +// @Param id path string true "id of the tag" +// @Param body body types.UpdateTag true "body" +// @Success 200 {object} types.Response{database.Tag} "OK" +// @Failure 400 {object} types.APIBadRequest "Bad request" +// @Failure 500 {object} types.APIInternalServerError "Internal server error" +// @Router /tag/{id} [put] +func (t *TagsHandler) UpdateTag(ctx *gin.Context) { + userName := httpbase.GetCurrentUser(ctx) + if userName == "" { + httpbase.UnauthorizedError(ctx, component.ErrUserNotFound) + return + } + id, err := strconv.ParseInt(ctx.Param("id"), 10, 64) + if err != nil { + slog.Error("Bad request format", slog.Any("error", err)) + httpbase.BadRequest(ctx, err.Error()) + return + } + var req types.UpdateTag + if err := ctx.ShouldBindJSON(&req); err != nil { + slog.Error("Bad request format", slog.Any("error", err)) + httpbase.BadRequest(ctx, err.Error()) + return + } + tag, err := t.tc.UpdateTag(ctx, userName, id, req) + if err != nil { + slog.Error("Failed to update tag", slog.Int64("id", id), slog.Any("error", err)) + httpbase.ServerError(ctx, err) + return + } + ctx.JSON(http.StatusOK, gin.H{"data": tag}) +} + +// DeleteTag godoc +// @Security ApiKey +// @Summary Delete a tag by id +// @Description Delete a tag by id +// @Tags Tag +// @Accept json +// @Produce json +// @Param id path string true "id of the tag" +// @Success 200 {object} types.Response{} "OK" +// @Failure 400 {object} types.APIBadRequest "Bad request" +// @Failure 500 {object} types.APIInternalServerError "Internal server error" +// @Router /tag/{id} [delete] +func (t *TagsHandler) DeleteTag(ctx *gin.Context) { + userName := httpbase.GetCurrentUser(ctx) + if userName == "" { + httpbase.UnauthorizedError(ctx, component.ErrUserNotFound) + return + } + id, err := strconv.ParseInt(ctx.Param("id"), 10, 64) + if err != nil { + slog.Error("Bad request format", slog.Any("error", err)) + httpbase.BadRequest(ctx, err.Error()) + return + } + err = t.tc.DeleteTag(ctx, userName, id) + if err != nil { + slog.Error("Failed to delete tag", slog.Int64("id", id), slog.Any("error", err)) + httpbase.ServerError(ctx, err) + return + } + ctx.JSON(http.StatusOK, nil) +} diff --git a/api/handler/tag_test.go b/api/handler/tag_test.go new file mode 100644 index 00000000..ed47c226 --- /dev/null +++ b/api/handler/tag_test.go @@ -0,0 +1,197 @@ +package handler + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + mockcom "opencsg.com/csghub-server/_mocks/opencsg.com/csghub-server/component" + "opencsg.com/csghub-server/api/httpbase" + "opencsg.com/csghub-server/builder/store/database" + "opencsg.com/csghub-server/common/types" + "opencsg.com/csghub-server/component" +) + +func NewTestTagHandler( + tagComp component.TagComponent, +) (*TagsHandler, error) { + return &TagsHandler{ + tc: tagComp, + }, nil +} + +func TestTagHandler_AllTags(t *testing.T) { + var tags []*database.Tag + tags = append(tags, &database.Tag{ID: 1, Name: "test1"}) + + values := url.Values{} + values.Add("category", "testcate") + values.Add("scope", "testscope") + req := httptest.NewRequest("get", "/api/v1/tags?"+values.Encode(), nil) + + hr := httptest.NewRecorder() + ginContext, _ := gin.CreateTestContext(hr) + ginContext.Request = req + + tagComp := mockcom.NewMockTagComponent(t) + tagComp.EXPECT().AllTagsByScopeAndCategory(ginContext, "testscope", "testcate").Return(tags, nil) + + tagHandler, err := NewTestTagHandler(tagComp) + require.Nil(t, err) + + tagHandler.AllTags(ginContext) + + require.Equal(t, http.StatusOK, hr.Code) + + var resp httpbase.R + + err = json.Unmarshal(hr.Body.Bytes(), &resp) + require.Nil(t, err) + + require.Equal(t, 0, resp.Code) + require.Equal(t, "", resp.Msg) + require.NotNil(t, resp.Data) +} + +func TestTagHandler_CreateTag(t *testing.T) { + username := "testuser" + data := types.CreateTag{ + Name: "testtag", + Scope: "testscope", + Category: "testcategory", + } + + reqBody, _ := json.Marshal(data) + + req := httptest.NewRequest("post", "/api/v1/tags", bytes.NewBuffer(reqBody)) + + hr := httptest.NewRecorder() + ginContext, _ := gin.CreateTestContext(hr) + ginContext.Set("currentUser", username) + ginContext.Request = req + + tagComp := mockcom.NewMockTagComponent(t) + tagComp.EXPECT().CreateTag(ginContext, username, mock.Anything).Return(&database.Tag{ID: 1, Name: "testtag"}, nil) + + tagHandler, err := NewTestTagHandler(tagComp) + require.Nil(t, err) + + tagHandler.CreateTag(ginContext) + + require.Equal(t, http.StatusOK, hr.Code) + + var resp httpbase.R + + err = json.Unmarshal(hr.Body.Bytes(), &resp) + require.Nil(t, err) + + require.Equal(t, 0, resp.Code) + require.Equal(t, "", resp.Msg) + require.NotNil(t, resp.Data) +} + +func TestTagHandler_GetTagByID(t *testing.T) { + username := "testuser" + + req := httptest.NewRequest("get", "/api/v1/tags/1", nil) + + hr := httptest.NewRecorder() + ginContext, _ := gin.CreateTestContext(hr) + ginContext.Set("currentUser", username) + ginContext.AddParam("id", "1") + ginContext.Request = req + + tagComp := mockcom.NewMockTagComponent(t) + tagComp.EXPECT().GetTagByID(ginContext, username, int64(1)).Return(&database.Tag{ID: 1, Name: "test1"}, nil) + + tagHandler, err := NewTestTagHandler(tagComp) + require.Nil(t, err) + + tagHandler.GetTagByID(ginContext) + + require.Equal(t, http.StatusOK, hr.Code) + + var resp httpbase.R + + err = json.Unmarshal(hr.Body.Bytes(), &resp) + require.Nil(t, err) + + require.Equal(t, 0, resp.Code) + require.Equal(t, "", resp.Msg) + require.NotNil(t, resp.Data) +} + +func TestTagHandler_UpdateTag(t *testing.T) { + username := "testuser" + data := types.UpdateTag{ + Name: "testtag", + Scope: "testscope", + Category: "testcategory", + } + + reqBody, _ := json.Marshal(data) + + req := httptest.NewRequest("put", "/api/v1/tags/1", bytes.NewBuffer(reqBody)) + + hr := httptest.NewRecorder() + ginContext, _ := gin.CreateTestContext(hr) + ginContext.Set("currentUser", username) + ginContext.AddParam("id", "1") + ginContext.Request = req + + tagComp := mockcom.NewMockTagComponent(t) + tagComp.EXPECT().UpdateTag(ginContext, username, int64(1), mock.Anything).Return(&database.Tag{ID: 1, Name: "testtag"}, nil) + + tagHandler, err := NewTestTagHandler(tagComp) + require.Nil(t, err) + + tagHandler.UpdateTag(ginContext) + + require.Equal(t, http.StatusOK, hr.Code) + + var resp httpbase.R + + err = json.Unmarshal(hr.Body.Bytes(), &resp) + require.Nil(t, err) + + require.Equal(t, 0, resp.Code) + require.Equal(t, "", resp.Msg) + require.NotNil(t, resp.Data) +} + +func TestTagHandler_DeleteTag(t *testing.T) { + username := "testuser" + + req := httptest.NewRequest("delete", "/api/v1/tags/1", nil) + + hr := httptest.NewRecorder() + ginContext, _ := gin.CreateTestContext(hr) + ginContext.Set("currentUser", username) + ginContext.AddParam("id", "1") + ginContext.Request = req + + tagComp := mockcom.NewMockTagComponent(t) + tagComp.EXPECT().DeleteTag(ginContext, username, int64(1)).Return(nil) + + tagHandler, err := NewTestTagHandler(tagComp) + require.Nil(t, err) + + tagHandler.DeleteTag(ginContext) + + require.Equal(t, http.StatusOK, hr.Code) + + var resp httpbase.R + + err = json.Unmarshal(hr.Body.Bytes(), &resp) + require.Nil(t, err) + + require.Equal(t, 0, resp.Code) + require.Equal(t, "", resp.Msg) + require.Nil(t, resp.Data) +} diff --git a/api/router/api.go b/api/router/api.go index 9b01c3e8..3ba007c5 100644 --- a/api/router/api.go +++ b/api/router/api.go @@ -222,15 +222,13 @@ func NewRouter(config *config.Config, enableSwagger bool) (*gin.Engine, error) { apiGroup.PUT("/organization/:namespace/members/:username", userProxyHandler.ProxyToApi("/api/v1/organization/%s/members/%s", "namespace", "username")) apiGroup.DELETE("/organization/:namespace/members/:username", userProxyHandler.ProxyToApi("/api/v1/organization/%s/members/%s", "namespace", "username")) } + // Tag tagCtrl, err := handler.NewTagHandler(config) if err != nil { return nil, fmt.Errorf("error creating tag controller:%w", err) } - apiGroup.GET("/tags", tagCtrl.AllTags) - // apiGroup.POST("/tag", tagCtrl.NewTag) - // apiGroup.PUT("/tag", tagCtrl.UpdateTag) - // apiGroup.DELETE("/tag", tagCtrl.DeleteTag) + createTagsRoutes(apiGroup, tagCtrl) // JWT token apiGroup.POST("/jwt/token", needAPIKey, userProxyHandler.Proxy) @@ -783,3 +781,14 @@ func createPromptRoutes(apiGroup *gin.RouterGroup, promptHandler *handler.Prompt promptGrp.POST("/:namespace/:name/update_downloads", promptHandler.UpdateDownloads) } } + +func createTagsRoutes(apiGroup *gin.RouterGroup, tagHandler *handler.TagsHandler) { + tagsGrp := apiGroup.Group("/tags") + { + tagsGrp.GET("", tagHandler.AllTags) + tagsGrp.POST("", tagHandler.CreateTag) + tagsGrp.GET("/:id", tagHandler.GetTagByID) + tagsGrp.PUT("/:id", tagHandler.UpdateTag) + tagsGrp.DELETE("/:id", tagHandler.DeleteTag) + } +} diff --git a/builder/store/database/tag.go b/builder/store/database/tag.go index e0ced7b6..2194afc4 100644 --- a/builder/store/database/tag.go +++ b/builder/store/database/tag.go @@ -40,6 +40,9 @@ type TagStore interface { RemoveRepoTags(ctx context.Context, repoID int64, tagIDs []int64) (err error) FindOrCreate(ctx context.Context, tag Tag) (*Tag, error) FindTag(ctx context.Context, name, scope, category string) (*Tag, error) + FindTagByID(ctx context.Context, id int64) (*Tag, error) + UpdateTagByID(ctx context.Context, tag *Tag) (*Tag, error) + DeleteTagByID(ctx context.Context, id int64) error } func NewTagStore() TagStore { @@ -393,3 +396,47 @@ func (ts *tagStoreImpl) FindTag(ctx context.Context, name, scope, category strin } return &tag, nil } + +// find tag by id +func (ts *tagStoreImpl) FindTagByID(ctx context.Context, id int64) (*Tag, error) { + var tag Tag + err := ts.db.Operator.Core.NewSelect(). + Model(&tag). + Where("id = ?", id). + Scan(ctx) + if err != nil { + return nil, fmt.Errorf("select tag by id %d error: %w", id, err) + } + return &tag, nil +} + +func (ts *tagStoreImpl) UpdateTagByID(ctx context.Context, tag *Tag) (*Tag, error) { + _, err := ts.db.Operator.Core.NewUpdate(). + Model(tag).WherePK().Exec(ctx) + if err != nil { + return nil, fmt.Errorf("update tag by id %d error: %w", tag.ID, err) + } + return tag, nil +} + +func (ts *tagStoreImpl) DeleteTagByID(ctx context.Context, id int64) error { + err := ts.db.Operator.Core.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + _, err := tx.NewDelete(). + Model(&Tag{}). + Where("id = ?", id). + Exec(ctx) + if err != nil { + return fmt.Errorf("delete tag by id %d error: %w", id, err) + } + _, err = tx.NewDelete(). + Model(&RepositoryTag{}). + Where("tag_id = ?", id). + Exec(ctx) + if err != nil { + return fmt.Errorf("delete repository_tag by tag_id %d error: %w", id, err) + } + return nil + }) + return err + +} diff --git a/builder/store/database/tag_test.go b/builder/store/database/tag_test.go index 777b4f4c..27d3bc0f 100644 --- a/builder/store/database/tag_test.go +++ b/builder/store/database/tag_test.go @@ -542,3 +542,86 @@ func TestTagStore_RemoveRepoTags(t *testing.T) { require.EqualValues(t, repoTags[0], tag1) } + +func TestTagStore_FindTagByID(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + var err error + ts := database.NewTagStoreWithDB(db) + t1, err := ts.CreateTag(ctx, "task", "tag_"+uuid.NewString(), "", database.ModelTagScope) + require.Empty(t, err) + require.NotEmpty(t, t1.ID) + + tag, err := ts.FindTagByID(ctx, t1.ID) + require.Empty(t, err) + require.Equal(t, tag.ID, t1.ID) + require.Equal(t, tag.Name, t1.Name) +} + +func TestTagStore_UpdateTagByID(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + ts := database.NewTagStoreWithDB(db) + + t1, err := ts.CreateTag(ctx, "task", "tag_"+uuid.NewString(), "", database.ModelTagScope) + require.Empty(t, err) + require.NotEmpty(t, t1.ID) + + newName := "new_tag_" + uuid.NewString() + + t1.Name = newName + + tag, err := ts.UpdateTagByID(ctx, &t1) + require.Empty(t, err) + require.Equal(t, tag.Name, newName) + +} + +func TestTagStore_DeleteTagByID(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + ts := database.NewTagStoreWithDB(db) + tag1, err := ts.CreateTag(ctx, "task", "tag_"+uuid.NewString(), "", database.ModelTagScope) + require.Empty(t, err) + require.NotEmpty(t, tag1.ID) + + // insert a new repo with tags + rs := database.NewRepoStoreWithDB(db) + userName := "user_name_" + uuid.NewString() + repoName := "repo_name_" + uuid.NewString() + repo, err := rs.CreateRepo(ctx, database.Repository{ + UserID: 1, + Path: fmt.Sprintf("%s/%s", userName, repoName), + GitPath: fmt.Sprintf("models_%s/%s", userName, repoName), + Name: repoName, + Nickname: "", + Description: "", + Private: false, + RepositoryType: types.ModelRepo, + }) + require.Empty(t, err) + require.NotNil(t, repo) + + // set repo tags + err = ts.UpsertRepoTags(ctx, repo.ID, []int64{}, []int64{tag1.ID}) + require.Empty(t, err) + + err = ts.DeleteTagByID(ctx, tag1.ID) + require.Empty(t, err) + + _, err = ts.FindTagByID(ctx, tag1.ID) + require.NotEmpty(t, err) + +} diff --git a/common/types/tag.go b/common/types/tag.go index 7e20074f..d85fa91c 100644 --- a/common/types/tag.go +++ b/common/types/tag.go @@ -1,6 +1,8 @@ package types -import "time" +import ( + "time" +) type RepoTag struct { Name string `json:"name"` @@ -22,3 +24,14 @@ const ( LanguageCategory TagCategory = "language" EvaluationCategory TagCategory = "evaluation" ) + +type CreateTag struct { + Name string `json:"name" binding:"required"` + Category string `json:"category" binding:"required"` + Group string `json:"group"` + Scope string `json:"scope" binding:"required"` + BuiltIn bool `json:"built_in"` + ShowName string `json:"show_name"` +} + +type UpdateTag CreateTag diff --git a/component/tag.go b/component/tag.go index 2c95e4f3..44c41506 100644 --- a/component/tag.go +++ b/component/tag.go @@ -20,6 +20,10 @@ type TagComponent interface { UpdateMetaTags(ctx context.Context, tagScope database.TagScope, namespace, name, content string) ([]*database.RepositoryTag, error) UpdateLibraryTags(ctx context.Context, tagScope database.TagScope, namespace, name, oldFilePath, newFilePath string) error UpdateRepoTagsByCategory(ctx context.Context, tagScope database.TagScope, repoID int64, category string, tagNames []string) error + CreateTag(ctx context.Context, username string, req types.CreateTag) (*database.Tag, error) + GetTagByID(ctx context.Context, username string, id int64) (*database.Tag, error) + UpdateTag(ctx context.Context, username string, id int64, req types.UpdateTag) (*database.Tag, error) + DeleteTag(ctx context.Context, username string, id int64) error } func NewTagComponent(config *config.Config) (TagComponent, error) { @@ -29,6 +33,7 @@ func NewTagComponent(config *config.Config) (TagComponent, error) { if config.SensitiveCheck.Enable { tc.sensitiveChecker = rpc.NewModerationSvcHttpClient(fmt.Sprintf("%s:%d", config.Moderation.Host, config.Moderation.Port)) } + tc.userStore = database.NewUserStore() return tc, nil } @@ -36,6 +41,7 @@ type tagComponentImpl struct { tagStore database.TagStore repoStore database.RepoStore sensitiveChecker rpc.ModerationSvcClient + userStore database.UserStore } func (tc *tagComponentImpl) AllTagsByScopeAndCategory(ctx context.Context, scope string, category string) ([]*database.Tag, error) { @@ -187,3 +193,101 @@ func (c *tagComponentImpl) UpdateRepoTagsByCategory(ctx context.Context, tagScop } return c.tagStore.UpsertRepoTags(ctx, repoID, oldTagIDs, tagIDs) } + +func (c *tagComponentImpl) CreateTag(ctx context.Context, username string, req types.CreateTag) (*database.Tag, error) { + user, err := c.userStore.FindByUsername(ctx, username) + if err != nil { + return nil, fmt.Errorf("failed to get user, error: %w", err) + } + if !user.CanAdmin() { + return nil, fmt.Errorf("user %s do not allowed create tag", username) + } + + if c.sensitiveChecker != nil { + result, err := c.sensitiveChecker.PassTextCheck(ctx, string(sensitive.ScenarioNicknameDetection), req.Name) + if err != nil { + return nil, fmt.Errorf("failed to check tag name sensitivity, error: %w", err) + } + if result.IsSensitive { + return nil, fmt.Errorf("tag name contains sensitive words") + } + } + + newTag := database.Tag{ + Name: req.Name, + Category: req.Category, + Group: req.Group, + Scope: database.TagScope(req.Scope), + BuiltIn: req.BuiltIn, + } + + tag, err := c.tagStore.FindOrCreate(ctx, newTag) + if err != nil { + return nil, fmt.Errorf("failed to create tag, error: %w", err) + } + return tag, nil +} + +func (c *tagComponentImpl) GetTagByID(ctx context.Context, username string, id int64) (*database.Tag, error) { + user, err := c.userStore.FindByUsername(ctx, username) + if err != nil { + return nil, fmt.Errorf("failed to get user, error: %w", err) + } + if !user.CanAdmin() { + return nil, fmt.Errorf("user %s do not allowed create tag", username) + } + tag, err := c.tagStore.FindTagByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get tag id %d, error: %w", id, err) + } + return tag, nil +} + +func (c *tagComponentImpl) UpdateTag(ctx context.Context, username string, id int64, req types.UpdateTag) (*database.Tag, error) { + user, err := c.userStore.FindByUsername(ctx, username) + if err != nil { + return nil, fmt.Errorf("failed to get user, error: %w", err) + } + if !user.CanAdmin() { + return nil, fmt.Errorf("user %s do not allowed create tag", username) + } + + if c.sensitiveChecker != nil { + result, err := c.sensitiveChecker.PassTextCheck(ctx, string(sensitive.ScenarioNicknameDetection), req.Name) + if err != nil { + return nil, fmt.Errorf("failed to check tag name sensitivity, error: %w", err) + } + if result.IsSensitive { + return nil, fmt.Errorf("tag name contains sensitive words") + } + } + + tag := &database.Tag{ + ID: id, + Category: req.Category, + Name: req.Name, + Group: req.Group, + Scope: database.TagScope(req.Scope), + BuiltIn: req.BuiltIn, + } + newTag, err := c.tagStore.UpdateTagByID(ctx, tag) + if err != nil { + return nil, fmt.Errorf("failed to update tag id %d, error: %w", id, err) + } + return newTag, nil +} + +func (c *tagComponentImpl) DeleteTag(ctx context.Context, username string, id int64) error { + user, err := c.userStore.FindByUsername(ctx, username) + if err != nil { + return fmt.Errorf("failed to get user, error: %w", err) + } + if !user.CanAdmin() { + return fmt.Errorf("user %s do not allowed create tag", username) + } + err = c.tagStore.DeleteTagByID(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete tag id %d, error: %w", id, err) + } + return nil +} diff --git a/component/tag_test.go b/component/tag_test.go index 2f53aa30..acfa168d 100644 --- a/component/tag_test.go +++ b/component/tag_test.go @@ -4,11 +4,158 @@ import ( "context" "testing" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "opencsg.com/csghub-server/builder/rpc" "opencsg.com/csghub-server/builder/store/database" "opencsg.com/csghub-server/common/types" ) +func TestTagComponent_CreateTag(t *testing.T) { + ctx := context.TODO() + + username := "testUser" + + req := types.CreateTag{ + Name: "my first tag", + Category: "testCategory", + Group: "testGroup", + Scope: "testScope", + BuiltIn: true, + } + + newTag := database.Tag{ + Name: req.Name, + Category: req.Category, + Group: req.Group, + Scope: database.TagScope(req.Scope), + BuiltIn: req.BuiltIn, + } + + t.Run("admin", func(t *testing.T) { + tc := initializeTestTagComponent(ctx, t) + + tc.mocks.stores.UserMock().EXPECT().FindByUsername(ctx, username).Return(database.User{UUID: "testUUID", RoleMask: "admin"}, nil) + tc.mocks.stores.TagMock().EXPECT().FindOrCreate(ctx, newTag).Return(&newTag, nil) + tc.mocks.moderationClient.EXPECT().PassTextCheck(ctx, mock.Anything, req.Name).Return(&rpc.CheckResult{ + IsSensitive: false, + }, nil) + + tag, err := tc.CreateTag(ctx, username, req) + require.Nil(t, err) + require.Equal(t, req.Name, tag.Name) + require.Equal(t, true, tag.BuiltIn) + }) + + t.Run("non-admin", func(t *testing.T) { + tc := initializeTestTagComponent(ctx, t) + + tc.mocks.stores.UserMock().EXPECT().FindByUsername(ctx, username).Return(database.User{UUID: "testUUID", RoleMask: "persion"}, nil) + + tag, err := tc.CreateTag(ctx, username, req) + require.NotNil(t, err) + require.Nil(t, tag) + }) +} + +func TestTagComponent_GetTagByID(t *testing.T) { + ctx := context.TODO() + username := "testUser" + t.Run("admin", func(t *testing.T) { + tc := initializeTestTagComponent(ctx, t) + tc.mocks.stores.UserMock().EXPECT().FindByUsername(ctx, username).Return(database.User{UUID: "testUUID", RoleMask: "admin"}, nil) + tc.mocks.stores.TagMock().EXPECT().FindTagByID(ctx, int64(1)).Return(&database.Tag{ID: int64(1), Name: "test-tag"}, nil) + + tag, err := tc.GetTagByID(ctx, username, int64(1)) + require.Nil(t, err) + require.Equal(t, int64(1), tag.ID) + require.Equal(t, "test-tag", tag.Name) + }) + + t.Run("non-admin", func(t *testing.T) { + tc := initializeTestTagComponent(ctx, t) + + tc.mocks.stores.UserMock().EXPECT().FindByUsername(ctx, username).Return(database.User{UUID: "testUUID", RoleMask: "person"}, nil) + + tag, err := tc.GetTagByID(ctx, username, int64(1)) + require.NotNil(t, err) + require.Nil(t, tag) + }) +} + +func TestTagComponent_UpdateTag(t *testing.T) { + ctx := context.TODO() + + username := "testUser" + + req := types.UpdateTag{ + Name: "testTag", + Category: "testCategory", + Group: "testGroup", + Scope: "testScope", + BuiltIn: true, + } + + newTag := database.Tag{ + ID: int64(1), + Name: req.Name, + Category: req.Category, + Group: req.Group, + Scope: database.TagScope(req.Scope), + BuiltIn: req.BuiltIn, + } + + t.Run("admin", func(t *testing.T) { + tc := initializeTestTagComponent(ctx, t) + + tc.mocks.stores.UserMock().EXPECT().FindByUsername(ctx, username).Return(database.User{UUID: "testUUID", RoleMask: "admin"}, nil) + tc.mocks.stores.TagMock().EXPECT().UpdateTagByID(ctx, &newTag).Return(&newTag, nil) + tc.mocks.moderationClient.EXPECT().PassTextCheck(ctx, mock.Anything, req.Name).Return(&rpc.CheckResult{ + IsSensitive: false, + }, nil) + + tag, err := tc.UpdateTag(ctx, username, int64(1), req) + require.Nil(t, err) + require.Equal(t, req.Name, tag.Name) + require.Equal(t, true, tag.BuiltIn) + }) + + t.Run("non-admin", func(t *testing.T) { + tc := initializeTestTagComponent(ctx, t) + + tc.mocks.stores.UserMock().EXPECT().FindByUsername(ctx, username).Return(database.User{UUID: "testUUID", RoleMask: "persion"}, nil) + + tag, err := tc.UpdateTag(ctx, username, int64(1), req) + require.NotNil(t, err) + require.Nil(t, tag) + }) +} + +func TestTagComponent_DeleteTag(t *testing.T) { + ctx := context.TODO() + + username := "testUser" + + t.Run("admin", func(t *testing.T) { + tc := initializeTestTagComponent(ctx, t) + + tc.mocks.stores.UserMock().EXPECT().FindByUsername(ctx, username).Return(database.User{UUID: "testUUID", RoleMask: "admin"}, nil) + tc.mocks.stores.TagMock().EXPECT().DeleteTagByID(ctx, int64(1)).Return(nil) + + err := tc.DeleteTag(ctx, username, int64(1)) + require.Nil(t, err) + }) + + t.Run("non-admin", func(t *testing.T) { + tc := initializeTestTagComponent(ctx, t) + + tc.mocks.stores.UserMock().EXPECT().FindByUsername(ctx, username).Return(database.User{UUID: "testUUID", RoleMask: "persion"}, nil) + + err := tc.DeleteTag(ctx, username, int64(1)) + require.NotNil(t, err) + }) +} + func TestTagComponent_AllTagsByScopeAndCategory(t *testing.T) { ctx := context.TODO() tc := initializeTestTagComponent(ctx, t) diff --git a/component/wireset.go b/component/wireset.go index 6723031f..e9394a66 100644 --- a/component/wireset.go +++ b/component/wireset.go @@ -465,6 +465,7 @@ func NewTestTagComponent(config *config.Config, stores *tests.MockStores, sensit tagStore: stores.Tag, repoStore: stores.Repo, sensitiveChecker: sensitiveChecker, + userStore: stores.User, } } diff --git a/docs/docs.go b/docs/docs.go index 3b75dac4..cf8b79d2 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4023,77 +4023,6 @@ const docTemplate = `{ } } }, - "/models/{namespace}/{name}/predict": { - "post": { - "security": [ - { - "ApiKey": [] - } - ], - "description": "invoke model prediction", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Model" - ], - "summary": "Invoke model prediction", - "parameters": [ - { - "type": "string", - "description": "namespace", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "name", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "current user", - "name": "current_user", - "in": "query" - }, - { - "description": "input for model prediction", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/types.ModelPredictReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "string" - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/types.APIBadRequest" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/types.APIInternalServerError" - } - } - } - } - }, "/models/{namespace}/{name}/relations": { "get": { "security": [ @@ -10022,6 +9951,158 @@ const docTemplate = `{ } } }, + "/tag/{id}": { + "get": { + "security": [ + { + "ApiKey": [] + } + ], + "description": "Get a tag by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "Get a tag by id", + "parameters": [ + { + "type": "string", + "description": "id of the tag", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.Response" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/types.APIBadRequest" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/types.APIInternalServerError" + } + } + } + }, + "put": { + "security": [ + { + "ApiKey": [] + } + ], + "description": "Update a tag by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "Update a tag by id", + "parameters": [ + { + "type": "string", + "description": "id of the tag", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/types.UpdateTag" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.Response" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/types.APIBadRequest" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/types.APIInternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKey": [] + } + ], + "description": "Delete a tag by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "Delete a tag by id", + "parameters": [ + { + "type": "string", + "description": "id of the tag", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.Response" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/types.APIBadRequest" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/types.APIInternalServerError" + } + } + } + } + }, "/tags": { "get": { "security": [ @@ -10029,7 +10110,7 @@ const docTemplate = `{ "ApiKey": [] } ], - "description": "get all tags", + "description": "Get all tags", "consumes": [ "application/json" ], @@ -10074,15 +10155,67 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/database.Tag" } - }, - "total": { - "type": "integer" } } } ] } }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/types.APIBadRequest" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/types.APIInternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "ApiKey": [] + } + ], + "description": "Create new tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "Create new tag", + "parameters": [ + { + "description": "body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/types.CreateTag" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.Response" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/types.APIBadRequest" + } + }, "500": { "description": "Internal server error", "schema": { @@ -16919,6 +17052,45 @@ const docTemplate = `{ } } }, + "types.CreateTag": { + "type": "object", + "required": [ + "category", + "name", + "scope" + ], + "properties": { + "built_in": { + "type": "boolean" + }, + "category": { + "type": "string" + }, + "group": { + "type": "string" + }, + "name": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "show_name": { + "type": "string" + } + } + }, + "types.CreateUserResourceReq": { + "type": "object", + "properties": { + "order_details": { + "type": "array", + "items": { + "$ref": "#/definitions/types.AcctOrderDetailReq" + } + } + } + }, "types.CreateUserTokenRequest": { "type": "object", "properties": { @@ -17590,20 +17762,6 @@ const docTemplate = `{ } } }, - "types.ModelPredictReq": { - "type": "object", - "properties": { - "current_user": { - "type": "string" - }, - "input": { - "type": "string" - }, - "version": { - "type": "string" - } - } - }, "types.ModelResp": { "type": "object", "properties": { @@ -18556,6 +18714,34 @@ const docTemplate = `{ } } }, + "types.UpdateTag": { + "type": "object", + "required": [ + "category", + "name", + "scope" + ], + "properties": { + "built_in": { + "type": "boolean" + }, + "category": { + "type": "string" + }, + "group": { + "type": "string" + }, + "name": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "show_name": { + "type": "string" + } + } + }, "types.UpdateUserRequest": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index c5dfdef4..c55503dd 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4012,77 +4012,6 @@ } } }, - "/models/{namespace}/{name}/predict": { - "post": { - "security": [ - { - "ApiKey": [] - } - ], - "description": "invoke model prediction", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Model" - ], - "summary": "Invoke model prediction", - "parameters": [ - { - "type": "string", - "description": "namespace", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "name", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "current user", - "name": "current_user", - "in": "query" - }, - { - "description": "input for model prediction", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/types.ModelPredictReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "string" - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/types.APIBadRequest" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/types.APIInternalServerError" - } - } - } - } - }, "/models/{namespace}/{name}/relations": { "get": { "security": [ @@ -10011,6 +9940,158 @@ } } }, + "/tag/{id}": { + "get": { + "security": [ + { + "ApiKey": [] + } + ], + "description": "Get a tag by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "Get a tag by id", + "parameters": [ + { + "type": "string", + "description": "id of the tag", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.Response" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/types.APIBadRequest" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/types.APIInternalServerError" + } + } + } + }, + "put": { + "security": [ + { + "ApiKey": [] + } + ], + "description": "Update a tag by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "Update a tag by id", + "parameters": [ + { + "type": "string", + "description": "id of the tag", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/types.UpdateTag" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.Response" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/types.APIBadRequest" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/types.APIInternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKey": [] + } + ], + "description": "Delete a tag by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "Delete a tag by id", + "parameters": [ + { + "type": "string", + "description": "id of the tag", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.Response" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/types.APIBadRequest" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/types.APIInternalServerError" + } + } + } + } + }, "/tags": { "get": { "security": [ @@ -10018,7 +10099,7 @@ "ApiKey": [] } ], - "description": "get all tags", + "description": "Get all tags", "consumes": [ "application/json" ], @@ -10063,15 +10144,67 @@ "items": { "$ref": "#/definitions/database.Tag" } - }, - "total": { - "type": "integer" } } } ] } }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/types.APIBadRequest" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/types.APIInternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "ApiKey": [] + } + ], + "description": "Create new tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "Create new tag", + "parameters": [ + { + "description": "body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/types.CreateTag" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.Response" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/types.APIBadRequest" + } + }, "500": { "description": "Internal server error", "schema": { @@ -16908,6 +17041,48 @@ } } }, +<<<<<<< HEAD +======= + "types.CreateTag": { + "type": "object", + "required": [ + "category", + "name", + "scope" + ], + "properties": { + "built_in": { + "type": "boolean" + }, + "category": { + "type": "string" + }, + "group": { + "type": "string" + }, + "name": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "show_name": { + "type": "string" + } + } + }, + "types.CreateUserResourceReq": { + "type": "object", + "properties": { + "order_details": { + "type": "array", + "items": { + "$ref": "#/definitions/types.AcctOrderDetailReq" + } + } + } + }, +>>>>>>> 43adc0f4 ([Tags] add tag management feature and UT for ce/ee/saas) "types.CreateUserTokenRequest": { "type": "object", "properties": { @@ -17579,20 +17754,6 @@ } } }, - "types.ModelPredictReq": { - "type": "object", - "properties": { - "current_user": { - "type": "string" - }, - "input": { - "type": "string" - }, - "version": { - "type": "string" - } - } - }, "types.ModelResp": { "type": "object", "properties": { @@ -18545,6 +18706,34 @@ } } }, + "types.UpdateTag": { + "type": "object", + "required": [ + "category", + "name", + "scope" + ], + "properties": { + "built_in": { + "type": "boolean" + }, + "category": { + "type": "string" + }, + "group": { + "type": "string" + }, + "name": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "show_name": { + "type": "string" + } + } + }, "types.UpdateUserRequest": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c8f611fc..623f9236 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1236,6 +1236,35 @@ definitions: required: - token type: object +<<<<<<< HEAD +======= + types.CreateTag: + properties: + built_in: + type: boolean + category: + type: string + group: + type: string + name: + type: string + scope: + type: string + show_name: + type: string + required: + - category + - name + - scope + type: object + types.CreateUserResourceReq: + properties: + order_details: + items: + $ref: '#/definitions/types.AcctOrderDetailReq' + type: array + type: object +>>>>>>> 43adc0f4 ([Tags] add tag management feature and UT for ce/ee/saas) types.CreateUserTokenRequest: properties: application: @@ -1687,15 +1716,6 @@ definitions: description: 'widget UI style: generation,chat' example: generation type: object - types.ModelPredictReq: - properties: - current_user: - type: string - input: - type: string - version: - type: string - type: object types.ModelResp: properties: description: @@ -2343,6 +2363,25 @@ definitions: version: type: string type: object + types.UpdateTag: + properties: + built_in: + type: boolean + category: + type: string + group: + type: string + name: + type: string + scope: + type: string + show_name: + type: string + required: + - category + - name + - scope + type: object types.UpdateUserRequest: properties: avatar: @@ -6629,52 +6668,6 @@ paths: summary: Stop a finetune instance tags: - Model - /models/{namespace}/{name}/predict: - post: - consumes: - - application/json - description: invoke model prediction - parameters: - - description: namespace - in: path - name: namespace - required: true - type: string - - description: name - in: path - name: name - required: true - type: string - - description: current user - in: query - name: current_user - type: string - - description: input for model prediction - in: body - name: body - required: true - schema: - $ref: '#/definitions/types.ModelPredictReq' - produces: - - application/json - responses: - "200": - description: OK - schema: - type: string - "400": - description: Bad request - schema: - $ref: '#/definitions/types.APIBadRequest' - "500": - description: Internal server error - schema: - $ref: '#/definitions/types.APIInternalServerError' - security: - - ApiKey: [] - summary: Invoke model prediction - tags: - - Model /models/{namespace}/{name}/relations: get: consumes: @@ -10477,11 +10470,108 @@ paths: summary: Get latest version tags: - Sync + /tag/{id}: + delete: + consumes: + - application/json + description: Delete a tag by id + parameters: + - description: id of the tag + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/types.Response' + "400": + description: Bad request + schema: + $ref: '#/definitions/types.APIBadRequest' + "500": + description: Internal server error + schema: + $ref: '#/definitions/types.APIInternalServerError' + security: + - ApiKey: [] + summary: Delete a tag by id + tags: + - Tag + get: + consumes: + - application/json + description: Get a tag by id + parameters: + - description: id of the tag + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/types.Response' + "400": + description: Bad request + schema: + $ref: '#/definitions/types.APIBadRequest' + "500": + description: Internal server error + schema: + $ref: '#/definitions/types.APIInternalServerError' + security: + - ApiKey: [] + summary: Get a tag by id + tags: + - Tag + put: + consumes: + - application/json + description: Update a tag by id + parameters: + - description: id of the tag + in: path + name: id + required: true + type: string + - description: body + in: body + name: body + required: true + schema: + $ref: '#/definitions/types.UpdateTag' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/types.Response' + "400": + description: Bad request + schema: + $ref: '#/definitions/types.APIBadRequest' + "500": + description: Internal server error + schema: + $ref: '#/definitions/types.APIInternalServerError' + security: + - ApiKey: [] + summary: Update a tag by id + tags: + - Tag /tags: get: consumes: - application/json - description: get all tags + description: Get all tags parameters: - description: category name in: query @@ -10507,9 +10597,11 @@ paths: items: $ref: '#/definitions/database.Tag' type: array - total: - type: integer type: object + "400": + description: Bad request + schema: + $ref: '#/definitions/types.APIBadRequest' "500": description: Internal server error schema: @@ -10519,6 +10611,37 @@ paths: summary: Get all tags tags: - Tag + post: + consumes: + - application/json + description: Create new tag + parameters: + - description: body + in: body + name: body + required: true + schema: + $ref: '#/definitions/types.CreateTag' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/types.Response' + "400": + description: Bad request + schema: + $ref: '#/definitions/types.APIBadRequest' + "500": + description: Internal server error + schema: + $ref: '#/definitions/types.APIInternalServerError' + security: + - ApiKey: [] + summary: Create new tag + tags: + - Tag /telemetry/usage: post: consumes: