package assetloader import ( "encoding/base64" "os" "path/filepath" "strings" "testing" ) func TestProcessEmbedDirectives_TextLiteralEscapesQuotes(t *testing.T) { dir := t.TempDir() sqlPath := filepath.Join(dir, "1_001_seed.sql") if err := os.WriteFile(filepath.Join(dir, "body.txt"), []byte("Line 1\nIt's fine"), 0o644); err != nil { t.Fatal(err) } got, err := ProcessEmbedDirectives(sqlPath, ` -- @embed: path=body.txt var=:body mode=text INSERT INTO notes (body) VALUES (:body); `) if err != nil { t.Fatalf("ProcessEmbedDirectives failed: %v", err) } if !strings.Contains(got, "VALUES ('Line 1\nIt''s fine');") { t.Fatalf("embedded SQL did not contain escaped text literal:\n%s", got) } if strings.Contains(got, "VALUES (:body);") { t.Fatalf("placeholder was not replaced:\n%s", got) } } func TestProcessEmbedDirectives_Base64Literal(t *testing.T) { dir := t.TempDir() sqlPath := filepath.Join(dir, "1_001_seed.sql") binary := []byte{0x00, 0xff, 0x10, 0x20} if err := os.WriteFile(filepath.Join(dir, "blob.bin"), binary, 0o644); err != nil { t.Fatal(err) } got, err := ProcessEmbedDirectives(sqlPath, ` -- @embed: path=blob.bin var=:payload mode=base64 INSERT INTO files (payload) VALUES (decode(:payload, 'base64')::bytea); `) if err != nil { t.Fatalf("ProcessEmbedDirectives failed: %v", err) } want := "decode('" + base64.StdEncoding.EncodeToString(binary) + "', 'base64')::bytea" if !strings.Contains(got, want) { t.Fatalf("embedded SQL did not contain base64 literal %q:\n%s", want, got) } } func TestProcessEmbedDirectives_RelativeToSQLFile(t *testing.T) { root := t.TempDir() sqlDir := filepath.Join(root, "nested", "seed") if err := os.MkdirAll(filepath.Join(sqlDir, "assets"), 0o755); err != nil { t.Fatal(err) } sqlPath := filepath.Join(sqlDir, "1_001_seed.sql") if err := os.WriteFile(filepath.Join(sqlDir, "assets", "body.txt"), []byte("relative body"), 0o644); err != nil { t.Fatal(err) } got, err := ProcessEmbedDirectives(sqlPath, ` -- @embed: path=assets/body.txt var=:body mode=text SELECT :body; `) if err != nil { t.Fatalf("ProcessEmbedDirectives failed: %v", err) } if !strings.Contains(got, "SELECT 'relative body';") { t.Fatalf("path was not resolved relative to SQL file:\n%s", got) } } func TestProcessEmbedDirectives_InvalidDirectiveAndFiles(t *testing.T) { dir := t.TempDir() sqlPath := filepath.Join(dir, "1_001_seed.sql") if err := os.WriteFile(filepath.Join(dir, "body.txt"), []byte("ok"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "binary.txt"), []byte{0xff, 0xfe}, 0o644); err != nil { t.Fatal(err) } tests := []struct { name string sql string }{ { name: "missing mode", sql: "-- @embed: path=body.txt var=:body\nSELECT :body;", }, { name: "invalid var", sql: "-- @embed: path=body.txt var=body mode=text\nSELECT :body;", }, { name: "missing file", sql: "-- @embed: path=missing.txt var=:body mode=text\nSELECT :body;", }, { name: "invalid utf8 text", sql: "-- @embed: path=binary.txt var=:body mode=text\nSELECT :body;", }, { name: "placeholder not found", sql: "-- @embed: path=body.txt var=:body mode=text\nSELECT 1;", }, { name: "unknown attribute", sql: "-- @embed: path=body.txt var=:body mode=text extra=yes\nSELECT :body;", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if _, err := ProcessEmbedDirectives(sqlPath, tt.sql); err == nil { t.Fatal("expected error, got nil") } }) } } func TestProcessEmbedDirectives_DoesNotReplacePlaceholderPrefix(t *testing.T) { dir := t.TempDir() sqlPath := filepath.Join(dir, "1_001_seed.sql") if err := os.WriteFile(filepath.Join(dir, "body.txt"), []byte("ok"), 0o644); err != nil { t.Fatal(err) } got, err := ProcessEmbedDirectives(sqlPath, ` -- @embed: path=body.txt var=:body mode=text SELECT :body, :body_extra; `) if err != nil { t.Fatalf("ProcessEmbedDirectives failed: %v", err) } if !strings.Contains(got, "SELECT 'ok', :body_extra;") { t.Fatalf("placeholder boundary was not respected:\n%s", got) } } func TestEmbedRejectsTraversalAbsolutePathsAndSymlinks(t *testing.T) { root := t.TempDir() scriptDir := filepath.Join(root, "scripts") if err := os.Mkdir(scriptDir, 0o755); err != nil { t.Fatal(err) } secret := filepath.Join(root, "secret.txt") if err := os.WriteFile(secret, []byte("never print this secret"), 0o600); err != nil { t.Fatal(err) } for _, path := range []string{"../secret.txt", secret, `..\secret.txt`, `C:\secret.txt`, `\\server\secret.txt`} { sql := "-- @embed: path='" + path + "' var=:body mode=text\nSELECT :body;" got, err := ProcessEmbedDirectives(filepath.Join(scriptDir, "seed.sql"), sql) if err == nil || got != "" { t.Fatalf("unsafe embed path accepted %q: %q %v", path, got, err) } if strings.Contains(err.Error(), "never print this secret") { t.Fatal("diagnostic leaked asset bytes") } } if err := os.Symlink(root, filepath.Join(scriptDir, "link")); err != nil { t.Skip(err) } if _, err := ProcessEmbedDirectives(filepath.Join(scriptDir, "seed.sql"), "-- @embed: path=link/secret.txt var=:body mode=text\nSELECT :body;"); err == nil { t.Fatal("symlink parent must be rejected") } if err := os.Symlink(secret, filepath.Join(scriptDir, "leaf.txt")); err != nil { t.Fatal(err) } if _, err := ProcessEmbedDirectives(filepath.Join(scriptDir, "seed.sql"), "-- @embed: path=leaf.txt var=:body mode=text\nSELECT :body;"); err == nil { t.Fatal("symlink asset must be rejected") } } func TestEmbedExplicitRootAndSizeLimit(t *testing.T) { root := t.TempDir() dir := filepath.Join(root, "scripts") if err := os.Mkdir(dir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "body.txt"), []byte("12345"), 0o600); err != nil { t.Fatal(err) } sql := "-- @embed: path=body.txt var=:body mode=text\nSELECT :body;" path := filepath.Join(dir, "seed.sql") if _, err := ProcessEmbedDirectivesWithOptions(path, sql, EmbedOptions{AssetRoot: root, MaxFileBytes: 4}); err == nil || !strings.Contains(err.Error(), "byte limit") { t.Fatalf("expected size limit error: %v", err) } if got, err := ProcessEmbedDirectivesWithOptions(path, sql, EmbedOptions{AssetRoot: root, MaxFileBytes: 5}); err != nil || !strings.Contains(got, "SELECT '12345';") { t.Fatalf("valid bounded embed: %q %v", got, err) } if _, err := ProcessEmbedDirectivesWithOptions(path, sql, EmbedOptions{AssetRoot: t.TempDir()}); err == nil { t.Fatal("source path outside explicit root must fail") } if _, err := ProcessEmbedDirectivesWithOptions(path, sql, EmbedOptions{MaxFileBytes: -1}); err == nil { t.Fatal("negative limits must fail") } } func TestManifestReadRechecksContainmentAndSize(t *testing.T) { root, external := t.TempDir(), t.TempDir() file := filepath.Join(root, "body.txt") if err := os.WriteFile(file, []byte("valid"), 0o600); err != nil { t.Fatal(err) } item := Item{AssetRoot: root, MaxFileBytes: 4, Entry: ManifestEntry{File: file}} if _, err := readItemAsset(item); err == nil { t.Fatal("manifest size bound must apply at execution") } item.MaxFileBytes = 5 if got, err := readItemAsset(item); err != nil || string(got) != "valid" { t.Fatalf("valid manifest asset: %q %v", got, err) } secret := filepath.Join(external, "secret.txt") if err := os.WriteFile(secret, []byte("secret"), 0o600); err != nil { t.Fatal(err) } if err := os.Remove(file); err != nil { t.Fatal(err) } if err := os.Symlink(secret, file); err != nil { t.Skip(err) } if _, err := readItemAsset(item); err == nil { t.Fatal("replacement symlink after scanning must fail") } item.Entry.File = secret if _, err := readItemAsset(item); err == nil { t.Fatal("forged asset path must fail") } } func TestEmbedDefaultLimitRejectsOversizedFile(t *testing.T) { dir := t.TempDir() file, err := os.Create(filepath.Join(dir, "large.bin")) if err != nil { t.Fatal(err) } if err := file.Truncate(DefaultMaxAssetBytes + 1); err != nil { file.Close() t.Fatal(err) } if err := file.Close(); err != nil { t.Fatal(err) } _, err = ProcessEmbedDirectives(filepath.Join(dir, "seed.sql"), "-- @embed: path=large.bin var=:payload mode=base64\nSELECT :payload;") if err == nil || !strings.Contains(err.Error(), "byte limit") { t.Fatalf("expected default size bound: %v", err) } }