Skip to content

Commit e771d11

Browse files
committed
More unit tests
1 parent cc5e8a4 commit e771d11

10 files changed

Lines changed: 486 additions & 22 deletions

File tree

internal/files/files_test.go

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package files
22

33
import (
4+
"errors"
45
"fmt"
56
"io/ioutil"
67
"net/http/httptest"
78
"os"
89
"path/filepath"
910
"testing"
1011

12+
"github.com/Azure/azure-extension-foundation/msi"
13+
"github.com/Azure/azure-extension-platform/vmextension"
14+
"github.com/Azure/run-command-handler-linux/internal/constants"
1115
"github.com/Azure/run-command-handler-linux/internal/handlersettings"
1216
"github.com/Azure/run-command-handler-linux/pkg/download"
1317
"github.com/ahmetalpbalkan/go-httpbin"
@@ -98,6 +102,7 @@ func Test_urlToFileName_badURL(t *testing.T) {
98102
_, err := UrlToFileName("http://192.168.0.%31/")
99103
require.NotNil(t, err)
100104
require.Contains(t, err.Error(), `unable to parse URL: "http://192.168.0.%31/"`)
105+
VerifyErrorClarification(t, constants.FileDownload_UnableToParseFileName, err)
101106
}
102107

103108
func Test_urlToFileName_noFileName(t *testing.T) {
@@ -117,6 +122,7 @@ func Test_urlToFileName_noFileName(t *testing.T) {
117122
_, err := UrlToFileName(c)
118123
require.NotNil(t, err, "not failed: %s", "url=%s", c)
119124
require.Contains(t, err.Error(), "cannot extract file name from URL", "url=%s", c)
125+
VerifyErrorClarification(t, constants.FileDownload_CannotExtractFileNameFromUrl, err)
120126
}
121127
}
122128

@@ -136,7 +142,8 @@ func Test_urlToFileName(t *testing.T) {
136142
}
137143

138144
func Test_postProcessFile_fail(t *testing.T) {
139-
require.NotNil(t, PostProcessFile("/non/existing/path"))
145+
err := PostProcessFile("/non/existing/path")
146+
VerifyErrorClarification(t, constants.Internal_FailedToOpenFileForReading, err)
140147
}
141148

142149
func Test_postProcessFile(t *testing.T) {
@@ -227,3 +234,78 @@ func Test_saveScriptFile(t *testing.T) {
227234
require.Nil(t, err)
228235
require.Equal(t, content, string(result))
229236
}
237+
238+
func TestGetDownloaders_NonBlobURL_ReturnsPublicOnly(t *testing.T) {
239+
publicURL := "https://example.com/scripts/a.sh"
240+
241+
mock := &mockMsiDownloader{providerToReturn: providerSuccess()}
242+
downloaders, err := getDownloaders(publicURL, nil, mock)
243+
244+
require.NoError(t, err)
245+
require.Len(t, downloaders, 1, "non-blob URL must return only public downloader")
246+
require.Equal(t, 0, mock.calledGet+mock.calledByClientID+mock.calledByObjectID,
247+
"msi downloader must not be used for non-blob URL")
248+
}
249+
250+
func TestGetDownloaders_EmptyURL_ReturnsClarification(t *testing.T) {
251+
dl, err := getDownloaders("", nil, &mockMsiDownloader{providerToReturn: providerSuccess()})
252+
require.Nil(t, dl)
253+
VerifyErrorClarification(t, constants.FileDownload_Empty, err)
254+
}
255+
256+
func VerifyErrorClarification(t *testing.T, expectedCode int, err error) {
257+
require.NotNil(t, err, "No error returned when one was expected")
258+
var ewc vmextension.ErrorWithClarification
259+
require.True(t, errors.As(err, &ewc), "Error is not of type ErrorWithClarification")
260+
require.Equal(t, expectedCode, ewc.ErrorCode, "Expected error %d but received %d", expectedCode, ewc.ErrorCode)
261+
}
262+
263+
func TestGetDownloaders_BlobURL_BothClientAndObjectID_ReturnsClarification(t *testing.T) {
264+
blobURL := "https://acct.blob.core.windows.net/container/blob.txt"
265+
266+
mi := &handlersettings.RunCommandManagedIdentity{
267+
ClientId: "11111111-1111-1111-1111-111111111111",
268+
ObjectId: "22222222-2222-2222-2222-222222222222",
269+
}
270+
271+
mock := &mockMsiDownloader{providerToReturn: providerSuccess()}
272+
downloaders, err := getDownloaders(blobURL, mi, mock)
273+
274+
require.Nil(t, downloaders)
275+
VerifyErrorClarification(t, constants.CustomerInput_ClientIdObjectIdBothSpecified, err)
276+
}
277+
278+
// MsiProvider is invoked as: _, err := msiProvider()
279+
type mockMsiDownloader struct {
280+
calledGet int
281+
calledByClientID int
282+
calledByObjectID int
283+
lastURL string
284+
lastClientID string
285+
lastObjectID string
286+
providerToReturn download.MsiProvider
287+
}
288+
289+
func (m *mockMsiDownloader) GetMsiProvider(url string) download.MsiProvider {
290+
m.calledGet++
291+
m.lastURL = url
292+
return m.providerToReturn
293+
}
294+
295+
func (m *mockMsiDownloader) GetMsiProviderByClientId(url, clientId string) download.MsiProvider {
296+
m.calledByClientID++
297+
m.lastURL = url
298+
m.lastClientID = clientId
299+
return m.providerToReturn
300+
}
301+
302+
func (m *mockMsiDownloader) GetMsiProviderByObjectId(url, objectId string) download.MsiProvider {
303+
m.calledByObjectID++
304+
m.lastURL = url
305+
m.lastObjectID = objectId
306+
return m.providerToReturn
307+
}
308+
309+
func providerSuccess() download.MsiProvider {
310+
return func() (msi.Msi, error) { return msi.Msi{}, nil }
311+
}

internal/hostgacommunicator/hostgacommunicator.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@ import (
1515
)
1616

1717
const (
18-
hostGaPluginPort = "32526"
18+
hostGaPluginPort = "32526"
19+
)
20+
21+
var (
1922
WireServerFallbackAddress = "http://168.63.129.16:32526"
2023
)
2124

25+
// test seam
26+
var withRetriesFn = requesthelper.WithRetries
27+
2228
type ResponseData struct {
2329
VMSettings *VMImmediateExtensionsGoalState
2430
ETag string
@@ -50,7 +56,7 @@ func (c *HostGACommunicator) GetImmediateVMSettings(ctx *log.Context, eTag strin
5056
return nil, handlersettings.InternalWrapErrorWithClarification(err, "could not create the request manager to get immediate VMsettings")
5157
}
5258

53-
resp, err := requesthelper.WithRetries(ctx, requestManager, requesthelper.ActualSleep, eTag)
59+
resp, err := withRetriesFn(ctx, requestManager, requesthelper.ActualSleep, eTag)
5460
if err != nil {
5561
return nil, handlersettings.InternalWrapErrorWithClarification(err, "request to retrieve VMSettings failed with retries.")
5662
}

internal/hostgacommunicator/hostgacommunicator_test.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
package hostgacommunicator
22

33
import (
4+
"bytes"
5+
"io"
6+
"net/http"
47
"os"
58
"testing"
69

10+
"github.com/Azure/run-command-handler-linux/internal/constants"
11+
"github.com/Azure/run-command-handler-linux/internal/requesthelper"
712
"github.com/go-kit/kit/log"
13+
"github.com/pkg/errors"
814
"github.com/stretchr/testify/require"
915
)
1016

@@ -16,3 +22,159 @@ func Test_GetOperationUri(t *testing.T) {
1622
require.NotNil(t, uri)
1723
require.Contains(t, uri, operationName)
1824
}
25+
26+
type fakeVMSettingsRequestManager struct {
27+
rm *requesthelper.RequestManager
28+
err error
29+
}
30+
31+
func (f fakeVMSettingsRequestManager) GetVMSettingsRequestManager(ctx *log.Context) (*requesthelper.RequestManager, error) {
32+
return f.rm, f.err
33+
}
34+
35+
func TestGetImmediateVMSettings_RequestManagerError(t *testing.T) {
36+
orig := withRetriesFn
37+
t.Cleanup(func() { withRetriesFn = orig })
38+
39+
// withRetries should never be called in this branch
40+
withRetriesFn = func(_ *log.Context, _ *requesthelper.RequestManager, _ requesthelper.SleepFunc, _ string) (*http.Response, error) {
41+
t.Fatalf("withRetriesFn should not have been called")
42+
return nil, nil
43+
}
44+
45+
rmErr := errors.New("the chipmunks have new management")
46+
c := NewHostGACommunicator(fakeVMSettingsRequestManager{rm: nil, err: rmErr})
47+
48+
_, err := c.GetImmediateVMSettings(nil, "etag0")
49+
VerifyErrorClarification(t, constants.Internal_UnknownError, err)
50+
}
51+
52+
func TestGetImmediateVMSettings_WithRetriesError_WrappedWithClarification(t *testing.T) {
53+
orig := withRetriesFn
54+
t.Cleanup(func() { withRetriesFn = orig })
55+
56+
withRetriesFn = func(_ *log.Context, _ *requesthelper.RequestManager, _ requesthelper.SleepFunc, _ string) (*http.Response, error) {
57+
return nil, errors.New("network fail")
58+
}
59+
60+
c := NewHostGACommunicator(fakeVMSettingsRequestManager{rm: &requesthelper.RequestManager{}, err: nil})
61+
62+
_, err := c.GetImmediateVMSettings(nil, "etag0")
63+
VerifyErrorClarification(t, constants.Internal_UnknownError, err)
64+
}
65+
66+
func TestGetImmediateVMSettings_NotModified304_ReturnsUnmodifiedResponse(t *testing.T) {
67+
orig := withRetriesFn
68+
t.Cleanup(func() { withRetriesFn = orig })
69+
70+
withRetriesFn = func(_ *log.Context, _ *requesthelper.RequestManager, _ requesthelper.SleepFunc, _ string) (*http.Response, error) {
71+
return &http.Response{
72+
StatusCode: http.StatusNotModified,
73+
Body: io.NopCloser(bytes.NewReader(nil)),
74+
Header: make(http.Header),
75+
}, nil
76+
}
77+
78+
c := NewHostGACommunicator(fakeVMSettingsRequestManager{rm: &requesthelper.RequestManager{}, err: nil})
79+
80+
resp, err := c.GetImmediateVMSettings(nil, "etag0")
81+
require.Nil(t, err, "unexpected err: %v", err)
82+
require.Nil(t, resp.VMSettings, "expected VMSettings nil")
83+
require.Equal(t, "etag0", resp.ETag, "expected ETag preserved, got %q", resp.ETag)
84+
require.False(t, resp.Modified, "expected Modified=false")
85+
}
86+
87+
func TestGetImmediateVMSettings_NotFound404_ReturnsUnmodifiedResponse(t *testing.T) {
88+
orig := withRetriesFn
89+
t.Cleanup(func() { withRetriesFn = orig })
90+
91+
withRetriesFn = func(_ *log.Context, _ *requesthelper.RequestManager, _ requesthelper.SleepFunc, _ string) (*http.Response, error) {
92+
return &http.Response{
93+
StatusCode: http.StatusNotFound,
94+
Body: io.NopCloser(bytes.NewReader(nil)),
95+
Header: make(http.Header),
96+
}, nil
97+
}
98+
99+
c := NewHostGACommunicator(fakeVMSettingsRequestManager{rm: &requesthelper.RequestManager{}, err: nil})
100+
101+
resp, err := c.GetImmediateVMSettings(nil, "etag0")
102+
require.Nil(t, err, "unexpected err: %v", err)
103+
require.Nil(t, resp.VMSettings, "expected VMSettings nil")
104+
require.Equal(t, "etag0", resp.ETag, "expected ETag preserved, got %q", resp.ETag)
105+
require.False(t, resp.Modified, "expected Modified=false")
106+
}
107+
108+
func TestGetImmediateVMSettings_BadJSON_ReturnsFailedToParseSettings(t *testing.T) {
109+
orig := withRetriesFn
110+
t.Cleanup(func() { withRetriesFn = orig })
111+
112+
withRetriesFn = func(_ *log.Context, _ *requesthelper.RequestManager, _ requesthelper.SleepFunc, _ string) (*http.Response, error) {
113+
h := make(http.Header)
114+
h.Set(constants.ETagHeaderName, "etag1") // still present, but parse should fail first
115+
return &http.Response{
116+
StatusCode: http.StatusOK,
117+
Body: io.NopCloser(bytes.NewReader([]byte("{not-json"))),
118+
Header: h,
119+
}, nil
120+
}
121+
122+
c := NewHostGACommunicator(fakeVMSettingsRequestManager{rm: &requesthelper.RequestManager{}, err: nil})
123+
124+
_, err := c.GetImmediateVMSettings(nil, "etag0")
125+
VerifyErrorClarification(t, constants.Hgap_FailedToParseImmediateSettings, err)
126+
}
127+
128+
func TestGetImmediateVMSettings_MissingETagHeader_ReturnsEtagNotFoundClarification(t *testing.T) {
129+
orig := withRetriesFn
130+
t.Cleanup(func() { withRetriesFn = orig })
131+
132+
withRetriesFn = func(_ *log.Context, _ *requesthelper.RequestManager, _ requesthelper.SleepFunc, _ string) (*http.Response, error) {
133+
// minimal valid JSON for VMImmediateExtensionsGoalState; if required fields exist, update accordingly.
134+
body := []byte(`{}`)
135+
return &http.Response{
136+
StatusCode: http.StatusOK,
137+
Body: io.NopCloser(bytes.NewReader(body)),
138+
Header: make(http.Header), // no ETag set
139+
}, nil
140+
}
141+
142+
c := NewHostGACommunicator(fakeVMSettingsRequestManager{rm: &requesthelper.RequestManager{}, err: nil})
143+
144+
_, err := c.GetImmediateVMSettings(nil, "etag0")
145+
VerifyErrorClarification(t, constants.Hgap_EtagNotFound, err)
146+
}
147+
148+
func TestGetImmediateVMSettings_Success_ModifiedFlagAndETagReturned(t *testing.T) {
149+
orig := withRetriesFn
150+
t.Cleanup(func() { withRetriesFn = orig })
151+
152+
withRetriesFn = func(_ *log.Context, _ *requesthelper.RequestManager, _ requesthelper.SleepFunc, _ string) (*http.Response, error) {
153+
h := make(http.Header)
154+
h.Set(constants.ETagHeaderName, "etag1")
155+
return &http.Response{
156+
StatusCode: http.StatusOK,
157+
Body: io.NopCloser(bytes.NewReader([]byte(`{}`))),
158+
Header: h,
159+
}, nil
160+
}
161+
162+
c := NewHostGACommunicator(fakeVMSettingsRequestManager{rm: &requesthelper.RequestManager{}, err: nil})
163+
164+
resp, err := c.GetImmediateVMSettings(nil, "etag0")
165+
require.Nil(t, err, "unexpected err: %v", err)
166+
require.NotNil(t, resp.VMSettings, "expected VMSettings non-nil")
167+
require.Equal(t, "etag1", resp.ETag, "expected etag1 preserved, got %q", resp.ETag)
168+
require.True(t, resp.Modified, "expected Modified=true when etag changes")
169+
}
170+
171+
func TestGetOperationUri_InvalidFallbackAddress(t *testing.T) {
172+
orig := WireServerFallbackAddress
173+
t.Cleanup(func() { WireServerFallbackAddress = orig })
174+
175+
// This should make url.Parse fail (unclosed IPv6 literal).
176+
WireServerFallbackAddress = "http://[::1:32526"
177+
178+
_, err := getOperationUri(nil, "/machine")
179+
VerifyErrorClarification(t, constants.Hgap_FailedToParseAddress, err)
180+
}

internal/hostgacommunicator/vmsettings.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ const (
2424
vmSettingsRequestTimeout = 30 * time.Second
2525
)
2626

27+
var (
28+
getHandlerEnvFn = handlersettings.GetHandlerEnv
29+
)
30+
2731
type VMImmediateExtensionsGoalState struct {
2832
ImmediateExtensionGoalStates []ImmediateExtensionGoalState `json:"immediateExtensionsGoalStates"`
2933
}
@@ -52,7 +56,7 @@ func GetVMSettingsRequestManager(ctx *log.Context) (*requesthelper.RequestManage
5256
func newVMSettingsRequestFactory(ctx *log.Context) (*requestFactory, error) {
5357
url, err := getOperationUri(ctx, vmSettingsOperation)
5458
if err != nil {
55-
return nil, errors.Wrapf(err, "failed to obtain VMSettingsURI")
59+
return nil, handlersettings.InternalWrapErrorWithClarification(err, "failed to obtain VMSettingsURI")
5660
}
5761

5862
return &requestFactory{url}, nil
@@ -73,7 +77,7 @@ func (u requestFactory) GetRequest(ctx *log.Context, eTag string) (*http.Request
7377
}
7478

7579
func (goalState *ImmediateExtensionGoalState) ValidateSignature() (bool, error) {
76-
he, err := handlersettings.GetHandlerEnv()
80+
he, err := getHandlerEnvFn()
7781
if err != nil {
7882
return false, err
7983
}

0 commit comments

Comments
 (0)