Files
petrbalvin a9b8039ef7
Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
feat: full NFSv4.2 server and client in pure Go
Assisted-by: GLM 5.3 Flash
2026-09-21 18:51:17 +02:00

200 lines
5.8 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package rpc
import (
"bytes"
"errors"
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
func TestCallRoundTrip(t *testing.T) {
cred := Auth{Flavor: FlavorSys, Body: []byte{1, 2, 3}}
c := Call{XID: 0xfeedface, Program: 100003, Version: 4, Procedure: 1, Cred: cred}
rec, err := AppendCall(nil, c)
if err != nil {
t.Fatalf("encode: %v", err)
}
rec = append(rec, xdr.AppendString(nil, "tag")...)
got, args, err := DecodeCall(rec)
if err != nil {
t.Fatalf("decode: %v", err)
}
if got.XID != c.XID || got.Program != c.Program || got.Version != c.Version ||
got.Procedure != c.Procedure ||
got.Cred.Flavor != c.Cred.Flavor || !bytes.Equal(got.Cred.Body, c.Cred.Body) {
t.Fatalf("round trip: got %+v, want %+v", got, c)
}
tag, err := xdr.NewDecoder(args).String()
if err != nil || tag != "tag" {
t.Fatalf("arguments after the header: %q, %v", tag, err)
}
}
func TestDecodeCallRejectsReplies(t *testing.T) {
rec := xdr.AppendUint32(xdr.AppendUint32(nil, 1), MsgReply)
if _, _, err := DecodeCall(rec); err == nil {
t.Fatal("a reply record decoded as a call")
}
}
func TestOversizedAuthBody(t *testing.T) {
c := Call{XID: 1, Program: 100003, Version: Version, Procedure: 0,
Cred: Auth{Flavor: FlavorSys, Body: make([]byte, maxAuthBody+1)}}
if _, err := AppendCall(nil, c); err == nil {
t.Fatal("an oversized credential encoded without error")
}
}
func TestAcceptedReplyRoundTrip(t *testing.T) {
rec, err := AppendAcceptedReply(nil, 7, AuthNull, AcceptSuccess, Mismatch{})
if err != nil {
t.Fatalf("encode: %v", err)
}
rec = append(rec, xdr.AppendUint32(nil, 42)...)
r, err := DecodeReply(rec)
if err != nil {
t.Fatalf("decode: %v", err)
}
if r.XID != 7 || r.Status != AcceptSuccess {
t.Fatalf("got xid %d status %d", r.XID, r.Status)
}
body, err := xdr.NewDecoder(r.Body).Uint32()
if err != nil || body != 42 {
t.Fatalf("result body: %d, %v", body, err)
}
}
func TestProgMismatchReply(t *testing.T) {
rec, err := AppendAcceptedReply(nil, 9, AuthNull, AcceptProgMismatch, Mismatch{Low: 4, High: 4})
if err != nil {
t.Fatalf("encode: %v", err)
}
r, err := DecodeReply(rec)
if err != nil {
t.Fatalf("decode: %v", err)
}
if r.Status != AcceptProgMismatch || r.Mismatch != (Mismatch{4, 4}) {
t.Fatalf("got status %d mismatch %+v", r.Status, r.Mismatch)
}
}
func TestUnknownAcceptStatus(t *testing.T) {
if _, err := AppendAcceptedReply(nil, 1, AuthNull, 99, Mismatch{}); err == nil {
t.Fatal("an unknown accept status encoded without error")
}
}
func TestAuthSysRoundTrip(t *testing.T) {
a := AuthSys{Stamp: 12, Machine: "client", UID: 1000, GID: 100, GIDs: []uint32{100, 5, 27}}
body, err := a.Body()
if err != nil {
t.Fatalf("encode: %v", err)
}
got, err := DecodeAuthSysBody(body)
if err != nil {
t.Fatalf("decode: %v", err)
}
if got.Stamp != a.Stamp || got.Machine != a.Machine || got.UID != a.UID ||
got.GID != a.GID || !equalU32(got.GIDs, a.GIDs) {
t.Fatalf("round trip: got %+v, want %+v", got, a)
}
}
func equalU32(a, b []uint32) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func TestAuthSysTooManyGroups(t *testing.T) {
a := AuthSys{GIDs: make([]uint32, 17)}
if _, err := a.Body(); err == nil {
t.Fatal("17 supplementary groups encoded without error")
}
if _, err := DecodeAuthSysBody(xdr.AppendUint32(xdr.AppendUint32(nil, 0), 17)); err == nil {
t.Fatal("a credential claiming 17 groups decoded without error")
}
}
func TestDecodeRejectedReply(t *testing.T) {
// RPC_MISMATCH carries the version range.
rec := xdr.AppendUint32(nil, 5)
rec = xdr.AppendUint32(rec, MsgReply)
rec = xdr.AppendUint32(rec, 1) // rejected
rec = xdr.AppendUint32(rec, RejectRPCMismatch)
rec = xdr.AppendUint32(rec, 2)
rec = xdr.AppendUint32(rec, 4)
r, err := DecodeReply(rec)
if err != nil || r.XID != 5 || !r.Rejected || r.Mismatch != (Mismatch{2, 4}) {
t.Fatalf("rpc mismatch: %+v, %v", r, err)
}
if r.Status != 0 || r.Body != nil {
t.Fatalf("a rejected reply carries no result: %+v", r)
}
// An auth error carries one status word.
rec = xdr.AppendUint32(nil, 6)
rec = xdr.AppendUint32(rec, MsgReply)
rec = xdr.AppendUint32(rec, 1)
rec = xdr.AppendUint32(rec, RejectAuthError)
rec = xdr.AppendUint32(rec, AuthBadVerf)
if r, err = DecodeReply(rec); err != nil || r.XID != 6 || !r.Rejected || r.AuthStat != AuthBadVerf {
t.Fatalf("auth error: %+v, %v", r, err)
}
}
func TestDecodeAuthOversizeBody(t *testing.T) {
// A credential body beyond 400 bytes is refused, not buffered.
huge := xdr.AppendUint32(nil, FlavorSys)
huge = xdr.AppendUint32(huge, maxAuthBody+1)
huge = append(huge, make([]byte, 8)...)
rec := xdr.AppendUint32(nil, 1)
rec = xdr.AppendUint32(rec, MsgCall)
rec = xdr.AppendUint32(rec, Version)
rec = xdr.AppendUint32(rec, 100003)
rec = xdr.AppendUint32(rec, 4)
rec = xdr.AppendUint32(rec, 0)
rec = append(rec, huge...)
if _, _, err := DecodeCall(rec); !errors.Is(err, ErrBadMessage) {
t.Fatalf("an oversized credential decoded as %v", err)
}
}
func TestDecodeReplyGarbage(t *testing.T) {
for _, rec := range [][]byte{
nil,
{0, 0, 0}, // short
xdr.AppendUint32(nil, 1), // no type
{0, 0, 0, 1, 0, 0, 0, 9}, // unknown reply stat 9
} {
if _, err := DecodeReply(rec); !errors.Is(err, ErrBadMessage) {
t.Fatalf("record %x decoded with %v, want ErrBadMessage", rec, err)
}
}
}
func TestPeekHeader(t *testing.T) {
// A minimal call record: xid and the call type.
b := xdr.AppendUint32(xdr.AppendUint32(nil, 0x55), MsgCall)
xid, mtype, err := PeekHeader(b)
if err != nil || xid != 0x55 || mtype != MsgCall {
t.Fatalf("call peek: %x %d, %v", xid, mtype, err)
}
if _, _, err := PeekHeader(nil); !errors.Is(err, ErrBadMessage) {
t.Fatalf("an empty record: %v", err)
}
}