Files
hamlab/internal/api/files_test.go
Mitchell-JohnsonandClaude Fable 5.1 285278abdb A sweep of the whole console: status codes, a data race, atomic writes, dead code
From the race detector, staticcheck, a garbage-in battery over every safe route and a
headless pass through every tab on two machines. Fixed:
- docker inspect/health of a container that isn't there answered 500; now 404, and a
  daemon that is down 503 (dockerErr, with a test).
- /api/virt/instances/{name} on a machine without Incus answered 502 "incus not found";
  it now says Incus isn't ready, like its siblings.
- Deleting a ZFS snapshot with a bad name answered 500; a bad name is 400, a snapshot
  that doesn't exist 404.
- The task list read a task's Running flag without the task's lock while the waiter
  goroutine cleared it: a data race, now read under the lock.
- The Virtualization export config and run record were written in place; a crash
  mid-write would have left a half file. Atomic now, like the config and state files.
- An unused function in kvm.go, one in incus/subnet.go, an unused field in files/move.go
  and a capitalised error string, from staticcheck.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 17:52:18 -07:00

124 lines
4.1 KiB
Go

package api
import (
"bytes"
"errors"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"
"hamlab/internal/files"
)
// A file read answers Range requests — what a <video> needs to seek — and HEAD, through
// the same handler that streams everything else.
func TestFilesReadRanges(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "clip.mp4")
data := make([]byte, 10000)
for i := range data {
data[i] = byte(i % 251)
}
if err := os.WriteFile(p, data, 0o644); err != nil {
t.Fatal(err)
}
s := &Server{fsLocal: files.Local{}}
get := func(method, extra string, hdr map[string]string) *httptest.ResponseRecorder {
r := httptest.NewRequest(method, "/api/files/read?path="+url.QueryEscape(p)+extra, nil)
for k, v := range hdr {
r.Header.Set(k, v)
}
w := httptest.NewRecorder()
s.filesRead(w, r)
return w
}
w := get("GET", "", nil)
if w.Code != 200 || w.Body.Len() != 10000 || w.Header().Get("Content-Type") != "video/mp4" || w.Header().Get("Accept-Ranges") != "bytes" {
t.Fatalf("whole: %d %d %q %q", w.Code, w.Body.Len(), w.Header().Get("Content-Type"), w.Header().Get("Accept-Ranges"))
}
w = get("GET", "", map[string]string{"Range": "bytes=100-199"})
if w.Code != 206 || w.Header().Get("Content-Range") != "bytes 100-199/10000" || w.Body.Len() != 100 || w.Body.Bytes()[0] != data[100] {
t.Fatalf("range: %d %q %d", w.Code, w.Header().Get("Content-Range"), w.Body.Len())
}
w = get("GET", "", map[string]string{"Range": "bytes=9990-"})
if w.Code != 206 || w.Body.Len() != 10 || w.Body.Bytes()[9] != data[9999] {
t.Fatalf("tail range: %d %d", w.Code, w.Body.Len())
}
w = get("HEAD", "", nil)
if w.Code != 200 || w.Body.Len() != 0 || w.Header().Get("Content-Length") != "10000" {
t.Fatalf("head: %d %d %q", w.Code, w.Body.Len(), w.Header().Get("Content-Length"))
}
if w := get("GET", "&max=50", nil); w.Code != 413 {
t.Fatalf("the editor's cap: %d", w.Code)
}
if w := get("GET", "", map[string]string{"Range": "bytes=20000-"}); w.Code != 416 {
t.Fatalf("a range past the end: %d", w.Code)
}
if err := os.Chmod(p, 0); err == nil && os.Geteuid() != 0 {
if w := get("GET", "", nil); w.Code != 403 {
t.Fatalf("unreadable: %d", w.Code)
}
}
}
// A write over the cap is refused whole: the file is not truncated first.
func TestFilesWriteCap(t *testing.T) {
p := filepath.Join(t.TempDir(), "f.txt")
if err := os.WriteFile(p, []byte("keep me"), 0o644); err != nil {
t.Fatal(err)
}
s := &Server{fsLocal: files.Local{}}
body := make([]byte, 8<<20+1)
r := httptest.NewRequest("PUT", "/api/files/write?path="+url.QueryEscape(p), bytes.NewReader(body))
w := httptest.NewRecorder()
s.filesWrite(w, r)
if w.Code != 413 {
t.Fatalf("got %d", w.Code)
}
if b, _ := os.ReadFile(p); string(b) != "keep me" {
t.Fatalf("the file was touched: %q", b)
}
r = httptest.NewRequest("PUT", "/api/files/write?path="+url.QueryEscape(p), bytes.NewReader([]byte("new")))
w = httptest.NewRecorder()
s.filesWrite(w, r)
if b, _ := os.ReadFile(p); w.Code != 204 || string(b) != "new" {
t.Fatalf("a write within the cap: %d %q", w.Code, b)
}
}
// A link to nothing is a 404, not an empty 200.
func TestFilesReadDanglingLink(t *testing.T) {
dir := t.TempDir()
l := filepath.Join(dir, "broken")
if err := os.Symlink("nowhere", l); err != nil {
t.Fatal(err)
}
s := &Server{fsLocal: files.Local{}}
r := httptest.NewRequest("GET", "/api/files/read?path="+url.QueryEscape(l), nil)
w := httptest.NewRecorder()
s.filesRead(w, r)
if w.Code != 404 {
t.Fatalf("got %d: %s", w.Code, w.Body.String())
}
}
// dockerErr tells a thing that isn't there from a daemon that isn't up from anything else.
func TestDockerErr(t *testing.T) {
for _, c := range []struct {
msg string
want int
}{
{"error: No such object: zzz", 404}, {"Error response from daemon: No such container: x", 404},
{"Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?", 503},
{"something else entirely", 500},
} {
w := httptest.NewRecorder()
dockerErr(w, errors.New(c.msg))
if w.Code != c.want {
t.Errorf("%q → %d, want %d", c.msg, w.Code, c.want)
}
}
}