Copyright (c) 2026 Huawei Technologies Co., Ltd.
openFuyao is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
*/
package remote
import (
"crypto/tls"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gitcode.com/openFuyao/ub-ssu-csi/pkg/backend"
)
const (
testSize1GiB = 1 << 30
testSize2GiB = 2 << 30
testSize4GiB = 4 << 30
testNamespaceCount2 = 2
testNamespaceCount4 = 4
truncateBodyMaxLen = 256
ellipsisLen = 3
)
func setupTestClient(handler http.HandlerFunc) (*httptest.Server, *remoteClient) {
ts := httptest.NewTLSServer(handler)
httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
ubseClient := NewHTTPClientBuilder(ts.URL).WithHTTPClient(httpClient).Build()
client := &remoteClient{httpClient: ubseClient}
return ts, client
}
func mockAllocSingleNSHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
resp := ubseResponse[allocResponseData]{
Code: 0,
Msg: "success",
Data: allocResponseData{
Name: "vol-1",
Strategy: 2,
NameSpaceList: []allocNamespace{
{
NsUuid: "uuid-123",
NsDevPath: "/dev/nvme0n1",
TgtEid: "eid-1",
TgtNqn: "nqn-1",
NsSize: testSize2GiB,
LbaFormat: 512,
NamespaceId: 1,
},
},
},
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
panic(err)
}
}
func mockAllocMultiNSHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
resp := ubseResponse[allocResponseData]{
Code: 0,
Data: allocResponseData{
Name: "vol-2",
Strategy: 0,
NameSpaceList: []allocNamespace{
{NsUuid: "uuid-1", NsDevPath: "/dev/nvme0n1", NsSize: testSize1GiB},
{NsUuid: "uuid-2", NsDevPath: "/dev/nvme1n1", NsSize: testSize1GiB},
{NsUuid: "uuid-3", NsDevPath: "/dev/nvme2n1", NsSize: testSize1GiB},
{NsUuid: "uuid-4", NsDevPath: "/dev/nvme3n1", NsSize: testSize1GiB},
},
},
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
panic(err)
}
}
func mockAllocHTTPErrorHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
if _, err := w.Write([]byte("internal error")); err != nil {
panic(err)
}
}
func mockAllocBusinessErrorHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
resp := ubseResponse[allocResponseData]{
Code: 1,
Msg: "volume already exists",
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
panic(err)
}
}
func mockDeleteSuccessHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
resp := ubseResponse[emptyData]{
Code: 0,
Msg: "success",
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
panic(err)
}
}
func mockDeleteHTTPErrorHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
if _, err := w.Write([]byte("internal error")); err != nil {
panic(err)
}
}
func TestAllocVolume(t *testing.T) {
t.Run("成功场景", func(t *testing.T) {
testAllocVolumeSuccess(t)
})
t.Run("失败场景", func(t *testing.T) {
testAllocVolumeFailure(t)
})
}
func testAllocVolumeSuccess(t *testing.T) {
t.Run("单namespace分配成功", testAllocVolumeSingleNS)
t.Run("多namespace分配成功", testAllocVolumeMultiNS)
t.Run("LBA格式4096转换成功", testAllocVolumeLBAFormat)
}
func testAllocVolumeSingleNS(t *testing.T) {
ts, client := setupTestClient(mockAllocSingleNSHandler)
defer ts.Close()
vol, err := client.AllocVolume("vol-1", testSize2GiB, backend.LogicalConfig{
NsNum: 1,
LbaFormat: 0,
Strategy: 2,
})
require.NoError(t, err)
require.NotNil(t, vol)
assert.Equal(t, "vol-1", vol.VolumeID)
assert.Equal(t, uint64(testSize2GiB), vol.SizeBytes)
assert.Len(t, vol.Namespaces, 1)
assert.Equal(t, "uuid-123", vol.Namespaces[0].DeviceUUID)
}
func testAllocVolumeMultiNS(t *testing.T) {
ts, client := setupTestClient(mockAllocMultiNSHandler)
defer ts.Close()
vol, err := client.AllocVolume("vol-2", testSize4GiB, backend.LogicalConfig{
NsNum: 4,
LbaFormat: 0,
Strategy: 0,
})
require.NoError(t, err)
assert.Len(t, vol.Namespaces, testNamespaceCount4)
assert.Equal(t, uint64(testSize4GiB), vol.SizeBytes)
}
func testAllocVolumeLBAFormat(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
var req allocRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Logf("decode error in test handler: %v", err)
}
assert.Equal(t, uint32(4096), req.LbaFormat)
w.WriteHeader(http.StatusCreated)
resp := ubseResponse[allocResponseData]{
Code: 0,
Data: allocResponseData{
Name: "vol-3",
NameSpaceList: []allocNamespace{{NsUuid: "uuid-1", LbaFormat: 4096}},
},
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
panic(err)
}
}
ts, client := setupTestClient(handler)
defer ts.Close()
vol, err := client.AllocVolume("vol-3", testSize1GiB, backend.LogicalConfig{
NsNum: 1,
LbaFormat: 1,
Strategy: 2,
})
require.NoError(t, err)
assert.NotNil(t, vol)
}
func testAllocVolumeFailure(t *testing.T) {
t.Run("HTTP错误", func(t *testing.T) {
ts, client := setupTestClient(mockAllocHTTPErrorHandler)
defer ts.Close()
_, err := client.AllocVolume("vol-1", testSize1GiB, backend.LogicalConfig{})
require.Error(t, err)
assert.Contains(t, err.Error(), "alloc request")
})
t.Run("UBSE业务错误", func(t *testing.T) {
ts, client := setupTestClient(mockAllocBusinessErrorHandler)
defer ts.Close()
_, err := client.AllocVolume("vol-1", testSize1GiB, backend.LogicalConfig{})
require.Error(t, err)
assert.Contains(t, err.Error(), "alloc request")
})
}
func TestDeleteVolume(t *testing.T) {
t.Run("成功场景", func(t *testing.T) {
t.Run("删除成功", func(t *testing.T) {
ts, client := setupTestClient(mockDeleteSuccessHandler)
defer ts.Close()
err := client.DeleteVolume("vol-1")
require.NoError(t, err)
})
t.Run("幂等性验证", func(t *testing.T) {
ts, client := setupTestClient(mockDeleteSuccessHandler)
defer ts.Close()
err1 := client.DeleteVolume("vol-1")
require.NoError(t, err1)
err2 := client.DeleteVolume("vol-1")
require.NoError(t, err2)
})
})
t.Run("失败场景", func(t *testing.T) {
t.Run("HTTP错误", func(t *testing.T) {
ts, client := setupTestClient(mockDeleteHTTPErrorHandler)
defer ts.Close()
err := client.DeleteVolume("vol-1")
require.Error(t, err)
assert.Contains(t, err.Error(), "delete request")
})
})
}
func TestLbaFormatConversion(t *testing.T) {
t.Run("格式0转换为512字节", func(t *testing.T) {
assert.Equal(t, uint32(512), lbaFormatToBytes(0))
})
t.Run("格式1转换为4096字节", func(t *testing.T) {
assert.Equal(t, uint32(4096), lbaFormatToBytes(1))
})
}
func TestTruncateBody(t *testing.T) {
t.Run("短内容不截断", func(t *testing.T) {
body := []byte("short body")
result := truncateBody(body)
assert.Equal(t, "short body", result)
})
t.Run("长内容截断并添加省略号", func(t *testing.T) {
longBody := make([]byte, 300)
for i := range longBody {
longBody[i] = 'a'
}
result := truncateBody(longBody)
assert.Len(t, result, truncateBodyMaxLen+ellipsisLen)
assert.Contains(t, result, "...")
})
}
func TestUnsupportedMethods(t *testing.T) {
client := &remoteClient{}
t.Run("GetVolumeStatus不支持", func(t *testing.T) {
_, err := client.GetVolumeStatus("vol-1")
require.Error(t, err)
assert.Contains(t, err.Error(), "not supported")
})
t.Run("GetVolumeInfo返回错误", func(t *testing.T) {
_, err := client.GetVolumeInfo("vol-1")
require.Error(t, err)
assert.Equal(t, backend.ErrVolumeInfoNotFound, err)
})
t.Run("SetAccessPermission不支持", func(t *testing.T) {
err := client.SetAccessPermission("vol-1", "host-nqn")
require.Error(t, err)
assert.Contains(t, err.Error(), "not supported")
})
t.Run("RevokeAccessPermission不支持", func(t *testing.T) {
err := client.RevokeAccessPermission("vol-1", "host-nqn")
require.Error(t, err)
assert.Contains(t, err.Error(), "not supported")
})
t.Run("GetConnectionInfo返回nil", func(t *testing.T) {
connInfo, err := client.GetConnectionInfo("vol-1")
require.NoError(t, err)
assert.Nil(t, connInfo)
})
}