diff --git a/.github/workflows/api-unit-test.yml b/.github/workflows/api-unit-test.yml index ac8e48458..816636d10 100644 --- a/.github/workflows/api-unit-test.yml +++ b/.github/workflows/api-unit-test.yml @@ -26,6 +26,9 @@ jobs: with: go-version: 1.21 + - name: Install gotestsum + run: go install gotest.tools/gotestsum@latest + - name: Create MongoDB Container run: cd ../deploy/docker && make test_api @@ -33,4 +36,19 @@ jobs: run: make - name: Test - run: go test -p 1 ./... + run: gotestsum --junitfile tests-api.xml -- -coverpkg=./... -coverprofile=coverage_api.out -p 1 ./... + + - name: Log Test Coverage + run: go tool cover -func coverage_api.out | grep total + + - name: Test Report + uses: dorny/test-reporter@v1 + if: always() # run this step even if previous steps failed + with: + name: API Tests Report # Name of the check run which will be created + path: ./API/tests-api.xml # Path to test results + reporter: java-junit # Format of test results + - uses: actions/upload-artifact@v3 + with: + name: coverage + path: ./API/coverage_api.out diff --git a/.github/workflows/cli-unit-test.yml b/.github/workflows/cli-unit-test.yml index 0001db293..badc3fb07 100644 --- a/.github/workflows/cli-unit-test.yml +++ b/.github/workflows/cli-unit-test.yml @@ -23,9 +23,27 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + + - name: Install gotestsum + run: go install gotest.tools/gotestsum@latest - name: Build run: make - name: Test - run: go test ./... + run: gotestsum --junitfile tests-cli.xml -- -coverprofile=coverage_cli.out -p 1 `go list ./... | grep -v ./readline | grep -v ./mocks` + + - name: Log Test Coverage + run: go tool cover -func coverage_cli.out | grep total + + - name: Test Report + uses: dorny/test-reporter@v1 + if: always() # run this step even if previous steps failed + with: + name: CLI Tests Report # Name of the check run which will be created + path: ./CLI/tests-cli.xml # Path to test results + reporter: java-junit # Format of test results + - uses: actions/upload-artifact@v3 + with: + name: coverage_cli + path: ./CLI/coverage_cli.out diff --git a/.gitignore b/.gitignore index 2637a9224..6ec36fe69 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ deploy/mdb/* *.DS_Store API/.env gin-bin +API/coverage # CLI Ignore .idea/ @@ -22,6 +23,7 @@ cli.mac *.ocli log.txt unitylog.txt +CLI/coverage # APP Ignore deploy/docker/*.env diff --git a/API/controllers/authControllers.go b/API/controllers/auth.go similarity index 100% rename from API/controllers/authControllers.go rename to API/controllers/auth.go diff --git a/API/controllers/auth_test.go b/API/controllers/auth_test.go new file mode 100644 index 000000000..83fca3e6d --- /dev/null +++ b/API/controllers/auth_test.go @@ -0,0 +1,492 @@ +package controllers_test + +import ( + "encoding/json" + "net/http" + "p3/models" + "p3/test/e2e" + u "p3/utils" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func getUserToken(email string, password string) string { + acc, e := models.Login(email, password) + if e != nil { + return "" + } + return acc.Token +} + +func TestCreateUserInvalidBody(t *testing.T) { + e2e.TestInvalidBody(t, "POST", "/api/users", "Invalid request: wrong format body") +} + +// Tests domain bulk creation (/api/users/bulk) +func TestCreateBulkUsersInvalidBody(t *testing.T) { + requestBody := []byte(`[ + { + "name": "invalid json body"", + }, + ]`) + + recorder := e2e.MakeRequest("POST", "/api/users/bulk", requestBody) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Invalid request", message) +} + +func TestCreateBulkUsers(t *testing.T) { + // Test create two separate users + requestBody := []byte(`[ + { + "name": "User With No Passsword", + "roles": { + "*": "manager" + }, + "email": "user_no_password@test.com" + }, + { + "name": "User With Passsword", + "password": "fake_password", + "roles": { + "*": "user" + }, + "email": "user_with_password@test.com" + } + ]`) + + recorder := e2e.MakeRequest("POST", "/api/users/bulk", requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + userWithoutPassword, exists := response["user_no_password@test.com"].(map[string]interface{}) + assert.True(t, exists) + status, exists := userWithoutPassword["status"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully created", status) + // A random password should be created and passed in the response + password, exists := userWithoutPassword["password"].(string) + assert.True(t, exists) + assert.True(t, len(password) > 0) + + userWithPassword, exists := response["user_with_password@test.com"].(map[string]interface{}) + assert.True(t, exists) + status, exists = userWithPassword["status"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully created", status) + _, exists = userWithPassword["password"] + assert.False(t, exists) +} + +// Tests Login +func TestLoginInvalidBody(t *testing.T) { + e2e.TestInvalidBody(t, "POST", "/api/login", "Invalid request") +} + +func TestLoginWrongPassword(t *testing.T) { + requestBody := []byte(`{ + "email": "user_with_password@test.com", + "password": "wrong_password" + }`) + + recorder := e2e.MakeRequest("POST", "/api/login", requestBody) + assert.Equal(t, http.StatusUnauthorized, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Invalid login credentials", message) +} + +func TestLoginSuccess(t *testing.T) { + requestBody := []byte(`{ + "email": "user_with_password@test.com", + "password": "fake_password" + }`) + + recorder := e2e.MakeRequest("POST", "/api/login", requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Login succesful", message) + + account, exists := response["account"].(map[string]interface{}) + assert.True(t, exists) + email, exists := account["email"].(string) + assert.True(t, exists) + assert.Equal(t, "user_with_password@test.com", email) + token, exists := account["token"].(string) + assert.True(t, exists) + assert.NotEmpty(t, token) +} + +func TestVerifyToken(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/token/valid", nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "working", message) +} + +func TestRequestWithEmptyAuthorizationHeader(t *testing.T) { + header := map[string]string{ + "Authorization": "", + } + recorder := e2e.MakeRequestWithHeaders("GET", "/api/users", nil, header) + assert.Equal(t, http.StatusForbidden, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Missing auth token", message) +} + +func TestRequestWithNoToken(t *testing.T) { + header := map[string]string{ + "Authorization": "Basic", + } + recorder := e2e.MakeRequestWithHeaders("GET", "/api/users", nil, header) + assert.Equal(t, http.StatusForbidden, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Invalid/Malformed auth token", message) +} + +func TestRequestWithInvalidToken(t *testing.T) { + recorder := e2e.MakeRequestWithToken("GET", "/api/users", nil, "invalid") + assert.Equal(t, http.StatusForbidden, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Malformed authentication token", message) +} + +func TestGetAllUsers(t *testing.T) { + // As admin, we get all users + + recorder := e2e.MakeRequest("GET", "/api/users", nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully got users", message) + + data, exists := response["data"].([]interface{}) + assert.True(t, exists) + assert.Equal(t, 5, len(data)) +} + +func TestGetUsersWithNormalUser(t *testing.T) { + userToken := getUserToken("user_with_password@test.com", "fake_password") + assert.NotEmpty(t, userToken) + + recorder := e2e.MakeRequestWithToken("GET", "/api/users", nil, userToken) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully got users", message) + + data, exists := response["data"].([]interface{}) + assert.True(t, exists) + assert.Equal(t, 0, len(data)) +} + +func TestDeleteWithoutEnoughPermissions(t *testing.T) { + userId := models.GetUserByEmail("user_no_password@test.com").ID.Hex() + assert.NotEmpty(t, userId) + userToken := getUserToken("user_with_password@test.com", "fake_password") + assert.NotEmpty(t, userToken) + + recorder := e2e.MakeRequestWithToken("DELETE", "/api/users/"+userId, nil, userToken) + assert.Equal(t, http.StatusUnauthorized, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Caller does not have permission to delete this user", message) +} + +func TestDeleteUser(t *testing.T) { + // we get the user ID + userId := models.GetUserByEmail("user_no_password@test.com").ID.Hex() + assert.NotEmpty(t, userId) + + recorder := e2e.MakeRequest("DELETE", "/api/users/"+userId, nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully removed user", message) + + // We get a Not Found if we try to delete again + recorder = e2e.MakeRequest("DELETE", "/api/users/"+userId, nil) + assert.Equal(t, http.StatusNotFound, recorder.Code) + + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists = response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "User not found", message) + +} + +func TestDeleteWithInvalidIdReturnsError(t *testing.T) { + recorder := e2e.MakeRequest("DELETE", "/api/users/unknown", nil) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "User ID is not valid", message) +} + +// Tests modify user role +func TestModifyUserInvalidBody(t *testing.T) { + userId := models.GetUserByEmail("user_with_password@test.com").ID.Hex() + e2e.TestInvalidBody(t, "PATCH", "/api/users/"+userId, "Invalid request") +} + +func TestModifyRoleWithMoreDataReturnsError(t *testing.T) { + // we get the user ID + userId := models.GetUserByEmail("user_with_password@test.com").ID.Hex() + assert.NotEmpty(t, userId) + + requestBody := []byte(`{ + "roles": { + "*": "user" + }, + "name": "other name" + }`) + + recorder := e2e.MakeRequest("PATCH", "/api/users/"+userId, requestBody) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Only 'roles' should be provided to patch", message) +} + +func TestModifyRoleWithInvalidRole(t *testing.T) { + // we get the user ID + userId := models.GetUserByEmail("user_with_password@test.com").ID.Hex() + assert.NotEmpty(t, userId) + + requestBody := []byte(`{ + "roles": { + "*": "invalid" + } + }`) + + recorder := e2e.MakeRequest("PATCH", "/api/users/"+userId, requestBody) + assert.Equal(t, http.StatusInternalServerError, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Role assigned is not valid: ", message) +} + +func TestModifyRoleWithInvalidId(t *testing.T) { + requestBody := []byte(`{ + "roles": { + "*": "user" + } + }`) + + recorder := e2e.MakeRequest("PATCH", "/api/users/invalid", requestBody) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "User ID is not valid", message) +} + +func TestModifyRoleWithNormalUser(t *testing.T) { + userId := models.GetUserByEmail("user_with_password@test.com").ID.Hex() + assert.NotEmpty(t, userId) + userToken := getUserToken("user_with_password@test.com", "fake_password") + assert.NotEmpty(t, userToken) + + requestBody := []byte(`{ + "roles": { + "*": "manager" + } + }`) + + recorder := e2e.MakeRequestWithToken("PATCH", "/api/users/"+userId, requestBody, userToken) + assert.Equal(t, http.StatusUnauthorized, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Caller does not have permission to modify this user", message) +} + +func TestModifyRoleSuccess(t *testing.T) { + // we get the user ID + userId := models.GetUserByEmail("user_with_password@test.com").ID.Hex() + assert.NotEmpty(t, userId) + + requestBody := []byte(`{ + "roles": { + "*": "viewer" + } + }`) + + recorder := e2e.MakeRequest("PATCH", "/api/users/"+userId, requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully updated user roles", message) +} + +// Tests modify and reset user password +func TestModifyPasswordInvalidBody(t *testing.T) { + e2e.TestInvalidBody(t, "POST", "/api/users/password/change", "Invalid request") +} + +func TestModifyPasswordNotEnoughArguments(t *testing.T) { + userToken := getUserToken("user_with_password@test.com", "fake_password") + requestBody := []byte(`{ + "newPassword": "fake_password" + }`) + + recorder := e2e.MakeRequestWithToken("POST", "/api/users/password/change", requestBody, userToken) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Invalid request: wrong body format", message) +} + +func TestModifyPasswordSuccess(t *testing.T) { + userToken := getUserToken("user_with_password@test.com", "fake_password") + requestBody := []byte(`{ + "currentPassword": "fake_password", + "newPassword": "fake_password2" + }`) + + recorder := e2e.MakeRequestWithToken("POST", "/api/users/password/change", requestBody, userToken) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully updated user password", message) + + token, exists := response["token"].(string) + assert.True(t, exists) + assert.NotEmpty(t, token) +} + +func TestResetPasswordErrorWhenResetTokenIsNotValid(t *testing.T) { + // User token is not a reset token + userToken := getUserToken("user_with_password@test.com", "fake_password2") + requestBody := []byte(`{ + "newPassword": "fake_password" + }`) + + recorder := e2e.MakeRequestWithToken("POST", "/api/users/password/reset", requestBody, userToken) + assert.Equal(t, http.StatusForbidden, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Token is not valid.", message) +} + +func TestResetPasswordNotEnoughArguments(t *testing.T) { + userId := models.GetUserByEmail("user_with_password@test.com").ID + resetToken := models.GenerateToken(u.RESET_TAG, userId, time.Minute) + requestBody := []byte(`{}`) + + recorder := e2e.MakeRequestWithToken("POST", "/api/users/password/reset", requestBody, resetToken) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Invalid request: wrong body format", message) +} + +func TestResetPasswordSuccess(t *testing.T) { + userId := models.GetUserByEmail("user_with_password@test.com").ID + resetToken := models.GenerateToken(u.RESET_TAG, userId, time.Minute) + //current password is not needed + requestBody := []byte(`{ + "newPassword": "fake_password" + }`) + + recorder := e2e.MakeRequestWithToken("POST", "/api/users/password/reset", requestBody, resetToken) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully updated user password", message) +} diff --git a/API/controllers/entityController.go b/API/controllers/entity.go similarity index 98% rename from API/controllers/entityController.go rename to API/controllers/entity.go index c87209d63..b78f30308 100644 --- a/API/controllers/entityController.go +++ b/API/controllers/entity.go @@ -9,7 +9,6 @@ import ( "net/url" "os" "p3/models" - "p3/utils" u "p3/utils" "strconv" "strings" @@ -275,9 +274,9 @@ func getBulkDomainsRecursively(parent string, listDomains []map[string]interface } domainObj["category"] = "domain" if desc, ok := domain["description"].(string); ok { - domainObj["description"] = []string{desc} + domainObj["description"] = desc } else { - domainObj["description"] = []string{name} + domainObj["description"] = name } domainObj["attributes"] = map[string]string{} if color, ok := domain["color"].(string); ok { @@ -456,7 +455,7 @@ func HandleGenericObjects(w http.ResponseWriter, r *http.Request) { for _, entStr := range entities { // Get objects - entData, err := models.GetManyObjects(entStr, req, filters, nil, user.Roles) + entData, err := models.GetManyObjects(entStr, req, filters, "", user.Roles) if err != nil { u.ErrLog("Error while looking for objects at "+entStr, "HandleGenericObjects", err.Message, r) u.RespondWithError(w, err) @@ -635,6 +634,8 @@ func HandleComplexFilters(w http.ResponseWriter, r *http.Request) { fmt.Println("******************************************************") DispRequestMetaData(r) var complexFilters map[string]interface{} + var complexFilterExp string + var ok bool matchingObjects := []map[string]interface{}{} // Get user roles for permissions @@ -649,8 +650,12 @@ func HandleComplexFilters(w http.ResponseWriter, r *http.Request) { u.Respond(w, u.Message("Error while decoding request body")) u.ErrLog("Error while decoding request body", "HANDLE COMPLEX FILTERS", "", r) return + } else if complexFilterExp, ok = complexFilters["filter"].(string); !ok || len(complexFilterExp) == 0 { + w.WriteHeader(http.StatusBadRequest) + u.Respond(w, u.Message("Invalid body format: must contain a filter key with a not empty string as value")) + u.ErrLog("Error while decoding request body", "HANDLE COMPLEX FILTERS", "", r) + return } - utils.ApplyWildcardsOnComplexFilter(complexFilters) // Get objects filters := getFiltersFromQueryParams(r) @@ -659,7 +664,7 @@ func HandleComplexFilters(w http.ResponseWriter, r *http.Request) { for _, entStr := range entities { // Get objects - entData, err := models.GetManyObjects(entStr, req, filters, complexFilters, user.Roles) + entData, err := models.GetManyObjects(entStr, req, filters, complexFilterExp, user.Roles) if err != nil { u.ErrLog("Error while looking for objects at "+entStr, "HandleComplexFilters", err.Message, r) u.RespondWithError(w, err) @@ -810,7 +815,7 @@ func GetEntity(w http.ResponseWriter, r *http.Request) { } } -// swagger:operation GET /api/layers/{id}/objects Objects GetLayerObjects +// swagger:operation GET /api/layers/{slug}/objects Objects GetLayerObjects // Gets the object of a given layer. // Apply the layer filters to get children objects of a given root query param. // --- @@ -819,7 +824,7 @@ func GetEntity(w http.ResponseWriter, r *http.Request) { // produces: // - application/json // parameters: -// - name: id +// - name: slug // in: path // description: 'ID of desired layer.' // required: true @@ -873,11 +878,14 @@ func GetLayerObjects(w http.ResponseWriter, r *http.Request) { data, modelErr = models.GetObject(bson.M{"slug": id}, u.EntityToString(u.LAYER), u.RequestFilters{}, user.Roles) if modelErr != nil { u.RespondWithError(w, modelErr) + return + } else if len(data) == 0 { + w.WriteHeader(http.StatusNotFound) + return } // Apply layer to get objects request req := bson.M{} - u.AddFilterToReq(req, "filter", data["filter"].(string)) var searchId string if filters.IsRecursive { searchId = filters.Root + ".**.*" @@ -889,8 +897,10 @@ func GetLayerObjects(w http.ResponseWriter, r *http.Request) { // Get objects matchingObjects := []map[string]interface{}{} entities := u.GetEntitiesByNamespace(u.Any, searchId) + fmt.Println(req) + fmt.Println(entities) for _, entStr := range entities { - entData, err := models.GetManyObjects(entStr, req, u.RequestFilters{}, nil, user.Roles) + entData, err := models.GetManyObjects(entStr, req, u.RequestFilters{}, data["filter"].(string), user.Roles) if err != nil { u.RespondWithError(w, err) return @@ -981,7 +991,7 @@ func GetAllEntities(w http.ResponseWriter, r *http.Request) { // Get entities req := bson.M{} - data, e := models.GetManyObjects(entStr, req, u.RequestFilters{}, nil, user.Roles) + data, e := models.GetManyObjects(entStr, req, u.RequestFilters{}, "", user.Roles) // Respond if e != nil { @@ -1339,7 +1349,7 @@ func GetEntityByQuery(w http.ResponseWriter, r *http.Request) { } } - data, modelErr = models.GetManyObjects(entStr, bsonMap, filters, nil, user.Roles) + data, modelErr = models.GetManyObjects(entStr, bsonMap, filters, "", user.Roles) if modelErr != nil { u.ErrLog("Error while getting "+entStr, "GET ENTITYQUERY", modelErr.Message, r) @@ -1415,19 +1425,19 @@ func GetTempUnit(w http.ResponseWriter, r *http.Request) { // - application/json // parameters: // - name: entity -// in: query +// in: path // description: 'Indicates the entity.' // required: true // type: string // default: sites // - name: ID -// in: query +// in: path // description: ID of object // required: true -// type: int +// type: string // default: siteA // - name: subent -// in: query +// in: path // description: 'Indicates the subentity to search for children.' // required: true // type: string @@ -1716,7 +1726,7 @@ func GetCompleteHierarchyAttributes(w http.ResponseWriter, r *http.Request) { } } -// swagger:operation POST /api/{entity}/{id}/unlink Objects UnlinkObject +// swagger:operation PATCH /api/{entity}/{id}/unlink Objects UnlinkObject // Removes the object from its original entity and hierarchy tree to make it stray. // The object will no longer have a parent, its id will change as well as the id of all its children. // The object will then belong to the stray-objects entity. @@ -1753,7 +1763,7 @@ func GetCompleteHierarchyAttributes(w http.ResponseWriter, r *http.Request) { // '500': // description: 'Internal error. Unable to remove object from entity and create it as stray.' -// swagger:operation POST /api/stray-objects/{id}/link Objects LinkObject +// swagger:operation PATCH /api/stray-objects/{id}/link Objects LinkObject // Removes the object from stray and add it to the entity of its category attribute. // The object will again have a parent, its id will change as well as the id of all its children. // The object will then belong to the given entity. diff --git a/API/controllers/entity_test.go b/API/controllers/entity_test.go new file mode 100644 index 000000000..a464ff9f7 --- /dev/null +++ b/API/controllers/entity_test.go @@ -0,0 +1,866 @@ +package controllers_test + +import ( + "encoding/json" + "net/http" + "net/url" + "p3/models" + "p3/test/e2e" + "p3/test/integration" + "p3/utils" + "slices" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func init() { + integration.RequireCreateSite("site-no-temperature") + integration.RequireCreateBuilding("site-no-temperature", "building-1") + integration.RequireCreateBuilding("site-no-temperature", "building-2") + integration.RequireCreateBuilding("site-no-temperature", "building-3") + integration.RequireCreateRoom("site-no-temperature.building-1", "room-1") + integration.RequireCreateRoom("site-no-temperature.building-1", "room-2") + integration.RequireCreateRoom("site-no-temperature.building-2", "room-1") + integration.RequireCreateRack("site-no-temperature.building-1.room-1", "rack-1") + integration.RequireCreateRack("site-no-temperature.building-1.room-1", "rack-2") + integration.RequireCreateDevice("site-no-temperature.building-1.room-1.rack-2", "device-1") + integration.RequireCreateRack("site-no-temperature.building-1.room-2", "rack-1") + integration.RequireCreateSite("site-with-temperature") + integration.RequireCreateBuilding("site-with-temperature", "building-3") + var ManagerUserRoles = map[string]models.Role{ + models.ROOT_DOMAIN: models.Manager, + } + temperatureData := map[string]any{ + "attributes": map[string]any{ + "temperatureUnit": "30", + }, + } + + models.UpdateObject("site", "site-with-temperature", temperatureData, true, ManagerUserRoles, false) + layer := map[string]any{ + "slug": "racks-layer", + "filter": "category=rack", + "applicability": "site-no-temperature.building-1.room-1", + } + models.CreateEntity(utils.LAYER, layer, ManagerUserRoles) + layer2 := map[string]any{ + "slug": "racks-1-layer", + "filter": "category=rack & name=rack-1", + "applicability": "site-no-temperature.building-1.room-*", + } + models.CreateEntity(utils.LAYER, layer2, ManagerUserRoles) +} + +func testInvalidBody(t *testing.T, httpMethod string, endpoint string) { + e2e.TestInvalidBody(t, httpMethod, endpoint, "Error while decoding request body") +} + +func TestCreateEntityInvalidBody(t *testing.T) { + testInvalidBody(t, "POST", "/api/sites") +} + +// Tests domain bulk creation (/api/domains/bulk) +func TestCreateBulkInvalidBody(t *testing.T) { + testInvalidBody(t, "POST", "/api/domains/bulk") +} + +func TestCreateBulkDomains(t *testing.T) { + // Test create two separate domains + requestBody := []byte(`[ + { + "name": "domain1", + "parentId": "", + "color": "ffffff" + }, + { + "name": "domain2", + "parentId": "", + "description": "Domain 2" + } + ]`) + + recorder := e2e.MakeRequest("POST", "/api/domains/bulk", requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["domain1"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully created domain", message) + + message, exists = response["domain2"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully created domain", message) +} + +func TestCreateBulkDomainWithSubdomains(t *testing.T) { + // Test create one domaain with a sub domain + requestBody := []byte(`[ + { + "name": "domain3", + "description": "Domain 3", + "color": "00ED00", + "domains": [ + { + "name": "subDomain1", + "description": "subDomain 1", + "color": "ffffff" + } + ] + }, + { + "name": "domain4", + "description": "Domain 4", + "color": "00ED00", + "domains": [ + { + "name": "subDomain1", + "description": "subDomain 1", + "color": "00ED00" + }, + { + "name": "subDomain2", + "description": "subDomain 2", + "color": "ffffff" + } + ] + } + ]`) + + recorder := e2e.MakeRequest("POST", "/api/domains/bulk", requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["domain3"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully created domain", message) + + message, exists = response["domain3.subDomain1"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully created domain", message) + + message, exists = response["domain4"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully created domain", message) + + message, exists = response["domain4.subDomain1"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully created domain", message) + + message, exists = response["domain4.subDomain2"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully created domain", message) +} + +func TestCreateBulkDomainWithDuplicateError(t *testing.T) { + // Test try to create a domain that already exists + requestBody := []byte(`[ + { + "name": "domain3", + "description": "Domain 3", + "color": "00ED00" + } + ]`) + + recorder := e2e.MakeRequest("POST", "/api/domains/bulk", requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["domain3"].(string) + assert.True(t, exists) + assert.Equal(t, "Error while creating domain: Duplicates not allowed", message) +} + +// Tests delete subdomains (/api/objects) +func TestDeleteSubdomains(t *testing.T) { + // Test delete subdomain using a pattern + params, _ := url.ParseQuery("id=domain3.*") + + recorder := e2e.MakeRequest("DELETE", "/api/objects?"+params.Encode(), nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully deleted objects", message) + + data, exists := response["data"].([]interface{}) + assert.True(t, exists) + assert.Equal(t, 1, len(data)) + deletedDomain := data[0].(map[string]interface{}) + id, exists := deletedDomain["id"].(string) + assert.True(t, exists) + assert.Equal(t, "domain3.subDomain1", id) +} + +// Tests handle complex filters (/api/objects/search) +func TestComplexFilterSearchInvalidBody(t *testing.T) { + testInvalidBody(t, "POST", "/api/objects/search") +} + +func TestComplexFilterWithNoFilterInput(t *testing.T) { + requestBody := []byte(`{}`) + + recorder := e2e.MakeRequest("POST", "/api/objects/search", requestBody) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Invalid body format: must contain a filter key with a not empty string as value", message) +} + +func TestComplexFilterSearch(t *testing.T) { + // Test get subdomains of domain4 with color 00ED00 + requestBody := []byte(`{ + "filter": "id=domain4.* & color=00ED00" + }`) + + recorder := e2e.MakeRequest("POST", "/api/objects/search", requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully processed request", message) + + data, exists := response["data"].([]interface{}) + assert.True(t, exists) + assert.Equal(t, 1, len(data)) + + domain := data[0].(map[string]interface{}) + id, exists := domain["id"].(string) + assert.True(t, exists) + assert.Equal(t, "domain4.subDomain1", id) +} + +func TestComplexFilterSearchWithStartDateFilter(t *testing.T) { + // Test get subdomains of domain4 with color 00ED00 and different startDate + requestBody := []byte(`{ + "filter": "id=domain4.* & color=00ED00" + }`) + + yesterday := time.Now().Add(-24 * time.Hour) + tomorrow := time.Now().Add(24 * time.Hour) + recorder := e2e.MakeRequest("POST", "/api/objects/search?startDate="+yesterday.Format("2006-01-02"), requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully processed request", message) + + data, exists := response["data"].([]interface{}) + assert.True(t, exists) + assert.Equal(t, 1, len(data)) + + recorder = e2e.MakeRequest("POST", "/api/objects/search?startDate="+tomorrow.Format("2006-01-02"), requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists = response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully processed request", message) + + data, exists = response["data"].([]interface{}) + assert.True(t, exists) + assert.Equal(t, 0, len(data)) +} + +func TestComplexFilterSearchWithEndtDateFilter(t *testing.T) { + // Test get subdomains of domain4 with color 00ED00 and different endDate + requestBody := []byte(`{ + "filter": "id=domain4.* & color=00ED00" + }`) + + yesterday := time.Now().Add(-24 * time.Hour) + tomorrow := time.Now().Add(24 * time.Hour) + recorder := e2e.MakeRequest("POST", "/api/objects/search?endDate="+tomorrow.Format("2006-01-02"), requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully processed request", message) + + data, exists := response["data"].([]interface{}) + assert.True(t, exists) + assert.Equal(t, 1, len(data)) + + recorder = e2e.MakeRequest("POST", "/api/objects/search?endDate="+yesterday.Format("2006-01-02"), requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists = response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully processed request", message) + + data, exists = response["data"].([]interface{}) + assert.True(t, exists) + assert.Equal(t, 0, len(data)) +} + +// Tests handle delete with complex filters (/api/objects/search) +func TestComplexFilterDelete(t *testing.T) { + // Test delete subdomains of domain4 with color 00ED00 + requestBody := []byte(`{ + "filter": "id=domain4.* & color=00ED00" + }`) + + recorder := e2e.MakeRequest("DELETE", "/api/objects/search", requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully deleted objects", message) + + data, exists := response["data"].([]interface{}) + assert.True(t, exists) + assert.Equal(t, 1, len(data)) + + domain := data[0].(map[string]interface{}) + id, exists := domain["id"].(string) + assert.True(t, exists) + assert.Equal(t, "domain4.subDomain1", id) +} + +// Tests get different entities +func TestGetDomainEntity(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/domains", nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully got domains", message) + + // we have multiple domains + data, exists := response["data"].(map[string]interface{}) + assert.True(t, exists) + + objects, exists := data["objects"].([]interface{}) + assert.True(t, exists) + assert.Equal(t, true, len(objects) > 0) // we have domains created in this file and others + + // domain3 exists but domain3.subDomain1 does not + hasDomain3 := slices.ContainsFunc(objects, func(value interface{}) bool { + domain := value.(map[string]interface{}) + return domain["id"].(string) == "domain3" + }) + assert.Equal(t, true, hasDomain3) + + hasDomain3Subdomain1 := slices.ContainsFunc(objects, func(value interface{}) bool { + domain := value.(map[string]interface{}) + return domain["id"].(string) == "domain3.subDomain1" + }) + assert.Equal(t, false, hasDomain3Subdomain1) +} + +func TestGetBuildingsEntity(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/buildings", nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully got buildings", message) + + // we have multiple buildings + data, exists := response["data"].(map[string]interface{}) + assert.True(t, exists) + + objects, exists := data["objects"].([]interface{}) + assert.True(t, exists) + assert.Equal(t, true, len(objects) > 0) +} + +func TestGetUnknownEntity(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/unknown", nil) + assert.Equal(t, http.StatusNotFound, recorder.Code) +} + +func TestGetDomainEntitiesFilteredByColor(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/domains?color=00ED00", nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully got query for domain", message) + + // we have multiple domains + data, exists := response["data"].(map[string]interface{}) + assert.True(t, exists) + + objects, exists := data["objects"].([]interface{}) + assert.True(t, exists) + assert.Equal(t, 2, len(objects)) // domain3 and domain4 +} + +// Test get temperature unit +func TestGetTemperatureForDomain(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/tempunits/domain3", nil) + assert.Equal(t, http.StatusNotFound, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Could not find parent site for given object", message) +} + +func TestGetTemperatureForParentWithNoTemperature(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/tempunits/site-no-temperature.building-1", nil) + assert.Equal(t, http.StatusNotFound, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Parent site has no temperatureUnit in attributes", message) +} + +func TestGetTemperature(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/tempunits/site-with-temperature.building-3", nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully got temperatureUnit from object's parent site", message) + + data, exists := response["data"].(map[string]interface{}) + assert.True(t, exists) + temperatureUnit, exists := data["temperatureUnit"].(string) + assert.True(t, exists) + assert.Equal(t, "30", temperatureUnit) +} + +// Tests get subentities +func TestErrorGetRoomsBuildingsInvalidHierarchy(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/rooms/site-no-temperature.building-1.room-1/buildings", nil) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Invalid set of entities in URL: first entity should be parent of the second entity", message) +} + +func TestErrorGetSiteRoomsUnknownEntity(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/sites/unknown/rooms", nil) + assert.Equal(t, http.StatusNotFound, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Nothing matches this request", message) +} + +func TestGetSitesRooms(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/sites/site-no-temperature/rooms", nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully got object", message) + + data, exists := response["data"].(map[string]interface{}) + assert.True(t, exists) + objects, exists := data["objects"].([]interface{}) + assert.True(t, exists) + assert.Equal(t, 3, len(objects)) + + areRooms := true + for _, element := range objects { + if element.(map[string]interface{})["category"] != "room" { + areRooms = false + break + } + } + assert.True(t, areRooms) +} + +func TestGetHierarchyAttributes(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/hierarchy/attributes", nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully got attrs hierarchy", message) + + data, exists := response["data"].(map[string]interface{}) + assert.True(t, exists) + keys := make([]int, len(data)) + assert.True(t, len(keys) > 0) + + // we test the color attribute is present for domain1 + domain1, exists := data["domain1"].(map[string]interface{}) + assert.True(t, exists) + color, exists := domain1["color"].(string) + assert.True(t, exists) + assert.Equal(t, "ffffff", color) +} + +// Tests link and unlink entity +func TestErrorUnlinkWithNotAllowedAttributes(t *testing.T) { + requestBody := []byte(`{ + "name": "StrayRoom", + "other": "other" + }`) + + recorder := e2e.MakeRequest("PATCH", "/api/rooms/site-no-temperature.building-2.room-1/unlink", requestBody) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Body must be empty or only contain valid name", message) +} + +func TestUnlinkRoom(t *testing.T) { + requestBody := []byte(`{ + "name": "StrayRoom" + }`) + + recorder := e2e.MakeRequest("PATCH", "/api/rooms/site-no-temperature.building-2.room-1/unlink", requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully unlinked", message) + + // We verify room-1 does not exist + recorder = e2e.MakeRequest("GET", "/api/rooms/site-no-temperature.building-2.room-1", requestBody) + assert.Equal(t, http.StatusNotFound, recorder.Code) + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists = response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Nothing matches this request", message) + + // We verify the StrayRoom exists + recorder = e2e.MakeRequest("GET", "/api/stray-objects/StrayRoom", requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + json.Unmarshal(recorder.Body.Bytes(), &response) + data, exists := response["data"].(map[string]interface{}) + assert.True(t, exists) + id := data["id"].(string) + assert.Equal(t, "StrayRoom", id) +} + +func TestErrorLinkWithoutParentId(t *testing.T) { + requestBody := []byte(`{ + "name": "room-1" + }`) + + recorder := e2e.MakeRequest("PATCH", "/api/stray-objects/StrayRoom/link", requestBody) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Error while decoding request body: must contain parentId", message) +} + +func TestLinkRoom(t *testing.T) { + requestBody := []byte(`{ + "parentId": "site-no-temperature.building-2", + "name": "room-1" + }`) + + recorder := e2e.MakeRequest("PATCH", "/api/stray-objects/StrayRoom/link", requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully linked", message) + + // We verify the StrayRoom does not exist + recorder = e2e.MakeRequest("GET", "/api/stray-objects/StrayRoom", requestBody) + assert.Equal(t, http.StatusNotFound, recorder.Code) + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists = response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Nothing matches this request", message) + + // We verify room-1 exists again + recorder = e2e.MakeRequest("GET", "/api/rooms/site-no-temperature.building-2.room-1", requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + json.Unmarshal(recorder.Body.Bytes(), &response) + data, exists := response["data"].(map[string]interface{}) + assert.True(t, exists) + id := data["id"].(string) + assert.Equal(t, "site-no-temperature.building-2.room-1", id) +} + +// Tests entity validation +func TestValidateInvalidBody(t *testing.T) { + testInvalidBody(t, "POST", "/api/validate/rooms") +} + +func TestValidateNonExistentEntity(t *testing.T) { + requestBody := []byte(`{}`) + + recorder := e2e.MakeRequest("POST", "/api/validate/invalid", requestBody) + assert.Equal(t, http.StatusNotFound, recorder.Code) +} + +func TestValidateEntityWithoutAttributes(t *testing.T) { + requestBody := []byte(`{ + "category": "room", + "description": "room", + "domain": "domain1", + "name": "roomA", + "parentId": "site-no-temperature.building-1" + }`) + + recorder := e2e.MakeRequest("POST", "/api/validate/rooms", requestBody) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "JSON body doesn't validate with the expected JSON schema", message) +} + +func TestValidateEntityNonExistentDomain(t *testing.T) { + requestBody := []byte(`{ + "attributes": { + "floorUnit": "t", + "height": "2.8", + "heightUnit": "m", + "axisOrientation": "+x+y", + "rotation": "-90", + "posXY": "[0, 0]", + "posXYUnit": "m", + "size": "[-13, -2.9]", + "sizeUnit": "m", + "template": "" + }, + "category": "room", + "description": "room", + "domain": "invalid", + "name": "roomA", + "parentId": "site-no-temperature.building-1" + }`) + + recorder := e2e.MakeRequest("POST", "/api/validate/rooms", requestBody) + assert.Equal(t, http.StatusNotFound, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Domain not found: invalid", message) +} + +func TestValidateEntityInvalidDomain(t *testing.T) { + requestBody := []byte(`{ + "attributes": { + "floorUnit": "t", + "height": "2.8", + "heightUnit": "m", + "axisOrientation": "+x+y", + "rotation": "-90", + "posXY": "[0, 0]", + "posXYUnit": "m", + "size": "[-13, -2.9]", + "sizeUnit": "m", + "template": "" + }, + "category": "room", + "description": "room", + "domain": "domain1", + "name": "roomA", + "parentId": "site-no-temperature.building-1" + }`) + + recorder := e2e.MakeRequest("POST", "/api/validate/rooms", requestBody) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Object domain is not equal or child of parent's domain", message) +} + +func TestValidateValidRoomEntity(t *testing.T) { + requestBody := []byte(`{ + "attributes": { + "floorUnit": "t", + "height": "2.8", + "heightUnit": "m", + "axisOrientation": "+x+y", + "rotation": "-90", + "posXY": "[0, 0]", + "posXYUnit": "m", + "size": "[-13, -2.9]", + "sizeUnit": "m", + "template": "" + }, + "category": "room", + "description": "room", + "domain": "` + integration.TestDBName + `", + "name": "roomA", + "parentId": "site-no-temperature.building-1" + }`) + + recorder := e2e.MakeRequest("POST", "/api/validate/rooms", requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "This object can be created", message) +} + +func TestErrorValidateValidRoomEntityNotEnoughPermissions(t *testing.T) { + requestBody := []byte(`{ + "attributes": { + "floorUnit": "t", + "height": "2.8", + "heightUnit": "m", + "axisOrientation": "+x+y", + "rotation": "-90", + "posXY": "[0, 0]", + "posXYUnit": "m", + "size": "[-13, -2.9]", + "sizeUnit": "m", + "template": "" + }, + "category": "room", + "description": "room", + "domain": "` + integration.TestDBName + `", + "name": "roomA", + "parentId": "site-no-temperature.building-1" + }`) + recorder := e2e.MakeRequestWithUser("POST", "/api/validate/rooms", requestBody, "viewer") + assert.Equal(t, http.StatusUnauthorized, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "This user does not have sufficient permissions to create this object under this domain ", message) +} + +func TestGetStats(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/stats", nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + numberOfRacks, exists := response["Number of racks:"].(float64) + assert.True(t, exists) + assert.True(t, numberOfRacks > 0) +} + +func TestGetApiVersion(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/version", nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + status, exists := response["status"].(bool) + assert.True(t, exists) + assert.True(t, status) + + data, exists := response["data"].(map[string]interface{}) + assert.True(t, exists) + customer, exists := data["Customer"].(string) + assert.True(t, exists) + assert.True(t, len(customer) > 0) +} + +// Tests layers objects +func TestGetLayersObjectsRootRequired(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/layers/racks-layer/objects", nil) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Query param root is mandatory", message) +} + +func TestGetLayersObjectsLayerUnknown(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/layers/unknown/objects?root=site-no-temperature.building-1.room-1", nil) + assert.Equal(t, http.StatusNotFound, recorder.Code) +} + +func TestGetLayersObjectsWithSimpleFilter(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/layers/racks-layer/objects?root=site-no-temperature.building-1.room-1", nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully processed request", message) + + data, exists := response["data"].([]any) + assert.True(t, exists) + assert.Equal(t, 2, len(data)) + + condition := true + for _, rack := range data { + condition = condition && rack.(map[string]any)["parentId"] == "site-no-temperature.building-1.room-1" + condition = condition && rack.(map[string]any)["category"] == "rack" + } + + assert.True(t, condition) +} + +func TestGetLayersObjectsWithDoubleFilter(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/layers/racks-1-layer/objects?root=site-no-temperature.building-1.room-*", nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully processed request", message) + + data, exists := response["data"].([]any) + assert.True(t, exists) + assert.Equal(t, 2, len(data)) + + condition := true + for _, rack := range data { + condition = condition && strings.HasPrefix(rack.(map[string]any)["parentId"].(string), "site-no-temperature.building-1.room-") + condition = condition && rack.(map[string]any)["category"] == "rack" + condition = condition && rack.(map[string]any)["name"] == "rack-1" + } + + assert.True(t, condition) +} diff --git a/API/controllers/webController.go b/API/controllers/web.go similarity index 99% rename from API/controllers/webController.go rename to API/controllers/web.go index b2dd4317f..4e772974d 100644 --- a/API/controllers/webController.go +++ b/API/controllers/web.go @@ -23,7 +23,7 @@ import ( // in: query // description: 'Email of the user whose projects are being requested. // Example: /api/projects?user=user@test.com' -// required: false +// required: true // type: string // default: user@test.com // responses: diff --git a/API/controllers/web_test.go b/API/controllers/web_test.go new file mode 100644 index 000000000..ce1cccd15 --- /dev/null +++ b/API/controllers/web_test.go @@ -0,0 +1,149 @@ +package controllers_test + +import ( + "encoding/json" + "net/http" + "p3/test/e2e" + "p3/test/integration" + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + integration.RequireCreateSite("site-project") +} + +var project = map[string]any{ + "attributes": []string{"domain"}, + "authorLastUpdate": "admin@admin.com", + "dateRange": "01/01/2023-02/02/2023", + "lastUpdate": "02/02/2023", + "name": "project1", + "namespace": "physical", + "objects": []string{"site-project"}, + "showAvg": false, + "showSum": false, + "permissions": []string{"admin@admin.com"}, +} +var projectId string + +func TestCreateProjectInvalidBody(t *testing.T) { + e2e.TestInvalidBody(t, "POST", "/api/projects", "Invalid request") +} + +func TestCreateProject(t *testing.T) { + json.Marshal(project) + requestBody, _ := json.Marshal(project) + + recorder := e2e.MakeRequest("POST", "/api/projects", requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully handled project request", message) +} + +// Tests domain bulk creation (/api/users/bulk) +func TestGetProjectsWithNoUserRespondsWithError(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/projects", nil) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Error: user should be sent as query param", message) +} + +func TestGetProjectsFromUserWithNoProjects(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/projects?user=someUser", nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully got projects", message) + + data, exists := response["data"].(map[string]interface{}) + assert.True(t, exists) + projects, exists := data["projects"].([]interface{}) + assert.True(t, exists) + assert.Equal(t, 0, len(projects)) +} + +func TestGetProjects(t *testing.T) { + recorder := e2e.MakeRequest("GET", "/api/projects?user=admin@admin.com", nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully got projects", message) + + data, exists := response["data"].(map[string]interface{}) + assert.True(t, exists) + projects, exists := data["projects"].([]interface{}) + assert.True(t, exists) + assert.Equal(t, 1, len(projects)) + + projectName, exists := projects[0].(map[string]interface{})["name"].(string) + assert.True(t, exists) + assert.Equal(t, "project1", projectName) + + projectId = projects[0].(map[string]interface{})["Id"].(string) +} + +func TestUpdateProject(t *testing.T) { + project["showAvg"] = true + json.Marshal(project) + requestBody, _ := json.Marshal(project) + + recorder := e2e.MakeRequest("PUT", "/api/projects/"+projectId, requestBody) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully handled project request", message) + + data, exists := response["data"].(map[string]interface{}) + assert.True(t, exists) + showAvg, exists := data["showAvg"].(bool) + assert.True(t, exists) + assert.True(t, showAvg) +} + +func TestDeleteProject(t *testing.T) { + recorder := e2e.MakeRequest("DELETE", "/api/projects/"+projectId, nil) + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "successfully removed project", message) +} + +func TestDeleteProjectNonExistent(t *testing.T) { + recorder := e2e.MakeRequest("DELETE", "/api/projects/"+projectId, nil) + assert.Equal(t, http.StatusNotFound, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, "Project not found", message) +} diff --git a/API/models/create_object_test.go b/API/models/create_object_test.go index 197e7cb62..004a2464d 100644 --- a/API/models/create_object_test.go +++ b/API/models/create_object_test.go @@ -1,6 +1,7 @@ package models_test import ( + "log" "p3/models" "p3/test/integration" "p3/test/unit" @@ -10,6 +11,246 @@ import ( "github.com/stretchr/testify/assert" ) +func init() { + integration.RequireCreateSite("siteA") + integration.RequireCreateBuilding("siteA", "building-1") + integration.RequireCreateRoom("siteA.building-1", "room-1") + integration.RequireCreateRack("siteA.building-1.room-1", "rack-1") + integration.RequireCreateDevice("siteA.building-1.room-1.rack-1", "device-1") + integration.RequireCreateDevice("siteA.building-1.room-1.rack-1", "device-2") + integration.RequireCreateDevice("siteA.building-1.room-1.rack-1.device-1", "device-2") + ManagerUserRoles := map[string]models.Role{ + models.ROOT_DOMAIN: models.Manager, + } + rackTemplate := map[string]any{ + "slug": "rack-with-slots", + "description": "rack with slots", + "category": "rack", + "sizeWDHmm": []any{605, 1200, 2003}, + "fbxModel": "", + "attributes": map[string]any{ + "vendor": "IBM", + "model": "9360-4PX", + }, + "colors": []any{}, + "components": []any{}, + "slots": []any{ + map[string]any{ + "location": "u01", + "type": "u", + "elemOrient": []any{33.3, -44.4, 107}, + "elemPos": []any{58, 51, 44.45}, + "elemSize": []any{482.6, 1138, 44.45}, + "mandatory": "no", + "labelPos": "frontrear", + "color": "@color1", + }, + }, + "sensors": []any{ + map[string]any{ + "location": "se1", + "elemPos": []any{"right", "rear", "upper"}, + "elemSize": []any{50, 20, 20}, + }, + }, + } + rack := map[string]any{ + "attributes": map[string]any{ + "height": "47", + "heightUnit": "U", + "rotation": "[45, 45, 45]", + "posXYZ": "[4.6666666666667, -2, 0]", + "posXYUnit": "m", + "size": "[80, 100.532442]", + "sizeUnit": "cm", + "template": "rack-with-slots", + }, + "category": "rack", + "description": "rack with slots", + "domain": integration.TestDBName, + "name": "rack-slots", + "parentId": "siteA.building-1.room-1", + } + + _, err := models.CreateEntity(u.OBJTMPL, rackTemplate, ManagerUserRoles) + if err != nil { + log.Fatalln("Error while creating template", err.Error()) + } + _, err = models.CreateEntity(u.RACK, rack, ManagerUserRoles) + if err != nil { + log.Fatalln("Error while creating rack", err.Error()) + } +} + +func TestValidateEntityDomainParent(t *testing.T) { + template := map[string]any{ + "parentId": "", + "name": "domainA", + "category": "domain", + "description": "domainA", + "attributes": map[string]any{}, + } + err := models.ValidateEntity(u.DOMAIN, template) + assert.Nil(t, err) +} + +func TestValidateEntityRoomParent(t *testing.T) { + template := map[string]any{ + "parentId": "siteA", + "name": "roomA", + "category": "room", + "description": "roomA", + "domain": integration.TestDBName, + "attributes": map[string]any{ + "floorUnit": "t", + "height": "2.8", + "heightUnit": "m", + "axisOrientation": "+x+y", + "rotation": "-90", + "posXY": "[0, 0]", + "posXYUnit": "m", + "size": "[-13, -2.9]", + "sizeUnit": "m", + "template": "", + }, + } + err := models.ValidateEntity(u.ROOM, template) + assert.NotNil(t, err) + assert.Equal(t, "ParentID should correspond to existing building ID", err.Message) + + template["parentId"] = "siteA.building-1" + err = models.ValidateEntity(u.ROOM, template) + assert.Nil(t, err) +} + +func TestValidateEntityDeviceParent(t *testing.T) { + template := map[string]any{ + "parentId": "siteA", + "name": "deviceA", + "category": "device", + "description": "deviceA", + "domain": integration.TestDBName, + "attributes": map[string]any{ + "TDP": "", + "TDPmax": "", + "fbxModel": "https://github.com/test.fbx", + "height": "40.1", + "heightUnit": "mm", + "model": "TNF2LTX", + "orientation": "front", + "partNumber": "0303XXXX", + "size": "[388.4, 205.9]", + "sizeUnit": "mm", + "template": "huawei-xxxxxx", + "type": "blade", + "vendor": "Huawei", + "weightKg": "1.81", + }, + } + err := models.ValidateEntity(u.DEVICE, template) + assert.NotNil(t, err) + assert.Equal(t, "ParentID should correspond to existing rack or device ID", err.Message) + + template["parentId"] = "siteA.building-1.room-1.rack-1" + err = models.ValidateEntity(u.DEVICE, template) + assert.Nil(t, err) + + template["parentId"] = "siteA.building-1.room-1.rack-1.device-1" + template["name"] = "deviceA" + delete(template, "id") + err = models.ValidateEntity(u.DEVICE, template) + assert.Nil(t, err) +} + +func TestValidateEntityDeviceSlot(t *testing.T) { + template := map[string]any{ + "parentId": "siteA.building-1.room-1.rack-slots", + "name": "deviceA", + "category": "device", + "description": "deviceA", + "domain": integration.TestDBName, + "attributes": map[string]any{ + "slot": "[unknown]", + "TDP": "", + "TDPmax": "", + "fbxModel": "https://github.com/test.fbx", + "height": "40.1", + "heightUnit": "mm", + "model": "TNF2LTX", + "orientation": "front", + "partNumber": "0303XXXX", + "size": "[388.4, 205.9]", + "sizeUnit": "mm", + "template": "huawei-xxxxxx", + "type": "blade", + "vendor": "Huawei", + "weightKg": "1.81", + }, + } + err := models.ValidateEntity(u.DEVICE, template) + assert.NotNil(t, err) + assert.Equal(t, "Invalid slot: parent does not have all the requested slots", err.Message) + + // We add a valid slot + template["attributes"].(map[string]any)["slot"] = "[u01]" + err = models.ValidateEntity(u.DEVICE, template) + assert.Nil(t, err) + + // We add a device to the slot + delete(template, "id") + ManagerUserRoles := map[string]models.Role{ + models.ROOT_DOMAIN: models.Manager, + } + _, err = models.CreateEntity(u.DEVICE, template, ManagerUserRoles) + assert.Nil(t, err, "The device") + + // we verify if we can add another device in the same slot + template["attributes"].(map[string]any)["slot"] = "[u01]" + template["name"] = "deviceB" + delete(template, "id") + delete(template, "createdDate") + delete(template, "lastUpdated") + err = models.ValidateEntity(u.DEVICE, template) + assert.NotNil(t, err) + assert.Equal(t, "Invalid slot: one or more requested slots are already in use", err.Message) +} + +func TestValidateEntityGroupParent(t *testing.T) { + template := map[string]any{ + "parentId": "siteA", + "name": "groupA", + "category": "group", + "description": "groupA", + "domain": integration.TestDBName, + "attributes": map[string]any{ + "content": "device-1,device-1.device-2", + }, + } + err := models.ValidateEntity(u.GROUP, template) + assert.NotNil(t, err) + assert.Equal(t, "Group parent should correspond to existing rack or room", err.Message) + + template["parentId"] = "siteA.building-1.room-1" + template["name"] = "groupA" + err = models.ValidateEntity(u.GROUP, template) + assert.NotNil(t, err) + assert.Equal(t, "All group objects must be directly under the parent (no . allowed)", err.Message) + + template["parentId"] = "siteA.building-1.room-1" + template["name"] = "groupA" + template["attributes"].(map[string]any)["content"] = "rack-1" + delete(template, "id") + err = models.ValidateEntity(u.GROUP, template) + assert.Nil(t, err) + + template["parentId"] = "siteA.building-1.room-1.rack-1" + template["name"] = "groupA" + template["attributes"].(map[string]any)["content"] = "device-1,device-2" + delete(template, "id") + err = models.ValidateEntity(u.GROUP, template) + assert.Nil(t, err) +} + func TestCreateRackWithoutAttributesReturnsError(t *testing.T) { _, err := models.CreateEntity( u.RACK, diff --git a/API/models/model.go b/API/models/model.go index d064e1c8c..a1d81e9f5 100644 --- a/API/models/model.go +++ b/API/models/model.go @@ -9,6 +9,7 @@ import ( "os" "p3/repository" u "p3/utils" + "regexp" "strconv" "strings" "time" @@ -226,7 +227,7 @@ func GetObject(req bson.M, entityStr string, filters u.RequestFilters, userRoles return object, nil } -func GetManyObjects(entityStr string, req bson.M, filters u.RequestFilters, complexFilters map[string]any, userRoles map[string]Role) ([]map[string]interface{}, *u.Error) { +func GetManyObjects(entityStr string, req bson.M, filters u.RequestFilters, complexFilterExp string, userRoles map[string]Role) ([]map[string]interface{}, *u.Error) { ctx, cancel := u.Connect() var err error var c *mongo.Cursor @@ -246,12 +247,17 @@ func GetManyObjects(entityStr string, req bson.M, filters u.RequestFilters, comp return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} } - if complexFilters != nil { - err = getDatesFromComplexFilters(complexFilters) - if err != nil { + if complexFilterExp != "" { + if complexFilters, err := ComplexFilterToMap(complexFilterExp); err != nil { return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} + } else { + err = getDatesFromComplexFilters(complexFilters) + if err != nil { + return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} + } + u.ApplyWildcardsOnComplexFilter(complexFilters) + maps.Copy(req, complexFilters) } - maps.Copy(req, complexFilters) } if opts != nil { @@ -288,11 +294,98 @@ func GetManyObjects(entityStr string, req bson.M, filters u.RequestFilters, comp return data, nil } +func ComplexFilterToMap(complexFilter string) (map[string]any, error) { + // Split the input string into individual filter expressions + chars := []string{"(", ")", "&", "|"} + for _, char := range chars { + complexFilter = strings.ReplaceAll(complexFilter, char, " "+char+" ") + } + return complexExpressionToMap(strings.Fields(complexFilter)) +} + +func complexExpressionToMap(expressions []string) (map[string]any, error) { + // Find the rightmost operator (AND, OR) outside of parentheses + parenCount := 0 + for i := len(expressions) - 1; i >= 0; i-- { + switch expressions[i] { + case "(": + parenCount++ + case ")": + parenCount-- + case "&": + if parenCount == 0 { + first, _ := complexExpressionToMap(expressions[:i]) + second, _ := complexExpressionToMap(expressions[i+1:]) + return map[string]any{"$and": []map[string]any{ + first, + second, + }}, nil + } + case "|": + if parenCount == 0 { + first, _ := complexExpressionToMap(expressions[:i]) + second, _ := complexExpressionToMap(expressions[i+1:]) + return map[string]any{"$or": []map[string]any{ + first, + second, + }}, nil + } + } + } + + // If there are no operators outside of parentheses, look for the innermost pair of parentheses + for i := 0; i < len(expressions); i++ { + if expressions[i] == "(" { + start, end := i+1, i+1 + for parenCount := 1; end < len(expressions) && parenCount > 0; end++ { + switch expressions[end] { + case "(": + parenCount++ + case ")": + parenCount-- + } + } + return complexExpressionToMap(append(expressions[:start-1], expressions[start:end-1]...)) + } + } + + // Base case: single filter expression + re := regexp.MustCompile(`^([\w-.]+)\s*(<=|>=|<|>|!=|=)\s*([\w-.*]+)$`) + + ops := map[string]string{"<=": "$lte", ">=": "$gte", "<": "$lt", ">": "$gt", "!=": "$not"} + + if len(expressions) <= 3 { + expression := strings.Join(expressions[:], "") + + if match := re.FindStringSubmatch(expression); match != nil { + switch match[1] { + case "startDate": + return map[string]any{"lastUpdated": map[string]any{"$gte": match[3]}}, nil + case "endDate": + return map[string]any{"lastUpdated": map[string]any{"$lte": match[3]}}, nil + case "id", "name", "category", "description", "domain", "createdDate", "lastUpdated", "slug": + if match[2] == "=" { + return map[string]any{match[1]: match[3]}, nil + } + return map[string]any{match[1]: map[string]any{ops[match[2]]: match[3]}}, nil + default: + if match[2] == "=" { + return map[string]any{"attributes." + match[1]: match[3]}, nil + } + return map[string]any{"attributes." + match[1]: map[string]any{ops[match[2]]: match[3]}}, nil + } + } + } + + fmt.Println("Error: Invalid filter expression") + return nil, errors.New("invalid filter expression") +} + func getDatesFromComplexFilters(req map[string]any) error { for k, v := range req { if k == "$and" || k == "$or" { - for _, complexFilter := range v.([]any) { - err := getDatesFromComplexFilters(complexFilter.(map[string]any)) + for _, complexFilter := range v.([]map[string]any) { + err := getDatesFromComplexFilters(complexFilter) if err != nil { return err } @@ -854,7 +947,7 @@ func getChildren(entity, hierarchyName string, limit int, filters u.RequestFilte // Obj should include parentName and not surpass limit range pattern := primitive.Regex{Pattern: "^" + hierarchyName + "(." + u.NAME_REGEX + "){1," + strconv.Itoa(limit) + "}$", Options: ""} - children, e1 := GetManyObjects(checkEntName, bson.M{"id": pattern}, filters, nil, nil) + children, e1 := GetManyObjects(checkEntName, bson.M{"id": pattern}, filters, "", nil) if e1 != nil { println("SUBENT: ", checkEntName) println("ERR: ", e1.Message) @@ -896,7 +989,7 @@ func GetEntitiesOfAncestor(id string, entStr, wantedEnt string, userRoles map[st // Get sub entity objects pattern := primitive.Regex{Pattern: "^" + id + u.HN_DELIMETER, Options: ""} req = bson.M{"id": pattern} - sub, e1 := GetManyObjects(wantedEnt, req, u.RequestFilters{}, nil, userRoles) + sub, e1 := GetManyObjects(wantedEnt, req, u.RequestFilters{}, "", userRoles) if e1 != nil { return nil, e1 } diff --git a/API/models/rbac_test.go b/API/models/rbac_test.go new file mode 100644 index 000000000..729eb422f --- /dev/null +++ b/API/models/rbac_test.go @@ -0,0 +1,176 @@ +package models + +import ( + u "p3/utils" + "testing" + + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestGetRequestFilterByDomainRootRoles(t *testing.T) { + roles := map[string]Role{ + "*": Manager, + } + _, ok := GetRequestFilterByDomain(roles) + assert.True(t, ok) + + roles["*"] = User + _, ok = GetRequestFilterByDomain(roles) + assert.True(t, ok) +} + +func TestGetRequestFilterByDomain(t *testing.T) { + domain := "domain1" + subdomain := domain + ".subdomain1" + roles := map[string]Role{ + "*": Viewer, + domain: Manager, + subdomain: User, + } + filter, ok := GetRequestFilterByDomain(roles) + assert.True(t, ok) + regex := filter["domain"].(primitive.Regex) + // the pattern only has the manager domains + assert.Equal(t, domain, regex.Pattern) + + // we change subdomain to manager role + roles[subdomain] = Manager + filter, ok = GetRequestFilterByDomain(roles) + assert.True(t, ok) + regex = filter["domain"].(primitive.Regex) + condition := domain+"|"+subdomain == regex.Pattern + condition = condition || subdomain+"|"+domain == regex.Pattern + assert.True(t, condition) + + // only viewer roles + roles[subdomain] = Viewer + roles[domain] = Viewer + _, ok = GetRequestFilterByDomain(roles) + assert.False(t, ok) +} + +func TestCheckUserPermissionsDomain(t *testing.T) { + entity := u.DOMAIN + roles := map[string]Role{ + "*": Manager, + "domain2": Viewer, + "domain1": Viewer, + } + domain := "domain1.subdomain1" + + // root manager + permission := CheckUserPermissions(roles, entity, domain) + if permission != WRITE { + t.Error("Root manager should have write permission") + } + + // domain1 manager + roles["*"] = User + roles["domain1"] = Manager + permission = CheckUserPermissions(roles, entity, domain) + if permission != WRITE { + t.Error("Parent domain manager should have write permission") + } + + // domain1.subdomain1 manager + roles["domain1"] = User + roles[domain] = Manager + permission = CheckUserPermissions(roles, entity, domain) + if permission != WRITE { + t.Error("Domain manager should have write permission") + } + + // domain1.subdomain1 user + roles["domain1"] = User + roles[domain] = User + permission = CheckUserPermissions(roles, entity, domain) + if permission != NONE { + t.Error("User should not have permission") + } +} + +func TestCheckUserPermissions(t *testing.T) { + entity := u.ROOM + rootRoles := map[string]Role{ + "*": Manager, + } + domain := "domain1" + subdomain := domain + ".subdomain1" + childSubdomain := subdomain + ".child" + + // root manager + permission := CheckUserPermissions(rootRoles, entity, subdomain) + if permission != WRITE { + t.Error("Root manager should have write permission") + } + + // root user + rootRoles["*"] = User + permission = CheckUserPermissions(rootRoles, entity, subdomain) + if permission != WRITE { + t.Error("Root user should have write permission") + } + + // root viewer + rootRoles["*"] = Viewer + permission = CheckUserPermissions(rootRoles, entity, subdomain) + if permission != READ { + t.Error("Root viewer should have read permission") + } + + roles := map[string]Role{ + domain: Manager, + } + + // domain1 manager + permission = CheckUserPermissions(roles, entity, subdomain) + if permission != WRITE { + t.Error("Parent domain manager should have write permission") + } + + // domain1 viewer + roles[domain] = Viewer + permission = CheckUserPermissions(roles, entity, subdomain) + if permission != READ { + t.Error("Parent domain viewer should have read permission") + } + + // domain1.subdomain1 manager + delete(roles, domain) + roles[subdomain] = Manager + permission = CheckUserPermissions(roles, entity, subdomain) + if permission != WRITE { + t.Error("Domain manager should have write permission") + } + + // domain1.subdomain1 viewer + roles[subdomain] = Viewer + permission = CheckUserPermissions(roles, entity, subdomain) + if permission != READ { + t.Error("Domain viewer should should have read permission") + } + + // domain1.subdomain1.child manager + delete(roles, subdomain) + roles[childSubdomain] = Manager + permission = CheckUserPermissions(roles, entity, subdomain) + if permission != READONLYNAME { + t.Error("Child manager should should have read only name permission") + } + + // domain1.subdomain1.child viewer + delete(roles, subdomain) + roles[childSubdomain] = Viewer + permission = CheckUserPermissions(roles, entity, subdomain) + if permission != READONLYNAME { + t.Error("Child viewer should should have read only name permission") + } + + // no roles + delete(roles, childSubdomain) + permission = CheckUserPermissions(roles, entity, subdomain) + if permission != NONE { + t.Error("User with no roles should not have any permission") + } +} diff --git a/API/models/validateEntity.go b/API/models/validateEntity.go index d2944f036..88f0dbd49 100644 --- a/API/models/validateEntity.go +++ b/API/models/validateEntity.go @@ -386,7 +386,7 @@ func ValidateEntity(entity int, t map[string]interface{}) *u.Error { for _, entStr := range entities { if entStr != u.EntityToString(u.GROUP) { // Get objects - entData, err := GetManyObjects(entStr, bson.M{"id": t["id"]}, u.RequestFilters{}, nil, nil) + entData, err := GetManyObjects(entStr, bson.M{"id": t["id"]}, u.RequestFilters{}, "", nil) if err != nil { err.Message = "Error while check id unicity at " + entStr + ":" + err.Message return err @@ -403,7 +403,7 @@ func ValidateEntity(entity int, t map[string]interface{}) *u.Error { idPattern := primitive.Regex{Pattern: "^" + t["parentId"].(string) + "(." + u.NAME_REGEX + "){1}$", Options: ""} // find siblings if siblings, err := GetManyObjects(u.EntityToString(u.DEVICE), bson.M{"id": idPattern}, - u.RequestFilters{}, nil, nil); err != nil { + u.RequestFilters{}, "", nil); err != nil { return err } else { for _, obj := range siblings { diff --git a/API/models/validateEntity_test.go b/API/models/validateEntity_test.go index 8c6bd5e5d..a8688d6dc 100644 --- a/API/models/validateEntity_test.go +++ b/API/models/validateEntity_test.go @@ -141,6 +141,38 @@ func TestErrorValidateJsonSchema(t *testing.T) { } } +func TestSlotStrToSliceError(t *testing.T) { + attributes := map[string]any{ + "slot": "", + } + + _, err := slotStrToSlice(attributes) + if err == nil { + t.Error("Slot with lenght less than 3 should return error") + } + + attributes["slot"] = "slot]" + _, err = slotStrToSlice(attributes) + if err == nil { + t.Error("Slot should start with [") + } + + attributes["slot"] = "[slot" + _, err = slotStrToSlice(attributes) + if err == nil { + t.Error("Slot should end with ]") + } + + attributes["slot"] = "[1,2]" + slot, err := slotStrToSlice(attributes) + if err != nil { + t.Error("There should be no error") + } + if len(slot) != 2 { + t.Error("There should be 2 elements in the list") + } +} + // helper functions func contains(slice []string, elem string) bool { for _, e := range slice { diff --git a/API/test/e2e/e2e.go b/API/test/e2e/e2e.go index 9704ab771..284e2e674 100644 --- a/API/test/e2e/e2e.go +++ b/API/test/e2e/e2e.go @@ -10,48 +10,69 @@ import ( "p3/models" "p3/router" _ "p3/test/integration" + "testing" "github.com/elliotchance/pie/v2" "github.com/gorilla/mux" - "go.mongodb.org/mongo-driver/bson/primitive" + "github.com/stretchr/testify/assert" ) var appRouter *mux.Router -var AdminId primitive.ObjectID -var AdminToken string +var users map[string]any func init() { appRouter = router.Router(app.JwtAuthentication) - createAdminAccount() + users = map[string]any{} + createUser("admin", map[string]models.Role{"*": "manager"}) + createUser("user", map[string]models.Role{"*": "user"}) + createUser("viewer", map[string]models.Role{"*": "viewer"}) } -func createAdminAccount() { - // Create admin account - admin := models.Account{} - admin.Email = "admin@admin.com" - admin.Password = "admin123" - admin.Roles = map[string]models.Role{"*": "manager"} +func createUser(userType string, role map[string]models.Role) { + user := models.Account{} + user.Email = userType + "@" + userType + ".com" + user.Password = userType + "123" + user.Roles = role - newAcc, err := admin.Create(map[string]models.Role{"*": "manager"}) + newAcc, err := user.Create(map[string]models.Role{"*": "manager"}) if err != nil { - log.Fatalln("Error while creating admin account:", err.Error()) + log.Fatalln("Error while creating "+userType+"account:", err.Error()) } if newAcc != nil { - AdminId = newAcc.ID - AdminToken = newAcc.Token + users[userType] = map[string]any{ + "id": newAcc.ID, + "token": newAcc.Token, + } } } -func MakeRequest(method, url string, requestBody []byte) *httptest.ResponseRecorder { +func MakeRequestWithHeaders(method, url string, requestBody []byte, header map[string]string) *httptest.ResponseRecorder { recorder := httptest.NewRecorder() request, _ := http.NewRequest(method, url, bytes.NewBuffer(requestBody)) - request.Header.Set("Authorization", "Bearer "+AdminToken) + for key, value := range header { + request.Header.Set(key, value) + } appRouter.ServeHTTP(recorder, request) - return recorder } +func MakeRequestWithToken(method, url string, requestBody []byte, token string) *httptest.ResponseRecorder { + header := map[string]string{ + "Authorization": "Bearer " + token, + } + return MakeRequestWithHeaders(method, url, requestBody, header) +} + +func MakeRequestWithUser(method, url string, requestBody []byte, user string) *httptest.ResponseRecorder { + token := users[user].(map[string]any)["token"].(string) + return MakeRequestWithToken(method, url, requestBody, token) +} + +func MakeRequest(method, url string, requestBody []byte) *httptest.ResponseRecorder { + return MakeRequestWithUser(method, url, requestBody, "admin") +} + func GetObjects(queryParams string) (*httptest.ResponseRecorder, []map[string]any) { response := MakeRequest(http.MethodGet, router.GenericObjectsURL+"?"+queryParams, nil) @@ -66,3 +87,16 @@ func GetObjects(queryParams string) (*httptest.ResponseRecorder, []map[string]an return response, objects } + +func TestInvalidBody(t *testing.T, httpMethod string, endpoint string, errorMessage string) { + invalidBody := []byte(`{`) + + recorder := MakeRequest(httpMethod, endpoint, invalidBody) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + + var response map[string]interface{} + json.Unmarshal(recorder.Body.Bytes(), &response) + message, exists := response["message"].(string) + assert.True(t, exists) + assert.Equal(t, errorMessage, message) +} diff --git a/API/utils/util.go b/API/utils/util.go index 529cacf39..2c774d988 100644 --- a/API/utils/util.go +++ b/API/utils/util.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "encoding/json" + "flag" "log" "net/http" "net/url" @@ -160,6 +161,9 @@ func RespondWithError(w http.ResponseWriter, err *Error) { } func ErrLog(message, funcname, details string, r *http.Request) { + if flag.Lookup("test.v") != nil { + return + } f, err := os.OpenFile("resources/debug.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { diff --git a/API/utils/util_test.go b/API/utils/util_test.go index dd6d672f1..bc4f8360f 100644 --- a/API/utils/util_test.go +++ b/API/utils/util_test.go @@ -1,7 +1,11 @@ package utils import ( + "encoding/json" + "net/http" "testing" + + "github.com/stretchr/testify/assert" ) func TestMessageToReturnMSI(t *testing.T) { @@ -101,4 +105,107 @@ func TestEntityToStringToReturnTrue(t *testing.T) { if ent != "device" { t.Error("Test Case 6 failed") } + + //Test Case 7 + testString = -1 + ent = EntityToString(testString) + if ent != "INVALID" { + t.Error("Test Case 7 failed") + } +} + +func TestErrTypeToStatusCodeToReturnTrue(t *testing.T) { + //Test Case 1 + testString := ErrForbidden + ent := ErrTypeToStatusCode(testString) + if ent != http.StatusForbidden { + t.Error("Test Case 1 failed") + } + + //Test Case 2 + testString = ErrUnauthorized + ent = ErrTypeToStatusCode(testString) + if ent != http.StatusUnauthorized { + t.Error("Test Case 2 failed") + } + + //Test Case 3 + testString = ErrNotFound + ent = ErrTypeToStatusCode(testString) + if ent != http.StatusNotFound { + t.Error("Test Case 3 failed") + } + + //Test Case 4 + testString = ErrDuplicate + ent = ErrTypeToStatusCode(testString) + if ent != http.StatusBadRequest { + t.Error("Test Case 4 failed") + } + + //Test Case 5 + testString = ErrBadFormat + ent = ErrTypeToStatusCode(testString) + if ent != http.StatusBadRequest { + t.Error("Test Case 5 failed") + } + + //Test Case 6 + testString = ErrDBError + ent = ErrTypeToStatusCode(testString) + if ent != http.StatusInternalServerError { + t.Error("Test Case 6 failed") + } + + //Test Case 7 + testString = ErrInternal + ent = ErrTypeToStatusCode(testString) + if ent != http.StatusInternalServerError { + t.Error("Test Case 7 failed") + } + + //Test Case 8 + testString = -1 + ent = ErrTypeToStatusCode(testString) + if ent != http.StatusInternalServerError { + t.Error("Test Case 8 failed") + } +} + +func TestStrSliceContains(t *testing.T) { + assert.True(t, StrSliceContains([]string{"hello", "world"}, "hello")) + assert.False(t, StrSliceContains([]string{"hello", "world"}, "bye")) + assert.False(t, StrSliceContains([]string{}, "bye")) +} + +func TestFormatNotifyData(t *testing.T) { + //Test Case 1 + message := FormatNotifyData("create", "room", nil) + var messageJson map[string]any + json.Unmarshal([]byte(message), &messageJson) + if messageJson["type"] != "create" || messageJson["data"] != nil { + t.Error("Test Case 1 failed") + } + + //Test Case 2 + message = FormatNotifyData("create", "tag", nil) + json.Unmarshal([]byte(message), &messageJson) + if messageJson["type"].(string) != "create-tag" || messageJson["data"] != nil { + t.Error("Test Case 2 failed") + } + + //Test Case 3 + message = FormatNotifyData("create", "layer", nil) + json.Unmarshal([]byte(message), &messageJson) + if messageJson["type"].(string) != "create-layer" || messageJson["data"] != nil { + t.Error("Test Case 3 failed") + } + + //Test Case 4 + message = FormatNotifyData("create", "room", map[string]string{"extra": "data"}) + json.Unmarshal([]byte(message), &messageJson) + data, exists := messageJson["data"].(map[string]interface{}) + if messageJson["type"].(string) != "create" || !exists || data["extra"].(string) != "data" { + t.Error("Test Case 4 failed") + } } diff --git a/API/utils/wildcard.go b/API/utils/wildcard.go index 1487bd7c3..b052150b5 100644 --- a/API/utils/wildcard.go +++ b/API/utils/wildcard.go @@ -22,7 +22,9 @@ func ApplyWildcardsOnComplexFilter(filter map[string]interface{}) { switch v := val.(type) { case string: if key == "$not" || !strings.HasPrefix(key, "$") { // only for '=' and '!=' operators - filter[key] = regexToMongoFilter(applyWildcards(v)) + if strings.Contains(v, "*") { + filter[key] = regexToMongoFilter(applyWildcards(v)) + } } case []interface{}: for _, item := range v { @@ -30,6 +32,10 @@ func ApplyWildcardsOnComplexFilter(filter map[string]interface{}) { ApplyWildcardsOnComplexFilter(m) } } + case []map[string]any: + for _, item := range v { + ApplyWildcardsOnComplexFilter(item) + } case map[string]interface{}: ApplyWildcardsOnComplexFilter(v) } diff --git a/APP/lib/l10n/app_en.arb b/APP/lib/l10n/app_en.arb index 04ac9e75b..a301473ab 100644 --- a/APP/lib/l10n/app_en.arb +++ b/APP/lib/l10n/app_en.arb @@ -179,10 +179,11 @@ "attributes": "Attributes:", "attribute": "Attribute", "applicability": "Applicable object", + "applicabilityTooltip": "The ID of the object where the layer should be applied\nSelect below if it should also apply to children.", "applyAlso": "Apply also to:", "directChildren": "Direct children", "allChildren": "All children", - "filtersTwo": "Filters:", + "filterLayerTooltip": "An expression with key, operator (=, !=, <, <=, >, >=) and value\nMultiple expressions possible if joined with & or |\nExamples:\ncategory=rack & name!=rack1\nheight>=40 | height<10", "filter": "Filter", "example": "Example:", "viewEditNode": "View and edit this node", diff --git a/APP/lib/l10n/app_es.arb b/APP/lib/l10n/app_es.arb index 33839e70f..ec4f5bac6 100644 --- a/APP/lib/l10n/app_es.arb +++ b/APP/lib/l10n/app_es.arb @@ -179,10 +179,11 @@ "attributes": "Atributos:", "attribute": "Atributo", "applicability": "Objetos donde aplicar", + "applicabilityTooltip": "El ID del objeto donde se aplica el layer.\nEligir abajo si se aplica también a sus hijos.", "applyAlso": "Aplicar también a:", "directChildren": "Hijos directos", "allChildren": "Todos los hijos", - "filtersTwo": "Filtros:", + "filterLayerTooltip": "Una expresión con clave, operador (=, !=, <, <=, >, >=) y valor\nEs posible escribir múltiples expresiones con & o |\nEjemplos:\ncategory=rack & name!=rack1\nheight>=40 | height<10", "filter": "Filtro", "example": "Ejemplo:", "viewEditNode": "Ver y editar este nodo", diff --git a/APP/lib/l10n/app_fr.arb b/APP/lib/l10n/app_fr.arb index 710593b68..b5df51fe8 100644 --- a/APP/lib/l10n/app_fr.arb +++ b/APP/lib/l10n/app_fr.arb @@ -217,10 +217,11 @@ "attributes": "Attributs :", "attribute": "Attribut", "applicability": "Objet applicable", + "applicabilityTooltip": "L'id de l'objet où appliquer ce layer.\nChoisissez en bas si le layer doit aussi être appliquer à ces enfants.", "applyAlso": "Applicabilité :", "directChildren": "Enfants directs", "allChildren": "Tous les enfants", - "filtersTwo": "Filtres :", + "filterLayerTooltip": "Une expression avec clé, opérateur (=, !=, <, <=, >, >=) et valeur\nPlusieurs expressions possibles si reliées par & ou |\nExemples :\ncategory=rack & name!=rack1\nheight>=40 | height<10", "filter": "Filtre", "example": "Exemple :", "viewEditNode": "Visualiser et modifier ce noeud", diff --git a/APP/lib/l10n/app_pt.arb b/APP/lib/l10n/app_pt.arb index 8efd9cb33..41cc11e00 100644 --- a/APP/lib/l10n/app_pt.arb +++ b/APP/lib/l10n/app_pt.arb @@ -179,10 +179,11 @@ "attributes": "Atributos:", "attribute": "Atributo", "applicability": "Objeto onde aplicar", + "applicabilityTooltip": "ID do objeto no qual aplicar o layer.\nSelecione abaixo se também deve ser aplicado a seus filhos.", "applyAlso": "Aplicar também a:", "directChildren": "Filhos diretos", "allChildren": "Todos os filhos", - "filtersTwo": "Filtros:", + "filterLayerTooltip": "Uma expressão com chave, operador (=, !=, <, <=, >, >=) e valor\nPossível escrever múltiplas expressões se ligadas por & ou |\nExemplos:\ncategory=rack & name!=rack1\nheight>=40 | height<10", "filter": "Filtro", "example": "Exemplo:", "viewEditNode": "Visualizar e editar este nó", diff --git a/APP/lib/widgets/select_objects/object_popup.dart b/APP/lib/widgets/select_objects/object_popup.dart index 644f8cd54..29d9514fb 100644 --- a/APP/lib/widgets/select_objects/object_popup.dart +++ b/APP/lib/widgets/select_objects/object_popup.dart @@ -478,14 +478,6 @@ class _ObjectPopupState extends State { // layers _objCategory = LogCategories.layer.name; objData = value; - objDataAttrs = Map.from(objData["filters"]); - for (var attr in objDataAttrs.entries) { - // add filters - customAttributesRows.add(CustomAttrRow( - customAttributesRows.length, - givenAttrName: attr.key, - givenAttrValue: attr.value)); - } if (objData["applicability"].toString().endsWith(".**.*")) { objData["applicability"] = objData["applicability"] .toString() @@ -730,7 +722,7 @@ class _ObjectPopupState extends State { : domainAutoFillField()) : Container(), CustomFormField( - save: (newValue) => objData["description"] = [newValue], + save: (newValue) => objData["description"] = newValue, label: localeMsg.description, icon: Icons.edit, shouldValidate: false, @@ -816,6 +808,20 @@ class _ObjectPopupState extends State { getLayerForm() { final localeMsg = AppLocalizations.of(context)!; + checkBoxWrapper(Checkbox checkbox, String text) => Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + SizedBox(height: 24, width: 24, child: checkbox), + const SizedBox(width: 3), + Text( + text, + style: const TextStyle( + fontSize: 14, + color: Colors.black, + ), + ), + ], + ); return ListView(padding: EdgeInsets.zero, children: [ CustomFormField( save: (newValue) => objData["slug"] = newValue, @@ -826,7 +832,9 @@ class _ObjectPopupState extends State { save: (newValue) => objData["applicability"] = newValue, label: localeMsg.applicability, icon: Icons.edit, + tipStr: localeMsg.applicabilityTooltip, initialValue: objData["applicability"]), + const SizedBox(height: 3), Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( localeMsg.applyAlso, @@ -835,75 +843,33 @@ class _ObjectPopupState extends State { color: Colors.black, ), ), - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - SizedBox( - height: 24, - width: 24, - child: Checkbox( - value: _applyDirectChild, - onChanged: _applyAllChild - ? null - : (bool? value) => - setState(() => _applyDirectChild = value!), - ), - ), - const SizedBox(width: 3), - Text( - localeMsg.directChildren, - style: const TextStyle( - fontSize: 14, - color: Colors.black, - ), - ), - ], + checkBoxWrapper( + Checkbox( + value: _applyDirectChild, + onChanged: _applyAllChild + ? null + : (bool? value) => setState(() => _applyDirectChild = value!), + ), + localeMsg.directChildren, ), - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - SizedBox( - height: 24, - width: 24, - child: Checkbox( - value: _applyAllChild, - onChanged: (bool? value) => setState(() { - _applyAllChild = value!; - _applyDirectChild = value!; - }), - ), - ), - const SizedBox(width: 3), - Text( - localeMsg.allChildren, - style: const TextStyle( - fontSize: 14, - color: Colors.black, - ), - ), - ], + checkBoxWrapper( + Checkbox( + value: _applyAllChild, + onChanged: (bool? value) => setState(() { + _applyAllChild = value!; + _applyDirectChild = value; + }), + ), + localeMsg.allChildren, ), ]), - Padding( - padding: const EdgeInsets.only(top: 10.0, left: 6, bottom: 6), - child: Text(localeMsg.filtersTwo)), - Padding( - padding: const EdgeInsets.only(left: 4), - child: Column(children: customAttributesRows), - ), - Padding( - padding: const EdgeInsets.only(left: 6), - child: Align( - alignment: Alignment.bottomLeft, - child: TextButton.icon( - onPressed: () => setState(() { - customAttributesRows - .add(CustomAttrRow(customAttributesRows.length)); - }), - icon: const Icon(Icons.add), - label: Text(localeMsg.filter)), - ), - ), + const SizedBox(height: 10), + CustomFormField( + save: (newValue) => objData["filter"] = newValue, + label: localeMsg.filter, + icon: Icons.filter_alt, + tipStr: localeMsg.filterLayerTooltip, + initialValue: objData["filter"]), ]); } @@ -1006,7 +972,6 @@ class _ObjectPopupState extends State { _formKey.currentState!.save(); if (_objCategory == LogCategories.layer.name) { - objData["filters"] = objDataAttrs; if (_applyAllChild) { objData["applicability"] = objData["applicability"] + ".**.*"; } else if (_applyDirectChild) { @@ -1029,7 +994,7 @@ class _ObjectPopupState extends State { showSnackBar(errorMessenger, exception.toString(), isError: true, copyTextTap: exception.toString(), - duration: Duration(seconds: 30)); + duration: const Duration(seconds: 30)); } } } @@ -1040,7 +1005,6 @@ class _ObjectPopupState extends State { _formKey.currentState!.save(); if (_objCategory == LogCategories.layer.name) { - objData["filters"] = objDataAttrs; if (_applyAllChild) { objData["applicability"] = objData["applicability"] + ".**.*"; } else if (_applyDirectChild) { diff --git a/CLI/ast.go b/CLI/ast.go index 3e6826d59..0310c979c 100644 --- a/CLI/ast.go +++ b/CLI/ast.go @@ -154,7 +154,7 @@ func (n *focusNode) execute() (interface{}, error) { if err != nil { return nil, err } - return nil, cmd.FocusUI(path) + return nil, cmd.C.FocusUI(path) } type cdNode struct { @@ -247,7 +247,7 @@ func (n *getUNode) execute() (interface{}, error) { return nil, fmt.Errorf("The U value must be positive") } - return nil, cmd.GetByAttr(path, u) + return nil, cmd.C.GetByAttr(path, u) } type getSlotNode struct { @@ -265,7 +265,7 @@ func (n *getSlotNode) execute() (interface{}, error) { return nil, err } - return nil, cmd.GetByAttr(path, slot) + return nil, cmd.C.GetByAttr(path, slot) } type loadNode struct { @@ -367,7 +367,7 @@ func (n *isEntityDrawableNode) execute() (interface{}, error) { if err != nil { return nil, err } - drawable, err := cmd.IsEntityDrawable(path) + drawable, err := cmd.C.IsEntityDrawable(path) if err != nil { return nil, err } @@ -385,7 +385,7 @@ func (n *isAttrDrawableNode) execute() (interface{}, error) { if err != nil { return nil, err } - drawable, err := cmd.IsAttrDrawable(path, n.attr) + drawable, err := cmd.C.IsAttrDrawable(path, n.attr) if err != nil { return nil, err } @@ -970,9 +970,9 @@ func (n *unsetAttrNode) execute() (interface{}, error) { if err != nil { return nil, err } - return cmd.UnsetInObj(path, n.attr, idx) + return cmd.C.UnsetInObj(path, n.attr, idx) } - return nil, cmd.UnsetAttribute(path, n.attr) + return nil, cmd.C.UnsetAttribute(path, n.attr) } type setEnvNode struct { @@ -1295,7 +1295,7 @@ func (n *createTagNode) execute() (interface{}, error) { return nil, err } - return nil, cmd.CreateTag(slug, color) + return nil, cmd.C.CreateTag(slug, color) } type createLayerNode struct { @@ -1408,7 +1408,7 @@ func (n *createUserNode) execute() (interface{}, error) { return nil, err } - return nil, cmd.CreateUser(email, role, domain) + return nil, cmd.C.CreateUser(email, role, domain) } type addRoleNode struct { @@ -1431,7 +1431,7 @@ func (n *addRoleNode) execute() (interface{}, error) { return nil, err } - return nil, cmd.AddRole(email, role, domain) + return nil, cmd.C.AddRole(email, role, domain) } type changePasswordNode struct{} @@ -1460,7 +1460,7 @@ type uiDelayNode struct { } func (n *uiDelayNode) execute() (interface{}, error) { - return nil, cmd.UIDelay(n.time) + return nil, cmd.C.UIDelay(n.time) } type uiToggleNode struct { @@ -1469,7 +1469,7 @@ type uiToggleNode struct { } func (n *uiToggleNode) execute() (interface{}, error) { - return nil, cmd.UIToggle(n.feature, n.enable) + return nil, cmd.C.UIToggle(n.feature, n.enable) } type uiHighlightNode struct { @@ -1481,14 +1481,14 @@ func (n *uiHighlightNode) execute() (interface{}, error) { if err != nil { return nil, err } - return nil, cmd.UIHighlight(path) + return nil, cmd.C.UIHighlight(path) } type uiClearCacheNode struct { } func (n *uiClearCacheNode) execute() (interface{}, error) { - return nil, cmd.UIClearCache() + return nil, cmd.C.UIClearCache() } type cameraMoveNode struct { @@ -1507,7 +1507,7 @@ func (n *cameraMoveNode) execute() (interface{}, error) { return nil, err } - return nil, cmd.CameraMove(n.command, position, rotation) + return nil, cmd.C.CameraMove(n.command, position, rotation) } type cameraWaitNode struct { @@ -1515,7 +1515,7 @@ type cameraWaitNode struct { } func (n *cameraWaitNode) execute() (interface{}, error) { - return nil, cmd.CameraWait(n.time) + return nil, cmd.C.CameraWait(n.time) } type linkObjectNode struct { @@ -1557,7 +1557,7 @@ func (n *linkObjectNode) execute() (interface{}, error) { } } - return nil, cmd.LinkObject(source, dest, n.attrs, values, slots) + return nil, cmd.C.LinkObject(source, dest, n.attrs, values, slots) } type unlinkObjectNode struct { @@ -1569,7 +1569,7 @@ func (n *unlinkObjectNode) execute() (interface{}, error) { if err != nil { return nil, err } - return nil, cmd.UnlinkObject(source) + return nil, cmd.C.UnlinkObject(source) } type symbolReferenceNode struct { diff --git a/CLI/ast_test.go b/CLI/ast_test.go new file mode 100644 index 000000000..42669c153 --- /dev/null +++ b/CLI/ast_test.go @@ -0,0 +1,1005 @@ +package main + +import ( + "cli/controllers" + mocks "cli/mocks/controllers" + "cli/models" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "golang.org/x/exp/maps" +) + +func setMainEnvironmentMock(t *testing.T) (*mocks.APIPort, *mocks.Ogree3DPort, *mocks.ClockPort, func()) { + oldDynamicSymbolTable := controllers.State.DynamicSymbolTable + oldFuncTable := controllers.State.FuncTable + oldClipboard := controllers.State.ClipBoard + oldPrevPath := controllers.State.PrevPath + oldCurrPath := controllers.State.CurrPath + oldDrawableObjs := controllers.State.DrawableObjs + controllers.State.DynamicSymbolTable = map[string]any{} + controllers.State.FuncTable = map[string]any{} + controllers.State.ClipBoard = []string{} + controllers.State.DrawableObjs = []int{} + + mockAPI := mocks.NewAPIPort(t) + mockOgree3D := mocks.NewOgree3DPort(t) + mockClock := mocks.NewClockPort(t) + controller := controllers.Controller{ + API: mockAPI, + Ogree3D: mockOgree3D, + Clock: mockClock, + } + oldControllerValue := controllers.C + controllers.C = controller + oldHierarchy := controllers.State.Hierarchy + controllers.State.Hierarchy = controllers.BuildBaseTree(controller) + + deferFunction := func() { + controllers.State.DynamicSymbolTable = oldDynamicSymbolTable + controllers.State.FuncTable = oldFuncTable + controllers.C = oldControllerValue + controllers.State.Hierarchy = oldHierarchy + controllers.State.ClipBoard = oldClipboard + controllers.State.DrawableObjs = oldDrawableObjs + controllers.State.PrevPath = oldPrevPath + controllers.State.CurrPath = oldCurrPath + } + return mockAPI, mockOgree3D, mockClock, deferFunction +} + +func TestValueNodeExecute(t *testing.T) { + valNode := valueNode{5} + value, err := valNode.execute() + + assert.Nil(t, err) + assert.Equal(t, 5, value) +} + +func TestAstExecute(t *testing.T) { + _, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + commands := ast{ + statements: []node{ + &assignNode{"i", &valueNode{5}}, + &assignNode{"j", &valueNode{10}}, + }, + } + value, err := commands.execute() + + assert.Nil(t, err) + assert.Nil(t, value) + + assert.Contains(t, controllers.State.DynamicSymbolTable, "i") + assert.Contains(t, controllers.State.DynamicSymbolTable, "j") + assert.Equal(t, 5, controllers.State.DynamicSymbolTable["i"]) + assert.Equal(t, 10, controllers.State.DynamicSymbolTable["j"]) +} + +func TestFuncDefNodeExecute(t *testing.T) { + _, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + // alias my_function { print $i } + functionBody := printNode{&formatStringNode{&valueNode{"%v"}, []node{&symbolReferenceNode{"i"}}}} + funcNode := funcDefNode{ + name: "my_function", + body: &functionBody, + } + value, err := funcNode.execute() + + assert.Nil(t, err) + assert.Nil(t, value) + + assert.Contains(t, controllers.State.FuncTable, "my_function") + assert.Equal(t, &functionBody, controllers.State.FuncTable["my_function"]) +} + +func TestFuncCallNodeExecute(t *testing.T) { + _, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + // we define the function + // alias my_function { .var: i = 5 } + functionName := "my_function" + functionBody := assignNode{"i", &valueNode{5}} + funcNode := funcDefNode{ + name: functionName, + body: &functionBody, + } + value, err := funcNode.execute() + + assert.Nil(t, err) + assert.Nil(t, value) + + callNode := funcCallNode{functionName} + value, err = callNode.execute() + assert.Nil(t, err) + assert.Nil(t, value) + + assert.Contains(t, controllers.State.DynamicSymbolTable, "i") + assert.Equal(t, 5, controllers.State.DynamicSymbolTable["i"]) +} + +func TestFuncCallNodeExecuteUndefinedFunction(t *testing.T) { + _, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + functionName := "my_function" + callNode := funcCallNode{functionName} + value, err := callNode.execute() + + assert.Nil(t, value) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "undefined function "+functionName) +} + +func TestArrNodeExecute(t *testing.T) { + array := arrNode{[]node{&valueNode{5}, &valueNode{6}}} + value, err := array.execute() + + assert.Nil(t, err) + assert.Equal(t, []float64{5, 6}, value) // it only returns an array of floats +} + +func TestLenNodeExecute(t *testing.T) { + _, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + controllers.State.DynamicSymbolTable["myArray"] = []float64{1, 2, 3, 4} + array := lenNode{"myArray"} + value, err := array.execute() + + assert.Nil(t, err) + assert.Equal(t, 4, value) + + array = lenNode{"myArray2"} + _, err = array.execute() + + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Undefined variable myArray2") +} + +func TestCdNodeExecute(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": map[string]any{ + "category": "site", + "id": "site", + "name": "site", + "parentId": "", + }, + }, + }, nil, + ).Once() + + array := cdNode{&pathNode{path: &valueNode{"/Physical/site"}}} + value, err := array.execute() + + assert.Nil(t, err) + assert.Nil(t, value) +} + +func TestLsNodeExecute(t *testing.T) { + mockAPI, _, mockClock, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site/all?limit=1", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": map[string]any{ + "category": "site", + "id": "site", + "name": "site", + "parentId": "", + }, + }, + }, nil, + ).Once() + mockAPI.On( + "Request", "GET", + "/api/layers", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": map[string]any{ + "objects": []any{}, + }, + }, + }, nil, + ).Once() + mockClock.On("Now").Return(time.Now()).Once() + + ls := lsNode{ + path: &pathNode{path: &valueNode{"/Physical/site"}}, + } + value, err := ls.execute() + + assert.Nil(t, err) + assert.Nil(t, value) +} + +func TestGetUNodeExecute(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site.building.room.rack/all?limit=1", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": map[string]any{ + "category": "rack", + "children": []any{}, + "id": "site.building.room.rack", + "name": "rack", + "parentId": "site.building.room", + }, + }, + }, nil, + ).Once() + + uNode := getUNode{ + path: &pathNode{path: &valueNode{"/Physical/site/building/room/rack"}}, + u: &valueNode{-42}, + } + value, err := uNode.execute() + + assert.Nil(t, value) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "The U value must be positive") + + uNode = getUNode{ + path: &pathNode{path: &valueNode{"/Physical/site/building/room/rack"}}, + u: &valueNode{42}, + } + value, err = uNode.execute() + + assert.Nil(t, value) + assert.Nil(t, err) +} + +func TestGetSlotNodeExecute(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site.building.room.rack/all?limit=1", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": map[string]any{ + "category": "rack", + "children": []any{map[string]any{ + "category": "device", + "attributes": map[string]any{ + "type": "chassis", + "slot": "slot", + }, + "children": []any{}, + "id": "BASIC.A.R1.A01.chT", + "name": "chT", + "parentId": "BASIC.A.R1.A01", + }}, + "id": "site.building.room.rack", + "name": "rack", + "parentId": "site.building.room", + "attributes": map[string]any{ + "slot": []any{ + map[string]any{ + "location": "slot", + "type": "u", + "elemOrient": []any{33.3, -44.4, 107}, + "elemPos": []any{58, 51, 44.45}, + "elemSize": []any{482.6, 1138, 44.45}, + "mandatory": "no", + "labelPos": "frontrear", + "color": "@color1", + }, + }, + }, + }, + }, + }, nil, + ).Once() + + slotNode := getSlotNode{ + path: &pathNode{path: &valueNode{"/Physical/site/building/room/rack"}}, + slot: &valueNode{"slot"}, + } + value, err := slotNode.execute() + + assert.Nil(t, value) + assert.Nil(t, err) +} + +func TestPrintNodeExecute(t *testing.T) { + executable := printNode{&formatStringNode{&valueNode{"%v"}, []node{&valueNode{5}}}} + value, err := executable.execute() + + assert.Nil(t, value) + assert.Nil(t, err) +} + +func TestDeleteObjNodeExecute(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + mockAPI.On( + "Request", "DELETE", + "/api/objects?id=site.building.room.rack&namespace=physical.hierarchy", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": []any{ + map[string]any{ + "category": "rack", + "children": []any{}, + "id": "site.building.room.rack", + "name": "rack", + "parentId": "site.building.room", + }, + }, + }, + }, nil, + ).Once() + + executable := deleteObjNode{&pathNode{path: &valueNode{"/Physical/site/building/room/rack"}}} + value, err := executable.execute() + + assert.Nil(t, value) + assert.Nil(t, err) +} + +func TestDeleteSelectionNodeExecute(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + controllers.State.ClipBoard = []string{"/Physical/site/building/room/rack", "/Physical/site/building/room2/rack2"} + + mockAPI.On( + "Request", "DELETE", + "/api/objects?id=site.building.room.rack&namespace=physical.hierarchy", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": []any{ + map[string]any{ + "category": "rack", + "children": []any{}, + "id": "site.building.room.rack", + "name": "rack", + "parentId": "site.building.room", + }, + }, + }, + }, nil, + ).Once() + + mockAPI.On( + "Request", "DELETE", + "/api/objects?id=site.building.room2.rack2&namespace=physical.hierarchy", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": []any{ + map[string]any{ + "category": "rack", + "children": []any{}, + "id": "site.building.room2.rack2", + "name": "rack2", + "parentId": "site.building.room2", + }, + }, + }, + }, nil, + ).Once() + + executable := deleteSelectionNode{} + value, err := executable.execute() + + assert.Nil(t, value) + assert.Nil(t, err) +} + +func TestIsEntityDrawableNodeExecute(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + rack := map[string]any{ + "category": "rack", + "children": []any{}, + "id": "site.building.room.rack", + "name": "rack", + "parentId": "site.building.room", + } + + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site.building.room.rack", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": rack, + }, + }, nil, + ).Once() + + executable := isEntityDrawableNode{&pathNode{path: &valueNode{"/Physical/site/building/room/rack"}}} + value, err := executable.execute() + + assert.False(t, value.(bool)) + assert.Nil(t, err) + + // We add the Rack to the drawable objects list + controllers.State.DrawableObjs = []int{models.RACK} + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site.building.room.rack", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": rack, + }, + }, nil, + ).Once() + + value, err = executable.execute() + + assert.True(t, value.(bool)) + assert.Nil(t, err) +} + +func TestIsAttrDrawableNodeExecute(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + rack := map[string]any{ + "category": "rack", + "children": []any{}, + "id": "site.building.room.rack", + "name": "rack", + "parentId": "site.building.room", + } + + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site.building.room.rack", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": rack, + }, + }, nil, + ).Once() + + executable := isAttrDrawableNode{&pathNode{path: &valueNode{"/Physical/site/building/room/rack"}}, "sdsdasd"} + value, err := executable.execute() + + assert.Nil(t, err) + assert.True(t, value.(bool)) +} + +func TestGetObjectNodeExecute(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + rack := map[string]any{ + "category": "rack", + "children": []any{}, + "id": "site.building.room.rack", + "name": "rack", + "parentId": "site.building.room", + } + + mockAPI.On( + "Request", "POST", + "/api/objects/search?id=%2A%2A.site.building.room&namespace=physical.hierarchy", + map[string]interface{}{"filter": "(category=rack) & (name=rack)"}, 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": []any{rack}, + }, + }, nil, + ).Once() + + executable := getObjectNode{ + path: &pathNode{path: &valueNode{"/Physical/site/building/room"}}, + filters: map[string]node{"filter": &valueNode{"(category=rack) & (name=rack)"}}, + recursive: recursiveArgs{isRecursive: true}, + } + value, err := executable.execute() + + assert.Nil(t, err) + assert.Len(t, value, 1) + assert.Equal(t, rack["id"], value.([]map[string]any)[0]["id"]) +} + +func TestSelectObjectNodeExecuteOnePath(t *testing.T) { + mockAPI, mockOgree3D, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + rack := map[string]any{ + "category": "rack", + "children": []any{}, + "id": "site.building.room.rack", + "name": "rack", + "parentId": "site.building.room", + } + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site.building.room.rack", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": rack, + }, + }, nil, + ).Twice() + mockOgree3D.On( + "InformOptional", "SetClipBoard", + -1, map[string]interface{}{"data": "[\"site.building.room.rack\"]", "type": "select"}, + ).Return(nil) + + executable := selectObjectNode{&pathNode{path: &valueNode{"/Physical/site/building/room/rack"}}} + value, err := executable.execute() + + assert.Nil(t, err) + assert.Nil(t, value) + assert.Len(t, controllers.State.ClipBoard, 1) + assert.Equal(t, []string{"/Physical/site/building/room/rack"}, controllers.State.ClipBoard) +} + +func TestSelectObjectNodeExecuteReset(t *testing.T) { + _, mockOgree3D, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + controllers.State.ClipBoard = []string{"/Physical/site/building/room/rack"} + mockOgree3D.On( + "InformOptional", "SetClipBoard", + -1, map[string]interface{}{"data": "[]", "type": "select"}, + ).Return(nil) + + executable := selectObjectNode{&valueNode{""}} + value, err := executable.execute() + + assert.Nil(t, err) + assert.Nil(t, value) + assert.Len(t, controllers.State.ClipBoard, 0) +} + +func TestSetRoomAreas(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + room := map[string]any{ + "category": "room", + "children": []any{}, + "id": "site.building.room", + "name": "room", + "parentId": "site.building", + } + roomResponse := maps.Clone(room) + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site.building.room", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": room, + }, + }, nil, + ).Once() + + roomResponse["attributes"] = map[string]any{ + "reserved": "[1, 2, 3, 4]", + "technical": "[1, 2, 3, 4]", + } + mockAPI.On( + "Request", "PATCH", + "/api/hierarchy-objects/site.building.room", + map[string]interface{}{"attributes": map[string]interface{}{"reserved": "[1, 2, 3, 4]", "technical": "[1, 2, 3, 4]"}}, + 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": roomResponse, + }, + }, nil, + ).Once() + + reservedArea := []float64{1, 2, 3, 4} + technicalArea := []float64{1, 2, 3, 4} + value, err := setRoomAreas("/Physical/site/building/room", []any{reservedArea, technicalArea}) + + assert.Nil(t, err) + assert.NotNil(t, value) +} + +func TestSetLabel(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + room := map[string]any{ + "category": "rack", + "children": []any{}, + "id": "site.building.room.rack", + "name": "rack", + "parentId": "site.building.room", + } + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site.building.room.rack", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": room, + }, + }, nil, + ).Once() + value, err := setLabel("/Physical/site/building/room/rack", []any{"myLabel"}, false) + + assert.Nil(t, err) + assert.Nil(t, value) +} + +func TestAddToStringMap(t *testing.T) { + newMap, replaced := addToStringMap[int]("{\"a\":3}", "b", 10) + + assert.Equal(t, "{\"a\":3,\"b\":10}", newMap) + assert.False(t, replaced) + + newMap, replaced = addToStringMap[int](newMap, "b", 15) + assert.Equal(t, "{\"a\":3,\"b\":15}", newMap) + assert.True(t, replaced) +} + +func TestRemoveFromStringMap(t *testing.T) { + newMap, deleted := removeFromStringMap[int]("{\"a\":3,\"b\":10}", "b") + + assert.Equal(t, "{\"a\":3}", newMap) + assert.True(t, deleted) + + newMap, deleted = removeFromStringMap[int](newMap, "b") + assert.Equal(t, "{\"a\":3}", newMap) + assert.False(t, deleted) +} + +func TestAddRoomSeparatorError(t *testing.T) { + obj, err := addRoomSeparator("/Physical/site/building/room", []any{"mySeparator"}) + + assert.Nil(t, obj) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "4 values (name, startPos, endPos, type) expected to add a separator") +} + +func TestAddRoomSeparator(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + room := map[string]any{ + "category": "room", + "children": []any{}, + "id": "site.building.room", + "name": "room", + "parentId": "site.building", + "attributes": map[string]any{}, + } + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site.building.room", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": room, + }, + }, nil, + ).Twice() + + newAttributes := map[string]interface{}{ + "separators": "{\"mySeparator\":{\"startPosXYm\":[1,2],\"endPosXYm\":[1,2],\"type\":\"wireframe\"}}", + } + room["attributes"] = newAttributes + mockAPI.On( + "Request", "PATCH", + "/api/hierarchy-objects/site.building.room", + map[string]interface{}{ + "attributes": newAttributes, + }, + 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": room, + }, + }, nil, + ).Once() + + obj, err := addRoomSeparator("/Physical/site/building/room", []any{"mySeparator", []float64{1., 2.}, []float64{1., 2.}, "wireframe"}) + + assert.Nil(t, err) + assert.NotNil(t, obj) + +} + +func TestAddRoomPillarError(t *testing.T) { + obj, err := addRoomPillar("/Physical/site/building/room", []any{"myPillar"}) + + assert.Nil(t, obj) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "4 values (name, centerXY, sizeXY, rotation) expected to add a pillar") +} + +func TestAddRoomPillar(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + room := map[string]any{ + "category": "room", + "children": []any{}, + "id": "site.building.room", + "name": "room", + "parentId": "site.building", + "attributes": map[string]any{}, + } + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site.building.room", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": room, + }, + }, nil, + ).Twice() + + newAttributes := map[string]interface{}{ + "pillars": "{\"myPillar\":{\"centerXY\":[1,2],\"sizeXY\":[1,2],\"rotation\":\"2.5\"}}", + } + room["attributes"] = newAttributes + mockAPI.On( + "Request", "PATCH", + "/api/hierarchy-objects/site.building.room", + map[string]interface{}{ + "attributes": newAttributes, + }, + 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": room, + }, + }, nil, + ).Once() + + obj, err := addRoomPillar("/Physical/site/building/room", []any{"myPillar", []float64{1., 2.}, []float64{1., 2.}, 2.5}) + + assert.Nil(t, err) + assert.NotNil(t, obj) +} + +func TestDeleteRoomPillarOrSeparatorInvalidArgument(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + room := map[string]any{ + "category": "room", + "children": []any{}, + "id": "site.building.room", + "name": "room", + "parentId": "site.building", + "attributes": map[string]any{}, + } + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site.building.room", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": room, + }, + }, nil, + ).Once() + obj, err := deleteRoomPillarOrSeparator("/Physical/site/building/room", "other", "separator") + + assert.Nil(t, obj) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "\"separator\" or \"pillar\" expected") +} + +func TestDeleteRoomPillarOrSeparatorSeparatorDoesNotExist(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + room := map[string]any{ + "category": "room", + "children": []any{}, + "id": "site.building.room", + "name": "room", + "parentId": "site.building", + "attributes": map[string]any{}, + } + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site.building.room", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": room, + }, + }, nil, + ).Once() + obj, err := deleteRoomPillarOrSeparator("/Physical/site/building/room", "separator", "mySeparator") + + assert.Nil(t, obj) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "separator mySeparator does not exist") +} + +func TestDeleteRoomPillarOrSeparatorSeparator(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + room := map[string]any{ + "category": "room", + "children": []any{}, + "id": "site.building.room", + "name": "room", + "parentId": "site.building", + "attributes": map[string]any{ + "separators": "{\"mySeparator\":{\"startPosXYm\":[1,2],\"endPosXYm\":[1,2],\"type\":\"wireframe\"}}", + }, + } + updatedRoom := maps.Clone(room) + updatedRoom["attributes"] = map[string]any{"separators": "{}"} + + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site.building.room", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": room, + }, + }, nil, + ).Twice() + mockAPI.On( + "Request", "PATCH", + "/api/hierarchy-objects/site.building.room", + map[string]interface{}{"attributes": map[string]interface{}{"separators": "{}"}}, + 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": updatedRoom, + }, + }, nil, + ).Once() + obj, err := deleteRoomPillarOrSeparator("/Physical/site/building/room", "separator", "mySeparator") + + assert.Nil(t, err) + assert.NotNil(t, obj) +} + +func TestDeleteRoomPillarOrSeparatorPillar(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + room := map[string]any{ + "category": "room", + "children": []any{}, + "id": "site.building.room", + "name": "room", + "parentId": "site.building", + "attributes": map[string]any{ + "pillars": "{\"myPillar\":{\"centerXY\":[1,2],\"sizeXY\":[1,2],\"rotation\":\"2.5\"}}", + }, + } + updatedRoom := maps.Clone(room) + updatedRoom["attributes"] = map[string]any{"pillars": "{}"} + + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site.building.room", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": room, + }, + }, nil, + ).Twice() + mockAPI.On( + "Request", "PATCH", + "/api/hierarchy-objects/site.building.room", + map[string]interface{}{"attributes": map[string]interface{}{"pillars": "{}"}}, + 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": updatedRoom, + }, + }, nil, + ).Once() + obj, err := deleteRoomPillarOrSeparator("/Physical/site/building/room", "pillar", "myPillar") + + assert.Nil(t, err) + assert.NotNil(t, obj) +} + +func TestUpdateObjNodeExecuteUpdateDescription(t *testing.T) { + mockAPI, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + room := map[string]any{ + "category": "room", + "children": []any{}, + "id": "site.building.room", + "name": "room", + "parentId": "site.building", + "description": "description 1", + } + + mockAPI.On( + "Request", "GET", + "/api/hierarchy-objects/site.building.room", + "mock.Anything", 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": room, + }, + }, nil, + ).Once() + room["description"] = "newDescription" + mockAPI.On( + "Request", "PATCH", + "/api/hierarchy-objects/site.building.room", + map[string]interface{}{"description": "newDescription"}, + 200, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": room, + }, + }, nil, + ).Once() + + array := updateObjNode{ + path: &pathNode{path: &valueNode{"/Physical/site/building/room"}}, + attr: "description", + values: []node{&valueNode{"newDescription"}}, + hasSharpe: false, + } + value, err := array.execute() + + assert.Nil(t, err) + assert.Nil(t, value) +} diff --git a/CLI/astbool_test.go b/CLI/astbool_test.go new file mode 100644 index 000000000..7f6046c45 --- /dev/null +++ b/CLI/astbool_test.go @@ -0,0 +1,94 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEqualityNodeExecute(t *testing.T) { + valNode := equalityNode{"==", &valueNode{5}, &valueNode{6}} + value, err := valNode.execute() + + assert.Nil(t, err) + assert.False(t, value.(bool)) + + valNode = equalityNode{"==", &valueNode{5}, &valueNode{5}} + value, err = valNode.execute() + + assert.Nil(t, err) + assert.True(t, value.(bool)) + + valNode = equalityNode{"!=", &valueNode{5}, &valueNode{6}} + value, err = valNode.execute() + + assert.Nil(t, err) + assert.True(t, value.(bool)) + + valNode = equalityNode{"=", &valueNode{5}, &valueNode{5}} + _, err = valNode.execute() + + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid equality node operator : =") +} + +func TestComparatorNodeExecute(t *testing.T) { + valNode := comparatorNode{"<", &valueNode{5}, &valueNode{5}} + value, err := valNode.execute() + assert.Nil(t, err) + assert.False(t, value.(bool)) + + valNode = comparatorNode{"<=", &valueNode{5}, &valueNode{5}} + value, err = valNode.execute() + assert.Nil(t, err) + assert.True(t, value.(bool)) + + valNode = comparatorNode{">", &valueNode{6}, &valueNode{5}} + value, err = valNode.execute() + assert.Nil(t, err) + assert.True(t, value.(bool)) + + valNode = comparatorNode{">=", &valueNode{6}, &valueNode{5}} + value, err = valNode.execute() + assert.Nil(t, err) + assert.True(t, value.(bool)) + + valNode = comparatorNode{">!", &valueNode{6}, &valueNode{5}} + _, err = valNode.execute() + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid comparison operator : >!") +} + +func TestLogicalNodeExecute(t *testing.T) { + valNode := logicalNode{"||", &valueNode{false}, &valueNode{true}} + value, err := valNode.execute() + assert.Nil(t, err) + assert.True(t, value.(bool)) + + valNode = logicalNode{"&&", &valueNode{false}, &valueNode{true}} + value, err = valNode.execute() + assert.Nil(t, err) + assert.False(t, value.(bool)) + + valNode = logicalNode{"&|", &valueNode{false}, &valueNode{true}} + _, err = valNode.execute() + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid logical operator : &|") +} + +func TestNegateBoolNodeExecute(t *testing.T) { + valNode := negateBoolNode{&valueNode{"true"}} + value, err := valNode.execute() + assert.Nil(t, err) + assert.False(t, value.(bool)) + + valNode = negateBoolNode{&valueNode{"false"}} + value, err = valNode.execute() + assert.Nil(t, err) + assert.True(t, value.(bool)) + + valNode = negateBoolNode{&valueNode{"3.5"}} + _, err = valNode.execute() + assert.NotNil(t, err) + assert.ErrorContains(t, err, "expression should be a boolean") +} diff --git a/CLI/astflow.go b/CLI/astflow.go index 239d5dc7b..dd72b3aa8 100644 --- a/CLI/astflow.go +++ b/CLI/astflow.go @@ -61,6 +61,7 @@ type forNode struct { body node } +// ToDo: this expression is not possible to obtain. Add it to parser func (n *forNode) execute() (interface{}, error) { _, err := n.init.execute() if err != nil { @@ -92,6 +93,7 @@ type forArrayNode struct { body node } +// ToDo: this expression is not possible to obtain. Add it to parser func (n *forArrayNode) execute() (interface{}, error) { val, err := n.arr.execute() if err != nil { diff --git a/CLI/astflow_test.go b/CLI/astflow_test.go new file mode 100644 index 000000000..64c0c258f --- /dev/null +++ b/CLI/astflow_test.go @@ -0,0 +1,151 @@ +package main + +import ( + "cli/controllers" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIfNodeExecute(t *testing.T) { + // we execute the if condition and returns nil + valNode := ifNode{&valueNode{true}, &valueNode{5}, &valueNode{3}} + value, err := valNode.execute() + assert.Nil(t, err) + assert.Nil(t, value) + + // we execute the if condition which is invalid, so it returns error + valNode = ifNode{&valueNode{true}, &negateBoolNode{&valueNode{"3.5"}}, &valueNode{3}} + _, err = valNode.execute() + assert.NotNil(t, err) + assert.ErrorContains(t, err, "expression should be a boolean") + + // we execute the else condition which is invalid, so it returns error + valNode = ifNode{&valueNode{false}, &valueNode{3}, &negateBoolNode{&valueNode{"3.5"}}} + _, err = valNode.execute() + assert.NotNil(t, err) + assert.ErrorContains(t, err, "expression should be a boolean") +} + +func TestWhileNodeExecute(t *testing.T) { + _, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + variable := &assignNode{"i", &valueNode{"0"}} + variable.execute() + // "while $i<6 {var: i = eval $i+1}" + valNode := whileNode{ + condition: &comparatorNode{"<", &symbolReferenceNode{"i"}, &valueNode{6}}, + body: &assignNode{"i", &arithNode{"+", &symbolReferenceNode{"i"}, &valueNode{1}}}, + } + value, err := valNode.execute() + assert.Nil(t, err) + assert.Nil(t, value) + assert.Contains(t, controllers.State.DynamicSymbolTable, "i") + assert.Equal(t, 6, controllers.State.DynamicSymbolTable["i"]) +} + +func TestForNodeExecute(t *testing.T) { + _, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + // "for i=0;i<10;i=i+1 { print $i }" + valNode := forNode{ + init: &assignNode{"i", &valueNode{"0"}}, + condition: &comparatorNode{"<", &symbolReferenceNode{"i"}, &valueNode{10}}, + incrementor: &assignNode{"i", &arithNode{"+", &symbolReferenceNode{"i"}, &valueNode{1}}}, + body: &printNode{&formatStringNode{&valueNode{"%v"}, []node{&symbolReferenceNode{"i"}}}}, + } + value, err := valNode.execute() + assert.Nil(t, err) + assert.Nil(t, value) + + assert.Contains(t, controllers.State.DynamicSymbolTable, "i") + assert.Equal(t, 10, controllers.State.DynamicSymbolTable["i"]) +} + +// ToDo: enable this test once the forArrayNode.execute is fixed +// func TestForArrayNodeExecute(t *testing.T) { +// oldValue := controllers.State.DynamicSymbolTable +// controllers.State.DynamicSymbolTable = map[string]any{} + +// // "for i in [1,3,5] { print $i }" +// array := arrNode{[]node{&valueNode{"a"}, &valueNode{"b"}, &valueNode{"c"}}} + +// valNode := forArrayNode{ +// variable: "i", +// arr: &array, +// body: &printNode{&formatStringNode{&valueNode{"%v"}, []node{&symbolReferenceNode{"i"}}}}, +// } +// value, err := valNode.execute() +// assert.Nil(t, err) +// assert.Nil(t, value) + +// assert.Contains(t, controllers.State.DynamicSymbolTable, "i") +// assert.Equal(t, "c", controllers.State.DynamicSymbolTable["i"]) +// controllers.State.DynamicSymbolTable = oldValue +// } + +func TestForArrayNodeExecuteError(t *testing.T) { + _, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + // "for i in 2 { print $i }" + valNode := forArrayNode{ + variable: "i", + arr: &valueNode{2}, + body: &printNode{&formatStringNode{&valueNode{"%v"}, []node{&symbolReferenceNode{"i"}}}}, + } + value, err := valNode.execute() + assert.Nil(t, value) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "only an array can be iterated") +} + +func TestForRangeNodeExecute(t *testing.T) { + _, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + // "for i in 0..3 { print $i }" + valNode := forRangeNode{ + variable: "i", + start: &valueNode{0}, + end: &valueNode{3}, + body: &printNode{&formatStringNode{&valueNode{"%v"}, []node{&symbolReferenceNode{"i"}}}}, + } + value, err := valNode.execute() + assert.Nil(t, err) + assert.Nil(t, value) + + assert.Contains(t, controllers.State.DynamicSymbolTable, "i") + assert.Equal(t, 3, controllers.State.DynamicSymbolTable["i"]) +} + +func TestForRangeNodeExecuteError(t *testing.T) { + _, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + // Start value higher than end value + // "for i in 3..0 { print $i }" + valNode := forRangeNode{ + variable: "i", + start: &valueNode{3}, + end: &valueNode{0}, + body: &printNode{&formatStringNode{&valueNode{"%v"}, []node{&symbolReferenceNode{"i"}}}}, + } + value, err := valNode.execute() + assert.Nil(t, value) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "start index should be lower than end index") + + // Body is nil + valNode = forRangeNode{ + variable: "i", + start: &valueNode{0}, + end: &valueNode{3}, + body: nil, + } + value, err = valNode.execute() + assert.Nil(t, value) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "loop body should not be empty") +} diff --git a/CLI/astnum_test.go b/CLI/astnum_test.go new file mode 100644 index 000000000..65e6d92f1 --- /dev/null +++ b/CLI/astnum_test.go @@ -0,0 +1,93 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestArithNodeExecute(t *testing.T) { + left := 10 + right := 5 + results := map[string]any{ + "+": 15, + "-": 5, + "*": 50, + } + for op, result := range results { + // apply operand with int values + valNode := arithNode{op, &valueNode{left}, &valueNode{right}} + value, err := valNode.execute() + assert.Nil(t, err) + assert.Equal(t, result, value) + + // apply operand with one float value + valNode = arithNode{op, &valueNode{float64(left)}, &valueNode{right}} + value, err = valNode.execute() + assert.Nil(t, err) + assert.Equal(t, float64(result.(int)), value) + } + + valNode := arithNode{"/", &valueNode{10}, &valueNode{4}} + value, err := valNode.execute() + assert.Nil(t, err) + assert.Equal(t, 2.5, value) + + valNode = arithNode{"/", &valueNode{10}, &valueNode{0}} + _, err = valNode.execute() + assert.NotNil(t, err) + assert.ErrorContains(t, err, "cannot divide by 0") + + valNode = arithNode{"/", &valueNode{10.0}, &valueNode{4}} + value, err = valNode.execute() + assert.Nil(t, err) + assert.Equal(t, 2.5, value) + + valNode = arithNode{"/", &valueNode{10.0}, &valueNode{0}} + _, err = valNode.execute() + assert.NotNil(t, err) + assert.ErrorContains(t, err, "cannot divide by 0") + + // integer division + valNode = arithNode{"\\", &valueNode{10}, &valueNode{4}} + value, err = valNode.execute() + assert.Nil(t, err) + assert.Equal(t, 2, value) + + valNode = arithNode{"\\", &valueNode{10}, &valueNode{0}} + _, err = valNode.execute() + assert.NotNil(t, err) + assert.ErrorContains(t, err, "cannot divide by 0") + + valNode = arithNode{"%", &valueNode{10}, &valueNode{4}} + value, err = valNode.execute() + assert.Nil(t, err) + assert.Equal(t, 2, value) + + valNode = arithNode{"%", &valueNode{10.0}, &valueNode{4}} + _, err = valNode.execute() + assert.NotNil(t, err) + assert.ErrorContains(t, err, "invalid operator for float operands") + + valNode = arithNode{"/%", &valueNode{10}, &valueNode{4}} + _, err = valNode.execute() + assert.NotNil(t, err) + assert.ErrorContains(t, err, "invalid operator for integer operands") +} + +func TestNegateNodeExecute(t *testing.T) { + valNode := negateNode{&valueNode{10}} + value, err := valNode.execute() + assert.Nil(t, err) + assert.Equal(t, -10, value) + + valNode = negateNode{&valueNode{10.0}} + value, err = valNode.execute() + assert.Nil(t, err) + assert.Equal(t, -10.0, value) + + valNode = negateNode{&valueNode{true}} + _, err = valNode.execute() + assert.NotNil(t, err) + assert.ErrorContains(t, err, "cannot negate non numeric value") +} diff --git a/CLI/aststr_test.go b/CLI/aststr_test.go new file mode 100644 index 000000000..e07579301 --- /dev/null +++ b/CLI/aststr_test.go @@ -0,0 +1,35 @@ +package main + +import ( + "cli/models" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPathNode(t *testing.T) { + path := models.PhysicalPath + "site/building/room/rack/../rack2/" + valNode := pathNode{path: &valueNode{path}} + value, err := valNode.Path() + assert.Nil(t, err) + assert.Equal(t, path, value) + + translatedPath, err := valNode.execute() + assert.Nil(t, err) + assert.Equal(t, models.PhysicalPath+"site/building/room/rack2", translatedPath) +} + +func TestFormatStringNodeExecute(t *testing.T) { + vals := []node{&valueNode{3}, &valueNode{4}, &valueNode{7}} + valNode := formatStringNode{&valueNode{"%d + %d = %d"}, vals} + value, err := valNode.execute() + assert.Nil(t, err) + assert.Equal(t, "3 + 4 = 7", value) + + vector := []float64{1, 2, 3} + vals = []node{&valueNode{vector}} + valNode = formatStringNode{&valueNode{"%v"}, vals} + value, err = valNode.execute() + assert.Nil(t, err) + assert.Equal(t, vector, value) +} diff --git a/CLI/astutil_test.go b/CLI/astutil_test.go new file mode 100644 index 000000000..075aaa38d --- /dev/null +++ b/CLI/astutil_test.go @@ -0,0 +1,235 @@ +package main + +import ( + "cli/models" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNodeToFloat(t *testing.T) { + valNode := valueNode{"3.5"} + value, err := nodeToFloat(&valNode, "") + + assert.Nil(t, err) + assert.Equal(t, 3.5, value) + + valNode = valueNode{"q3.5"} + _, err = nodeToFloat(&valNode, "invalidFloatNode") + + assert.NotNil(t, err) + assert.ErrorContains(t, err, "invalidFloatNode should be a number") +} + +func TestNodeToNum(t *testing.T) { + valNode := valueNode{"3.5"} + value, err := nodeToNum(&valNode, "") + + assert.Nil(t, err) + assert.Equal(t, 3.5, value) + + valNode = valueNode{"3"} + value, err = nodeToNum(&valNode, "") + + assert.Nil(t, err) + assert.Equal(t, 3, value) + + valNode = valueNode{"q3"} + _, err = nodeToNum(&valNode, "invalidNumberNode") + + assert.NotNil(t, err) + assert.ErrorContains(t, err, "invalidNumberNode should be a number") +} + +func TestNodeToInt(t *testing.T) { + valNode := valueNode{"3"} + value, err := nodeToInt(&valNode, "") + + assert.Nil(t, err) + assert.Equal(t, 3, value) + + valNode = valueNode{"3.5"} + _, err = nodeToInt(&valNode, "invalidIntNode") + + assert.NotNil(t, err) + assert.ErrorContains(t, err, "invalidIntNode should be an integer") +} + +func TestNodeToBool(t *testing.T) { + valNode := valueNode{"false"} + value, err := nodeToBool(&valNode, "") + + assert.Nil(t, err) + assert.False(t, value) + + valNode = valueNode{"3.5"} + _, err = nodeToBool(&valNode, "invalidBoolNode") + + assert.NotNil(t, err) + assert.ErrorContains(t, err, "invalidBoolNode should be a boolean") +} + +func TestNodeTo3dRotation(t *testing.T) { + valNode := valueNode{"front"} + value, err := nodeTo3dRotation(&valNode) + + assert.Nil(t, err) + assert.Equal(t, []float64{0, 0, 180}, value) + + valNode = valueNode{"3.5"} + _, err = nodeTo3dRotation(&valNode) + + assert.NotNil(t, err) + assert.ErrorContains(t, err, + `rotation should be a vector3, or one of the following keywords : + front, rear, left, right, top, bottom`) +} + +func TestNodeToString(t *testing.T) { + valNode := valueNode{3} + value, err := nodeToString(&valNode, "int") + + assert.Nil(t, err) + assert.Equal(t, "3", value) +} + +func TestNodeToVec(t *testing.T) { + valNode := valueNode{[]float64{1, 2, 3}} + value, err := nodeToVec(&valNode, -1, "vector") + + assert.Nil(t, err) + assert.Equal(t, []float64{1, 2, 3}, value) +} + +func TestNodeToColorString(t *testing.T) { + valNode := valueNode{"abcacc"} + value, err := nodeToColorString(&valNode) + + assert.Nil(t, err) + assert.Equal(t, "abcacc", value) + + valNode = valueNode{"3.5"} + _, err = nodeToColorString(&valNode) + + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Please provide a valid 6 digit Hex value for the color") +} + +func TestFileToJson(t *testing.T) { + basePath := t.TempDir() // temporary directory that will be deleted after the tests have finished + fileContent := "{\"value\": [3,4,5]}\n" + + filename := "file_to_json_test_file.json" + filePath := basePath + "/" + filename + err := os.WriteFile(filePath, []byte(fileContent), 0644) + + if err != nil { + t.Errorf("an error ocurred while creating the test file: %s", err) + } + + json := fileToJSON(filePath) + assert.Len(t, json, 1) + assert.Equal(t, []any{3.0, 4.0, 5.0}, json["value"]) + + json = fileToJSON(basePath + "invalidPath.json") + assert.Nil(t, json) +} + +func TestEvalNodeArr(t *testing.T) { + nodes := []node{&valueNode{"abcacc"}, &valueNode{"abcddd"}} + + values, err := evalNodeArr(&nodes, []string{}) + assert.Nil(t, err) + assert.Len(t, values, 2) + assert.Equal(t, []string{"abcacc", "abcddd"}, values) + + nodes = []node{&valueNode{"abcacc"}, &valueNode{3}} + + _, err = evalNodeArr(&nodes, []string{}) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Error unexpected element") +} + +func TestErrorResponder(t *testing.T) { + err := errorResponder("reserved", "4", false) + assert.ErrorContains(t, err, "Invalid reserved attribute provided. It must be an array/list/vector with 4 elements. Please refer to the wiki or manual reference for more details on how to create objects using this syntax") + + err = errorResponder("reserved", "4", true) + assert.ErrorContains(t, err, "Invalid reserved attributes provided. They must be arrays/lists/vectors with 4 elements. Please refer to the wiki or manual reference for more details on how to create objects using this syntax") +} + +func TestFiltersToMapString(t *testing.T) { + filters := map[string]node{ + "tag": &valueNode{"my-tag"}, + } + mapFilters, err := filtersToMapString(filters) + assert.Nil(t, err) + assert.Len(t, mapFilters, 1) + assert.Equal(t, "my-tag", mapFilters["tag"]) +} + +func TestRecursiveArgsToParams(t *testing.T) { + args := recursiveArgs{false, "1", "2"} + path := models.PhysicalPath + "site/building" + recParams, err := args.toParams(path) + assert.Nil(t, err) + assert.Nil(t, recParams) + + args = recursiveArgs{true, "1", "2"} + recParams, err = args.toParams(path) + assert.Nil(t, err) + assert.Equal(t, 1, recParams.MinDepth) + assert.Equal(t, 2, recParams.MaxDepth) + assert.Equal(t, path, recParams.PathEntered) +} + +func TestStringToIntOr(t *testing.T) { + value, err := stringToIntOr("3", 5) + assert.Nil(t, err) + assert.Equal(t, 3, value) + + value, err = stringToIntOr("", 5) + assert.Nil(t, err) + assert.Equal(t, 5, value) + + _, err = stringToIntOr("s", 5) + assert.NotNil(t, err) +} + +func TestAddSizeOrTemplate(t *testing.T) { + valNode := valueNode{[]float64{1, 2, 3}} + attributes := map[string]any{} + err := addSizeOrTemplate(&valNode, attributes, models.ROOM) + assert.Nil(t, err) + assert.Contains(t, attributes, "size") + + valNode = valueNode{3.5} + err = addSizeOrTemplate(&valNode, attributes, models.ROOM) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "vector3 (size) or string (template) expected") + + valNode = valueNode{"my-template"} + err = addSizeOrTemplate(&valNode, attributes, models.ROOM) + assert.Nil(t, err) + assert.Contains(t, attributes, "template") +} + +func TestNodeToSize(t *testing.T) { + valNode := valueNode{[]float64{1, 2, 3}} + size, err := nodeToSize(&valNode) + assert.Nil(t, err) + assert.Equal(t, []float64{1, 2, 3}, size) +} + +func TestNodeToPosXYZ(t *testing.T) { + valNode := valueNode{[]float64{1, 2, 3, 4}} + _, err := nodeToPosXYZ(&valNode) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "position should be a vector2 or a vector3") + + valNode = valueNode{[]float64{1, 2, 3}} + position, err := nodeToPosXYZ(&valNode) + assert.Nil(t, err) + assert.Equal(t, []float64{1, 2, 3}, position) +} diff --git a/CLI/controllers/cd_test.go b/CLI/controllers/cd_test.go new file mode 100644 index 000000000..046dcb696 --- /dev/null +++ b/CLI/controllers/cd_test.go @@ -0,0 +1,49 @@ +package controllers_test + +import ( + "cli/controllers" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCdToALayer(t *testing.T) { + controller, _, _ := layersSetup(t) + + path := "/Physical/" + strings.Replace(roomWithoutChildren["id"].(string), ".", "/", -1) + "/#my-layer" + oldCurrentPath := controllers.State.CurrPath + + err := controller.CD(path) + assert.NotNil(t, err) + assert.Equal(t, "it is not possible to cd into a layer", err.Error()) + assert.Equal(t, oldCurrentPath, controllers.State.PrevPath) + assert.Equal(t, oldCurrentPath, controllers.State.CurrPath) +} + +func TestCdObjectNotFound(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + path := "/Physical/" + strings.Replace(rack1["id"].(string), ".", "/", -1) + oldCurrentPath := controllers.State.CurrPath + mockObjectNotFound(mockAPI, "/api/hierarchy-objects/"+rack1["id"].(string)) + + err := controller.CD(path) + assert.NotNil(t, err) + assert.Equal(t, "object not found", err.Error()) + assert.Equal(t, oldCurrentPath, controllers.State.PrevPath) + assert.Equal(t, oldCurrentPath, controllers.State.CurrPath) +} + +func TestCdWorks(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockGetObject(mockAPI, rack1) + path := "/Physical/" + strings.Replace(rack1["id"].(string), ".", "/", -1) + oldCurrentPath := controllers.State.CurrPath + + err := controller.CD(path) + assert.Nil(t, err) + assert.Equal(t, oldCurrentPath, controllers.State.PrevPath) + assert.Equal(t, path, controllers.State.CurrPath) +} diff --git a/CLI/controllers/commandController.go b/CLI/controllers/commandController.go index c75746db4..e4aa89900 100755 --- a/CLI/controllers/commandController.go +++ b/CLI/controllers/commandController.go @@ -151,7 +151,7 @@ func (controller Controller) ObjectUrlGeneric(pathStr string, depth int, filters return url.String(), nil } -func GetSlot(rack map[string]any, location string) (map[string]any, error) { +func (controller Controller) GetSlot(rack map[string]any, location string) (map[string]any, error) { templateAny, ok := rack["attributes"].(map[string]any)["template"] if !ok { return nil, nil @@ -160,7 +160,7 @@ func GetSlot(rack map[string]any, location string) (map[string]any, error) { if template == "" { return nil, nil } - resp, err := API.Request("GET", "/api/obj-templates/"+template, nil, http.StatusOK) + resp, err := controller.API.Request("GET", "/api/obj-templates/"+template, nil, http.StatusOK) if err != nil { return nil, err } @@ -177,8 +177,8 @@ func GetSlot(rack map[string]any, location string) (map[string]any, error) { return nil, fmt.Errorf("the slot %s does not exist", location) } -func UnsetAttribute(path string, attr string) error { - obj, err := C.GetObject(path) +func (controller Controller) UnsetAttribute(path string, attr string) error { + obj, err := controller.GetObject(path) if err != nil { return err } @@ -190,16 +190,16 @@ func UnsetAttribute(path string, attr string) error { return fmt.Errorf("object has no attributes") } delete(attributes, attr) - url, err := C.ObjectUrl(path, 0) + url, err := controller.ObjectUrl(path, 0) if err != nil { return err } - _, err = API.Request("PUT", url, obj, http.StatusOK) + _, err = controller.API.Request("PUT", url, obj, http.StatusOK) return err } // Specific update for deleting elements in an array of an obj -func UnsetInObj(Path, attr string, idx int) (map[string]interface{}, error) { +func (controller Controller) UnsetInObj(Path, attr string, idx int) (map[string]interface{}, error) { var arr []interface{} //Check for valid idx @@ -209,7 +209,7 @@ func UnsetInObj(Path, attr string, idx int) (map[string]interface{}, error) { } //Get the object - obj, err := C.GetObject(Path) + obj, err := controller.GetObject(Path) if err != nil { return nil, err } @@ -266,12 +266,12 @@ func UnsetInObj(Path, attr string, idx int) (map[string]interface{}, error) { obj[attr] = arr } - URL, err := C.ObjectUrl(Path, 0) + URL, err := controller.ObjectUrl(Path, 0) if err != nil { return nil, err } - _, err = API.Request("PUT", URL, obj, http.StatusOK) + _, err = controller.API.Request("PUT", URL, obj, http.StatusOK) if err != nil { return nil, err } @@ -370,8 +370,8 @@ func Env(userVars, userFuncs map[string]interface{}) { } } -func GetByAttr(path string, u interface{}) error { - obj, err := C.GetObjectWithChildren(path, 1) +func (controller Controller) GetByAttr(path string, u interface{}) error { + obj, err := controller.GetObjectWithChildren(path, 1) if err != nil { return err } @@ -475,28 +475,28 @@ func Disconnect3D() { Ogree3D.Disconnect() } -func UIDelay(time float64) error { +func (controller Controller) UIDelay(time float64) error { subdata := map[string]interface{}{"command": "delay", "data": time} data := map[string]interface{}{"type": "ui", "data": subdata} if State.DebugLvl > WARNING { Disp(data) } - return Ogree3D.Inform("HandleUI", -1, data) + return controller.Ogree3D.Inform("HandleUI", -1, data) } -func UIToggle(feature string, enable bool) error { +func (controller Controller) UIToggle(feature string, enable bool) error { subdata := map[string]interface{}{"command": feature, "data": enable} data := map[string]interface{}{"type": "ui", "data": subdata} if State.DebugLvl > WARNING { Disp(data) } - return Ogree3D.Inform("HandleUI", -1, data) + return controller.Ogree3D.Inform("HandleUI", -1, data) } -func UIHighlight(path string) error { - obj, err := C.GetObject(path) +func (controller Controller) UIHighlight(path string) error { + obj, err := controller.GetObject(path) if err != nil { return err } @@ -507,20 +507,20 @@ func UIHighlight(path string) error { Disp(data) } - return Ogree3D.Inform("HandleUI", -1, data) + return controller.Ogree3D.Inform("HandleUI", -1, data) } -func UIClearCache() error { +func (controller Controller) UIClearCache() error { subdata := map[string]interface{}{"command": "clearcache", "data": ""} data := map[string]interface{}{"type": "ui", "data": subdata} if State.DebugLvl > WARNING { Disp(data) } - return Ogree3D.Inform("HandleUI", -1, data) + return controller.Ogree3D.Inform("HandleUI", -1, data) } -func CameraMove(command string, position []float64, rotation []float64) error { +func (controller Controller) CameraMove(command string, position []float64, rotation []float64) error { subdata := map[string]interface{}{"command": command} subdata["position"] = map[string]interface{}{"x": position[0], "y": position[1], "z": position[2]} subdata["rotation"] = map[string]interface{}{"x": rotation[0], "y": rotation[1]} @@ -529,10 +529,10 @@ func CameraMove(command string, position []float64, rotation []float64) error { Disp(data) } - return Ogree3D.Inform("HandleUI", -1, data) + return controller.Ogree3D.Inform("HandleUI", -1, data) } -func CameraWait(time float64) error { +func (controller Controller) CameraWait(time float64) error { subdata := map[string]interface{}{"command": "wait"} subdata["position"] = map[string]interface{}{"x": 0, "y": 0, "z": 0} subdata["rotation"] = map[string]interface{}{"x": 999, "y": time} @@ -541,13 +541,13 @@ func CameraWait(time float64) error { Disp(data) } - return Ogree3D.Inform("HandleUI", -1, data) + return controller.Ogree3D.Inform("HandleUI", -1, data) } -func FocusUI(path string) error { +func (controller Controller) FocusUI(path string) error { var id string if path != "" { - obj, err := C.GetObject(path) + obj, err := controller.GetObject(path) if err != nil { return err } @@ -564,13 +564,13 @@ func FocusUI(path string) error { } data := map[string]interface{}{"type": "focus", "data": id} - err := Ogree3D.Inform("FocusUI", -1, data) + err := controller.Ogree3D.Inform("FocusUI", -1, data) if err != nil { return err } if path != "" { - return C.CD(path) + return controller.CD(path) } else { fmt.Println("Focus is now empty") } @@ -578,12 +578,12 @@ func FocusUI(path string) error { return nil } -func LinkObject(source string, destination string, attrs []string, values []any, slots []string) error { - sourceUrl, err := C.ObjectUrl(source, 0) +func (controller Controller) LinkObject(source string, destination string, attrs []string, values []any, slots []string) error { + sourceUrl, err := controller.ObjectUrl(source, 0) if err != nil { return err } - destPath, err := C.SplitPath(destination) + destPath, err := controller.SplitPath(destination) if err != nil { return err } @@ -603,24 +603,24 @@ func LinkObject(source string, destination string, attrs []string, values []any, payload["slot"] = "[" + strings.Join(slots, ",") + "]" } - _, err = API.Request("PATCH", sourceUrl+"/link", payload, http.StatusOK) + _, err = controller.API.Request("PATCH", sourceUrl+"/link", payload, http.StatusOK) if err != nil { return err } return nil } -func UnlinkObject(path string) error { - sourceUrl, err := C.ObjectUrl(path, 0) +func (controller Controller) UnlinkObject(path string) error { + sourceUrl, err := controller.ObjectUrl(path, 0) if err != nil { return err } - _, err = API.Request("PATCH", sourceUrl+"/unlink", nil, http.StatusOK) + _, err = controller.API.Request("PATCH", sourceUrl+"/unlink", nil, http.StatusOK) return err } -func IsEntityDrawable(path string) (bool, error) { - obj, err := C.GetObject(path) +func (controller Controller) IsEntityDrawable(path string) (bool, error) { + obj, err := controller.GetObject(path) if err != nil { return false, err } @@ -661,8 +661,8 @@ func IsCategoryAttrDrawable(category string, attr string) bool { } } -func IsAttrDrawable(path string, attr string) (bool, error) { - obj, err := C.GetObject(path) +func (controller Controller) IsAttrDrawable(path string, attr string) (bool, error) { + obj, err := controller.GetObject(path) if err != nil { return false, err } @@ -721,9 +721,9 @@ func randPassword(n int) string { return string(b) } -func CreateUser(email string, role string, domain string) error { +func (controller Controller) CreateUser(email string, role string, domain string) error { password := randPassword(14) - response, err := API.Request( + response, err := controller.API.Request( "POST", "/api/users", map[string]any{ @@ -743,8 +743,8 @@ func CreateUser(email string, role string, domain string) error { return nil } -func AddRole(email string, role string, domain string) error { - response, err := API.Request("GET", "/api/users", nil, http.StatusOK) +func (controller Controller) AddRole(email string, role string, domain string) error { + response, err := controller.API.Request("GET", "/api/users", nil, http.StatusOK) if err != nil { return err } @@ -768,7 +768,7 @@ func AddRole(email string, role string, domain string) error { if userID == "" { return fmt.Errorf("user not found") } - response, err = API.Request("PATCH", fmt.Sprintf("/api/users/%s", userID), + response, err = controller.API.Request("PATCH", fmt.Sprintf("/api/users/%s", userID), map[string]any{ "roles": map[string]any{ domain: role, diff --git a/CLI/controllers/commandController_test.go b/CLI/controllers/commandController_test.go new file mode 100644 index 000000000..a22848173 --- /dev/null +++ b/CLI/controllers/commandController_test.go @@ -0,0 +1,942 @@ +package controllers_test + +import ( + "cli/controllers" + "cli/models" + "errors" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/exp/slices" +) + +// Test PWD +func TestPWD(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + controller.CD("/") + location := controllers.PWD() + assert.Equal(t, "/", location) + + mockGetObject(mockAPI, rack1) + path := "/Physical/" + strings.Replace(rack1["id"].(string), ".", "/", -1) + err := controller.CD(path) + assert.Nil(t, err) + + location = controllers.PWD() + assert.Equal(t, path, location) +} + +// Tests ObjectUrl +func TestObjectUrlInvalidPath(t *testing.T) { + _, err := controllers.C.ObjectUrl("/invalid/path", 0) + assert.NotNil(t, err) + assert.Equal(t, "invalid object path", err.Error()) +} + +func TestObjectUrlPaths(t *testing.T) { + paths := map[string]any{ + models.StrayPath + "stray-object": "/api/stray-objects/stray-object", + models.PhysicalPath + "BASIC/A": "/api/hierarchy-objects/BASIC.A", + models.ObjectTemplatesPath + "my-template": "/api/obj-templates/my-template", + models.RoomTemplatesPath + "my-room-template": "/api/room-templates/my-room-template", + models.BuildingTemplatesPath + "my-building-template": "/api/bldg-templates/my-building-template", + models.GroupsPath + "group1": "/api/groups/group1", + models.TagsPath + "my-tag": "/api/tags/my-tag", + models.LayersPath + "my-layer": "/api/layers/my-layer", + models.DomainsPath + "domain1": "/api/domains/domain1", + models.DomainsPath + "domain1/subdomain": "/api/domains/domain1.subdomain", + } + + for key, value := range paths { + basePath, err := controllers.C.ObjectUrl(key, 0) + assert.Nil(t, err) + assert.Equal(t, value, basePath) + } +} + +// Tests ObjectUrlGeneric +func TestObjectUrlGenericInvalidPath(t *testing.T) { + _, err := controllers.C.ObjectUrlGeneric("/invalid/path", 0, nil, nil) + assert.NotNil(t, err) + assert.Equal(t, "invalid object path", err.Error()) +} + +func TestObjectUrlGenericWithNoFilters(t *testing.T) { + paths := []map[string]any{ + map[string]any{ + "basePath": models.StrayPath, + "objectId": "stray-object", + "endpoint": "/api/objects", + "idName": "id", + "namespace": "physical.stray", + }, + map[string]any{ + "basePath": models.PhysicalPath, + "objectId": "BASIC/A", + "endpoint": "/api/objects", + "idName": "id", + "namespace": "physical.hierarchy", + }, + map[string]any{ + "basePath": models.ObjectTemplatesPath, + "objectId": "my-template", + "endpoint": "/api/objects", + "idName": "slug", + "namespace": "logical.objtemplate", + }, + map[string]any{ + "basePath": models.RoomTemplatesPath, + "objectId": "my-room-template", + "endpoint": "/api/objects", + "idName": "slug", + "namespace": "logical.roomtemplate", + }, + map[string]any{ + "basePath": models.BuildingTemplatesPath, + "objectId": "my-building-template", + "endpoint": "/api/objects", + "idName": "slug", + "namespace": "logical.bldgtemplate", + }, + map[string]any{ + "basePath": models.GroupsPath, + "objectId": "group1", + "endpoint": "/api/objects", + "idName": "id", + "namespace": "logical", + "extraParams": map[string]any{ + "category": "group", + }, + }, + map[string]any{ + "basePath": models.TagsPath, + "objectId": "my-tag", + "endpoint": "/api/objects", + "idName": "slug", + "namespace": "logical.tag", + }, + map[string]any{ + "basePath": models.LayersPath, + "objectId": "my-layer", + "endpoint": "/api/objects", + "idName": "slug", + "namespace": "logical.layer", + }, + map[string]any{ + "basePath": models.DomainsPath, + "objectId": "domain1", + "endpoint": "/api/objects", + "idName": "id", + "namespace": "organisational", + }, + map[string]any{ + "basePath": models.DomainsPath, + "objectId": "domain1/subdomain", + "endpoint": "/api/objects", + "idName": "id", + "namespace": "organisational", + }, + } + for _, value := range paths { + resultUrl, err := controllers.C.ObjectUrlGeneric(value["basePath"].(string)+value["objectId"].(string), 0, nil, nil) + assert.Nil(t, err) + assert.NotNil(t, resultUrl) + + parsedUrl, _ := url.Parse(resultUrl) + assert.Equal(t, value["endpoint"], parsedUrl.Path) + assert.Equal(t, strings.Replace(value["objectId"].(string), "/", ".", -1), parsedUrl.Query().Get(value["idName"].(string))) + assert.Equal(t, value["namespace"], parsedUrl.Query().Get("namespace")) + + if extraParams, ok := value["extraParams"]; ok { + for k, v := range extraParams.(map[string]any) { + assert.Equal(t, v, parsedUrl.Query().Get(k)) + } + } + } +} + +func TestObjectUrlGenericWithNormalFilters(t *testing.T) { + filters := map[string]string{ + "color": "00ED00", + } + id := "BASIC/A" + resultUrl, err := controllers.C.ObjectUrlGeneric(models.PhysicalPath+id, 0, filters, nil) + assert.Nil(t, err) + assert.NotNil(t, resultUrl) + + parsedUrl, _ := url.Parse(resultUrl) + assert.Equal(t, "/api/objects", parsedUrl.Path) + assert.Equal(t, strings.Replace(id, "/", ".", -1), parsedUrl.Query().Get("id")) + assert.Equal(t, "physical.hierarchy", parsedUrl.Query().Get("namespace")) + assert.Equal(t, "00ED00", parsedUrl.Query().Get("color")) +} + +func TestObjectUrlGenericWithFilterField(t *testing.T) { + filters := map[string]string{ + "filter": "color=00ED00", + } + id := "BASIC/A" + resultUrl, err := controllers.C.ObjectUrlGeneric(models.PhysicalPath+id, 0, filters, nil) + assert.Nil(t, err) + assert.NotNil(t, resultUrl) + + parsedUrl, _ := url.Parse(resultUrl) + assert.Equal(t, "/api/objects/search", parsedUrl.Path) + assert.Equal(t, strings.Replace(id, "/", ".", -1), parsedUrl.Query().Get("id")) + assert.Equal(t, "physical.hierarchy", parsedUrl.Query().Get("namespace")) +} + +// Tests GetSlot +func TestGetSlotWithNoTemplate(t *testing.T) { + rack := map[string]any{ + "attributes": map[string]any{}, + } + result, err := controllers.C.GetSlot(rack, "") + assert.Nil(t, err) + assert.Nil(t, result) + + rack["attributes"].(map[string]any)["template"] = "" + result, err = controllers.C.GetSlot(rack, "") + assert.Nil(t, err) + assert.Nil(t, result) +} + +func TestGetSlotWithTemplateNonExistentSlot(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + template := map[string]any{ + "slug": "rack-template", + "description": "", + "category": "rack", + "sizeWDHmm": []any{605, 1200, 2003}, + "fbxModel": "", + "attributes": map[string]any{ + "vendor": "IBM", + "model": "9360-4PX", + }, + "slots": []any{}, + } + + mockGetObjTemplate(mockAPI, template) + rack := map[string]any{ + "attributes": map[string]any{ + "template": "rack-template", + }, + } + _, err := controller.GetSlot(rack, "u02") + assert.NotNil(t, err) + assert.Equal(t, "the slot u02 does not exist", err.Error()) +} + +func TestGetSlotWithTemplateWorks(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + slot := map[string]any{ + "location": "u01", + "type": "u", + "elemOrient": []any{33.3, -44.4, 107}, + "elemPos": []any{58, 51, 44.45}, + "elemSize": []any{482.6, 1138, 44.45}, + "mandatory": "no", + "labelPos": "frontrear", + } + + template := map[string]any{ + "slug": "rack-template", + "description": "", + "category": "rack", + "sizeWDHmm": []any{605, 1200, 2003}, + "fbxModel": "", + "attributes": map[string]any{ + "vendor": "IBM", + "model": "9360-4PX", + }, + "slots": []any{ + slot, + }, + } + + mockGetObjTemplate(mockAPI, template) + rack := map[string]any{ + "attributes": map[string]any{ + "template": "rack-template", + }, + } + result, err := controller.GetSlot(rack, "u01") + assert.Nil(t, err) + assert.Equal(t, slot["location"], result["location"]) +} + +// Tests UnsetAttribute +func TestUnsetAttributeObjectNotFound(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockObjectNotFound(mockAPI, "/api/hierarchy-objects/BASIC.A.R1.A01") + + err := controller.UnsetAttribute("/Physical/BASIC/A/R1/A01", "color") + assert.NotNil(t, err) + assert.Equal(t, "object not found", err.Error()) +} + +func TestUnsetAttributeWorks(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + rack := map[string]any{ + "category": "rack", + "id": "BASIC.A.R1.A01", + "name": "A01", + "parentId": "BASIC.A.R1", + "domain": "test-domain", + "description": "", + "attributes": map[string]any{ + "height": "47", + "heightUnit": "U", + "rotation": `[45, 45, 45]`, + "posXYZ": `[4.6666666666667, -2, 0]`, + "posXYUnit": "m", + "size": `[1, 1]`, + "sizeUnit": "cm", + "color": "00ED00", + }, + } + updatedRack := copyMap(rack) + delete(updatedRack["attributes"].(map[string]any), "color") + delete(updatedRack, "id") + + mockGetObject(mockAPI, rack) + mockPutObject(mockAPI, updatedRack, updatedRack) + + err := controller.UnsetAttribute("/Physical/BASIC/A/R1/A01", "color") + assert.Nil(t, err) +} + +// Tests UnsetInObj +func TestUnsetInObjInvalidIndex(t *testing.T) { + controller, _, _ := layersSetup(t) + + result, err := controller.UnsetInObj("/Physical/BASIC/A/R1/A01", "color", -1) + assert.NotNil(t, err) + assert.Nil(t, result) + assert.Equal(t, "Index out of bounds. Please provide an index greater than 0", err.Error()) +} + +func TestUnsetInObjObjectNotFound(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockObjectNotFound(mockAPI, "/api/hierarchy-objects/BASIC.A.R1.A01") + + result, err := controller.UnsetInObj("/Physical/BASIC/A/R1/A01", "color", 0) + assert.NotNil(t, err) + assert.Nil(t, result) + assert.Equal(t, "object not found", err.Error()) +} + +func TestUnsetInObjAttributeNotFound(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + rack := copyMap(rack1) + rack["attributes"] = map[string]any{} + + mockGetObject(mockAPI, rack) + + result, err := controller.UnsetInObj("/Physical/BASIC/A/R1/A01", "color", 0) + assert.NotNil(t, err) + assert.Nil(t, result) + assert.Equal(t, "Attribute :color was not found", err.Error()) +} + +func TestUnsetInObjAttributeNotAnArray(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + rack := copyMap(rack1) + rack["attributes"] = map[string]any{ + "color": "00ED00", + } + + mockGetObject(mockAPI, rack) + + result, err := controller.UnsetInObj("/Physical/BASIC/A/R1/A01", "color", 0) + assert.NotNil(t, err) + assert.Nil(t, result) + assert.Equal(t, "Attribute is not an array", err.Error()) +} + +func TestUnsetInObjEmptyArray(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + rack := copyMap(rack1) + rack["attributes"] = map[string]any{ + "posXYZ": []any{}, + } + + mockGetObject(mockAPI, rack) + + result, err := controller.UnsetInObj("/Physical/BASIC/A/R1/A01", "posXYZ", 0) + assert.NotNil(t, err) + assert.Nil(t, result) + assert.Equal(t, "Cannot delete anymore elements", err.Error()) +} + +func TestUnsetInObjWorksWithNestedAttribute(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + rack := copyMap(rack1) + rack["attributes"] = map[string]any{ + "posXYZ": []any{1, 2, 3}, + } + updatedRack := copyMap(rack1) + updatedRack["attributes"] = map[string]any{ + "posXYZ": []any{1.0, 3.0}, + } + delete(updatedRack, "children") + + mockGetObject(mockAPI, rack) + mockPutObject(mockAPI, updatedRack, updatedRack) + + result, err := controller.UnsetInObj("/Physical/BASIC/A/R1/A01", "posXYZ", 1) + assert.Nil(t, err) + assert.Nil(t, result) +} + +func TestUnsetInObjWorksWithAttribute(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + template := map[string]any{ + "slug": "small-room", + "category": "room", + "axisOrientation": "+x+y", + "sizeWDHm": []any{9.6, 22.8, 3.0}, + "floorUnit": "t", + "technicalArea": []any{5.0, 0.0, 0.0, 0.0}, + "reservedArea": []any{3.0, 1.0, 1.0, 3.0}, + "colors": []any{ + map[string]any{ + "name": "my-color1", + "value": "00ED00", + }, + map[string]any{ + "name": "my-color2", + "value": "ffffff", + }, + }, + } + updatedTemplate := copyMap(template) + updatedTemplate["colors"] = slices.Delete(updatedTemplate["colors"].([]any), 1, 2) + mockPutObject(mockAPI, updatedTemplate, updatedTemplate) + mockGetRoomTemplate(mockAPI, template) + + result, err := controller.UnsetInObj(models.RoomTemplatesPath+"small-room", "colors", 1) + assert.Nil(t, err) + assert.Nil(t, result) +} + +// Tests GetByAttr +func TestGetByAttrErrorWhenObjIsNotRack(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockGetObjectHierarchy(mockAPI, chassis) + + err := controller.GetByAttr(models.PhysicalPath+"BASIC/A/R1/A01/chT", "colors") + assert.NotNil(t, err) + assert.Equal(t, "command may only be performed on rack objects", err.Error()) +} + +func TestGetByAttrErrorWhenObjIsRackWithSlotName(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + rack := copyMap(rack1) + rack["attributes"] = map[string]any{ + "slot": []any{ + map[string]any{ + "location": "u01", + "type": "u", + "elemOrient": []any{33.3, -44.4, 107}, + "elemPos": []any{58, 51, 44.45}, + "elemSize": []any{482.6, 1138, 44.45}, + "mandatory": "no", + "labelPos": "frontrear", + "color": "@color1", + }, + }, + } + mockGetObjectHierarchy(mockAPI, rack) + + err := controller.GetByAttr(models.PhysicalPath+"BASIC/A/R1/A01", "u01") + assert.Nil(t, err) +} + +func TestGetByAttrErrorWhenObjIsRackWithHeight(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + rack := copyMap(rack1) + rack["height"] = "47" + mockGetObjectHierarchy(mockAPI, rack) + + err := controller.GetByAttr(models.PhysicalPath+"BASIC/A/R1/A01", 47) + assert.Nil(t, err) +} + +// Test UI (UIDelay, UIToggle, UIHighlight) +func TestUIDelay(t *testing.T) { + controller, _, ogree3D := layersSetup(t) + // ogree3D. + time := 15.0 + data := map[string]interface{}{ + "type": "ui", + "data": map[string]interface{}{ + "command": "delay", + "data": time, + }, + } + ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once + err := controller.UIDelay(time) + assert.Nil(t, err) +} + +func TestUIToggle(t *testing.T) { + controller, _, ogree3D := layersSetup(t) + // ogree3D. + feature := "feature" + enable := true + data := map[string]interface{}{ + "type": "ui", + "data": map[string]interface{}{ + "command": feature, + "data": enable, + }, + } + + ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once + err := controller.UIToggle(feature, enable) + assert.Nil(t, err) +} + +func TestUIHighlightObjectNotFound(t *testing.T) { + controller, mockAPI, ogree3D := layersSetup(t) + path := "/api/hierarchy-objects/BASIC.A.R1.A01" + + mockObjectNotFound(mockAPI, path) + + data := map[string]interface{}{ + "type": "ui", + "data": map[string]interface{}{ + "command": "highlight", + "data": "BASIC.A.R1.A01", + }, + } + + ogree3D.AssertNotCalled(t, "HandleUI", -1, data) + err := controller.UIHighlight("/Physical/BASIC/A/R1/A01") + assert.NotNil(t, err) + assert.Equal(t, "object not found", err.Error()) +} + +func TestUIHighlightWorks(t *testing.T) { + controller, mockAPI, ogree3D := layersSetup(t) + data := map[string]interface{}{ + "type": "ui", + "data": map[string]interface{}{ + "command": "highlight", + "data": rack1["id"], + }, + } + + mockGetObject(mockAPI, rack1) + ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once + err := controller.UIHighlight("/Physical/BASIC/A/R1/A01") + assert.Nil(t, err) +} + +func TestUIClearCache(t *testing.T) { + controller, _, ogree3D := layersSetup(t) + data := map[string]interface{}{ + "type": "ui", + "data": map[string]interface{}{ + "command": "clearcache", + "data": "", + }, + } + + ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once + err := controller.UIClearCache() + assert.Nil(t, err) +} + +func TestCameraMove(t *testing.T) { + controller, _, ogree3D := layersSetup(t) + data := map[string]interface{}{ + "type": "camera", + "data": map[string]interface{}{ + "command": "move", + "position": map[string]interface{}{"x": 0.0, "y": 1.0, "z": 2.0}, + "rotation": map[string]interface{}{"x": 0.0, "y": 0.0}, + }, + } + + ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once + err := controller.CameraMove("move", []float64{0, 1, 2}, []float64{0, 0}) + assert.Nil(t, err) +} + +func TestCameraWait(t *testing.T) { + controller, _, ogree3D := layersSetup(t) + time := 15.0 + data := map[string]interface{}{ + "type": "camera", + "data": map[string]interface{}{ + "command": "wait", + "position": map[string]interface{}{"x": 0, "y": 0, "z": 0}, + "rotation": map[string]interface{}{"x": 999, "y": time}, + }, + } + + ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once + err := controller.CameraWait(time) + assert.Nil(t, err) +} + +func TestFocusUIObjectNotFound(t *testing.T) { + controller, mockAPI, ogree3D := layersSetup(t) + + mockObjectNotFound(mockAPI, "/api/hierarchy-objects/"+rack1["id"].(string)) + err := controller.FocusUI("/Physical/" + strings.Replace(rack1["id"].(string), ".", "/", -1)) + ogree3D.AssertNotCalled(t, "Inform", "mock.Anything", "mock.Anything", "mock.Anything") + assert.NotNil(t, err) + assert.Equal(t, "object not found", err.Error()) +} + +func TestFocusUIEmptyPath(t *testing.T) { + controller, mockAPI, ogree3D := layersSetup(t) + data := map[string]interface{}{ + "type": "focus", + "data": "", + } + + ogree3D.On("Inform", "FocusUI", -1, data).Return(nil).Once() // The inform should be called once + err := controller.FocusUI("") + mockAPI.AssertNotCalled(t, "Request", "GET", "mock.Anything", "mock.Anything", "mock.Anything") + assert.Nil(t, err) +} + +func TestFocusUIErrorWithRoom(t *testing.T) { + controller, mockAPI, ogree3D := layersSetup(t) + errorMessage := "You cannot focus on this object. Note you cannot focus on Sites, Buildings and Rooms. " + errorMessage += "For more information please refer to the help doc (man >)" + + mockGetObject(mockAPI, roomWithoutChildren) + err := controller.FocusUI("/Physical/" + strings.Replace(roomWithoutChildren["id"].(string), ".", "/", -1)) + ogree3D.AssertNotCalled(t, "Inform", "mock.Anything", "mock.Anything", "mock.Anything") + assert.NotNil(t, err) + assert.Equal(t, errorMessage, err.Error()) +} + +func TestFocusUIWorks(t *testing.T) { + controller, mockAPI, ogree3D := layersSetup(t) + data := map[string]interface{}{ + "type": "focus", + "data": rack1["id"], + } + + ogree3D.On("Inform", "FocusUI", -1, data).Return(nil).Once() // The inform should be called once + // Get Object will be called two times: Once in FocusUI and a second time in FocusUI->CD->Tree + mockGetObject(mockAPI, rack1) + mockGetObject(mockAPI, rack1) + err := controller.FocusUI("/Physical/" + strings.Replace(rack1["id"].(string), ".", "/", -1)) + assert.Nil(t, err) +} + +// Tests LinkObject +func TestLinkObjectErrorNotStaryObject(t *testing.T) { + controller, _, _ := layersSetup(t) + + err := controller.LinkObject(models.PhysicalPath+"BASIC/A/R1/A01", models.PhysicalPath+"BASIC/A/R1/A01", []string{}, []any{}, []string{}) + assert.NotNil(t, err) + assert.Equal(t, "only stray objects can be linked", err.Error()) +} + +func TestLinkObjectWithoutSlots(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + strayDevice := copyMap(chassis) + delete(strayDevice, "id") + delete(strayDevice, "parentId") + response := map[string]any{"message": "successfully linked"} + body := map[string]any{"parentId": "BASIC.A.R1.A01", "slot": "[]", "type": "chassis"} + + mockUpdateObject(mockAPI, body, response) + + slots := []string{} + attributes := []string{} + values := []any{} + for key, value := range strayDevice["attributes"].(map[string]any) { + attributes = append(attributes, key) + values = append(values, value) + } + err := controller.LinkObject(models.StrayPath+"chT", models.PhysicalPath+"BASIC/A/R1/A01", attributes, values, slots) + assert.Nil(t, err) +} + +func TestLinkObjectWithInvalidSlots(t *testing.T) { + controller, _, _ := layersSetup(t) + + strayDevice := copyMap(chassis) + delete(strayDevice, "id") + delete(strayDevice, "parentId") + + slots := []string{"slot01..slot03", "slot4"} + attributes := []string{} + values := []any{} + for key, value := range strayDevice["attributes"].(map[string]any) { + attributes = append(attributes, key) + values = append(values, value) + } + err := controller.LinkObject(models.StrayPath+"chT", models.PhysicalPath+"BASIC/A/R1/A01", attributes, values, slots) + assert.NotNil(t, err) + assert.Equal(t, "Invalid device syntax: .. can only be used in a single element vector", err.Error()) +} + +func TestLinkObjectWithValidSlots(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + strayDevice := copyMap(chassis) + delete(strayDevice, "id") + delete(strayDevice, "parentId") + response := map[string]any{"message": "successfully linked"} + body := map[string]any{"parentId": "BASIC.A.R1.A01", "slot": "[slot01]", "type": "chassis"} + + mockUpdateObject(mockAPI, body, response) + + slots := []string{"slot01"} + attributes := []string{} + values := []any{} + for key, value := range strayDevice["attributes"].(map[string]any) { + attributes = append(attributes, key) + values = append(values, value) + } + err := controller.LinkObject(models.StrayPath+"chT", models.PhysicalPath+"BASIC/A/R1/A01", attributes, values, slots) + assert.Nil(t, err) +} + +// Tests UnlinkObject +func TestUnlinkObjectWithInvalidPath(t *testing.T) { + controller, _, _ := layersSetup(t) + + err := controller.UnlinkObject("/invalid/path") + assert.NotNil(t, err) + assert.Equal(t, "invalid object path", err.Error()) +} + +func TestUnlinkObjectWithValidPath(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockUpdateObject(mockAPI, nil, map[string]any{"message": "successfully unlinked"}) + + err := controller.UnlinkObject(models.PhysicalPath + "BASIC/A/R1/A01") + assert.Nil(t, err) +} + +// Tests IsEntityDrawable +func TestIsEntityDrawableObjectNotFound(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockObjectNotFound(mockAPI, "/api/hierarchy-objects/BASIC.A.R1.A01") + + isDrawable, err := controller.IsEntityDrawable(models.PhysicalPath + "BASIC/A/R1/A01") + assert.False(t, isDrawable) + assert.NotNil(t, err) + assert.Equal(t, "object not found", err.Error()) +} + +func TestIsEntityDrawableCategoryIsNotDrawable(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + controllers.State.DrawableObjs = []int{models.EntityStrToInt("device")} + + mockGetObject(mockAPI, rack1) + + isDrawable, err := controller.IsEntityDrawable(models.PhysicalPath + "BASIC/A/R1/A01") + assert.False(t, isDrawable) + assert.Nil(t, err) +} + +func TestIsEntityDrawableWorks(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + controllers.State.DrawableObjs = []int{models.EntityStrToInt("rack")} + + mockGetObject(mockAPI, rack1) + + isDrawable, err := controller.IsEntityDrawable(models.PhysicalPath + "BASIC/A/R1/A01") + assert.True(t, isDrawable) + assert.Nil(t, err) +} + +// Tests IsAttrDrawable (and IsCategoryAttrDrawable) +func TestIsAttrDrawableObjectNotFound(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + path := "/api/hierarchy-objects/BASIC.A.R1.A01" + + mockObjectNotFound(mockAPI, path) + + isAttrDrawable, err := controller.IsAttrDrawable(models.PhysicalPath+"BASIC/A/R1/A01", "color") + assert.False(t, isAttrDrawable) + assert.NotNil(t, err) + assert.Equal(t, "object not found", err.Error()) +} + +func TestIsAttrDrawableTemplateJsonIsNil(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + controllers.State.DrawableObjs = []int{models.EntityStrToInt("rack")} + + controllers.State.DrawableJsons = map[string]map[string]any{ + "rack": nil, + } + + mockGetObject(mockAPI, rack1) + + isAttrDrawable, err := controller.IsAttrDrawable(models.PhysicalPath+"BASIC/A/R1/A01", "color") + assert.True(t, isAttrDrawable) + assert.Nil(t, err) +} + +func TestIsAttrDrawableSpecialAttribute(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + controllers.State.DrawableObjs = []int{models.EntityStrToInt("rack")} + + controllers.State.DrawableJsons = map[string]map[string]any{ + "rack": map[string]any{ + "name": true, + "parentId": true, + "category": true, + "description": false, + "domain": true, + "attributes": map[string]any{ + "color": true, + }, + }, + } + + mockGetObject(mockAPI, rack1) + isAttrDrawable, err := controller.IsAttrDrawable(models.PhysicalPath+"BASIC/A/R1/A01", "name") + assert.True(t, isAttrDrawable) + assert.Nil(t, err) + + // description is set to false so it should return false + mockGetObject(mockAPI, rack1) + isAttrDrawable, err = controller.IsAttrDrawable(models.PhysicalPath+"BASIC/A/R1/A01", "description") + assert.False(t, isAttrDrawable) + assert.Nil(t, err) +} + +func TestIsAttrDrawableDefaultAttribute(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + controllers.State.DrawableObjs = []int{models.EntityStrToInt("rack")} + + controllers.State.DrawableJsons = map[string]map[string]any{ + "rack": map[string]any{ + "name": true, + "parentId": true, + "category": true, + "description": false, + "domain": true, + "attributes": map[string]any{ + "color": true, + }, + }, + } + + // color is not in the first case. So it will be searched in attributes field + mockGetObject(mockAPI, rack1) + isAttrDrawable, err := controller.IsAttrDrawable(models.PhysicalPath+"BASIC/A/R1/A01", "color") + assert.True(t, isAttrDrawable) + assert.Nil(t, err) + + // height is not present in attributes, so it should return false + mockGetObject(mockAPI, rack1) + isAttrDrawable, err = controller.IsAttrDrawable(models.PhysicalPath+"BASIC/A/R1/A01", "height") + assert.False(t, isAttrDrawable) + assert.Nil(t, err) +} + +// Tests CreateUser +func TestCreateUserInvalidEmail(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockAPI.On( + "Request", "POST", + "/api/users", + "mock.Anything", 201, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "message": "A valid email address is required", + }, + Status: 400, + }, errors.New("[Response From API] A valid email address is required"), + ).Once() + + err := controller.CreateUser("email", "manager", "*") + assert.NotNil(t, err) + assert.Equal(t, "[Response From API] A valid email address is required", err.Error()) +} + +func TestCreateUserWorks(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockAPI.On("Request", "POST", + "/api/users", + "mock.Anything", 201, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "message": "Account has been created", + }, + }, nil, + ).Once() + + err := controller.CreateUser("email@email.com", "manager", "*") + assert.Nil(t, err) +} + +// Tests AddRole +func TestAddRoleUserNotFound(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockAPI.On("Request", "GET", "/api/users", "mock.Anything", 200).Return( + &controllers.Response{ + Body: map[string]any{ + "data": []any{}, + }, + }, nil, + ).Once() + + err := controller.AddRole("email@email.com", "manager", "*") + assert.NotNil(t, err) + assert.Equal(t, "user not found", err.Error()) +} + +func TestAddRoleWorks(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockAPI.On("Request", "GET", "/api/users", "mock.Anything", 200).Return( + &controllers.Response{ + Body: map[string]any{ + "data": []any{ + map[string]any{ + "_id": "507f1f77bcf86cd799439011", + "email": "email@email.com", + }, + }, + }, + }, nil, + ).Once() + + mockAPI.On("Request", "PATCH", "/api/users/507f1f77bcf86cd799439011", "mock.Anything", 200).Return( + &controllers.Response{ + Body: map[string]any{ + "message": "successfully updated user roles", + }, + }, nil, + ).Once() + + err := controller.AddRole("email@email.com", "manager", "*") + assert.Nil(t, err) +} diff --git a/CLI/controllers/controller_test.go b/CLI/controllers/controller_test.go index 6fde3ce2c..0f70a3aec 100644 --- a/CLI/controllers/controller_test.go +++ b/CLI/controllers/controller_test.go @@ -230,6 +230,16 @@ func mockUpdateObject(mockAPI *mocks.APIPort, dataUpdate map[string]any, dataUpd ) } +func mockPutObject(mockAPI *mocks.APIPort, dataUpdate map[string]any, dataUpdated map[string]any) { + mockAPI.On("Request", http.MethodPut, mock.Anything, dataUpdate, http.StatusOK).Return( + &controllers.Response{ + Body: map[string]any{ + "data": dataUpdated, + }, + }, nil, + ) +} + func mockObjectNotFound(mockAPI *mocks.APIPort, path string) { mockAPI.On( "Request", http.MethodGet, @@ -255,3 +265,17 @@ func mockGetObjTemplate(mockAPI *mocks.APIPort, template map[string]any) { }, nil, ).Once() } + +func mockGetRoomTemplate(mockAPI *mocks.APIPort, template map[string]any) { + mockAPI.On( + "Request", http.MethodGet, + "/api/room-templates/"+template["slug"].(string), + mock.Anything, http.StatusOK, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "data": template, + }, + }, nil, + ).Once() +} diff --git a/CLI/controllers/create.go b/CLI/controllers/create.go index 6c83dddf5..bd5944822 100644 --- a/CLI/controllers/create.go +++ b/CLI/controllers/create.go @@ -42,7 +42,6 @@ func (controller Controller) CreateObject(path string, ent int, data map[string] l.GetWarningLogger().Println("Invalid path name provided for OCLI object creation") return fmt.Errorf("Invalid path name provided for OCLI object creation") } - data["name"] = name data["category"] = models.EntityToString(ent) data["description"] = "" @@ -303,7 +302,7 @@ func (controller Controller) CreateObject(path string, ent int, data map[string] if len(slots) != 1 { return fmt.Errorf("Invalid device syntax: only one slot can be provided if no template") } - slot, err = GetSlot(parent, slots[0]) + slot, err = C.GetSlot(parent, slots[0]) if err != nil { return err } @@ -367,8 +366,8 @@ func (controller Controller) CreateObject(path string, ent int, data map[string] return controller.PostObj(ent, data["category"].(string), data, path) } -func CreateTag(slug, color string) error { - return C.PostObj(models.TAG, models.EntityToString(models.TAG), map[string]any{ +func (controller Controller) CreateTag(slug, color string) error { + return controller.PostObj(models.TAG, models.EntityToString(models.TAG), map[string]any{ "slug": slug, "description": slug, // the description is initially set with the value of the slug "color": color, diff --git a/CLI/controllers/create_test.go b/CLI/controllers/create_test.go index 82511e48a..4ddbab12c 100644 --- a/CLI/controllers/create_test.go +++ b/CLI/controllers/create_test.go @@ -1,12 +1,38 @@ package controllers_test import ( + "cli/controllers" "cli/models" "testing" "github.com/stretchr/testify/assert" ) +var baseSite = map[string]any{ + "category": "site", + "children": []any{}, + "id": "BASIC", + "name": "BASIC", + "parentId": "", + "domain": "test-domain", +} + +var baseBuilding = map[string]any{ + "category": "building", + "id": "BASIC.A", + "name": "A", + "parentId": "BASIC", + "domain": "test-domain", + "attributes": map[string]any{ + "heightUnit": "m", + "rotation": 30.5, + "posXY": []float64{4.6666666666667, -2}, + "posXYUnit": "m", + "size": []float64{3, 3, 5}, + "sizeUnit": "m", + }, +} + var createRoom = map[string]any{ "category": "room", "id": "BASIC.A.R1", @@ -15,6 +41,22 @@ var createRoom = map[string]any{ "domain": "test-domain", } +func TestCreateObjectInvalidPath(t *testing.T) { + controller, _, _ := layersSetup(t) + + err := controller.CreateObject("/.", models.RACK, map[string]any{}) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid path name provided for OCLI object creation") +} + +func TestCreateObjectErrorParentNotFound(t *testing.T) { + controller, _, _ := layersSetup(t) + + err := controller.CreateObject("/", models.RACK, map[string]any{}) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "parent not found") +} + func TestCreateObjectWithNotExistentTemplateReturnsError(t *testing.T) { controller, mockAPI, _ := layersSetup(t) @@ -136,3 +178,503 @@ func TestCreateGenericWithTemplateWorks(t *testing.T) { }) assert.Nil(t, err) } + +func TestCreateDomain(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + // domain with no parent + mockCreateObject(mockAPI, "domain", map[string]any{ + "category": "domain", + "id": "dom1", + "name": "dom1", + "parentId": "", + "description": "", + "attributes": map[string]any{}, + }) + + err := controller.CreateObject("/Organisation/Domain/dom1", models.DOMAIN, map[string]any{ + "category": "domain", + "id": "dom1", + "name": "dom1", + "description": "", + }) + assert.Nil(t, err) + + // domain with parent + mockGetObjectByEntity(mockAPI, "domains", map[string]any{ + "category": "domain", + "id": "domParent", + "name": "domParent", + "parentId": "", + }) + + mockCreateObject(mockAPI, "domain", map[string]any{ + "category": "domain", + "id": "domParent.dom2", + "name": "dom2", + "parentId": "domParent", + "description": "", + "attributes": map[string]any{}, + }) + + err = controller.CreateObject("/Organisation/Domain/domParent/dom2", models.DOMAIN, map[string]any{ + "category": "domain", + "id": "domParent.dom2", + "name": "dom2", + "parentId": "domParent", + "description": "", + }) + assert.Nil(t, err) +} + +func TestCreateBuildingInvalidSize(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + buildingInvalidSize := copyMap(baseBuilding) + buildingInvalidSize["attributes"].(map[string]any)["size"] = []float64{} + + mockGetObject(mockAPI, baseSite) + + // with state.DebugLvl = 0 + err := controller.CreateObject("/Physical/BASIC/A", models.BLDG, buildingInvalidSize) + // returns nil but the object is not created + assert.Nil(t, err) + + // with state.DebugLvl > 0 + controllers.State.DebugLvl = 1 + mockGetObject(mockAPI, baseSite) + err = controller.CreateObject("/Physical/BASIC/A", models.BLDG, buildingInvalidSize) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid size attribute provided."+ + " \nIt must be an array/list/vector with 3 elements."+ + " Please refer to the wiki or manual reference"+ + " for more details on how to create objects "+ + "using this syntax") + controllers.State.DebugLvl = 0 +} + +func TestCreateBuildingInvalidPosXY(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + buildingInvalidPosXY := copyMap(baseBuilding) + buildingInvalidPosXY["attributes"].(map[string]any)["posXY"] = []float64{} + + // with state.DebugLvl = 0 + mockGetObject(mockAPI, baseSite) + err := controller.CreateObject("/Physical/BASIC/A", models.BLDG, copyMap(buildingInvalidPosXY)) + // returns nil but the object is not created + assert.Nil(t, err) + + // with state.DebugLvl > 0 + controllers.State.DebugLvl = 1 + mockGetObject(mockAPI, baseSite) + err = controller.CreateObject("/Physical/BASIC/A", models.BLDG, buildingInvalidPosXY) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid posXY attribute provided."+ + " \nIt must be an array/list/vector with 2 elements."+ + " Please refer to the wiki or manual reference"+ + " for more details on how to create objects "+ + "using this syntax") + controllers.State.DebugLvl = 0 +} + +func TestCreateBuilding(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockGetObject(mockAPI, baseSite) + mockCreateObject(mockAPI, "building", map[string]any{ + "category": "building", + "id": "BASIC.A", + "name": "A", + "parentId": "BASIC", + "domain": "test-domain", + "description": "", + "attributes": map[string]any{ + "height": "5", + "heightUnit": "m", + "rotation": `30.5`, + "posXY": `[4.6666666666667, -2]`, + "posXYUnit": "m", + "size": `[3, 3]`, + "sizeUnit": "m", + }, + }) + + err := controller.CreateObject("/Physical/BASIC/A", models.BLDG, copyMap(baseBuilding)) + assert.Nil(t, err) +} + +func TestCreateRoomInvalidSize(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + roomsBuilding := copyMap(baseBuilding) + room := map[string]any{ + "category": "room", + "id": "BASIC.A.R1", + "name": "R1", + "parentId": "BASIC.A", + "domain": "test-domain", + "attributes": map[string]any{ + "floorUnit": "t", + "heightUnit": "m", + "rotation": 30.5, + "axisOrientation": "+x+y", + "posXY": []float64{4.6666666666667, -2}, + "posXYUnit": "m", + "size": []float64{}, + "sizeUnit": "m", + }, + } + + // with state.DebugLvl = 0 + mockGetObject(mockAPI, roomsBuilding) + err := controller.CreateObject("/Physical/BASIC/A/R1", models.ROOM, room) + assert.Nil(t, err) + + // with state.DebugLvl > 0 + controllers.State.DebugLvl = 1 + mockGetObject(mockAPI, roomsBuilding) + err = controller.CreateObject("/Physical/BASIC/A/R1", models.ROOM, room) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid size attribute provided."+ + " \nIt must be an array/list/vector with 3 elements."+ + " Please refer to the wiki or manual reference"+ + " for more details on how to create objects "+ + "using this syntax") + controllers.State.DebugLvl = 0 +} + +func TestCreateRoomInvalidPosXY(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + roomsBuilding := copyMap(baseBuilding) + room := map[string]any{ + "category": "room", + "id": "BASIC.A.R1", + "name": "R1", + "parentId": "BASIC.A", + "domain": "test-domain", + "attributes": map[string]any{ + "floorUnit": "t", + "heightUnit": "m", + "rotation": 30.5, + "axisOrientation": "+x+y", + "posXY": []float64{}, + "posXYUnit": "m", + "size": []float64{2, 3, 3}, + "sizeUnit": "m", + }, + } + + // with state.DebugLvl = 0 + mockGetObject(mockAPI, roomsBuilding) + + err := controller.CreateObject("/Physical/BASIC/A/R1", models.ROOM, copyMap(room)) + assert.Nil(t, err) + + // with state.DebugLvl > 0 + controllers.State.DebugLvl = 1 + mockGetObject(mockAPI, roomsBuilding) + err = controller.CreateObject("/Physical/BASIC/A/R1", models.ROOM, room) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid posXY attribute provided."+ + " \nIt must be an array/list/vector with 2 elements."+ + " Please refer to the wiki or manual reference"+ + " for more details on how to create objects "+ + "using this syntax") + controllers.State.DebugLvl = 0 +} + +func TestCreateRoom(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockGetObject(mockAPI, copyMap(baseBuilding)) + + mockCreateObject(mockAPI, "room", map[string]any{ + "category": "room", + "id": "BASIC.A.R1", + "name": "R1", + "parentId": "BASIC.A", + "domain": "test-domain", + "description": "", + "attributes": map[string]any{ + "floorUnit": "t", + "height": "5", + "heightUnit": "m", + "axisOrientation": "+x+y", + "rotation": `30.5`, + "posXY": `[4.6666666666667, -2]`, + "posXYUnit": "m", + "size": `[3, 3]`, + "sizeUnit": "m", + }, + }) + + err := controller.CreateObject("/Physical/BASIC/A/R1", models.ROOM, map[string]any{ + "category": "room", + "id": "BASIC.A.R1", + "name": "R1", + "parentId": "BASIC.A", + "domain": "test-domain", + "attributes": map[string]any{ + "floorUnit": "t", + "heightUnit": "m", + "rotation": 30.5, + "axisOrientation": "+x+y", + "posXY": []float64{4.6666666666667, -2}, + "posXYUnit": "m", + "size": []float64{3, 3, 5}, + "sizeUnit": "m", + }, + }) + assert.Nil(t, err) +} + +func TestCreateRackInvalidSize(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + room := map[string]any{ + "category": "room", + "children": []any{}, + "id": "BASIC.A.R1", + "name": "R1", + "parentId": "BASIC.A", + "domain": "test-domain", + } + rack := map[string]any{ + "category": "rack", + "id": "BASIC.A.R1.A01", + "name": "A01", + "parentId": "BASIC.A.R1", + "domain": "test-domain", + "attributes": map[string]any{ + "heightUnit": "U", + "rotation": []float64{45, 45, 45}, + "posXYZ": []float64{4.6666666666667, -2, 0}, + "posXYUnit": "m", + "size": []float64{}, + "sizeUnit": "cm", + }, + } + + // with state.DebugLvl = 0 + mockGetObject(mockAPI, room) + err := controller.CreateObject("/Physical/BASIC/A/R1/A01", models.RACK, rack) + assert.Nil(t, err) + + // with state.DebugLvl > 0 + controllers.State.DebugLvl = 1 + mockGetObject(mockAPI, room) + err = controller.CreateObject("/Physical/BASIC/A/R1/A01", models.RACK, rack) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid size attribute/template provided."+ + " \nThe size must be an array/list/vector with "+ + "3 elements."+"\n\nIf you have provided a"+ + " template, please check that you are referring to "+ + "an existing template"+ + "\n\nFor more information "+ + "please refer to the wiki or manual reference"+ + " for more details on how to create objects "+ + "using this syntax") + controllers.State.DebugLvl = 0 +} + +func TestCreateRack(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockGetObject(mockAPI, map[string]any{ + "category": "room", + "children": []any{}, + "id": "BASIC.A.R1", + "name": "R1", + "parentId": "BASIC.A", + "domain": "test-domain", + }) + + mockCreateObject(mockAPI, "rack", map[string]any{ + "category": "rack", + "id": "BASIC.A.R1.A01", + "name": "A01", + "parentId": "BASIC.A.R1", + "domain": "test-domain", + "description": "", + "attributes": map[string]any{ + "height": "47", + "heightUnit": "U", + "rotation": `[45, 45, 45]`, + "posXYZ": `[4.6666666666667, -2, 0]`, + "posXYUnit": "m", + "size": `[1, 1]`, + "sizeUnit": "cm", + }, + }) + + err := controller.CreateObject("/Physical/BASIC/A/R1/A01", models.RACK, map[string]any{ + "category": "rack", + "id": "BASIC.A.R1.A01", + "name": "A01", + "parentId": "BASIC.A.R1", + "domain": "test-domain", + "attributes": map[string]any{ + "heightUnit": "U", + "rotation": []float64{45, 45, 45}, + "posXYZ": []float64{4.6666666666667, -2, 0}, + "posXYUnit": "m", + "size": []float64{1, 1, 47}, + "sizeUnit": "cm", + }, + }) + assert.Nil(t, err) +} + +func TestCreateDevice(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockGetObject(mockAPI, map[string]any{ + "category": "rack", + "children": []any{}, + "id": "BASIC.A.R1.A01", + "name": "A01", + "parentId": "BASIC.A.R1", + "domain": "test-domain", + }) + + mockCreateObject(mockAPI, "device", map[string]any{ + "category": "device", + "id": "BASIC.A.R1.A01.D1", + "name": "D1", + "parentId": "BASIC.A.R1.A01", + "domain": "test-domain", + "description": "", + "attributes": map[string]any{ + "height": "47", + "heightUnit": "U", + "orientation": "front", + "size": `[1,1]`, + "sizeUnit": "cm", + }, + }) + + err := controller.CreateObject("/Physical/BASIC/A/R1/A01/D1", models.DEVICE, map[string]any{ + "category": "device", + "id": "BASIC.A.R1.A01.D1", + "name": "D1", + "parentId": "BASIC.A.R1.A01", + "domain": "test-domain", + "attributes": map[string]any{ + "height": "47", + "heightUnit": "U", + "orientation": "front", + "size": []float64{1, 1}, + "sizeUnit": "cm", + }, + }) + assert.Nil(t, err) +} + +func TestCreateDeviceWithSizeU(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockGetResponse := map[string]any{ + "category": "rack", + "children": []any{}, + "id": "BASIC.A.R1.A01", + "name": "A01", + "parentId": "BASIC.A.R1", + "domain": "test-domain", + } + + mockCreateResponse := map[string]any{ + "category": "device", + "id": "BASIC.A.R1.A01.D1", + "name": "D1", + "parentId": "BASIC.A.R1.A01", + "domain": "test-domain", + "description": "", + "attributes": map[string]any{ + "height": "89", + "sizeU": "2", + "heightUnit": "U", + "orientation": "front", + "size": `[1,1]`, + "sizeUnit": "cm", + }, + } + + // SizeU of int type + mockGetObject(mockAPI, mockGetResponse) + mockCreateObject(mockAPI, "device", mockCreateResponse) + err := controller.CreateObject("/Physical/BASIC/A/R1/A01/D1", models.DEVICE, map[string]any{ + "category": "device", + "id": "BASIC.A.R1.A01.D1", + "name": "D1", + "parentId": "BASIC.A.R1.A01", + "domain": "test-domain", + "attributes": map[string]any{ + "sizeU": 2, + "heightUnit": "U", + "orientation": "front", + "size": []float64{1, 1}, + "sizeUnit": "cm", + }, + }) + assert.Nil(t, err) + + // SizeU of float type + mockGetObject(mockAPI, mockGetResponse) + mockCreateObject(mockAPI, "device", mockCreateResponse) + err = controller.CreateObject("/Physical/BASIC/A/R1/A01/D1", models.DEVICE, map[string]any{ + "category": "device", + "id": "BASIC.A.R1.A01.D1", + "name": "D1", + "parentId": "BASIC.A.R1.A01", + "domain": "test-domain", + "attributes": map[string]any{ + "sizeU": 2.0, + "heightUnit": "U", + "orientation": "front", + "size": []float64{1, 1}, + "sizeUnit": "cm", + }, + }) + assert.Nil(t, err) +} + +func TestCreateGroup(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockGetObject(mockAPI, baseSite) + + mockCreateObject(mockAPI, "group", map[string]any{ + "attributes": map[string]any{ + "content": "R1,R2", + }, + "category": "group", + "description": "", + "domain": "test-domain", + "name": "G1", + "parentId": "BASIC", + }) + + err := controller.CreateObject("/Physical/BASIC/G1", models.GROUP, map[string]any{ + "attributes": map[string]any{ + "content": []string{"R1", "R2"}, + }, + "category": "group", + "description": "", + "domain": "test-domain", + "name": "G1", + "parentId": "BASIC", + }) + assert.Nil(t, err) +} + +func TestCreateTag(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + color := "D0FF78" + slug := "my-tag" + + mockCreateObject(mockAPI, "tag", map[string]any{ + "color": color, + "description": slug, + "slug": slug, + }) + + err := controller.CreateTag(slug, color) + assert.Nil(t, err) +} diff --git a/CLI/controllers/delete.go b/CLI/controllers/delete.go index cc5216325..b8336a32e 100644 --- a/CLI/controllers/delete.go +++ b/CLI/controllers/delete.go @@ -2,7 +2,6 @@ package controllers import ( "cli/models" - "cli/utils" "net/http" ) @@ -22,7 +21,7 @@ func (controller Controller) DeleteObj(path string) ([]string, error) { if pathSplit.Layer != nil { pathSplit.Layer.ApplyFilters(filters) } - body := utils.ComplexFilterToMap(filters["filter"]) + body := map[string]any{"filter": filters["filter"]} resp, err = controller.API.Request(http.MethodDelete, url, body, http.StatusOK) } else { resp, err = controller.API.Request(http.MethodDelete, url, nil, http.StatusOK) diff --git a/CLI/controllers/get.go b/CLI/controllers/get.go index a1b759481..4fb605d87 100644 --- a/CLI/controllers/get.go +++ b/CLI/controllers/get.go @@ -37,7 +37,7 @@ func (controller Controller) GetObjectsWildcard(pathStr string, filters map[stri } if complexFilter, ok := filters["filter"]; ok { - body := utils.ComplexFilterToMap(complexFilter) + body := map[string]any{"filter": complexFilter} method = "POST " resp, err = controller.API.Request(http.MethodPost, url, body, http.StatusOK) } else { diff --git a/CLI/controllers/get_test.go b/CLI/controllers/get_test.go index 1b6888449..30d3611d7 100644 --- a/CLI/controllers/get_test.go +++ b/CLI/controllers/get_test.go @@ -55,13 +55,7 @@ func TestGetWithComplexFilters(t *testing.T) { mockAPI, "id=BASIC.A.R1&namespace=physical.hierarchy", map[string]any{ - "$and": []map[string]any{ - {"category": "room"}, - {"$or": []map[string]any{ - {"name": "R1"}, - {"attributes.height": map[string]any{"$gt": "3"}}, - }}, - }, + "filter": "(category=room) & (name=R1 | height>3) ", }, []any{roomWithChildren}, ) diff --git a/CLI/controllers/layers_test.go b/CLI/controllers/layers_test.go index 172ef95a4..2b36ed008 100644 --- a/CLI/controllers/layers_test.go +++ b/CLI/controllers/layers_test.go @@ -261,7 +261,7 @@ func TestLsOnRacksLayerShowsRacks(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"category": "rack"}, []any{rack1, rack2}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "category=rack"}, []any{rack1, rack2}) objects, err := controller.Ls("/Physical/BASIC/A/R1/#racks", map[string]string{}, nil) assert.Nil(t, err) @@ -275,7 +275,7 @@ func TestLsOnGroupLayerShowsGroups(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"category": "group"}, []any{roomGroup}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "category=group"}, []any{roomGroup}) objects, err := controller.Ls("/Physical/BASIC/A/R1/#groups", map[string]string{}, nil) assert.Nil(t, err) @@ -288,7 +288,7 @@ func TestLsOnCorridorsLayerShowsCorridors(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"category": "corridor"}, []any{corridor}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "category=corridor"}, []any{corridor}) objects, err := controller.Ls("/Physical/BASIC/A/R1/#corridors", map[string]string{}, nil) assert.Nil(t, err) @@ -301,7 +301,7 @@ func TestLsOnGenericLayerShowsGeneric(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"category": "generic"}, []any{generic}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "category=generic"}, []any{generic}) objects, err := controller.Ls("/Physical/BASIC/A/R1/#generics", map[string]string{}, nil) assert.Nil(t, err) @@ -317,7 +317,7 @@ func TestLsOnDeviceTypeLayerShowsDevicesOfThatType(t *testing.T) { mockGetObjectsWithComplexFilters( mockAPI, "id=BASIC.A.R1.A01.*&namespace=physical.hierarchy", - map[string]any{"$and": []map[string]any{{"category": "device"}, {"attributes.type": "chassis"}}}, + map[string]any{"filter": "category=device&type=chassis"}, []any{chassis}, ) @@ -335,7 +335,7 @@ func TestLsOnGenericTypeLayerShowsDevicesOfThatType(t *testing.T) { mockGetObjectsWithComplexFilters( mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", - map[string]any{"$and": []map[string]any{{"category": "generic"}, {"attributes.type": "table"}}}, + map[string]any{"filter": "category=generic&type=table"}, []any{generic}, ) @@ -369,7 +369,7 @@ func TestLsOnNestedLayerWorks(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) mockGetObjectHierarchy(mockAPI, rack1) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.A01.*&namespace=physical.hierarchy", map[string]any{"category": "group"}, []any{rackGroup}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.A01.*&namespace=physical.hierarchy", map[string]any{"filter": "category=group"}, []any{rackGroup}) objects, err := controller.Ls("/Physical/BASIC/A/R1/#racks/A01/#groups", map[string]string{}, nil) assert.Nil(t, err) @@ -382,7 +382,7 @@ func TestGetOnRacksLayerGetsRacksAttributes(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"$and": []map[string]any{{"category": "rack"}, {"category": "rack"}}}, []any{rack1, rack2}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "(category=rack) & (category=rack)"}, []any{rack1, rack2}) objects, _, err := controller.GetObjectsWildcard("/Physical/BASIC/A/R1/#racks", map[string]string{}, nil) assert.Nil(t, err) @@ -396,7 +396,7 @@ func TestGetOnCorridorsLayerGetsCorridorsAttributes(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"$and": []map[string]any{{"category": "corridor"}, {"category": "corridor"}}}, []any{corridor}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "(category=corridor) & (category=corridor)"}, []any{corridor}) objects, _, err := controller.GetObjectsWildcard("/Physical/BASIC/A/R1/#corridors", map[string]string{}, nil) assert.Nil(t, err) @@ -409,7 +409,7 @@ func TestGetOnGroupLayerGetsGroupsAttributes(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"$and": []map[string]any{{"category": "group"}, {"category": "group"}}}, []any{roomGroup}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "(category=group) & (category=group)"}, []any{roomGroup}) objects, _, err := controller.GetObjectsWildcard("/Physical/BASIC/A/R1/#groups", map[string]string{}, nil) assert.Nil(t, err) @@ -422,7 +422,7 @@ func TestGetOnAllLayerGetsAllAttributes(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"$and": []map[string]any{{"category": "rack"}, {"category": "rack"}}}, []any{rack1, rack2}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "(category=rack) & (category=rack)"}, []any{rack1, rack2}) objects, _, err := controller.GetObjectsWildcard("/Physical/BASIC/A/R1/#racks/*", map[string]string{}, nil) assert.Nil(t, err) @@ -436,7 +436,7 @@ func TestGetOnWildcardLayerGetsAttributes(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.A*&namespace=physical.hierarchy", map[string]any{"$and": []map[string]any{{"category": "rack"}, {"category": "rack"}}}, []any{rack1}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.A*&namespace=physical.hierarchy", map[string]any{"filter": "(category=rack) & (category=rack)"}, []any{rack1}) objects, _, err := controller.GetObjectsWildcard("/Physical/BASIC/A/R1/#racks/A*", map[string]string{}, nil) assert.Nil(t, err) @@ -449,7 +449,7 @@ func TestGetOnLayerChildGetsAttributes(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.A01&namespace=physical.hierarchy", map[string]any{"$and": []map[string]any{{"category": "rack"}, {"category": "rack"}}}, []any{rack1}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.A01&namespace=physical.hierarchy", map[string]any{"filter": "(category=rack) & (category=rack)"}, []any{rack1}) objects, _, err := controller.GetObjectsWildcard("/Physical/BASIC/A/R1/#racks/A01", map[string]string{}, nil) assert.Nil(t, err) @@ -463,7 +463,7 @@ func TestGetOnNestedLayerGetsAttributes(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) mockGetObjectHierarchy(mockAPI, rack1) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.A01.*&namespace=physical.hierarchy", map[string]any{"$and": []map[string]any{{"category": "group"}, {"category": "group"}}}, []any{rackGroup}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.A01.*&namespace=physical.hierarchy", map[string]any{"filter": "(category=group) & (category=group)"}, []any{rackGroup}) objects, _, err := controller.GetObjectsWildcard("/Physical/BASIC/A/R1/#racks/A01/#groups", map[string]string{}, nil) assert.Nil(t, err) @@ -575,7 +575,7 @@ func TestSelectLayerSelectsAll(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"category": "rack"}, []any{rack1, rack2}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "category=rack"}, []any{rack1, rack2}) mockGetObject(mockAPI, rack1) mockGetObject(mockAPI, rack2) @@ -596,7 +596,7 @@ func TestSelectGroupsLayerSelectsAll(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"category": "group"}, []any{roomGroup}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "category=group"}, []any{roomGroup}) mockGetObject(mockAPI, roomGroup) mockOgree3D.On( @@ -615,7 +615,7 @@ func TestSelectLayerAllSelectsAll(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"category": "rack"}, []any{rack1, rack2}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "category=rack"}, []any{rack1, rack2}) mockGetObject(mockAPI, rack1) mockGetObject(mockAPI, rack2) @@ -636,7 +636,7 @@ func TestSelectLayerWildcardSelectsWildcard(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.A*&namespace=physical.hierarchy", map[string]any{"category": "rack"}, []any{rack1}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.A*&namespace=physical.hierarchy", map[string]any{"filter": "category=rack"}, []any{rack1}) mockGetObject(mockAPI, rack1) mockOgree3D.On( @@ -655,7 +655,7 @@ func TestSelectLayerChildSelectsChild(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.A01&namespace=physical.hierarchy", map[string]any{"category": "rack"}, []any{rack1}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.A01&namespace=physical.hierarchy", map[string]any{"filter": "category=rack"}, []any{rack1}) mockGetObject(mockAPI, rack1) mockOgree3D.On( @@ -675,7 +675,7 @@ func TestSelectNestedLayerSelectsAll(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) mockGetObjectHierarchy(mockAPI, rack1) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.A01.*&namespace=physical.hierarchy", map[string]any{"category": "group"}, []any{rackGroup}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.A01.*&namespace=physical.hierarchy", map[string]any{"filter": "category=group"}, []any{rackGroup}) mockGetObject(mockAPI, rackGroup) mockOgree3D.On( @@ -694,7 +694,7 @@ func TestRemoveLayerRemovesAllObjectsOfTheLayer(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockDeleteObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"category": "rack"}, []any{rack1, rack2}) + mockDeleteObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "category=rack"}, []any{rack1, rack2}) controllers.State.ObjsForUnity = controllers.SetObjsForUnity([]string{"all"}) @@ -707,7 +707,7 @@ func TestDrawLayerDrawsAllObjectsOfTheLayer(t *testing.T) { mockGetObjectHierarchy(mockAPI, roomWithChildren) mockGetObjectsByEntity(mockAPI, "layers", []any{}) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"category": "rack"}, []any{rack1, rack2}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "category=rack"}, []any{rack1, rack2}) mockGetObject(mockAPI, rack1) mockGetObject(mockAPI, rack2) @@ -731,7 +731,7 @@ func TestDrawLayerWithDepthDrawsAllObjectsOfTheLayerAndChildren(t *testing.T) { mockGetObjectHierarchy(mockAPI, roomWithChildren) mockGetObjectsByEntity(mockAPI, "layers", []any{}) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"category": "rack"}, []any{rack1, rack2}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "category=rack"}, []any{rack1, rack2}) mockGetObjectHierarchy(mockAPI, rack1) mockGetObjectHierarchy(mockAPI, rack2) @@ -755,7 +755,7 @@ func TestUndrawLayerUndrawAllObjectsOfTheLayer(t *testing.T) { mockGetObjectHierarchy(mockAPI, roomWithChildren) mockGetObjectsByEntity(mockAPI, "layers", []any{}) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"category": "rack"}, []any{rack1, rack2}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "category=rack"}, []any{rack1, rack2}) mockGetObject(mockAPI, rack1) mockGetObject(mockAPI, rack2) @@ -1277,7 +1277,7 @@ func TestLsOnLayerUpdatedAfterLastUpdateDoesUpdatedFilter(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{testLayer}) mockGetObjectHierarchy(mockAPI, roomWithoutChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"category": "rack"}, []any{}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "category = rack"}, []any{}) objects, err := controller.Ls("/Physical/BASIC/A/R1/#test", map[string]string{}, nil) assert.Nil(t, err) @@ -1295,7 +1295,7 @@ func TestLsOnLayerUpdatedAfterLastUpdateDoesUpdatedFilter(t *testing.T) { err = controller.UpdateLayer("/Logical/Layers/test", models.LayerFiltersAdd, "category = device") assert.Nil(t, err) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"category": "device"}, []any{}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "category = device"}, []any{}) objects, err = controller.Ls("/Physical/BASIC/A/R1/#test", map[string]string{}, nil) assert.Nil(t, err) @@ -1313,7 +1313,7 @@ func TestLsOnUserDefinedLayerAppliesFilters(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{testLayer}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"category": "rack"}, []any{rack1, rack2}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.*&namespace=physical.hierarchy", map[string]any{"filter": "category = rack"}, []any{rack1, rack2}) objects, err := controller.Ls("/Physical/BASIC/A/R1/#test", map[string]string{}, nil) assert.Nil(t, err) @@ -1333,7 +1333,7 @@ func TestLsRecursiveOnLayerListLayerRecursive(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{devices}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.**.*&namespace=physical.hierarchy", map[string]any{"category": "device"}, []any{chassis, pdu}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.**.*&namespace=physical.hierarchy", map[string]any{"filter": "category = device"}, []any{chassis, pdu}) objects, err := controller.Ls("/Physical/BASIC/A/R1/#devices", map[string]string{}, &controllers.RecursiveParams{MaxDepth: models.UnlimitedDepth}) assert.Nil(t, err) @@ -1353,7 +1353,7 @@ func TestGetRecursiveOnLayerReturnsLayerRecursive(t *testing.T) { mockGetObjectsByEntity(mockAPI, "layers", []any{devices}) mockGetObjectHierarchy(mockAPI, roomWithChildren) - mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.**.*&namespace=physical.hierarchy", map[string]any{"category": "device"}, []any{chassis, pdu}) + mockGetObjectsWithComplexFilters(mockAPI, "id=BASIC.A.R1.**.*&namespace=physical.hierarchy", map[string]any{"filter": "category = device"}, []any{chassis, pdu}) objects, _, err := controller.GetObjectsWildcard("/Physical/BASIC/A/R1/#devices", nil, &controllers.RecursiveParams{MaxDepth: models.UnlimitedDepth}) assert.Nil(t, err) diff --git a/CLI/controllers/ls.go b/CLI/controllers/ls.go index 95ac2a96d..49e68190a 100644 --- a/CLI/controllers/ls.go +++ b/CLI/controllers/ls.go @@ -2,7 +2,6 @@ package controllers import ( "cli/models" - "cli/utils" "errors" "fmt" "net/http" @@ -69,7 +68,7 @@ func (controller Controller) lsObjectsWithFilters(path string, filters map[strin var resp *Response if complexFilter, ok := filters["filter"]; ok { - body := utils.ComplexFilterToMap(complexFilter) + body := map[string]any{"filter": complexFilter} resp, err = controller.API.Request(http.MethodPost, url, body, http.StatusOK) } else { resp, err = controller.API.Request(http.MethodGet, url, map[string]any{}, http.StatusOK) diff --git a/CLI/controllers/ls_test.go b/CLI/controllers/ls_test.go index 849bb5a35..12102cd7f 100644 --- a/CLI/controllers/ls_test.go +++ b/CLI/controllers/ls_test.go @@ -108,13 +108,7 @@ func TestLsWithComplexFilters(t *testing.T) { mockAPI, "id=BASIC.A.R1.%2A&namespace=physical.hierarchy", map[string]any{ - "$or": []map[string]any{ - {"category": "corridor"}, - {"$and": []map[string]any{ - {"category": "rack"}, - {"name": map[string]any{"$not": "A01"}}, - }}, - }, + "filter": "(category=corridor) | (category=rack & name!=A01) ", }, []any{corridor, rack2}, ) diff --git a/CLI/controllers/sendSchemaController_test.go b/CLI/controllers/sendSchemaController_test.go new file mode 100644 index 000000000..2ae7b6067 --- /dev/null +++ b/CLI/controllers/sendSchemaController_test.go @@ -0,0 +1,119 @@ +package controllers_test + +import ( + "cli/controllers" + l "cli/logger" + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + l.InitLogs() +} + +func TestMergeMaps(t *testing.T) { + x := map[string]any{ + "a": "10", + "b": "11", + } + y := map[string]any{ + "b": "25", + "c": "40", + } + testMap := copyMap(x) + controllers.MergeMaps(testMap, y, false) + assert.Contains(t, testMap, "a") + assert.Contains(t, testMap, "b") + assert.Contains(t, testMap, "c") + assert.Equal(t, x["a"], testMap["a"]) + assert.Equal(t, x["b"], testMap["b"]) + assert.Equal(t, y["c"], testMap["c"]) + + testMap = copyMap(x) + controllers.MergeMaps(testMap, y, true) + assert.Contains(t, testMap, "a") + assert.Contains(t, testMap, "b") + assert.Contains(t, testMap, "c") + assert.Equal(t, x["a"], testMap["a"]) + assert.Equal(t, y["b"], testMap["b"]) + assert.Equal(t, y["c"], testMap["c"]) +} + +func TestGenerateFilteredJson(t *testing.T) { + controllers.State.DrawableJsons = map[string]map[string]any{ + "rack": map[string]any{ + "name": true, + "parentId": true, + "category": true, + "description": false, + "domain": true, + "attributes": map[string]any{ + "color": true, + }, + }, + } + object := map[string]any{ + "name": "rack", + "parentId": "site.building.room", + "category": "rack", + "description": "", + "domain": "domain", + "attributes": map[string]any{ + "color": "aaaaaa", + }, + } + + filteredObject := controllers.GenerateFilteredJson(object) + + assert.Contains(t, filteredObject, "name") + assert.Contains(t, filteredObject, "parentId") + assert.Contains(t, filteredObject, "category") + assert.Contains(t, filteredObject, "domain") + assert.NotContains(t, filteredObject, "description") + assert.Contains(t, filteredObject, "attributes") + assert.Contains(t, filteredObject["attributes"], "color") +} + +func TestStringify(t *testing.T) { + assert.Equal(t, "text", controllers.Stringify("text")) + assert.Equal(t, "35", controllers.Stringify(35)) + assert.Equal(t, "35", controllers.Stringify(35.0)) + assert.Equal(t, "true", controllers.Stringify(true)) + assert.Equal(t, "hello,world", controllers.Stringify([]string{"hello", "world"})) + assert.Equal(t, "[45,21]", controllers.Stringify([]float64{45, 21})) + assert.Equal(t, "[hello,5,[world,450]]", controllers.Stringify([]any{"hello", 5, []any{"world", 450}})) + assert.Equal(t, "", controllers.Stringify(map[string]any{"hello": 5})) +} + +func TestExpandSlotVector(t *testing.T) { + slots, err := controllers.ExpandSlotVector([]string{"slot1..slot3", "slot4"}) + assert.Nil(t, slots) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid device syntax: .. can only be used in a single element vector") + + slots, err = controllers.ExpandSlotVector([]string{"slot1..slot3..slot7"}) + assert.Nil(t, slots) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid device syntax: incorrect use of .. for slot") + + slots, err = controllers.ExpandSlotVector([]string{"slot1..slots3"}) + assert.Nil(t, slots) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid device syntax: incorrect use of .. for slot") + + slots, err = controllers.ExpandSlotVector([]string{"slot1..slotE"}) + assert.Nil(t, slots) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid device syntax: incorrect use of .. for slot") + + slots, err = controllers.ExpandSlotVector([]string{"slot1..slot3"}) + assert.Nil(t, err) + assert.NotNil(t, slots) + assert.EqualValues(t, []string{"slot1", "slot2", "slot3"}, slots) + + slots, err = controllers.ExpandSlotVector([]string{"slot1", "slot3"}) + assert.Nil(t, err) + assert.NotNil(t, slots) + assert.EqualValues(t, []string{"slot1", "slot3"}, slots) +} diff --git a/CLI/controllers/template.go b/CLI/controllers/template.go index da7aa86ee..376b770f2 100644 --- a/CLI/controllers/template.go +++ b/CLI/controllers/template.go @@ -107,14 +107,9 @@ func (controller Controller) ApplyTemplate(attr, data map[string]interface{}, en } else if val, ok := sizeInf[2].(int); ok { res = int((float64(val) / 1000) / RACKUNIT) } else { - //Resort to default value - msg := "Warning, invalid value provided for" + - " sizeU. Defaulting to 5" - println(msg) - res = int((5 / 1000) / RACKUNIT) + return errors.New("invalid size vector on given template") } attr["sizeU"] = strconv.Itoa(res) - } } } @@ -248,9 +243,9 @@ func (controller Controller) ApplyTemplate(attr, data map[string]interface{}, en } } else { if State.DebugLvl > 1 { - println("Warning, invalid size value in template.", - "Default values will be assigned") + println("Warning, invalid size value in template.") } + return errors.New("invalid size vector on given template") } } else { //Serialise size and posXY if given diff --git a/CLI/controllers/template_test.go b/CLI/controllers/template_test.go index 460fdbbfb..4d15d6d5f 100644 --- a/CLI/controllers/template_test.go +++ b/CLI/controllers/template_test.go @@ -1,6 +1,9 @@ package controllers_test import ( + "cli/controllers" + "cli/models" + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -26,3 +29,194 @@ func TestCreateTemplateOfTypeGenericWorks(t *testing.T) { err := controller.LoadTemplate(template) assert.Nil(t, err) } + +func TestApplyTemplateOfTypeDeviceWorks(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + device := copyMap(chassis) + attributes := map[string]any{ + "template": "device-template", + "TDP": "", + "TDPmax": "", + "fbxModel": "https://github.com/test.fbx", + "height": "40.1", + "heightUnit": "mm", + "model": "TNF2LTX", + "orientation": "front", + "partNumber": "0303XXXX", + "size": "[388.4, 205.9]", + "sizeUnit": "mm", + "weightKg": "1.81", + } + device["attributes"] = attributes + template := map[string]any{ + "slug": "device-template", + "description": "", + "category": "device", + "sizeWDHmm": []any{216, 659, 100}, + "fbxModel": "", + "attributes": map[string]any{ + "type": "chassis", + "vendor": "IBM", + }, + "colors": []any{}, + "components": []any{}, + } + + mockGetObjTemplate(mockAPI, template) + + sizeU := int((float64(template["sizeWDHmm"].([]any)[2].(int)) / 1000) / controllers.RACKUNIT) + err := controller.ApplyTemplate(attributes, device, models.DEVICE) + assert.Nil(t, err) + + // we verify if the template was applied + assert.Equal(t, "100", device["attributes"].(map[string]any)["height"]) + assert.Equal(t, template["attributes"].(map[string]any)["type"], device["attributes"].(map[string]any)["type"]) + assert.Equal(t, template["attributes"].(map[string]any)["vendor"], device["attributes"].(map[string]any)["vendor"]) + assert.Equal(t, "[216, 659]", device["attributes"].(map[string]any)["size"]) + assert.Equal(t, strconv.Itoa(sizeU), device["attributes"].(map[string]any)["sizeU"]) +} + +func TestApplyTemplateOfTypeDeviceError(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + device := copyMap(chassis) + attributes := map[string]any{ + "template": "device-template", + } + device["attributes"] = attributes + template := map[string]any{ + "slug": "device-template", + "description": "", + "category": "device", + "sizeWDHmm": []any{216, 659, "100"}, + "fbxModel": "", + "attributes": map[string]any{ + "type": "chassis", + "vendor": "IBM", + }, + "colors": []any{}, + "components": []any{}, + } + + mockGetObjTemplate(mockAPI, template) + + err := controller.ApplyTemplate(attributes, device, models.DEVICE) + assert.NotNil(t, err) + + assert.Equal(t, "invalid size vector on given template", err.Error()) +} + +func TestApplyTemplateOfTypeRoomWorks(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + room := copyMap(roomWithoutChildren) + attributes := map[string]any{ + "template": "room-template", + "height": "2.8", + "heightUnit": "m", + "rotation": "-90", + "posXY": "[0, 0]", + "posXYUnit": "m", + } + room["attributes"] = attributes + template := map[string]any{ + "slug": "room-template", + "category": "room", + "axisOrientation": "+x+y", + "sizeWDHm": []any{216, 659, 41}, + "floorUnit": "m", + "technicalArea": []string{"front", "front", "front", "front"}, + "reservedArea": []string{"front", "front", "front", "front"}, + "vertices": []any{0, 0, 0}, + "tileAngle": 0, + "separators": map[string]any{ + "sepname": map[string]any{ + "startPosXYm": []any{0, 0}, + "endPosXYm": []any{0, 0}, + "type": "wireframe|plain", + }, + }, + "pillars": map[string]any{ + "pillarname": map[string]any{ + "centerXY": []any{0, 0}, + "sizeXY": []any{0, 0}, + "rotation": 0, + }, + }, + "tiles": []any{ + map[string]any{ + "location": "0/0", + "name": "my-tile", + "label": "my-tile", + "texture": "", + "color": "00ED00", + }, + }, + "colors": []any{"my-color"}, + // "rows" : [], + // "center" : [0,0], + } + + mockGetRoomTemplate(mockAPI, template) + + err := controller.ApplyTemplate(attributes, room, models.ROOM) + assert.Nil(t, err) + + // we verify if the template was applied + assert.Equal(t, "41", room["attributes"].(map[string]any)["height"]) + assert.Equal(t, "[216, 659]", room["attributes"].(map[string]any)["size"]) + assert.Equal(t, template["axisOrientation"], room["attributes"].(map[string]any)["axisOrientation"]) + assert.Equal(t, template["floorUnit"], room["attributes"].(map[string]any)["floorUnit"]) + assert.Equal(t, "[0,0,0]", room["attributes"].(map[string]any)["vertices"]) + assert.Equal(t, "[\"my-color\"]", room["attributes"].(map[string]any)["colors"]) +} + +func TestLoadTemplateRoom(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + template := map[string]any{ + "slug": "room-example", + "description": "room example", + "category": "room", + "sizeWDHm": []any{216, 659, 41}, + } + + mockCreateObject(mockAPI, "room-template", template) + + err := controller.LoadTemplate(template) + assert.Nil(t, err) +} + +func TestLoadTemplateBuilding(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + template := map[string]any{ + "slug": "building-example", + "description": "building example", + "category": "building", + "sizeWDHm": []any{216, 659, 41}, + "center": []any{0, 0}, + } + + mockCreateObject(mockAPI, "bldg-template", template) + + err := controller.LoadTemplate(template) + assert.Nil(t, err) +} + +func TestLoadTemplateInvalidCategory(t *testing.T) { + controller, _, _ := layersSetup(t) + + template := map[string]any{ + "slug": "invalid-example", + "description": "invalid example", + "category": "invalid", + "sizeWDHm": []any{216, 659, 41}, + "center": []any{0, 0}, + } + + err := controller.LoadTemplate(template) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "this template does not have a valid category. Please add a category attribute with a value of building, room, rack, device or generic") +} diff --git a/CLI/controllers/update_test.go b/CLI/controllers/update_test.go index 918f804cd..8abb9e330 100644 --- a/CLI/controllers/update_test.go +++ b/CLI/controllers/update_test.go @@ -3,6 +3,7 @@ package controllers_test import ( "cli/controllers" "cli/models" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -67,3 +68,184 @@ func TestUpdateTagSlug(t *testing.T) { _, err := controller.UpdateObj(path, dataUpdate, false) assert.Nil(t, err) } + +func TestUpdateRoomTilesColor(t *testing.T) { + controller, mockAPI, mockOgree3D, _ := newControllerWithMocks(t) + + room := copyMap(roomWithoutChildren) + room["attributes"] = map[string]any{ + "tilesColor": "aaaaaa", + } + newColor := "aaaaab" + updatedRoom := copyMap(room) + updatedRoom["attributes"].(map[string]any)["tilesColor"] = newColor + dataUpdate := updatedRoom["attributes"].(map[string]any) + entity := models.ROOM + + path := "/Physical/" + strings.Replace(room["id"].(string), ".", "/", -1) + message := map[string]any{ + "type": "interact", + "data": map[string]any{ + "id": room["id"], + "param": "tilesColor", + "value": newColor, + }, + } + + mockOgree3D.On("InformOptional", "UpdateObj", entity, message).Return(nil) + + mockGetObject(mockAPI, room) + + dataUpdated := copyMap(room) + dataUpdated["attributes"].(map[string]any)["tilesColor"] = newColor + + mockUpdateObject(mockAPI, dataUpdate, dataUpdated) + + controllers.State.ObjsForUnity = controllers.SetObjsForUnity([]string{"all"}) + + result, err := controller.UpdateObj(path, dataUpdate, false) + assert.Nil(t, err) + assert.Equal(t, result["data"].(map[string]any)["attributes"].(map[string]any)["tilesColor"], newColor) + mockOgree3D.AssertCalled(t, "InformOptional", "UpdateObj", entity, message) +} + +func TestUpdateRoomTilesName(t *testing.T) { + controller, mockAPI, mockOgree3D, _ := newControllerWithMocks(t) + + room := copyMap(roomWithoutChildren) + room["attributes"] = map[string]any{ + "tilesName": "t1", + } + newName := "t2" + updatedRoom := copyMap(room) + updatedRoom["attributes"].(map[string]any)["tilesName"] = newName + dataUpdate := updatedRoom["attributes"].(map[string]any) + entity := models.ROOM + + path := "/Physical/" + strings.Replace(room["id"].(string), ".", "/", -1) + message := map[string]any{ + "type": "interact", + "data": map[string]any{ + "id": room["id"], + "param": "tilesName", + "value": newName, + }, + } + + mockOgree3D.On("InformOptional", "UpdateObj", entity, message).Return(nil) + + mockGetObject(mockAPI, room) + + dataUpdated := copyMap(room) + dataUpdated["attributes"].(map[string]any)["tilesName"] = newName + + mockUpdateObject(mockAPI, dataUpdate, dataUpdated) + + controllers.State.ObjsForUnity = controllers.SetObjsForUnity([]string{"all"}) + + result, err := controller.UpdateObj(path, dataUpdate, false) + assert.Nil(t, err) + assert.Equal(t, result["data"].(map[string]any)["attributes"].(map[string]any)["tilesName"], newName) + mockOgree3D.AssertCalled(t, "InformOptional", "UpdateObj", entity, message) +} + +func TestUpdateRackU(t *testing.T) { + controller, mockAPI, mockOgree3D, _ := newControllerWithMocks(t) + rack := copyMap(rack2) + rack["attributes"] = map[string]any{ + "U": true, + } + updatedRack := copyMap(rack) + updatedRack["attributes"].(map[string]any)["U"] = false + dataUpdate := updatedRack["attributes"].(map[string]any) + entity := models.RACK + + path := "/Physical/" + strings.Replace(rack["id"].(string), ".", "/", -1) + message := map[string]any{ + "type": "interact", + "data": map[string]any{ + "id": rack["id"], + "param": "U", + "value": false, + }, + } + + mockOgree3D.On("InformOptional", "UpdateObj", entity, message).Return(nil) + + mockGetObject(mockAPI, rack) + mockUpdateObject(mockAPI, dataUpdate, updatedRack) + + controllers.State.ObjsForUnity = controllers.SetObjsForUnity([]string{"all"}) + + result, err := controller.UpdateObj(path, dataUpdate, false) + assert.Nil(t, err) + assert.False(t, result["data"].(map[string]any)["attributes"].(map[string]any)["U"].(bool)) + mockOgree3D.AssertCalled(t, "InformOptional", "UpdateObj", entity, message) +} + +func TestUpdateDeviceAlpha(t *testing.T) { + controller, mockAPI, mockOgree3D, _ := newControllerWithMocks(t) + device := copyMap(chassis) + device["attributes"].(map[string]any)["alpha"] = true + updatedDevice := copyMap(device) + updatedDevice["attributes"].(map[string]any)["alpha"] = false + dataUpdate := updatedDevice["attributes"].(map[string]any) + entity := models.DEVICE + + path := "/Physical/" + strings.Replace(device["id"].(string), ".", "/", -1) + message := map[string]any{ + "type": "interact", + "data": map[string]any{ + "id": device["id"], + "param": "alpha", + "value": false, + }, + } + + mockOgree3D.On("InformOptional", "UpdateObj", entity, message).Return(nil) + + mockGetObject(mockAPI, device) + mockUpdateObject(mockAPI, dataUpdate, updatedDevice) + + controllers.State.ObjsForUnity = controllers.SetObjsForUnity([]string{"all"}) + + result, err := controller.UpdateObj(path, dataUpdate, false) + assert.Nil(t, err) + assert.False(t, result["data"].(map[string]any)["attributes"].(map[string]any)["alpha"].(bool)) + mockOgree3D.AssertCalled(t, "InformOptional", "UpdateObj", entity, message) +} + +func TestUpdateGroupContent(t *testing.T) { + controller, mockAPI, mockOgree3D, _ := newControllerWithMocks(t) + group := copyMap(rackGroup) + group["attributes"] = map[string]any{ + "content": "A,B", + } + newValue := "A,B,C" + updatedGroup := copyMap(group) + updatedGroup["attributes"].(map[string]any)["content"] = newValue + dataUpdate := updatedGroup["attributes"].(map[string]any) + entity := models.GROUP + + path := "/Physical/" + strings.Replace(group["id"].(string), ".", "/", -1) + message := map[string]any{ + "type": "interact", + "data": map[string]any{ + "id": group["id"], + "param": "content", + "value": newValue, + }, + } + + mockOgree3D.On("InformOptional", "UpdateObj", entity, message).Return(nil) + + mockGetObject(mockAPI, group) + mockUpdateObject(mockAPI, dataUpdate, updatedGroup) + + controllers.State.ObjsForUnity = controllers.SetObjsForUnity([]string{"all"}) + + result, err := controller.UpdateObj(path, dataUpdate, false) + assert.Nil(t, err) + assert.Equal(t, result["data"].(map[string]any)["attributes"].(map[string]any)["content"].(string), newValue) + mockOgree3D.AssertCalled(t, "InformOptional", "UpdateObj", entity, message) +} diff --git a/CLI/lexer_test.go b/CLI/lexer_test.go index 754e99198..5dc4c314d 100644 --- a/CLI/lexer_test.go +++ b/CLI/lexer_test.go @@ -2,8 +2,70 @@ package main import ( "testing" + + "github.com/stretchr/testify/assert" ) +func TestTokenTypeString(t *testing.T) { + tokenStrings := map[tokenType]string{ + tokEOF: "eof", + tokDeref: "deref", + tokInt: "int", + tokFloat: "float", + tokBool: "bool", + tokDoubleQuote: "doublQuote", + tokLeftBrac: "leftBrac", + tokRightBrac: "rightBrac", + tokComma: "comma", + tokSemiCol: "semicol", + tokAt: "at", + tokLeftParen: "leftParen", + tokRightParen: "rightParen", + tokNot: "not", + tokAdd: "add", + tokSub: "sub", + tokMul: "mul", + tokDiv: "div", + tokIntDiv: "intdiv", + tokMod: "mod", + tokOr: "or", + tokAnd: "and", + tokEq: "eq", + tokNeq: "neq", + tokLeq: "leq", + tokGeq: "geq", + tokGtr: "gtr", + tokLss: "lss", + tokColor: "color", + tokText: "text", + } + + for key, value := range tokenStrings { + assert.Equal(t, value, key.String()) + } +} + +func TestTokenTypePrecedence(t *testing.T) { + precedenceMap := map[int][]tokenType{ + 1: []tokenType{tokOr}, + 2: []tokenType{tokAnd}, + 3: []tokenType{tokEq, tokNeq, tokLss, tokLeq, tokGtr, tokGeq}, + 4: []tokenType{tokAdd, tokSub}, + 5: []tokenType{tokMul, tokDiv, tokIntDiv, tokMod}, + 6: []tokenType{tokNot}, + 0: []tokenType{tokEOF}, + } + + for key, value := range precedenceMap { + for _, tokType := range value { + tok := token{ + t: tokType, + } + assert.Equal(t, key, tok.precedence()) + } + } +} + func checkTokSequence(lexFunc func() token, expectedTypes []tokenType, expectedVals []any, t *testing.T) { for i := 0; i < len(expectedTypes); i++ { tok := lexFunc() @@ -37,3 +99,40 @@ func TestLexFormattedString(t *testing.T) { expectedVals := []any{"a", nil, "ab", nil} checkTokSequence(p.parseUnquotedStringToken, expectedTypes, expectedVals, t) } + +func TestParserParseExprToken(t *testing.T) { + p := newParser("\"[],()+-*/\\%") + simpleTokens := []tokenType{tokDoubleQuote, tokLeftBrac, tokRightBrac, tokComma, tokLeftParen, tokRightParen, tokAdd, tokSub, tokMul, tokDiv, tokIntDiv, tokMod} + for _, value := range simpleTokens { + tok := p.parseExprToken() + assert.Equal(t, value, tok.t) + } + + // other cases + tokensMap := map[string]tokenType{ + "${var}": tokDeref, + "$((": tokLeftEval, + "||": tokOr, + "|": tokEOF, + "&": tokEOF, + "&&": tokAnd, + "=": tokEOF, + "==": tokEq, + "!=": tokNeq, + "!": tokNot, + "<": tokLss, + "<=": tokLeq, + ">": tokGtr, + ">=": tokGeq, + "10": tokInt, + "1.0": tokFloat, + "true": tokBool, + "format": tokFormat, + "abc": tokEOF, + } + for key, value := range tokensMap { + p = newParser(key) + tok := p.parseExprToken() + assert.Equal(t, value, tok.t) + } +} diff --git a/CLI/models/entity_test.go b/CLI/models/entity_test.go new file mode 100644 index 000000000..994e79f8b --- /dev/null +++ b/CLI/models/entity_test.go @@ -0,0 +1,64 @@ +package models_test + +import ( + "cli/models" + "testing" + + "github.com/stretchr/testify/assert" +) + +var entityStrings = map[int][]string{ + models.DOMAIN: []string{"domain"}, + models.SITE: []string{"site", "si"}, + models.BLDG: []string{"building", "bldg", "bd"}, + models.ROOM: []string{"room", "ro"}, + models.RACK: []string{"rack", "rk"}, + models.DEVICE: []string{"device", "dv", "dev"}, + models.AC: []string{"ac"}, + models.PWRPNL: []string{"panel", "pn"}, + models.STRAY_DEV: []string{"stray_device"}, + models.ROOMTMPL: []string{"room_template"}, + models.OBJTMPL: []string{"obj_template"}, + models.BLDGTMPL: []string{"bldg_template"}, + models.CABINET: []string{"cabinet", "cb"}, + models.GROUP: []string{"group", "gr"}, + models.CORRIDOR: []string{"corridor", "co"}, + models.TAG: []string{"tag"}, + models.LAYER: []string{"layer"}, + models.GENERIC: []string{"generic", "ge"}, +} + +func TestEntityToString(t *testing.T) { + invalidValue := 100 + for key, values := range entityStrings { + assert.Equal(t, values[0], models.EntityToString(key)) + } + assert.Equal(t, "INVALID", models.EntityToString(invalidValue)) +} + +func TestEntityStrToInt(t *testing.T) { + invalidValue := "invalid_value" + for key, values := range entityStrings { + for _, value := range values { + assert.Equal(t, key, models.EntityStrToInt(value)) + } + } + assert.Equal(t, -1, models.EntityStrToInt(invalidValue)) +} + +func TestGetParentOfEntity(t *testing.T) { + negativeOneCases := []int{models.ROOMTMPL, models.BLDGTMPL, models.OBJTMPL, models.GROUP} + for _, value := range negativeOneCases { + assert.Equal(t, -1, models.GetParentOfEntity(value)) + } + hierarchyCases := []int{models.SITE, models.BLDG, models.ROOM, models.DEVICE} + for _, value := range hierarchyCases { + assert.Equal(t, value-1, models.GetParentOfEntity(value)) + } + roomCases := []int{models.RACK, models.AC, models.PWRPNL, models.CABINET, models.CORRIDOR, models.GENERIC} + for _, value := range roomCases { + assert.Equal(t, models.ROOM, models.GetParentOfEntity(value)) + } + invalidValue := 100 + assert.Equal(t, -3, models.GetParentOfEntity(invalidValue)) +} diff --git a/CLI/models/layer_test.go b/CLI/models/layer_test.go new file mode 100644 index 000000000..69e074b71 --- /dev/null +++ b/CLI/models/layer_test.go @@ -0,0 +1,146 @@ +package models_test + +import ( + "cli/models" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/exp/slices" +) + +func TestAutomaticLayerName(t *testing.T) { + assert.Equal(t, "#racks", models.RacksLayer.Name()) +} + +func TestAutomaticLayerApplyFilters(t *testing.T) { + filters := map[string]string{} + models.RacksLayer.ApplyFilters(filters) + assert.Contains(t, filters, "filter") + assert.Equal(t, "category=rack", filters["filter"]) + + filters["filter"] = "name=A01" + models.RacksLayer.ApplyFilters(filters) + assert.Contains(t, filters, "filter") + assert.Equal(t, "(name=A01) & (category=rack)", filters["filter"]) +} + +func TestUserDefinedLayerName(t *testing.T) { + layerName := "racks" + layer := models.UserDefinedLayer{ + Slug: layerName, + Applicability: "BASIC.A.R1", + Filter: "category=rack", + } + assert.Equal(t, "#"+layerName, layer.Name()) +} + +func TestUserDefinedLayerMatches(t *testing.T) { + layerName := "#racks" + layer := models.UserDefinedLayer{ + Slug: layerName, + Applicability: "BASIC.A.R*.**", + Filter: "category=rack", + } + assert.True(t, layer.Matches(models.PhysicalPath+"BASIC/A/R1/A01")) + assert.True(t, layer.Matches(models.PhysicalPath+"BASIC/A/R2/A01")) + assert.True(t, layer.Matches(models.PhysicalPath+"BASIC/A/R1/A01/D01")) + assert.False(t, layer.Matches(models.PhysicalPath+"BASIC/A/U1/A01")) +} + +func TestUserDefinedLayerApplyFilters(t *testing.T) { + layerName := "#racks" + layer := models.UserDefinedLayer{ + Slug: layerName, + Applicability: "BASIC.A.R*.**", + Filter: "category=rack", + } + filters := map[string]string{} + layer.ApplyFilters(filters) + assert.Contains(t, filters, "filter") + assert.Equal(t, layer.Filter, filters["filter"]) + + filters["filter"] = "name=R01" + layer.ApplyFilters(filters) + assert.Contains(t, filters, "filter") + assert.Equal(t, "(name=R01) & (category=rack)", filters["filter"]) +} + +func TestLayerByCategoryFromObjects(t *testing.T) { + objects := []any{ + map[string]any{ + "category": "device", + }, + map[string]any{ + "category": "group", + }, + } + automaticLayers := models.GroupsLayerFactory.FromObjects(objects) + assert.Len(t, automaticLayers, 1) + assert.Equal(t, "#groups", automaticLayers[0].Name()) + + objects = slices.Delete(objects, 1, 2) // we delete the group + automaticLayers = models.GroupsLayerFactory.FromObjects(objects) + assert.Len(t, automaticLayers, 0) +} + +func TestLayerByAttributeFromObjects(t *testing.T) { + objects := []any{ + map[string]any{ + "category": "device", + "attributes": map[string]any{ + "type": "blade", + }, + }, + map[string]any{ + "category": "device", + "attributes": map[string]any{ + "type": "blade", + }, + }, + map[string]any{ + "category": "device", + "attributes": map[string]any{ + "type": "table", + }, + }, + } + results := map[string]string{ + "#tables": "category=device&type=table", + "#blades": "category=device&type=blade", + } + automaticLayers := models.DeviceTypeLayers.FromObjects(objects) + assert.Len(t, automaticLayers, 2) + for key, value := range results { + filters := map[string]string{} + index := slices.IndexFunc(automaticLayers, func(e models.AutomaticLayer) bool { return e.Name() == key }) + assert.True(t, index >= 0) + automaticLayers[index].ApplyFilters(filters) + assert.Equal(t, value, filters["filter"]) + } +} + +func TestIsIDElementLayer(t *testing.T) { + assert.True(t, models.IsIDElementLayer("#layer-id")) + assert.False(t, models.IsIDElementLayer("id")) +} + +func TestIsObjectIDLayer(t *testing.T) { + assert.True(t, models.IsObjectIDLayer("room1.#racks")) + assert.False(t, models.IsObjectIDLayer("room1.rack1")) +} + +func TestPathIsLayer(t *testing.T) { + assert.True(t, models.PathIsLayer("/site/building/room1/room1/#racks")) + assert.False(t, models.PathIsLayer("/site/building/room1/room1/rack1")) +} + +func TestPathHasLayer(t *testing.T) { + assert.True(t, models.PathHasLayer("/site/building/room1/room1/#racks/rack1")) + assert.False(t, models.PathHasLayer("/site/building/room1/room1/rack1")) +} + +func TestPathRemoveLayer(t *testing.T) { + expectedPath := "/site/building/room1/room1/rack1" + assert.Equal(t, expectedPath, models.PathRemoveLayer("/site/building/room1/room1/#racks/rack1")) + assert.Equal(t, expectedPath, models.PathRemoveLayer("/site/building/room1/room1/rack1")) +} diff --git a/CLI/models/path_test.go b/CLI/models/path_test.go new file mode 100644 index 000000000..eb009a82a --- /dev/null +++ b/CLI/models/path_test.go @@ -0,0 +1,111 @@ +package models_test + +import ( + "cli/models" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMakeRecursive(t *testing.T) { + id := "site.building.room.rack" + path := models.Path{ + Prefix: models.PhysicalPath, + ObjectID: id, + } + err := path.MakeRecursive(2, 1, models.PhysicalPath+"site") + assert.NotNil(t, err) + assert.ErrorContains(t, err, "max depth cannot be less than the min depth") + + err = path.MakeRecursive(1, 2, models.PhysicalPath+"site") + assert.Nil(t, err) + assert.Equal(t, "**{1,2}."+id, path.ObjectID) + + path.ObjectID = id + err = path.MakeRecursive(1, -1, models.PhysicalPath+"site") + assert.Nil(t, err) + assert.Equal(t, "**{1,}."+id, path.ObjectID) + + id += ".*" + path.ObjectID = id + err = path.MakeRecursive(1, 2, models.PhysicalPath+"site") + assert.Nil(t, err) + assert.Equal(t, strings.Replace(id, ".*", ".**{1,2}.*", -1), path.ObjectID) +} + +func TestIsPhysical(t *testing.T) { + assert.True(t, models.IsPhysical(models.PhysicalPath+"site1")) + assert.False(t, models.IsPhysical(models.OrganisationPath+"domain1/")) +} + +func TestIsStray(t *testing.T) { + assert.True(t, models.IsStray(models.StrayPath+"stray-device1")) + assert.False(t, models.IsStray(models.OrganisationPath+"domain1")) +} + +func TestIsObjectTemplate(t *testing.T) { + assert.True(t, models.IsObjectTemplate(models.ObjectTemplatesPath+"template1")) + assert.False(t, models.IsObjectTemplate(models.OrganisationPath+"domain1")) +} + +func TestIsRoomTemplate(t *testing.T) { + assert.True(t, models.IsRoomTemplate(models.RoomTemplatesPath+"template1")) + assert.False(t, models.IsRoomTemplate(models.OrganisationPath+"domain1")) +} + +func TestIsBuildingTemplate(t *testing.T) { + assert.True(t, models.IsBuildingTemplate(models.BuildingTemplatesPath+"template1")) + assert.False(t, models.IsBuildingTemplate(models.OrganisationPath+"domain1")) +} + +func TestIsTag(t *testing.T) { + assert.True(t, models.IsTag(models.TagsPath+"tag1")) + assert.False(t, models.IsTag(models.OrganisationPath+"domain1")) +} + +func TestIsLayer(t *testing.T) { + assert.True(t, models.IsLayer(models.LayersPath+"layer1")) + assert.False(t, models.IsLayer(models.OrganisationPath+"domain1")) +} + +func TestIsGroup(t *testing.T) { + assert.True(t, models.IsGroup(models.GroupsPath+"group1")) + assert.False(t, models.IsGroup(models.OrganisationPath+"domain1")) +} + +func TestSplitPath(t *testing.T) { + parts := models.SplitPath(models.PhysicalPath + "site1/building") + assert.Len(t, parts, 4) // first element is nil -> [,Physical,site1,room] + assert.Equal(t, "site1", parts[2]) + assert.Equal(t, "building", parts[3]) +} + +func TestJoinPath(t *testing.T) { + path := models.JoinPath([]string{models.PhysicalPath, "site1", "building"}) + assert.Equal(t, models.PhysicalPath+"/site1/building", path) +} + +func TestPhysicalPathToObjectID(t *testing.T) { + path := models.PhysicalPathToObjectID(models.PhysicalPath + "site1/building") + assert.Equal(t, "site1.building", path) +} + +func TestPhysicalIDToPath(t *testing.T) { + path := models.PhysicalIDToPath("site1.building") + assert.Equal(t, models.PhysicalPath+"site1/building", path) +} + +func TestPathRemoveLast(t *testing.T) { + path := models.PhysicalPath + "site1/building/room" + assert.Equal(t, models.PhysicalPath+"site1/building", models.PathRemoveLast(path, 1)) + assert.Equal(t, path, models.PathRemoveLast(path, 0)) + assert.Equal(t, models.PhysicalPath+"site1", models.PathRemoveLast(path, 2)) +} + +func TestObjectIDToRelativePath(t *testing.T) { + id := "site1.building.room" + assert.Equal(t, "building/room", models.ObjectIDToRelativePath(id, models.PhysicalPath+"site1")) + assert.Equal(t, "room", models.ObjectIDToRelativePath(id, models.PhysicalPath+"site1/building")) + assert.Equal(t, "site1/building/room", models.ObjectIDToRelativePath(id, models.PhysicalPath)) +} diff --git a/CLI/ocli_test.go b/CLI/ocli_test.go new file mode 100644 index 000000000..e8d273334 --- /dev/null +++ b/CLI/ocli_test.go @@ -0,0 +1,158 @@ +package main + +import ( + "cli/controllers" + "fmt" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFileParseErrorError(t *testing.T) { + fileParser := fileParseError{ + filename: "file", + lineErrors: []string{"line1", "line2"}, + } + message := fileParser.Error() + expectedMessage := "Syntax errors were found in the file: " + fileParser.filename + expectedMessage += "\nThe following commands were invalid" + expectedMessage += "\n" + strings.Join(fileParser.lineErrors, "\n") + assert.Equal(t, expectedMessage, message) +} + +func TestAddLineError(t *testing.T) { + err := fmt.Errorf("my error message") + filename := "my-file" + lineNumber := 3 + line := "line1" + fileParser := addLineError(nil, err, filename, lineNumber, line) + assert.Equal(t, filename, fileParser.filename) + assert.Len(t, fileParser.lineErrors, 1) + assert.Equal(t, fmt.Sprintf(" LINE#: %d\tCOMMAND:%s", lineNumber, line), fileParser.lineErrors[0]) + + lineNumber = 10 + line = "line2" + addLineError(fileParser, err, filename, lineNumber, line) + assert.Equal(t, filename, fileParser.filename) + assert.Len(t, fileParser.lineErrors, 2) + assert.Equal(t, fmt.Sprintf(" LINE#: %d\tCOMMAND:%s", lineNumber, line), fileParser.lineErrors[1]) +} + +func TestParseFileError(t *testing.T) { + invalidPath := "/invalid/path/file.ocli" + _, err := parseFile(invalidPath) + assert.ErrorContains(t, err, "open "+invalidPath+": no such file or directory") +} + +func TestParseFile(t *testing.T) { + basePath := t.TempDir() // temporary directory that will be deleted after the tests have finished + fileContent := ".var:siteName=siteB\n" + fileContent += "+site:$siteName\n" + fileContent += "+bd:/P/$siteName/blgdB@[0,0]@-90@[25,29.4,1]\n\n" + fileContent += "//This is a comment line\n" + fileContent += "+ro:/P/$siteName/blgdB/R2@[0,0]@0@[22.8,19.8,0.5]@+x+y\n\n" + fileContent += "for i in 1..2 { \\\n" + fileContent += " .var:multbyten=eval 10*$i; \\\n" + fileContent += " +rk:/P/$siteName/blgdB/R2/A${multbyten}@[$i,2]@t@[0,0,180]@[60,120,42] \\\n" + fileContent += "}\n" + + filename := "parse_test_file.ocli" + filePath := basePath + "/" + filename + + err := os.WriteFile(filePath, []byte(fileContent), 0644) + + if err != nil { + t.Errorf("an error ocurred while creating the test file: %s", err) + } + parsedLines, err := parseFile(filePath) + if err != nil { + t.Errorf("an error ocurred parsing the file: %s", err) + } + assert.Len(t, parsedLines, 5) + assert.Equal(t, "siteB", parsedLines[0].root.(*assignNode).val.(*valueNode).val) + assert.Equal(t, "siteName", parsedLines[0].root.(*assignNode).variable) + assert.IsType(t, &createSiteNode{}, parsedLines[1].root) + assert.IsType(t, &createBuildingNode{}, parsedLines[2].root) + assert.IsType(t, &createRoomNode{}, parsedLines[3].root) + assert.IsType(t, &forRangeNode{}, parsedLines[4].root) +} + +func TestNewStackTraceError(t *testing.T) { + err := fmt.Errorf("my-error") + stackTrace := newStackTraceError(err, "my_file", "line", 1) + msg := "Stack trace (most recent call last):\n" + msg += stackTrace.history + "Error : " + err.Error() + assert.Equal(t, msg, stackTrace.Error()) +} + +func TestLoadFile(t *testing.T) { + _, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + basePath := t.TempDir() // temporary directory that will be deleted after the tests have finished + fileContent := ".var:siteName=siteB\n" + + filename := "load_test_file.ocli" + filePath := basePath + "/" + filename + + err := os.WriteFile(filePath, []byte(fileContent), 0644) + + if err != nil { + t.Errorf("an error ocurred while creating the test file: %s", err) + } + err = LoadFile(filePath) + if err != nil { + t.Errorf("an error ocurred parsing the file: %s", err) + } + + assert.Contains(t, controllers.State.DynamicSymbolTable, "siteName") + assert.Equal(t, "siteB", controllers.State.DynamicSymbolTable["siteName"]) +} + +func TestLoadFileParseError(t *testing.T) { + _, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + basePath := t.TempDir() // temporary directory that will be deleted after the tests have finished + fileContent := "siteName=siteB\n" + + filename := "load_test_file.ocli" + filePath := basePath + "/" + filename + + err := os.WriteFile(filePath, []byte(fileContent), 0644) + + if err != nil { + t.Errorf("an error ocurred while creating the test file: %s", err) + } + err = LoadFile(filePath) + assert.NotNil(t, err) + assert.IsType(t, &fileParseError{}, err) + + errMsg := "Syntax errors were found in the file: " + filename + "\nThe following commands were invalid\n LINE#: 1\tCOMMAND:siteName=siteB" + assert.ErrorContains(t, err, errMsg) +} + +func TestLoadFileStackError(t *testing.T) { + _, _, _, deferFunction := setMainEnvironmentMock(t) + defer deferFunction() + + basePath := t.TempDir() // temporary directory that will be deleted after the tests have finished + fileContent := ".var: i = eval 10/0\n" + + filename := "load_test_file.ocli" + filePath := basePath + "/" + filename + + err := os.WriteFile(filePath, []byte(fileContent), 0644) + + if err != nil { + t.Errorf("an error ocurred while creating the test file: %s", err) + } + err = LoadFile(filePath) + assert.NotNil(t, err) + assert.IsType(t, &stackTraceError{}, err) + + errMsg := "Stack trace (most recent call last):\n File \"" + filename + "\", line 1\n .var: i = eval 10/0\nError : cannot divide by 0" + assert.ErrorContains(t, err, errMsg) +} diff --git a/CLI/parser_test.go b/CLI/parser_test.go index 4dfca99ed..f2d7a6fb0 100644 --- a/CLI/parser_test.go +++ b/CLI/parser_test.go @@ -1,11 +1,13 @@ package main import ( + "cli/models" "reflect" "runtime/debug" "testing" "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/assert" ) func (p *parser) remaining() string { @@ -454,3 +456,237 @@ func TestElif(t *testing.T) { expected := &ifNode{condition, ifBody, elif} testCommand(command, expected, t) } + +func TestParseUrl(t *testing.T) { + url := "http://url.com/route" + p := newParser(url + " other") + parsedUrl := p.parseUrl("url") + assert.Equal(t, url, parsedUrl) +} + +func parserRecoverFunction(t *testing.T, p *parser, expectedErrorMessage string) { + if panicInfo := recover(); panicInfo != nil { + assert.Equal(t, expectedErrorMessage, p.err) + } else { + t.Errorf("The function should have ended with an error") + } +} + +func TestParseIntError(t *testing.T) { + p := newParser("2s") + defer parserRecoverFunction(t, p, "integer expected") + p.parseInt("integer") +} + +func TestParseFloat(t *testing.T) { + p := newParser("2 2.5 2.g") + defer parserRecoverFunction(t, p, "float expected") + + parsedFloat := p.parseFloat("float") + assert.Equal(t, 2.0, parsedFloat) + + parsedFloat = p.parseFloat("float") + assert.Equal(t, 2.5, parsedFloat) + + p.parseFloat("float") +} + +func TestParseBoolError(t *testing.T) { + p := newParser("tru") + defer parserRecoverFunction(t, p, "boolean expected") + p.parseBool() +} + +func TestParseIndexing(t *testing.T) { + p := newParser("[12]") + parsedNode := p.parseIndexing().(*valueNode) + assert.Equal(t, 12, parsedNode.val) +} + +func TestParseEnv(t *testing.T) { + p := newParser("var=12") + parsedNode := p.parseEnv().(*setEnvNode) + assert.Equal(t, "var", parsedNode.arg) + assert.Equal(t, 12, parsedNode.expr.(*valueNode).val) +} + +func TestParseLink(t *testing.T) { + sourcePath := models.StrayPath + "stray-device" + destinationPath := models.PhysicalPath + "site/building/room/rack" + p := newParser(sourcePath + "@" + destinationPath) + parsedNode := p.parseLink().(*linkObjectNode) + assert.Equal(t, sourcePath, parsedNode.source.(*pathNode).path.(*valueNode).val) + assert.Equal(t, destinationPath, parsedNode.destination.(*pathNode).path.(*valueNode).val) + + p = newParser(sourcePath + "@" + destinationPath + "@slot=[slot1,slot2]@orientation=front") + parsedNode = p.parseLink().(*linkObjectNode) + assert.Equal(t, sourcePath, parsedNode.source.(*pathNode).path.(*valueNode).val) + assert.Equal(t, destinationPath, parsedNode.destination.(*pathNode).path.(*valueNode).val) + assert.Equal(t, []string{"orientation"}, parsedNode.attrs) + assert.Len(t, parsedNode.values, 1) + assert.Equal(t, "front", parsedNode.values[0].(*valueNode).val) + + assert.Len(t, parsedNode.slots, 2) + assert.Equal(t, "slot1", parsedNode.slots[0].(*valueNode).val) + assert.Equal(t, "slot2", parsedNode.slots[1].(*valueNode).val) +} + +func TestParseUnlink(t *testing.T) { + path := models.PhysicalPath + "site/building/room/rack" + p := newParser(path) + parsedNode := p.parseUnlink().(*unlinkObjectNode) + assert.Equal(t, path, parsedNode.source.(*pathNode).path.(*valueNode).val) +} + +func TestParseAlias(t *testing.T) { + p := newParser("aliasName { print $i }") + parsedNode := p.parseAlias().(*funcDefNode) + assert.Equal(t, "aliasName", parsedNode.name) + assert.Equal(t, "%v", parsedNode.body.(*printNode).expr.(*formatStringNode).str.(*valueNode).val) + assert.Len(t, parsedNode.body.(*printNode).expr.(*formatStringNode).vals, 1) + assert.Equal(t, "i", parsedNode.body.(*printNode).expr.(*formatStringNode).vals[0].(*symbolReferenceNode).va) +} + +func TestParseCreateDomain(t *testing.T) { + p := newParser("domain@00000A") + parsedNode := p.parseCreateDomain().(*createDomainNode) + assert.Equal(t, "domain", parsedNode.path.(*pathNode).path.(*valueNode).val) + assert.Equal(t, "00000A", parsedNode.color.(*valueNode).val) +} + +func TestParseCreateTag(t *testing.T) { + p := newParser("tag@00000A") + parsedNode := p.parseCreateTag().(*createTagNode) + assert.Equal(t, "tag", parsedNode.slug.(*valueNode).val) + assert.Equal(t, "00000A", parsedNode.color.(*valueNode).val) +} + +func TestParseCreateLayer(t *testing.T) { + p := newParser("layer@site.building.room@category=rack") + parsedNode := p.parseCreateLayer().(*createLayerNode) + assert.Equal(t, "layer", parsedNode.slug.(*valueNode).val) + assert.Equal(t, "site.building.room", parsedNode.applicability.(*pathNode).path.(*valueNode).val) + assert.Equal(t, "category=rack", parsedNode.filterValue.(*valueNode).val) +} + +func TestParseCreateOrphan(t *testing.T) { + path := models.StrayPath + "orphan" + templateName := "my-template" + p := newParser("device : " + path + "@" + templateName) + parsedNode := p.parseCreateOrphan().(*createOrphanNode) + assert.Equal(t, path, parsedNode.path.(*pathNode).path.(*valueNode).val) + assert.Equal(t, templateName, parsedNode.template.(*valueNode).val) +} + +func TestParseCreateUser(t *testing.T) { + email := "email@mail.com" + role := "my-role" + domain := "my-domain" + p := newParser(`"` + email + `"` + "@" + role + "@" + domain) + parsedNode := p.parseCreateUser().(*createUserNode) + assert.Equal(t, email, parsedNode.email.(*valueNode).val) + assert.Equal(t, role, parsedNode.role.(*valueNode).val) + assert.Equal(t, domain, parsedNode.domain.(*valueNode).val) +} + +func TestParseAddRole(t *testing.T) { + email := "email@mail.com" + role := "my-role-2" + domain := "my-domain" + p := newParser(`"` + email + `"` + "@" + role + "@" + domain) + parsedNode := p.parseAddRole().(*addRoleNode) + assert.Equal(t, email, parsedNode.email.(*valueNode).val) + assert.Equal(t, role, parsedNode.role.(*valueNode).val) + assert.Equal(t, domain, parsedNode.domain.(*valueNode).val) +} + +func TestParseCp(t *testing.T) { + source := models.LayersPath + "layer1" + destination := "layer2" + p := newParser(source + " " + destination) + parsedNode := p.parseCp().(*cpNode) + assert.Equal(t, source, parsedNode.source.(*pathNode).path.(*valueNode).val) + assert.Equal(t, destination, parsedNode.dest.(*valueNode).val) +} + +func TestParseExprList(t *testing.T) { + p := newParser("-1") + parsedNode := p.parseUnaryExpr().(*negateNode) + assert.Equal(t, 1, parsedNode.val.(*valueNode).val) + + p = newParser("!true") + parsedNode2 := p.parseUnaryExpr().(*negateBoolNode) + assert.Equal(t, true, parsedNode2.expr.(*valueNode).val) + + p = newParser("+1") + parsedNode3 := p.parseUnaryExpr().(*valueNode) + assert.Equal(t, 1, parsedNode3.val) +} + +func TestParseLsStarError(t *testing.T) { + p := newParser("-r /*") + defer parserRecoverFunction(t, p, "unexpected character in path: '*'") + p.parseLs("") +} + +func TestParseLsPathError(t *testing.T) { + p := newParser("-r path/$ra") + defer parserRecoverFunction(t, p, "path expected") + p.parseLs("") +} + +func TestParseDrawable(t *testing.T) { + path := "/path/to/draw" + p := newParser(path) + parsedNode := p.parseDrawable().(*isEntityDrawableNode) + assert.Equal(t, path, parsedNode.path.(*pathNode).path.(*valueNode).val) + + attribute := "color" + p = newParser(path + " " + attribute) + parsedNodes := p.parseDrawable().(*isAttrDrawableNode) + assert.Equal(t, path, parsedNodes.path.(*pathNode).path.(*valueNode).val) + assert.Equal(t, attribute, parsedNodes.attr) +} + +func TestParseUnsetVariable(t *testing.T) { + varName := "myVar" + p := newParser("-v " + varName) + parsedNode := p.parseUnset().(*unsetVarNode) + assert.Equal(t, varName, parsedNode.varName) +} + +func TestParseUnsetFunction(t *testing.T) { + functionName := "myFunction" + p := newParser("-f " + functionName) + parsedNode := p.parseUnset().(*unsetFuncNode) + assert.Equal(t, functionName, parsedNode.funcName) +} + +func TestParseUnsetAttribute(t *testing.T) { + path := "path/to/room" + attribute := "template" + p := newParser(path + ":" + attribute) + parsedNode := p.parseUnset().(*unsetAttrNode) + assert.Equal(t, path, parsedNode.path.(*pathNode).path.(*valueNode).val) + assert.Equal(t, attribute, parsedNode.attr) +} + +func TestParseTree(t *testing.T) { + path := "/path" + p := newParser(path) + parsedNode := p.parseTree().(*treeNode) + assert.Equal(t, path, parsedNode.path.(*pathNode).path.(*valueNode).val) + assert.Equal(t, 1, parsedNode.depth) + + p = newParser(path + " 3") + parsedNode = p.parseTree().(*treeNode) + assert.Equal(t, path, parsedNode.path.(*pathNode).path.(*valueNode).val) + assert.Equal(t, 3, parsedNode.depth) +} + +func TestParseConnect3D(t *testing.T) { + url := "url.com/path" + p := newParser(url) + parsedNode := p.parseConnect3D().(*connect3DNode) + assert.Equal(t, url, parsedNode.url) +} diff --git a/CLI/utils/util.go b/CLI/utils/util.go index 4ee868d5c..42c146127 100755 --- a/CLI/utils/util.go +++ b/CLI/utils/util.go @@ -7,9 +7,7 @@ import ( "os" "path/filepath" "reflect" - "regexp" "strconv" - "strings" ) func ExeDir() string { @@ -257,86 +255,3 @@ func ObjectAttr(obj map[string]any, attr string) (any, bool) { } return val, true } - -func ComplexFilterToMap(complexFilter string) map[string]any { - // Split the input string into individual filter expressions - chars := []string{"(", ")", "&", "|"} - for _, char := range chars { - complexFilter = strings.ReplaceAll(complexFilter, char, " "+char+" ") - } - return complexExpressionToMap(strings.Fields(complexFilter)) -} - -func complexExpressionToMap(expressions []string) map[string]any { - // Find the rightmost operator (AND, OR) outside of parentheses - parenCount := 0 - for i := len(expressions) - 1; i >= 0; i-- { - switch expressions[i] { - case "(": - parenCount++ - case ")": - parenCount-- - case "&": - if parenCount == 0 { - return map[string]any{"$and": []map[string]any{ - complexExpressionToMap(expressions[:i]), - complexExpressionToMap(expressions[i+1:]), - }} - } - case "|": - if parenCount == 0 { - return map[string]any{"$or": []map[string]any{ - complexExpressionToMap(expressions[:i]), - complexExpressionToMap(expressions[i+1:]), - }} - } - } - } - - // If there are no operators outside of parentheses, look for the innermost pair of parentheses - for i := 0; i < len(expressions); i++ { - if expressions[i] == "(" { - start, end := i+1, i+1 - for parenCount := 1; end < len(expressions) && parenCount > 0; end++ { - switch expressions[end] { - case "(": - parenCount++ - case ")": - parenCount-- - } - } - return complexExpressionToMap(append(expressions[:start-1], expressions[start:end-1]...)) - } - } - - // Base case: single filter expression - re := regexp.MustCompile(`^([\w-.]+)\s*(<=|>=|<|>|!=|=)\s*([\w-.*]+)$`) - - ops := map[string]string{"<=": "$lte", ">=": "$gte", "<": "$lt", ">": "$gt", "!=": "$not"} - - if len(expressions) <= 3 { - expression := strings.Join(expressions[:], "") - - if match := re.FindStringSubmatch(expression); match != nil { - switch match[1] { - case "startDate": - return map[string]any{"lastUpdated": map[string]any{"$gte": match[3]}} - case "endDate": - return map[string]any{"lastUpdated": map[string]any{"$lte": match[3]}} - case "id", "name", "category", "description", "domain", "createdDate", "lastUpdated", "slug": - if match[2] == "=" { - return map[string]any{match[1]: match[3]} - } - return map[string]any{match[1]: map[string]any{ops[match[2]]: match[3]}} - default: - if match[2] == "=" { - return map[string]any{"attributes." + match[1]: match[3]} - } - return map[string]any{"attributes." + match[1]: map[string]any{ops[match[2]]: match[3]}} - } - } - } - - fmt.Println("Error: Invalid filter expression") - return map[string]any{"error": "invalid filter expression"} -} diff --git a/CLI/utils/util_test.go b/CLI/utils/util_test.go new file mode 100644 index 000000000..39b6114e9 --- /dev/null +++ b/CLI/utils/util_test.go @@ -0,0 +1,341 @@ +package utils_test + +import ( + "cli/utils" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetFloat(t *testing.T) { + number, err := utils.GetFloat(5) + assert.Nil(t, err) + assert.Equal(t, 5.0, number) + + number, err = utils.GetFloat("5") + assert.NotNil(t, err) + assert.Equal(t, 0.0, number) + assert.ErrorContains(t, err, "cannot convert string to float64") +} + +func TestValToFloat(t *testing.T) { + number, err := utils.ValToFloat(5, "number") + assert.Nil(t, err) + assert.Equal(t, 5.0, number) + + number, err = utils.ValToFloat("5.5", "string number") + assert.Nil(t, err) + assert.Equal(t, 5.5, number) + + number, err = utils.ValToFloat("fifty", "string value") + assert.NotNil(t, err) + assert.Equal(t, 0.0, number) + assert.ErrorContains(t, err, "string value should be a number") + + number, err = utils.ValToFloat([]int{5}, "list") + assert.NotNil(t, err) + assert.Equal(t, 0.0, number) + assert.ErrorContains(t, err, "list should be a number") +} + +func TestStringToNum(t *testing.T) { + number, err := utils.StringToNum("5") + assert.Nil(t, err) + assert.Equal(t, 5, number) // returns an int + + number, err = utils.StringToNum("5.5") + assert.Nil(t, err) + assert.Equal(t, 5.5, number) // returns a float + + number, err = utils.StringToNum("fifty") + assert.NotNil(t, err) + assert.Nil(t, number) + assert.ErrorContains(t, err, "the string is not a number") +} + +func TestValToNum(t *testing.T) { + number, err := utils.ValToNum("5", "string int") + assert.Nil(t, err) + assert.Equal(t, 5, number) // returns an int + + number, err = utils.ValToNum("5.5", "string float") + assert.Nil(t, err) + assert.Equal(t, 5.5, number) // returns a float + + number, err = utils.ValToNum("fifty", "string value") + assert.NotNil(t, err) + assert.Nil(t, number) + assert.ErrorContains(t, err, "string value should be a number") + + number, err = utils.ValToNum(5, "int") + assert.Nil(t, err) + assert.Equal(t, 5, number) +} + +func TestValToInt(t *testing.T) { + number, err := utils.ValToInt("5", "string int") + assert.Nil(t, err) + assert.Equal(t, 5, number) // returns an int + + number, err = utils.ValToInt("5.5", "string float") + assert.NotNil(t, err) + assert.Equal(t, 0, number) + assert.ErrorContains(t, err, "string float should be an integer") + + number, err = utils.ValToInt("fifty", "string value") + assert.NotNil(t, err) + assert.Equal(t, 0, number) + assert.ErrorContains(t, err, "string value should be an integer") + + number, err = utils.ValToInt([]int{5}, "list") + assert.NotNil(t, err) + assert.Equal(t, 0, number) + assert.ErrorContains(t, err, "list should be an integer") + + number, err = utils.ValToInt(5, "int") + assert.Nil(t, err) + assert.Equal(t, 5, number) +} + +func TestValToBool(t *testing.T) { + result, err := utils.ValToBool(true, "true boolean") + assert.Nil(t, err) + assert.True(t, result) + + result, err = utils.ValToBool(false, "false boolean") + assert.Nil(t, err) + assert.False(t, result) + + result, err = utils.ValToBool("true", "true string") + assert.Nil(t, err) + assert.True(t, result) + + result, err = utils.ValToBool("false", "false string") + assert.Nil(t, err) + assert.False(t, result) + + result, err = utils.ValToBool("fals", "error string") + assert.NotNil(t, err) + assert.False(t, result) + assert.ErrorContains(t, err, "error string should be a boolean") + + result, err = utils.ValToBool(1, "int") + assert.NotNil(t, err) + assert.False(t, result) + assert.ErrorContains(t, err, "int should be a boolean") + +} + +func TestValTo3dRotation(t *testing.T) { + result, err := utils.ValTo3dRotation([]float64{1, 1}) + assert.Nil(t, err) + assert.Equal(t, []float64{1, 1}, result) + + stringValues := map[string][]float64{ + "front": []float64{0, 0, 180}, + "rear": []float64{0, 0, 0}, + "left": []float64{0, 90, 0}, + "right": []float64{0, -90, 0}, + "top": []float64{90, 0, 0}, + "bottom": []float64{-90, 0, 0}, + } + + for key, value := range stringValues { + result, err = utils.ValTo3dRotation(key) + assert.Nil(t, err) + assert.Equal(t, value, result) + } + + result, err = utils.ValTo3dRotation(false) + assert.NotNil(t, err) + assert.Nil(t, result) + assert.ErrorContains(t, err, + `rotation should be a vector3, or one of the following keywords : + front, rear, left, right, top, bottom`) +} + +func TestValToString(t *testing.T) { + result, err := utils.ValToString(5, "int") + assert.Nil(t, err) + assert.Equal(t, "5", result) + + result, err = utils.ValToString("value", "string") + assert.Nil(t, err) + assert.Equal(t, "value", result) + + result, err = utils.ValToString(5.5, "float") + assert.NotNil(t, err) + assert.Equal(t, "", result) + assert.ErrorContains(t, err, "float should be a string") +} + +func TestValToVec(t *testing.T) { + value := []float64{0, 1, 2} + result, err := utils.ValToVec(value, len(value), "float vector") + assert.Nil(t, err) + assert.Equal(t, value, result) + + result, err = utils.ValToVec(value, len(value)-1, "float vector invalid size") + assert.NotNil(t, err) + assert.Nil(t, result) + assert.ErrorContains(t, err, "float vector invalid size should be a vector2") + + result, err = utils.ValToVec("[0,0]", 2, "string") + assert.NotNil(t, err) + assert.Nil(t, result) + assert.ErrorContains(t, err, "string should be a vector2") +} + +func TestValToColor(t *testing.T) { + color, ok := utils.ValToColor([]int{1}) + assert.False(t, ok) + assert.Equal(t, "", color) + + // hex string of length != 6 + color, ok = utils.ValToColor("abcac") + assert.False(t, ok) + assert.Equal(t, "", color) + + // not hex string of length == 6 + color, ok = utils.ValToColor("zabaca") + assert.False(t, ok) + assert.Equal(t, "", color) + + // hex string of length == 6 + color, ok = utils.ValToColor("abcaca") + assert.True(t, ok) + assert.Equal(t, "abcaca", color) + + // int with 6 digits + color, ok = utils.ValToColor(255255) + assert.True(t, ok) + assert.Equal(t, "255255", color) + + // int without 6 digits + color, ok = utils.ValToColor(255) + assert.False(t, ok) + assert.Equal(t, "", color) + + // float without 6 digits + color, ok = utils.ValToColor(255.0) + assert.False(t, ok) + assert.Equal(t, "", color) + + // float with 6 digits + color, ok = utils.ValToColor(255255.0) + assert.True(t, ok) + assert.Equal(t, "255255", color) +} + +func TestIsInfArr(t *testing.T) { + ok := utils.IsInfArr([]any{1}) + assert.True(t, ok) + + ok = utils.IsInfArr([]any{1.0}) + assert.True(t, ok) + + ok = utils.IsInfArr("string") + assert.False(t, ok) +} + +func TestIsString(t *testing.T) { + ok := utils.IsString(1) + assert.False(t, ok) + + ok = utils.IsString(1.0) + assert.False(t, ok) + + ok = utils.IsString("string") + assert.True(t, ok) +} + +func TestIsHexString(t *testing.T) { + ok := utils.IsHexString("1.0") + assert.False(t, ok) + + ok = utils.IsHexString("string") + assert.False(t, ok) + + ok = utils.IsHexString("abc4") + assert.True(t, ok) +} + +func TestIsInt(t *testing.T) { + ok := utils.IsInt(1.0) + assert.False(t, ok) + + ok = utils.IsInt("string") + assert.False(t, ok) + + ok = utils.IsInt(1) + assert.True(t, ok) +} + +func TestIsFloat(t *testing.T) { + ok := utils.IsFloat(1.0) + assert.True(t, ok) + + ok = utils.IsFloat("string") + assert.False(t, ok) + + ok = utils.IsFloat(1) + assert.False(t, ok) +} + +func TestCompareVals(t *testing.T) { + comparison, ok := utils.CompareVals(1.0, 2.0) + assert.True(t, ok) + assert.True(t, comparison) + + comparison, ok = utils.CompareVals(2.0, 1.0) + assert.True(t, ok) + assert.False(t, comparison) + + comparison, ok = utils.CompareVals("value1", "value2") + assert.True(t, ok) + assert.True(t, comparison) + + comparison, ok = utils.CompareVals("value2", "value1") + assert.True(t, ok) + assert.False(t, comparison) + + comparison, ok = utils.CompareVals(1.0, "abc") + assert.False(t, ok) + assert.False(t, comparison) +} + +func TestNameOrSlug(t *testing.T) { + result := utils.NameOrSlug(map[string]any{"slug": "my-slug"}) + assert.Equal(t, "my-slug", result) + + result = utils.NameOrSlug(map[string]any{"name": "my-name"}) + assert.Equal(t, "my-name", result) + + result = utils.NameOrSlug(map[string]any{"slug": "my-slug", "name": "my-name"}) + assert.Equal(t, "my-slug", result) +} + +func TestObjectAttr(t *testing.T) { + object := map[string]any{ + "name": "my-name", + } + value, ok := utils.ObjectAttr(object, "name") + assert.True(t, ok) + assert.Equal(t, object["name"], value) + + value, ok = utils.ObjectAttr(object, "color") + assert.False(t, ok) + assert.Nil(t, value) + + object["attributes"] = map[string]any{ + "color": "blue", + } + + value, ok = utils.ObjectAttr(object, "color") + assert.True(t, ok) + assert.Equal(t, object["attributes"].(map[string]any)["color"], value) + + value, ok = utils.ObjectAttr(object, "other") + assert.False(t, ok) + assert.Nil(t, value) +} diff --git a/README.md b/README.md index ff480a4a9..81317d810 100644 --- a/README.md +++ b/README.md @@ -1,167 +1,94 @@ -# OGrEE-Core +
+NetBox logo +

A smart datacenter digital twin

-OGrEE-Core assembles 3 essential components of OGrEE, allowing you to create an OGrEE Tenant to store and interact with your datacenter data. - -## Quick Intro -![ogree-schema](https://github.com/ditrit/OGrEE-Core/assets/37706737/378c6cbe-aea2-4db0-82d6-6c3a18ecc6c5) - -An OGrEE Tenant consists of a DB populated with objects of a datacenter (sites, buildings, devices, etc.) that can be accessed through an API. For a user friendly access, a WebAPP can be deployed for each Tenant or a locally installed CLI can be used. Check the [OGrEE-3D](https://github.com/ditrit/OGrEE-3D) repo for the 3D datacenter viewer. To launch and manage a tenant, a WebAPP in "SuperAdmin" version with its backend in Go are available. - -## How to deploy an OGrEE Tenant -The prefered way to deploy the API is to use the superadmin interface. See the [OGrEE-APP documentation](https://github.com/ditrit/OGrEE-Core/tree/main/APP). - -## Quickstart to deploy an OGrEE Tenant without OGrEE-APP - -Run: -```docker compose --project-name -f deploy/docker/docker-compose.yml up``` - -The config can be updated beforehand in ```deploy/docker/.env``` - -## Frontend config -To use the frontend (CLI, 3D, APP), a ```config.toml``` file must be created at the root of the repo. - -Example : -``` -[OGrEE-CLI] -Verbose = "ERROR" -APIURL = "http://127.0.0.1:3001" -UnityURL = "127.0.0.1:5500" -UnityTimeout = "10ms" -HistPath = "./.history" -Script = "" -Drawable = ["all"] -DrawLimit = 100 -Updates = ["all"] -User = "" -Variables = [ - {Name = "ROOT", Value = "path_to_root"}, - {Name = "ROOT2", Value = "$ROOT/path_to_root2"}, -] - -[OGrEE-CLI.DrawableJson] -tenant = "./other/drawTemplates/tenant.json" - -[OGrEE-3D] -verbose = true -fullscreen = false -cachePath = "C:/" -cacheLimitMo = 100 -# Port used to receive messages from OGrEE-CLI. 5500 by default -cliPort = 5500 -# Value clamped from 0 to 100 -alphaOnInteract = 50 - -[OGrEE-3D.textures] -# Textures loaded in the 3D client at startup, in "name = url" format -perf22 = "https://raw.githubusercontent.com/ditrit/OGREE-3D/master/Assets/Resources/Textures/TilePerf22.png" -perf29 = "https://raw.githubusercontent.com/ditrit/OGREE-3D/master/Assets/Resources/Textures/TilePerf29.png" - -[OGrEE-3D.colors] -# Colors can be defines with hexadecimal codes or html colors -selection = "#21FF00" -edit = "#C900FF" -focus = "#FF9F00" -highlight = "#00D5FF" - -usableZone = "#DBEDF2" -reservedZone = "#F2F2F2" -technicalZone = "#EBF2DE" - -[OGrEE-3D.temperature] -# Minimum and maximum values for temperatures in Celsius and Fahrenheit to define the range of the temperature gradients -minC = 0 -maxC = 100 -minF = 32 -maxF = 212 - -# Define a custom gradient by defining up to 8 colors in rgba format (rgb from 0 to 255, a from 0 to 100) -useCustomGradient = true -customTemperatureGradient = [ - [0,0,255,0], - [255,0,0,100], - [255,255,0,50] -] -``` -## How to checkout a single component -OGrEE-Core is a monorepo hosting 3 differents applications: API, CLI and APP. With git sparse checkout functionality, you can choose which component you want to checkout instead of the whole repo. - -``` -mkdir sparse -cd sparse -git init -git remote add -f origin https://github.com/ditrit/OGrEE-Core.git -git sparse-checkout init -# subfolders to checkout -git sparse-checkout set "deploy" "APP" -# branch you wish to checkout -git pull origin main -``` +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=helderbetiol_OGrEE-Core&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=helderbetiol_OGrEE-Core) +[![⚙️ Build - Publish](https://github.com/ditrit/OGrEE-Core/actions/workflows/build-deploy.yaml/badge.svg)](https://github.com/ditrit/OGrEE-Core/actions/workflows/build-deploy.yaml) +
-# 🔁 OGree-Core GitFlow +OGrEE exists so managing a datacenter can be easier. It aggregates data from different datacenter tools to create a digital twin easily accessible from your computer, smartphone or even VR/AR glasses. Its analysis capabilities helps identify issues faster, better prepare for maintenance and migrations, minimize risks and errors. -![Workflows diagram](/assets/images/actions.png) +

+ Why OGrEE | + OGrEE-Core | + OGrEE-3D | + Quickstart | + Quick Demo | + Get Involved +

-The development process begins with the creation of a new [issue](https://github.com/ditrit/OGrEE-Core/issues). Issues can be created for things such as reporting a bug or requesting a new feature. To work on an issue, a new dedicated branch must be created, and all code changes must be commited to this new branch (**never commit directly to main!**). +![ogree-schema](https://github.com/ditrit/OGrEE-Core/assets/37706737/78e512d0-0f24-4475-b38e-446bf3561e74) -Once the development is complete, a [pull request](https://github.com/ditrit/OGrEE-Core/pulls) can be opened. The opening of a pull request automatically triggers two Github workflows: `Branch Naming` and `Commit name check`. If the pull request involves changes to the API, APP and/or CLI, `🕵️‍♂️ API Unit Tests`, `🕵️‍♂️ APP Unit Tests` and/or `🕵️‍♂️ CLI Unit Tests` workflows are also automatically triggered to build and test the changes. If the pull request involves changes to the Wiki, the `📚 Verify conflicts in Wiki` workflow is automatically triggered to check the changes. -If all checks are performed successfully, another member of the team will perform a code review on the pull request. If no further changes are requested, the pull request can be merged into the [main branch](https://github.com/ditrit/OGrEE-Core/tree/main) safely, closing all related issues. The merging of a pull request involving changes to the API, APP and/or CLI will automatically trigger the `🆕 Create Release Candidate` workflow, explained in the [Release candidate](#release-candidate) section below. The merging of a pull request involving changes to the Wiki will automatically trigger the `📚 Publish docs to Wiki` workflow. +## Why OGrEE +Different professionals work together in a datacenter with its different tools, OGrEE is here to integrate them. Here are some use cases and questions OGrEE may help with: +- An **AR/VR** headset connected to OGrEE can give an administrator or expert live access to help an in-place technician. AR/VR view can also guide a technician to find the exact server and disk that needs to be replaced. +- OGrEE's **3D view** from OGrEE can help the datacenter team organize space and prepare new installations. +- **Data analysis** in the 2D view can let you know that those docker containers that are not performing as they should are all running in servers from the same rack that is warmer than it should. A port in the switch needs to be unplugged? A power pnale needs maintenance? OGrEE can tell you what may be impacted, from racks and servers to even software components. +- **Migrating** to cloud? Have a complete view of your datacenter and mitigate impacts with OGrEE. -## Release candidate +OGrEE has an **offline mode**, working just with dumps and logs extracted from other tools to avoiding floating the network with constant requests. -![Release candidate diagram](/assets/images/main.jpg) +## OGrEE-Core +OGrEE-Core assembles 3 essential components of OGrEE: +- **API**: an API developed in Go with a MongoDB to store all the datacenter information (sites, buildings, devices, applications; etc.) and provide secure access to it. +- **APP**: APP is a Flutter application that can run as a native app (iOS, Android, Windows, Linux, Mac) or webapp (all main browsers) and its Go backend to view and interact with the datacenter data, providing reports and analysis. +- **CLI**: command line client to interact with the data from the API and to pilot the 3D view from OGrEE-3D. -After merging a dev branch into main, the `🆕 Create Release Candidate` workflow will create a new branch named `release-candidate/x.x.x`. - -Semver bump are defined by the following rules: -- One commit between last tag and main contains: break/breaking -> Bump major version; -- One commit between last tag and main contains: feat/features -> Bump minor version; -- Any other cases -> Bump patch version. - -If a branch release-candidate with the same semver already exists, it will be deleted and recreated from the new commit. +Together, these components form an **OGrEE Tenant**, a deployment instance of OGrEE. +
+ +![ogree-schema](https://github.com/ditrit/OGrEE-Core/assets/37706737/378c6cbe-aea2-4db0-82d6-6c3a18ecc6c5) -Example: A patch is merged after another, which has not yet been released. +
-This workflow will automatically trigger the `⚙️ Build - Publish` workflow. This workflow is responsible for building the binaries of the API, BACK and CLI (for Windows, MacOS and Linux), the WebAPP, and the Windows Installer, which includes the Windows API, APP, CLI and 3D packet binaries). All of these binaries are then published into [OGrEE's Nextcloud](https://nextcloud.ditrit.io/index.php/apps/files/?dir=/Ogree&fileid=2304). The `⚙️ Build - Publish` workflow is also responsible for building Docker Images for the API, WebAPP and BACK and for publishing these images into OGrEE's private Docker Registry `registry.ogree.ditrit.io`. +## OGrEE-3D +This is OGrEE's 3D client, a 3D datacenter viewer based on Unity game engine to display and interact with an OGrEE Tenant. +You can access to the OGrEE-3D repo [here](https://github.com/ditrit/OGrEE-3D). -## Release +## Quickstart -After validating a release candidate, the `📦 Create Release` workflow can be manually run from the [Github Actions panel](https://github.com/ditrit/OGrEE-Core/actions) on the release-candidate branch. This workflow will create a new branch named `release/x.x.x`. +A few options are avaiable to help deploy your first OGrEE Tenant: -![Github Actions panel](/assets/images/github.png) +#### Option 1: SuperAdmin APP +The APP has a **SuperAdmin** version to create and manage OGrEE Tenants with a pretty UI. To quickly deploy it, just execute the launch script appropriate to your OS from the `deploy/app` folder. +```console +cd deploy/app -Note: If release workflow is launch on another branch other than a release-candidate, it will fail. +# Windows (use PowerShell) +.\launch.ps1 +# Linux +./launch.sh +# MacOS +./launch.sh -m +``` +> This will use docker to compile APP and BACK (a Go backend for SuperAdmin), then run a docker container for the SuperAdmin webapp and locally run the compiled binary of BACK. For more launch options, check the documentation under [deploy](https://github.com/ditrit/OGrEE-Core/tree/main/deploy). -Besides creating a new [Github Release](https://github.com/ditrit/OGrEE-Core/releases) for the project, this workflow will also automatically trigger the `⚙️ Build - Publish`, explained in the [Release candidate](#release-candidate) section above. +Check the [SuperAdmin user guide](https://github.com/ditrit/OGrEE-Core/wiki/Quick-Windows-Deploy) on how to create and manage a tenant as well as download a CLi and 3D client from it. -## Build docker images and CLI +#### Option 2: Docker compose +Don't want a pretty UI to manage a tenant? Just run docker compose to create a new tenant: -### Docker images -When a branch release-candidate or release are created, the `⚙️ Build - Publish` workflow will automatically trigger workflows for creatinh the Docker Images, tags with semver, into the private Docker Registry `registry.ogree.ditrit.io`. +```docker compose --project-name --profile web -f deploy/docker/docker-compose.yml up``` +> This will create a docker deployment with an API, a DB and a WebAPP -Docker images created are: -- mongo-api/x.x.x: image provided by API/Dockerfile; -- ogree-app/x.x.x: image provided by APP/Dockerfile; -- ogree_app_backend/x.x.x: image provided by BACK/app/Dockerfile. +The config can be updated beforehand in ```deploy/docker/.env``` -### CLI +#### Option 3: Windows Installer (Windows only) +We have a Windows Installer to quickly install SuperAdmin with a CLI and a 3D client. Download the latest from here and it will guide through the installation. Then, check the [SuperAdmin user guide](https://github.com/ditrit/OGrEE-Core/wiki/Quick-Windows-Deploy) on how to create and manage a tenant -CLI will be built and pushed into [OGrEE's Nextcloud](https://nextcloud.ditrit.io/index.php/apps/files/?dir=/Ogree&fileid=2304) folder `/bin/x.x.x/` +## OGrEE-Tools +OGrEE-Tools is a collection of tools help populate OGrEE with data. They can help extract and parse data from multiple logs, create 3D models of servers using Machine Learning and much more. Check out its repo [here](https://github.com/ditrit/OGrEE-Tools). -### Sermver for Docker Images and CLI +## Quick Demo -If the build workflow is triggered by a release-candidate branch, the workflow will add `.rc` after semver. +https://github.com/ditrit/OGrEE-Core/assets/35805113/d6fe1a3e-9c5f-42e7-b926-1c6211a7df0d -- Example: release-candidate/1.0.0 will be made mongo-api/1.0.0.rc +## Get Involved -If the build workflow is triggered by a release branch, the workflow will tag OGrEE-Core with semver. +New contributors are more than welcome! +- Have an idea? Let's talk about it on Discord or on the [discussion forum](https://github.com/ditrit/OGrEE-Core/discussions). +- Want to to code? Check out our [how to contribute](https://github.com/ditrit/OGrEE-Core/wiki/How-to-contribute-(Dev-Guide)) guide. -## Secrets needs -- NEXT_CREDENTIALS: nextcloud credentials -- TEAM_DOCKER_URL: Url of the docker registry -- TEAM_DOCKER_PASSWORD: password of the docker registry -- TEAM_DOCKER_USERNAME: username of the docker registry -- PAT_GITHUB_TOKEN: a personal access github token (required to trigger build workflows) -- GITHUB_TOKEN: an admin github automatic token diff --git "a/wiki/\360\237\223\227-[User-Guide]-CLI-\342\200\220-Language.md" "b/wiki/\360\237\223\227-[User-Guide]-CLI-\342\200\220-Language.md" index 16a07b965..8421781af 100644 --- "a/wiki/\360\237\223\227-[User-Guide]-CLI-\342\200\220-Language.md" +++ "b/wiki/\360\237\223\227-[User-Guide]-CLI-\342\200\220-Language.md" @@ -1,106 +1,97 @@ # Contents - [Glossary](#glossary) -- [Comments](#comments) -- [Variables](#variables) - - [Set a variable](#set-a-variable) - - [Use a variable](#use-a-variable) -- [Expressions](#expressions) - - [Primary expressions](#primary-expressions) - - [Operators](#operators) - - [Compute operators](#compute-operators) - - [Boolean operators](#boolean-operators) -- [Print](#print) -- [String formatting](#string-formatting) -- [Loading commands](#loading-commands) - - [Load commands from a text file](#load-commands-from-a-text-file) - - [Commands over multiple lines](#commands-over-multiple-lines) - - [Load template from JSON](#load-template-from-json) -- [Hierarchy commands](#hierarchy-commands) - - [Select an object](#select-an-object) - - [Select child / children object](#select-child--children-object) - - [Select parent object](#select-parent-object) - - [Get object/s](#get-objects) - - [Wildcards](#wildcards) - - [Filters](#filters) - - [Simple filters](#simple-filters) - - [Complex filters](#complex-filters) - - [Ls object](#ls-object) - - [Layers](#layers) - - [Room's automatic layers](#rooms-automatic-layers) - - [Rack's automatic layers](#racks-automatic-layers) - - [Device's automatic layers](#devices-automatic-layers) - - [Filters](#filters-1) - - [Simple filters](#simple-filters-1) - - [Complex filters](#complex-filters-1) - - [Filter by category](#filter-by-category) - - [Tree](#tree) - - [Delete object](#delete-object) - - [Focus an object](#focus-an-object) - - [Copy object](#copy-object) - - [Link/Unlink object](#linkunlink-object) -- [Create commands](#create-commands) - - [Create a Domain](#create-a-domain) - - [Create a Site](#create-a-site) - - [Create a Building](#create-a-building) - - [Create a Room](#create-a-room) - - [Create a Rack](#create-a-rack) - - [Create a Device](#create-a-device) - - [Create a Group](#create-a-group) - - [Create a Corridor](#create-a-corridor) - - [Create a Tag](#create-a-tag) - - [Create a Layer](#create-a-layer) - - [Applicability Patterns](#applicability-patterns) - - [Character Classes](#character-classes) - - [Create a Generic Object](#create-a-generic-object) -- [Set commands](#set-commands) - - [Set colors for zones of all rooms in a datacenter](#set-colors-for-zones-of-all-rooms-in-a-datacenter) - - [Set reserved and technical zones of a room](#set-reserved-and-technical-zones-of-a-room) - - [Room separators](#room-separators) - - [Room pillars](#room-pillars) - - [Modify object's attribute](#modify-objects-attribute) - - [Tags](#tags) - - [Labels](#labels) - - [Choose Label](#choose-label) - - [Modify label's font](#modify-labels-font) - - [Modify label's background color](#modify-labels-background-color) - - [Interact with objects](#interact-with-objects) - - [Room](#room) - - [Rack](#rack) - - [Device](#device) - - [Group](#group) +- [Language Syntax](#language-syntax) + * [Comments](#comments) + * [Variables](#variables) + + [Set a variable](#set-a-variable) + + [Use a variable](#use-a-variable) + * [Expressions](#expressions) + + [Primary expressions](#primary-expressions) + + [Operators](#operators) + * [Print](#print) + + [String formatting](#string-formatting) + * [Control flow](#control-flow) + + [Conditions](#conditions) + + [Loops](#loops) + + [Aliases](#aliases) +- [Loading Commands](#loading-commands) + * [Load commands from a text file](#load-commands-from-a-text-file) + * [Commands over multiple lines](#commands-over-multiple-lines) + * [Load template from JSON](#load-template-from-json) +- [Object Generic Commands](#object-commands) + * [Select an object](#select-an-object) + + [Select child object](#select-child-object) + + [Select parent object](#select-parent-object) + * [Focus an object](#focus-an-object) + * [Get object](#get-object) + + [Wildcards](#wildcards) + + [Filters](#filters) + * [Ls object](#ls-object) + + [Layers](#layers) + + [Filters](#filters-1) + * [Tree](#tree) + * [Delete object](#delete-object) + * [Modify object attribute](#modify-object-attribute) + * [Delete object attribute](#delete-object-attribute) + * [Link/Unlink object](#linkunlink-object) +- [Object Specific Commands](#object-specific-commands) + * [Domain](#domain) + * [Site](#site) + + [Set colors for zones of all rooms in a datacenter](#set-colors-for-zones-of-all-rooms-in-a-datacenter) + * [Building](#building) + * [Room](#room) + + [Set reserved and technical zones of a room](#set-reserved-and-technical-zones-of-a-room) + + [Room separators](#room-separators) + + [Room pillars](#room-pillars) + + [Interact with Room](#interact-with-room) + * [Rack](#rack) + + [Interact with Rack](#interact-with-rack) + * [Device](#device) + + [Interact with Device](#interact-with-device) + * [Group](#group) + + [Interact with Group](#interact-with-group) + * [Corridor](#corridor) + * [Generic Object](#generic-object) + * [Tag](#tag) + + [Apply tags to objects](#apply-tags-to-objects) + * [Layer](#layer) + + [Applicability Patterns](#applicability-patterns) + + [Copy Layer](#copy-layer) + * [Labels](#labels) + + [Choose Label](#choose-label) + + [Modify label's font](#modify-labels-font) + + [Modify label's background color](#modify-labels-background-color) - [Manipulate UI](#manipulate-ui) - - [Delay commands](#delay-commands) - - [Display infos panel](#display-infos-panel) - - [Display debug panel](#display-debug-panel) - - [Highlight object](#highlight-object) + * [Delay commands](#delay-commands) + * [Display infos panel](#display-infos-panel) + * [Display debug panel](#display-debug-panel) + * [Highlight object](#highlight-object) - [Manipulate camera](#manipulate-camera) - - [Move camera](#move-camera) - - [Translate camera](#translate-camera) - - [Wait between two translations](#wait-between-two-translations) -- [Control flow](#control-flow) - - [Conditions](#conditions) - - [Loops](#loops) - - [Aliases](#aliases) -- [Examples](#examples) + * [Move camera](#move-camera) + * [Translate camera](#translate-camera) + * [Wait between two translations](#wait-between-two-translations) # Glossary `[name]` is case sensitive. It includes the whole path of the object (for example: `tn/si/bd/ro/rk`) `[color]` is a hexadecimal code (*ffffff*) -# Comments +# Language Syntax +Just like a programming language, the CLI Language allows the user to use variables, control flow such as for loops and much more, as described below. -You can put comments in an .ocli file with the `//` indicator. +## Comments + +You can put comments in an .ocli file with the `//` indicator. This can be useful in `.ocli` files, that is, text files with multiple CLI commands (check [Load commands from a text file](#load-commands-from-a-text-file)). ``` // This is a comment +si:example@ffffff // This is another comment ``` -# Variables +## Variables -## Set a variable +### Set a variable ``` .var:[name]=[value] @@ -122,17 +113,29 @@ where ```[value]``` can be either: // even though it can be used as a number in expressions ``` -## Use a variable +To unset a variable, that is, to completely remove it, use: +``` +unset -v [name] +``` + +### Use a variable `${[name]}` or `$[name]` in the second case, the longest identifier is used as ```[name]``` ``` +.var:siteNameVar=SITE +si:$siteNameVar + +.var:ROOM=/P/$siteNameVar/BLDG/ROOM1 +${ROOM}/Rack:description="Rack of site $siteNameVar" + +.var:i=eval (10+10)*10; // i=200 +.var:j=eval $i/10-1; // j=19 ``` -# Expressions +## Expressions -## Primary expressions +### Primary expressions - booleans true / false - integers @@ -143,22 +146,23 @@ in the second case, the longest identifier is used as ```[name]``` They can be used to build more complex expressions through operators. -## Operators +### Operators -### Compute operators +#### Compute operators these will only work if both side return a number +, -, *, /, \ (integer division), % (modulo) -### Boolean operators : + +#### Boolean operators <, >, <=, >=, ==, !=, || (or), && (and) -# Print +## Print The ```print``` command prints the given string. The argument can take the same values as for variable assignments. The ```printf``` command is equivalent to a ```print format```, see next section for details about ```format```. -## String formatting +### String formatting You can dereference variables inside strings with ```${[name]}``` or ```$[name]```. @@ -179,7 +183,78 @@ For a sprintf-like formatting, you can use the format function, it uses the go f print format("2+3 equals %02d", 2+3) // prints "2+3 equals 05" ``` -# Loading commands +## Control flow + +### Conditions +``` +if condition { commands } elif condition { commands } else { commands } +``` +`If-elif-else` statements have a similar syntax to Go. It expects an expression to be evaluated to true or false, followed by `{}` contaning the commands to execute if true. Examples: +``` +> if 42 > 43 { print toto } elif 42 == 43 { print tata } else { print titi } +titi + +// Multiple lines +if $shouldCreateSite == true { \ + +si:/P/SITE \ + /P/SITE:reservedColor=AAAAAA \ +} +``` + +### Loops +``` +for index in start..end { commands } +``` +A `for` loop expects the name to give the index followed by a range and then `{}` with the commands. The range must be a start interger number followed by `..` and an end integer number superior to start. Examples: +``` +> for i in 0..3 { .var: i2 = $(($i * $i)) ; print $i^2 = $i2 } +0^2 = 0 +1^2 = 1 +2^2 = 4 +3^2 = 9 + +// Multiple lines +for i in 0..5 { \ + .var:r=eval 10+$i; \ + .var:x=eval (36+$i*4)/3; \ + +rk:/P/SI/BLDG/ROOM/J${r}@[ $x, 52]@[80,120,42]@rear; \ + +rk:/P/SI/BLDG/ROOM/K${r}@[ $x, 55]@[80,120,42]@front \ +} +``` +Another loop comandavaiable is the `while`. +``` +while condition { commands } +``` +A while loop expects an expression to be evaluated to true or false, followed by `{}` contaning the commands to execute repeteadly with the expression remains true. Examples: +``` +>.var: i = 0; while $i<4 {print $i^2 = $(($i * $i)); .var: i = eval $i+1 } +0^2 = 0 +1^2 = 1 +2^2 = 4 +3^2 = 9 +``` + +### Aliases +``` +alias name { commands } +``` + +An `alias` can be created to replace a list of commands, that is, to create a function without arguments. It expects the name of the alias followed by `{}` containing the commands it should evoke. Examples: +``` +>alias pi2 { .var: i2 = $(($i * $i)) ; print $i^2 = $i2 } +>for i in 0..3 { pi2 } +0^2 = 0 +1^2 = 1 +2^2 = 4 +3^2 = 9 +``` + +To unset a function, that is, remove the alias, use: +``` +unset -f [name] +``` + +# Loading Commands ## Load commands from a text file ``` @@ -192,18 +267,18 @@ print format("2+3 equals %02d", 2+3) // prints "2+3 equals 05" .cmds:/path/to/file.ocli ``` -By convention, these files carry the extension .ocli. +By convention, these files carry the extension .ocli. It should include a sequence of commands accepted by the CLI. For example, it can include the creation of variables used in a for loop with commands to create sites, buildings, rooms, etc. It can also include a `.cmds`command to call for another file with more commands. Check some examples [here](https://github.com/ditrit/OGrEE-Core/wiki/📗-%5BUser-Guide%5D-CLI-%E2%80%90-Get-Started#create-with-ocli-files). ## Commands over multiple lines In .ocli script files, commands are usually separated by line breaks, however it is possible to have commands over multiple lines by using the \ character at the end of one or more consecutive lines, as shown below: ``` -for i in 0..5 { \ - .var:r=eval 10+$i; \ - .var:x=eval (36+$i*4)/3; \ - +rk:/P/NSQSI/NSQBD/NSQRO/J${r}@[ $x, 52]@[80,120,42]@rear; \ - +rk:/P/NSQSI/NSQBD/NSQRO/K${r}@[ $x, 55]@[80,120,42]@front \ +for i in 0..5 { \ + .var:r=eval 10+$i; \ + .var:x=eval (36+$i*4)/3; \ + +rk:/P/SI/BLDG/ROOM/J${r}@[ $x, 52]@[80,120,42]@rear; \ + +rk:/P/SI/BLDG/ROOM/K${r}@[ $x, 55]@[80,120,42]@front \ } ``` @@ -218,7 +293,9 @@ for i in 0..5 { \ .template:/path/to/template.json ``` -# Hierarchy commands +# Object Commands + +The following commands are generic and can be applied to most of the OGrEE objects. ## Select an object @@ -231,7 +308,7 @@ for i in 0..5 { \ =/Physical/Site/Building/RoomToSelect ``` -## Select child / children object +### Select child object Select one or several children of current selected object. *`[relativeName]` is the hierarchy name without the selected object part* @@ -241,13 +318,21 @@ Select one or several children of current selected object. ={[relativeName],[relativeName],...} ``` -## Select parent object +### Select parent object + +``` +=.. +``` + +## Focus an object + +*If `[name]` is empty, unfocus all items* ``` -.. +>[name] ``` -## Get object/s +## Get object The information of an object or a list of objects can be obtained using the get command: @@ -257,6 +342,15 @@ get [path] where `[path]` can be either the path of a single object (get rack1) or a list of objects using wildcards (get rack1/*) (see [Wildcards](#wildcards)). +``` +get SiteA +get /Physical/SiteB +get * // get all objs in current path +get * -f category=rack & height>10 // complex filter +get A* // get all objs with name starting by A +get A* category=rack // simple filter +``` + To see all possible options run: ``` @@ -349,7 +443,17 @@ ls [path] ``` ls can also be used without [path] to do ls on the current path. - +``` +ls +ls DEMO_RACK/DeviceA +ls /Physical/SiteA +ls $x // using a variable +ls -s height // sort by height +ls -a height:size // show obj's height and size on ls result +ls -r #racks // recursive, all levels below +ls . height=12 // simple filter +ls . -f category=rack & height>10 // complex filter +``` To see all possible options run: ``` @@ -441,6 +545,14 @@ tree // default path will be the current path (.) tree [path] // default depth will be 1 tree [path] [depth] ``` +Examples: +``` +tree . +tree . 2 +tree DEMO_RACK/DeviceA 2 +tree $x 4 +tree /Physical/SiteA +``` ## Delete object @@ -448,26 +560,58 @@ Works with single or multi selection. ``` -[name] --selection +``` +Examples: +``` +-. +-BUILDING/ROOM +-selection // delete all objects previously selected ``` -## Focus an object +## Modify object attribute +``` +[name]:[attribute]=[value] +``` +To add or modify new attributes use the syntax bellow, giving the name of the object, followed by the attribute and its value. +It also works with single or multi selection. +*`[name]` can be `selection` or `_` for modifying selected objects attributes* -*If `[name]` is empty, unfocus all items* +``` +selection:[attribute]=[value] +_:[attribute]=[value] +``` + +- Object's domain can be changed recursively for changing all it's children's domains at the same time. ``` ->[name] +[name]:domain=[value]@recursive ``` -## Copy object +- Object's description attribute is a string. Use `\n` to represent line-breaks. + +``` +[name]:description=[value] +// Example: +/P/SI/BLDG/R1/RACK:description="My Rack\nNew Servers\nWith GPU A4000" +``` -Currently it is only possible to copy layers. To copy an object use: +- Object's clearance are vector6, they define how much gap (in mm) has to be left on each side of the object: ``` -cp [source] [dest] +[name]:clearance=[front, rear, left, right, top, bottom] +// Example: +/P/SI/BLDG/R1/RACK:clearance=[800,500,0,0,0,0] ``` -where `[source]` is the path of the object to be copied (currently only objects in /Logical/Layers are accepted) and `[dest]` is the destination path or slug of the destination layer. +## Delete object attribute +``` +-[name]:[attribute] +``` +Use this command to remove not requirde attributes of a given object. + +``` +-/P/site/building:mycustomattr +``` ## Link/Unlink object Unlink an object transforms the object in a stray. In other words, it moves the object from the OGrEE hierarchy (no longer has a parent) and changes its type to stray object. @@ -491,30 +635,55 @@ link /Physical/Stray@/Physical/site/bldg/room/rack link /Physical/Stray@/Physical/site/bldg/room/rack@slots=[slot1,slot2]@orientation=front ``` -# Create commands +# Object Specific Commands -## Create a Domain +Each object entity has its own create command and may have some special commands to allow interaction. -To create a domain, a name and color should be provided. Should be in the `/Organisation/Domain` (`/O/Domain` on short version) path or include it in the domain's name. -Domains can have a hierarchy, that is, a domain can have a parent domain. -*`[color]` should be a 6 digit HEX Value (ie 00000A)* +## Domain ``` +domain:[name]@[color] +do:[name]@[color] ``` +To create a domain, a name and color should be provided. Should be in the `/Organisation/Domain` (`/O/Domain` on short version) path or include it in the domain's name. +Domains can have a hierarchy, that is, a domain can have a parent domain. +*`[color]` should be a 6 digit HEX Value (ie 00000A)* -## Create a Site +``` ++domain:Newdomain@ff00ee ++do:/O/Domain/Newdomain/Newsubdomain@000AAA +``` -Sites have no parent, only a name is needed to create it. +## Site ``` +site:[name] +si:[name] ``` +Sites have no parent, only a name is needed to create it. + +``` ++site:siteA // current path: /Physical ++si:/P/siteB +``` + +### Set colors for zones of all rooms in a datacenter + +Add or modify the following color attributes of site: +``` +[site]:usableColor=[color] +[site]:reservedColor=[color] +[site]:technicalColor=[color] +``` -## Create a Building +## Building +``` ++building:[name]@[pos]@[rotation]@[size] ++building:[name]@[pos]@[rotation]@[template] ++bd:[name]@[pos]@[rotation]@[size] ++bd:[name]@[pos]@[rotation]@[template] +``` Building must be child of a Site. *`[pos]` is a Vector2 [x,y] (m,m) `[rotation]` is the rotation of the building around its lower left corner, in degree @@ -522,14 +691,18 @@ Building must be child of a Site. `[template]` is the name (slug) of the building template* ``` -+building:[name]@[pos]@[rotation]@[size] -+building:[name]@[pos]@[rotation]@[template] -+bd:[name]@[pos]@[rotation]@[size] -+bd:[name]@[pos]@[rotation]@[template] ++building:/P/siteA/BldgA@[5,5]@49.1@[300,300,300] ++bd:BldgA@[5,5]@-27.89@BldgTemplateA ``` -## Create a Room +## Room +``` ++room:[name]@[pos]@[rotation]@[size]@[axisOrientation]@[floorUnit] ++room:[name]@[pos]@[rotation]@[template] ++ro:[name]@[pos]@[rotation]@[size]@[axisOrientation]@[floorUnit] ++ro:[name]@[pos]@[rotation]@[template] +``` Room must be child of a building. Its name will be displayed in the center of the room in its local coordinates system. *`[pos]` is a Vector2 [x,y] (m,m) @@ -540,350 +713,324 @@ Its name will be displayed in the center of the room in its local coordinates sy `[floorUnit]` is optional: by default set to "t" (tiles), can also be m (meters) or f (feet)* ``` -+room:[name]@[pos]@[rotation]@[size]@[axisOrientation]@[floorUnit] -+room:[name]@[pos]@[rotation]@[template] -+ro:[name]@[pos]@[rotation]@[size]@[axisOrientation]@[floorUnit] -+ro:[name]@[pos]@[rotation]@[template] -``` - -## Create a Rack - -Rack must be child of a room. -`[pos]` is a Vector2 [x,y] (tile,tile) or a Vector3 [x,y,z] (tile,tile,cm) if the rack is wall mounted. It can be decimal or fraction. Can also be negative -`[unit]` is t(tiles), m(meters) or f(feet) -`[rotation]` is a Vector3 of angles or one of following keywords : - "front": [0, 0, 180] - "rear": [0, 0, 0] - "left": [0, 90, 0] - "right": [0, -90, 0] - "top": [90, 0, 0] - "bottom": [-90, 0, 0] -`[size]` is a Vector3 [width,length,height] (cm,cm,u) -`[template]` is the name of the rack template - -``` -+rack:[name]@[pos]@[unit]@[rotation]@[size] -+rack:[name]@[pos]@[unit]@[rotation]@[template] -+rk:[name]@[pos]@[unit]@[rotation]@[size] -+rk:[name]@[pos]@[unit]@[rotation]@[template] ++ro:/P/siteA/BldgA/R1@[0,0]@-36.202@[22.8,19.8,2]@+N+W@t ++room:/P/siteA/BldgA/R1@[0,0]@-36.202@RoomTemplateA ``` -## Create a Device -A chassis is a *parent* device racked at a defined U position. -*`[posU]` is the position in U in a rack -`[sizeU]` is the height in U in a rack -`[slot]` is a square brackets list [] with the names of slots in which you want to place the device separated by a comma. Example: [slot1, slot2, slot3]. A shorter version with `..` can be used for a single range of slots: [slot1..slot3]. If no template is given, only one slot can be provided in the list. -`[template]` is the name of the device template -`[invertOffset]` is a boolean that tells the 3D client to invert the default offset for positioning the device in its slot (false by default, if not provided) -`[side]` is from which side you can see the device if not "fullsize". This value is for overriding the one defined in the template. It can be front | rear | frontflipped | rearflipped* -If the parent rack doesn't have slots: +### Set reserved and technical zones of a room ``` -+device:[name]@[posU]@[sizeU] -+device:[name]@[posU]@[template] +[room]:areas=[reserved]@[technical] ``` +Enables tiles edges display. +You can modify areas only if the room has no racks in it. +**Technical** area : typically a restricted zone where power panels and AC systems are installed. separated from "IT space" with either a wall or a wire mesh +**Reserved** area : some tiles around the room that must be kept free to move racks and walk (usually 2 or 3 tiles) -If the parent rack has slots: +*`[reserved]` is a vector4: [front,back,right,left] (tile,tile,tile,tile) +`[technical]` is a vector4: [front,back,right,left] (tile,tile,tile,tile)* ``` -+device:[name]@[slot]@[sizeU] -+device:[name]@[slot]@[template] -+device:[name]@[slot]@[sizeU]@[invertOffset] -+device:[name]@[slot]@[template]@[invertOffset] -``` - -All other devices (blades / components like processor, memory, adapters, disks...) have to be declared with a parent's slot and a template. - +/P/SI/BLDG/ROOM:areas=[2,2,2,2]@[3,1,1,1] ``` -+device:[name]@[slot]@[template] -+device:[name]@[slot]@[template]@[invertOffset] -+device:[name]@[slot]@[template]@[invertOffset]@[side] -+dv:[name]@[slot]@[template] -+dv:[name]@[slot]@[template]@[invertOffset] -+dv:[name]@[slot]@[template]@[invertOffset]@[side] -``` - -## Create a Group - -Group must be child of a room or a rack -A group is a box containing all given children. -- If the group is a child of a room, it can contain racks and corridors. -- If the group is a child of a rack, it can contain devices. +### Room separators -`c1,c2,...,cN` are the short names (eg. A01 instead of tn.si.bd.ro.A01) +Separators (wired or plain walls) can be added inside rooms. To do it, use: ``` -+group:[name]@{c1,c2,...,cN} -+gr:[name]@{c1,c2,...,cN} +[room]:separators+=[name]@[startPos]@[endPos]@[type] ``` -## Create a Corridor +Where: +*`[name]` is an identifier for the separator +`[startPos]` is a vector2: [x,y] (m,m) +`[endPos]` is a vector2: [x,y] (m,m) +`[type]` is the type of wall: wireframe or plain* -Corridor must be child of a room -A corridor is a cold or warm corridor. -`[pos]` is a Vector2 [x,y] (tile,tile) or a Vector3 [x,y,z] (tile,tile,cm) if the corridor is wall mounted. It can be decimal or fraction. Can also be negative -`[unit]` is t(tiles), m(meters) or f(feet) -`[rotation]` is a Vector3 of angles or one of following keywords : - "front": [0, 0, 180] - "rear": [0, 0, 0] - "left": [0, 90, 0] - "right": [0, -90, 0] - "top": [90, 0, 0] - "bottom": [-90, 0, 0] -`[size]` is a Vector3 [width,length,height] (cm,cm,cm) -`[temperature]` is cold or warm. +It will add the given separator to `[room].attributes["separators"]`, which is a list of all its separators. ``` -+corridor:[name]@[pos]@[unit]@[rotation]@[size]@[temperature] -+co:[name]@[pos]@[unit]@[rotation]@[size]@[temperature] +/P/SI/BLDG/ROOM:separators+=sep1@[1.2,10.2]@[1.2,14.2]@wireframe ``` -## Create a Tag - -Tags are identified by a slug. In addition, they have a color, a description and an image (optional). To create a tag, use: +Separators can be removed using: ``` -+tag:[slug]@[color] +[room]:separators-=[name] ``` -The description will initially be defined the same as the slug, but can be modified (see [Modify object's attribute](#modify-objects-attribute)). Image can only be modified from the web version. - -After the tag is created, it can be seen in /Logical/Tags. The command `get /Logical/Tags/[slug]` can be used to get the tag information. In doing so, the tag image will be the route in which the image can be obtained via an http request. +Where: +*`[name]` is the identifier of the separator to be removed -## Create a Layer +### Room pillars -Layers are identified by a slug. In addition, they have an applicability and the filters they apply. To create a layer, use: +Pillars can be added inside rooms. To do it, use: ``` -+layer:[slug]@[applicability]@[filter] +[room]:pillars+=[name]@[centerXY]@[sizeXY]@[rotation] ``` -The applicability is the path in which the layer should be added when doing ls. Patterns can be used in the applicability (see [Applicability Patterns](#applicability-patterns)). +Where: +*`[name]` is an identifier for the pillar +`[centerXY]` is a vector2: [x,y] (m,m) +`[sizeXY]` is a vector2: [x,y] (m,m) +`[rotation]` is the angle of the pillar, in degrees* -Layers can have simple filters in the format `field=value` or complex ones, composed of boolean expressions with the operators `=`, `!=`, `<`, `<=`, `>`, `>=`, `&` and `|`; parenthesis can also be used to separate the complex expressions. A first filter should be given to to create the layer. +It will add the given pillar to `[room].attributes["pillars"]`, which is a list of all its pillars. ``` -+layer:[slug]@[applicability]@name=[name] -+layer:[slug]@[applicability]@height=[height] -+layer:[slug]@[applicability]@category=[category] & name!=[name] -+layer:[slug]@[applicability]@(name=[name] & height<[height]) | domain=[domain] +/P/SI/BLDG/ROOM:pillars+=pillar1@[4.22,3.85]@[0.25,0.25]@0 ``` -To add more filters, simple or complex ones, edit the layer using the following syntax: +Pillars can be removed using: ``` -[layer_path]:filters+=[filter] +[room]:pillars-=[name] ``` -This action will add an `AND` operation between the new filter and the existing layer filter. +Where: +*`[name]` is the identifier of the pillar to be removed -Examples: -``` -[layer_path]:filters+=name=[name] -[layer_path]:filters+=height=[height] -[layer_path]:filters+=category=[category] & name!=[name] -[layer_path]:filters+=(name=[name] & height<[height]) | domain=[domain] -``` +### Interact with Room -Where [layer_path] is `/Logical/Layers/[slug]` (or only `[slug]` if the current path is /Logical/Layers). +The same way you can modify object's attributes, you can interact with them through specific commands. -To redefine the filter of a layer, editi using the following syntax: +- Display or hide tiles name ``` -[layer_path]:filters=[filter] +[name]:tilesName=[true|false] ``` -Examples: +- Display or hide colors and textures + ``` -[layer_path]:filters=name=[name] -[layer_path]:filters=height=[height] -[layer_path]:filters=category=[category] & name!=[name] -[layer_path]:filters=(name=[name] & height<[height]) | domain=[domain] +[name]:tilesColor=[true|false] ``` -For the layer to filter the children whose category is device. When adding filters on different attributes, all must be fulfilled for a child to be part of the layer. -Layers are not applied until their filters are defined. - -After the layer is created, it can be seen in /Logical/Layers. The command `get /Logical/Layers/[slug]` can be used to get the layer information. +## Rack -### Applicability Patterns +``` ++rack:[name]@[pos]@[unit]@[rotation]@[size] ++rack:[name]@[pos]@[unit]@[rotation]@[template] ++rk:[name]@[pos]@[unit]@[rotation]@[size] ++rk:[name]@[pos]@[unit]@[rotation]@[template] +``` +Rack must be child of a room. +`[pos]` is a Vector2 [x,y] (tile,tile) or a Vector3 [x,y,z] (tile,tile,cm) if the rack is wall mounted. It can be decimal or fraction. Can also be negative +`[unit]` is t(tiles), m(meters) or f(feet) +`[rotation]` is a Vector3 of angles or one of following keywords : + "front": [0, 0, 180] + "rear": [0, 0, 0] + "left": [0, 90, 0] + "right": [0, -90, 0] + "top": [90, 0, 0] + "bottom": [-90, 0, 0] +`[size]` is a Vector3 [width,length,height] (cm,cm,u) +`[template]` is the name of the rack template -The following special terms are supported in the patterns: +``` ++rack:/P/siteA/BldgA/R1/A01@[1,2]@t@[0,0,180]@[60,120,42] ++rk:A01@[9,1]@t@[60,120,45]@BldgTemplate // current path /P/siteA/BldgA +``` -Special Terms | Meaning -------------- | ------- -`*` | matches any sequence of non-path-separators -`/**/` | matches zero or more directories -`?` | matches any single non-path-separator character -`[class]` | matches any single non-path-separator character against a class of characters (see [Character classes](#character-classes)) -`{alt1,...}` | matches a sequence of characters if one of the comma-separated alternatives matches +### Interact with Rack -Any character with a special meaning can be escaped with a backslash (`\`). +- Display or hide rack's box. This will also affect its label -A doublestar (`**`) should appear surrounded by path separators such as `/**/`. -A mid-pattern doublestar (`**`) behaves like bash's globstar option: a pattern -such as `path/to/**` would return the same results as `path/to/*`. The -pattern you're looking for is `path/to/**/*`. +``` +[name]:alpha=[true|false] +``` -#### Character Classes +- Display or hide rack's U helpers to simply identify objects in a rack. -Character classes support the following: +``` +[name]:U=[true|false] +``` -Class | Meaning ----------- | ------- -`[abc]` | matches any single character within the set -`[a-z]` | matches any single character in the range -`[^class]` | matches any single character which does *not* match the class -`[!class]` | same as `^`: negates the class +- Display or hide rack's slots -## Create a Generic Object +``` +[name]:slots=[true|false] +``` -Generic objects allow you to model any type of object that is not of the previous classes (tables, cabinets, doors, etc). +- Display or hide rack's local coordinate system -They must be child of a room. +``` +[name]:localCS=[true|false] +``` -To create them, use one of the following options: +## Device -``` -+generic:[name]@[pos]@[unit]@[rotation]@[size]@[shape]@[type] -``` +If the parent rack doesn't have slots: ``` -+generic:[name]@[pos]@[unit]@[rotation]@[template] ++device:[name]@[posU]@[sizeU] ++device:[name]@[posU]@[template] ``` +If the parent rack has slots: + ``` -+ge:[name]@[pos]@[unit]@[rotation]@[size]@[shape]@[type] -``` ++device:[name]@[slot]@[sizeU] ++device:[name]@[slot]@[template] ++device:[name]@[slot]@[sizeU]@[invertOffset] ++device:[name]@[slot]@[template]@[invertOffset] +``` +A chassis is a *parent* device racked at a defined U position. +All other devices (blades / components like processor, memory, adapters, disks...) have to be declared with a parent's slot and a template. ``` -+ge:[name]@[pos]@[unit]@[rotation]@[template] ++device:[name]@[slot]@[template] ++device:[name]@[slot]@[template]@[invertOffset] ++device:[name]@[slot]@[template]@[invertOffset]@[side] ++dv:[name]@[slot]@[template] ++dv:[name]@[slot]@[template]@[invertOffset] ++dv:[name]@[slot]@[template]@[invertOffset]@[side] +``` + +*`[posU]` is the position in U in a rack +`[sizeU]` is the height in U in a rack +`[slot]` is a square brackets list [] with the names of slots in which you want to place the device separated by a comma. Example: [slot1, slot2, slot3]. A shorter version with `..` can be used for a single range of slots: [slot1..slot3]. If no template is given, only one slot can be provided in the list. +`[template]` is the name of the device template +`[invertOffset]` is a boolean that tells the 3D client to invert the default offset for positioning the device in its slot (false by default, if not provided) +`[side]` is from which side you can see the device if not "fullsize". This value is for overriding the one defined in the template. It can be front | rear | frontflipped | rearflipped* ``` ++dv:/P/siteA/BldgA/R1/A01/chassis@12@10 ++dv:/P/siteA/BldgA/R1/A01/devA@[SlotA,SlotB]@10 ++dv:/P/siteA/BldgA/R1/A01/devB@[SlotC]@DevTemplate ++dv:/P/siteA/BldgA/R1/A01/devB@[SlotC]@DevTemplate@true@front +``` -Where: - -- `[pos]` is a Vector3 [x,y,z] or a Vector2 [x,y] if z is 0. Each value can be decimal (1, 1.2, etc.) or fraction (1/2, 2/3, etc.). Can also be negative (-1, -1.2, -1/2). -- `[unit]` is the unit of the position [pos]. It can be: `t` (tiles), `m` (meters) or `f` (feet). -- `[rotation]` is a Vector3 of angles or one of following keywords: +### Interact with Device - "front": [0, 0, 180] - "rear": [0, 0, 0] - "left": [0, 90, 0] - "right": [0, -90, 0] - "top": [90, 0, 0] - "bottom": [-90, 0, 0] -- `[size]` is a Vector3 [width,length,height] . All values are in cm. -- `[shape]` is a string defining the shape of the object. It can be: `cube`, `sphere` or `cylinder`. -- `[type]` is a string defining the type of the object. No predefined values. -- `[template]` is the name of the rack template +- Display or hide device's box. This will also affect its label -# Set commands +``` +[name]:alpha=[true|false] +``` -## Set colors for zones of all rooms in a datacenter +- Display or hide device's slots ``` -[datacenter]:usableColor=[color] -[datacenter]:reservedColor=[color] -[datacenter]:technicalColor=[color] -``` +[name]:slots=[true|false] +``` -## Set reserved and technical zones of a room +- Display or hide device's local coordinate system -Enables tiles edges display. -You can modify areas only if the room has no racks in it. -**Technical** area : typically a restricted zone where power panels and AC systems are installed. separated from "IT space" with either a wall or a wire mesh -**Reserved** area : some tiles around the room that must be kept free to move racks and walk (usually 2 or 3 tiles) +``` +[name]:localCS=[true|false] +``` -*`[reserved]` is a vector4: [front,back,right,left] (tile,tile,tile,tile) -`[technical]` is a vector4: [front,back,right,left] (tile,tile,tile,tile)* +## Group ``` -[room]:areas=[reserved]@[technical] ++group:[name]@{c1,c2,...,cN} ++gr:[name]@{c1,c2,...,cN} ``` +Group must be child of a room or a rack +A group is represented as a single box in the 3D client, containing all given children. -## Room separators +- If the group is a child of a room, it can contain racks and corridors. +- If the group is a child of a rack, it can contain devices. -Separators (wired or plain walls) can be added inside rooms. To do it, use: +`c1,c2,...,cN` are the short names (eg. A01 instead of /P/siteA/BldgA/R1/A01) ``` -[room]:separators+=[name]@[startPos]@[endPos]@[type] ++gr:/P/siteA/BldgA/R1/GR1@{A01,A02,A03} // group child of room, contains racks ``` -Where: -*`[name]` is an identifier for the separator -`[startPos]` is a vector2: [x,y] (m,m) -`[endPos]` is a vector2: [x,y] (m,m) -`[type]` is the type of wall: wireframe or plain* - -It will add the given separator to `[room].attributes["separators"]`, which is a list of all its separators. +### Interact with Group -Separators can be removed using: +- Display or hide contained objects ``` -[room]:separators-=[name] +[name]:content=[true|false] ``` -Where: -*`[name]` is the identifier of the separator to be removed +## Corridor -## Room pillars +``` ++corridor:[name]@[pos]@[unit]@[rotation]@[size]@[temperature] ++co:[name]@[pos]@[unit]@[rotation]@[size]@[temperature] +``` +Corridor must be child of a room. -Pillars can be added inside rooms. To do it, use: +`[pos]` is a Vector2 [x,y] (tile,tile) or a Vector3 [x,y,z] (tile,tile,cm) if the corridor is wall mounted. It can be decimal or fraction. Can also be negative +`[unit]` is t(tiles), m(meters) or f(feet) +`[rotation]` is a Vector3 of angles or one of following keywords : + "front": [0, 0, 180] + "rear": [0, 0, 0] + "left": [0, 90, 0] + "right": [0, -90, 0] + "top": [90, 0, 0] + "bottom": [-90, 0, 0] +`[size]` is a Vector3 [width,length,height] (cm,cm,cm) +`[temperature]` is cold or warm. ``` -[room]:pillars+=[name]@[centerXY]@[sizeXY]@[rotation] ++co:/P/siteA/BldgA/R1/CO1@[0,2]@t@[0,0,0]@[180,120,200]@warm ++co:/P/siteA/BldgA/R1/CO2@[3,2]@m@rear@[3*60,2*60,200]@cold ``` -Where: -*`[name]` is an identifier for the pillar -`[centerXY]` is a vector2: [x,y] (m,m) -`[sizeXY]` is a vector2: [x,y] (m,m) -`[rotation]` is the angle of the pillar, in degrees* +## Generic Object -It will add the given pillar to `[room].attributes["pillars"]`, which is a list of all its pillars. +Generic objects allow you to model any type of object that is not of the previous classes (tables, cabinets, doors, etc). -Pillars can be removed using: +They must be child of a room. + +To create them, use one of the following options: ``` -[room]:pillars-=[name] ++generic:[name]@[pos]@[unit]@[rotation]@[size]@[shape]@[type] ++generic:[name]@[pos]@[unit]@[rotation]@[template] ++ge:[name]@[pos]@[unit]@[rotation]@[size]@[shape]@[type] ++ge:[name]@[pos]@[unit]@[rotation]@[template] ``` -Where: -*`[name]` is the identifier of the pillar to be removed +Where: + +- `[pos]` is a Vector3 [x,y,z] or a Vector2 [x,y] if z is 0. Each value can be decimal (1, 1.2, etc.) or fraction (1/2, 2/3, etc.). Can also be negative (-1, -1.2, -1/2). +- `[unit]` is the unit of the position [pos]. It can be: `t` (tiles), `m` (meters) or `f` (feet). +- `[rotation]` is a Vector3 of angles or one of following keywords: -## Modify object's attribute + "front": [0, 0, 180] + "rear": [0, 0, 0] + "left": [0, 90, 0] + "right": [0, -90, 0] + "top": [90, 0, 0] + "bottom": [-90, 0, 0] +- `[size]` is a Vector3 [width,length,height] . All values are in cm. +- `[shape]` is a string defining the shape of the object. It can be: `cube`, `sphere` or `cylinder`. +- `[type]` is a string defining the type of the object. No predefined values. +- `[template]` is the name of the rack template -Works with single or multi selection. -*`[name]` can be `selection` or `_` for modifying selected objects attributes* +Examples: ``` -[name]:[attribute]=[value] - -selection:[attribute]=[value] -_:[attribute]=[value] ++ge:/P/SI/BLDG/ROOM/BOX@[0,6,10]@t@[0,0,90]@[10,10,10]@cube@box ++ge:/P/SI/BLDG/ROOM/CHAIR@[5,5]@t@front@chair // with template ``` -- Object's domain can be changed recursively for changing all it's children's domains at the same time. -``` -[name]:domain=[value]@recursive -``` +## Tag -- Object's description attribute is a string. Use `\n` to represent line-breaks. +Tags are identified by a slug. In addition, they have a color, a description and an image (optional). To create a tag, use: ``` -[name]:description=[value] ++tag:[slug]@[color] ``` -- Object's clearance are vector6, they define how much gap (in mm) has to be left on each side of the object: +The description will initially be defined the same as the slug, but can be modified (see [Modify object's attribute](#modify-objects-attribute)). Image can only be added or modified through the APP. + +After the tag is created, it can be seen in /Logical/Tags. The command `get /Logical/Tags/[slug]` can be used to get the tag information. In this get response, the field image contains a value that can be used to download the image from the API (check the endpoint /api/images in the API documentation). ``` -[name]:clearance=[front, rear, left, right, top, bottom] ++tag:gpu_servers@00ff00 ``` -## Tags +### Apply tags to objects Any object can be taged. When getting an object, it will contain a list of tags, example: @@ -904,7 +1051,7 @@ To add a tag to an object use: [name]:tags+=[tag_slug] ``` -Where tag_slug is the slug of an existing tag, which can be found in /Logical/Tags. +Where tag_slug is the slug of an existing tag, which can be found in /Logical/Tags. The tag _must_ be previously created (check [Create Tag](https://github.com/ditrit/OGrEE-Core/wiki/%F0%9F%93%97-%5BUser-Guide%5D-CLI-%E2%80%90-Language#create-a-tag)). To remove a tag from an object use: @@ -912,117 +1059,149 @@ To remove a tag from an object use: [name]:tags-=[tag_slug] ``` -## Labels +## Layer -Some objects have a label displayed in there 3D model: racks, devices, rack groups and corridors. -The default label is the object's name. +Layers are identified by a slug. In addition, they have an applicability and the filters they apply. To create a layer, use: -### Choose Label +``` ++layer:[slug]@[applicability]@[filter] +``` -You can change the label by a string or with a chosen attribute: -*`#[attribute]` is one of the attribute of the object.* -*Use `\n` to insert line-breaks.* +The applicability is the path in which the layer should be added when running the `ls` command. Patterns can be used in the applicability (see [Applicability Patterns](#applicability-patterns)). + +Layers can have simple filters in the format `field=value` or complex ones, composed of boolean expressions with the operators `=`, `!=`, `<`, `<=`, `>`, `>=`, `&` and `|`; parenthesis can also be used to separate the complex expressions. A first filter should be given to to create the layer. ``` -[name]:label=#[attribute] -[name]:label=[string] ++layer:Aobjs@/P/site/bldg/room@name=A* // all objs of room starting by A +// only racks that does not start by A: ++layer:RacksNotA@/P/site/bldg/room@category=racks & name!=A* ``` -Examples: +To add more filters, simple or complex ones, edit the layer using the following syntax: + ``` -[name]:label=#id -[name]:label=This is a rack -[name]:label=My name is #name\nMy id is #id +[layer_path]:filter+=[filter] ``` -### Modify label's font - -You can make the font bold, italic or change its color. +This action will add an `AND` operation between the new filter and the existing layer filter. +Examples: ``` -[name]:labelFont=bold //will toggle bold -[name]:labelFont=italic //will toggle bold -[name]:labelFont=color@[color] +[layer_path]:filter+=name=[name] +[layer_path]:filter+=(name=[name] & height<[height]) | domain=[domain] ``` -### Modify label's background color +Where [layer_path] is `/Logical/Layers/[slug]` (or only `[slug]` if the current path is /Logical/Layers). -You can change the label's background color when it is hovering over the object. +To redefine the filter of a layer, edit using the following syntax: ``` -[name]:labelBackground=[color] +[layer_path]:filter=[filter] ``` -## Interact with objects +For the layer to filter the children whose category is device. When adding filters on different attributes, all must be fulfilled for a child to be part of the layer. -The same way you can modify object's attributes, you can interact with them through specific commands. +After the layer is created, it can be seen in /Logical/Layers. The command `get /Logical/Layers/[slug]` can be used to get the layer information. -### Room +### Applicability Patterns -- Display or hide tiles name +The following special terms are supported in the patterns: -``` -[name]:tilesName=[true|false] -``` +Special Terms | Meaning +------------- | ------- +`/*` | matches anything in the following directory +`/**/` | matches zero or more directories +`?` | matches any single character +`[class]` | matches any single character against a class of characters (see [Character classes](#character-classes)) +`{alt1,...}` | matches a sequence of characters if one of the comma-separated alternatives matches -- Display or hide colors and textures +Any character with a special meaning can be escaped with a backslash (`\`). -``` -[name]:tilesColor=[true|false] -``` +A doublestar (`**`) should appear surrounded by path separators such as `/**/`. +A mid-pattern doublestar (`**`) behaves like bash's globstar option: a pattern +such as `path/to/**` would return the same results as `path/to/*`. The +pattern you're looking is probably `path/to/**/*`. -### Rack +### Copy Layer -- Display or hide rack's box. This will also affect its label +Currently it is only possible to copy **layers**. To copy an object use: ``` -[name]:alpha=[true|false] +cp [source] [dest] ``` -- Display or hide rack's U helpers to simply identify objects in a rack. +where `[source]` is the path of the object to be copied (currently only objects in /Logical/Layers are accepted) and `[dest]` is the destination path or slug of the destination layer. -``` -[name]:U=[true|false] -``` +#### Character Classes -- Display or hide rack's slots +Character classes support the following: + +Class | Meaning +---------- | ------- +`[abc]` | matches any single character within the set +`[a-z]` | matches any single character in the range +`[^class]` | matches any single character which does *not* match the class +`[!class]` | same as `^`: negates the class + +#### Examples ``` -[name]:slots=[true|false] -``` +// layer available at all levels under /P/si/bldg +// e.g. at /P/si/bldg/room/rack/device ++layer:[slug]@/P/si/bldg/**/*@[filter] -- Display or hide rack's local coordinate system +// layer available only at levels directly under /P/si/bldg +// e.g. at /P/si/bldg/room but not at /P/si/bldg/room/rack ++layer:[slug]@/P/si/bldg/*@[filter] + +// layer available only at room level with id starting as /P/site/bldg/RoomA +// e.g. at /P/site/bldg/RoomA1 and /P/site/bldg/RoomA2 ++layer:[slug]@/P/si/bldg/RoomA?@[filter] +// layer available only at levels /P/site/bldg/RoomA and /P/site/bldg/RoomB +// e.g. not at /P/site/bldg/RoomC ++layer:[slug]@/P/si/bldg/Room[AB]@[filter] ``` -[name]:localCS=[true|false] -``` -### Device +## Labels -- Display or hide device's box. This will also affect its label +Some objects have a label displayed in there 3D model: racks, devices, rack groups and corridors. +The default label is the object's name. + +### Choose Label + +You can change the label by a string or with a chosen attribute: +*`#[attribute]` is one of the attribute of the object.* +*Use `\n` to insert line-breaks.* ``` -[name]:alpha=[true|false] +[name]:label=#[attribute] +[name]:label=[string] ``` -- Display or hide device's slots - +Examples: ``` -[name]:slots=[true|false] +[name]:label=#id +[name]:label=This is a rack +[name]:label=My name is #name\nMy id is #id ``` -- Display or hide device's local coordinate system +### Modify label's font + +You can make the font bold, italic or change its color. ``` -[name]:localCS=[true|false] +[name]:labelFont=bold //will toggle bold +[name]:labelFont=italic //will toggle bold +[name]:labelFont=color@[color] ``` -### Group +### Modify label's background color -- Display or hide contained racks/devices +You can change the label's background color when it is hovering over the object. ``` -[name]:content=[true|false] +[name]:labelBackground=[color] ``` # Manipulate UI @@ -1032,7 +1211,9 @@ The same way you can modify object's attributes, you can interact with them thro You can put delay before each command: up to 2 seconds. ``` -ui.delay=[time] +ui.delay=[time in seconds] +// Example: +ui.delay=0.5 // 500ms ``` ## Display infos panel @@ -1049,185 +1230,53 @@ ui.debug=[true|false] ## Highlight object -*This is a "toggle" command: use it to turn on/off the highlighting of an object. -If given object is hidden in its parent, the parent will be highlighted.* +This is a "toggle" command: use it to turn on/off the highlighting of an object. +If given object is hidden in its parent, the parent will be highlighted. ``` ui.highlight=[name] ui.hl=[name] +// Example: +ui.hl=/P/SI/BLDG/R1/RACK ``` # Manipulate camera ## Move camera +``` +camera.move=[position]@[rotation] +``` Move the camera to the given point. *`[position]` is a Vector3: the new position of the camera `[rotation]` is a Vector2: the rotation of the camera* ``` -camera.move=[position]@[rotation] +camera.move=[-20.2;-71.98,21.32]@[37,0] ``` ## Translate camera +``` +camera.translate=[position]@[rotation] +``` Move the camera to the given destination. You can stack several destinations, the camera will move to each point in the given order. *`[position]` is a Vector3: the position of the camera's destination `[rotation]` is a Vector2: the rotation of the camera's destination* ``` -camera.translate=[position]@[rotation] +camera.translate=[-17,15.5,22]@[78,-90] ``` ## Wait between two translations -You can define a delay between two camera translations. -*`[time]` is the time to wait in seconds* - ``` camera.wait=[time] ``` +You can define a delay between two camera translations. +*`[time]` is the time to wait in seconds* -# Control flow - -## Conditions - -``` ->if 42 > 43 { print toto } elif 42 == 43 { print tata } else { print titi } -titi -``` - -## Loops - -``` ->for i in 0..3 { .var: i2 = $(($i * $i)) ; print $i^2 = $i2 } -0^2 = 0 -1^2 = 1 -2^2 = 4 -3^2 = 9 -``` - -``` ->.var: i = 0; while $i<4 {print $i^2 = $(($i * $i)); .var: i = eval $i+1 } -0^2 = 0 -1^2 = 1 -2^2 = 4 -3^2 = 9 -``` - -## Aliases ``` ->alias pi2 { .var: i2 = $(($i * $i)) ; print $i^2 = $i2 } ->for i in 0..3 { pi2 } -0^2 = 0 -1^2 = 1 -2^2 = 4 -3^2 = 9 +camera.wait=5 // 5s ``` -# Examples - -``` -+do:DEMO@ffffff - DEMO.mainContact=Ced - DEMO.mainPhone=0612345678 - DEMO.mainEmail=ced@ogree3D.com - -+si:DEMO.ALPHA@NW - DEMO.ALPHA.description=This is a demo... - DEMO.ALPHA.address=1 rue bidule - DEMO.ALPHA.zipcode=42000 - DEMO.ALPHA.city=Truc - DEMO.ALPHA.country=FRANCE - DEMO.ALPHA.gps=[1,2,0] - DEMO.ALPHA.usableColor=5BDCFF - DEMO.ALPHA.reservedColor=AAAAAA - DEMO.ALPHA.technicalColor=D0FF78 - -// Building A - -+bd:DEMO.ALPHA.A@[0,0,0]@[12,12,5] - DEMO.ALPHA.A.description=Building A - DEMO.ALPHA.A.nbFloors=1 -+ro:DEMO.ALPHA.A.R0_EN@[6,6,0]@[4.2,5.4,1]@EN -+ro:DEMO.ALPHA.A.R0_NW@[6,6,0]@[4.2,5.4,1]@NW -+ro:DEMO.ALPHA.A.R0_WS@[6,6,0]@[4.2,5.4,1]@WS -+ro:DEMO.ALPHA.A.R0_SE@[6,6,0]@[4.2,5.4,1]@SE - -+rk:DEMO.ALPHA.A.R0_EN.TEST_EN@[ 1,1]@[60,120,42]@front -+rk:DEMO.ALPHA.A.R0_NW.TEST_NW@[1 ,1]@[60,120,42]@front -+rk:DEMO.ALPHA.A.R0_WS.TEST_WS@[1, 1]@[60,120,42]@front -+rk:DEMO.ALPHA.A.R0_SE.TEST_SE@[1,1 ]@[60,120,42]@front - -// Building B - -+bd:DEMO.ALPHA.B@[-30,10,0]@[25,29.4,5] - DEMO.ALPHA.B.description=Building B - DEMO.ALPHA.B.nbFloors=1 - -+ro:DEMO.ALPHA.B.R1@[0,0,0]@[22.8,19.8,4]@NW - DEMO.ALPHA.B.R1.areas=[2,1,5,2]@[3,3,1,1] - DEMO.ALPHA.B.R1.description=First room - -+ro:DEMO.ALPHA.B.R2@[22.8,19.8,0]@[9.6,22.8,3]@WS - DEMO.ALPHA.B.R2.areas=[3,1,1,3]@[5,0,0,0] - DEMO.ALPHA.B.R2.description=Second room, owned by Marcus - DEMO.ALPHA.B.R2.tenant=Marcus - -// Racks for R1 - -+rk:DEMO.ALPHA.B.R1.A01@[1,1]@[60,120,42]@front - DEMO.ALPHA.B.R1.A01.description=Rack A01 - DEMO.ALPHA.B.R1.A01.vendor=someVendor - DEMO.ALPHA.B.R1.A01.type=someType - DEMO.ALPHA.B.R1.A01.model=someModel - DEMO.ALPHA.B.R1.A01.serial=someSerial - -+rk:DEMO.ALPHA.B.R1.A02@[2,1]@[60,120,42]@front -+rk:DEMO.ALPHA.B.R1.A03@[3,1]@[60,120,42]@front -+rk:DEMO.ALPHA.B.R1.A04@[4,1]@[60,120,42]@front -+rk:DEMO.ALPHA.B.R1.A05@[5,1]@[60,120,42]@front - DEMO.ALPHA.B.R1.A05.tenant=Billy - -+rk:DEMO.ALPHA.B.R1.B05 @[8,6] @[60,120,42]@rear -+rk:DEMO.ALPHA.B.R1.B09 @[9,6] @[60,120,42]@rear -+rk:DEMO.ALPHA.B.R1.B010@[10,6]@[60,120,42]@rear -+rk:DEMO.ALPHA.B.R1.B011@[11,6]@[60,120,42]@rear -+rk:DEMO.ALPHA.B.R1.B012@[12,6]@[60,120,42]@rear - -+rk:DEMO.ALPHA.B.R1.C08 @[8,9] @[60,120,42]@front -+rk:DEMO.ALPHA.B.R1.C09 @[9,9] @[60,120,42]@front -+rk:DEMO.ALPHA.B.R1.C010@[10,9]@[60,120,42]@front -+rk:DEMO.ALPHA.B.R1.C011@[11,9]@[60,120,42]@front -+rk:DEMO.ALPHA.B.R1.C012@[12,9]@[60,120,42]@front - -+rk:DEMO.ALPHA.B.R1.D01@[20,5]@[60,120,42]@left - DEMO.ALPHA.B.R1.D01.tenant=Marcus -+rk:DEMO.ALPHA.B.R1.D02@[20,6]@[60,120,42]@left - DEMO.ALPHA.B.R1.D02.tenant=Marcus -+rk:DEMO.ALPHA.B.R1.D03@[20,7]@[60,120,42]@left - DEMO.ALPHA.B.R1.D03.tenant=Marcus - -+rk:DEMO.ALPHA.B.R1.E01@[23,5]@[60,120,42]@right - DEMO.ALPHA.B.R1.E01.tenant=Marcus -+rk:DEMO.ALPHA.B.R1.E02@[23,6]@[60,120,42]@right - DEMO.ALPHA.B.R1.E02.tenant=Marcus -+rk:DEMO.ALPHA.B.R1.E03@[23,7]@[60,120,42]@right - DEMO.ALPHA.B.R1.E03.tenant=Marcus - -// Racks for R2 - -+rk:DEMO.ALPHA.B.R2.A01@[1,3]@[60,120,42]@rear -+rk:DEMO.ALPHA.B.R2.A02@[2,3]@[60,120,42]@rear -+rk:DEMO.ALPHA.B.R2.A03@[3,3]@[60,120,42]@rear -+rk:DEMO.ALPHA.B.R2.A04@[4,3]@[60,120,42]@rear -+rk:DEMO.ALPHA.B.R2.A05@[5,3]@[60,120,42]@rear - -+rk:DEMO.ALPHA.B.R2.B01@[1,5]@[60,120,42]@front - DEMO.ALPHA.B.R2.B01.tenant=Billy - DEMO.ALPHA.B.R2.B01.alpha=50 - -// Edit description of several racks in R1 -={B05,B09,B10,B11,B12} -selection.description=Row B -```