6265133ac9
Anade tests unitarios e integracion sobre SQLite in-memory: - CRUDDefineResource: casos felices y rechazo de inputs invalidos (nombre vacio, tipos no soportados, nombres reservados, duplicados). - CRUDGenerateTableSQL: columnas base, NOT NULL/UNIQUE/DEFAULT, deleted_at con soft_delete y verificacion de que el DDL es ejecutable en sqlite. - Create + Get: creacion feliz, validaciones required/min_length/max_length/ enum/min/max, 409 en UNIQUE, GET 200/404. - List: paginacion, filtros, orden ascendente, campos desconocidos ignorados. - Update: partial update, 404 y validacion de campos enviados. - Delete: hard delete, soft delete, 404, ocultar soft-deleted en list. - Integracion end-to-end con httptest.NewServer cubriendo CRUD completo y multiples recursos registrados en el mismo mux.
654 lines
20 KiB
Go
654 lines
20 KiB
Go
package infra
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
// --- helpers ---
|
|
|
|
func openCRUDTestDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
db, err := sql.Open("sqlite3", ":memory:")
|
|
if err != nil {
|
|
t.Fatalf("cannot open test DB: %v", err)
|
|
}
|
|
t.Cleanup(func() { db.Close() })
|
|
return db
|
|
}
|
|
|
|
func sampleProjectFields() []CRUDField {
|
|
return []CRUDField{
|
|
{Name: "name", Type: "TEXT", Required: true, Unique: true,
|
|
Validations: map[string]string{"min_length": "1", "max_length": "50"}},
|
|
{Name: "description", Type: "TEXT"},
|
|
{Name: "status", Type: "TEXT", Default: "'active'",
|
|
Validations: map[string]string{"enum": "active,archived"}},
|
|
{Name: "priority", Type: "INTEGER", Default: "0",
|
|
Validations: map[string]string{"min": "0", "max": "10"}},
|
|
}
|
|
}
|
|
|
|
func buildProjectResource(t *testing.T, softDelete bool) (CRUDResource, *sql.DB) {
|
|
t.Helper()
|
|
res, err := CRUDDefineResource("project", "projects", sampleProjectFields(), softDelete)
|
|
if err != nil {
|
|
t.Fatalf("define resource: %v", err)
|
|
}
|
|
db := openCRUDTestDB(t)
|
|
if _, err := db.Exec(CRUDGenerateTableSQL(res)); err != nil {
|
|
t.Fatalf("create table: %v", err)
|
|
}
|
|
return res, db
|
|
}
|
|
|
|
func doJSONRequest(t *testing.T, mux http.Handler, method, path string, body any) (*httptest.ResponseRecorder, map[string]any) {
|
|
t.Helper()
|
|
var reqBody io.Reader
|
|
if body != nil {
|
|
b, err := json.Marshal(body)
|
|
if err != nil {
|
|
t.Fatalf("marshal body: %v", err)
|
|
}
|
|
reqBody = bytes.NewReader(b)
|
|
}
|
|
req := httptest.NewRequest(method, path, reqBody)
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
var got map[string]any
|
|
if rec.Body.Len() > 0 && strings.HasPrefix(rec.Header().Get("Content-Type"), "application/json") {
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &got)
|
|
}
|
|
return rec, got
|
|
}
|
|
|
|
// --- CRUDDefineResource ---
|
|
|
|
func TestCRUDDefineResource(t *testing.T) {
|
|
t.Run("construye un recurso valido", func(t *testing.T) {
|
|
res, err := CRUDDefineResource("project", "projects", sampleProjectFields(), false)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if res.Name != "project" || res.Table != "projects" || len(res.Fields) != 4 {
|
|
t.Errorf("unexpected resource: %+v", res)
|
|
}
|
|
})
|
|
|
|
t.Run("rechaza nombre vacio", func(t *testing.T) {
|
|
_, err := CRUDDefineResource("", "projects", sampleProjectFields(), false)
|
|
if err == nil {
|
|
t.Error("expected error for empty name")
|
|
}
|
|
})
|
|
|
|
t.Run("rechaza tabla vacia", func(t *testing.T) {
|
|
_, err := CRUDDefineResource("project", "", sampleProjectFields(), false)
|
|
if err == nil {
|
|
t.Error("expected error for empty table")
|
|
}
|
|
})
|
|
|
|
t.Run("rechaza lista de campos vacia", func(t *testing.T) {
|
|
_, err := CRUDDefineResource("project", "projects", nil, false)
|
|
if err == nil {
|
|
t.Error("expected error for empty fields")
|
|
}
|
|
})
|
|
|
|
t.Run("rechaza tipos invalidos", func(t *testing.T) {
|
|
_, err := CRUDDefineResource("project", "projects", []CRUDField{{Name: "x", Type: "FOO"}}, false)
|
|
if err == nil {
|
|
t.Error("expected error for invalid type")
|
|
}
|
|
})
|
|
|
|
t.Run("rechaza nombres reservados", func(t *testing.T) {
|
|
for _, reserved := range []string{"id", "created_at", "updated_at", "deleted_at"} {
|
|
_, err := CRUDDefineResource("project", "projects", []CRUDField{{Name: reserved, Type: "TEXT"}}, false)
|
|
if err == nil {
|
|
t.Errorf("expected error for reserved field %q", reserved)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("rechaza duplicados", func(t *testing.T) {
|
|
_, err := CRUDDefineResource("project", "projects", []CRUDField{
|
|
{Name: "name", Type: "TEXT"},
|
|
{Name: "name", Type: "TEXT"},
|
|
}, false)
|
|
if err == nil {
|
|
t.Error("expected error for duplicate field")
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- CRUDGenerateTableSQL ---
|
|
|
|
func TestCRUDGenerateTableSQL(t *testing.T) {
|
|
t.Run("genera tabla basica con timestamps", func(t *testing.T) {
|
|
res, _ := CRUDDefineResource("project", "projects",
|
|
[]CRUDField{{Name: "name", Type: "TEXT"}}, false)
|
|
ddl := CRUDGenerateTableSQL(res)
|
|
for _, want := range []string{
|
|
"CREATE TABLE IF NOT EXISTS projects",
|
|
"id TEXT PRIMARY KEY",
|
|
"name TEXT",
|
|
"created_at TEXT NOT NULL",
|
|
"updated_at TEXT NOT NULL",
|
|
} {
|
|
if !strings.Contains(ddl, want) {
|
|
t.Errorf("DDL missing %q:\n%s", want, ddl)
|
|
}
|
|
}
|
|
if strings.Contains(ddl, "deleted_at") {
|
|
t.Errorf("DDL should not contain deleted_at:\n%s", ddl)
|
|
}
|
|
})
|
|
|
|
t.Run("aplica NOT NULL y UNIQUE", func(t *testing.T) {
|
|
res, _ := CRUDDefineResource("project", "projects",
|
|
[]CRUDField{{Name: "name", Type: "TEXT", Required: true, Unique: true}}, false)
|
|
ddl := CRUDGenerateTableSQL(res)
|
|
if !strings.Contains(ddl, "name TEXT NOT NULL UNIQUE") {
|
|
t.Errorf("expected NOT NULL UNIQUE:\n%s", ddl)
|
|
}
|
|
})
|
|
|
|
t.Run("aplica DEFAULT", func(t *testing.T) {
|
|
res, _ := CRUDDefineResource("project", "projects",
|
|
[]CRUDField{{Name: "priority", Type: "INTEGER", Default: "0"}}, false)
|
|
ddl := CRUDGenerateTableSQL(res)
|
|
if !strings.Contains(ddl, "priority INTEGER DEFAULT 0") {
|
|
t.Errorf("expected DEFAULT clause:\n%s", ddl)
|
|
}
|
|
})
|
|
|
|
t.Run("anade deleted_at si soft_delete", func(t *testing.T) {
|
|
res, _ := CRUDDefineResource("project", "projects",
|
|
[]CRUDField{{Name: "name", Type: "TEXT"}}, true)
|
|
ddl := CRUDGenerateTableSQL(res)
|
|
if !strings.Contains(ddl, "deleted_at TEXT") {
|
|
t.Errorf("expected deleted_at for soft_delete:\n%s", ddl)
|
|
}
|
|
})
|
|
|
|
t.Run("el DDL generado es valido en sqlite", func(t *testing.T) {
|
|
res, _ := CRUDDefineResource("project", "projects", sampleProjectFields(), true)
|
|
db := openCRUDTestDB(t)
|
|
if _, err := db.Exec(CRUDGenerateTableSQL(res)); err != nil {
|
|
t.Fatalf("DDL not valid: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- CRUDCreateHandler + CRUDGetHandler ---
|
|
|
|
func TestCRUDCreateAndGet(t *testing.T) {
|
|
res, db := buildProjectResource(t, false)
|
|
mux := http.NewServeMux()
|
|
CRUDRegisterRoutes(mux, "/api/projects", res, db)
|
|
|
|
t.Run("crea un registro valido y retorna 201", func(t *testing.T) {
|
|
rec, body := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{
|
|
"name": "demo", "description": "hola", "status": "active", "priority": 5,
|
|
})
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("got status %d, want 201: body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if body["name"] != "demo" {
|
|
t.Errorf("got name=%v, want demo", body["name"])
|
|
}
|
|
if body["id"] == nil || body["id"] == "" {
|
|
t.Errorf("expected generated id, got %v", body["id"])
|
|
}
|
|
if body["created_at"] == nil {
|
|
t.Errorf("expected created_at, got nil")
|
|
}
|
|
})
|
|
|
|
t.Run("retorna 400 si faltan required", func(t *testing.T) {
|
|
rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{})
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("got %d, want 400", rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("valida min_length", func(t *testing.T) {
|
|
rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": ""})
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("got %d, want 400 for empty name", rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("valida max_length", func(t *testing.T) {
|
|
longName := strings.Repeat("a", 60)
|
|
rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": longName})
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("got %d, want 400 for too-long name", rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("valida enum", func(t *testing.T) {
|
|
rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{
|
|
"name": "x", "status": "pirate",
|
|
})
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("got %d, want 400 for invalid enum", rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("valida min y max numericos", func(t *testing.T) {
|
|
rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{
|
|
"name": "numeric-min", "priority": -1,
|
|
})
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("got %d, want 400 for priority<0", rec.Code)
|
|
}
|
|
rec, _ = doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{
|
|
"name": "numeric-max", "priority": 999,
|
|
})
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("got %d, want 400 for priority>10", rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("retorna 409 si se viola UNIQUE", func(t *testing.T) {
|
|
_, _ = doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "unique-1"})
|
|
rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "unique-1"})
|
|
if rec.Code != http.StatusConflict {
|
|
t.Errorf("got %d, want 409", rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("GET recupera el registro creado", func(t *testing.T) {
|
|
rec, created := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "get-me"})
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("create failed: %d", rec.Code)
|
|
}
|
|
id := created["id"].(string)
|
|
rec, body := doJSONRequest(t, mux, "GET", "/api/projects/"+id, nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("got %d, want 200", rec.Code)
|
|
}
|
|
if body["name"] != "get-me" {
|
|
t.Errorf("got name=%v, want get-me", body["name"])
|
|
}
|
|
})
|
|
|
|
t.Run("GET no existente retorna 404", func(t *testing.T) {
|
|
rec, _ := doJSONRequest(t, mux, "GET", "/api/projects/nonexistent", nil)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("got %d, want 404", rec.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- CRUDListHandler ---
|
|
|
|
func TestCRUDListHandler(t *testing.T) {
|
|
res, db := buildProjectResource(t, false)
|
|
mux := http.NewServeMux()
|
|
CRUDRegisterRoutes(mux, "/api/projects", res, db)
|
|
|
|
// Sembrar datos
|
|
for i, name := range []string{"a", "b", "c", "d", "e"} {
|
|
status := "active"
|
|
if i%2 == 1 {
|
|
status = "archived"
|
|
}
|
|
rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{
|
|
"name": name, "status": status, "priority": i,
|
|
})
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("seed %s failed: %d", name, rec.Code)
|
|
}
|
|
}
|
|
|
|
t.Run("lista todo con paginacion default", func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, httptest.NewRequest("GET", "/api/projects", nil))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("got %d, want 200", rec.Code)
|
|
}
|
|
var got CRUDListResult
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("parse body: %v", err)
|
|
}
|
|
if got.Total != 5 {
|
|
t.Errorf("got total=%d, want 5", got.Total)
|
|
}
|
|
if len(got.Items) != 5 {
|
|
t.Errorf("got %d items, want 5", len(got.Items))
|
|
}
|
|
})
|
|
|
|
t.Run("respeta page y per_page", func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, httptest.NewRequest("GET", "/api/projects?page=1&per_page=2", nil))
|
|
var got CRUDListResult
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &got)
|
|
if len(got.Items) != 2 {
|
|
t.Errorf("got %d items, want 2", len(got.Items))
|
|
}
|
|
if got.TotalPages != 3 {
|
|
t.Errorf("got total_pages=%d, want 3", got.TotalPages)
|
|
}
|
|
})
|
|
|
|
t.Run("filtra por campo con filter_<field>", func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, httptest.NewRequest("GET", "/api/projects?filter_status=archived", nil))
|
|
var got CRUDListResult
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &got)
|
|
if got.Total != 2 {
|
|
t.Errorf("got total=%d, want 2 archived", got.Total)
|
|
}
|
|
})
|
|
|
|
t.Run("ordena con sort_by y sort_dir", func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, httptest.NewRequest("GET", "/api/projects?sort_by=name&sort_dir=asc", nil))
|
|
var got CRUDListResult
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &got)
|
|
if len(got.Items) < 2 {
|
|
t.Fatalf("not enough items: %d", len(got.Items))
|
|
}
|
|
if got.Items[0]["name"] != "a" || got.Items[1]["name"] != "b" {
|
|
t.Errorf("sort asc failed: first=%v second=%v", got.Items[0]["name"], got.Items[1]["name"])
|
|
}
|
|
})
|
|
|
|
t.Run("ignora filtros con campos desconocidos", func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, httptest.NewRequest("GET", "/api/projects?filter_unknown=xxx", nil))
|
|
var got CRUDListResult
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &got)
|
|
if got.Total != 5 {
|
|
t.Errorf("got total=%d, want 5 (filter should be ignored)", got.Total)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- CRUDUpdateHandler ---
|
|
|
|
func TestCRUDUpdateHandler(t *testing.T) {
|
|
res, db := buildProjectResource(t, false)
|
|
mux := http.NewServeMux()
|
|
CRUDRegisterRoutes(mux, "/api/projects", res, db)
|
|
|
|
createRec, created := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{
|
|
"name": "original", "description": "first", "priority": 2,
|
|
})
|
|
if createRec.Code != http.StatusCreated {
|
|
t.Fatalf("create failed: %d", createRec.Code)
|
|
}
|
|
id := created["id"].(string)
|
|
originalUpdatedAt := fmt.Sprintf("%v", created["updated_at"])
|
|
|
|
t.Run("actualiza solo los campos enviados", func(t *testing.T) {
|
|
rec, body := doJSONRequest(t, mux, "PUT", "/api/projects/"+id, map[string]any{
|
|
"description": "updated",
|
|
})
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("got %d, want 200: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if body["description"] != "updated" {
|
|
t.Errorf("description not updated: %v", body["description"])
|
|
}
|
|
if body["name"] != "original" {
|
|
t.Errorf("name should not change: %v", body["name"])
|
|
}
|
|
if fmt.Sprintf("%v", body["updated_at"]) == originalUpdatedAt {
|
|
t.Errorf("updated_at should change")
|
|
}
|
|
})
|
|
|
|
t.Run("retorna 404 si no existe", func(t *testing.T) {
|
|
rec, _ := doJSONRequest(t, mux, "PUT", "/api/projects/nonexistent", map[string]any{"description": "x"})
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("got %d, want 404", rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("valida los campos enviados", func(t *testing.T) {
|
|
rec, _ := doJSONRequest(t, mux, "PUT", "/api/projects/"+id, map[string]any{"priority": 999})
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("got %d, want 400", rec.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- CRUDDeleteHandler ---
|
|
|
|
func TestCRUDDeleteHandler(t *testing.T) {
|
|
t.Run("hard delete borra fisicamente", func(t *testing.T) {
|
|
res, db := buildProjectResource(t, false)
|
|
mux := http.NewServeMux()
|
|
CRUDRegisterRoutes(mux, "/api/projects", res, db)
|
|
|
|
_, created := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "hard"})
|
|
id := created["id"].(string)
|
|
|
|
rec, _ := doJSONRequest(t, mux, "DELETE", "/api/projects/"+id, nil)
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Errorf("got %d, want 204", rec.Code)
|
|
}
|
|
if rec.Body.Len() != 0 {
|
|
t.Errorf("expected empty body, got %s", rec.Body.String())
|
|
}
|
|
|
|
// Verificar que ya no existe
|
|
rec, _ = doJSONRequest(t, mux, "GET", "/api/projects/"+id, nil)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("after delete, GET got %d, want 404", rec.Code)
|
|
}
|
|
|
|
// Verificar que la fila ya no esta en la tabla
|
|
var count int
|
|
if err := db.QueryRow("SELECT COUNT(*) FROM projects WHERE id = ?", id).Scan(&count); err != nil {
|
|
t.Fatalf("count: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Errorf("hard delete should remove row, got count=%d", count)
|
|
}
|
|
})
|
|
|
|
t.Run("soft delete actualiza deleted_at", func(t *testing.T) {
|
|
res, db := buildProjectResource(t, true)
|
|
mux := http.NewServeMux()
|
|
CRUDRegisterRoutes(mux, "/api/projects", res, db)
|
|
|
|
_, created := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "soft"})
|
|
id := created["id"].(string)
|
|
|
|
rec, _ := doJSONRequest(t, mux, "DELETE", "/api/projects/"+id, nil)
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Errorf("got %d, want 204", rec.Code)
|
|
}
|
|
|
|
// GET debe dar 404 (la fila esta oculta por soft delete)
|
|
rec, _ = doJSONRequest(t, mux, "GET", "/api/projects/"+id, nil)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("after soft delete, GET got %d, want 404", rec.Code)
|
|
}
|
|
|
|
// Pero la fila sigue fisica en la tabla con deleted_at no nulo
|
|
var deletedAt sql.NullString
|
|
if err := db.QueryRow("SELECT deleted_at FROM projects WHERE id = ?", id).Scan(&deletedAt); err != nil {
|
|
t.Fatalf("select: %v", err)
|
|
}
|
|
if !deletedAt.Valid || deletedAt.String == "" {
|
|
t.Errorf("expected deleted_at set, got %+v", deletedAt)
|
|
}
|
|
})
|
|
|
|
t.Run("retorna 404 si no existe", func(t *testing.T) {
|
|
res, db := buildProjectResource(t, false)
|
|
mux := http.NewServeMux()
|
|
CRUDRegisterRoutes(mux, "/api/projects", res, db)
|
|
rec, _ := doJSONRequest(t, mux, "DELETE", "/api/projects/nonexistent", nil)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("got %d, want 404", rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("soft delete no lista registros ocultos", func(t *testing.T) {
|
|
res, db := buildProjectResource(t, true)
|
|
mux := http.NewServeMux()
|
|
CRUDRegisterRoutes(mux, "/api/projects", res, db)
|
|
|
|
_, _ = doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "keep"})
|
|
_, del := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "remove"})
|
|
delID := del["id"].(string)
|
|
_, _ = doJSONRequest(t, mux, "DELETE", "/api/projects/"+delID, nil)
|
|
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, httptest.NewRequest("GET", "/api/projects", nil))
|
|
var got CRUDListResult
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &got)
|
|
if got.Total != 1 {
|
|
t.Errorf("got total=%d, want 1 (soft deleted should be hidden)", got.Total)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- CRUDGenerateHandlers + CRUDRegisterRoutes integration ---
|
|
|
|
func TestCRUDGenerateHandlers(t *testing.T) {
|
|
t.Run("retorna las 5 keys esperadas", func(t *testing.T) {
|
|
res, db := buildProjectResource(t, false)
|
|
handlers := CRUDGenerateHandlers(res, db)
|
|
for _, key := range []string{"list", "get", "create", "update", "delete"} {
|
|
if handlers[key] == nil {
|
|
t.Errorf("handler %q is nil", key)
|
|
}
|
|
}
|
|
if len(handlers) != 5 {
|
|
t.Errorf("expected 5 handlers, got %d", len(handlers))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCRUDRegisterRoutesIntegration(t *testing.T) {
|
|
t.Run("CRUD completo end-to-end via servidor HTTP", func(t *testing.T) {
|
|
res, db := buildProjectResource(t, false)
|
|
mux := http.NewServeMux()
|
|
CRUDRegisterRoutes(mux, "/api/projects", res, db)
|
|
srv := httptest.NewServer(mux)
|
|
t.Cleanup(srv.Close)
|
|
|
|
// Create
|
|
createBody, _ := json.Marshal(map[string]any{"name": "e2e", "description": "integration", "priority": 3})
|
|
resp, err := http.Post(srv.URL+"/api/projects", "application/json", bytes.NewReader(createBody))
|
|
if err != nil {
|
|
t.Fatalf("POST: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("create: got %d, want 201", resp.StatusCode)
|
|
}
|
|
var created map[string]any
|
|
_ = json.NewDecoder(resp.Body).Decode(&created)
|
|
id := created["id"].(string)
|
|
|
|
// Get
|
|
resp, err = http.Get(srv.URL + "/api/projects/" + id)
|
|
if err != nil {
|
|
t.Fatalf("GET: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("get: got %d, want 200", resp.StatusCode)
|
|
}
|
|
|
|
// Update
|
|
updateBody, _ := json.Marshal(map[string]any{"description": "modified"})
|
|
req, _ := http.NewRequest("PUT", srv.URL+"/api/projects/"+id, bytes.NewReader(updateBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err = http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("PUT: %v", err)
|
|
}
|
|
var updated map[string]any
|
|
_ = json.NewDecoder(resp.Body).Decode(&updated)
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("update: got %d, want 200", resp.StatusCode)
|
|
}
|
|
if updated["description"] != "modified" {
|
|
t.Errorf("update did not persist: %v", updated["description"])
|
|
}
|
|
|
|
// List
|
|
resp, err = http.Get(srv.URL + "/api/projects")
|
|
if err != nil {
|
|
t.Fatalf("LIST: %v", err)
|
|
}
|
|
var list CRUDListResult
|
|
_ = json.NewDecoder(resp.Body).Decode(&list)
|
|
resp.Body.Close()
|
|
if list.Total != 1 {
|
|
t.Errorf("list total: got %d, want 1", list.Total)
|
|
}
|
|
|
|
// Delete
|
|
req, _ = http.NewRequest("DELETE", srv.URL+"/api/projects/"+id, nil)
|
|
resp, err = http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("DELETE: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNoContent {
|
|
t.Errorf("delete: got %d, want 204", resp.StatusCode)
|
|
}
|
|
|
|
// Get after delete: 404
|
|
resp, _ = http.Get(srv.URL + "/api/projects/" + id)
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Errorf("after delete: got %d, want 404", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("multiples recursos en el mismo mux", func(t *testing.T) {
|
|
projectRes, db := buildProjectResource(t, false)
|
|
// Segundo recurso sobre la misma DB
|
|
userFields := []CRUDField{
|
|
{Name: "email", Type: "TEXT", Required: true, Unique: true},
|
|
}
|
|
userRes, err := CRUDDefineResource("user", "users", userFields, false)
|
|
if err != nil {
|
|
t.Fatalf("define user: %v", err)
|
|
}
|
|
if _, err := db.Exec(CRUDGenerateTableSQL(userRes)); err != nil {
|
|
t.Fatalf("create users table: %v", err)
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
CRUDRegisterRoutes(mux, "/api/projects", projectRes, db)
|
|
CRUDRegisterRoutes(mux, "/api/users", userRes, db)
|
|
|
|
rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "p1"})
|
|
if rec.Code != http.StatusCreated {
|
|
t.Errorf("projects: got %d", rec.Code)
|
|
}
|
|
rec, _ = doJSONRequest(t, mux, "POST", "/api/users", map[string]any{"email": "a@b.c"})
|
|
if rec.Code != http.StatusCreated {
|
|
t.Errorf("users: got %d", rec.Code)
|
|
}
|
|
})
|
|
}
|