From 55c6b24b9e4e59a3c6ca06cca46f4f26c085408a Mon Sep 17 00:00:00 2001 From: Pubudu Gunatilaka Date: Mon, 19 Feb 2024 15:26:17 +0530 Subject: [PATCH 1/4] Add Semantic versioning related integration tests --- test/cucumber-tests/CRs/artifacts.yaml | 76 +++++++++++ .../semantic-versioning/sem_api_v1-0.yaml | 28 +++++ .../semantic-versioning/sem_api_v1-1.yaml | 28 +++++ .../semantic-versioning/sem_api_v1-5.yaml | 28 +++++ .../semantic-versioning/sem_api_v2-1.yaml | 27 ++++ .../tests/api/SemanticVersioning.feature | 118 ++++++++++++++++++ 6 files changed, 305 insertions(+) create mode 100644 test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-0.yaml create mode 100644 test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-1.yaml create mode 100644 test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-5.yaml create mode 100644 test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v2-1.yaml create mode 100644 test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature diff --git a/test/cucumber-tests/CRs/artifacts.yaml b/test/cucumber-tests/CRs/artifacts.yaml index a9ae0b918..2da9444f3 100644 --- a/test/cucumber-tests/CRs/artifacts.yaml +++ b/test/cucumber-tests/CRs/artifacts.yaml @@ -491,6 +491,61 @@ data: "body": "{\n \"keys\":[\n {\n \"kty\":\"RSA\",\n \"n\":\"m0YNpM5MVYToWZMZ9wL4KQOygvG0f6y0dw4wZ02T4C3SxiC1zEBCZLh2clj7bncyA3EV2bFrTIBNeq-1pFEfbNDMZB88Jcg0S9QyYujr6GM0AqLA7WjZQ6lLxLpeQdEQroEZI-c8rnGmzU8Qb25aiPbRf6Vh7vFYGQz5FnZ8E0LcEMYQ-4KPMkAqnMon1UKWDkqszTY5a-DGMAi5w7imKzXaU4qiEKVKIcezv9nLUVC5Od0T4FkUQi462ZA9SoHx1HNhcVAj8Nf9TG_C65GbsMMFJVcRXwZR99cVzVxVqEtxGlK7Qr0woYKQ3S5kHZPRFcMFXI6WHhEQXqyOMBdUfQ\",\n \"e\":\"AQAB\",\n \"alg\":\"RS256\",\n \"kid\":\"123-456\",\n \"use\":\"sig\"\n }\n ]\n}" } } + sem-versioning.json: | + {"mappings": [ + { + "request": { + "method": "GET", + "url": "/sem-api/v1.0/employee" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\n \"version\":\"v1.0\" \n}" + } + }, + { + "request": { + "method": "GET", + "url": "/sem-api/v1.1/employee" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\n \"version\":\"v1.1\" \n}" + } + }, + { + "request": { + "method": "GET", + "url": "/sem-api/v1.5/employee" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\n \"version\":\"v1.5\" \n}" + } + }, + { + "request": { + "method": "GET", + "url": "/sem-api/v2.1/employee" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\n \"version\":\"v2.1\" \n}" + } + } + ]} --- kind: TokenIssuer apiVersion: dp.wso2.com/v1alpha1 @@ -818,3 +873,24 @@ data: y5Oi4A4+id+xO0XnHIkkqCfPtFzxl3hwytcy8EqISynzzHWNJ8bFZIYX4tgX+PLq u0/ITEw= -----END CERTIFICATE----- +--- +apiVersion: cp.wso2.com/v1alpha2 +kind: Subscription +metadata: + name: semantic-versioning-subscription + namespace: apk-integration-test +spec: + organization: "default" + subscriptionStatus: "ACTIVE" + api: + name: "Semantic Versioning API" + version: "v\\d+(\\.\\d+)?" +--- +apiVersion: cp.wso2.com/v1alpha2 +kind: ApplicationMapping +metadata: + name: semantic-versioning-app-mapping + namespace: apk-integration-test +spec: + applicationRef: 583e4146-7ef5-11ee-b962-0242ac120003 + subscriptionRef: semantic-versioning-subscription diff --git a/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-0.yaml b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-0.yaml new file mode 100644 index 000000000..2933aae06 --- /dev/null +++ b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-0.yaml @@ -0,0 +1,28 @@ +--- +name: "Semantic Versioning API" +basePath: "/sem-api" +version: "v1.0" +id: "sem-api-v1-0" +type: "REST" +defaultVersion: false +subscriptionValidation: true +endpointConfigurations: + production: + endpoint: "http://dynamic-backend-service:8080/sem-api/v1.0" +operations: + - target: "/employee" + verb: "GET" + secured: true + scopes: [] + - target: "/employee" + verb: "POST" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "PUT" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "DELETE" + secured: true + scopes: [] diff --git a/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-1.yaml b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-1.yaml new file mode 100644 index 000000000..79a65908b --- /dev/null +++ b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-1.yaml @@ -0,0 +1,28 @@ +--- +name: "Semantic Versioning API" +basePath: "/sem-api" +version: "v1.1" +id: "sem-api-v1-1" +type: "REST" +defaultVersion: false +subscriptionValidation: true +endpointConfigurations: + production: + endpoint: "http://dynamic-backend-service:8080/sem-api/v1.1" +operations: + - target: "/employee" + verb: "GET" + secured: true + scopes: [] + - target: "/employee" + verb: "POST" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "PUT" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "DELETE" + secured: true + scopes: [] diff --git a/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-5.yaml b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-5.yaml new file mode 100644 index 000000000..ba9650c17 --- /dev/null +++ b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-5.yaml @@ -0,0 +1,28 @@ +--- +name: "Semantic Versioning API" +basePath: "/sem-api" +version: "v1.5" +id: "sem-api-v1-5" +type: "REST" +defaultVersion: false +subscriptionValidation: true +endpointConfigurations: + production: + endpoint: "http://dynamic-backend-service:8080/sem-api/v1.5" +operations: + - target: "/employee" + verb: "GET" + secured: true + scopes: [] + - target: "/employee" + verb: "POST" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "PUT" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "DELETE" + secured: true + scopes: [] diff --git a/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v2-1.yaml b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v2-1.yaml new file mode 100644 index 000000000..f02706214 --- /dev/null +++ b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v2-1.yaml @@ -0,0 +1,27 @@ +--- +name: "Semantic Versioning API" +basePath: "/sem-api" +version: "v2.1" +id: "sem-api-v2-1" +type: "REST" +defaultVersion: false +endpointConfigurations: + production: + endpoint: "http://dynamic-backend-service:8080/sem-api/v2.1" +operations: + - target: "/employee" + verb: "GET" + secured: true + scopes: [] + - target: "/employee" + verb: "POST" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "PUT" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "DELETE" + secured: true + scopes: [] diff --git a/test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature b/test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature new file mode 100644 index 000000000..e7555f763 --- /dev/null +++ b/test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature @@ -0,0 +1,118 @@ +Feature: Semantic Versioning Based Intelligent Routing + + Scenario: API version with Major and Minor + Given The system is ready + And I have a valid subscription + When I use the APK Conf file "artifacts/apk-confs/semantic-versioning/sem_api_v1-0.yaml" + And the definition file "artifacts/definitions/employees_api.json" + And make the API deployment request + Then the response status code should be 200 + Then I generate JWT token from idp1 with kid "123-456" and consumer_key "45f1c5c8-a92e-11ed-afa1-0242ac120005" + Then I set headers + |Authorization|bearer ${idp-1-45f1c5c8-a92e-11ed-afa1-0242ac120005-token}| + + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1.0/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.0\"" + + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.0\"" + + When I use the APK Conf file "artifacts/apk-confs/semantic-versioning/sem_api_v1-1.yaml" + And the definition file "artifacts/definitions/employees_api.json" + And make the API deployment request + Then the response status code should be 200 + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1.1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.1\"" + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.1\"" + + When I use the APK Conf file "artifacts/apk-confs/semantic-versioning/sem_api_v1-5.yaml" + And the definition file "artifacts/definitions/employees_api.json" + And make the API deployment request + Then the response status code should be 200 + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1.5/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.5\"" + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.5\"" + + When I undeploy the API whose ID is "sem-api-v1-5" + Then the response status code should be 202 + And I wait for 1 seconds + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + And the response body should contain "\"version\":\"v1.1\"" + + When I undeploy the API whose ID is "sem-api-v1-1" + Then the response status code should be 202 + And I wait for 1 seconds + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + And the response body should contain "\"version\":\"v1.0\"" + + When I undeploy the API whose ID is "sem-api-v1-0" + Then the response status code should be 202 + + Scenario: Multiple Major and minor versions for an API + Given The system is ready + And I have a valid subscription + When I use the APK Conf file "artifacts/apk-confs/semantic-versioning/sem_api_v1-0.yaml" + And the definition file "artifacts/definitions/employees_api.json" + And make the API deployment request + Then the response status code should be 200 + When I use the APK Conf file "artifacts/apk-confs/semantic-versioning/sem_api_v1-1.yaml" + And the definition file "artifacts/definitions/employees_api.json" + And make the API deployment request + Then the response status code should be 200 + When I use the APK Conf file "artifacts/apk-confs/semantic-versioning/sem_api_v1-5.yaml" + And the definition file "artifacts/definitions/employees_api.json" + And make the API deployment request + Then the response status code should be 200 + Then I generate JWT token from idp1 with kid "123-456" and consumer_key "45f1c5c8-a92e-11ed-afa1-0242ac120005" + Then I set headers + |Authorization|bearer ${idp-1-45f1c5c8-a92e-11ed-afa1-0242ac120005-token}| + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.5\"" + + When I undeploy the API whose ID is "sem-api-v1-1" + Then the response status code should be 202 + And I wait for 1 seconds + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.5\"" + + When I use the APK Conf file "artifacts/apk-confs/semantic-versioning/sem_api_v2-1.yaml" + And the definition file "artifacts/definitions/employees_api.json" + And make the API deployment request + Then the response status code should be 200 + + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v2/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v2.1\"" + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v2.1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v2.1\"" + + When I undeploy the API whose ID is "sem-api-v1-0" + Then the response status code should be 202 + And I wait for 1 seconds + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.5\"" + + When I undeploy the API whose ID is "sem-api-v1-5" + Then the response status code should be 202 + And I wait for 1 seconds + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + Then the response status code should be 404 + + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v2/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v2.1\"" + + When I undeploy the API whose ID is "sem-api-v2-1" + Then the response status code should be 202 From 796b93df7d9a823dab1bfba98d89ca0e9e9dd9bd Mon Sep 17 00:00:00 2001 From: Pubudu Gunatilaka Date: Wed, 21 Feb 2024 12:06:06 +0530 Subject: [PATCH 2/4] Adding unit tests for the semantic versioning feature --- .../discovery/xds/semantic_versioning_test.go | 328 ++++++++++++++++++ .../pkg/semanticversion/semantic_version.go | 2 +- .../semanticversion/semantic_version_test.go | 186 ++++++++++ 3 files changed, 515 insertions(+), 1 deletion(-) create mode 100644 adapter/internal/discovery/xds/semantic_versioning_test.go create mode 100644 adapter/pkg/semanticversion/semantic_version_test.go diff --git a/adapter/internal/discovery/xds/semantic_versioning_test.go b/adapter/internal/discovery/xds/semantic_versioning_test.go new file mode 100644 index 000000000..942f13348 --- /dev/null +++ b/adapter/internal/discovery/xds/semantic_versioning_test.go @@ -0,0 +1,328 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * 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 xds + +import ( + "regexp" + "testing" + + "github.com/wso2/apk/adapter/config" + semantic_version "github.com/wso2/apk/adapter/pkg/semanticversion" +) + +func TestGetVersionMatchRegex(t *testing.T) { + tests := []struct { + name string + version string + expectedResult string + }{ + { + name: "Version with single digit components", + version: "1.2.3", + expectedResult: "1\\.2\\.3", + }, + { + name: "Version with multi-digit components", + version: "123.456.789", + expectedResult: "123\\.456\\.789", + }, + { + name: "Version with alpha components", + version: "v1.0-alpha", + expectedResult: "v1\\.0-alpha", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetVersionMatchRegex(tt.version) + + if result != tt.expectedResult { + t.Errorf("Expected regex: %s, Got: %s", tt.expectedResult, result) + } + + // Test if the regex works correctly + match, err := regexp.MatchString(result, tt.version) + if err != nil { + t.Errorf("Error when matching regex: %v", err) + } + if !match { + t.Errorf("Regex failed to match the version: %s %s", tt.version, result) + } + }) + } +} + +func TestGetMajorMinorVersionRangeRegex(t *testing.T) { + tests := []struct { + name string + semVersion semantic_version.SemVersion + expectedResult string + }{ + { + name: "Major and minor version only", + semVersion: semantic_version.SemVersion{Major: 1, Minor: 2}, + expectedResult: "v1(?:\\.2)?", + }, + { + name: "Major, minor, and patch version", + semVersion: semantic_version.SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + expectedResult: "v1(?:\\.2(?:\\.3)?)?", + }, + { + name: "Major version only", + semVersion: semantic_version.SemVersion{Major: 1}, + expectedResult: "v1(?:\\.0)?", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetMajorMinorVersionRangeRegex(tt.semVersion) + + if result != tt.expectedResult { + t.Errorf("Expected regex: %s, Got: %s", tt.expectedResult, result) + } + }) + } +} + +func TestGetMinorVersionRangeRegex(t *testing.T) { + tests := []struct { + name string + semVersion semantic_version.SemVersion + expectedResult string + }{ + { + name: "Major, minor, and patch version", + semVersion: semantic_version.SemVersion{Version: "v1.2.3", Major: 1, Minor: 2, Patch: PtrInt(3)}, + expectedResult: "v1\\.2(?:\\.3)?", + }, + { + name: "Major and minor version only", + semVersion: semantic_version.SemVersion{Version: "v1.2", Major: 1, Minor: 2}, + expectedResult: "v1\\.2", + }, + { + name: "Major version only", + semVersion: semantic_version.SemVersion{Version: "v1", Major: 1}, + expectedResult: "v1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetMinorVersionRangeRegex(tt.semVersion) + + if result != tt.expectedResult { + t.Errorf("Expected regex: %s, Got: %s", tt.expectedResult, result) + } + }) + } +} + +func TestGetMajorVersionRange(t *testing.T) { + tests := []struct { + name string + semVersion semantic_version.SemVersion + expectedResult string + }{ + { + name: "Major and minor version 1.2.3", + semVersion: semantic_version.SemVersion{Version: "v1.2.3", Major: 1, Minor: 2, Patch: PtrInt(3)}, + expectedResult: "v1", + }, + { + name: "Major version 2", + semVersion: semantic_version.SemVersion{Major: 2}, + expectedResult: "v2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetMajorVersionRange(tt.semVersion) + + if result != tt.expectedResult { + t.Errorf("Expected result: %s, Got: %s", tt.expectedResult, result) + } + }) + } +} + +func TestGetMinorVersionRange(t *testing.T) { + tests := []struct { + name string + semVersion semantic_version.SemVersion + expectedResult string + }{ + { + name: "Major and minor version 1.2", + semVersion: semantic_version.SemVersion{Major: 1, Minor: 2}, + expectedResult: "v1.2", + }, + { + name: "Major and minor version 1.2.3", + semVersion: semantic_version.SemVersion{Version: "v1.2.3", Major: 1, Minor: 2, Patch: PtrInt(3)}, + expectedResult: "v1.2", + }, + { + name: "Major only", + semVersion: semantic_version.SemVersion{Major: 10}, + expectedResult: "v10.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetMinorVersionRange(tt.semVersion) + + if result != tt.expectedResult { + t.Errorf("Expected result: %s, Got: %s", tt.expectedResult, result) + } + }) + } +} + +func TestIsSemanticVersioningEnabled(t *testing.T) { + + conf := config.ReadConfigs() + + tests := []struct { + name string + apiName string + apiVersion string + intelligentRoutingEnabled bool + expectedResult bool + }{ + { + name: "Semantic versioning enabled and valid version provided", + apiName: "TestAPI", + apiVersion: "v1.2.3", + intelligentRoutingEnabled: true, + expectedResult: true, + }, + { + name: "Semantic versioning enabled and valid version provided", + apiName: "TestAPI", + apiVersion: "v1.2", + intelligentRoutingEnabled: true, + expectedResult: true, + }, + { + name: "Semantic versioning enabled and version only contains major version", + apiName: "TestAPI", + apiVersion: "v1", + intelligentRoutingEnabled: true, + expectedResult: false, + }, + { + name: "Semantic versioning enabled and invalid version provided", + apiName: "TestAPI", + apiVersion: "1.2.3", + intelligentRoutingEnabled: true, + expectedResult: false, + }, + { + name: "Semantic versioning disabled and valid version provided", + apiName: "TestAPI", + apiVersion: "v1.2.3", + intelligentRoutingEnabled: false, + expectedResult: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + conf.Envoy.EnableIntelligentRouting = tt.intelligentRoutingEnabled + result := isSemanticVersioningEnabled(tt.apiName, tt.apiVersion) + + if result != tt.expectedResult { + t.Errorf("Expected result: %v, Got: %v", tt.expectedResult, result) + } + }) + } +} + +func TestIsVHostMatched(t *testing.T) { + // Mock orgIDAPIvHostsMap for testing + orgIDAPIvHostsMap = map[string]map[string][]string{ + "org1": { + "api1": {"example.com", "api.example.com"}, + "api2": {"test.com"}, + }, + "org2": { + "api3": {"example.org"}, + "api4": {"test.org"}, + }, + } + + tests := []struct { + name string + organizationID string + vHost string + expectedResult bool + }{ + { + name: "Matching vHost in org1", + organizationID: "org1", + vHost: "example.com", + expectedResult: true, + }, + { + name: "Matching vHost in org2", + organizationID: "org2", + vHost: "example.org", + expectedResult: true, + }, + { + name: "Non-matching vHost in org1", + organizationID: "org1", + vHost: "nonexistent.com", + expectedResult: false, + }, + { + name: "Non-matching vHost in org2", + organizationID: "org2", + vHost: "nonexistent.org", + expectedResult: false, + }, + { + name: "VHost not found for organization", + organizationID: "org3", + vHost: "example.com", + expectedResult: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isVHostMatched(tt.organizationID, tt.vHost) + + if result != tt.expectedResult { + t.Errorf("Expected result: %v, Got: %v", tt.expectedResult, result) + } + }) + } +} + +// PtrInt returns a pointer to an integer value +func PtrInt(i int) *int { + return &i +} diff --git a/adapter/pkg/semanticversion/semantic_version.go b/adapter/pkg/semanticversion/semantic_version.go index 88afaf9a0..3750e5940 100644 --- a/adapter/pkg/semanticversion/semantic_version.go +++ b/adapter/pkg/semanticversion/semantic_version.go @@ -97,7 +97,7 @@ func (baseVersion SemVersion) Compare(version SemVersion) bool { // Compare patch version if baseVersion.Patch != nil && version.Patch != nil { - return *baseVersion.Patch < *version.Patch + return *baseVersion.Patch <= *version.Patch } else if baseVersion.Patch != nil { return false } else if version.Patch != nil { diff --git a/adapter/pkg/semanticversion/semantic_version_test.go b/adapter/pkg/semanticversion/semantic_version_test.go new file mode 100644 index 000000000..53a7e1bf2 --- /dev/null +++ b/adapter/pkg/semanticversion/semantic_version_test.go @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * 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 semanticversion + +import ( + "errors" + "testing" +) + +func TestSemVersionCompare(t *testing.T) { + tests := []struct { + name string + baseVersion SemVersion + compareVersion SemVersion + expected bool + }{ + { + name: "Same versions", + baseVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + compareVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + expected: true, + }, + { + name: "Base version major is greater", + baseVersion: SemVersion{Major: 2, Minor: 1, Patch: PtrInt(3)}, + compareVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + expected: false, + }, + { + name: "Base version minor is greater", + baseVersion: SemVersion{Major: 1, Minor: 3, Patch: PtrInt(3)}, + compareVersion: SemVersion{Major: 1, Minor: 4, Patch: PtrInt(3)}, + expected: true, + }, + { + name: "Base version patch is greater", + baseVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(4)}, + compareVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + expected: false, + }, + { + name: "Compare version major is greater", + baseVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + compareVersion: SemVersion{Major: 2, Minor: 2, Patch: PtrInt(3)}, + expected: true, + }, + { + name: "Compare version minor is greater", + baseVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + compareVersion: SemVersion{Major: 1, Minor: 3, Patch: PtrInt(3)}, + expected: true, + }, + { + name: "Compare version patch is greater", + baseVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + compareVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(4)}, + expected: true, + }, + { + name: "Base version patch is nil", + baseVersion: SemVersion{Major: 1, Minor: 2}, + compareVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(4)}, + expected: true, + }, + { + name: "Compare version patch is nil", + baseVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + compareVersion: SemVersion{Major: 1, Minor: 2}, + expected: false, + }, + { + name: "Both patch versions are nil", + baseVersion: SemVersion{Major: 1, Minor: 2}, + compareVersion: SemVersion{Major: 1, Minor: 2}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.baseVersion.Compare(tt.compareVersion) + if result != tt.expected { + t.Errorf("Expected %v, but got %v", tt.expected, result) + } + }) + } +} + +func TestValidateAndGetVersionComponents(t *testing.T) { + tests := []struct { + name string + version string + apiName string + expectedResult *SemVersion + expectedError error + }{ + { + name: "Valid version format", + version: "v1.2.3", + apiName: "TestAPI", + expectedResult: &SemVersion{Version: "v1.2.3", Major: 1, Minor: 2, Patch: PtrInt(3)}, + expectedError: nil, + }, + { + name: "Valid version format without patch", + version: "v1.2", + apiName: "TestAPI", + expectedResult: &SemVersion{Version: "v1.2", Major: 1, Minor: 2, Patch: nil}, + expectedError: nil, + }, + { + name: "Invalid version format - missing 'v' prefix", + version: "1.2.3", + apiName: "TestAPI", + expectedResult: nil, + expectedError: errors.New("Invalid version: 1.2.3 for API: TestAPI. API version should be in the format x.y.z, x.y, vx.y.z or vx.y where x,y,z are non-negative integers and v is version prefix"), + }, + { + name: "Invalid version format - negative major version", + version: "v-1.2.3", + apiName: "TestAPI", + expectedResult: nil, + expectedError: errors.New("invalid version format"), + }, + { + name: "Invalid version format - negative minor version", + version: "v1.-2.3", + apiName: "TestAPI", + expectedResult: nil, + expectedError: errors.New("invalid version format"), + }, + { + name: "Invalid version format - patch version not an integer", + version: "v1.2.three", + apiName: "TestAPI", + expectedResult: nil, + expectedError: errors.New("invalid version format"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ValidateAndGetVersionComponents(tt.version, tt.apiName) + + // Check for errors + if (err != nil && tt.expectedError == nil) || (err == nil && tt.expectedError != nil) || (err != nil && tt.expectedError != nil && err.Error() != tt.expectedError.Error()) { + t.Errorf("Unexpected error. Expected: %v, Got: %v", tt.expectedError, err) + } + + // Check for nil results + if result == nil && tt.expectedResult != nil { + t.Errorf("Unexpected nil result") + } else if result != nil && tt.expectedResult == nil { + t.Errorf("Unexpected non-nil result") + } + + // Check for result equality + if result != nil && tt.expectedResult != nil { + if result.Version != tt.expectedResult.Version || result.Major != tt.expectedResult.Major || result.Minor != tt.expectedResult.Minor || (result.Patch != nil && (*result.Patch != *tt.expectedResult.Patch)) { + t.Errorf("Unexpected result. Expected: %v, Got: %v", tt.expectedResult, result) + } + } + }) + } + +} + +// PtrInt returns a pointer to an integer value +func PtrInt(i int) *int { + return &i +} From 39ac4e7ac5c71adf43b8ff0ba71d07cfbd64014b Mon Sep 17 00:00:00 2001 From: Pubudu Gunatilaka Date: Mon, 26 Feb 2024 17:09:12 +0530 Subject: [PATCH 3/4] Add unit tests for update and delete methods in sem versioning --- .../discovery/xds/semantic_versioning_test.go | 356 ++++++++++++++++++ 1 file changed, 356 insertions(+) diff --git a/adapter/internal/discovery/xds/semantic_versioning_test.go b/adapter/internal/discovery/xds/semantic_versioning_test.go index 942f13348..d08d27718 100644 --- a/adapter/internal/discovery/xds/semantic_versioning_test.go +++ b/adapter/internal/discovery/xds/semantic_versioning_test.go @@ -18,10 +18,14 @@ package xds import ( + "reflect" "regexp" "testing" + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + envoy_type_matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" "github.com/wso2/apk/adapter/config" + "github.com/wso2/apk/adapter/internal/oasparser/model" semantic_version "github.com/wso2/apk/adapter/pkg/semanticversion" ) @@ -322,6 +326,358 @@ func TestIsVHostMatched(t *testing.T) { } } +func TestGetRoutesForAPIIdentifier(t *testing.T) { + + orgAPIMap = map[string]map[string]*EnvoyInternalAPI{ + "org1": { + "gw.com:apiID1": &EnvoyInternalAPI{ + routes: []*routev3.Route{ + { + Name: "route1", + }, + { + Name: "route2", + }, + }, + }, + "gw.com:apiID2": &EnvoyInternalAPI{ + routes: []*routev3.Route{ + { + Name: "route3", + }, + }, + }, + }, + "org2": { + "test.gw.com:apiID1": &EnvoyInternalAPI{ + routes: []*routev3.Route{ + { + Name: "route4", + }, + }, + }, + }, + } + + tests := []struct { + name string + organizationID string + apiIdentifier string + expectedRoutes []*routev3.Route + expectedNumRoute int + }{ + { + name: "Existing organization and API identifier", + organizationID: "org1", + apiIdentifier: "gw.com:apiID1", + expectedRoutes: []*routev3.Route{ + { + Name: "route1", + }, + { + Name: "route2", + }, + }, + expectedNumRoute: 2, + }, + { + name: "Non-existing organization", + organizationID: "org3", + apiIdentifier: "dev.gw.com:apiID1", + expectedRoutes: []*routev3.Route{}, + expectedNumRoute: 0, + }, + { + name: "Non-existing API identifier", + organizationID: "org1", + apiIdentifier: "api3", + expectedRoutes: []*routev3.Route{}, + expectedNumRoute: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getRoutesForAPIIdentifier(tt.organizationID, tt.apiIdentifier) + + if len(result) != tt.expectedNumRoute { + t.Errorf("Expected number of routes: %d, Got: %d", tt.expectedNumRoute, len(result)) + } + + if len(result) > 0 { + if !reflect.DeepEqual(result, tt.expectedRoutes) { + t.Errorf("Expected routes: %v, Got: %v", tt.expectedRoutes, result) + } + } + }) + } +} + +func TestUpdateRoutingRulesOnAPIUpdate(t *testing.T) { + + var apiID1 model.AdapterInternalAPI + apiID1.SetName("Test API") + apiID1.SetVersion("v1.0") + apiID1ResourcePath := "^/test-api/v1\\.0/orders([/]{0,1})" + + var apiID2 model.AdapterInternalAPI + apiID2.SetName("Mock API") + apiID2.SetVersion("v1.1") + apiID2ResourcePath := "^/mock-api/v1\\.1/orders([/]{0,1})" + + var apiID3 model.AdapterInternalAPI + apiID3.SetName("Test API") + apiID3.SetVersion("v1.1") + apiID3ResourcePath := "^/test-api/v1\\.1/orders([/]{0,1})" + + orgAPIMap = map[string]map[string]*EnvoyInternalAPI{ + "org1": { + "gw.com:apiID1": &EnvoyInternalAPI{ + adapterInternalAPI: apiID1, + routes: generateRoutes(apiID1ResourcePath), + }, + "gw.com:apiID2": &EnvoyInternalAPI{ + adapterInternalAPI: apiID2, + routes: generateRoutes(apiID2ResourcePath), + }, + "gw.com:apiID3": &EnvoyInternalAPI{ + adapterInternalAPI: apiID3, + routes: generateRoutes(apiID3ResourcePath), + }, + }, + } + + orgIDAPIvHostsMap = map[string]map[string][]string{ + "org1": { + "api1": {"gw.com", "api.example.com"}, + "api2": {"test.com"}, + }, + } + + tests := []struct { + name string + organizationID string + apiIdentifier string + apiName string + apiVersion string + vHost string + expectedRegex string + expectedRewrite string + finalRegex string + finalRewrite string + }{ + { + name: "Create an API with major version", + organizationID: "org1", + apiIdentifier: "gw.com:apiID1", + apiName: "Test API", + apiVersion: "v1.0", + vHost: "gw.com", + expectedRegex: "^/test-api/v1(?:\\.0)?/orders([/]{0,1})", + expectedRewrite: "^/test-api/v1(?:\\.0)?/orders([/]{0,1})", + finalRegex: apiID1ResourcePath, + finalRewrite: apiID1ResourcePath, + }, + { + name: "Create an API with major and minor version", + organizationID: "org1", + apiIdentifier: "gw.com:apiID2", + apiName: "Mock API", + apiVersion: "v1.1", + vHost: "gw.com", + expectedRegex: "^/mock-api/v1(?:\\.1)?/orders([/]{0,1})", + expectedRewrite: "^/mock-api/v1(?:\\.1)?/orders([/]{0,1})", + finalRegex: "^/mock-api/v1(?:\\.1)?/orders([/]{0,1})", + finalRewrite: "^/mock-api/v1(?:\\.1)?/orders([/]{0,1})", + }, + { + name: "Create an API with major and minor version", + organizationID: "org1", + apiIdentifier: "gw.com:apiID3", + apiName: "Test API", + apiVersion: "v1.1", + vHost: "gw.com", + expectedRegex: "^/test-api/v1(?:\\.1)?/orders([/]{0,1})", + expectedRewrite: "^/test-api/v1(?:\\.1)?/orders([/]{0,1})", + finalRegex: "^/test-api/v1(?:\\.1)?/orders([/]{0,1})", + finalRewrite: "^/test-api/v1(?:\\.1)?/orders([/]{0,1})", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + updateRoutingRulesOnAPIUpdate(tt.organizationID, tt.apiIdentifier, tt.apiName, tt.apiVersion, tt.vHost) + api1 := orgAPIMap[tt.organizationID][tt.apiIdentifier] + routes := api1.routes + + if routes[0].GetMatch().GetSafeRegex().GetRegex() != tt.expectedRegex { + t.Errorf("Expected regex: %s, Got: %s", tt.expectedRegex, routes[0].GetMatch().GetSafeRegex().GetRegex()) + } + if routes[0].GetRoute().GetRegexRewrite().GetPattern().GetRegex() != tt.expectedRewrite { + t.Errorf("Expected rewrite pattern: %s, Got: %s", tt.expectedRewrite, routes[0].GetRoute().GetRegexRewrite().GetPattern().GetRegex()) + } + }) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + api1 := orgAPIMap[tt.organizationID][tt.apiIdentifier] + routes := api1.routes + + if routes[0].GetMatch().GetSafeRegex().GetRegex() != tt.finalRegex { + t.Errorf("Expected final regex: %s, Got: %s", tt.finalRegex, routes[0].GetMatch().GetSafeRegex().GetRegex()) + } + if routes[0].GetRoute().GetRegexRewrite().GetPattern().GetRegex() != tt.finalRewrite { + t.Errorf("Expected final rewrite pattern: %s, Got: %s", tt.finalRewrite, routes[0].GetRoute().GetRegexRewrite().GetPattern().GetRegex()) + } + }) + } +} + +func generateRoutes(resourcePath string) []*routev3.Route { + + var routes []*routev3.Route + match := &routev3.RouteMatch{ + PathSpecifier: &routev3.RouteMatch_SafeRegex{ + SafeRegex: &envoy_type_matcherv3.RegexMatcher{ + Regex: resourcePath, + }, + }, + } + + action := &routev3.Route_Route{ + Route: &routev3.RouteAction{ + RegexRewrite: &envoy_type_matcherv3.RegexMatchAndSubstitute{ + Pattern: &envoy_type_matcherv3.RegexMatcher{ + Regex: resourcePath, + }, + Substitution: "/bar", + }, + }, + } + + route := routev3.Route{ + Name: "example-route", + Match: match, + Action: action, + Metadata: nil, + Decorator: nil, + } + + return append(routes, &route) +} + +func TestUpdateRoutingRulesOnAPIDelete(t *testing.T) { + + orgIDLatestAPIVersionMap = map[string]map[string]map[string]semantic_version.SemVersion{ + "org3": { + "gw.com:Test API": { + "v1": { + Version: "v1.0", + Major: 1, + Minor: 0, + Patch: nil, + }, + }, + }, + "org4": { + "gw.com:Mock API": { + "v1.0": { + Version: "v1.0", + Major: 1, + Minor: 0, + Patch: nil, + }, + "v1.5": { + Version: "v1.5", + Major: 1, + Minor: 5, + Patch: nil, + }, + "v1": { + Version: "v1.5", + Major: 1, + Minor: 5, + Patch: nil, + }, + }, + }, + } + + var apiID1 model.AdapterInternalAPI + apiID1.SetName("Test API") + apiID1.SetVersion("v1.0") + apiID1ResourcePath := "^/test-api/v1\\.0/orders([/]{0,1})" + + var apiID2 model.AdapterInternalAPI + apiID2.SetName("Mock API") + apiID2.SetVersion("v1.0") + apiID2ResourcePath := "^/mock-api/v1\\.0/orders([/]{0,1})" + + var apiID3 model.AdapterInternalAPI + apiID3.SetName("Mock API") + apiID3.SetVersion("v1.5") + apiID3ResourcePath := "^/mock-api/v1(?:\\.5)?/orders([/]{0,1})" + + orgAPIMap = map[string]map[string]*EnvoyInternalAPI{ + "org3": { + "gw.com:apiID1": &EnvoyInternalAPI{ + adapterInternalAPI: apiID1, + routes: generateRoutes(apiID1ResourcePath), + }, + }, + "org4": { + "gw.com:apiID2": &EnvoyInternalAPI{ + adapterInternalAPI: apiID2, + routes: generateRoutes(apiID2ResourcePath), + }, + "gw.com:apiID3": &EnvoyInternalAPI{ + adapterInternalAPI: apiID3, + routes: generateRoutes(apiID3ResourcePath), + }, + }, + } + + tests := []struct { + name string + organizationID string + apiIdentifier string + api model.AdapterInternalAPI + deleteVersion string + }{ + { + name: "Delete latest major version", + organizationID: "org3", + apiIdentifier: "gw.com:apiID1", + api: apiID1, + deleteVersion: "v1.0", + }, + { + name: "Delete latest minor version", + organizationID: "org4", + apiIdentifier: "gw.com:apiID3", + api: apiID3, + deleteVersion: "v1.5", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + updateRoutingRulesOnAPIDelete(tt.organizationID, tt.apiIdentifier, tt.api) + + if _, ok := orgIDLatestAPIVersionMap[tt.organizationID]; ok { + if _, ok := orgIDLatestAPIVersionMap[tt.organizationID][tt.apiIdentifier]; ok { + if _, ok := orgIDLatestAPIVersionMap[tt.organizationID][tt.apiIdentifier][tt.deleteVersion]; ok { + t.Errorf("API deletion is not successful: %s", tt.deleteVersion) + } + } + } + }) + } +} + // PtrInt returns a pointer to an integer value func PtrInt(i int) *int { return &i From ea9675b052bce7f41942caa2c7f1be6f81efd103 Mon Sep 17 00:00:00 2001 From: Pubudu Gunatilaka Date: Tue, 27 Feb 2024 11:17:39 +0530 Subject: [PATCH 4/4] Increase timeout for api deployment --- .../resources/tests/api/SemanticVersioning.feature | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature b/test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature index e7555f763..6aa4ca1d2 100644 --- a/test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature +++ b/test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature @@ -43,13 +43,13 @@ Feature: Semantic Versioning Based Intelligent Routing When I undeploy the API whose ID is "sem-api-v1-5" Then the response status code should be 202 - And I wait for 1 seconds + And I wait for 2 seconds And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" And the response body should contain "\"version\":\"v1.1\"" When I undeploy the API whose ID is "sem-api-v1-1" Then the response status code should be 202 - And I wait for 1 seconds + And I wait for 2 seconds And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" And the response body should contain "\"version\":\"v1.0\"" @@ -80,7 +80,7 @@ Feature: Semantic Versioning Based Intelligent Routing When I undeploy the API whose ID is "sem-api-v1-1" Then the response status code should be 202 - And I wait for 1 seconds + And I wait for 2 seconds And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" Then the response status code should be 200 And the response body should contain "\"version\":\"v1.5\"" @@ -99,14 +99,14 @@ Feature: Semantic Versioning Based Intelligent Routing When I undeploy the API whose ID is "sem-api-v1-0" Then the response status code should be 202 - And I wait for 1 seconds + And I wait for 2 seconds And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" Then the response status code should be 200 And the response body should contain "\"version\":\"v1.5\"" When I undeploy the API whose ID is "sem-api-v1-5" Then the response status code should be 202 - And I wait for 1 seconds + And I wait for 2 seconds And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" Then the response status code should be 404