// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package nfs4server import ( "testing" "sourcedock.dev/petrbalvin/nfs/internal/nfs4" "sourcedock.dev/petrbalvin/nfs/internal/xdr" ) // A configured referral answers LOOKUP with a stub, GETATTR with the // location attributes, and everything that touches the backend with // NFS4ERR_MOVED, RFC 5661 sections 8.4.2 and 11.9. func TestReferralAndMoved(t *testing.T) { h := testTree(t) h.SetReferral("migrated", Referral{ Servers: []string{"10.0.0.9.8.1", "10.0.0.9.8.2"}, RootPath: []string{"exports", "data"}, }) sid, seq := newSession(t, h) run := func(s uint32, ops [][]byte) (nfs4.CompoundRes, [][]byte) { t.Helper() all := append([][]byte{nfs4.AppendSequenceArgs(nil, sid, s, 0, defaultSlots-1, true)}, ops...) payload := nfs4.AppendCompoundArgs(nil, "ref", nfs4.MinorVersion, all) body, ok := h.compound(payload, cred{uid: 0, gid: 0}) if !ok { t.Fatal("garbage") } r, bodies, derr := nfs4.DecodeCompoundResBodies(body) if derr != nil { t.Fatalf("decode: %v", derr) } return r, bodies[1:] } // LOOKUP lands on the stub and GETATTR carries the locations. seq++ res, bodies := run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "migrated"), // The blob carries only the one attribute: attributes encode in // numeric order, so the test reads them one call at a time. nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrFsLocations)), }) if res.Status != nfs4.ErrOK { t.Fatalf("lookup stub: status %d", res.Status) } ad := xdr.NewDecoder(bodies[2]) words, aerr := ad.Uint32() // the response bitmap: count then words if aerr != nil { t.Fatal(aerr) } if _, rerr := ad.Raw(int(words) * 4); rerr != nil { t.Fatal(rerr) } blob, aerr := ad.VarOpaque() if aerr != nil { t.Fatal(aerr) } fsRoot, locations, err := nfs4.DecodeFsLocations(blob) if err != nil { t.Fatalf("decode fs_locations: %v", err) } if len(locations) != 1 || len(locations[0].Servers) != 2 || locations[0].Servers[1] != "10.0.0.9.8.2" { t.Fatalf("locations %+v", locations) } if len(fsRoot) != 1 || fsRoot[0] != "" { t.Fatalf("fs root %v", fsRoot) } // A write against the stub is MOVED. seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "migrated"), nfs4.AppendWriteArgs(nil, nfs4.Stateid{}, 0, nfs4.StableFileSync, []byte("x")), }) if res.Status != nfs4.ErrMoved { t.Fatalf("write on stub: status %d, want MOVED", res.Status) } // The real tree still serves files under their own names. seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrSize)), }) wantStatus(t, "real file", res.Status, nfs4.ErrOK) }