Repository: Azure/Azurite (ts)

Issue:
HealthCheck fails with 400 bad request
### Which service(blob, file, queue, table) does this issue concern?
Table

### Which version of the Azurite was used?
3.31.0

### Where do you get Azurite? (npm, DockerHub, NuGet, Visual Studio Code Extension)
DockerHub

### What's the Node.js version?
NA

### What problem was encountered?

```
The query condition specified in the request is invalid.\nRequestId:47a6a1bb-5a89-47e9-8dda-954ba86566dd\nTime:2024-08-09T14:03:38.156Z\r\nStatus: 400 (Bad Request)\r\nErrorCode: InvalidInput\r\n\r\nContent:\r\n{\"odata.error\":{\"code\":\"InvalidInput\",\"message\":{\"lang\":\"en-US\",\"value\":\"The query condition specified in the request is invalid.\\nRequestId:47a6a1bb-5a89-47e9-8dda-954ba86566dd\\nTime:2024-08-09T14:03:38.156Z\"}}}\r\n\r\nHeaders:\r\nServer: Azurite-Table/3.31.0\r\nx-ms-error-code: REDACTED\r\nx-ms-request-id: 47a6a1bb-5a89-47e9-8dda-954ba86566dd\r\nx-ms-version: REDACTED\r\nDate: Fri, 09 Aug 2024 14:03:38 GMT\r\nConnection: keep-alive\r\nKeep-Alive: REDACTED\r\nTransfer-Encoding: chunked\r\nContent-Type: application/json;odata=minimalmetadata;streaming=true;charset=utf-8\r\n
```

Health check source code:

https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/blob/master/src/HealthChecks.Azure.Data.Tables/AzureTableServiceHealthCheck.cs

### Steps to reproduce the issue?

After switching to Azurite, using the default health checker that comes with the Aspire component for adding azure table storage, the health check is receiving a 400 bad request.

1. Create a new Aspire app
2. Add azurite to apphost
  ```C#
  // Add Azure Storage
  var storage = builder.AddAzureStorage("storage")
      .RunAsEmulator(c => c.WithImageTag("3.31.0").WithDataVolume();
  );
  var yourComponent = storage.AddTables("yourcomponent");
  ```
3. Add a client app which loads the component, then enable health checks
  ```C#
  builder.AddKeyedAzureTableClient("yourcomponent");
  ```

### Have you found a mitigation/solution?

Not yet


Pull request:
Fixed default table HealthCheck fails with 400 bad request
Fixed #2450, #1573
Thanks for contribution! Please go through following checklist before sending PR.

### PR Branch Destination

- For Azurite V3, please send PR to `main` branch.
- For legacy Azurite V2, please send PR to `legacy-dev` branch.

### Always Add Test Cases

Make sure test cases are added to cover the code change.

### Add Change Log

Add change log for the code change in `Upcoming Release` section in `ChangeLog.md`.

### Development Guideline

Please go to CONTRIBUTION.md for steps about setting up development environment and recommended Visual Studio Code extensions.


Patch:
diff --git a/ChangeLog.md b/ChangeLog.md
index b786ad2..cc1f7ac 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -24,6 +24,7 @@ Blob:
 Table:
 
 - Fixed "Unexpected EOF" error when batch InsertReplace entities with Go SDK (issue #2519)
+- Added support to query entity with simple filters as empty string, 'true' or 'false'. (issue #2450, #1573)
 
 ## 2024.10 Version 3.33.0
 
diff --git a/src/table/persistence/LokiTableStoreQueryGenerator.ts b/src/table/persistence/LokiTableStoreQueryGenerator.ts
index b189572..341d170 100644
--- a/src/table/persistence/LokiTableStoreQueryGenerator.ts
+++ b/src/table/persistence/LokiTableStoreQueryGenerator.ts
@@ -47,10 +47,14 @@ export default class LokiTableStoreQueryGenerator {
   public static generateQueryTableWhereFunction(
     query: string | undefined
   ): (entity: Table) => boolean {
-    if (query === undefined) {
+    if (query === undefined || query === "" || query === "true") {
       return () => true;
     }
 
+    if (query === "false") {
+      return () => false;
+    }
+
     const queryTree = parseQuery(query);
     validateQueryTree(queryTree);
     return (entity) => executeQuery(entity, queryTree);
@@ -80,10 +84,14 @@ export default class LokiTableStoreQueryGenerator {
   private static generateQueryEntityWhereFunction(
     query: string | undefined
   ): (entity: Entity) => boolean {
-    if (query === undefined || query === "") {
+    if (query === undefined || query === "" || query === "true") {
       return () => true;
     }
 
+    if (query === "false") {
+      return () => false;
+    }
+
     const queryTree = parseQuery(query);
     validateQueryTree(queryTree);
     return (entity) => executeQuery(entity, queryTree);


Test patch:
diff --git a/tests/table/apis/table.entity.issues.test.ts b/tests/table/apis/table.entity.issues.test.ts
index a8d3af0..615b492 100644
--- a/tests/table/apis/table.entity.issues.test.ts
+++ b/tests/table/apis/table.entity.issues.test.ts
@@ -4,7 +4,8 @@ import {
   GetTableEntityResponse,
   odata,
   TableEntity,
-  TableEntityResult
+  TableEntityResult,
+  TableItem
 } from "@azure/data-tables";
 import { configLogger } from "../../../src/common/Logger";
 import StorageError from "../../../src/table/errors/StorageError";
@@ -12,6 +13,7 @@ import TableServer from "../../../src/table/TableServer";
 import { getUniqueName } from "../../testutils";
 import {
   createAzureDataTablesClient,
+  createAzureDataTableServiceClient,
   createTableServerForTestHttps,
   createUniquePartitionKey
 } from "../utils/table.entity.test.utils";
@@ -454,5 +456,144 @@ describe("table Entity APIs test : Issues", () => {
       assert.strictEqual(storageError.statusCode, "InvalidInput");
       assert.strictEqual(storageError.storageErrorCode, 400);
     });
+
+    await tableClient.deleteTable();
+  });
+
+  //from issue #2450
+  it("should parce a simple query entity @loki", async () => {
+    const partitionKeyForQueryTest1 = createUniquePartitionKey("1_");
+    const totalItems = 2;
+    const tableClient = createAzureDataTablesClient(
+      testLocalAzuriteInstance,
+      tableName
+    );
+    await tableClient.createTable();
+
+    // first partition
+    // creates entities individually
+    for (let i = 0; i < totalItems; i++) {
+      const result = await tableClient.createEntity({
+        partitionKey: partitionKeyForQueryTest1,
+        rowKey: `${i}`,
+        number: i
+      });
+      assert.notStrictEqual(result.etag, undefined);
+    }
+
+    const maxPageSize = 5; // this should work with a page size of 1000, but fails during response serialization on my machine
+    let testsCompleted = 0;
+    // take note of the different whitespacing and query formatting:
+    const queriesAndExpectedResult = [
+      {
+        queryOptions: {
+          filter: ``
+        },
+        expectedResult: 2
+      },
+      {
+        queryOptions: {
+          filter: `false`
+        },
+        expectedResult: 0
+      },
+      {
+        queryOptions: {
+          filter: `true`
+        },
+        expectedResult: 2
+      }
+    ];
+    
+        for (const queryTest of queriesAndExpectedResult) {
+          const entities = tableClient.listEntities<TableEntity<{ number: number }>>({
+            queryOptions: queryTest.queryOptions,
+            disableTypeConversion: true
+          });
+          let all: TableEntity<{ number: number }>[] = [];
+          for await (const entity of entities.byPage({
+            maxPageSize
+          })) {
+            all = [...all, ...entity];
+          }
+          assert.strictEqual(
+            all.length,
+            queryTest.expectedResult,
+            `Failed with query ${queryTest.queryOptions.filter}`
+          );
+          testsCompleted++;
+        }
+        assert.strictEqual(testsCompleted, queriesAndExpectedResult.length);
+        await tableClient.deleteTable();
+
+  });
+  //from issue #2450
+  it("should parce a simple query tables @loki", async () => {
+    const tableClient = createAzureDataTableServiceClient(
+      testLocalAzuriteInstance
+    );
+
+    // not all test remove tables after running
+    // so we need to remove the table if it exists
+    const maxPageSize = 5; 
+    const tables = tableClient.listTables();
+    let all: TableItem[] = [];
+    for await (const table of tables.byPage({
+      maxPageSize
+    })) {
+      all = [...all, ...table];
+    }
+
+    for (const table of all) {
+      if(table.name) {     
+        await tableClient.deleteTable(table.name);
+      }
+    }
+
+    await tableClient.createTable(tableName);
+
+    let testsCompleted = 0;
+    // take note of the different whitespacing and query formatting:
+    const queriesAndExpectedResult = [
+      {
+        queryOptions: {
+          filter: ``
+        },
+        expectedResult: 1
+      },
+      {
+        queryOptions: {
+          filter: `false`
+        },
+        expectedResult: 0
+      },
+      {
+        queryOptions: {
+          filter: `true`
+        },
+        expectedResult: 1
+      }
+    ];
+    
+        for (const queryTest of queriesAndExpectedResult) {
+          const tables = tableClient.listTables({
+            queryOptions: queryTest.queryOptions
+          });
+          let all: TableItem[] = [];
+          for await (const table of tables.byPage({
+            maxPageSize
+          })) {
+            all = [...all, ...table];
+          }
+          assert.strictEqual(
+            all.length,
+            queryTest.expectedResult,
+            `Failed with query ${queryTest.queryOptions.filter}`
+          );
+          testsCompleted++;
+        }
+        assert.strictEqual(testsCompleted, queriesAndExpectedResult.length);
+        await tableClient.deleteTable(tableName);
+
   });
 });
diff --git a/tests/table/utils/table.entity.test.utils.ts b/tests/table/utils/table.entity.test.utils.ts
index 1f6aed6..d1d43ab 100644
--- a/tests/table/utils/table.entity.test.utils.ts
+++ b/tests/table/utils/table.entity.test.utils.ts
@@ -8,7 +8,8 @@ import TableServer from "../../../src/table/TableServer";
 import {
   AzureNamedKeyCredential,
   AzureSASCredential,
-  TableClient
+  TableClient,
+  TableServiceClient
 } from "@azure/data-tables";
 import { copyFile } from "fs";
 import TableTestServerFactory, { ITableTestServerFactoryParams } from "./TableTestServerFactory";
@@ -183,7 +184,40 @@ export function createAzureDataTablesClient(
     );
   }
 }
+/**
+ * creates an Azure Data Tables client for local or service tests
+ *
+ * @export
+ * @param {boolean} local
+ * @param {string} tableName
+ * @return {*}  {TableClient}
+ */
+export function createAzureDataTableServiceClient(
+  local: boolean
+): TableServiceClient {
+  if (local) {
+    const sharedKeyCredential = new AzureNamedKeyCredential(
+      EMULATOR_ACCOUNT_NAME,
+      EMULATOR_ACCOUNT_KEY
+    );
+
+    return new TableServiceClient(
+      `https://${HOST}:${PORT}/${EMULATOR_ACCOUNT_NAME}`,
+      sharedKeyCredential
+    );
+  } else {
+    // return new TableClient(
+    //   process.env[AZURE_DATATABLES_STORAGE_STRING]! +
+    //     process.env[AZURE_DATATABLES_SAS]!,
+    //   tableName
+    // );
 
+    return new TableServiceClient(
+      process.env[AZURE_DATATABLES_STORAGE_STRING]!,
+      new AzureSASCredential(process.env[AZURE_DATATABLES_SAS]!)
+    );
+  }
+}
 /**
  * Default behavior will overwrite target.
  * This will copy the old db file with older schema on which we then