diff --git a/internal/api/endpoints.go b/internal/api/endpoints.go index 5b2c4ca..045bfbc 100644 --- a/internal/api/endpoints.go +++ b/internal/api/endpoints.go @@ -38,6 +38,25 @@ type EndpointNetworkVolume struct { DataCenterID string `json:"dataCenterId,omitempty"` } +// UnmarshalJSON tolerates both shapes of networkVolumeIds: the rest read +// endpoint returns bare id strings (["vol-1"]) while the graphql saveEndpoint +// write path uses objects ([{"networkVolumeId":"vol-1"}]). +func (v *EndpointNetworkVolume) UnmarshalJSON(data []byte) error { + var id string + if err := json.Unmarshal(data, &id); err == nil { + v.NetworkVolumeID = id + return nil + } + + type alias EndpointNetworkVolume + var obj alias + if err := json.Unmarshal(data, &obj); err != nil { + return err + } + *v = EndpointNetworkVolume(obj) + return nil +} + // EndpointListResponse is the response from listing endpoints type EndpointListResponse struct { Endpoints []Endpoint `json:"endpoints"` diff --git a/internal/api/endpoints_test.go b/internal/api/endpoints_test.go index 411aa79..b5f1706 100644 --- a/internal/api/endpoints_test.go +++ b/internal/api/endpoints_test.go @@ -15,10 +15,14 @@ func TestListEndpoints(t *testing.T) { if r.URL.Path != "/endpoints" { t.Errorf("expected /endpoints, got %s", r.URL.Path) } - json.NewEncoder(w).Encode([]Endpoint{ - {ID: "ep-1", Name: "endpoint-1"}, - {ID: "ep-2", Name: "endpoint-2"}, - }) + // serve the raw rest wire shape, not a re-encoded Endpoint struct, so + // the test exercises the real api format (networkVolumeIds as bare id + // strings) — a struct round-trip would emit the object shape instead + // and hide read/write divergences. + w.Write([]byte(`[ + {"id":"ep-1","name":"endpoint-1","networkVolumeIds":["vol-1"]}, + {"id":"ep-2","name":"endpoint-2"} + ]`)) })) defer server.Close() @@ -34,6 +38,29 @@ func TestListEndpoints(t *testing.T) { if len(endpoints) != 2 { t.Errorf("expected 2 endpoints, got %d", len(endpoints)) } + if len(endpoints[0].NetworkVolumeIDs) != 1 || endpoints[0].NetworkVolumeIDs[0].NetworkVolumeID != "vol-1" { + t.Errorf("expected vol-1 on first endpoint, got %+v", endpoints[0].NetworkVolumeIDs) + } +} + +func TestEndpointNetworkVolumeIDsUnmarshal(t *testing.T) { + // rest read shape: bare id strings. + var strShape Endpoint + if err := json.Unmarshal([]byte(`{"id":"ep-1","networkVolumeIds":["vol-1","vol-2"]}`), &strShape); err != nil { + t.Fatalf("failed to unmarshal string shape: %v", err) + } + if len(strShape.NetworkVolumeIDs) != 2 || strShape.NetworkVolumeIDs[0].NetworkVolumeID != "vol-1" { + t.Fatalf("unexpected string-shape parse: %+v", strShape.NetworkVolumeIDs) + } + + // graphql write shape: objects. + var objShape Endpoint + if err := json.Unmarshal([]byte(`{"id":"ep-2","networkVolumeIds":[{"networkVolumeId":"vol-3","dataCenterId":"US-GA-1"}]}`), &objShape); err != nil { + t.Fatalf("failed to unmarshal object shape: %v", err) + } + if len(objShape.NetworkVolumeIDs) != 1 || objShape.NetworkVolumeIDs[0].NetworkVolumeID != "vol-3" || objShape.NetworkVolumeIDs[0].DataCenterID != "US-GA-1" { + t.Fatalf("unexpected object-shape parse: %+v", objShape.NetworkVolumeIDs) + } } func TestGetEndpoint(t *testing.T) { @@ -41,12 +68,16 @@ func TestGetEndpoint(t *testing.T) { if r.URL.Path != "/endpoints/ep-123" { t.Errorf("expected /endpoints/ep-123, got %s", r.URL.Path) } - json.NewEncoder(w).Encode(Endpoint{ - ID: "ep-123", - Name: "my-endpoint", - WorkersMin: 0, - WorkersMax: 3, - }) + // raw rest wire shape, including a network volume returned as a bare id + // string — this is what regressed serverless get in production. + w.Write([]byte(`{ + "id":"ep-123", + "name":"my-endpoint", + "workersMin":0, + "workersMax":3, + "networkVolumeId":"vol-9", + "networkVolumeIds":["vol-9"] + }`)) })) defer server.Close() @@ -62,6 +93,9 @@ func TestGetEndpoint(t *testing.T) { if endpoint.ID != "ep-123" { t.Errorf("expected ep-123, got %s", endpoint.ID) } + if len(endpoint.NetworkVolumeIDs) != 1 || endpoint.NetworkVolumeIDs[0].NetworkVolumeID != "vol-9" { + t.Errorf("expected vol-9, got %+v", endpoint.NetworkVolumeIDs) + } } func TestEndpointCreateGQLInputSerialization(t *testing.T) {