Files
nfs/internal/nfsfs/ro_test.go
T

100 lines
2.9 KiB
Go
Raw Normal View History

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfsfs
import (
"errors"
"os"
"path/filepath"
"testing"
)
// roTestRoot builds a backend over a fresh directory holding one file.
func roTestRoot(t *testing.T) (*Local, Handle, string) {
t.Helper()
root := t.TempDir()
l, err := NewLocal(root)
if err != nil {
t.Fatalf("NewLocal: %v", err)
}
p := filepath.Join(root, "file")
if err := os.WriteFile(p, []byte("read only"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
rh, err := l.Root()
if err != nil {
t.Fatalf("Root: %v", err)
}
h, _, err := l.Lookup(rh, "file")
if err != nil {
t.Fatalf("Lookup: %v", err)
}
return l, h, p
}
// TestReadOnlyHidesWriter covers the contract the dispatcher relies on: a
// read only export carries no Writer half, so every mutating operation
// answers NFS4ERR_ROFS without the backend ever being asked.
func TestReadOnlyHidesWriter(t *testing.T) {
l, _, _ := roTestRoot(t)
ro := ReadOnly(l)
if _, ok := ro.(Writer); ok {
t.Fatal("the read only export still carries a Writer")
}
var fs FS = l
if _, ok := fs.(Writer); !ok {
t.Fatal("the plain backend lost its Writer")
}
}
// TestReadOnlyRefusesWrites covers the optional halves: their reads pass
// through, their writes answer ErrReadOnly.
func TestReadOnlyRefusesWrites(t *testing.T) {
l, h, _ := roTestRoot(t)
ro := ReadOnly(l)
if err := ro.(XattrFS).SetXattr(h, "user.note", []byte("x"), XattrModeCreate); !errors.Is(err, ErrReadOnly) {
t.Fatalf("SetXattr: %v, want ErrReadOnly", err)
}
if err := ro.(XattrFS).RemoveXattr(h, "user.note"); !errors.Is(err, ErrReadOnly) {
t.Fatalf("RemoveXattr: %v, want ErrReadOnly", err)
}
if err := ro.(interface {
Allocate(Handle, int64, int64) error
Deallocate(Handle, int64, int64) error
}).Allocate(h, 0, 4096); !errors.Is(err, ErrReadOnly) {
t.Fatalf("Allocate: %v, want ErrReadOnly", err)
}
if err := ro.(interface {
Allocate(Handle, int64, int64) error
Deallocate(Handle, int64, int64) error
}).Deallocate(h, 0, 4096); !errors.Is(err, ErrReadOnly) {
t.Fatalf("Deallocate: %v, want ErrReadOnly", err)
}
// The reads pass through: whatever the platform answers for a missing
// attribute and a seeking question, neither is the read only refusal.
if _, err := ro.(XattrFS).GetXattr(h, "user.note", 1024); errors.Is(err, ErrReadOnly) {
t.Fatal("GetXattr refused as a write")
}
if _, _, err := ro.(interface {
SeekHole(Handle, int64) (int64, bool, error)
SeekData(Handle, int64) (int64, bool, error)
}).SeekHole(h, 0); errors.Is(err, ErrReadOnly) {
t.Fatal("SeekHole refused as a write")
}
}
// TestReadOnlyReadsWork covers that the read side of the wrapped export
// is the export itself.
func TestReadOnlyReadsWork(t *testing.T) {
l, h, _ := roTestRoot(t)
ro := ReadOnly(l)
got, err := ro.Read(h, 0, 9)
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(got) != "read only" {
t.Fatalf("Read: %q", got)
}
}