From d2cbc183f69f457681d1dfccfb01bb0e3b2c3693 Mon Sep 17 00:00:00 2001 From: "F. Levi" <55688616+flevi29@users.noreply.github.com> Date: Sat, 5 Apr 2025 16:42:02 +0300 Subject: [PATCH 1/6] Refactor key types --- src/meilisearch.ts | 79 +++++++++++++---------------------------- src/types/index.ts | 1 + src/types/keys.ts | 87 ++++++++++++++++++++++++++++++++++++++++++++++ src/types/types.ts | 34 ------------------ tests/keys.test.ts | 14 ++++---- 5 files changed, 119 insertions(+), 96 deletions(-) create mode 100644 src/types/keys.ts diff --git a/src/meilisearch.ts b/src/meilisearch.ts index 25f1c4e8d..7efd7ee07 100644 --- a/src/meilisearch.ts +++ b/src/meilisearch.ts @@ -7,19 +7,14 @@ import { Index } from "./indexes.js"; import type { - KeyCreation, Config, IndexOptions, IndexObject, - Key, Health, Stats, Version, - KeyUpdate, IndexesQuery, IndexesResults, - KeysQuery, - KeysResults, IndexSwap, MultiSearchParams, FederatedMultiSearchParams, @@ -28,6 +23,11 @@ import type { ExtraRequestInit, Network, RecordAny, + CreateApiKey, + KeyView, + KeyViewList, + ListApiKeys, + PatchApiKey, } from "./types/index.js"; import { ErrorStatusCode } from "./types/index.js"; import { HttpRequests } from "./http-requests.js"; @@ -302,72 +302,41 @@ export class MeiliSearch { /// KEYS /// - /** - * Get all API keys - * - * @param parameters - Parameters to browse the indexes - * @returns Promise returning an object with keys - */ - async getKeys(parameters?: KeysQuery): Promise { - const keys = await this.httpRequest.get({ + /** {@link https://www.meilisearch.com/docs/reference/api/keys#get-all-keys} */ + async getKeys(listApiKeys?: ListApiKeys): Promise { + return await this.httpRequest.get({ path: "keys", - params: parameters, + params: listApiKeys, }); - - keys.results = keys.results.map((key) => ({ - ...key, - createdAt: new Date(key.createdAt), - updatedAt: new Date(key.updatedAt), - })); - - return keys; } - /** - * Get one API key - * - * @param keyOrUid - Key or uid of the API key - * @returns Promise returning a key - */ - async getKey(keyOrUid: string): Promise { - return await this.httpRequest.get({ + /** {@link https://www.meilisearch.com/docs/reference/api/keys#get-one-key} */ + async getKey(keyOrUid: string): Promise { + return await this.httpRequest.get({ path: `keys/${keyOrUid}`, }); } - /** - * Create one API key - * - * @param options - Key options - * @returns Promise returning a key - */ - async createKey(options: KeyCreation): Promise { - return await this.httpRequest.post({ + /** {@link https://www.meilisearch.com/docs/reference/api/keys#create-a-key} */ + async createKey(createApiKey: CreateApiKey): Promise { + return await this.httpRequest.post({ path: "keys", - body: options, + body: createApiKey, }); } - /** - * Update one API key - * - * @param keyOrUid - Key - * @param options - Key options - * @returns Promise returning a key - */ - async updateKey(keyOrUid: string, options: KeyUpdate): Promise { - return await this.httpRequest.patch({ + /** {@link https://www.meilisearch.com/docs/reference/api/keys#update-a-key} */ + async updateKey( + keyOrUid: string, + patchApiKey: PatchApiKey, + ): Promise { + return await this.httpRequest.patch({ path: `keys/${keyOrUid}`, - body: options, + body: patchApiKey, }); } - /** - * Delete one API key - * - * @param keyOrUid - Key - * @returns - */ + /** {@link https://www.meilisearch.com/docs/reference/api/keys#delete-a-key} */ async deleteKey(keyOrUid: string): Promise { await this.httpRequest.delete({ path: `keys/${keyOrUid}` }); } diff --git a/src/types/index.ts b/src/types/index.ts index 2a13bc669..fad2099bc 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,4 @@ +export * from "./keys.js"; export * from "./task_and_batch.js"; export * from "./token.js"; export * from "./types.js"; diff --git a/src/types/keys.ts b/src/types/keys.ts new file mode 100644 index 000000000..c8d2a5a79 --- /dev/null +++ b/src/types/keys.ts @@ -0,0 +1,87 @@ +type ALL = "*"; +type GET = "get"; +type CREATE = "create"; +type UPDATE = "update"; +type DELETE = "delete"; + +/** + * {@link https://www.meilisearch.com/docs/reference/api/keys#actions} + * + * @see `meilisearch_types::keys::Action` + */ +export type Action = + | ALL + | "search" + | `documents.${ALL | "add" | GET | DELETE}` + | `indexes.${ALL | CREATE | GET | UPDATE | DELETE | "swap"}` + | `tasks.${ALL | "cancel" | DELETE | GET}` + | `settings.${ALL | GET | UPDATE}` + | `stats.${GET}` + | `metrics.${GET}` + | `dumps.${CREATE}` + | `snapshots.${CREATE}` + | "version" + | `keys.${CREATE | GET | UPDATE | DELETE}` + | `experimental.${GET | UPDATE}` + | `network.${GET | UPDATE}`; + +/** + * {@link https://www.meilisearch.com/docs/reference/api/keys#body} + * + * @see `meilisearch_types::keys::CreateApiKey` + */ +export type CreateApiKey = { + description?: string | null; + name?: string | null; + uid?: string; + actions: Action[]; + indexes: string[]; + expiresAt: string | null; +}; + +/** + * {@link https://www.meilisearch.com/docs/reference/api/keys#key-object} + * + * @see `meilisearch::routes::api_key::KeyView` + */ +export type KeyView = { + name: string | null; + description: string | null; + key: string; + uid: string; + actions: Action[]; + expiresAt: string | null; + createdAt: string; + updatedAt: string; +}; + +/** + * {@link https://www.meilisearch.com/docs/reference/api/keys#query-parameters} + * + * @see `meilisearch::routes::api_key::ListApiKeys` + */ +export type ListApiKeys = { + offset?: number; + limit?: number; +}; + +/** @see `meilisearch::routes::PaginationView` */ +export type PaginationView = { + results: T[]; + offset: number; + limit: number; + total: number; +}; + +/** {@link https://www.meilisearch.com/docs/reference/api/keys#response} */ +export type KeyViewList = PaginationView; + +/** + * {@link https://www.meilisearch.com/docs/reference/api/keys#body-1} + * + * @see `meilisearch_types::keys::PatchApiKey` + */ +export type PatchApiKey = { + description?: string | null; + name?: string | null; +}; diff --git a/src/types/types.ts b/src/types/types.ts index 9b1640f14..a31042268 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -673,40 +673,6 @@ export type Stats = { }; }; -/* - ** Keys - */ - -export type Key = { - uid: string; - description: string; - name: string | null; - key: string; - actions: string[]; - indexes: string[]; - expiresAt: Date; - createdAt: Date; - updatedAt: Date; -}; - -export type KeyCreation = { - uid?: string; - name?: string; - description?: string; - actions: string[]; - indexes: string[]; - expiresAt: Date | null; -}; - -export type KeyUpdate = { - name?: string; - description?: string; -}; - -export type KeysQuery = ResourceQuery & {}; - -export type KeysResults = ResourceResults & {}; - /* ** version */ diff --git a/tests/keys.test.ts b/tests/keys.test.ts index a1e305d07..eea712b59 100644 --- a/tests/keys.test.ts +++ b/tests/keys.test.ts @@ -54,9 +54,9 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( expect(searchKey).toHaveProperty("indexes"); expect(searchKey).toHaveProperty("expiresAt", null); expect(searchKey).toHaveProperty("createdAt"); - expect(searchKey?.createdAt).toBeInstanceOf(Date); + expect(searchKey?.createdAt).toBeTypeOf("string"); expect(searchKey).toHaveProperty("updatedAt"); - expect(searchKey?.updatedAt).toBeInstanceOf(Date); + expect(searchKey?.updatedAt).toBeTypeOf("string"); const adminKey = keys.results.find( (key) => key.name === "Default Admin API Key", @@ -72,9 +72,9 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( expect(adminKey).toHaveProperty("indexes"); expect(adminKey).toHaveProperty("expiresAt", null); expect(adminKey).toHaveProperty("createdAt"); - expect(searchKey?.createdAt).toBeInstanceOf(Date); + expect(searchKey?.createdAt).toBeTypeOf("string"); expect(adminKey).toHaveProperty("updatedAt"); - expect(searchKey?.updatedAt).toBeInstanceOf(Date); + expect(searchKey?.updatedAt).toBeTypeOf("string"); }); test(`${permission} key: get keys with pagination`, async () => { @@ -156,7 +156,7 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( description: "Indexing Products API key", actions: ["documents.add"], indexes: ["products"], - expiresAt: new Date("2050-11-13T00:00:00Z"), // Test will fail in 2050 + expiresAt: new Date("2050-11-13T00:00:00Z").toISOString(), // Test will fail in 2050 }); expect(key).toBeDefined(); @@ -171,7 +171,7 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( description: "Indexing Products API key", actions: ["documents.add"], indexes: ["products"], - expiresAt: new Date("2050-11-13T00:00:00Z"), // Test will fail in 2050 + expiresAt: new Date("2050-11-13T00:00:00Z").toISOString(), // Test will fail in 2050 }); const updatedKey = await client.updateKey(key.key, { @@ -194,7 +194,7 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( description: "Indexing Products API key", actions: ["documents.add"], indexes: ["products"], - expiresAt: new Date("2050-11-13T00:00:00Z"), // Test will fail in 2050 + expiresAt: new Date("2050-11-13T00:00:00Z").toISOString(), // Test will fail in 2050 }); const deletedKey = await client.deleteKey(key.key); From 391e4fd3bd30b6d36380d34eb31a6a9601690ad0 Mon Sep 17 00:00:00 2001 From: "F. Levi" <55688616+flevi29@users.noreply.github.com> Date: Sun, 6 Apr 2025 11:07:20 +0300 Subject: [PATCH 2/6] Fix test --- src/types/indexes.ts | 23 +++++++++++++++++++++++ tests/keys.test.ts | 4 ++-- 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 src/types/indexes.ts diff --git a/src/types/indexes.ts b/src/types/indexes.ts new file mode 100644 index 000000000..352c0424b --- /dev/null +++ b/src/types/indexes.ts @@ -0,0 +1,23 @@ +/** @see `meilisearch::routes::indexes::ListIndexes` */ +export type ListIndexes = { + offset?: number; + limit?: number; +}; + +/** @see `meilisearch::routes::PaginationView` */ +type PaginationView = { + results: T[]; + offset: number; + limit: number; + total: number; +}; + +/** @see `meilisearch::routes::indexes::IndexView` */ +export type IndexView = { + uid: string; + createdAt: string; + updatedAt: string; + primaryKey: string | null; +}; + +export type IndexViewList = PaginationView; diff --git a/tests/keys.test.ts b/tests/keys.test.ts index eea712b59..f2220ae89 100644 --- a/tests/keys.test.ts +++ b/tests/keys.test.ts @@ -72,9 +72,9 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( expect(adminKey).toHaveProperty("indexes"); expect(adminKey).toHaveProperty("expiresAt", null); expect(adminKey).toHaveProperty("createdAt"); - expect(searchKey?.createdAt).toBeTypeOf("string"); + expect(adminKey?.createdAt).toBeTypeOf("string"); expect(adminKey).toHaveProperty("updatedAt"); - expect(searchKey?.updatedAt).toBeTypeOf("string"); + expect(adminKey?.updatedAt).toBeTypeOf("string"); }); test(`${permission} key: get keys with pagination`, async () => { From 8cc208f92008f57d676a92ed5ff758f11c94fc4d Mon Sep 17 00:00:00 2001 From: "F. Levi" <55688616+flevi29@users.noreply.github.com> Date: Sun, 6 Apr 2025 11:24:04 +0300 Subject: [PATCH 3/6] Remove accidentally added file --- src/types/indexes.ts | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 src/types/indexes.ts diff --git a/src/types/indexes.ts b/src/types/indexes.ts deleted file mode 100644 index 352c0424b..000000000 --- a/src/types/indexes.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** @see `meilisearch::routes::indexes::ListIndexes` */ -export type ListIndexes = { - offset?: number; - limit?: number; -}; - -/** @see `meilisearch::routes::PaginationView` */ -type PaginationView = { - results: T[]; - offset: number; - limit: number; - total: number; -}; - -/** @see `meilisearch::routes::indexes::IndexView` */ -export type IndexView = { - uid: string; - createdAt: string; - updatedAt: string; - primaryKey: string | null; -}; - -export type IndexViewList = PaginationView; From 5a4e942a6e8cc2057c4d35f848e77d8378c216a4 Mon Sep 17 00:00:00 2001 From: "F. Levi" <55688616+flevi29@users.noreply.github.com> Date: Tue, 13 May 2025 11:26:06 +0300 Subject: [PATCH 4/6] Refactor tests, add missing property --- src/types/keys.ts | 1 + tests/keys.test.ts | 451 ++++++++++++-------------- tests/utils/meilisearch-test-utils.ts | 11 + 3 files changed, 223 insertions(+), 240 deletions(-) diff --git a/src/types/keys.ts b/src/types/keys.ts index c8d2a5a79..82ce256e2 100644 --- a/src/types/keys.ts +++ b/src/types/keys.ts @@ -50,6 +50,7 @@ export type KeyView = { key: string; uid: string; actions: Action[]; + indexes: string[]; expiresAt: string | null; createdAt: string; updatedAt: string; diff --git a/tests/keys.test.ts b/tests/keys.test.ts index f2220ae89..4c9530dc6 100644 --- a/tests/keys.test.ts +++ b/tests/keys.test.ts @@ -1,258 +1,229 @@ -import { expect, test, describe, beforeEach, afterAll } from "vitest"; -import { MeiliSearch } from "../src/index.js"; -import { ErrorStatusCode } from "../src/types/index.js"; +import { describe, test } from "vitest"; +import type { + Action, + CreateApiKey, + KeyView, + ListApiKeys, +} from "../src/index.js"; import { - clearAllIndexes, - config, + assert as extAssert, getClient, - getKey, - HOST, + objectEntries, + objectKeys, } from "./utils/meilisearch-test-utils.js"; -beforeEach(async () => { - await clearAllIndexes(config); -}); - -afterAll(() => { - return clearAllIndexes(config); -}); - -describe.each([{ permission: "Master" }, { permission: "Admin" }])( - "Test on keys", - ({ permission }) => { - beforeEach(async () => { - const client = await getClient("Master"); - await clearAllIndexes(config); - - const keys = await client.getKeys(); - - const customKeys = keys.results.filter( - (key) => - key.name !== "Default Search API Key" && - key.name !== "Default Admin API Key", - ); - - // Delete all custom keys - await Promise.all(customKeys.map((key) => client.deleteKey(key.uid))); - }); - - test(`${permission} key: get keys`, async () => { - const client = await getClient(permission); - const keys = await client.getKeys(); - - const searchKey = keys.results.find( - (key) => key.name === "Default Search API Key", - ); - - expect(searchKey).toBeDefined(); - expect(searchKey).toHaveProperty( - "description", - "Use it to search from the frontend", - ); - expect(searchKey).toHaveProperty("key"); - expect(searchKey).toHaveProperty("actions"); - expect(searchKey).toHaveProperty("indexes"); - expect(searchKey).toHaveProperty("expiresAt", null); - expect(searchKey).toHaveProperty("createdAt"); - expect(searchKey?.createdAt).toBeTypeOf("string"); - expect(searchKey).toHaveProperty("updatedAt"); - expect(searchKey?.updatedAt).toBeTypeOf("string"); - - const adminKey = keys.results.find( - (key) => key.name === "Default Admin API Key", - ); - - expect(adminKey).toBeDefined(); - expect(adminKey).toHaveProperty( - "description", - "Use it for anything that is not a search operation. Caution! Do not expose it on a public frontend", - ); - expect(adminKey).toHaveProperty("key"); - expect(adminKey).toHaveProperty("actions"); - expect(adminKey).toHaveProperty("indexes"); - expect(adminKey).toHaveProperty("expiresAt", null); - expect(adminKey).toHaveProperty("createdAt"); - expect(adminKey?.createdAt).toBeTypeOf("string"); - expect(adminKey).toHaveProperty("updatedAt"); - expect(adminKey?.updatedAt).toBeTypeOf("string"); - }); - - test(`${permission} key: get keys with pagination`, async () => { - const client = await getClient(permission); - const keys = await client.getKeys({ limit: 1, offset: 2 }); - - expect(keys.limit).toEqual(1); - expect(keys.offset).toEqual(2); - expect(keys.total).toEqual(2); - }); - - test(`${permission} key: get on key`, async () => { - const client = await getClient(permission); - const apiKey = await getKey("Admin"); - - const key = await client.getKey(apiKey); - - expect(key).toBeDefined(); - expect(key).toHaveProperty( - "description", - "Use it for anything that is not a search operation. Caution! Do not expose it on a public frontend", - ); - expect(key).toHaveProperty("key"); - expect(key).toHaveProperty("actions"); - expect(key).toHaveProperty("indexes"); - expect(key).toHaveProperty("expiresAt", null); - expect(key).toHaveProperty("createdAt"); - expect(key).toHaveProperty("updatedAt"); - }); - - test(`${permission} key: create key with no expiresAt`, async () => { - const client = await getClient(permission); - const uid = "3db051e0-423d-4b5c-a63a-f82a7043dce6"; - - const key = await client.createKey({ - uid, - description: "Indexing Products API key", - actions: ["documents.add"], - indexes: ["products"], - expiresAt: null, - }); +const customAssert = { + isKeyView(value: KeyView) { + extAssert.lengthOf(Object.keys(value), 9); + const { + name, + description, + key, + uid, + actions, + indexes, + expiresAt, + createdAt, + updatedAt, + } = value; + + extAssert( + name === null || typeof name === "string", + "expected name to be null or string", + ); + extAssert( + description === null || typeof description === "string", + "expected description to be null or string", + ); + + extAssert.typeOf(key, "string"); + extAssert.typeOf(uid, "string"); + + for (const action of actions) { + extAssert.oneOf(action, possibleActions); + } + + for (const index of indexes) { + extAssert.typeOf(index, "string"); + } + + extAssert( + expiresAt === null || typeof expiresAt === "string", + "expected expiresAt to be null or string", + ); + + extAssert.typeOf(createdAt, "string"); + extAssert.typeOf(updatedAt, "string"); + }, +}; - expect(key).toBeDefined(); - expect(key).toHaveProperty("description", "Indexing Products API key"); - expect(key).toHaveProperty("uid", uid); - expect(key).toHaveProperty("expiresAt", null); - }); +const assert: typeof extAssert & typeof customAssert = Object.assign( + extAssert, + customAssert, +); - test(`${permission} key: create key with actions using wildcards to provide rights`, async () => { - const client = await getClient(permission); - const uid = "3db051e0-423d-4b5c-a63a-f82a7043dce6"; +type TestRecord = { + [TKey in keyof CreateApiKey]-?: [ + name: string | undefined, + value: CreateApiKey[TKey], + assertion: (a: CreateApiKey[TKey], b: CreateApiKey[TKey]) => void, + ][]; +}; + +type SimplifiedTestRecord = Record< + keyof CreateApiKey, + [ + name: string | undefined, + value: CreateApiKey[keyof CreateApiKey], + assertion: ( + a: CreateApiKey[keyof CreateApiKey], + b: CreateApiKey[keyof CreateApiKey], + ) => void, + ][] +>; + +const possibleActions = objectKeys({ + "*": null, + search: null, + "documents.*": null, + "documents.add": null, + "documents.get": null, + "documents.delete": null, + "indexes.*": null, + "indexes.get": null, + "indexes.delete": null, + "indexes.create": null, + "indexes.update": null, + "indexes.swap": null, + "tasks.*": null, + "tasks.get": null, + "tasks.delete": null, + "tasks.cancel": null, + "settings.*": null, + "settings.get": null, + "settings.update": null, + "stats.get": null, + "metrics.get": null, + "dumps.create": null, + "snapshots.create": null, + version: null, + "keys.get": null, + "keys.delete": null, + "keys.create": null, + "keys.update": null, + "experimental.get": null, + "experimental.update": null, + "network.get": null, + "network.update": null, +}); - const key = await client.createKey({ - uid, - description: "Indexing Products API key", - actions: ["indexes.*", "tasks.*", "documents.*"], - indexes: ["wildcard_keys_permission"], +const testRecord = { + description: [ + [ + undefined, + "The Skeleton Key is an unbreakable lockpick and Daedric Artifact in The Elder Scrolls IV: Oblivion.", + (a, b) => { + assert.strictEqual(a, b); + }, + ], + ], + name: [ + [ + undefined, + "Skeleton Key", + (a, b) => { + assert.strictEqual(a, b); + }, + ], + ], + uid: [ + [ + undefined, + "bd2cbad1-6c5f-48e3-bb92-bc9961bc011e", + (a, b) => { + assert.strictEqual(a, b); + }, + ], + ], + actions: possibleActions.map((action) => [ + action, + [action], + (a, b) => { + assert.sameMembers(a, b); + }, + ]), + indexes: [ + [ + undefined, + ["indexEins", "indexZwei"], + (a, b) => { + assert.sameMembers(a, b); + }, + ], + ], + expiresAt: [ + [ + undefined, + new Date("9999-12-5").toISOString(), + (a, b) => { + assert.strictEqual(Date.parse(a!), Date.parse(b!)); + }, + ], + ], +} satisfies TestRecord as SimplifiedTestRecord; + +// transform names +for (const testValues of Object.values(testRecord)) { + for (const testValue of testValues) { + testValue[0] = testValue[0] === undefined ? "" : ` with "${testValue[0]}"`; + } +} + +const ms = await getClient("Master"); + +describe.for(objectEntries(testRecord))("`%s`", ([key, values]) => { + test.for(values)( + `${ms.createKey.name} method%s`, + async ([, value, assertion]) => { + const keyView = await ms.createKey({ + actions: ["*"], + indexes: ["*"], expiresAt: null, + [key]: value, }); - const newClient = new MeiliSearch({ host: HOST, apiKey: key.key }); - await newClient.createIndex("wildcard_keys_permission"); // test index creation - const task = await newClient - .index("wildcard_keys_permission") - .addDocuments([{ id: 1 }]) - .waitTask(); // test document addition - - expect(key).toBeDefined(); - expect(task.status).toBe("succeeded"); - expect(key).toHaveProperty("description", "Indexing Products API key"); - expect(key).toHaveProperty("uid", uid); - expect(key).toHaveProperty("expiresAt", null); - }); - - test(`${permission} key: create key with an expiresAt`, async () => { - const client = await getClient(permission); - - const key = await client.createKey({ - description: "Indexing Products API key", - actions: ["documents.add"], - indexes: ["products"], - expiresAt: new Date("2050-11-13T00:00:00Z").toISOString(), // Test will fail in 2050 - }); - - expect(key).toBeDefined(); - expect(key).toHaveProperty("description", "Indexing Products API key"); - expect(key).toHaveProperty("expiresAt", "2050-11-13T00:00:00Z"); - }); - - test(`${permission} key: update a key`, async () => { - const client = await getClient(permission); - - const key = await client.createKey({ - description: "Indexing Products API key", - actions: ["documents.add"], - indexes: ["products"], - expiresAt: new Date("2050-11-13T00:00:00Z").toISOString(), // Test will fail in 2050 - }); - - const updatedKey = await client.updateKey(key.key, { - description: "Indexing Products API key 2", - name: "Product admin", - }); + assert.isKeyView(keyView); - expect(updatedKey).toBeDefined(); - expect(updatedKey).toHaveProperty( - "description", - "Indexing Products API key 2", - ); - expect(updatedKey).toHaveProperty("name", "Product admin"); - }); + assertion(keyView[key as keyof typeof keyView], value); + }, + ); +}); - test(`${permission} key: delete a key`, async () => { - const client = await getClient(permission); +test(`${ms.getKeys.name}, ${ms.getKey.name} and ${ms.deleteKey.name} methods`, async () => { + const keyList = await ms.getKeys({ + offset: 0, + limit: 10_000, + } satisfies Required); - const key = await client.createKey({ - description: "Indexing Products API key", - actions: ["documents.add"], - indexes: ["products"], - expiresAt: new Date("2050-11-13T00:00:00Z").toISOString(), // Test will fail in 2050 - }); + for (const { uid, name } of keyList.results) { + const keyView = await ms.getKey(uid); - const deletedKey = await client.deleteKey(key.key); + // avoid deleting default keys that might be used by other tests + if (name !== "Default Search API Key" && name !== "Default Admin API Key") { + await ms.deleteKey(uid); + } - expect(deletedKey).toBeUndefined(); - }); - }, -); + assert.isKeyView(keyView); + } -describe.each([{ permission: "Search" }])( - "Test on keys with search key", - ({ permission }) => { - test(`${permission} key: get keys denied`, async () => { - const client = await getClient(permission); - await expect(client.getKeys()).rejects.toHaveProperty( - "cause.code", - ErrorStatusCode.INVALID_API_KEY, - ); - }); + assert.lengthOf(Object.keys(keyList), 4); + const { results, offset, limit, total } = keyList; - test(`${permission} key: create key denied`, async () => { - const client = await getClient(permission); - await expect( - client.createKey({ - description: "Indexing Products API key", - actions: ["documents.add"], - indexes: ["products"], - expiresAt: null, - }), - ).rejects.toHaveProperty("cause.code", ErrorStatusCode.INVALID_API_KEY); - }); - }, -); - -describe.each([{ permission: "No" }])( - "Test on keys with No key", - ({ permission }) => { - test(`${permission} key: get keys denied`, async () => { - const client = await getClient(permission); - await expect(client.getKeys()).rejects.toHaveProperty( - "cause.code", - ErrorStatusCode.MISSING_AUTHORIZATION_HEADER, - ); - }); + for (const keyView of results) { + assert.isKeyView(keyView); + } - test(`${permission} key: create key denied`, async () => { - const client = await getClient(permission); - await expect( - client.createKey({ - description: "Indexing Products API key", - actions: ["documents.add"], - indexes: ["products"], - expiresAt: null, - }), - ).rejects.toHaveProperty( - "cause.code", - ErrorStatusCode.MISSING_AUTHORIZATION_HEADER, - ); - }); - }, -); + assert.typeOf(offset, "number"); + assert.typeOf(limit, "number"); + assert.typeOf(total, "number"); +}); diff --git a/tests/utils/meilisearch-test-utils.ts b/tests/utils/meilisearch-test-utils.ts index e9faf9510..48348b3d7 100644 --- a/tests/utils/meilisearch-test-utils.ts +++ b/tests/utils/meilisearch-test-utils.ts @@ -244,7 +244,18 @@ export type Book = { author: string; }; +function objectKeys(o: { [TKey in T]: null }): T[] { + return Object.keys(o) as T[]; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const objectEntries = Object.entries as >( + o: T, +) => [key: keyof T, val: T[keyof T]][]; + export { + objectKeys, + objectEntries, clearAllIndexes, config, masterClient, From 6a01288237af7c69c9a6ba35fff18c1cddaf82b6 Mon Sep 17 00:00:00 2001 From: "F. Levi" <55688616+flevi29@users.noreply.github.com> Date: Tue, 13 May 2025 11:38:15 +0300 Subject: [PATCH 5/6] Test update method as well --- src/types/keys.ts | 5 +---- tests/keys.test.ts | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/types/keys.ts b/src/types/keys.ts index 82ce256e2..04a8a64bc 100644 --- a/src/types/keys.ts +++ b/src/types/keys.ts @@ -82,7 +82,4 @@ export type KeyViewList = PaginationView; * * @see `meilisearch_types::keys::PatchApiKey` */ -export type PatchApiKey = { - description?: string | null; - name?: string | null; -}; +export type PatchApiKey = Pick; diff --git a/tests/keys.test.ts b/tests/keys.test.ts index 4c9530dc6..c0e186a73 100644 --- a/tests/keys.test.ts +++ b/tests/keys.test.ts @@ -62,6 +62,8 @@ const assert: typeof extAssert & typeof customAssert = Object.assign( customAssert, ); +const KEY_UID = "bd2cbad1-6c5f-48e3-bb92-bc9961bc011e"; + type TestRecord = { [TKey in keyof CreateApiKey]-?: [ name: string | undefined, @@ -139,7 +141,7 @@ const testRecord = { uid: [ [ undefined, - "bd2cbad1-6c5f-48e3-bb92-bc9961bc011e", + KEY_UID, (a, b) => { assert.strictEqual(a, b); }, @@ -199,6 +201,24 @@ describe.for(objectEntries(testRecord))("`%s`", ([key, values]) => { ); }); +const pickedTestRecord = (() => { + const { name, description } = testRecord; + return { name, description }; +})(); + +describe.for(objectEntries(pickedTestRecord))("`%s`", ([key, values]) => { + test.for(values)( + `${ms.updateKey.name} method%s`, + async ([, value, assertion]) => { + const keyView = await ms.updateKey(KEY_UID, { [key]: value }); + + assert.isKeyView(keyView); + + assertion(keyView[key as keyof typeof keyView], value); + }, + ); +}); + test(`${ms.getKeys.name}, ${ms.getKey.name} and ${ms.deleteKey.name} methods`, async () => { const keyList = await ms.getKeys({ offset: 0, From 79c88e65af4b2c3adcd25f79f306ef91a29d4b99 Mon Sep 17 00:00:00 2001 From: "F. Levi" <55688616+flevi29@users.noreply.github.com> Date: Mon, 19 May 2025 20:53:12 +0300 Subject: [PATCH 6/6] Use randomUUID instead of fixed UUID --- tests/keys.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/keys.test.ts b/tests/keys.test.ts index c0e186a73..0d5de208a 100644 --- a/tests/keys.test.ts +++ b/tests/keys.test.ts @@ -1,3 +1,4 @@ +import { randomUUID } from "node:crypto"; import { describe, test } from "vitest"; import type { Action, @@ -62,7 +63,7 @@ const assert: typeof extAssert & typeof customAssert = Object.assign( customAssert, ); -const KEY_UID = "bd2cbad1-6c5f-48e3-bb92-bc9961bc011e"; +const KEY_UID = randomUUID(); type TestRecord = { [TKey in keyof CreateApiKey]-?: [