diff --git a/LICENSE b/LICENSE index 261eeb9e9..38ffdcdc5 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2024 Momento Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/client-sdk-nodejs/package.json b/packages/client-sdk-nodejs/package.json index be8d7ef7e..e5fbbf62e 100644 --- a/packages/client-sdk-nodejs/package.json +++ b/packages/client-sdk-nodejs/package.json @@ -14,8 +14,9 @@ }, "scripts": { "prebuild": "eslint . --ext .ts", - "test": "jest --testPathIgnorePatterns auth-client.test.ts --maxWorkers 1", + "test": "jest --testPathIgnorePatterns=\"auth-client\\.test\\.ts|http-apis\\.test\\.ts\" --maxWorkers 1", "integration-test-auth": "jest auth/ --maxWorkers 1 -- useConsistentReads", + "integration-test-http": "jest http/ --maxWorkers 1 -- useConsistentReads", "integration-test-cache": "jest cache/ --maxWorkers 1 -- useConsistentReads", "integration-test-control-cache-topics": "npm run integration-test-cache && npm run integration-test-topics", "integration-test-leaderboard": "jest leaderboard/ --maxWorkers 1 -- useConsistentReads", @@ -26,7 +27,7 @@ "integration-test": "jest integration --maxWorkers 1", "unit-test": "jest unit", "build-deps": "cd ../core && npm run build && cd - && cd ../common-integration-tests && npm run build && cd -", - "build-and-run-tests": "npm run build-deps && jest --testPathIgnorePatterns auth-client.test.ts --maxWorkers 1", + "build-and-run-tests": "npm run build-deps && jest --testPathIgnorePatterns=\"auth-client\\.test\\.ts|http-apis\\.test\\.ts\" --maxWorkers 1", "lint": "eslint . --ext .ts", "format": "eslint . --ext .ts --fix", "watch": "tsc -w", diff --git a/packages/client-sdk-nodejs/test/integration/integration-setup.ts b/packages/client-sdk-nodejs/test/integration/integration-setup.ts index ffc87ad43..256c0215b 100644 --- a/packages/client-sdk-nodejs/test/integration/integration-setup.ts +++ b/packages/client-sdk-nodejs/test/integration/integration-setup.ts @@ -204,6 +204,7 @@ export function SetupIntegrationTest(): { cacheClientWithBalancedReadConcern: CacheClient; cacheClientWithConsistentReadConcern: CacheClient; integrationTestCacheName: string; + credentialProvider: CredentialProvider; } { const cacheName = testCacheName(); @@ -238,6 +239,7 @@ export function SetupIntegrationTest(): { cacheClientWithBalancedReadConcern: clientWithBalancedReadConcern, cacheClientWithConsistentReadConcern: clientWithConsistentReadConcern, integrationTestCacheName: cacheName, + credentialProvider: credsProvider(), }; } diff --git a/packages/client-sdk-nodejs/test/integration/shared/http/http-apis.test.ts b/packages/client-sdk-nodejs/test/integration/shared/http/http-apis.test.ts new file mode 100644 index 000000000..6ea3e29ce --- /dev/null +++ b/packages/client-sdk-nodejs/test/integration/shared/http/http-apis.test.ts @@ -0,0 +1,6 @@ +import {SetupIntegrationTest} from '../../integration-setup'; +import {runHttpApiTest} from '@gomomento/common-integration-tests/dist/src/http/http-apis'; + +const {credentialProvider, integrationTestCacheName} = SetupIntegrationTest(); + +runHttpApiTest(credentialProvider, integrationTestCacheName); diff --git a/packages/client-sdk-web/package.json b/packages/client-sdk-web/package.json index 4ec70ab64..c2ae5375c 100644 --- a/packages/client-sdk-web/package.json +++ b/packages/client-sdk-web/package.json @@ -14,9 +14,10 @@ }, "scripts": { "prebuild": "eslint . --ext .ts", - "test": "jest --testPathIgnorePatterns auth-client.test.ts --maxWorkers 1", + "test": "jest --testPathIgnorePatterns=\"auth-client\\.test\\.ts|http-apis\\.test\\.ts\" --maxWorkers 1", "unit-test": "jest unit", "integration-test-auth": "jest --env=jsdom auth/ --maxWorkers 1 -- useConsistentReads", + "integration-test-http": "jest --env=jsdom http/ --maxWorkers 1 -- useConsistentReads", "integration-test-cache": "jest --env=jsdom cache/ --maxWorkers 1 -- useConsistentReads", "integration-test-control-cache-topics": "npm run integration-test-cache && npm run integration-test-topics", "integration-test-leaderboard": "jest --env=jsdom leaderboard/ --maxWorkers 1 -- useConsistentReads", @@ -30,7 +31,7 @@ "integration-test": "npm run integration-test-happy-dom && npm run integration-test-jsdom", "integration-test-consistent-reads": "npm run integration-test-happy-dom-consistent-reads && npm run integration-test-jsdom-consistent-reads", "build-deps": "cd ../core && npm run build && cd - && cd ../common-integration-tests && npm run build && cd -", - "build-and-run-tests": "npm run build-deps && jest --testPathIgnorePatterns auth-client.test.ts --maxWorkers 1", + "build-and-run-tests": "npm run build-deps && jest --testPathIgnorePatterns=\"auth-client\\.test\\.ts|http-apis\\.test\\.ts\" --maxWorkers 1", "lint": "eslint . --ext .ts", "format": "eslint . --ext .ts --fix", "watch": "tsc -w", diff --git a/packages/client-sdk-web/test/integration/integration-setup.ts b/packages/client-sdk-web/test/integration/integration-setup.ts index e5761b533..4938febe5 100644 --- a/packages/client-sdk-web/test/integration/integration-setup.ts +++ b/packages/client-sdk-web/test/integration/integration-setup.ts @@ -193,6 +193,7 @@ export function SetupIntegrationTest(): { cacheClientWithBalancedReadConcern: CacheClient; cacheClientWithConsistentReadConcern: CacheClient; integrationTestCacheName: string; + credentialProvider: CredentialProvider; } { const cacheName = testCacheName(); @@ -224,6 +225,7 @@ export function SetupIntegrationTest(): { cacheClientWithBalancedReadConcern: clientWithBalancedReadConcern, cacheClientWithConsistentReadConcern: clientWithConsistentReadConcern, integrationTestCacheName: cacheName, + credentialProvider: credsProvider(), }; } diff --git a/packages/client-sdk-web/test/integration/shared/http/http-api.test.ts b/packages/client-sdk-web/test/integration/shared/http/http-api.test.ts new file mode 100644 index 000000000..6ea3e29ce --- /dev/null +++ b/packages/client-sdk-web/test/integration/shared/http/http-api.test.ts @@ -0,0 +1,6 @@ +import {SetupIntegrationTest} from '../../integration-setup'; +import {runHttpApiTest} from '@gomomento/common-integration-tests/dist/src/http/http-apis'; + +const {credentialProvider, integrationTestCacheName} = SetupIntegrationTest(); + +runHttpApiTest(credentialProvider, integrationTestCacheName); diff --git a/packages/common-integration-tests/src/http/http-apis.ts b/packages/common-integration-tests/src/http/http-apis.ts new file mode 100644 index 000000000..3b17dd21a --- /dev/null +++ b/packages/common-integration-tests/src/http/http-apis.ts @@ -0,0 +1,206 @@ +import {CredentialProvider} from '@gomomento/sdk-core'; +import * as https from 'https'; +import {v4} from 'uuid'; + +function makeRequest( + method: string, + hostname: string, + path: string, + headers: Record, + body?: string +): Promise<{ + statusCode: number; + statusMessage: string | undefined; + body: string; +}> { + return new Promise((resolve, reject) => { + const options = { + hostname, + path, + method, + headers, + }; + + const req = https.request(options, res => { + let data = ''; + + res.on('data', chunk => { + data += chunk; + }); + + res.on('end', () => { + resolve({ + statusCode: res.statusCode || 0, + statusMessage: res.statusMessage, + body: data, + }); + }); + }); + + req.on('error', reject); + + if (body) { + req.write(body); + } + + req.end(); + }); +} + +function putValue( + baseUrl: string, + apiKey: string, + cacheName: string, + key: string, + value: string, + ttl: number +): Promise<{statusCode: number; statusMessage: string | undefined}> { + const path = `/cache/${encodeValue(cacheName)}?key=${encodeValue( + key + )}&value=${encodeValue(value)}&ttl_seconds=${ttl}`; + const headers = { + Authorization: apiKey, + 'Content-Type': 'application/json', + }; + + return makeRequest('PUT', baseUrl, path, headers, value); +} + +function getValue( + baseUrl: string, + apiKey: string, + cacheName: string, + key: string +): Promise<{ + statusCode: number; + statusMessage: string | undefined; + body: string; +}> { + const path = `/cache/${encodeValue(cacheName)}?key=${encodeValue(key)}`; + const headers = { + Authorization: apiKey, + }; + + return makeRequest('GET', baseUrl, path, headers); +} + +function deleteValue( + baseUrl: string, + apiKey: string, + cacheName: string, + key: string +): Promise<{statusCode: number; statusMessage: string | undefined}> { + const path = `/cache/${encodeValue(cacheName)}?key=${encodeValue(key)}`; + const headers = { + Authorization: apiKey, + }; + + return makeRequest('DELETE', baseUrl, path, headers); +} + +function encodeValue(value: string): string { + return encodeURIComponent(value); +} + +export function runHttpApiTest( + credentialProvider: CredentialProvider, + cacheName: string +) { + describe('Momento HTTP API', () => { + let apiKey: string; + let baseUrl: string; + let cacheEndpoint: string; + + beforeAll(() => { + cacheEndpoint = credentialProvider.getCacheEndpoint(); + apiKey = credentialProvider.getAuthToken(); + baseUrl = `api.${cacheEndpoint}`; + }); + + it('should return error on non-existing cache', async () => { + const key = v4(); + const value = v4(); + const ttl = 300; + + const nonExistentCache = v4(); + + // PUT API + const putRes = await putValue( + baseUrl, + apiKey, + nonExistentCache, + key, + value, + ttl + ); + expect(putRes.statusCode).toBe(404); + expect(putRes.statusMessage?.toLowerCase()).toBe('not found'); + + // GET API + const getRes = await getValue(baseUrl, apiKey, nonExistentCache, key); + expect(getRes.statusCode).toBe(404); + expect(getRes.statusMessage?.toLowerCase()).toBe('not found'); + + // DELETE API + const delRes = await deleteValue(baseUrl, apiKey, nonExistentCache, key); + expect(delRes.statusCode).toBe(404); + expect(delRes.statusMessage?.toLowerCase()).toBe('not found'); + }); + + it('should successfully PUT and GET a value from a cache', async () => { + const key = v4(); + const value = v4(); + const ttl = 300; + + // Use PUT API to set the string value + const putRes = await putValue( + baseUrl, + apiKey, + cacheName, + key, + value, + ttl + ); + expect(putRes.statusCode).toBe(204); + + // Use GET API to retrieve the value + const getRes = await getValue(baseUrl, apiKey, cacheName, key); + expect(getRes.statusCode).toBe(200); + expect(getRes.body).toBe(value); + }); + + it('should return error on GET on non-existing key', async () => { + // Use GET API to retrieve the value that was not set + const getRes2 = await getValue(baseUrl, apiKey, cacheName, v4()); + expect(getRes2.statusCode).toBe(404); + expect(getRes2.statusMessage?.toLowerCase()).toBe('not found'); + }); + + it('should successfully DELETE a value from a cache', async () => { + const key = v4(); + const value = v4(); + const ttl = 300; + + // Use PUT API to set the value + const putRes = await putValue( + baseUrl, + apiKey, + cacheName, + key, + value, + ttl + ); + expect(putRes.statusCode).toBe(204); + + // Use DELETE API to delete the value that was set + const delRes = await deleteValue(baseUrl, apiKey, cacheName, key); + expect(delRes.statusCode).toBe(204); + }); + + it('should return success on DELETE on non-existing key', async () => { + // Use DELETE API to delete the value that was not set + const delRes2 = await deleteValue(baseUrl, apiKey, cacheName, v4()); + expect(delRes2.statusCode).toBe(204); + }); + }); +}