From 7ada368b57f7f44fe1e33c4cfe0af24d84ce71b4 Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Sat, 2 May 2026 13:49:46 +0500 Subject: [PATCH 01/16] Enable URL liveness check for release builds Signed-off-by: Abdulbois --- .github/workflows/release.yml | 1 + Makefile | 8 +++-- internal/config/config.go | 5 +++ internal/config/config_dev.go | 5 +++ utils/validator/validations.go | 47 ++++++++++++++++++++++++-- utils/validator/validations_test.go | 52 +++++++++++++++++++++++++++++ 6 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 internal/config/config.go create mode 100644 internal/config/config_dev.go create mode 100644 utils/validator/validations_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cb1e2c60d..ca15a1ee6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,7 @@ on: env: BIN_NAME: dcld COSMOVISOR_VERSION: 1.5.0 + URL_LIVENESS_CHECK_ENABLED: true jobs: diff --git a/Makefile b/Makefile index bf6a61daa..d0c5eda2e 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ endif NAME ?= dcl APPNAME ?= $(NAME)d LEDGER_ENABLED ?= true - +URL_LIVENESS_CHECK_ENABLED ?= true OUTPUT_DIR ?= build ### Process ld flags @@ -56,6 +56,10 @@ ifeq (cleveldb,$(findstring cleveldb,$(COSMOS_BUILD_OPTIONS))) build_tags += gcc endif +ifeq ($(URL_LIVENESS_CHECK_ENABLED),false) + build_tags += dev +endif + whitespace := whitespace += $(whitespace) comma := , @@ -110,7 +114,7 @@ go.sum: go.mod GO111MODULE=on go mod verify test: - go test -v $(PACKAGES) + URL_LIVENESS_CHECK_ENABLED=false go test -tags=dev -v $(PACKAGES) lint: golangci-lint run ./... --timeout 5m0s diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 000000000..0d3636893 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,5 @@ +//go:build !dev + +package config + +const DisableURLLivenessCheck = false diff --git a/internal/config/config_dev.go b/internal/config/config_dev.go new file mode 100644 index 000000000..4ec9614cb --- /dev/null +++ b/internal/config/config_dev.go @@ -0,0 +1,5 @@ +//go:build dev + +package config + +const DisableURLLivenessCheck = true diff --git a/utils/validator/validations.go b/utils/validator/validations.go index 83a10789c..88984aa95 100644 --- a/utils/validator/validations.go +++ b/utils/validator/validations.go @@ -15,10 +15,13 @@ package validator import ( + "net/http" "net/url" "reflect" + "time" "github.com/go-playground/validator/v10" + "github.com/zigbee-alliance/distributed-compliance-ledger/internal/config" ) func requiredIfBit0Set(fl validator.FieldLevel) bool { @@ -56,20 +59,58 @@ func isValidHttpsUrl(fl validator.FieldLevel) bool { //nolint:stylecheck return _validURL(fl, "https") } +var allowed4XXStatusCodes = []int{ + http.StatusUnauthorized, + http.StatusForbidden, + http.StatusUnavailableForLegalReasons, +} +var httpClient = &http.Client{Timeout: 10 * time.Second} + func _validURL(fl validator.FieldLevel, allowedSchemas ...string) bool { raw := fl.Field().String() - // Field is empty or omitempty is set, skip checks + // Field is empty, or omitempty is set, skip checks if raw == "" { return true } - u, _ := url.Parse(raw) - if u.Host == "" { + u, err := url.ParseRequestURI(raw) + if err != nil || u.Host == "" { return false } + isSchemaAllowed := false for _, schema := range allowedSchemas { if u.Scheme == schema { + isSchemaAllowed = true + break + } + } + + if _isLiveURL(u) || !isSchemaAllowed { + return isSchemaAllowed + } + + return false +} + +func _isLiveURL(u *url.URL) bool { + if config.DisableURLLivenessCheck { + return true + } + + // HEAD request only retrieves headers, not the body + resp, err := httpClient.Head(u.String()) + if err != nil { + return false + } + defer resp.Body.Close() + + if resp.StatusCode >= 200 && resp.StatusCode < 400 { + return true + } + + for _, code := range allowed4XXStatusCodes { + if code == resp.StatusCode { return true } } diff --git a/utils/validator/validations_test.go b/utils/validator/validations_test.go new file mode 100644 index 000000000..cf0982b28 --- /dev/null +++ b/utils/validator/validations_test.go @@ -0,0 +1,52 @@ +// Copyright 2020 DSR Corporation +// +// 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. + +//go:build !dev + +package validator + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestURLLivenessCheck(t *testing.T) { + negativeTests := []string{ + "https://dcl-test.org", + "https://httpbin.org/status/404", + "https://httpbin.org/status/500", + } + positiveTests := []string{ + "http://github.com/", // Redirects to https://github.com/ + "https://httpbin.org/status/401", // Private repo + "https://httpbin.org/status/403", // Unavailable for some reason + } + + for _, testUrl := range negativeTests { + u, err := url.ParseRequestURI(testUrl) + require.NoError(t, err) + + require.False(t, _isLiveURL(u)) + } + + for _, testUrl := range positiveTests { + u, err := url.ParseRequestURI(testUrl) + require.NoError(t, err) + + require.True(t, _isLiveURL(u)) + } + +} From b98d70068f43fd6308f2be5fad8abfb376b8f4f1 Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Sat, 2 May 2026 14:32:14 +0500 Subject: [PATCH 02/16] Refactor Signed-off-by: Abdulbois --- Makefile | 1 + .../{validations_test.go => url_liveness_test.go} | 1 - utils/validator/validations.go | 10 ++++++++-- 3 files changed, 9 insertions(+), 3 deletions(-) rename utils/validator/{validations_test.go => url_liveness_test.go} (99%) diff --git a/Makefile b/Makefile index d0c5eda2e..2dcab1080 100644 --- a/Makefile +++ b/Makefile @@ -115,6 +115,7 @@ go.sum: go.mod test: URL_LIVENESS_CHECK_ENABLED=false go test -tags=dev -v $(PACKAGES) + go test -v utils/validator/url_liveness_test.go lint: golangci-lint run ./... --timeout 5m0s diff --git a/utils/validator/validations_test.go b/utils/validator/url_liveness_test.go similarity index 99% rename from utils/validator/validations_test.go rename to utils/validator/url_liveness_test.go index cf0982b28..962ab87d4 100644 --- a/utils/validator/validations_test.go +++ b/utils/validator/url_liveness_test.go @@ -48,5 +48,4 @@ func TestURLLivenessCheck(t *testing.T) { require.True(t, _isLiveURL(u)) } - } diff --git a/utils/validator/validations.go b/utils/validator/validations.go index 88984aa95..39c797d8a 100644 --- a/utils/validator/validations.go +++ b/utils/validator/validations.go @@ -15,6 +15,7 @@ package validator import ( + "context" "net/http" "net/url" "reflect" @@ -82,6 +83,7 @@ func _validURL(fl validator.FieldLevel, allowedSchemas ...string) bool { for _, schema := range allowedSchemas { if u.Scheme == schema { isSchemaAllowed = true + break } } @@ -97,9 +99,13 @@ func _isLiveURL(u *url.URL) bool { if config.DisableURLLivenessCheck { return true } - // HEAD request only retrieves headers, not the body - resp, err := httpClient.Head(u.String()) + req, err := http.NewRequestWithContext(context.Background(), http.MethodHead, u.String(), nil) + if err != nil { + return false + } + + resp, err := httpClient.Do(req) if err != nil { return false } From 99c96d8a3ef97e64067f316cff4733dff1827b38 Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Sat, 2 May 2026 15:12:09 +0500 Subject: [PATCH 03/16] Fix dockerfiles Signed-off-by: Abdulbois --- Dockerfile-build | 1 + integration_tests/deploy/Dockerfile-build | 1 + 2 files changed, 2 insertions(+) diff --git a/Dockerfile-build b/Dockerfile-build index 8f2eaafdb..68d845c8e 100644 --- a/Dockerfile-build +++ b/Dockerfile-build @@ -39,6 +39,7 @@ COPY app ./app/ COPY cmd ./cmd/ COPY testutil ./testutil/ COPY utils ./utils/ +COPY internal ./internal/ COPY x ./x/ COPY types ./types/ COPY go.mod go.sum Makefile ./ diff --git a/integration_tests/deploy/Dockerfile-build b/integration_tests/deploy/Dockerfile-build index d5b1b5a91..ea0f94d07 100644 --- a/integration_tests/deploy/Dockerfile-build +++ b/integration_tests/deploy/Dockerfile-build @@ -25,6 +25,7 @@ COPY app ./app/ COPY cmd ./cmd/ COPY testutil ./testutil/ COPY utils ./utils/ +COPY internal ./internal/ COPY x ./x/ COPY types ./types/ COPY go.mod go.sum Makefile ./ From 45e70e358bd7624a5e80c7941568842f470bb216 Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Sat, 2 May 2026 15:14:22 +0500 Subject: [PATCH 04/16] Fix CI file Signed-off-by: Abdulbois --- .github/workflows/verify.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 10f6aece0..318739f87 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -107,7 +107,8 @@ jobs: run: | set -euo pipefail # shellcheck disable=SC2046 - go test -json -v $(go list ./... | grep -v '/integration_tests') -coverprofile=./cover.out -covermode=set -coverpkg=./... 2>&1 | tee /tmp/gotest.log | gotestfmt + go test -tags=dev -json -v $(go list ./... | grep -v '/integration_tests') -coverprofile=./cover.out -covermode=set -coverpkg=./... 2>&1 | tee /tmp/gotest.log | gotestfmt + go test -v -run TestURLLivenessCheck github.com/zigbee-alliance/distributed-compliance-ledger/utils/validator env: GH_TOKEN: ${{ secrets.GH_TOKEN }} GOCOVER_MODE: set From 3678b10cf89eb5570ba864d6c841003cfc03c7cc Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Mon, 4 May 2026 12:33:29 +0500 Subject: [PATCH 05/16] Update Ubuntu version to resolve image incompatibility issue in github CI Signed-off-by: Abdulbois --- integration_tests/deploy/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_tests/deploy/Dockerfile b/integration_tests/deploy/Dockerfile index fd8678312..bd22788af 100644 --- a/integration_tests/deploy/Dockerfile +++ b/integration_tests/deploy/Dockerfile @@ -15,7 +15,7 @@ ################################################################################ # Image of a clean machine to deploy the new node onto. ################################################################################ -FROM jrei/systemd-ubuntu:20.04 +FROM jrei/systemd-ubuntu:22.04 RUN apt-get update && apt-get install -y \ sudo \ From 014001f1267d40f456471190c8cc8077c96eec3a Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Mon, 4 May 2026 14:46:09 +0500 Subject: [PATCH 06/16] Disable URL liveness check for CI integration tests Signed-off-by: Abdulbois --- .github/workflows/verify.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 318739f87..7f2f84b32 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -141,7 +141,8 @@ jobs: run: integration_tests/run-all.sh all cover env: GH_TOKEN: ${{ secrets.GH_TOKEN }} - GOCOVER_MODE: set # Ensure integration tests use the same mode as unit tests + GOCOVER_MODE: set # Ensure integration tests use the same mode as unit tests\ + URL_LIVENESS_CHECK_ENABLED: false # Disable network calls to check URLs for integration tests - name: Generate test coverage shell: bash run: go tool covdata textfmt -i=integration_tests/gocover -o cover.out From 13c3757bcf40bfc2dd35cff93841410ffa9f9279 Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Mon, 4 May 2026 15:36:01 +0500 Subject: [PATCH 07/16] Refactor Signed-off-by: Abdulbois --- utils/validator/url_liveness_test.go | 52 ++++++++++++++++++---------- utils/validator/validations.go | 44 +++++++++++++---------- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/utils/validator/url_liveness_test.go b/utils/validator/url_liveness_test.go index 962ab87d4..00a6976cd 100644 --- a/utils/validator/url_liveness_test.go +++ b/utils/validator/url_liveness_test.go @@ -17,35 +17,49 @@ package validator import ( + "net/http" + "net/http/httptest" "net/url" "testing" "github.com/stretchr/testify/require" ) -func TestURLLivenessCheck(t *testing.T) { - negativeTests := []string{ - "https://dcl-test.org", - "https://httpbin.org/status/404", - "https://httpbin.org/status/500", - } - positiveTests := []string{ - "http://github.com/", // Redirects to https://github.com/ - "https://httpbin.org/status/401", // Private repo - "https://httpbin.org/status/403", // Unavailable for some reason +func TestIsLiveURL(t *testing.T) { + tests := []struct { + name string + statusCode int + want bool + }{ + {"200 OK", http.StatusOK, true}, + {"301 redirect", http.StatusMovedPermanently, true}, + {"401 unauthorized", http.StatusUnauthorized, true}, + {"403 forbidden", http.StatusForbidden, true}, + {"451 unavailable for legal reasons", http.StatusUnavailableForLegalReasons, true}, + {"404 not found", http.StatusNotFound, false}, + {"500 internal server error", http.StatusInternalServerError, false}, + {"502 bad gateway", http.StatusBadGateway, false}, } - for _, testUrl := range negativeTests { - u, err := url.ParseRequestURI(testUrl) - require.NoError(t, err) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodHead, r.Method) + w.WriteHeader(tt.statusCode) + })) + defer srv.Close() + + u, err := url.ParseRequestURI(srv.URL) + require.NoError(t, err) - require.False(t, _isLiveURL(u)) + require.Equal(t, tt.want, isLiveURL(u)) + }) } +} - for _, testUrl := range positiveTests { - u, err := url.ParseRequestURI(testUrl) - require.NoError(t, err) +func TestIsLiveURLUnreachable(t *testing.T) { + u, err := url.ParseRequestURI("http://192.0.2.1:1") + require.NoError(t, err) - require.True(t, _isLiveURL(u)) - } + require.False(t, isLiveURL(u)) } diff --git a/utils/validator/validations.go b/utils/validator/validations.go index 39c797d8a..c2f0880d2 100644 --- a/utils/validator/validations.go +++ b/utils/validator/validations.go @@ -53,11 +53,11 @@ func requiredIfBit0Set(fl validator.FieldLevel) bool { } func isValidHttpOrHttpsUrl(fl validator.FieldLevel) bool { //nolint:stylecheck - return _validURL(fl, "http", "https") + return validURL(fl, "http", "https") } func isValidHttpsUrl(fl validator.FieldLevel) bool { //nolint:stylecheck - return _validURL(fl, "https") + return validURL(fl, "https") } var allowed4XXStatusCodes = []int{ @@ -65,11 +65,15 @@ var allowed4XXStatusCodes = []int{ http.StatusForbidden, http.StatusUnavailableForLegalReasons, } -var httpClient = &http.Client{Timeout: 10 * time.Second} -func _validURL(fl validator.FieldLevel, allowedSchemas ...string) bool { +const ( + livenessCheckTimeout = 10 * time.Second +) + +var httpClient = &http.Client{Timeout: livenessCheckTimeout} + +func validURL(fl validator.FieldLevel, allowedSchemes ...string) bool { raw := fl.Field().String() - // Field is empty, or omitempty is set, skip checks if raw == "" { return true } @@ -79,28 +83,32 @@ func _validURL(fl validator.FieldLevel, allowedSchemas ...string) bool { return false } - isSchemaAllowed := false - for _, schema := range allowedSchemas { - if u.Scheme == schema { - isSchemaAllowed = true - - break - } + if !isSchemeAllowed(u.Scheme, allowedSchemes) { + return false } - if _isLiveURL(u) || !isSchemaAllowed { - return isSchemaAllowed + return isLiveURL(u) +} + +func isSchemeAllowed(scheme string, allowed []string) bool { + for _, s := range allowed { + if scheme == s { + return true + } } return false } -func _isLiveURL(u *url.URL) bool { +func isLiveURL(u *url.URL) bool { if config.DisableURLLivenessCheck { return true } - // HEAD request only retrieves headers, not the body - req, err := http.NewRequestWithContext(context.Background(), http.MethodHead, u.String(), nil) + + ctx, cancel := context.WithTimeout(context.Background(), livenessCheckTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodHead, u.String(), nil) if err != nil { return false } @@ -111,7 +119,7 @@ func _isLiveURL(u *url.URL) bool { } defer resp.Body.Close() - if resp.StatusCode >= 200 && resp.StatusCode < 400 { + if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusBadRequest { return true } From 187548c461e45df79aae1d17e09af7a96059b1ab Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Thu, 7 May 2026 17:40:41 +0500 Subject: [PATCH 08/16] Disable URL liveness checker in test Signed-off-by: Abdulbois --- Makefile | 2 +- integration_tests/run-all.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 2dcab1080..e91a40a4f 100644 --- a/Makefile +++ b/Makefile @@ -114,7 +114,7 @@ go.sum: go.mod GO111MODULE=on go mod verify test: - URL_LIVENESS_CHECK_ENABLED=false go test -tags=dev -v $(PACKAGES) + go test -tags=dev -v $(PACKAGES) go test -v utils/validator/url_liveness_test.go lint: diff --git a/integration_tests/run-all.sh b/integration_tests/run-all.sh index 8e1e6e98c..9800e7452 100755 --- a/integration_tests/run-all.sh +++ b/integration_tests/run-all.sh @@ -155,8 +155,8 @@ set -euo pipefail log "Verifying the environment" check_env -log "Compiling local binaries" -make install &>${DETAILED_OUTPUT_TARGET} +log "Compiling local binaries with URL liveness checker disabled" +URL_LIVENESS_CHECK_ENABLED=false make install &>${DETAILED_OUTPUT_TARGET} log "Building docker image" make image &>${DETAILED_OUTPUT_TARGET} From 97932b928e2afb7c0b8259ff2b75e356b1f88a7b Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Thu, 7 May 2026 19:58:00 +0500 Subject: [PATCH 09/16] Fix Signed-off-by: Abdulbois --- integration_tests/run-all.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_tests/run-all.sh b/integration_tests/run-all.sh index 9800e7452..84ff4aa6b 100755 --- a/integration_tests/run-all.sh +++ b/integration_tests/run-all.sh @@ -155,7 +155,7 @@ set -euo pipefail log "Verifying the environment" check_env -log "Compiling local binaries with URL liveness checker disabled" +log "Compiling local binaries. URL liveness check is DISABLED" URL_LIVENESS_CHECK_ENABLED=false make install &>${DETAILED_OUTPUT_TARGET} log "Building docker image" From 86daf85f6f50cac76b84b01a20ddb65efda2e679 Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Sat, 9 May 2026 14:14:56 +0500 Subject: [PATCH 10/16] Refactor URL liveness checks Signed-off-by: Abdulbois --- utils/cli/url_liveness.go | 86 +++++++++++ utils/cli/url_liveness_test.go | 133 ++++++++++++++++++ utils/validator/url_liveness_test.go | 65 --------- utils/validator/validations.go | 54 +------ x/model/client/cli/tx_model.go | 14 ++ x/model/client/cli/tx_model_version.go | 9 ++ ...x_add_pki_revocation_distribution_point.go | 5 + ...pdate_pki_revocation_distribution_point.go | 5 + x/vendorinfo/client/cli/tx_vendor_info.go | 10 ++ 9 files changed, 263 insertions(+), 118 deletions(-) create mode 100644 utils/cli/url_liveness.go create mode 100644 utils/cli/url_liveness_test.go delete mode 100644 utils/validator/url_liveness_test.go diff --git a/utils/cli/url_liveness.go b/utils/cli/url_liveness.go new file mode 100644 index 000000000..be3896337 --- /dev/null +++ b/utils/cli/url_liveness.go @@ -0,0 +1,86 @@ +package cli + +import ( + "context" + "net/http" + "sync" + "time" + + "github.com/zigbee-alliance/distributed-compliance-ledger/internal/config" +) + +const ( + livenessCheckTimeout = 10 * time.Second +) + +var allowed4XXStatusCodes = []int{ + http.StatusUnauthorized, + http.StatusForbidden, + http.StatusUnavailableForLegalReasons, +} +var httpClient = &http.Client{Timeout: livenessCheckTimeout} + +func IsLiveURL(u string) bool { + if config.DisableURLLivenessCheck { + return true + } + + ctx, cancel := context.WithTimeout(context.Background(), livenessCheckTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodHead, u, nil) + if err != nil { + return false + } + + resp, err := httpClient.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusBadRequest { + return true + } + + for _, code := range allowed4XXStatusCodes { + if code == resp.StatusCode { + return true + } + } + + return false +} + +// FirstUnreachableURL checks the liveness of the given URLs concurrently and +// returns the first one that is not reachable. +// Empty strings are skipped. +// +// Returns an empty string if all non-empty URLs are reachable. +func FirstUnreachableURL(urls ...string) string { + unreachable := make([]string, len(urls)) + + var wg sync.WaitGroup + for i, u := range urls { + if u == "" { + continue + } + // Call each URL concurrently + wg.Add(1) + go func(i int, u string) { + defer wg.Done() + if !IsLiveURL(u) { + unreachable[i] = u + } + }(i, u) + } + wg.Wait() + + for _, u := range unreachable { + if u != "" { + return u + } + } + + return "" +} diff --git a/utils/cli/url_liveness_test.go b/utils/cli/url_liveness_test.go new file mode 100644 index 000000000..a2146b17b --- /dev/null +++ b/utils/cli/url_liveness_test.go @@ -0,0 +1,133 @@ +// Copyright 2020 DSR Corporation +// +// 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. + +//go:build !dev + +package cli + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +const unreachableURL = "http://192.0.2.1:1" + +func TestIsLiveURL(t *testing.T) { + tests := []struct { + name string + statusCode int + want bool + }{ + {"200 OK", http.StatusOK, true}, + {"301 redirect", http.StatusMovedPermanently, true}, + {"401 unauthorized", http.StatusUnauthorized, true}, + {"403 forbidden", http.StatusForbidden, true}, + {"451 unavailable for legal reasons", http.StatusUnavailableForLegalReasons, true}, + {"404 not found", http.StatusNotFound, false}, + {"500 internal server error", http.StatusInternalServerError, false}, + {"502 bad gateway", http.StatusBadGateway, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodHead, r.Method) + w.WriteHeader(tt.statusCode) + })) + defer srv.Close() + + u, err := url.ParseRequestURI(srv.URL) + require.NoError(t, err) + + require.Equal(t, tt.want, IsLiveURL(u.String())) + }) + } +} + +func TestIsLiveURLUnreachable(t *testing.T) { + u, err := url.ParseRequestURI(unreachableURL) + require.NoError(t, err) + + require.False(t, IsLiveURL(u.String())) +} + +func TestFirstUnreachableURL(t *testing.T) { + okSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer okSrv.Close() + + notFoundSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer notFoundSrv.Close() + + tests := []struct { + name string + urls []string + want string + }{ + {"no URLs", nil, ""}, + {"all empty strings", []string{"", "", ""}, ""}, + {"all reachable", []string{okSrv.URL, okSrv.URL}, ""}, + {"single unreachable", []string{okSrv.URL, notFoundSrv.URL}, notFoundSrv.URL}, + {"empties skipped", []string{"", okSrv.URL, ""}, ""}, + { + "first unreachable wins over later unreachable", + []string{okSrv.URL, notFoundSrv.URL, unreachableURL}, + notFoundSrv.URL, + }, + { + "unreachable later in list", + []string{okSrv.URL, "", notFoundSrv.URL}, + notFoundSrv.URL, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, FirstUnreachableURL(tt.urls...)) + }) + } +} + +func TestFirstUnreachableURLRunsConcurrently(t *testing.T) { + const handlerDelay = 200 * time.Millisecond + const concurrentURLs = 5 + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(handlerDelay) + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + urls := make([]string, concurrentURLs) + for i := range urls { + urls[i] = srv.URL + } + + start := time.Now() + require.Equal(t, "", FirstUnreachableURL(urls...)) + elapsed := time.Since(start) + + // Sequential calls would take approximately concurrentURLs*handlerDelay time + // Concurrent execution should finish in roughly handlerDelay. + require.Less(t, elapsed, time.Duration(concurrentURLs)*handlerDelay/2, + "URL checks did not run concurrently (elapsed %s)", elapsed) +} diff --git a/utils/validator/url_liveness_test.go b/utils/validator/url_liveness_test.go deleted file mode 100644 index 00a6976cd..000000000 --- a/utils/validator/url_liveness_test.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2020 DSR Corporation -// -// 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. - -//go:build !dev - -package validator - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestIsLiveURL(t *testing.T) { - tests := []struct { - name string - statusCode int - want bool - }{ - {"200 OK", http.StatusOK, true}, - {"301 redirect", http.StatusMovedPermanently, true}, - {"401 unauthorized", http.StatusUnauthorized, true}, - {"403 forbidden", http.StatusForbidden, true}, - {"451 unavailable for legal reasons", http.StatusUnavailableForLegalReasons, true}, - {"404 not found", http.StatusNotFound, false}, - {"500 internal server error", http.StatusInternalServerError, false}, - {"502 bad gateway", http.StatusBadGateway, false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodHead, r.Method) - w.WriteHeader(tt.statusCode) - })) - defer srv.Close() - - u, err := url.ParseRequestURI(srv.URL) - require.NoError(t, err) - - require.Equal(t, tt.want, isLiveURL(u)) - }) - } -} - -func TestIsLiveURLUnreachable(t *testing.T) { - u, err := url.ParseRequestURI("http://192.0.2.1:1") - require.NoError(t, err) - - require.False(t, isLiveURL(u)) -} diff --git a/utils/validator/validations.go b/utils/validator/validations.go index c2f0880d2..5eccf435e 100644 --- a/utils/validator/validations.go +++ b/utils/validator/validations.go @@ -15,14 +15,10 @@ package validator import ( - "context" - "net/http" "net/url" "reflect" - "time" "github.com/go-playground/validator/v10" - "github.com/zigbee-alliance/distributed-compliance-ledger/internal/config" ) func requiredIfBit0Set(fl validator.FieldLevel) bool { @@ -60,18 +56,6 @@ func isValidHttpsUrl(fl validator.FieldLevel) bool { //nolint:stylecheck return validURL(fl, "https") } -var allowed4XXStatusCodes = []int{ - http.StatusUnauthorized, - http.StatusForbidden, - http.StatusUnavailableForLegalReasons, -} - -const ( - livenessCheckTimeout = 10 * time.Second -) - -var httpClient = &http.Client{Timeout: livenessCheckTimeout} - func validURL(fl validator.FieldLevel, allowedSchemes ...string) bool { raw := fl.Field().String() if raw == "" { @@ -83,11 +67,7 @@ func validURL(fl validator.FieldLevel, allowedSchemes ...string) bool { return false } - if !isSchemeAllowed(u.Scheme, allowedSchemes) { - return false - } - - return isLiveURL(u) + return isSchemeAllowed(u.Scheme, allowedSchemes) } func isSchemeAllowed(scheme string, allowed []string) bool { @@ -99,35 +79,3 @@ func isSchemeAllowed(scheme string, allowed []string) bool { return false } - -func isLiveURL(u *url.URL) bool { - if config.DisableURLLivenessCheck { - return true - } - - ctx, cancel := context.WithTimeout(context.Background(), livenessCheckTimeout) - defer cancel() - - req, err := http.NewRequestWithContext(ctx, http.MethodHead, u.String(), nil) - if err != nil { - return false - } - - resp, err := httpClient.Do(req) - if err != nil { - return false - } - defer resp.Body.Close() - - if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusBadRequest { - return true - } - - for _, code := range allowed4XXStatusCodes { - if code == resp.StatusCode { - return true - } - } - - return false -} diff --git a/x/model/client/cli/tx_model.go b/x/model/client/cli/tx_model.go index efa6ef82a..16084d212 100644 --- a/x/model/client/cli/tx_model.go +++ b/x/model/client/cli/tx_model.go @@ -1,6 +1,8 @@ package cli import ( + "fmt" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" @@ -99,6 +101,12 @@ func CmdCreateModel() *cobra.Command { commissioningFallbackURL, ) + unreachable := cli.FirstUnreachableURL(commissioningCustomFlowURL, userManualURL, supportURL, productURL, lsfURL, + enhancedSetupFlowTCURL, enhancedSetupFlowMaintenanceURL, commissioningFallbackURL) + if unreachable != "" { + return fmt.Errorf("%v is not reachable", unreachable) + } + // validate basic will be called in GenerateOrBroadcastTxCLI err = tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) if cli.IsWriteInsteadReadRPCError(err) { @@ -284,6 +292,12 @@ func CmdUpdateModel() *cobra.Command { factoryResetStepsInstruction, ) + unreachable := cli.FirstUnreachableURL(commissioningCustomFlowURL, userManualURL, supportURL, productURL, lsfURL, + enhancedSetupFlowTCURL, enhancedSetupFlowMaintenanceURL, commissioningFallbackURL) + if unreachable != "" { + return fmt.Errorf("%v is not reachable", unreachable) + } + // validate basic will be called in GenerateOrBroadcastTxCLI err = tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) if cli.IsWriteInsteadReadRPCError(err) { diff --git a/x/model/client/cli/tx_model_version.go b/x/model/client/cli/tx_model_version.go index 4ec678abe..32f418430 100644 --- a/x/model/client/cli/tx_model_version.go +++ b/x/model/client/cli/tx_model_version.go @@ -1,6 +1,8 @@ package cli import ( + "fmt" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" @@ -59,6 +61,9 @@ func CmdCreateModelVersion() *cobra.Command { specificationVersion, ) + if releaseNotesURL != "" && cli.IsLiveURL(releaseNotesURL) { + return fmt.Errorf("%s is not reachable", releaseNotesURL) + } // validate basic will be called in GenerateOrBroadcastTxCLI err = tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) if cli.IsWriteInsteadReadRPCError(err) { @@ -166,6 +171,10 @@ func CmdUpdateModelVersion() *cobra.Command { schemaVersion, ) + if releaseNotesURL != "" && cli.IsLiveURL(releaseNotesURL) { + return fmt.Errorf("%s is not reachable", releaseNotesURL) + } + // validate basic will be called in GenerateOrBroadcastTxCLI err = tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) if cli.IsWriteInsteadReadRPCError(err) { diff --git a/x/pki/client/cli/tx_add_pki_revocation_distribution_point.go b/x/pki/client/cli/tx_add_pki_revocation_distribution_point.go index 0fa50d384..b152c35a0 100644 --- a/x/pki/client/cli/tx_add_pki_revocation_distribution_point.go +++ b/x/pki/client/cli/tx_add_pki_revocation_distribution_point.go @@ -1,6 +1,7 @@ package cli import ( + "fmt" "strconv" "github.com/cosmos/cosmos-sdk/client" @@ -69,6 +70,10 @@ func CmdAddPkiRevocationDistributionPoint() *cobra.Command { schemaVersion, ) + if dataURL != "" && cli.IsLiveURL(dataURL) { + return fmt.Errorf("%s is not reachable", dataURL) + } + // validate basic will be called in GenerateOrBroadcastTxCLI err = tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) if cli.IsWriteInsteadReadRPCError(err) { diff --git a/x/pki/client/cli/tx_update_pki_revocation_distribution_point.go b/x/pki/client/cli/tx_update_pki_revocation_distribution_point.go index deca28601..afa1ad9d3 100644 --- a/x/pki/client/cli/tx_update_pki_revocation_distribution_point.go +++ b/x/pki/client/cli/tx_update_pki_revocation_distribution_point.go @@ -1,6 +1,7 @@ package cli import ( + "fmt" "strconv" "github.com/cosmos/cosmos-sdk/client" @@ -63,6 +64,10 @@ func CmdUpdatePkiRevocationDistributionPoint() *cobra.Command { schemaVersion, ) + if dataURL != "" && cli.IsLiveURL(dataURL) { + return fmt.Errorf("%s is not reachable", dataURL) + } + // validate basic will be called in GenerateOrBroadcastTxCLI err = tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) if cli.IsWriteInsteadReadRPCError(err) { diff --git a/x/vendorinfo/client/cli/tx_vendor_info.go b/x/vendorinfo/client/cli/tx_vendor_info.go index edf498f25..18fba649a 100644 --- a/x/vendorinfo/client/cli/tx_vendor_info.go +++ b/x/vendorinfo/client/cli/tx_vendor_info.go @@ -1,6 +1,8 @@ package cli import ( + "fmt" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" @@ -40,6 +42,10 @@ func CmdCreateVendorInfo() *cobra.Command { schemaVersion, ) + if vendorLandingPageURL != "" && cli.IsLiveURL(vendorLandingPageURL) { + return fmt.Errorf("%s is not reachable", vendorLandingPageURL) + } + // validate basic will be called in GenerateOrBroadcastTxCLI err = tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) if cli.IsWriteInsteadReadRPCError(err) { @@ -101,6 +107,10 @@ func CmdUpdateVendorInfo() *cobra.Command { schemaVersion, ) + if vendorLandingPageURL != "" && cli.IsLiveURL(vendorLandingPageURL) { + return fmt.Errorf("%s is not reachable", vendorLandingPageURL) + } + // validate basic will be called in GenerateOrBroadcastTxCLI err = tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) if cli.IsWriteInsteadReadRPCError(err) { From 3ecb03782dcaa2bc0bd8e1bc3f237d3461c500bb Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Sat, 9 May 2026 14:33:16 +0500 Subject: [PATCH 11/16] Update CI and Make files to run URL liveness checks Signed-off-by: Abdulbois --- .github/workflows/verify.yml | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 7f2f84b32..646825320 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -108,7 +108,7 @@ jobs: set -euo pipefail # shellcheck disable=SC2046 go test -tags=dev -json -v $(go list ./... | grep -v '/integration_tests') -coverprofile=./cover.out -covermode=set -coverpkg=./... 2>&1 | tee /tmp/gotest.log | gotestfmt - go test -v -run TestURLLivenessCheck github.com/zigbee-alliance/distributed-compliance-ledger/utils/validator + go test -v utils/cli/url_liveness.go utils/cli/url_liveness_test.go env: GH_TOKEN: ${{ secrets.GH_TOKEN }} GOCOVER_MODE: set diff --git a/Makefile b/Makefile index e91a40a4f..1a00fff62 100644 --- a/Makefile +++ b/Makefile @@ -115,7 +115,7 @@ go.sum: go.mod test: go test -tags=dev -v $(PACKAGES) - go test -v utils/validator/url_liveness_test.go + go test -v utils/cli/url_liveness.go utils/cli/url_liveness_test.go lint: golangci-lint run ./... --timeout 5m0s From ae9c4dc383dbb3f38796fe206cb1d01a0cb114d9 Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Sat, 9 May 2026 14:44:01 +0500 Subject: [PATCH 12/16] Fix `if` conditions Signed-off-by: Abdulbois --- x/model/client/cli/tx_model_version.go | 4 ++-- x/pki/client/cli/tx_add_pki_revocation_distribution_point.go | 2 +- .../client/cli/tx_update_pki_revocation_distribution_point.go | 2 +- x/vendorinfo/client/cli/tx_vendor_info.go | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x/model/client/cli/tx_model_version.go b/x/model/client/cli/tx_model_version.go index 32f418430..05729e7cf 100644 --- a/x/model/client/cli/tx_model_version.go +++ b/x/model/client/cli/tx_model_version.go @@ -61,7 +61,7 @@ func CmdCreateModelVersion() *cobra.Command { specificationVersion, ) - if releaseNotesURL != "" && cli.IsLiveURL(releaseNotesURL) { + if releaseNotesURL != "" && !cli.IsLiveURL(releaseNotesURL) { return fmt.Errorf("%s is not reachable", releaseNotesURL) } // validate basic will be called in GenerateOrBroadcastTxCLI @@ -171,7 +171,7 @@ func CmdUpdateModelVersion() *cobra.Command { schemaVersion, ) - if releaseNotesURL != "" && cli.IsLiveURL(releaseNotesURL) { + if releaseNotesURL != "" && !cli.IsLiveURL(releaseNotesURL) { return fmt.Errorf("%s is not reachable", releaseNotesURL) } diff --git a/x/pki/client/cli/tx_add_pki_revocation_distribution_point.go b/x/pki/client/cli/tx_add_pki_revocation_distribution_point.go index b152c35a0..efff903c1 100644 --- a/x/pki/client/cli/tx_add_pki_revocation_distribution_point.go +++ b/x/pki/client/cli/tx_add_pki_revocation_distribution_point.go @@ -70,7 +70,7 @@ func CmdAddPkiRevocationDistributionPoint() *cobra.Command { schemaVersion, ) - if dataURL != "" && cli.IsLiveURL(dataURL) { + if dataURL != "" && !cli.IsLiveURL(dataURL) { return fmt.Errorf("%s is not reachable", dataURL) } diff --git a/x/pki/client/cli/tx_update_pki_revocation_distribution_point.go b/x/pki/client/cli/tx_update_pki_revocation_distribution_point.go index afa1ad9d3..5f1c14ea8 100644 --- a/x/pki/client/cli/tx_update_pki_revocation_distribution_point.go +++ b/x/pki/client/cli/tx_update_pki_revocation_distribution_point.go @@ -64,7 +64,7 @@ func CmdUpdatePkiRevocationDistributionPoint() *cobra.Command { schemaVersion, ) - if dataURL != "" && cli.IsLiveURL(dataURL) { + if dataURL != "" && !cli.IsLiveURL(dataURL) { return fmt.Errorf("%s is not reachable", dataURL) } diff --git a/x/vendorinfo/client/cli/tx_vendor_info.go b/x/vendorinfo/client/cli/tx_vendor_info.go index 18fba649a..526a2754a 100644 --- a/x/vendorinfo/client/cli/tx_vendor_info.go +++ b/x/vendorinfo/client/cli/tx_vendor_info.go @@ -42,7 +42,7 @@ func CmdCreateVendorInfo() *cobra.Command { schemaVersion, ) - if vendorLandingPageURL != "" && cli.IsLiveURL(vendorLandingPageURL) { + if vendorLandingPageURL != "" && !cli.IsLiveURL(vendorLandingPageURL) { return fmt.Errorf("%s is not reachable", vendorLandingPageURL) } @@ -107,7 +107,7 @@ func CmdUpdateVendorInfo() *cobra.Command { schemaVersion, ) - if vendorLandingPageURL != "" && cli.IsLiveURL(vendorLandingPageURL) { + if vendorLandingPageURL != "" && !cli.IsLiveURL(vendorLandingPageURL) { return fmt.Errorf("%s is not reachable", vendorLandingPageURL) } From 4056ef4a4d4e60ca580f4621707f011be646b519 Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Mon, 11 May 2026 13:48:32 +0500 Subject: [PATCH 13/16] Revert golang image version change Signed-off-by: Abdulbois --- integration_tests/deploy/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_tests/deploy/Dockerfile b/integration_tests/deploy/Dockerfile index bd22788af..fd8678312 100644 --- a/integration_tests/deploy/Dockerfile +++ b/integration_tests/deploy/Dockerfile @@ -15,7 +15,7 @@ ################################################################################ # Image of a clean machine to deploy the new node onto. ################################################################################ -FROM jrei/systemd-ubuntu:22.04 +FROM jrei/systemd-ubuntu:20.04 RUN apt-get update && apt-get install -y \ sudo \ From 9e47934bd136ba2c84d14809fc4d5097de21b11e Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Tue, 12 May 2026 10:41:30 +0500 Subject: [PATCH 14/16] Enable checker in missed places Signed-off-by: Abdulbois --- utils/cli/url_liveness.go | 1 + utils/cli/url_liveness_test.go | 1 + x/model/client/cli/tx_model_version.go | 10 ++++++---- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/utils/cli/url_liveness.go b/utils/cli/url_liveness.go index be3896337..1895bb0ee 100644 --- a/utils/cli/url_liveness.go +++ b/utils/cli/url_liveness.go @@ -17,6 +17,7 @@ var allowed4XXStatusCodes = []int{ http.StatusUnauthorized, http.StatusForbidden, http.StatusUnavailableForLegalReasons, + http.StatusMethodNotAllowed, } var httpClient = &http.Client{Timeout: livenessCheckTimeout} diff --git a/utils/cli/url_liveness_test.go b/utils/cli/url_liveness_test.go index a2146b17b..b234a7521 100644 --- a/utils/cli/url_liveness_test.go +++ b/utils/cli/url_liveness_test.go @@ -38,6 +38,7 @@ func TestIsLiveURL(t *testing.T) { {"301 redirect", http.StatusMovedPermanently, true}, {"401 unauthorized", http.StatusUnauthorized, true}, {"403 forbidden", http.StatusForbidden, true}, + {"405 method not allowed", http.StatusMethodNotAllowed, true}, {"451 unavailable for legal reasons", http.StatusUnavailableForLegalReasons, true}, {"404 not found", http.StatusNotFound, false}, {"500 internal server error", http.StatusInternalServerError, false}, diff --git a/x/model/client/cli/tx_model_version.go b/x/model/client/cli/tx_model_version.go index 05729e7cf..79ed6b6aa 100644 --- a/x/model/client/cli/tx_model_version.go +++ b/x/model/client/cli/tx_model_version.go @@ -61,8 +61,9 @@ func CmdCreateModelVersion() *cobra.Command { specificationVersion, ) - if releaseNotesURL != "" && !cli.IsLiveURL(releaseNotesURL) { - return fmt.Errorf("%s is not reachable", releaseNotesURL) + unreachable := cli.FirstUnreachableURL(otaURL, releaseNotesURL) + if unreachable != "" { + return fmt.Errorf("%s is not reachable", unreachable) } // validate basic will be called in GenerateOrBroadcastTxCLI err = tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) @@ -171,8 +172,9 @@ func CmdUpdateModelVersion() *cobra.Command { schemaVersion, ) - if releaseNotesURL != "" && !cli.IsLiveURL(releaseNotesURL) { - return fmt.Errorf("%s is not reachable", releaseNotesURL) + unreachable := cli.FirstUnreachableURL(otaURL, releaseNotesURL) + if unreachable != "" { + return fmt.Errorf("%s is not reachable", unreachable) } // validate basic will be called in GenerateOrBroadcastTxCLI From 2d8fc46d1c3338add5dc646727a9e407b1f29f96 Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Thu, 14 May 2026 16:43:42 +0500 Subject: [PATCH 15/16] Improve namings and impl Signed-off-by: Abdulbois --- internal/config/config.go | 2 +- internal/config/config_dev.go | 2 +- utils/cli/url_liveness.go | 21 +++++++++++---------- utils/cli/url_liveness_test.go | 26 +++++++++++++------------- x/model/client/cli/tx_model.go | 13 +++++++------ x/model/client/cli/tx_model_version.go | 13 +++++++------ 6 files changed, 40 insertions(+), 37 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 0d3636893..d1aefea1f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,4 +2,4 @@ package config -const DisableURLLivenessCheck = false +const URLLivenessCheckEnabled = true diff --git a/internal/config/config_dev.go b/internal/config/config_dev.go index 4ec9614cb..9e82b500d 100644 --- a/internal/config/config_dev.go +++ b/internal/config/config_dev.go @@ -2,4 +2,4 @@ package config -const DisableURLLivenessCheck = true +const URLLivenessCheckEnabled = false diff --git a/utils/cli/url_liveness.go b/utils/cli/url_liveness.go index 1895bb0ee..f980126d0 100644 --- a/utils/cli/url_liveness.go +++ b/utils/cli/url_liveness.go @@ -22,7 +22,7 @@ var allowed4XXStatusCodes = []int{ var httpClient = &http.Client{Timeout: livenessCheckTimeout} func IsLiveURL(u string) bool { - if config.DisableURLLivenessCheck { + if !config.URLLivenessCheckEnabled { return true } @@ -53,13 +53,13 @@ func IsLiveURL(u string) bool { return false } -// FirstUnreachableURL checks the liveness of the given URLs concurrently and -// returns the first one that is not reachable. +// CheckURLsForLiveness checks the liveness of the given URLs concurrently and +// returns unreachable URLs as a list. // Empty strings are skipped. // -// Returns an empty string if all non-empty URLs are reachable. -func FirstUnreachableURL(urls ...string) string { - unreachable := make([]string, len(urls)) +// Returns an empty list if all non-empty URLs are reachable. +func CheckURLsForLiveness(urls ...string) []string { + results := make([]string, len(urls)) var wg sync.WaitGroup for i, u := range urls { @@ -71,17 +71,18 @@ func FirstUnreachableURL(urls ...string) string { go func(i int, u string) { defer wg.Done() if !IsLiveURL(u) { - unreachable[i] = u + results[i] = u } }(i, u) } wg.Wait() - for _, u := range unreachable { + var unreachable []string + for _, u := range results { if u != "" { - return u + unreachable = append(unreachable, u) } } - return "" + return unreachable } diff --git a/utils/cli/url_liveness_test.go b/utils/cli/url_liveness_test.go index b234a7521..ddf353a6b 100644 --- a/utils/cli/url_liveness_test.go +++ b/utils/cli/url_liveness_test.go @@ -68,7 +68,7 @@ func TestIsLiveURLUnreachable(t *testing.T) { require.False(t, IsLiveURL(u.String())) } -func TestFirstUnreachableURL(t *testing.T) { +func TestCheckURLsForLiveness(t *testing.T) { okSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) @@ -82,33 +82,33 @@ func TestFirstUnreachableURL(t *testing.T) { tests := []struct { name string urls []string - want string + want []string }{ - {"no URLs", nil, ""}, - {"all empty strings", []string{"", "", ""}, ""}, - {"all reachable", []string{okSrv.URL, okSrv.URL}, ""}, - {"single unreachable", []string{okSrv.URL, notFoundSrv.URL}, notFoundSrv.URL}, - {"empties skipped", []string{"", okSrv.URL, ""}, ""}, + {"no URLs", nil, nil}, + {"all empty strings", []string{"", "", ""}, nil}, + {"all reachable", []string{okSrv.URL, okSrv.URL}, nil}, + {"single unreachable", []string{okSrv.URL, notFoundSrv.URL}, []string{notFoundSrv.URL}}, + {"empties skipped", []string{"", okSrv.URL, ""}, nil}, { - "first unreachable wins over later unreachable", + "multiple unreachable preserve input order", []string{okSrv.URL, notFoundSrv.URL, unreachableURL}, - notFoundSrv.URL, + []string{notFoundSrv.URL, unreachableURL}, }, { "unreachable later in list", []string{okSrv.URL, "", notFoundSrv.URL}, - notFoundSrv.URL, + []string{notFoundSrv.URL}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.want, FirstUnreachableURL(tt.urls...)) + require.Equal(t, tt.want, CheckURLsForLiveness(tt.urls...)) }) } } -func TestFirstUnreachableURLRunsConcurrently(t *testing.T) { +func TestCheckURLsForLivenessRunsConcurrently(t *testing.T) { const handlerDelay = 200 * time.Millisecond const concurrentURLs = 5 @@ -124,7 +124,7 @@ func TestFirstUnreachableURLRunsConcurrently(t *testing.T) { } start := time.Now() - require.Equal(t, "", FirstUnreachableURL(urls...)) + require.Empty(t, CheckURLsForLiveness(urls...)) elapsed := time.Since(start) // Sequential calls would take approximately concurrentURLs*handlerDelay time diff --git a/x/model/client/cli/tx_model.go b/x/model/client/cli/tx_model.go index 16084d212..9f15bcba2 100644 --- a/x/model/client/cli/tx_model.go +++ b/x/model/client/cli/tx_model.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "strings" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" @@ -101,10 +102,10 @@ func CmdCreateModel() *cobra.Command { commissioningFallbackURL, ) - unreachable := cli.FirstUnreachableURL(commissioningCustomFlowURL, userManualURL, supportURL, productURL, lsfURL, + unreachable := cli.CheckURLsForLiveness(commissioningCustomFlowURL, userManualURL, supportURL, productURL, lsfURL, enhancedSetupFlowTCURL, enhancedSetupFlowMaintenanceURL, commissioningFallbackURL) - if unreachable != "" { - return fmt.Errorf("%v is not reachable", unreachable) + if len(unreachable) > 0 { + return fmt.Errorf("URLs not reachable: %s", strings.Join(unreachable, ", ")) } // validate basic will be called in GenerateOrBroadcastTxCLI @@ -292,10 +293,10 @@ func CmdUpdateModel() *cobra.Command { factoryResetStepsInstruction, ) - unreachable := cli.FirstUnreachableURL(commissioningCustomFlowURL, userManualURL, supportURL, productURL, lsfURL, + unreachable := cli.CheckURLsForLiveness(commissioningCustomFlowURL, userManualURL, supportURL, productURL, lsfURL, enhancedSetupFlowTCURL, enhancedSetupFlowMaintenanceURL, commissioningFallbackURL) - if unreachable != "" { - return fmt.Errorf("%v is not reachable", unreachable) + if len(unreachable) > 0 { + return fmt.Errorf("URLs not reachable: %s", strings.Join(unreachable, ", ")) } // validate basic will be called in GenerateOrBroadcastTxCLI diff --git a/x/model/client/cli/tx_model_version.go b/x/model/client/cli/tx_model_version.go index 79ed6b6aa..6a66987dd 100644 --- a/x/model/client/cli/tx_model_version.go +++ b/x/model/client/cli/tx_model_version.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "strings" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" @@ -61,9 +62,9 @@ func CmdCreateModelVersion() *cobra.Command { specificationVersion, ) - unreachable := cli.FirstUnreachableURL(otaURL, releaseNotesURL) - if unreachable != "" { - return fmt.Errorf("%s is not reachable", unreachable) + unreachable := cli.CheckURLsForLiveness(otaURL, releaseNotesURL) + if len(unreachable) > 0 { + return fmt.Errorf("URLs not reachable: %s", strings.Join(unreachable, ", ")) } // validate basic will be called in GenerateOrBroadcastTxCLI err = tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) @@ -172,9 +173,9 @@ func CmdUpdateModelVersion() *cobra.Command { schemaVersion, ) - unreachable := cli.FirstUnreachableURL(otaURL, releaseNotesURL) - if unreachable != "" { - return fmt.Errorf("%s is not reachable", unreachable) + unreachable := cli.CheckURLsForLiveness(otaURL, releaseNotesURL) + if len(unreachable) > 0 { + return fmt.Errorf("URLs not reachable: %s", strings.Join(unreachable, ", ")) } // validate basic will be called in GenerateOrBroadcastTxCLI From 68b35e1d9778b62a227b71f7a5ae93fcadb609f3 Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Thu, 14 May 2026 16:45:15 +0500 Subject: [PATCH 16/16] Fix typo Signed-off-by: Abdulbois --- .github/workflows/verify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 646825320..343da0a75 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -141,7 +141,7 @@ jobs: run: integration_tests/run-all.sh all cover env: GH_TOKEN: ${{ secrets.GH_TOKEN }} - GOCOVER_MODE: set # Ensure integration tests use the same mode as unit tests\ + GOCOVER_MODE: set # Ensure integration tests use the same mode as unit tests URL_LIVENESS_CHECK_ENABLED: false # Disable network calls to check URLs for integration tests - name: Generate test coverage shell: bash