From 6428151f04411be03df09820465005b27e4b238a Mon Sep 17 00:00:00 2001 From: Boris Valo Date: Fri, 12 Jun 2026 17:16:51 +0200 Subject: [PATCH 1/6] SYN-5569: add SSL test create client --- syntheticsclientv2/common_models.go | 49 ++++ syntheticsclientv2/create_sslcheckv2.go | 55 +++++ syntheticsclientv2/create_sslcheckv2_test.go | 223 +++++++++++++++++++ 3 files changed, 327 insertions(+) create mode 100644 syntheticsclientv2/create_sslcheckv2.go create mode 100644 syntheticsclientv2/create_sslcheckv2_test.go diff --git a/syntheticsclientv2/common_models.go b/syntheticsclientv2/common_models.go index 299a6bf..008a102 100644 --- a/syntheticsclientv2/common_models.go +++ b/syntheticsclientv2/common_models.go @@ -314,6 +314,55 @@ type ChecksV2Response struct { Totalcount int `json:"totalCount"` } +type SslCheckV2Response struct { + Test struct { + ID int `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Active bool `json:"active"` + Frequency int `json:"frequency,omitempty"` + SchedulingStrategy string `json:"schedulingStrategy,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` + UpdatedAt time.Time `json:"updatedAt,omitempty"` + LocationIds []string `json:"locationIds,omitempty"` + Type string `json:"type,omitempty"` + Host string `json:"host,omitempty"` + Port int `json:"port,omitempty"` + ServerName string `json:"serverName,omitempty"` + AllowSelfSigned bool `json:"allowSelfSigned"` + AllowUntrustedRoot bool `json:"allowUntrustedRoot"` + CaCertificateID *int `json:"caCertificateId"` + Validations []Validations `json:"validations"` + Customproperties []CustomProperties `json:"customProperties"` + Lastrunstatus string `json:"lastRunStatus"` + Lastrunat time.Time `json:"lastRunAt"` + LastRunCoreMetricsPublishedAt time.Time `json:"lastRunCoreMetricsPublishedAt"` + LastRunLocationId string `json:"lastRunLocationId"` + LastRunId int `json:"lastRunId"` + Automaticretries int `json:"automaticRetries"` + Createdby string `json:"createdBy"` + Updatedby string `json:"updatedBy"` + } `json:"test"` +} + +type SslCheckV2Input struct { + Test struct { + Name string `json:"name"` + LocationIds []string `json:"locationIds"` + Frequency int `json:"frequency"` + SchedulingStrategy string `json:"schedulingStrategy"` + Active bool `json:"active"` + Customproperties []CustomProperties `json:"customProperties"` + Automaticretries int `json:"automaticRetries"` + Host string `json:"host"` + Port int `json:"port"` + ServerName string `json:"serverName"` + AllowSelfSigned bool `json:"allowSelfSigned"` + AllowUntrustedRoot bool `json:"allowUntrustedRoot"` + CaCertificateID *int `json:"caCertificateId"` + Validations []Validations `json:"validations"` + } `json:"test"` +} + type PortCheckV2Response struct { Test struct { ID int `json:"id"` diff --git a/syntheticsclientv2/create_sslcheckv2.go b/syntheticsclientv2/create_sslcheckv2.go new file mode 100644 index 0000000..fcf7e64 --- /dev/null +++ b/syntheticsclientv2/create_sslcheckv2.go @@ -0,0 +1,55 @@ +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "bytes" + "encoding/json" +) + +func parseCreateSslCheckV2Response(response string) (*SslCheckV2Response, error) { + var createSslCheckV2 SslCheckV2Response + JSONResponse := []byte(response) + err := json.Unmarshal(JSONResponse, &createSslCheckV2) + if err != nil { + return nil, err + } + + return &createSslCheckV2, err +} + +func (c Client) CreateSslCheckV2(SslCheckV2Details *SslCheckV2Input) (*SslCheckV2Response, *RequestDetails, error) { + if SslCheckV2Details.Test.Validations == nil { + validation := make([]Validations, 0) + SslCheckV2Details.Test.Validations = validation + } + + body, err := json.Marshal(SslCheckV2Details) + if err != nil { + return nil, nil, err + } + + details, err := c.makePublicAPICall("POST", "/tests/ssl", bytes.NewBuffer(body), nil) + if err != nil { + return nil, details, err + } + + newSslCheckV2, err := parseCreateSslCheckV2Response(details.ResponseBody) + if err != nil { + return newSslCheckV2, details, err + } + + return newSslCheckV2, details, nil +} diff --git a/syntheticsclientv2/create_sslcheckv2_test.go b/syntheticsclientv2/create_sslcheckv2_test.go new file mode 100644 index 0000000..9c176e5 --- /dev/null +++ b/syntheticsclientv2/create_sslcheckv2_test.go @@ -0,0 +1,223 @@ +//go:build unit_tests +// +build unit_tests + +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "encoding/json" + "io" + "net/http" + "reflect" + "testing" +) + +var ( + createSslCheckV2Body = `{"test":{"name":"ssl-check","frequency":5,"schedulingStrategy":"round_robin","active":true,"locationIds":["aws-us-east-1"],"customProperties":[{"key":"env","value":"prod"}],"automaticRetries":1,"host":"www.splunk.com","port":443,"serverName":"www.splunk.com","allowSelfSigned":true,"allowUntrustedRoot":false,"caCertificateId":42,"validations":[{"name":"Certificate expires later","type":"assert_numeric","actual":"{{certificate.days_until_expiration}}","expected":"30","comparator":"is_greater_than"}]}}` + inputSslCheckV2Data = SslCheckV2Input{} +) + +func TestCreateSslCheckV2(t *testing.T) { + setup() + defer teardown() + + expectedCaCertificateID := 42 + expectedValidations := []Validations{ + { + Name: "Certificate expires later", + Type: "assert_numeric", + Actual: "{{certificate.days_until_expiration}}", + Expected: "30", + Comparator: "is_greater_than", + }, + } + expectedCustomProperties := []CustomProperties{ + { + Key: "env", + Value: "prod", + }, + } + + testMux.HandleFunc("/tests/ssl", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + requestBody, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + var requestEnvelope map[string]json.RawMessage + err = json.Unmarshal(requestBody, &requestEnvelope) + if err != nil { + t.Fatal(err) + } + rawTest, ok := requestEnvelope["test"] + if !ok { + t.Fatal("request body missing test envelope") + } + + var requestTestFields map[string]json.RawMessage + err = json.Unmarshal(rawTest, &requestTestFields) + if err != nil { + t.Fatal(err) + } + for _, field := range []string{"host", "port", "serverName", "allowSelfSigned", "allowUntrustedRoot", "caCertificateId", "validations", "customProperties"} { + if _, ok := requestTestFields[field]; !ok { + t.Errorf("request body missing test.%s", field) + } + } + + requestSslCheckV2Data := SslCheckV2Input{} + err = json.Unmarshal(requestBody, &requestSslCheckV2Data) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(requestSslCheckV2Data.Test.Host, "www.splunk.com") { + t.Errorf("request body host \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.Host, "www.splunk.com") + } + if !reflect.DeepEqual(requestSslCheckV2Data.Test.Port, 443) { + t.Errorf("request body port \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.Port, 443) + } + if !reflect.DeepEqual(requestSslCheckV2Data.Test.ServerName, "www.splunk.com") { + t.Errorf("request body server name \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.ServerName, "www.splunk.com") + } + if !reflect.DeepEqual(requestSslCheckV2Data.Test.AllowSelfSigned, true) { + t.Errorf("request body allow self signed \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.AllowSelfSigned, true) + } + if !reflect.DeepEqual(requestSslCheckV2Data.Test.AllowUntrustedRoot, false) { + t.Errorf("request body allow untrusted root \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.AllowUntrustedRoot, false) + } + if requestSslCheckV2Data.Test.CaCertificateID == nil || !reflect.DeepEqual(*requestSslCheckV2Data.Test.CaCertificateID, expectedCaCertificateID) { + t.Errorf("request body ca certificate id \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.CaCertificateID, expectedCaCertificateID) + } + if !reflect.DeepEqual(requestSslCheckV2Data.Test.Validations, expectedValidations) { + t.Errorf("request body validations \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.Validations, expectedValidations) + } + if !reflect.DeepEqual(requestSslCheckV2Data.Test.Customproperties, expectedCustomProperties) { + t.Errorf("request body custom properties \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.Customproperties, expectedCustomProperties) + } + + _, err = w.Write([]byte(createSslCheckV2Body)) + if err != nil { + t.Fatal(err) + } + }) + + err := json.Unmarshal([]byte(createSslCheckV2Body), &inputSslCheckV2Data) + if err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.CreateSslCheckV2(&inputSslCheckV2Data) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(resp.Test.Name, inputSslCheckV2Data.Test.Name) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Name, inputSslCheckV2Data.Test.Name) + } + if !reflect.DeepEqual(resp.Test.Host, inputSslCheckV2Data.Test.Host) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Host, inputSslCheckV2Data.Test.Host) + } + if !reflect.DeepEqual(resp.Test.Port, inputSslCheckV2Data.Test.Port) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Port, inputSslCheckV2Data.Test.Port) + } + if !reflect.DeepEqual(resp.Test.ServerName, inputSslCheckV2Data.Test.ServerName) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.ServerName, inputSslCheckV2Data.Test.ServerName) + } + if !reflect.DeepEqual(resp.Test.AllowSelfSigned, inputSslCheckV2Data.Test.AllowSelfSigned) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.AllowSelfSigned, inputSslCheckV2Data.Test.AllowSelfSigned) + } + if !reflect.DeepEqual(resp.Test.AllowUntrustedRoot, inputSslCheckV2Data.Test.AllowUntrustedRoot) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.AllowUntrustedRoot, inputSslCheckV2Data.Test.AllowUntrustedRoot) + } + if !reflect.DeepEqual(resp.Test.LocationIds, inputSslCheckV2Data.Test.LocationIds) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.LocationIds, inputSslCheckV2Data.Test.LocationIds) + } + if !reflect.DeepEqual(resp.Test.Customproperties, inputSslCheckV2Data.Test.Customproperties) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Customproperties, inputSslCheckV2Data.Test.Customproperties) + } + if !reflect.DeepEqual(resp.Test.Automaticretries, inputSslCheckV2Data.Test.Automaticretries) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Automaticretries, inputSslCheckV2Data.Test.Automaticretries) + } + if !reflect.DeepEqual(resp.Test.CaCertificateID, inputSslCheckV2Data.Test.CaCertificateID) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.CaCertificateID, inputSslCheckV2Data.Test.CaCertificateID) + } + if !reflect.DeepEqual(resp.Test.Validations, inputSslCheckV2Data.Test.Validations) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Validations, inputSslCheckV2Data.Test.Validations) + } +} + +func TestCreateSslCheckV2DefaultsNilValidations(t *testing.T) { + setup() + defer teardown() + + inputSslCheckV2Data := SslCheckV2Input{} + err := json.Unmarshal([]byte(createSslCheckV2Body), &inputSslCheckV2Data) + if err != nil { + t.Fatal(err) + } + inputSslCheckV2Data.Test.Validations = nil + + testMux.HandleFunc("/tests/ssl", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + requestBody, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + var requestEnvelope map[string]json.RawMessage + err = json.Unmarshal(requestBody, &requestEnvelope) + if err != nil { + t.Fatal(err) + } + rawTest, ok := requestEnvelope["test"] + if !ok { + t.Fatal("request body missing test envelope") + } + + var requestTestFields map[string]json.RawMessage + err = json.Unmarshal(rawTest, &requestTestFields) + if err != nil { + t.Fatal(err) + } + rawValidations, ok := requestTestFields["validations"] + if !ok { + t.Fatal("request body missing test.validations") + } + + var validations []json.RawMessage + err = json.Unmarshal(rawValidations, &validations) + if err != nil { + t.Fatal(err) + } + if validations == nil { + t.Fatalf("request body validations \n\n%#v want empty slice", string(rawValidations)) + } + if len(validations) != 0 { + t.Errorf("request body validations length \n\n%#v want \n\n%#v", len(validations), 0) + } + + _, err = w.Write([]byte(createSslCheckV2Body)) + if err != nil { + t.Fatal(err) + } + }) + + _, _, err = testClient.CreateSslCheckV2(&inputSslCheckV2Data) + if err != nil { + t.Fatal(err) + } +} From 6b0b8b21fc5699fb4e9ee8b96e3f86c84827e02b Mon Sep 17 00:00:00 2001 From: Boris Valo Date: Fri, 12 Jun 2026 18:14:51 +0200 Subject: [PATCH 2/6] SYN-5569: add SSL test read update delete client --- syntheticsclientv2/delete_sslcheckv2.go | 39 ++++ syntheticsclientv2/delete_sslcheckv2_test.go | 41 ++++ syntheticsclientv2/get_sslcheckv2.go | 45 ++++ syntheticsclientv2/get_sslcheckv2_test.go | 88 +++++++ syntheticsclientv2/update_sslcheckv2.go | 57 +++++ syntheticsclientv2/update_sslcheckv2_test.go | 228 +++++++++++++++++++ 6 files changed, 498 insertions(+) create mode 100644 syntheticsclientv2/delete_sslcheckv2.go create mode 100644 syntheticsclientv2/delete_sslcheckv2_test.go create mode 100644 syntheticsclientv2/get_sslcheckv2.go create mode 100644 syntheticsclientv2/get_sslcheckv2_test.go create mode 100644 syntheticsclientv2/update_sslcheckv2.go create mode 100644 syntheticsclientv2/update_sslcheckv2_test.go diff --git a/syntheticsclientv2/delete_sslcheckv2.go b/syntheticsclientv2/delete_sslcheckv2.go new file mode 100644 index 0000000..ce743cf --- /dev/null +++ b/syntheticsclientv2/delete_sslcheckv2.go @@ -0,0 +1,39 @@ +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "bytes" + "errors" + "fmt" + "strconv" +) + +func (c Client) DeleteSslCheckV2(id int) (int, error) { + requestDetails, err := c.makePublicAPICall("DELETE", fmt.Sprintf("/tests/ssl/%d", id), bytes.NewBufferString("{}"), nil) + if err != nil { + return 1, err + } + var status = requestDetails.StatusCode + + fmt.Println(status) + + if status >= 300 || status < 200 { + errorMsg := fmt.Sprintf("error: Response code %v. Expecting 2XX.", strconv.Itoa(status)) + return status, errors.New(errorMsg) + } + + return status, err +} diff --git a/syntheticsclientv2/delete_sslcheckv2_test.go b/syntheticsclientv2/delete_sslcheckv2_test.go new file mode 100644 index 0000000..b7d6911 --- /dev/null +++ b/syntheticsclientv2/delete_sslcheckv2_test.go @@ -0,0 +1,41 @@ +//go:build unit_tests +// +build unit_tests + +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "net/http" + "testing" +) + +func TestDeleteSslCheckV2(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/ssl/19", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + + resp, err := testClient.DeleteSslCheckV2(19) + if err != nil { + t.Fatal(err) + } + if resp != http.StatusNoContent { + t.Errorf("returned \n\n%#v want \n\n%#v", resp, http.StatusNoContent) + } +} diff --git a/syntheticsclientv2/get_sslcheckv2.go b/syntheticsclientv2/get_sslcheckv2.go new file mode 100644 index 0000000..5606b44 --- /dev/null +++ b/syntheticsclientv2/get_sslcheckv2.go @@ -0,0 +1,45 @@ +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "bytes" + "encoding/json" + "fmt" +) + +func parseGetSslCheckV2Response(response string) (*SslCheckV2Response, error) { + var sslCheckV2 SslCheckV2Response + err := json.Unmarshal([]byte(response), &sslCheckV2) + if err != nil { + return nil, err + } + + return &sslCheckV2, err +} + +func (c Client) GetSslCheckV2(id int) (*SslCheckV2Response, *RequestDetails, error) { + details, err := c.makePublicAPICall("GET", fmt.Sprintf("/tests/ssl/%d", id), bytes.NewBufferString("{}"), nil) + if err != nil { + return nil, details, err + } + + sslCheckV2, err := parseGetSslCheckV2Response(details.ResponseBody) + if err != nil { + return sslCheckV2, details, err + } + + return sslCheckV2, details, nil +} diff --git a/syntheticsclientv2/get_sslcheckv2_test.go b/syntheticsclientv2/get_sslcheckv2_test.go new file mode 100644 index 0000000..c915964 --- /dev/null +++ b/syntheticsclientv2/get_sslcheckv2_test.go @@ -0,0 +1,88 @@ +//go:build unit_tests +// +build unit_tests + +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "encoding/json" + "net/http" + "reflect" + "testing" +) + +var ( + getSslCheckV2Body = `{"test":{"id":1647,"type":"ssl","name":"ssl-check","frequency":5,"schedulingStrategy":"round_robin","active":true,"locationIds":["aws-us-east-1"],"createdAt":"2022-09-14T14:35:37.801Z","updatedAt":"2022-09-14T14:35:38.099Z","createdBy":"abc1234","updatedBy":"abc1234","customProperties":[{"key":"env","value":"prod"}],"automaticRetries":1,"lastRunStatus":"success","lastRunAt":"2024-03-07T00:47:43.741Z","lastRunCoreMetricsPublishedAt":"2024-03-07T00:47:43.741Z","lastRunLocationId":"aws-us-east-1","lastRunId":999,"host":"www.splunk.com","port":443,"serverName":"www.splunk.com","allowSelfSigned":true,"allowUntrustedRoot":false,"caCertificateId":42,"validations":[{"name":"Certificate expires later","type":"assert_numeric","actual":"{{certificate.days_until_expiration}}","expected":"30","comparator":"is_greater_than"}]}}` + inputGetSslCheckV2 = verifySslCheckV2Input(string(getSslCheckV2Body)) +) + +func TestGetSslCheckV2(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/ssl/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte(getSslCheckV2Body)) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.GetSslCheckV2(1) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(resp.Test.ID, inputGetSslCheckV2.Test.ID) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.ID, inputGetSslCheckV2.Test.ID) + } + if !reflect.DeepEqual(resp.Test.Name, inputGetSslCheckV2.Test.Name) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Name, inputGetSslCheckV2.Test.Name) + } + if !reflect.DeepEqual(resp.Test.Type, inputGetSslCheckV2.Test.Type) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Type, inputGetSslCheckV2.Test.Type) + } + if !reflect.DeepEqual(resp.Test.Host, inputGetSslCheckV2.Test.Host) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Host, inputGetSslCheckV2.Test.Host) + } + if !reflect.DeepEqual(resp.Test.Port, inputGetSslCheckV2.Test.Port) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Port, inputGetSslCheckV2.Test.Port) + } + if !reflect.DeepEqual(resp.Test.ServerName, inputGetSslCheckV2.Test.ServerName) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.ServerName, inputGetSslCheckV2.Test.ServerName) + } + if !reflect.DeepEqual(resp.Test.AllowSelfSigned, inputGetSslCheckV2.Test.AllowSelfSigned) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.AllowSelfSigned, inputGetSslCheckV2.Test.AllowSelfSigned) + } + if !reflect.DeepEqual(resp.Test.AllowUntrustedRoot, inputGetSslCheckV2.Test.AllowUntrustedRoot) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.AllowUntrustedRoot, inputGetSslCheckV2.Test.AllowUntrustedRoot) + } + if !reflect.DeepEqual(resp.Test.CaCertificateID, inputGetSslCheckV2.Test.CaCertificateID) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.CaCertificateID, inputGetSslCheckV2.Test.CaCertificateID) + } + if !reflect.DeepEqual(resp.Test.Validations, inputGetSslCheckV2.Test.Validations) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Validations, inputGetSslCheckV2.Test.Validations) + } +} + +func verifySslCheckV2Input(stringInput string) *SslCheckV2Response { + check := &SslCheckV2Response{} + err := json.Unmarshal([]byte(stringInput), check) + if err != nil { + panic(err) + } + return check +} diff --git a/syntheticsclientv2/update_sslcheckv2.go b/syntheticsclientv2/update_sslcheckv2.go new file mode 100644 index 0000000..6c10c63 --- /dev/null +++ b/syntheticsclientv2/update_sslcheckv2.go @@ -0,0 +1,57 @@ +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "bytes" + "encoding/json" + "fmt" +) + +func parseUpdateSslCheckV2Response(response string) (*SslCheckV2Response, error) { + var updateSslCheckV2 SslCheckV2Response + if response != "" { + err := json.Unmarshal([]byte(response), &updateSslCheckV2) + if err != nil { + return nil, err + } + return &updateSslCheckV2, err + } + return &updateSslCheckV2, nil +} + +func (c Client) UpdateSslCheckV2(id int, SslCheckV2Details *SslCheckV2Input) (*SslCheckV2Response, *RequestDetails, error) { + if SslCheckV2Details.Test.Validations == nil { + validation := make([]Validations, 0) + SslCheckV2Details.Test.Validations = validation + } + + body, err := json.Marshal(SslCheckV2Details) + if err != nil { + return nil, nil, err + } + + requestDetails, err := c.makePublicAPICall("PUT", fmt.Sprintf("/tests/ssl/%d", id), bytes.NewBuffer(body), nil) + if err != nil { + return nil, requestDetails, err + } + + updateSslCheckV2, err := parseUpdateSslCheckV2Response(requestDetails.ResponseBody) + if err != nil { + return updateSslCheckV2, requestDetails, err + } + + return updateSslCheckV2, requestDetails, nil +} diff --git a/syntheticsclientv2/update_sslcheckv2_test.go b/syntheticsclientv2/update_sslcheckv2_test.go new file mode 100644 index 0000000..9c6a7fd --- /dev/null +++ b/syntheticsclientv2/update_sslcheckv2_test.go @@ -0,0 +1,228 @@ +//go:build unit_tests +// +build unit_tests + +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "encoding/json" + "io" + "net/http" + "reflect" + "testing" +) + +var ( + updateSslCheckV2Body = `{"test":{"name":"ssl-check-updated","frequency":10,"schedulingStrategy":"concurrent","active":false,"locationIds":["aws-us-east-1"],"customProperties":[{"key":"env","value":"stage"}],"automaticRetries":2,"host":"example.com","port":8443,"serverName":"example.com","allowSelfSigned":false,"allowUntrustedRoot":true,"caCertificateId":42,"validations":[{"name":"Certificate expires later","type":"assert_numeric","actual":"{{certificate.days_until_expiration}}","expected":"15","comparator":"is_greater_than"}]}}` + inputSslCheckV2Update = SslCheckV2Input{} +) + +func TestUpdateSslCheckV2(t *testing.T) { + setup() + defer teardown() + + expectedCaCertificateID := 42 + expectedValidations := []Validations{ + { + Name: "Certificate expires later", + Type: "assert_numeric", + Actual: "{{certificate.days_until_expiration}}", + Expected: "15", + Comparator: "is_greater_than", + }, + } + expectedCustomProperties := []CustomProperties{ + { + Key: "env", + Value: "stage", + }, + } + + testMux.HandleFunc("/tests/ssl/1650", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + requestBody, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + var requestEnvelope map[string]json.RawMessage + err = json.Unmarshal(requestBody, &requestEnvelope) + if err != nil { + t.Fatal(err) + } + rawTest, ok := requestEnvelope["test"] + if !ok { + t.Fatal("request body missing test envelope") + } + + var requestTestFields map[string]json.RawMessage + err = json.Unmarshal(rawTest, &requestTestFields) + if err != nil { + t.Fatal(err) + } + for _, field := range []string{"host", "port", "serverName", "allowSelfSigned", "allowUntrustedRoot", "caCertificateId", "validations", "customProperties"} { + if _, ok := requestTestFields[field]; !ok { + t.Errorf("request body missing test.%s", field) + } + } + + requestSslCheckV2Data := SslCheckV2Input{} + err = json.Unmarshal(requestBody, &requestSslCheckV2Data) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(requestSslCheckV2Data.Test.Host, "example.com") { + t.Errorf("request body host \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.Host, "example.com") + } + if !reflect.DeepEqual(requestSslCheckV2Data.Test.Port, 8443) { + t.Errorf("request body port \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.Port, 8443) + } + if !reflect.DeepEqual(requestSslCheckV2Data.Test.ServerName, "example.com") { + t.Errorf("request body server name \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.ServerName, "example.com") + } + if !reflect.DeepEqual(requestSslCheckV2Data.Test.AllowSelfSigned, false) { + t.Errorf("request body allow self signed \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.AllowSelfSigned, false) + } + if !reflect.DeepEqual(requestSslCheckV2Data.Test.AllowUntrustedRoot, true) { + t.Errorf("request body allow untrusted root \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.AllowUntrustedRoot, true) + } + if requestSslCheckV2Data.Test.CaCertificateID == nil || !reflect.DeepEqual(*requestSslCheckV2Data.Test.CaCertificateID, expectedCaCertificateID) { + t.Errorf("request body ca certificate id \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.CaCertificateID, expectedCaCertificateID) + } + if !reflect.DeepEqual(requestSslCheckV2Data.Test.Validations, expectedValidations) { + t.Errorf("request body validations \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.Validations, expectedValidations) + } + if !reflect.DeepEqual(requestSslCheckV2Data.Test.Customproperties, expectedCustomProperties) { + t.Errorf("request body custom properties \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.Customproperties, expectedCustomProperties) + } + + _, err = w.Write([]byte(updateSslCheckV2Body)) + if err != nil { + t.Fatal(err) + } + }) + + err := json.Unmarshal([]byte(updateSslCheckV2Body), &inputSslCheckV2Update) + if err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.UpdateSslCheckV2(1650, &inputSslCheckV2Update) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(resp.Test.Name, inputSslCheckV2Update.Test.Name) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Name, inputSslCheckV2Update.Test.Name) + } + if !reflect.DeepEqual(resp.Test.Host, inputSslCheckV2Update.Test.Host) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Host, inputSslCheckV2Update.Test.Host) + } + if !reflect.DeepEqual(resp.Test.Port, inputSslCheckV2Update.Test.Port) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Port, inputSslCheckV2Update.Test.Port) + } + if !reflect.DeepEqual(resp.Test.CaCertificateID, inputSslCheckV2Update.Test.CaCertificateID) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.CaCertificateID, inputSslCheckV2Update.Test.CaCertificateID) + } + if !reflect.DeepEqual(resp.Test.Validations, inputSslCheckV2Update.Test.Validations) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Validations, inputSslCheckV2Update.Test.Validations) + } +} + +func TestUpdateSslCheckV2DefaultsNilValidations(t *testing.T) { + setup() + defer teardown() + + inputSslCheckV2Data := SslCheckV2Input{} + err := json.Unmarshal([]byte(updateSslCheckV2Body), &inputSslCheckV2Data) + if err != nil { + t.Fatal(err) + } + inputSslCheckV2Data.Test.Validations = nil + + testMux.HandleFunc("/tests/ssl/1651", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + requestBody, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + var requestEnvelope map[string]json.RawMessage + err = json.Unmarshal(requestBody, &requestEnvelope) + if err != nil { + t.Fatal(err) + } + rawTest, ok := requestEnvelope["test"] + if !ok { + t.Fatal("request body missing test envelope") + } + + var requestTestFields map[string]json.RawMessage + err = json.Unmarshal(rawTest, &requestTestFields) + if err != nil { + t.Fatal(err) + } + rawValidations, ok := requestTestFields["validations"] + if !ok { + t.Fatal("request body missing test.validations") + } + + var validations []json.RawMessage + err = json.Unmarshal(rawValidations, &validations) + if err != nil { + t.Fatal(err) + } + if validations == nil { + t.Fatalf("request body validations \n\n%#v want empty slice", string(rawValidations)) + } + if len(validations) != 0 { + t.Errorf("request body validations length \n\n%#v want \n\n%#v", len(validations), 0) + } + + _, err = w.Write([]byte(updateSslCheckV2Body)) + if err != nil { + t.Fatal(err) + } + }) + + _, _, err = testClient.UpdateSslCheckV2(1651, &inputSslCheckV2Data) + if err != nil { + t.Fatal(err) + } +} + +func TestUpdateSslCheckV2BlankResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/ssl/1652", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + w.WriteHeader(http.StatusOK) + }) + + err := json.Unmarshal([]byte(updateSslCheckV2Body), &inputSslCheckV2Update) + if err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.UpdateSslCheckV2(1652, &inputSslCheckV2Update) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("expected non-nil response for blank successful update body") + } +} From 90564b48eb7547cb9ea1c7d6715f159aa098c642 Mon Sep 17 00:00:00 2001 From: Boris Valo Date: Fri, 12 Jun 2026 18:29:12 +0200 Subject: [PATCH 3/6] SYN-5569: add CA certificate create read client --- syntheticsclientv2/common_models.go | 34 +++++ syntheticsclientv2/create_cacertificatev2.go | 50 +++++++ .../create_cacertificatev2_test.go | 124 ++++++++++++++++++ syntheticsclientv2/get_cacertificatesv2.go | 44 +++++++ .../get_cacertificatesv2_test.go | 61 +++++++++ syntheticsclientv2/get_cacertificatev2.go | 45 +++++++ .../get_cacertificatev2_test.go | 61 +++++++++ 7 files changed, 419 insertions(+) create mode 100644 syntheticsclientv2/create_cacertificatev2.go create mode 100644 syntheticsclientv2/create_cacertificatev2_test.go create mode 100644 syntheticsclientv2/get_cacertificatesv2.go create mode 100644 syntheticsclientv2/get_cacertificatesv2_test.go create mode 100644 syntheticsclientv2/get_cacertificatev2.go create mode 100644 syntheticsclientv2/get_cacertificatev2_test.go diff --git a/syntheticsclientv2/common_models.go b/syntheticsclientv2/common_models.go index 008a102..e95f87b 100644 --- a/syntheticsclientv2/common_models.go +++ b/syntheticsclientv2/common_models.go @@ -314,6 +314,40 @@ type ChecksV2Response struct { Totalcount int `json:"totalCount"` } +type CaCertificate struct { + ID int `json:"id,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Content string `json:"content,omitempty"` + FileExtension string `json:"fileExtension,omitempty"` + Filename string `json:"filename,omitempty"` + ExpiresAt time.Time `json:"expiresAt,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` + CreatedBy string `json:"createdBy,omitempty"` + UpdatedAt time.Time `json:"updatedAt,omitempty"` + UpdatedBy string `json:"updatedBy,omitempty"` +} + +type CaCertificateInput struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Content string `json:"content,omitempty"` + FileExtension string `json:"fileExtension,omitempty"` + Filename string `json:"filename,omitempty"` +} + +type CaCertificateV2Input struct { + CaCert CaCertificateInput `json:"cacert"` +} + +type CaCertificateV2Response struct { + CaCert CaCertificate `json:"cacert"` +} + +type CaCertificatesV2Response struct { + CaCerts []CaCertificate `json:"cacerts"` +} + type SslCheckV2Response struct { Test struct { ID int `json:"id,omitempty"` diff --git a/syntheticsclientv2/create_cacertificatev2.go b/syntheticsclientv2/create_cacertificatev2.go new file mode 100644 index 0000000..1fd6056 --- /dev/null +++ b/syntheticsclientv2/create_cacertificatev2.go @@ -0,0 +1,50 @@ +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "bytes" + "encoding/json" +) + +func parseCreateCaCertificateV2Response(response string) (*CaCertificateV2Response, error) { + var createCaCertificateV2 CaCertificateV2Response + JSONResponse := []byte(response) + err := json.Unmarshal(JSONResponse, &createCaCertificateV2) + if err != nil { + return nil, err + } + + return &createCaCertificateV2, err +} + +func (c Client) CreateCaCertificateV2(CaCertificateV2Details *CaCertificateV2Input) (*CaCertificateV2Response, *RequestDetails, error) { + body, err := json.Marshal(CaCertificateV2Details) + if err != nil { + return nil, nil, err + } + + details, err := c.makePublicAPICall("POST", "/cacerts", bytes.NewBuffer(body), nil) + if err != nil { + return nil, details, err + } + + newCaCertificateV2, err := parseCreateCaCertificateV2Response(details.ResponseBody) + if err != nil { + return newCaCertificateV2, details, err + } + + return newCaCertificateV2, details, nil +} diff --git a/syntheticsclientv2/create_cacertificatev2_test.go b/syntheticsclientv2/create_cacertificatev2_test.go new file mode 100644 index 0000000..8c8c512 --- /dev/null +++ b/syntheticsclientv2/create_cacertificatev2_test.go @@ -0,0 +1,124 @@ +//go:build unit_tests +// +build unit_tests + +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "encoding/json" + "io" + "net/http" + "reflect" + "testing" +) + +var ( + createCaCertificateV2Body = `{"cacert":{"name":"foo","description":"My CA certificate","content":"Q2VydGlmaWNhdGU=","fileExtension":"pem","filename":"ca_cert_file"}}` + createCaCertificateV2ResponseBody = `{"cacert":{"id":1,"name":"foo","description":"My CA certificate","content":"","fileExtension":"pem","filename":"ca_cert_file","expiresAt":"2026-09-14T14:35:37.801Z","createdAt":"2022-09-14T14:35:37.801Z","createdBy":"abcdefgh1234","updatedAt":"2022-09-14T14:35:38.099Z","updatedBy":"abcdefgh1234"}}` + inputCaCertificateV2Data = CaCertificateV2Input{} + outputCaCertificateV2Data = CaCertificateV2Response{} +) + +func TestCreateCaCertificateV2(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/cacerts", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + + requestBody, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + var requestEnvelope map[string]json.RawMessage + err = json.Unmarshal(requestBody, &requestEnvelope) + if err != nil { + t.Fatal(err) + } + if len(requestEnvelope) != 1 { + t.Fatalf("request body envelope keys \n\n%#v want only cacert", requestEnvelope) + } + rawCaCert, ok := requestEnvelope["cacert"] + if !ok { + t.Fatal("request body missing cacert envelope") + } + + var requestCaCertFields map[string]json.RawMessage + err = json.Unmarshal(rawCaCert, &requestCaCertFields) + if err != nil { + t.Fatal(err) + } + writableFields := []string{"name", "description", "content", "fileExtension", "filename"} + if len(requestCaCertFields) != len(writableFields) { + t.Errorf("request body cacert field count \n\n%#v want \n\n%#v", len(requestCaCertFields), len(writableFields)) + } + for _, field := range writableFields { + if _, ok := requestCaCertFields[field]; !ok { + t.Errorf("request body missing cacert.%s", field) + } + } + for _, field := range []string{"id", "expiresAt", "createdAt", "createdBy", "updatedAt", "updatedBy"} { + if _, ok := requestCaCertFields[field]; ok { + t.Errorf("request body includes response-only cacert.%s", field) + } + } + + requestCaCertificateV2Data := CaCertificateV2Input{} + err = json.Unmarshal(requestBody, &requestCaCertificateV2Data) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(requestCaCertificateV2Data.CaCert.Name, inputCaCertificateV2Data.CaCert.Name) { + t.Errorf("request body name \n\n%#v want \n\n%#v", requestCaCertificateV2Data.CaCert.Name, inputCaCertificateV2Data.CaCert.Name) + } + if !reflect.DeepEqual(requestCaCertificateV2Data.CaCert.Description, inputCaCertificateV2Data.CaCert.Description) { + t.Errorf("request body description \n\n%#v want \n\n%#v", requestCaCertificateV2Data.CaCert.Description, inputCaCertificateV2Data.CaCert.Description) + } + if !reflect.DeepEqual(requestCaCertificateV2Data.CaCert.Content, inputCaCertificateV2Data.CaCert.Content) { + t.Errorf("request body content \n\n%#v want \n\n%#v", requestCaCertificateV2Data.CaCert.Content, inputCaCertificateV2Data.CaCert.Content) + } + if !reflect.DeepEqual(requestCaCertificateV2Data.CaCert.FileExtension, inputCaCertificateV2Data.CaCert.FileExtension) { + t.Errorf("request body file extension \n\n%#v want \n\n%#v", requestCaCertificateV2Data.CaCert.FileExtension, inputCaCertificateV2Data.CaCert.FileExtension) + } + if !reflect.DeepEqual(requestCaCertificateV2Data.CaCert.Filename, inputCaCertificateV2Data.CaCert.Filename) { + t.Errorf("request body filename \n\n%#v want \n\n%#v", requestCaCertificateV2Data.CaCert.Filename, inputCaCertificateV2Data.CaCert.Filename) + } + + _, err = w.Write([]byte(createCaCertificateV2ResponseBody)) + if err != nil { + t.Fatal(err) + } + }) + + err := json.Unmarshal([]byte(createCaCertificateV2Body), &inputCaCertificateV2Data) + if err != nil { + t.Fatal(err) + } + err = json.Unmarshal([]byte(createCaCertificateV2ResponseBody), &outputCaCertificateV2Data) + if err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.CreateCaCertificateV2(&inputCaCertificateV2Data) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(resp.CaCert, outputCaCertificateV2Data.CaCert) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.CaCert, outputCaCertificateV2Data.CaCert) + } +} diff --git a/syntheticsclientv2/get_cacertificatesv2.go b/syntheticsclientv2/get_cacertificatesv2.go new file mode 100644 index 0000000..791a256 --- /dev/null +++ b/syntheticsclientv2/get_cacertificatesv2.go @@ -0,0 +1,44 @@ +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "bytes" + "encoding/json" +) + +func parseGetCaCertificatesV2Response(response string) (*CaCertificatesV2Response, error) { + var caCertificatesV2 CaCertificatesV2Response + err := json.Unmarshal([]byte(response), &caCertificatesV2) + if err != nil { + return nil, err + } + + return &caCertificatesV2, err +} + +func (c Client) GetCaCertificatesV2() (*CaCertificatesV2Response, *RequestDetails, error) { + details, err := c.makePublicAPICall("GET", "/cacerts", bytes.NewBufferString("{}"), nil) + if err != nil { + return nil, details, err + } + + caCertificatesV2, err := parseGetCaCertificatesV2Response(details.ResponseBody) + if err != nil { + return caCertificatesV2, details, err + } + + return caCertificatesV2, details, nil +} diff --git a/syntheticsclientv2/get_cacertificatesv2_test.go b/syntheticsclientv2/get_cacertificatesv2_test.go new file mode 100644 index 0000000..5b30388 --- /dev/null +++ b/syntheticsclientv2/get_cacertificatesv2_test.go @@ -0,0 +1,61 @@ +//go:build unit_tests +// +build unit_tests + +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "encoding/json" + "net/http" + "reflect" + "testing" +) + +var ( + getCaCertificatesV2Body = `{"cacerts":[{"id":1,"name":"foo","description":"My CA certificate","content":"","fileExtension":"pem","filename":"ca_cert_file","expiresAt":"2026-09-14T14:35:37.801Z","createdAt":"2022-09-14T14:35:37.801Z","createdBy":"abcdefgh1234","updatedAt":"2022-09-14T14:35:38.099Z","updatedBy":"abcdefgh1234"},{"id":2,"name":"bar","description":"Another CA certificate","content":"","fileExtension":"crt","filename":"ca_cert_file_2","expiresAt":"2027-09-14T14:35:37.801Z","createdAt":"2022-09-14T14:35:37.801Z","createdBy":"abcdefgh1234","updatedAt":"2022-09-14T14:35:38.099Z","updatedBy":"abcdefgh1234"}]}` + inputGetCaCertificatesV2 = verifyCaCertificatesV2Input(string(getCaCertificatesV2Body)) +) + +func TestGetCaCertificatesV2(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/cacerts", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte(getCaCertificatesV2Body)) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.GetCaCertificatesV2() + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(resp.CaCerts, inputGetCaCertificatesV2.CaCerts) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.CaCerts, inputGetCaCertificatesV2.CaCerts) + } +} + +func verifyCaCertificatesV2Input(stringInput string) *CaCertificatesV2Response { + check := &CaCertificatesV2Response{} + err := json.Unmarshal([]byte(stringInput), check) + if err != nil { + panic(err) + } + return check +} diff --git a/syntheticsclientv2/get_cacertificatev2.go b/syntheticsclientv2/get_cacertificatev2.go new file mode 100644 index 0000000..a5d8b57 --- /dev/null +++ b/syntheticsclientv2/get_cacertificatev2.go @@ -0,0 +1,45 @@ +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "bytes" + "encoding/json" + "fmt" +) + +func parseGetCaCertificateV2Response(response string) (*CaCertificateV2Response, error) { + var caCertificateV2 CaCertificateV2Response + err := json.Unmarshal([]byte(response), &caCertificateV2) + if err != nil { + return nil, err + } + + return &caCertificateV2, err +} + +func (c Client) GetCaCertificateV2(id int) (*CaCertificateV2Response, *RequestDetails, error) { + details, err := c.makePublicAPICall("GET", fmt.Sprintf("/cacerts/%d", id), bytes.NewBufferString("{}"), nil) + if err != nil { + return nil, details, err + } + + caCertificateV2, err := parseGetCaCertificateV2Response(details.ResponseBody) + if err != nil { + return caCertificateV2, details, err + } + + return caCertificateV2, details, nil +} diff --git a/syntheticsclientv2/get_cacertificatev2_test.go b/syntheticsclientv2/get_cacertificatev2_test.go new file mode 100644 index 0000000..c3fdc5a --- /dev/null +++ b/syntheticsclientv2/get_cacertificatev2_test.go @@ -0,0 +1,61 @@ +//go:build unit_tests +// +build unit_tests + +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "encoding/json" + "net/http" + "reflect" + "testing" +) + +var ( + getCaCertificateV2Body = `{"cacert":{"id":1,"name":"foo","description":"My CA certificate","content":"","fileExtension":"pem","filename":"ca_cert_file","expiresAt":"2026-09-14T14:35:37.801Z","createdAt":"2022-09-14T14:35:37.801Z","createdBy":"abcdefgh1234","updatedAt":"2022-09-14T14:35:38.099Z","updatedBy":"abcdefgh1234"}}` + inputGetCaCertificateV2 = verifyCaCertificateV2Input(string(getCaCertificateV2Body)) +) + +func TestGetCaCertificateV2(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/cacerts/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte(getCaCertificateV2Body)) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.GetCaCertificateV2(1) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(resp.CaCert, inputGetCaCertificateV2.CaCert) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.CaCert, inputGetCaCertificateV2.CaCert) + } +} + +func verifyCaCertificateV2Input(stringInput string) *CaCertificateV2Response { + check := &CaCertificateV2Response{} + err := json.Unmarshal([]byte(stringInput), check) + if err != nil { + panic(err) + } + return check +} From 647271d6e15cb217803f16521c73c33d76d45a85 Mon Sep 17 00:00:00 2001 From: Boris Valo Date: Fri, 12 Jun 2026 21:14:50 +0200 Subject: [PATCH 4/6] SYN-5569: add CA certificate update delete client --- syntheticsclientv2/common_models.go | 2 +- syntheticsclientv2/delete_cacertificatev2.go | 39 +++++++ .../delete_cacertificatev2_test.go | 41 +++++++ syntheticsclientv2/update_cacertificatev2.go | 52 +++++++++ .../update_cacertificatev2_test.go | 104 ++++++++++++++++++ 5 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 syntheticsclientv2/delete_cacertificatev2.go create mode 100644 syntheticsclientv2/delete_cacertificatev2_test.go create mode 100644 syntheticsclientv2/update_cacertificatev2.go create mode 100644 syntheticsclientv2/update_cacertificatev2_test.go diff --git a/syntheticsclientv2/common_models.go b/syntheticsclientv2/common_models.go index e95f87b..a934419 100644 --- a/syntheticsclientv2/common_models.go +++ b/syntheticsclientv2/common_models.go @@ -329,7 +329,7 @@ type CaCertificate struct { } type CaCertificateInput struct { - Name string `json:"name"` + Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` Content string `json:"content,omitempty"` FileExtension string `json:"fileExtension,omitempty"` diff --git a/syntheticsclientv2/delete_cacertificatev2.go b/syntheticsclientv2/delete_cacertificatev2.go new file mode 100644 index 0000000..7ffff08 --- /dev/null +++ b/syntheticsclientv2/delete_cacertificatev2.go @@ -0,0 +1,39 @@ +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "bytes" + "errors" + "fmt" + "strconv" +) + +func (c Client) DeleteCaCertificateV2(id int) (int, error) { + requestDetails, err := c.makePublicAPICall("DELETE", fmt.Sprintf("/cacerts/%d", id), bytes.NewBufferString("{}"), nil) + if err != nil { + return 1, err + } + var status = requestDetails.StatusCode + + fmt.Println(status) + + if status >= 300 || status < 200 { + errorMsg := fmt.Sprintf("error: Response code %v. Expecting 2XX.", strconv.Itoa(status)) + return status, errors.New(errorMsg) + } + + return status, err +} diff --git a/syntheticsclientv2/delete_cacertificatev2_test.go b/syntheticsclientv2/delete_cacertificatev2_test.go new file mode 100644 index 0000000..9c2a625 --- /dev/null +++ b/syntheticsclientv2/delete_cacertificatev2_test.go @@ -0,0 +1,41 @@ +//go:build unit_tests +// +build unit_tests + +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "net/http" + "testing" +) + +func TestDeleteCaCertificateV2(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/cacerts/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + + resp, err := testClient.DeleteCaCertificateV2(1) + if err != nil { + t.Fatal(err) + } + if resp != http.StatusNoContent { + t.Errorf("returned \n\n%#v want \n\n%#v", resp, http.StatusNoContent) + } +} diff --git a/syntheticsclientv2/update_cacertificatev2.go b/syntheticsclientv2/update_cacertificatev2.go new file mode 100644 index 0000000..2543781 --- /dev/null +++ b/syntheticsclientv2/update_cacertificatev2.go @@ -0,0 +1,52 @@ +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "bytes" + "encoding/json" + "fmt" +) + +func parseUpdateCaCertificateV2Response(response string) (*CaCertificateV2Response, error) { + var updateCaCertificateV2 CaCertificateV2Response + if response != "" { + err := json.Unmarshal([]byte(response), &updateCaCertificateV2) + if err != nil { + return nil, err + } + return &updateCaCertificateV2, err + } + return &updateCaCertificateV2, nil +} + +func (c Client) UpdateCaCertificateV2(id int, CaCertificateV2Details *CaCertificateV2Input) (*CaCertificateV2Response, *RequestDetails, error) { + body, err := json.Marshal(CaCertificateV2Details) + if err != nil { + return nil, nil, err + } + + requestDetails, err := c.makePublicAPICall("PUT", fmt.Sprintf("/cacerts/%d", id), bytes.NewBuffer(body), nil) + if err != nil { + return nil, requestDetails, err + } + + updateCaCertificateV2, err := parseUpdateCaCertificateV2Response(requestDetails.ResponseBody) + if err != nil { + return updateCaCertificateV2, requestDetails, err + } + + return updateCaCertificateV2, requestDetails, nil +} diff --git a/syntheticsclientv2/update_cacertificatev2_test.go b/syntheticsclientv2/update_cacertificatev2_test.go new file mode 100644 index 0000000..7a33cab --- /dev/null +++ b/syntheticsclientv2/update_cacertificatev2_test.go @@ -0,0 +1,104 @@ +//go:build unit_tests +// +build unit_tests + +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "encoding/json" + "io" + "net/http" + "testing" +) + +var ( + updateCaCertificateV2Body = `{"cacert":{"description":"Updated CA certificate","content":"Q2VydGlmaWNhdGU=","fileExtension":"pem","filename":"updated_ca_cert_file"}}` + inputCaCertificateV2Update = CaCertificateV2Input{} +) + +func TestUpdateCaCertificateV2(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/cacerts/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + assertCaCertificateUpdateRequestBody(t, r) + w.WriteHeader(http.StatusOK) + }) + + err := json.Unmarshal([]byte(updateCaCertificateV2Body), &inputCaCertificateV2Update) + if err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.UpdateCaCertificateV2(1, &inputCaCertificateV2Update) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("expected non-nil response for blank successful update body") + } +} + +func assertCaCertificateUpdateRequestBody(t *testing.T, r *http.Request) { + t.Helper() + + requestBody, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + var requestEnvelope map[string]json.RawMessage + err = json.Unmarshal(requestBody, &requestEnvelope) + if err != nil { + t.Fatal(err) + } + rawCaCert, ok := requestEnvelope["cacert"] + if !ok { + t.Fatal("request body missing cacert envelope") + } + + var requestCaCertFields map[string]json.RawMessage + err = json.Unmarshal(rawCaCert, &requestCaCertFields) + if err != nil { + t.Fatal(err) + } + + expectedFields := map[string]string{ + "description": `"Updated CA certificate"`, + "content": `"Q2VydGlmaWNhdGU="`, + "fileExtension": `"pem"`, + "filename": `"updated_ca_cert_file"`, + } + if len(requestCaCertFields) != len(expectedFields) { + t.Errorf("request body cacert field count %d want %d", len(requestCaCertFields), len(expectedFields)) + } + for field, expected := range expectedFields { + rawValue, ok := requestCaCertFields[field] + if !ok { + t.Errorf("request body missing cacert.%s", field) + continue + } + if string(rawValue) != expected { + t.Errorf("request body cacert.%s %s want %s", field, rawValue, expected) + } + } + for _, field := range []string{"name", "id", "expiresAt", "createdAt", "createdBy", "updatedAt", "updatedBy"} { + if _, ok := requestCaCertFields[field]; ok { + t.Errorf("request body should not include cacert.%s", field) + } + } +} From cde9486a357b01765b13d029eb31b519ac6ee4de Mon Sep 17 00:00:00 2001 From: Boris Valo Date: Sat, 13 Jun 2026 11:01:50 +0200 Subject: [PATCH 5/6] SYN-5569: preserve nullable SSL server name --- syntheticsclientv2/common_models.go | 4 +- syntheticsclientv2/create_sslcheckv2_test.go | 69 +++++++++++++++++++- syntheticsclientv2/get_sslcheckv2_test.go | 25 +++++++ syntheticsclientv2/update_sslcheckv2_test.go | 58 +++++++++++++++- 4 files changed, 148 insertions(+), 8 deletions(-) diff --git a/syntheticsclientv2/common_models.go b/syntheticsclientv2/common_models.go index a934419..750f9a8 100644 --- a/syntheticsclientv2/common_models.go +++ b/syntheticsclientv2/common_models.go @@ -361,7 +361,7 @@ type SslCheckV2Response struct { Type string `json:"type,omitempty"` Host string `json:"host,omitempty"` Port int `json:"port,omitempty"` - ServerName string `json:"serverName,omitempty"` + ServerName *string `json:"serverName,omitempty"` AllowSelfSigned bool `json:"allowSelfSigned"` AllowUntrustedRoot bool `json:"allowUntrustedRoot"` CaCertificateID *int `json:"caCertificateId"` @@ -389,7 +389,7 @@ type SslCheckV2Input struct { Automaticretries int `json:"automaticRetries"` Host string `json:"host"` Port int `json:"port"` - ServerName string `json:"serverName"` + ServerName *string `json:"serverName"` AllowSelfSigned bool `json:"allowSelfSigned"` AllowUntrustedRoot bool `json:"allowUntrustedRoot"` CaCertificateID *int `json:"caCertificateId"` diff --git a/syntheticsclientv2/create_sslcheckv2_test.go b/syntheticsclientv2/create_sslcheckv2_test.go index 9c176e5..679d0ce 100644 --- a/syntheticsclientv2/create_sslcheckv2_test.go +++ b/syntheticsclientv2/create_sslcheckv2_test.go @@ -22,6 +22,7 @@ import ( "io" "net/http" "reflect" + "strings" "testing" ) @@ -90,9 +91,7 @@ func TestCreateSslCheckV2(t *testing.T) { if !reflect.DeepEqual(requestSslCheckV2Data.Test.Port, 443) { t.Errorf("request body port \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.Port, 443) } - if !reflect.DeepEqual(requestSslCheckV2Data.Test.ServerName, "www.splunk.com") { - t.Errorf("request body server name \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.ServerName, "www.splunk.com") - } + assertStringPtr(t, requestSslCheckV2Data.Test.ServerName, "www.splunk.com") if !reflect.DeepEqual(requestSslCheckV2Data.Test.AllowSelfSigned, true) { t.Errorf("request body allow self signed \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.AllowSelfSigned, true) } @@ -160,6 +159,17 @@ func TestCreateSslCheckV2(t *testing.T) { } } +func assertStringPtr(t *testing.T, got *string, want string) { + t.Helper() + + if got == nil { + t.Fatalf("got nil string pointer want \n\n%#v", want) + } + if !reflect.DeepEqual(*got, want) { + t.Errorf("got string pointer value \n\n%#v want \n\n%#v", *got, want) + } +} + func TestCreateSslCheckV2DefaultsNilValidations(t *testing.T) { setup() defer teardown() @@ -221,3 +231,56 @@ func TestCreateSslCheckV2DefaultsNilValidations(t *testing.T) { t.Fatal(err) } } + +func TestCreateSslCheckV2PreservesNilServerName(t *testing.T) { + setup() + defer teardown() + + inputBody := strings.Replace(createSslCheckV2Body, `"serverName":"www.splunk.com"`, `"serverName":null`, 1) + inputSslCheckV2Data := SslCheckV2Input{} + err := json.Unmarshal([]byte(inputBody), &inputSslCheckV2Data) + if err != nil { + t.Fatal(err) + } + + testMux.HandleFunc("/tests/ssl", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + requestBody, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + var requestEnvelope map[string]json.RawMessage + err = json.Unmarshal(requestBody, &requestEnvelope) + if err != nil { + t.Fatal(err) + } + rawTest, ok := requestEnvelope["test"] + if !ok { + t.Fatal("request body missing test envelope") + } + + var requestTestFields map[string]json.RawMessage + err = json.Unmarshal(rawTest, &requestTestFields) + if err != nil { + t.Fatal(err) + } + rawServerName, ok := requestTestFields["serverName"] + if !ok { + t.Fatal("request body missing test.serverName") + } + if string(rawServerName) != "null" { + t.Fatalf("request body server name \n\n%#v want JSON null", string(rawServerName)) + } + + _, err = w.Write([]byte(createSslCheckV2Body)) + if err != nil { + t.Fatal(err) + } + }) + + _, _, err = testClient.CreateSslCheckV2(&inputSslCheckV2Data) + if err != nil { + t.Fatal(err) + } +} diff --git a/syntheticsclientv2/get_sslcheckv2_test.go b/syntheticsclientv2/get_sslcheckv2_test.go index c915964..9fa6ef9 100644 --- a/syntheticsclientv2/get_sslcheckv2_test.go +++ b/syntheticsclientv2/get_sslcheckv2_test.go @@ -21,6 +21,7 @@ import ( "encoding/json" "net/http" "reflect" + "strings" "testing" ) @@ -78,6 +79,30 @@ func TestGetSslCheckV2(t *testing.T) { } } +func TestGetSslCheckV2PreservesNilServerName(t *testing.T) { + setup() + defer teardown() + + responseBody := strings.Replace(getSslCheckV2Body, `"serverName":"www.splunk.com"`, `"serverName":null`, 1) + + testMux.HandleFunc("/tests/ssl/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte(responseBody)) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.GetSslCheckV2(1) + if err != nil { + t.Fatal(err) + } + + if resp.Test.ServerName != nil { + t.Fatalf("returned server name \n\n%#v want nil", resp.Test.ServerName) + } +} + func verifySslCheckV2Input(stringInput string) *SslCheckV2Response { check := &SslCheckV2Response{} err := json.Unmarshal([]byte(stringInput), check) diff --git a/syntheticsclientv2/update_sslcheckv2_test.go b/syntheticsclientv2/update_sslcheckv2_test.go index 9c6a7fd..190168f 100644 --- a/syntheticsclientv2/update_sslcheckv2_test.go +++ b/syntheticsclientv2/update_sslcheckv2_test.go @@ -22,6 +22,7 @@ import ( "io" "net/http" "reflect" + "strings" "testing" ) @@ -90,9 +91,7 @@ func TestUpdateSslCheckV2(t *testing.T) { if !reflect.DeepEqual(requestSslCheckV2Data.Test.Port, 8443) { t.Errorf("request body port \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.Port, 8443) } - if !reflect.DeepEqual(requestSslCheckV2Data.Test.ServerName, "example.com") { - t.Errorf("request body server name \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.ServerName, "example.com") - } + assertStringPtr(t, requestSslCheckV2Data.Test.ServerName, "example.com") if !reflect.DeepEqual(requestSslCheckV2Data.Test.AllowSelfSigned, false) { t.Errorf("request body allow self signed \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.AllowSelfSigned, false) } @@ -204,6 +203,59 @@ func TestUpdateSslCheckV2DefaultsNilValidations(t *testing.T) { } } +func TestUpdateSslCheckV2PreservesNilServerName(t *testing.T) { + setup() + defer teardown() + + inputBody := strings.Replace(updateSslCheckV2Body, `"serverName":"example.com"`, `"serverName":null`, 1) + inputSslCheckV2Data := SslCheckV2Input{} + err := json.Unmarshal([]byte(inputBody), &inputSslCheckV2Data) + if err != nil { + t.Fatal(err) + } + + testMux.HandleFunc("/tests/ssl/1653", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + requestBody, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + var requestEnvelope map[string]json.RawMessage + err = json.Unmarshal(requestBody, &requestEnvelope) + if err != nil { + t.Fatal(err) + } + rawTest, ok := requestEnvelope["test"] + if !ok { + t.Fatal("request body missing test envelope") + } + + var requestTestFields map[string]json.RawMessage + err = json.Unmarshal(rawTest, &requestTestFields) + if err != nil { + t.Fatal(err) + } + rawServerName, ok := requestTestFields["serverName"] + if !ok { + t.Fatal("request body missing test.serverName") + } + if string(rawServerName) != "null" { + t.Fatalf("request body server name \n\n%#v want JSON null", string(rawServerName)) + } + + _, err = w.Write([]byte(updateSslCheckV2Body)) + if err != nil { + t.Fatal(err) + } + }) + + _, _, err = testClient.UpdateSslCheckV2(1653, &inputSslCheckV2Data) + if err != nil { + t.Fatal(err) + } +} + func TestUpdateSslCheckV2BlankResponse(t *testing.T) { setup() defer teardown() From abf0d51600a22c0218f9d9e6e9f354b7bdaa1302 Mon Sep 17 00:00:00 2001 From: Boris Valo Date: Sat, 13 Jun 2026 15:04:56 +0200 Subject: [PATCH 6/6] SYN-5569 support partial SSL and CA certificate updates --- syntheticsclientv2/common_models.go | 100 +++++++++++++++ syntheticsclientv2/update_cacertificatev2.go | 2 +- .../update_cacertificatev2_test.go | 99 +++++++++++++-- syntheticsclientv2/update_sslcheckv2.go | 7 +- syntheticsclientv2/update_sslcheckv2_test.go | 120 +++++++++++++----- 5 files changed, 274 insertions(+), 54 deletions(-) diff --git a/syntheticsclientv2/common_models.go b/syntheticsclientv2/common_models.go index 750f9a8..c6dd41e 100644 --- a/syntheticsclientv2/common_models.go +++ b/syntheticsclientv2/common_models.go @@ -15,10 +15,79 @@ package syntheticsclientv2 import ( + "encoding/json" "time" ) // Common and shared struct models used for more complex requests +type NullableString struct { + Value *string +} + +func NewNullableString(value string) *NullableString { + return &NullableString{Value: &value} +} + +func NewNullString() *NullableString { + return &NullableString{} +} + +func (n NullableString) MarshalJSON() ([]byte, error) { + if n.Value == nil { + return []byte("null"), nil + } + + return json.Marshal(*n.Value) +} + +func (n *NullableString) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + n.Value = nil + return nil + } + + var value string + if err := json.Unmarshal(data, &value); err != nil { + return err + } + n.Value = &value + return nil +} + +type NullableInt struct { + Value *int +} + +func NewNullableInt(value int) *NullableInt { + return &NullableInt{Value: &value} +} + +func NewNullInt() *NullableInt { + return &NullableInt{} +} + +func (n NullableInt) MarshalJSON() ([]byte, error) { + if n.Value == nil { + return []byte("null"), nil + } + + return json.Marshal(*n.Value) +} + +func (n *NullableInt) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + n.Value = nil + return nil + } + + var value int + if err := json.Unmarshal(data, &value); err != nil { + return err + } + n.Value = &value + return nil +} + type Networkconnection struct { Description string `json:"description,omitempty"` Downloadbandwidth int `json:"downloadBandwidth,omitempty"` @@ -336,10 +405,22 @@ type CaCertificateInput struct { Filename string `json:"filename,omitempty"` } +type CaCertificateUpdateInput struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Content *string `json:"content,omitempty"` + FileExtension *string `json:"fileExtension,omitempty"` + Filename *string `json:"filename,omitempty"` +} + type CaCertificateV2Input struct { CaCert CaCertificateInput `json:"cacert"` } +type CaCertificateV2UpdateInput struct { + CaCert CaCertificateUpdateInput `json:"cacert"` +} + type CaCertificateV2Response struct { CaCert CaCertificate `json:"cacert"` } @@ -397,6 +478,25 @@ type SslCheckV2Input struct { } `json:"test"` } +type SslCheckV2UpdateInput struct { + Test struct { + Name *string `json:"name,omitempty"` + LocationIds *[]string `json:"locationIds,omitempty"` + Frequency *int `json:"frequency,omitempty"` + SchedulingStrategy *string `json:"schedulingStrategy,omitempty"` + Active *bool `json:"active,omitempty"` + Customproperties *[]CustomProperties `json:"customProperties,omitempty"` + Automaticretries *int `json:"automaticRetries,omitempty"` + Host *string `json:"host,omitempty"` + Port *int `json:"port,omitempty"` + ServerName *NullableString `json:"serverName,omitempty"` + AllowSelfSigned *bool `json:"allowSelfSigned,omitempty"` + AllowUntrustedRoot *bool `json:"allowUntrustedRoot,omitempty"` + CaCertificateID *NullableInt `json:"caCertificateId,omitempty"` + Validations *[]Validations `json:"validations,omitempty"` + } `json:"test"` +} + type PortCheckV2Response struct { Test struct { ID int `json:"id"` diff --git a/syntheticsclientv2/update_cacertificatev2.go b/syntheticsclientv2/update_cacertificatev2.go index 2543781..b213d58 100644 --- a/syntheticsclientv2/update_cacertificatev2.go +++ b/syntheticsclientv2/update_cacertificatev2.go @@ -32,7 +32,7 @@ func parseUpdateCaCertificateV2Response(response string) (*CaCertificateV2Respon return &updateCaCertificateV2, nil } -func (c Client) UpdateCaCertificateV2(id int, CaCertificateV2Details *CaCertificateV2Input) (*CaCertificateV2Response, *RequestDetails, error) { +func (c Client) UpdateCaCertificateV2(id int, CaCertificateV2Details *CaCertificateV2UpdateInput) (*CaCertificateV2Response, *RequestDetails, error) { body, err := json.Marshal(CaCertificateV2Details) if err != nil { return nil, nil, err diff --git a/syntheticsclientv2/update_cacertificatev2_test.go b/syntheticsclientv2/update_cacertificatev2_test.go index 7a33cab..1ca927a 100644 --- a/syntheticsclientv2/update_cacertificatev2_test.go +++ b/syntheticsclientv2/update_cacertificatev2_test.go @@ -26,7 +26,7 @@ import ( var ( updateCaCertificateV2Body = `{"cacert":{"description":"Updated CA certificate","content":"Q2VydGlmaWNhdGU=","fileExtension":"pem","filename":"updated_ca_cert_file"}}` - inputCaCertificateV2Update = CaCertificateV2Input{} + inputCaCertificateV2Update = CaCertificateV2UpdateInput{} ) func TestUpdateCaCertificateV2(t *testing.T) { @@ -53,29 +53,56 @@ func TestUpdateCaCertificateV2(t *testing.T) { } } -func assertCaCertificateUpdateRequestBody(t *testing.T, r *http.Request) { - t.Helper() +func TestUpdateCaCertificateV2AllowsEmptyDescription(t *testing.T) { + setup() + defer teardown() - requestBody, err := io.ReadAll(r.Body) - if err != nil { - t.Fatal(err) - } + description := "" + inputCaCertificateV2Update := CaCertificateV2UpdateInput{} + inputCaCertificateV2Update.CaCert.Description = &description - var requestEnvelope map[string]json.RawMessage - err = json.Unmarshal(requestBody, &requestEnvelope) + testMux.HandleFunc("/cacerts/2", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + assertSingleCaCertificateUpdateField(t, r, "description", `""`) + w.WriteHeader(http.StatusOK) + }) + + resp, _, err := testClient.UpdateCaCertificateV2(2, &inputCaCertificateV2Update) if err != nil { t.Fatal(err) } - rawCaCert, ok := requestEnvelope["cacert"] - if !ok { - t.Fatal("request body missing cacert envelope") + if resp == nil { + t.Fatal("expected non-nil response for blank successful update body") } +} - var requestCaCertFields map[string]json.RawMessage - err = json.Unmarshal(rawCaCert, &requestCaCertFields) +func TestUpdateCaCertificateV2AllowsNameUpdate(t *testing.T) { + setup() + defer teardown() + + name := "updated-ca-cert-name" + inputCaCertificateV2Update := CaCertificateV2UpdateInput{} + inputCaCertificateV2Update.CaCert.Name = &name + + testMux.HandleFunc("/cacerts/3", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + assertSingleCaCertificateUpdateField(t, r, "name", `"updated-ca-cert-name"`) + w.WriteHeader(http.StatusOK) + }) + + resp, _, err := testClient.UpdateCaCertificateV2(3, &inputCaCertificateV2Update) if err != nil { t.Fatal(err) } + if resp == nil { + t.Fatal("expected non-nil response for blank successful update body") + } +} + +func assertCaCertificateUpdateRequestBody(t *testing.T, r *http.Request) { + t.Helper() + + _, requestCaCertFields := readCaCertificateUpdateRequestFields(t, r) expectedFields := map[string]string{ "description": `"Updated CA certificate"`, @@ -102,3 +129,47 @@ func assertCaCertificateUpdateRequestBody(t *testing.T, r *http.Request) { } } } + +func assertSingleCaCertificateUpdateField(t *testing.T, r *http.Request, field string, expected string) { + t.Helper() + + requestBody, requestCaCertFields := readCaCertificateUpdateRequestFields(t, r) + if len(requestCaCertFields) != 1 { + t.Fatalf("request body cacert field count %d want 1: %s", len(requestCaCertFields), requestBody) + } + + rawValue, ok := requestCaCertFields[field] + if !ok { + t.Fatalf("request body missing cacert.%s", field) + } + if string(rawValue) != expected { + t.Fatalf("request body cacert.%s %s want %s", field, rawValue, expected) + } +} + +func readCaCertificateUpdateRequestFields(t *testing.T, r *http.Request) ([]byte, map[string]json.RawMessage) { + t.Helper() + + requestBody, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + var requestEnvelope map[string]json.RawMessage + err = json.Unmarshal(requestBody, &requestEnvelope) + if err != nil { + t.Fatal(err) + } + rawCaCert, ok := requestEnvelope["cacert"] + if !ok { + t.Fatal("request body missing cacert envelope") + } + + var requestCaCertFields map[string]json.RawMessage + err = json.Unmarshal(rawCaCert, &requestCaCertFields) + if err != nil { + t.Fatal(err) + } + + return requestBody, requestCaCertFields +} diff --git a/syntheticsclientv2/update_sslcheckv2.go b/syntheticsclientv2/update_sslcheckv2.go index 6c10c63..4e0ac3f 100644 --- a/syntheticsclientv2/update_sslcheckv2.go +++ b/syntheticsclientv2/update_sslcheckv2.go @@ -32,12 +32,7 @@ func parseUpdateSslCheckV2Response(response string) (*SslCheckV2Response, error) return &updateSslCheckV2, nil } -func (c Client) UpdateSslCheckV2(id int, SslCheckV2Details *SslCheckV2Input) (*SslCheckV2Response, *RequestDetails, error) { - if SslCheckV2Details.Test.Validations == nil { - validation := make([]Validations, 0) - SslCheckV2Details.Test.Validations = validation - } - +func (c Client) UpdateSslCheckV2(id int, SslCheckV2Details *SslCheckV2UpdateInput) (*SslCheckV2Response, *RequestDetails, error) { body, err := json.Marshal(SslCheckV2Details) if err != nil { return nil, nil, err diff --git a/syntheticsclientv2/update_sslcheckv2_test.go b/syntheticsclientv2/update_sslcheckv2_test.go index 190168f..8e4459d 100644 --- a/syntheticsclientv2/update_sslcheckv2_test.go +++ b/syntheticsclientv2/update_sslcheckv2_test.go @@ -22,13 +22,12 @@ import ( "io" "net/http" "reflect" - "strings" "testing" ) var ( updateSslCheckV2Body = `{"test":{"name":"ssl-check-updated","frequency":10,"schedulingStrategy":"concurrent","active":false,"locationIds":["aws-us-east-1"],"customProperties":[{"key":"env","value":"stage"}],"automaticRetries":2,"host":"example.com","port":8443,"serverName":"example.com","allowSelfSigned":false,"allowUntrustedRoot":true,"caCertificateId":42,"validations":[{"name":"Certificate expires later","type":"assert_numeric","actual":"{{certificate.days_until_expiration}}","expected":"15","comparator":"is_greater_than"}]}}` - inputSslCheckV2Update = SslCheckV2Input{} + inputSslCheckV2Update = SslCheckV2UpdateInput{} ) func TestUpdateSslCheckV2(t *testing.T) { @@ -80,31 +79,37 @@ func TestUpdateSslCheckV2(t *testing.T) { } } - requestSslCheckV2Data := SslCheckV2Input{} + requestSslCheckV2Data := SslCheckV2UpdateInput{} err = json.Unmarshal(requestBody, &requestSslCheckV2Data) if err != nil { t.Fatal(err) } - if !reflect.DeepEqual(requestSslCheckV2Data.Test.Host, "example.com") { + if requestSslCheckV2Data.Test.Host == nil || !reflect.DeepEqual(*requestSslCheckV2Data.Test.Host, "example.com") { t.Errorf("request body host \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.Host, "example.com") } - if !reflect.DeepEqual(requestSslCheckV2Data.Test.Port, 8443) { + if requestSslCheckV2Data.Test.Port == nil || !reflect.DeepEqual(*requestSslCheckV2Data.Test.Port, 8443) { t.Errorf("request body port \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.Port, 8443) } - assertStringPtr(t, requestSslCheckV2Data.Test.ServerName, "example.com") - if !reflect.DeepEqual(requestSslCheckV2Data.Test.AllowSelfSigned, false) { + if requestSslCheckV2Data.Test.ServerName == nil || + requestSslCheckV2Data.Test.ServerName.Value == nil || + !reflect.DeepEqual(*requestSslCheckV2Data.Test.ServerName.Value, "example.com") { + t.Errorf("request body server name \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.ServerName, "example.com") + } + if requestSslCheckV2Data.Test.AllowSelfSigned == nil || !reflect.DeepEqual(*requestSslCheckV2Data.Test.AllowSelfSigned, false) { t.Errorf("request body allow self signed \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.AllowSelfSigned, false) } - if !reflect.DeepEqual(requestSslCheckV2Data.Test.AllowUntrustedRoot, true) { + if requestSslCheckV2Data.Test.AllowUntrustedRoot == nil || !reflect.DeepEqual(*requestSslCheckV2Data.Test.AllowUntrustedRoot, true) { t.Errorf("request body allow untrusted root \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.AllowUntrustedRoot, true) } - if requestSslCheckV2Data.Test.CaCertificateID == nil || !reflect.DeepEqual(*requestSslCheckV2Data.Test.CaCertificateID, expectedCaCertificateID) { + if requestSslCheckV2Data.Test.CaCertificateID == nil || + requestSslCheckV2Data.Test.CaCertificateID.Value == nil || + !reflect.DeepEqual(*requestSslCheckV2Data.Test.CaCertificateID.Value, expectedCaCertificateID) { t.Errorf("request body ca certificate id \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.CaCertificateID, expectedCaCertificateID) } - if !reflect.DeepEqual(requestSslCheckV2Data.Test.Validations, expectedValidations) { + if requestSslCheckV2Data.Test.Validations == nil || !reflect.DeepEqual(*requestSslCheckV2Data.Test.Validations, expectedValidations) { t.Errorf("request body validations \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.Validations, expectedValidations) } - if !reflect.DeepEqual(requestSslCheckV2Data.Test.Customproperties, expectedCustomProperties) { + if requestSslCheckV2Data.Test.Customproperties == nil || !reflect.DeepEqual(*requestSslCheckV2Data.Test.Customproperties, expectedCustomProperties) { t.Errorf("request body custom properties \n\n%#v want \n\n%#v", requestSslCheckV2Data.Test.Customproperties, expectedCustomProperties) } @@ -124,33 +129,86 @@ func TestUpdateSslCheckV2(t *testing.T) { t.Fatal(err) } - if !reflect.DeepEqual(resp.Test.Name, inputSslCheckV2Update.Test.Name) { - t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Name, inputSslCheckV2Update.Test.Name) + if !reflect.DeepEqual(resp.Test.Name, "ssl-check-updated") { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Name, "ssl-check-updated") } - if !reflect.DeepEqual(resp.Test.Host, inputSslCheckV2Update.Test.Host) { - t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Host, inputSslCheckV2Update.Test.Host) + if !reflect.DeepEqual(resp.Test.Host, "example.com") { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Host, "example.com") } - if !reflect.DeepEqual(resp.Test.Port, inputSslCheckV2Update.Test.Port) { - t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Port, inputSslCheckV2Update.Test.Port) + if !reflect.DeepEqual(resp.Test.Port, 8443) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Port, 8443) } - if !reflect.DeepEqual(resp.Test.CaCertificateID, inputSslCheckV2Update.Test.CaCertificateID) { - t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.CaCertificateID, inputSslCheckV2Update.Test.CaCertificateID) + if resp.Test.CaCertificateID == nil || !reflect.DeepEqual(*resp.Test.CaCertificateID, expectedCaCertificateID) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.CaCertificateID, expectedCaCertificateID) } - if !reflect.DeepEqual(resp.Test.Validations, inputSslCheckV2Update.Test.Validations) { - t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Validations, inputSslCheckV2Update.Test.Validations) + if !reflect.DeepEqual(resp.Test.Validations, expectedValidations) { + t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Validations, expectedValidations) } } -func TestUpdateSslCheckV2DefaultsNilValidations(t *testing.T) { +func TestUpdateSslCheckV2AllowsPartialPayload(t *testing.T) { setup() defer teardown() - inputSslCheckV2Data := SslCheckV2Input{} - err := json.Unmarshal([]byte(updateSslCheckV2Body), &inputSslCheckV2Data) + active := false + inputSslCheckV2Update := SslCheckV2UpdateInput{} + inputSslCheckV2Update.Test.Active = &active + + testMux.HandleFunc("/tests/ssl/1654", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + + requestBody, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + var requestEnvelope map[string]json.RawMessage + err = json.Unmarshal(requestBody, &requestEnvelope) + if err != nil { + t.Fatal(err) + } + + rawTest, ok := requestEnvelope["test"] + if !ok { + t.Fatal("request body missing test envelope") + } + + var requestTestFields map[string]json.RawMessage + err = json.Unmarshal(rawTest, &requestTestFields) + if err != nil { + t.Fatal(err) + } + + if len(requestTestFields) != 1 { + t.Fatalf("request body test field count %d want 1: %s", len(requestTestFields), requestBody) + } + rawActive, ok := requestTestFields["active"] + if !ok { + t.Fatal("request body missing test.active") + } + if string(rawActive) != "false" { + t.Fatalf("request body test.active %s want false", rawActive) + } + + w.WriteHeader(http.StatusOK) + }) + + resp, _, err := testClient.UpdateSslCheckV2(1654, &inputSslCheckV2Update) if err != nil { t.Fatal(err) } - inputSslCheckV2Data.Test.Validations = nil + if resp == nil { + t.Fatal("expected non-nil response for blank successful update body") + } +} + +func TestUpdateSslCheckV2AllowsEmptyValidations(t *testing.T) { + setup() + defer teardown() + + inputSslCheckV2Data := SslCheckV2UpdateInput{} + validations := make([]Validations, 0) + inputSslCheckV2Data.Test.Validations = &validations testMux.HandleFunc("/tests/ssl/1651", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "PUT") @@ -197,7 +255,7 @@ func TestUpdateSslCheckV2DefaultsNilValidations(t *testing.T) { } }) - _, _, err = testClient.UpdateSslCheckV2(1651, &inputSslCheckV2Data) + _, _, err := testClient.UpdateSslCheckV2(1651, &inputSslCheckV2Data) if err != nil { t.Fatal(err) } @@ -207,12 +265,8 @@ func TestUpdateSslCheckV2PreservesNilServerName(t *testing.T) { setup() defer teardown() - inputBody := strings.Replace(updateSslCheckV2Body, `"serverName":"example.com"`, `"serverName":null`, 1) - inputSslCheckV2Data := SslCheckV2Input{} - err := json.Unmarshal([]byte(inputBody), &inputSslCheckV2Data) - if err != nil { - t.Fatal(err) - } + inputSslCheckV2Data := SslCheckV2UpdateInput{} + inputSslCheckV2Data.Test.ServerName = NewNullString() testMux.HandleFunc("/tests/ssl/1653", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "PUT") @@ -250,7 +304,7 @@ func TestUpdateSslCheckV2PreservesNilServerName(t *testing.T) { } }) - _, _, err = testClient.UpdateSslCheckV2(1653, &inputSslCheckV2Data) + _, _, err := testClient.UpdateSslCheckV2(1653, &inputSslCheckV2Data) if err != nil { t.Fatal(err) }