package assetloader_test import ( "os" "path/filepath" "testing" "git.warky.dev/wdevs/relspecgo/pkg/assetloader" ) func TestLoadManifest_ValidList(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "assets.yaml", ` - file: hello.txt call: INSERT INTO files (name, data) VALUES (:filename, :bytes) - file: logo.png call: UPDATE branding SET logo = :bytes WHERE id = 1 `) writeFile(t, dir, "hello.txt", "hello world") writeFile(t, dir, "logo.png", "\x89PNG\r\n\x1a\n") m, err := assetloader.LoadManifest(dir) if err != nil { t.Fatalf("LoadManifest failed: %v", err) } if len(m) != 2 { t.Fatalf("expected 2 entries, got %d", len(m)) } if m[0].File != "hello.txt" { t.Errorf("entry 0 file: got %q, want %q", m[0].File, "hello.txt") } if m[1].File != "logo.png" { t.Errorf("entry 1 file: got %q, want %q", m[1].File, "logo.png") } } func TestLoadManifest_WithStaticParams(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "assets.yaml", ` - file: template.md call: INSERT INTO templates (owner_id, name, data) VALUES (:owner_id, :filename, :bytes) params: owner_id: "42" `) writeFile(t, dir, "template.md", "# Template") m, err := assetloader.LoadManifest(dir) if err != nil { t.Fatalf("LoadManifest failed: %v", err) } if len(m) != 1 { t.Fatalf("expected 1 entry, got %d", len(m)) } if m[0].Params["owner_id"] != "42" { t.Errorf("static param owner_id: got %q, want %q", m[0].Params["owner_id"], "42") } } func TestLoadManifest_MissingFile(t *testing.T) { dir := t.TempDir() // No assets.yaml present _, err := assetloader.LoadManifest(dir) if err == nil { t.Fatal("expected error for missing assets.yaml, got nil") } } func TestLoadManifest_InvalidYAML(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "assets.yaml", `{not: [valid yaml`) _, err := assetloader.LoadManifest(dir) if err == nil { t.Fatal("expected error for invalid YAML, got nil") } } func TestScanDir_FindsManifests(t *testing.T) { root := t.TempDir() // Directory named with priority-sequence pattern dir1 := filepath.Join(root, "1_010_seed_templates") if err := os.MkdirAll(dir1, 0o755); err != nil { t.Fatal(err) } writeFile(t, dir1, "assets.yaml", ` - file: a.txt call: INSERT INTO t (data) VALUES (:bytes) `) writeFile(t, dir1, "a.txt", "aaa") dir2 := filepath.Join(root, "2_001_branding") if err := os.MkdirAll(dir2, 0o755); err != nil { t.Fatal(err) } writeFile(t, dir2, "assets.yaml", ` - file: logo.png call: UPDATE branding SET logo = :bytes `) writeFile(t, dir2, "logo.png", "PNG") items, err := assetloader.ScanDir(root) if err != nil { t.Fatalf("ScanDir failed: %v", err) } if len(items) != 2 { t.Fatalf("expected 2 items, got %d", len(items)) } // Should be ordered by priority then sequence if items[0].Priority != 1 || items[0].Sequence != 10 { t.Errorf("item[0]: got priority=%d seq=%d, want 1,10", items[0].Priority, items[0].Sequence) } if items[1].Priority != 2 || items[1].Sequence != 1 { t.Errorf("item[1]: got priority=%d seq=%d, want 2,1", items[1].Priority, items[1].Sequence) } } func TestScanDir_OrdersByPriorityThenSequence(t *testing.T) { root := t.TempDir() for _, d := range []string{"2_002_b", "1_001_a", "2_001_c", "1_002_d"} { dir := filepath.Join(root, d) if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } writeFile(t, dir, "assets.yaml", ` - file: x.txt call: SELECT :bytes `) writeFile(t, dir, "x.txt", "x") } items, err := assetloader.ScanDir(root) if err != nil { t.Fatalf("ScanDir failed: %v", err) } if len(items) != 4 { t.Fatalf("expected 4 items, got %d", len(items)) } type ps struct{ p int; s uint } want := []ps{{1, 1}, {1, 2}, {2, 1}, {2, 2}} for i, w := range want { got := ps{items[i].Priority, items[i].Sequence} if got != w { t.Errorf("items[%d]: got {%d,%d}, want {%d,%d}", i, got.p, got.s, w.p, w.s) } } } func TestScanDir_SkipsSymlinks(t *testing.T) { root := t.TempDir() dir1 := filepath.Join(root, "1_001_real") if err := os.MkdirAll(dir1, 0o755); err != nil { t.Fatal(err) } writeFile(t, dir1, "assets.yaml", ` - file: a.txt call: SELECT :bytes `) writeFile(t, dir1, "a.txt", "real") // Symlink to an asset file - should be skipped during file read realFile := filepath.Join(root, "real.txt") writeFile(t, root, "real.txt", "symlink target") symlink := filepath.Join(dir1, "link.txt") if err := os.Symlink(realFile, symlink); err != nil { t.Skip("symlinks not supported:", err) } // Add a manifest entry that references the symlink writeFile(t, dir1, "assets.yaml", ` - file: a.txt call: SELECT :bytes - file: link.txt call: SELECT :bytes `) items, err := assetloader.ScanDir(root) if err != nil { t.Fatalf("ScanDir failed: %v", err) } // The symlink entry should be skipped; only a.txt should remain if len(items) != 1 { t.Fatalf("expected 1 item after symlink skip, got %d", len(items)) } if filepath.Base(items[0].Entry.File) != "a.txt" { t.Errorf("expected non-symlink entry, got %q", items[0].Entry.File) } } func TestScanDir_RejectsPathTraversal(t *testing.T) { root := t.TempDir() dir1 := filepath.Join(root, "1_001_evil") if err := os.MkdirAll(dir1, 0o755); err != nil { t.Fatal(err) } writeFile(t, dir1, "assets.yaml", ` - file: ../../etc/passwd call: SELECT :bytes `) items, err := assetloader.ScanDir(root) if err != nil { t.Fatalf("ScanDir failed: %v", err) } // Path traversal entry should be skipped if len(items) != 0 { t.Fatalf("expected 0 items after path traversal rejection, got %d", len(items)) } } func TestBuildQuery_BasicPlaceholders(t *testing.T) { sql, args, err := assetloader.BuildQuery( "INSERT INTO t (name, data) VALUES (:filename, :bytes)", []byte("hello"), "hello.txt", nil, ) if err != nil { t.Fatalf("BuildQuery failed: %v", err) } if sql != "INSERT INTO t (name, data) VALUES ($1, $2)" { t.Errorf("unexpected SQL: %s", sql) } if len(args) != 2 { t.Fatalf("expected 2 args, got %d", len(args)) } if string(args[0].(string)) != "hello.txt" { t.Errorf("args[0]: got %q, want %q", args[0], "hello.txt") } if string(args[1].([]byte)) != "hello" { t.Errorf("args[1]: got %v, want %v", args[1], []byte("hello")) } } func TestBuildQuery_StaticParams(t *testing.T) { sql, args, err := assetloader.BuildQuery( "INSERT INTO t (owner, name, data) VALUES (:owner_id, :filename, :bytes)", []byte("data"), "file.bin", map[string]string{"owner_id": "99"}, ) if err != nil { t.Fatalf("BuildQuery failed: %v", err) } if sql != "INSERT INTO t (owner, name, data) VALUES ($1, $2, $3)" { t.Errorf("unexpected SQL: %s", sql) } if len(args) != 3 { t.Fatalf("expected 3 args, got %d: %v", len(args), args) } if args[0].(string) != "99" { t.Errorf("args[0]: got %q, want %q", args[0], "99") } } func TestBuildQuery_RepeatedPlaceholder(t *testing.T) { sql, args, err := assetloader.BuildQuery( "SELECT length(:bytes), encode(:bytes, 'base64')", []byte("abc"), "f.bin", nil, ) if err != nil { t.Fatalf("BuildQuery failed: %v", err) } // :bytes appears twice but maps to same $1 if sql != "SELECT length($1), encode($1, 'base64')" { t.Errorf("unexpected SQL: %s", sql) } if len(args) != 1 { t.Fatalf("expected 1 arg, got %d", len(args)) } } func TestBuildQuery_PostgresCastNotMatched(t *testing.T) { // ::text should NOT be treated as a placeholder sql, args, err := assetloader.BuildQuery( "SELECT :bytes::text, :filename", []byte("data"), "f.txt", nil, ) if err != nil { t.Fatalf("BuildQuery failed: %v", err) } if sql != "SELECT $1::text, $2" { t.Errorf("unexpected SQL: %s", sql) } if len(args) != 2 { t.Fatalf("expected 2 args, got %d", len(args)) } } func TestBuildQuery_UnknownPlaceholder(t *testing.T) { _, _, err := assetloader.BuildQuery( "SELECT :unknown_param", []byte("data"), "f.txt", nil, ) if err == nil { t.Fatal("expected error for unknown placeholder, got nil") } } func TestBuildQuery_BinaryFileByteExact(t *testing.T) { // Binary data with null bytes, high bytes - must pass through unchanged binary := []byte{0x00, 0xFF, 0x80, 0x01, 0xFE} _, args, err := assetloader.BuildQuery( "INSERT INTO blobs (data) VALUES (:bytes)", binary, "blob.bin", nil, ) if err != nil { t.Fatalf("BuildQuery failed: %v", err) } if len(args) != 1 { t.Fatalf("expected 1 arg") } got := args[0].([]byte) if len(got) != len(binary) { t.Fatalf("byte count: got %d, want %d", len(got), len(binary)) } for i, b := range binary { if got[i] != b { t.Errorf("byte[%d]: got 0x%02x, want 0x%02x", i, got[i], b) } } } // writeFile is a test helper that writes content to a file. func writeFile(t *testing.T, dir, name, content string) { t.Helper() if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil { t.Fatalf("writeFile %s: %v", name, err) } }