package command import ( "bytes" "context" "io" "log/slog" "net/http" "net/http/httptest" "testing" "connectrpc.com/connect" "github.com/stretchr/testify/require" "github.com/bytebase/bytebase/action/world" v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" "github.com/bytebase/bytebase/backend/generated-go/v1/v1connect" ) func TestCheckVersionCompatibility(t *testing.T) { t.Parallel() tests := []struct { name string cliVersion string serverVersion string wantError string }{ { name: "same release version", cliVersion: "3.14.0", serverVersion: "3.14.0", }, { name: "release version within two minor versions", cliVersion: "3.13.0", serverVersion: "3.15.0", }, { name: "release version outside two minor versions", cliVersion: "3.12.0", serverVersion: "3.15.0", wantError: "outside the compatibility window", }, { name: "newer release version is outside window", cliVersion: "3.17.0", serverVersion: "3.15.0", wantError: "outside the compatibility window", }, { name: "release version with different major is outside window", cliVersion: "2.15.0", serverVersion: "3.15.0", wantError: "outside the compatibility window", }, { name: "plain cloud action is not a dated cloud build", cliVersion: "cloud-20260604", serverVersion: "cloud", wantError: "unable to parse", }, { name: "plain cloud server is not a dated cloud build", cliVersion: "cloud", serverVersion: "cloud-20260604", wantError: "unable to parse", }, { name: "dated cloud builds match", cliVersion: "cloud-20260604", serverVersion: "cloud-20260604", }, { name: "dated cloud builds within seven days", cliVersion: "cloud-20260604", serverVersion: "cloud-20260611", }, { name: "newer dated cloud build is outside window", cliVersion: "cloud-20260611", serverVersion: "cloud-20260604", wantError: "outside the compatibility window", }, { name: "dated cloud builds outside seven days", cliVersion: "cloud-20260603", serverVersion: "cloud-20260611", wantError: "outside the compatibility window", }, { name: "cloud action is not compatible with release server", cliVersion: "cloud-20260604", serverVersion: "3.14.0", wantError: "unable to compare", }, { name: "invalid release version is parse error", cliVersion: "3", serverVersion: "3.14.0", wantError: "unable to parse", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() client := newActuatorInfoTestClient(t, tt.serverVersion) w := &world.World{Logger: slog.New(slog.NewTextHandler(io.Discard, nil))} err := checkVersionCompatibility(w, client, tt.cliVersion) if tt.wantError == "" { require.NoError(t, err) return } require.Error(t, err) require.Contains(t, err.Error(), tt.wantError) }) } } func TestRootTargetsHelp(t *testing.T) { t.Parallel() var output bytes.Buffer cmd := NewRootCommand(&world.World{}) cmd.SetArgs([]string{"--help"}) cmd.SetOut(&output) cmd.SetErr(&output) require.NoError(t, cmd.Execute()) require.Contains(t, output.String(), "workspace or project-instance databases") require.Contains(t, output.String(), "single project database group") } func TestRecommendedActionTag(t *testing.T) { t.Parallel() releaseVersion, err := parseVersion("3.14.0") require.NoError(t, err) require.Equal(t, "3.14.0", recommendedActionTag(releaseVersion)) cloudVersion, err := parseVersion("cloud-20260604") require.NoError(t, err) require.Equal(t, "cloud", recommendedActionTag(cloudVersion)) } func TestCheckVersionCompatibilityReturnsErrorOutsideWindow(t *testing.T) { t.Parallel() client := newActuatorInfoTestClient(t, "3.15.0") w := &world.World{Logger: slog.New(slog.NewTextHandler(io.Discard, nil))} err := checkVersionCompatibility(w, client, "3.12.0") require.Error(t, err) require.Contains(t, err.Error(), "outside the compatibility window") require.Contains(t, err.Error(), "same major version") require.Contains(t, err.Error(), "within 2 minor versions") } func TestCheckVersionCompatibilityReturnsUsefulCloudError(t *testing.T) { t.Parallel() client := newActuatorInfoTestClient(t, "cloud-20260604") w := &world.World{Logger: slog.New(slog.NewTextHandler(io.Discard, nil))} err := checkVersionCompatibility(w, client, "cloud-20260527") require.Error(t, err) require.Contains(t, err.Error(), "cloud-YYYYMMDD") require.Contains(t, err.Error(), "within 7 days") } func TestCheckVersionCompatibilityReturnsParseError(t *testing.T) { t.Parallel() client := newActuatorInfoTestClient(t, "cloud") w := &world.World{Logger: slog.New(slog.NewTextHandler(io.Discard, nil))} err := checkVersionCompatibility(w, client, "cloud-20260604") require.Error(t, err) require.Contains(t, err.Error(), "unable to parse") require.NotContains(t, err.Error(), "outside the compatibility window") } func TestCheckVersionCompatibilityAllowsVersionsWithinWindow(t *testing.T) { t.Parallel() client := newActuatorInfoTestClient(t, "cloud-20260604") w := &world.World{Logger: slog.New(slog.NewTextHandler(io.Discard, nil))} require.NoError(t, checkVersionCompatibility(w, client, "cloud-20260528")) } func newActuatorInfoTestClient(t *testing.T, version string) *client { t.Helper() path, handler := v1connect.NewActuatorServiceHandler(&actuatorInfoTestService{version: version}) mux := http.NewServeMux() mux.Handle(path, handler) server := httptest.NewServer(mux) t.Cleanup(server.Close) client, err := newClient(server.URL, "token", "", "", defaultClientOptions()) require.NoError(t, err) t.Cleanup(client.close) return client } type actuatorInfoTestService struct { version string } func (s *actuatorInfoTestService) GetActuatorInfo(context.Context, *connect.Request[v1pb.GetActuatorInfoRequest]) (*connect.Response[v1pb.ActuatorInfo], error) { return connect.NewResponse(&v1pb.ActuatorInfo{Version: s.version}), nil }