rpm 0.23.4

A pure rust library for building and parsing RPMs
Documentation
"""Smoke tests for PackageBuilder, FileOptions, and BuildConfig."""

import os
import tempfile

from rpm_rs import (
    BuildConfig,
    CompressionType,
    FileFlags,
    FileOptions,
    Package,
    PackageBuilder,
    RpmFormat,
    Signer,
    Verifier,
)

from conftest import ASSETS, PRIVATE_KEY, PUBLIC_KEY

SOURCES = os.path.join(ASSETS, "SOURCES")
EXAMPLE_CONFIG = os.path.join(SOURCES, "example_config.toml")
EXAMPLE_SCRIPT = os.path.join(SOURCES, "multiplication_tables.py")


class TestBasicBuild:
    def test_minimal_package(self):
        b = PackageBuilder("test-pkg", "1.0.0", "MIT", "noarch")
        pkg = b.build()
        m = pkg.metadata
        assert m.name == "test-pkg"
        assert m.version == "1.0.0"
        assert m.license == "MIT"
        assert m.arch == "noarch"

    def test_with_metadata(self):
        b = PackageBuilder("test-pkg", "2.0", "Apache-2.0", "x86_64", "A test package")
        b.description("A longer description of the test package")
        b.release("3.fc40")
        b.epoch(1)
        b.url("https://example.com")
        b.vcs("https://github.com/example/repo")
        b.vendor("Test Vendor")
        b.packager("Test Packager")
        b.build_host("localhost")

        pkg = b.build()
        m = pkg.metadata
        assert m.summary == "A test package"
        assert m.description == "A longer description of the test package"
        assert m.release == "3.fc40"
        assert m.epoch == 1
        assert m.url == "https://example.com"
        assert m.vcs == "https://github.com/example/repo"
        assert m.vendor == "Test Vendor"
        assert m.packager == "Test Packager"
        assert m.build_host == "localhost"


class TestBuildConfig:
    def test_default_config(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.using_config(BuildConfig())
        pkg = b.build()
        assert pkg.metadata.name == "test"

    def test_v4_config(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.using_config(BuildConfig(format=RpmFormat.V4))
        pkg = b.build()
        assert pkg.metadata.name == "test"

    def test_compression(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.using_config(BuildConfig(compression=CompressionType.Gzip))
        pkg = b.build()
        assert pkg.metadata.payload_compressor == "Gzip"

    def test_compression_with_level(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.using_config(
            BuildConfig(compression=CompressionType.Gzip, compression_level=1)
        )
        pkg = b.build()
        assert pkg.metadata.payload_compressor == "Gzip"

    def test_source_date(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.using_config(BuildConfig(source_date=1_600_000_000))
        pkg = b.build()
        assert pkg.metadata.build_time == 1_600_000_000

    def test_reserved_space_custom(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.using_config(BuildConfig(reserved_space=8192))
        pkg = b.build()
        assert pkg.metadata.name == "test"

    def test_reserved_space_disabled(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.using_config(BuildConfig(reserved_space=0))
        pkg = b.build()
        assert pkg.metadata.name == "test"


class TestFiles:
    def test_with_file(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.with_file(EXAMPLE_CONFIG, FileOptions.new("/etc/test.toml"))
        pkg = b.build()
        paths = pkg.metadata.file_paths()
        assert "/etc/test.toml" in paths

    def test_with_file_contents(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.with_file_contents(b"hello world", FileOptions.new("/usr/share/test.txt"))
        pkg = b.build()
        paths = pkg.metadata.file_paths()
        assert "/usr/share/test.txt" in paths

    def test_with_file_permissions(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.with_file_contents(
            b"#!/bin/sh\necho hello",
            FileOptions.new("/usr/bin/test", permissions=0o755, user="testuser"),
        )
        pkg = b.build()
        entries = pkg.metadata.file_entries()
        entry = [e for e in entries if e.path == "/usr/bin/test"][0]
        assert entry.mode.permissions == 0o755
        assert entry.ownership.user == "testuser"

    def test_with_config_file(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.with_file_contents(
            b"key=value",
            FileOptions.new("/etc/test.conf", config=True, noreplace=True),
        )
        pkg = b.build()
        entries = pkg.metadata.file_entries()
        entry = [e for e in entries if e.path == "/etc/test.conf"][0]
        # CONFIG | NOREPLACE flags should be set
        assert int(entry.flags) != 0

    def test_with_doc_file(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.with_file_contents(
            b"README content",
            FileOptions.new("/usr/share/doc/test/README", doc=True),
        )
        pkg = b.build()
        entries = pkg.metadata.file_entries()
        entry = [e for e in entries if e.path == "/usr/share/doc/test/README"][0]
        assert int(entry.flags) & FileFlags.DOC

    def test_with_license_file(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.with_file_contents(
            b"MIT License...",
            FileOptions.new("/usr/share/licenses/test/LICENSE", license=True),
        )
        pkg = b.build()
        entries = pkg.metadata.file_entries()
        entry = [e for e in entries if e.path == "/usr/share/licenses/test/LICENSE"][0]
        assert int(entry.flags) & FileFlags.LICENSE

    def test_with_missingok(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.with_file_contents(
            b"optional",
            FileOptions.new("/etc/test.conf", config=True, missingok=True),
        )
        pkg = b.build()
        entries = pkg.metadata.file_entries()
        entry = [e for e in entries if e.path == "/etc/test.conf"][0]
        assert int(entry.flags) & FileFlags.MISSINGOK

    def test_with_artifact(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.with_file_contents(
            b"build log",
            FileOptions.new("/usr/share/test/build.log", artifact=True),
        )
        pkg = b.build()
        entries = pkg.metadata.file_entries()
        entry = [e for e in entries if e.path == "/usr/share/test/build.log"][0]
        assert int(entry.flags) & FileFlags.ARTIFACT

    def test_with_caps(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.with_file_contents(
            b"#!/bin/sh\necho hello",
            FileOptions.new("/usr/bin/test", caps="cap_net_admin=pe"),
        )
        pkg = b.build()
        entries = pkg.metadata.file_entries()
        entry = [e for e in entries if e.path == "/usr/bin/test"][0]
        assert entry.caps == "cap_net_admin=ep"

    def test_with_dir_entry(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.with_dir_entry(FileOptions.dir("/var/log/test"))
        pkg = b.build()
        paths = pkg.metadata.file_paths()
        assert "/var/log/test" in paths

    def test_with_symlink(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.with_symlink(FileOptions.symlink("/usr/bin/link", "/usr/bin/target"))
        pkg = b.build()
        paths = pkg.metadata.file_paths()
        assert "/usr/bin/link" in paths

    def test_with_ghost(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.with_ghost(FileOptions.ghost("/var/run/test.pid"))
        pkg = b.build()
        paths = pkg.metadata.file_paths()
        assert "/var/run/test.pid" in paths


class TestDependencies:
    def test_requires(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.requires("bash")
        b.requires("coreutils", "8.0")
        pkg = b.build()
        requires = pkg.metadata.requires()
        names = [r.name for r in requires]
        assert "bash" in names
        assert "coreutils" in names

    def test_provides(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.provides("test-capability", "1.0")
        pkg = b.build()
        provides = pkg.metadata.provides()
        names = [p.name for p in provides]
        assert "test-capability" in names

    def test_conflicts_obsoletes(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.conflicts("old-test")
        b.obsoletes("legacy-test", "0.9")
        pkg = b.build()
        assert any(c.name == "old-test" for c in pkg.metadata.conflicts())
        assert any(o.name == "legacy-test" for o in pkg.metadata.obsoletes())

    def test_weak_deps(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.recommends("optional-dep")
        b.suggests("nice-to-have")
        pkg = b.build()
        assert any(r.name == "optional-dep" for r in pkg.metadata.recommends())
        assert any(s.name == "nice-to-have" for s in pkg.metadata.suggests())


class TestScriptlets:
    def test_pre_install(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.pre_install_script("echo pre")
        pkg = b.build()
        script = pkg.metadata.pre_install_script()
        assert script.script == "echo pre"

    def test_post_install(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.post_install_script("echo post")
        pkg = b.build()
        script = pkg.metadata.post_install_script()
        assert script.script == "echo post"


class TestChangelog:
    def test_changelog(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.add_changelog_entry(
            "Test Author <test@example.com> - 1.0-1",
            "- Initial release",
            1_600_000_000,
        )
        pkg = b.build()
        entries = pkg.metadata.changelog_entries()
        assert len(entries) >= 1
        assert "Test Author" in entries[0].name
        assert "Initial release" in entries[0].description


class TestWriteAndReadBack:
    def test_roundtrip(self):
        b = PackageBuilder(
            "roundtrip-test", "1.0", "MIT", "noarch", "roundtrip test package"
        )
        b.with_file_contents(b"test content", FileOptions.new("/usr/share/test.txt"))
        pkg = b.build()

        with tempfile.NamedTemporaryFile(suffix=".rpm", delete=False) as f:
            path = f.name
        try:
            pkg.write_file(path)
            reopened = Package.open(path)
            assert reopened.metadata.name == "roundtrip-test"
            assert reopened.metadata.summary == "roundtrip test package"
            reopened.verify_digests()
        finally:
            os.unlink(path)


class TestSigning:
    def test_build_and_sign(self):
        with open(PRIVATE_KEY, "rb") as f:
            signer = Signer(f.read())
        with open(PUBLIC_KEY, "rb") as f:
            verifier = Verifier(f.read())

        b = PackageBuilder("signed-test", "1.0", "MIT", "noarch")
        pkg = b.build_and_sign(signer)
        pkg.verify_signature(verifier)


class TestBuilderReuse:
    def test_double_build_raises(self):
        b = PackageBuilder("test", "1.0", "MIT", "noarch")
        b.build()
        try:
            b.build()
            assert False, "Expected ValueError on double build"
        except ValueError as e:
            assert "already been consumed" in str(e)