Repository: osbuild/osbuild-composer
Pull request #126 (2019-11-27 18:04:20)

distro/f30: rework in the same way as RHEL8.2
The produced pipelines are unchanged.

Signed-off-by: Tom Gundersen <teg@jklm.no>


Patch:
diff --git a/internal/distro/fedora30/ami.go b/internal/distro/fedora30/ami.go
deleted file mode 100644
index ed17f4c45..000000000
--- a/internal/distro/fedora30/ami.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package fedora30
-
-import (
-	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
-)
-
-type amiOutput struct{}
-
-func (t *amiOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
-	packages := [...]string{
-		"@Core",
-		"chrony",
-		"kernel",
-		"selinux-policy-targeted",
-		"grub2-pc",
-		"langpacks-en",
-		"libxcrypt-compat",
-		"xfsprogs",
-		"cloud-init",
-		"checkpolicy",
-		"net-tools",
-	}
-	excludedPackages := [...]string{
-		"dracut-config-rescue",
-	}
-	p := newF30Pipeline(packages[:], excludedPackages[:], b)
-	err := customizeAll(p, b.Customizations)
-	if err != nil {
-		return nil, err
-	}
-	setFilesystems(p)
-	setBootloader(p, "ro no_timer_check console=ttyS0,115200n8 console=tty1 biosdevname=0 net.ifnames=0 console=ttyS0,115200", b)
-	setFirewall(p, nil, nil, b)
-	setServices(p, []string{"cloud-init.service"}, nil, b)
-	setQemuAssembler(p, "raw", t.getName())
-
-	return p, nil
-}
-
-func (t *amiOutput) getName() string {
-	return "image.ami"
-}
-
-func (t *amiOutput) getMime() string {
-	return "application/octet-stream"
-}
diff --git a/internal/distro/fedora30/customizations.go b/internal/distro/fedora30/customizations.go
deleted file mode 100644
index e2362e3dd..000000000
--- a/internal/distro/fedora30/customizations.go
+++ /dev/null
@@ -1,234 +0,0 @@
-package fedora30
-
-import (
-	"log"
-	"strconv"
-	"strings"
-
-	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	"github.com/osbuild/osbuild-composer/internal/crypt"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
-)
-
-func customizeAll(p *pipeline.Pipeline, c *blueprint.Customizations) error {
-	if c == nil {
-		c = &blueprint.Customizations{}
-	}
-	customizeHostname(p, c)
-	customizeGroup(p, c)
-	if err := customizeUserAndSSHKey(p, c); err != nil {
-		return err
-	}
-	customizeTimezone(p, c)
-	customizeNTPServers(p, c)
-	customizeLocale(p, c)
-	customizeKeyboard(p, c)
-
-	return nil
-}
-
-func customizeHostname(p *pipeline.Pipeline, c *blueprint.Customizations) {
-	if c.Hostname == nil {
-		return
-	}
-
-	p.AddStage(
-		pipeline.NewHostnameStage(
-			&pipeline.HostnameStageOptions{Hostname: *c.Hostname},
-		),
-	)
-}
-
-func customizeGroup(p *pipeline.Pipeline, c *blueprint.Customizations) {
-	if len(c.Group) == 0 {
-		return
-	}
-
-	groups := make(map[string]pipeline.GroupsStageOptionsGroup)
-	for _, group := range c.Group {
-		if userCustomizationsContainUsername(c.User, group.Name) {
-			log.Println("group with name ", group.Name, " was not created, because user with same name was defined!")
-			continue
-		}
-
-		groupData := pipeline.GroupsStageOptionsGroup{}
-		if group.GID != nil {
-			gid := strconv.Itoa(*group.GID)
-			groupData.GID = &gid
-		}
-
-		groups[group.Name] = groupData
-	}
-
-	p.AddStage(
-		pipeline.NewGroupsStage(
-			&pipeline.GroupsStageOptions{Groups: groups},
-		),
-	)
-}
-
-func assertAllUsersExistForSSHCustomizations(c *blueprint.Customizations) error {
-	for _, sshkey := range c.SSHKey {
-		userFound := false
-		for _, user := range c.User {
-			if user.Name == sshkey.User {
-				userFound = true
-			}
-		}
-
-		if !userFound {
-			return &blueprint.CustomizationError{"Cannot set SSH key for non-existing user " + sshkey.User}
-		}
-	}
-	return nil
-}
-
-func customizeUserAndSSHKey(p *pipeline.Pipeline, c *blueprint.Customizations) error {
-	if len(c.User) == 0 {
-		if len(c.SSHKey) > 0 {
-			return &blueprint.CustomizationError{"SSH key customization defined but no user customizations are defined"}
-		}
-
-		return nil
-	}
-
-	// return error if ssh key customization without user defined in user customization if found
-	if e := assertAllUsersExistForSSHCustomizations(c); e != nil {
-		return e
-	}
-
-	users := make(map[string]pipeline.UsersStageOptionsUser)
-	for _, user := range c.User {
-
-		if user.Password != nil && !crypt.PasswordIsCrypted(*user.Password) {
-			cryptedPassword, err := crypt.CryptSHA512(*user.Password)
-			if err != nil {
-				return err
-			}
-
-			user.Password = &cryptedPassword
-		}
-
-		userData := pipeline.UsersStageOptionsUser{
-			Groups:      user.Groups,
-			Description: user.Description,
-			Home:        user.Home,
-			Shell:       user.Shell,
-			Password:    user.Password,
-			Key:         user.Key,
-		}
-
-		if user.UID != nil {
-			uid := strconv.Itoa(*user.UID)
-			userData.UID = &uid
-		}
-
-		if user.GID != nil {
-			gid := strconv.Itoa(*user.GID)
-			userData.GID = &gid
-		}
-
-		// process sshkey customizations
-		if additionalKeys := findKeysForUser(c.SSHKey, user.Name); len(additionalKeys) > 0 {
-			joinedKeys := strings.Join(additionalKeys, "\n")
-
-			if userData.Key != nil {
-				*userData.Key += "\n" + joinedKeys
-			} else {
-				userData.Key = &joinedKeys
-			}
-		}
-
-		users[user.Name] = userData
-	}
-
-	p.AddStage(
-		pipeline.NewUsersStage(
-			&pipeline.UsersStageOptions{Users: users},
-		),
-	)
-
-	return nil
-}
-
-func customizeTimezone(p *pipeline.Pipeline, c *blueprint.Customizations) {
-	if c.Timezone == nil || c.Timezone.Timezone == nil {
-		return
-	}
-
-	// TODO: lorax (anaconda) automatically installs chrony if timeservers are defined
-	// except for the case when chrony is explicitly removed from installed packages (using -chrony)
-	// this is currently not supported, no checks whether chrony is installed are not performed
-
-	p.AddStage(
-		pipeline.NewTimezoneStage(&pipeline.TimezoneStageOptions{
-			Zone: *c.Timezone.Timezone,
-		}),
-	)
-}
-
-func customizeNTPServers(p *pipeline.Pipeline, c *blueprint.Customizations) {
-	if c.Timezone == nil || len(c.Timezone.NTPServers) == 0 {
-		return
-	}
-
-	p.AddStage(
-		pipeline.NewChronyStage(&pipeline.ChronyStageOptions{
-			Timeservers: c.Timezone.NTPServers,
-		}),
-	)
-}
-
-func customizeLocale(p *pipeline.Pipeline, c *blueprint.Customizations) {
-	locale := c.Locale
-	if locale == nil {
-		locale = &blueprint.LocaleCustomization{
-			Languages: []string{
-				"en_US",
-			},
-		}
-	}
-
-	// TODO: you can specify more languages in customization
-	// The first one is the primary one, we can set in the locale stage, this should currently work
-	// Also, ALL the listed languages are installed using langpack-* packages
-	// This is currently not implemented!
-	// See anaconda src: pyanaconda/payload/dnfpayload.py:772
-
-	p.AddStage(
-		pipeline.NewLocaleStage(&pipeline.LocaleStageOptions{
-			Language: locale.Languages[0],
-		}),
-	)
-}
-
-func customizeKeyboard(p *pipeline.Pipeline, c *blueprint.Customizations) {
-	if c.Locale == nil || c.Locale.Keyboard == nil {
-		return
-	}
-
-	p.AddStage(
-		pipeline.NewKeymapStage(&pipeline.KeymapStageOptions{
-			Keymap: *c.Locale.Keyboard,
-		}),
-	)
-}
-
-func findKeysForUser(sshKeyCustomizations []blueprint.SSHKeyCustomization, user string) (keys []string) {
-	for _, sshKey := range sshKeyCustomizations {
-		if sshKey.User == user {
-			keys = append(keys, sshKey.Key)
-		}
-	}
-	return
-}
-
-func userCustomizationsContainUsername(userCustomizations []blueprint.UserCustomization, name string) bool {
-	for _, usr := range userCustomizations {
-		if usr.Name == name {
-			return true
-		}
-	}
-
-	return false
-}
diff --git a/internal/distro/fedora30/disk.go b/internal/distro/fedora30/disk.go
deleted file mode 100644
index 617f77f72..000000000
--- a/internal/distro/fedora30/disk.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package fedora30
-
-import (
-	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
-)
-
-type diskOutput struct{}
-
-func (t *diskOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
-	packages := [...]string{
-		"@core",
-		"chrony",
-		"firewalld",
-		"grub2-pc",
-		"kernel",
-		"langpacks-en",
-		"selinux-policy-targeted",
-	}
-	excludedPackages := [...]string{
-		"dracut-config-rescue",
-	}
-	p := newF30Pipeline(packages[:], excludedPackages[:], b)
-	err := customizeAll(p, b.Customizations)
-	if err != nil {
-		return nil, err
-	}
-	setFilesystems(p)
-	setBootloader(p, "ro biosdevname=0 net.ifnames=0", b)
-	setFirewall(p, nil, nil, b)
-	setServices(p, nil, nil, b)
-	setQemuAssembler(p, "raw", t.getName())
-
-	return p, nil
-}
-
-func (t *diskOutput) getName() string {
-	return "disk.img"
-}
-
-func (t *diskOutput) getMime() string {
-	return "application/octet-stream"
-}
diff --git a/internal/distro/fedora30/distro.go b/internal/distro/fedora30/distro.go
new file mode 100644
index 000000000..6452dd805
--- /dev/null
+++ b/internal/distro/fedora30/distro.go
@@ -0,0 +1,536 @@
+package fedora30
+
+import (
+	"sort"
+	"strconv"
+
+	"github.com/google/uuid"
+
+	"github.com/osbuild/osbuild-composer/internal/blueprint"
+	"github.com/osbuild/osbuild-composer/internal/crypt"
+	"github.com/osbuild/osbuild-composer/internal/distro"
+	"github.com/osbuild/osbuild-composer/internal/pipeline"
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
+)
+
+type Fedora30 struct {
+	outputs map[string]output
+}
+
+type output struct {
+	Name             string
+	MimeType         string
+	Packages         []string
+	ExcludedPackages []string
+	EnabledServices  []string
+	DisabledServices []string
+	KernelOptions    string
+	IncludeFSTab     bool
+	Assembler        *pipeline.Assembler
+}
+
+func init() {
+	r := Fedora30{
+		outputs: map[string]output{},
+	}
+
+	r.outputs["ami"] = output{
+		Name:     "image.ami",
+		MimeType: "application/octet-stream",
+		Packages: []string{
+			"@Core",
+			"chrony",
+			"kernel",
+			"selinux-policy-targeted",
+			"grub2-pc",
+			"langpacks-en",
+			"libxcrypt-compat",
+			"xfsprogs",
+			"cloud-init",
+			"checkpolicy",
+			"net-tools",
+		},
+		ExcludedPackages: []string{
+			"dracut-config-rescue",
+		},
+		EnabledServices: []string{
+			"cloud-init.service",
+		},
+		KernelOptions: "ro no_timer_check console=ttyS0,115200n8 console=tty1 biosdevname=0 net.ifnames=0 console=ttyS0,115200",
+		IncludeFSTab:  true,
+		Assembler:     r.qemuAssembler("raw", "image.ami"),
+	}
+
+	r.outputs["ext4-filesystem"] = output{
+		Name:     "filesystem.img",
+		MimeType: "application/octet-stream",
+		Packages: []string{
+			"policycoreutils",
+			"selinux-policy-targeted",
+			"kernel",
+			"firewalld",
+			"chrony",
+			"langpacks-en",
+		},
+		ExcludedPackages: []string{
+			"dracut-config-rescue",
+		},
+		KernelOptions: "ro biosdevname=0 net.ifnames=0",
+		IncludeFSTab:  false,
+		Assembler:     r.rawFSAssembler("filesystem.img"),
+	}
+
+	r.outputs["partitioned-disk"] = output{
+		Name:     "disk.img",
+		MimeType: "application/octet-stream",
+		Packages: []string{
+			"@core",
+			"chrony",
+			"firewalld",
+			"grub2-pc",
+			"kernel",
+			"langpacks-en",
+			"selinux-policy-targeted",
+		},
+		ExcludedPackages: []string{
+			"dracut-config-rescue",
+		},
+		KernelOptions: "ro biosdevname=0 net.ifnames=0",
+		IncludeFSTab:  true,
+		Assembler:     r.qemuAssembler("raw", "disk.img"),
+	}
+
+	r.outputs["qcow2"] = output{
+		Name:     "image.qcow2",
+		MimeType: "application/x-qemu-disk",
+		Packages: []string{
+			"kernel-core",
+			"@Fedora Cloud Server",
+			"chrony",
+			"polkit",
+			"systemd-udev",
+			"selinux-policy-targeted",
+			"grub2-pc",
+			"langpacks-en",
+		},
+		ExcludedPackages: []string{
+			"dracut-config-rescue",
+			"etables",
+			"firewalld",
+			"gobject-introspection",
+			"plymouth",
+		},
+		KernelOptions: "ro biosdevname=0 net.ifnames=0",
+		IncludeFSTab:  true,
+		Assembler:     r.qemuAssembler("qcow2", "image.qcow2"),
+	}
+
+	r.outputs["openstack"] = output{
+		Name:     "image.qcow2",
+		MimeType: "application/x-qemu-disk",
+		Packages: []string{
+			"@Core",
+			"chrony",
+			"kernel",
+			"selinux-policy-targeted",
+			"grub2-pc",
+			"spice-vdagent",
+			"qemu-guest-agent",
+			"xen-libs",
+			"langpacks-en",
+			"cloud-init",
+			"libdrm",
+		},
+		ExcludedPackages: []string{
+			"dracut-config-rescue",
+		},
+		KernelOptions: "ro biosdevname=0 net.ifnames=0",
+		IncludeFSTab:  true,
+		Assembler:     r.qemuAssembler("qcow2", "image.qcow2"),
+	}
+
+	r.outputs["tar"] = output{
+		Name:     "root.tar.xz",
+		MimeType: "application/x-tar",
+		Packages: []string{
+			"policycoreutils",
+			"selinux-policy-targeted",
+			"kernel",
+			"firewalld",
+			"chrony",
+			"langpacks-en",
+		},
+		ExcludedPackages: []string{
+			"dracut-config-rescue",
+		},
+		KernelOptions: "ro biosdevname=0 net.ifnames=0",
+		IncludeFSTab:  false,
+		Assembler:     r.tarAssembler("root.tar.xz", "xz"),
+	}
+
+	r.outputs["vhd"] = output{
+		Name:     "image.vhd",
+		MimeType: "application/x-vhd",
+		Packages: []string{
+			"@Core",
+			"chrony",
+			"kernel",
+			"selinux-policy-targeted",
+			"grub2-pc",
+			"langpacks-en",
+			"net-tools",
+			"ntfsprogs",
+			"WALinuxAgent",
+			"libxcrypt-compat",
+		},
+		ExcludedPackages: []string{
+			"dracut-config-rescue",
+		},
+		KernelOptions: "ro biosdevname=0 net.ifnames=0",
+		IncludeFSTab:  true,
+		Assembler:     r.qemuAssembler("vpc", "image.vhd"),
+	}
+
+	r.outputs["vmdk"] = output{
+		Name:     "disk.vmdk",
+		MimeType: "application/x-vmdk",
+		Packages: []string{
+			"@core",
+			"chrony",
+			"firewalld",
+			"grub2-pc",
+			"kernel",
+			"langpacks-en",
+			"open-vm-tools",
+			"selinux-policy-targeted",
+		},
+		ExcludedPackages: []string{
+			"dracut-config-rescue",
+		},
+		KernelOptions: "ro biosdevname=0 net.ifnames=0",
+		IncludeFSTab:  true,
+		Assembler:     r.qemuAssembler("vmdk", "disk.vmdk"),
+	}
+
+	distro.Register("fedora-30", &r)
+}
+
+func (r *Fedora30) Repositories() []rpmmd.RepoConfig {
+	return []rpmmd.RepoConfig{
+		{
+			Id:       "fedora",
+			Name:     "Fedora 30",
+			Metalink: "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+			Checksum: "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97",
+			GPGKey: `-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm
+bbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75
+L+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy
+KJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R
+n7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO
+4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53
+2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc
+YKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq
+SDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ
+g0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3
+DlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB
+tDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v
+cmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK
+CRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac
+g9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8
+f79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va
+N9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D
+K07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ
+Ox5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o
+8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml
+SMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7
++zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7
+CxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O
+pMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==
+=BfZ/
+-----END PGP PUBLIC KEY BLOCK-----
+`,
+		},
+	}
+}
+
+func (r *Fedora30) ListOutputFormats() []string {
+	formats := make([]string, 0, len(r.outputs))
+	for name := range r.outputs {
+		formats = append(formats, name)
+	}
+	sort.Strings(formats)
+	return formats
+}
+
+func (r *Fedora30) FilenameFromType(outputFormat string) (string, string, error) {
+	if output, exists := r.outputs[outputFormat]; exists {
+		return output.Name, output.MimeType, nil
+	}
+	return "", "", &distro.InvalidOutputFormatError{outputFormat}
+}
+
+func (r *Fedora30) Pipeline(b *blueprint.Blueprint, outputFormat string) (*pipeline.Pipeline, error) {
+	output, exists := r.outputs[outputFormat]
+	if !exists {
+		return nil, &distro.InvalidOutputFormatError{outputFormat}
+	}
+
+	p := &pipeline.Pipeline{}
+	p.SetBuild(r.buildPipeline(), "org.osbuild.fedora30")
+
+	packages := append(output.Packages, b.GetPackages()...)
+	p.AddStage(pipeline.NewDNFStage(r.dnfStageOptions(packages, output.ExcludedPackages)))
+	p.AddStage(pipeline.NewFixBLSStage())
+
+	// TODO support setting all languages and install corresponding langpack-* package
+	language, keyboard := b.GetPrimaryLocale()
+
+	if language != nil {
+		p.AddStage(pipeline.NewLocaleStage(&pipeline.LocaleStageOptions{*language}))
+	} else {
+		p.AddStage(pipeline.NewLocaleStage(&pipeline.LocaleStageOptions{"en_US"}))
+	}
+
+	if keyboard != nil {
+		p.AddStage(pipeline.NewKeymapStage(&pipeline.KeymapStageOptions{*keyboard}))
+	}
+
+	if hostname := b.GetHostname(); hostname != nil {
+		p.AddStage(pipeline.NewHostnameStage(&pipeline.HostnameStageOptions{*hostname}))
+	}
+
+	timezone, ntpServers := b.GetTimezoneSettings()
+
+	// TODO install chrony when this is set?
+	if timezone != nil {
+		p.AddStage(pipeline.NewTimezoneStage(&pipeline.TimezoneStageOptions{*timezone}))
+	}
+
+	if len(ntpServers) > 0 {
+		p.AddStage(pipeline.NewChronyStage(&pipeline.ChronyStageOptions{ntpServers}))
+	}
+
+	if users := b.GetUsers(); len(users) > 0 {
+		options, err := r.userStageOptions(users)
+		if err != nil {
+			return nil, err
+		}
+		p.AddStage(pipeline.NewUsersStage(options))
+	}
+
+	if groups := b.GetGroups(); len(groups) > 0 {
+		p.AddStage(pipeline.NewGroupsStage(r.groupStageOptions(groups)))
+	}
+
+	if output.IncludeFSTab {
+		p.AddStage(pipeline.NewFSTabStage(r.fsTabStageOptions()))
+	}
+	p.AddStage(pipeline.NewGRUB2Stage(r.grub2StageOptions(output.KernelOptions, b.GetKernel())))
+
+	if services := b.GetServices(); services != nil || output.EnabledServices != nil {
+		p.AddStage(pipeline.NewSystemdStage(r.systemdStageOptions(output.EnabledServices, output.DisabledServices, services)))
+	}
+
+	if firewall := b.GetFirewall(); firewall != nil {
+		p.AddStage(pipeline.NewFirewallStage(r.firewallStageOptions(firewall)))
+	}
+
+	p.AddStage(pipeline.NewSELinuxStage(r.selinuxStageOptions()))
+	p.Assembler = output.Assembler
+
+	return p, nil
+}
+
+func (r *Fedora30) buildPipeline() *pipeline.Pipeline {
+	packages := []string{
+		"dnf",
+		"e2fsprogs",
+		"policycoreutils",
+		"qemu-img",
+		"systemd",
+		"grub2-pc",
+		"tar",
+	}
+	p := &pipeline.Pipeline{}
+	p.AddStage(pipeline.NewDNFStage(r.dnfStageOptions(packages, nil)))
+	return p
+}
+
+func (r *Fedora30) dnfStageOptions(packages, excludedPackages []string) *pipeline.DNFStageOptions {
+	options := &pipeline.DNFStageOptions{
+		ReleaseVersion:   "30",
+		BaseArchitecture: "x86_64",
+	}
+	for _, repo := range r.Repositories() {
+		options.AddRepository(&pipeline.DNFRepository{
+			BaseURL:    repo.BaseURL,
+			MetaLink:   repo.Metalink,
+			MirrorList: repo.MirrorList,
+			Checksum:   repo.Checksum,
+			GPGKey:     repo.GPGKey,
+		})
+	}
+
+	for _, pkg := range packages {
+		options.AddPackage(pkg)
+	}
+
+	for _, pkg := range excludedPackages {
+		options.ExcludePackage(pkg)
+	}
+
+	return options
+}
+
+func (r *Fedora30) userStageOptions(users []blueprint.UserCustomization) (*pipeline.UsersStageOptions, error) {
+	options := pipeline.UsersStageOptions{
+		Users: make(map[string]pipeline.UsersStageOptionsUser),
+	}
+
+	for _, c := range users {
+		if c.Password != nil && !crypt.PasswordIsCrypted(*c.Password) {
+			cryptedPassword, err := crypt.CryptSHA512(*c.Password)
+			if err != nil {
+				return nil, err
+			}
+
+			c.Password = &cryptedPassword
+		}
+
+		user := pipeline.UsersStageOptionsUser{
+			Groups:      c.Groups,
+			Description: c.Description,
+			Home:        c.Home,
+			Shell:       c.Shell,
+			Password:    c.Password,
+			Key:         c.Key,
+		}
+
+		if c.UID != nil {
+			uid := strconv.Itoa(*c.UID)
+			user.UID = &uid
+		}
+
+		if c.GID != nil {
+			gid := strconv.Itoa(*c.GID)
+			user.GID = &gid
+		}
+
+		options.Users[c.Name] = user
+	}
+
+	return &options, nil
+}
+
+func (r *Fedora30) groupStageOptions(groups []blueprint.GroupCustomization) *pipeline.GroupsStageOptions {
+	options := pipeline.GroupsStageOptions{
+		Groups: map[string]pipeline.GroupsStageOptionsGroup{},
+	}
+
+	for _, group := range groups {
+		groupData := pipeline.GroupsStageOptionsGroup{
+			Name: group.Name,
+		}
+		if group.GID != nil {
+			gid := strconv.Itoa(*group.GID)
+			groupData.GID = &gid
+		}
+
+		options.Groups[group.Name] = groupData
+	}
+
+	return &options
+}
+
+func (r *Fedora30) firewallStageOptions(firewall *blueprint.FirewallCustomization) *pipeline.FirewallStageOptions {
+	options := pipeline.FirewallStageOptions{
+		Ports: firewall.Ports,
+	}
+
+	if firewall.Services != nil {
+		options.EnabledServices = firewall.Services.Enabled
+		options.DisabledServices = firewall.Services.Disabled
+	}
+
+	return &options
+}
+
+func (r *Fedora30) systemdStageOptions(enabledServices, disabledServices []string, s *blueprint.ServicesCustomization) *pipeline.SystemdStageOptions {
+	if s != nil {
+		enabledServices = append(enabledServices, s.Enabled...)
+		enabledServices = append(disabledServices, s.Disabled...)
+	}
+	return &pipeline.SystemdStageOptions{
+		EnabledServices:  enabledServices,
+		DisabledServices: disabledServices,
+	}
+}
+
+func (r *Fedora30) fsTabStageOptions() *pipeline.FSTabStageOptions {
+	id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
+	if err != nil {
+		panic("invalid UUID")
+	}
+	options := pipeline.FSTabStageOptions{}
+	options.AddFilesystem(id, "ext4", "/", "defaults", 1, 1)
+	return &options
+}
+
+func (r *Fedora30) grub2StageOptions(kernelOptions string, kernel *blueprint.KernelCustomization) *pipeline.GRUB2StageOptions {
+	id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
+	if err != nil {
+		panic("invalid UUID")
+	}
+
+	if kernel != nil {
+		kernelOptions += " " + kernel.Append
+	}
+
+	return &pipeline.GRUB2StageOptions{
+		RootFilesystemUUID: id,
+		KernelOptions:      kernelOptions,
+	}
+}
+
+func (r *Fedora30) selinuxStageOptions() *pipeline.SELinuxStageOptions {
+	return &pipeline.SELinuxStageOptions{
+		FileContexts: "etc/selinux/targeted/contexts/files/file_contexts",
+	}
+}
+
+func (r *Fedora30) qemuAssembler(format string, filename string) *pipeline.Assembler {
+	id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
+	if err != nil {
+		panic("invalid UUID")
+	}
+	return pipeline.NewQEMUAssembler(
+		&pipeline.QEMUAssemblerOptions{
+			Format:             format,
+			Filename:           filename,
+			PTUUID:             "0x14fc63d2",
+			RootFilesystemUUDI: id,
+			Size:               3222274048,
+		})
+}
+
+func (r *Fedora30) tarAssembler(filename, compression string) *pipeline.Assembler {
+	return pipeline.NewTarAssembler(
+		&pipeline.TarAssemblerOptions{
+			Filename: filename,
+		})
+}
+
+func (r *Fedora30) rawFSAssembler(filename string) *pipeline.Assembler {
+	id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
+	if err != nil {
+		panic("invalid UUID")
+	}
+	return pipeline.NewRawFSAssembler(
+		&pipeline.RawFSAssemblerOptions{
+			Filename:           filename,
+			RootFilesystemUUDI: id,
+			Size:               3222274048,
+		})
+}
diff --git a/internal/distro/fedora30/os_test.go b/internal/distro/fedora30/distro_test.go
similarity index 100%
rename from internal/distro/fedora30/os_test.go
rename to internal/distro/fedora30/distro_test.go
diff --git a/internal/distro/fedora30/ext4.go b/internal/distro/fedora30/ext4.go
deleted file mode 100644
index ffa381e64..000000000
--- a/internal/distro/fedora30/ext4.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package fedora30
-
-import (
-	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
-)
-
-type ext4Output struct{}
-
-func (t *ext4Output) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
-	packages := [...]string{
-		"policycoreutils",
-		"selinux-policy-targeted",
-		"kernel",
-		"firewalld",
-		"chrony",
-		"langpacks-en",
-	}
-	excludedPackages := [...]string{
-		"dracut-config-rescue",
-	}
-	p := newF30Pipeline(packages[:], excludedPackages[:], b)
-	err := customizeAll(p, b.Customizations)
-	if err != nil {
-		return nil, err
-	}
-	setBootloader(p, "ro biosdevname=0 net.ifnames=0", b)
-	setFirewall(p, nil, nil, b)
-	setServices(p, nil, nil, b)
-	setRawFSAssembler(p, t.getName())
-
-	return p, nil
-}
-
-func (t *ext4Output) getName() string {
-	return "filesystem.img"
-}
-
-func (t *ext4Output) getMime() string {
-	return "application/octet-stream"
-}
diff --git a/internal/distro/fedora30/helpers.go b/internal/distro/fedora30/helpers.go
deleted file mode 100644
index 327968b98..000000000
--- a/internal/distro/fedora30/helpers.go
+++ /dev/null
@@ -1,221 +0,0 @@
-package fedora30
-
-import (
-	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
-
-	"github.com/google/uuid"
-)
-
-var f30GPGKey string = `-----BEGIN PGP PUBLIC KEY BLOCK-----
-
-mQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm
-bbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75
-L+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy
-KJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R
-n7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO
-4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53
-2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc
-YKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq
-SDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ
-g0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3
-DlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB
-tDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v
-cmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK
-CRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac
-g9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8
-f79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va
-N9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D
-K07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ
-Ox5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o
-8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml
-SMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7
-+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7
-CxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O
-pMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==
-=BfZ/
------END PGP PUBLIC KEY BLOCK-----
-`
-
-func getF30Repository() *pipeline.DNFRepository {
-	repo := pipeline.NewDNFRepository("https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch", "", "")
-	repo.SetChecksum("sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97")
-	repo.SetGPGKey(f30GPGKey)
-	return repo
-}
-
-func getF30BuildPipeline() *pipeline.Pipeline {
-	p := &pipeline.Pipeline{}
-	options := &pipeline.DNFStageOptions{
-		ReleaseVersion:   "30",
-		BaseArchitecture: "x86_64",
-	}
-	options.AddRepository(getF30Repository())
-	options.AddPackage("dnf")
-	options.AddPackage("e2fsprogs")
-	options.AddPackage("policycoreutils")
-	options.AddPackage("qemu-img")
-	options.AddPackage("systemd")
-	options.AddPackage("grub2-pc")
-	options.AddPackage("tar")
-	p.AddStage(pipeline.NewDNFStage(options))
-	return p
-}
-
-func newF30Pipeline(packages []string, excludedPackages []string, blueprint *blueprint.Blueprint) *pipeline.Pipeline {
-	p := &pipeline.Pipeline{}
-	p.SetBuild(getF30BuildPipeline(), "org.osbuild.fedora30")
-	options := &pipeline.DNFStageOptions{
-		ReleaseVersion:   "30",
-		BaseArchitecture: "x86_64",
-	}
-	options.AddRepository(getF30Repository())
-	for _, pkg := range packages {
-		options.AddPackage(pkg)
-	}
-	for _, pkg := range excludedPackages {
-		options.ExcludePackage(pkg)
-	}
-
-	for _, pkg := range blueprint.Packages {
-		options.AddPackage(pkg.ToNameVersion())
-	}
-
-	for _, pkg := range blueprint.Modules {
-		options.AddPackage(pkg.ToNameVersion())
-	}
-
-	for _, group := range blueprint.Groups {
-		options.AddPackage(group.Name)
-	}
-
-	p.AddStage(pipeline.NewDNFStage(options))
-
-	/* grub2 mangles the BLS snippets, we must fix them manually */
-	p.AddStage(pipeline.NewFixBLSStage())
-
-	return p
-}
-
-func setBootloader(p *pipeline.Pipeline, kernelOptions string, blueprint *blueprint.Blueprint) {
-	id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
-	if err != nil {
-		panic("invalid UUID")
-	}
-
-	kernelCustomization := blueprint.GetKernel()
-	if kernelCustomization != nil {
-		kernelOptions += " " + kernelCustomization.Append
-	}
-
-	p.AddStage(pipeline.NewGRUB2Stage(
-		&pipeline.GRUB2StageOptions{
-			RootFilesystemUUID: id,
-			KernelOptions:      kernelOptions,
-		},
-	))
-}
-
-func setFilesystems(p *pipeline.Pipeline) {
-	id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
-	if err != nil {
-		panic("invalid UUID")
-	}
-	options := &pipeline.FSTabStageOptions{}
-	options.AddFilesystem(id, "ext4", "/", "defaults", 1, 1)
-	p.AddStage(pipeline.NewFSTabStage(options))
-}
-
-func setFirewall(p *pipeline.Pipeline, enabledServices []string, disabledServices []string, b *blueprint.Blueprint) {
-	f := b.GetFirewall()
-	ports := []string{}
-
-	if f != nil {
-		if f.Services != nil {
-			enabledServices = append(enabledServices, f.Services.Enabled...)
-			disabledServices = append(disabledServices, f.Services.Disabled...)
-		}
-		ports = f.Ports
-	}
-
-	if len(enabledServices) == 0 && len(disabledServices) == 0 && len(ports) == 0 {
-		return
-	}
-
-	p.AddStage(
-		pipeline.NewFirewallStage(&pipeline.FirewallStageOptions{
-			Ports:            ports,
-			EnabledServices:  enabledServices,
-			DisabledServices: disabledServices,
-		}),
-	)
-}
-
-func setServices(p *pipeline.Pipeline, enabledServices []string, disabledServices []string, b *blueprint.Blueprint) {
-	s := b.GetServices()
-
-	if s != nil {
-		enabledServices = append(enabledServices, s.Enabled...)
-		disabledServices = append(disabledServices, s.Enabled...)
-	}
-
-	if len(enabledServices) == 0 && len(disabledServices) == 0 {
-		return
-	}
-
-	p.AddStage(
-		pipeline.NewSystemdStage(&pipeline.SystemdStageOptions{
-			EnabledServices:  enabledServices,
-			DisabledServices: disabledServices,
-		}),
-	)
-}
-
-func setQemuAssembler(p *pipeline.Pipeline, format string, filename string) {
-	p.AddStage(pipeline.NewSELinuxStage(
-		&pipeline.SELinuxStageOptions{
-			FileContexts: "etc/selinux/targeted/contexts/files/file_contexts",
-		}))
-	id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
-	if err != nil {
-		panic("invalid UUID")
-	}
-	p.Assembler = pipeline.NewQEMUAssembler(
-		&pipeline.QEMUAssemblerOptions{
-			Format:             format,
-			Filename:           filename,
-			PTUUID:             "0x14fc63d2",
-			RootFilesystemUUDI: id,
-			// Azure requires this size to be a multiple of 1MB. If you change this, make sure
-			// the size still fulfills this requirement to prevent regressions.
-			Size: 3222274048,
-		})
-}
-
-func setTarAssembler(p *pipeline.Pipeline, filename, compression string) {
-	p.AddStage(pipeline.NewSELinuxStage(
-		&pipeline.SELinuxStageOptions{
-			FileContexts: "etc/selinux/targeted/contexts/files/file_contexts",
-		}))
-	p.Assembler = pipeline.NewTarAssembler(
-		&pipeline.TarAssemblerOptions{
-			Filename: filename,
-		})
-}
-
-func setRawFSAssembler(p *pipeline.Pipeline, filename string) {
-	p.AddStage(pipeline.NewSELinuxStage(
-		&pipeline.SELinuxStageOptions{
-			FileContexts: "etc/selinux/targeted/contexts/files/file_contexts",
-		}))
-	id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
-	if err != nil {
-		panic("invalid UUID")
-	}
-	p.Assembler = pipeline.NewRawFSAssembler(
-		&pipeline.RawFSAssemblerOptions{
-			Filename:           filename,
-			RootFilesystemUUDI: id,
-			Size:               3222274048,
-		})
-}
diff --git a/internal/distro/fedora30/openstack.go b/internal/distro/fedora30/openstack.go
deleted file mode 100644
index 1b56e469f..000000000
--- a/internal/distro/fedora30/openstack.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package fedora30
-
-import (
-	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
-)
-
-type openstackOutput struct{}
-
-func (t *openstackOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
-	packages := [...]string{
-		"@Core",
-		"chrony",
-		"kernel",
-		"selinux-policy-targeted",
-		"grub2-pc",
-		"spice-vdagent",
-		"qemu-guest-agent",
-		"xen-libs",
-		"langpacks-en",
-		"cloud-init",
-		"libdrm",
-	}
-	excludedPackages := [...]string{
-		"dracut-config-rescue",
-	}
-	p := newF30Pipeline(packages[:], excludedPackages[:], b)
-	err := customizeAll(p, b.Customizations)
-	if err != nil {
-		return nil, err
-	}
-	setFilesystems(p)
-	setBootloader(p, "ro biosdevname=0 net.ifnames=0", b)
-	setFirewall(p, nil, nil, b)
-	setServices(p, nil, nil, b)
-	setQemuAssembler(p, "qcow2", t.getName())
-
-	return p, nil
-}
-
-func (t *openstackOutput) getName() string {
-	return "image.qcow2"
-}
-
-func (t *openstackOutput) getMime() string {
-	return "application/x-qemu-disk"
-}
diff --git a/internal/distro/fedora30/os.go b/internal/distro/fedora30/os.go
deleted file mode 100644
index 3f90a89b7..000000000
--- a/internal/distro/fedora30/os.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package fedora30
-
-import (
-	"sort"
-
-	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	"github.com/osbuild/osbuild-composer/internal/distro"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
-	"github.com/osbuild/osbuild-composer/internal/rpmmd"
-)
-
-type Fedora30 struct {
-	outputs map[string]output
-}
-
-type output interface {
-	translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error)
-	getName() string
-	getMime() string
-}
-
-func init() {
-	distro.Register("fedora-30", &Fedora30{
-		outputs: map[string]output{
-			"ami":              &amiOutput{},
-			"ext4-filesystem":  &ext4Output{},
-			"partitioned-disk": &diskOutput{},
-			"qcow2":            &qcow2Output{},
-			"openstack":        &openstackOutput{},
-			"tar":              &tarOutput{},
-			"vhd":              &vhdOutput{},
-			"vmdk":             &vmdkOutput{},
-		},
-	})
-}
-
-func (f *Fedora30) Repositories() []rpmmd.RepoConfig {
-	return []rpmmd.RepoConfig{
-		{
-			Id:       "fedora",
-			Name:     "Fedora 30",
-			Metalink: "https://mirrors.fedoraproject.org/metalink?repo=fedora-30&arch=x86_64",
-			Checksum: "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97",
-		},
-	}
-}
-
-// ListOutputFormats returns a sorted list of the supported output formats
-func (f *Fedora30) ListOutputFormats() []string {
-	formats := make([]string, 0, len(f.outputs))
-	for name := range f.outputs {
-		formats = append(formats, name)
-	}
-	sort.Strings(formats)
-	return formats
-}
-
-func (f *Fedora30) FilenameFromType(outputFormat string) (string, string, error) {
-	if output, exists := f.outputs[outputFormat]; exists {
-		return output.getName(), output.getMime(), nil
-	}
-	return "", "", &distro.InvalidOutputFormatError{outputFormat}
-}
-
-func (f *Fedora30) Pipeline(b *blueprint.Blueprint, outputFormat string) (*pipeline.Pipeline, error) {
-	if output, exists := f.outputs[outputFormat]; exists {
-		return output.translate(b)
-	}
-	return nil, &distro.InvalidOutputFormatError{outputFormat}
-}
diff --git a/internal/distro/fedora30/qcow2.go b/internal/distro/fedora30/qcow2.go
deleted file mode 100644
index 2d3f83fce..000000000
--- a/internal/distro/fedora30/qcow2.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package fedora30
-
-import (
-	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
-)
-
-type qcow2Output struct{}
-
-func (t *qcow2Output) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
-	packages := [...]string{
-		"kernel-core",
-		"@Fedora Cloud Server",
-		"chrony",
-		"polkit",
-		"systemd-udev",
-		"selinux-policy-targeted",
-		"grub2-pc",
-		"langpacks-en",
-	}
-	excludedPackages := [...]string{
-		"dracut-config-rescue",
-		"etables",
-		"firewalld",
-		"gobject-introspection",
-		"plymouth",
-	}
-	p := newF30Pipeline(packages[:], excludedPackages[:], b)
-	err := customizeAll(p, b.Customizations)
-	if err != nil {
-		return nil, err
-	}
-	setFilesystems(p)
-	setBootloader(p, "ro biosdevname=0 net.ifnames=0", b)
-	setFirewall(p, nil, nil, b)
-	setServices(p, nil, nil, b)
-	setQemuAssembler(p, "qcow2", t.getName())
-
-	return p, nil
-}
-
-func (t *qcow2Output) getName() string {
-	return "image.qcow2"
-}
-
-func (t *qcow2Output) getMime() string {
-	return "application/x-qemu-disk"
-}
diff --git a/internal/distro/fedora30/tar.go b/internal/distro/fedora30/tar.go
deleted file mode 100644
index d469abcb4..000000000
--- a/internal/distro/fedora30/tar.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package fedora30
-
-import (
-	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
-)
-
-type tarOutput struct{}
-
-func (t *tarOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
-	packages := [...]string{
-		"policycoreutils",
-		"selinux-policy-targeted",
-		"kernel",
-		"firewalld",
-		"chrony",
-		"langpacks-en",
-	}
-	excludedPackages := [...]string{
-		"dracut-config-rescue",
-	}
-	p := newF30Pipeline(packages[:], excludedPackages[:], b)
-	err := customizeAll(p, b.Customizations)
-	if err != nil {
-		return nil, err
-	}
-	setBootloader(p, "ro biosdevname=0 net.ifnames=0", b)
-	setFirewall(p, nil, nil, b)
-	setServices(p, nil, nil, b)
-	setTarAssembler(p, t.getName(), "xz")
-
-	return p, nil
-}
-
-func (t *tarOutput) getName() string {
-	return "root.tar.xz"
-}
-
-func (t *tarOutput) getMime() string {
-	return "application/x-tar"
-}
diff --git a/internal/distro/fedora30/vhd.go b/internal/distro/fedora30/vhd.go
deleted file mode 100644
index 5fb7239a4..000000000
--- a/internal/distro/fedora30/vhd.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package fedora30
-
-import (
-	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
-)
-
-type vhdOutput struct{}
-
-func (t *vhdOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
-	packages := [...]string{
-		"@Core",
-		"chrony",
-		"kernel",
-		"selinux-policy-targeted",
-		"grub2-pc",
-		"langpacks-en",
-		"net-tools",
-		"ntfsprogs",
-		"WALinuxAgent",
-		"libxcrypt-compat",
-	}
-	excludedPackages := [...]string{
-		"dracut-config-rescue",
-	}
-	p := newF30Pipeline(packages[:], excludedPackages[:], b)
-	err := customizeAll(p, b.Customizations)
-	if err != nil {
-		return nil, err
-	}
-	setFilesystems(p)
-	setBootloader(p, "ro biosdevname=0 net.ifnames=0", b)
-	setFirewall(p, nil, nil, b)
-	setServices(p, nil, nil, b)
-	setQemuAssembler(p, "vpc", t.getName())
-
-	return p, nil
-}
-
-func (t *vhdOutput) getName() string {
-	return "image.vhd"
-}
-
-func (t *vhdOutput) getMime() string {
-	return "application/x-vhd"
-}
diff --git a/internal/distro/fedora30/vmdk.go b/internal/distro/fedora30/vmdk.go
deleted file mode 100644
index adb7e8cb7..000000000
--- a/internal/distro/fedora30/vmdk.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package fedora30
-
-import (
-	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
-)
-
-type vmdkOutput struct{}
-
-func (t *vmdkOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
-	packages := [...]string{
-		"@core",
-		"chrony",
-		"firewalld",
-		"grub2-pc",
-		"kernel",
-		"langpacks-en",
-		"open-vm-tools",
-		"selinux-policy-targeted",
-	}
-	excludedPackages := [...]string{
-		"dracut-config-rescue",
-	}
-	p := newF30Pipeline(packages[:], excludedPackages[:], b)
-	err := customizeAll(p, b.Customizations)
-	if err != nil {
-		return nil, err
-	}
-	setFilesystems(p)
-	setBootloader(p, "ro biosdevname=0 net.ifnames=0", b)
-	setFirewall(p, nil, nil, b)
-	setServices(p, nil, nil, b)
-	setQemuAssembler(p, "vmdk", t.getName())
-
-	return p, nil
-}
-
-func (t *vmdkOutput) getName() string {
-	return "disk.vmdk"
-}
-
-func (t *vmdkOutput) getMime() string {
-	return "application/x-vmdk"
-}
diff --git a/internal/mocks/rpmmd/fixtures.go b/internal/mocks/rpmmd/fixtures.go
index e0bf6b451..8e6912558 100644
--- a/internal/mocks/rpmmd/fixtures.go
+++ b/internal/mocks/rpmmd/fixtures.go
@@ -2,10 +2,16 @@ package rpmmd_mock
 
 import (
 	"fmt"
+	"github.com/google/uuid"
+	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
+	"github.com/osbuild/osbuild-composer/internal/store"
+	"sort"
 	"time"
 )
 
+type FixtureGenerator func() Fixture
+
 func generatePackageList() rpmmd.PackageList {
 	baseTime, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
 
@@ -37,72 +43,140 @@ func generatePackageList() rpmmd.PackageList {
 		packageList = append(packageList, basePackage, secondBuild)
 	}
 
+	sort.Slice(packageList, func(i, j int) bool {
+		return packageList[i].Name < packageList[j].Name
+	})
+
 	return packageList
 }
 
-var basePackageList = fetchPackageList{
-	generatePackageList(),
-	nil,
+func createBaseStoreFixture() *store.Store {
+	var bName = "test"
+	var b = blueprint.Blueprint{Name: bName, Version: "0.0.0"}
+
+	var date = time.Date(2019, 11, 27, 13, 19, 0, 0, time.FixedZone("UTC+1", 60*60))
+
+	s := store.New(nil)
+
+	s.Blueprints[bName] = b
+	s.Composes = map[uuid.UUID]store.Compose{
+		uuid.MustParse("e65f76f8-b0d9-4974-9dd7-745ae80b4721"): store.Compose{
+			QueueStatus: "WAITING",
+			Blueprint:   &b,
+			OutputType:  "tar",
+			Targets:     nil,
+			JobCreated:  date,
+		},
+		uuid.MustParse("e65f76f8-b0d9-4974-9dd7-745ae80b4722"): store.Compose{
+			QueueStatus: "RUNNING",
+			Blueprint:   &b,
+			OutputType:  "tar",
+			Targets:     nil,
+			JobCreated:  date,
+			JobStarted:  date,
+		},
+		uuid.MustParse("e65f76f8-b0d9-4974-9dd7-745ae80b4723"): store.Compose{
+			QueueStatus: "FINISHED",
+			Blueprint:   &b,
+			OutputType:  "tar",
+			Targets:     nil,
+			JobCreated:  date,
+			JobStarted:  date,
+			JobFinished: date,
+		},
+		uuid.MustParse("e65f76f8-b0d9-4974-9dd7-745ae80b4724"): store.Compose{
+			QueueStatus: "FAILED",
+			Blueprint:   &b,
+			OutputType:  "tar",
+			Targets:     nil,
+			JobCreated:  date,
+			JobStarted:  date,
+			JobFinished: date,
+		},
+	}
+
+	return s
 }
 
-var BaseFixture = Fixture{
-	basePackageList,
-	depsolve{
-		[]rpmmd.PackageSpec{
-			{
-				Name:    "dep-package1",
-				Epoch:   0,
-				Version: "1.33",
-				Release: "2.fc30",
-				Arch:    "x86_64",
-			},
-			{
-				Name:    "dep-package2",
-				Epoch:   0,
-				Version: "2.9",
-				Release: "1.fc30",
-				Arch:    "x86_64",
+func BaseFixture() Fixture {
+	return Fixture{
+		fetchPackageList{
+			generatePackageList(),
+			nil,
+		},
+		depsolve{
+			[]rpmmd.PackageSpec{
+				{
+					Name:    "dep-package1",
+					Epoch:   0,
+					Version: "1.33",
+					Release: "2.fc30",
+					Arch:    "x86_64",
+				},
+				{
+					Name:    "dep-package2",
+					Epoch:   0,
+					Version: "2.9",
+					Release: "1.fc30",
+					Arch:    "x86_64",
+				},
 			},
+			nil,
 		},
-		nil,
-	},
+		createBaseStoreFixture(),
+	}
 }
 
-var NonExistingPackage = Fixture{
-	basePackageList,
-	depsolve{
-		nil,
-		&rpmmd.DNFError{
-			Kind:   "MarkingErrors",
-			Reason: "Error occurred when marking packages for installation: Problems in request:\nmissing packages: fash",
+func NonExistingPackage() Fixture {
+	return Fixture{
+		fetchPackageList{
+			generatePackageList(),
+			nil,
+		},
+		depsolve{
+			nil,
+			&rpmmd.DNFError{
+				Kind:   "MarkingErrors",
+				Reason: "Error occurred when marking packages for installation: Problems in request:\nmissing packages: fash",
+			},
 		},
-	},
+		createBaseStoreFixture(),
+	}
 }
 
-var BadDepsolve = Fixture{
-	basePackageList,
-	depsolve{
-		nil,
-		&rpmmd.DNFError{
-			Kind:   "DepsolveError",
-			Reason: "There was a problem depsolving ['go2rpm']: \n Problem: conflicting requests\n  - nothing provides askalono-cli needed by go2rpm-1-4.fc31.noarch",
+func BadDepsolve() Fixture {
+	return Fixture{
+		fetchPackageList{
+			generatePackageList(),
+			nil,
 		},
-	},
+		depsolve{
+			nil,
+			&rpmmd.DNFError{
+				Kind:   "DepsolveError",
+				Reason: "There was a problem depsolving ['go2rpm']: \n Problem: conflicting requests\n  - nothing provides askalono-cli needed by go2rpm-1-4.fc31.noarch",
+			},
+		},
+		createBaseStoreFixture(),
+	}
 }
 
-var BadFetch = Fixture{
-	fetchPackageList{
-		ret: nil,
-		err: &rpmmd.DNFError{
-			Kind:   "FetchError",
-			Reason: "There was a problem when fetching packages.",
+func BadFetch() Fixture {
+	return Fixture{
+		fetchPackageList{
+			ret: nil,
+			err: &rpmmd.DNFError{
+				Kind:   "FetchError",
+				Reason: "There was a problem when fetching packages.",
+			},
 		},
-	},
-	depsolve{
-		nil,
-		&rpmmd.DNFError{
-			Kind:   "DepsolveError",
-			Reason: "There was a problem depsolving ['go2rpm']: \n Problem: conflicting requests\n  - nothing provides askalono-cli needed by go2rpm-1-4.fc31.noarch",
+		depsolve{
+			nil,
+			&rpmmd.DNFError{
+				Kind:   "DepsolveError",
+				Reason: "There was a problem depsolving ['go2rpm']: \n Problem: conflicting requests\n  - nothing provides askalono-cli needed by go2rpm-1-4.fc31.noarch",
+			},
 		},
-	},
+		createBaseStoreFixture(),
+	}
 }
diff --git a/internal/mocks/rpmmd/rpmmd_mock.go b/internal/mocks/rpmmd/rpmmd_mock.go
index a5b08e04a..2a85da0be 100644
--- a/internal/mocks/rpmmd/rpmmd_mock.go
+++ b/internal/mocks/rpmmd/rpmmd_mock.go
@@ -1,6 +1,9 @@
 package rpmmd_mock
 
-import "github.com/osbuild/osbuild-composer/internal/rpmmd"
+import (
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
+	"github.com/osbuild/osbuild-composer/internal/store"
+)
 
 type fetchPackageList struct {
 	ret rpmmd.PackageList
@@ -14,6 +17,7 @@ type depsolve struct {
 type Fixture struct {
 	fetchPackageList
 	depsolve
+	*store.Store
 }
 
 type rpmmdMock struct {
diff --git a/internal/rpmmd/repository.go b/internal/rpmmd/repository.go
index 465d2dc35..fc07970ac 100644
--- a/internal/rpmmd/repository.go
+++ b/internal/rpmmd/repository.go
@@ -19,6 +19,7 @@ type RepoConfig struct {
 	Metalink   string `json:"metalink,omitempty"`
 	MirrorList string `json:"mirrorlist,omitempty"`
 	Checksum   string `json:"checksum,omitempty"`
+	GPGKey     string `json:"gpgkey,omitempty"`
 }
 
 type PackageList []Package


Test patch:
diff --git a/internal/jobqueue/api_test.go b/internal/jobqueue/api_test.go
index 9ac513165..75864532b 100644
--- a/internal/jobqueue/api_test.go
+++ b/internal/jobqueue/api_test.go
@@ -1,76 +1,17 @@
 package jobqueue_test
 
 import (
-	"bytes"
-	"encoding/json"
-	"io/ioutil"
 	"net/http"
-	"net/http/httptest"
-	"reflect"
-	"strings"
 	"testing"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/jobqueue"
 	"github.com/osbuild/osbuild-composer/internal/store"
+	"github.com/osbuild/osbuild-composer/internal/test"
 
 	"github.com/google/uuid"
 )
 
-func sendHTTP(api *jobqueue.API, method, path, body string) {
-	req := httptest.NewRequest(method, path, bytes.NewReader([]byte(body)))
-	req.Header.Set("Content-Type", "application/json")
-	resp := httptest.NewRecorder()
-	api.ServeHTTP(resp, req)
-}
-
-func testRoute(t *testing.T, api *jobqueue.API, method, path, body string, expectedStatus int, expectedJSON string) {
-	req := httptest.NewRequest(method, path, bytes.NewReader([]byte(body)))
-	req.Header.Set("Content-Type", "application/json")
-	resp := httptest.NewRecorder()
-	api.ServeHTTP(resp, req)
-
-	if resp.Code != expectedStatus {
-		t.Errorf("%s: expected status %v, but got %v", path, expectedStatus, resp.Code)
-		return
-	}
-
-	replyJSON, err := ioutil.ReadAll(resp.Body)
-	if err != nil {
-		t.Errorf("%s: could not read reponse body: %v", path, err)
-		return
-	}
-
-	if expectedJSON == "" {
-		if len(replyJSON) != 0 {
-			t.Errorf("%s: expected no response body, but got:\n%s", path, replyJSON)
-		}
-		return
-	}
-
-	var reply, expected interface{}
-	err = json.Unmarshal(replyJSON, &reply)
-	if err != nil {
-		t.Errorf("%s: %v\n%s", path, err, string(replyJSON))
-		return
-	}
-
-	if expectedJSON == "*" {
-		return
-	}
-
-	err = json.Unmarshal([]byte(expectedJSON), &expected)
-	if err != nil {
-		t.Errorf("%s: expected JSON is invalid: %v", path, err)
-		return
-	}
-
-	if !reflect.DeepEqual(reply, expected) {
-		t.Errorf("%s: reply != expected:\n   reply: %s\nexpected: %s", path, strings.TrimSpace(string(replyJSON)), expectedJSON)
-		return
-	}
-}
-
 func TestBasic(t *testing.T) {
 	var cases = []struct {
 		Method         string
@@ -92,7 +33,7 @@ func TestBasic(t *testing.T) {
 	for _, c := range cases {
 		api := jobqueue.New(nil, store.New(nil))
 
-		testRoute(t, api, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
+		test.TestRoute(t, api, false, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
 	}
 }
 
@@ -106,7 +47,7 @@ func TestCreate(t *testing.T) {
 		t.Fatalf("error pushing compose: %v", err)
 	}
 
-	testRoute(t, api, "POST", "/job-queue/v1/jobs", `{}`, http.StatusCreated,
+	test.TestRoute(t, api, false, "POST", "/job-queue/v1/jobs", `{}`, http.StatusCreated,
 		`{"id":"ffffffff-ffff-ffff-ffff-ffffffffffff","pipeline":{"build":{"pipeline":{"stages":[{"name":"org.osbuild.dnf","options":{"repos":[{"metalink":"https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever\u0026arch=$basearch","gpgkey":"-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n","checksum":"sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"}],"packages":["dnf","e2fsprogs","policycoreutils","qemu-img","systemd","grub2-pc","tar"],"releasever":"30","basearch":"x86_64"}}]},"runner":"org.osbuild.fedora30"},"stages":[{"name":"org.osbuild.dnf","options":{"repos":[{"metalink":"https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever\u0026arch=$basearch","gpgkey":"-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n","checksum":"sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"}],"packages":["policycoreutils","selinux-policy-targeted","kernel","firewalld","chrony","langpacks-en"],"exclude_packages":["dracut-config-rescue"],"releasever":"30","basearch":"x86_64"}},{"name":"org.osbuild.fix-bls","options":{}},{"name":"org.osbuild.locale","options":{"language":"en_US"}},{"name":"org.osbuild.grub2","options":{"root_fs_uuid":"76a22bf4-f153-4541-b6c7-0332c0dfaeac","boot_fs_uuid":"00000000-0000-0000-0000-000000000000","kernel_opts":"ro biosdevname=0 net.ifnames=0"}},{"name":"org.osbuild.selinux","options":{"file_contexts":"etc/selinux/targeted/contexts/files/file_contexts"}}],"assembler":{"name":"org.osbuild.tar","options":{"filename":"root.tar.xz"}}},"targets":[{"name":"org.osbuild.local","options":{"location":"/var/lib/osbuild-composer/outputs/ffffffff-ffff-ffff-ffff-ffffffffffff"}}]}`)
 }
 
@@ -121,14 +62,14 @@ func testUpdateTransition(t *testing.T, from, to string, expectedStatus int) {
 			t.Fatalf("error pushing compose: %v", err)
 		}
 		if from != "WAITING" {
-			sendHTTP(api, "POST", "/job-queue/v1/jobs", `{}`)
+			test.SendHTTP(api, false, "POST", "/job-queue/v1/jobs", `{}`)
 			if from != "RUNNING" {
-				sendHTTP(api, "PATCH", "/job-queue/v1/jobs/ffffffff-ffff-ffff-ffff-ffffffffffff", `{"status":"`+from+`"}`)
+				test.SendHTTP(api, false, "PATCH", "/job-queue/v1/jobs/ffffffff-ffff-ffff-ffff-ffffffffffff", `{"status":"`+from+`"}`)
 			}
 		}
 	}
 
-	testRoute(t, api, "PATCH", "/job-queue/v1/jobs/ffffffff-ffff-ffff-ffff-ffffffffffff", `{"status":"`+to+`"}`, expectedStatus, ``)
+	test.TestRoute(t, api, false, "PATCH", "/job-queue/v1/jobs/ffffffff-ffff-ffff-ffff-ffffffffffff", `{"status":"`+to+`"}`, expectedStatus, ``)
 }
 
 func TestUpdate(t *testing.T) {
diff --git a/internal/test/helpers.go b/internal/test/helpers.go
new file mode 100644
index 000000000..6cd77f153
--- /dev/null
+++ b/internal/test/helpers.go
@@ -0,0 +1,144 @@
+package test
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"reflect"
+	"strings"
+	"testing"
+)
+
+type API interface {
+	ServeHTTP(writer http.ResponseWriter, request *http.Request)
+}
+
+func externalRequest(method, path, body string) *http.Response {
+	client := http.Client{
+		Transport: &http.Transport{
+			DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
+				return net.Dial("unix", "/run/weldr/api.socket")
+			},
+		},
+	}
+
+	req, err := http.NewRequest(method, "http://localhost"+path, bytes.NewReader([]byte(body)))
+	if err != nil {
+		panic(err)
+	}
+
+	if method == "POST" {
+		req.Header.Set("Content-Type", "application/json")
+	}
+
+	resp, err := client.Do(req)
+	if err != nil {
+		panic(err)
+	}
+
+	return resp
+}
+
+func internalRequest(api API, method, path, body string) *http.Response {
+	req := httptest.NewRequest(method, path, bytes.NewReader([]byte(body)))
+	req.Header.Set("Content-Type", "application/json")
+	resp := httptest.NewRecorder()
+	api.ServeHTTP(resp, req)
+
+	return resp.Result()
+}
+
+func SendHTTP(api API, external bool, method, path, body string) *http.Response {
+	if len(os.Getenv("OSBUILD_COMPOSER_TEST_EXTERNAL")) > 0 {
+		if !external {
+			return nil
+		}
+		return externalRequest(method, path, body)
+	} else {
+		return internalRequest(api, method, path, body)
+	}
+}
+
+// this function serves to drop fields that shouldn't be tested from the unmarshalled json objects
+func dropFields(obj interface{}, fields ...string) {
+	switch v := obj.(type) {
+	// if the interface type is a map attempt to delete the fields
+	case map[string]interface{}:
+		for i, field := range fields {
+			if _, ok := v[field]; ok {
+				delete(v, field)
+				// if the field is found remove it from the fields slice
+				if len(fields) < i-1 {
+					fields = append(fields[:i], fields[i+1:]...)
+				} else {
+					fields = fields[:i]
+				}
+			}
+		}
+		// call dropFields on the remaining elements since they may contain a map containing the field
+		for _, val := range v {
+			dropFields(val, fields...)
+		}
+	// if the type is a list of interfaces call dropFields on each interface
+	case []interface{}:
+		for _, element := range v {
+			dropFields(element, fields...)
+		}
+	default:
+		return
+	}
+}
+
+func TestRoute(t *testing.T, api API, external bool, method, path, body string, expectedStatus int, expectedJSON string, ignoreFields ...string) {
+	resp := SendHTTP(api, external, method, path, body)
+	if resp == nil {
+		t.Skip("This test is for internal testing only")
+	}
+
+	if resp.StatusCode != expectedStatus {
+		t.Errorf("%s: expected status %v, but got %v", path, expectedStatus, resp.StatusCode)
+	}
+
+	replyJSON, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		t.Errorf("%s: could not read response body: %v", path, err)
+		return
+	}
+
+	if expectedJSON == "" {
+		if len(replyJSON) != 0 {
+			t.Errorf("%s: expected no response body, but got:\n%s", path, replyJSON)
+		}
+		return
+	}
+
+	var reply, expected interface{}
+	err = json.Unmarshal(replyJSON, &reply)
+	if err != nil {
+		t.Errorf("%s: %v\n%s", path, err, string(replyJSON))
+		return
+	}
+
+	if expectedJSON == "*" {
+		return
+	}
+
+	err = json.Unmarshal([]byte(expectedJSON), &expected)
+	if err != nil {
+		t.Errorf("%s: expected JSON is invalid: %v", path, err)
+		return
+	}
+
+	dropFields(reply, ignoreFields...)
+	dropFields(expected, ignoreFields...)
+
+	if !reflect.DeepEqual(reply, expected) {
+		t.Errorf("%s: reply != expected:\n   reply: %s\nexpected: %s", path, strings.TrimSpace(string(replyJSON)), expectedJSON)
+		return
+	}
+}
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index 8a3087529..d6179b874 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -1,163 +1,27 @@
 package weldr_test
 
 import (
-	"bytes"
-	"context"
-	"encoding/json"
-	"io/ioutil"
 	"math/rand"
-	"net"
 	"net/http"
-	"net/http/httptest"
 	"os"
-	"reflect"
 	"strconv"
-	"strings"
 	"testing"
 	"time"
 
 	rpmmd_mock "github.com/osbuild/osbuild-composer/internal/mocks/rpmmd"
-	"github.com/osbuild/osbuild-composer/internal/store"
+	"github.com/osbuild/osbuild-composer/internal/test"
 	"github.com/osbuild/osbuild-composer/internal/weldr"
 
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	_ "github.com/osbuild/osbuild-composer/internal/distro/test"
 )
 
-func externalRequest(method, path, body string) *http.Response {
-	client := http.Client{
-		Transport: &http.Transport{
-			DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
-				return net.Dial("unix", "/run/weldr/api.socket")
-			},
-		},
-	}
-
-	req, err := http.NewRequest(method, "http://localhost"+path, bytes.NewReader([]byte(body)))
-	if err != nil {
-		panic(err)
-	}
-
-	if method == "POST" {
-		req.Header.Set("Content-Type", "application/json")
-	}
-
-	resp, err := client.Do(req)
-	if err != nil {
-		panic(err)
-	}
-
-	return resp
-}
-
-func internalRequest(api *weldr.API, method, path, body string) *http.Response {
-	req := httptest.NewRequest(method, path, bytes.NewReader([]byte(body)))
-
-	if method == "POST" {
-		req.Header.Set("Content-Type", "application/json")
-	}
-	resp := httptest.NewRecorder()
-	api.ServeHTTP(resp, req)
-
-	return resp.Result()
-}
-
-func sendHTTP(api *weldr.API, external bool, method, path, body string) *http.Response {
-	if len(os.Getenv("OSBUILD_COMPOSER_TEST_EXTERNAL")) > 0 {
-		if !external {
-			return nil
-		}
-		return externalRequest(method, path, body)
-	} else {
-		return internalRequest(api, method, path, body)
-	}
-}
-
-// this function serves to drop fields that shouldn't be tested from the unmarshalled json objects
-func dropFields(obj interface{}, fields ...string) {
-	switch v := obj.(type) {
-	// if the interface type is a map attempt to delete the fields
-	case map[string]interface{}:
-		for i, field := range fields {
-			if _, ok := v[field]; ok {
-				delete(v, field)
-				// if the field is found remove it from the fields slice
-				if len(fields) < i-1 {
-					fields = append(fields[:i], fields[i+1:]...)
-				} else {
-					fields = fields[:i]
-				}
-			}
-		}
-		// call dropFields on the remaining elements since they may contain a map containing the field
-		for _, val := range v {
-			dropFields(val, fields...)
-		}
-	// if the type is a list of interfaces call dropFields on each interface
-	case []interface{}:
-		for _, element := range v {
-			dropFields(element, fields...)
-		}
-	default:
-		return
-	}
-}
-
-func testRoute(t *testing.T, api *weldr.API, external bool, method, path, body string, expectedStatus int, expectedJSON string, ignoreFields ...string) {
-	resp := sendHTTP(api, external, method, path, body)
-	if resp == nil {
-		t.Skip("This test is for internal testing only")
-	}
-
-	if resp.StatusCode != expectedStatus {
-		t.Errorf("%s: expected status %v, but got %v", path, expectedStatus, resp.StatusCode)
-	}
-
-	replyJSON, err := ioutil.ReadAll(resp.Body)
-	if err != nil {
-		t.Errorf("%s: could not read response body: %v", path, err)
-		return
-	}
-
-	if expectedJSON == "" {
-		if len(replyJSON) != 0 {
-			t.Errorf("%s: expected no response body, but got:\n%s", path, replyJSON)
-		}
-		return
-	}
-
-	var reply, expected interface{}
-	err = json.Unmarshal(replyJSON, &reply)
-	if err != nil {
-		t.Errorf("%s: %v\n%s", path, err, string(replyJSON))
-		return
-	}
-
-	if expectedJSON == "*" {
-		return
-	}
-
-	err = json.Unmarshal([]byte(expectedJSON), &expected)
-	if err != nil {
-		t.Errorf("%s: expected JSON is invalid: %v", path, err)
-		return
-	}
-
-	dropFields(reply, ignoreFields...)
-	dropFields(expected, ignoreFields...)
-
-	if !reflect.DeepEqual(reply, expected) {
-		t.Errorf("%s: reply != expected:\n   reply: %s\nexpected: %s", path, strings.TrimSpace(string(replyJSON)), expectedJSON)
-		return
-	}
-}
-
-func createWeldrAPI(fixture rpmmd_mock.Fixture) (*weldr.API, *store.Store) {
-	s := store.New(nil)
+func createWeldrAPI(fixtureGenerator rpmmd_mock.FixtureGenerator) *weldr.API {
+	fixture := fixtureGenerator()
 	rpm := rpmmd_mock.NewRPMMDMock(fixture)
 	d := distro.New("test")
 
-	return weldr.New(rpm, d, nil, s), s
+	return weldr.New(rpm, d, nil, fixture.Store)
 }
 
 func TestBasic(t *testing.T) {
@@ -176,14 +40,14 @@ func TestBasic(t *testing.T) {
 		{"/api/v0/projects/source/info/test", http.StatusOK, `{"sources":{"test":{"name":"test","type":"yum-baseurl","url":"http://example.com/test/os","check_gpg":true,"check_ssl":true,"system":true}},"errors":[]}`},
 		{"/api/v0/projects/source/info/*", http.StatusOK, `{"sources":{"test":{"name":"test","type":"yum-baseurl","url":"http://example.com/test/os","check_gpg":true,"check_ssl":true,"system":true}},"errors":[]}`},
 
-		{"/api/v0/blueprints/list", http.StatusOK, `{"total":0,"offset":0,"limit":0,"blueprints":[]}`},
+		{"/api/v0/blueprints/list", http.StatusOK, `{"total":1,"offset":0,"limit":1,"blueprints":["test"]}`},
 		{"/api/v0/blueprints/info/", http.StatusNotFound, `{"errors":[{"code":404,"id":"HTTPError","msg":"Not Found"}],"status":false}`},
 		{"/api/v0/blueprints/info/foo", http.StatusBadRequest, `{"errors":[{"id":"UnknownBlueprint","msg":"foo: "}],"status":false}`},
 	}
 
 	for _, c := range cases {
-		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
-		testRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
+		api := createWeldrAPI(rpmmd_mock.BaseFixture)
+		test.TestRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
 	}
 }
 
@@ -199,8 +63,8 @@ func TestBlueprintsNew(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
-		testRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
+		api := createWeldrAPI(rpmmd_mock.BaseFixture)
+		test.TestRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
 	}
 }
 
@@ -216,9 +80,9 @@ func TestBlueprintsWorkspace(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
-		sendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
-		testRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
+		api := createWeldrAPI(rpmmd_mock.BaseFixture)
+		test.SendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
+		test.TestRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
 	}
 }
 
@@ -237,19 +101,19 @@ func TestBlueprintsInfo(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
-		sendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test1","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
-		sendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test2","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
-		sendHTTP(api, true, "POST", "/api/v0/blueprints/workspace", `{"name":"test2","description":"Test","packages":[{"name":"systemd","version":"123"}],"version":"0.0.0"}`)
-		testRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
-		sendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/test2", ``)
-		sendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/test1", ``)
+		api := createWeldrAPI(rpmmd_mock.BaseFixture)
+		test.SendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test1","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
+		test.SendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test2","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
+		test.SendHTTP(api, true, "POST", "/api/v0/blueprints/workspace", `{"name":"test2","description":"Test","packages":[{"name":"systemd","version":"123"}],"version":"0.0.0"}`)
+		test.TestRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
+		test.SendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/test2", ``)
+		test.SendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/test1", ``)
 	}
 }
 
 func TestBlueprintsFreeze(t *testing.T) {
 	var cases = []struct {
-		Fixture        rpmmd_mock.Fixture
+		Fixture        rpmmd_mock.FixtureGenerator
 		Path           string
 		ExpectedStatus int
 		ExpectedJSON   string
@@ -258,10 +122,10 @@ func TestBlueprintsFreeze(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api, _ := createWeldrAPI(c.Fixture)
-		sendHTTP(api, false, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"dep-package1","version":"*"}],"version":"0.0.0"}`)
-		testRoute(t, api, false, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
-		sendHTTP(api, false, "DELETE", "/api/v0/blueprints/delete/test", ``)
+		api := createWeldrAPI(c.Fixture)
+		test.SendHTTP(api, false, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"dep-package1","version":"*"}],"version":"0.0.0"}`)
+		test.TestRoute(t, api, false, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
+		test.SendHTTP(api, false, "DELETE", "/api/v0/blueprints/delete/test", ``)
 	}
 }
 
@@ -277,11 +141,11 @@ func TestBlueprintsDiff(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
-		sendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
-		sendHTTP(api, true, "POST", "/api/v0/blueprints/workspace", `{"name":"test","description":"Test","packages":[{"name":"systemd","version":"123"}],"version":"0.0.0"}`)
-		testRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
-		sendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/test", ``)
+		api := createWeldrAPI(rpmmd_mock.BaseFixture)
+		test.SendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
+		test.SendHTTP(api, true, "POST", "/api/v0/blueprints/workspace", `{"name":"test","description":"Test","packages":[{"name":"systemd","version":"123"}],"version":"0.0.0"}`)
+		test.TestRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
+		test.SendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/test", ``)
 	}
 }
 
@@ -297,26 +161,26 @@ func TestBlueprintsDelete(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
-		sendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
-		testRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
-		sendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/test", ``)
+		api := createWeldrAPI(rpmmd_mock.BaseFixture)
+		test.SendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
+		test.TestRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
+		test.SendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/test", ``)
 	}
 }
 
 func TestBlueprintsChanges(t *testing.T) {
-	api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
+	api := createWeldrAPI(rpmmd_mock.BaseFixture)
 	rand.Seed(time.Now().UnixNano())
 	id := strconv.Itoa(rand.Int())
 	ignoreFields := []string{"commit", "timestamp"}
 
-	sendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"`+id+`","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
-	testRoute(t, api, true, "GET", "/api/v0/blueprints/changes/failing"+id, ``, http.StatusOK, `{"blueprints":[],"errors":[{"id":"UnknownBlueprint","msg":"failing`+id+`"}],"limit":20,"offset":0}`, ignoreFields...)
-	testRoute(t, api, true, "GET", "/api/v0/blueprints/changes/"+id, ``, http.StatusOK, `{"blueprints":[{"changes":[{"commit":"","message":"Recipe `+id+`, version 0.0.0 saved.","revision":null,"timestamp":""}],"name":"`+id+`","total":1}],"errors":[],"limit":20,"offset":0}`, ignoreFields...)
-	sendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/"+id, ``)
-	sendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"`+id+`","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
-	testRoute(t, api, true, "GET", "/api/v0/blueprints/changes/"+id, ``, http.StatusOK, `{"blueprints":[{"changes":[{"commit":"","message":"Recipe `+id+`, version 0.0.0 saved.","revision":null,"timestamp":""},{"commit":"","message":"Recipe `+id+`, version 0.0.0 saved.","revision":null,"timestamp":""}],"name":"`+id+`","total":2}],"errors":[],"limit":20,"offset":0}`, ignoreFields...)
-	sendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/"+id, ``)
+	test.SendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"`+id+`","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
+	test.TestRoute(t, api, true, "GET", "/api/v0/blueprints/changes/failing"+id, ``, http.StatusOK, `{"blueprints":[],"errors":[{"id":"UnknownBlueprint","msg":"failing`+id+`"}],"limit":20,"offset":0}`, ignoreFields...)
+	test.TestRoute(t, api, true, "GET", "/api/v0/blueprints/changes/"+id, ``, http.StatusOK, `{"blueprints":[{"changes":[{"commit":"","message":"Recipe `+id+`, version 0.0.0 saved.","revision":null,"timestamp":""}],"name":"`+id+`","total":1}],"errors":[],"limit":20,"offset":0}`, ignoreFields...)
+	test.SendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/"+id, ``)
+	test.SendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"`+id+`","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
+	test.TestRoute(t, api, true, "GET", "/api/v0/blueprints/changes/"+id, ``, http.StatusOK, `{"blueprints":[{"changes":[{"commit":"","message":"Recipe `+id+`, version 0.0.0 saved.","revision":null,"timestamp":""},{"commit":"","message":"Recipe `+id+`, version 0.0.0 saved.","revision":null,"timestamp":""}],"name":"`+id+`","total":2}],"errors":[],"limit":20,"offset":0}`, ignoreFields...)
+	test.SendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/"+id, ``)
 }
 
 func TestCompose(t *testing.T) {
@@ -334,15 +198,16 @@ func TestCompose(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
-		sendHTTP(api, c.External, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
-		testRoute(t, api, c.External, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON, c.IgnoreFields...)
-		sendHTTP(api, c.External, "DELETE", "/api/v0/blueprints/delete/test", ``)
+		api := createWeldrAPI(rpmmd_mock.BaseFixture)
+		test.SendHTTP(api, c.External, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
+		test.TestRoute(t, api, c.External, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON, c.IgnoreFields...)
+		test.SendHTTP(api, c.External, "DELETE", "/api/v0/blueprints/delete/test", ``)
 	}
 }
 
 func TestComposeQueue(t *testing.T) {
 	var cases = []struct {
+		Fixture        rpmmd_mock.FixtureGenerator
 		Method         string
 		Path           string
 		Body           string
@@ -350,7 +215,7 @@ func TestComposeQueue(t *testing.T) {
 		ExpectedJSON   string
 		IgnoreFields   []string
 	}{
-		{"GET", "/api/v0/compose/queue", ``, http.StatusOK, `{"new":[{"blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"WAITING"}],"run":[{"blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"RUNNING"}]}`, []string{"id", "job_created", "job_started"}},
+		{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/queue", ``, http.StatusOK, `{"new":[{"blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"WAITING"}],"run":[{"blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"RUNNING"}]}`, []string{"id", "job_created", "job_started"}},
 	}
 
 	if len(os.Getenv("OSBUILD_COMPOSER_TEST_EXTERNAL")) > 0 {
@@ -358,24 +223,8 @@ func TestComposeQueue(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api, s := createWeldrAPI(rpmmd_mock.BaseFixture)
-		sendHTTP(api, false, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
-		// create job and leave it waiting
-		sendHTTP(api, false, "POST", "/api/v0/compose", `{"blueprint_name": "test","compose_type": "tar","branch": "master"}`)
-		// create job and leave it running
-		sendHTTP(api, false, "POST", "/api/v0/compose", `{"blueprint_name": "test","compose_type": "tar","branch": "master"}`)
-		s.PopCompose()
-		// create job and mark it as finished
-		sendHTTP(api, false, "POST", "/api/v0/compose", `{"blueprint_name": "test","compose_type": "tar","branch": "master"}`)
-		job := s.PopCompose()
-		s.UpdateCompose(job.ComposeID, "FINISHED")
-		// create job and mark it as failed
-		sendHTTP(api, false, "POST", "/api/v0/compose", `{"blueprint_name": "test","compose_type": "tar","branch": "master"}`)
-		job = s.PopCompose()
-		s.UpdateCompose(job.ComposeID, "FAILED")
-
-		testRoute(t, api, false, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON, c.IgnoreFields...)
-		sendHTTP(api, false, "DELETE", "/api/v0/blueprints/delete/test", ``)
+		api := createWeldrAPI(rpmmd_mock.BaseFixture)
+		test.TestRoute(t, api, false, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON, c.IgnoreFields...)
 	}
 }
 
@@ -392,9 +241,9 @@ func TestSourcesNew(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
-		testRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
-		sendHTTP(api, true, "DELETE", "/api/v0/projects/source/delete/fish", ``)
+		api := createWeldrAPI(rpmmd_mock.BaseFixture)
+		test.TestRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
+		test.SendHTTP(api, true, "DELETE", "/api/v0/projects/source/delete/fish", ``)
 	}
 }
 
@@ -411,16 +260,16 @@ func TestSourcesDelete(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
-		sendHTTP(api, true, "POST", "/api/v0/projects/source/new", `{"name": "fish","url": "https://download.opensuse.org/repositories/shells:/fish:/release:/3/Fedora_29/","type": "yum-baseurl","check_ssl": false,"check_gpg": false}`)
-		testRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
-		sendHTTP(api, true, "DELETE", "/api/v0/projects/source/delete/fish", ``)
+		api := createWeldrAPI(rpmmd_mock.BaseFixture)
+		test.SendHTTP(api, true, "POST", "/api/v0/projects/source/new", `{"name": "fish","url": "https://download.opensuse.org/repositories/shells:/fish:/release:/3/Fedora_29/","type": "yum-baseurl","check_ssl": false,"check_gpg": false}`)
+		test.TestRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
+		test.SendHTTP(api, true, "DELETE", "/api/v0/projects/source/delete/fish", ``)
 	}
 }
 
 func TestProjectsDepsolve(t *testing.T) {
 	var cases = []struct {
-		Fixture        rpmmd_mock.Fixture
+		Fixture        rpmmd_mock.FixtureGenerator
 		Path           string
 		ExpectedStatus int
 		ExpectedJSON   string
@@ -431,14 +280,14 @@ func TestProjectsDepsolve(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api, _ := createWeldrAPI(c.Fixture)
-		testRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
+		api := createWeldrAPI(c.Fixture)
+		test.TestRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
 	}
 }
 
 func TestProjectsInfo(t *testing.T) {
 	var cases = []struct {
-		Fixture        rpmmd_mock.Fixture
+		Fixture        rpmmd_mock.FixtureGenerator
 		Path           string
 		ExpectedStatus int
 		ExpectedJSON   string
@@ -447,19 +296,19 @@ func TestProjectsInfo(t *testing.T) {
 		{rpmmd_mock.BaseFixture, "/api/v0/projects/info/", http.StatusBadRequest, `{"status":false,"errors":[{"id":"UnknownProject","msg":"No packages specified."}]}`},
 		{rpmmd_mock.BaseFixture, "/api/v0/projects/info/nonexistingpkg", http.StatusBadRequest, `{"status":false,"errors":[{"id":"UnknownProject","msg":"No packages have been found."}]}`},
 		{rpmmd_mock.BaseFixture, "/api/v0/projects/info/*", http.StatusOK, `{"projects":[{"name":"package0","summary":"pkg0 sum","description":"pkg0 desc","homepage":"https://pkg0.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-01-03T15:04:05Z","epoch":0,"release":"0.fc30","source":{"license":"MIT","version":"0.1"}},{"arch":"x86_64","build_time":"2006-01-02T15:04:05Z","epoch":0,"release":"0.fc30","source":{"license":"MIT","version":"0.0"}}]},{"name":"package1","summary":"pkg1 sum","description":"pkg1 desc","homepage":"https://pkg1.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-02-02T15:04:05Z","epoch":0,"release":"1.fc30","source":{"license":"MIT","version":"1.0"}},{"arch":"x86_64","build_time":"2006-02-03T15:04:05Z","epoch":0,"release":"1.fc30","source":{"license":"MIT","version":"1.1"}}]},{"name":"package10","summary":"pkg10 sum","description":"pkg10 desc","homepage":"https://pkg10.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-11-02T15:04:05Z","epoch":0,"release":"10.fc30","source":{"license":"MIT","version":"10.0"}},{"arch":"x86_64","build_time":"2006-11-03T15:04:05Z","epoch":0,"release":"10.fc30","source":{"license":"MIT","version":"10.1"}}]},{"name":"package11","summary":"pkg11 sum","description":"pkg11 desc","homepage":"https://pkg11.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-12-03T15:04:05Z","epoch":0,"release":"11.fc30","source":{"license":"MIT","version":"11.1"}},{"arch":"x86_64","build_time":"2006-12-02T15:04:05Z","epoch":0,"release":"11.fc30","source":{"license":"MIT","version":"11.0"}}]},{"name":"package12","summary":"pkg12 sum","description":"pkg12 desc","homepage":"https://pkg12.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-01-02T15:04:05Z","epoch":0,"release":"12.fc30","source":{"license":"MIT","version":"12.0"}},{"arch":"x86_64","build_time":"2007-01-03T15:04:05Z","epoch":0,"release":"12.fc30","source":{"license":"MIT","version":"12.1"}}]},{"name":"package13","summary":"pkg13 sum","description":"pkg13 desc","homepage":"https://pkg13.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-02-02T15:04:05Z","epoch":0,"release":"13.fc30","source":{"license":"MIT","version":"13.0"}},{"arch":"x86_64","build_time":"2007-02-03T15:04:05Z","epoch":0,"release":"13.fc30","source":{"license":"MIT","version":"13.1"}}]},{"name":"package14","summary":"pkg14 sum","description":"pkg14 desc","homepage":"https://pkg14.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-03-03T15:04:05Z","epoch":0,"release":"14.fc30","source":{"license":"MIT","version":"14.1"}},{"arch":"x86_64","build_time":"2007-03-02T15:04:05Z","epoch":0,"release":"14.fc30","source":{"license":"MIT","version":"14.0"}}]},{"name":"package15","summary":"pkg15 sum","description":"pkg15 desc","homepage":"https://pkg15.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-04-03T15:04:05Z","epoch":0,"release":"15.fc30","source":{"license":"MIT","version":"15.1"}},{"arch":"x86_64","build_time":"2007-04-02T15:04:05Z","epoch":0,"release":"15.fc30","source":{"license":"MIT","version":"15.0"}}]},{"name":"package16","summary":"pkg16 sum","description":"pkg16 desc","homepage":"https://pkg16.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-05-02T15:04:05Z","epoch":0,"release":"16.fc30","source":{"license":"MIT","version":"16.0"}},{"arch":"x86_64","build_time":"2007-05-03T15:04:05Z","epoch":0,"release":"16.fc30","source":{"license":"MIT","version":"16.1"}}]},{"name":"package17","summary":"pkg17 sum","description":"pkg17 desc","homepage":"https://pkg17.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-06-03T15:04:05Z","epoch":0,"release":"17.fc30","source":{"license":"MIT","version":"17.1"}},{"arch":"x86_64","build_time":"2007-06-02T15:04:05Z","epoch":0,"release":"17.fc30","source":{"license":"MIT","version":"17.0"}}]},{"name":"package18","summary":"pkg18 sum","description":"pkg18 desc","homepage":"https://pkg18.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-07-02T15:04:05Z","epoch":0,"release":"18.fc30","source":{"license":"MIT","version":"18.0"}},{"arch":"x86_64","build_time":"2007-07-03T15:04:05Z","epoch":0,"release":"18.fc30","source":{"license":"MIT","version":"18.1"}}]},{"name":"package19","summary":"pkg19 sum","description":"pkg19 desc","homepage":"https://pkg19.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-08-03T15:04:05Z","epoch":0,"release":"19.fc30","source":{"license":"MIT","version":"19.1"}},{"arch":"x86_64","build_time":"2007-08-02T15:04:05Z","epoch":0,"release":"19.fc30","source":{"license":"MIT","version":"19.0"}}]},{"name":"package2","summary":"pkg2 sum","description":"pkg2 desc","homepage":"https://pkg2.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-03-02T15:04:05Z","epoch":0,"release":"2.fc30","source":{"license":"MIT","version":"2.0"}},{"arch":"x86_64","build_time":"2006-03-03T15:04:05Z","epoch":0,"release":"2.fc30","source":{"license":"MIT","version":"2.1"}}]},{"name":"package20","summary":"pkg20 sum","description":"pkg20 desc","homepage":"https://pkg20.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-09-03T15:04:05Z","epoch":0,"release":"20.fc30","source":{"license":"MIT","version":"20.1"}},{"arch":"x86_64","build_time":"2007-09-02T15:04:05Z","epoch":0,"release":"20.fc30","source":{"license":"MIT","version":"20.0"}}]},{"name":"package21","summary":"pkg21 sum","description":"pkg21 desc","homepage":"https://pkg21.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-10-02T15:04:05Z","epoch":0,"release":"21.fc30","source":{"license":"MIT","version":"21.0"}},{"arch":"x86_64","build_time":"2007-10-03T15:04:05Z","epoch":0,"release":"21.fc30","source":{"license":"MIT","version":"21.1"}}]},{"name":"package3","summary":"pkg3 sum","description":"pkg3 desc","homepage":"https://pkg3.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-04-03T15:04:05Z","epoch":0,"release":"3.fc30","source":{"license":"MIT","version":"3.1"}},{"arch":"x86_64","build_time":"2006-04-02T15:04:05Z","epoch":0,"release":"3.fc30","source":{"license":"MIT","version":"3.0"}}]},{"name":"package4","summary":"pkg4 sum","description":"pkg4 desc","homepage":"https://pkg4.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-05-03T15:04:05Z","epoch":0,"release":"4.fc30","source":{"license":"MIT","version":"4.1"}},{"arch":"x86_64","build_time":"2006-05-02T15:04:05Z","epoch":0,"release":"4.fc30","source":{"license":"MIT","version":"4.0"}}]},{"name":"package5","summary":"pkg5 sum","description":"pkg5 desc","homepage":"https://pkg5.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-06-03T15:04:05Z","epoch":0,"release":"5.fc30","source":{"license":"MIT","version":"5.1"}},{"arch":"x86_64","build_time":"2006-06-02T15:04:05Z","epoch":0,"release":"5.fc30","source":{"license":"MIT","version":"5.0"}}]},{"name":"package6","summary":"pkg6 sum","description":"pkg6 desc","homepage":"https://pkg6.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-07-02T15:04:05Z","epoch":0,"release":"6.fc30","source":{"license":"MIT","version":"6.0"}},{"arch":"x86_64","build_time":"2006-07-03T15:04:05Z","epoch":0,"release":"6.fc30","source":{"license":"MIT","version":"6.1"}}]},{"name":"package7","summary":"pkg7 sum","description":"pkg7 desc","homepage":"https://pkg7.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-08-02T15:04:05Z","epoch":0,"release":"7.fc30","source":{"license":"MIT","version":"7.0"}},{"arch":"x86_64","build_time":"2006-08-03T15:04:05Z","epoch":0,"release":"7.fc30","source":{"license":"MIT","version":"7.1"}}]},{"name":"package8","summary":"pkg8 sum","description":"pkg8 desc","homepage":"https://pkg8.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-09-03T15:04:05Z","epoch":0,"release":"8.fc30","source":{"license":"MIT","version":"8.1"}},{"arch":"x86_64","build_time":"2006-09-02T15:04:05Z","epoch":0,"release":"8.fc30","source":{"license":"MIT","version":"8.0"}}]},{"name":"package9","summary":"pkg9 sum","description":"pkg9 desc","homepage":"https://pkg9.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-10-02T15:04:05Z","epoch":0,"release":"9.fc30","source":{"license":"MIT","version":"9.0"}},{"arch":"x86_64","build_time":"2006-10-03T15:04:05Z","epoch":0,"release":"9.fc30","source":{"license":"MIT","version":"9.1"}}]}]}`},
-		{rpmmd_mock.BaseFixture, "/api/v0/projects/info/package2*,package16", http.StatusOK, `{"projects":[{"name":"package16","summary":"pkg16 sum","description":"pkg16 desc","homepage":"https://pkg16.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-05-02T15:04:05Z","epoch":0,"release":"16.fc30","source":{"license":"MIT","version":"16.0"}},{"arch":"x86_64","build_time":"2007-05-03T15:04:05Z","epoch":0,"release":"16.fc30","source":{"license":"MIT","version":"16.1"}}]},{"name":"package2","summary":"pkg2 sum","description":"pkg2 desc","homepage":"https://pkg2.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-03-03T15:04:05Z","epoch":0,"release":"2.fc30","source":{"license":"MIT","version":"2.1"}},{"arch":"x86_64","build_time":"2006-03-02T15:04:05Z","epoch":0,"release":"2.fc30","source":{"license":"MIT","version":"2.0"}}]},{"name":"package20","summary":"pkg20 sum","description":"pkg20 desc","homepage":"https://pkg20.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-09-03T15:04:05Z","epoch":0,"release":"20.fc30","source":{"license":"MIT","version":"20.1"}},{"arch":"x86_64","build_time":"2007-09-02T15:04:05Z","epoch":0,"release":"20.fc30","source":{"license":"MIT","version":"20.0"}}]},{"name":"package21","summary":"pkg21 sum","description":"pkg21 desc","homepage":"https://pkg21.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-10-03T15:04:05Z","epoch":0,"release":"21.fc30","source":{"license":"MIT","version":"21.1"}},{"arch":"x86_64","build_time":"2007-10-02T15:04:05Z","epoch":0,"release":"21.fc30","source":{"license":"MIT","version":"21.0"}}]}]}`},
+		{rpmmd_mock.BaseFixture, "/api/v0/projects/info/package2*,package16", http.StatusOK, `{"projects":[{"name":"package16","summary":"pkg16 sum","description":"pkg16 desc","homepage":"https://pkg16.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-05-02T15:04:05Z","epoch":0,"release":"16.fc30","source":{"license":"MIT","version":"16.0"}},{"arch":"x86_64","build_time":"2007-05-03T15:04:05Z","epoch":0,"release":"16.fc30","source":{"license":"MIT","version":"16.1"}}]},{"name":"package2","summary":"pkg2 sum","description":"pkg2 desc","homepage":"https://pkg2.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-03-02T15:04:05Z","epoch":0,"release":"2.fc30","source":{"license":"MIT","version":"2.0"}},{"arch":"x86_64","build_time":"2006-03-03T15:04:05Z","epoch":0,"release":"2.fc30","source":{"license":"MIT","version":"2.1"}}]},{"name":"package20","summary":"pkg20 sum","description":"pkg20 desc","homepage":"https://pkg20.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-09-03T15:04:05Z","epoch":0,"release":"20.fc30","source":{"license":"MIT","version":"20.1"}},{"arch":"x86_64","build_time":"2007-09-02T15:04:05Z","epoch":0,"release":"20.fc30","source":{"license":"MIT","version":"20.0"}}]},{"name":"package21","summary":"pkg21 sum","description":"pkg21 desc","homepage":"https://pkg21.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-10-02T15:04:05Z","epoch":0,"release":"21.fc30","source":{"license":"MIT","version":"21.0"}},{"arch":"x86_64","build_time":"2007-10-03T15:04:05Z","epoch":0,"release":"21.fc30","source":{"license":"MIT","version":"21.1"}}]}]}`},
 		{rpmmd_mock.BadFetch, "/api/v0/projects/info/package2*,package16", http.StatusBadRequest, `{"status":false,"errors":[{"id":"ModulesError","msg":"msg: DNF error occured: FetchError: There was a problem when fetching packages."}]}`},
 	}
 
 	for _, c := range cases {
-		api, _ := createWeldrAPI(c.Fixture)
-		testRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
+		api := createWeldrAPI(c.Fixture)
+		test.TestRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
 	}
 }
 
 func TestModulesInfo(t *testing.T) {
 	var cases = []struct {
-		Fixture        rpmmd_mock.Fixture
+		Fixture        rpmmd_mock.FixtureGenerator
 		Path           string
 		ExpectedStatus int
 		ExpectedJSON   string
@@ -469,19 +318,19 @@ func TestModulesInfo(t *testing.T) {
 		{rpmmd_mock.BaseFixture, "/api/v0/modules/info/nonexistingpkg", http.StatusBadRequest, `{"status":false,"errors":[{"id":"UnknownModule","msg":"No packages have been found."}]}`},
 		{rpmmd_mock.BadDepsolve, "/api/v0/modules/info/package1", http.StatusBadRequest, `{"status":false,"errors":[{"id":"ModulesError","msg":"Cannot depsolve package package1: DNF error occured: DepsolveError: There was a problem depsolving ['go2rpm']: \n Problem: conflicting requests\n  - nothing provides askalono-cli needed by go2rpm-1-4.fc31.noarch"}]}`},
 		{rpmmd_mock.BaseFixture, "/api/v0/modules/info/package2*,package16", http.StatusOK, `{"modules":[{"name":"package16","summary":"pkg16 sum","description":"pkg16 desc","homepage":"https://pkg16.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-05-02T15:04:05Z","epoch":0,"release":"16.fc30","source":{"license":"MIT","version":"16.0"}},{"arch":"x86_64","build_time":"2007-05-03T15:04:05Z","epoch":0,"release":"16.fc30","source":{"license":"MIT","version":"16.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package2","summary":"pkg2 sum","description":"pkg2 desc","homepage":"https://pkg2.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-03-02T15:04:05Z","epoch":0,"release":"2.fc30","source":{"license":"MIT","version":"2.0"}},{"arch":"x86_64","build_time":"2006-03-03T15:04:05Z","epoch":0,"release":"2.fc30","source":{"license":"MIT","version":"2.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package20","summary":"pkg20 sum","description":"pkg20 desc","homepage":"https://pkg20.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-09-03T15:04:05Z","epoch":0,"release":"20.fc30","source":{"license":"MIT","version":"20.1"}},{"arch":"x86_64","build_time":"2007-09-02T15:04:05Z","epoch":0,"release":"20.fc30","source":{"license":"MIT","version":"20.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package21","summary":"pkg21 sum","description":"pkg21 desc","homepage":"https://pkg21.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-10-02T15:04:05Z","epoch":0,"release":"21.fc30","source":{"license":"MIT","version":"21.0"}},{"arch":"x86_64","build_time":"2007-10-03T15:04:05Z","epoch":0,"release":"21.fc30","source":{"license":"MIT","version":"21.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]}]}`},
-		{rpmmd_mock.BaseFixture, "/api/v0/modules/info/*", http.StatusOK, `{"modules":[{"name":"package0","summary":"pkg0 sum","description":"pkg0 desc","homepage":"https://pkg0.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-01-02T15:04:05Z","epoch":0,"release":"0.fc30","source":{"license":"MIT","version":"0.0"}},{"arch":"x86_64","build_time":"2006-01-03T15:04:05Z","epoch":0,"release":"0.fc30","source":{"license":"MIT","version":"0.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package1","summary":"pkg1 sum","description":"pkg1 desc","homepage":"https://pkg1.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-02-02T15:04:05Z","epoch":0,"release":"1.fc30","source":{"license":"MIT","version":"1.0"}},{"arch":"x86_64","build_time":"2006-02-03T15:04:05Z","epoch":0,"release":"1.fc30","source":{"license":"MIT","version":"1.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package10","summary":"pkg10 sum","description":"pkg10 desc","homepage":"https://pkg10.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-11-03T15:04:05Z","epoch":0,"release":"10.fc30","source":{"license":"MIT","version":"10.1"}},{"arch":"x86_64","build_time":"2006-11-02T15:04:05Z","epoch":0,"release":"10.fc30","source":{"license":"MIT","version":"10.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package11","summary":"pkg11 sum","description":"pkg11 desc","homepage":"https://pkg11.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-12-03T15:04:05Z","epoch":0,"release":"11.fc30","source":{"license":"MIT","version":"11.1"}},{"arch":"x86_64","build_time":"2006-12-02T15:04:05Z","epoch":0,"release":"11.fc30","source":{"license":"MIT","version":"11.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package12","summary":"pkg12 sum","description":"pkg12 desc","homepage":"https://pkg12.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-01-02T15:04:05Z","epoch":0,"release":"12.fc30","source":{"license":"MIT","version":"12.0"}},{"arch":"x86_64","build_time":"2007-01-03T15:04:05Z","epoch":0,"release":"12.fc30","source":{"license":"MIT","version":"12.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package13","summary":"pkg13 sum","description":"pkg13 desc","homepage":"https://pkg13.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-02-02T15:04:05Z","epoch":0,"release":"13.fc30","source":{"license":"MIT","version":"13.0"}},{"arch":"x86_64","build_time":"2007-02-03T15:04:05Z","epoch":0,"release":"13.fc30","source":{"license":"MIT","version":"13.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package14","summary":"pkg14 sum","description":"pkg14 desc","homepage":"https://pkg14.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-03-03T15:04:05Z","epoch":0,"release":"14.fc30","source":{"license":"MIT","version":"14.1"}},{"arch":"x86_64","build_time":"2007-03-02T15:04:05Z","epoch":0,"release":"14.fc30","source":{"license":"MIT","version":"14.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package15","summary":"pkg15 sum","description":"pkg15 desc","homepage":"https://pkg15.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-04-03T15:04:05Z","epoch":0,"release":"15.fc30","source":{"license":"MIT","version":"15.1"}},{"arch":"x86_64","build_time":"2007-04-02T15:04:05Z","epoch":0,"release":"15.fc30","source":{"license":"MIT","version":"15.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package16","summary":"pkg16 sum","description":"pkg16 desc","homepage":"https://pkg16.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-05-02T15:04:05Z","epoch":0,"release":"16.fc30","source":{"license":"MIT","version":"16.0"}},{"arch":"x86_64","build_time":"2007-05-03T15:04:05Z","epoch":0,"release":"16.fc30","source":{"license":"MIT","version":"16.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package17","summary":"pkg17 sum","description":"pkg17 desc","homepage":"https://pkg17.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-06-03T15:04:05Z","epoch":0,"release":"17.fc30","source":{"license":"MIT","version":"17.1"}},{"arch":"x86_64","build_time":"2007-06-02T15:04:05Z","epoch":0,"release":"17.fc30","source":{"license":"MIT","version":"17.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package18","summary":"pkg18 sum","description":"pkg18 desc","homepage":"https://pkg18.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-07-02T15:04:05Z","epoch":0,"release":"18.fc30","source":{"license":"MIT","version":"18.0"}},{"arch":"x86_64","build_time":"2007-07-03T15:04:05Z","epoch":0,"release":"18.fc30","source":{"license":"MIT","version":"18.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package19","summary":"pkg19 sum","description":"pkg19 desc","homepage":"https://pkg19.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-08-02T15:04:05Z","epoch":0,"release":"19.fc30","source":{"license":"MIT","version":"19.0"}},{"arch":"x86_64","build_time":"2007-08-03T15:04:05Z","epoch":0,"release":"19.fc30","source":{"license":"MIT","version":"19.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package2","summary":"pkg2 sum","description":"pkg2 desc","homepage":"https://pkg2.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-03-03T15:04:05Z","epoch":0,"release":"2.fc30","source":{"license":"MIT","version":"2.1"}},{"arch":"x86_64","build_time":"2006-03-02T15:04:05Z","epoch":0,"release":"2.fc30","source":{"license":"MIT","version":"2.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package20","summary":"pkg20 sum","description":"pkg20 desc","homepage":"https://pkg20.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-09-03T15:04:05Z","epoch":0,"release":"20.fc30","source":{"license":"MIT","version":"20.1"}},{"arch":"x86_64","build_time":"2007-09-02T15:04:05Z","epoch":0,"release":"20.fc30","source":{"license":"MIT","version":"20.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package21","summary":"pkg21 sum","description":"pkg21 desc","homepage":"https://pkg21.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-10-03T15:04:05Z","epoch":0,"release":"21.fc30","source":{"license":"MIT","version":"21.1"}},{"arch":"x86_64","build_time":"2007-10-02T15:04:05Z","epoch":0,"release":"21.fc30","source":{"license":"MIT","version":"21.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package3","summary":"pkg3 sum","description":"pkg3 desc","homepage":"https://pkg3.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-04-03T15:04:05Z","epoch":0,"release":"3.fc30","source":{"license":"MIT","version":"3.1"}},{"arch":"x86_64","build_time":"2006-04-02T15:04:05Z","epoch":0,"release":"3.fc30","source":{"license":"MIT","version":"3.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package4","summary":"pkg4 sum","description":"pkg4 desc","homepage":"https://pkg4.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-05-03T15:04:05Z","epoch":0,"release":"4.fc30","source":{"license":"MIT","version":"4.1"}},{"arch":"x86_64","build_time":"2006-05-02T15:04:05Z","epoch":0,"release":"4.fc30","source":{"license":"MIT","version":"4.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package5","summary":"pkg5 sum","description":"pkg5 desc","homepage":"https://pkg5.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-06-02T15:04:05Z","epoch":0,"release":"5.fc30","source":{"license":"MIT","version":"5.0"}},{"arch":"x86_64","build_time":"2006-06-03T15:04:05Z","epoch":0,"release":"5.fc30","source":{"license":"MIT","version":"5.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package6","summary":"pkg6 sum","description":"pkg6 desc","homepage":"https://pkg6.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-07-02T15:04:05Z","epoch":0,"release":"6.fc30","source":{"license":"MIT","version":"6.0"}},{"arch":"x86_64","build_time":"2006-07-03T15:04:05Z","epoch":0,"release":"6.fc30","source":{"license":"MIT","version":"6.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package7","summary":"pkg7 sum","description":"pkg7 desc","homepage":"https://pkg7.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-08-03T15:04:05Z","epoch":0,"release":"7.fc30","source":{"license":"MIT","version":"7.1"}},{"arch":"x86_64","build_time":"2006-08-02T15:04:05Z","epoch":0,"release":"7.fc30","source":{"license":"MIT","version":"7.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package8","summary":"pkg8 sum","description":"pkg8 desc","homepage":"https://pkg8.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-09-03T15:04:05Z","epoch":0,"release":"8.fc30","source":{"license":"MIT","version":"8.1"}},{"arch":"x86_64","build_time":"2006-09-02T15:04:05Z","epoch":0,"release":"8.fc30","source":{"license":"MIT","version":"8.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package9","summary":"pkg9 sum","description":"pkg9 desc","homepage":"https://pkg9.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-10-03T15:04:05Z","epoch":0,"release":"9.fc30","source":{"license":"MIT","version":"9.1"}},{"arch":"x86_64","build_time":"2006-10-02T15:04:05Z","epoch":0,"release":"9.fc30","source":{"license":"MIT","version":"9.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]}]}`},
+		{rpmmd_mock.BaseFixture, "/api/v0/modules/info/*", http.StatusOK, `{"modules":[{"name":"package0","summary":"pkg0 sum","description":"pkg0 desc","homepage":"https://pkg0.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-01-03T15:04:05Z","epoch":0,"release":"0.fc30","source":{"license":"MIT","version":"0.1"}},{"arch":"x86_64","build_time":"2006-01-02T15:04:05Z","epoch":0,"release":"0.fc30","source":{"license":"MIT","version":"0.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package1","summary":"pkg1 sum","description":"pkg1 desc","homepage":"https://pkg1.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-02-02T15:04:05Z","epoch":0,"release":"1.fc30","source":{"license":"MIT","version":"1.0"}},{"arch":"x86_64","build_time":"2006-02-03T15:04:05Z","epoch":0,"release":"1.fc30","source":{"license":"MIT","version":"1.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package10","summary":"pkg10 sum","description":"pkg10 desc","homepage":"https://pkg10.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-11-02T15:04:05Z","epoch":0,"release":"10.fc30","source":{"license":"MIT","version":"10.0"}},{"arch":"x86_64","build_time":"2006-11-03T15:04:05Z","epoch":0,"release":"10.fc30","source":{"license":"MIT","version":"10.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package11","summary":"pkg11 sum","description":"pkg11 desc","homepage":"https://pkg11.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-12-03T15:04:05Z","epoch":0,"release":"11.fc30","source":{"license":"MIT","version":"11.1"}},{"arch":"x86_64","build_time":"2006-12-02T15:04:05Z","epoch":0,"release":"11.fc30","source":{"license":"MIT","version":"11.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package12","summary":"pkg12 sum","description":"pkg12 desc","homepage":"https://pkg12.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-01-02T15:04:05Z","epoch":0,"release":"12.fc30","source":{"license":"MIT","version":"12.0"}},{"arch":"x86_64","build_time":"2007-01-03T15:04:05Z","epoch":0,"release":"12.fc30","source":{"license":"MIT","version":"12.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package13","summary":"pkg13 sum","description":"pkg13 desc","homepage":"https://pkg13.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-02-02T15:04:05Z","epoch":0,"release":"13.fc30","source":{"license":"MIT","version":"13.0"}},{"arch":"x86_64","build_time":"2007-02-03T15:04:05Z","epoch":0,"release":"13.fc30","source":{"license":"MIT","version":"13.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package14","summary":"pkg14 sum","description":"pkg14 desc","homepage":"https://pkg14.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-03-03T15:04:05Z","epoch":0,"release":"14.fc30","source":{"license":"MIT","version":"14.1"}},{"arch":"x86_64","build_time":"2007-03-02T15:04:05Z","epoch":0,"release":"14.fc30","source":{"license":"MIT","version":"14.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package15","summary":"pkg15 sum","description":"pkg15 desc","homepage":"https://pkg15.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-04-03T15:04:05Z","epoch":0,"release":"15.fc30","source":{"license":"MIT","version":"15.1"}},{"arch":"x86_64","build_time":"2007-04-02T15:04:05Z","epoch":0,"release":"15.fc30","source":{"license":"MIT","version":"15.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package16","summary":"pkg16 sum","description":"pkg16 desc","homepage":"https://pkg16.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-05-02T15:04:05Z","epoch":0,"release":"16.fc30","source":{"license":"MIT","version":"16.0"}},{"arch":"x86_64","build_time":"2007-05-03T15:04:05Z","epoch":0,"release":"16.fc30","source":{"license":"MIT","version":"16.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package17","summary":"pkg17 sum","description":"pkg17 desc","homepage":"https://pkg17.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-06-03T15:04:05Z","epoch":0,"release":"17.fc30","source":{"license":"MIT","version":"17.1"}},{"arch":"x86_64","build_time":"2007-06-02T15:04:05Z","epoch":0,"release":"17.fc30","source":{"license":"MIT","version":"17.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package18","summary":"pkg18 sum","description":"pkg18 desc","homepage":"https://pkg18.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-07-02T15:04:05Z","epoch":0,"release":"18.fc30","source":{"license":"MIT","version":"18.0"}},{"arch":"x86_64","build_time":"2007-07-03T15:04:05Z","epoch":0,"release":"18.fc30","source":{"license":"MIT","version":"18.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package19","summary":"pkg19 sum","description":"pkg19 desc","homepage":"https://pkg19.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-08-03T15:04:05Z","epoch":0,"release":"19.fc30","source":{"license":"MIT","version":"19.1"}},{"arch":"x86_64","build_time":"2007-08-02T15:04:05Z","epoch":0,"release":"19.fc30","source":{"license":"MIT","version":"19.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package2","summary":"pkg2 sum","description":"pkg2 desc","homepage":"https://pkg2.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-03-02T15:04:05Z","epoch":0,"release":"2.fc30","source":{"license":"MIT","version":"2.0"}},{"arch":"x86_64","build_time":"2006-03-03T15:04:05Z","epoch":0,"release":"2.fc30","source":{"license":"MIT","version":"2.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package20","summary":"pkg20 sum","description":"pkg20 desc","homepage":"https://pkg20.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-09-03T15:04:05Z","epoch":0,"release":"20.fc30","source":{"license":"MIT","version":"20.1"}},{"arch":"x86_64","build_time":"2007-09-02T15:04:05Z","epoch":0,"release":"20.fc30","source":{"license":"MIT","version":"20.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package21","summary":"pkg21 sum","description":"pkg21 desc","homepage":"https://pkg21.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2007-10-02T15:04:05Z","epoch":0,"release":"21.fc30","source":{"license":"MIT","version":"21.0"}},{"arch":"x86_64","build_time":"2007-10-03T15:04:05Z","epoch":0,"release":"21.fc30","source":{"license":"MIT","version":"21.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package3","summary":"pkg3 sum","description":"pkg3 desc","homepage":"https://pkg3.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-04-03T15:04:05Z","epoch":0,"release":"3.fc30","source":{"license":"MIT","version":"3.1"}},{"arch":"x86_64","build_time":"2006-04-02T15:04:05Z","epoch":0,"release":"3.fc30","source":{"license":"MIT","version":"3.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package4","summary":"pkg4 sum","description":"pkg4 desc","homepage":"https://pkg4.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-05-03T15:04:05Z","epoch":0,"release":"4.fc30","source":{"license":"MIT","version":"4.1"}},{"arch":"x86_64","build_time":"2006-05-02T15:04:05Z","epoch":0,"release":"4.fc30","source":{"license":"MIT","version":"4.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package5","summary":"pkg5 sum","description":"pkg5 desc","homepage":"https://pkg5.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-06-03T15:04:05Z","epoch":0,"release":"5.fc30","source":{"license":"MIT","version":"5.1"}},{"arch":"x86_64","build_time":"2006-06-02T15:04:05Z","epoch":0,"release":"5.fc30","source":{"license":"MIT","version":"5.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package6","summary":"pkg6 sum","description":"pkg6 desc","homepage":"https://pkg6.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-07-02T15:04:05Z","epoch":0,"release":"6.fc30","source":{"license":"MIT","version":"6.0"}},{"arch":"x86_64","build_time":"2006-07-03T15:04:05Z","epoch":0,"release":"6.fc30","source":{"license":"MIT","version":"6.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package7","summary":"pkg7 sum","description":"pkg7 desc","homepage":"https://pkg7.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-08-02T15:04:05Z","epoch":0,"release":"7.fc30","source":{"license":"MIT","version":"7.0"}},{"arch":"x86_64","build_time":"2006-08-03T15:04:05Z","epoch":0,"release":"7.fc30","source":{"license":"MIT","version":"7.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package8","summary":"pkg8 sum","description":"pkg8 desc","homepage":"https://pkg8.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-09-03T15:04:05Z","epoch":0,"release":"8.fc30","source":{"license":"MIT","version":"8.1"}},{"arch":"x86_64","build_time":"2006-09-02T15:04:05Z","epoch":0,"release":"8.fc30","source":{"license":"MIT","version":"8.0"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]},{"name":"package9","summary":"pkg9 sum","description":"pkg9 desc","homepage":"https://pkg9.example.com","upstream_vcs":"","builds":[{"arch":"x86_64","build_time":"2006-10-02T15:04:05Z","epoch":0,"release":"9.fc30","source":{"license":"MIT","version":"9.0"}},{"arch":"x86_64","build_time":"2006-10-03T15:04:05Z","epoch":0,"release":"9.fc30","source":{"license":"MIT","version":"9.1"}}],"dependencies":[{"name":"dep-package1","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"dep-package2","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]}]}`},
 		{rpmmd_mock.BadFetch, "/api/v0/modules/info/package2*,package16", http.StatusBadRequest, `{"status":false,"errors":[{"id":"ModulesError","msg":"msg: DNF error occured: FetchError: There was a problem when fetching packages."}]}`},
 	}
 
 	for _, c := range cases {
-		api, _ := createWeldrAPI(c.Fixture)
-		testRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
+		api := createWeldrAPI(c.Fixture)
+		test.TestRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
 	}
 }
 
 func TestProjectsList(t *testing.T) {
 	var cases = []struct {
-		Fixture        rpmmd_mock.Fixture
+		Fixture        rpmmd_mock.FixtureGenerator
 		Path           string
 		ExpectedStatus int
 		ExpectedJSON   string
@@ -493,14 +342,14 @@ func TestProjectsList(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api, _ := createWeldrAPI(c.Fixture)
-		testRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
+		api := createWeldrAPI(c.Fixture)
+		test.TestRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
 	}
 }
 
 func TestModulesList(t *testing.T) {
 	var cases = []struct {
-		Fixture        rpmmd_mock.Fixture
+		Fixture        rpmmd_mock.FixtureGenerator
 		Path           string
 		ExpectedStatus int
 		ExpectedJSON   string
@@ -515,7 +364,7 @@ func TestModulesList(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api, _ := createWeldrAPI(c.Fixture)
-		testRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
+		api := createWeldrAPI(c.Fixture)
+		test.TestRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
 	}
 }