636 lines
23 KiB
Go
636 lines
23 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/kazier/backend/dbtest"
|
|
"github.com/kazier/backend/handlers"
|
|
"github.com/kazier/backend/pkg/auth"
|
|
"github.com/kazier/backend/pkg/passwd"
|
|
"github.com/kazier/backend/repository"
|
|
"github.com/kazier/backend/service"
|
|
)
|
|
|
|
const handlersTestURL = "postgres://kazier:kazier@localhost:5432/kazier_handlers_test?sslmode=disable"
|
|
|
|
type envelope struct {
|
|
Data json.RawMessage `json:"data"`
|
|
Meta *struct {
|
|
Page int `json:"page"`
|
|
PageSize int `json:"pageSize"`
|
|
Total int `json:"total"`
|
|
} `json:"meta,omitempty"`
|
|
}
|
|
|
|
type fileDTO struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Size int64 `json:"size"`
|
|
MimeType string `json:"mimeType"`
|
|
FolderID string `json:"folderId"`
|
|
CreatedAt string `json:"createdAt"`
|
|
}
|
|
|
|
type fileOcrDTO struct {
|
|
Text string `json:"text"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
}
|
|
|
|
type apiError struct {
|
|
Error struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"error"`
|
|
}
|
|
|
|
// wrapperError réutilise apiError pour casser l'imbrication.
|
|
func errorCode(rec *httptest.ResponseRecorder) string {
|
|
var e apiError
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &e); err != nil {
|
|
return "<unmarshal: " + err.Error() + ">"
|
|
}
|
|
return e.Error.Code
|
|
}
|
|
|
|
func setup(t *testing.T) (*gin.Engine, *service.Resources, *repository.Repository) {
|
|
t.Helper()
|
|
conn := dbtest.OpenTestDatabase(t, handlersTestURL)
|
|
repo := repository.NewRepository(conn)
|
|
// max 10 bytes pour couvrir FILE_TOO_LARGE en test
|
|
store := service.NewResources(repo, t.TempDir(), 10)
|
|
|
|
manager, err := auth.NewManager("test-secret")
|
|
if err != nil {
|
|
t.Fatalf("auth manager: %v", err)
|
|
}
|
|
handlers.Auth = manager
|
|
handlers.Store = store
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
handlers.RegisterRoutes(r)
|
|
return r, store, repo
|
|
}
|
|
|
|
func doRequest(t *testing.T, r *gin.Engine, method, path, token string, body []byte, contentType string) (*httptest.ResponseRecorder, envelope) {
|
|
t.Helper()
|
|
req := httptest.NewRequest(method, path, bytes.NewReader(body))
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
}
|
|
if contentType != "" {
|
|
req.Header.Set("Content-Type", contentType)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
r.ServeHTTP(rec, req)
|
|
return rec, envelope{}
|
|
}
|
|
|
|
func expectOK(t *testing.T, rec *httptest.ResponseRecorder, path string) envelope {
|
|
t.Helper()
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("%s: attendu 200, got %d body=%s", path, rec.Code, rec.Body.String())
|
|
}
|
|
var env envelope
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &env); err != nil {
|
|
t.Fatalf("%s: unmarshal: %v body=%s", path, err, rec.Body.String())
|
|
}
|
|
return env
|
|
}
|
|
|
|
func expectError(t *testing.T, rec *httptest.ResponseRecorder, status int, code, path string) {
|
|
t.Helper()
|
|
if rec.Code != status {
|
|
t.Fatalf("%s: attendu %d, got %d body=%s", path, status, rec.Code, rec.Body.String())
|
|
}
|
|
if got := errorCode(rec); got != code {
|
|
t.Errorf("%s: code erreur attendu %s, got %s (body=%s)", path, code, got, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func registerAndLogin(t *testing.T, r *gin.Engine, repo *repository.Repository, username, password, deviceID string) (token, userID string) {
|
|
t.Helper()
|
|
rec, _ := doRequest(t, r, http.MethodPost, "/api/v1/devices", "", []byte(fmt.Sprintf(`{"deviceId":%q}`, deviceID)), "application/json")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("register: status %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
username = service.NormalizeUsername(username)
|
|
if _, err := repo.Users.GetByUsernameNormalized(username); err == repository.ErrNotFound {
|
|
hash, herr := passwd.Hash(password)
|
|
if herr != nil {
|
|
t.Fatalf("hash: %v", herr)
|
|
}
|
|
if _, cerr := repo.Users.Create(username, username, hash, false); cerr != nil {
|
|
t.Fatalf("create user: %v", cerr)
|
|
}
|
|
} else if err != nil {
|
|
t.Fatalf("get user: %v", err)
|
|
}
|
|
|
|
body := fmt.Sprintf(`{"username":%q,"password":%q,"device_id":%q}`, username, password, deviceID)
|
|
rec, _ = doRequest(t, r, http.MethodPost, "/api/v1/auth/login", "", []byte(body), "application/json")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("login: status %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var env struct {
|
|
Data struct {
|
|
Token string `json:"token"`
|
|
User struct {
|
|
ID string `json:"id"`
|
|
} `json:"user"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &env); err != nil {
|
|
t.Fatalf("login: unmarshal: %v body=%s", err, rec.Body.String())
|
|
}
|
|
if env.Data.Token == "" || env.Data.User.ID == "" {
|
|
t.Fatalf("login: token/user manquant %s", rec.Body.String())
|
|
}
|
|
return env.Data.Token, env.Data.User.ID
|
|
}
|
|
|
|
// testUserUsername fournit un username unique par device (les bases de test
|
|
// sont reset, mais deux devices d'un même test ne doivent pas partager un
|
|
// compte).
|
|
func testUserUsername(deviceID, suffix string) string {
|
|
return "u" + suffix + "-" + deviceID[:8]
|
|
}
|
|
|
|
func uploadMultipart(t *testing.T, r *gin.Engine, token, folderID, filename string, content []byte) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
part, err := writer.CreateFormFile("file", filename)
|
|
if err != nil {
|
|
t.Fatalf("create form file: %v", err)
|
|
}
|
|
if _, err := part.Write(content); err != nil {
|
|
t.Fatalf("write body: %v", err)
|
|
}
|
|
if folderID != "" {
|
|
if err := writer.WriteField("folderId", folderID); err != nil {
|
|
t.Fatalf("write folderId: %v", err)
|
|
}
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("close writer: %v", err)
|
|
}
|
|
rec, _ := doRequest(t, r, http.MethodPost, "/api/v1/files/upload", token, body.Bytes(), writer.FormDataContentType())
|
|
return rec
|
|
}
|
|
|
|
// uploadMultipartToResource uploads bytes targeted at an existing resource_id
|
|
// (metadata-only file from the outbox), le cas d'usage OCR.
|
|
func uploadMultipartToResource(t *testing.T, r *gin.Engine, token, resourceID, filename string, content []byte) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
part, err := writer.CreateFormFile("file", filename)
|
|
if err != nil {
|
|
t.Fatalf("create form file: %v", err)
|
|
}
|
|
if _, err := part.Write(content); err != nil {
|
|
t.Fatalf("write body: %v", err)
|
|
}
|
|
if err := writer.WriteField("resourceId", resourceID); err != nil {
|
|
t.Fatalf("write resourceId: %v", err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("close writer: %v", err)
|
|
}
|
|
rec, _ := doRequest(t, r, http.MethodPost, "/api/v1/files/upload", token, body.Bytes(), writer.FormDataContentType())
|
|
return rec
|
|
}
|
|
|
|
func TestFilesFlow(t *testing.T) {
|
|
r, _, repo := setup(t)
|
|
deviceA := repository.NewID()
|
|
deviceB := repository.NewID()
|
|
passA := "files-test-password-a"
|
|
passB := "files-test-password-b"
|
|
tokenA, userA := registerAndLogin(t, r, repo, testUserUsername(deviceA, "a"), passA, deviceA)
|
|
tokenB, userB := registerAndLogin(t, r, repo, testUserUsername(deviceB, "b"), passB, deviceB)
|
|
|
|
folderID := repository.NewID()
|
|
if err := repo.Resources.InsertFolder(userA, folderID, "Docs", ""); err != nil {
|
|
t.Fatalf("insert root folder: %v", err)
|
|
}
|
|
|
|
// Upload root + dossier
|
|
rec := uploadMultipart(t, r, tokenA, "", "hello.txt", []byte("hello"))
|
|
env := expectOK(t, rec, "upload")
|
|
var uploaded fileDTO
|
|
if err := json.Unmarshal(env.Data, &uploaded); err != nil {
|
|
t.Fatalf("upload: unmarshal: %v", err)
|
|
}
|
|
if uploaded.Name != "hello.txt" || uploaded.Size != 5 || uploaded.FolderID != "" || uploaded.ID == "" {
|
|
t.Errorf("FileDto inattendu: %+v", uploaded)
|
|
}
|
|
|
|
rec = uploadMultipart(t, r, tokenA, folderID, "doc.txt", []byte("doc"))
|
|
env = expectOK(t, rec, "upload-doc")
|
|
var doc fileDTO
|
|
if err := json.Unmarshal(env.Data, &doc); err != nil {
|
|
t.Fatalf("upload-doc: unmarshal: %v", err)
|
|
}
|
|
if doc.FolderID != folderID {
|
|
t.Errorf("folderId attendu %s, got %s", folderID, doc.FolderID)
|
|
}
|
|
|
|
// Same name, same folder → NAME_CONFLICT
|
|
rec = uploadMultipart(t, r, tokenA, folderID, "doc.txt", []byte("doc"))
|
|
expectError(t, rec, http.StatusConflict, "NAME_CONFLICT", "upload-dupe")
|
|
|
|
// Création de fichiers du user B
|
|
if err := repo.Resources.InsertFile(userB, repository.NewID(), "secret.txt", "", 4, nil, nil); err != nil {
|
|
t.Fatalf("insert B file: %v", err)
|
|
}
|
|
|
|
// Liste racine (A)
|
|
rec, _ = doRequest(t, r, http.MethodGet, "/api/v1/files", tokenA, nil, "")
|
|
env = expectOK(t, rec, "list")
|
|
var files []fileDTO
|
|
if err := json.Unmarshal(env.Data, &files); err != nil {
|
|
t.Fatalf("list: unmarshal: %v", err)
|
|
}
|
|
if len(files) != 1 || files[0].ID != uploaded.ID {
|
|
t.Errorf("liste racine A: %+v", files)
|
|
}
|
|
if env.Meta == nil || env.Meta.Total != 1 {
|
|
t.Errorf("meta attendu total=1, got %+v", env.Meta)
|
|
}
|
|
|
|
// Liste dans le dossier (A)
|
|
rec, _ = doRequest(t, r, http.MethodGet, "/api/v1/files?folderId="+folderID, tokenA, nil, "")
|
|
env = expectOK(t, rec, "list-folder")
|
|
files = nil
|
|
if err := json.Unmarshal(env.Data, &files); err != nil {
|
|
t.Fatalf("list-folder: unmarshal: %v", err)
|
|
}
|
|
if len(files) != 1 || files[0].ID != doc.ID {
|
|
t.Errorf("liste dossier: %+v", files)
|
|
}
|
|
|
|
// B (autre user) ne voit pas les fichiers de A
|
|
rec, _ = doRequest(t, r, http.MethodGet, "/api/v1/files/"+uploaded.ID, tokenB, nil, "")
|
|
expectError(t, rec, http.StatusNotFound, "NOT_FOUND", "get-cross-device")
|
|
|
|
// Get + delete côté A
|
|
rec, _ = doRequest(t, r, http.MethodGet, "/api/v1/files/"+uploaded.ID, tokenA, nil, "")
|
|
env = expectOK(t, rec, "get")
|
|
var got fileDTO
|
|
if err := json.Unmarshal(env.Data, &got); err != nil {
|
|
t.Fatalf("get: unmarshal: %v", err)
|
|
}
|
|
if got.ID != uploaded.ID || got.MimeType != "application/octet-stream" || got.CreatedAt == "" {
|
|
t.Errorf("get FileDto inattendu: %+v", got)
|
|
}
|
|
|
|
rec, _ = doRequest(t, r, http.MethodDelete, "/api/v1/files/"+uploaded.ID, tokenA, nil, "")
|
|
env = expectOK(t, rec, "delete")
|
|
var deleted struct {
|
|
ID string `json:"id"`
|
|
}
|
|
if err := json.Unmarshal(env.Data, &deleted); err != nil {
|
|
t.Fatalf("delete: unmarshal: %v", err)
|
|
}
|
|
if deleted.ID != uploaded.ID {
|
|
t.Errorf("delete id: %+v", deleted)
|
|
}
|
|
|
|
rec, _ = doRequest(t, r, http.MethodGet, "/api/v1/files/"+uploaded.ID, tokenA, nil, "")
|
|
expectError(t, rec, http.StatusNotFound, "NOT_FOUND", "get-after-delete")
|
|
}
|
|
|
|
func TestSearchFiles(t *testing.T) {
|
|
r, _, repo := setup(t)
|
|
device := repository.NewID()
|
|
token, user := registerAndLogin(t, r, repo, testUserUsername(device, "s"), "search-test-password", device)
|
|
|
|
if err := repo.Resources.InsertFile(user, repository.NewID(), "vacances-août.jpg", "", 100, nil, nil); err != nil {
|
|
t.Fatalf("insert: %v", err)
|
|
}
|
|
if err := repo.Resources.InsertFile(user, repository.NewID(), "rapport-q3.pdf", "", 100, nil, nil); err != nil {
|
|
t.Fatalf("insert: %v", err)
|
|
}
|
|
if err := repo.Resources.InsertFile(user, repository.NewID(), "toto.txt", "", 100, nil, nil); err != nil {
|
|
t.Fatalf("insert: %v", err)
|
|
}
|
|
|
|
// q obligatoire
|
|
rec, _ := doRequest(t, r, http.MethodGet, "/api/v1/files/search", token, nil, "")
|
|
expectError(t, rec, http.StatusBadRequest, "INVALID_REQUEST", "search-no-q")
|
|
|
|
// insensible à la casse + sous-chaîne
|
|
rec, _ = doRequest(t, r, http.MethodGet, "/api/v1/files/search?q=RAPPORT", token, nil, "")
|
|
env := expectOK(t, rec, "search")
|
|
var files []fileDTO
|
|
if err := json.Unmarshal(env.Data, &files); err != nil {
|
|
t.Fatalf("search: unmarshal: %v", err)
|
|
}
|
|
if len(files) != 1 || files[0].Name != "rapport-q3.pdf" {
|
|
t.Errorf("search 'RAPPORT': %+v", files)
|
|
}
|
|
if env.Meta == nil || env.Meta.Total != 1 {
|
|
t.Errorf("meta search: %+v", env.Meta)
|
|
}
|
|
|
|
// wildcards neutralisés (trouve que "toto", pas tous les fichiers)
|
|
rec, _ = doRequest(t, r, http.MethodGet, "/api/v1/files/search?q=%25", token, nil, "")
|
|
env = expectOK(t, rec, "search-escaped")
|
|
files = nil
|
|
if err := json.Unmarshal(env.Data, &files); err != nil {
|
|
t.Fatalf("search-escaped: unmarshal: %v", err)
|
|
}
|
|
if len(files) != 0 {
|
|
t.Errorf("q=%% doit ne rien matcher, got %+v", files)
|
|
}
|
|
}
|
|
|
|
func TestSearchFilesEscapesPercent(t *testing.T) {
|
|
r, _, repo := setup(t)
|
|
device := repository.NewID()
|
|
token, user := registerAndLogin(t, r, repo, testUserUsername(device, "sp"), "search-test-password", device)
|
|
|
|
if err := repo.Resources.InsertFile(user, repository.NewID(), "half%price.txt", "", 1, nil, nil); err != nil {
|
|
t.Fatalf("insert percent file: %v", err)
|
|
}
|
|
if err := repo.Resources.InsertFile(user, repository.NewID(), "plain.txt", "", 1, nil, nil); err != nil {
|
|
t.Fatalf("insert plain file: %v", err)
|
|
}
|
|
|
|
// q=% (encodé %25) ne doit matcher QUE le nom contenant un '%' littéral.
|
|
rec, _ := doRequest(t, r, http.MethodGet, "/api/v1/files/search?q=%25", token, nil, "")
|
|
env := expectOK(t, rec, "search-percent")
|
|
var files []fileDTO
|
|
if err := json.Unmarshal(env.Data, &files); err != nil {
|
|
t.Fatalf("search-percent: unmarshal: %v", err)
|
|
}
|
|
if len(files) != 1 || files[0].Name != "half%price.txt" {
|
|
t.Errorf("'%%' littéral : attendu 1 résultat, got %+v", files)
|
|
}
|
|
}
|
|
|
|
func TestUploadTooLarge(t *testing.T) {
|
|
r, _, repo := setup(t)
|
|
device := repository.NewID()
|
|
token, _ := registerAndLogin(t, r, repo, testUserUsername(device, "l"), "upload-test-password", device)
|
|
|
|
rec := uploadMultipart(t, r, token, "", "big.txt", []byte("0123456789ABCDEF"))
|
|
expectError(t, rec, http.StatusRequestEntityTooLarge, "FILE_TOO_LARGE", "upload-big")
|
|
}
|
|
|
|
func TestUploadIntoUnknownFolder(t *testing.T) {
|
|
r, _, repo := setup(t)
|
|
device := repository.NewID()
|
|
token, _ := registerAndLogin(t, r, repo, testUserUsername(device, "uf"), "upload-test-password", device)
|
|
|
|
rec := uploadMultipart(t, r, token, repository.NewID(), "orphan.txt", []byte("hi"))
|
|
expectError(t, rec, http.StatusNotFound, "NOT_FOUND", "upload-unknown-folder")
|
|
}
|
|
|
|
func TestUploadNameConflict(t *testing.T) {
|
|
r, _, repo := setup(t)
|
|
device := repository.NewID()
|
|
token, _ := registerAndLogin(t, r, repo, testUserUsername(device, "uc"), "upload-test-password", device)
|
|
|
|
rec := uploadMultipart(t, r, token, "", "dupe.txt", []byte("hi"))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("premier upload: status %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// Même nom à la racine → conflit d'unicité (parent_id NULL)
|
|
rec = uploadMultipart(t, r, token, "", "dupe.txt", []byte("hi"))
|
|
expectError(t, rec, http.StatusConflict, "NAME_CONFLICT", "upload-dupe")
|
|
}
|
|
|
|
func TestUploadTargetsExistingResource(t *testing.T) {
|
|
r, _, repo := setup(t)
|
|
device := repository.NewID()
|
|
token, user := registerAndLogin(t, r, repo, testUserUsername(device, "tr"), "upload-test-password", device)
|
|
|
|
// Fiche metadata-only (même sémantique que l'outbox create_resource).
|
|
resourceID := repository.NewID()
|
|
if err := repo.Resources.InsertFile(user, resourceID, "scan.png", "", 0, nil, nil); err != nil {
|
|
t.Fatalf("insert metadata-only file: %v", err)
|
|
}
|
|
|
|
rec := uploadMultipartToResource(t, r, token, resourceID, "scan.png", []byte("PNGDATA"))
|
|
env := expectOK(t, rec, "upload-target")
|
|
var uploaded fileDTO
|
|
if err := json.Unmarshal(env.Data, &uploaded); err != nil {
|
|
t.Fatalf("upload-target: unmarshal: %v", err)
|
|
}
|
|
if uploaded.ID != resourceID {
|
|
t.Errorf("id attendu %s, got %s", resourceID, uploaded.ID)
|
|
}
|
|
if uploaded.Size != 7 {
|
|
t.Errorf("taille = %d, attendu 7", uploaded.Size)
|
|
}
|
|
|
|
// Toujours une seule ligne (pas de doublon).
|
|
rec, _ = doRequest(t, r, http.MethodGet, "/api/v1/files", token, nil, "")
|
|
env = expectOK(t, rec, "list-after-targeted-upload")
|
|
var files []fileDTO
|
|
if err := json.Unmarshal(env.Data, &files); err != nil {
|
|
t.Fatalf("list: unmarshal: %v", err)
|
|
}
|
|
if len(files) != 1 || files[0].ID != resourceID {
|
|
t.Errorf("aucun doublon attendu, got %+v", files)
|
|
}
|
|
}
|
|
|
|
func TestUploadTargetForeignResourceNotFound(t *testing.T) {
|
|
r, _, repo := setup(t)
|
|
deviceA := repository.NewID()
|
|
deviceB := repository.NewID()
|
|
tokenA, _ := registerAndLogin(t, r, repo, testUserUsername(deviceA, "tf"), "upload-test-password", deviceA)
|
|
_, userB := registerAndLogin(t, r, repo, testUserUsername(deviceB, "tg"), "upload-test-password-b", deviceB)
|
|
|
|
resourceID := repository.NewID()
|
|
if err := repo.Resources.InsertFile(userB, resourceID, "mine.png", "", 0, nil, nil); err != nil {
|
|
t.Fatalf("insert B file: %v", err)
|
|
}
|
|
|
|
rec := uploadMultipartToResource(t, r, tokenA, resourceID, "mine.png", []byte("DATA"))
|
|
expectError(t, rec, http.StatusNotFound, "NOT_FOUND", "upload-foreign")
|
|
}
|
|
|
|
func TestUploadTargetInvalidID(t *testing.T) {
|
|
r, _, repo := setup(t)
|
|
device := repository.NewID()
|
|
token, _ := registerAndLogin(t, r, repo, testUserUsername(device, "ti"), "upload-test-password", device)
|
|
|
|
rec := uploadMultipartToResource(t, r, token, "NOT-HEX", "x.png", []byte("DATA"))
|
|
expectError(t, rec, http.StatusNotFound, "NOT_FOUND", "upload-invalid-id")
|
|
}
|
|
|
|
func TestFoldersListAndScoping(t *testing.T) {
|
|
r, _, repo := setup(t)
|
|
deviceA := repository.NewID()
|
|
deviceB := repository.NewID()
|
|
tokenA, userA := registerAndLogin(t, r, repo, testUserUsername(deviceA, "fa"), "folder-test-password", deviceA)
|
|
_, userB := registerAndLogin(t, r, repo, testUserUsername(deviceB, "fb"), "folder-test-password-b", deviceB)
|
|
|
|
if err := repo.Resources.InsertFolder(userA, repository.NewID(), "AA", ""); err != nil {
|
|
t.Fatalf("insert folder: %v", err)
|
|
}
|
|
if err := repo.Resources.InsertFolder(userA, repository.NewID(), "BB", ""); err != nil {
|
|
t.Fatalf("insert folder: %v", err)
|
|
}
|
|
if err := repo.Resources.InsertFolder(userB, repository.NewID(), "CC", ""); err != nil {
|
|
t.Fatalf("insert folder B: %v", err)
|
|
}
|
|
|
|
rec, _ := doRequest(t, r, http.MethodGet, "/api/v1/files/folders", tokenA, nil, "")
|
|
env := expectOK(t, rec, "folders")
|
|
var folders []struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
if err := json.Unmarshal(env.Data, &folders); err != nil {
|
|
t.Fatalf("folders: unmarshal: %v", err)
|
|
}
|
|
if len(folders) != 2 || folders[0].Name != "AA" || folders[1].Name != "BB" {
|
|
t.Errorf("folders A: %+v", folders)
|
|
}
|
|
}
|
|
|
|
func TestFilesPaginationDefaultsAndClamp(t *testing.T) {
|
|
r, _, repo := setup(t)
|
|
device := repository.NewID()
|
|
token, _ := registerAndLogin(t, r, repo, testUserUsername(device, "pg"), "pagination-test-password", device)
|
|
|
|
// pageSize au-delà de 200 → clampé à 200
|
|
rec, _ := doRequest(t, r, http.MethodGet, "/api/v1/files?pageSize=9999", token, nil, "")
|
|
env := expectOK(t, rec, "pageSize-clamp")
|
|
if env.Meta == nil || env.Meta.Page != 1 || env.Meta.PageSize != 200 {
|
|
t.Errorf("clamp pageSize: %+v", env.Meta)
|
|
}
|
|
|
|
// Paramètres invalides → défauts (page=1, pageSize=50)
|
|
rec, _ = doRequest(t, r, http.MethodGet, "/api/v1/files?page=0&pageSize=-5", token, nil, "")
|
|
env = expectOK(t, rec, "invalid-params")
|
|
if env.Meta == nil || env.Meta.Page != 1 || env.Meta.PageSize != 50 {
|
|
t.Errorf("defauts page/pageSize: %+v", env.Meta)
|
|
}
|
|
|
|
// sort/order inconnus → pas d'erreur (défaut created_at desc)
|
|
rec, _ = doRequest(t, r, http.MethodGet, "/api/v1/files?sort=zzz&order=up&pageSize=10", token, nil, "")
|
|
expectOK(t, rec, "invalid-sort")
|
|
}
|
|
|
|
func TestFilesListSortBySize(t *testing.T) {
|
|
r, _, repo := setup(t)
|
|
device := repository.NewID()
|
|
token, user := registerAndLogin(t, r, repo, testUserUsername(device, "sr"), "sort-test-password", device)
|
|
|
|
// Tri volontairement désordonné : 30, 10, 20.
|
|
if err := repo.Resources.InsertFile(user, repository.NewID(), "a.txt", "", 30, nil, nil); err != nil {
|
|
t.Fatalf("insert a: %v", err)
|
|
}
|
|
if err := repo.Resources.InsertFile(user, repository.NewID(), "b.txt", "", 10, nil, nil); err != nil {
|
|
t.Fatalf("insert b: %v", err)
|
|
}
|
|
if err := repo.Resources.InsertFile(user, repository.NewID(), "c.txt", "", 20, nil, nil); err != nil {
|
|
t.Fatalf("insert c: %v", err)
|
|
}
|
|
|
|
rec, _ := doRequest(t, r, http.MethodGet, "/api/v1/files?sort=size&order=asc", token, nil, "")
|
|
env := expectOK(t, rec, "sort-size")
|
|
var files []fileDTO
|
|
if err := json.Unmarshal(env.Data, &files); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if len(files) != 3 || files[0].Size != 10 || files[1].Size != 20 || files[2].Size != 30 {
|
|
t.Errorf("ordre size asc inattendu: %+v", files)
|
|
}
|
|
}
|
|
|
|
func TestFilesGetDeleteRejectInvalidID(t *testing.T) {
|
|
r, _, repo := setup(t)
|
|
device := repository.NewID()
|
|
token, user := registerAndLogin(t, r, repo, testUserUsername(device, "ii"), "invalid-id-test-password", device)
|
|
|
|
// ID non 32-hex → 404 avant toute requête.
|
|
rec, _ := doRequest(t, r, http.MethodGet, "/api/v1/files/NOT-HEX-ID", token, nil, "")
|
|
expectError(t, rec, http.StatusNotFound, "NOT_FOUND", "get-invalid-id")
|
|
|
|
rec, _ = doRequest(t, r, http.MethodDelete, "/api/v1/files/xyz", token, nil, "")
|
|
expectError(t, rec, http.StatusNotFound, "NOT_FOUND", "delete-invalid-id")
|
|
|
|
// ID 32-hex mais inconnu (du même user) → 404.
|
|
rec, _ = doRequest(t, r, http.MethodGet, "/api/v1/files/"+repository.NewID(), token, nil, "")
|
|
expectError(t, rec, http.StatusNotFound, "NOT_FOUND", "get-unknown")
|
|
|
|
// Le user ne voit jamais les fichiers d'un autre même avec un ID valide.
|
|
fileID := repository.NewID()
|
|
if err := repo.Resources.InsertFile(user, fileID, "mine.txt", "", 4, nil, nil); err != nil {
|
|
t.Fatalf("insert: %v", err)
|
|
}
|
|
otherDevice := repository.NewID()
|
|
otherToken, _ := registerAndLogin(t, r, repo, testUserUsername(otherDevice, "ii2"), "invalid-id-test-password-b", otherDevice)
|
|
rec, _ = doRequest(t, r, http.MethodGet, "/api/v1/files/"+fileID, otherToken, nil, "")
|
|
expectError(t, rec, http.StatusNotFound, "NOT_FOUND", "get-cross-user")
|
|
}
|
|
|
|
func TestFilesGetOcrOwnerShareAndScoping(t *testing.T) {
|
|
r, _, repo := setup(t)
|
|
ownerDevice := repository.NewID()
|
|
ownerToken, owner := registerAndLogin(t, r, repo, testUserUsername(ownerDevice, "oc"), "ocr-get-test-password", ownerDevice)
|
|
granteeDevice := repository.NewID()
|
|
granteeToken, grantee := registerAndLogin(t, r, repo, testUserUsername(granteeDevice, "og"), "ocr-get-test-password-b", granteeDevice)
|
|
outsiderDevice := repository.NewID()
|
|
outsiderToken, _ := registerAndLogin(t, r, repo, testUserUsername(outsiderDevice, "oh"), "ocr-get-test-password-c", outsiderDevice)
|
|
|
|
fileID := repository.NewID()
|
|
if err := repo.Resources.InsertFile(owner, fileID, "scan.png", "", 128, nil, nil); err != nil {
|
|
t.Fatalf("insert file: %v", err)
|
|
}
|
|
if err := repo.Resources.UpdateOcrText(owner, fileID, "extrait du scan"); err != nil {
|
|
t.Fatalf("write ocr_text: %v", err)
|
|
}
|
|
|
|
// Owner : texte + updatedAt.
|
|
rec, _ := doRequest(t, r, http.MethodGet, "/api/v1/files/"+fileID+"/ocr", ownerToken, nil, "")
|
|
envOcr := expectOK(t, rec, "ocr-owner")
|
|
var ownerOcr fileOcrDTO
|
|
if err := json.Unmarshal(envOcr.Data, &ownerOcr); err != nil {
|
|
t.Fatalf("ocr-owner: unmarshal: %v", err)
|
|
}
|
|
if ownerOcr.Text != "extrait du scan" || ownerOcr.UpdatedAt == "" {
|
|
t.Errorf("ocr owner = %+v, attendu texte+updatedAt", ownerOcr)
|
|
}
|
|
|
|
// Grantée viewer+ : même texte (partage rend l'OCR lisible).
|
|
if err := repo.Shares.Upsert(fileID, grantee, "viewer", false, nil, owner); err != nil {
|
|
t.Fatalf("share: %v", err)
|
|
}
|
|
rec, _ = doRequest(t, r, http.MethodGet, "/api/v1/files/"+fileID+"/ocr", granteeToken, nil, "")
|
|
envOcr = expectOK(t, rec, "ocr-grantee")
|
|
var granteeOcr fileOcrDTO
|
|
if err := json.Unmarshal(envOcr.Data, &granteeOcr); err != nil {
|
|
t.Fatalf("ocr-grantee: unmarshal: %v", err)
|
|
}
|
|
if granteeOcr.Text != "extrait du scan" {
|
|
t.Errorf("ocr grantee = %+v, attendu texte partagé", granteeOcr)
|
|
}
|
|
|
|
// Outsider : invisible (404).
|
|
rec, _ = doRequest(t, r, http.MethodGet, "/api/v1/files/"+fileID+"/ocr", outsiderToken, nil, "")
|
|
expectError(t, rec, http.StatusNotFound, "NOT_FOUND", "ocr-outsider")
|
|
|
|
// ID invalide → 404.
|
|
rec, _ = doRequest(t, r, http.MethodGet, "/api/v1/files/not-hex/ocr", ownerToken, nil, "")
|
|
expectError(t, rec, http.StatusNotFound, "NOT_FOUND", "ocr-invalid-id")
|
|
}
|