Files
dingtalk-workspace-cli/internal/app/upgrade_test.go
T

431 lines
11 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0
package app
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"strings"
"testing"
"github.com/spf13/cobra"
)
// --- ensureV ---
func TestEnsureV(t *testing.T) {
tests := []struct {
in string
want string
}{
{"1.0.6", "v1.0.6"},
{"v1.0.6", "v1.0.6"},
{"0.0.1", "v0.0.1"},
{"dev", "dev"},
{"unknown", "unknown"},
{"", "v0.0.0"},
{"v", "v"},
}
for _, tt := range tests {
got := ensureV(tt.in)
if got != tt.want {
t.Errorf("ensureV(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}
// --- parseChangelogEntries ---
func TestParseChangelogEntries(t *testing.T) {
body := `## Changelog
* abcdef1234567 - fix login bug
* 0123456789abc Merge branch 'main' into main
* fedcba9876543 - add upgrade command
* deadbeef12345 Merge pull request #42
* 1234567890abc - improve error handling
`
entries := parseChangelogEntries(body, 10)
if len(entries) != 3 {
t.Fatalf("len(entries) = %d, want 3 (merge commits should be filtered)", len(entries))
}
if entries[0] != "fix login bug" {
t.Errorf("entries[0] = %q, want %q", entries[0], "fix login bug")
}
if entries[1] != "add upgrade command" {
t.Errorf("entries[1] = %q, want %q", entries[1], "add upgrade command")
}
if entries[2] != "improve error handling" {
t.Errorf("entries[2] = %q, want %q", entries[2], "improve error handling")
}
}
func TestParseChangelogEntries_MaxLimit(t *testing.T) {
body := "* abc1234 - msg1\n* def5678 - msg2\n* ghi9012 - msg3\n"
entries := parseChangelogEntries(body, 2)
if len(entries) != 2 {
t.Errorf("len = %d, want 2 (should respect maxEntries)", len(entries))
}
}
func TestParseChangelogEntries_EmptyBody(t *testing.T) {
entries := parseChangelogEntries("", 10)
if len(entries) != 0 {
t.Errorf("len = %d, want 0 for empty body", len(entries))
}
}
func TestParseChangelogEntries_OnlyHeaders(t *testing.T) {
body := "## Changelog\n## Another heading\n"
entries := parseChangelogEntries(body, 10)
if len(entries) != 0 {
t.Errorf("len = %d, want 0 for headers-only body", len(entries))
}
}
func TestParseChangelogEntries_OnlyMergeCommits(t *testing.T) {
body := "* abc1234 Merge branch 'main'\n* def5678 Merge pull request #10\n"
entries := parseChangelogEntries(body, 10)
if len(entries) != 0 {
t.Errorf("len = %d, want 0 (all merge commits should be filtered)", len(entries))
}
}
func TestParseChangelogEntries_DashPrefixedLines(t *testing.T) {
body := "- fix bug\n- add feature\n"
entries := parseChangelogEntries(body, 10)
if len(entries) != 2 {
t.Fatalf("len = %d, want 2", len(entries))
}
if entries[0] != "fix bug" {
t.Errorf("entries[0] = %q, want %q", entries[0], "fix bug")
}
}
// --- stripCommitHash ---
func TestStripCommitHash(t *testing.T) {
tests := []struct {
in string
want string
}{
{"abcdef1234567 - fix bug", "fix bug"},
{"abcdef1234567 fix bug", "fix bug"},
{"short", "short"}, // too short to be a hash
{"abc123", "abc123"}, // less than 7 hex chars
{"no hash here", "no hash here"},
{"ABCDEF1234567 - upper case hash", "upper case hash"},
}
for _, tt := range tests {
got := stripCommitHash(tt.in)
if got != tt.want {
t.Errorf("stripCommitHash(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}
// --- isNoiseCommit ---
func TestIsNoiseCommit(t *testing.T) {
tests := []struct {
msg string
want bool
}{
{"Merge branch 'main'", true},
{"merge branch 'develop'", true},
{"Merge pull request #42", true},
{"Merge remote-tracking branch 'origin/main'", true},
{"fix login bug", false},
{"add new feature", false},
{"merge conflicts resolved", false}, // doesn't start with "merge branch"
}
for _, tt := range tests {
got := isNoiseCommit(tt.msg)
if got != tt.want {
t.Errorf("isNoiseCommit(%q) = %v, want %v", tt.msg, got, tt.want)
}
}
}
// --- truncateChangelog ---
func TestTruncateChangelog(t *testing.T) {
body := "## Changelog\n* abc1234 - fix A\n* def5678 - fix B\n* ghi9012 - fix C\n* jkl3456 - fix D\n"
result := truncateChangelog(body)
if result == "" {
t.Error("truncateChangelog returned empty")
}
// Should contain max 3 entries separated by "; "
parts := strings.Split(result, "; ")
if len(parts) > 3 {
t.Errorf("truncateChangelog should have at most 3 entries, got %d", len(parts))
}
}
func TestTruncateChangelog_EmptyBody(t *testing.T) {
if got := truncateChangelog(""); got != "" {
t.Errorf("truncateChangelog('') = %q, want empty", got)
}
}
// --- truncateChangelogForList ---
func TestTruncateChangelogForList(t *testing.T) {
tests := []struct {
body string
maxLen int
want string
}{
{"", 40, "-"},
{"## Changelog\n", 40, "-"},
}
for _, tt := range tests {
got := truncateChangelogForList(tt.body, tt.maxLen)
if got != tt.want {
t.Errorf("truncateChangelogForList(%q, %d) = %q, want %q", tt.body, tt.maxLen, got, tt.want)
}
}
}
func TestTruncateChangelogForList_Truncation(t *testing.T) {
body := "* abc1234 - a very long commit message that should be truncated eventually\n"
result := truncateChangelogForList(body, 20)
if len(result) > 20 {
t.Errorf("result len = %d, want <= 20", len(result))
}
if !strings.HasSuffix(result, "...") {
t.Errorf("truncated result should end with '...' , got %q", result)
}
}
// --- progressBar ---
func TestProgressBar(t *testing.T) {
tests := []struct {
percent float64
filled int
}{
{0, 0},
{50, 10},
{100, 20},
{150, 20}, // capped
}
for _, tt := range tests {
bar := progressBar(tt.percent)
if len(bar) != 20*len("█") && len(bar) != 20*len("░") {
// Since multi-byte chars, just check total rune count
runes := []rune(bar)
if len(runes) != 20 {
t.Errorf("progressBar(%v) rune count = %d, want 20", tt.percent, len(runes))
}
}
filledCount := strings.Count(bar, "█")
if filledCount != tt.filled {
t.Errorf("progressBar(%v) filled = %d, want %d", tt.percent, filledCount, tt.filled)
}
}
}
// --- shortenHome ---
func TestShortenHome(t *testing.T) {
// Non-home path should be unchanged
got := shortenHome("/tmp/somewhere")
if got != "/tmp/somewhere" {
t.Errorf("shortenHome(/tmp/somewhere) = %q", got)
}
}
// --- resolveUpgradeFormat ---
func TestResolveUpgradeFormat_Default(t *testing.T) {
root := &cobra.Command{}
root.PersistentFlags().String("format", "json", "output format")
child := &cobra.Command{}
root.AddCommand(child)
// format not changed => should default to "table" for upgrade
got := resolveUpgradeFormat(child)
if got != "table" {
t.Errorf("resolveUpgradeFormat(unchanged) = %q, want %q", got, "table")
}
}
func TestResolveUpgradeFormat_ExplicitJSON(t *testing.T) {
root := &cobra.Command{}
root.PersistentFlags().String("format", "json", "output format")
child := &cobra.Command{}
root.AddCommand(child)
// Simulate user explicitly setting format
root.PersistentFlags().Set("format", "json")
got := resolveUpgradeFormat(child)
if got != "json" {
t.Errorf("resolveUpgradeFormat(explicit json) = %q, want %q", got, "json")
}
}
func TestResolveUpgradeFormat_ExplicitTable(t *testing.T) {
root := &cobra.Command{}
root.PersistentFlags().String("format", "json", "output format")
child := &cobra.Command{}
root.AddCommand(child)
root.PersistentFlags().Set("format", "table")
got := resolveUpgradeFormat(child)
if got != "table" {
t.Errorf("resolveUpgradeFormat(explicit table) = %q, want %q", got, "table")
}
}
// --- writeJSON ---
func TestWriteJSON(t *testing.T) {
var buf bytes.Buffer
data := map[string]any{
"version": "v1.0.6",
"ok": true,
}
if err := writeJSON(&buf, data); err != nil {
t.Fatalf("writeJSON() error = %v", err)
}
output := buf.String()
if !strings.Contains(output, `"version": "v1.0.6"`) {
t.Errorf("output missing version: %s", output)
}
if !strings.Contains(output, `"ok": true`) {
t.Errorf("output missing ok: %s", output)
}
}
// --- strictVerifyFile ---
func TestStrictVerifyFile_MatchesChecksums(t *testing.T) {
dir := t.TempDir()
filePath := filepath.Join(dir, "test.tar.gz")
content := []byte("valid binary content")
os.WriteFile(filePath, content, 0644)
hash := computeTestSHA256(t, content)
checksums := hash + " test.tar.gz\n"
err := strictVerifyFile("[1/5]", filePath, "test.tar.gz", "", checksums)
if err != nil {
t.Errorf("expected success, got %v", err)
}
}
func TestStrictVerifyFile_ChecksumMismatch(t *testing.T) {
dir := t.TempDir()
filePath := filepath.Join(dir, "test.tar.gz")
os.WriteFile(filePath, []byte("tampered content"), 0644)
checksums := "0000000000000000000000000000000000000000000000000000000000000000 test.tar.gz\n"
err := strictVerifyFile("[1/5]", filePath, "test.tar.gz", "", checksums)
if err == nil {
t.Fatal("expected error for checksum mismatch")
}
if !strings.Contains(err.Error(), "校验失败") {
t.Errorf("error = %q, want to contain '校验失败'", err.Error())
}
}
func TestStrictVerifyFile_DigestMismatch(t *testing.T) {
dir := t.TempDir()
filePath := filepath.Join(dir, "test.tar.gz")
os.WriteFile(filePath, []byte("tampered"), 0644)
err := strictVerifyFile("[1/5]", filePath, "test.tar.gz",
"sha256:0000000000000000000000000000000000000000000000000000000000000000",
"")
if err == nil {
t.Fatal("expected error for digest mismatch")
}
}
func TestStrictVerifyFile_NoChecksumInfo(t *testing.T) {
dir := t.TempDir()
filePath := filepath.Join(dir, "test.tar.gz")
os.WriteFile(filePath, []byte("content"), 0644)
err := strictVerifyFile("[1/5]", filePath, "test.tar.gz", "", "")
if err != nil {
t.Errorf("no checksum info should skip, not error: %v", err)
}
}
func TestStrictVerifyFile_FileNotInChecksums_FallsToDigest(t *testing.T) {
dir := t.TempDir()
filePath := filepath.Join(dir, "skills.zip")
content := []byte("skills content")
os.WriteFile(filePath, content, 0644)
hash := computeTestSHA256(t, content)
// checksums.txt has entries but NOT skills.zip
checksums := "abcdef1234567890 other-file.tar.gz\n"
err := strictVerifyFile("[1/5]", filePath, "skills.zip", "sha256:"+hash, checksums)
if err != nil {
t.Errorf("should fall through to digest and succeed: %v", err)
}
}
func computeTestSHA256(t *testing.T, data []byte) string {
t.Helper()
h := sha256.Sum256(data)
return hex.EncodeToString(h[:])
}
// --- newUpgradeCommand ---
func TestNewUpgradeCommand_Flags(t *testing.T) {
cmd := newUpgradeCommand()
if cmd.Use != "upgrade" {
t.Errorf("Use = %q, want upgrade", cmd.Use)
}
expectedFlags := []string{"check", "list", "version", "rollback", "force", "skip-skills"}
for _, name := range expectedFlags {
if cmd.Flags().Lookup(name) == nil {
t.Errorf("missing flag: --%s", name)
}
}
}
func TestNewUpgradeCommand_NoArgs(t *testing.T) {
cmd := newUpgradeCommand()
// Simulate passing positional args - should error with cobra.NoArgs
cmd.SetArgs([]string{"rollback"})
err := cmd.Execute()
if err == nil {
t.Error("expected error for positional args (NoArgs)")
}
}
func TestNewUpgradeCommand_Help(t *testing.T) {
cmd := newUpgradeCommand()
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetArgs([]string{"--help"})
cmd.Execute()
help := buf.String()
if !strings.Contains(help, "upgrade") {
t.Error("help should contain 'upgrade'")
}
if !strings.Contains(help, "--check") {
t.Error("help should contain --check")
}
if !strings.Contains(help, "--rollback") {
t.Error("help should contain --rollback")
}
}