Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 2 additions & 69 deletions gnmi_server/gnoi_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,10 @@ package gnmi

import (
"context"
"strconv"
"strings"

log "github.com/golang/glog"
gnoi_file_pb "github.com/openconfig/gnoi/file"
gnoifile "github.com/sonic-net/sonic-gnmi/pkg/gnoi/file"
ssc "github.com/sonic-net/sonic-gnmi/sonic_service_client"

// Renamed local package alias to avoid redeclaration / collision.
filepkg "github.com/sonic-net/sonic-gnmi/pkg/gnoi/file"

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
Expand All @@ -24,68 +18,7 @@ func (srv *FileServer) Stat(ctx context.Context, req *gnoi_file_pb.StatRequest)
log.Errorf("authentication failed in Stat RPC: %v", err)
return nil, err
}
path := req.GetPath()
log.V(1).Info("Request: ", req)
statInfo, err := readFileStat(path)
if err != nil {
log.Errorf("readFileStat error: %v", err)
return nil, err
}
resp := &gnoi_file_pb.StatResponse{
Stats: []*gnoi_file_pb.StatInfo{statInfo},
}
return resp, nil
}

func readFileStat(path string) (*gnoi_file_pb.StatInfo, error) {
sc, err := ssc.NewDbusClient()
if err != nil {
log.Errorf("DbusClient init failed: %v", err)
return nil, status.Errorf(codes.Internal, "DBus client init failed: %v", err)
}
defer sc.Close()
data, err := sc.GetFileStat(path)
if err != nil {
log.V(2).Infof("Failed to read file stat at path %s: %v. Error ", path, err)
return nil, err
}
// Parse the data and populate StatInfo
lastModified, err := strconv.ParseUint(data["last_modified"], 10, 64)
if err != nil {
log.Errorf("Stat Fails on Invalid last_modified %v", err)
return nil, err
}

permissions, err := strconv.ParseUint(data["permissions"], 8, 32)
if err != nil {
log.Errorf("Stat Fails on Invalid permissions: %v", err)
return nil, err
}

size, err := strconv.ParseUint(data["size"], 10, 64)
if err != nil {
log.Errorf("Stat Fails on Invalid size: %v", err)
return nil, err
}

umaskStr := data["umask"]
if strings.HasPrefix(umaskStr, "o") {
umaskStr = umaskStr[1:] // Remove leading "o"
}
umask, err := strconv.ParseUint(umaskStr, 8, 32)
if err != nil {
log.Errorf("Stat Fails on Invalid umaskStr: %v", err)
return nil, err
}

statInfo := &gnoi_file_pb.StatInfo{
Path: data["path"],
LastModified: lastModified,
Permissions: uint32(permissions),
Size: size,
Umask: uint32(umask),
}
return statInfo, nil
return gnoifile.HandleStat(ctx, req)
}

// Get RPC is unimplemented.
Expand Down Expand Up @@ -139,5 +72,5 @@ func (srv *FileServer) Remove(ctx context.Context, req *gnoi_file_pb.RemoveReque
return nil, err
}
// Delegate to handler (all logic except authentication is in the handler)
return filepkg.HandleFileRemove(ctx, req)
return gnoifile.HandleFileRemove(ctx, req)
}
126 changes: 13 additions & 113 deletions gnmi_server/gnoi_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"net"
"os"
"reflect"
"testing"

"github.com/agiledragon/gomonkey/v2"
Expand Down Expand Up @@ -71,22 +70,10 @@ func TestGnoiFileServer(t *testing.T) {

client := gnoi_file_pb.NewFileClient(conn)

t.Run("Stat Success", func(t *testing.T) {
patch1 := gomonkey.ApplyFuncReturn(authenticate, nil, nil)
patch2 := gomonkey.ApplyFuncReturn(ssc.NewDbusClient, &ssc.FakeClient{}, nil)
defer patch1.Reset()
defer patch2.Reset()

req := &gnoi_file_pb.StatRequest{Path: "/tmp/test.txt"}
resp, err := client.Stat(context.Background(), req)
if err != nil {
t.Fatalf("Expected success, got error: %v", err)
}
if len(resp.GetStats()) == 0 || resp.Stats[0].Path != "/tmp/test.txt" {
t.Fatalf("Unexpected Stat response: %+v", resp)
}
})

// Behavior coverage for HandleStat lives in pkg/gnoi/file/stat_test.go.
// The gnmi_server tests below only verify the server wiring: that the
// authenticate hook fires before the handler, and that handler errors
// surface as gRPC status codes through the server stack.
t.Run("Stat Fails with Auth Error", func(t *testing.T) {
patch := gomonkey.ApplyFuncReturn(authenticate, nil, status.Error(codes.Unauthenticated, "unauth"))
defer patch.Reset()
Expand All @@ -98,107 +85,20 @@ func TestGnoiFileServer(t *testing.T) {
}
})

t.Run("Stat Fails with Dbus Error", func(t *testing.T) {
patch1 := gomonkey.ApplyFuncReturn(authenticate, nil, nil)
patch2 := gomonkey.ApplyFuncReturn(ssc.NewDbusClient, nil, fmt.Errorf("dbus failure"))
defer patch1.Reset()
defer patch2.Reset()
t.Run("Stat Delegates to Handler", func(t *testing.T) {
// Smoke test: an authenticated request reaches HandleStat and a
// handler-level error (empty path -> InvalidArgument) propagates
// through the server stack as the matching gRPC status code.
patch := gomonkey.ApplyFuncReturn(authenticate, nil, nil)
defer patch.Reset()

req := &gnoi_file_pb.StatRequest{Path: "/tmp/test.txt"}
req := &gnoi_file_pb.StatRequest{Path: ""}
_, err := client.Stat(context.Background(), req)
if err == nil || status.Code(err) != codes.Internal {
t.Fatalf("Expected internal error, got: %v", err)
if err == nil || status.Code(err) != codes.InvalidArgument {
t.Fatalf("Expected InvalidArgument from handler, got: %v", err)
}
})

t.Run("Stat Fails on Invalid last_modified", func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()

badClient := &ssc.FakeClient{}
patches.ApplyFuncReturn(authenticate, nil, nil)
patches.ApplyFuncReturn(ssc.NewDbusClient, badClient, nil)
patches.ApplyMethod(reflect.TypeOf(badClient), "GetFileStat", func(_ *ssc.FakeClient, path string) (map[string]string, error) {
return map[string]string{
"path": path,
"last_modified": "not_a_number",
"permissions": "644",
"size": "100",
"umask": "022",
}, nil
})

_, err := readFileStat("/path/to/file")
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid syntax")
})

t.Run("Stat Fails on Invalid permissions", func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()

badClient := &ssc.FakeClient{}
patches.ApplyFuncReturn(authenticate, nil, nil)
patches.ApplyFuncReturn(ssc.NewDbusClient, badClient, nil)
patches.ApplyMethod(reflect.TypeOf(badClient), "GetFileStat", func(_ *ssc.FakeClient, path string) (map[string]string, error) {
return map[string]string{
"path": path,
"last_modified": "1686999999",
"permissions": "xyz",
"size": "100",
"umask": "022",
}, nil
})

_, err := readFileStat("/path/to/file")
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid syntax")
})

t.Run("Stat Fails on Invalid size", func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()

badClient := &ssc.FakeClient{}
patches.ApplyFuncReturn(authenticate, nil, nil)
patches.ApplyFuncReturn(ssc.NewDbusClient, badClient, nil)
patches.ApplyMethod(reflect.TypeOf(badClient), "GetFileStat", func(_ *ssc.FakeClient, path string) (map[string]string, error) {
return map[string]string{
"path": path,
"last_modified": "1686999999",
"permissions": "644",
"size": "abc",
"umask": "022",
}, nil
})

_, err := readFileStat("/path/to/file")
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid syntax")
})

t.Run("Stat Fails on Invalid umask", func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()

badClient := &ssc.FakeClient{}
patches.ApplyFuncReturn(authenticate, nil, nil)
patches.ApplyFuncReturn(ssc.NewDbusClient, badClient, nil)
patches.ApplyMethod(reflect.TypeOf(badClient), "GetFileStat", func(_ *ssc.FakeClient, path string) (map[string]string, error) {
return map[string]string{
"path": path,
"last_modified": "1686999999",
"permissions": "644",
"size": "100",
"umask": "oXYZ",
}, nil
})

_, err := readFileStat("/path/to/file")
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid syntax")
})

t.Run("Put Fails with Auth Error", func(t *testing.T) {
patch := gomonkey.ApplyFuncReturn(authenticate, nil, status.Error(codes.Unauthenticated, "unauthenticated"))
defer patch.Reset()
Expand Down
73 changes: 3 additions & 70 deletions gnmi_server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ import (
cacheclient "github.com/openconfig/gnmi/client"
gclient "github.com/openconfig/gnmi/client/gnmi"
gnmipb "github.com/openconfig/gnmi/proto/gnmi"
gnoi_file_pb "github.com/openconfig/gnoi/file"
gnoi_os_pb "github.com/openconfig/gnoi/os"
gnoi_system_pb "github.com/openconfig/gnoi/system"
)
Expand Down Expand Up @@ -4266,75 +4265,9 @@ func TestGNOI(t *testing.T) {
}
})

t.Run("FileStatSuccess", func(t *testing.T) {
mockClient := &ssc.DbusClient{}
expectedResult := map[string]string{
"last_modified": "1609459200000000000",
"permissions": "644",
"size": "1024",
"umask": "o022",
}
mock := gomonkey.ApplyMethod(reflect.TypeOf(mockClient), "GetFileStat", func(_ *ssc.DbusClient, path string) (map[string]string, error) {
return expectedResult, nil
})
defer mock.Reset()

// Prepare context and request
ctx := context.Background()
req := &gnoi_file_pb.StatRequest{Path: "/etc/sonic/config_db.json"}
fc := gnoi_file_pb.NewFileClient(conn)

resp, err := fc.Stat(ctx, req)
if err != nil {
t.Fatalf("FileStat failed: %v", err)
}
// Validate the response
if len(resp.Stats) == 0 {
t.Fatalf("Expected at least one StatInfo in response")
}

statInfo := resp.Stats[0]

if statInfo.LastModified != 1609459200000000000 {
t.Errorf("Expected last_modified %d but got %d", 1609459200000000000, statInfo.LastModified)
}
if statInfo.Permissions != 420 {
t.Errorf("Expected permissions 420 but got %d", statInfo.Permissions)
}
if statInfo.Size != 1024 {
t.Errorf("Expected size 1024 but got %d", statInfo.Size)
}
if statInfo.Umask != 18 {
t.Errorf("Expected umask 18 but got %d", statInfo.Umask)
}
})

t.Run("FileStatFailure", func(t *testing.T) {
mockClient := &ssc.DbusClient{}
expectedError := fmt.Errorf("failed to get file stats")

mock := gomonkey.ApplyMethod(reflect.TypeOf(mockClient), "GetFileStat", func(_ *ssc.DbusClient, path string) (map[string]string, error) {
return nil, expectedError
})
defer mock.Reset()

// Prepare context and request
ctx := context.Background()
req := &gnoi_file_pb.StatRequest{Path: "/etc/sonic/config_db.json"}
fc := gnoi_file_pb.NewFileClient(conn)

resp, err := fc.Stat(ctx, req)
if err == nil {
t.Fatalf("Expected error but got none")
}
if resp != nil {
t.Fatalf("Expected nil response but got: %v", resp)
}

if !strings.Contains(err.Error(), expectedError.Error()) {
t.Errorf("Expected error to contain '%v' but got '%v'", expectedError, err)
}
})
// Stat behavior is covered by pkg/gnoi/file/stat_test.go; the
// gnmi_server-level wiring (auth + delegation) is covered by
// TestGnoiFile in gnoi_file_test.go. No FileStat sub-test here.

t.Run("OSVerifySuccess", func(t *testing.T) {
mockClient := &ssc.DbusClient{}
Expand Down
Loading
Loading