136 lines
4.6 KiB
Go
136 lines
4.6 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/metazla/meta-core/internal/config"
|
|
)
|
|
|
|
// newRegisterTestServer wires handleRegisterFile to a temp store and a temp
|
|
// FILES_PATH. Built directly, not through NewServer, so no watcher runs —
|
|
// which is exactly the configuration this route exists for.
|
|
func newRegisterTestServer(t *testing.T) (*Server, string) {
|
|
t.Helper()
|
|
files := t.TempDir()
|
|
return &Server{storage: newTestStorage(t), config: &config.Config{FilesPath: files}}, files
|
|
}
|
|
|
|
func writeFixture(t *testing.T, files, rel, body string) string {
|
|
t.Helper()
|
|
full := filepath.Join(files, filepath.FromSlash(rel))
|
|
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(full, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return full
|
|
}
|
|
|
|
func postRegister(t *testing.T, s *Server, path string) (int, registerFileResponse) {
|
|
t.Helper()
|
|
body, _ := json.Marshal(registerFileRequest{Path: path})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/files/register", strings.NewReader(string(body)))
|
|
rr := httptest.NewRecorder()
|
|
s.handleRegisterFile(rr, req)
|
|
var resp registerFileResponse
|
|
_ = json.Unmarshal(rr.Body.Bytes(), &resp)
|
|
return rr.Code, resp
|
|
}
|
|
|
|
func streamLen(t *testing.T, s *Server) int {
|
|
t.Helper()
|
|
n, err := s.storage.GetStreamLength("file:events")
|
|
if err != nil {
|
|
t.Fatalf("stream length: %v", err)
|
|
}
|
|
return int(n)
|
|
}
|
|
|
|
// The whole point of the route: with the watcher off, a file written under
|
|
// plugin/ becomes a record resolvable by its midhash, and meta-share hears
|
|
// about it on file:events.
|
|
func TestRegisterFile_MintsAliasAndPublishesEvent(t *testing.T) {
|
|
s, files := newRegisterTestServer(t)
|
|
rel := "plugin/subtitles/ab12cd34.eng.srt"
|
|
full := writeFixture(t, files, rel, "1\n00:00:01,000 --> 00:00:02,000\nHello\n")
|
|
|
|
code, resp := postRegister(t, s, rel)
|
|
if code != http.StatusCreated || !resp.Created {
|
|
t.Fatalf("want 201 created, got %d %+v", code, resp)
|
|
}
|
|
if !strings.HasPrefix(resp.CID, "b") || resp.Root == "" {
|
|
t.Fatalf("want a multibase cid and a root, got %+v", resp)
|
|
}
|
|
|
|
root, err := s.storage.GetByCID(resp.CID)
|
|
if err != nil || root != resp.Root {
|
|
t.Fatalf("cid alias must resolve to the root: got %q err=%v, want %q", root, err, resp.Root)
|
|
}
|
|
if fp, _ := s.storage.GetProperty(resp.Root, "filePath"); fp != full {
|
|
t.Fatalf("filePath = %q, want %q", fp, full)
|
|
}
|
|
|
|
entries, err := s.storage.XRevRange("file:events", "+", "-", 0)
|
|
if err != nil || len(entries) != 1 {
|
|
t.Fatalf("want exactly one file:events entry, got %d err=%v", len(entries), err)
|
|
}
|
|
if vals := entries[0].Values; vals["midhash256"] != resp.CID || vals["path"] != rel {
|
|
t.Fatalf("event must carry the midhash and the relative path, got %v", vals)
|
|
}
|
|
}
|
|
|
|
// Re-uploading the same subtitle is a no-op: same root, no second event (which
|
|
// would make meta-share re-chunk and re-provide the file).
|
|
func TestRegisterFile_IsIdempotent(t *testing.T) {
|
|
s, files := newRegisterTestServer(t)
|
|
rel := "plugin/subtitles/same.eng.srt"
|
|
writeFixture(t, files, rel, "same bytes")
|
|
|
|
_, first := postRegister(t, s, rel)
|
|
code, second := postRegister(t, s, rel)
|
|
if code != http.StatusOK || second.Created {
|
|
t.Fatalf("second register must be 200 created=false, got %d %+v", code, second)
|
|
}
|
|
if second.Root != first.Root || second.CID != first.CID {
|
|
t.Fatalf("second register must return the same record: %+v vs %+v", second, first)
|
|
}
|
|
if n := streamLen(t, s); n != 1 {
|
|
t.Fatalf("an idempotent re-register must not publish again, stream has %d", n)
|
|
}
|
|
}
|
|
|
|
// Only plugin output may be registered here. /files/watch is the user's
|
|
// library and belongs to the watcher; traversal must not escape either.
|
|
func TestRegisterFile_RejectsPathsOutsidePlugin(t *testing.T) {
|
|
s, files := newRegisterTestServer(t)
|
|
writeFixture(t, files, "watch/movie.mkv", "x")
|
|
for _, p := range []string{
|
|
"", "plugin", "plugin/", "watch/movie.mkv", "/watch/movie.mkv",
|
|
"plugin/../watch/movie.mkv", "../etc/passwd", "plugin/../../etc/passwd",
|
|
} {
|
|
if code, _ := postRegister(t, s, p); code != http.StatusBadRequest {
|
|
t.Errorf("path %q: want 400, got %d", p, code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegisterFile_MissingFileAndDirectory(t *testing.T) {
|
|
s, files := newRegisterTestServer(t)
|
|
if code, _ := postRegister(t, s, "plugin/subtitles/nope.srt"); code != http.StatusNotFound {
|
|
t.Errorf("missing file: want 404, got %d", code)
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(files, "plugin", "subtitles", "dir"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if code, _ := postRegister(t, s, "plugin/subtitles/dir"); code != http.StatusBadRequest {
|
|
t.Errorf("directory: want 400, got %d", code)
|
|
}
|
|
}
|