mirror of
https://github.com/btouchard/ackify.git
synced 2026-02-12 00:39:15 -06:00
Backend: - Add Search() method to DocumentRepository for ILIKE pattern matching on doc_id, title, url, description - Add Count() method to accurately count total matching documents (used for pagination) - Update admin handler to support search query parameter with proper pagination - Implement full public documents API with search support (previously stub) - Update test mocks to include new repository methods Frontend: - Replace client-side filtering with server-side search in admin dashboard - Add debounced search input (300ms delay) to reduce API calls - Separate loading states: initial page load vs. search/pagination (prevents input focus loss) - Add visual feedback: spinning loader icon in search field during active search - Enable pagination during search (previously disabled) - Pass search parameter to API service
886 lines
24 KiB
Go
886 lines
24 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
package documents
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/btouchard/ackify-ce/backend/internal/application/services"
|
|
"github.com/btouchard/ackify-ce/backend/internal/domain/models"
|
|
"github.com/btouchard/ackify-ce/backend/internal/presentation/api/shared"
|
|
)
|
|
|
|
// ============================================================================
|
|
// TEST FIXTURES & MOCKS
|
|
// ============================================================================
|
|
|
|
var (
|
|
testDoc = &models.Document{
|
|
DocID: "test-doc-123",
|
|
Title: "Test Document",
|
|
URL: "https://example.com/doc.pdf",
|
|
Description: "Test description",
|
|
Checksum: "abc123",
|
|
ChecksumAlgorithm: "SHA-256",
|
|
CreatedAt: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
|
|
UpdatedAt: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
|
|
CreatedBy: "user@example.com",
|
|
}
|
|
|
|
testSignature = &models.Signature{
|
|
ID: 1,
|
|
DocID: "test-doc-123",
|
|
UserSub: "oauth2|123",
|
|
UserEmail: "user@example.com",
|
|
UserName: "Test User",
|
|
SignedAtUTC: time.Date(2024, 1, 1, 12, 30, 0, 0, time.UTC),
|
|
PayloadHash: "payload-hash-123",
|
|
Signature: "signature-123",
|
|
Nonce: "nonce-123",
|
|
CreatedAt: time.Date(2024, 1, 1, 12, 30, 0, 0, time.UTC),
|
|
PrevHash: stringPtr("prev-hash-123"),
|
|
Referer: stringPtr("https://example.com"),
|
|
}
|
|
|
|
testUser = &models.User{
|
|
Sub: "oauth2|123",
|
|
Email: "user@example.com",
|
|
Name: "Test User",
|
|
}
|
|
)
|
|
|
|
func stringPtr(s string) *string {
|
|
return &s
|
|
}
|
|
|
|
// Mock document service
|
|
type mockDocumentService struct {
|
|
createDocFunc func(ctx context.Context, req services.CreateDocumentRequest) (*models.Document, error)
|
|
findOrCreateDocFunc func(ctx context.Context, ref string) (*models.Document, bool, error)
|
|
findByReferenceFunc func(ctx context.Context, ref string, refType string) (*models.Document, error)
|
|
}
|
|
|
|
func (m *mockDocumentService) CreateDocument(ctx context.Context, req services.CreateDocumentRequest) (*models.Document, error) {
|
|
if m.createDocFunc != nil {
|
|
return m.createDocFunc(ctx, req)
|
|
}
|
|
return testDoc, nil
|
|
}
|
|
|
|
func (m *mockDocumentService) FindOrCreateDocument(ctx context.Context, ref string) (*models.Document, bool, error) {
|
|
if m.findOrCreateDocFunc != nil {
|
|
return m.findOrCreateDocFunc(ctx, ref)
|
|
}
|
|
return testDoc, true, nil
|
|
}
|
|
|
|
func (m *mockDocumentService) FindByReference(ctx context.Context, ref string, refType string) (*models.Document, error) {
|
|
if m.findByReferenceFunc != nil {
|
|
return m.findByReferenceFunc(ctx, ref, refType)
|
|
}
|
|
return nil, fmt.Errorf("document not found")
|
|
}
|
|
|
|
// Mock signature service
|
|
type mockSignatureService struct {
|
|
getDocumentSignaturesFunc func(ctx context.Context, docID string) ([]*models.Signature, error)
|
|
}
|
|
|
|
func (m *mockSignatureService) GetDocumentSignatures(ctx context.Context, docID string) ([]*models.Signature, error) {
|
|
if m.getDocumentSignaturesFunc != nil {
|
|
return m.getDocumentSignaturesFunc(ctx, docID)
|
|
}
|
|
return []*models.Signature{testSignature}, nil
|
|
}
|
|
|
|
func createTestHandler() *Handler {
|
|
return &Handler{
|
|
signatureService: &services.SignatureService{}, // Not used in these tests
|
|
documentService: &mockDocumentService{},
|
|
}
|
|
}
|
|
|
|
func addUserToContext(ctx context.Context, user *models.User) context.Context {
|
|
return context.WithValue(ctx, shared.ContextKeyUser, user)
|
|
}
|
|
|
|
// ============================================================================
|
|
// TESTS - Constructor
|
|
// ============================================================================
|
|
|
|
func TestNewHandler(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sigService := &services.SignatureService{}
|
|
docService := &mockDocumentService{}
|
|
|
|
handler := NewHandler(sigService, docService, nil, nil, nil, nil, false)
|
|
|
|
assert.NotNil(t, handler)
|
|
assert.Equal(t, sigService, handler.signatureService)
|
|
assert.Equal(t, docService, handler.documentService)
|
|
}
|
|
|
|
// ============================================================================
|
|
// TESTS - HandleCreateDocument
|
|
// ============================================================================
|
|
|
|
func TestHandler_HandleCreateDocument_Success(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
reference string
|
|
title string
|
|
}{
|
|
{
|
|
name: "with title",
|
|
reference: "https://example.com/doc.pdf",
|
|
title: "My Document",
|
|
},
|
|
{
|
|
name: "without title",
|
|
reference: "https://example.com/doc.pdf",
|
|
title: "",
|
|
},
|
|
{
|
|
name: "with file path reference",
|
|
reference: "/path/to/document.pdf",
|
|
title: "Local Document",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockDocService := &mockDocumentService{
|
|
createDocFunc: func(ctx context.Context, req services.CreateDocumentRequest) (*models.Document, error) {
|
|
assert.Equal(t, tt.reference, req.Reference)
|
|
assert.Equal(t, tt.title, req.Title)
|
|
return testDoc, nil
|
|
},
|
|
}
|
|
|
|
handler := &Handler{
|
|
documentService: mockDocService,
|
|
}
|
|
|
|
reqBody := CreateDocumentRequest{
|
|
Reference: tt.reference,
|
|
Title: tt.title,
|
|
}
|
|
body, err := json.Marshal(reqBody)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/documents", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleCreateDocument(rec, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, rec.Code)
|
|
assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
|
|
|
|
var wrapper struct {
|
|
Data CreateDocumentResponse `json:"data"`
|
|
}
|
|
err = json.Unmarshal(rec.Body.Bytes(), &wrapper)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, testDoc.DocID, wrapper.Data.DocID)
|
|
assert.Equal(t, testDoc.Title, wrapper.Data.Title)
|
|
assert.Equal(t, testDoc.URL, wrapper.Data.URL)
|
|
assert.NotEmpty(t, wrapper.Data.CreatedAt)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandler_HandleCreateDocument_ValidationErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
requestBody interface{}
|
|
expectedStatus int
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "empty reference",
|
|
requestBody: CreateDocumentRequest{Reference: "", Title: "Title"},
|
|
expectedStatus: http.StatusBadRequest,
|
|
expectedError: "Reference is required",
|
|
},
|
|
{
|
|
name: "invalid JSON",
|
|
requestBody: "invalid json",
|
|
expectedStatus: http.StatusBadRequest,
|
|
expectedError: "Invalid request body",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := createTestHandler()
|
|
|
|
var body []byte
|
|
var err error
|
|
if str, ok := tt.requestBody.(string); ok {
|
|
body = []byte(str)
|
|
} else {
|
|
body, err = json.Marshal(tt.requestBody)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/documents", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleCreateDocument(rec, req)
|
|
|
|
assert.Equal(t, tt.expectedStatus, rec.Code)
|
|
|
|
var response map[string]interface{}
|
|
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Contains(t, response, "error")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandler_HandleCreateDocument_ServiceError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockDocService := &mockDocumentService{
|
|
createDocFunc: func(ctx context.Context, req services.CreateDocumentRequest) (*models.Document, error) {
|
|
return nil, fmt.Errorf("database error")
|
|
},
|
|
}
|
|
|
|
handler := &Handler{
|
|
documentService: mockDocService,
|
|
}
|
|
|
|
reqBody := CreateDocumentRequest{
|
|
Reference: "https://example.com/doc.pdf",
|
|
Title: "Test",
|
|
}
|
|
body, err := json.Marshal(reqBody)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/documents", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleCreateDocument(rec, req)
|
|
|
|
assert.Equal(t, http.StatusInternalServerError, rec.Code)
|
|
|
|
var response map[string]interface{}
|
|
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Contains(t, response, "error")
|
|
}
|
|
|
|
// ============================================================================
|
|
// TESTS - HandleListDocuments
|
|
// ============================================================================
|
|
|
|
func TestHandler_HandleListDocuments_Success(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
queryParams string
|
|
expectedPage int
|
|
expectedLimit int
|
|
}{
|
|
{
|
|
name: "default pagination",
|
|
queryParams: "",
|
|
expectedPage: 1,
|
|
expectedLimit: 20,
|
|
},
|
|
{
|
|
name: "custom page and limit",
|
|
queryParams: "?page=2&limit=50",
|
|
expectedPage: 2,
|
|
expectedLimit: 50,
|
|
},
|
|
{
|
|
name: "limit max capped at 100",
|
|
queryParams: "?limit=200",
|
|
expectedPage: 1,
|
|
expectedLimit: 20, // Will use default since > 100
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := createTestHandler()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/documents"+tt.queryParams, nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleListDocuments(rec, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rec.Code)
|
|
assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
|
|
|
|
var wrapper struct {
|
|
Data interface{} `json:"data"`
|
|
Meta struct {
|
|
Page int `json:"page"`
|
|
Limit int `json:"limit"`
|
|
Total int `json:"total"`
|
|
} `json:"meta"`
|
|
}
|
|
err := json.Unmarshal(rec.Body.Bytes(), &wrapper)
|
|
require.NoError(t, err)
|
|
|
|
// Currently returns empty list
|
|
assert.NotNil(t, wrapper.Data)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// TESTS - HandleGetDocument
|
|
// ============================================================================
|
|
|
|
func TestHandler_HandleGetDocument_MissingDocID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := createTestHandler()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/", nil)
|
|
|
|
// Empty docId parameter
|
|
rctx := chi.NewRouteContext()
|
|
rctx.URLParams.Add("docId", "")
|
|
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetDocument(rec, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
|
}
|
|
|
|
// ============================================================================
|
|
// TESTS - HandleGetDocumentSignatures
|
|
// ============================================================================
|
|
|
|
func TestHandler_HandleGetDocumentSignatures_MissingDocID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := createTestHandler()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/documents//signatures", nil)
|
|
|
|
rctx := chi.NewRouteContext()
|
|
rctx.URLParams.Add("docId", "")
|
|
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetDocumentSignatures(rec, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
|
}
|
|
|
|
// ============================================================================
|
|
// TESTS - HandleFindOrCreateDocument
|
|
// ============================================================================
|
|
|
|
func TestHandler_HandleFindOrCreateDocument_FindExisting(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockDocService := &mockDocumentService{
|
|
findByReferenceFunc: func(ctx context.Context, ref string, refType string) (*models.Document, error) {
|
|
assert.Equal(t, "https://example.com/doc.pdf", ref)
|
|
assert.Equal(t, "url", refType)
|
|
return testDoc, nil
|
|
},
|
|
}
|
|
|
|
handler := &Handler{
|
|
documentService: mockDocService,
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/find-or-create?ref=https://example.com/doc.pdf", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleFindOrCreateDocument(rec, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
var wrapper struct {
|
|
Data FindOrCreateDocumentResponse `json:"data"`
|
|
}
|
|
err := json.Unmarshal(rec.Body.Bytes(), &wrapper)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, testDoc.DocID, wrapper.Data.DocID)
|
|
assert.False(t, wrapper.Data.IsNew, "Should not be new since document was found")
|
|
}
|
|
|
|
func TestHandler_HandleFindOrCreateDocument_CreateNew(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockDocService := &mockDocumentService{
|
|
findByReferenceFunc: func(ctx context.Context, ref string, refType string) (*models.Document, error) {
|
|
// Document not found - return nil, nil (not an error)
|
|
return nil, nil
|
|
},
|
|
findOrCreateDocFunc: func(ctx context.Context, ref string) (*models.Document, bool, error) {
|
|
assert.Equal(t, "https://example.com/new-doc.pdf", ref)
|
|
return testDoc, true, nil
|
|
},
|
|
}
|
|
|
|
handler := &Handler{
|
|
documentService: mockDocService,
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/find-or-create?ref=https://example.com/new-doc.pdf", nil)
|
|
|
|
// Add authenticated user to context
|
|
ctx := addUserToContext(req.Context(), testUser)
|
|
req = req.WithContext(ctx)
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleFindOrCreateDocument(rec, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
var wrapper struct {
|
|
Data FindOrCreateDocumentResponse `json:"data"`
|
|
}
|
|
err := json.Unmarshal(rec.Body.Bytes(), &wrapper)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, testDoc.DocID, wrapper.Data.DocID)
|
|
assert.True(t, wrapper.Data.IsNew, "Should be new since document was created")
|
|
}
|
|
|
|
func TestHandler_HandleFindOrCreateDocument_UnauthenticatedCreate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockDocService := &mockDocumentService{
|
|
findByReferenceFunc: func(ctx context.Context, ref string, refType string) (*models.Document, error) {
|
|
// Document not found - return nil, nil (not an error)
|
|
return nil, nil
|
|
},
|
|
}
|
|
|
|
handler := &Handler{
|
|
documentService: mockDocService,
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/find-or-create?ref=https://example.com/new-doc.pdf", nil)
|
|
// No user in context
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleFindOrCreateDocument(rec, req)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, rec.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(rec.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Contains(t, response, "error")
|
|
}
|
|
|
|
func TestHandler_HandleFindOrCreateDocument_MissingRef(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := createTestHandler()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/find-or-create", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleFindOrCreateDocument(rec, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(rec.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Contains(t, response, "error")
|
|
}
|
|
|
|
// ============================================================================
|
|
// TESTS - detectReferenceType
|
|
// ============================================================================
|
|
|
|
func Test_detectReferenceType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
ref string
|
|
expected ReferenceType
|
|
}{
|
|
{
|
|
name: "HTTP URL",
|
|
ref: "http://example.com/doc.pdf",
|
|
expected: "url",
|
|
},
|
|
{
|
|
name: "HTTPS URL",
|
|
ref: "https://example.com/doc.pdf",
|
|
expected: "url",
|
|
},
|
|
{
|
|
name: "Unix file path",
|
|
ref: "/path/to/document.pdf",
|
|
expected: "path",
|
|
},
|
|
{
|
|
name: "Windows file path",
|
|
ref: "C:\\path\\to\\document.pdf",
|
|
expected: "path",
|
|
},
|
|
{
|
|
name: "Simple reference",
|
|
ref: "doc-12345",
|
|
expected: "reference",
|
|
},
|
|
{
|
|
name: "Hash reference",
|
|
ref: "abc123def456",
|
|
expected: "reference",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
result := detectReferenceType(tt.ref)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// TESTS - signatureToDTO
|
|
// ============================================================================
|
|
|
|
func Test_signatureToDTO(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
sig *models.Signature
|
|
checkDTO func(t *testing.T, dto SignatureDTO)
|
|
}{
|
|
{
|
|
name: "with prevHash",
|
|
sig: testSignature,
|
|
checkDTO: func(t *testing.T, dto SignatureDTO) {
|
|
assert.Equal(t, "1", dto.ID)
|
|
assert.Equal(t, testSignature.DocID, dto.DocID)
|
|
assert.Equal(t, testSignature.UserEmail, dto.UserEmail)
|
|
assert.Equal(t, testSignature.UserName, dto.UserName)
|
|
assert.Equal(t, testSignature.Signature, dto.Signature)
|
|
assert.Equal(t, testSignature.PayloadHash, dto.PayloadHash)
|
|
assert.Equal(t, testSignature.Nonce, dto.Nonce)
|
|
assert.Equal(t, *testSignature.PrevHash, dto.PrevHash)
|
|
assert.NotEmpty(t, dto.SignedAt)
|
|
},
|
|
},
|
|
{
|
|
name: "without prevHash",
|
|
sig: &models.Signature{
|
|
ID: 2,
|
|
DocID: "doc-456",
|
|
UserSub: "oauth2|456",
|
|
UserEmail: "user2@example.com",
|
|
UserName: "User 2",
|
|
SignedAtUTC: time.Date(2024, 1, 2, 10, 0, 0, 0, time.UTC),
|
|
PayloadHash: "hash-456",
|
|
Signature: "sig-456",
|
|
Nonce: "nonce-456",
|
|
PrevHash: nil,
|
|
},
|
|
checkDTO: func(t *testing.T, dto SignatureDTO) {
|
|
assert.Equal(t, "2", dto.ID)
|
|
assert.Empty(t, dto.PrevHash)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dto := signatureToDTO(tt.sig)
|
|
tt.checkDTO(t, dto)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// TESTS - Concurrency
|
|
// ============================================================================
|
|
|
|
func TestHandler_HandleCreateDocument_Concurrent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := createTestHandler()
|
|
|
|
const numRequests = 50
|
|
done := make(chan bool, numRequests)
|
|
errors := make(chan error, numRequests)
|
|
|
|
for i := 0; i < numRequests; i++ {
|
|
go func(id int) {
|
|
defer func() { done <- true }()
|
|
|
|
reqBody := CreateDocumentRequest{
|
|
Reference: fmt.Sprintf("https://example.com/doc-%d.pdf", id),
|
|
Title: fmt.Sprintf("Document %d", id),
|
|
}
|
|
body, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
errors <- err
|
|
return
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/documents", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleCreateDocument(rec, req)
|
|
|
|
if rec.Code != http.StatusCreated {
|
|
errors <- fmt.Errorf("unexpected status: %d", rec.Code)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
for i := 0; i < numRequests; i++ {
|
|
<-done
|
|
}
|
|
close(errors)
|
|
|
|
var errCount int
|
|
for err := range errors {
|
|
t.Logf("Concurrent request error: %v", err)
|
|
errCount++
|
|
}
|
|
|
|
assert.Equal(t, 0, errCount, "All concurrent requests should succeed")
|
|
}
|
|
|
|
// ============================================================================
|
|
// ADMIN-ONLY DOCUMENT CREATION TESTS
|
|
// ============================================================================
|
|
|
|
func TestHandler_HandleCreateDocument_AdminOnlyEnabled_AdminUser(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := &Handler{
|
|
signatureService: &services.SignatureService{},
|
|
documentService: &mockDocumentService{},
|
|
adminEmails: []string{"admin@example.com"},
|
|
onlyAdminCanCreate: true,
|
|
}
|
|
|
|
reqBody := CreateDocumentRequest{
|
|
Reference: "https://example.com/doc.pdf",
|
|
Title: "Admin Document",
|
|
}
|
|
body, err := json.Marshal(reqBody)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/documents", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Add admin user to context
|
|
adminUser := &models.User{
|
|
Sub: "oauth2|admin",
|
|
Email: "admin@example.com",
|
|
Name: "Admin User",
|
|
}
|
|
ctx := context.WithValue(req.Context(), shared.ContextKeyUser, adminUser)
|
|
req = req.WithContext(ctx)
|
|
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleCreateDocument(rec, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, rec.Code)
|
|
assert.Contains(t, rec.Body.String(), "test-doc-123")
|
|
}
|
|
|
|
func TestHandler_HandleCreateDocument_AdminOnlyEnabled_NonAdminUser(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := &Handler{
|
|
signatureService: &services.SignatureService{},
|
|
documentService: &mockDocumentService{},
|
|
adminEmails: []string{"admin@example.com"},
|
|
onlyAdminCanCreate: true,
|
|
}
|
|
|
|
reqBody := CreateDocumentRequest{
|
|
Reference: "https://example.com/doc.pdf",
|
|
Title: "User Document",
|
|
}
|
|
body, err := json.Marshal(reqBody)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/documents", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Add non-admin user to context
|
|
regularUser := &models.User{
|
|
Sub: "oauth2|user",
|
|
Email: "user@example.com",
|
|
Name: "Regular User",
|
|
}
|
|
ctx := context.WithValue(req.Context(), shared.ContextKeyUser, regularUser)
|
|
req = req.WithContext(ctx)
|
|
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleCreateDocument(rec, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, rec.Code)
|
|
assert.Contains(t, rec.Body.String(), "Only administrators can create documents")
|
|
}
|
|
|
|
func TestHandler_HandleCreateDocument_AdminOnlyEnabled_Unauthenticated(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := &Handler{
|
|
signatureService: &services.SignatureService{},
|
|
documentService: &mockDocumentService{},
|
|
adminEmails: []string{"admin@example.com"},
|
|
onlyAdminCanCreate: true,
|
|
}
|
|
|
|
reqBody := CreateDocumentRequest{
|
|
Reference: "https://example.com/doc.pdf",
|
|
Title: "Unauthenticated Document",
|
|
}
|
|
body, err := json.Marshal(reqBody)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/documents", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
// No user in context (unauthenticated)
|
|
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleCreateDocument(rec, req)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, rec.Code)
|
|
assert.Contains(t, rec.Body.String(), "Authentication required")
|
|
}
|
|
|
|
func TestHandler_HandleCreateDocument_AdminOnlyDisabled_AnyUser(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := &Handler{
|
|
signatureService: &services.SignatureService{},
|
|
documentService: &mockDocumentService{},
|
|
adminEmails: []string{"admin@example.com"},
|
|
onlyAdminCanCreate: false, // Disabled
|
|
}
|
|
|
|
reqBody := CreateDocumentRequest{
|
|
Reference: "https://example.com/doc.pdf",
|
|
Title: "Public Document",
|
|
}
|
|
body, err := json.Marshal(reqBody)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/documents", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
// No authentication needed when admin-only is disabled
|
|
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleCreateDocument(rec, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, rec.Code)
|
|
assert.Contains(t, rec.Body.String(), "test-doc-123")
|
|
}
|
|
|
|
// ============================================================================
|
|
// BENCHMARKS
|
|
// ============================================================================
|
|
|
|
func BenchmarkHandler_HandleCreateDocument(b *testing.B) {
|
|
handler := createTestHandler()
|
|
|
|
reqBody := CreateDocumentRequest{
|
|
Reference: "https://example.com/doc.pdf",
|
|
Title: "Test Document",
|
|
}
|
|
body, _ := json.Marshal(reqBody)
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/documents", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleCreateDocument(rec, req)
|
|
}
|
|
}
|
|
|
|
func BenchmarkHandler_HandleCreateDocument_Parallel(b *testing.B) {
|
|
handler := createTestHandler()
|
|
|
|
reqBody := CreateDocumentRequest{
|
|
Reference: "https://example.com/doc.pdf",
|
|
Title: "Test Document",
|
|
}
|
|
body, _ := json.Marshal(reqBody)
|
|
|
|
b.RunParallel(func(pb *testing.PB) {
|
|
for pb.Next() {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/documents", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleCreateDocument(rec, req)
|
|
}
|
|
})
|
|
}
|
|
|
|
func Benchmark_detectReferenceType(b *testing.B) {
|
|
refs := []string{
|
|
"https://example.com/doc.pdf",
|
|
"/path/to/file.pdf",
|
|
"simple-reference",
|
|
}
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
detectReferenceType(refs[i%len(refs)])
|
|
}
|
|
}
|