Using graphql-yoga I am attempting to write jest tests to cover subscriptions.
I am able to successfully test the happy path were a subscription works (with auth). Unfortunately I am not able to test the situation where a subscription websocket connection is rejected.
In my server setup I reject any websocket connections that do not pass my auth criteria:
const app = await server.start({
cors,
port: process.env.NODE_ENV === "test" ? 0 : 4000,
subscriptions: {
path: "/",
onConnect: async (connectionParams: any) => {
const token = connectionParams.token;
if (!token) {
throw new AssertionError({ message: "NO TOKEN PRESENT" });
}
const decoded = parseToken(token, process.env.JWT_SECRET as string);
const user = await validateTokenVersion(decoded, redis);
if (user === {}) {
throw new AssertionError({ message: "NO VALID USER" });
}
return { user };
}
}
});
Now in my relevant tests: https://github.com/jakelowen/typescript-graphql-boilerplate-server/blob/master/src/modules/counter/counter.test.ts
The first test (the happy path) passes as I would hope:
// works as expected.
test("should start a subscription on network interface and unsubscribe", async done => {
const client = new TestClientApollo(process.env.TEST_HOST as string);
await client.register(email, password);
await User.update({ email }, { confirmed: true });
await client.login(email, password);
// set up subscription listener
const sub = client.client.subscribe(defaultOptions).subscribe({
next(result) {
expect(result).toEqual({
data: {
counter: {
count: 0
}
}
});
sub.unsubscribe();
done();
}
});
});
Then I try 3 different ways to catch the expection I expect to see in unauthed scenarios. None of these tests pass as I would hope:
// Does not work! I am expecting an error.
test("Unauthed subscriptions are rejected", done => {
const client = new TestClientApollo(process.env.TEST_HOST as string);
const sub = client.client.subscribe(defaultOptions).subscribe({
next(result) {
expect(result).toEqual({
data: {
counter: {
count: 0
}
}
});
sub.unsubscribe();
done();
}
});
// Received value must be a function, but instead "object" was found
expect(sub).toThrow();
});
// does not work
// Error: Uncaught { message: 'NO TOKEN PRESENT' }
test("Unauthed subscriptions are rejected second attempt", done => {
const client = new TestClientApollo(process.env.TEST_HOST as string);
try {
const sub = client.client.subscribe(defaultOptions).subscribe({
next(result) {
expect(result).toEqual({
data: {
counter: {
count: 0
}
}
});
sub.unsubscribe();
// done();
}
});
} catch (error) {
console.log(error);
expect(error).toEqual({
message: "NO TOKEN PRESENT"
});
done();
}
});
// does not work
// Error: Uncaught { message: 'NO TOKEN PRESENT' }
test("Unauthed subscriptions are rejected second attempt", done => {
const client = new TestClientApollo(process.env.TEST_HOST as string);
try {
const sub = client.client.subscribe(defaultOptions).subscribe({
next(result) {
expect(result).toEqual({
data: {
counter: {
count: 0
}
}
});
sub.unsubscribe();
// done();
}
});
} catch (error) {
console.log(error);
expect(error).toEqual({
message: "NO TOKEN PRESENT"
});
done();
}
});
// does not work
// Expected the function to throw an error.
// But it didn't throw anything.
test("Unauthed subscriptions are rejected third attempt", done => {
const client = new TestClientApollo(process.env.TEST_HOST as string);
expect(async () => {
const sub = await client.client.subscribe(defaultOptions).subscribe({
next(result) {
expect(result).toEqual({
data: {
counter: {
count: 0
}
}
});
sub.unsubscribe();
done();
}
});
}).toThrowError();
});
// does not work
// Expected the function to throw an error.
// But it didn't throw anything.
test("Unauthed subscriptions are rejected fourth attempt", done => {
const client = new TestClientApollo(process.env.TEST_HOST as string);
const attempt = async () => {
const sub = await client.client.subscribe(defaultOptions).subscribe({
next(result) {
expect(result).toEqual({
data: {
counter: {
count: 0
}
}
});
sub.unsubscribe();
done();
}
});
};
expect(attempt).toThrowError();
});
Any idea how to expect the assertion error I am expecting in a test for the unauthed scenario?
full repo here: https://github.com/jakelowen/typescript-graphql-boilerplate-server
Booyah. I read up on observables in general and observable.subscribe() in particular and discovered that the second optional parameter is an onError callback function. Refactoring the test to:
test("Unauthed subscriptions are rejected", done => {
const client = new TestClientApollo(process.env.TEST_HOST as string);
// jest.setTimeout(1000); // increase timeout
client.client.subscribe(defaultOptions).subscribe(
res => {
console.log(res);
},
err => {
expect(err).toEqual({ message: "NO TOKEN PRESENT" });
done();
}
);
});
And all works as expected. Hooray!
Side editorial: It is a bit amazing to me that after dozens of hours of google, stack overflow, and github searches that I never found a simple, clear cut tutorial on how to properly test graphql subscriptions.