mirror of
https://github.com/go-gitea/gitea.git
synced 2025-12-14 10:03:59 +01:00
some improvements
This commit is contained in:
parent
75fd8b5b3e
commit
89af9e2f01
82
modules/renderplugin/manifest_test.go
Normal file
82
modules/renderplugin/manifest_test.go
Normal 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)
|
||||||
|
}
|
||||||
@ -241,10 +241,16 @@ func replacePluginFiles(identifier, srcDir string) error {
|
|||||||
func deletePluginFiles(identifier string) error {
|
func deletePluginFiles(identifier string) error {
|
||||||
store := renderplugin.Storage()
|
store := renderplugin.Storage()
|
||||||
prefix := renderplugin.ObjectPrefix(identifier)
|
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()
|
_ = obj.Close()
|
||||||
return store.Delete(path)
|
return store.Delete(path)
|
||||||
})
|
}); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadPluginDir(identifier, src string) error {
|
func uploadPluginDir(identifier, src string) error {
|
||||||
|
|||||||
188
tests/integration/render_plugin_test.go
Normal file
188
tests/integration/render_plugin_test.go
Normal 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
|
||||||
|
}
|
||||||
26
web_src/js/features/file-view.test.ts
Normal file
26
web_src/js/features/file-view.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -11,7 +11,7 @@ const plugins: FileRenderPlugin[] = [];
|
|||||||
let pluginsInitialized = false;
|
let pluginsInitialized = false;
|
||||||
let pluginsInitPromise: Promise<void> | null = null;
|
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;
|
if (!value) return null;
|
||||||
try {
|
try {
|
||||||
const binary = window.atob(value);
|
const binary = window.atob(value);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user