github.com/GoogleCloudPlatform/compute-image-tools/cli_tools@v0.0.0-20240516224744-de2dabc4ed1b/common/utils/logging/service/logger_test.go (about)

     1  //  Copyright 2019 Google Inc. All Rights Reserved.
     2  //
     3  //  Licensed under the Apache License, Version 2.0 (the "License");
     4  //  you may not use this file except in compliance with the License.
     5  //  You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  //  Unless required by applicable law or agreed to in writing, software
    10  //  distributed under the License is distributed on an "AS IS" BASIS,
    11  //  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  //  See the License for the specific language governing permissions and
    13  //  limitations under the License.
    14  
    15  package service
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"io/ioutil"
    21  	"net/http"
    22  	"os"
    23  	"strings"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/stretchr/testify/assert"
    28  
    29  	"github.com/GoogleCloudPlatform/compute-image-tools/proto/go/pb"
    30  )
    31  
    32  var (
    33  	logger                    *Logger
    34  	serverLogEnabledPrevValue bool
    35  )
    36  
    37  func TestMain(m *testing.M) {
    38  	setup()
    39  	code := m.Run()
    40  	shutdown()
    41  	os.Exit(code)
    42  }
    43  
    44  func setup() {
    45  	serverLogEnabledPrevValue = serverLogEnabled
    46  	serverLogEnabled = true
    47  }
    48  
    49  func shutdown() {
    50  	serverLogEnabled = serverLogEnabledPrevValue
    51  }
    52  
    53  func TestLogStart(t *testing.T) {
    54  	prepareTestLogger(t, nil, buildLogResponses(deleteRequest))
    55  
    56  	e, r := logger.logStart()
    57  
    58  	if r != logResult(deleteRequest) {
    59  		t.Errorf("Unexpected logResult: %v, expect: %v", r, deleteRequest)
    60  	}
    61  	if e.Status != statusStart {
    62  		t.Errorf("Unexpected Status %v, expect: %v", e.Status, statusStart)
    63  	}
    64  }
    65  
    66  func TestLogSuccess(t *testing.T) {
    67  	prepareTestLogger(t, nil, buildLogResponses(deleteRequest))
    68  	time.Sleep(20 * time.Millisecond)
    69  
    70  	w := literalLoggable{
    71  		strings: map[string]string{
    72  			importFileFormat: "vmdk",
    73  		},
    74  		int64s: map[string][]int64{
    75  			targetSizeGb: {5},
    76  			sourceSizeGb: {3, 2, 1},
    77  		},
    78  		traceLogs: []string{
    79  			"serial-log1", "serial-log2",
    80  		},
    81  		inspectionResults: &pb.InspectionResults{
    82  			OsCount:      1,
    83  			UefiBootable: true,
    84  			OsRelease: &pb.OsRelease{
    85  				Architecture: pb.Architecture_X64,
    86  				CliFormatted: "ubuntu-2004",
    87  				Distro:       "ubuntu",
    88  				MajorVersion: "20",
    89  				MinorVersion: "04",
    90  				DistroId:     pb.Distro_UBUNTU,
    91  			},
    92  		},
    93  	}
    94  
    95  	e, r := logger.logSuccess(w)
    96  
    97  	if r != logResult(deleteRequest) {
    98  		t.Errorf("Unexpected logResult: %v, expect: %v", r, deleteRequest)
    99  	}
   100  	if e.Status != statusSuccess {
   101  		t.Errorf("Unexpected Status %v, expect: %v", e.Status, statusSuccess)
   102  	}
   103  
   104  	expectedInspectionResults := pb.InspectionResults{
   105  		OsCount:      1,
   106  		UefiBootable: true,
   107  		OsRelease: &pb.OsRelease{
   108  			Architecture: pb.Architecture_X64,
   109  			CliFormatted: "ubuntu-2004",
   110  			Distro:       "ubuntu",
   111  			MajorVersion: "20",
   112  			MinorVersion: "04",
   113  			DistroId:     pb.Distro_UBUNTU,
   114  		},
   115  	}
   116  
   117  	expectedOutputInfo := OutputInfo{
   118  		SourcesSizeGb:     []int64{3, 2, 1},
   119  		TargetsSizeGb:     []int64{5},
   120  		ImportFileFormat:  "vmdk",
   121  		SerialOutputs:     nil, // don't send serial output on success
   122  		InspectionResults: &expectedInspectionResults,
   123  	}
   124  
   125  	assert.Equal(t, &expectedInspectionResults,
   126  		e.InputParams.ImageImportParams.InspectionResults)
   127  	assert.Equal(t, expectedOutputInfo, *e.OutputInfo)
   128  
   129  	if e.ElapsedTimeMs < 20 {
   130  		t.Errorf("Unexpected ElapsedTimeMs %v < %v", e.ElapsedTimeMs, 20)
   131  	}
   132  }
   133  
   134  func TestLogFailure(t *testing.T) {
   135  	prepareTestLogger(t, nil, buildLogResponses(deleteRequest))
   136  	time.Sleep(20 * time.Millisecond)
   137  
   138  	w := literalLoggable{
   139  		strings: map[string]string{
   140  			importFileFormat: "vmdk",
   141  		},
   142  		int64s: map[string][]int64{
   143  			targetSizeGb: {5},
   144  			sourceSizeGb: {3, 2, 1},
   145  		},
   146  		traceLogs: []string{
   147  			"serial-log1", "serial-log2",
   148  		},
   149  	}
   150  	e, r := logger.logFailure(fmt.Errorf("error - [Privacy-> sensitive <-Privacy]"), w)
   151  
   152  	expected := OutputInfo{
   153  		SourcesSizeGb:                    []int64{3, 2, 1},
   154  		TargetsSizeGb:                    []int64{5},
   155  		FailureMessage:                   "error -  sensitive ",
   156  		FailureMessageWithoutPrivacyInfo: "error - ",
   157  		ImportFileFormat:                 "vmdk",
   158  		SerialOutputs:                    []string{"serial-log1", "serial-log2"},
   159  	}
   160  	assert.Equal(t, expected, *e.OutputInfo)
   161  
   162  	if r != logResult(deleteRequest) {
   163  		t.Errorf("Unexpected logResult: %v, expect: %v", r, deleteRequest)
   164  	}
   165  	if e.Status != statusFailure {
   166  		t.Errorf("Unexpected Status %v, expect: %v", e.Status, statusFailure)
   167  	}
   168  	if e.ElapsedTimeMs < 20 {
   169  		t.Errorf("Unexpected ElapsedTimeMs %v < %v", e.ElapsedTimeMs, 20)
   170  	}
   171  }
   172  
   173  func TestRunWithServerLoggingSuccess(t *testing.T) {
   174  	prepareTestLogger(t, nil, buildLogResponses(deleteRequest, deleteRequest))
   175  
   176  	logExtension, _ := logger.runWithServerLogging(
   177  		func() (Loggable, error) {
   178  			return literalLoggable{}, nil
   179  		}, nil)
   180  	if logExtension.Status != statusSuccess {
   181  		t.Errorf("Unexpected Status: %v, expect: %v", logExtension.Status, statusSuccess)
   182  	}
   183  }
   184  
   185  func TestRunWithServerLoggingFailed(t *testing.T) {
   186  	prepareTestLogger(t, nil, buildLogResponses(deleteRequest, deleteRequest))
   187  
   188  	logExtension, _ := logger.runWithServerLogging(
   189  		func() (Loggable, error) {
   190  			return literalLoggable{}, fmt.Errorf("test msg - failure by purpose")
   191  		}, nil)
   192  	if logExtension.Status != statusFailure {
   193  		t.Errorf("Unexpected Status: %v, expect: %v", logExtension.Status, statusFailure)
   194  	}
   195  }
   196  
   197  func TestRunWithServerLogging_LogsFailure_WhenApplicationPanics(t *testing.T) {
   198  	prepareTestLogger(t, nil, buildLogResponses(deleteRequest, deleteRequest))
   199  
   200  	panicMessage := "client code panic"
   201  	logExtension, err := logger.runWithServerLogging(
   202  		func() (Loggable, error) {
   203  			panic(panicMessage)
   204  		}, nil)
   205  	assert.EqualError(t, err, "A fatal error has occurred. Please submit an issue at https://github.com/GoogleCloudPlatform/compute-image-tools/issues")
   206  	assert.Equal(t, statusFailure, logExtension.Status)
   207  
   208  	// Include stacktrace and panic message in serial outputs.
   209  	assertContainsSubstring(t, "stacktrace", logExtension.OutputInfo.SerialOutputs)
   210  	assertContainsSubstring(t, panicMessage, logExtension.OutputInfo.SerialOutputs)
   211  }
   212  
   213  func assertContainsSubstring(t *testing.T, sub string, arr []string) {
   214  	for _, s := range arr {
   215  		if strings.Contains(s, sub) {
   216  			return
   217  		}
   218  	}
   219  	t.Errorf("Substring %q not found in %v", sub, arr)
   220  }
   221  
   222  func TestRunWithServerLoggingSuccessWithUpdatedProject(t *testing.T) {
   223  	prepareTestLogger(t, nil, buildLogResponses(deleteRequest, deleteRequest))
   224  
   225  	project := "dummy-project"
   226  	logExtension, _ := logger.runWithServerLogging(
   227  		func() (Loggable, error) {
   228  			return literalLoggable{}, nil
   229  		}, &project)
   230  	if logExtension.Status != statusSuccess {
   231  		t.Errorf("Unexpected Status: %v, expect: %v", logExtension.Status, statusSuccess)
   232  	}
   233  	if logExtension.InputParams.ImageImportParams.Project != project {
   234  		t.Errorf("Unexpected Updated Project: %v, expect: %v",
   235  			logExtension.InputParams.ImageImportParams.Project, project)
   236  	}
   237  }
   238  
   239  func TestSendLogToServerSuccess(t *testing.T) {
   240  	testSendLogToServerWithResponses(t, logResult(deleteRequest), buildLogResponses(deleteRequest))
   241  }
   242  
   243  func TestSendLogToServerResponseActionUnknown(t *testing.T) {
   244  	testSendLogToServerWithResponses(t, logResult(responseActionUnknown), buildLogResponses(responseActionUnknown))
   245  }
   246  
   247  func TestSendLogToServerSuccessAfterRetry(t *testing.T) {
   248  	testSendLogToServerWithResponses(t, logResult(deleteRequest), buildLogResponses(retryRequestLater, retryRequestLater, deleteRequest))
   249  }
   250  
   251  func TestSendLogToServerFailedOnCreateRequest(t *testing.T) {
   252  	backupServerURL := serverURL
   253  	serverURL = "%%bad-url"
   254  	defer func() { serverURL = backupServerURL }()
   255  	testSendLogToServerWithResponses(t, failedOnCreateRequest, nil)
   256  }
   257  
   258  func TestSendLogToServerFailedOnCreateRequestJSON(t *testing.T) {
   259  	prepareTestLogger(t, nil, nil)
   260  	r := logger.sendLogToServer(nil)
   261  	if r != logResult(failedOnCreateRequestJSON) {
   262  		t.Errorf("Unexpected Status: %v, expect: %v", r, failedOnCreateRequestJSON)
   263  	}
   264  }
   265  
   266  func TestSendLogToServerLogDisabled(t *testing.T) {
   267  	serverLogEnabled = false
   268  	defer func() { serverLogEnabled = true }()
   269  
   270  	testSendLogToServerWithResponses(t, serverLogDisabled, buildLogResponses(deleteRequest))
   271  }
   272  
   273  func TestSendLogToServerFailedToParseResponse(t *testing.T) {
   274  	prepareTestLoggerWithJSONLogResponse(t, nil, []string{"bad-json"})
   275  	r := logger.sendLogToServer(buildComputeImageToolsLogExtension())
   276  	if r != logResult(failedToParseResponse) {
   277  		t.Errorf("Unexpected Status: %v, expect: %v", r, failedToParseResponse)
   278  	}
   279  }
   280  
   281  func TestSendLogToServerFailedOnUndefinedResponse(t *testing.T) {
   282  	testSendLogToServerWithResponses(t, failedOnUndefinedResponse, buildLogResponses("UndefinedResponseForTest"))
   283  }
   284  
   285  func TestSendLogToServerFailedOnMissingResponseDetails(t *testing.T) {
   286  	testSendLogToServerWithResponses(t, failedOnMissingResponseDetails, []logResponse{{}})
   287  }
   288  
   289  func TestSendLogToServerFailedAfterRetry(t *testing.T) {
   290  	testSendLogToServerWithResponses(t, failedAfterRetry, buildLogResponses(retryRequestLater, retryRequestLater, retryRequestLater, deleteRequest))
   291  }
   292  
   293  func testSendLogToServerWithResponses(t *testing.T, expectedLogResult logResult, resps []logResponse) {
   294  	prepareTestLogger(t, nil, resps)
   295  	r := logger.sendLogToServer(buildComputeImageToolsLogExtension())
   296  	if r != logResult(expectedLogResult) {
   297  		t.Errorf("Unexpected Status: %v, expect: %v", r, expectedLogResult)
   298  	}
   299  }
   300  
   301  func TestRemoveNewLinesFromMultilineErrorSingleLine(t *testing.T) {
   302  	testRemoveNewLinesFromMultilineError(t, "Single line", "Single line")
   303  }
   304  
   305  func TestRemoveNewLinesFromMultilineErrorMultiLine(t *testing.T) {
   306  	testRemoveNewLinesFromMultilineError(t, "Header:\nFirst line\nSecond line", "Header: First line; Second line")
   307  }
   308  
   309  func TestRemoveNewLinesFromMultilineErrorEmptyString(t *testing.T) {
   310  	testRemoveNewLinesFromMultilineError(t, "", "")
   311  }
   312  
   313  func testRemoveNewLinesFromMultilineError(t *testing.T, input string, expected string) {
   314  	result := removeNewLinesFromMultilineError(input)
   315  	if result != expected {
   316  		t.Errorf("Expected %v, got %v", expected, result)
   317  	}
   318  }
   319  
   320  func prepareTestLogger(t *testing.T, err error, resps []logResponse) {
   321  	var lrs []string
   322  	for _, resp := range resps {
   323  		bytes, _ := json.Marshal(resp)
   324  		lrs = append(lrs, string(bytes))
   325  	}
   326  
   327  	prepareTestLoggerWithJSONLogResponse(t, err, lrs)
   328  }
   329  
   330  func prepareTestLoggerWithJSONLogResponse(t *testing.T, err error, lrs []string) {
   331  	httpClient = &MockHTTPClient{
   332  		t:   t,
   333  		lrs: lrs,
   334  		err: err,
   335  	}
   336  
   337  	logger = NewLoggingServiceLogger(ImageImportAction, InputParams{
   338  		ImageImportParams: &ImageImportParams{
   339  			CommonParams: &CommonParams{
   340  				ClientID: "test-client",
   341  			},
   342  		},
   343  	})
   344  }
   345  
   346  func buildComputeImageToolsLogExtension() *ComputeImageToolsLogExtension {
   347  	logExtension := &ComputeImageToolsLogExtension{
   348  		ID:           "dummy-id",
   349  		CloudBuildID: "dummy-cloud-build-id",
   350  		ToolAction:   ImageImportAction,
   351  		Status:       statusStart,
   352  		InputParams: &InputParams{
   353  			ImageImportParams: &ImageImportParams{
   354  				CommonParams: &CommonParams{
   355  					Project:           "dummy-project",
   356  					ObfuscatedProject: Hash("dummy-project"),
   357  				},
   358  				SourceImage: "dummy-image",
   359  			},
   360  		},
   361  	}
   362  	return logExtension
   363  }
   364  
   365  func buildLogResponses(actions ...responseAction) []logResponse {
   366  	var lrs []logResponse
   367  	for _, a := range actions {
   368  		lrs = append(lrs, logResponse{
   369  			NextRequestWaitMillis: 100,
   370  			LogResponseDetails: []logResponseDetails{
   371  				{
   372  					ResponseAction: a,
   373  				},
   374  			},
   375  		})
   376  	}
   377  	return lrs
   378  }
   379  
   380  type MockHTTPClient struct {
   381  	t     *testing.T
   382  	lrs   []string
   383  	index int
   384  	err   error
   385  }
   386  
   387  func (c *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {
   388  	if c.index >= len(c.lrs) {
   389  		c.t.Fatal("Exceeds time of prepared mock calls")
   390  	}
   391  
   392  	bodyReader := ioutil.NopCloser(strings.NewReader(c.lrs[c.index]))
   393  	resp := http.Response{
   394  		Body: bodyReader,
   395  	}
   396  
   397  	c.index++
   398  
   399  	return &resp, c.err
   400  }