Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

shared undici mock (cont.) #552

Merged
merged 4 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 16 additions & 37 deletions test/deploy-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {Logger} from "../src/logger.js";
import {commandRequiresAuthenticationMessage} from "../src/observableApiAuth.js";
import type {DeployConfig} from "../src/observableApiConfig.js";
import {MockLogger} from "./mocks/logger.js";
import {ObservableApiMock} from "./mocks/observableApi.js";
import {getCurentObservableApi, mockObservableApi} from "./mocks/observableApi.js";
import {invalidApiKey, validApiKey} from "./mocks/observableApi.js";

// These files are implicitly generated by the CLI. This may change over time,
Expand Down Expand Up @@ -109,11 +109,13 @@ const TEST_CONFIG = await normalizeConfig({

// TODO These tests need mockJsDelivr, too!
describe("deploy", () => {
mockObservableApi();

it("makes expected API calls for an existing project", async () => {
const projectId = "project123";
const deployConfig = {projectId};
const deployId = "deploy456";
const apiMock = new ObservableApiMock()
getCurentObservableApi()
.handleGetProject({
workspaceLogin: TEST_CONFIG.deploy!.workspace,
projectSlug: TEST_CONFIG.deploy!.project,
Expand All @@ -127,15 +129,14 @@ describe("deploy", () => {
const effects = new MockDeployEffects({deployConfig}).addIoResponse(/^Deploy message: /, "fix some bugs");
await deploy({config: TEST_CONFIG}, effects);

apiMock.close();
effects.close();
});

it("makes expected API calls for non-existent project, user chooses to create", async () => {
const projectId = "project123";
const deployConfig = {projectId};
const deployId = "deploy456";
const apiMock = new ObservableApiMock()
getCurentObservableApi()
.handleGetProject({
workspaceLogin: TEST_CONFIG.deploy!.workspace,
projectSlug: TEST_CONFIG.deploy!.project,
Expand All @@ -155,14 +156,13 @@ describe("deploy", () => {

await deploy({config: TEST_CONFIG}, effects);

apiMock.close();
effects.close();
});

it("makes expected API calls for non-existent project, user chooses not to create", async () => {
const projectId = "project123";
const deployConfig = {projectId};
const apiMock = new ObservableApiMock()
getCurentObservableApi()
.handleGetProject({
workspaceLogin: TEST_CONFIG.deploy!.workspace,
projectSlug: TEST_CONFIG.deploy!.project,
Expand All @@ -183,14 +183,13 @@ describe("deploy", () => {
CliError.assert(error, {message: "User cancelled deploy.", print: false, exitCode: 2});
}

apiMock.close();
effects.close();
});

it("makes expected API calls for non-existent project, non-interactive", async () => {
const projectId = "project123";
const deployConfig = {projectId};
const apiMock = new ObservableApiMock()
getCurentObservableApi()
.handleGetProject({
workspaceLogin: TEST_CONFIG.deploy!.workspace,
projectSlug: TEST_CONFIG.deploy!.project,
Expand All @@ -208,7 +207,6 @@ describe("deploy", () => {
CliError.assert(error, {message: "Cancelling deploy due to non-existent project."});
}

apiMock.close();
effects.close();
});

Expand All @@ -220,7 +218,7 @@ describe("deploy", () => {
// no title!
deploy: {workspace: "mock-user-ws", project: "bi"}
});
const apiMock = new ObservableApiMock()
getCurentObservableApi()
.handleGetProject({
workspaceLogin: config.deploy!.workspace,
projectSlug: config.deploy!.project,
Expand All @@ -241,7 +239,6 @@ describe("deploy", () => {
CliError.assert(err, {message: /You haven't configured a project title/});
}

apiMock.close();
effects.close();
});

Expand All @@ -253,7 +250,7 @@ describe("deploy", () => {
title: "Some title",
deploy: {workspace: "super-ws-123", project: "bi"}
});
const apiMock = new ObservableApiMock()
getCurentObservableApi()
.handleGetProject({
workspaceLogin: config.deploy!.workspace,
projectSlug: config.deploy!.project,
Expand All @@ -275,7 +272,6 @@ describe("deploy", () => {
CliError.assert(err, {message: /Workspace super-ws-123 not found/});
}

apiMock.close();
effects.close();
});

Expand All @@ -284,7 +280,6 @@ describe("deploy", () => {
root: TEST_SOURCE_ROOT,
deploy: {workspace: "ACME Inc.", project: "bi"}
});
const apiMock = new ObservableApiMock().start();
const effects = new MockDeployEffects({isTty: true});

try {
Expand All @@ -294,7 +289,6 @@ describe("deploy", () => {
CliError.assert(err, {message: /"ACME Inc.".*isn't valid.*"acme-inc"/});
}

apiMock.close();
effects.close();
});

Expand All @@ -303,7 +297,6 @@ describe("deploy", () => {
root: TEST_SOURCE_ROOT,
deploy: {workspace: "acme", project: "Business Intelligence"}
});
const apiMock = new ObservableApiMock().start();
const effects = new MockDeployEffects({isTty: true});

try {
Expand All @@ -313,12 +306,10 @@ describe("deploy", () => {
CliError.assert(err, {message: /"Business Intelligence".*isn't valid.*"business-intelligence"/});
}

apiMock.close();
effects.close();
});

it("shows message for missing API key", async () => {
const apiMock = new ObservableApiMock().start();
const effects = new MockDeployEffects({apiKey: null});

try {
Expand All @@ -329,12 +320,10 @@ describe("deploy", () => {
assert.equal(err.message, "no key available in this test");
effects.logger.assertExactLogs([/^You need to be authenticated/]);
}

apiMock.close();
});

it("throws an error with an invalid API key", async () => {
const apiMock = new ObservableApiMock()
getCurentObservableApi()
.handleGetProject({
workspaceLogin: TEST_CONFIG.deploy!.workspace,
projectSlug: TEST_CONFIG.deploy!.project,
Expand All @@ -350,14 +339,12 @@ describe("deploy", () => {
assert.ok(isHttpError(error));
assert.equal(error.statusCode, 401);
}

apiMock.close();
});

it("throws an error if deploy creation fails", async () => {
const projectId = "project123";
const deployId = "deploy456";
const apiMock = new ObservableApiMock()
getCurentObservableApi()
.handleGetProject({
workspaceLogin: TEST_CONFIG.deploy!.workspace,
projectSlug: TEST_CONFIG.deploy!.project,
Expand All @@ -375,14 +362,13 @@ describe("deploy", () => {
assert.equal(error.statusCode, 500);
}

apiMock.close();
effects.close();
});

it("throws an error if file upload fails", async () => {
const projectId = "project123";
const deployId = "deploy456";
const apiMock = new ObservableApiMock()
getCurentObservableApi()
.handleGetProject({
workspaceLogin: TEST_CONFIG.deploy!.workspace,
projectSlug: TEST_CONFIG.deploy!.project,
Expand All @@ -401,14 +387,13 @@ describe("deploy", () => {
assert.equal(error.statusCode, 500);
}

apiMock.close();
effects.close();
});

it("throws an error if deploy uploaded fails", async () => {
const projectId = "project123";
const deployId = "deploy456";
const apiMock = new ObservableApiMock()
getCurentObservableApi()
.handleGetProject({
workspaceLogin: TEST_CONFIG.deploy!.workspace,
projectSlug: TEST_CONFIG.deploy!.project,
Expand All @@ -428,28 +413,25 @@ describe("deploy", () => {
assert.equal(error.statusCode, 500);
}

apiMock.close();
effects.close();
});

it("throws an error when a deploy target is not configured", async () => {
const config = {...TEST_CONFIG, deploy: null};
const apiMock = new ObservableApiMock().start();
const effects = new MockDeployEffects();
try {
await deploy({config}, effects);
assert.fail("expected error");
} catch (err) {
CliError.assert(err, {message: /You haven't configured a project to deploy to/});
}
apiMock.close();
});

describe("when deploy state doesn't match", () => {
it("interactive, when the user chooses to update", async () => {
const newProjectId = "newId";
const deployId = "deploy";
const apiMock = new ObservableApiMock()
getCurentObservableApi()
.handleGetProject({
workspaceLogin: TEST_CONFIG.deploy!.workspace,
projectSlug: TEST_CONFIG.deploy!.project,
Expand All @@ -464,13 +446,12 @@ describe("deploy", () => {
.addIoResponse(/^Deploy message: /, "deploying to re-created project");
await deploy({config: TEST_CONFIG}, effects);
effects.logger.assertExactLogs([/^This project was last deployed/]);
apiMock.close();
effects.close();
});

it("interactive, when the user chooses not to update", async () => {
const newProjectId = "newId";
const apiMock = new ObservableApiMock()
getCurentObservableApi()
.handleGetProject({
workspaceLogin: TEST_CONFIG.deploy!.workspace,
projectSlug: TEST_CONFIG.deploy!.project,
Expand All @@ -488,13 +469,12 @@ describe("deploy", () => {
CliError.assert(error, {message: "User cancelled deploy.", print: false, exitCode: 2});
}
effects.logger.assertExactLogs([/^This project was last deployed/]);
apiMock.close();
effects.close();
});

it("non-interactive", async () => {
const newProjectId = "newId";
const apiMock = new ObservableApiMock()
getCurentObservableApi()
.handleGetProject({
workspaceLogin: TEST_CONFIG.deploy!.workspace,
projectSlug: TEST_CONFIG.deploy!.project,
Expand All @@ -509,7 +489,6 @@ describe("deploy", () => {
CliError.assert(error, {message: "Cancelling deploy due to misconfiguration."});
}
effects.logger.assertExactLogs([/^This project was last deployed/]);
apiMock.close();
});
});
});
14 changes: 3 additions & 11 deletions test/mocks/jsdelivr.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {type Dispatcher, MockAgent, getGlobalDispatcher, setGlobalDispatcher} from "undici";
import {getCurrentAgent, mockAgent} from "./undici.js";

const packages: [name: string, version: string][] = [
["@duckdb/duckdb-wasm", "1.28.0"],
Expand All @@ -23,12 +23,9 @@ const packages: [name: string, version: string][] = [
];

export function mockJsDelivr() {
let globalDispatcher: Dispatcher;

mockAgent();
before(async () => {
globalDispatcher = getGlobalDispatcher();
const agent = new MockAgent();
agent.disableNetConnect();
const agent = getCurrentAgent();
const dataClient = agent.get("https://data.jsdelivr.com");
for (const [name, version] of packages) {
dataClient
Expand All @@ -41,10 +38,5 @@ export function mockJsDelivr() {
.intercept({path: `/npm/${name}@${version}/+esm`, method: "GET"})
.reply(200, "", {headers: {"cache-control": "public, immutable", "content-type": "text/javascript; charset=utf-8"}}); // prettier-ignore
}
setGlobalDispatcher(agent);
});

after(async () => {
setGlobalDispatcher(globalDispatcher!);
});
}
57 changes: 40 additions & 17 deletions test/mocks/observableApi.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,58 @@
import {type Dispatcher, type Interceptable, MockAgent, getGlobalDispatcher, setGlobalDispatcher} from "undici";
import type {MockAgent} from "undici";
import {type Interceptable} from "undici";
import {getObservableApiOrigin, getObservableUiOrigin} from "../../src/observableApiClient.js";
import {getCurrentAgent, mockAgent} from "./undici.js";

export const validApiKey = "MOCK-VALID-KEY";
export const invalidApiKey = "MOCK-INVALID-KEY";

const emptyErrorBody = JSON.stringify({errors: []});

export class ObservableApiMock {
let apiMock;

export function mockObservableApi() {
mockAgent();

beforeEach(() => {
apiMock = new ObservableApiMock();
const agent = getCurrentAgent();
agent.get(getOrigin());
});

afterEach(() => {
apiMock.after();
});
}

export function getCurentObservableApi() {
if (!apiMock) throw new Error("mockObservableApi not initialized");
return apiMock;
}

function getOrigin() {
return getObservableApiOrigin().toString().replace(/\/$/, "");
}

class ObservableApiMock {
private _agent: MockAgent | null = null;
private _handlers: ((pool: Interceptable) => void)[] = [];
private _originalDispatcher: Dispatcher | null = null;

public start(): ObservableApiMock {
this._agent = new MockAgent();
this._agent.disableNetConnect();
const origin = getObservableApiOrigin().toString().replace(/\/$/, "");
const mockPool = this._agent.get(origin);
this._agent = getCurrentAgent();
const mockPool = this._agent.get(getOrigin());
for (const handler of this._handlers) handler(mockPool);
this._originalDispatcher = getGlobalDispatcher();
setGlobalDispatcher(this._agent);
return this;
}

public close() {
if (!this._agent) throw new Error("ObservableApiMock not started");
this._agent.assertNoPendingInterceptors();
this._agent.close();
this._agent = null;
if (this._originalDispatcher) {
setGlobalDispatcher(this._originalDispatcher);
this._originalDispatcher = null;
public after() {
const agent = getCurrentAgent();
for (const intercept of agent.pendingInterceptors()) {
if (intercept.origin === getOrigin()) {
console.log(`Expected All intercepts for ${getOrigin()} to be handled`);
// This will include other interceptors that are not related to the
// Observable API, but it has a nice output.
agent.assertNoPendingInterceptors();
}
}
}

Expand Down
Loading