736 lines
31 KiB
Go
736 lines
31 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/fxamacker/cbor/v2"
|
|
"github.com/tailscale/hujson"
|
|
)
|
|
|
|
type fakeRow struct {
|
|
scan func(dest ...any) error
|
|
}
|
|
|
|
func (row fakeRow) Scan(dest ...any) error {
|
|
return row.scan(dest...)
|
|
}
|
|
|
|
func TestScanInstallationRecordDefaultsMaterializationStatus(t *testing.T) {
|
|
record, err := scanInstallationRecord(fakeRow{scan: func(dest ...any) error {
|
|
*(dest[0].(*string)) = "installation-1"
|
|
*(dest[1].(*string)) = "MangoPig"
|
|
*(dest[2].(*string)) = "personal"
|
|
*(dest[3].(*string)) = "local"
|
|
*(dest[4].(*string)) = "http"
|
|
*(dest[5].(*string)) = "localhost"
|
|
*(dest[6].(*bool)) = true
|
|
*(dest[7].(*string)) = ""
|
|
*(dest[8].(**string)) = nil
|
|
return nil
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("scanInstallationRecord: %v", err)
|
|
}
|
|
|
|
if record.MaterializationStatus != materializationNotStarted {
|
|
t.Fatalf("expected default materialization status %q, got %q", materializationNotStarted, record.MaterializationStatus)
|
|
}
|
|
if record.MaterializationError != nil {
|
|
t.Fatalf("expected nil materialization error, got %#v", record.MaterializationError)
|
|
}
|
|
}
|
|
|
|
func TestScanInstallationRecordPreservesMaterializationFields(t *testing.T) {
|
|
failure := "projection rebuild failed"
|
|
|
|
record, err := scanInstallationRecord(fakeRow{scan: func(dest ...any) error {
|
|
*(dest[0].(*string)) = "installation-2"
|
|
*(dest[1].(*string)) = "MangoPig"
|
|
*(dest[2].(*string)) = "personal"
|
|
*(dest[3].(*string)) = "local"
|
|
*(dest[4].(*string)) = "http"
|
|
*(dest[5].(*string)) = "localhost"
|
|
*(dest[6].(*bool)) = true
|
|
*(dest[7].(*string)) = materializationFailed
|
|
*(dest[8].(**string)) = &failure
|
|
return nil
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("scanInstallationRecord: %v", err)
|
|
}
|
|
|
|
if record.MaterializationStatus != materializationFailed {
|
|
t.Fatalf("expected materialization status %q, got %q", materializationFailed, record.MaterializationStatus)
|
|
}
|
|
if record.MaterializationError == nil || *record.MaterializationError != failure {
|
|
t.Fatalf("expected materialization error %q, got %#v", failure, record.MaterializationError)
|
|
}
|
|
}
|
|
|
|
func TestEnsureBootstrapPOSIXSkeletonInitializesEmptyRoot(t *testing.T) {
|
|
rootPath := filepath.Join(t.TempDir(), "POSIX")
|
|
t.Setenv("POSIX_ROOT", rootPath)
|
|
|
|
if _, err := os.Stat(rootPath); !os.IsNotExist(err) {
|
|
t.Fatalf("expected isolated POSIX root to start absent, got err=%v", err)
|
|
}
|
|
|
|
service := NewService(nil, os.Getenv("POSIX_ROOT"))
|
|
|
|
err := service.ensureBootstrapPOSIXSkeleton(
|
|
InstallationRecord{
|
|
ID: "installation-1",
|
|
Name: "MangoPig",
|
|
Mode: "personal",
|
|
Access: "local",
|
|
Protocol: "http",
|
|
Host: "localhost",
|
|
IsBootstrapped: true,
|
|
},
|
|
AdminSummary{
|
|
ID: "admin-1",
|
|
Email: "ronald@example.com",
|
|
DisplayName: "Ronald",
|
|
},
|
|
namedRecord{ID: "org-1", Name: "Primary Organization", Slug: "primary-organization"},
|
|
namedRecord{ID: "dept-1", Name: "Primary Department", Slug: "primary-department"},
|
|
namedRecord{ID: "team-1", Name: "Primary Team", Slug: "primary-team"},
|
|
namedRecord{ID: "project-1", Name: "Primary Project", Slug: "primary-project"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("ensure bootstrap POSIX skeleton: %v", err)
|
|
}
|
|
|
|
requiredPaths := []string{
|
|
filepath.Join(rootPath, posixSettingsFileName),
|
|
filepath.Join(rootPath, posixLayoutFileName),
|
|
filepath.Join(rootPath, "catalog", "packs"),
|
|
filepath.Join(rootPath, "catalog", "standalone"),
|
|
filepath.Join(rootPath, "catalog", "packs", "pack-core", posixManifestFileName),
|
|
filepath.Join(rootPath, "catalog", "packs", "pack-core", "entries", "app-shell", posixManifestFileName),
|
|
filepath.Join(rootPath, "catalog", "standalone", "app-shell", posixManifestFileName),
|
|
filepath.Join(rootPath, "departments", "department-primary-department", posixSettingsFileName),
|
|
filepath.Join(rootPath, "departments", "department-primary-department", posixUsersFileName),
|
|
filepath.Join(rootPath, "departments", "department-primary-department", "teams", "team-primary-team", posixSettingsFileName),
|
|
filepath.Join(rootPath, "departments", "department-primary-department", "teams", "team-primary-team", posixUsersFileName),
|
|
filepath.Join(rootPath, "projects", "project-primary-project", posixSettingsFileName),
|
|
filepath.Join(rootPath, "projects", "project-primary-project", posixHomeFileName),
|
|
filepath.Join(rootPath, "projects", "project-primary-project", posixACLFileName),
|
|
filepath.Join(rootPath, "projects", "project-primary-project", "children"),
|
|
filepath.Join(rootPath, "projects", "project-primary-project", "tree"),
|
|
filepath.Join(rootPath, "users", posixSettingsFileName),
|
|
filepath.Join(rootPath, "users", posixDataFileName),
|
|
filepath.Join(rootPath, "users", "personals"),
|
|
filepath.Join(rootPath, "users", "personals", "personal-ronald", posixSettingsFileName),
|
|
filepath.Join(rootPath, "users", "personals", "personal-ronald", posixLayoutFileName),
|
|
filepath.Join(rootPath, "users", "personals", "personal-ronald", posixHomeFileName),
|
|
filepath.Join(rootPath, "users", "personals", "personal-ronald", "tree"),
|
|
}
|
|
|
|
for _, path := range requiredPaths {
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatalf("expected path to exist %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
settingsPayload := readStructuredFileForTest[map[string]any](t, filepath.Join(rootPath, posixSettingsFileName))
|
|
installationPayload, ok := settingsPayload["installation"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("%s missing installation object: %#v", posixSettingsFileName, settingsPayload)
|
|
}
|
|
if installationPayload["name"] != "MangoPig" {
|
|
t.Fatalf("expected installation name MangoPig, got %#v", installationPayload["name"])
|
|
}
|
|
if installationPayload["isBootstrapped"] != true {
|
|
t.Fatalf("expected installation to be bootstrapped, got %#v", installationPayload["isBootstrapped"])
|
|
}
|
|
|
|
layoutPayload := readStructuredFileForTest[map[string]any](t, filepath.Join(rootPath, posixLayoutFileName))
|
|
homePayload, ok := layoutPayload["home"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("%s missing home object: %#v", posixLayoutFileName, layoutPayload)
|
|
}
|
|
if homePayload["defaultProjectSlug"] != "primary-project" {
|
|
t.Fatalf("expected default project slug primary-project, got %#v", homePayload["defaultProjectSlug"])
|
|
}
|
|
|
|
packManifest := readStructuredFileForTest[map[string]any](t, filepath.Join(rootPath, "catalog", "packs", "pack-core", posixManifestFileName))
|
|
if packManifest["type"] != "pack" {
|
|
t.Fatalf("expected pack manifest type pack, got %#v", packManifest["type"])
|
|
}
|
|
if packManifest["slug"] != "core" {
|
|
t.Fatalf("expected pack manifest slug core, got %#v", packManifest["slug"])
|
|
}
|
|
|
|
entryManifest := readStructuredFileForTest[map[string]any](t, filepath.Join(rootPath, "catalog", "packs", "pack-core", "entries", "app-shell", posixManifestFileName))
|
|
runtimePayload, ok := entryManifest["runtime"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("%s missing runtime object: %#v", posixManifestFileName, entryManifest)
|
|
}
|
|
if runtimePayload["path"] != "/v1/app-shell" {
|
|
t.Fatalf("expected app-shell runtime path /v1/app-shell, got %#v", runtimePayload["path"])
|
|
}
|
|
|
|
standaloneManifest := readStructuredFileForTest[map[string]any](t, filepath.Join(rootPath, "catalog", "standalone", "app-shell", posixManifestFileName))
|
|
if standaloneManifest["source"] != "standalone" {
|
|
t.Fatalf("expected standalone manifest source standalone, got %#v", standaloneManifest["source"])
|
|
}
|
|
|
|
projectSettings := readStructuredFileForTest[map[string]any](t, filepath.Join(rootPath, "projects", "project-primary-project", posixSettingsFileName))
|
|
if projectSettings["type"] != "project" {
|
|
t.Fatalf("expected project settings type project, got %#v", projectSettings["type"])
|
|
}
|
|
|
|
projectACL := readStructuredFileForTest[map[string]any](t, filepath.Join(rootPath, "projects", "project-primary-project", posixACLFileName))
|
|
if projectACL["inherits"] != true {
|
|
t.Fatalf("expected project acl to inherit by default, got %#v", projectACL["inherits"])
|
|
}
|
|
|
|
usersSettings := readStructuredFileForTest[map[string]any](t, filepath.Join(rootPath, "users", posixSettingsFileName))
|
|
if usersSettings["primaryAdminId"] != "admin-1" {
|
|
t.Fatalf("expected primary admin id admin-1, got %#v", usersSettings["primaryAdminId"])
|
|
}
|
|
|
|
personalSettings := readStructuredFileForTest[map[string]any](t, filepath.Join(rootPath, "users", "personals", "personal-ronald", posixSettingsFileName))
|
|
if personalSettings["type"] != "personal" {
|
|
t.Fatalf("expected personal settings type personal, got %#v", personalSettings["type"])
|
|
}
|
|
if personalSettings["name"] != "Ronald" {
|
|
t.Fatalf("expected personal name Ronald, got %#v", personalSettings["name"])
|
|
}
|
|
if personalSettings["slug"] != "ronald" {
|
|
t.Fatalf("expected personal slug ronald, got %#v", personalSettings["slug"])
|
|
}
|
|
personalTheme, ok := personalSettings["theme"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected personal settings to contain theme object, got %#v", personalSettings)
|
|
}
|
|
if personalTheme["presetId"] != defaultThemePresetID {
|
|
t.Fatalf("expected personal theme preset %s, got %#v", defaultThemePresetID, personalTheme["presetId"])
|
|
}
|
|
|
|
personalHome := readStructuredFileForTest[map[string]any](t, filepath.Join(rootPath, "users", "personals", "personal-ronald", posixHomeFileName))
|
|
if personalHome["type"] != "personal-home" {
|
|
t.Fatalf("expected personal home type personal-home, got %#v", personalHome["type"])
|
|
}
|
|
if personalHome["title"] != "Ronald's Home" {
|
|
t.Fatalf("expected personal home title Ronald's Home, got %#v", personalHome["title"])
|
|
}
|
|
}
|
|
|
|
func TestCreateProjectHierarchyFolderOnDiskCreatesExpectedFolderShape(t *testing.T) {
|
|
rootPath := filepath.Join(t.TempDir(), "POSIX")
|
|
service := NewService(nil, rootPath)
|
|
|
|
err := service.ensureBootstrapPOSIXSkeleton(
|
|
InstallationRecord{ID: "installation-1", Name: "MangoPig", Mode: "personal", Access: "local", Protocol: "http", Host: "localhost", IsBootstrapped: true},
|
|
AdminSummary{ID: "admin-1", Email: "ronald@example.com", DisplayName: "Ronald"},
|
|
namedRecord{ID: "org-1", Name: "Primary Organization", Slug: "primary-organization"},
|
|
namedRecord{ID: "dept-1", Name: "Primary Department", Slug: "primary-department"},
|
|
namedRecord{ID: "team-1", Name: "Primary Team", Slug: "primary-team"},
|
|
namedRecord{ID: "project-1", Name: "Primary Project", Slug: "primary-project"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("ensure bootstrap POSIX skeleton: %v", err)
|
|
}
|
|
|
|
createdPath, createdSlug, err := service.createProjectHierarchyFolderOnDisk("primary-project", "", "Design System")
|
|
if err != nil {
|
|
t.Fatalf("createProjectHierarchyFolderOnDisk root folder: %v", err)
|
|
}
|
|
if createdPath != "projects/project-primary-project/children/folder-design-system" {
|
|
t.Fatalf("unexpected created path: %s", createdPath)
|
|
}
|
|
if createdSlug != "design-system" {
|
|
t.Fatalf("unexpected created slug: %s", createdSlug)
|
|
}
|
|
|
|
createdFolderPath := filepath.Join(rootPath, "projects", "project-primary-project", "children", "folder-design-system")
|
|
for _, path := range []string{
|
|
filepath.Join(createdFolderPath, posixFolderFileName),
|
|
filepath.Join(createdFolderPath, posixACLFileName),
|
|
filepath.Join(createdFolderPath, "children"),
|
|
} {
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatalf("expected path to exist %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
folderPayload := readStructuredFileForTest[map[string]any](t, filepath.Join(createdFolderPath, posixFolderFileName))
|
|
if strings.TrimSpace(asStringForTest(folderPayload["id"])) == "" {
|
|
t.Fatalf("expected created folder to have stable id, got %#v", folderPayload["id"])
|
|
}
|
|
if folderPayload["name"] != "Design System" {
|
|
t.Fatalf("expected folder name Design System, got %#v", folderPayload["name"])
|
|
}
|
|
if folderPayload["slug"] != "design-system" {
|
|
t.Fatalf("expected folder slug design-system, got %#v", folderPayload["slug"])
|
|
}
|
|
|
|
nestedPath, nestedSlug, err := service.createProjectHierarchyFolderOnDisk("primary-project", createdPath, "Research")
|
|
if err != nil {
|
|
t.Fatalf("createProjectHierarchyFolderOnDisk nested folder: %v", err)
|
|
}
|
|
if nestedPath != "projects/project-primary-project/children/folder-design-system/children/folder-research" {
|
|
t.Fatalf("unexpected nested path: %s", nestedPath)
|
|
}
|
|
if nestedSlug != "research" {
|
|
t.Fatalf("unexpected nested slug: %s", nestedSlug)
|
|
}
|
|
}
|
|
|
|
func TestPersonalSettingsPathForDisplayName(t *testing.T) {
|
|
path := personalSettingsPathForDisplayName(" Ronald ")
|
|
if path != "users/personals/personal-ronald/settings.cbor" {
|
|
t.Fatalf("unexpected personal settings path: %s", path)
|
|
}
|
|
}
|
|
|
|
func TestApplyThemePresetToSettingsPreservesExistingFields(t *testing.T) {
|
|
settings := map[string]any{
|
|
"type": "personal",
|
|
"slug": "ronald",
|
|
"theme": map[string]any{
|
|
"density": "comfortable",
|
|
},
|
|
}
|
|
|
|
updated := applyThemePresetToSettings(settings, "moku-default")
|
|
|
|
if updated["type"] != "personal" {
|
|
t.Fatalf("expected type personal, got %#v", updated["type"])
|
|
}
|
|
theme, ok := updated["theme"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected theme object, got %#v", updated["theme"])
|
|
}
|
|
if theme["presetId"] != "moku-default" {
|
|
t.Fatalf("expected presetId moku-default, got %#v", theme["presetId"])
|
|
}
|
|
if theme["density"] != "comfortable" {
|
|
t.Fatalf("expected density to be preserved, got %#v", theme["density"])
|
|
}
|
|
}
|
|
|
|
func TestCreateProjectTreeFolderOnDiskCreatesExpectedFolderShape(t *testing.T) {
|
|
rootPath := filepath.Join(t.TempDir(), "POSIX")
|
|
service := NewService(nil, rootPath)
|
|
|
|
err := service.ensureBootstrapPOSIXSkeleton(
|
|
InstallationRecord{ID: "installation-1", Name: "MangoPig", Mode: "personal", Access: "local", Protocol: "http", Host: "localhost", IsBootstrapped: true},
|
|
AdminSummary{ID: "admin-1", Email: "ronald@example.com", DisplayName: "Ronald"},
|
|
namedRecord{ID: "org-1", Name: "Primary Organization", Slug: "primary-organization"},
|
|
namedRecord{ID: "dept-1", Name: "Primary Department", Slug: "primary-department"},
|
|
namedRecord{ID: "team-1", Name: "Primary Team", Slug: "primary-team"},
|
|
namedRecord{ID: "project-1", Name: "Primary Project", Slug: "primary-project"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("ensure bootstrap POSIX skeleton: %v", err)
|
|
}
|
|
|
|
createdPath, createdSlug, err := service.createProjectTreeFolderOnDisk("primary-project", "", "Docs")
|
|
if err != nil {
|
|
t.Fatalf("createProjectTreeFolderOnDisk root folder: %v", err)
|
|
}
|
|
if createdPath != "projects/project-primary-project/tree/folder-docs" {
|
|
t.Fatalf("unexpected created path: %s", createdPath)
|
|
}
|
|
if createdSlug != "docs" {
|
|
t.Fatalf("unexpected created slug: %s", createdSlug)
|
|
}
|
|
|
|
createdFolderPath := filepath.Join(rootPath, "projects", "project-primary-project", "tree", "folder-docs")
|
|
for _, path := range []string{
|
|
filepath.Join(createdFolderPath, posixFolderFileName),
|
|
filepath.Join(createdFolderPath, posixACLFileName),
|
|
filepath.Join(createdFolderPath, "children"),
|
|
} {
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatalf("expected path to exist %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
nestedPath, nestedSlug, err := service.createProjectTreeFolderOnDisk("primary-project", createdPath, "Research")
|
|
if err != nil {
|
|
t.Fatalf("createProjectTreeFolderOnDisk nested folder: %v", err)
|
|
}
|
|
if nestedPath != "projects/project-primary-project/tree/folder-docs/children/folder-research" {
|
|
t.Fatalf("unexpected nested path: %s", nestedPath)
|
|
}
|
|
if nestedSlug != "research" {
|
|
t.Fatalf("unexpected nested slug: %s", nestedSlug)
|
|
}
|
|
}
|
|
|
|
func TestRenameProjectHierarchyFolderOnDiskRenamesFolderShape(t *testing.T) {
|
|
rootPath := filepath.Join(t.TempDir(), "POSIX")
|
|
service := NewService(nil, rootPath)
|
|
|
|
err := service.ensureBootstrapPOSIXSkeleton(
|
|
InstallationRecord{ID: "installation-1", Name: "MangoPig", Mode: "personal", Access: "local", Protocol: "http", Host: "localhost", IsBootstrapped: true},
|
|
AdminSummary{ID: "admin-1", Email: "ronald@example.com", DisplayName: "Ronald"},
|
|
namedRecord{ID: "org-1", Name: "Primary Organization", Slug: "primary-organization"},
|
|
namedRecord{ID: "dept-1", Name: "Primary Department", Slug: "primary-department"},
|
|
namedRecord{ID: "team-1", Name: "Primary Team", Slug: "primary-team"},
|
|
namedRecord{ID: "project-1", Name: "Primary Project", Slug: "primary-project"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("ensure bootstrap POSIX skeleton: %v", err)
|
|
}
|
|
|
|
createdPath, _, err := service.createProjectHierarchyFolderOnDisk("primary-project", "", "Design System")
|
|
if err != nil {
|
|
t.Fatalf("createProjectHierarchyFolderOnDisk root folder: %v", err)
|
|
}
|
|
|
|
nestedPath, _, err := service.createProjectHierarchyFolderOnDisk("primary-project", createdPath, "Research")
|
|
if err != nil {
|
|
t.Fatalf("createProjectHierarchyFolderOnDisk nested folder: %v", err)
|
|
}
|
|
|
|
previousPath, renamedPath, err := service.renameProjectHierarchyFolderOnDisk("primary-project", createdPath, "Platform Design")
|
|
if err != nil {
|
|
t.Fatalf("renameProjectHierarchyFolderOnDisk: %v", err)
|
|
}
|
|
if previousPath != createdPath {
|
|
t.Fatalf("expected previous path %s, got %s", createdPath, previousPath)
|
|
}
|
|
if renamedPath != "projects/project-primary-project/children/folder-platform-design" {
|
|
t.Fatalf("unexpected renamed path: %s", renamedPath)
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(rootPath, filepath.FromSlash(createdPath))); !os.IsNotExist(err) {
|
|
t.Fatalf("expected previous folder path to be gone, got err=%v", err)
|
|
}
|
|
|
|
renamedFolderPath := filepath.Join(rootPath, filepath.FromSlash(renamedPath))
|
|
if _, err := os.Stat(filepath.Join(renamedFolderPath, "children", filepath.Base(nestedPath))); err != nil {
|
|
t.Fatalf("expected nested child folder to move with renamed parent: %v", err)
|
|
}
|
|
|
|
folderPayload := readStructuredFileForTest[map[string]any](t, filepath.Join(renamedFolderPath, posixFolderFileName))
|
|
if strings.TrimSpace(asStringForTest(folderPayload["id"])) == "" {
|
|
t.Fatalf("expected renamed folder to preserve stable id, got %#v", folderPayload["id"])
|
|
}
|
|
if folderPayload["name"] != "Platform Design" {
|
|
t.Fatalf("expected renamed folder name Platform Design, got %#v", folderPayload["name"])
|
|
}
|
|
if folderPayload["slug"] != "platform-design" {
|
|
t.Fatalf("expected renamed folder slug platform-design, got %#v", folderPayload["slug"])
|
|
}
|
|
if folderPayload["type"] != "folder" {
|
|
t.Fatalf("expected renamed folder type folder, got %#v", folderPayload["type"])
|
|
}
|
|
}
|
|
|
|
func TestRenameProjectTreeFolderOnDiskRenamesFolderShape(t *testing.T) {
|
|
rootPath := filepath.Join(t.TempDir(), "POSIX")
|
|
service := NewService(nil, rootPath)
|
|
|
|
err := service.ensureBootstrapPOSIXSkeleton(
|
|
InstallationRecord{ID: "installation-1", Name: "MangoPig", Mode: "personal", Access: "local", Protocol: "http", Host: "localhost", IsBootstrapped: true},
|
|
AdminSummary{ID: "admin-1", Email: "ronald@example.com", DisplayName: "Ronald"},
|
|
namedRecord{ID: "org-1", Name: "Primary Organization", Slug: "primary-organization"},
|
|
namedRecord{ID: "dept-1", Name: "Primary Department", Slug: "primary-department"},
|
|
namedRecord{ID: "team-1", Name: "Primary Team", Slug: "primary-team"},
|
|
namedRecord{ID: "project-1", Name: "Primary Project", Slug: "primary-project"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("ensure bootstrap POSIX skeleton: %v", err)
|
|
}
|
|
|
|
createdPath, _, err := service.createProjectTreeFolderOnDisk("primary-project", "", "Docs")
|
|
if err != nil {
|
|
t.Fatalf("createProjectTreeFolderOnDisk root folder: %v", err)
|
|
}
|
|
|
|
previousPath, renamedPath, err := service.renameProjectTreeFolderOnDisk("primary-project", createdPath, "Specifications")
|
|
if err != nil {
|
|
t.Fatalf("renameProjectTreeFolderOnDisk: %v", err)
|
|
}
|
|
if previousPath != createdPath {
|
|
t.Fatalf("expected previous path %s, got %s", createdPath, previousPath)
|
|
}
|
|
if renamedPath != "projects/project-primary-project/tree/folder-specifications" {
|
|
t.Fatalf("unexpected renamed path: %s", renamedPath)
|
|
}
|
|
|
|
folderPayload := readStructuredFileForTest[map[string]any](t, filepath.Join(rootPath, filepath.FromSlash(renamedPath), posixFolderFileName))
|
|
if folderPayload["name"] != "Specifications" {
|
|
t.Fatalf("expected renamed folder name Specifications, got %#v", folderPayload["name"])
|
|
}
|
|
if folderPayload["slug"] != "specifications" {
|
|
t.Fatalf("expected renamed folder slug specifications, got %#v", folderPayload["slug"])
|
|
}
|
|
}
|
|
|
|
func TestMoveProjectHierarchyFolderOnDiskMovesFolderToNewParent(t *testing.T) {
|
|
rootPath := filepath.Join(t.TempDir(), "POSIX")
|
|
service := NewService(nil, rootPath)
|
|
|
|
err := service.ensureBootstrapPOSIXSkeleton(
|
|
InstallationRecord{ID: "installation-1", Name: "MangoPig", Mode: "personal", Access: "local", Protocol: "http", Host: "localhost", IsBootstrapped: true},
|
|
AdminSummary{ID: "admin-1", Email: "ronald@example.com", DisplayName: "Ronald"},
|
|
namedRecord{ID: "org-1", Name: "Primary Organization", Slug: "primary-organization"},
|
|
namedRecord{ID: "dept-1", Name: "Primary Department", Slug: "primary-department"},
|
|
namedRecord{ID: "team-1", Name: "Primary Team", Slug: "primary-team"},
|
|
namedRecord{ID: "project-1", Name: "Primary Project", Slug: "primary-project"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("ensure bootstrap POSIX skeleton: %v", err)
|
|
}
|
|
|
|
designPath, _, err := service.createProjectHierarchyFolderOnDisk("primary-project", "", "Design")
|
|
if err != nil {
|
|
t.Fatalf("create design folder: %v", err)
|
|
}
|
|
operationsPath, _, err := service.createProjectHierarchyFolderOnDisk("primary-project", "", "Operations")
|
|
if err != nil {
|
|
t.Fatalf("create operations folder: %v", err)
|
|
}
|
|
researchPath, _, err := service.createProjectHierarchyFolderOnDisk("primary-project", designPath, "Research")
|
|
if err != nil {
|
|
t.Fatalf("create research folder: %v", err)
|
|
}
|
|
nestedPath, _, err := service.createProjectHierarchyFolderOnDisk("primary-project", researchPath, "Interview Notes")
|
|
if err != nil {
|
|
t.Fatalf("create nested folder: %v", err)
|
|
}
|
|
|
|
previousPath, movedPath, err := service.moveProjectHierarchyFolderOnDisk("primary-project", researchPath, operationsPath)
|
|
if err != nil {
|
|
t.Fatalf("moveProjectHierarchyFolderOnDisk: %v", err)
|
|
}
|
|
if previousPath != researchPath {
|
|
t.Fatalf("expected previous path %s, got %s", researchPath, previousPath)
|
|
}
|
|
if movedPath != "projects/project-primary-project/children/folder-operations/children/folder-research" {
|
|
t.Fatalf("unexpected moved path: %s", movedPath)
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(rootPath, filepath.FromSlash(researchPath))); !os.IsNotExist(err) {
|
|
t.Fatalf("expected previous folder path to be gone, got err=%v", err)
|
|
}
|
|
|
|
movedFolderPath := filepath.Join(rootPath, filepath.FromSlash(movedPath))
|
|
if _, err := os.Stat(filepath.Join(movedFolderPath, "children", filepath.Base(nestedPath))); err != nil {
|
|
t.Fatalf("expected nested child folder to move with moved parent: %v", err)
|
|
}
|
|
|
|
folderPayload := readStructuredFileForTest[map[string]any](t, filepath.Join(movedFolderPath, posixFolderFileName))
|
|
if strings.TrimSpace(asStringForTest(folderPayload["id"])) == "" {
|
|
t.Fatalf("expected moved folder to preserve stable id, got %#v", folderPayload["id"])
|
|
}
|
|
if folderPayload["name"] != "Research" {
|
|
t.Fatalf("expected moved folder name Research, got %#v", folderPayload["name"])
|
|
}
|
|
if folderPayload["slug"] != "research" {
|
|
t.Fatalf("expected moved folder slug research, got %#v", folderPayload["slug"])
|
|
}
|
|
if folderPayload["type"] != "folder" {
|
|
t.Fatalf("expected moved folder type folder, got %#v", folderPayload["type"])
|
|
}
|
|
}
|
|
|
|
func TestMoveProjectTreeFolderOnDiskMovesFolderToNewParent(t *testing.T) {
|
|
rootPath := filepath.Join(t.TempDir(), "POSIX")
|
|
service := NewService(nil, rootPath)
|
|
|
|
err := service.ensureBootstrapPOSIXSkeleton(
|
|
InstallationRecord{ID: "installation-1", Name: "MangoPig", Mode: "personal", Access: "local", Protocol: "http", Host: "localhost", IsBootstrapped: true},
|
|
AdminSummary{ID: "admin-1", Email: "ronald@example.com", DisplayName: "Ronald"},
|
|
namedRecord{ID: "org-1", Name: "Primary Organization", Slug: "primary-organization"},
|
|
namedRecord{ID: "dept-1", Name: "Primary Department", Slug: "primary-department"},
|
|
namedRecord{ID: "team-1", Name: "Primary Team", Slug: "primary-team"},
|
|
namedRecord{ID: "project-1", Name: "Primary Project", Slug: "primary-project"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("ensure bootstrap POSIX skeleton: %v", err)
|
|
}
|
|
|
|
docsPath, _, err := service.createProjectTreeFolderOnDisk("primary-project", "", "Docs")
|
|
if err != nil {
|
|
t.Fatalf("create docs folder: %v", err)
|
|
}
|
|
archivePath, _, err := service.createProjectTreeFolderOnDisk("primary-project", "", "Archive")
|
|
if err != nil {
|
|
t.Fatalf("create archive folder: %v", err)
|
|
}
|
|
|
|
previousPath, movedPath, err := service.moveProjectTreeFolderOnDisk("primary-project", docsPath, archivePath)
|
|
if err != nil {
|
|
t.Fatalf("moveProjectTreeFolderOnDisk: %v", err)
|
|
}
|
|
if previousPath != docsPath {
|
|
t.Fatalf("expected previous path %s, got %s", docsPath, previousPath)
|
|
}
|
|
if movedPath != "projects/project-primary-project/tree/folder-archive/children/folder-docs" {
|
|
t.Fatalf("unexpected moved path: %s", movedPath)
|
|
}
|
|
|
|
folderPayload := readStructuredFileForTest[map[string]any](t, filepath.Join(rootPath, filepath.FromSlash(movedPath), posixFolderFileName))
|
|
if folderPayload["name"] != "Docs" {
|
|
t.Fatalf("expected moved folder name Docs, got %#v", folderPayload["name"])
|
|
}
|
|
if folderPayload["slug"] != "docs" {
|
|
t.Fatalf("expected moved folder slug docs, got %#v", folderPayload["slug"])
|
|
}
|
|
}
|
|
|
|
func TestMoveProjectHierarchyFolderOnDiskRejectsDescendantTarget(t *testing.T) {
|
|
rootPath := filepath.Join(t.TempDir(), "POSIX")
|
|
service := NewService(nil, rootPath)
|
|
|
|
err := service.ensureBootstrapPOSIXSkeleton(
|
|
InstallationRecord{ID: "installation-1", Name: "MangoPig", Mode: "personal", Access: "local", Protocol: "http", Host: "localhost", IsBootstrapped: true},
|
|
AdminSummary{ID: "admin-1", Email: "ronald@example.com", DisplayName: "Ronald"},
|
|
namedRecord{ID: "org-1", Name: "Primary Organization", Slug: "primary-organization"},
|
|
namedRecord{ID: "dept-1", Name: "Primary Department", Slug: "primary-department"},
|
|
namedRecord{ID: "team-1", Name: "Primary Team", Slug: "primary-team"},
|
|
namedRecord{ID: "project-1", Name: "Primary Project", Slug: "primary-project"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("ensure bootstrap POSIX skeleton: %v", err)
|
|
}
|
|
|
|
parentPath, _, err := service.createProjectHierarchyFolderOnDisk("primary-project", "", "Parent")
|
|
if err != nil {
|
|
t.Fatalf("create parent folder: %v", err)
|
|
}
|
|
childPath, _, err := service.createProjectHierarchyFolderOnDisk("primary-project", parentPath, "Child")
|
|
if err != nil {
|
|
t.Fatalf("create child folder: %v", err)
|
|
}
|
|
|
|
_, _, err = service.moveProjectHierarchyFolderOnDisk("primary-project", parentPath, childPath)
|
|
if !errors.Is(err, ErrInvalidProjectFolderMove) {
|
|
t.Fatalf("expected ErrInvalidProjectFolderMove, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBuildProjectHierarchyFolderTreeBuildsNestedStructure(t *testing.T) {
|
|
rows := []projectHierarchyFolderRow{
|
|
{ID: "folder-design-id", Path: "projects/project-primary-project/children/folder-design", ParentPath: projectHierarchyRootPath("primary-project"), Label: "Design"},
|
|
{ID: "folder-research-id", Path: "projects/project-primary-project/children/folder-design/children/folder-research", ParentPath: "projects/project-primary-project/children/folder-design/children", Label: "Research"},
|
|
{ID: "folder-ops-id", Path: "projects/project-primary-project/children/folder-ops", ParentPath: projectHierarchyRootPath("primary-project"), Label: "Ops"},
|
|
}
|
|
|
|
folders := buildProjectHierarchyFolderTree(rows, projectHierarchyRootPath("primary-project"))
|
|
if len(folders) != 2 {
|
|
t.Fatalf("expected 2 top-level folders, got %d", len(folders))
|
|
}
|
|
if folders[0].Label != "Design" || folders[1].Label != "Ops" {
|
|
t.Fatalf("unexpected top-level folder labels: %#v", folders)
|
|
}
|
|
if len(folders[0].Children) != 1 || folders[0].Children[0].Label != "Research" {
|
|
t.Fatalf("unexpected nested folder structure: %#v", folders[0].Children)
|
|
}
|
|
if folders[0].ID != "folder-design-id" || folders[0].Path != "projects/project-primary-project/children/folder-design" {
|
|
t.Fatalf("expected design folder to retain stable id/path, got %#v", folders[0])
|
|
}
|
|
if folders[0].Children[0].ID != "folder-research-id" || folders[1].ID != "folder-ops-id" {
|
|
t.Fatalf("expected nested/top-level folder ids to be preserved, got %#v / %#v", folders[0].Children[0], folders[1])
|
|
}
|
|
}
|
|
|
|
func TestApplyProjectHierarchyFolderOrderingOrdersRootAndChildrenByStableID(t *testing.T) {
|
|
folders := []ProjectHierarchyFolderRecord{
|
|
{
|
|
ID: "folder-design-id",
|
|
Path: "projects/project-primary-project/children/folder-design",
|
|
Label: "Design",
|
|
Children: []ProjectHierarchyFolderRecord{
|
|
{ID: "folder-research-id", Path: "projects/project-primary-project/children/folder-design/children/folder-research", Label: "Research"},
|
|
{ID: "folder-assets-id", Path: "projects/project-primary-project/children/folder-design/children/folder-assets", Label: "Assets"},
|
|
},
|
|
},
|
|
{ID: "folder-ops-id", Path: "projects/project-primary-project/children/folder-ops", Label: "Ops"},
|
|
{ID: "folder-qa-id", Path: "projects/project-primary-project/children/folder-qa", Label: "QA"},
|
|
}
|
|
|
|
ordered := applyProjectHierarchyFolderOrdering(folders, map[string][]string{
|
|
projectFolderOrderRootKey: {"folder-qa-id", "folder-design-id"},
|
|
"folder-design-id": {"folder-assets-id", "folder-research-id"},
|
|
})
|
|
|
|
if len(ordered) != 3 {
|
|
t.Fatalf("expected 3 ordered root folders, got %d", len(ordered))
|
|
}
|
|
if ordered[0].ID != "folder-qa-id" || ordered[1].ID != "folder-design-id" || ordered[2].ID != "folder-ops-id" {
|
|
t.Fatalf("unexpected ordered root ids: %#v", ordered)
|
|
}
|
|
if len(ordered[1].Children) != 2 {
|
|
t.Fatalf("expected design folder children to be preserved, got %#v", ordered[1].Children)
|
|
}
|
|
if ordered[1].Children[0].ID != "folder-assets-id" || ordered[1].Children[1].ID != "folder-research-id" {
|
|
t.Fatalf("unexpected ordered child ids: %#v", ordered[1].Children)
|
|
}
|
|
}
|
|
|
|
func TestInsertFolderOrderReordersWithinSameParent(t *testing.T) {
|
|
folderOrder := map[string][]string{
|
|
projectFolderOrderRootKey: {"folder-a", "folder-b", "folder-c"},
|
|
}
|
|
|
|
insertFolderOrder(folderOrder, "", "folder-c", 0)
|
|
|
|
got := folderOrder[projectFolderOrderRootKey]
|
|
if len(got) != 3 || got[0] != "folder-c" || got[1] != "folder-a" || got[2] != "folder-b" {
|
|
t.Fatalf("unexpected reordered root children: %#v", got)
|
|
}
|
|
}
|
|
|
|
func asStringForTest(value any) string {
|
|
text, _ := value.(string)
|
|
return text
|
|
}
|
|
|
|
func readStructuredFileForTest[T any](t *testing.T, path string) T {
|
|
t.Helper()
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
|
|
var raw any
|
|
switch strings.ToLower(filepath.Ext(path)) {
|
|
case ".cbor":
|
|
if err := cbor.Unmarshal(data, &raw); err != nil {
|
|
t.Fatalf("unmarshal %s: %v", path, err)
|
|
}
|
|
case ".json":
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
t.Fatalf("unmarshal %s: %v", path, err)
|
|
}
|
|
case ".jsonc":
|
|
ast, err := hujson.Parse(data)
|
|
if err != nil {
|
|
t.Fatalf("parse jsonc %s: %v", path, err)
|
|
}
|
|
ast.Standardize()
|
|
if err := json.Unmarshal(ast.Pack(), &raw); err != nil {
|
|
t.Fatalf("unmarshal standardized %s: %v", path, err)
|
|
}
|
|
default:
|
|
t.Fatalf("unsupported structured test file %s", path)
|
|
}
|
|
|
|
normalizedBytes, err := json.Marshal(normalizeStructuredValue(raw))
|
|
if err != nil {
|
|
t.Fatalf("normalize %s: %v", path, err)
|
|
}
|
|
|
|
var payload T
|
|
if err := json.Unmarshal(normalizedBytes, &payload); err != nil {
|
|
t.Fatalf("decode normalized %s: %v", path, err)
|
|
}
|
|
|
|
return payload
|
|
}
|