Skip to content

Commit b725472

Browse files
committed
test: add e2e tests for backend upgrade API
- Test GET /api/backends/upgrades returns 200 (even with no upgrade checker) - Test POST /api/backends/upgrade/:name accepts request and returns job ID - Test full upgrade flow: trigger upgrade via API, wait for job completion, verify run.sh updated to v2 and metadata.json has version 2.0.0 - Test POST /api/backends/upgrades/check returns 200 - Fix nil check for applicationInstance in upgrade API routes
1 parent ca24956 commit b725472

2 files changed

Lines changed: 158 additions & 2 deletions

File tree

core/http/routes/ui_api.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,14 +1197,14 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
11971197

11981198
// Backend upgrade APIs
11991199
app.GET("/api/backends/upgrades", func(c echo.Context) error {
1200-
if applicationInstance.UpgradeChecker() == nil {
1200+
if applicationInstance == nil || applicationInstance.UpgradeChecker() == nil {
12011201
return c.JSON(200, map[string]any{})
12021202
}
12031203
return c.JSON(200, applicationInstance.UpgradeChecker().GetAvailableUpgrades())
12041204
}, adminMiddleware)
12051205

12061206
app.POST("/api/backends/upgrades/check", func(c echo.Context) error {
1207-
if applicationInstance.UpgradeChecker() == nil {
1207+
if applicationInstance == nil || applicationInstance.UpgradeChecker() == nil {
12081208
return c.JSON(200, map[string]any{})
12091209
}
12101210
applicationInstance.UpgradeChecker().TriggerCheck()

core/http/routes/ui_api_backends_test.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/mudler/LocalAI/pkg/system"
2121
. "github.com/onsi/ginkgo/v2"
2222
. "github.com/onsi/gomega"
23+
"gopkg.in/yaml.v3"
2324
)
2425

2526
func TestRoutes(t *testing.T) {
@@ -176,6 +177,161 @@ var _ = Describe("Backend API Routes", func() {
176177
Expect(response["processed"]).To(Equal(false))
177178
})
178179
})
180+
181+
Describe("Backend upgrade API", func() {
182+
var (
183+
galleryFile string
184+
upgradeApp *echo.Echo
185+
upgradeGallerySvc *galleryop.GalleryService
186+
)
187+
188+
BeforeEach(func() {
189+
// Place gallery file inside backends dir so it passes trusted root checks
190+
galleryFile = filepath.Join(systemState.Backend.BackendsPath, "test-gallery.yaml")
191+
192+
// Create a fake "v1" backend on disk (simulates a previously installed backend)
193+
backendDir := filepath.Join(systemState.Backend.BackendsPath, "test-upgrade-backend")
194+
err := os.MkdirAll(backendDir, 0750)
195+
Expect(err).NotTo(HaveOccurred())
196+
err = os.WriteFile(filepath.Join(backendDir, "run.sh"), []byte("#!/bin/sh\necho v1"), 0755)
197+
Expect(err).NotTo(HaveOccurred())
198+
199+
// Write metadata.json for the installed backend (v1)
200+
metadata := map[string]string{
201+
"name": "test-upgrade-backend",
202+
"version": "1.0.0",
203+
"installed_at": "2024-01-01T00:00:00Z",
204+
}
205+
metadataBytes, err := json.MarshalIndent(metadata, "", " ")
206+
Expect(err).NotTo(HaveOccurred())
207+
err = os.WriteFile(filepath.Join(backendDir, "metadata.json"), metadataBytes, 0644)
208+
Expect(err).NotTo(HaveOccurred())
209+
210+
// Create a "v2" source directory (the upgrade target)
211+
// Must be inside backends path to pass trusted root checks
212+
v2SrcDir := filepath.Join(systemState.Backend.BackendsPath, "v2-backend-src")
213+
err = os.MkdirAll(v2SrcDir, 0750)
214+
Expect(err).NotTo(HaveOccurred())
215+
err = os.WriteFile(filepath.Join(v2SrcDir, "run.sh"), []byte("#!/bin/sh\necho v2"), 0755)
216+
Expect(err).NotTo(HaveOccurred())
217+
218+
// Write gallery YAML pointing to v2
219+
galleryData := []map[string]any{
220+
{
221+
"name": "test-upgrade-backend",
222+
"uri": v2SrcDir,
223+
"version": "2.0.0",
224+
},
225+
}
226+
yamlBytes, err := yaml.Marshal(galleryData)
227+
Expect(err).NotTo(HaveOccurred())
228+
err = os.WriteFile(galleryFile, yamlBytes, 0644)
229+
Expect(err).NotTo(HaveOccurred())
230+
231+
// Configure the gallery in appConfig BEFORE creating the gallery service
232+
// so the backend manager captures the correct galleries
233+
appConfig.BackendGalleries = []config.Gallery{
234+
{Name: "test", URL: "file://" + galleryFile},
235+
}
236+
237+
// Create a fresh gallery service with the upgrade gallery configured
238+
upgradeGallerySvc = galleryop.NewGalleryService(appConfig, modelLoader)
239+
err = upgradeGallerySvc.Start(context.Background(), configLoader, systemState)
240+
Expect(err).NotTo(HaveOccurred())
241+
242+
// Register routes with the upgrade-aware gallery service
243+
upgradeApp = echo.New()
244+
opcache := galleryop.NewOpCache(upgradeGallerySvc)
245+
noopMw := func(next echo.HandlerFunc) echo.HandlerFunc { return next }
246+
routes.RegisterUIAPIRoutes(upgradeApp, configLoader, modelLoader, appConfig, upgradeGallerySvc, opcache, nil, noopMw)
247+
})
248+
249+
Describe("GET /api/backends/upgrades", func() {
250+
It("should return available upgrades", func() {
251+
req := httptest.NewRequest(http.MethodGet, "/api/backends/upgrades", nil)
252+
rec := httptest.NewRecorder()
253+
254+
upgradeApp.ServeHTTP(rec, req)
255+
256+
Expect(rec.Code).To(Equal(http.StatusOK))
257+
258+
var response map[string]any
259+
err := json.Unmarshal(rec.Body.Bytes(), &response)
260+
Expect(err).NotTo(HaveOccurred())
261+
// Response is empty (upgrade checker not running in test),
262+
// but the endpoint should not error
263+
})
264+
})
265+
266+
Describe("POST /api/backends/upgrade/:name", func() {
267+
It("should accept upgrade request and return job ID", func() {
268+
req := httptest.NewRequest(http.MethodPost, "/api/backends/upgrade/test-upgrade-backend", nil)
269+
req.Header.Set("Content-Type", "application/json")
270+
rec := httptest.NewRecorder()
271+
272+
upgradeApp.ServeHTTP(rec, req)
273+
274+
Expect(rec.Code).To(Equal(http.StatusOK))
275+
276+
var response map[string]any
277+
err := json.Unmarshal(rec.Body.Bytes(), &response)
278+
Expect(err).NotTo(HaveOccurred())
279+
Expect(response["uuid"]).NotTo(BeEmpty())
280+
Expect(response["statusUrl"]).NotTo(BeEmpty())
281+
})
282+
283+
It("should upgrade the backend and update metadata", func() {
284+
req := httptest.NewRequest(http.MethodPost, "/api/backends/upgrade/test-upgrade-backend", nil)
285+
req.Header.Set("Content-Type", "application/json")
286+
rec := httptest.NewRecorder()
287+
288+
upgradeApp.ServeHTTP(rec, req)
289+
Expect(rec.Code).To(Equal(http.StatusOK))
290+
291+
var response map[string]any
292+
err := json.Unmarshal(rec.Body.Bytes(), &response)
293+
Expect(err).NotTo(HaveOccurred())
294+
jobID := response["uuid"].(string)
295+
296+
// Wait for the upgrade job to complete
297+
Eventually(func() bool {
298+
jobReq := httptest.NewRequest(http.MethodGet, "/api/backends/job/"+jobID, nil)
299+
jobRec := httptest.NewRecorder()
300+
upgradeApp.ServeHTTP(jobRec, jobReq)
301+
302+
var jobResp map[string]any
303+
json.Unmarshal(jobRec.Body.Bytes(), &jobResp)
304+
305+
processed, _ := jobResp["processed"].(bool)
306+
return processed
307+
}, "10s", "200ms").Should(BeTrue())
308+
309+
// Verify the backend was upgraded: run.sh should now contain "v2"
310+
runContent, err := os.ReadFile(filepath.Join(
311+
systemState.Backend.BackendsPath, "test-upgrade-backend", "run.sh"))
312+
Expect(err).NotTo(HaveOccurred())
313+
Expect(string(runContent)).To(ContainSubstring("v2"))
314+
315+
// Verify metadata was updated with new version
316+
metadataContent, err := os.ReadFile(filepath.Join(
317+
systemState.Backend.BackendsPath, "test-upgrade-backend", "metadata.json"))
318+
Expect(err).NotTo(HaveOccurred())
319+
Expect(string(metadataContent)).To(ContainSubstring(`"version": "2.0.0"`))
320+
})
321+
})
322+
323+
Describe("POST /api/backends/upgrades/check", func() {
324+
It("should trigger an upgrade check and return 200", func() {
325+
req := httptest.NewRequest(http.MethodPost, "/api/backends/upgrades/check", nil)
326+
req.Header.Set("Content-Type", "application/json")
327+
rec := httptest.NewRecorder()
328+
329+
upgradeApp.ServeHTTP(rec, req)
330+
331+
Expect(rec.Code).To(Equal(http.StatusOK))
332+
})
333+
})
334+
})
179335
})
180336

181337
// Helper function to make POST request

0 commit comments

Comments
 (0)