From 55c66356c1645ddb869bd9fc712a759922950526 Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Fri, 3 Apr 2026 10:29:02 +0200 Subject: [PATCH] add api endpoint and test co-authored-by: gemini3-flash --- routers/api/v1/api.go | 3 +- routers/api/v1/packages/package.go | 37 ++++++- templates/swagger/v1_json.tmpl | 42 +++++++- tests/integration/api_packages_test.go | 44 ++++++++ tests/integration/packages_service_test.go | 118 +++++++++++++++++++++ 5 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 tests/integration/packages_service_test.go diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index ea595407d1..ef71a40a67 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1580,10 +1580,11 @@ func Routes() *web.Router { m.Group("/packages/{username}", func() { m.Group("/{type}/{name}", func() { m.Get("/", packages.ListPackageVersions) + m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) m.Group("/{version}", func() { m.Get("", packages.GetPackage) - m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) + m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackageVersion) m.Get("/files", packages.ListPackageFiles) }) diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index cee0daccae..376867ab82 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -118,7 +118,7 @@ func GetPackage(ctx *context.APIContext) { // DeletePackage deletes a package func DeletePackage(ctx *context.APIContext) { - // swagger:operation DELETE /packages/{owner}/{type}/{name}/{version} package deletePackage + // swagger:operation DELETE /packages/{owner}/{type}/{name} package deletePackage // --- // summary: Delete a package // parameters: @@ -137,6 +137,41 @@ func DeletePackage(ctx *context.APIContext) { // description: name of the package // type: string // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + err := packages_service.RemovePackage(ctx, ctx.Doer, ctx.Package.Descriptor.Package) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.Status(http.StatusNoContent) +} + +// DeletePackageVersion deletes a package version +func DeletePackageVersion(ctx *context.APIContext) { + // swagger:operation DELETE /packages/{owner}/{type}/{name}/{version} package deletePackageVersion + // --- + // summary: Delete a package version + // parameters: + // - name: owner + // in: path + // description: owner of the package + // type: string + // required: true + // - name: type + // in: path + // description: type of the package + // type: string + // required: true + // - name: name + // in: path + // description: name of the package + // type: string + // required: true // - name: version // in: path // description: version of the package diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index adc6c18175..865804ffd9 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3914,6 +3914,44 @@ "$ref": "#/responses/notFound" } } + }, + "delete": { + "tags": [ + "package" + ], + "summary": "Delete a package", + "operationId": "deletePackage", + "parameters": [ + { + "type": "string", + "description": "owner of the package", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "type of the package", + "name": "type", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the package", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } } }, "/packages/{owner}/{type}/{name}/-/latest": { @@ -4099,8 +4137,8 @@ "tags": [ "package" ], - "summary": "Delete a package", - "operationId": "deletePackage", + "summary": "Delete a package version", + "operationId": "deletePackageVersion", "parameters": [ { "type": "string", diff --git a/tests/integration/api_packages_test.go b/tests/integration/api_packages_test.go index 4fbb31b11f..913ce1bae2 100644 --- a/tests/integration/api_packages_test.go +++ b/tests/integration/api_packages_test.go @@ -85,6 +85,50 @@ func TestPackageAPI(t *testing.T) { assert.Equal(t, user.Name, p.Creator.UserName) }) + t.Run("DeleteEntirePackage", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + packageName := "test-package-entire-delete" + for _, version := range []string{"1.0.1", "1.0.2"} { + url := fmt.Sprintf("/api/packages/%s/generic/%s/%s/file.bin", user.Name, packageName, version) + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader([]byte{1})). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + } + + req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/packages/%s/generic/%s", user.Name, packageName)). + AddTokenAuth(tokenWritePackage) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s", user.Name, packageName)). + AddTokenAuth(tokenReadPackage) + MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("DeletePackageVersion", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + packageName := "test-package-version-delete" + for _, version := range []string{"1.0.1", "1.0.2"} { + url := fmt.Sprintf("/api/packages/%s/generic/%s/%s/file.bin", user.Name, packageName, version) + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader([]byte{1})). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + } + + req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/packages/%s/generic/%s/1.0.1", user.Name, packageName)). + AddTokenAuth(tokenWritePackage) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/1.0.1", user.Name, packageName)). + AddTokenAuth(tokenReadPackage) + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/1.0.2", user.Name, packageName)). + AddTokenAuth(tokenReadPackage) + MakeRequest(t, req, http.StatusOK) + }) + t.Run("ListPackageVersions", func(t *testing.T) { defer tests.PrintCurrentTest(t)() diff --git a/tests/integration/packages_service_test.go b/tests/integration/packages_service_test.go new file mode 100644 index 0000000000..39fe2017d5 --- /dev/null +++ b/tests/integration/packages_service_test.go @@ -0,0 +1,118 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "bytes" + "testing" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + packages_module "code.gitea.io/gitea/modules/packages" + packages_service "code.gitea.io/gitea/services/packages" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRemovePackage(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // 1. Setup: Create two packages with properties at all levels + createPackage := func(name string) (*packages_model.Package, *packages_model.PackageVersion, *packages_model.PackageFile) { + data, _ := packages_module.CreateHashedBufferFromReader(bytes.NewReader([]byte{1})) + pv, pf, err := packages_service.CreatePackageOrAddFileToExisting(t.Context(), &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: user, + PackageType: packages_model.TypeGeneric, + Name: name, + Version: "1.0.0", + }, + Creator: user, + PackageProperties: map[string]string{"pkg_prop": "val"}, + VersionProperties: map[string]string{"ver_prop": "val"}, + }, &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{Filename: "file.bin"}, + Creator: user, + Data: data, + Properties: map[string]string{"file_prop": "val"}, + }) + require.NoError(t, err) + + p, err := packages_model.GetPackageByID(t.Context(), pv.PackageID) + require.NoError(t, err) + + return p, pv, pf + } + + p1, pv1, pf1 := createPackage("package-1") + p2, pv2, pf2 := createPackage("package-2") + + // Verify properties exist before deletion + checkProps := func(p *packages_model.Package, pv *packages_model.PackageVersion, pf *packages_model.PackageFile, shouldExist bool) { + pps, err := packages_model.GetProperties(t.Context(), packages_model.PropertyTypePackage, p.ID) + require.NoError(t, err) + if shouldExist { + assert.NotEmpty(t, pps) + } else { + assert.Empty(t, pps) + } + + pps, err = packages_model.GetProperties(t.Context(), packages_model.PropertyTypeVersion, pv.ID) + require.NoError(t, err) + if shouldExist { + assert.NotEmpty(t, pps) + } else { + assert.Empty(t, pps) + } + + pps, err = packages_model.GetProperties(t.Context(), packages_model.PropertyTypeFile, pf.ID) + require.NoError(t, err) + if shouldExist { + assert.NotEmpty(t, pps) + } else { + assert.Empty(t, pps) + } + } + + checkProps(p1, pv1, pf1, true) + checkProps(p2, pv2, pf2, true) + + // 2. Act: Remove package 1 + err := packages_service.RemovePackage(t.Context(), user, p1) + assert.NoError(t, err) + + // 3. Assert: Package 1 is gone, Package 2 is untouched + + // Check P1 + _, err = packages_model.GetPackageByID(t.Context(), p1.ID) + assert.ErrorIs(t, err, packages_model.ErrPackageNotExist) + + _, err = packages_model.GetVersionByID(t.Context(), pv1.ID) + assert.ErrorIs(t, err, packages_model.ErrPackageNotExist) + + _, err = packages_model.GetFileForVersionByID(t.Context(), pv1.ID, pf1.ID) + assert.ErrorIs(t, err, packages_model.ErrPackageFileNotExist) + + checkProps(p1, pv1, pf1, false) + + // Check P2 + p2_after, err := packages_model.GetPackageByID(t.Context(), p2.ID) + assert.NoError(t, err) + assert.NotNil(t, p2_after) + + pv2_after, err := packages_model.GetVersionByID(t.Context(), pv2.ID) + assert.NoError(t, err) + assert.NotNil(t, pv2_after) + + pf2_after, err := packages_model.GetFileForVersionByID(t.Context(), pv2.ID, pf2.ID) + assert.NoError(t, err) + assert.NotNil(t, pf2_after) + + checkProps(p2, pv2, pf2, true) +}