Repository: osbuild/osbuild-composer
Pull request #470 (2020-04-06 14:16:35)

osbuild requirement: bump to version 11
The newest osbuild is needed for support for aarch64. The aarch64 images
are using GPT which requires stable partuuid which was included in the
latest osbuild release (11). This will be used to produce stable
image-info tests.

msehnout: Waiting for https://bodhi.fedoraproject.org/updates/FEDORA-2020-ab8aeef5a3 to land in stable.

Patch:
diff --git a/README.md b/README.md
index de5271cb8..3dd745dd0 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +45,7 @@ provided infrastructure and services.
 
 The requirements for this project are:
 
- * `osbuild >= 7`
+ * `osbuild >= 11`
  * `systemd >= 244`
 
 At build-time, the following software is required:
diff --git a/dnf-json b/dnf-json
index 0a146dd2e..a8af924d5 100755
--- a/dnf-json
+++ b/dnf-json
@@ -93,7 +93,7 @@ module_platform_id = arguments["module_platform_id"]
 with tempfile.TemporaryDirectory() as persistdir:
     try:
         base = create_base(repos, module_platform_id, persistdir, cachedir, arch)
-    except dnf.exceptions.RepoError as e:
+    except dnf.exceptions.Error as e:
         exit_with_dnf_error("RepoError", f"Error occurred when setting up repo: {e}")
 
     if command == "dump":
diff --git a/golang-github-osbuild-composer.spec b/golang-github-osbuild-composer.spec
index eecc0d802..020ed544d 100644
--- a/golang-github-osbuild-composer.spec
+++ b/golang-github-osbuild-composer.spec
@@ -39,7 +39,7 @@ BuildRequires:  golang(github.com/stretchr/testify)
 
 Requires: golang-github-osbuild-composer-worker
 Requires: systemd
-Requires: osbuild >= 7
+Requires: osbuild >= 11
 
 Provides: osbuild-composer
 Provides: weldr
diff --git a/internal/jobqueue/api.go b/internal/jobqueue/api.go
index 4196c0f0b..ed71e152d 100644
--- a/internal/jobqueue/api.go
+++ b/internal/jobqueue/api.go
@@ -2,6 +2,7 @@ package jobqueue
 
 import (
 	"encoding/json"
+	"fmt"
 	"log"
 	"net"
 	"net/http"
@@ -58,43 +59,36 @@ func (api *API) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
 	api.router.ServeHTTP(writer, request)
 }
 
-func methodNotAllowedHandler(writer http.ResponseWriter, request *http.Request) {
-	writer.WriteHeader(http.StatusMethodNotAllowed)
-}
+// jsonErrorf() is similar to http.Error(), but returns the message in a json
+// object with a "message" field.
+func jsonErrorf(writer http.ResponseWriter, code int, message string, args ...interface{}) {
+	writer.WriteHeader(code)
 
-func notFoundHandler(writer http.ResponseWriter, request *http.Request) {
-	writer.WriteHeader(http.StatusNotFound)
+	// ignore error, because we cannot do anything useful with it
+	_ = json.NewEncoder(writer).Encode(&errorResponse{
+		Message: fmt.Sprintf(message, args...),
+	})
 }
 
-func statusResponseOK(writer http.ResponseWriter) {
-	writer.WriteHeader(http.StatusOK)
+func methodNotAllowedHandler(writer http.ResponseWriter, request *http.Request) {
+	jsonErrorf(writer, http.StatusMethodNotAllowed, "method not allowed")
 }
 
-func statusResponseError(writer http.ResponseWriter, code int, errors ...string) {
-	writer.WriteHeader(code)
-
-	for _, err := range errors {
-		_, e := writer.Write([]byte(err))
-		if e != nil {
-			panic(e)
-		}
-	}
+func notFoundHandler(writer http.ResponseWriter, request *http.Request) {
+	jsonErrorf(writer, http.StatusNotFound, "not found")
 }
 
 func (api *API) addJobHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
-	type requestBody struct {
-	}
-
 	contentType := request.Header["Content-Type"]
 	if len(contentType) != 1 || contentType[0] != "application/json" {
-		statusResponseError(writer, http.StatusUnsupportedMediaType)
+		jsonErrorf(writer, http.StatusUnsupportedMediaType, "request must contain application/json data")
 		return
 	}
 
-	var body requestBody
+	var body addJobRequest
 	err := json.NewDecoder(request.Body).Decode(&body)
 	if err != nil {
-		statusResponseError(writer, http.StatusBadRequest, "invalid request: "+err.Error())
+		jsonErrorf(writer, http.StatusBadRequest, "%v", err)
 		return
 	}
 
@@ -113,60 +107,57 @@ func (api *API) addJobHandler(writer http.ResponseWriter, request *http.Request,
 func (api *API) updateJobHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
 	contentType := request.Header["Content-Type"]
 	if len(contentType) != 1 || contentType[0] != "application/json" {
-		statusResponseError(writer, http.StatusUnsupportedMediaType)
+		jsonErrorf(writer, http.StatusUnsupportedMediaType, "request must contain application/json data")
 		return
 	}
 
 	id, err := uuid.Parse(params.ByName("job_id"))
 	if err != nil {
-		statusResponseError(writer, http.StatusBadRequest, "invalid compose id: "+err.Error())
+		jsonErrorf(writer, http.StatusBadRequest, "cannot parse compose id: %v", err)
 		return
 	}
 
 	imageBuildId, err := strconv.Atoi(params.ByName("build_id"))
 
 	if err != nil {
-		statusResponseError(writer, http.StatusBadRequest, "invalid image build id: "+err.Error())
+		jsonErrorf(writer, http.StatusBadRequest, "cannot parse image build id: %v", err)
 		return
 	}
 
 	var body updateJobRequest
 	err = json.NewDecoder(request.Body).Decode(&body)
 	if err != nil {
-		statusResponseError(writer, http.StatusBadRequest, "invalid status: "+err.Error())
+		jsonErrorf(writer, http.StatusBadRequest, "cannot parse request body: %v", err)
 		return
 	}
 
 	err = api.store.UpdateImageBuildInCompose(id, imageBuildId, body.Status, body.Result)
 	if err != nil {
 		switch err.(type) {
-		case *store.NotFoundError:
-			statusResponseError(writer, http.StatusNotFound, err.Error())
-		case *store.NotPendingError:
-			statusResponseError(writer, http.StatusNotFound, err.Error())
-		case *store.NotRunningError:
-			statusResponseError(writer, http.StatusBadRequest, err.Error())
-		case *store.InvalidRequestError:
-			statusResponseError(writer, http.StatusBadRequest, err.Error())
+		case *store.NotFoundError, *store.NotPendingError:
+			jsonErrorf(writer, http.StatusNotFound, "%v", err)
+		case *store.NotRunningError, *store.InvalidRequestError:
+			jsonErrorf(writer, http.StatusBadRequest, "%v", err)
 		default:
-			statusResponseError(writer, http.StatusInternalServerError, err.Error())
+			jsonErrorf(writer, http.StatusInternalServerError, "%v", err)
 		}
 		return
 	}
-	statusResponseOK(writer)
+
+	_ = json.NewEncoder(writer).Encode(updateJobResponse{})
 }
 
 func (api *API) addJobImageHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
 	id, err := uuid.Parse(params.ByName("job_id"))
 	if err != nil {
-		statusResponseError(writer, http.StatusBadRequest, "invalid compose id: "+err.Error())
+		jsonErrorf(writer, http.StatusBadRequest, "cannot parse compose id: %v", err)
 		return
 	}
 
 	imageBuildId, err := strconv.Atoi(params.ByName("build_id"))
 
 	if err != nil {
-		statusResponseError(writer, http.StatusBadRequest, "invalid build id: "+err.Error())
+		jsonErrorf(writer, http.StatusBadRequest, "cannot parse image build id: %v", err)
 		return
 	}
 
@@ -175,14 +166,12 @@ func (api *API) addJobImageHandler(writer http.ResponseWriter, request *http.Req
 	if err != nil {
 		switch err.(type) {
 		case *store.NotFoundError:
-			statusResponseError(writer, http.StatusNotFound, err.Error())
+			jsonErrorf(writer, http.StatusNotFound, "%v", err)
 		case *store.NoLocalTargetError:
-			statusResponseError(writer, http.StatusBadRequest, err.Error())
+			jsonErrorf(writer, http.StatusBadRequest, "%v", err)
 		default:
-			statusResponseError(writer, http.StatusInternalServerError, err.Error())
+			jsonErrorf(writer, http.StatusInternalServerError, "%v", err)
 		}
 		return
 	}
-
-	statusResponseOK(writer)
 }
diff --git a/internal/jobqueue/client.go b/internal/jobqueue/client.go
index b9f1f4f1f..96fa7965f 100644
--- a/internal/jobqueue/client.go
+++ b/internal/jobqueue/client.go
@@ -8,7 +8,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net"
 	"net/http"
 
@@ -62,11 +61,8 @@ func NewClientUnix(path string) *Client {
 }
 
 func (c *Client) AddJob() (*Job, error) {
-	type request struct {
-	}
-
 	var b bytes.Buffer
-	err := json.NewEncoder(&b).Encode(request{})
+	err := json.NewEncoder(&b).Encode(addJobRequest{})
 	if err != nil {
 		panic(err)
 	}
@@ -77,9 +73,9 @@ func (c *Client) AddJob() (*Job, error) {
 	defer response.Body.Close()
 
 	if response.StatusCode != http.StatusCreated {
-		rawR, _ := ioutil.ReadAll(response.Body)
-		r := string(rawR)
-		return nil, fmt.Errorf("couldn't create job, got %d: %s", response.StatusCode, r)
+		var er errorResponse
+		_ = json.NewDecoder(response.Body).Decode(&er)
+		return nil, fmt.Errorf("couldn't create job, got %d: %s", response.StatusCode, er.Message)
 	}
 
 	var jr addJobResponse
diff --git a/internal/jobqueue/json.go b/internal/jobqueue/json.go
index f324479cd..c6a8f2df9 100644
--- a/internal/jobqueue/json.go
+++ b/internal/jobqueue/json.go
@@ -8,6 +8,13 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/target"
 )
 
+type errorResponse struct {
+	Message string `json:"message"`
+}
+
+type addJobRequest struct {
+}
+
 type addJobResponse struct {
 	ComposeID    uuid.UUID         `json:"compose_id"`
 	ImageBuildID int               `json:"image_build_id"`
@@ -19,3 +26,6 @@ type updateJobRequest struct {
 	Status common.ImageBuildState `json:"status"`
 	Result *common.ComposeResult  `json:"result"`
 }
+
+type updateJobResponse struct {
+}
diff --git a/internal/store/store.go b/internal/store/store.go
index a073eab59..784cc1ff9 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -57,12 +57,12 @@ type Job struct {
 }
 
 type SourceConfig struct {
-	Name     string `json:"name"`
-	Type     string `json:"type"`
-	URL      string `json:"url"`
-	CheckGPG bool   `json:"check_gpg"`
-	CheckSSL bool   `json:"check_ssl"`
-	System   bool   `json:"system"`
+	Name     string `json:"name" toml:"name"`
+	Type     string `json:"type" toml:"type"`
+	URL      string `json:"url" toml:"url"`
+	CheckGPG bool   `json:"check_gpg" toml:"check_gpg"`
+	CheckSSL bool   `json:"check_ssl" toml:"check_ssl"`
+	System   bool   `json:"system" toml:"system"`
 }
 
 type NotFoundError struct {
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index b4696d89e..93a5e831c 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -379,6 +379,15 @@ func (api *API) sourceNewHandler(writer http.ResponseWriter, request *http.Reque
 		err = errors_package.New("blueprint must be in json or toml format")
 	}
 
+	// Basic check of the source, should at least have a name and type
+	if err == nil {
+		if len(source.Name) == 0 {
+			err = errors_package.New("'name' field is missing from API v0 request")
+		} else if len(source.Type) == 0 {
+			err = errors_package.New("'type' field is missing from API v0 request")
+		}
+	}
+
 	if err != nil {
 		errors := responseError{
 			ID:  "ProjectsError",
diff --git a/internal/weldr/json.go b/internal/weldr/json.go
index be81cce4c..ed767e243 100644
--- a/internal/weldr/json.go
+++ b/internal/weldr/json.go
@@ -100,14 +100,14 @@ func (s *SourceInfoV0) SourceConfig(sourceName string) (ssc store.SourceConfig,
 
 // SourceConfigV0 holds the source repository information
 type SourceConfigV0 struct {
-	Name     string   `json:"name"`
-	Type     string   `json:"type"`
-	URL      string   `json:"url"`
-	CheckGPG bool     `json:"check_gpg"`
-	CheckSSL bool     `json:"check_ssl"`
-	System   bool     `json:"system"`
-	Proxy    string   `json:"proxy"`
-	GPGUrls  []string `json:"gpgkey_urls"`
+	Name     string   `json:"name" toml:"name"`
+	Type     string   `json:"type" toml:"type"`
+	URL      string   `json:"url" toml:"url"`
+	CheckGPG bool     `json:"check_gpg" toml:"check_gpg"`
+	CheckSSL bool     `json:"check_ssl" toml:"check_ssl"`
+	System   bool     `json:"system" toml:"system"`
+	Proxy    string   `json:"proxy" toml:"proxy"`
+	GPGUrls  []string `json:"gpgkey_urls" toml:"gpgkey_urls"`
 }
 
 // SourceConfig returns a SourceConfig struct populated with the supported variables
diff --git a/osbuild b/osbuild
index c8465ce06..953cf7bff 160000
--- a/osbuild
+++ b/osbuild
@@ -1,1 +1,1 @@
-Subproject commit c8465ce06fab84103dc1fa613a28f22d68554fb1
+Subproject commit 953cf7bff0924b1c19b7e73d6310b02b7047f62e
diff --git a/osbuild-composer.spec b/osbuild-composer.spec
index 3cf8c5574..51813bddd 100644
--- a/osbuild-composer.spec
+++ b/osbuild-composer.spec
@@ -43,7 +43,7 @@ BuildRequires:  golang(github.com/stretchr/testify/assert)
 
 Requires: osbuild-composer-worker
 Requires: systemd
-Requires: osbuild >= 7
+Requires: osbuild >= 11
 
 Provides: weldr
 


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 830e3c26e..ad33189ce 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -57,7 +57,7 @@ jobs:
       matrix:
         fedora_release: ["31"]
     container:
-      image: "docker.io/library/fedora:${{ matrix.fedora_release }}"
+      image: "quay.io/osbuild/osbuild-fedora${{ matrix.fedora_release }}:latest"
     steps:
       - name: "📥 Prepare container"
         run: |
@@ -96,9 +96,9 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        fedora_release: ["32", "rawhide"]
+        fedora_release: ["32", "33"]
     container:
-      image: "docker.io/library/fedora:${{ matrix.fedora_release }}"
+      image: "quay.io/osbuild/osbuild-fedora${{ matrix.fedora_release }}:latest"
     steps:
       - name: "📥 Prepare container"
         run: |
diff --git a/cmd/osbuild-dnf-json-tests/main_test.go b/cmd/osbuild-dnf-json-tests/main_test.go
index d51c3f97d..92ff036bd 100644
--- a/cmd/osbuild-dnf-json-tests/main_test.go
+++ b/cmd/osbuild-dnf-json-tests/main_test.go
@@ -6,18 +6,20 @@ package main
 
 import (
 	"fmt"
+	"io/ioutil"
+	"os"
+	"path"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/distro/fedora30"
 	"github.com/osbuild/osbuild-composer/internal/distro/fedora31"
 	"github.com/osbuild/osbuild-composer/internal/distro/fedora32"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/test"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	"io/ioutil"
-	"os"
-	"path"
-	"testing"
 )
 
 func TestFetchChecksum(t *testing.T) {
@@ -50,38 +52,35 @@ func TestCrossArchDepsolve(t *testing.T) {
 	rpm := rpmmd.NewRPMMD(dir)
 
 	// Load repositories from the definition we provide in the RPM package
-	repositories := "/usr/share/osbuild-composer"
+	repoDir := "/usr/share/osbuild-composer"
 
 	// NOTE: we can add RHEL, but don't make it hard requirement because it will fail outside of VPN
 	for _, distroStruct := range []distro.Distro{fedora30.New(), fedora31.New(), fedora32.New()} {
-		repoConfig, err := rpmmd.LoadRepositories([]string{repositories}, distroStruct.Name())
-		assert.Nilf(t, err, "Failed to LoadRepositories from %v for %v: %v", repositories, distroStruct.Name(), err)
-		if err != nil {
-			// There is no point in running the tests without having repositories, but we can still run tests
-			// for the remaining distros
-			continue
-		}
-		for _, archStr := range distroStruct.ListArches() {
-			arch, err := distroStruct.GetArch(archStr)
-			assert.Nilf(t, err, "Failed to GetArch from %v structure: %v", distroStruct.Name(), err)
-			if err != nil {
-				continue
-			}
-			for _, imgTypeStr := range arch.ListImageTypes() {
-				imgType, err := arch.GetImageType(imgTypeStr)
-				assert.Nilf(t, err, "Failed to GetImageType for %v on %v: %v", distroStruct.Name(), arch.Name(), err)
-				if err != nil {
-					continue
-				}
+		t.Run(distroStruct.Name(), func(t *testing.T) {
+			repos, err := rpmmd.LoadRepositories([]string{repoDir}, distroStruct.Name())
+			require.NoErrorf(t, err, "Failed to LoadRepositories %v", distroStruct.Name())
+
+			for _, archStr := range distroStruct.ListArches() {
+				t.Run(archStr, func(t *testing.T) {
+					arch, err := distroStruct.GetArch(archStr)
+					require.NoError(t, err)
+
+					for _, imgTypeStr := range arch.ListImageTypes() {
+						t.Run(imgTypeStr, func(t *testing.T) {
+							imgType, err := arch.GetImageType(imgTypeStr)
+							require.NoError(t, err)
 
-				buildPackages := imgType.BuildPackages()
-				_, _, err = rpm.Depsolve(buildPackages, []string{}, repoConfig[archStr], distroStruct.ModulePlatformID(), archStr)
-				assert.Nilf(t, err, "Failed to Depsolve build packages for %v %v %v image: %v", distroStruct.Name(), imgType.Name(), arch.Name(), err)
+							buildPackages := imgType.BuildPackages()
+							_, _, err = rpm.Depsolve(buildPackages, []string{}, repos[archStr], distroStruct.ModulePlatformID(), archStr)
+							assert.NoError(t, err)
 
-				basePackagesInclude, basePackagesExclude := imgType.BasePackages()
-				_, _, err = rpm.Depsolve(basePackagesInclude, basePackagesExclude, repoConfig[archStr], distroStruct.ModulePlatformID(), archStr)
-				assert.Nilf(t, err, "Failed to Depsolve base packages for %v %v %v image: %v", distroStruct.Name(), imgType.Name(), arch.Name(), err)
+							basePackagesInclude, basePackagesExclude := imgType.BasePackages()
+							_, _, err = rpm.Depsolve(basePackagesInclude, basePackagesExclude, repos[archStr], distroStruct.ModulePlatformID(), archStr)
+							assert.NoError(t, err)
+						})
+					}
+				})
 			}
-		}
+		})
 	}
 }
diff --git a/cmd/osbuild-image-tests/aws.go b/cmd/osbuild-image-tests/aws.go
new file mode 100644
index 000000000..b000bf70d
--- /dev/null
+++ b/cmd/osbuild-image-tests/aws.go
@@ -0,0 +1,281 @@
+// +build integration
+
+package main
+
+import (
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/credentials"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/osbuild/osbuild-composer/internal/upload/awsupload"
+)
+
+type awsCredentials struct {
+	AccessKeyId     string
+	SecretAccessKey string
+	Region          string
+	Bucket          string
+}
+
+// getAWSCredentialsFromEnv gets the credentials from environment variables
+// If none of the environment variables is set, it returns nil.
+// If some but not all environment variables are set, it returns an error.
+func getAWSCredentialsFromEnv() (*awsCredentials, error) {
+	accessKeyId, akExists := os.LookupEnv("AWS_ACCESS_KEY_ID")
+	secretAccessKey, sakExists := os.LookupEnv("AWS_SECRET_ACCESS_KEY")
+	region, regionExists := os.LookupEnv("AWS_REGION")
+	bucket, bucketExists := os.LookupEnv("AWS_BUCKET")
+
+	// Workaround Travis security feature. If non of the variables is set, just ignore the test
+	if !akExists && !sakExists && !bucketExists && !regionExists {
+		return nil, nil
+	}
+	// If only one/two of them are not set, then fail
+	if !akExists || !sakExists || !bucketExists || !regionExists {
+		return nil, errors.New("not all required env variables were set")
+	}
+
+	return &awsCredentials{
+		AccessKeyId:     accessKeyId,
+		SecretAccessKey: secretAccessKey,
+		Region:          region,
+		Bucket:          bucket,
+	}, nil
+}
+
+// encodeBase64 encodes string to base64-encoded string
+func encodeBase64(input string) string {
+	return base64.StdEncoding.EncodeToString([]byte(input))
+}
+
+// createUserData creates cloud-init's user-data that contains user redhat with
+// the specified public key
+func createUserData(publicKeyFile string) (string, error) {
+	publicKey, err := ioutil.ReadFile(publicKeyFile)
+	if err != nil {
+		return "", fmt.Errorf("cannot read the public key: %#v", err)
+	}
+
+	userData := fmt.Sprintf(`#cloud-config
+user: redhat
+ssh_authorized_keys:
+  - %s
+`, string(publicKey))
+
+	return userData, nil
+}
+
+// wrapErrorf returns error constructed using fmt.Errorf from format and any
+// other args. If innerError != nil, it's appended at the end of the new
+// error.
+func wrapErrorf(innerError error, format string, a ...interface{}) error {
+	if innerError != nil {
+		a = append(a, innerError)
+		return fmt.Errorf(format+"\n\ninner error: %#s", a...)
+	}
+
+	return fmt.Errorf(format, a...)
+}
+
+// uploadImageToAWS mimics the upload feature of osbuild-composer.
+// It takes an image and an image name and creates an ec2 instance from them.
+// The s3 key is never returned - the same thing is done in osbuild-composer,
+// the user has no way of getting the s3 key.
+func uploadImageToAWS(c *awsCredentials, imagePath string, imageName string) error {
+	uploader, err := awsupload.New(c.Region, c.AccessKeyId, c.SecretAccessKey)
+	if err != nil {
+		return fmt.Errorf("cannot create aws uploader: %#v", err)
+	}
+
+	_, err = uploader.Upload(imagePath, c.Bucket, imageName)
+	if err != nil {
+		return fmt.Errorf("cannot upload the image: %#v", err)
+	}
+	_, err = uploader.Register(imageName, c.Bucket, imageName)
+	if err != nil {
+		return fmt.Errorf("cannot register the image: %#v", err)
+	}
+
+	return nil
+}
+
+// newEC2 creates EC2 struct from given credentials
+func newEC2(c *awsCredentials) (*ec2.EC2, error) {
+	creds := credentials.NewStaticCredentials(c.AccessKeyId, c.SecretAccessKey, "")
+	sess, err := session.NewSession(&aws.Config{
+		Credentials: creds,
+		Region:      aws.String(c.Region),
+	})
+	if err != nil {
+		return nil, fmt.Errorf("cannot create aws session: %#v", err)
+	}
+
+	return ec2.New(sess), nil
+}
+
+type imageDescription struct {
+	Id         *string
+	SnapshotId *string
+	// this doesn't support multiple snapshots per one image,
+	// because this feature is not supported in composer
+}
+
+// describeEC2Image searches for EC2 image by its name and returns
+// its id and snapshot id
+func describeEC2Image(e *ec2.EC2, imageName string) (*imageDescription, error) {
+	imageDescriptions, err := e.DescribeImages(&ec2.DescribeImagesInput{
+		Filters: []*ec2.Filter{
+			{
+				Name: aws.String("name"),
+				Values: []*string{
+					aws.String(imageName),
+				},
+			},
+		},
+	})
+	if err != nil {
+		return nil, fmt.Errorf("cannot describe the image: %#v", err)
+	}
+	imageId := imageDescriptions.Images[0].ImageId
+	snapshotId := imageDescriptions.Images[0].BlockDeviceMappings[0].Ebs.SnapshotId
+
+	return &imageDescription{
+		Id:         imageId,
+		SnapshotId: snapshotId,
+	}, nil
+}
+
+// deleteEC2Image deletes the specified image and its associated snapshot
+func deleteEC2Image(e *ec2.EC2, imageDesc *imageDescription) error {
+	var retErr error
+
+	// firstly, deregister the image
+	_, err := e.DeregisterImage(&ec2.DeregisterImageInput{
+		ImageId: imageDesc.Id,
+	})
+
+	if err != nil {
+		retErr = wrapErrorf(retErr, "cannot deregister the image: %#v", err)
+	}
+
+	// now it's possible to delete the snapshot
+	_, err = e.DeleteSnapshot(&ec2.DeleteSnapshotInput{
+		SnapshotId: imageDesc.SnapshotId,
+	})
+
+	if err != nil {
+		retErr = wrapErrorf(retErr, "cannot delete the snapshot: %#v", err)
+	}
+
+	return retErr
+}
+
+// withBootedImageInEC2 runs the function f in the context of booted
+// image in AWS EC2
+func withBootedImageInEC2(e *ec2.EC2, imageDesc *imageDescription, publicKey string, f func(address string) error) (retErr error) {
+	// generate user data with given public key
+	userData, err := createUserData(publicKey)
+	if err != nil {
+		return err
+	}
+
+	// Security group must be now generated, because by default
+	// all traffic to EC2 instance is filtered.
+
+	securityGroupName, err := generateRandomString("osbuild-image-tests-security-group-")
+	if err != nil {
+		return fmt.Errorf("cannot generate a random name for the image: %#v", err)
+	}
+
+	// Firstly create a security group
+	securityGroup, err := e.CreateSecurityGroup(&ec2.CreateSecurityGroupInput{
+		GroupName:   aws.String(securityGroupName),
+		Description: aws.String("image-tests-security-group"),
+	})
+	if err != nil {
+		return fmt.Errorf("cannot create a new security group: %#v", err)
+	}
+
+	defer func() {
+		_, err = e.DeleteSecurityGroup(&ec2.DeleteSecurityGroupInput{
+			GroupId: securityGroup.GroupId,
+		})
+
+		if err != nil {
+			retErr = wrapErrorf(retErr, "cannot delete the security group: %#v", err)
+		}
+	}()
+
+	// Authorize incoming SSH connections.
+	_, err = e.AuthorizeSecurityGroupIngress(&ec2.AuthorizeSecurityGroupIngressInput{
+		CidrIp:     aws.String("0.0.0.0/0"),
+		GroupId:    securityGroup.GroupId,
+		FromPort:   aws.Int64(22),
+		ToPort:     aws.Int64(22),
+		IpProtocol: aws.String("tcp"),
+	})
+	if err != nil {
+		return fmt.Errorf("canot add a rule to the security group: %#v", err)
+	}
+
+	// Finally, run the instance from the given image and with the created security group
+	res, err := e.RunInstances(&ec2.RunInstancesInput{
+		MaxCount:         aws.Int64(1),
+		MinCount:         aws.Int64(1),
+		ImageId:          imageDesc.Id,
+		InstanceType:     aws.String("t3.micro"),
+		SecurityGroupIds: []*string{securityGroup.GroupId},
+		UserData:         aws.String(encodeBase64(userData)),
+	})
+	if err != nil {
+		return fmt.Errorf("cannot create a new instance: %#v", err)
+	}
+
+	describeInstanceInput := &ec2.DescribeInstancesInput{
+		InstanceIds: []*string{
+			res.Instances[0].InstanceId,
+		},
+	}
+
+	defer func() {
+		// We need to terminate the instance now and wait until the termination is done.
+		// Otherwise, it wouldn't be possible to delete the image.
+		_, err = e.TerminateInstances(&ec2.TerminateInstancesInput{
+			InstanceIds: []*string{
+				res.Instances[0].InstanceId,
+			},
+		})
+		if err != nil {
+			retErr = wrapErrorf(retErr, "cannot terminate the instance: %#v", err)
+			return
+		}
+
+		err = e.WaitUntilInstanceTerminated(describeInstanceInput)
+		if err != nil {
+			retErr = wrapErrorf(retErr, "waiting for the instance termination failed: %#v", err)
+		}
+	}()
+
+	// The instance has no IP address yet. It's assigned when the instance
+	// is in the state "EXISTS". However, in this state the instance is not
+	// much usable, therefore wait until "RUNNING" state, in which the instance
+	// actually can do something useful for us.
+	err = e.WaitUntilInstanceRunning(describeInstanceInput)
+	if err != nil {
+		return fmt.Errorf("waiting for the instance to be running failed: %#v", err)
+	}
+
+	// By describing the instance, we can get the ip address.
+	out, err := e.DescribeInstances(describeInstanceInput)
+	if err != nil {
+		return fmt.Errorf("cannot describe the instance: %#v", err)
+	}
+
+	return f(*out.Reservations[0].Instances[0].PublicIpAddress)
+}
diff --git a/cmd/osbuild-image-tests/constants-travis.go b/cmd/osbuild-image-tests/constants-travis.go
index 7028c80ff..f198143db 100644
--- a/cmd/osbuild-image-tests/constants-travis.go
+++ b/cmd/osbuild-image-tests/constants-travis.go
@@ -17,8 +17,16 @@ func getOsbuildCommand(store string) *exec.Cmd {
 	return cmd
 }
 
-var imageInfoPath = "tools/image-info"
-var privateKeyPath = "test/keyring/id_rsa"
-var testCasesDirectoryPath = "test/cases"
-var userDataPath = "test/cloud-init/user-data"
-var metaDataPath = "test/cloud-init/meta-data"
+var testPaths = struct {
+	imageInfo          string
+	privateKey         string
+	testCasesDirectory string
+	userData           string
+	metaData           string
+}{
+	imageInfo:          "tools/image-info",
+	privateKey:         "test/keyring/id_rsa",
+	testCasesDirectory: "test/cases",
+	userData:           "test/cloud-init/user-data",
+	metaData:           "test/cloud-init/meta-data",
+}
diff --git a/cmd/osbuild-image-tests/constants.go b/cmd/osbuild-image-tests/constants.go
index 4236e148c..2a6f17c34 100644
--- a/cmd/osbuild-image-tests/constants.go
+++ b/cmd/osbuild-image-tests/constants.go
@@ -13,8 +13,16 @@ func getOsbuildCommand(store string) *exec.Cmd {
 	)
 }
 
-var imageInfoPath = "/usr/libexec/osbuild-composer/image-info"
-var privateKeyPath = "/usr/share/tests/osbuild-composer/keyring/id_rsa"
-var testCasesDirectoryPath = "/usr/share/tests/osbuild-composer/cases"
-var userDataPath = "/usr/share/tests/osbuild-composer/cloud-init/user-data"
-var metaDataPath = "/usr/share/tests/osbuild-composer/cloud-init/meta-data"
+var testPaths = struct {
+	imageInfo          string
+	privateKey         string
+	testCasesDirectory string
+	userData           string
+	metaData           string
+}{
+	imageInfo:          "/usr/libexec/osbuild-composer/image-info",
+	privateKey:         "/usr/share/tests/osbuild-composer/keyring/id_rsa",
+	testCasesDirectory: "/usr/share/tests/osbuild-composer/cases",
+	userData:           "/usr/share/tests/osbuild-composer/cloud-init/user-data",
+	metaData:           "/usr/share/tests/osbuild-composer/cloud-init/meta-data",
+}
diff --git a/cmd/osbuild-image-tests/context-managers.go b/cmd/osbuild-image-tests/context-managers.go
index 7c66143e9..89974169f 100644
--- a/cmd/osbuild-image-tests/context-managers.go
+++ b/cmd/osbuild-image-tests/context-managers.go
@@ -48,6 +48,22 @@ func withTempFile(dir, pattern string, f func(file *os.File) error) error {
 	return f(tempFile)
 }
 
+func withTempDir(dir, pattern string, f func(dir string) error) error {
+	tempDir, err := ioutil.TempDir(dir, pattern)
+	if err != nil {
+		return fmt.Errorf("cannot create the temporary directory %#v", err)
+	}
+
+	defer func() {
+		err := os.RemoveAll(tempDir)
+		if err != nil {
+			log.Printf("cannot remove the temporary directory: %#v", err)
+		}
+	}()
+
+	return f(tempDir)
+}
+
 // writeCloudInitSO creates cloud-init iso from specified userData and
 // metaData and writes it to the writer
 func writeCloudInitISO(writer io.Writer, userData, metaData string) error {
@@ -78,8 +94,8 @@ func withBootedQemuImage(image string, ns netNS, f func() error) error {
 	return withTempFile("", "osbuild-image-tests-cloudinit", func(cloudInitFile *os.File) error {
 		err := writeCloudInitISO(
 			cloudInitFile,
-			userDataPath,
-			metaDataPath,
+			testPaths.userData,
+			testPaths.metaData,
 		)
 		if err != nil {
 			return err
@@ -173,30 +189,41 @@ func withBootedNspawnDirectory(dir, name string, ns netNS, f func() error) error
 // a path to the result to the function f. The result is deleted
 // immediately after the function returns.
 func withExtractedTarArchive(archive string, f func(dir string) error) error {
-	dir, err := ioutil.TempDir("", "tar-archive")
-	if err != nil {
-		return fmt.Errorf("cannot create a temporary dir: %#v", err)
-	}
+	return withTempDir("", "tar-archive", func(dir string) error {
+		cmd := exec.Command(
+			"tar",
+			"xf", archive,
+			"-C", dir,
+		)
+		cmd.Stderr = os.Stderr
+		cmd.Stdout = os.Stdout
 
-	defer func() {
-		err := os.RemoveAll(dir)
+		err := cmd.Run()
 		if err != nil {
-			log.Printf("cannot remove the temporary dir: %#v", err)
+			return fmt.Errorf("cannot untar the archive: %#v", err)
 		}
-	}()
 
-	cmd := exec.Command(
-		"tar",
-		"xf", archive,
-		"-C", dir,
-	)
-	cmd.Stderr = os.Stderr
-	cmd.Stdout = os.Stdout
+		return f(dir)
+	})
+}
 
-	err = cmd.Run()
-	if err != nil {
-		return fmt.Errorf("cannot untar the archive: %#v", err)
-	}
+// withSSHKeyPair runs the function f with a newly generated
+// ssh key-pair, they key-pair is deleted immediately after
+// the function f returns
+func withSSHKeyPair(f func(privateKey, publicKey string) error) error {
+	return withTempDir("", "keys", func(dir string) error {
+		privateKey := dir + "/id_rsa"
+		publicKey := dir + "/id_rsa.pub"
+		cmd := exec.Command("ssh-keygen",
+			"-N", "",
+			"-f", privateKey,
+		)
+
+		err := cmd.Run()
+		if err != nil {
+			return fmt.Errorf("ssh-keygen failed: %#v", err)
+		}
 
-	return f(dir)
+		return f(privateKey, publicKey)
+	})
 }
diff --git a/cmd/osbuild-image-tests/helpers.go b/cmd/osbuild-image-tests/helpers.go
index 75f6da2b9..d67954913 100644
--- a/cmd/osbuild-image-tests/helpers.go
+++ b/cmd/osbuild-image-tests/helpers.go
@@ -7,6 +7,8 @@ import (
 	"os"
 	"syscall"
 	"time"
+
+	"github.com/google/uuid"
 )
 
 // durationMin returns the smaller of two given durations
@@ -50,3 +52,14 @@ func killProcessCleanly(process *os.Process, timeout time.Duration) error {
 
 	return process.Kill()
 }
+
+// generateRandomString generates a new random string with specified prefix.
+// The random part is based on UUID.
+func generateRandomString(prefix string) (string, error) {
+	id, err := uuid.NewRandom()
+	if err != nil {
+		return "", err
+	}
+
+	return prefix + id.String(), nil
+}
diff --git a/cmd/osbuild-image-tests/main_test.go b/cmd/osbuild-image-tests/main_test.go
index c92c76602..6652fea41 100644
--- a/cmd/osbuild-image-tests/main_test.go
+++ b/cmd/osbuild-image-tests/main_test.go
@@ -50,7 +50,9 @@ func runOsbuild(manifest []byte, store string) (string, error) {
 
 	if err != nil {
 		if _, ok := err.(*exec.ExitError); ok {
-			return "", fmt.Errorf("running osbuild failed: %s", outBuffer.String())
+			var formattedOutput bytes.Buffer
+			_ = json.Indent(&formattedOutput, outBuffer.Bytes(), "", "  ")
+			return "", fmt.Errorf("running osbuild failed: %s", formattedOutput.String())
 		}
 		return "", fmt.Errorf("running osbuild failed from an unexpected reason: %#v", err)
 	}
@@ -94,22 +96,22 @@ func splitExtension(path string) (string, string) {
 func testImageInfo(t *testing.T, imagePath string, rawImageInfoExpected []byte) {
 	var imageInfoExpected interface{}
 	err := json.Unmarshal(rawImageInfoExpected, &imageInfoExpected)
-	require.Nilf(t, err, "cannot decode expected image info: %#v", err)
+	require.NoErrorf(t, err, "cannot decode expected image info: %#v", err)
 
-	cmd := exec.Command(imageInfoPath, imagePath)
+	cmd := exec.Command(testPaths.imageInfo, imagePath)
 	cmd.Stderr = os.Stderr
 	reader, writer := io.Pipe()
 	cmd.Stdout = writer
 
 	err = cmd.Start()
-	require.Nilf(t, err, "image-info cannot start: %#v", err)
+	require.NoErrorf(t, err, "image-info cannot start: %#v", err)
 
 	var imageInfoGot interface{}
 	err = json.NewDecoder(reader).Decode(&imageInfoGot)
-	require.Nilf(t, err, "decoding image-info output failed: %#v", err)
+	require.NoErrorf(t, err, "decoding image-info output failed: %#v", err)
 
 	err = cmd.Wait()
-	require.Nilf(t, err, "running image-info failed: %#v", err)
+	require.NoErrorf(t, err, "running image-info failed: %#v", err)
 
 	assert.Equal(t, imageInfoExpected, imageInfoGot)
 }
@@ -123,19 +125,28 @@ func (*timeoutError) Error() string { return "" }
 // that 10 seconds or if systemd-is-running returns starting.
 // It returns nil if systemd-is-running returns running or degraded.
 // It can also return other errors in other error cases.
-func trySSHOnce(ns netNS) error {
+func trySSHOnce(address string, privateKey string, ns *netNS) error {
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 	defer cancel()
-	cmd := ns.NamespacedCommandContext(
-		ctx,
-		"ssh",
+
+	cmdName := "ssh"
+	cmdArgs := []string{
 		"-p", "22",
-		"-i", privateKeyPath,
+		"-i", privateKey,
 		"-o", "StrictHostKeyChecking=no",
 		"-o", "UserKnownHostsFile=/dev/null",
-		"redhat@localhost",
+		"redhat@" + address,
 		"systemctl --wait is-system-running",
-	)
+	}
+
+	var cmd *exec.Cmd
+
+	if ns != nil {
+		cmd = ns.NamespacedCommandContext(ctx, cmdName, cmdArgs...)
+	} else {
+		cmd = exec.CommandContext(ctx, cmdName, cmdArgs...)
+	}
+
 	output, err := cmd.Output()
 
 	if ctx.Err() == context.DeadlineExceeded {
@@ -169,10 +180,10 @@ func trySSHOnce(ns netNS) error {
 // testSSH tests the running image using ssh.
 // It tries 20 attempts before giving up. If a major error occurs, it might
 // return earlier.
-func testSSH(t *testing.T, ns netNS) {
+func testSSH(t *testing.T, address string, privateKey string, ns *netNS) {
 	const attempts = 20
 	for i := 0; i < attempts; i++ {
-		err := trySSHOnce(ns)
+		err := trySSHOnce(address, privateKey, ns)
 		if err == nil {
 			// pass the test
 			return
@@ -189,39 +200,103 @@ func testSSH(t *testing.T, ns netNS) {
 	t.Errorf("ssh test failure, %d attempts were made", attempts)
 }
 
+func testBootUsingQemu(t *testing.T, imagePath string) {
+	err := withNetworkNamespace(func(ns netNS) error {
+		return withBootedQemuImage(imagePath, ns, func() error {
+			testSSH(t, "localhost", testPaths.privateKey, &ns)
+			return nil
+		})
+	})
+	require.NoError(t, err)
+}
+
+func testBootUsingNspawnImage(t *testing.T, imagePath string, outputID string) {
+	err := withNetworkNamespace(func(ns netNS) error {
+		return withBootedNspawnImage(imagePath, outputID, ns, func() error {
+			testSSH(t, "localhost", testPaths.privateKey, &ns)
+			return nil
+		})
+	})
+	require.NoError(t, err)
+}
+
+func testBootUsingNspawnDirectory(t *testing.T, imagePath string, outputID string) {
+	err := withNetworkNamespace(func(ns netNS) error {
+		return withExtractedTarArchive(imagePath, func(dir string) error {
+			return withBootedNspawnDirectory(dir, outputID, ns, func() error {
+				testSSH(t, "localhost", testPaths.privateKey, &ns)
+				return nil
+			})
+		})
+	})
+	require.NoError(t, err)
+}
+
+func testBootUsingAWS(t *testing.T, imagePath string) {
+	creds, err := getAWSCredentialsFromEnv()
+	require.NoError(t, err)
+
+	// if no credentials are given, fall back to qemu
+	if creds == nil {
+		log.Print("no AWS credentials given, falling back to booting using qemu")
+		testBootUsingQemu(t, imagePath)
+		return
+
+	}
+
+	imageName, err := generateRandomString("osbuild-image-tests-image-")
+	require.NoError(t, err)
+
+	e, err := newEC2(creds)
+	require.NoError(t, err)
+
+	// the following line should be done by osbuild-composer at some point
+	err = uploadImageToAWS(creds, imagePath, imageName)
+	require.NoErrorf(t, err, "upload to amazon failed, resources could have been leaked")
+
+	imageDesc, err := describeEC2Image(e, imageName)
+	require.NoErrorf(t, err, "cannot describe the ec2 image")
+
+	// delete the image after the test is over
+	defer func() {
+		err = deleteEC2Image(e, imageDesc)
+		require.NoErrorf(t, err, "cannot delete the ec2 image, resources could have been leaked")
+	}()
+
+	// boot the uploaded image and try to connect to it
+	err = withSSHKeyPair(func(privateKey, publicKey string) error {
+		return withBootedImageInEC2(e, imageDesc, publicKey, func(address string) error {
+			testSSH(t, address, privateKey, nil)
+			return nil
+		})
+	})
+	require.NoError(t, err)
+}
+
 // testBoot tests if the image is able to successfully boot
 // Before the test it boots the image respecting the specified bootType.
 // The test passes if the function is able to connect to the image via ssh
 // in defined number of attempts and systemd-is-running returns running
 // or degraded status.
 func testBoot(t *testing.T, imagePath string, bootType string, outputID string) {
-	err := withNetworkNamespace(func(ns netNS) error {
-		switch bootType {
-		case "qemu":
-			fallthrough
-		case "qemu-extract":
-			return withBootedQemuImage(imagePath, ns, func() error {
-				testSSH(t, ns)
-				return nil
-			})
-		case "nspawn":
-			return withBootedNspawnImage(imagePath, outputID, ns, func() error {
-				testSSH(t, ns)
-				return nil
-			})
-		case "nspawn-extract":
-			return withExtractedTarArchive(imagePath, func(dir string) error {
-				return withBootedNspawnDirectory(dir, outputID, ns, func() error {
-					testSSH(t, ns)
-					return nil
-				})
-			})
-		default:
-			panic("unknown boot type!")
-		}
-	})
+	switch bootType {
+	case "qemu":
+		fallthrough
+	case "qemu-extract":
+		testBootUsingQemu(t, imagePath)
+
+	case "nspawn":
+		testBootUsingNspawnImage(t, imagePath, outputID)
+
+	case "nspawn-extract":
+		testBootUsingNspawnDirectory(t, imagePath, outputID)
 
-	require.Nil(t, err)
+	case "aws":
+		testBootUsingAWS(t, imagePath)
+
+	default:
+		panic("unknown boot type!")
+	}
 }
 
 // testImage performs a series of tests specified in the testcase
@@ -244,7 +319,7 @@ func testImage(t *testing.T, testcase testcaseStruct, imagePath, outputID string
 // tests the result
 func runTestcase(t *testing.T, testcase testcaseStruct) {
 	store, err := ioutil.TempDir("/var/tmp", "osbuild-image-tests-")
-	require.Nilf(t, err, "cannot create temporary store: %#v", err)
+	require.NoErrorf(t, err, "cannot create temporary store: %#v", err)
 
 	defer func() {
 		err := os.RemoveAll(store)
@@ -254,7 +329,7 @@ func runTestcase(t *testing.T, testcase testcaseStruct) {
 	}()
 
 	outputID, err := runOsbuild(testcase.Manifest, store)
-	require.Nil(t, err)
+	require.NoError(t, err)
 
 	imagePath := fmt.Sprintf("%s/refs/%s/%s", store, outputID, testcase.ComposeRequest.Filename)
 
@@ -264,7 +339,7 @@ func runTestcase(t *testing.T, testcase testcaseStruct) {
 		_, ex = splitExtension(base)
 		if ex != ".tar" {
 			err := extractXZ(imagePath)
-			require.Nil(t, err)
+			require.NoError(t, err)
 			imagePath = base
 		}
 	}
@@ -274,7 +349,7 @@ func runTestcase(t *testing.T, testcase testcaseStruct) {
 
 // getAllCases returns paths to all testcases in the testcase directory
 func getAllCases() ([]string, error) {
-	cases, err := ioutil.ReadDir(testCasesDirectoryPath)
+	cases, err := ioutil.ReadDir(testPaths.testCasesDirectory)
 	if err != nil {
 		return nil, fmt.Errorf("cannot list test cases: %#v", err)
 	}
@@ -285,7 +360,7 @@ func getAllCases() ([]string, error) {
 			continue
 		}
 
-		casePath := fmt.Sprintf("%s/%s", testCasesDirectoryPath, c.Name())
+		casePath := fmt.Sprintf("%s/%s", testPaths.testCasesDirectory, c.Name())
 		casesPaths = append(casesPaths, casePath)
 	}
 
@@ -297,11 +372,11 @@ func runTests(t *testing.T, cases []string) {
 	for _, path := range cases {
 		t.Run(path, func(t *testing.T) {
 			f, err := os.Open(path)
-			require.Nilf(t, err, "%s: cannot open test case: %#v", path, err)
+			require.NoErrorf(t, err, "%s: cannot open test case: %#v", path, err)
 
 			var testcase testcaseStruct
 			err = json.NewDecoder(f).Decode(&testcase)
-			require.Nilf(t, err, "%s: cannot decode test case: %#v", path, err)
+			require.NoErrorf(t, err, "%s: cannot decode test case: %#v", path, err)
 
 			currentArch := common.CurrentArch()
 			if testcase.ComposeRequest.Arch != currentArch {
@@ -320,7 +395,7 @@ func TestImages(t *testing.T) {
 	if len(cases) == 0 {
 		var err error
 		cases, err = getAllCases()
-		require.Nil(t, err)
+		require.NoError(t, err)
 	}
 
 	runTests(t, cases)
diff --git a/internal/client/source_test.go b/internal/client/source_test.go
index e8a5eedcd..a38d78eaa 100644
--- a/internal/client/source_test.go
+++ b/internal/client/source_test.go
@@ -109,6 +109,25 @@ func TestPOSTInvalidTOMLSourceV0(t *testing.T) {
 	require.False(t, resp.Status, "did not return an error")
 }
 
+// POST a wrong TOML source
+func TestPOSTWrongTOMLSourceV0(t *testing.T) {
+	// Should not have a [] section
+	source := `
+		[package-repo-toml-v0]
+		name = "package-repo-toml-v0"
+		url = "file://REPO-PATH
+		type = "yum-baseurl"
+		proxy = "https://proxy-url/"
+		check_ssl = true
+		check_gpg = true
+		gpgkey_urls = ["https://url/path/to/gpg-key"]
+	`
+
+	resp, err := PostTOMLSourceV0(testState.socket, source)
+	require.NoError(t, err, "POST source failed with a client error")
+	require.False(t, resp.Status, "did not return an error")
+}
+
 // list sources
 func TestListSourcesV0(t *testing.T) {
 	sources := []string{`{
@@ -183,3 +202,102 @@ func TestGetSourceInfoV0(t *testing.T) {
 	require.NoError(t, err, "DELETE source failed with a client error")
 	require.True(t, resp.Status, "DELETE source failed: %#v", resp)
 }
+
+func UploadUserDefinedSources(t *testing.T, sources []string) {
+	for i := range sources {
+		source := strings.Replace(sources[i], "REPO-PATH", testState.repoDir, 1)
+		resp, err := PostJSONSourceV0(testState.socket, source)
+		require.NoError(t, err, "POST source failed with a client error")
+		require.True(t, resp.Status, "POST source failed: %#v", resp)
+	}
+}
+
+// verify user defined sources are not present
+func VerifyNoUserDefinedSources(t *testing.T, source_names []string) {
+	list, api, err := ListSourcesV0(testState.socket)
+	require.NoError(t, err, "GET source failed with a client error")
+	require.Nil(t, api, "ListSources failed: %#v", api)
+	require.GreaterOrEqual(t, len(list), 1, "Not enough sources returned")
+	for i := range source_names {
+		require.NotContains(t, list, source_names[i])
+	}
+}
+
+func TestDeleteUserDefinedSourcesV0(t *testing.T) {
+	source_names := []string{"package-repo-1", "package-repo-2"}
+	sources := []string{`{
+		"name": "package-repo-1",
+		"url": "file://REPO-PATH",
+		"type": "yum-baseurl",
+		"proxy": "https://proxy-url/",
+		"check_ssl": true,
+		"check_gpg": true,
+		"gpgkey_urls": ["https://url/path/to/gpg-key"]
+	}`,
+		`{
+		"name": "package-repo-2",
+		"url": "file://REPO-PATH",
+		"type": "yum-baseurl",
+		"proxy": "https://proxy-url/",
+		"check_ssl": true,
+		"check_gpg": true,
+		"gpgkey_urls": ["https://url/path/to/gpg-key"]
+	}`}
+
+	// verify test starts without user defined sources
+	VerifyNoUserDefinedSources(t, source_names)
+
+	// post user defined sources
+	UploadUserDefinedSources(t, sources)
+	// note: not verifying user defined sources have been pushed b/c correct
+	// operation of PostJSONSourceV0 is validated in the test functions above
+
+	// Remove the test sources
+	for _, n := range source_names {
+		resp, err := DeleteSourceV0(testState.socket, n)
+		require.NoError(t, err, "DELETE source failed with a client error")
+		require.True(t, resp.Status, "DELETE source failed: %#v", resp)
+	}
+
+	// verify removed sources are not present after removal
+	VerifyNoUserDefinedSources(t, source_names)
+}
+
+func Index(vs []string, t string) int {
+	for i, v := range vs {
+		if v == t {
+			return i
+		}
+	}
+	return -1
+}
+
+func Include(vs []string, t string) bool {
+	return Index(vs, t) >= 0
+}
+
+func TestDeleteSystemSourcesV0(t *testing.T) {
+	sources_list, api, err := ListSourcesV0(testState.socket)
+	require.NoError(t, err, "GET source failed with a client error")
+	require.Nil(t, api, "ListSources failed: %#v", api)
+
+	for _, repo_name := range []string{"test-system-repo", "fedora", "baseos"} {
+		// skip repository names which are not present b/c this test can be
+		// executed both as a unit test and as an integration test
+		if !Include(sources_list, repo_name) {
+			continue
+		}
+
+		// try removing system source
+		resp, err := DeleteSourceV0(testState.socket, repo_name)
+		require.NoError(t, err, "DELETE source failed with a client error")
+		require.True(t, resp.Status, "DELETE source failed: %#v", resp)
+
+		// verify that system sources are still there
+		list, api, err := ListSourcesV0(testState.socket)
+		require.NoError(t, err, "GET source failed with a client error")
+		require.Nil(t, api, "ListSources failed: %#v", api)
+		require.GreaterOrEqual(t, len(list), 1, "Not enough sources returned")
+		require.Contains(t, list, repo_name)
+	}
+}
diff --git a/internal/client/unit_test.go b/internal/client/unit_test.go
index 00e33d87d..d1cde6bf6 100644
--- a/internal/client/unit_test.go
+++ b/internal/client/unit_test.go
@@ -45,7 +45,7 @@ func TestMain(m *testing.M) {
 	if err != nil {
 		panic(err)
 	}
-	repos := []rpmmd.RepoConfig{{Id: "test-id", BaseURL: "http://example.com/test/os/test_arch"}}
+	repos := []rpmmd.RepoConfig{{Id: "test-system-repo", BaseURL: "http://example.com/test/os/test_arch"}}
 	logger := log.New(os.Stdout, "", 0)
 	api := weldr.New(rpm, arch, distro, repos, logger, fixture.Store)
 	server := http.Server{Handler: api}
diff --git a/internal/jobqueue/api_test.go b/internal/jobqueue/api_test.go
index c1a28d413..37d25d818 100644
--- a/internal/jobqueue/api_test.go
+++ b/internal/jobqueue/api_test.go
@@ -13,28 +13,30 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/test"
 )
 
-func TestBasic(t *testing.T) {
+func TestErrors(t *testing.T) {
 	var cases = []struct {
-		Method           string
-		Path             string
-		Body             string
-		ExpectedStatus   int
-		ExpectedResponse string
+		Method         string
+		Path           string
+		Body           string
+		ExpectedStatus int
 	}{
+		// Bogus path
+		{"GET", "/foo", ``, http.StatusNotFound},
 		// Create job with invalid body
-		{"POST", "/job-queue/v1/jobs", ``, http.StatusBadRequest, `invalid request: EOF`},
+		{"POST", "/job-queue/v1/jobs", ``, http.StatusBadRequest},
+		// Wrong method
+		{"GET", "/job-queue/v1/jobs", ``, http.StatusMethodNotAllowed},
 		// Update job with invalid ID
-		{"PATCH", "/job-queue/v1/jobs/foo/builds/0", `{"status":"RUNNING"}`, http.StatusBadRequest, `invalid compose id: invalid UUID length: 3`},
+		{"PATCH", "/job-queue/v1/jobs/foo/builds/0", `{"status":"RUNNING"}`, http.StatusBadRequest},
 		// Update job that does not exist, with invalid body
-		{"PATCH", "/job-queue/v1/jobs/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/builds/0", ``, http.StatusBadRequest, `invalid status: EOF`},
+		{"PATCH", "/job-queue/v1/jobs/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/builds/0", ``, http.StatusBadRequest},
 		// Update job that does not exist
-		{"PATCH", "/job-queue/v1/jobs/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/builds/0", `{"status":"RUNNING"}`, http.StatusNotFound, `compose does not exist`},
+		{"PATCH", "/job-queue/v1/jobs/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/builds/0", `{"status":"RUNNING"}`, http.StatusNotFound},
 	}
 
 	for _, c := range cases {
 		api := jobqueue.New(nil, store.New(nil))
-
-		test.TestNonJsonRoute(t, api, false, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedResponse)
+		test.TestRoute(t, api, false, c.Method, c.Path, c.Body, c.ExpectedStatus, "{}", "message")
 	}
 }
 
@@ -60,7 +62,7 @@ func TestCreate(t *testing.T) {
 		`{"compose_id":"`+id.String()+`","image_build_id":0,"manifest":{"sources":{},"pipeline":{}},"targets":[]}`, "created")
 }
 
-func testUpdateTransition(t *testing.T, from, to string, expectedStatus int, expectedResponse string) {
+func testUpdateTransition(t *testing.T, from, to string, expectedStatus int) {
 	distroStruct := fedoratest.New()
 	arch, err := distroStruct.GetArch("x86_64")
 	if err != nil {
@@ -88,39 +90,38 @@ func testUpdateTransition(t *testing.T, from, to string, expectedStatus int, exp
 		}
 	}
 
-	test.TestNonJsonRoute(t, api, false, "PATCH", "/job-queue/v1/jobs/"+id.String()+"/builds/0", `{"status":"`+to+`"}`, expectedStatus, expectedResponse)
+	test.TestRoute(t, api, false, "PATCH", "/job-queue/v1/jobs/"+id.String()+"/builds/0", `{"status":"`+to+`"}`, expectedStatus, "{}", "message")
 }
 
 func TestUpdate(t *testing.T) {
 	var cases = []struct {
-		From             string
-		To               string
-		ExpectedStatus   int
-		ExpectedResponse string
+		From           string
+		To             string
+		ExpectedStatus int
 	}{
-		{"VOID", "WAITING", http.StatusNotFound, "compose does not exist"},
-		{"VOID", "RUNNING", http.StatusNotFound, "compose does not exist"},
-		{"VOID", "FINISHED", http.StatusNotFound, "compose does not exist"},
-		{"VOID", "FAILED", http.StatusNotFound, "compose does not exist"},
-		{"WAITING", "WAITING", http.StatusNotFound, "compose has not been popped"},
-		{"WAITING", "RUNNING", http.StatusNotFound, "compose has not been popped"},
-		{"WAITING", "FINISHED", http.StatusNotFound, "compose has not been popped"},
-		{"WAITING", "FAILED", http.StatusNotFound, "compose has not been popped"},
-		{"RUNNING", "WAITING", http.StatusBadRequest, "invalid state transition: image build cannot be moved into waiting state"},
-		{"RUNNING", "RUNNING", http.StatusOK, ""},
-		{"RUNNING", "FINISHED", http.StatusOK, ""},
-		{"RUNNING", "FAILED", http.StatusOK, ""},
-		{"FINISHED", "WAITING", http.StatusBadRequest, "invalid state transition: image build cannot be moved into waiting state"},
-		{"FINISHED", "RUNNING", http.StatusBadRequest, "invalid state transition: only waiting image build can be transitioned into running state"},
-		{"FINISHED", "FINISHED", http.StatusBadRequest, "invalid state transition: only running image build can be transitioned into finished or failed state"},
-		{"FINISHED", "FAILED", http.StatusBadRequest, "invalid state transition: only running image build can be transitioned into finished or failed state"},
-		{"FAILED", "WAITING", http.StatusBadRequest, "invalid state transition: image build cannot be moved into waiting state"},
-		{"FAILED", "RUNNING", http.StatusBadRequest, "invalid state transition: only waiting image build can be transitioned into running state"},
-		{"FAILED", "FINISHED", http.StatusBadRequest, "invalid state transition: only running image build can be transitioned into finished or failed state"},
-		{"FAILED", "FAILED", http.StatusBadRequest, "invalid state transition: only running image build can be transitioned into finished or failed state"},
+		{"VOID", "WAITING", http.StatusNotFound},
+		{"VOID", "RUNNING", http.StatusNotFound},
+		{"VOID", "FINISHED", http.StatusNotFound},
+		{"VOID", "FAILED", http.StatusNotFound},
+		{"WAITING", "WAITING", http.StatusNotFound},
+		{"WAITING", "RUNNING", http.StatusNotFound},
+		{"WAITING", "FINISHED", http.StatusNotFound},
+		{"WAITING", "FAILED", http.StatusNotFound},
+		{"RUNNING", "WAITING", http.StatusBadRequest},
+		{"RUNNING", "RUNNING", http.StatusOK},
+		{"RUNNING", "FINISHED", http.StatusOK},
+		{"RUNNING", "FAILED", http.StatusOK},
+		{"FINISHED", "WAITING", http.StatusBadRequest},
+		{"FINISHED", "RUNNING", http.StatusBadRequest},
+		{"FINISHED", "FINISHED", http.StatusBadRequest},
+		{"FINISHED", "FAILED", http.StatusBadRequest},
+		{"FAILED", "WAITING", http.StatusBadRequest},
+		{"FAILED", "RUNNING", http.StatusBadRequest},
+		{"FAILED", "FINISHED", http.StatusBadRequest},
+		{"FAILED", "FAILED", http.StatusBadRequest},
 	}
 
 	for _, c := range cases {
-		testUpdateTransition(t, c.From, c.To, c.ExpectedStatus, c.ExpectedResponse)
+		testUpdateTransition(t, c.From, c.To, c.ExpectedStatus)
 	}
 }
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index c5c29efbc..2f7f680c4 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -878,6 +878,31 @@ check_gpg = false
 	test.SendHTTP(api, true, "DELETE", "/api/v0/projects/source/delete/fish", ``)
 }
 
+// Empty TOML, and invalid TOML should return an error
+func TestSourcesNewWrongToml(t *testing.T) {
+	sources := []string{``, `
+[fish]
+name = "fish"
+url = "https://download.opensuse.org/repositories/shells:/fish:/release:/3/Fedora_29/"
+type = "yum-baseurl"
+check_ssl = false
+check_gpg = false
+`}
+	for _, source := range sources {
+		req := httptest.NewRequest("POST", "/api/v0/projects/source/new", bytes.NewReader([]byte(source)))
+		req.Header.Set("Content-Type", "text/x-toml")
+		recorder := httptest.NewRecorder()
+
+		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
+		api.ServeHTTP(recorder, req)
+
+		r := recorder.Result()
+		if r.StatusCode != http.StatusBadRequest {
+			t.Errorf("unexpected status %v", r.StatusCode)
+		}
+	}
+}
+
 func TestSourcesInfo(t *testing.T) {
 	sourceStr := `{"name":"fish","type":"yum-baseurl","url":"https://download.opensuse.org/repositories/shells:/fish:/release:/3/Fedora_29/","check_gpg":false,"check_ssl":false,"system":false}`
 
diff --git a/jenkins/run_tests.sh b/jenkins/run_tests.sh
index 6ca3a047d..35f1d1003 100644
--- a/jenkins/run_tests.sh
+++ b/jenkins/run_tests.sh
@@ -21,8 +21,8 @@ pushd ansible-osbuild
 popd
 
 # Run the integration tests.
-# /usr/libexec/tests/osbuild-composer/osbuild-dnf-json-tests
+/usr/libexec/tests/osbuild-composer/osbuild-dnf-json-tests -test.v
 # /usr/libexec/tests/osbuild-composer/osbuild-image-tests
 /usr/libexec/tests/osbuild-composer/osbuild-rcm-tests -test.v
-# /usr/libexec/tests/osbuild-composer/osbuild-tests -test.v
-# /usr/libexec/tests/osbuild-composer/osbuild-weldr-tests -test.v
+/usr/libexec/tests/osbuild-composer/osbuild-tests -test.v
+/usr/libexec/tests/osbuild-composer/osbuild-weldr-tests -test.v
diff --git a/test/cases/f30-x86_64-ami-boot.json b/test/cases/f30-x86_64-ami-boot.json
index ba125b2b6..19da682b2 100644
--- a/test/cases/f30-x86_64-ami-boot.json
+++ b/test/cases/f30-x86_64-ami-boot.json
@@ -1,6 +1,6 @@
 {
   "boot": {
-    "type": "qemu-extract"
+    "type": "aws"
   },
   "compose-request": {
     "distro": "fedora-30",
diff --git a/test/cases/f31-x86_64-ami-boot.json b/test/cases/f31-x86_64-ami-boot.json
index 18e0551b9..c26203d51 100644
--- a/test/cases/f31-x86_64-ami-boot.json
+++ b/test/cases/f31-x86_64-ami-boot.json
@@ -1,6 +1,6 @@
 {
   "boot": {
-    "type": "qemu-extract"
+    "type": "aws"
   },
   "compose-request": {
     "distro": "fedora-31",
diff --git a/tools/test-case-generators/format-request-map.json b/tools/test-case-generators/format-request-map.json
index a62993d88..712f0ed89 100644
--- a/tools/test-case-generators/format-request-map.json
+++ b/tools/test-case-generators/format-request-map.json
@@ -1,7 +1,7 @@
 {
   "ami": {
     "boot": {
-      "type": "qemu-extract"
+      "type": "aws"
     },
     "compose-request": {
       "distro": "",
diff --git a/tools/test-case-generators/generate-test-case b/tools/test-case-generators/generate-test-case
index 7ce4e7705..d1ae3713c 100755
--- a/tools/test-case-generators/generate-test-case
+++ b/tools/test-case-generators/generate-test-case
@@ -26,9 +26,18 @@ It then outputs a json test case as standard output.
 '''
 
 
+def get_subprocess_stdout(*args, **kwargs):
+    sp = subprocess.run(*args, **kwargs, stdout=subprocess.PIPE)
+    if sp.returncode != 0:
+        sys.stderr.write(sp.stdout)
+        sys.exit(1)
+    
+    return sp.stdout
+
+
 def run_osbuild(manifest, store):
     osbuild_cmd = ["osbuild", "--store", store, "--json", "-"]
-    result = json.loads(subprocess.check_output(osbuild_cmd, encoding="utf-8", input=json.dumps(manifest)))
+    result = json.loads(get_subprocess_stdout(osbuild_cmd, encoding="utf-8", input=json.dumps(manifest)))
     return result.get("output_id")
 
 
@@ -37,10 +46,10 @@ def main(test_case, store):
     compose_request = json.dumps(test_case["compose-request"])
 
     pipeline_command = ["go", "run", "./cmd/osbuild-pipeline", "-"]
-    test_case["manifest"] = json.loads(subprocess.check_output(pipeline_command, input=compose_request, encoding="utf-8"))
+    test_case["manifest"] = json.loads(get_subprocess_stdout(pipeline_command, input=compose_request, encoding="utf-8"))
 
     pipeline_command = ["go", "run", "./cmd/osbuild-pipeline", "-rpmmd", "-"]
-    test_case["rpmmd"] = json.loads(subprocess.check_output(pipeline_command, input=compose_request, encoding="utf-8"))
+    test_case["rpmmd"] = json.loads(get_subprocess_stdout(pipeline_command, input=compose_request, encoding="utf-8"))
 
     if boot_type != "nspawn-extract":
         output_id = run_osbuild(test_case["manifest"], store)
@@ -53,7 +62,7 @@ def main(test_case, store):
                 with open(fn, "w") as f:
                     subprocess.run(["xz", "--decompress", "--stdout", image_file], stdout=f)
                 image_file = fn
-        test_case["image-info"] = json.loads(subprocess.check_output(["tools/image-info", image_file]))
+        test_case["image-info"] = json.loads(get_subprocess_stdout(["tools/image-info", image_file], encoding="utf-8"))
 
     return test_case
 
diff --git a/tools/test-case-generators/generate-test-cases b/tools/test-case-generators/generate-test-cases
index 022ed3839..3be0b528d 100755
--- a/tools/test-case-generators/generate-test-cases
+++ b/tools/test-case-generators/generate-test-cases
@@ -6,6 +6,14 @@ import json
 import os
 import sys
 
+def get_subprocess_stdout(*args, **kwargs):
+    sp = subprocess.run(*args, **kwargs, stdout=subprocess.PIPE)
+    if sp.returncode != 0:
+        sys.stderr.write(sp.stdout)
+        sys.exit(1)
+    
+    return sp.stdout
+
 def main(distro, arch, store, output):
     with open("tools/test-case-generators/format-request-map.json") as format_request_json:
         format_request_dict = json.load(format_request_json)
@@ -15,7 +23,7 @@ def main(distro, arch, store, output):
         test_case_request["compose-request"]["distro"] = distro
         test_case_request["compose-request"]["arch"] = arch
         test_case_request["compose-request"]["repositories"] = repos_dict[distro][arch]
-        test_case = json.loads(subprocess.check_output(["tools/test-case-generators/generate-test-case", store], input=json.dumps(test_case_request), encoding="utf-8"))
+        test_case = json.loads(get_subprocess_stdout(["tools/test-case-generators/generate-test-case", store], input=json.dumps(test_case_request), encoding="utf-8"))
         name = distro.replace("-", "_") + "-" + arch + "-" + output_format.replace("-", "_") + "-boot.json"
         file_name = output + "/" + name
         with open(file_name, 'w') as case_file: