Skip to content

Commit a735839

Browse files
committed
Implement operation unit tests to mirror the Python SDK
- Added tests for creating, updating, deleting, and retrieving sources, destinations, workflows, and jobs. - Implemented API error handling with the new `APIError` type. - Updated `CreateSource` and `CreateDestination` methods to ensure correct endpoint paths (with trailing slash). - Updated `CreateWorkflowRequest` to accept a pointer to the request type for consistency. - Implemented tests with assertions mirroring the Python SDK.
1 parent 0796e6d commit a735839

30 files changed

+1500
-102
lines changed

.golangci.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,14 @@ linters:
5858
check-type-assertions: false
5959
exclude-functions:
6060
- io/ioutil.ReadFile
61-
- io.Copy(*bytes.Buffer)
62-
- io.Copy(os.Stdout)
61+
- io.Copy
6362
- (io.Closer).Close
63+
- (net/http.ResponseWriter).Write
64+
- io.Writer.Write
65+
gosec:
66+
excludes:
67+
- G101
68+
- G104
6469

6570
# Issues configuration
6671
issues:

client.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package unstructured
33
import (
44
"cmp"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"io"
89
"net/http"
@@ -123,11 +124,17 @@ func (c *Client) do(req *http.Request, out any) error {
123124
if resp.StatusCode == http.StatusUnprocessableEntity {
124125
var validationErr HTTPValidationError
125126
if err := json.Unmarshal(body, &validationErr); err == nil {
126-
return &validationErr
127+
return &APIError{
128+
Code: resp.StatusCode,
129+
Err: &validationErr,
130+
}
127131
}
128132
}
129133

130-
return fmt.Errorf("[%s]: %s", resp.Status, string(body))
134+
return &APIError{
135+
Code: resp.StatusCode,
136+
Err: errors.New(string(body)),
137+
}
131138
}
132139

133140
if out != nil {

client_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package unstructured
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/aws-gopher/unstructured-sdk-go/test"
9+
)
10+
11+
func testclient(t *testing.T) (*Client, *test.Mux) {
12+
mux := test.NewMux()
13+
14+
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15+
val := r.Header.Get("unstructured-api-key")
16+
if val == "" {
17+
http.Error(w, "Unauthorized: missing header", http.StatusUnauthorized)
18+
return
19+
}
20+
21+
if val != test.FakeAPIKey {
22+
http.Error(w, "Unauthorized: invalid key", http.StatusUnauthorized)
23+
return
24+
}
25+
26+
mux.ServeHTTP(w, r)
27+
}))
28+
t.Cleanup(server.Close)
29+
30+
c, err := New(
31+
WithClient(server.Client()),
32+
WithEndpoint(server.URL),
33+
WithKey(test.FakeAPIKey),
34+
)
35+
if err != nil {
36+
t.Fatalf("failed to create client: %v", err)
37+
}
38+
39+
return c, mux
40+
}

destination_create.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (c *Client) CreateDestination(ctx context.Context, in CreateDestinationRequ
3333

3434
req, err := http.NewRequestWithContext(ctx,
3535
http.MethodPost,
36-
c.endpoint.JoinPath("/destinations").String(),
36+
c.endpoint.JoinPath("/destinations/").String(),
3737
bytes.NewReader(body),
3838
)
3939
if err != nil {
@@ -54,7 +54,6 @@ func (c *Client) CreateDestination(ctx context.Context, in CreateDestinationRequ
5454
// It contains the name, type, and configuration for the destination.
5555
type CreateDestinationRequest struct {
5656
Name string
57-
Type string
5857
Config DestinationConfigInput
5958
}
6059

destination_create_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package unstructured
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"strconv"
7+
"testing"
8+
"time"
9+
)
10+
11+
func TestCreateDestination(t *testing.T) {
12+
t.Parallel()
13+
14+
client, mux := testclient(t)
15+
16+
mux.CreateDestination = func(w http.ResponseWriter, _ *http.Request) {
17+
response := []byte(`{` +
18+
` "config": {` +
19+
` "remote_url": "s3://mock-s3-connector",` +
20+
` "key": "blah",` +
21+
` "secret": "blah",` +
22+
` "anonymous": false` +
23+
` },` +
24+
` "created_at": "2023-09-15T01:06:53.146Z",` +
25+
` "id": "b25d4161-77a0-4e08-b65e-86f398ce15ad",` +
26+
` "name": "test_destination_name",` +
27+
` "type": "s3"` +
28+
`}`)
29+
30+
w.Header().Set("Content-Type", "application/json")
31+
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
32+
w.Write(response)
33+
}
34+
35+
destination, err := client.CreateDestination(t.Context(), CreateDestinationRequest{
36+
Name: "test_destination_name",
37+
38+
Config: &S3DestinationConnectorConfigInput{
39+
RemoteURL: "s3://mock-s3-connector",
40+
Key: String("blah"),
41+
Secret: String("blah"),
42+
},
43+
})
44+
if err != nil {
45+
t.Fatalf("failed to create destination: %v", err)
46+
}
47+
48+
if err := errors.Join(
49+
eq("destination.id", destination.ID, "b25d4161-77a0-4e08-b65e-86f398ce15ad"),
50+
eq("destination.name", destination.Name, "test_destination_name"),
51+
equal("destination.created_at", destination.CreatedAt, time.Date(2023, 9, 15, 1, 6, 53, 146000000, time.UTC)),
52+
); err != nil {
53+
t.Error(err)
54+
}
55+
56+
cfg, ok := destination.Config.(*S3DestinationConnectorConfig)
57+
if !ok {
58+
t.Errorf("expected destination config to be %T, got %T", cfg, destination.Config)
59+
}
60+
}

destination_delete_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package unstructured
2+
3+
import (
4+
"net/http"
5+
"strconv"
6+
"testing"
7+
)
8+
9+
func TestDeleteDestination(t *testing.T) {
10+
t.Parallel()
11+
12+
client, mux := testclient(t)
13+
14+
id := "b25d4161-77a0-4e08-b65e-86f398ce15ad"
15+
16+
mux.DeleteDestination = func(w http.ResponseWriter, r *http.Request) {
17+
if val := r.PathValue("id"); val != id {
18+
http.Error(w, "destination ID "+val+" not found", http.StatusNotFound)
19+
return
20+
}
21+
22+
response := []byte(`{"detail": "Destination with id ` + id + ` successfully deleted."}`)
23+
24+
w.Header().Set("Content-Type", "application/json")
25+
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
26+
w.WriteHeader(http.StatusOK)
27+
w.Write(response)
28+
}
29+
30+
err := client.DeleteDestination(t.Context(), "b25d4161-77a0-4e08-b65e-86f398ce15ad")
31+
if err != nil {
32+
t.Fatalf("failed to delete destination: %v", err)
33+
}
34+
}

destination_get_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package unstructured
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"strconv"
7+
"testing"
8+
"time"
9+
)
10+
11+
func TestGetDestination(t *testing.T) {
12+
t.Parallel()
13+
14+
client, mux := testclient(t)
15+
16+
id := "0c363dec-3c70-45ee-8041-481044a6e1cc"
17+
mux.GetDestination = func(w http.ResponseWriter, r *http.Request) {
18+
if val := r.PathValue("id"); val != id {
19+
http.Error(w, "destination ID "+val+" not found", http.StatusNotFound)
20+
return
21+
}
22+
23+
response := []byte(`{` +
24+
` "config": {` +
25+
` "remote_url": "s3://mock-s3-connector",` +
26+
` "anonymous": false,` +
27+
` "key": "**********",` +
28+
` "secret": "**********",` +
29+
` "token": null,` +
30+
` "endpoint_url": null` +
31+
` },` +
32+
` "created_at": "2025-08-22T08:47:29.802Z",` +
33+
` "id": "` + id + `",` +
34+
` "name": "test_destination_name",` +
35+
` "type": "s3"` +
36+
`}`)
37+
38+
w.Header().Set("Content-Type", "application/json")
39+
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
40+
w.Write(response)
41+
}
42+
43+
destination, err := client.GetDestination(t.Context(), id)
44+
if err != nil {
45+
t.Fatalf("failed to get destination: %v", err)
46+
}
47+
48+
if err := errors.Join(
49+
eq("destination.id", destination.ID, id),
50+
eq("destination.name", destination.Name, "test_destination_name"),
51+
eq("destination.type", destination.Type, "s3"),
52+
equal("destination.created_at", destination.CreatedAt, time.Date(2025, 8, 22, 8, 47, 29, 802000000, time.UTC)),
53+
); err != nil {
54+
t.Error(err)
55+
}
56+
57+
cfg, ok := destination.Config.(*S3DestinationConnectorConfig)
58+
if !ok {
59+
t.Errorf("expected destination config to be %T, got %T", cfg, destination.Config)
60+
}
61+
}
62+
63+
func TestGetDestinationNotFound(t *testing.T) {
64+
t.Parallel()
65+
66+
client, mux := testclient(t)
67+
68+
id := "0c363dec-3c70-45ee-8041-481044a6e1cc"
69+
mux.GetDestination = func(w http.ResponseWriter, r *http.Request) {
70+
http.Error(w, "destination ID "+r.PathValue("id")+" not found", http.StatusNotFound)
71+
}
72+
73+
_, err := client.GetDestination(t.Context(), id)
74+
if err == nil {
75+
t.Fatalf("expected error, got nil")
76+
}
77+
78+
var apierr *APIError
79+
if !errors.As(err, &apierr) {
80+
t.Fatalf("expected error to be an %T, got %T", apierr, err)
81+
}
82+
83+
if apierr.Code != http.StatusNotFound {
84+
t.Fatalf("expected error code to be %d, got %d", http.StatusNotFound, apierr.Code)
85+
}
86+
}

0 commit comments

Comments
 (0)