Repository: osbuild/osbuild-composer
Pull request #231 (2020-02-13 14:25:48)

RCM API: use the new PushComposeRequest method
see individual commits


Patch:
diff --git a/.gitignore b/.gitignore
index 85d481647..744e2d4f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ __pycache__
 /osbuild-pipeline
 /osbuild-upload-azure
 /osbuild-upload-aws
+/osbuild-dnf-json-tests
 /osbuild-tests
 
 /*.tar.gz
diff --git a/Makefile b/Makefile
index e88a46199..55b97dbc9 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,8 @@ build:
 	go build -o osbuild-pipeline ./cmd/osbuild-pipeline/
 	go build -o osbuild-upload-azure ./cmd/osbuild-upload-azure/
 	go build -o osbuild-upload-aws ./cmd/osbuild-upload-aws/
-	go build -o osbuild-tests ./cmd/osbuild-tests
+	go build -o osbuild-tests ./cmd/osbuild-tests/
+	go build -o osbuild-dnf-json-tests ./cmd/osbuild-dnf-json-tests/
 
 .PHONY: install
 install:
diff --git a/cmd/osbuild-composer/main.go b/cmd/osbuild-composer/main.go
index e8d7fb07e..c5db90fd2 100644
--- a/cmd/osbuild-composer/main.go
+++ b/cmd/osbuild-composer/main.go
@@ -72,7 +72,7 @@ func main() {
 	// Optionally run RCM API as well as Weldr API
 	if len(listeners) == 3 {
 		rcmListener := listeners[2]
-		rcmAPI := rcm.New(logger, store)
+		rcmAPI := rcm.New(logger, store, rpmmd.NewRPMMD())
 		go rcmAPI.Serve(rcmListener)
 	}
 
diff --git a/golang-github-osbuild-composer.spec b/golang-github-osbuild-composer.spec
index 78a15d572..714cbc099 100644
--- a/golang-github-osbuild-composer.spec
+++ b/golang-github-osbuild-composer.spec
@@ -65,6 +65,7 @@ export GOFLAGS=-mod=vendor
 %gobuild -o _bin/osbuild-composer %{goipath}/cmd/osbuild-composer
 %gobuild -o _bin/osbuild-worker %{goipath}/cmd/osbuild-worker
 %gobuild -o _bin/osbuild-tests %{goipath}/cmd/osbuild-tests
+%gobuild -o _bin/osbuild-dnf-json-tests %{goipath}/cmd/osbuild-dnf-json-tests
 
 %install
 install -m 0755 -vd                                         %{buildroot}%{_libexecdir}/osbuild-composer
@@ -74,6 +75,7 @@ install -m 0755 -vp dnf-json                                %{buildroot}%{_libex
 
 install -m 0755 -vd                                         %{buildroot}%{_libexecdir}/tests/osbuild-composer
 install -m 0755 -vp _bin/osbuild-tests                      %{buildroot}%{_libexecdir}/tests/osbuild-composer/
+install -m 0755 -vp _bin/osbuild-dnf-json-tests             %{buildroot}%{_libexecdir}/tests/osbuild-composer/
 
 install -m 0755 -vd                                         %{buildroot}%{_datadir}/osbuild-composer/repositories
 install -m 0644 -vp repositories/*                          %{buildroot}%{_datadir}/osbuild-composer/repositories/
@@ -116,12 +118,14 @@ export GOPATH=$PWD/_build:%{gopath}
 Summary:	Integration tests
 Requires: 	osbuild-composer
 Requires: 	composer-cli
+Requires:	createrepo_c
 
 %description tests
 Integration tests to be run on a pristine-dedicated system to test the osbuild-composer package.
 
 %files tests
 %{_libexecdir}/tests/osbuild-composer/osbuild-tests
+%{_libexecdir}/tests/osbuild-composer/osbuild-dnf-json-tests
 
 %changelog
 * Sun Dec 1 11:00:00 CEST 2019 Ondrej Budai <obudai@redhat.com> - 4-1
diff --git a/internal/common/types.go b/internal/common/types.go
index c3b2af0c2..d62e4ea76 100644
--- a/internal/common/types.go
+++ b/internal/common/types.go
@@ -18,6 +18,16 @@ func (err *CustomJsonConversionError) Error() string {
 	return err.reason
 }
 
+// CustomTypeError is thrown when parsing strings into enumerations
+type CustomTypeError struct {
+	reason string
+}
+
+// Error returns the error as a string
+func (err *CustomTypeError) Error() string {
+	return err.reason
+}
+
 // Since Go has no generics, this is the only way to write common code for all the types present in this package.
 // It uses weakly typed maps to convert between strings and integers which are then wrapped into type aliases.
 // Specific implementations are bellow each type.
@@ -124,12 +134,12 @@ func ArchitectureExists(testedArch string) bool {
 
 // UnmarshalJSON is a custom unmarshaling function to limit the set of allowed values
 // in case the input is JSON.
-func (arch Architecture) UnmarshalJSON(data []byte) error {
+func (arch *Architecture) UnmarshalJSON(data []byte) error {
 	value, err := unmarshalHelper(data, " is not a valid JSON value", " is not a valid architecture", getArchMapping())
 	if err != nil {
 		return err
 	}
-	arch = Architecture(value)
+	*arch = Architecture(value)
 	return nil
 }
 
@@ -139,7 +149,7 @@ func (arch Architecture) MarshalJSON() ([]byte, error) {
 }
 
 func (arch Architecture) ToString() (string, bool) {
-	return toStringHelper(getDistributionMapping(), int(arch))
+	return toStringHelper(getArchMapping(), int(arch))
 }
 
 type ImageType int
@@ -199,12 +209,12 @@ func getCompatImageTypeMapping() map[int]string {
 	return mapping
 }
 
-func (imgType ImageType) UnmarshalJSON(data []byte) error {
+func (imgType *ImageType) UnmarshalJSON(data []byte) error {
 	value, err := unmarshalHelper(data, " is not a valid JSON value", " is not a valid image type", getImageTypeMapping())
 	if err != nil {
 		return err
 	}
-	imgType = ImageType(value)
+	*imgType = ImageType(value)
 	return nil
 }
 
@@ -243,6 +253,7 @@ const (
 // getArchMapping is a helper function that defines the conversion from JSON string value
 // to Distribution.
 func getDistributionMapping() map[string]int {
+	// Don't forget to add module-platform-id mapping below
 	mapping := map[string]int{
 		"fedora-30": int(Fedora30),
 		"fedora-31": int(Fedora31),
@@ -252,6 +263,22 @@ func getDistributionMapping() map[string]int {
 	return mapping
 }
 
+func (dist *Distribution) ModulePlatformID() (string, error) {
+	mapping := map[Distribution]string{
+		// TODO: this could be refactored so we don't have these strings hard coded in two places
+		Fedora30: "platform:f30",
+		Fedora31: "platform:f31",
+		Fedora32: "platform:f32",
+		RHEL82:   "platform:el8",
+	}
+	id, exists := mapping[*dist]
+	if !exists {
+		return "", &CustomTypeError{reason: "Distribution does not have a module platform ID assigned"}
+	} else {
+		return id, nil
+	}
+}
+
 func ListDistributions() []string {
 	return listHelper(getDistributionMapping())
 }
@@ -260,12 +287,12 @@ func DistributionExists(testedDistro string) bool {
 	return existsHelper(getDistributionMapping(), testedDistro)
 }
 
-func (distro Distribution) UnmarshalJSON(data []byte) error {
+func (distro *Distribution) UnmarshalJSON(data []byte) error {
 	value, err := unmarshalHelper(data, " is not a valid JSON value", " is not a valid distribution", getDistributionMapping())
 	if err != nil {
 		return err
 	}
-	distro = Distribution(value)
+	*distro = Distribution(value)
 	return nil
 }
 
@@ -300,12 +327,12 @@ func getUploadTargetMapping() map[string]int {
 	return mapping
 }
 
-func (ut UploadTarget) UnmarshalJSON(data []byte) error {
+func (ut *UploadTarget) UnmarshalJSON(data []byte) error {
 	value, err := unmarshalHelper(data, " is not a valid JSON value", " is not a valid upload target", getUploadTargetMapping())
 	if err != nil {
 		return err
 	}
-	ut = UploadTarget(value)
+	*ut = UploadTarget(value)
 	return nil
 }
 
@@ -325,5 +352,6 @@ type ComposeRequest struct {
 	Distro          Distribution        `json:"distro"`
 	Arch            Architecture        `json:"arch"`
 	Repositories    []rpmmd.RepoConfig  `json:"repositories"`
+	Checksums       map[string]string   `json:"checksums"`
 	RequestedImages []ImageRequest      `json:"requested_images"`
 }
diff --git a/internal/mocks/rpmmd/rpmmd_mock.go b/internal/mocks/rpmmd/rpmmd_mock.go
index 3bc8fe40e..fabbe7fda 100644
--- a/internal/mocks/rpmmd/rpmmd_mock.go
+++ b/internal/mocks/rpmmd/rpmmd_mock.go
@@ -30,7 +30,7 @@ func NewRPMMDMock(fixture Fixture) rpmmd.RPMMD {
 	return &rpmmdMock{Fixture: fixture}
 }
 
-func (r *rpmmdMock) FetchPackageList(repos []rpmmd.RepoConfig, modulePlatformID string) (rpmmd.PackageList, map[string]string, error) {
+func (r *rpmmdMock) FetchMetadata(repos []rpmmd.RepoConfig, modulePlatformID string) (rpmmd.PackageList, map[string]string, error) {
 	return r.Fixture.fetchPackageList.ret, r.Fixture.fetchPackageList.checksums, r.Fixture.fetchPackageList.err
 }
 
diff --git a/internal/rcm/api.go b/internal/rcm/api.go
index f319fba53..86b1f0742 100644
--- a/internal/rcm/api.go
+++ b/internal/rcm/api.go
@@ -4,6 +4,9 @@ package rcm
 
 import (
 	"encoding/json"
+	"fmt"
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"log"
 	"net"
 	"net/http"
@@ -20,14 +23,18 @@ type API struct {
 	logger *log.Logger
 	store  *store.Store
 	router *httprouter.Router
+	// rpmMetadata is an interface to dnf-json and we include it here so that we can
+	// mock it in the unit tests
+	rpmMetadata rpmmd.RPMMD
 }
 
 // New creates new RCM API
-func New(logger *log.Logger, store *store.Store) *API {
+func New(logger *log.Logger, store *store.Store, rpmMetadata rpmmd.RPMMD) *API {
 	api := &API{
-		logger: logger,
-		store:  store,
-		router: httprouter.New(),
+		logger:      logger,
+		store:       store,
+		router:      httprouter.New(),
+		rpmMetadata: rpmMetadata,
 	}
 
 	api.router.RedirectTrailingSlash = false
@@ -85,10 +92,10 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 
 	// JSON structure expected from the client
 	var composeRequest struct {
-		Distribution  string       `json:"distribution"`
-		ImageTypes    []string     `json:"image_types"`
-		Architectures []string     `json:"architectures"`
-		Repositories  []Repository `json:"repositories"`
+		Distribution  common.Distribution   `json:"distribution"`
+		ImageTypes    []common.ImageType    `json:"image_types"`
+		Architectures []common.Architecture `json:"architectures"`
+		Repositories  []Repository          `json:"repositories"`
 	}
 	// JSON structure with error message
 	var errorReason struct {
@@ -98,15 +105,12 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 	decoder := json.NewDecoder(request.Body)
 	decoder.DisallowUnknownFields()
 	err := decoder.Decode(&composeRequest)
-	if err != nil || composeRequest.Distribution == "" || len(composeRequest.Architectures) == 0 || len(composeRequest.Repositories) == 0 {
+	if err != nil || len(composeRequest.Architectures) == 0 || len(composeRequest.Repositories) == 0 || len(composeRequest.ImageTypes) == 0 {
 		writer.WriteHeader(http.StatusBadRequest)
 		errors := []string{}
 		if err != nil {
 			errors = append(errors, err.Error())
 		}
-		if composeRequest.Distribution == "" {
-			errors = append(errors, "input must specify a distribution")
-		}
 		if len(composeRequest.ImageTypes) == 0 {
 			errors = append(errors, "input must specify an image type")
 		} else if len(composeRequest.ImageTypes) != 1 {
@@ -121,16 +125,68 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 			errors = append(errors, "input must specify repositories")
 		}
 		errorReason.Error = strings.Join(errors, ", ")
-		// TODO: handle error
-		_ = json.NewEncoder(writer).Encode(errorReason)
+		err = json.NewEncoder(writer).Encode(errorReason)
+		if err != nil {
+			// JSON encoding is clearly our fault.
+			panic("Failed to encode errors in RCM API. This is a bug.")
+		}
 		return
 	}
 
+	// Create repo configurations from the URLs in the request. Use made up repo id and name, because
+	// we don't want to bother clients of this API with details like this
+	repoConfigs := []rpmmd.RepoConfig{}
+	for n, repo := range composeRequest.Repositories {
+		repoConfigs = append(repoConfigs, rpmmd.RepoConfig{
+			Id:        fmt.Sprintf("repo-%d", n),
+			Name:      fmt.Sprintf("repo-%d", n),
+			BaseURL:   repo.URL,
+			IgnoreSSL: false,
+		})
+	}
+
+	// Image requests are derived from requested image types. All of them are uploaded to Koji, because
+	// this API is only for RCM.
+	requestedImages := []common.ImageRequest{}
+	for _, imgType := range composeRequest.ImageTypes {
+		requestedImages = append(requestedImages, common.ImageRequest{
+			ImgType: imgType,
+			// TODO: use koji upload type as soon as it is available
+			UpTarget: []common.UploadTarget{},
+		})
+	}
+
+	modulePlatformID, err := composeRequest.Distribution.ModulePlatformID()
+	if err != nil {
+		writer.WriteHeader(http.StatusBadRequest)
+		_, err := writer.Write([]byte(err.Error()))
+		if err != nil {
+			panic("Failed to write response")
+		}
+	}
+
+	// map( repo-id => checksum )
+	_, checksums, err := api.rpmMetadata.FetchMetadata(repoConfigs, modulePlatformID)
+	if err != nil {
+		writer.WriteHeader(http.StatusBadRequest)
+		_, err := writer.Write([]byte(err.Error()))
+		if err != nil {
+			panic("Failed to write response")
+		}
+	}
+
 	// Push the requested compose to the store
 	composeUUID := uuid.New()
 	// nil is used as an upload target, because LocalTarget is already used in the PushCompose function
-	// TODO: replace this with generalized version of push compose
-	err = api.store.PushCompose(composeUUID, &blueprint.Blueprint{}, nil, nil, make(map[string]string), composeRequest.Architectures[0], composeRequest.ImageTypes[0], 0, nil)
+	err = api.store.PushComposeRequest(common.ComposeRequest{
+		Blueprint:       blueprint.Blueprint{},
+		ComposeID:       composeUUID,
+		Distro:          composeRequest.Distribution,
+		Arch:            composeRequest.Architectures[0],
+		Repositories:    repoConfigs,
+		Checksums:       checksums,
+		RequestedImages: requestedImages,
+	})
 	if err != nil {
 		if api.logger != nil {
 			api.logger.Println("RCM API failed to push compose:", err)
diff --git a/internal/rpmmd/repository.go b/internal/rpmmd/repository.go
index 5227a8ee9..9175438a0 100644
--- a/internal/rpmmd/repository.go
+++ b/internal/rpmmd/repository.go
@@ -95,7 +95,13 @@ type PackageInfo struct {
 }
 
 type RPMMD interface {
-	FetchPackageList(repos []RepoConfig, modulePlatformID string) (PackageList, map[string]string, error)
+	// FetchMetadata returns all metadata about the repositories we use in the code. Specifically it is a
+	// list of packages and dictionary of checksums of the repositories.
+	FetchMetadata(repos []RepoConfig, modulePlatformID string) (PackageList, map[string]string, error)
+
+	// Depsolve takes a list of required content (specs), explicitly unwanted content (excludeSpecs), list
+	// or repositories, and platform ID for modularity. It returns a list of all packages (with solved
+	// dependencies) that will be installed into the system.
 	Depsolve(specs, excludeSpecs []string, repos []RepoConfig, modulePlatformID string, clean bool) ([]PackageSpec, map[string]string, error)
 }
 
@@ -108,6 +114,14 @@ func (err *DNFError) Error() string {
 	return fmt.Sprintf("DNF error occured: %s: %s", err.Kind, err.Reason)
 }
 
+type RepositoryError struct {
+	msg string
+}
+
+func (re *RepositoryError) Error() string {
+	return re.msg
+}
+
 func LoadRepositories(confPaths []string, distro string) (map[string][]RepoConfig, error) {
 	var f *os.File
 	var err error
@@ -199,7 +213,7 @@ func NewRPMMD() RPMMD {
 	return &rpmmdImpl{}
 }
 
-func (*rpmmdImpl) FetchPackageList(repos []RepoConfig, modulePlatformID string) (PackageList, map[string]string, error) {
+func (*rpmmdImpl) FetchMetadata(repos []RepoConfig, modulePlatformID string) (PackageList, map[string]string, error) {
 	var arguments = struct {
 		Repos            []RepoConfig `json:"repos"`
 		ModulePlatformID string       `json:"module_platform_id"`
diff --git a/internal/store/store.go b/internal/store/store.go
index db157348e..774f4f3ff 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -15,6 +15,7 @@ import (
 	"log"
 	"os"
 	"path/filepath"
+	"reflect"
 	"sort"
 	"strconv"
 	"strings"
@@ -586,7 +587,7 @@ func (s *Store) PushComposeRequest(request common.ComposeRequest) error {
 	}
 
 	distroStruct := s.distroRegistry.GetDistro(distroString)
-	if distroStruct == nil {
+	if distroStruct == nil || (reflect.ValueOf(distroStruct).Kind() == reflect.Ptr && reflect.ValueOf(distroStruct).IsNil()) {
 		panic("fatal error, distro should exist but it is not in the registry")
 	}
 
@@ -602,13 +603,12 @@ func (s *Store) PushComposeRequest(request common.ComposeRequest) error {
 	for imageBuildID, imageRequest := range request.RequestedImages {
 		// TODO: handle custom upload targets
 		// TODO: this requires changes in the Compose Request struct
-		// TODO: implment support for no checksums
 		// This should never happen and once distro.Pipeline is refactored this check will go away
 		imgTypeCompatStr, exists := imageRequest.ImgType.ToCompatString()
 		if !exists {
 			panic("fatal error, image type should exist but it does not")
 		}
-		pipelineStruct, err := distroStruct.Pipeline(&request.Blueprint, request.Repositories, nil, nil, nil, arch, imgTypeCompatStr, 0)
+		pipelineStruct, err := distroStruct.Pipeline(&request.Blueprint, request.Repositories, nil, nil, request.Checksums, arch, imgTypeCompatStr, 0)
 		if err != nil {
 			return err
 		}
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index 1b1c82a21..47c40a1f9 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -1839,7 +1839,7 @@ func (api *API) fetchPackageList() (rpmmd.PackageList, error) {
 		repos = append(repos, source.RepoConfig())
 	}
 
-	packages, _, err := api.rpmmd.FetchPackageList(repos, api.distro.ModulePlatformID())
+	packages, _, err := api.rpmmd.FetchMetadata(repos, api.distro.ModulePlatformID())
 	return packages, err
 }
 


Test patch:
diff --git a/cmd/osbuild-dnf-json-tests/main.go b/cmd/osbuild-dnf-json-tests/main.go
new file mode 100644
index 000000000..4c56b1845
--- /dev/null
+++ b/cmd/osbuild-dnf-json-tests/main.go
@@ -0,0 +1,71 @@
+// This package contains tests related to dnf-json and rpmmd package.
+package main
+
+import (
+	"fmt"
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/exec"
+	"path"
+)
+
+func main() {
+	// Tests that the package wrapping dnf-json works as expected
+	dir, err := setUpTemporaryRepository()
+	defer func(dir string) {
+		err := tearDownTemporaryRepository(dir)
+		if err != nil {
+			log.Print("Warning: failed to clean up temporary repository.")
+		}
+	}(dir)
+	if err != nil {
+		log.Panic("Failed to set up temporary repository:", err)
+	}
+	TestFetchChecksum(false, dir)
+}
+
+func setUpTemporaryRepository() (string, error) {
+	dir, err := ioutil.TempDir("/tmp", "osbuild-composer-test-")
+	if err != nil {
+		return "", err
+	}
+	cmd := exec.Command("createrepo_c", path.Join(dir))
+	err = cmd.Start()
+	if err != nil {
+		return "", err
+	}
+	err = cmd.Wait()
+	if err != nil {
+		return "", err
+	}
+	return dir, nil
+}
+
+func tearDownTemporaryRepository(dir string) error {
+	return os.RemoveAll(dir)
+}
+
+func TestFetchChecksum(quiet bool, dir string) {
+	repoCfg := rpmmd.RepoConfig{
+		Id:        "repo",
+		Name:      "repo",
+		BaseURL:   fmt.Sprintf("file://%s", dir),
+		IgnoreSSL: true,
+	}
+	if !quiet {
+		log.Println("Running TestFetchChecksum on:", dir)
+	}
+	rpmMetadata := rpmmd.NewRPMMD()
+	_, c, err := rpmMetadata.FetchMetadata([]rpmmd.RepoConfig{repoCfg}, "platform:f31")
+	if err != nil {
+		log.Panic("Failed to fetch checksum:", err)
+	}
+	if c["repo"] == "" {
+		log.Panic("The checksum is empty")
+	}
+	if !quiet {
+		log.Println("TestFetchChecksum: SUCCESS")
+	}
+}
diff --git a/internal/common/types_test.go b/internal/common/types_test.go
index 20a8b67e8..f7ad573ee 100644
--- a/internal/common/types_test.go
+++ b/internal/common/types_test.go
@@ -129,5 +129,21 @@ func TestJSONConversionsComposeRequest(t *testing.T) {
 			t.Error("Marshaled compose request is not the one expected")
 		}
 	}
+}
 
+func TestImageType_UnmarshalJSON(t *testing.T) {
+	dict := struct {
+		ImageTypes []ImageType `json:"image_types"`
+	}{}
+	input := `{"image_types":["qcow2", "Alibaba"]}`
+	err := json.Unmarshal([]byte(input), &dict)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if dict.ImageTypes[0] != Qcow2Generic {
+		t.Fatal("failed to umarshal image type qcow2; got tag:", dict.ImageTypes[0])
+	}
+	if dict.ImageTypes[1] != Alibaba {
+		t.Fatal("failed to umarshal image type Alibaba; got tag:", dict.ImageTypes[0])
+	}
 }
diff --git a/internal/rcm/api_test.go b/internal/rcm/api_test.go
index 57ac5f5bd..1e0009d00 100644
--- a/internal/rcm/api_test.go
+++ b/internal/rcm/api_test.go
@@ -3,14 +3,15 @@ package rcm_test
 import (
 	"bytes"
 	"encoding/json"
+	"github.com/google/uuid"
 	"net/http"
 	"net/http/httptest"
 	"regexp"
 	"testing"
 
-	"github.com/google/uuid"
 	"github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
 	distro_mock "github.com/osbuild/osbuild-composer/internal/mocks/distro"
+	rpmmd_mock "github.com/osbuild/osbuild-composer/internal/mocks/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/rcm"
 	"github.com/osbuild/osbuild-composer/internal/store"
 )
@@ -43,14 +44,14 @@ func TestBasicRcmAPI(t *testing.T) {
 		{"GET", "/v1/compose", ``, "", http.StatusMethodNotAllowed, ``},
 		{"POST", "/v1/compose", `{"status":"RUNNING"}`, "application/json", http.StatusBadRequest, ``},
 		{"POST", "/v1/compose", `{"status":"RUNNING"}`, "text/plain", http.StatusBadRequest, ``},
-		{"POST", "/v1/compose", `{"distribution": "fedora-30", "image_types": ["qcow2"], "architectures":["x86_64"], "repositories": [{"url": "aaa"}]}`, "application/json", http.StatusOK, `{"compose_id":".*"}`},
+		{"POST", "/v1/compose", `{"distribution": "fedora-30", "image_types": ["qcow2"], "architectures":["x86_64"], "repositories": []}`, "application/json", http.StatusBadRequest, ""},
 		{"POST", "/v1/compose/111-222-333", `{"status":"RUNNING"}`, "application/json", http.StatusMethodNotAllowed, ``},
 		{"GET", "/v1/compose/7802c476-9cd1-41b7-ba81-43c1906bce73", `{"status":"RUNNING"}`, "application/json", http.StatusBadRequest, `{"error_reason":"Compose UUID does not exist"}`},
 	}
 
 	registry := distro_mock.NewRegistry()
 	distroStruct := fedoratest.New()
-	api := rcm.New(nil, store.New(nil, distroStruct, *registry))
+	api := rcm.New(nil, store.New(nil, distroStruct, *registry), rpmmd_mock.NewRPMMDMock(rpmmd_mock.BaseFixture()))
 
 	for _, c := range cases {
 		resp := internalRequest(api, c.Method, c.Path, c.Body, c.ContentType)
@@ -72,9 +73,9 @@ func TestBasicRcmAPI(t *testing.T) {
 
 func TestSubmitCompose(t *testing.T) {
 	// Test the most basic use case: Submit a new job and get its status.
-	registry := distro_mock.NewRegistry()
 	distroStruct := fedoratest.New()
-	api := rcm.New(nil, store.New(nil, distroStruct, *registry))
+	registry := distro_mock.NewRegistry()
+	api := rcm.New(nil, store.New(nil, distroStruct, *registry), rpmmd_mock.NewRPMMDMock(rpmmd_mock.BaseFixture()))
 
 	var submit_reply struct {
 		UUID uuid.UUID `json:"compose_id"`
@@ -93,7 +94,12 @@ func TestSubmitCompose(t *testing.T) {
 		{
 			"POST",
 			"/v1/compose",
-			`{"distribution": "fedora-30", "image_types": ["qcow2"], "architectures":["x86_64"], "repositories": [{"url": "aaa"}]}`,
+			`{"distribution": "fedora-30", 
+					"image_types": ["qcow2"], 
+					"architectures":["x86_64"], 
+					"repositories": [{
+						"url": "http://download.fedoraproject.org/pub/fedora/linux/releases/30/Everything/x86_64/os/"
+					}]}`,
 			"application/json",
 		},
 	}
@@ -101,7 +107,6 @@ func TestSubmitCompose(t *testing.T) {
 	for n, c := range cases {
 		// Submit job
 		t.Logf("RCM API submit compose, case %d\n", n)
-		//resp := internalRequest(api, "POST", "/v1/compose", `{"distribution": "fedora-30", "image_types": ["test_output"], "architectures":["test_arch"], "repositories": [{"url": "aaa", "checksum": "bbb"}]}`, "application/json")
 		resp := internalRequest(api, c.Method, c.Path, c.Body, c.ContentType)
 		if resp.StatusCode != http.StatusOK {
 			t.Fatal("Failed to call /v1/compose, HTTP status code:", resp.StatusCode)