diff --git a/cmd/mock-dnf-json/dnf-json.go b/cmd/mock-dnf-json/dnf-json.go new file mode 100644 index 000000000..9a6d06182 --- /dev/null +++ b/cmd/mock-dnf-json/dnf-json.go @@ -0,0 +1,93 @@ +// Mock dnf-json +// +// The purpose of this program is to return fake but expected responses to +// dnf-json depsolve and dump queries. Tests should initialise a +// dnfjson.Solver and configure it to run this program via the SetDNFJSONPath() +// method. This utility accepts queries and returns responses with the same +// structure as the dnf-json Python script. +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + + "github.com/osbuild/osbuild-composer/internal/dnfjson" +) + +func maybeFail(err error) { + if err != nil { + fail(err) + } +} + +func fail(err error) { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) +} + +func readRequest(r io.Reader) dnfjson.Request { + j := json.NewDecoder(os.Stdin) + j.DisallowUnknownFields() + + var req dnfjson.Request + err := j.Decode(&req) + maybeFail(err) + return req +} + +func readTestCase() string { + if len(os.Args) < 2 { + fail(errors.New("no test case specified")) + } + if len(os.Args) > 2 { + fail(errors.New("invalid number of arguments: you must specify a test case")) + } + return os.Args[1] +} + +func parseResponse(resp []byte, command string) json.RawMessage { + parsedResponse := make(map[string]json.RawMessage) + err := json.Unmarshal(resp, &parsedResponse) + maybeFail(err) + if command == "chain-depsolve" { + // treat chain-depsolve and depsolve the same + command = "depsolve" + } + return parsedResponse[command] +} + +func checkForError(msg json.RawMessage) bool { + j := json.NewDecoder(bytes.NewReader(msg)) + j.DisallowUnknownFields() + dnferror := new(dnfjson.Error) + err := j.Decode(dnferror) + return err == nil +} + +func main() { + testFilePath := readTestCase() + + req := readRequest(os.Stdin) + + testFile, err := os.Open(testFilePath) + if err != nil { + fail(fmt.Errorf("failed to open test file %q\n", testFilePath)) + } + defer testFile.Close() + response, err := io.ReadAll(testFile) + if err != nil { + fail(fmt.Errorf("failed to read test file %q\n", testFilePath)) + } + + res := parseResponse(response, req.Command) + fmt.Print(string(parseResponse(response, req.Command))) + + // check if we should return with error + if checkForError(res) { + os.Exit(1) + } +} diff --git a/internal/mocks/dnfjson/dnfjson.go b/internal/mocks/dnfjson/dnfjson.go new file mode 100644 index 000000000..f4d0cd589 --- /dev/null +++ b/internal/mocks/dnfjson/dnfjson.go @@ -0,0 +1,203 @@ +// dnfjson_mock provides data and methods for testing the dnfjson package. +package dnfjson_mock + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/osbuild/osbuild-composer/internal/dnfjson" + "github.com/osbuild/osbuild-composer/internal/rpmmd" +) + +func generatePackageList() rpmmd.PackageList { + baseTime, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") + + if err != nil { + panic(err) + } + + var packageList rpmmd.PackageList + + for i := 0; i < 22; i++ { + basePackage := rpmmd.Package{ + Name: fmt.Sprintf("package%d", i), + Summary: fmt.Sprintf("pkg%d sum", i), + Description: fmt.Sprintf("pkg%d desc", i), + URL: fmt.Sprintf("https://pkg%d.example.com", i), + Epoch: 0, + Version: fmt.Sprintf("%d.0", i), + Release: fmt.Sprintf("%d.fc30", i), + Arch: "x86_64", + BuildTime: baseTime.AddDate(0, i, 0), + License: "MIT", + } + + secondBuild := basePackage + + secondBuild.Version = fmt.Sprintf("%d.1", i) + secondBuild.BuildTime = basePackage.BuildTime.AddDate(0, 0, 1) + + packageList = append(packageList, basePackage, secondBuild) + } + + return packageList +} + +func createBaseDepsolveFixture() []dnfjson.PackageSpec { + return []dnfjson.PackageSpec{ + { + Name: "dep-package3", + Epoch: 7, + Version: "3.0.3", + Release: "1.fc30", + Arch: "x86_64", + RepoID: "0", + }, + { + Name: "dep-package1", + Epoch: 0, + Version: "1.33", + Release: "2.fc30", + Arch: "x86_64", + RepoID: "0", + }, + { + Name: "dep-package2", + Epoch: 0, + Version: "2.9", + Release: "1.fc30", + Arch: "x86_64", + RepoID: "0", + }, + } +} + +// BaseDeps is the expected list of dependencies (as rpmmd.PackageSpec) from +// the Base ResponseGenerator +func BaseDeps() []rpmmd.PackageSpec { + return []rpmmd.PackageSpec{ + { + Name: "dep-package3", + Epoch: 7, + Version: "3.0.3", + Release: "1.fc30", + Arch: "x86_64", + CheckGPG: true, + }, + { + Name: "dep-package1", + Epoch: 0, + Version: "1.33", + Release: "2.fc30", + Arch: "x86_64", + CheckGPG: true, + }, + { + Name: "dep-package2", + Epoch: 0, + Version: "2.9", + Release: "1.fc30", + Arch: "x86_64", + CheckGPG: true, + }, + } +} + +type ResponseGenerator func(string) string + +func Base(tmpdir string) string { + deps := map[string]interface{}{ + "checksums": map[string]string{ + "0": "test:responsechecksum", + }, + "dependencies": createBaseDepsolveFixture(), + } + + pkgs := map[string]interface{}{ + "checksums": map[string]string{ + "0": "test:responsechecksum", + }, + "packages": generatePackageList(), + } + + data := map[string]interface{}{ + "depsolve": deps, + "dump": pkgs, + } + path := filepath.Join(tmpdir, "base.json") + write(data, path) + return path +} + +func NonExistingPackage(tmpdir string) string { + deps := dnfjson.Error{ + Kind: "MarkingErrors", + Reason: "Error occurred when marking packages for installation: Problems in request:\nmissing packages: fash", + } + data := map[string]interface{}{ + "depsolve": deps, + } + path := filepath.Join(tmpdir, "notexist.json") + write(data, path) + return path +} + +func BadDepsolve(tmpdir string) string { + deps := dnfjson.Error{ + Kind: "DepsolveError", + Reason: "There was a problem depsolving ['go2rpm']: \n Problem: conflicting requests\n - nothing provides askalono-cli needed by go2rpm-1-4.fc31.noarch", + } + pkgs := map[string]interface{}{ + "checksums": map[string]string{ + "0": "test:responsechecksum", + }, + "packages": generatePackageList(), + } + + data := map[string]interface{}{ + "depsolve": deps, + "dump": pkgs, + } + path := filepath.Join(tmpdir, "baddepsolve.json") + write(data, path) + return path +} + +func BadFetch(tmpdir string) string { + deps := dnfjson.Error{ + Kind: "DepsolveError", + Reason: "There was a problem depsolving ['go2rpm']: \n Problem: conflicting requests\n - nothing provides askalono-cli needed by go2rpm-1-4.fc31.noarch", + } + pkgs := dnfjson.Error{ + Kind: "FetchError", + Reason: "There was a problem when fetching packages.", + } + data := map[string]interface{}{ + "depsolve": deps, + "dump": pkgs, + } + path := filepath.Join(tmpdir, "badfetch.json") + write(data, path) + return path +} + +func marshal(data interface{}) []byte { + jdata, err := json.Marshal(data) + if err != nil { + panic(err) + } + return jdata +} + +func write(data interface{}, path string) { + fp, err := os.Create(path) + if err != nil { + panic(err) + } + if _, err := fp.Write(marshal(data)); err != nil { + panic(err) + } +}