mirror of https://github.com/helm/helm
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.
400 lines
11 KiB
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)
|
|
}
|
|
})
|
|
}
|
|
}
|