mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-06-04 07:22:56 +00:00
b17a06fbba
A clean, customizable homelab dashboard inspired by CasaOS. Features: - Empty-first dashboard (no demo data) - 3 themes: Light, Dark, CasaOS glassmorphism - Widgets: Clock (multi-timezone), Pi-hole, Memos, Immich, Image - Drag & drop app organization - Grid + list view for apps - Groups with collapse/expand - Proper widget refresh handling - Visual timezone picker - Square app cards with hover effects Stack: Go + Gin + PostgreSQL + Next.js 15 + React 19 + Tailwind CSS + shadcn/ui
144 lines
3.3 KiB
Go
144 lines
3.3 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"dash/backend/internal/testutil"
|
|
)
|
|
|
|
func TestDashboardLayoutAndGroupDeleteIntegration(t *testing.T) {
|
|
pool := testutil.TestPool(t)
|
|
st := New(pool)
|
|
ctx := context.Background()
|
|
|
|
infra, err := st.CreateGroup(ctx, "Infra")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
media, err := st.CreateGroup(ctx, "Media")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service, err := st.CreateService(ctx, ServiceInput{
|
|
GroupID: &infra.ID,
|
|
Name: "Pi-hole",
|
|
URLs: []ServiceURLInput{
|
|
{Label: "local", Kind: "local", URL: "http://pihole.local", IsPrimary: true},
|
|
{Label: "wan", Kind: "external", URL: "https://pihole.example.com"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dashboard, err := st.Dashboard(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(dashboard.Groups) != 2 || len(dashboard.Groups[0].Services) != 1 {
|
|
t.Fatalf("unexpected dashboard: %+v", dashboard)
|
|
}
|
|
|
|
dashboard, err = st.ApplyLayout(ctx, LayoutInput{
|
|
GroupIDs: []string{media.ID, infra.ID},
|
|
WidgetIDs: []string{},
|
|
UngroupedServices: []string{},
|
|
GroupServices: map[string][]string{
|
|
media.ID: {service.ID},
|
|
infra.ID: {},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if dashboard.Groups[0].ID != media.ID || len(dashboard.Groups[0].Services) != 1 {
|
|
t.Fatalf("service was not moved to media group: %+v", dashboard.Groups)
|
|
}
|
|
|
|
if err := st.DeleteGroup(ctx, media.ID, false); !errors.Is(err, ErrConflict) {
|
|
t.Fatalf("DeleteGroup() error = %v, want conflict", err)
|
|
}
|
|
if err := st.DeleteGroup(ctx, media.ID, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service, err = st.Service(ctx, service.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if service.GroupID != nil {
|
|
t.Fatalf("service group id = %v, want nil", *service.GroupID)
|
|
}
|
|
}
|
|
|
|
func TestApplyLayoutRejectsPartialServiceSetIntegration(t *testing.T) {
|
|
pool := testutil.TestPool(t)
|
|
st := New(pool)
|
|
ctx := context.Background()
|
|
|
|
service, err := st.CreateService(ctx, ServiceInput{
|
|
Name: "Router",
|
|
URLs: []ServiceURLInput{
|
|
{Label: "local", Kind: "local", URL: "http://router.local", IsPrimary: true},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = st.CreateService(ctx, ServiceInput{
|
|
Name: "NAS",
|
|
URLs: []ServiceURLInput{
|
|
{Label: "local", Kind: "local", URL: "http://nas.local", IsPrimary: true},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = st.ApplyLayout(ctx, LayoutInput{
|
|
GroupIDs: []string{},
|
|
WidgetIDs: []string{},
|
|
UngroupedServices: []string{service.ID},
|
|
GroupServices: map[string][]string{},
|
|
})
|
|
if !errors.Is(err, ErrValidation) {
|
|
t.Fatalf("ApplyLayout() error = %v, want validation", err)
|
|
}
|
|
}
|
|
|
|
func TestWidgetCacheIntegration(t *testing.T) {
|
|
pool := testutil.TestPool(t)
|
|
st := New(pool)
|
|
ctx := context.Background()
|
|
|
|
widget, err := st.CreateWidget(ctx, WidgetInput{
|
|
Type: "clock",
|
|
Title: "Clock",
|
|
Config: json.RawMessage(`{"timezones":["Europe/Prague"]}`),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Now().UTC()
|
|
err = st.SaveWidgetData(ctx, WidgetData{
|
|
WidgetID: widget.ID,
|
|
Status: "fresh",
|
|
Data: json.RawMessage(`{"ok":true}`),
|
|
FetchedAt: &now,
|
|
ExpiresAt: &now,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := st.WidgetData(ctx, widget.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if data.Status != "fresh" || len(data.Data) == 0 {
|
|
t.Fatalf("unexpected widget data: %+v", data)
|
|
}
|
|
}
|