Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions integration-tests/autoscaling_enabled_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ func TestAutoscalingSettingsSetEnabled(t *testing.T) {
// setUpAutoscaling configures a project with an "app" and the given services'
// autoscaling settings.
func setUpAutoscaling(apiHandler *mockapi.Handler, services map[string]any) (projectID string) {
trigger := map[string]any{
"enabled": false,
"up": map[string]any{"threshold": 80, "duration": 60},
"down": map[string]any{"threshold": 20, "duration": 600},
}
return setUpAutoscalingWithDefaults(apiHandler, map[string]any{
"triggers": map[string]any{"cpu": trigger, "memory": trigger},
"scale_cooldown": map[string]any{"up": 300, "down": 300},
"instances": map[string]any{"min": 1, "max": 10},
}, services)
}

// setUpAutoscalingWithDefaults is setUpAutoscaling with the given autoscaling defaults.
func setUpAutoscalingWithDefaults(apiHandler *mockapi.Handler, defaults, services map[string]any) (projectID string) {
projectID = mockapi.ProjectID()
apiHandler.SetProjects([]*mockapi.Project{{
ID: projectID,
Expand Down Expand Up @@ -117,19 +131,7 @@ func setUpAutoscaling(apiHandler *mockapi.Handler, services map[string]any) (pro
})
})

trigger := map[string]any{
"enabled": false,
"up": map[string]any{"threshold": 80, "duration": 60},
"down": map[string]any{"threshold": 20, "duration": 600},
}
main.SetAutoscalingSettings(map[string]any{
"defaults": map[string]any{
"triggers": map[string]any{"cpu": trigger, "memory": trigger},
"scale_cooldown": map[string]any{"up": 300, "down": 300},
"instances": map[string]any{"min": 1, "max": 10},
},
"services": services,
})
main.SetAutoscalingSettings(map[string]any{"defaults": defaults, "services": services})

return projectID
}
107 changes: 107 additions & 0 deletions integration-tests/autoscaling_get_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package tests

import (
"net/http/httptest"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/upsun/cli/pkg/mockapi"
)

// TestAutoscalingSettingsGetDirections checks that each direction of a trigger shows whether the trigger is enabled.
func TestAutoscalingSettingsGetDirections(t *testing.T) {
t.Parallel()
apiHandler := mockapi.NewHandler(t)
projectID := setUpAutoscaling(apiHandler, map[string]any{
"app": map[string]any{
"triggers": map[string]any{
"cpu": map[string]any{
"enabled": true,
"up": map[string]any{"threshold": 80, "duration": 60},
"down": map[string]any{"threshold": 20, "duration": 300},
},
// The memory trigger doesn't say whether it is enabled.
"memory": map[string]any{"up": map[string]any{"threshold": 70, "duration": 60}},
},
"instances": map[string]any{"min": 1, "max": 3},
"scale_cooldown": map[string]any{"up": 300, "down": 600},
},
})
authServer := mockapi.NewAuthServer(t)
defer authServer.Close()
apiServer := httptest.NewServer(apiHandler)
defer apiServer.Close()
f := newCommandFactory(t, apiServer.URL, authServer.URL)

stdout, stderr, err := f.RunCombinedOutput("autoscaling:get", "-p", projectID, "-e", "main", "--format", "csv",
"--columns", "service,metric,direction,threshold,duration,enabled,cooldown,min_instances,max_instances")
require.NoError(t, err, stderr)
assertTrimmed(t, `
App or service,Metric,Direction,Threshold (%),Duration (s),Enabled,Cooldown (s),Minimum instances,Maximum instances
app,cpu,up,80.0%,60,true,300,1,3
app,cpu,down,20.0%,300,true,600,1,3
app,memory,up,70.0%,60,,300,1,3
`, stdout)
}

// TestAutoscalingSettingsSetPartial checks changes to settings that omit values, e.g. after a partial update.
func TestAutoscalingSettingsSetPartial(t *testing.T) {
t.Parallel()
apiHandler := mockapi.NewHandler(t)
projectID := setUpAutoscaling(apiHandler, map[string]any{
"app": map[string]any{
"triggers": map[string]any{
"cpu": map[string]any{"enabled": true, "up": map[string]any{"threshold": 80}},
},
"instances": map[string]any{"max": 3},
"scale_cooldown": map[string]any{"down": 600},
},
})
authServer := mockapi.NewAuthServer(t)
defer authServer.Close()
apiServer := httptest.NewServer(apiHandler)
defer apiServer.Close()
f := newCommandFactory(t, apiServer.URL, authServer.URL)

// Verbose output shows PHP warnings.
_, stderr, err := f.RunCombinedOutput("autoscaling:set", "-v", "-p", projectID, "-e", "main",
"--service", "app", "--metric", "cpu", "--duration-up", "2m", "--cooldown-up", "5m", "--instances-min", "2",
"--dry-run")
require.NoError(t, err, stderr)
assert.NotContains(t, stderr, "Warning")
assert.Contains(t, stderr, "Duration: 2m")
assert.Contains(t, stderr, "Cooldown: 5m")
assert.Contains(t, stderr, "Min: 2")
}

// TestAutoscalingSettingsSetInteractivePartialDefaults checks the interactive form when the defaults omit a value.
func TestAutoscalingSettingsSetInteractivePartialDefaults(t *testing.T) {
t.Parallel()
apiHandler := mockapi.NewHandler(t)
projectID := setUpAutoscalingWithDefaults(apiHandler, map[string]any{
"triggers": map[string]any{"cpu": map[string]any{
"up": map[string]any{"threshold": 80, "duration": 60},
"down": map[string]any{"threshold": 20, "duration": 600},
}},
"instances": map[string]any{"min": 1},
}, map[string]any{})
authServer := mockapi.NewAuthServer(t)
defer authServer.Close()
apiServer := httptest.NewServer(apiHandler)
defer apiServer.Close()
f := newCommandFactory(t, apiServer.URL, authServer.URL)

// Accept the defaults for the metric, and for each scaling direction's threshold and duration
// (the only app is chosen without a question). The cooldowns and the maximum instance count
// have no default: an empty answer is rejected and the question is asked again.
_, stderr, err := f.RunInteractive("\n\n\n\n5m\n\n\n\n5m\n\n\n3\n",
"autoscaling:set", "-p", projectID, "-e", "main", "--dry-run")
require.NoError(t, err, stderr)
assert.NotContains(t, stderr, "TypeError")
assert.Equal(t, 3, strings.Count(stderr, "A value is required."))
assert.Contains(t, stderr, "Cooldown: 5m")
assert.Contains(t, stderr, "Max: 3")
}
33 changes: 29 additions & 4 deletions integration-tests/resources_build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ import (

// setUpBuildResources serves a project with build resources of 1 CPU and 2048 MB, and maximums of 4 CPU and 8192 MB.
func setUpBuildResources(t *testing.T) (apiHandler *mockapi.Handler, f *cmdFactory, projectID string) {
return setUpBuildResourcesWith(t, map[string]any{"cpu": 1, "memory": 2048})
}

// setUpBuildResourcesWith is setUpBuildResources with the given build resources settings.
func setUpBuildResourcesWith(t *testing.T, buildResources map[string]any) (
apiHandler *mockapi.Handler, f *cmdFactory, projectID string,
) {
apiHandler = mockapi.NewHandler(t)
projectID = mockapi.ProjectID()
apiHandler.SetProjects([]*mockapi.Project{{
Expand All @@ -24,7 +31,7 @@ func setUpBuildResources(t *testing.T) (apiHandler *mockapi.Handler, f *cmdFacto
DefaultBranch: "main",
Settings: map[string]any{
"sizing_api_enabled": true,
"build_resources": map[string]any{"cpu": 1, "memory": 2048},
"build_resources": buildResources,
},
}})
apiHandler.SetEnvironments([]*mockapi.Environment{makeEnv(projectID, "main", "production", "active", nil)})
Expand Down Expand Up @@ -108,10 +115,28 @@ func TestBuildResourcesSet(t *testing.T) {
assert.Contains(t, stderr, c.wantErr)
}

_, stderr, err := f.RunCombinedOutput("resources:build:set", "-p", projectID, "--memory", "2048")
require.NoError(t, err, stderr)
assert.Contains(t, stderr, "No changes were provided: nothing to update.")
// The current values are the API's integers.
for _, args := range [][]string{{"--memory", "2048"}, {"--cpu", "1"}} {
_, stderr, err := f.RunCombinedOutput(append([]string{"resources:build:set", "-p", projectID}, args...)...)
require.NoError(t, err, stderr)
assert.Contains(t, stderr, "No changes were provided: nothing to update.")
}

assert.Empty(t, apiHandler.ProjectSettingsPatches(projectID))
})
}

// TestBuildResourcesPartial checks the build resources commands when the settings omit a value.
func TestBuildResourcesPartial(t *testing.T) {
t.Parallel()
apiHandler, f, projectID := setUpBuildResourcesWith(t, map[string]any{"cpu": 1})

assertTrimmed(t, "CPU\tMemory (MB)\n1\t", f.Run("resources:build:get", "-p", projectID, "--format", "tsv"))

_, stderr, err := f.RunCombinedOutput("resources:build:set", "-p", projectID, "--memory", "1024")
require.NoError(t, err, stderr)
assert.Contains(t, stderr, "Memory: 1024 MB")
patches := apiHandler.ProjectSettingsPatches(projectID)
require.Len(t, patches, 1)
assert.Equal(t, map[string]any{"build_resources": map[string]any{"memory": 1024.0}}, patches[0])
}
Loading
Loading