100 lines
2.9 KiB
Go
100 lines
2.9 KiB
Go
// 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)
|
||
|
|
}
|
||
|
|
}
|