refactor: rework gitea client and add tests (#44)

This commit is contained in:
Robert Kaussow 2024-05-08 12:54:51 +02:00 committed by GitHub
parent 8b057ea06b
commit a196e5f4ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 471 additions and 91 deletions

View File

@ -23,7 +23,6 @@ linters:
- errchkjson
- errname
- errorlint
- execinquery
- exhaustive
- exportloopref
- forcetypeassert
@ -37,12 +36,12 @@ linters:
- gocyclo
- godot
- godox
- goerr113
- err113
- gofmt
- gofumpt
- goheader
- goimports
- gomnd
- mnd
- gomoddirectives
- gomodguard
- goprintffuncname
@ -95,3 +94,9 @@ run:
linters-settings:
gofumpt:
extra-rules: true
issues:
exclude-rules:
- path: "_test.go"
linters:
- err113

View File

@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"path/filepath"
"strings"
@ -70,19 +69,17 @@ func (p *Plugin) Validate() error {
// Execute provides the implementation of the plugin.
func (p *Plugin) Execute() error {
httpClient := &http.Client{}
client, err := gitea.NewClient(
gitea, err := gitea.NewClient(
p.Settings.baseURL.String(),
gitea.SetToken(p.Settings.APIKey),
gitea.SetHTTPClient(httpClient),
gitea.SetHTTPClient(p.Network.Client),
)
if err != nil {
return err
}
r := &Release{
Client: client,
client := NewGiteaClient(gitea)
client.Release.Opt = GiteaReleaseOpt{
Owner: p.Metadata.Repository.Owner,
Repo: p.Metadata.Repository.Name,
Tag: strings.TrimPrefix(p.Settings.CommitRef, "refs/tags/"),
@ -93,12 +90,20 @@ func (p *Plugin) Execute() error {
Note: p.Settings.Note,
}
release, err := r.buildRelease()
if err != nil {
return fmt.Errorf("failed to create the release: %w", err)
release, err := client.Release.Find()
if err != nil && !errors.Is(err, ErrReleaseNotFound) {
return fmt.Errorf("failed to retrieve release: %w", err)
}
if err := r.uploadFiles(release.ID, p.Settings.files); err != nil {
// If no release was found by that tag, create a new one.
if release == nil {
release, err = client.Release.Create()
if err != nil {
return fmt.Errorf("failed to create release: %w", err)
}
}
if err := client.Release.AddAttachments(release.ID, p.Settings.files); err != nil {
return fmt.Errorf("failed to upload the files: %w", err)
}

View File

@ -15,9 +15,17 @@ var (
ErrFileExists = errors.New("asset file already exist")
)
// Release represents a release for a Gitea repository.
type Release struct {
*gitea.Client
type GiteaClient struct {
client *gitea.Client
Release *GiteaRelease
}
type GiteaRelease struct {
client *gitea.Client
Opt GiteaReleaseOpt
}
type GiteaReleaseOpt struct {
Owner string
Repo string
Tag string
@ -28,65 +36,66 @@ type Release struct {
Note string
}
// buildRelease attempts to retrieve an existing release by the specified tag name.
func (rc *Release) buildRelease() (*gitea.Release, error) {
// first attempt to get a release by that tag
release, err := rc.getRelease()
type FileExists string
if err != nil && release == nil {
fmt.Println(err)
} else if release != nil {
return release, nil
const (
FileExistsOverwrite FileExists = "overwrite"
FileExistsFail FileExists = "fail"
FileExistsSkip FileExists = "skip"
)
// NewGiteaClient creates a new GiteaClient instance with the provided Gitea client.
func NewGiteaClient(client *gitea.Client) *GiteaClient {
return &GiteaClient{
client: client,
Release: &GiteaRelease{
client: client,
Opt: GiteaReleaseOpt{},
},
}
// if no release was found by that tag, create a new one
release, err = rc.newRelease()
if err != nil {
return nil, fmt.Errorf("failed to retrieve or create a release: %w", err)
}
return release, nil
}
// getRelease retrieves the release with the specified tag name from the repository.
func (rc *Release) getRelease() (*gitea.Release, error) {
releases, _, err := rc.Client.ListReleases(rc.Owner, rc.Repo, gitea.ListReleasesOptions{})
// Find retrieves the release with the specified tag name from the repository.
// If the release is not found, it returns an ErrReleaseNotFound error.
func (r *GiteaRelease) Find() (*gitea.Release, error) {
releases, _, err := r.client.ListReleases(r.Opt.Owner, r.Opt.Repo, gitea.ListReleasesOptions{})
if err != nil {
return nil, err
}
for _, release := range releases {
if release.TagName == rc.Tag {
log.Info().Msgf("successfully retrieved %s release", rc.Tag)
if release.TagName == r.Opt.Tag {
log.Info().Msgf("found release: %s", r.Opt.Tag)
return release, nil
}
}
return nil, fmt.Errorf("%w: %s", ErrReleaseNotFound, rc.Tag)
return nil, fmt.Errorf("%w: %s", ErrReleaseNotFound, r.Opt.Tag)
}
// newRelease creates a new release on the repository with the specified options.
func (rc *Release) newRelease() (*gitea.Release, error) {
r := gitea.CreateReleaseOption{
TagName: rc.Tag,
IsDraft: rc.Draft,
IsPrerelease: rc.Prerelease,
Title: rc.Title,
Note: rc.Note,
// Create creates a new release on the Gitea repository with the specified options.
// It returns the created release or an error if the creation failed.
func (r *GiteaRelease) Create() (*gitea.Release, error) {
opts := gitea.CreateReleaseOption{
TagName: r.Opt.Tag,
IsDraft: r.Opt.Draft,
IsPrerelease: r.Opt.Prerelease,
Title: r.Opt.Title,
Note: r.Opt.Note,
}
release, _, err := rc.Client.CreateRelease(rc.Owner, rc.Repo, r)
release, _, err := r.client.CreateRelease(r.Opt.Owner, r.Opt.Repo, opts)
if err != nil {
return nil, fmt.Errorf("failed to create release: %w", err)
}
log.Info().Msgf("successfully created %s release", rc.Tag)
log.Info().Msgf("created release: %s", r.Opt.Tag)
return release, nil
}
// uploadFiles uploads the specified files as attachments to the release with the given ID.
// AddAttachments uploads the specified files as attachments to the release with the given ID.
// It first checks for any existing attachments with the same names,
// and handles them according to the FileExists option:
//
@ -95,61 +104,66 @@ func (rc *Release) newRelease() (*gitea.Release, error) {
// - "skip": skips uploading the file and logs a warning
//
// If there are no conflicts, it uploads the new files as attachments to the release.
func (rc *Release) uploadFiles(releaseID int64, files []string) error {
attachments, _, err := rc.Client.ListReleaseAttachments(
rc.Owner,
rc.Repo,
func (r *GiteaRelease) AddAttachments(releaseID int64, files []string) error {
attachments, _, err := r.client.ListReleaseAttachments(
r.Opt.Owner,
r.Opt.Repo,
releaseID,
gitea.ListReleaseAttachmentsOptions{},
)
if err != nil {
return fmt.Errorf("failed to fetch existing assets: %w", err)
return fmt.Errorf("failed to fetch attachments: %w", err)
}
var uploadFiles []string
existingAttachments := make(map[string]bool)
attachmentsMap := make(map[string]*gitea.Attachment)
for _, attachment := range attachments {
attachmentsMap[attachment.Name] = attachment
existingAttachments[attachment.Name] = true
}
files:
for _, file := range files {
for _, attachment := range attachments {
if attachment.Name == path.Base(file) {
switch rc.FileExists {
case "overwrite":
// do nothing
case "fail":
return fmt.Errorf("%w: %s", ErrFileExists, path.Base(file))
case "skip":
log.Warn().Msgf("skipping pre-existing %s artifact", attachment.Name)
continue files
fileName := path.Base(file)
if existingAttachments[fileName] {
switch FileExists(r.Opt.FileExists) {
case FileExistsOverwrite:
_, err := r.client.DeleteReleaseAttachment(r.Opt.Owner, r.Opt.Repo, releaseID, attachmentsMap[fileName].ID)
if err != nil {
return fmt.Errorf("failed to delete artifact: %s: %w", fileName, err)
}
log.Info().Msgf("deleted artifact: %s", fileName)
case FileExistsFail:
return fmt.Errorf("%w: %s", ErrFileExists, fileName)
case FileExistsSkip:
log.Warn().Msgf("skip existing artifact: %s", fileName)
continue
}
}
uploadFiles = append(uploadFiles, file)
}
for _, file := range uploadFiles {
handle, err := os.Open(file)
if err != nil {
return fmt.Errorf("failed to read %s artifact: %w", file, err)
if err := r.uploadFile(releaseID, file); err != nil {
return err
}
for _, attachment := range attachments {
if attachment.Name == path.Base(file) {
if _, err := rc.Client.DeleteReleaseAttachment(rc.Owner, rc.Repo, releaseID, attachment.ID); err != nil {
return fmt.Errorf("failed to delete %s artifact: %w", file, err)
}
log.Info().Msgf("successfully deleted old %s artifact", attachment.Name)
}
}
if _, _, err = rc.Client.CreateReleaseAttachment(rc.Owner, rc.Repo, releaseID, handle, path.Base(file)); err != nil {
return fmt.Errorf("failed to upload %s artifact: %w", file, err)
}
log.Info().Msgf("successfully uploaded %s artifact", file)
}
return nil
}
func (r *GiteaRelease) uploadFile(releaseID int64, file string) error {
handle, err := os.Open(file)
if err != nil {
return fmt.Errorf("failed to read artifact: %s: %w", file, err)
}
defer handle.Close()
_, _, err = r.client.CreateReleaseAttachment(r.Opt.Owner, r.Opt.Repo, releaseID, handle, path.Base(file))
if err != nil {
return fmt.Errorf("failed to upload artifact: %s: %w", file, err)
}
log.Info().Msgf("uploaded artifact: %s", path.Base(file))
return nil
}

356
plugin/release_test.go Normal file
View File

@ -0,0 +1,356 @@
package plugin
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/rs/zerolog/log"
"code.gitea.io/sdk/gitea"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
)
func giteaMockHandler(t *testing.T, opt GiteaReleaseOpt) func(http.ResponseWriter, *http.Request) {
t.Helper()
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Println(r.RequestURI)
switch r.RequestURI {
case "/api/v1/version":
_, err := io.WriteString(w, `{"version":"1.21.0"}`)
if err != nil {
t.Fail()
}
case "/api/v1/repos/test-owner/test-repo/releases?limit=0&page=1":
_, err := io.WriteString(w, `[{
"id": 1,
"tag_name": "v1.0.0",
"name": "Release v1.0.0",
"body": "This is the release notes for v1.0.0",
"draft": false,
"prerelease": false,
"created_at": "2023-05-01T12:00:00Z",
"published_at": "2023-05-01T12:30:00Z"
}]`)
if err != nil {
t.Fail()
}
case "/api/v1/repos/test-owner/test-repo/releases":
_, err := io.WriteString(w, fmt.Sprintf(`{
"id": 1,
"tag_name": "%s",
"name": "Release %s",
"body": "This is the release notes for %s",
"draft": %t,
"prerelease": %t,
"created_at": "2023-05-01T12:00:00Z",
"published_at": "2023-05-01T12:30:00Z"
}`, opt.Tag, opt.Tag, opt.Tag, opt.Draft, opt.Prerelease))
if err != nil {
t.Fail()
}
case "/api/v1/repos/test-owner/test-repo/releases/1/assets?limit=0&page=1":
_, err := io.WriteString(w, `[{
"id": 1,
"name": "file1.txt",
"size": 1024,
"created_at": "2023-05-01T12:30:00Z"
}]`)
if err != nil {
t.Fail()
}
case "/api/v1/repos/test-owner/test-repo/releases/1/assets":
_, err := io.WriteString(w, `{
"id": 1,
"name": "file1.txt",
"size": 1024,
"created_at": "2023-05-01T12:30:00Z"
}`)
if err != nil {
t.Fail()
}
}
}
}
func TestGiteaReleaseFind(t *testing.T) {
tests := []struct {
name string
opt GiteaReleaseOpt
want *gitea.Release
wantErr error
}{
{
name: "find release by tag",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v1.0.0",
},
want: &gitea.Release{
TagName: "v1.0.0",
},
},
{
name: "release not found",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v1.1.0",
},
want: nil,
wantErr: ErrReleaseNotFound,
},
}
for _, tt := range tests {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
giteaMockHandler(t, tt.opt)(w, r)
}))
defer ts.Close()
g, _ := gitea.NewClient(ts.URL)
client := NewGiteaClient(g)
t.Run(tt.name, func(t *testing.T) {
client.Release.Opt = tt.opt
release, err := client.Release.Find()
if tt.want == nil {
assert.Error(t, err)
assert.Nil(t, release)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want.TagName, release.TagName)
})
}
}
func TestGiteaReleaseCreate(t *testing.T) {
tests := []struct {
name string
opt GiteaReleaseOpt
want *gitea.Release
wantErr error
}{
{
name: "create release",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v1.1.0",
Title: "Release v1.1.0",
Note: "This is the release notes for v1.1.0",
Draft: false,
Prerelease: false,
},
want: &gitea.Release{
TagName: "v1.1.0",
Title: "Release v1.1.0",
Note: "This is the release notes for v1.1.0",
IsDraft: false,
IsPrerelease: false,
},
},
{
name: "create draft release",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v1.2.0",
Title: "Release v1.2.0",
Note: "This is the release notes for v1.2.0",
Draft: true,
Prerelease: false,
},
want: &gitea.Release{
TagName: "v1.2.0",
Title: "Release v1.2.0",
Note: "This is the release notes for v1.2.0",
IsDraft: true,
IsPrerelease: false,
},
},
{
name: "create prerelease",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v1.3.0-rc1",
Title: "Release v1.3.0-rc1",
Note: "This is the release notes for v1.3.0-rc1",
Draft: false,
Prerelease: true,
},
want: &gitea.Release{
TagName: "v1.3.0-rc1",
Title: "Release v1.3.0-rc1",
Note: "This is the release notes for v1.3.0-rc1",
IsDraft: false,
IsPrerelease: true,
},
},
}
for _, tt := range tests {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
giteaMockHandler(t, tt.opt)(w, r)
}))
defer ts.Close()
g, _ := gitea.NewClient(ts.URL)
client := NewGiteaClient(g)
t.Run(tt.name, func(t *testing.T) {
client.Release.Opt = tt.opt
release, err := client.Release.Create()
if tt.wantErr != nil {
assert.Error(t, err)
assert.Nil(t, release)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want.TagName, release.TagName)
assert.Equal(t, tt.want.Title, release.Title)
assert.Equal(t, tt.want.Note, release.Note)
assert.Equal(t, tt.want.IsDraft, release.IsDraft)
assert.Equal(t, tt.want.IsPrerelease, release.IsPrerelease)
})
}
}
func TestGiteaReleaseAddAttachments(t *testing.T) {
logBuffer := &bytes.Buffer{}
logger := zerolog.New(logBuffer)
log.Logger = logger
tests := []struct {
name string
opt GiteaReleaseOpt
files []string
fileExists string
wantErr error
wantLogs []string
}{
{
name: "add new attachments",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v2.0.0",
Title: "Release v2.0.0",
FileExists: "overwrite",
},
files: []string{createTempFile(t, "file1.txt"), createTempFile(t, "file2.txt")},
wantLogs: []string{"uploaded artifact: file1.txt", "uploaded artifact: file2.txt"},
},
{
name: "fail on existing attachments",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v2.0.0",
Title: "Release v2.0.0",
FileExists: "fail",
},
files: []string{createTempFile(t, "file1.txt"), createTempFile(t, "file2.txt")},
wantErr: ErrFileExists,
},
{
name: "overwrite on existing attachments",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v2.0.0",
Title: "Release v2.0.0",
FileExists: "overwrite",
},
files: []string{createTempFile(t, "file1.txt"), createTempFile(t, "file2.txt")},
wantErr: nil,
wantLogs: []string{"deleted artifact: file1.txt", "uploaded artifact: file1.txt"},
},
{
name: "skip on existing attachments",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v2.0.0",
Title: "Release v2.0.0",
FileExists: "skip",
},
files: []string{createTempFile(t, "file1.txt"), createTempFile(t, "file2.txt")},
wantErr: nil,
wantLogs: []string{"skip existing artifact: file1"},
},
{
name: "fail on invalid file",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v2.0.0",
Title: "Release v2.0.0",
FileExists: "overwrite",
},
files: []string{"testdata/file1.txt", "testdata/invalid.txt"},
wantErr: errors.New("no such file or directory"),
},
}
for _, tt := range tests {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
giteaMockHandler(t, tt.opt)(w, r)
}))
defer ts.Close()
logBuffer.Reset()
g, _ := gitea.NewClient(ts.URL)
client := NewGiteaClient(g)
t.Run(tt.name, func(t *testing.T) {
client.Release.Opt = tt.opt
release, _ := client.Release.Create()
err := client.Release.AddAttachments(release.ID, tt.files)
// Assert log output.
for _, l := range tt.wantLogs {
assert.Contains(t, logBuffer.String(), l)
}
if tt.wantErr != nil {
assert.Error(t, err)
assert.ErrorContains(t, err, tt.wantErr.Error())
return
}
assert.NoError(t, err)
})
}
}
func createTempFile(t *testing.T, name string) string {
t.Helper()
name = filepath.Join(t.TempDir(), name)
_ = os.WriteFile(name, []byte("hello"), 0o600)
return name
}