From a78c538d8e4928d8d6a1c9ac8f8d2018a80a9a5c Mon Sep 17 00:00:00 2001 From: Gusted Date: Sat, 24 Jun 2023 15:11:39 +0200 Subject: [PATCH] [GITEA] Add Upload URL to release API - Resolves https://codeberg.org/forgejo/forgejo/issues/580 - Return a `upload_field` to any release API response, which points to the API URL for uploading new assets. - Adds unit test. - Adds integration testing to verify URL is returned correctly and that upload endpoint actually works (cherry picked from commit 074413a2dc2071ca580b2d45b3c26d448b9855c7) (cherry picked from commit 33feed47238aeef0e81517e72fdec29eba12b191) (cherry picked from commit 1ca21b95ffa433ea23e2946ad9ed1970244cbbe7) (cherry picked from commit 874f07cec2837bbbf43d55844bb65d1c68fbb207) --- models/repo/release.go | 5 ++++ modules/structs/release.go | 1 + services/convert/release.go | 1 + services/convert/release_test.go | 28 ++++++++++++++++++ templates/swagger/v1_json.tmpl | 4 +++ tests/integration/api_releases_test.go | 40 ++++++++++++++++++++++++++ 6 files changed, 79 insertions(+) create mode 100644 services/convert/release_test.go diff --git a/models/repo/release.go b/models/repo/release.go index c63b324457..070b94db71 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -132,6 +132,11 @@ func (r *Release) HTMLURL() string { return r.Repo.HTMLURL() + "/releases/tag/" + util.PathEscapeSegments(r.TagName) } +// APIUploadURL the api url to upload assets to a release. release must have attributes loaded +func (r *Release) APIUploadURL() string { + return r.APIURL() + "/assets" +} + // Link the relative url for a release on the web UI. release must have attributes loaded func (r *Release) Link() string { return r.Repo.Link() + "/releases/tag/" + util.PathEscapeSegments(r.TagName) diff --git a/modules/structs/release.go b/modules/structs/release.go index 3fe40389b1..c7378645c2 100644 --- a/modules/structs/release.go +++ b/modules/structs/release.go @@ -18,6 +18,7 @@ type Release struct { HTMLURL string `json:"html_url"` TarURL string `json:"tarball_url"` ZipURL string `json:"zipball_url"` + UploadURL string `json:"upload_url"` IsDraft bool `json:"draft"` IsPrerelease bool `json:"prerelease"` // swagger:strfmt date-time diff --git a/services/convert/release.go b/services/convert/release.go index d8aa46d432..bfff53e62f 100644 --- a/services/convert/release.go +++ b/services/convert/release.go @@ -22,6 +22,7 @@ func ToAPIRelease(ctx context.Context, repo *repo_model.Repository, r *repo_mode HTMLURL: r.HTMLURL(), TarURL: r.TarURL(), ZipURL: r.ZipURL(), + UploadURL: r.APIUploadURL(), IsDraft: r.IsDraft, IsPrerelease: r.IsPrerelease, CreatedAt: r.CreatedUnix.AsTime(), diff --git a/services/convert/release_test.go b/services/convert/release_test.go new file mode 100644 index 0000000000..57d2dd415d --- /dev/null +++ b/services/convert/release_test.go @@ -0,0 +1,28 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestRelease_ToRelease(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + release1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 1}) + release1.LoadAttributes(db.DefaultContext) + + apiRelease := ToAPIRelease(db.DefaultContext, repo1, release1) + assert.NotNil(t, apiRelease) + assert.EqualValues(t, 1, apiRelease.ID) + assert.EqualValues(t, "https://try.gitea.io/api/v1/repos/user2/repo1/releases/1", apiRelease.URL) + assert.EqualValues(t, "https://try.gitea.io/api/v1/repos/user2/repo1/releases/1/assets", apiRelease.UploadURL) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 7cb9bac95d..c050dc178e 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -20893,6 +20893,10 @@ "type": "string", "x-go-name": "Target" }, + "upload_url": { + "type": "string", + "x-go-name": "UploadURL" + }, "url": { "type": "string", "x-go-name": "URL" diff --git a/tests/integration/api_releases_test.go b/tests/integration/api_releases_test.go index 7f43939083..526842d5ac 100644 --- a/tests/integration/api_releases_test.go +++ b/tests/integration/api_releases_test.go @@ -4,9 +4,13 @@ package integration import ( + "bytes" "fmt" + "io" + "mime/multipart" "net/http" "net/url" + "strings" "testing" auth_model "code.gitea.io/gitea/models/auth" @@ -38,12 +42,15 @@ func TestAPIListReleases(t *testing.T) { case 1: assert.False(t, release.IsDraft) assert.False(t, release.IsPrerelease) + assert.True(t, strings.HasSuffix(release.UploadURL, "/api/v1/repos/user2/repo1/releases/1/assets"), release.UploadURL) case 4: assert.True(t, release.IsDraft) assert.False(t, release.IsPrerelease) + assert.True(t, strings.HasSuffix(release.UploadURL, "/api/v1/repos/user2/repo1/releases/4/assets"), release.UploadURL) case 5: assert.False(t, release.IsDraft) assert.True(t, release.IsPrerelease) + assert.True(t, strings.HasSuffix(release.UploadURL, "/api/v1/repos/user2/repo1/releases/5/assets"), release.UploadURL) default: assert.NoError(t, fmt.Errorf("unexpected release: %v", release)) } @@ -248,3 +255,36 @@ func TestAPIDeleteReleaseByTagName(t *testing.T) { req = NewRequestf(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/tags/release-tag?token=%s", owner.Name, repo.Name, token)) _ = MakeRequest(t, req, http.StatusNoContent) } + +func TestAPIUploadAssetRelease(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + session := loginUser(t, owner.LowerName) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + r := createNewReleaseUsingAPI(t, session, token, owner, repo, "release-tag", "", "Release Tag", "test") + + filename := "image.png" + buff := generateImg() + body := &bytes.Buffer{} + + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("attachment", filename) + assert.NoError(t, err) + _, err = io.Copy(part, &buff) + assert.NoError(t, err) + err = writer.Close() + assert.NoError(t, err) + + req := NewRequestWithBody(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets?name=test-asset&token=%s", owner.Name, repo.Name, r.ID, token), body) + req.Header.Add("Content-Type", writer.FormDataContentType()) + resp := MakeRequest(t, req, http.StatusCreated) + + var attachment *api.Attachment + DecodeJSON(t, resp, &attachment) + + assert.EqualValues(t, "test-asset", attachment.Name) + assert.EqualValues(t, 104, attachment.Size) +}