- go.mod module path (and every internal/vibetree import) inventree -> vibetree - cmd/inventree/ -> cmd/vibetree/, deploy/inventree.service -> vibetree.service - Default site name (seed data + the siteName template func's fallback) is now "vibetree", shown in the navbar, <title>, and login page - Session cookie renamed inventree_session -> vibetree_session - Default DB path data/inventree.db -> data/vibetree.db, CLI/log text, Dockerfile, docker-compose.yml, README, .gitignore/.dockerignore all updated to match Existing installs: this changes the session cookie name (users will be signed out once) and the default DB filename (point -db at your existing file, or rename it, to keep your data).
203 lines
5.7 KiB
Go
203 lines
5.7 KiB
Go
package assets_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"vibetree/internal/assets"
|
|
"vibetree/internal/database"
|
|
"vibetree/internal/folders"
|
|
"vibetree/internal/models"
|
|
"vibetree/internal/settings"
|
|
)
|
|
|
|
// Seed data from migrations/0002_seed.sql gives us stable IDs to rely on:
|
|
// item types 1=Laptop 2=Desktop 3=Server 4=Switch ... statuses 1=In Use 2=Spare ...
|
|
const (
|
|
itemTypeLaptop = 1
|
|
itemTypeSwitch = 4
|
|
statusInUse = 1
|
|
statusSpare = 2
|
|
)
|
|
|
|
func testStores(t *testing.T) (*assets.Store, *folders.Store, *sql.DB) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
db, err := database.Open(filepath.Join(dir, "test.db"))
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
t.Cleanup(func() { db.Close() })
|
|
fs := folders.NewStore(db)
|
|
st := settings.NewStore(db)
|
|
return assets.NewStore(db, st, fs), fs, db
|
|
}
|
|
|
|
func TestAssetCreateAutoGeneratesID(t *testing.T) {
|
|
ctx := context.Background()
|
|
as, _, _ := testStores(t)
|
|
|
|
a1 := &models.Asset{Name: "Core Switch", ItemTypeID: itemTypeSwitch, StatusID: statusInUse}
|
|
if err := as.Create(ctx, a1, nil); err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
if a1.AssetTag != "IT-000001" {
|
|
t.Fatalf("expected IT-000001, got %s", a1.AssetTag)
|
|
}
|
|
|
|
a2 := &models.Asset{Name: "Firewall", ItemTypeID: itemTypeSwitch, StatusID: statusInUse}
|
|
if err := as.Create(ctx, a2, nil); err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
if a2.AssetTag != "IT-000002" {
|
|
t.Fatalf("expected IT-000002, got %s", a2.AssetTag)
|
|
}
|
|
}
|
|
|
|
func TestAssetCreateWithManualIDAndCollision(t *testing.T) {
|
|
ctx := context.Background()
|
|
as, _, _ := testStores(t)
|
|
|
|
a1 := &models.Asset{AssetTag: "CUSTOM-1", Name: "Core Switch", ItemTypeID: itemTypeSwitch, StatusID: statusInUse}
|
|
if err := as.Create(ctx, a1, nil); err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
|
|
dup := &models.Asset{AssetTag: "CUSTOM-1", Name: "Other", ItemTypeID: itemTypeSwitch, StatusID: statusInUse}
|
|
if err := as.Create(ctx, dup, nil); err != assets.ErrTagTaken {
|
|
t.Fatalf("expected ErrTagTaken, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAssetDuplicateGeneratesNewID(t *testing.T) {
|
|
ctx := context.Background()
|
|
as, _, _ := testStores(t)
|
|
|
|
orig := &models.Asset{Name: "Laptop 1", ItemTypeID: itemTypeLaptop, StatusID: statusInUse, Notes: "hello"}
|
|
if err := as.Create(ctx, orig, nil); err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
|
|
dup, err := as.Duplicate(ctx, orig.ID)
|
|
if err != nil {
|
|
t.Fatalf("duplicate: %v", err)
|
|
}
|
|
if dup.AssetTag == orig.AssetTag {
|
|
t.Fatalf("expected different asset tag, got same: %s", dup.AssetTag)
|
|
}
|
|
if dup.Notes != "hello" {
|
|
t.Fatalf("expected notes copied, got %q", dup.Notes)
|
|
}
|
|
}
|
|
|
|
func TestAssetListFilterByFolder(t *testing.T) {
|
|
ctx := context.Background()
|
|
as, fs, _ := testStores(t)
|
|
|
|
f1 := &models.Folder{Name: "Office"}
|
|
must(t, fs.Create(ctx, f1))
|
|
f2 := &models.Folder{Name: "Warehouse"}
|
|
must(t, fs.Create(ctx, f2))
|
|
|
|
a1 := &models.Asset{Name: "A1", ItemTypeID: itemTypeLaptop, StatusID: statusInUse, FolderID: &f1.ID}
|
|
must(t, as.Create(ctx, a1, nil))
|
|
a2 := &models.Asset{Name: "A2", ItemTypeID: itemTypeLaptop, StatusID: statusInUse, FolderID: &f2.ID}
|
|
must(t, as.Create(ctx, a2, nil))
|
|
|
|
list, total, err := as.List(ctx, assets.ListFilter{FolderID: &f1.ID, FolderSet: true, Page: 1, PageSize: 10})
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
if total != 1 || len(list) != 1 || list[0].Name != "A1" {
|
|
t.Fatalf("unexpected list result: total=%d list=%+v", total, list)
|
|
}
|
|
}
|
|
|
|
func TestAssetCSVExportImportRoundTrip(t *testing.T) {
|
|
ctx := context.Background()
|
|
as, fs, db := testStores(t)
|
|
|
|
office := &models.Folder{Name: "Main Office"}
|
|
must(t, fs.Create(ctx, office))
|
|
orig := &models.Asset{Name: "Core Switch", ItemTypeID: itemTypeSwitch, StatusID: statusInUse, FolderID: &office.ID}
|
|
must(t, as.Create(ctx, orig, nil))
|
|
|
|
var buf bytes.Buffer
|
|
if err := as.Export(ctx, &buf, assets.ListFilter{}); err != nil {
|
|
t.Fatalf("export: %v", err)
|
|
}
|
|
if !strings.Contains(buf.String(), "Core Switch") {
|
|
t.Fatalf("expected exported CSV to contain asset name, got: %s", buf.String())
|
|
}
|
|
|
|
// Import a new asset referencing a not-yet-existing nested folder path,
|
|
// with CreateMissingFolders enabled.
|
|
csvData := "Name,Item Type,Folder,Status\nBackup Router,Switch,Main Office/Network,In Use\n"
|
|
|
|
itemTypes := seedItemTypes(t, db)
|
|
statuses := seedStatuses(t, db)
|
|
|
|
result, err := as.Import(ctx, strings.NewReader(csvData), assets.ImportOptions{CreateMissingFolders: true}, itemTypes, statuses, nil)
|
|
if err != nil {
|
|
t.Fatalf("import: %v", err)
|
|
}
|
|
if result.Created != 1 {
|
|
t.Fatalf("expected 1 created, got %+v", result)
|
|
}
|
|
|
|
all, total, err := as.List(ctx, assets.ListFilter{Page: 1, PageSize: 10})
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
if total != 2 {
|
|
t.Fatalf("expected 2 assets total after import, got %d (%+v)", total, all)
|
|
}
|
|
}
|
|
|
|
func seedItemTypes(t *testing.T, db *sql.DB) []models.ItemType {
|
|
t.Helper()
|
|
rows, err := db.Query(`SELECT id, name, icon, enabled, sort_order FROM item_types`)
|
|
if err != nil {
|
|
t.Fatalf("query item types: %v", err)
|
|
}
|
|
defer rows.Close()
|
|
var out []models.ItemType
|
|
for rows.Next() {
|
|
var it models.ItemType
|
|
if err := rows.Scan(&it.ID, &it.Name, &it.Icon, &it.Enabled, &it.SortOrder); err != nil {
|
|
t.Fatalf("scan: %v", err)
|
|
}
|
|
out = append(out, it)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func seedStatuses(t *testing.T, db *sql.DB) []models.Status {
|
|
t.Helper()
|
|
rows, err := db.Query(`SELECT id, name, color, sort_order FROM statuses`)
|
|
if err != nil {
|
|
t.Fatalf("query statuses: %v", err)
|
|
}
|
|
defer rows.Close()
|
|
var out []models.Status
|
|
for rows.Next() {
|
|
var st models.Status
|
|
if err := rows.Scan(&st.ID, &st.Name, &st.Color, &st.SortOrder); err != nil {
|
|
t.Fatalf("scan: %v", err)
|
|
}
|
|
out = append(out, st)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func must(t *testing.T, err error) {
|
|
t.Helper()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|