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) } }