diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index c905b809684..50206fff96f 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -1895,7 +1895,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 90c1f443b49..bec302f529a 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -1674,7 +1674,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-react_components.api.md b/.api-reports/api-report-react_components.api.md index 02cf3ee426f..b1e1546be42 100644 --- a/.api-reports/api-report-react_components.api.md +++ b/.api-reports/api-report-react_components.api.md @@ -1488,7 +1488,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-react_context.api.md b/.api-reports/api-report-react_context.api.md index 3e841319c74..3ae10667777 100644 --- a/.api-reports/api-report-react_context.api.md +++ b/.api-reports/api-report-react_context.api.md @@ -1416,7 +1416,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-react_hoc.api.md b/.api-reports/api-report-react_hoc.api.md index 7ce2a0b5c7b..97ed9396214 100644 --- a/.api-reports/api-report-react_hoc.api.md +++ b/.api-reports/api-report-react_hoc.api.md @@ -1461,7 +1461,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index 75a5ddf0cba..6576d200b6b 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -1543,7 +1543,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-react_internal.api.md b/.api-reports/api-report-react_internal.api.md index 89eb10890d9..073b56afbcf 100644 --- a/.api-reports/api-report-react_internal.api.md +++ b/.api-reports/api-report-react_internal.api.md @@ -1594,7 +1594,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index 15c9ebcf378..1f470c15801 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -1401,7 +1401,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-testing.api.md b/.api-reports/api-report-testing.api.md index d8be3b90f9b..ccc35eb16f8 100644 --- a/.api-reports/api-report-testing.api.md +++ b/.api-reports/api-report-testing.api.md @@ -1482,7 +1482,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-testing_core.api.md b/.api-reports/api-report-testing_core.api.md index 65672566046..98adfa454f3 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -1439,7 +1439,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report-utilities.api.md b/.api-reports/api-report-utilities.api.md index 9b9718a06f6..30fa559078e 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -2209,7 +2209,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 974b50d78eb..25f7b5c0f5d 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -2246,7 +2246,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; diff --git a/.size-limits.json b/.size-limits.json index b2be55833cd..1b40dd3f3b0 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 41301, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 33987 + "dist/apollo-client.min.cjs": 41335, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34036 } diff --git a/src/__tests__/dataMasking.ts b/src/__tests__/dataMasking.ts index eeddd834379..e09350b43f8 100644 --- a/src/__tests__/dataMasking.ts +++ b/src/__tests__/dataMasking.ts @@ -14,10 +14,16 @@ import { Reference, TypedDocumentNode, } from "../core"; -import { MockLink } from "../testing"; +import { + MockedResponse, + MockLink, + MockSubscriptionLink, + wait, +} from "../testing"; import { ObservableStream, spyOnConsole } from "../testing/internal"; import { invariant } from "../utilities/globals"; import { createFragmentRegistry } from "../cache/inmemory/fragmentRegistry"; +import { isSubscriptionOperation } from "../utilities"; describe("client.watchQuery", () => { test("masks queries when dataMasking is `true`", async () => { @@ -2617,6 +2623,681 @@ describe("client.query", () => { }); }); +describe("client.subscribe", () => { + test("masks data returned from subscriptions when dataMasking is `true`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ query: subscription }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + }, + }); + }); + + test("does not mask data returned from subscriptions when dataMasking is `false`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: false, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ query: subscription }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }); + }); + + test("does not mask data returned from subscriptions by default", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ query: subscription }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }); + }); + + test("handles errors returned from the subscription when errorPolicy is `none`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ + query: subscription, + errorPolicy: "none", + }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: null, + }, + errors: [{ message: "Something went wrong" }], + }, + }); + + const error = await stream.takeError(); + + expect(error).toEqual( + new ApolloError({ graphQLErrors: [{ message: "Something went wrong" }] }) + ); + }); + + test("handles errors returned from the subscription when errorPolicy is `all`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ + query: subscription, + errorPolicy: "all", + }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: null, + }, + errors: [{ message: "Something went wrong" }], + }, + }); + + const { data, errors } = await stream.takeNext(); + + expect(data).toEqual({ addedComment: null }); + expect(errors).toEqual([{ message: "Something went wrong" }]); + }); + + test("masks partial data for errors returned from the subscription when errorPolicy is `all`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ + query: subscription, + errorPolicy: "all", + }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: null, + }, + }, + errors: [{ message: "Could not get author" }], + }, + }); + + const { data, errors } = await stream.takeNext(); + + expect(data).toEqual({ addedComment: { __typename: "Comment", id: 1 } }); + expect(errors).toEqual([{ message: "Could not get author" }]); + }); +}); + +describe("observableQuery.subscribeToMore", () => { + test("masks query data, does not mask updateQuery callback when dataMasking is `true`", async () => { + const fragment = gql` + fragment CommentFields on Comment { + comment + author + } + `; + + const query = gql` + query RecentCommentQuery { + recentComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { + data: { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + }, + }, + ]; + + const subscriptionLink = new MockSubscriptionLink(); + const link = ApolloLink.split( + (operation) => isSubscriptionOperation(operation.query), + subscriptionLink, + new MockLink(mocks) + ); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link, + }); + + const observable = client.watchQuery({ query }); + const queryStream = new ObservableStream(observable); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ recentComment: { __typename: "Comment", id: 1 } }); + } + + const updateQuery = jest.fn((_, { subscriptionData }) => { + return { recentComment: subscriptionData.data.addedComment }; + }); + + observable.subscribeToMore({ document: subscription, updateQuery }); + + subscriptionLink.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + }); + + await wait(0); + + expect(updateQuery).toHaveBeenLastCalledWith( + { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + { + variables: {}, + subscriptionData: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + } + ); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ recentComment: { __typename: "Comment", id: 2 } }); + } + }); + + test("does not mask data returned from subscriptions when dataMasking is `false`", async () => { + const fragment = gql` + fragment CommentFields on Comment { + comment + author + } + `; + + const query = gql` + query RecentCommentQuery { + recentComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { + data: { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + }, + }, + ]; + + const subscriptionLink = new MockSubscriptionLink(); + const link = ApolloLink.split( + (operation) => isSubscriptionOperation(operation.query), + subscriptionLink, + new MockLink(mocks) + ); + + const client = new ApolloClient({ + dataMasking: false, + cache: new InMemoryCache(), + link, + }); + + const observable = client.watchQuery({ query }); + const queryStream = new ObservableStream(observable); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }); + } + + const updateQuery = jest.fn((_, { subscriptionData }) => { + return { recentComment: subscriptionData.data.addedComment }; + }); + + observable.subscribeToMore({ document: subscription, updateQuery }); + + subscriptionLink.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + }); + + await wait(0); + + expect(updateQuery).toHaveBeenLastCalledWith( + { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + { + variables: {}, + subscriptionData: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + } + ); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ + recentComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }); + } + }); + + test("does not mask data by default", async () => { + const fragment = gql` + fragment CommentFields on Comment { + comment + author + } + `; + + const query = gql` + query RecentCommentQuery { + recentComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { + data: { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + }, + }, + ]; + + const subscriptionLink = new MockSubscriptionLink(); + const link = ApolloLink.split( + (operation) => isSubscriptionOperation(operation.query), + subscriptionLink, + new MockLink(mocks) + ); + + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const observable = client.watchQuery({ query }); + const queryStream = new ObservableStream(observable); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }); + } + + const updateQuery = jest.fn((_, { subscriptionData }) => { + return { recentComment: subscriptionData.data.addedComment }; + }); + + observable.subscribeToMore({ document: subscription, updateQuery }); + + subscriptionLink.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + }); + + await wait(0); + + expect(updateQuery).toHaveBeenLastCalledWith( + { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + { + variables: {}, + subscriptionData: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + } + ); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ + recentComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }); + } + }); +}); + class TestCache extends ApolloCache { public diff(query: Cache.DiffOptions): DataProxy.DiffResult { return {}; diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 68cfade9b7c..998286a1929 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -607,6 +607,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, query: options.document, variables: options.variables, context: options.context, + [Symbol.for("apollo.dataMasking")]: false, }) .subscribe({ next: (subscriptionData: { data: TSubscriptionData }) => { diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 23d1ddd78c8..36948a4c13f 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -994,16 +994,22 @@ export class QueryManager { this.getQuery(observableQuery.queryId).setObservableQuery(observableQuery); } - public startGraphQLSubscription({ - query, - fetchPolicy, - errorPolicy = "none", - variables, - context = {}, - extensions = {}, - }: SubscriptionOptions): Observable> { + public startGraphQLSubscription( + options: SubscriptionOptions + ): Observable> { + let { query, variables } = options; + const { + fetchPolicy, + errorPolicy = "none", + context = {}, + extensions = {}, + } = options; + query = this.transform(query); variables = this.getVariables(query, variables); + const dataMasking: boolean | undefined = (options as any)[ + Symbol.for("apollo.dataMasking") + ]; const makeObservable = (variables: OperationVariables) => this.getObservableFromLink(query, context, variables, extensions).map( @@ -1046,6 +1052,12 @@ export class QueryManager { delete result.errors; } + if (dataMasking !== false) + result.data = this.maskOperation({ + document: query, + data: result.data, + }); + return result; } ); diff --git a/src/react/hooks/__tests__/useSubscription.test.tsx b/src/react/hooks/__tests__/useSubscription.test.tsx index 0c9002638d1..ec9d33473f1 100644 --- a/src/react/hooks/__tests__/useSubscription.test.tsx +++ b/src/react/hooks/__tests__/useSubscription.test.tsx @@ -2060,6 +2060,315 @@ describe("ignoreResults", () => { }); }); +describe("data masking", () => { + test("masks data returned when dataMasking is `true`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + dataMasking: true, + cache: new Cache(), + link, + }); + + const ProfiledHook = profileHook(() => useSubscription(subscription)); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(true); + expect(data).toBeUndefined(); + expect(error).toBeUndefined(); + } + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(false); + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + }, + }); + expect(error).toBeUndefined(); + } + + await expect(ProfiledHook).not.toRerender(); + }); + + test("does not mask data returned from subscriptions when dataMasking is `false`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + dataMasking: false, + cache: new Cache(), + link, + }); + + const ProfiledHook = profileHook(() => useSubscription(subscription)); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(true); + expect(data).toBeUndefined(); + expect(error).toBeUndefined(); + } + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(false); + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }); + expect(error).toBeUndefined(); + } + + await expect(ProfiledHook).not.toRerender(); + }); + + test("masks data passed to onData callback when dataMasking is `true`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + dataMasking: true, + cache: new Cache(), + link, + }); + + const onData = jest.fn(); + const ProfiledHook = profileHook(() => + useSubscription(subscription, { onData }) + ); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(true); + expect(data).toBeUndefined(); + expect(error).toBeUndefined(); + } + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(false); + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + }, + }); + expect(error).toBeUndefined(); + + expect(onData).toHaveBeenCalledTimes(1); + expect(onData).toHaveBeenCalledWith({ + client: expect.anything(), + data: { + data: { addedComment: { __typename: "Comment", id: 1 } }, + loading: false, + error: undefined, + variables: undefined, + }, + }); + } + + await expect(ProfiledHook).not.toRerender(); + }); + + test("uses unmasked data when using the @unmask directive", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields @unmask + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + dataMasking: true, + cache: new Cache(), + link, + }); + + const onData = jest.fn(); + const ProfiledHook = profileHook(() => + useSubscription(subscription, { onData }) + ); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(true); + expect(data).toBeUndefined(); + expect(error).toBeUndefined(); + } + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(false); + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }); + expect(error).toBeUndefined(); + + expect(onData).toHaveBeenCalledTimes(1); + expect(onData).toHaveBeenCalledWith({ + client: expect.anything(), + data: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + loading: false, + error: undefined, + variables: undefined, + }, + }); + } + + await expect(ProfiledHook).not.toRerender(); + }); +}); + describe.skip("Type Tests", () => { test("NoInfer prevents adding arbitrary additional variables", () => { const typedNode = {} as TypedDocumentNode<{ foo: string }, { bar: number }>;