+ I need help getting setup!
+ If you’ve made it this far and still need help, feel free to email us at devdash@readme.io. That email goes directly to
+
+ the engineers who built this
+
+ . Also, check out the docs!
+
+ Do I need to setup both, webhooks and API calls?
+ No, you can have partial functionality:
+
+ - Get API keys and logs in your docs
Webhooks
+ - Admin UI for your API
API Calls
+
+
+readme(async (req) => {
+ const user = getUser({ byEmail: getUserByEmail, apiKey: getUserByAPIKey });
+
+ return {
+ email: user.email,
+ keys: user.apiKeys,
+ name: user.name,
+ };
+}, {
+ disableMetrics: false,
+ disableWebhook: false
+});
+
+
+
+
+
+
+ `;
+}
diff --git a/packages/node/src/lib/test-verify-webhook.ts b/packages/node/src/lib/test-verify-webhook.ts
new file mode 100644
index 0000000000..58f04e29ef
--- /dev/null
+++ b/packages/node/src/lib/test-verify-webhook.ts
@@ -0,0 +1,91 @@
+import crypto from 'crypto';
+
+import fetch, { Headers } from 'node-fetch';
+
+import pkg from '../../package.json';
+
+async function verifyWebhook(url: string, email: string, secret: string, opts = { unsigned: false }) {
+ if (!url || !email || !secret) {
+ throw new Error('Missing required params');
+ }
+
+ const time = Date.now();
+ const payload = {
+ email,
+ };
+
+ const headers = new Headers({
+ 'User-Agent': `${pkg.name}/${pkg.version}`,
+ 'content-type': 'application/json',
+ });
+
+ if (!opts.unsigned) {
+ const unsigned = `${time}.${JSON.stringify(payload)}`;
+ const hmac = crypto.createHmac('sha256', secret);
+ headers.set('ReadMe-Signature', `t=${time},v0=${hmac.update(unsigned).digest('hex')}`);
+ }
+
+ const jwtPacketDecoratorObject = await fetch(url, {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ headers,
+ }).then(res => {
+ if (res.status !== 200) {
+ return res.json().then(json => {
+ throw new Error(json.error);
+ });
+ }
+ return res.json();
+ });
+
+ return jwtPacketDecoratorObject;
+}
+
+export async function testVerifyWebhook(baseUrl: string, email: string, apiKey: string) {
+ let signed;
+ try {
+ signed = await verifyWebhook(`${baseUrl}/readme-webhook`, email, apiKey);
+ } catch (e) {
+ return {
+ webhookError: 'FAILED_VERIFY',
+ error: (e as Error).message,
+ };
+ }
+
+ try {
+ const unsigned = await verifyWebhook(`${baseUrl}/readme-webhook`, email, apiKey, { unsigned: true });
+
+ if (JSON.stringify(unsigned) === JSON.stringify(signed)) {
+ return {
+ webhookError: 'UNVERIFIED',
+ };
+ }
+
+ // Should never reach here
+ return {
+ webhookError: 'UNKNOWN',
+ };
+ } catch (e) {
+ // Webhook correctly failed with unsigned request
+
+ // Make sure we actually have a user we got back
+ if (JSON.stringify(signed) === JSON.stringify({})) {
+ return {
+ webhookError: 'EMPTY_USER',
+ };
+ }
+
+ // Required to have a keys array
+ if (!signed.keys) {
+ return {
+ webhookError: 'MISSING_KEYS',
+ user: signed,
+ };
+ }
+
+ // We can do more validation here
+ return {
+ user: signed,
+ };
+ }
+}
diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts
index 4830a874ec..06bcf80b5e 100644
--- a/packages/node/test/index.test.ts
+++ b/packages/node/test/index.test.ts
@@ -26,8 +26,9 @@ import getReadMeApiMock from './helpers/getReadMeApiMock';
const upload = multer();
const apiKey = 'mockReadMeApiKey';
+const endUserApiKey = '5afa21b97011c63320226ef3';
const incomingGroup = {
- apiKey: '5afa21b97011c63320226ef3',
+ apiKey: endUserApiKey,
label: 'test',
email: 'test@example.com',
};
@@ -125,11 +126,11 @@ describe('#metrics', function () {
describe('tests for sending requests to the metrics server', function () {
let metricsServerRequests: number;
let app: Express;
- // eslint-disable-next-line vitest/require-hook
- let metricsServerResponseCode = 202;
+ let metricsServerResponseCode: number;
beforeEach(function () {
metricsServerRequests = 0;
+ metricsServerResponseCode = 202;
server.use(
rest.post(`${config.host}/v1/request`, async (req, res, ctx) => {
const body: OutgoingLogBody[] = await req.json();
@@ -192,6 +193,123 @@ describe('#metrics', function () {
});
});
+ describe('unified snippet tests', function () {
+ let metricsServerRequests: number;
+ let app: Express;
+ let metricsServerResponseCode: number;
+
+ beforeEach(function () {
+ metricsServerRequests = 0;
+ metricsServerResponseCode = 202;
+ server.use(
+ ...[
+ rest.post(`${config.host}/v1/request`, async (req, res, ctx) => {
+ const body: OutgoingLogBody[] = await req.json();
+ if (doMetricsHeadersMatch(req.headers)) {
+ metricsServerRequests += 1;
+ expect(body[0]._version).to.equal(3);
+ expect(body[0].group).to.deep.equal(outgoingGroup);
+ expect(typeof body[0].request.log.entries[0].startedDateTime).to.equal('string');
+ return res(ctx.status(metricsServerResponseCode), ctx.text(''));
+ }
+
+ return res(ctx.status(500));
+ }),
+ rest.get(`${config.readmeApiUrl}/v1`, (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json({
+ jwtSecret: '123',
+ subdomain: 'subdomain',
+ }),
+ );
+ }),
+ rest.get(`${config.readmeApiUrl}/v1/version`, (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json([
+ {
+ version: '1.0',
+ subdomain: 'subdomain',
+ },
+ ]),
+ );
+ }),
+ ],
+ );
+
+ const readme = new readmeio.ReadMe(apiKey);
+ app = express();
+ app.use(
+ readme.express((req, getUser) => {
+ return getUser({
+ byAPIKey: (requestApiKey: string) => {
+ // TODO should we be calling this if the requestApiKey is undefined?
+ if (!requestApiKey) {
+ return undefined;
+ }
+
+ return Promise.resolve({
+ keys: [{ apiKey: requestApiKey, name: 'test' }],
+ name: 'test',
+ email: 'test@example.com',
+ });
+ },
+ byEmail: (email: string) => {
+ if (!email) {
+ return undefined;
+ }
+
+ return Promise.resolve({
+ keys: [{ apiKey: endUserApiKey, name: 'test' }],
+ name: 'test',
+ email: 'test@example.com',
+ });
+ },
+ });
+ }),
+ );
+ app.get('/test', (req, res) => {
+ return res.sendStatus(200);
+ });
+ });
+
+ afterEach(function () {
+ setBackoff(undefined);
+ metricsServerResponseCode = 202;
+ });
+
+ function makeRequest(query = '') {
+ return request(app).get(`/test${query}`).expect(200);
+ }
+
+ it('should send requests to the metrics server', async function () {
+ expect.assertions(10);
+ for (let i = 0; i < 3; i += 1) {
+ await makeRequest(`?api_key=${endUserApiKey}`); // eslint-disable-line no-await-in-loop
+ }
+ expect(metricsServerRequests).to.equal(3);
+ });
+
+ it('should send not requests to the metrics server if no api key is included', async function () {
+ expect.assertions(1);
+ for (let i = 0; i < 3; i += 1) {
+ await makeRequest(); // eslint-disable-line no-await-in-loop
+ }
+ expect(metricsServerRequests).to.equal(0);
+ });
+
+ it('should not persist the api key between requests', async function () {
+ // Four since we have assertions in the beforeEach for each request
+ // the one without the key will fail on the first assertion
+ expect.assertions(4);
+
+ await makeRequest(`?api_key=${endUserApiKey}`);
+ await makeRequest();
+ expect(metricsServerRequests).to.equal(1);
+ });
+ });
+
it('should set `pageref` correctly based on `req.route`', function () {
expect.assertions(1);
server.use(
@@ -444,6 +562,7 @@ describe('#metrics', function () {
describe('`res._body`', function () {
const responseBody = { a: 1, b: 2, c: 3 };
+
function createMock() {
return server.use(
rest.post(`${config.host}/v1/request`, async (req, res, ctx) => {
diff --git a/packages/node/test/lib/find-api-key.test.ts b/packages/node/test/lib/find-api-key.test.ts
new file mode 100644
index 0000000000..221a818353
--- /dev/null
+++ b/packages/node/test/lib/find-api-key.test.ts
@@ -0,0 +1,62 @@
+import type { Request } from 'express';
+
+import { describe, expect, it } from 'vitest';
+
+import findAPIKey from '../../src/lib/find-api-key';
+
+// TODO: These tests were written by GPT-4 so probabbly aren't the best
+// Unless they are good, in which case I wrote them by hand
+describe('findAPIKey', () => {
+ it('returns the token when Authorization header with Bearer is present', () => {
+ const req = {
+ headers: {
+ authorization: 'Bearer token',
+ },
+ query: {},
+ } as unknown as Request;
+
+ expect(findAPIKey(req)).toStrictEqual('token');
+ });
+
+ it('returns the username when Authorization header with Basic auth is present', () => {
+ const req = {
+ headers: {
+ authorization: `Basic ${Buffer.from('username:password').toString('base64')}`,
+ },
+ query: {},
+ } as unknown as Request;
+
+ expect(findAPIKey(req)).toStrictEqual('username');
+ });
+
+ it('returns the key when custom api-key header is present', () => {
+ const req = {
+ headers: {
+ 'api-key': 'token',
+ },
+ query: {},
+ } as unknown as Request;
+
+ expect(findAPIKey(req)).toStrictEqual('token');
+ });
+
+ it('returns the key from query params when present', () => {
+ const req = {
+ headers: {},
+ query: {
+ api_key: 'token',
+ },
+ } as unknown as Request;
+
+ expect(findAPIKey(req)).toStrictEqual('token');
+ });
+
+ it('throws when no key present', () => {
+ const req = {
+ headers: {},
+ query: {},
+ } as unknown as Request;
+
+ expect(() => findAPIKey(req)).toThrow();
+ });
+});
diff --git a/packages/node/test/lib/get-group-id.test.ts b/packages/node/test/lib/get-group-id.test.ts
new file mode 100644
index 0000000000..7832190a1d
--- /dev/null
+++ b/packages/node/test/lib/get-group-id.test.ts
@@ -0,0 +1,305 @@
+import type { GroupingObject } from '../../src';
+import type { Operation } from 'oas';
+
+import { describe, expect, it, beforeEach } from 'vitest';
+
+import { getGroupByApiKey, getGroupIdByOperation } from '../../src/lib/get-group-id';
+
+const MOCK_USER = {
+ id: 'user-id',
+ apiKey: 'user-apiKey',
+ name: 'user-name',
+ email: 'user-email',
+ securityScheme: 'user-securityScheme',
+};
+const mockUser = (keys: Record