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

UiPath / uipathcli / 22833036745

09 Mar 2026 12:08AM UTC coverage: 90.732% (-0.2%) from 90.914%
22833036745

Pull #212

github

Chibi Vikram
Fix TestWriteMultipartFormWithClosedWriter to use failing writer

Use a writer that errors on Write() instead of a closed multipart writer,
since multipart.Writer.CreatePart doesn't check closed state.

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

598 of 674 new or added lines in 16 files covered. (88.72%)

3 existing lines in 1 file now uncovered.

7372 of 8125 relevant lines covered (90.73%)

1.02 hits per line

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

94.74
/utils/api/studio_client.go
1
package api
2

3
import (
4
        "bytes"
5
        "context"
6
        "encoding/json"
7
        "fmt"
8
        "io"
9
        "mime/multipart"
10
        "net/http"
11
        "net/textproto"
12
        "net/url"
13

14
        "github.com/UiPath/uipathcli/auth"
15
        "github.com/UiPath/uipathcli/log"
16
        "github.com/UiPath/uipathcli/plugin"
17
        "github.com/UiPath/uipathcli/utils/converter"
18
        "github.com/UiPath/uipathcli/utils/network"
19
        "github.com/UiPath/uipathcli/utils/stream"
20
        "github.com/UiPath/uipathcli/utils/visualization"
21
)
22

23
// StudioClient is an HTTP client for the Studio Web backend API.
24
type StudioClient struct {
25
        baseUri      url.URL
26
        organization string
27
        token        *auth.AuthToken
28
        debug        bool
29
        settings     plugin.ExecutionSettings
30
        logger       log.Logger
31
}
32

33
// PushSolution uploads a .uis file to Studio Web.
34
func (c StudioClient) PushSolution(file stream.Stream, solutionId string, uploadBar *visualization.ProgressBar) (*PushSolutionResponse, error) {
1✔
35
        ctx, cancel := context.WithCancelCause(context.Background())
1✔
36
        request := c.createPushSolutionRequest(file, solutionId, uploadBar, cancel)
1✔
37
        client := network.NewHttpClient(c.logger, c.httpClientSettings())
1✔
38
        response, err := client.SendWithContext(request, ctx)
1✔
39
        if err != nil {
2✔
40
                return nil, err
1✔
41
        }
1✔
42
        defer func() { _ = response.Body.Close() }()
2✔
43
        body, err := io.ReadAll(response.Body)
1✔
44
        if err != nil {
1✔
NEW
45
                return nil, fmt.Errorf("Error reading response: %w", err)
×
NEW
46
        }
×
47
        if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusCreated {
2✔
48
                return nil, fmt.Errorf("Studio Web returned status code '%v' and body '%v'", response.StatusCode, string(body))
1✔
49
        }
1✔
50
        var result PushSolutionResponse
1✔
51
        err = json.Unmarshal(body, &result)
1✔
52
        if err != nil {
2✔
53
                return &PushSolutionResponse{}, nil
1✔
54
        }
1✔
55
        return &result, nil
1✔
56
}
57

58
func (c StudioClient) createPushSolutionRequest(file stream.Stream, solutionId string, uploadBar *visualization.ProgressBar, cancel context.CancelCauseFunc) *network.HttpRequest {
1✔
59
        bodyReader, bodyWriter := io.Pipe()
1✔
60
        streamSize, _ := file.Size()
1✔
61
        contentType := c.writeMultipartBody(bodyWriter, file, "application/octet-stream", cancel)
1✔
62
        uploadReader := c.progressReader("uploading...", "completing  ", bodyReader, streamSize, uploadBar)
1✔
63

1✔
64
        uriBuilder := c.newUriBuilder("/api/v1/ExternalSolution/Push")
1✔
65
        if solutionId != "" {
2✔
66
                uriBuilder.AddQueryString("solutionId", solutionId)
1✔
67
        }
1✔
68
        uri := uriBuilder.Build()
1✔
69
        header := http.Header{
1✔
70
                "Content-Type": {contentType},
1✔
71
        }
1✔
72
        return network.NewHttpPostRequest(uri, c.toAuthorization(c.token), header, uploadReader, -1)
1✔
73
}
74

75
// PullSolution downloads a solution from Studio Web as a .uis file.
76
func (c StudioClient) PullSolution(solutionId string) (io.ReadCloser, error) {
1✔
77
        request := c.createPullSolutionRequest(solutionId)
1✔
78
        client := network.NewHttpClient(c.logger, c.httpClientSettings())
1✔
79
        response, err := client.Send(request)
1✔
80
        if err != nil {
2✔
81
                return nil, err
1✔
82
        }
1✔
83
        if response.StatusCode != http.StatusOK {
2✔
84
                defer func() { _ = response.Body.Close() }()
2✔
85
                body, err := io.ReadAll(response.Body)
1✔
86
                if err != nil {
1✔
NEW
87
                        return nil, fmt.Errorf("Error reading response: %w", err)
×
NEW
88
                }
×
89
                return nil, fmt.Errorf("Studio Web returned status code '%v' and body '%v'", response.StatusCode, string(body))
1✔
90
        }
91
        return response.Body, nil
1✔
92
}
93

94
func (c StudioClient) createPullSolutionRequest(solutionId string) *network.HttpRequest {
1✔
95
        uri := c.newUriBuilder("/api/v1/ExternalSolution/Pull").
1✔
96
                AddQueryString("solutionId", solutionId).
1✔
97
                Build()
1✔
98
        header := http.Header{
1✔
99
                "Accept": {"application/octet-stream"},
1✔
100
        }
1✔
101
        return network.NewHttpGetRequest(uri, c.toAuthorization(c.token), header)
1✔
102
}
1✔
103

104
// ListSolutions retrieves the list of solutions from Studio Web.
105
func (c StudioClient) ListSolutions() ([]SolutionInfo, error) {
1✔
106
        request := c.createListSolutionsRequest()
1✔
107
        client := network.NewHttpClient(c.logger, c.httpClientSettings())
1✔
108
        response, err := client.Send(request)
1✔
109
        if err != nil {
2✔
110
                return nil, err
1✔
111
        }
1✔
112
        defer func() { _ = response.Body.Close() }()
2✔
113
        body, err := io.ReadAll(response.Body)
1✔
114
        if err != nil {
1✔
NEW
115
                return nil, fmt.Errorf("Error reading response: %w", err)
×
NEW
116
        }
×
117
        if response.StatusCode != http.StatusOK {
2✔
118
                return nil, fmt.Errorf("Studio Web returned status code '%v' and body '%v'", response.StatusCode, string(body))
1✔
119
        }
1✔
120
        var result []SolutionInfo
1✔
121
        err = json.Unmarshal(body, &result)
1✔
122
        if err != nil {
2✔
123
                return nil, fmt.Errorf("Studio Web returned invalid response body '%v'", string(body))
1✔
124
        }
1✔
125
        return result, nil
1✔
126
}
127

128
func (c StudioClient) createListSolutionsRequest() *network.HttpRequest {
1✔
129
        uri := c.newUriBuilder("/api/v1/ExternalSolution/List").Build()
1✔
130
        header := http.Header{
1✔
131
                "Content-Type": {"application/json"},
1✔
132
        }
1✔
133
        return network.NewHttpGetRequest(uri, c.toAuthorization(c.token), header)
1✔
134
}
1✔
135

136
// PublishSolution publishes a solution for deployment.
137
func (c StudioClient) PublishSolution(solutionId string) (*PublishSolutionResponse, error) {
1✔
138
        requestBody, err := json.Marshal(publishSolutionRequestJson{
1✔
139
                SolutionId: solutionId,
1✔
140
        })
1✔
141
        if err != nil {
1✔
NEW
142
                return nil, err
×
NEW
143
        }
×
144

145
        uri := c.newUriBuilder("/api/v1/Publish-Requests").Build()
1✔
146
        header := http.Header{
1✔
147
                "Content-Type": {"application/json"},
1✔
148
        }
1✔
149
        request := network.NewHttpPostRequest(uri, c.toAuthorization(c.token), header, bytes.NewBuffer(requestBody), -1)
1✔
150
        client := network.NewHttpClient(c.logger, c.httpClientSettings())
1✔
151
        response, err := client.Send(request)
1✔
152
        if err != nil {
2✔
153
                return nil, err
1✔
154
        }
1✔
155
        defer func() { _ = response.Body.Close() }()
2✔
156
        body, err := io.ReadAll(response.Body)
1✔
157
        if err != nil {
1✔
NEW
158
                return nil, fmt.Errorf("Error reading response: %w", err)
×
NEW
159
        }
×
160
        if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusCreated && response.StatusCode != http.StatusAccepted {
2✔
161
                return nil, fmt.Errorf("Studio Web returned status code '%v' and body '%v'", response.StatusCode, string(body))
1✔
162
        }
1✔
163
        var result PublishSolutionResponse
1✔
164
        err = json.Unmarshal(body, &result)
1✔
165
        if err != nil {
2✔
166
                return &PublishSolutionResponse{}, nil
1✔
167
        }
1✔
168
        return &result, nil
1✔
169
}
170

171
func (c StudioClient) writeMultipartBody(bodyWriter *io.PipeWriter, stream stream.Stream, contentType string, cancel context.CancelCauseFunc) string {
1✔
172
        formWriter := multipart.NewWriter(bodyWriter)
1✔
173
        go func() {
2✔
174
                defer func() { _ = bodyWriter.Close() }()
2✔
175
                defer func() { _ = formWriter.Close() }()
2✔
176
                err := c.writeMultipartForm(formWriter, stream, contentType)
1✔
177
                if err != nil {
2✔
178
                        cancel(err)
1✔
179
                        return
1✔
180
                }
1✔
181
        }()
182
        return formWriter.FormDataContentType()
1✔
183
}
184

185
func (c StudioClient) writeMultipartForm(writer *multipart.Writer, stream stream.Stream, contentType string) error {
1✔
186
        filePart := textproto.MIMEHeader{}
1✔
187
        filePart.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, stream.Name()))
1✔
188
        filePart.Set("Content-Type", contentType)
1✔
189
        w, err := writer.CreatePart(filePart)
1✔
190
        if err != nil {
2✔
191
                return fmt.Errorf("Error creating form field 'file': %w", err)
1✔
192
        }
1✔
193
        data, err := stream.Data()
1✔
194
        if err != nil {
2✔
195
                return err
1✔
196
        }
1✔
197
        defer func() { _ = data.Close() }()
2✔
198
        _, err = io.Copy(w, data)
1✔
199
        if err != nil {
2✔
200
                return fmt.Errorf("Error writing form field 'file': %w", err)
1✔
201
        }
1✔
202
        return nil
1✔
203
}
204

205
func (c StudioClient) progressReader(text string, completedText string, reader io.Reader, length int64, progressBar *visualization.ProgressBar) io.Reader {
1✔
206
        if progressBar == nil || length < 10*1024*1024 {
2✔
207
                return reader
1✔
208
        }
1✔
209
        return visualization.NewProgressReader(reader, func(progress visualization.Progress) {
2✔
210
                displayText := text
1✔
211
                if progress.Completed {
2✔
212
                        displayText = completedText
1✔
213
                }
1✔
214
                progressBar.UpdateProgress(displayText, progress.BytesRead, length, progress.BytesPerSecond)
1✔
215
        })
216
}
217

218
func (c StudioClient) httpClientSettings() network.HttpClientSettings {
1✔
219
        return *network.NewHttpClientSettings(
1✔
220
                c.debug,
1✔
221
                c.settings.OperationId,
1✔
222
                c.settings.Header,
1✔
223
                c.settings.Timeout,
1✔
224
                c.settings.MaxAttempts,
1✔
225
                c.settings.Insecure)
1✔
226
}
1✔
227

228
func (c StudioClient) newUriBuilder(path string) *converter.UriBuilder {
1✔
229
        baseUri := c.baseUri
1✔
230
        if baseUri.Path == "" {
2✔
231
                baseUri.Path = "/{organization}/studio_/backend"
1✔
232
        }
1✔
233
        return converter.NewUriBuilder(baseUri, path).
1✔
234
                FormatPath("organization", c.organization)
1✔
235
}
236

237
func (c StudioClient) toAuthorization(token *auth.AuthToken) *network.Authorization {
1✔
238
        if token == nil {
2✔
239
                return nil
1✔
240
        }
1✔
241
        return network.NewAuthorization(token.Type, token.Value)
1✔
242
}
243

244
type publishSolutionRequestJson struct {
245
        SolutionId string `json:"solutionId"`
246
}
247

248
// PushSolutionResponse is the response from pushing a solution.
249
type PushSolutionResponse struct {
250
        SolutionId string `json:"solutionId"`
251
        Status     string `json:"status"`
252
}
253

254
// PublishSolutionResponse is the response from publishing a solution.
255
type PublishSolutionResponse struct {
256
        RequestId string `json:"requestId"`
257
        Status    string `json:"status"`
258
}
259

260
// SolutionInfo describes a solution returned from List.
261
type SolutionInfo struct {
262
        SolutionId string `json:"solutionId"`
263
        Name       string `json:"name"`
264
        Status     string `json:"status"`
265
}
266

267
// NewStudioClient creates a new Studio Web API client.
268
func NewStudioClient(
269
        baseUri url.URL,
270
        organization string,
271
        token *auth.AuthToken,
272
        debug bool,
273
        settings plugin.ExecutionSettings,
274
        logger log.Logger,
275
) *StudioClient {
1✔
276
        return &StudioClient{
1✔
277
                baseUri,
1✔
278
                organization,
1✔
279
                token,
1✔
280
                debug,
1✔
281
                settings,
1✔
282
                logger,
1✔
283
        }
1✔
284
}
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