debian-forge-composer/internal/rcm/api_test.go
Tom Gundersen fa015a70f6 rcm: rework the API for submitting composes
The API allowed composes to have multiple architectures, image
types and repositories. Turns out that's not exactly what we want
it is not clear how to combine the lits of each. Each architecture
might not combine with each image type, and it is not clear which
repositories are need for each image build. Lastly, while allowing
different image builds to have different distros in the same
compose does not appear immediately useful, there is no particular
reason to disallow that. This patch reworks the way composes are
specified.

The intention remains the same, to be able to submit several image
builds as one compose. But rather than taking arrays of image types
and architectures, take one array of image builds instead, each of
which consists of one distro, one architecture, one image build and
an array of repositories.

In a follow-up patch they will also each contain an array of upload
targets.

This means that each image build will have the same sort of structure
as a compose request in the weldr API. The reason we want to submit
an array of them rather than have them as individual composes, is that
in a follow-up patch we will introduce the concept of a "finalizer",
or "call-back" or something to that effect, which will be triggered
when all the images have been built successfully. The use-case is,
as always, koji, which requires this.

Signed-off-by: Tom Gundersen <teg@jklm.no>
2020-03-29 16:08:04 +02:00

281 lines
7.2 KiB
Go

package rcm_test
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"regexp"
"testing"
"github.com/google/uuid"
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"
)
type API interface {
ServeHTTP(writer http.ResponseWriter, request *http.Request)
}
func internalRequest(api API, method, path, body, contentType string) *http.Response {
req := httptest.NewRequest(method, path, bytes.NewReader([]byte(body)))
req.Header.Set("Content-Type", contentType)
resp := httptest.NewRecorder()
api.ServeHTTP(resp, req)
return resp.Result()
}
func TestBasicRcmAPI(t *testing.T) {
// Test the HTTP API responses
// This test mainly focuses on HTTP status codes and JSON structures, not necessarily on their content
var cases = []struct {
Method string
Path string
Body string
ContentType string
ExpectedStatus int
ExpectedBodyRegex string
}{
{"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", `{"image_builds":[]}`, "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, err := distro_mock.NewDefaultRegistry()
if err != nil {
t.Fatal(err)
}
api := rcm.New(nil, store.New(nil), rpmmd_mock.NewRPMMDMock(rpmmd_mock.BaseFixture()), registry)
for _, c := range cases {
resp := internalRequest(api, c.Method, c.Path, c.Body, c.ContentType)
buf := new(bytes.Buffer)
_, _ = buf.ReadFrom(resp.Body)
response_body := buf.String()
if resp.StatusCode != c.ExpectedStatus {
t.Errorf("%s request to %s should return status code %d but returns %d, response: %s", c.Method, c.Path, c.ExpectedStatus, resp.StatusCode, response_body)
}
matched, err := regexp.Match(c.ExpectedBodyRegex, []byte(response_body))
if err != nil {
t.Fatalf("Failed to match regex, correct the test definition!")
}
if !matched {
t.Errorf("The response to %s request to %s should match this regex %s but returns %s", c.Method, c.Path, c.ExpectedBodyRegex, response_body)
}
}
}
func TestSubmit(t *testing.T) {
registry, err := distro_mock.NewDefaultRegistry()
if err != nil {
t.Fatal(err)
}
api := rcm.New(nil, store.New(nil), rpmmd_mock.NewRPMMDMock(rpmmd_mock.BaseFixture()), registry)
var submit_reply struct {
UUID uuid.UUID `json:"compose_id"`
}
var cases = []struct {
Method string
Path string
Body string
ContentType string
ExpectedStatus int
}{
{
"POST",
"/v1/compose",
`{
"image_builds":
[
{
"distribution": "fedora-30",
"architecture": "x86_64",
"image_type": "qcow2",
"repositories":
[
{
"baseurl": "http://download.fedoraproject.org/pub/fedora/linux/releases/30/Everything/x86_64/os/"
}
]
}
]
}`,
"application/json",
http.StatusOK,
},
{
"POST",
"/v1/compose",
`{
"image_builds":
[
{
"distribution": "invalid",
"architecture": "x86_64",
"image_type": "qcow2",
"repositories":
[
{
"baseurl": "http://download.fedoraproject.org/pub/fedora/linux/releases/30/Everything/x86_64/os/"
}
]
}
]
}`,
"application/json",
http.StatusBadRequest,
},
{
"POST",
"/v1/compose",
`{
"image_builds":
[
{
"distribution": "fedora-30",
"architecture": "invalid",
"image_type": "qcow2",
"repositories":
[
{
"baseurl": "http://download.fedoraproject.org/pub/fedora/linux/releases/30/Everything/x86_64/os/"
}
]
}
]
}`,
"application/json",
http.StatusBadRequest,
},
{
"POST",
"/v1/compose",
`{
"image_builds":
[
{
"distribution": "fedora-30",
"architecture": "x86_64",
"image_type": "invalid",
"repositories":
[
{
"baseurl": "http://download.fedoraproject.org/pub/fedora/linux/releases/30/Everything/x86_64/os/"
}
]
}
]
}`,
"application/json",
http.StatusBadRequest,
},
}
for n, c := range cases {
// Submit job
t.Logf("RCM API submit compose, case %d\n", n)
resp := internalRequest(api, c.Method, c.Path, c.Body, c.ContentType)
if resp.StatusCode != c.ExpectedStatus {
errReason, _ := ioutil.ReadAll(resp.Body)
t.Fatal("Failed to call /v1/compose, HTTP status code: ", resp.StatusCode, "Error message: ", string(errReason))
}
if resp.StatusCode == http.StatusOK {
decoder := json.NewDecoder(resp.Body)
decoder.DisallowUnknownFields()
err := decoder.Decode(&submit_reply)
if err != nil {
t.Fatal("Failed to decode response to /v1/compose:", err)
}
}
}
}
func TestStatus(t *testing.T) {
// Test the most basic use case: Submit a new job and get its status.
registry, err := distro_mock.NewDefaultRegistry()
if err != nil {
t.Fatal(err)
}
api := rcm.New(nil, store.New(nil), rpmmd_mock.NewRPMMDMock(rpmmd_mock.BaseFixture()), registry)
var submit_reply struct {
UUID uuid.UUID `json:"compose_id"`
}
var status_reply struct {
Status string `json:"status,omitempty"`
ErrorReason string `json:"error_reason,omitempty"`
}
// Submit a job
resp := internalRequest(api,
"POST",
"/v1/compose",
`{
"image_builds":
[
{
"distribution": "fedora-30",
"architecture": "x86_64",
"image_type": "qcow2",
"repositories":
[
{
"baseurl": "http://download.fedoraproject.org/pub/fedora/linux/releases/30/Everything/x86_64/os/"
}
]
}
]
}`,
"application/json")
if resp.StatusCode != http.StatusOK {
errReason, _ := ioutil.ReadAll(resp.Body)
t.Fatal("Failed to call /v1/compose, HTTP status code: ", resp.StatusCode, "Error message: ", string(errReason))
}
decoder := json.NewDecoder(resp.Body)
decoder.DisallowUnknownFields()
err = decoder.Decode(&submit_reply)
if err != nil {
t.Fatal("Failed to decode response to /v1/compose:", err)
}
var cases = []struct {
Method string
Path string
Body string
ContentType string
ExpectedStatus string
}{
{
"GET",
"/v1/compose/" + submit_reply.UUID.String(),
``,
"application/json",
"WAITING",
},
}
for n, c := range cases {
// Get the status
t.Logf("RCM API get compose status, case %d\n", n)
resp = internalRequest(api, c.Method, c.Path, c.Body, c.ContentType)
decoder = json.NewDecoder(resp.Body)
decoder.DisallowUnknownFields()
err = decoder.Decode(&status_reply)
if err != nil {
t.Fatal("Failed to decode response to /v1/compose/UUID:", err)
}
if status_reply.Status != c.ExpectedStatus {
t.Error("Failed to get compose status:", status_reply.Status)
}
}
}