pulumi/pkg/backend/httpstate/client/client_test.go

362 lines
11 KiB
Go

// Copyright 2016-2021, Pulumi Corporation.
//
// 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 client
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newMockServer(statusCode int, message string) *httptest.Server {
return httptest.NewServer(
http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(statusCode)
_, err := rw.Write([]byte(message))
if err != nil {
return
}
}))
}
func newMockServerRequestProcessor(statusCode int, processor func(req *http.Request) string) *httptest.Server {
return httptest.NewServer(
http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(statusCode)
_, err := rw.Write([]byte(processor(req)))
if err != nil {
return
}
}))
}
func newMockClient(server *httptest.Server) *Client {
httpClient := http.DefaultClient
return &Client{
apiURL: server.URL,
apiToken: "",
apiUser: "",
diag: nil,
httpClient: httpClient,
restClient: &defaultRESTClient{
client: &defaultHTTPClient{
client: httpClient,
},
},
}
}
func TestAPIErrorResponses(t *testing.T) {
t.Parallel()
t.Run("TestAuthError", func(t *testing.T) {
t.Parallel()
// check 401 error is handled
unauthorizedServer := newMockServer(401, "401: Unauthorized")
defer unauthorizedServer.Close()
unauthorizedClient := newMockClient(unauthorizedServer)
_, _, _, unauthorizedErr := unauthorizedClient.GetCLIVersionInfo(context.Background())
assert.EqualError(t, unauthorizedErr, "this command requires logging in; try running `pulumi login` first")
})
t.Run("TestRateLimitError", func(t *testing.T) {
t.Parallel()
// test handling 429: Too Many Requests/rate-limit response
rateLimitedServer := newMockServer(429, "rate-limit error")
defer rateLimitedServer.Close()
rateLimitedClient := newMockClient(rateLimitedServer)
_, _, _, rateLimitErr := rateLimitedClient.GetCLIVersionInfo(context.Background())
assert.EqualError(t, rateLimitErr, "pulumi service: request rate-limit exceeded")
})
t.Run("TestDefaultError", func(t *testing.T) {
t.Parallel()
// test handling non-standard error message
defaultErrorServer := newMockServer(418, "I'm a teapot")
defer defaultErrorServer.Close()
defaultErrorClient := newMockClient(defaultErrorServer)
_, _, _, defaultErrorErr := defaultErrorClient.GetCLIVersionInfo(context.Background())
assert.Error(t, defaultErrorErr)
})
}
func TestAPIVersionResponses(t *testing.T) {
t.Parallel()
versionServer := newMockServer(
200,
`{"latestVersion": "1.0.0", "oldestWithoutWarning": "0.1.0", "latestDevVersion": "1.0.0-11-gdeadbeef"}`,
)
defer versionServer.Close()
versionClient := newMockClient(versionServer)
latestVersion, oldestWithoutWarning, latestDevVersion, err := versionClient.GetCLIVersionInfo(context.Background())
assert.NoError(t, err)
assert.Equal(t, latestVersion.String(), "1.0.0")
assert.Equal(t, oldestWithoutWarning.String(), "0.1.0")
assert.Equal(t, latestDevVersion.String(), "1.0.0-11-gdeadbeef")
}
func TestGzip(t *testing.T) {
t.Parallel()
// test handling non-standard error message
gzipCheckServer := newMockServerRequestProcessor(200, func(req *http.Request) string {
assert.Equal(t, req.Header.Get("Content-Encoding"), "gzip")
return "{}"
})
defer gzipCheckServer.Close()
client := newMockClient(gzipCheckServer)
identifier := StackIdentifier{
Stack: tokens.MustParseStackName("stack"),
}
// POST /import
_, err := client.ImportStackDeployment(context.Background(), identifier, nil)
assert.NoError(t, err)
tok := updateTokenStaticSource("")
// PATCH /checkpoint
err = client.PatchUpdateCheckpoint(context.Background(), UpdateIdentifier{
StackIdentifier: identifier,
}, nil, tok)
assert.NoError(t, err)
// POST /events/batch
err = client.RecordEngineEvents(context.Background(), UpdateIdentifier{
StackIdentifier: identifier,
}, apitype.EngineEventBatch{}, tok)
assert.NoError(t, err)
// POST /events/batch
_, err = client.BulkDecryptValue(context.Background(), identifier, nil)
assert.NoError(t, err)
}
func TestPatchUpdateCheckpointVerbatimIndents(t *testing.T) {
t.Parallel()
deployment := apitype.DeploymentV3{
Resources: []apitype.ResourceV3{
{URN: resource.URN("urn1")},
{URN: resource.URN("urn2")},
},
}
var serializedDeployment json.RawMessage
serializedDeployment, err := json.Marshal(deployment)
assert.NoError(t, err)
untypedDeployment, err := json.Marshal(apitype.UntypedDeployment{
Version: 3,
Deployment: serializedDeployment,
})
assert.NoError(t, err)
var request apitype.PatchUpdateVerbatimCheckpointRequest
server := newMockServerRequestProcessor(200, func(req *http.Request) string {
reader, err := gzip.NewReader(req.Body)
assert.NoError(t, err)
defer reader.Close()
err = json.NewDecoder(reader).Decode(&request)
assert.NoError(t, err)
return "{}"
})
client := newMockClient(server)
sequenceNumber := 1
indented, err := marshalDeployment(&deployment)
require.NoError(t, err)
newlines := bytes.Count(indented, []byte{'\n'})
err = client.PatchUpdateCheckpointVerbatim(context.Background(),
UpdateIdentifier{
StackIdentifier: StackIdentifier{
Stack: tokens.MustParseStackName("stack"),
},
}, sequenceNumber, indented, updateTokenStaticSource("token"))
assert.NoError(t, err)
compacted := func(raw json.RawMessage) string {
var buf bytes.Buffer
err := json.Compact(&buf, []byte(raw))
assert.NoError(t, err)
return buf.String()
}
// It should have more than one line as json.Marshal would produce.
assert.Equal(t, newlines+1, len(strings.Split(string(request.UntypedDeployment), "\n")))
// Compacting should recover the same form as json.Marshal would produce.
assert.Equal(t, string(untypedDeployment), compacted(request.UntypedDeployment))
}
func TestGetCapabilities(t *testing.T) {
t.Parallel()
t.Run("legacy-service-404", func(t *testing.T) {
t.Parallel()
s := newMockServer(404, "NOT FOUND")
defer s.Close()
c := newMockClient(s)
resp, err := c.GetCapabilities(context.Background())
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Empty(t, resp.Capabilities)
})
t.Run("updated-service-with-delta-checkpoint-capability", func(t *testing.T) {
t.Parallel()
cfg := apitype.DeltaCheckpointUploadsConfigV1{
CheckpointCutoffSizeBytes: 1024 * 1024 * 4,
}
cfgJSON, err := json.Marshal(cfg)
require.NoError(t, err)
actualResp := apitype.CapabilitiesResponse{
Capabilities: []apitype.APICapabilityConfig{{
Version: 3,
Capability: apitype.DeltaCheckpointUploads,
Configuration: json.RawMessage(cfgJSON),
}},
}
respJSON, err := json.Marshal(actualResp)
require.NoError(t, err)
s := newMockServer(200, string(respJSON))
defer s.Close()
c := newMockClient(s)
resp, err := c.GetCapabilities(context.Background())
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Len(t, resp.Capabilities, 1)
assert.Equal(t, apitype.DeltaCheckpointUploads, resp.Capabilities[0].Capability)
assert.Equal(t, `{"checkpointCutoffSizeBytes":4194304}`,
string(resp.Capabilities[0].Configuration))
})
}
func TestDeploymentSettingsApi(t *testing.T) {
t.Parallel()
t.Run("get-stack-deployment-settings", func(t *testing.T) {
t.Parallel()
payload := `{
"sourceContext": {
"git": {
"repoUrl": "git@github.com:pulumi/test-repo.git",
"branch": "main",
"repoDir": ".",
"gitAuth": {
"basicAuth": {
"userName": "jdoe",
"password": {
"secret": "[secret]",
"ciphertext": "AAABAMcGtHDraogfM3Qk4WyaNp3F/syk2cjHPQTb6Hu6ps8="
}
}
}
}
},
"operationContext": {
"oidc": {
"aws": {
"duration": "1h0m0s",
"policyArns": [
"policy:arn"
],
"roleArn": "the_role",
"sessionName": "the_session_name"
}
},
"options": {
"skipIntermediateDeployments": true
}
},
"agentPoolID": "51035bee-a4d6-4b63-9ff6-418775c5da8d"
}`
s := newMockServerRequestProcessor(200, func(req *http.Request) string {
assert.Equal(t, req.RequestURI, "/api/stacks/owner/project/stack/deployments/settings")
return payload
})
defer s.Close()
c := newMockClient(s)
stack, _ := tokens.ParseStackName("stack")
resp, err := c.GetStackDeploymentSettings(context.Background(), StackIdentifier{
Owner: "owner",
Project: "project",
Stack: stack,
})
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.NotNil(t, resp.SourceContext)
assert.NotNil(t, resp.SourceContext.Git)
assert.Equal(t, "main", resp.SourceContext.Git.Branch)
assert.Equal(t, "git@github.com:pulumi/test-repo.git", resp.SourceContext.Git.RepoURL)
assert.Equal(t, ".", resp.SourceContext.Git.RepoDir)
assert.NotNil(t, resp.SourceContext.Git.GitAuth)
assert.NotNil(t, resp.SourceContext.Git.GitAuth.BasicAuth)
assert.NotNil(t, resp.SourceContext.Git.GitAuth.BasicAuth.UserName)
assert.Equal(t, "jdoe", resp.SourceContext.Git.GitAuth.BasicAuth.UserName.Value)
assert.NotNil(t, resp.SourceContext.Git.GitAuth.BasicAuth.Password)
assert.Equal(t, "AAABAMcGtHDraogfM3Qk4WyaNp3F/syk2cjHPQTb6Hu6ps8=",
resp.SourceContext.Git.GitAuth.BasicAuth.Password.Ciphertext)
assert.NotNil(t, resp.Operation)
assert.NotNil(t, resp.Operation.Options)
assert.True(t, resp.Operation.Options.SkipIntermediateDeployments)
assert.False(t, resp.Operation.Options.DeleteAfterDestroy)
assert.False(t, resp.Operation.Options.RemediateIfDriftDetected)
assert.False(t, resp.Operation.Options.SkipInstallDependencies)
assert.NotNil(t, resp.Operation.OIDC)
assert.Nil(t, resp.Operation.OIDC.Azure)
assert.Nil(t, resp.Operation.OIDC.GCP)
assert.NotNil(t, resp.Operation.OIDC.AWS)
assert.Equal(t, "the_session_name", resp.Operation.OIDC.AWS.SessionName)
assert.Equal(t, "the_role", resp.Operation.OIDC.AWS.RoleARN)
duration, _ := time.ParseDuration("1h0m0s")
assert.Equal(t, apitype.DeploymentDuration(duration), resp.Operation.OIDC.AWS.Duration)
assert.Equal(t, []string{"policy:arn"}, resp.Operation.OIDC.AWS.PolicyARNs)
assert.Equal(t, "51035bee-a4d6-4b63-9ff6-418775c5da8d", *resp.AgentPoolID)
})
}