0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-12-09 04:41:48 +01:00

some improvements

This commit is contained in:
Lunny Xiao 2025-12-06 11:12:41 -08:00
parent 75fd8b5b3e
commit 89af9e2f01
No known key found for this signature in database
GPG Key ID: C3B7C91B632F738A
5 changed files with 305 additions and 3 deletions

View File

@ -0,0 +1,82 @@
package renderplugin
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestManifestNormalizeDefaults(t *testing.T) {
manifest := Manifest{
SchemaVersion: SupportedManifestVersion,
ID: " Example.Plugin ",
Name: " Demo Plugin ",
Version: " 1.0.0 ",
Description: "test",
Entry: "",
FilePatterns: []string{" *.TXT ", "README.md", ""},
}
require.NoError(t, manifest.Normalize())
assert.Equal(t, "example.plugin", manifest.ID)
assert.Equal(t, "render.js", manifest.Entry)
assert.Equal(t, []string{"*.TXT", "README.md"}, manifest.FilePatterns)
}
func TestManifestNormalizeErrors(t *testing.T) {
base := Manifest{
SchemaVersion: SupportedManifestVersion,
ID: "example",
Name: "demo",
Version: "1.0",
Entry: "render.js",
FilePatterns: []string{"*.md"},
}
tests := []struct {
name string
mutate func(m *Manifest)
message string
}{
{"missing schema version", func(m *Manifest) { m.SchemaVersion = 0 }, "schemaVersion is required"},
{"unsupported schema", func(m *Manifest) { m.SchemaVersion = SupportedManifestVersion + 1 }, "not supported"},
{"invalid id", func(m *Manifest) { m.ID = "bad id" }, "manifest id"},
{"missing name", func(m *Manifest) { m.Name = "" }, "name is required"},
{"missing version", func(m *Manifest) { m.Version = "" }, "version is required"},
{"no patterns", func(m *Manifest) { m.FilePatterns = nil }, "at least one file pattern"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
m := base
tt.mutate(&m)
err := m.Normalize()
require.Error(t, err)
assert.Contains(t, err.Error(), tt.message)
})
}
}
func TestLoadManifest(t *testing.T) {
dir := t.TempDir()
manifestJSON := `{
"schemaVersion": 1,
"id": "Example",
"name": "Example",
"version": "2.0.0",
"description": "demo",
"entry": "render.js",
"filePatterns": ["*.txt", "*.md"]
}`
path := filepath.Join(dir, "manifest.json")
require.NoError(t, os.WriteFile(path, []byte(manifestJSON), 0o644))
manifest, err := LoadManifest(dir)
require.NoError(t, err)
assert.Equal(t, "example", manifest.ID)
assert.Equal(t, []string{"*.md", "*.txt"}, manifest.FilePatterns)
}

View File

@ -241,10 +241,16 @@ func replacePluginFiles(identifier, srcDir string) error {
func deletePluginFiles(identifier string) error {
store := renderplugin.Storage()
prefix := renderplugin.ObjectPrefix(identifier)
return store.IterateObjects(prefix, func(path string, obj storage.Object) error {
if err := store.IterateObjects(prefix, func(path string, obj storage.Object) error {
_ = obj.Close()
return store.Delete(path)
})
}); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
return nil
}
func uploadPluginDir(identifier, src string) error {

View File

@ -0,0 +1,188 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"archive/zip"
"bytes"
"fmt"
"mime/multipart"
"net/http"
"path"
"strconv"
"strings"
"testing"
render_model "code.gitea.io/gitea/models/render"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/renderplugin"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/tests"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRenderPluginLifecycle(t *testing.T) {
defer tests.PrepareTestEnv(t)()
require.NoError(t, storage.Clean(renderplugin.Storage()))
t.Cleanup(func() {
_ = storage.Clean(renderplugin.Storage())
})
const pluginID = "itest-plugin"
session := loginUser(t, "user1")
uploadArchive(t, session, "/-/admin/render-plugins/upload", buildRenderPluginArchive(t, pluginID, "Integration Plugin", "1.0.0"))
flash := expectFlashSuccess(t, session)
assert.Contains(t, flash.SuccessMsg, "installed")
row := requireRenderPluginRow(t, session, pluginID)
assert.Equal(t, "1.0.0", row.Version)
assert.False(t, row.Enabled)
postPluginAction(t, session, fmt.Sprintf("/-/admin/render-plugins/%d/enable", row.ID))
flash = expectFlashSuccess(t, session)
assert.Contains(t, flash.SuccessMsg, "enabled")
row = requireRenderPluginRow(t, session, pluginID)
assert.True(t, row.Enabled)
metas := fetchRenderPluginMetadata(t)
require.Len(t, metas, 1)
assert.Equal(t, pluginID, metas[0].ID)
assert.Contains(t, metas[0].EntryURL, "render.js")
MakeRequest(t, NewRequest(t, "GET", metas[0].EntryURL), http.StatusOK)
uploadArchive(t, session, fmt.Sprintf("/-/admin/render-plugins/%d/upgrade", row.ID), buildRenderPluginArchive(t, pluginID, "Integration Plugin", "2.0.0"))
flash = expectFlashSuccess(t, session)
assert.Contains(t, flash.SuccessMsg, "upgraded")
row = requireRenderPluginRow(t, session, pluginID)
assert.Equal(t, "2.0.0", row.Version)
postPluginAction(t, session, fmt.Sprintf("/-/admin/render-plugins/%d/disable", row.ID))
flash = expectFlashSuccess(t, session)
assert.Contains(t, flash.SuccessMsg, "disabled")
row = requireRenderPluginRow(t, session, pluginID)
assert.False(t, row.Enabled)
require.Len(t, fetchRenderPluginMetadata(t), 0)
postPluginAction(t, session, fmt.Sprintf("/-/admin/render-plugins/%d/delete", row.ID))
flash = expectFlashSuccess(t, session)
assert.Contains(t, flash.SuccessMsg, "deleted")
unittest.AssertNotExistsBean(t, &render_model.Plugin{Identifier: pluginID})
_, err := renderplugin.Storage().Stat(renderplugin.ObjectPath(pluginID, "render.js"))
assert.Error(t, err)
require.Nil(t, findRenderPluginRow(t, session, pluginID))
}
func postPluginAction(t *testing.T, session *TestSession, path string) {
req := NewRequestWithValues(t, "POST", path, map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
session.MakeRequest(t, req, http.StatusSeeOther)
}
func uploadArchive(t *testing.T, session *TestSession, path string, archive []byte) {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
require.NoError(t, writer.WriteField("_csrf", GetUserCSRFToken(t, session)))
part, err := writer.CreateFormFile("plugin", "plugin.zip")
require.NoError(t, err)
_, err = part.Write(archive)
require.NoError(t, err)
require.NoError(t, writer.Close())
req := NewRequestWithBody(t, "POST", path, bytes.NewReader(body.Bytes()))
req.Header.Set("Content-Type", writer.FormDataContentType())
session.MakeRequest(t, req, http.StatusSeeOther)
}
func buildRenderPluginArchive(t *testing.T, id, name, version string) []byte {
manifest := fmt.Sprintf(`{
"schemaVersion": 1,
"id": %q,
"name": %q,
"version": %q,
"description": "integration test plugin",
"entry": "render.js",
"filePatterns": ["*.itest"]
}`, id, name, version)
var buf bytes.Buffer
zipWriter := zip.NewWriter(&buf)
file, err := zipWriter.Create("manifest.json")
require.NoError(t, err)
_, err = file.Write([]byte(manifest))
require.NoError(t, err)
file, err = zipWriter.Create("render.js")
require.NoError(t, err)
_, err = file.Write([]byte("export default {render(){}};"))
require.NoError(t, err)
require.NoError(t, zipWriter.Close())
return buf.Bytes()
}
func fetchRenderPluginMetadata(t *testing.T) []renderplugin.Metadata {
resp := MakeRequest(t, NewRequest(t, "GET", "/assets/render-plugins/index.json"), http.StatusOK)
var metas []renderplugin.Metadata
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &metas))
return metas
}
func expectFlashSuccess(t *testing.T, session *TestSession) *middleware.Flash {
flash := session.GetCookieFlashMessage()
require.NotNil(t, flash, "expected flash message")
require.Empty(t, flash.ErrorMsg)
return flash
}
type renderPluginRow struct {
ID int64
Identifier string
Version string
Enabled bool
}
func requireRenderPluginRow(t *testing.T, session *TestSession, identifier string) *renderPluginRow {
row := findRenderPluginRow(t, session, identifier)
require.NotNil(t, row, "plugin %s not found", identifier)
return row
}
func findRenderPluginRow(t *testing.T, session *TestSession, identifier string) *renderPluginRow {
resp := session.MakeRequest(t, NewRequest(t, "GET", "/-/admin/render-plugins"), http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
var result *renderPluginRow
doc.Find("table tbody tr").EachWithBreak(func(_ int, s *goquery.Selection) bool {
cols := s.Find("td")
if cols.Length() < 6 {
return true
}
idText := strings.TrimSpace(cols.Eq(1).Text())
if idText != identifier {
return true
}
link := cols.Eq(5).Find("a[href]").First()
href, _ := link.Attr("href")
id, err := strconv.ParseInt(path.Base(href), 10, 64)
if err != nil {
return true
}
version := strings.TrimSpace(cols.Eq(2).Text())
enabled := cols.Eq(4).Find(".ui.green").Length() > 0
result = &renderPluginRow{
ID: id,
Identifier: idText,
Version: version,
Enabled: enabled,
}
return false
})
return result
}

View File

@ -0,0 +1,26 @@
import {Buffer} from 'node:buffer';
import {describe, expect, it, vi} from 'vitest';
import {decodeHeadChunk} from './file-view.ts';
describe('decodeHeadChunk', () => {
it('returns null when input is empty', () => {
expect(decodeHeadChunk(null)).toBeNull();
expect(decodeHeadChunk('')).toBeNull();
});
it('decodes base64 content into a Uint8Array', () => {
const data = 'Gitea Render Plugin';
const encoded = Buffer.from(data, 'utf-8').toString('base64');
const decoded = decodeHeadChunk(encoded);
expect(decoded).not.toBeNull();
expect(new TextDecoder().decode(decoded!)).toBe(data);
});
it('logs and returns null for invalid input', () => {
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
const result = decodeHeadChunk('%invalid-base64%');
expect(result).toBeNull();
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
});

View File

@ -11,7 +11,7 @@ const plugins: FileRenderPlugin[] = [];
let pluginsInitialized = false;
let pluginsInitPromise: Promise<void> | null = null;
function decodeHeadChunk(value: string | null): Uint8Array | null {
export function decodeHeadChunk(value: string | null): Uint8Array | null {
if (!value) return null;
try {
const binary = window.atob(value);