package handler import ( "net/http/httptest" "strings" "testing" "github.com/DATA-DOG/go-sqlmock" "github.com/gin-gonic/gin" ) func TestPhaseOneAndLegacyRoutesAreRegistered(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() NewHandler().RegisterRoutes(router) registered := make(map[string]bool) for _, route := range router.Routes() { registered[route.Method+" "+route.Path] = true } want := []string{ "GET /api/auth/organizations", "POST /api/auth/organization-context", "GET /api/material-categories", "POST /api/material-categories", "PUT /api/material-categories/:id", "GET /api/units", "POST /api/units", "PUT /api/units/:id", "GET /api/code-rules", "POST /api/code-rules", "PUT /api/code-rules/:id", "GET /api/code-rules/next", "GET /api/organizations", "GET /api/roles", "PUT /api/roles/:id", "GET /api/permissions", "GET /api/partners", "POST /api/partners", "PUT /api/partners/:id", "GET /api/warehouses", "POST /api/warehouses/:id/locations", "POST /api/materials/:id/obsolete", "POST /api/materials/:id/restore", "PUT /api/stock/:materialId/warning", "GET /api/stock/:materialId/details", "POST /api/applications", "GET /api/logs", "GET /api/material-import/template", "POST /api/material-import", } for _, route := range want { if !registered[route] { t.Errorf("route not registered: %s", route) } } if registered["DELETE /api/materials/:id"] { t.Error("hard-delete material route must not be exposed") } } func TestOrganizationBusinessRoutesRequireHeaderBeforeHandler(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() NewHandler().RegisterRoutes(router) for _, route := range router.Routes() { path := route.Path if strings.HasPrefix(path, "/api/auth/") || strings.HasPrefix(path, "/api/users") || strings.HasPrefix(path, "/api/organizations") || strings.HasPrefix(path, "/api/roles") || strings.HasPrefix(path, "/api/permissions") || path == "/api/health" || strings.HasPrefix(path, "/api/upload") { continue } t.Run(route.Method+" "+path, func(t *testing.T) { mock := contextMock(t) expectContextPrincipal(mock, "warehouse_admin") for _, param := range []string{":id", ":materialId", ":locationId"} { path = strings.ReplaceAll(path, param, "1") } req := httptest.NewRequest(route.Method, path, nil) req.Header.Set("Authorization", "Bearer "+contextToken(t)) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != 400 || !strings.Contains(w.Body.String(), "X-Organization-ID") { t.Fatalf("status=%d body=%s", w.Code, w.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }) } } func TestOrganizationMasterDataReadAndWritePermissions(t *testing.T) { for _, tc := range []struct{ path, query string }{ {"/api/material-categories", "SELECT id, organization_id, code, name"}, {"/api/units", "SELECT id, organization_id, code, name"}, {"/api/code-rules", "SELECT cr.id"}, } { for _, method := range []string{"GET", "POST", "PUT"} { t.Run(method+tc.path, func(t *testing.T) { mock := contextMock(t) expectContextPrincipal(mock, "warehouse_admin") mock.ExpectQuery("SELECT EXISTS").WithArgs(int64(2), int64(7), int64(7)).WillReturnRows(sqlmock.NewRows([]string{"allowed"}).AddRow(true)) want := 403 if method == "GET" { want = 200 mock.ExpectQuery(tc.query).WithArgs(int64(2)).WillReturnRows(sqlmock.NewRows([]string{"id"})) } path := tc.path if method == "PUT" { path += "/1" } router := gin.New() NewHandler().RegisterRoutes(router) req := httptest.NewRequest(method, path, nil) req.Header.Set("Authorization", "Bearer "+contextToken(t)) req.Header.Set("X-Organization-ID", "2") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != want { t.Fatalf("status=%d body=%s", w.Code, w.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }) } } }