You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
helm/pkg/registry/transport_test.go

400 lines
11 KiB

/*
Copyright The Helm 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 registry
import (
"bytes"
"errors"
"io"
"net/http"
"testing"
)
var errMockRead = errors.New("mock read error")
type errorReader struct{}
func (e *errorReader) Read(_ []byte) (n int, err error) {
return 0, errMockRead
}
func Test_isPrintableContentType(t *testing.T) {
tests := []struct {
name string
contentType string
want bool
}{
{
name: "Empty content type",
contentType: "",
want: false,
},
{
name: "General JSON type",
contentType: "application/json",
want: true,
},
{
name: "General JSON type with charset",
contentType: "application/json; charset=utf-8",
want: true,
},
{
name: "Random type with application/json prefix",
contentType: "application/jsonwhatever",
want: false,
},
{
name: "Manifest type in JSON",
contentType: "application/vnd.oci.image.manifest.v1+json",
want: true,
},
{
name: "Manifest type in JSON with charset",
contentType: "application/vnd.oci.image.manifest.v1+json; charset=utf-8",
want: true,
},
{
name: "Random content type in JSON",
contentType: "application/whatever+json",
want: true,
},
{
name: "Plain text type",
contentType: "text/plain",
want: true,
},
{
name: "Plain text type with charset",
contentType: "text/plain; charset=utf-8",
want: true,
},
{
name: "Random type with text/plain prefix",
contentType: "text/plainnnnn",
want: false,
},
{
name: "HTML type",
contentType: "text/html",
want: true,
},
{
name: "Plain text type with charset",
contentType: "text/html; charset=utf-8",
want: true,
},
{
name: "Random type with text/html prefix",
contentType: "text/htmlllll",
want: false,
},
{
name: "Binary type",
contentType: "application/octet-stream",
want: false,
},
{
name: "Unknown type",
contentType: "unknown/unknown",
want: false,
},
{
name: "Invalid type",
contentType: "text/",
want: false,
},
{
name: "Random string",
contentType: "random123!@#",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isPrintableContentType(tt.contentType); got != tt.want {
t.Errorf("isPrintableContentType() = %v, want %v", got, tt.want)
}
})
}
}
func Test_logResponseBody(t *testing.T) {
tests := []struct {
name string
resp *http.Response
want string
wantData []byte
}{
{
name: "Nil body",
resp: &http.Response{
Body: nil,
Header: http.Header{"Content-Type": []string{"application/json"}},
},
want: " No response body to print",
},
{
name: "No body",
wantData: nil,
resp: &http.Response{
Body: http.NoBody,
ContentLength: 100, // in case of HEAD response, the content length is set but the body is empty
Header: http.Header{"Content-Type": []string{"application/json"}},
},
want: " No response body to print",
},
{
name: "Empty body",
wantData: []byte(""),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte(""))),
ContentLength: 0,
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: " Response body is empty",
},
{
name: "Unknown content length",
wantData: []byte("whatever"),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("whatever"))),
ContentLength: -1,
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: "whatever",
},
{
name: "Missing content type header",
wantData: []byte("whatever"),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("whatever"))),
ContentLength: 8,
},
want: " Response body without a content type is not printed",
},
{
name: "Empty content type header",
wantData: []byte("whatever"),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("whatever"))),
ContentLength: 8,
Header: http.Header{"Content-Type": []string{""}},
},
want: " Response body without a content type is not printed",
},
{
name: "Non-printable content type",
wantData: []byte("binary data"),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("binary data"))),
ContentLength: 11,
Header: http.Header{"Content-Type": []string{"application/octet-stream"}},
},
want: " Response body of content type \"application/octet-stream\" is not printed",
},
{
name: "Body at the limit",
wantData: bytes.Repeat([]byte("a"), int(payloadSizeLimit)),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader(bytes.Repeat([]byte("a"), int(payloadSizeLimit)))),
ContentLength: payloadSizeLimit,
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: string(bytes.Repeat([]byte("a"), int(payloadSizeLimit))),
},
{
name: "Body larger than limit",
wantData: bytes.Repeat([]byte("a"), int(payloadSizeLimit+1)),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader(bytes.Repeat([]byte("a"), int(payloadSizeLimit+1)))), // 1 byte larger than limit
ContentLength: payloadSizeLimit + 1,
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: string(bytes.Repeat([]byte("a"), int(payloadSizeLimit))) + "\n...(truncated)",
},
{
name: "Printable content type within limit",
wantData: []byte("data"),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("data"))),
ContentLength: 4,
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: "data",
},
{
name: "Actual body size is larger than content length",
wantData: []byte("data"),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("data"))),
ContentLength: 3, // mismatched content length
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: "data",
},
{
name: "Actual body size is larger than content length and exceeds limit",
wantData: bytes.Repeat([]byte("a"), int(payloadSizeLimit+1)),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader(bytes.Repeat([]byte("a"), int(payloadSizeLimit+1)))), // 1 byte larger than limit
ContentLength: 1, // mismatched content length
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: string(bytes.Repeat([]byte("a"), int(payloadSizeLimit))) + "\n...(truncated)",
},
{
name: "Actual body size is smaller than content length",
wantData: []byte("data"),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("data"))),
ContentLength: 5, // mismatched content length
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: "data",
},
{
name: "Body contains token",
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte(`{"token":"12345"}`))),
ContentLength: 17,
Header: http.Header{"Content-Type": []string{"application/json"}},
},
wantData: []byte(`{"token":"12345"}`),
want: " Response body redacted due to potential credentials",
},
{
name: "Body contains access_token",
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte(`{"access_token":"12345"}`))),
ContentLength: 17,
Header: http.Header{"Content-Type": []string{"application/json"}},
},
wantData: []byte(`{"access_token":"12345"}`),
want: " Response body redacted due to potential credentials",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := logResponseBody(tt.resp); got != tt.want {
t.Errorf("logResponseBody() = %v, want %v", got, tt.want)
}
// validate the response body
if tt.resp.Body != nil {
readBytes, err := io.ReadAll(tt.resp.Body)
if err != nil {
t.Errorf("failed to read body after logResponseBody(), err= %v", err)
}
if !bytes.Equal(readBytes, tt.wantData) {
t.Errorf("resp.Body after logResponseBody() = %v, want %v", readBytes, tt.wantData)
}
if closeErr := tt.resp.Body.Close(); closeErr != nil {
t.Errorf("failed to close body after logResponseBody(), err= %v", closeErr)
}
}
})
}
}
func Test_logResponseBody_error(t *testing.T) {
tests := []struct {
name string
resp *http.Response
want string
}{
{
name: "Error reading body",
resp: &http.Response{
Body: io.NopCloser(&errorReader{}),
ContentLength: 10,
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: " Error reading response body: mock read error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := logResponseBody(tt.resp); got != tt.want {
t.Errorf("logResponseBody() = %v, want %v", got, tt.want)
}
if closeErr := tt.resp.Body.Close(); closeErr != nil {
t.Errorf("failed to close body after logResponseBody(), err= %v", closeErr)
}
})
}
}
func Test_containsCredentials(t *testing.T) {
tests := []struct {
name string
body string
want bool
}{
{
name: "Contains token keyword",
body: `{"token": "12345"}`,
want: true,
},
{
name: "Contains quoted token keyword",
body: `whatever "token" blah`,
want: true,
},
{
name: "Contains unquoted token keyword",
body: `whatever token blah`,
want: false,
},
{
name: "Contains access_token keyword",
body: `{"access_token": "12345"}`,
want: true,
},
{
name: "Contains quoted access_token keyword",
body: `whatever "access_token" blah`,
want: true,
},
{
name: "Contains unquoted access_token keyword",
body: `whatever access_token blah`,
want: false,
},
{
name: "Does not contain credentials",
body: `{"key": "value"}`,
want: false,
},
{
name: "Empty body",
body: ``,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := containsCredentials(tt.body); got != tt.want {
t.Errorf("containsCredentials() = %v, want %v", got, tt.want)
}
})
}
}