diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8252ed59e735..3ceb0ad85742 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,27 +1,263 @@
# Contributing to Appsmith
Thank you for your interest in Appsmith and for taking the time to contribute to this project. 🙌
-Appsmith is a project by developers for developers and there are a lot of ways you can contribute.
-If you don't know where to start contributing, ask us on our [Discord channel](https://discord.com/invite/rBTTVJp).
-## Code of conduct
+Appsmith is a project by developers for developers and there are a lot of ways you can contribute. If you don't know where to start contributing, ask us on our [Discord channel](https://discord.com/invite/rBTTVJp).
-Read our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing
+## Table of Contents
-## How can I contribute?
+- [Code of Conduct](#code-of-conduct)
+- [Project Architecture](#project-architecture)
+- [Quick Start Checklist](#quick-start-checklist)
+- [How Can I Contribute?](#how-can-i-contribute)
+- [Development Setup](#development-setup)
+- [Pull Request Process](#pull-request-process)
+- [Community Support](#community-support)
-There are many ways in which you can contribute to Appsmith.
+---
+
+## Code of Conduct
+
+Read our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing. We are committed to fostering an open, welcoming, and safe environment in the community.
+
+---
+
+## Project Architecture
+
+Understanding the project structure will help you navigate the codebase more effectively:
+
+```
+appsmith/
+├── app/
+│ ├── client/ # Frontend (React + TypeScript)
+│ │ ├── src/ # React components, widgets, utilities
+│ │ ├── cypress/ # Cypress integration tests
+│ │ └── packages/ # Shared packages including RTS (Real-Time Server)
+│ └── server/ # Backend (Java + Spring + WebFlux)
+│ ├── appsmith-server/ # Main server application
+│ └── appsmith-plugins/ # Database/API connectors
+├── deploy/ # Docker & Kubernetes deployment configs
+├── contributions/ # Contribution guides and documentation
+└── static/ # Static assets for documentation
+```
+
+### Tech Stack Overview
+
+| Component | Technologies |
+|-----------|--------------|
+| **Frontend** | React, TypeScript, Redux, Redux-Saga |
+| **Backend** | Java 25, Spring, WebFlux, MongoDB, Redis |
+| **Testing** | Jest (unit), Cypress (integration), JUnit (server) |
+| **Deployment** | Docker, Kubernetes |
+
+---
+
+## Quick Start Checklist
+
+New to contributing? Follow this checklist to get started:
+
+### Before You Start
+- [ ] Read this CONTRIBUTING guide
+- [ ] Read our [Code of Conduct](CODE_OF_CONDUCT.md)
+- [ ] Join our [Discord community](https://discord.com/invite/rBTTVJp) for support
+
+### For Code Contributions
+- [ ] Find an issue to work on (see [Finding Issues](#finding-issues))
+- [ ] Comment on the issue to get it assigned to you
+- [ ] Fork and clone the repository
+- [ ] Set up your development environment (see [Development Setup](#development-setup))
+- [ ] Create a feature branch from `release`
+- [ ] Make your changes and write tests
+- [ ] Submit a pull request
+
+### For Documentation Contributions
+- [ ] Visit the [appsmith-docs repository](https://github.com/appsmithorg/appsmith-docs)
+- [ ] Read the [Docs Contribution Guide](https://github.com/appsmithorg/appsmith-docs/blob/main/CONTRIBUTING.md)
+
+---
+
+## How Can I Contribute?
+
+### 🐛 Report a Bug
-#### 🐛 Report a bug
Report all issues through GitHub Issues using the [Report a Bug](https://github.com/appsmithorg/appsmith/issues/new?assignees=Nikhil-Nandagopal&labels=Bug%2CNeeds+Triaging&template=--bug-report.yaml&title=%5BBug%5D%3A+) template.
-To help resolve your issue as quickly as possible, read the template and provide all the requested information.
-#### 🛠 File a feature request
-We welcome all feature requests, whether it's to add new functionality to an existing extension or to offer an idea for a brand new extension.
-File your feature request through GitHub Issues using the [Feature Request](https://github.com/appsmithorg/appsmith/issues/new?assignees=Nikhil-Nandagopal&labels=Enhancement&template=--feature-request.yaml&title=%5BFeature%5D%3A+) template.
+**Tips for a good bug report:**
+- Use a clear and descriptive title
+- Include steps to reproduce the issue
+- Describe the expected vs. actual behavior
+- Include screenshots or screen recordings if helpful
+- Mention your environment (OS, browser, Appsmith version)
+
+### 🛠 File a Feature Request
+
+We welcome all feature requests! File your request through GitHub Issues using the [Feature Request](https://github.com/appsmithorg/appsmith/issues/new?assignees=Nikhil-Nandagopal&labels=Enhancement&template=--feature-request.yaml&title=%5BFeature%5D%3A+) template.
+
+**Tips for a good feature request:**
+- Describe the problem you're trying to solve
+- Explain how your suggestion would help solve it
+- Include examples or mockups if available
+
+### 📝 Improve the Documentation
+
+Help us keep our documentation up to date! You can:
+- Suggest improvements using the [Documentation templates](https://github.com/appsmithorg/appsmith-docs/issues/new/choose)
+- Contribute directly to our [Docs repository](https://github.com/appsmithorg/appsmith-docs)
+
+### ⚙️ Contribute Code
+
+#### Finding Issues
+
+Looking for issues to contribute to? Here are some great starting points:
+
+| Issue Type | Link |
+|------------|------|
+| Good First Issues | [Browse →](https://github.com/appsmithorg/appsmith/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22) |
+| Inviting Contributions | [Browse →](https://github.com/appsmithorg/appsmith/issues?q=is%3Aopen+is%3Aissue+label%3A%22Inviting+Contribution%22) |
+| Help Wanted | [Browse →](https://github.com/appsmithorg/appsmith/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22) |
+
+> ⚠️ **Important:** Always get an issue assigned to you before starting work. Comment on the issue expressing your interest. Tag `@contributor-support` if needed. Working on issues without assignment may result in your contribution being rejected.
+
+#### Types of Code Contributions
+
+| Contribution Type | Guide |
+|-------------------|-------|
+| Frontend (React/TypeScript) | [Client Setup Guide](contributions/ClientSetup.md) |
+| Backend (Java/Spring) | [Server Setup Guide](contributions/ServerSetup.md) |
+| New Widget | [Widget Development Guide](contributions/AppsmithWidgetDevelopmentGuide.md) |
+| New Plugin/Connector | [Plugin Contribution Guide](contributions/ServerCodeContributionsGuidelines/PluginCodeContributionsGuidelines.md) |
+| Add Custom JS Library | [Custom JS Library Guide](contributions/CustomJsLibrary.md) |
+| Write Tests | [Test Automation Guide](contributions/docs/TestAutomation.md) |
+
+---
+
+## Development Setup
+
+### Prerequisites
+
+Before setting up the development environment, ensure you have:
+
+| Tool | Version | Notes |
+|------|---------|-------|
+| Docker | Latest | Required for containerized services |
+| Node.js | 20.11.1 | Use nvm or fnm for version management |
+| Java | OpenJDK 25 | Eclipse Temurin recommended |
+| Maven | 3.9+ | Preferably 3.9.12 |
+| Git | Latest | For version control |
+| mkcert | Latest | For local HTTPS certificates |
+
+### Quick Setup
+
+#### Frontend Only (for UI contributions)
+
+```bash
+# Clone your fork
+git clone https://github.com/YOUR_USERNAME/appsmith.git
+cd appsmith
+
+# Set up local HTTPS certificates
+cd app/client/docker && mkcert -install && mkcert "*.appsmith.com" && cd ../../..
+
+# Add dev domain to hosts
+echo "127.0.0.1 dev.appsmith.com" | sudo tee -a /etc/hosts
+
+# Copy environment file
+cp .env.example .env
+
+# Install dependencies and start
+cd app/client
+yarn install
+./start-https.sh https://release.app.appsmith.com # Use staging backend
+yarn start
+```
+
+Your frontend will be running at https://dev.appsmith.com
+
+#### Full Stack Setup
+
+1. **Set up MongoDB and Redis** (using Docker):
+ ```bash
+ # MongoDB
+ docker run -d -p 127.0.0.1:27017:27017 --name appsmith-mongodb \
+ -e MONGO_INITDB_DATABASE=appsmith mongo --replSet rs0
+
+ # Redis
+ docker run -d -p 127.0.0.1:6379:6379 --name appsmith-redis redis
+ ```
+
+2. **Build and run the server**:
+ ```bash
+ cd app/server
+ mvn clean compile
+ cp envs/dev.env.example .env
+ # Edit .env to point to your local MongoDB and Redis
+ ./build.sh -Dmaven.test.skip
+ ./scripts/start-dev-server.sh
+ ```
+
+For detailed setup instructions, see:
+- [Client Setup Guide](contributions/ClientSetup.md) - Full frontend setup with troubleshooting
+- [Server Setup Guide](contributions/ServerSetup.md) - Complete backend setup including IntelliJ configuration
+
+---
+
+## Pull Request Process
+
+### Branch Naming
+
+Use descriptive branch names following these patterns:
+- `fix/bug-description` - For bug fixes
+- `feature/feature-name` - For new features
+- `docs/description` - For documentation changes
+
+### Commit Messages
+
+Write clear, descriptive commit messages:
+- Use the present tense ("Add feature" not "Added feature")
+- Reference issue numbers when applicable
+- Keep the first line under 72 characters
+
+### Before Submitting
+
+- [ ] Code compiles without errors
+- [ ] Tests pass locally (Jest for frontend, JUnit for backend)
+- [ ] New code includes appropriate tests
+- [ ] Code follows the existing style conventions
+- [ ] PR description clearly explains the changes
+
+### PR Guidelines
+
+1. **Create PR from your fork** to `appsmithorg/appsmith` `release` branch
+2. **Link the issue** in your PR description (e.g., "Fixes #123")
+3. **Tag the maintainer** you're collaborating with
+4. **Wait for CI** to pass before requesting review
+5. **Address review feedback** promptly and push new commits
+
+### What NOT to Do
+
+❌ Work on issues without getting them assigned first
+❌ Create PRs without proper description
+❌ Request review before CI passes
+❌ Submit PRs without tests
+❌ Skip reading the contribution guidelines
+
+---
+
+## Community Support
+
+Need help? We're here for you!
+
+| Channel | Best For |
+|---------|----------|
+| [Discord](https://discord.com/invite/rBTTVJp) | Real-time help, discussions, community |
+| [GitHub Discussions](https://github.com/appsmithorg/appsmith/discussions) | Questions, ideas, announcements |
+| [GitHub Issues](https://github.com/appsmithorg/appsmith/issues) | Bug reports, feature requests |
+| [support@appsmith.com](mailto:support@appsmith.com) | Private inquiries, security issues |
+
+---
+
+## Recognition
-#### 📝 Improve the documentation
-In the process of shipping features quickly, we may forget to keep our docs up to date. You can help by suggesting improvements to our documentation using the [Documentation templates](https://github.com/appsmithorg/appsmith-docs/issues/new/choose) or dive right into our [Docs Contribution Guide](https://github.com/appsmithorg/appsmith-docs/blob/main/CONTRIBUTING.md)!
+We ❤️ our contributors! All contributors are recognized in our [README](README.md#top-contributors). Your contributions help make Appsmith better for everyone.
-#### ⚙️ Close a Bug / Feature issue
-We welcome contributions that help make appsmith bug free & improve the experience of our users. You can also find issues tagged [Good First Issues](https://github.com/appsmithorg/appsmith/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22+bug). Check out our [Code Contribution Guide](contributions/CodeContributionsGuidelines.md) to begin.
+Let's build great software together! 🚀
\ No newline at end of file
diff --git a/app/client/src/utils/TypeHelpers.test.ts b/app/client/src/utils/TypeHelpers.test.ts
new file mode 100644
index 000000000000..f9040c3a4e8d
--- /dev/null
+++ b/app/client/src/utils/TypeHelpers.test.ts
@@ -0,0 +1,259 @@
+import { getType, Types, isURL } from "./TypeHelpers";
+
+describe("TypeHelpers", () => {
+ describe("getType", () => {
+ describe("STRING type", () => {
+ it("should return STRING for string values", () => {
+ expect(getType("hello")).toBe(Types.STRING);
+ expect(getType("")).toBe(Types.STRING);
+ expect(getType(" ")).toBe(Types.STRING);
+ expect(getType("123")).toBe(Types.STRING);
+ expect(getType("true")).toBe(Types.STRING);
+ });
+ });
+
+ describe("NUMBER type", () => {
+ it("should return NUMBER for numeric values", () => {
+ expect(getType(0)).toBe(Types.NUMBER);
+ expect(getType(123)).toBe(Types.NUMBER);
+ expect(getType(-123)).toBe(Types.NUMBER);
+ expect(getType(3.14)).toBe(Types.NUMBER);
+ expect(getType(-3.14)).toBe(Types.NUMBER);
+ expect(getType(Infinity)).toBe(Types.NUMBER);
+ expect(getType(-Infinity)).toBe(Types.NUMBER);
+ });
+
+ it("should return NUMBER for NaN", () => {
+ expect(getType(NaN)).toBe(Types.NUMBER);
+ });
+ });
+
+ describe("BOOLEAN type", () => {
+ it("should return BOOLEAN for boolean values", () => {
+ expect(getType(true)).toBe(Types.BOOLEAN);
+ expect(getType(false)).toBe(Types.BOOLEAN);
+ });
+ });
+
+ describe("ARRAY type", () => {
+ it("should return ARRAY for array values", () => {
+ expect(getType([])).toBe(Types.ARRAY);
+ expect(getType([1, 2, 3])).toBe(Types.ARRAY);
+ expect(getType(["a", "b", "c"])).toBe(Types.ARRAY);
+ expect(getType([1, "a", true])).toBe(Types.ARRAY);
+ expect(getType([null, undefined])).toBe(Types.ARRAY);
+ });
+ });
+
+ describe("FUNCTION type", () => {
+ it("should return FUNCTION for function values", () => {
+ expect(getType(() => {})).toBe(Types.FUNCTION);
+ expect(getType(function () {})).toBe(Types.FUNCTION);
+ expect(getType(async () => {})).toBe(Types.FUNCTION);
+ expect(getType(function* () {})).toBe(Types.FUNCTION);
+ });
+ });
+
+ describe("OBJECT type", () => {
+ it("should return OBJECT for plain objects", () => {
+ expect(getType({})).toBe(Types.OBJECT);
+ expect(getType({ key: "value" })).toBe(Types.OBJECT);
+ expect(getType({ a: 1, b: 2 })).toBe(Types.OBJECT);
+ });
+
+ it("should return OBJECT for Date objects", () => {
+ expect(getType(new Date())).toBe(Types.OBJECT);
+ });
+
+ it("should return OBJECT for RegExp objects", () => {
+ expect(getType(/test/)).toBe(Types.OBJECT);
+ expect(getType(new RegExp("test"))).toBe(Types.OBJECT);
+ });
+
+ it("should return OBJECT for Map and Set", () => {
+ expect(getType(new Map())).toBe(Types.OBJECT);
+ expect(getType(new Set())).toBe(Types.OBJECT);
+ });
+
+ it("should return OBJECT for Error objects", () => {
+ expect(getType(new Error())).toBe(Types.OBJECT);
+ expect(getType(new TypeError())).toBe(Types.OBJECT);
+ });
+ });
+
+ describe("UNDEFINED type", () => {
+ it("should return UNDEFINED for undefined values", () => {
+ expect(getType(undefined)).toBe(Types.UNDEFINED);
+ expect(getType(void 0)).toBe(Types.UNDEFINED);
+ });
+ });
+
+ describe("NULL type", () => {
+ it("should return NULL for null values", () => {
+ expect(getType(null)).toBe(Types.NULL);
+ });
+ });
+
+ describe("UNKNOWN type", () => {
+ it("should return UNKNOWN for symbols", () => {
+ // Symbols are not considered objects by lodash's isObject
+ expect(getType(Symbol("test"))).toBe(Types.UNKNOWN);
+ });
+
+ it("should return UNKNOWN for values that don't match other types", () => {
+ // Most primitive and object values have specific types
+ // UNKNOWN is a fallback for edge cases like Symbols
+ expect(getType(Symbol())).toBe(Types.UNKNOWN);
+ });
+ });
+
+ describe("Type priority", () => {
+ it("should correctly identify arrays over objects", () => {
+ // Arrays are also objects, but getType should return ARRAY
+ expect(getType([1, 2, 3])).toBe(Types.ARRAY);
+ expect(getType([])).toBe(Types.ARRAY);
+ });
+
+ it("should correctly distinguish between string numbers and actual numbers", () => {
+ expect(getType("123")).toBe(Types.STRING);
+ expect(getType(123)).toBe(Types.NUMBER);
+ });
+ });
+ });
+
+ describe("isURL", () => {
+ describe("Valid URLs", () => {
+ it("should return true for valid HTTP URLs with proper domain", () => {
+ expect(isURL("http://example.com")).toBe(true);
+ expect(isURL("http://www.example.com")).toBe(true);
+ expect(isURL("http://example.com/path")).toBe(true);
+ expect(isURL("http://example.com/path/to/page")).toBe(true);
+ expect(isURL("http://example.com/path?query=value")).toBe(true);
+ expect(isURL("http://example.com/path?query=value&other=123")).toBe(true);
+ expect(isURL("http://example.com#anchor")).toBe(true);
+ });
+
+ it("should return true for valid HTTPS URLs with proper domain", () => {
+ expect(isURL("https://example.com")).toBe(true);
+ expect(isURL("https://www.example.com")).toBe(true);
+ expect(isURL("https://example.com/path")).toBe(true);
+ expect(isURL("https://example.com/path?query=value")).toBe(true);
+ });
+
+ it("should return true for blob: URLs", () => {
+ expect(isURL("blob:http://example.com")).toBe(true);
+ expect(isURL("blob:https://example.com/path")).toBe(true);
+ });
+
+ it("should return true for URLs with ports", () => {
+ expect(isURL("http://example.com:8080")).toBe(true);
+ expect(isURL("https://example.com:443")).toBe(true);
+ // Note: localhost without TLD won't match the domain pattern
+ });
+
+ it("should return true for URLs with IP addresses", () => {
+ expect(isURL("http://192.168.1.1")).toBe(true);
+ expect(isURL("https://10.0.0.1/path")).toBe(true);
+ expect(isURL("http://127.0.0.1:8080")).toBe(true);
+ });
+
+ it("should return true for URLs with subdomains", () => {
+ expect(isURL("http://sub.example.com")).toBe(true);
+ expect(isURL("https://api.example.com/v1")).toBe(true);
+ expect(isURL("http://www.sub.example.com")).toBe(true);
+ });
+
+ it("should return true for URLs with fragments", () => {
+ expect(isURL("http://example.com#section")).toBe(true);
+ expect(isURL("http://example.com/path#section")).toBe(true);
+ expect(isURL("http://example.com/path?query=value#section")).toBe(true);
+ });
+
+ it("should return true for URLs with query strings", () => {
+ expect(isURL("http://example.com?a=1")).toBe(true);
+ expect(isURL("http://example.com?a=1&b=2")).toBe(true);
+ expect(isURL("http://example.com/path?a=1&b=2&c=3")).toBe(true);
+ });
+
+ it("should return true for URLs with trailing slash", () => {
+ expect(isURL("http://example.com/")).toBe(true);
+ expect(isURL("http://example.com/path/")).toBe(true);
+ });
+ });
+
+ describe("Invalid URLs", () => {
+ it("should return false for non-URL strings", () => {
+ expect(isURL("not a url")).toBe(false);
+ expect(isURL("example")).toBe(false);
+ });
+
+ it("should return false for empty strings", () => {
+ expect(isURL("")).toBe(false);
+ expect(isURL(" ")).toBe(false);
+ });
+
+ it("should accept domain URLs without protocol", () => {
+ // Note: The regex actually allows URLs without protocol (protocol is optional)
+ // The regex pattern allows domains like "example.com" as valid
+ expect(isURL("example.com")).toBe(true); // Domain without protocol is accepted
+ expect(isURL("www.example.com")).toBe(true); // Domain without protocol is accepted
+ });
+
+ it("should return false for domains without TLD", () => {
+ // The regex requires a domain with TLD (e.g., .com)
+ // localhost doesn't have a TLD so it won't match
+ expect(isURL("http://localhost")).toBe(false);
+ expect(isURL("http://localhost:3000")).toBe(false);
+ });
+
+ it("should return false for FTP and other non-HTTP protocols", () => {
+ expect(isURL("ftp://example.com")).toBe(false);
+ expect(isURL("file://example.com")).toBe(false);
+ expect(isURL("mailto:test@example.com")).toBe(false);
+ });
+
+ it("should return false for malformed URLs", () => {
+ expect(isURL("http://")).toBe(false);
+ expect(isURL("http://.com")).toBe(false);
+ });
+
+ it("should return false for URLs with invalid characters in domain", () => {
+ expect(isURL("http://example.com with space")).toBe(false);
+ });
+ });
+
+ describe("Edge cases", () => {
+ it("should handle case sensitivity", () => {
+ // The regex uses 'i' flag for case insensitivity
+ expect(isURL("HTTP://EXAMPLE.COM")).toBe(true);
+ expect(isURL("HTTPS://Example.Com")).toBe(true);
+ });
+
+ it("should handle complex paths", () => {
+ expect(isURL("http://example.com/path/to/resource/file.html")).toBe(true);
+ expect(isURL("http://example.com/path-with-dashes")).toBe(true);
+ expect(isURL("http://example.com/path_with_underscores")).toBe(true);
+ });
+
+ it("should handle special characters in path and query", () => {
+ expect(isURL("http://example.com/path?name=John%20Doe")).toBe(true);
+ expect(isURL("http://example.com/search?q=hello+world")).toBe(true);
+ });
+ });
+ });
+
+ describe("Types enum", () => {
+ it("should have all expected type values", () => {
+ expect(Types.URL).toBe("URL");
+ expect(Types.STRING).toBe("STRING");
+ expect(Types.NUMBER).toBe("NUMBER");
+ expect(Types.BOOLEAN).toBe("BOOLEAN");
+ expect(Types.OBJECT).toBe("OBJECT");
+ expect(Types.ARRAY).toBe("ARRAY");
+ expect(Types.FUNCTION).toBe("FUNCTION");
+ expect(Types.UNDEFINED).toBe("UNDEFINED");
+ expect(Types.NULL).toBe("NULL");
+ expect(Types.UNKNOWN).toBe("UNKNOWN");
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/client/src/utils/URLUtils.test.ts b/app/client/src/utils/URLUtils.test.ts
new file mode 100644
index 000000000000..8721594b4ae8
--- /dev/null
+++ b/app/client/src/utils/URLUtils.test.ts
@@ -0,0 +1,312 @@
+import {
+ getQueryParams,
+ convertObjectToQueryParams,
+ isValidURL,
+ matchesURLPattern,
+ sanitizeString,
+} from "./URLUtils";
+
+describe("URLUtils", () => {
+ describe("getQueryParams", () => {
+ let originalLocation: Location;
+
+ beforeEach(() => {
+ originalLocation = window.location;
+ delete (window as any).location;
+ });
+
+ afterEach(() => {
+ window.location = originalLocation;
+ });
+
+ it("should return an empty object when there are no query params", () => {
+ (window as any).location = { search: "" };
+
+ const result = getQueryParams();
+
+ expect(result).toEqual({});
+ });
+
+ it("should return query params as an object", () => {
+ (window as any).location = { search: "?key1=value1&key2=value2" };
+
+ const result = getQueryParams();
+
+ expect(result).toEqual({
+ key1: "value1",
+ key2: "value2",
+ });
+ });
+
+ it("should handle a single query param", () => {
+ (window as any).location = { search: "?key=value" };
+
+ const result = getQueryParams();
+
+ expect(result).toEqual({ key: "value" });
+ });
+
+ it("should handle empty values", () => {
+ (window as any).location = { search: "?key1=&key2=value2" };
+
+ const result = getQueryParams();
+
+ expect(result).toEqual({
+ key1: "",
+ key2: "value2",
+ });
+ });
+
+ it("should handle encoded values", () => {
+ (window as any).location = { search: "?name=John%20Doe&email=test%40example.com" };
+
+ const result = getQueryParams();
+
+ expect(result).toEqual({
+ name: "John Doe",
+ email: "test@example.com",
+ });
+ });
+
+ it("should handle special characters in values", () => {
+ (window as any).location = { search: "?key=value%26with%3Dspecial%20chars" };
+
+ const result = getQueryParams();
+
+ expect(result).toEqual({
+ key: "value&with=special chars",
+ });
+ });
+ });
+
+ describe("convertObjectToQueryParams", () => {
+ it("should convert a simple object to query params", () => {
+ const result = convertObjectToQueryParams({ key1: "value1", key2: "value2" });
+
+ expect(result).toBe("?key1=value1&key2=value2");
+ });
+
+ it("should return an empty string for null input", () => {
+ const result = convertObjectToQueryParams(null);
+
+ expect(result).toBe("");
+ });
+
+ it("should return an empty string for undefined input", () => {
+ const result = convertObjectToQueryParams(undefined);
+
+ expect(result).toBe("");
+ });
+
+ it("should handle an empty object", () => {
+ const result = convertObjectToQueryParams({});
+
+ expect(result).toBe("?");
+ });
+
+ it("should encode special characters in keys and values", () => {
+ const result = convertObjectToQueryParams({
+ "key with spaces": "value with spaces",
+ "key&special": "value&special",
+ });
+
+ expect(result).toContain("key%20with%20spaces=value%20with%20spaces");
+ expect(result).toContain("key%26special=value%26special");
+ });
+
+ it("should handle numeric values", () => {
+ const result = convertObjectToQueryParams({ count: 42, price: 19.99 });
+
+ expect(result).toBe("?count=42&price=19.99");
+ });
+
+ it("should handle boolean values", () => {
+ const result = convertObjectToQueryParams({ active: true, disabled: false });
+
+ expect(result).toBe("?active=true&disabled=false");
+ });
+
+ it("should handle empty string values", () => {
+ const result = convertObjectToQueryParams({ key: "" });
+
+ expect(result).toBe("?key=");
+ });
+
+ it("should handle array values by converting to string", () => {
+ const result = convertObjectToQueryParams({ items: "a,b,c" });
+
+ expect(result).toBe("?items=a%2Cb%2Cc");
+ });
+ });
+
+ describe("isValidURL", () => {
+ it("should return true for valid HTTP URLs", () => {
+ expect(isValidURL("http://example.com")).toBe(true);
+ expect(isValidURL("http://www.example.com")).toBe(true);
+ expect(isValidURL("http://example.com/path")).toBe(true);
+ expect(isValidURL("http://example.com/path?query=value")).toBe(true);
+ });
+
+ it("should return true for valid HTTPS URLs", () => {
+ expect(isValidURL("https://example.com")).toBe(true);
+ expect(isValidURL("https://www.example.com")).toBe(true);
+ expect(isValidURL("https://example.com/path")).toBe(true);
+ expect(isValidURL("https://example.com/path?query=value")).toBe(true);
+ });
+
+ it("should return true for valid FTP URLs", () => {
+ expect(isValidURL("ftp://ftp.example.com")).toBe(true);
+ });
+
+ it("should return true for valid mailto URLs", () => {
+ expect(isValidURL("mailto:test@example.com")).toBe(true);
+ });
+
+ it("should return true for valid tel URLs", () => {
+ expect(isValidURL("tel:+1234567890")).toBe(true);
+ });
+
+ it("should return true for localhost URLs", () => {
+ expect(isValidURL("http://localhost")).toBe(true);
+ expect(isValidURL("http://localhost:3000")).toBe(true);
+ });
+
+ it("should return true for URLs with ports", () => {
+ expect(isValidURL("http://example.com:8080")).toBe(true);
+ expect(isValidURL("https://example.com:443")).toBe(true);
+ });
+
+ it("should return false for invalid URLs", () => {
+ expect(isValidURL("not a url")).toBe(false);
+ expect(isValidURL("example.com")).toBe(false);
+ expect(isValidURL("")).toBe(false);
+ expect(isValidURL("http://")).toBe(false);
+ });
+
+ it("should return false for URLs without protocol", () => {
+ expect(isValidURL("www.example.com")).toBe(false);
+ });
+
+ it("should return true for URLs with fragments", () => {
+ expect(isValidURL("http://example.com#section")).toBe(true);
+ expect(isValidURL("http://example.com/path#section")).toBe(true);
+ });
+
+ it("should return true for URLs with complex paths", () => {
+ expect(isValidURL("http://example.com/path/to/resource")).toBe(true);
+ expect(isValidURL("http://example.com/path/to/resource/file.html")).toBe(true);
+ });
+ });
+
+ describe("matchesURLPattern", () => {
+ it("should match standard HTTP URLs", () => {
+ expect(matchesURLPattern("http://example.com")).toBe(true);
+ });
+
+ it("should match standard HTTPS URLs", () => {
+ expect(matchesURLPattern("https://example.com")).toBe(true);
+ });
+
+ it("should match URLs with www prefix", () => {
+ expect(matchesURLPattern("www.example.com")).toBe(true);
+ });
+
+ it("should match URLs with paths", () => {
+ expect(matchesURLPattern("http://example.com/path/to/page")).toBe(true);
+ });
+
+ it("should match URLs with query strings", () => {
+ expect(matchesURLPattern("http://example.com?key=value")).toBe(true);
+ });
+
+ it("should match URLs with fragments", () => {
+ expect(matchesURLPattern("http://example.com#section")).toBe(true);
+ });
+
+ it("should match localhost URLs with trailing slash", () => {
+ // Note: The regex has localhost(?=\/) which requires localhost to be followed by /
+ expect(matchesURLPattern("localhost/")).toBe(true);
+ expect(matchesURLPattern("localhost/path")).toBe(true);
+ });
+
+ it("should not match localhost URLs without trailing slash", () => {
+ // localhost without / doesn't match the pattern
+ expect(matchesURLPattern("localhost")).toBe(false);
+ expect(matchesURLPattern("http://localhost:3000")).toBe(false);
+ });
+
+ it("should match IP addresses", () => {
+ expect(matchesURLPattern("192.168.1.1")).toBe(true);
+ expect(matchesURLPattern("http://192.168.1.1:8080")).toBe(true);
+ });
+
+ it("should match URLs with ports", () => {
+ expect(matchesURLPattern("http://example.com:8080")).toBe(true);
+ });
+
+ it("should match URLs with subdomains", () => {
+ expect(matchesURLPattern("http://sub.example.com")).toBe(true);
+ expect(matchesURLPattern("http://api.v2.example.com")).toBe(true);
+ });
+
+ it("should match FTP URLs", () => {
+ expect(matchesURLPattern("ftp://ftp.example.com")).toBe(true);
+ });
+
+ it("should match mailto URLs", () => {
+ expect(matchesURLPattern("mailto:test@example.com")).toBe(true);
+ });
+
+ it("should not match tel URLs", () => {
+ // The regex doesn't properly match tel: URLs
+ expect(matchesURLPattern("tel:+1234567890")).toBe(false);
+ expect(matchesURLPattern("tel://+1234567890")).toBe(false);
+ });
+
+ it("should not match invalid strings", () => {
+ // Note: The regex is quite permissive, so many strings might match
+ // This test documents the behavior rather than asserting failure
+ expect(matchesURLPattern("")).toBe(false);
+ });
+ });
+
+ describe("sanitizeString", () => {
+ it("should convert uppercase letters to lowercase", () => {
+ expect(sanitizeString("HELLO")).toBe("hello");
+ expect(sanitizeString("Hello World")).toBe("hello_world");
+ });
+
+ it("should replace special characters with underscores", () => {
+ expect(sanitizeString("hello-world")).toBe("hello_world");
+ expect(sanitizeString("hello@world")).toBe("hello_world");
+ expect(sanitizeString("hello world")).toBe("hello_world");
+ });
+
+ it("should keep alphanumeric characters", () => {
+ expect(sanitizeString("abc123")).toBe("abc123");
+ expect(sanitizeString("ABC123")).toBe("abc123");
+ });
+
+ it("should handle multiple special characters in a row", () => {
+ expect(sanitizeString("hello--world")).toBe("hello__world");
+ expect(sanitizeString("hello@@world")).toBe("hello__world");
+ });
+
+ it("should handle strings starting or ending with special characters", () => {
+ expect(sanitizeString("-hello-")).toBe("_hello_");
+ expect(sanitizeString("@hello@")).toBe("_hello_");
+ });
+
+ it("should handle empty string", () => {
+ expect(sanitizeString("")).toBe("");
+ });
+
+ it("should handle strings with only special characters", () => {
+ expect(sanitizeString("@#$%")).toBe("____");
+ });
+
+ it("should handle strings with mixed content", () => {
+ expect(sanitizeString("My-App_v2.0")).toBe("my_app_v2_0");
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/client/src/utils/dayJsUtils.test.ts b/app/client/src/utils/dayJsUtils.test.ts
new file mode 100644
index 000000000000..fc689d298ca4
--- /dev/null
+++ b/app/client/src/utils/dayJsUtils.test.ts
@@ -0,0 +1,225 @@
+import { getHumanizedTime, getReadableDateInFormat, dayjs } from "./dayJsUtils";
+
+describe("dayJsUtils", () => {
+ let previousLocale: string;
+
+ beforeAll(() => {
+ previousLocale = dayjs.locale();
+ dayjs.locale("en");
+ });
+
+ afterAll(() => {
+ dayjs.locale(previousLocale);
+ });
+
+ describe("getHumanizedTime", () => {
+ it("should return 'a few seconds' for small time values", () => {
+ expect(getHumanizedTime(1000)).toBe("a few seconds");
+ expect(getHumanizedTime(5000)).toBe("a few seconds");
+ expect(getHumanizedTime(10000)).toBe("a few seconds");
+ });
+
+ it("should return correct humanized time for minutes", () => {
+ expect(getHumanizedTime(60000)).toBe("a minute");
+ expect(getHumanizedTime(120000)).toBe("2 minutes");
+ expect(getHumanizedTime(300000)).toBe("5 minutes");
+ expect(getHumanizedTime(1800000)).toBe("30 minutes");
+ });
+
+ it("should return correct humanized time for hours", () => {
+ expect(getHumanizedTime(3600000)).toBe("an hour");
+ expect(getHumanizedTime(7200000)).toBe("2 hours");
+ expect(getHumanizedTime(14400000)).toBe("4 hours");
+ expect(getHumanizedTime(28800000)).toBe("8 hours");
+ });
+
+ it("should return correct humanized time for days", () => {
+ expect(getHumanizedTime(86400000)).toBe("a day");
+ expect(getHumanizedTime(172800000)).toBe("2 days");
+ expect(getHumanizedTime(604800000)).toBe("7 days");
+ });
+
+ it("should return correct humanized time for months", () => {
+ expect(getHumanizedTime(2592000000)).toBe("a month");
+ expect(getHumanizedTime(5184000000)).toBe("2 months");
+ });
+
+ it("should return correct humanized time for years", () => {
+ expect(getHumanizedTime(31536000000)).toBe("a year");
+ expect(getHumanizedTime(63072000000)).toBe("2 years");
+ });
+
+ it("should handle zero milliseconds", () => {
+ expect(getHumanizedTime(0)).toBe("a few seconds");
+ });
+
+ it("should handle edge cases around time boundaries", () => {
+ // Just under a minute (dayjs may have different thresholds)
+ const underMinute = getHumanizedTime(45000);
+ expect(["a few seconds", "a minute"]).toContain(underMinute);
+ // Just at a minute
+ expect(getHumanizedTime(60000)).toBe("a minute");
+ // Just under an hour (dayjs duration humanize may round up)
+ const underHour = getHumanizedTime(3500000);
+ expect(["58 minutes", "59 minutes", "an hour"]).toContain(underHour);
+ // Just at an hour
+ expect(getHumanizedTime(3600000)).toBe("an hour");
+ });
+ });
+
+ describe("getReadableDateInFormat", () => {
+ it("should format date in default format", () => {
+ const date = new Date(2024, 2, 15, 12, 0, 0); // Local time, timezone-independent
+ expect(getReadableDateInFormat(date, "YYYY-MM-DD")).toBe("2024-03-15");
+ });
+
+ it("should format date with time", () => {
+ const date = new Date(2024, 2, 15, 14, 30, 0); // Local time, timezone-independent
+ expect(getReadableDateInFormat(date, "YYYY-MM-DD HH:mm")).toBe("2024-03-15 14:30");
+ });
+
+ it("should format date in various formats", () => {
+ const date = new Date(2024, 2, 15, 12, 0, 0); // Local time, timezone-independent
+
+ expect(getReadableDateInFormat(date, "DD/MM/YYYY")).toBe("15/03/2024");
+ expect(getReadableDateInFormat(date, "MM-DD-YYYY")).toBe("03-15-2024");
+ expect(getReadableDateInFormat(date, "MMMM Do, YYYY")).toBe("March 15th, 2024");
+ });
+
+ it("should handle month names", () => {
+ const date = new Date(2024, 5, 15, 12, 0, 0); // June 15, 2024 (month is 0-indexed)
+ expect(getReadableDateInFormat(date, "MMMM YYYY")).toBe("June 2024");
+ });
+
+ it("should handle day names", () => {
+ const date = new Date(2024, 2, 15, 12, 0, 0); // March 15, 2024 is a Friday
+ expect(getReadableDateInFormat(date, "dddd")).toBe("Friday");
+ });
+
+ it("should format with seconds", () => {
+ const date = new Date(2024, 2, 15, 14, 30, 45); // Local time, timezone-independent
+ expect(getReadableDateInFormat(date, "HH:mm:ss")).toBe("14:30:45");
+ });
+
+ it("should handle 12-hour format", () => {
+ const date = new Date(2024, 2, 15, 14, 30, 0); // 2:30 PM in local time
+ expect(getReadableDateInFormat(date, "hh:mm A")).toBe("02:30 PM");
+ });
+
+ it("should handle empty format string", () => {
+ const date = new Date(2024, 2, 15, 12, 0, 0);
+ // dayjs returns ISO format when format string is empty
+ const result = getReadableDateInFormat(date, "");
+ expect(result).toContain("2024");
+ });
+
+ it("should handle current date", () => {
+ const now = new Date();
+ const formatted = getReadableDateInFormat(now, "YYYY-MM-DD");
+ // Should be a valid date string
+ expect(formatted).toMatch(/^\d{4}-\d{2}-\d{2}$/);
+ });
+
+ it("should handle advanced format features", () => {
+ const date = new Date(2024, 2, 15, 14, 30, 45); // March 15, 2024 is Q1
+
+ // Quarter
+ expect(getReadableDateInFormat(date, "Q")).toBe("1");
+
+ // Note: week of year requires weekOfYear plugin which may not be loaded
+ // Testing day of year might require additional plugins
+ expect(typeof getReadableDateInFormat(date, "DD")).toBe("string");
+ });
+ });
+
+ describe("dayjs", () => {
+ it("should be a valid dayjs instance", () => {
+ const now = dayjs();
+ expect(now.isValid()).toBe(true);
+ });
+
+ it("should parse date strings", () => {
+ const date = dayjs("2024-03-15");
+ expect(date.isValid()).toBe(true);
+ expect(date.year()).toBe(2024);
+ expect(date.month()).toBe(2); // March is month 2 (0-indexed)
+ expect(date.date()).toBe(15);
+ });
+
+ it("should have duration plugin available", () => {
+ const duration = dayjs.duration(3600000, "milliseconds");
+ expect(duration.asMinutes()).toBe(60);
+ expect(duration.asHours()).toBe(1);
+ });
+
+ it("should have relativeTime plugin available", () => {
+ const now = dayjs();
+ const past = now.subtract(1, "hour");
+ expect(past.fromNow()).toContain("hour");
+ });
+
+ it("should have advancedFormat plugin available", () => {
+ const date = dayjs("2024-03-15");
+ // advancedFormat allows formatting like 'Do' for ordinal dates
+ expect(date.format("Do")).toBe("15th");
+ });
+
+ it("should support chaining operations", () => {
+ const date = dayjs("2024-03-15")
+ .add(1, "day")
+ .subtract(1, "month");
+
+ expect(date.format("YYYY-MM-DD")).toBe("2024-02-16");
+ });
+
+ it("should handle invalid dates", () => {
+ const invalidDate = dayjs("not a date");
+ expect(invalidDate.isValid()).toBe(false);
+ });
+
+ it("should compare dates correctly", () => {
+ const date1 = dayjs("2024-03-15");
+ const date2 = dayjs("2024-03-16");
+
+ expect(date1.isBefore(date2)).toBe(true);
+ expect(date2.isAfter(date1)).toBe(true);
+ expect(date1.isSame(date1)).toBe(true);
+ });
+
+ it("should handle date manipulation", () => {
+ const date = dayjs("2024-03-15");
+
+ expect(date.add(7, "day").format("YYYY-MM-DD")).toBe("2024-03-22");
+ expect(date.subtract(1, "month").format("YYYY-MM-DD")).toBe("2024-02-15");
+ expect(date.startOf("month").format("YYYY-MM-DD")).toBe("2024-03-01");
+ expect(date.endOf("month").format("YYYY-MM-DD")).toBe("2024-03-31");
+ });
+ });
+
+ describe("Integration tests", () => {
+ it("should work together for humanized time from dates", () => {
+ const now = dayjs();
+ const pastDate = now.subtract(2, "hour");
+ const diffMs = now.diff(pastDate);
+
+ expect(getHumanizedTime(diffMs)).toBe("2 hours");
+ });
+
+ it("should format relative times correctly", () => {
+ const now = dayjs();
+ const oneHourAgo = now.subtract(1, "hour");
+
+ expect(oneHourAgo.fromNow()).toBe("an hour ago");
+ });
+
+ it("should handle formatting and duration together", () => {
+ const startTime = dayjs("2024-03-15T10:00:00");
+ const endTime = dayjs("2024-03-15T14:30:00");
+ const duration = dayjs.duration(endTime.diff(startTime));
+
+ expect(getReadableDateInFormat(startTime.toDate(), "HH:mm")).toBe("10:00");
+ expect(getReadableDateInFormat(endTime.toDate(), "HH:mm")).toBe("14:30");
+ expect(duration.asHours()).toBe(4.5);
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/client/src/utils/formhelpers.test.ts b/app/client/src/utils/formhelpers.test.ts
index 8caa517ed3d1..468c3ff43cf4 100644
--- a/app/client/src/utils/formhelpers.test.ts
+++ b/app/client/src/utils/formhelpers.test.ts
@@ -1,43 +1,234 @@
-import { isEmail } from "./formhelpers";
-
-describe("isEmail test", () => {
- it("Check whether the valid emails are recognized as valid", () => {
- const validEmails = [
- "appsmith@yahoo.com",
- "appsmith-100@yahoo.com",
- "appsmith.100@yahoo.com",
- "appsmith111@appsmith.com",
- "appsmith-100@appsmith.net",
- "appsmith.100@appsmith.com.au",
- "appsmith@1.com",
- "appsmith@gmail.com.com",
- "appsmith+100@gmail.com",
- "appsmith-100@yahoo-test.com",
- ];
-
- validEmails.forEach((validEmail) => {
- expect(isEmail(validEmail)).toBeTruthy();
+import {
+ isEmail,
+ isEmptyString,
+ isStrongPassword,
+ noSpaces,
+ isRelevantEmail,
+ PASSWORD_MIN_LENGTH,
+ PASSWORD_MAX_LENGTH,
+} from "./formhelpers";
+
+describe("formhelpers", () => {
+ describe("PASSWORD constants", () => {
+ it("should have correct minimum password length", () => {
+ expect(PASSWORD_MIN_LENGTH).toBe(8);
+ });
+
+ it("should have correct maximum password length", () => {
+ expect(PASSWORD_MAX_LENGTH).toBe(48);
+ });
+ });
+
+ describe("isEmptyString", () => {
+ it("should return true for empty string", () => {
+ expect(isEmptyString("")).toBe(true);
+ });
+
+ it("should return true for string with only whitespace", () => {
+ expect(isEmptyString(" ")).toBe(true);
+ expect(isEmptyString("\t")).toBe(true);
+ expect(isEmptyString("\n")).toBe(true);
+ expect(isEmptyString(" \n\t ")).toBe(true);
+ });
+
+ it("should return true for null or undefined", () => {
+ expect(isEmptyString(null as any)).toBe(true);
+ expect(isEmptyString(undefined as any)).toBe(true);
+ });
+
+ it("should return false for strings with content", () => {
+ expect(isEmptyString("hello")).toBe(false);
+ expect(isEmptyString(" hello ")).toBe(false);
+ expect(isEmptyString("a")).toBe(false);
+ expect(isEmptyString("123")).toBe(false);
+ });
+
+ it("should return false for strings with mixed content", () => {
+ expect(isEmptyString(" hello world ")).toBe(false);
+ expect(isEmptyString("test@example.com")).toBe(false);
+ });
+ });
+
+ describe("isStrongPassword", () => {
+ it("should return true for passwords with valid length", () => {
+ expect(isStrongPassword("12345678")).toBe(true); // exactly 8 chars
+ expect(isStrongPassword("123456789")).toBe(true); // 9 chars
+ expect(isStrongPassword("a".repeat(48))).toBe(true); // exactly 48 chars
+ });
+
+ it("should return false for passwords that are too short", () => {
+ expect(isStrongPassword("")).toBe(false);
+ expect(isStrongPassword("1234567")).toBe(false); // 7 chars
+ expect(isStrongPassword("a")).toBe(false);
+ expect(isStrongPassword("123456")).toBe(false);
+ });
+
+ it("should return false for passwords that are too long", () => {
+ expect(isStrongPassword("a".repeat(49))).toBe(false); // 49 chars
+ expect(isStrongPassword("a".repeat(100))).toBe(false);
+ });
+
+ it("should handle passwords with leading/trailing whitespace", () => {
+ // The function uses trim(), so whitespace is removed
+ expect(isStrongPassword(" 12345678 ")).toBe(true);
+ expect(isStrongPassword(" 1234567 ")).toBe(false);
+ });
+
+ it("should return true for edge case lengths", () => {
+ expect(isStrongPassword("12345678")).toBe(true); // min length
+ expect(isStrongPassword("a".repeat(48))).toBe(true); // max length
+ });
+
+ it("should return true for complex passwords", () => {
+ expect(isStrongPassword("P@ssw0rd!")).toBe(true);
+ expect(isStrongPassword("MySecurePassword123!@#")).toBe(true);
+ });
+ });
+
+ describe("noSpaces", () => {
+ it("should return true for empty string", () => {
+ expect(noSpaces("")).toBe(true);
+ });
+
+ it("should return true for null or undefined", () => {
+ expect(noSpaces(null as any)).toBe(true);
+ expect(noSpaces(undefined as any)).toBe(true);
+ });
+
+ it("should return true for string with only whitespace", () => {
+ expect(noSpaces(" ")).toBe(true);
+ expect(noSpaces("\t\n")).toBe(true);
+ });
+
+ it("should return false for strings with content", () => {
+ expect(noSpaces("hello")).toBe(false);
+ expect(noSpaces("a")).toBe(false);
+ expect(noSpaces(" a ")).toBe(false);
+ });
+
+ it("should return false for strings with any non-whitespace character", () => {
+ expect(noSpaces("test")).toBe(false);
+ expect(noSpaces("123")).toBe(false);
+ expect(noSpaces("test example")).toBe(false);
+ });
+ });
+
+ describe("isEmail", () => {
+ it("Check whether the valid emails are recognized as valid", () => {
+ const validEmails = [
+ "appsmith@yahoo.com",
+ "appsmith-100@yahoo.com",
+ "appsmith.100@yahoo.com",
+ "appsmith111@appsmith.com",
+ "appsmith-100@appsmith.net",
+ "appsmith.100@appsmith.com.au",
+ "appsmith@1.com",
+ "appsmith@gmail.com.com",
+ "appsmith+100@gmail.com",
+ "appsmith-100@yahoo-test.com",
+ "test@example.com",
+ "user.name@example.co.uk",
+ "user+tag@example.org",
+ ];
+
+ validEmails.forEach((validEmail) => {
+ expect(isEmail(validEmail)).toBeTruthy();
+ });
+ });
+
+ it("Check whether the invalid emails are recognized as invalid", () => {
+ const invalidEmails = [
+ "appsmith",
+ "appsmith@.com.my",
+ "appsmith123@gmail.a",
+ "appsmith123@.com",
+ "appsmith123@.com.com",
+ ".appsmith@appsmith.com",
+ "appsmith()*@gmail.com",
+ "appsmith@%*.com",
+ "appsmith..2002@gmail.com",
+ "appsmith.@gmail.com",
+ "appsmith@appsmith@gmail.com",
+ "appsmith@gmail.com.1a",
+ "",
+ "plainaddress",
+ "@missingdomain.com",
+ "missing@.com",
+ ];
+
+ invalidEmails.forEach((invalidEmail) => {
+ expect(isEmail(invalidEmail)).toBeFalsy();
+ });
+ });
+
+ it("should handle edge cases", () => {
+ expect(isEmail("")).toBeFalsy();
+ expect(isEmail(" ")).toBeFalsy();
+ // Note: "a@b.c" doesn't pass because TLD must be at least 2 characters
+ expect(isEmail("a@b.cd")).toBeTruthy(); // minimal valid email with 2-char TLD
+ });
+ });
+
+ describe("isRelevantEmail", () => {
+ it("should return false for general email domains", () => {
+ expect(isRelevantEmail("user@gmail.com")).toBe(false);
+ expect(isRelevantEmail("user@yahoo.com")).toBe(false);
+ expect(isRelevantEmail("user@outlook.com")).toBe(false);
+ expect(isRelevantEmail("user@hotmail.com")).toBe(false);
+ expect(isRelevantEmail("user@aol.com")).toBe(false);
+ expect(isRelevantEmail("user@icloud.com")).toBe(false);
+ expect(isRelevantEmail("user@protonmail.com")).toBe(false);
+ expect(isRelevantEmail("user@zoho.com")).toBe(false);
+ expect(isRelevantEmail("user@yandex.com")).toBe(false);
+ expect(isRelevantEmail("user@appsmith.com")).toBe(false);
+ });
+
+ it("should return true for custom/business email domains", () => {
+ expect(isRelevantEmail("user@company.com")).toBe(true);
+ expect(isRelevantEmail("user@mybusiness.org")).toBe(true);
+ expect(isRelevantEmail("user@startup.io")).toBe(true);
+ expect(isRelevantEmail("user@enterprise.net")).toBe(true);
+ expect(isRelevantEmail("user@example.co")).toBe(true);
+ });
+
+ it("should handle case insensitivity", () => {
+ expect(isRelevantEmail("user@GMAIL.COM")).toBe(false);
+ expect(isRelevantEmail("user@YAHOO.COM")).toBe(false);
+ expect(isRelevantEmail("user@COMPANY.COM")).toBe(true);
+ });
+
+ it("should return false for invalid email format (missing domain)", () => {
+ expect(isRelevantEmail("invalid-email")).toBe(false);
+ expect(isRelevantEmail("no-at-sign")).toBe(false);
+ expect(isRelevantEmail("")).toBe(false);
+ });
+
+ it("should return false for email without domain part", () => {
+ expect(isRelevantEmail("user@")).toBe(false);
+ // Note: "@domain.com" split by "@" returns ["", "domain.com"], so domain is "domain.com"
+ // which is not in GENERAL_DOMAINS, so it returns true
+ expect(isRelevantEmail("@domain.com")).toBe(true);
+ });
+
+ it("should handle subdomains correctly", () => {
+ // Subdomains of general domains should still be detected
+ expect(isRelevantEmail("user@mail.gmail.com")).toBe(true); // gmail.com is not the direct domain
+ expect(isRelevantEmail("user@sub.company.com")).toBe(true);
+ });
+
+ it("should handle emails with multiple @ symbols", () => {
+ // Note: split("@")[1] gets the second part, which is "company" for "user@company@gmail.com"
+ // "company" is not in GENERAL_DOMAINS, so it returns true (might be a bug in the function)
+ expect(isRelevantEmail("user@company@gmail.com")).toBe(true);
});
});
- it("Check whether the invalid emails are recognized as invalid", () => {
- const invalidEmails = [
- "appsmith",
- "appsmith@.com.my",
- "appsmith123@gmail.a",
- "appsmith123@.com",
- "appsmith123@.com.com",
- ".appsmith@appsmith.com",
- "appsmith()*@gmail.com",
- "appsmith@%*.com",
- "appsmith..2002@gmail.com",
- "appsmith.@gmail.com",
- "appsmith@appsmith@gmail.com",
- "appsmith@gmail.com.1a",
- ];
-
- invalidEmails.forEach((invalidEmail) => {
- expect(isEmail(invalidEmail)).toBeFalsy();
+ describe("hashPassword", () => {
+ it("should return the password as-is", () => {
+ const { hashPassword } = require("./formhelpers");
+ expect(hashPassword("mypassword")).toBe("mypassword");
+ expect(hashPassword("")).toBe("");
+ expect(hashPassword("complexPassword123!@#")).toBe("complexPassword123!@#");
});
});
-});
+});
\ No newline at end of file
diff --git a/app/client/src/utils/treeUtils.test.ts b/app/client/src/utils/treeUtils.test.ts
new file mode 100644
index 000000000000..e97186ac0ee8
--- /dev/null
+++ b/app/client/src/utils/treeUtils.test.ts
@@ -0,0 +1,341 @@
+import { traverseTree, mapTree, sortObjectWithArray } from "./treeUtils";
+
+describe("treeUtils", () => {
+ describe("traverseTree", () => {
+ it("should call callback for a single node tree", () => {
+ const tree = { name: "root" };
+ const callback = jest.fn();
+
+ traverseTree(tree, callback);
+
+ expect(callback).toHaveBeenCalledTimes(1);
+ expect(callback).toHaveBeenCalledWith(tree);
+ });
+
+ it("should traverse a tree with children", () => {
+ const tree = {
+ name: "root",
+ children: [{ name: "child1" }, { name: "child2" }],
+ };
+ const callback = jest.fn();
+
+ traverseTree(tree, callback);
+
+ expect(callback).toHaveBeenCalledTimes(3);
+ expect(callback).toHaveBeenNthCalledWith(1, tree);
+ expect(callback).toHaveBeenNthCalledWith(2, { name: "child1" });
+ expect(callback).toHaveBeenNthCalledWith(3, { name: "child2" });
+ });
+
+ it("should traverse a deeply nested tree", () => {
+ const tree = {
+ name: "root",
+ children: [
+ {
+ name: "child1",
+ children: [{ name: "grandchild1" }, { name: "grandchild2" }],
+ },
+ { name: "child2" },
+ ],
+ };
+ const callback = jest.fn();
+
+ traverseTree(tree, callback);
+
+ expect(callback).toHaveBeenCalledTimes(5);
+ expect(callback).toHaveBeenNthCalledWith(1, { name: "root", children: expect.any(Array) });
+ expect(callback).toHaveBeenNthCalledWith(2, { name: "child1", children: expect.any(Array) });
+ expect(callback).toHaveBeenNthCalledWith(3, { name: "grandchild1" });
+ expect(callback).toHaveBeenNthCalledWith(4, { name: "grandchild2" });
+ expect(callback).toHaveBeenNthCalledWith(5, { name: "child2" });
+ });
+
+ it("should handle empty children array", () => {
+ const tree = {
+ name: "root",
+ children: [],
+ };
+ const callback = jest.fn();
+
+ traverseTree(tree, callback);
+
+ expect(callback).toHaveBeenCalledTimes(1);
+ });
+
+ it("should handle tree without children property", () => {
+ const tree = { name: "root" };
+ const callback = jest.fn();
+
+ traverseTree(tree, callback);
+
+ expect(callback).toHaveBeenCalledTimes(1);
+ });
+
+ it("should allow callback to access node properties", () => {
+ const tree = {
+ name: "root",
+ value: 10,
+ children: [{ name: "child", value: 20 }],
+ };
+ const names: string[] = [];
+
+ traverseTree(tree, (node) => {
+ names.push(node.name);
+ });
+
+ expect(names).toEqual(["root", "child"]);
+ });
+
+ it("should handle complex tree structures", () => {
+ const tree = {
+ id: 1,
+ data: { label: "Root" },
+ children: [
+ {
+ id: 2,
+ data: { label: "Child 1" },
+ children: [{ id: 3, data: { label: "Grandchild" } }],
+ },
+ {
+ id: 4,
+ data: { label: "Child 2" },
+ },
+ ],
+ };
+ const ids: number[] = [];
+
+ traverseTree(tree, (node) => {
+ ids.push(node.id);
+ });
+
+ expect(ids).toEqual([1, 2, 3, 4]);
+ });
+ });
+
+ describe("mapTree", () => {
+ it("should map a single node tree", () => {
+ const tree = { name: "root", value: 1 };
+ const callback = (node: any) => ({ ...node, mapped: true });
+
+ const result = mapTree(tree, callback);
+
+ expect(result).toEqual({ name: "root", value: 1, mapped: true });
+ });
+
+ it("should map a tree with children", () => {
+ const tree = {
+ name: "root",
+ children: [{ name: "child1" }, { name: "child2" }],
+ };
+ const callback = (node: any) => ({ ...node, visited: true });
+
+ const result = mapTree(tree, callback);
+
+ expect(result).toEqual({
+ name: "root",
+ visited: true,
+ children: [
+ { name: "child1", visited: true },
+ { name: "child2", visited: true },
+ ],
+ });
+ });
+
+ it("should map a deeply nested tree", () => {
+ const tree = {
+ name: "root",
+ children: [
+ {
+ name: "child1",
+ children: [{ name: "grandchild" }],
+ },
+ ],
+ };
+ const callback = (node: any) => ({
+ ...node,
+ level: node.name.split("").length,
+ });
+
+ const result = mapTree(tree, callback);
+
+ expect(result).toEqual({
+ name: "root",
+ level: 4,
+ children: [
+ {
+ name: "child1",
+ level: 6,
+ children: [{ name: "grandchild", level: 10 }],
+ },
+ ],
+ });
+ });
+
+ it("should not modify the original tree", () => {
+ const tree = {
+ name: "root",
+ children: [{ name: "child" }],
+ };
+ const originalTree = JSON.stringify(tree);
+
+ mapTree(tree, (node: any) => ({ ...node, modified: true }));
+
+ expect(JSON.stringify(tree)).toBe(originalTree);
+ });
+
+ it("should handle empty children array", () => {
+ const tree = {
+ name: "root",
+ children: [],
+ };
+ const callback = (node: any) => ({ ...node, processed: true });
+
+ const result = mapTree(tree, callback);
+
+ expect(result).toEqual({
+ name: "root",
+ processed: true,
+ children: [],
+ });
+ });
+
+ it("should handle tree without children property", () => {
+ const tree = { name: "root", value: 42 };
+ const callback = (node: any) => ({ ...node, doubled: node.value * 2 });
+
+ const result = mapTree(tree, callback);
+
+ expect(result).toEqual({ name: "root", value: 42, doubled: 84 });
+ });
+
+ it("should handle complex transformations", () => {
+ const tree = {
+ id: 1,
+ children: [
+ { id: 2, children: [{ id: 4 }] },
+ { id: 3 },
+ ],
+ };
+ const callback = (node: any) => ({ nodeId: node.id });
+
+ const result = mapTree(tree, callback);
+
+ expect(result).toEqual({
+ nodeId: 1,
+ children: [
+ { nodeId: 2, children: [{ nodeId: 4 }] },
+ { nodeId: 3 },
+ ],
+ });
+ });
+ });
+
+ describe("sortObjectWithArray", () => {
+ it("should sort array values for each key", () => {
+ const data = {
+ key1: ["c", "a", "b"],
+ key2: ["z", "x", "y"],
+ };
+
+ const result = sortObjectWithArray(data);
+
+ expect(result.key1).toEqual(["a", "b", "c"]);
+ expect(result.key2).toEqual(["x", "y", "z"]);
+ });
+
+ it("should handle already sorted arrays", () => {
+ const data = {
+ key: ["a", "b", "c"],
+ };
+
+ const result = sortObjectWithArray(data);
+
+ expect(result.key).toEqual(["a", "b", "c"]);
+ });
+
+ it("should handle empty arrays", () => {
+ const data = {
+ key: [],
+ };
+
+ const result = sortObjectWithArray(data);
+
+ expect(result.key).toEqual([]);
+ });
+
+ it("should handle multiple keys", () => {
+ const data = {
+ letters: ["b", "a", "c"],
+ numbers: ["3", "1", "2"],
+ mixed: ["z", "a", "m"],
+ };
+
+ const result = sortObjectWithArray(data);
+
+ expect(result.letters).toEqual(["a", "b", "c"]);
+ expect(result.numbers).toEqual(["1", "2", "3"]);
+ expect(result.mixed).toEqual(["a", "m", "z"]);
+ });
+
+ it("should handle single element arrays", () => {
+ const data = {
+ key: ["only"],
+ };
+
+ const result = sortObjectWithArray(data);
+
+ expect(result.key).toEqual(["only"]);
+ });
+
+ it("should handle numeric string sorting (lexicographic)", () => {
+ const data = {
+ numbers: ["10", "2", "1", "20"],
+ };
+
+ const result = sortObjectWithArray(data);
+
+ // String sorting is lexicographic, not numeric
+ expect(result.numbers).toEqual(["1", "10", "2", "20"]);
+ });
+
+ it("should modify the original object", () => {
+ const data = {
+ key: ["c", "a", "b"],
+ };
+
+ sortObjectWithArray(data);
+
+ // The function modifies the original object
+ expect(data.key).toEqual(["a", "b", "c"]);
+ });
+
+ it("should return the same object reference", () => {
+ const data = {
+ key: ["b", "a"],
+ };
+
+ const result = sortObjectWithArray(data);
+
+ expect(result).toBe(data);
+ });
+
+ it("should handle empty object", () => {
+ const data = {};
+
+ const result = sortObjectWithArray(data);
+
+ expect(result).toEqual({});
+ });
+
+ it("should handle case-sensitive sorting", () => {
+ const data = {
+ mixed: ["B", "a", "C", "b"],
+ };
+
+ const result = sortObjectWithArray(data);
+
+ // Default string sort is case-sensitive (uppercase comes before lowercase)
+ expect(result.mixed).toEqual(["B", "C", "a", "b"]);
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/client/src/utils/validation/getIsSafeURL.test.ts b/app/client/src/utils/validation/getIsSafeURL.test.ts
new file mode 100644
index 000000000000..e5c53fef6730
--- /dev/null
+++ b/app/client/src/utils/validation/getIsSafeURL.test.ts
@@ -0,0 +1,215 @@
+import getIsSafeURL from "./getIsSafeURL";
+
+// Note: getIsSafeURL returns a truthy match array for matching URLs, not a boolean true
+// For non-matching URLs, it returns false (for non-strings) or null/falsy (for non-matching strings)
+
+describe("getIsSafeURL", () => {
+ describe("Safe URLs", () => {
+ describe("HTTP/HTTPS URLs", () => {
+ it("should return truthy for valid HTTP URLs", () => {
+ expect(getIsSafeURL("http://example.com")).toBeTruthy();
+ expect(getIsSafeURL("http://example.com/path")).toBeTruthy();
+ expect(getIsSafeURL("http://example.com/path?query=value")).toBeTruthy();
+ });
+
+ it("should return truthy for valid HTTPS URLs", () => {
+ expect(getIsSafeURL("https://example.com")).toBeTruthy();
+ expect(getIsSafeURL("https://example.com/path")).toBeTruthy();
+ expect(getIsSafeURL("https://example.com/path?query=value#hash")).toBeTruthy();
+ });
+ });
+
+ describe("Mailto URLs", () => {
+ it("should return truthy for mailto URLs", () => {
+ expect(getIsSafeURL("mailto:test@example.com")).toBeTruthy();
+ expect(getIsSafeURL("mailto:user@domain.org")).toBeTruthy();
+ });
+ });
+
+ describe("FTP URLs", () => {
+ it("should return truthy for FTP URLs", () => {
+ expect(getIsSafeURL("ftp://ftp.example.com")).toBeTruthy();
+ expect(getIsSafeURL("ftp://files.example.com/file.txt")).toBeTruthy();
+ });
+ });
+
+ describe("Tel URLs", () => {
+ it("should return truthy for tel URLs", () => {
+ expect(getIsSafeURL("tel:+1234567890")).toBeTruthy();
+ expect(getIsSafeURL("tel:1234567890")).toBeTruthy();
+ });
+ });
+
+ describe("File URLs", () => {
+ it("should return truthy for file URLs", () => {
+ expect(getIsSafeURL("file:///path/to/file")).toBeTruthy();
+ expect(getIsSafeURL("file://localhost/path")).toBeTruthy();
+ });
+ });
+
+ describe("SMS URLs", () => {
+ it("should return truthy for sms URLs", () => {
+ expect(getIsSafeURL("sms:+1234567890")).toBeTruthy();
+ expect(getIsSafeURL("sms:1234567890?body=Hello")).toBeTruthy();
+ });
+ });
+ });
+
+ describe("Data URLs", () => {
+ describe("Image data URLs", () => {
+ it("should return truthy for valid image data URLs", () => {
+ expect(getIsSafeURL("data:image/png;base64,iVBORw0KGgo=")).toBeTruthy();
+ expect(getIsSafeURL("data:image/jpeg;base64,/9j/4AAQSkZJ=")).toBeTruthy();
+ expect(getIsSafeURL("data:image/gif;base64,R0lGODlh=")).toBeTruthy();
+ expect(getIsSafeURL("data:image/bmp;base64,Qk0=")).toBeTruthy();
+ expect(getIsSafeURL("data:image/tiff;base64,SUkqAA==")).toBeTruthy();
+ expect(getIsSafeURL("data:image/webp;base64,UklGRj==")).toBeTruthy();
+ });
+
+ it("should return truthy for image data URLs with different base64 patterns", () => {
+ expect(getIsSafeURL("data:image/png;base64,abc123+/==")).toBeTruthy();
+ expect(getIsSafeURL("data:image/jpg;base64,xyz789=")).toBeTruthy();
+ });
+ });
+
+ describe("Video data URLs", () => {
+ it("should return truthy for valid video data URLs", () => {
+ expect(getIsSafeURL("data:video/mp4;base64,AAAAIGZ0eXBpc29t")).toBeTruthy();
+ expect(getIsSafeURL("data:video/mpeg;base64,AAAAGGZ0eXBpc29t")).toBeTruthy();
+ expect(getIsSafeURL("data:video/ogg;base64,AAAAGGZ0eXBpc29t")).toBeTruthy();
+ expect(getIsSafeURL("data:video/webm;base64,AAAAGGZ0eXBpc29t")).toBeTruthy();
+ });
+ });
+
+ describe("Audio data URLs", () => {
+ it("should return truthy for valid audio data URLs", () => {
+ expect(getIsSafeURL("data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0U=")).toBeTruthy();
+ expect(getIsSafeURL("data:audio/oga;base64,SUQzBAAAAAAAI1RTU0U=")).toBeTruthy();
+ expect(getIsSafeURL("data:audio/ogg;base64,SUQzBAAAAAAAI1RTU0U=")).toBeTruthy();
+ expect(getIsSafeURL("data:audio/opus;base64,SUQzBAAAAAAAI1RTU0U=")).toBeTruthy();
+ });
+ });
+
+ describe("Invalid data URLs", () => {
+ it("should return falsy for data URLs with unsupported MIME types", () => {
+ expect(getIsSafeURL("data:text/html;base64,PGh0bWw+")).toBeFalsy();
+ expect(getIsSafeURL("data:text/javascript;base64,YWxlcnQoMSk=")).toBeFalsy();
+ expect(getIsSafeURL("data:application/pdf;base64,JVBERi0xLjQ=")).toBeFalsy();
+ expect(getIsSafeURL("data:application/json;base64,eyAiYWJjIjogMTIzIH0=")).toBeFalsy();
+ });
+
+ it("should return falsy for data URLs without base64 encoding", () => {
+ expect(getIsSafeURL("data:text/plain,Hello World")).toBeFalsy();
+ expect(getIsSafeURL("data:text/html,")).toBeFalsy();
+ });
+ });
+ });
+
+ describe("Unsafe URLs", () => {
+ describe("JavaScript URLs", () => {
+ it("should return falsy for javascript: URLs", () => {
+ expect(getIsSafeURL("javascript:alert(1)")).toBeFalsy();
+ expect(getIsSafeURL("javascript:void(0)")).toBeFalsy();
+ expect(getIsSafeURL("javascript:window.location='http://evil.com'")).toBeFalsy();
+ });
+ });
+
+ describe("Data URLs with malicious content", () => {
+ it("should return falsy for data URLs that could execute scripts", () => {
+ expect(getIsSafeURL("data:text/html,")).toBeFalsy();
+ expect(getIsSafeURL("data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==")).toBeFalsy();
+ });
+ });
+
+ describe("Other unsafe protocols", () => {
+ it("should return falsy for vbscript: URLs", () => {
+ expect(getIsSafeURL("vbscript:msgbox(1)")).toBeFalsy();
+ });
+
+ it("should return falsy for data URLs with HTML", () => {
+ expect(getIsSafeURL("data:text/html;charset=utf-8,")).toBeFalsy();
+ });
+ });
+ });
+
+ describe("Relative URLs and paths", () => {
+ it("should return truthy for relative paths", () => {
+ expect(getIsSafeURL("/path/to/resource")).toBeTruthy();
+ expect(getIsSafeURL("path/to/resource")).toBeTruthy();
+ expect(getIsSafeURL("./relative/path")).toBeTruthy();
+ expect(getIsSafeURL("../parent/path")).toBeTruthy();
+ });
+
+ it("should return truthy for paths with query strings", () => {
+ expect(getIsSafeURL("/path?query=value")).toBeTruthy();
+ expect(getIsSafeURL("path?a=1&b=2")).toBeTruthy();
+ });
+
+ it("should return truthy for paths with fragments", () => {
+ expect(getIsSafeURL("/path#section")).toBeTruthy();
+ expect(getIsSafeURL("#anchor")).toBeTruthy();
+ });
+ });
+
+ describe("Edge cases", () => {
+ it("should return falsy for non-string input", () => {
+ expect(getIsSafeURL(null as any)).toBeFalsy();
+ expect(getIsSafeURL(undefined as any)).toBeFalsy();
+ expect(getIsSafeURL(123 as any)).toBeFalsy();
+ expect(getIsSafeURL({} as any)).toBeFalsy();
+ });
+
+ it("should return truthy for empty string", () => {
+ // Note: The regex pattern matches empty string because [^&:/?#]* can match zero characters
+ // followed by $ which is end of string
+ expect(getIsSafeURL("")).toBeTruthy();
+ });
+
+ it("should handle URLs with special characters in path", () => {
+ expect(getIsSafeURL("http://example.com/path%20with%20spaces")).toBeTruthy();
+ expect(getIsSafeURL("http://example.com/path?query=value&other=123")).toBeTruthy();
+ });
+
+ it("should handle case insensitivity for MIME types", () => {
+ expect(getIsSafeURL("data:IMAGE/png;base64,iVBORw0KGgo=")).toBeTruthy();
+ expect(getIsSafeURL("data:Image/Png;base64,iVBORw0KGgo=")).toBeTruthy();
+ });
+ });
+
+ describe("URL safety validation", () => {
+ it("should prevent XSS via javascript: protocol", () => {
+ const xssPayloads = [
+ "javascript:alert(document.cookie)",
+ "javascript:window.location='http://evil.com'",
+ "javascript:void(document.body.innerHTML='
')",
+ ];
+
+ xssPayloads.forEach((payload) => {
+ expect(getIsSafeURL(payload)).toBeFalsy();
+ });
+ });
+
+ it("should prevent XSS via data: URLs with HTML", () => {
+ const htmlPayloads = [
+ "data:text/html,",
+ "data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==",
+ "data:text/html,
",
+ ];
+
+ htmlPayloads.forEach((payload) => {
+ expect(getIsSafeURL(payload)).toBeFalsy();
+ });
+ });
+
+ it("should allow safe image data URLs", () => {
+ const safeImageUrls = [
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
+ "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBEQAhEDEQAAL+fgA//9k=",
+ ];
+
+ safeImageUrls.forEach((url) => {
+ expect(getIsSafeURL(url)).toBeTruthy();
+ });
+ });
+ });
+});
\ No newline at end of file