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) } } }