Previously, only a subset from all Koji Compose unit test cases were run. Remove this limitation and run all defined unit tests, which were copied from `kojiapi`. In addition, fix unit tests and relevant cloudapi methods to make unit tests pass. Add `TestRouteWithReply()` to `test/helpers.go` to allow getting the compose ID when submitting a new compose. This is needed to make some unit tests deterministic. Do not delete values from `fields` slice in `dropFields()` in `test/helpers.go`. The behavior was previously not consistent. If the top-level map contained the value, it was deleted from it, but the nested maps also contained the value, it was not deleted. On the other hand, if the top level map didn't contain the value, but nested maps did contain it, the value was deleted from all nested maps.
250 lines
7.4 KiB
Go
250 lines
7.4 KiB
Go
package test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/uuid"
|
|
"github.com/osbuild/osbuild-composer/internal/common"
|
|
"github.com/osbuild/osbuild-composer/internal/distro"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func externalRequest(method, path, body string) *http.Response {
|
|
client := http.Client{
|
|
Transport: &http.Transport{
|
|
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
|
return net.Dial("unix", "/run/weldr/api.socket")
|
|
},
|
|
},
|
|
}
|
|
|
|
req, err := http.NewRequest(method, "http://localhost"+path, bytes.NewReader([]byte(body)))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if method == "POST" {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return resp
|
|
}
|
|
|
|
func internalRequest(api http.Handler, method, path, body string) *http.Response {
|
|
req := httptest.NewRequest(method, path, bytes.NewReader([]byte(body)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
api.ServeHTTP(resp, req)
|
|
|
|
return resp.Result()
|
|
}
|
|
|
|
func SendHTTP(api http.Handler, external bool, method, path, body string) *http.Response {
|
|
if len(os.Getenv("OSBUILD_COMPOSER_TEST_EXTERNAL")) > 0 {
|
|
if !external {
|
|
return nil
|
|
}
|
|
return externalRequest(method, path, body)
|
|
} else {
|
|
return internalRequest(api, method, path, body)
|
|
}
|
|
}
|
|
|
|
// this function serves to drop fields that shouldn't be tested from the unmarshalled json objects
|
|
func dropFields(obj interface{}, fields ...string) {
|
|
switch v := obj.(type) {
|
|
// if the interface type is a map attempt to delete the fields
|
|
case map[string]interface{}:
|
|
for _, field := range fields {
|
|
delete(v, field)
|
|
}
|
|
// call dropFields on the remaining elements since they may contain a map containing the field
|
|
for _, val := range v {
|
|
dropFields(val, fields...)
|
|
}
|
|
// if the type is a list of interfaces call dropFields on each interface
|
|
case []interface{}:
|
|
for _, element := range v {
|
|
dropFields(element, fields...)
|
|
}
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
|
|
func TestRoute(t *testing.T, api http.Handler, external bool, method, path, body string, expectedStatus int, expectedJSON string, ignoreFields ...string) {
|
|
t.Helper()
|
|
_ = TestRouteWithReply(t, api, external, method, path, body, expectedStatus, expectedJSON, ignoreFields...)
|
|
}
|
|
|
|
// TestRouteWithReply tests the given API endpoint and if the test passes, it returns the raw JSON reply.
|
|
func TestRouteWithReply(t *testing.T, api http.Handler, external bool, method, path, body string, expectedStatus int, expectedJSON string, ignoreFields ...string) (replyJSON []byte) {
|
|
t.Helper()
|
|
|
|
resp := SendHTTP(api, external, method, path, body)
|
|
if resp == nil {
|
|
t.Skip("This test is for internal testing only")
|
|
}
|
|
|
|
var err error
|
|
replyJSON, err = ioutil.ReadAll(resp.Body)
|
|
require.NoErrorf(t, err, "%s: could not read response body", path)
|
|
|
|
assert.Equalf(t, expectedStatus, resp.StatusCode, "SendHTTP failed for path %s: %v", path, string(replyJSON))
|
|
|
|
if expectedJSON == "" {
|
|
require.Lenf(t, replyJSON, 0, "%s: expected no response body, but got:\n%s", path, replyJSON)
|
|
}
|
|
|
|
if expectedJSON == "?" {
|
|
return
|
|
}
|
|
|
|
var reply, expected interface{}
|
|
err = json.Unmarshal(replyJSON, &reply)
|
|
require.NoErrorf(t, err, "%s: json.Unmarshal failed for\n%s", path, string(replyJSON))
|
|
|
|
if expectedJSON == "*" {
|
|
return
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(expectedJSON), &expected)
|
|
require.NoErrorf(t, err, "%s: expected JSON is invalid", path)
|
|
|
|
dropFields(reply, ignoreFields...)
|
|
dropFields(expected, ignoreFields...)
|
|
|
|
require.Equal(t, expected, reply)
|
|
|
|
return
|
|
}
|
|
|
|
func TestTOMLRoute(t *testing.T, api http.Handler, external bool, method, path, body string, expectedStatus int, expectedTOML string, ignoreFields ...string) {
|
|
t.Helper()
|
|
|
|
resp := SendHTTP(api, external, method, path, body)
|
|
if resp == nil {
|
|
t.Skip("This test is for internal testing only")
|
|
}
|
|
|
|
replyTOML, err := ioutil.ReadAll(resp.Body)
|
|
require.NoErrorf(t, err, "%s: could not read response body", path)
|
|
|
|
assert.Equalf(t, expectedStatus, resp.StatusCode, "SendHTTP failed for path %s: %v", path, string(replyTOML))
|
|
|
|
if expectedTOML == "" {
|
|
require.Lenf(t, replyTOML, 0, "%s: expected no response body, but got:\n%s", path, replyTOML)
|
|
}
|
|
|
|
var reply, expected interface{}
|
|
err = toml.Unmarshal(replyTOML, &reply)
|
|
require.NoErrorf(t, err, "%s: json.Unmarshal failed for\n%s", path, string(replyTOML))
|
|
|
|
if expectedTOML == "*" {
|
|
return
|
|
}
|
|
|
|
err = toml.Unmarshal([]byte(expectedTOML), &expected)
|
|
require.NoErrorf(t, err, "%s: expected TOML is invalid", path)
|
|
|
|
dropFields(reply, ignoreFields...)
|
|
dropFields(expected, ignoreFields...)
|
|
|
|
require.Equal(t, expected, reply)
|
|
}
|
|
|
|
func TestNonJsonRoute(t *testing.T, api http.Handler, external bool, method, path, body string, expectedStatus int, expectedResponse string) {
|
|
response := SendHTTP(api, external, method, path, body)
|
|
assert.Equalf(t, expectedStatus, response.StatusCode, "%s: status mismatch", path)
|
|
|
|
responseBodyBytes, err := ioutil.ReadAll(response.Body)
|
|
require.NoErrorf(t, err, "%s: could not read response body", path)
|
|
|
|
responseBody := string(responseBodyBytes)
|
|
require.Equalf(t, expectedResponse, responseBody, "%s: body mismatch", path)
|
|
}
|
|
|
|
func IgnoreDates() cmp.Option {
|
|
return cmp.Comparer(func(a, b time.Time) bool { return true })
|
|
}
|
|
|
|
func IgnoreUuids() cmp.Option {
|
|
return cmp.Comparer(func(a, b uuid.UUID) bool { return true })
|
|
}
|
|
|
|
func Ignore(what string) cmp.Option {
|
|
return cmp.FilterPath(func(p cmp.Path) bool { return p.String() == what }, cmp.Ignore())
|
|
}
|
|
|
|
// CompareImageType considers two image type objects equal if and only if the names of their distro/arch/imagetype
|
|
// are. The thinking is that the objects are static, and resolving by these three keys should always give equivalent
|
|
// objects. Whether we actually have object equality, is an implementation detail, so we don't want to rely on that.
|
|
func CompareImageTypes() cmp.Option {
|
|
return cmp.Comparer(func(x, y distro.ImageType) bool {
|
|
return x.Name() == y.Name() &&
|
|
x.Arch().Name() == y.Arch().Name() &&
|
|
x.Arch().Distro().Name() == y.Arch().Distro().Name()
|
|
})
|
|
}
|
|
|
|
// Create a temporary repository
|
|
func SetUpTemporaryRepository() (string, error) {
|
|
dir, err := ioutil.TempDir("/tmp", "osbuild-composer-test-")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// There's no potential command injection vector here
|
|
/* #nosec G204 */
|
|
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
|
|
}
|
|
|
|
// Remove the temporary repository
|
|
func TearDownTemporaryRepository(dir string) error {
|
|
return os.RemoveAll(dir)
|
|
}
|
|
|
|
// GenerateCIArtifactName generates a new identifier for CI artifacts which is based
|
|
// on environment variables specified by Jenkins
|
|
// note: in case of migration to sth else like Github Actions, change it to whatever variables GH Action provides
|
|
func GenerateCIArtifactName(prefix string) (string, error) {
|
|
distroCode := os.Getenv("DISTRO_CODE")
|
|
branchName := os.Getenv("BRANCH_NAME")
|
|
buildId := os.Getenv("BUILD_ID")
|
|
if branchName == "" || buildId == "" || distroCode == "" {
|
|
return "", fmt.Errorf("The environment variables must specify BRANCH_NAME, BUILD_ID, and DISTRO_CODE")
|
|
}
|
|
|
|
arch := common.CurrentArch()
|
|
|
|
return fmt.Sprintf("%s%s-%s-%s-%s", prefix, distroCode, arch, branchName, buildId), nil
|
|
}
|