Files
excalicloud/internal/storage/spaces_test.go
T
claude BotandClaude Opus 5.5 824871bd24 Espaces personnels et favoris : API par utilisateur, /api/me.id, tests
- Chaque route de schémas passe par la vue de l'utilisateur connecté
  (storage.Space) : arborescence commune, .perso et .favoris.
- /api/public résout aussi les liens des espaces personnels.
- /api/me renvoie id (dossier de l'utilisateur : email OIDC, sinon « local »).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174sFqehj4uGqJHQZvWy1gn
2026-10-08 10:26:31 +02:00

270 lines
8.2 KiB
Go

package storage
import (
"errors"
"os"
"path/filepath"
"testing"
)
func TestUserID(t *testing.T) {
for in, want := range map[string]string{
" Cedric.Killy@VLab.bzh ": "cedric.killy@vlab.bzh",
"jean dupont/../x": "jean_dupont_.._x",
"..": "_.",
"": LocalUser,
} {
if got := UserID(in); got != want {
t.Errorf("UserID(%q) = %q ; attendu %q", in, got, want)
}
}
}
func newSpaces(t *testing.T) *Spaces {
t.Helper()
return NewSpaces(newStore(t))
}
func space(t *testing.T, sp *Spaces, id string) *Space {
t.Helper()
u, err := sp.For(id)
if err != nil {
t.Fatal(err)
}
return u
}
func write(t *testing.T, u *Space, p string) {
t.Helper()
if _, _, err := u.Write(p, []byte(doc), "", true); err != nil {
t.Fatalf("écriture de %s : %v", p, err)
}
}
func favPaths(t *testing.T, u *Space) []string {
t.Helper()
es, err := u.Favorites()
if err != nil {
t.Fatal(err)
}
out := []string{}
for _, e := range es {
if e.Broken {
out = append(out, "!"+e.Path)
} else {
out = append(out, e.Path)
}
}
return out
}
func same(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func TestSpacesListAndPrivacy(t *testing.T) {
sp := newSpaces(t)
a, b := space(t, sp, "a@x.fr"), space(t, sp, "b@x.fr")
write(t, a, ".perso/secret.excalidraw")
write(t, a, "commun.excalidraw")
l, err := a.List("")
if err != nil || len(l) != 3 || l[0].Special != "favoris" || l[1].Special != "perso" || l[2].Path != "commun.excalidraw" {
t.Fatalf("racine = %+v, %v", l, err)
}
if l, _ := a.List(".perso"); len(l) != 1 || l[0].Path != ".perso/secret.excalidraw" {
t.Fatalf("espace personnel de a = %+v", l)
}
if l, _ := b.List(".perso"); len(l) != 0 {
t.Fatalf("b voit l'espace de a : %+v", l)
}
if _, _, err := b.Read(".perso/secret.excalidraw"); !errors.Is(err, ErrNotFound) {
t.Fatalf("b lit le schéma de a : %v", err)
}
// Ni l'arborescence commune ni la recherche ne montrent .users.
if _, _, err := b.Read(".users/a@x.fr/home/secret.excalidraw"); !errors.Is(err, ErrInvalidPath) {
t.Fatalf("accès direct à .users : %v", err)
}
if res, _ := b.Search("secret", Filter{}, 10); len(res) != 0 {
t.Fatalf("recherche de b = %+v", res)
}
if res, _ := a.Search("secret", Filter{}, 10); len(res) != 1 || res[0].Path != ".perso/secret.excalidraw" {
t.Fatalf("recherche de a = %+v", res)
}
}
func TestSpacesFavorites(t *testing.T) {
sp := newSpaces(t)
a, b := space(t, sp, "a@x.fr"), space(t, sp, "b@x.fr")
write(t, a, "d/s.excalidraw")
write(t, a, ".perso/p.excalidraw")
if err := a.SetFavorite([]string{"d"}, true); !errors.Is(err, ErrInvalidPath) {
t.Fatalf("dossier en favori : %v", err)
}
if err := a.SetFavorite([]string{"d/s.excalidraw", ".perso/p.excalidraw"}, true); err != nil {
t.Fatal(err)
}
if err := b.SetFavorite([]string{"d/s.excalidraw"}, true); err != nil {
t.Fatal(err)
}
if got := favPaths(t, a); !same(got, []string{".perso/p.excalidraw", "d/s.excalidraw"}) {
t.Fatalf("favoris de a = %v", got)
}
if l, _ := a.List("d"); len(l) != 1 || !l[0].Favorite {
t.Fatalf("étoile absente : %+v", l)
}
if res, _ := a.Search("", Filter{Favorite: true}, 10); len(res) != 2 {
t.Fatalf("filtre favoris = %+v", res)
}
// Le lien est relatif et vise l'original.
fi, err := os.Lstat(filepath.Join(sp.favDir("a@x.fr"), "s.excalidraw"))
if err != nil || fi.Mode()&os.ModeSymlink == 0 {
t.Fatalf("lien symbolique attendu : %v", err)
}
// Renommage du dossier : les favoris des deux utilisateurs suivent.
if _, err := a.Move("d", "e"); err != nil {
t.Fatal(err)
}
if got := favPaths(t, b); !same(got, []string{"e/s.excalidraw"}) {
t.Fatalf("favoris de b après renommage = %v", got)
}
// Passage dans l'espace personnel de a : a garde son favori, b le perd.
if _, err := a.Move("e/s.excalidraw", ".perso/s.excalidraw"); err != nil {
t.Fatal(err)
}
if got := favPaths(t, a); !same(got, []string{".perso/p.excalidraw", ".perso/s.excalidraw"}) {
t.Fatalf("favoris de a après déplacement = %v", got)
}
if got := favPaths(t, b); len(got) != 0 {
t.Fatalf("b garde un favori vers l'espace de a : %v", got)
}
// Retour dans l'arborescence commune par MoveMany.
res, err := a.MoveMany([]string{".perso/p.excalidraw"}, "")
if err != nil || res[0].To != "p.excalidraw" || res[0].Err() != nil {
t.Fatalf("MoveMany = %+v, %v", res, err)
}
if got := favPaths(t, a); !same(got, []string{"p.excalidraw", ".perso/s.excalidraw"}) {
t.Fatalf("favoris de a après MoveMany = %v", got)
}
// Suppression : le favori disparaît.
if err := a.Delete("p.excalidraw", false); err != nil {
t.Fatal(err)
}
if got := favPaths(t, a); !same(got, []string{".perso/s.excalidraw"}) {
t.Fatalf("favoris de a après suppression = %v", got)
}
// Original supprimé hors de l'application : favori cassé, retirable.
if err := os.Remove(filepath.Join(a.home.root, "s.excalidraw")); err != nil {
t.Fatal(err)
}
if got := favPaths(t, a); !same(got, []string{"!.favoris/s.excalidraw"}) {
t.Fatalf("favori cassé = %v", got)
}
if err := a.Delete(".favoris/s.excalidraw", false); err != nil {
t.Fatal(err)
}
if got := favPaths(t, a); len(got) != 0 {
t.Fatalf("favori cassé non retiré : %v", got)
}
}
func TestSpacesFavoriteNames(t *testing.T) {
sp := newSpaces(t)
a := space(t, sp, "a@x.fr")
write(t, a, "x/s.excalidraw")
write(t, a, "y/s.excalidraw")
if err := a.SetFavorite([]string{"x/s.excalidraw", "y/s.excalidraw", "x/s.excalidraw"}, true); err != nil {
t.Fatal(err)
}
es, _ := a.Favorites()
if len(es) != 2 || es[0].Link == es[1].Link {
t.Fatalf("favoris homonymes = %+v", es)
}
if err := a.SetFavorite([]string{"y/s.excalidraw"}, false); err != nil {
t.Fatal(err)
}
if got := favPaths(t, a); !same(got, []string{"x/s.excalidraw"}) {
t.Fatalf("après retrait = %v", got)
}
}
func TestSpacesSeedFromMeta(t *testing.T) {
sp := newSpaces(t)
write(t, space(t, sp, "a@x.fr"), "old.excalidraw")
if err := sp.Main.SetFavorite([]string{"old.excalidraw"}, true); err != nil {
t.Fatal(err)
}
// Un nouvel utilisateur reprend les favoris communs, une seule fois.
c := space(t, sp, "c@x.fr")
if got := favPaths(t, c); !same(got, []string{"old.excalidraw"}) {
t.Fatalf("favoris repris = %v", got)
}
if err := c.SetFavorite([]string{"old.excalidraw"}, false); err != nil {
t.Fatal(err)
}
sp.homes = map[string]*Store{} // redémarrage
if got := favPaths(t, space(t, sp, "c@x.fr")); len(got) != 0 {
t.Fatalf("favoris repris deux fois : %v", got)
}
}
func TestSpacesSharesAndCrossMove(t *testing.T) {
sp := newSpaces(t)
a := space(t, sp, "a@x.fr")
write(t, a, ".perso/p.excalidraw")
if err := a.EditTags([]string{".perso/p.excalidraw"}, []string{"NSX"}, nil); err != nil {
t.Fatal(err)
}
if _, err := a.home.Snapshot("p.excalidraw"); err != nil {
t.Fatal(err)
}
sh, err := a.CreateShare(".perso/p.excalidraw", 0)
if err != nil || sh.Path != ".perso/p.excalidraw" {
t.Fatalf("partage = %+v, %v", sh, err)
}
if got, _, err := sp.ReadShared(sh.Token); err != nil || got.Path != "p.excalidraw" {
t.Fatalf("lien personnel : %+v, %v", got, err)
}
// Vers l'arborescence commune : tags, historique et lien suivent.
if _, err := a.Move(".perso/p.excalidraw", "q.excalidraw"); err != nil {
t.Fatal(err)
}
if l, _ := a.List(""); len(l) != 3 || len(l[2].Tags) != 1 {
t.Fatalf("tags après déplacement : %+v", l)
}
if vs, err := a.Versions("q.excalidraw"); err != nil || len(vs) != 1 {
t.Fatalf("historique après déplacement : %+v, %v", vs, err)
}
if got, _, err := sp.ReadShared(sh.Token); err != nil || got.Path != "q.excalidraw" {
t.Fatalf("lien après déplacement : %+v, %v", got, err)
}
if tags, _ := a.Tags(); len(tags) != 1 || tags[0].Count != 1 {
t.Fatalf("tags = %+v", tags)
}
// Révocation globale : clé changée, liens personnels compris.
sh2, _ := a.CreateShare("q.excalidraw", 0)
write(t, a, ".perso/r.excalidraw")
sh3, _ := a.CreateShare(".perso/r.excalidraw", 0)
if err := a.RevokeAllShares(); err != nil {
t.Fatal(err)
}
for _, tok := range []string{sh2.Token, sh3.Token} {
if _, _, err := sp.ReadShared(tok); !errors.Is(err, ErrNotFound) {
t.Fatalf("lien encore valide après révocation : %v", err)
}
}
}