From 59abe1bd84aab6072bca32ace5df0cbf479f1fae Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 30 Mar 2026 22:25:04 +0200 Subject: [PATCH] test(plugins): add yaegi plugin integration tests - Smoke test: verify yaegi can load stdlib and vikunja symbols - Loader test: load example plugin and verify Name/Version - Routes test: verify plugin HTTP routes serve responses - Events test: verify plugin event listener registration via Init() --- pkg/plugins/yaegi/events_test.go | 48 ++++++++++++++++++ pkg/plugins/yaegi/loader_test.go | 70 ++++++++++++++++++++++++++ pkg/plugins/yaegi/routes_test.go | 63 +++++++++++++++++++++++ pkg/yaegi_symbols/stdlib_check_test.go | 26 ++++++++++ 4 files changed, 207 insertions(+) create mode 100644 pkg/plugins/yaegi/events_test.go create mode 100644 pkg/plugins/yaegi/loader_test.go create mode 100644 pkg/plugins/yaegi/routes_test.go create mode 100644 pkg/yaegi_symbols/stdlib_check_test.go diff --git a/pkg/plugins/yaegi/events_test.go b/pkg/plugins/yaegi/events_test.go new file mode 100644 index 000000000..97ab4fff6 --- /dev/null +++ b/pkg/plugins/yaegi/events_test.go @@ -0,0 +1,48 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package yaegi + +import ( + "testing" + + "code.vikunja.io/api/pkg/log" +) + +func TestPluginEventListener(t *testing.T) { + log.InitLogger() + + loaded, err := LoadPluginFull(examplePluginDir) + if err != nil { + t.Fatalf("LoadPluginFull failed: %v", err) + } + + // Call Init() — this registers the TestListener for TaskCreatedEvent. + // If the Listener interface boundary is broken, this will panic with a + // reflection error when calling events.RegisterListener. + err = loaded.Plugin.Init() + if err != nil { + t.Fatalf("plugin Init failed: %v", err) + } + t.Log("Init() succeeded — events.RegisterListener accepted the interpreted Listener") + + // Verify Shutdown works too + err = loaded.Plugin.Shutdown() + if err != nil { + t.Fatalf("plugin Shutdown failed: %v", err) + } + t.Log("Shutdown() succeeded") +} diff --git a/pkg/plugins/yaegi/loader_test.go b/pkg/plugins/yaegi/loader_test.go new file mode 100644 index 000000000..c2bbde1a8 --- /dev/null +++ b/pkg/plugins/yaegi/loader_test.go @@ -0,0 +1,70 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package yaegi + +import ( + "os" + "path/filepath" + "testing" +) + +const examplePluginDir = "../../../examples/plugins/example" + +func TestLoadPlugin(t *testing.T) { + pluginDir := examplePluginDir + + mainGo := filepath.Join(pluginDir, "main.go") + if _, err := os.Stat(mainGo); err != nil { + t.Fatalf("example plugin source not found at %s: %v", mainGo, err) + } + + p, err := LoadPlugin(pluginDir) + if err != nil { + t.Fatalf("LoadPlugin failed: %v", err) + } + + if p.Name() != "example" { + t.Errorf("expected plugin name 'example', got %q", p.Name()) + } + if p.Version() != "1.0.0" { + t.Errorf("expected version '1.0.0', got %q", p.Version()) + } +} + +func TestLoadPluginFull(t *testing.T) { + loaded, err := LoadPluginFull(examplePluginDir) + if err != nil { + t.Fatalf("LoadPluginFull failed: %v", err) + } + + if loaded.Plugin == nil { + t.Fatal("Plugin is nil") + } + if loaded.Plugin.Name() != "example" { + t.Errorf("expected plugin name 'example', got %q", loaded.Plugin.Name()) + } + + if loaded.AuthRouter == nil { + t.Fatal("AuthRouter is nil — typed factory NewAuthenticatedRouterPlugin not found") + } + t.Logf("AuthRouter type: %T, name: %s", loaded.AuthRouter, loaded.AuthRouter.Name()) + + if loaded.UnauthRouter == nil { + t.Fatal("UnauthRouter is nil — typed factory NewUnauthenticatedRouterPlugin not found") + } + t.Logf("UnauthRouter type: %T, name: %s", loaded.UnauthRouter, loaded.UnauthRouter.Name()) +} diff --git a/pkg/plugins/yaegi/routes_test.go b/pkg/plugins/yaegi/routes_test.go new file mode 100644 index 000000000..d765e95dc --- /dev/null +++ b/pkg/plugins/yaegi/routes_test.go @@ -0,0 +1,63 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package yaegi + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "code.vikunja.io/api/pkg/log" + "github.com/labstack/echo/v5" +) + +func TestPluginRoutesServeHTTP(t *testing.T) { + log.InitLogger() + loaded, err := LoadPluginFull(examplePluginDir) + if err != nil { + t.Fatalf("LoadPluginFull failed: %v", err) + } + + if loaded.UnauthRouter == nil { + t.Fatal("UnauthRouter is nil — cannot test route registration") + } + + // Create a real Echo instance and register the plugin's unauthenticated routes + e := echo.New() + g := e.Group("/plugins") + loaded.UnauthRouter.RegisterUnauthenticatedRoutes(g) + + // Make an HTTP request to the plugin's /status endpoint + req := httptest.NewRequest(http.MethodGet, "/plugins/status", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d, body: %s", rec.Code, rec.Body.String()) + } + + body := rec.Body.String() + if !strings.Contains(body, "example") { + t.Errorf("response body should contain plugin name 'example', got: %s", body) + } + if !strings.Contains(body, "ok") { + t.Errorf("response body should contain status 'ok', got: %s", body) + } + + t.Logf("HTTP response: %s", body) +} diff --git a/pkg/yaegi_symbols/stdlib_check_test.go b/pkg/yaegi_symbols/stdlib_check_test.go new file mode 100644 index 000000000..e6b1cfae5 --- /dev/null +++ b/pkg/yaegi_symbols/stdlib_check_test.go @@ -0,0 +1,26 @@ +package yaegi_symbols + +import ( + "testing" + + "github.com/traefik/yaegi/interp" + "github.com/traefik/yaegi/stdlib" +) + +func TestYaegiSmoke(t *testing.T) { + i := interp.New(interp.Options{}) + i.Use(stdlib.Symbols) + + _, err := i.Eval(`import "fmt"`) + if err != nil { + t.Fatalf("import failed: %v", err) + } + + v, err := i.Eval(`fmt.Sprintf("hello %s", "yaegi")`) + if err != nil { + t.Fatalf("eval failed: %v", err) + } + if v.String() != "hello yaegi" { + t.Fatalf("unexpected result: %s", v.String()) + } +}