diff --git a/test/e2e/internal/utils/exec.go b/test/e2e/internal/utils/exec.go index a7c5acb2b..54a2da185 100644 --- a/test/e2e/internal/utils/exec.go +++ b/test/e2e/internal/utils/exec.go @@ -121,6 +121,12 @@ func (opts *ExecOption) MatchErrKeyWords(keywords ...string) *ExecOption { return opts } +// MatchRequestHeaders adds keywords matching to each sent request. +func (opts *ExecOption) MatchRequestHeaders(headers ...string) *ExecOption { + opts.stderr = append(opts.stderr, match.NewRequestHeaderMatcher(headers)) + return opts +} + // MatchContent adds full content matching to the execution. func (opts *ExecOption) MatchContent(content string) *ExecOption { if opts.exitCode == 0 { diff --git a/test/e2e/internal/utils/match/keywords.go b/test/e2e/internal/utils/match/keywords.go index bdd4e8a0f..802499ab9 100644 --- a/test/e2e/internal/utils/match/keywords.go +++ b/test/e2e/internal/utils/match/keywords.go @@ -23,8 +23,8 @@ import ( ) // keywordMatcher provides selective matching of the output. -// The match will pass if all key words existed case-insensitively in the -// output. +// The match will pass if all the keywords exist case-insensitively +// in the output. type keywordMatcher []string func NewKeywordMatcher(kw []string) keywordMatcher { diff --git a/test/e2e/internal/utils/match/request.go b/test/e2e/internal/utils/match/request.go new file mode 100644 index 000000000..966e213b4 --- /dev/null +++ b/test/e2e/internal/utils/match/request.go @@ -0,0 +1,82 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package match + +import ( + "fmt" + "strings" + + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" +) + +// requestHeaderMatcher provides matching for request content. +// It looks into the debug output of an operation and the match will pass +// if all the headers exist in every sent request. +type requestHeaderMatcher []string + +// NewRequestHeaderMatcher returns a request header matcher. +func NewRequestHeaderMatcher(kw []string) requestHeaderMatcher { + return requestHeaderMatcher(kw) +} + +// Match matches got with wanted headers. +func (r requestHeaderMatcher) Match(got *gbytes.Buffer) { + var missed []string + + raw := string(got.Contents()) + reqs := getRequestHeaders(getRequests(raw)) + for _, req := range reqs { + for _, w := range r { + if !strings.Contains(req, w) { + missed = append(missed, w) + } + } + } + + if len(missed) != 0 { + fmt.Printf("Headers missed: %v\n", missed) + panic("failed to match all headers") + } +} + +// getRequests parses raw debug output to a string slice +// containing each request. +func getRequests(debugOutput string) []string { + reqs := strings.Split(debugOutput, "> Request URL:") + Expect(len(reqs) > 0).To(BeTrue(), "should output requests in debug logs") + reqs = reqs[1:] + // trim the response content + for i, req := range reqs { + req = strings.Split(req, "< Response Status:")[0] + reqs[i] = req + } + return reqs +} + +// getRequestHeaders takes a string slice containing requests +// and extract request headers from them. +func getRequestHeaders(reqs []string) []string { + headers := make([]string, len(reqs)) + for i, req := range reqs { + // extract the header content from each request + _, header, ok := strings.Cut(req, "> Request headers:\n") + if ok { + headers[i] = header + } + } + return headers +} diff --git a/test/e2e/suite/command/custom_header.go b/test/e2e/suite/command/custom_header.go index 044cf9c46..9a3dbdd83 100644 --- a/test/e2e/suite/command/custom_header.go +++ b/test/e2e/suite/command/custom_header.go @@ -16,10 +16,80 @@ limitations under the License. package command import ( + "fmt" + . "github.com/onsi/ginkgo/v2" + "oras.land/oras/test/e2e/internal/testdata/foobar" + "oras.land/oras/test/e2e/internal/testdata/multi_arch" . "oras.land/oras/test/e2e/internal/utils" ) +var _ = Describe("Common registry users:", func() { + prepareHeaderTestRepo := func(text string) string { + return fmt.Sprintf("command/headertest/%d/%s", GinkgoRandomSeed(), text) + } + var ( + FoobarHeaderInput = "Foo:bar" + FoobarHeader = "\"Foo\": \"bar\"\n" + AbHeaderInput = "A: b" + AbHeader = "\"A\": \" b\"\n" + ) + When("custom header is provided", func() { + It("attach", func() { + testRepo := prepareHeaderTestRepo("attach") + tempDir := PrepareTempFiles() + subjectRef := RegistryRef(Host, testRepo, foobar.Tag) + prepare(RegistryRef(Host, ImageRepo, foobar.Tag), subjectRef) + ORAS("attach", "--artifact-type", "test.attach", subjectRef, + fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), + "-d", "-H", FoobarHeaderInput, "-H", AbHeaderInput). + WithWorkDir(tempDir).MatchRequestHeaders(FoobarHeader, AbHeader).Exec() + }) + It("blob", func() { + blobDigest := "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" + ORAS("blob", "fetch", RegistryRef(Host, ImageRepo, blobDigest), "--descriptor", + "-d", "-H", FoobarHeaderInput, "-H", AbHeaderInput). + MatchRequestHeaders(FoobarHeader, AbHeader).Exec() + }) + It("manifest", func() { + ORAS("manifest", "fetch", RegistryRef(Host, ImageRepo, multi_arch.Tag), + "-d", "-H", FoobarHeaderInput, "-H", AbHeaderInput). + MatchRequestHeaders(FoobarHeader, AbHeader).Exec() + }) + It("pull", func() { + tempDir := GinkgoT().TempDir() + ORAS("pull", "-d", "-H", FoobarHeaderInput, "-H", AbHeaderInput, + RegistryRef(Host, ImageRepo, "foobar"), "--config", "config.json"). + WithWorkDir(tempDir).MatchRequestHeaders(FoobarHeader, AbHeader).Exec() + }) + It("push", func() { + repo := prepareHeaderTestRepo("push") + tempDir := GinkgoT().TempDir() + if err := CopyTestFiles(tempDir); err != nil { + panic(err) + } + ORAS("push", "-d", "-H", FoobarHeaderInput, "-H", AbHeaderInput, + RegistryRef(Host, repo, "latest"), "foobar/bar"). + WithWorkDir(tempDir).MatchRequestHeaders(FoobarHeader, AbHeader).Exec() + }) + It("repo", func() { + ORAS("repository", "list", Host, "-d", "-H", FoobarHeaderInput, "-H", AbHeaderInput). + MatchRequestHeaders(FoobarHeader, AbHeader).Exec() + }) + It("tag", func() { + digest := "sha256:e2bfc9cc6a84ec2d7365b5a28c6bc5806b7fa581c9ad7883be955a64e3cc034f" + ORAS("tag", RegistryRef(Host, ImageRepo, digest), "latest", + "-d", "-H", FoobarHeaderInput, "-H", AbHeaderInput). + MatchRequestHeaders(FoobarHeader, AbHeader).Exec() + }) + It("login", func() { + ORAS("login", Host, "-u", Username, "-p", Password, "--registry-config", AuthConfigPath, + "-H", FoobarHeaderInput, "-H", AbHeaderInput). + MatchRequestHeaders(FoobarHeader, AbHeader).Exec() + }) + }) +}) + var _ = Describe("OCI image layout users:", func() { When("custom header is provided", func() { It("should fail attach", func() {