Files
claude BotandClaude Opus 5.5 50d40cd929 Partage en lecture seule par lien signé
- Lien /s/<jeton> : jeton = id aléatoire 128 bits + HMAC-SHA256 tronqué par la
  clé du serveur (SHARE_SECRET, sinon DATA_DIR/.share-secret générée en 0600).
  Valide si la signature est correcte, le lien présent dans .shares.json et non
  expiré (1, 7, 30 jours ou sans expiration).
- API : GET/POST/DELETE /api/shares, POST /api/shares/revoke-all (renouvelle la
  clé), GET /api/public/{jeton} hors authentification (nom, titre, logo, scène).
- Auth Basic : /s/, /api/public/, /healthz et les fichiers statiques du build
  sont publics ; le reste de l'API et l'index restent protégés. Le serveur
  statique ne liste plus les répertoires. X-Robots-Tag: noindex sur les liens.
- Les liens suivent renommages et déplacements, disparaissent à la suppression.
- Interface : dialogue Partager (barre du haut, menu de l'explorateur), section
  Liens de partage dans Paramètres, vue publique Excalidraw en lecture seule
  avec téléchargement et bascule clair/sombre.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6J7vKwXx7uopxKhWfFJpm
2026-10-07 18:10:09 +02:00

108 lines
3.3 KiB
Go

package storage
import (
"errors"
"os"
"path/filepath"
"testing"
"time"
)
func TestShares(t *testing.T) {
s := newStore(t)
s.Write("d/a.excalidraw", []byte(doc), "", true)
if _, err := s.CreateShare("d/absent.excalidraw", 0); !errors.Is(err, ErrNotFound) {
t.Fatalf("schéma absent : %v", err)
}
if _, err := s.CreateShare("d", 0); !errors.Is(err, ErrInvalidPath) {
t.Fatalf("dossier : %v", err)
}
sh, err := s.CreateShare("d/a.excalidraw", 0)
if err != nil {
t.Fatal(err)
}
if fi, _ := os.Stat(filepath.Join(s.root, secretFile)); fi == nil || fi.Mode().Perm() != 0o600 {
t.Fatalf("clé : %v", fi)
}
got, data, err := s.ReadShared(sh.Token)
if err != nil || got.Path != "d/a.excalidraw" || string(data) != doc {
t.Fatalf("ReadShared = %+v, %s, %v", got, data, err)
}
// Jetons altérés ou mal formés.
bad := []string{"", "x", sh.ID, sh.ID + ".", sh.Token[:len(sh.Token)-1] + "A", "A" + sh.Token[1:]}
for _, tok := range bad {
if tok == sh.Token {
continue
}
if _, err := s.ResolveShare(tok); !errors.Is(err, ErrNotFound) {
t.Errorf("jeton %q accepté : %v", tok, err)
}
}
// La clé survit au redémarrage ; le lien suit le schéma déplacé.
s2, _ := New(s.root)
s2.Move("d", "e")
if got, err := s2.ResolveShare(sh.Token); err != nil || got.Path != "e/a.excalidraw" {
t.Fatalf("après déplacement : %+v, %v", got, err)
}
if l, _ := s2.Shares("e/a.excalidraw"); len(l) != 1 || l[0].Token != sh.Token {
t.Fatalf("Shares = %+v", l)
}
// Révocation unitaire.
if err := s2.RevokeShare(sh.ID); err != nil {
t.Fatal(err)
}
if _, err := s2.ResolveShare(sh.Token); !errors.Is(err, ErrNotFound) {
t.Fatalf("lien révoqué accepté : %v", err)
}
// Expiration : le lien expiré est refusé puis purgé de la liste.
exp, _ := s2.CreateShare("e/a.excalidraw", time.Millisecond)
time.Sleep(5 * time.Millisecond)
if _, err := s2.ResolveShare(exp.Token); !errors.Is(err, ErrNotFound) {
t.Fatalf("lien expiré accepté : %v", err)
}
if l, _ := s2.Shares(""); len(l) != 0 {
t.Fatalf("lien expiré listé : %+v", l)
}
// Tout révoquer change la clé : un lien recréé avec un ancien id ne passe plus.
keep, _ := s2.CreateShare("e/a.excalidraw", 0)
if err := s2.RevokeAllShares(); err != nil {
t.Fatal(err)
}
if _, err := s2.ResolveShare(keep.Token); !errors.Is(err, ErrNotFound) {
t.Fatalf("après révocation globale : %v", err)
}
// Suppression du schéma : ses liens disparaissent.
del, _ := s2.CreateShare("e/a.excalidraw", 0)
s2.Delete("e", true)
if _, err := s2.ResolveShare(del.Token); !errors.Is(err, ErrNotFound) {
t.Fatalf("schéma supprimé : %v", err)
}
}
func TestShareFixedSecret(t *testing.T) {
s := newStore(t)
s.SetShareSecret("une-clé-imposée-assez-longue")
s.Write("a.excalidraw", []byte(doc), "", true)
sh, _ := s.CreateShare("a.excalidraw", 0)
if _, err := os.Stat(filepath.Join(s.root, secretFile)); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("clé écrite sur disque malgré SHARE_SECRET : %v", err)
}
s2, _ := New(s.root)
s2.SetShareSecret("une-clé-imposée-assez-longue")
if _, err := s2.ResolveShare(sh.Token); err != nil {
t.Fatal(err)
}
s3, _ := New(s.root)
s3.SetShareSecret("une-autre-clé-assez-longue")
if _, err := s3.ResolveShare(sh.Token); !errors.Is(err, ErrNotFound) {
t.Fatalf("autre clé acceptée : %v", err)
}
}