• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

UiPath / uipathcli / 22833181054

09 Mar 2026 12:15AM UTC coverage: 90.831% (-0.08%) from 90.914%
22833181054

Pull #212

github

Chibi Vikram
Add direct Execute tests for defensive parameter validation

Test parameter type assertion failure and empty value checks by calling
Execute directly, bypassing CLI framework validation. Covers
getStringParameter false branch and source/solutionId empty checks in
unpack, push, pull, and publish commands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pull Request #212: Add studio solution commands, skill, and agents.md

606 of 674 new or added lines in 16 files covered. (89.91%)

3 existing lines in 1 file now uncovered.

7380 of 8125 relevant lines covered (90.83%)

1.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.3
/plugin/studio/solution/unpack/solution_unpack_command.go
1
// Package unpack implements the command plugin for extracting a .uis file
2
// (ZIP archive) into a solution directory.
3
package unpack
4

5
import (
6
        "archive/zip"
7
        "bytes"
8
        "encoding/json"
9
        "errors"
10
        "fmt"
11
        "io"
12
        "net/http"
13
        "os"
14
        "path/filepath"
15
        "strings"
16

17
        "github.com/UiPath/uipathcli/log"
18
        "github.com/UiPath/uipathcli/output"
19
        "github.com/UiPath/uipathcli/plugin"
20
)
21

22
// The SolutionUnpackCommand extracts a .uis file into a directory.
23
type SolutionUnpackCommand struct{}
24

25
func (c SolutionUnpackCommand) Command() plugin.Command {
1✔
26
        return *plugin.NewCommand("studio").
1✔
27
                WithCategory("solution", "UiPath Solution management", "Pack, unpack, push and pull UiPath Maestro solutions.").
1✔
28
                WithOperation("unpack", "Unpack Solution", "Extracts a .uis file into a solution directory").
1✔
29
                WithParameter(plugin.NewParameter("source", plugin.ParameterTypeString, "Path to .uis file").
1✔
30
                        WithRequired(true)).
1✔
31
                WithParameter(plugin.NewParameter("destination", plugin.ParameterTypeString, "Output directory path").
1✔
32
                        WithDefaultValue(""))
1✔
33
}
1✔
34

35
func (c SolutionUnpackCommand) Execute(ctx plugin.ExecutionContext, writer output.OutputWriter, logger log.Logger) error {
1✔
36
        source := c.getStringParameter("source", "", ctx.Parameters)
1✔
37
        if source == "" {
2✔
38
                return errors.New("Source .uis file is required")
1✔
39
        }
1✔
40
        source, _ = filepath.Abs(source)
1✔
41
        destination := c.getStringParameter("destination", "", ctx.Parameters)
1✔
42

1✔
43
        if _, err := os.Stat(source); err != nil {
2✔
44
                return fmt.Errorf("File not found: %s", source)
1✔
45
        }
1✔
46

47
        if destination == "" {
2✔
48
                basename := filepath.Base(source)
1✔
49
                destination = strings.TrimSuffix(basename, filepath.Ext(basename))
1✔
50
        }
1✔
51
        destination, _ = filepath.Abs(destination)
1✔
52

1✔
53
        params := newSolutionUnpackParams(source, destination)
1✔
54
        result, err := c.unpack(*params)
1✔
55
        if err != nil {
2✔
56
                return err
1✔
57
        }
1✔
58

59
        jsonData, err := json.Marshal(result)
1✔
60
        if err != nil {
1✔
NEW
61
                return fmt.Errorf("Unpack command failed: %w", err)
×
NEW
62
        }
×
63
        return writer.WriteResponse(*output.NewResponseInfo(http.StatusOK, "200 OK", "HTTP/1.1", map[string][]string{}, bytes.NewReader(jsonData)))
1✔
64
}
65

66
func (c SolutionUnpackCommand) unpack(params solutionUnpackParams) (*solutionUnpackResult, error) {
1✔
67
        reader, err := zip.OpenReader(params.Source)
1✔
68
        if err != nil {
2✔
69
                return nil, fmt.Errorf("Cannot open .uis file: %w", err)
1✔
70
        }
1✔
71
        defer func() { _ = reader.Close() }()
2✔
72

73
        for _, file := range reader.File {
2✔
74
                err := c.extractFile(file, params.Destination)
1✔
75
                if err != nil {
2✔
76
                        return nil, err
1✔
77
                }
1✔
NEW
78
        }
×
NEW
79

×
80
        solutionId, projectCount := c.readSolutionInfo(filepath.Join(params.Destination, "SolutionStorage.json"))
1✔
81

1✔
82
        return newSucceededSolutionUnpackResult(params.Destination, solutionId, projectCount), nil
1✔
NEW
83
}
×
NEW
84

×
NEW
85
const maxArchiveFileSize = 1 * 1024 * 1024 * 1024
×
NEW
86

×
87
func (c SolutionUnpackCommand) extractFile(file *zip.File, destination string) error {
1✔
88
        destPath, err := c.sanitizeArchivePath(destination, file.Name)
1✔
89
        if err != nil {
2✔
90
                return err
1✔
91
        }
1✔
NEW
92

×
93
        if file.FileInfo().IsDir() {
2✔
94
                return os.MkdirAll(destPath, 0750)
1✔
95
        }
1✔
NEW
96

×
97
        err = os.MkdirAll(filepath.Dir(destPath), 0750)
1✔
98
        if err != nil {
1✔
NEW
99
                return fmt.Errorf("Cannot create directory for '%s': %w", destPath, err)
×
NEW
100
        }
×
NEW
101

×
102
        rc, err := file.Open()
1✔
103
        if err != nil {
1✔
NEW
104
                return fmt.Errorf("Cannot read archive entry '%s': %w", file.Name, err)
×
NEW
105
        }
×
106
        defer func() { _ = rc.Close() }()
2✔
NEW
107

×
108
        outFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
1✔
109
        if err != nil {
1✔
NEW
110
                return fmt.Errorf("Cannot create file '%s': %w", destPath, err)
×
NEW
111
        }
×
112
        defer func() { _ = outFile.Close() }()
2✔
113

114
        _, err = io.CopyN(outFile, rc, maxArchiveFileSize)
1✔
115
        if err != nil && !errors.Is(err, io.EOF) {
1✔
NEW
116
                return fmt.Errorf("Error extracting '%s': %w", file.Name, err)
×
NEW
117
        }
×
118
        return nil
1✔
119
}
NEW
120

×
121
func (c SolutionUnpackCommand) sanitizeArchivePath(directory string, name string) (string, error) {
1✔
122
        result := filepath.Join(directory, name)
1✔
123
        if strings.HasPrefix(result, filepath.Clean(directory)) {
2✔
124
                return result, nil
1✔
125
        }
1✔
126
        return "", fmt.Errorf("File path '%s' is not allowed", name)
1✔
NEW
127
}
×
NEW
128

×
129
func (c SolutionUnpackCommand) readSolutionInfo(path string) (string, int) {
1✔
130
        data, err := os.ReadFile(path)
1✔
131
        if err != nil {
2✔
132
                return "", 0
1✔
133
        }
1✔
134
        var storage struct {
1✔
135
                SolutionId string `json:"SolutionId"`
1✔
136
                Projects   []struct {
1✔
137
                        ProjectId string `json:"ProjectId"`
1✔
138
                } `json:"Projects"`
1✔
139
        }
1✔
140
        err = json.Unmarshal(data, &storage)
1✔
141
        if err != nil {
2✔
142
                return "", 0
1✔
143
        }
1✔
144
        return storage.SolutionId, len(storage.Projects)
1✔
145
}
146

147
func (c SolutionUnpackCommand) getStringParameter(name string, defaultValue string, parameters []plugin.ExecutionParameter) string {
1✔
148
        result := defaultValue
1✔
149
        for _, p := range parameters {
2✔
150
                if p.Name == name {
2✔
151
                        if data, ok := p.Value.(string); ok {
2✔
152
                                result = data
1✔
153
                                break
1✔
154
                        }
155
                }
156
        }
157
        return result
1✔
158
}
159

160
func NewSolutionUnpackCommand() *SolutionUnpackCommand {
1✔
161
        return &SolutionUnpackCommand{}
1✔
162
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc