diff --git a/src/core/__tests__/masking.test.ts b/src/core/__tests__/masking.test.ts index 44eddab72fc..fc7636bca16 100644 --- a/src/core/__tests__/masking.test.ts +++ b/src/core/__tests__/masking.test.ts @@ -628,6 +628,231 @@ describe("maskOperation", () => { }); }); + test("can handle fragment spreads in inline fragments with mix masked and @unmask", () => { + const query = gql` + query { + user { + id + ... { + ...UserFields + ...ProfileFields @unmask + } + } + } + + fragment UserFields on User { + name + } + + fragment ProfileFields on User { + username + } + `; + + const data = maskOperation( + deepFreeze({ + user: { + __typename: "User", + id: 1, + name: "Test User", + username: "testuser", + }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + user: { + __typename: "User", + id: 1, + username: "testuser", + }, + }); + }); + + test("can handle overlapping fragment spreads in inline fragments with mix masked and @unmask", () => { + const query = gql` + query { + user { + id + ... { + ...UserFields + ...ProfileFields @unmask + } + } + } + + fragment UserFields on User { + username + name + } + + fragment ProfileFields on User { + username + email + } + `; + + const data = maskOperation( + deepFreeze({ + user: { + __typename: "User", + id: 1, + name: "Test User", + username: "testuser", + email: "testuser@example.com", + }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + user: { + __typename: "User", + id: 1, + username: "testuser", + email: "testuser@example.com", + }, + }); + }); + + // TODO: Remove .failing when refactoring migrate mode + test.failing( + 'can handle fragment spreads in inline fragments with mix masked and @unmask(mode: "migrate")', + () => { + using _ = spyOnConsole("warn"); + + const query = gql` + query GetUser { + user { + id + ... @defer { + ...UserFields + ...ProfileFields @unmask(mode: "migrate") + } + } + } + + fragment UserFields on User { + name + } + + fragment ProfileFields on User { + username + } + `; + + const data = maskOperation( + deepFreeze({ + user: { + __typename: "User", + id: 1, + name: "Test User", + username: "testuser", + }, + }), + query, + new InMemoryCache() + ); + + data.user.__typename; + data.user.id; + + expect(console.warn).not.toHaveBeenCalled(); + + data.user.username; + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "Accessing unmasked field on %s at path '%s'. This field will not be available when masking is enabled. Please read the field from the fragment instead.", + "query 'GetUser'", + "user.username" + ); + + expect(data).toEqual({ + user: { + __typename: "User", + id: 1, + username: "testuser", + }, + }); + } + ); + + // TODO: Remove .failing when refactoring migrate mode + test.failing( + 'can handle overlapping fragment spreads in inline fragments with mix masked and @unmask(mode: "migrate")', + () => { + using _ = spyOnConsole("warn"); + const query = gql` + query { + user { + id + ... { + ...UserFields + ...ProfileFields @unmask(mode: "migrate") + } + } + } + + fragment UserFields on User { + username + name + } + + fragment ProfileFields on User { + username + email + } + `; + + const data = maskOperation( + deepFreeze({ + user: { + __typename: "User", + id: 1, + name: "Test User", + username: "testuser", + email: "testuser@example.com", + }, + }), + query, + new InMemoryCache() + ); + + data.user.__typename; + data.user.id; + + expect(console.warn).not.toHaveBeenCalled(); + + data.user.username; + data.user.email; + + expect(console.warn).toHaveBeenCalledTimes(2); + expect(console.warn).toHaveBeenCalledWith( + "Accessing unmasked field on %s at path '%s'. This field will not be available when masking is enabled. Please read the field from the fragment instead.", + "query 'GetUser'", + "user.username" + ); + expect(console.warn).toHaveBeenCalledWith( + "Accessing unmasked field on %s at path '%s'. This field will not be available when masking is enabled. Please read the field from the fragment instead.", + "query 'GetUser'", + "user.email" + ); + + expect(data).toEqual({ + user: { + __typename: "User", + id: 1, + username: "testuser", + email: "testuser@example.com", + }, + }); + } + ); + test("handles field aliases", () => { const query = gql` query { @@ -1443,6 +1668,56 @@ describe("maskOperation", () => { ); }); + test("masks child fragments of @unmask", () => { + using _ = spyOnConsole("warn"); + + const query = gql` + query UnmaskedQuery { + currentUser { + id + name + ...UserFields @unmask + } + } + fragment UserFields on User { + age + ...UserSubfields + ...UserSubfields2 @unmask + } + fragment UserSubfields on User { + username + } + fragment UserSubfields2 on User { + email + } + `; + + const data = maskOperation( + deepFreeze({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + username: "testuser", + email: "test@example.com", + }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + email: "test@example.com", + }, + }); + }); + test("warns when accessing unmasked fields with complex selections with mode: 'migrate'", () => { using _ = spyOnConsole("warn"); const query = gql` @@ -1781,7 +2056,7 @@ describe("maskOperation", () => { `; const data = maskOperation( - { + deepFreeze({ playlist: { id: "1", name: "Playlist", @@ -1798,7 +2073,7 @@ describe("maskOperation", () => { topTracks: [{ id: "2", name: "Track 2", __typename: "Track" }], }, }, - }, + }), query, new InMemoryCache() ); @@ -2006,63 +2281,632 @@ describe("maskOperation", () => { "updateUser.name" ); }); -}); -describe("maskFragment", () => { - test("returns null when data is null", () => { - const fragment = gql` - fragment Foo on Query { - foo - ...QueryFields + test("masks partial data", () => { + const query = gql` + query { + greeting { + message + ...GreetingFragment + } } - fragment QueryFields on Query { - bar + fragment GreetingFragment on Greeting { + sentAt + recipient { + name + } } `; - const data = maskFragment(null, fragment, new InMemoryCache(), "Foo"); + { + const data = maskOperation( + deepFreeze({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }), + query, + new InMemoryCache() + ); - expect(data).toBe(null); - }); + expect(data).toEqual({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }); + } - test("returns undefined when data is undefined", () => { - const fragment = gql` - fragment Foo on Query { - foo - ...QueryFields - } + { + const data = maskOperation( + deepFreeze({ + greeting: { + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + }, + }), + query, + new InMemoryCache() + ); - fragment QueryFields on Query { - bar - } - `; + expect(data).toEqual({ + greeting: { + __typename: "Greeting", + message: "Hello world", + }, + }); + } - const data = maskFragment(undefined, fragment, new InMemoryCache(), "Foo"); + { + const data = maskOperation( + deepFreeze({ + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "__Person" }, + }, + }), + query, + new InMemoryCache() + ); - expect(data).toBe(undefined); + expect(data).toEqual({ + greeting: { + __typename: "Greeting", + message: "Hello world", + }, + }); + } }); - test("masks named fragments in fragment documents", () => { - const fragment = gql` - fragment UserFields on User { - id - ...UserProfile + + test("unmasks partial data with @unmask", () => { + const query = gql` + query { + greeting { + message + ...GreetingFragment @unmask + } } - fragment UserProfile on User { - age + fragment GreetingFragment on Greeting { + sentAt + recipient { + name + } } `; - const data = maskFragment( - deepFreeze({ __typename: "User", id: 1, age: 30 }), - fragment, - new InMemoryCache(), - "UserFields" - ); + { + const data = maskOperation( + deepFreeze({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }), + query, + new InMemoryCache() + ); - expect(data).toEqual({ __typename: "User", id: 1 }); - }); + expect(data).toEqual({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }); + } + + { + const data = maskOperation( + deepFreeze({ + greeting: { + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + greeting: { + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + }, + }); + } + + { + const data = maskOperation( + deepFreeze({ + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "__Person" }, + }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "__Person" }, + }, + }); + } + }); + + // TODO: Remove .failing when refactoring migrate mode + test.failing( + 'unmasks partial data with warnings with @unmask(mode: "migrate")', + () => { + using _ = spyOnConsole("warn"); + + const query = gql` + query { + greeting { + message + ...GreetingFragment @unmask(mode: "migrate") + } + } + + fragment GreetingFragment on Greeting { + sentAt + recipient { + name + } + } + `; + + { + const data = maskOperation( + deepFreeze({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }); + } + + { + const data = maskOperation( + deepFreeze({ + greeting: { + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + }, + }), + query, + new InMemoryCache() + ); + + data.greeting.__typename; + data.greeting.message; + + expect(console.warn).not.toHaveBeenCalled(); + + data.greeting.sentAt; + expect(console.warn).toHaveBeenCalledTimes(1); + + expect(data).toEqual({ + greeting: { + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + }, + }); + } + + { + const data = maskOperation( + deepFreeze({ + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "__Person" }, + }, + }), + query, + new InMemoryCache() + ); + + data.greeting.__typename; + data.greeting.message; + + expect(console.warn).not.toHaveBeenCalled(); + + data.greeting.recipient; + data.greeting.recipient.__typename; + expect(console.warn).toHaveBeenCalledTimes(1); + + expect(data).toEqual({ + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "__Person" }, + }, + }); + } + } + ); + + test("masks partial deferred data", () => { + const query = gql` + query { + greeting { + message + ... @defer { + sentAt + ...GreetingFragment + } + } + } + + fragment GreetingFragment on Greeting { + recipient { + name + } + } + `; + + { + const data = maskOperation( + deepFreeze({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }); + } + + { + const data = maskOperation( + deepFreeze({ + greeting: { + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + recipient: { __typename: "__Person", name: "Alice" }, + }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + greeting: { + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + }, + }); + } + }); + + test("unmasks partial deferred data with @unmask", () => { + const query = gql` + query { + greeting { + message + ... @defer { + sentAt + ...GreetingFragment @unmask + } + } + } + + fragment GreetingFragment on Greeting { + recipient { + name + } + } + `; + + { + const data = maskOperation( + deepFreeze({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }); + } + + { + const data = maskOperation( + deepFreeze({ + greeting: { + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + recipient: { __typename: "__Person", name: "Alice" }, + }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + greeting: { + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + recipient: { __typename: "__Person", name: "Alice" }, + }, + }); + } + }); + + // TODO: Remove .failing when refactoring migrate mode + test.failing( + 'unmasks partial deferred data with warnings with @unmask(mode: "migrate")', + () => { + using _ = spyOnConsole("warn"); + + const query = gql` + query { + greeting { + message + ... @defer { + sentAt + ...GreetingFragment @unmask(mode: "migrate") + } + } + } + + fragment GreetingFragment on Greeting { + recipient { + name + } + } + `; + + { + const data = maskOperation( + deepFreeze({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }); + } + + { + const data = maskOperation( + deepFreeze({ + greeting: { + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + recipient: { __typename: "__Person", name: "Alice" }, + }, + }), + query, + new InMemoryCache() + ); + + data.greeting.message; + data.greeting.sentAt; + data.greeting.__typename; + + expect(console.warn).not.toHaveBeenCalled(); + + data.greeting.recipient; + data.greeting.recipient.__typename; + data.greeting.recipient.name; + expect(console.warn).toHaveBeenCalledTimes(3); + + expect(data).toEqual({ + greeting: { + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + recipient: { __typename: "__Person", name: "Alice" }, + }, + }); + } + } + ); + + test("masks results with primitive arrays", () => { + const query = gql` + query { + listing { + id + reviews + ...ListingFragment + } + } + + fragment ListingFragment on Listing { + amenities + } + `; + + const data = maskOperation( + deepFreeze({ + listing: { + __typename: "Listing", + id: "1", + reviews: [5, 5, 3, 4], + amenities: ["pool", "hot tub", "backyard"], + }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + listing: { + __typename: "Listing", + id: "1", + reviews: [5, 5, 3, 4], + }, + }); + }); + + test("unmasks results with primitive arrays with @unmask", () => { + const query = gql` + query { + listing { + id + reviews + ...ListingFragment @unmask + } + } + + fragment ListingFragment on Listing { + amenities + } + `; + + const data = maskOperation( + deepFreeze({ + listing: { + __typename: "Listing", + id: "1", + reviews: [5, 5, 3, 4], + amenities: ["pool", "hot tub", "backyard"], + }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + listing: { + __typename: "Listing", + id: "1", + reviews: [5, 5, 3, 4], + amenities: ["pool", "hot tub", "backyard"], + }, + }); + }); + + test('unmasks results with warnings with primitive arrays with @unmask(mode: "migrate")', () => { + using _ = spyOnConsole("warn"); + + const query = gql` + query GetListing { + listing { + id + reviews + ...ListingFragment @unmask(mode: "migrate") + } + } + + fragment ListingFragment on Listing { + amenities + } + `; + + const data = maskOperation( + deepFreeze({ + listing: { + __typename: "Listing", + id: "1", + reviews: [5, 5, 3, 4], + amenities: ["pool", "hot tub", "backyard"], + }, + }), + query, + new InMemoryCache() + ); + + data.listing.__typename; + data.listing.id; + data.listing.reviews; + + expect(console.warn).not.toHaveBeenCalled(); + + data.listing.amenities; + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "Accessing unmasked field on %s at path '%s'. This field will not be available when masking is enabled. Please read the field from the fragment instead.", + "query 'GetListing'", + "listing.amenities" + ); + + expect(data).toEqual({ + listing: { + __typename: "Listing", + id: "1", + reviews: [5, 5, 3, 4], + amenities: ["pool", "hot tub", "backyard"], + }, + }); + }); +}); + +describe("maskFragment", () => { + test("returns null when data is null", () => { + const fragment = gql` + fragment Foo on Query { + foo + ...QueryFields + } + + fragment QueryFields on Query { + bar + } + `; + + const data = maskFragment(null, fragment, new InMemoryCache(), "Foo"); + + expect(data).toBe(null); + }); + + test("returns undefined when data is undefined", () => { + const fragment = gql` + fragment Foo on Query { + foo + ...QueryFields + } + + fragment QueryFields on Query { + bar + } + `; + + const data = maskFragment(undefined, fragment, new InMemoryCache(), "Foo"); + + expect(data).toBe(undefined); + }); + test("masks named fragments in fragment documents", () => { + const fragment = gql` + fragment UserFields on User { + id + ...UserProfile + } + + fragment UserProfile on User { + age + } + `; + + const data = maskFragment( + deepFreeze({ __typename: "User", id: 1, age: 30 }), + fragment, + new InMemoryCache(), + "UserFields" + ); + + expect(data).toEqual({ __typename: "User", id: 1 }); + }); test("masks named fragments in nested fragment objects", () => { const fragment = gql` @@ -2651,4 +3495,567 @@ describe("maskFragment", () => { "age" ); }); + + // TODO: Remove .failing when refactoring migrate mode + test.failing("masks child fragments of @unmask", () => { + using _ = spyOnConsole("warn"); + + const fragment = gql` + fragment UnmaskedUser on User { + id + name + ...UserFields @unmask + } + fragment UserFields on User { + age + ...UserSubfields + ...UserSubfields2 @unmask + } + fragment UserSubfields on User { + username + } + fragment UserSubfields2 on User { + email + } + `; + + const data = maskFragment( + deepFreeze({ + __typename: "User", + id: 1, + name: "Test User", + age: 30, + username: "testuser", + email: "test@example.com", + }), + fragment, + new InMemoryCache(), + "UnmaskedUser" + ); + + expect(data).toEqual({ + __typename: "User", + id: 1, + name: "Test User", + age: 30, + email: "test@example.com", + }); + }); + + test("masks partial data", () => { + const fragment = gql` + fragment GreetingFields on Greeting { + message + ...AdditionalFields + } + + fragment AdditionalFields on Greeting { + sentAt + recipient { + name + } + } + `; + + { + const data = maskFragment( + deepFreeze({ message: "Hello world", __typename: "Greeting" }), + fragment, + new InMemoryCache(), + "GreetingFields" + ); + + expect(data).toEqual({ + message: "Hello world", + __typename: "Greeting", + }); + } + + { + const data = maskFragment( + deepFreeze({ + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + }), + fragment, + new InMemoryCache(), + "GreetingFields" + ); + + expect(data).toEqual({ + __typename: "Greeting", + message: "Hello world", + }); + } + + { + const data = maskFragment( + deepFreeze({ + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "__Person" }, + }), + fragment, + new InMemoryCache(), + "GreetingFields" + ); + + expect(data).toEqual({ + __typename: "Greeting", + message: "Hello world", + }); + } + }); + + test("unmasks partial data with @unmask", () => { + const fragment = gql` + fragment GreetingFields on Greeting { + message + ...AdditionalFields @unmask + } + + fragment AdditionalFields on Greeting { + sentAt + recipient { + name + } + } + `; + + { + const data = maskFragment( + deepFreeze({ message: "Hello world", __typename: "Greeting" }), + fragment, + new InMemoryCache(), + "GreetingFields" + ); + + expect(data).toEqual({ + message: "Hello world", + __typename: "Greeting", + }); + } + + { + const data = maskFragment( + deepFreeze({ + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + }), + fragment, + new InMemoryCache(), + "GreetingFields" + ); + + expect(data).toEqual({ + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + }); + } + + { + const data = maskFragment( + deepFreeze({ + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "__Person" }, + }), + fragment, + new InMemoryCache(), + "GreetingFields" + ); + + expect(data).toEqual({ + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "__Person" }, + }); + } + }); + + // TODO: Remove .failing when refactoring migrate mode + test.failing( + 'unmasks partial data with warnings with @unmask(mode: "migrate")', + () => { + using _ = spyOnConsole("warn"); + + const fragment = gql` + fragment GreetingFields on Greeting { + message + ...AdditionalFields @unmask(mode: "migrate") + } + + fragment AdditionalFields on Greeting { + sentAt + recipient { + name + } + } + `; + + { + const data = maskFragment( + deepFreeze({ message: "Hello world", __typename: "Greeting" }), + fragment, + new InMemoryCache(), + "GreetingFields" + ); + + expect(data).toEqual({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }); + } + + { + const data = maskFragment( + deepFreeze({ + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + }), + fragment, + new InMemoryCache(), + "GreetingFields" + ); + + data.__typename; + data.message; + + expect(console.warn).not.toHaveBeenCalled(); + + data.sentAt; + expect(console.warn).toHaveBeenCalledTimes(1); + + expect(data).toEqual({ + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + }); + } + + { + const data = maskFragment( + deepFreeze({ + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "__Person" }, + }), + fragment, + new InMemoryCache(), + "GreetingFields" + ); + + data.__typename; + data.message; + + expect(console.warn).not.toHaveBeenCalled(); + + data.recipient; + data.recipient.__typename; + expect(console.warn).toHaveBeenCalledTimes(1); + + expect(data).toEqual({ + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "__Person" }, + }, + }); + } + } + ); + + test("masks partial deferred data", () => { + const fragment = gql` + fragment GreetingFields on Greeting { + message + ... @defer { + sentAt + ...AdditionalFields + } + } + + fragment AdditionalFields on Greeting { + recipient { + name + } + } + `; + + { + const data = maskFragment( + deepFreeze({ message: "Hello world", __typename: "Greeting" }), + fragment, + new InMemoryCache(), + "GreetingFields" + ); + + expect(data).toEqual({ + message: "Hello world", + __typename: "Greeting", + }); + } + + { + const data = maskFragment( + deepFreeze({ + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + recipient: { __typename: "__Person", name: "Alice" }, + }), + fragment, + new InMemoryCache(), + "GreetingFields" + ); + + expect(data).toEqual({ + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + }); + } + }); + + test("unmasks partial deferred data with @unmask", () => { + const fragment = gql` + fragment GreetingFields on Greeting { + message + ... @defer { + sentAt + ...AdditionalFields @unmask + } + } + + fragment AdditionalFields on Greeting { + recipient { + name + } + } + `; + + { + const data = maskFragment( + deepFreeze({ message: "Hello world", __typename: "Greeting" }), + fragment, + new InMemoryCache(), + "GreetingFields" + ); + + expect(data).toEqual({ + message: "Hello world", + __typename: "Greeting", + }); + } + + { + const data = maskFragment( + deepFreeze({ + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + recipient: { __typename: "__Person", name: "Alice" }, + }), + fragment, + new InMemoryCache(), + "GreetingFields" + ); + + expect(data).toEqual({ + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + recipient: { __typename: "__Person", name: "Alice" }, + }); + } + }); + + // TODO: Remove .failing when refactoring migrate mode + test.failing( + 'unmasks partial deferred data with warnings with @unmask(mode: "migrate")', + () => { + using _ = spyOnConsole("warn"); + + const fragment = gql` + fragment GreetingFields on Greeting { + message + ... @defer { + sentAt + ...AdditionalFields @unmask(mode: "migrate") + } + } + + fragment AdditionalFields on Greeting { + recipient { + name + } + } + `; + + { + const data = maskFragment( + deepFreeze({ message: "Hello world", __typename: "Greeting" }), + fragment, + new InMemoryCache(), + "GreetingFields" + ); + + expect(data).toEqual({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }); + } + + { + const data = maskFragment( + deepFreeze({ + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + recipient: { __typename: "__Person", name: "Alice" }, + }), + fragment, + new InMemoryCache(), + "GreetingFields" + ); + + data.message; + data.sentAt; + data.__typename; + + expect(console.warn).not.toHaveBeenCalled(); + + data.recipient; + data.recipient.__typename; + data.recipient.name; + expect(console.warn).toHaveBeenCalledTimes(3); + + expect(data).toEqual({ + greeting: { + __typename: "Greeting", + message: "Hello world", + sentAt: "2024-01-01", + recipient: { __typename: "__Person", name: "Alice" }, + }, + }); + } + } + ); + + test("masks results with primitive arrays", () => { + const fragment = gql` + fragment ListingFragment on Listing { + id + reviews + ...ListingFields + } + + fragment ListingFields on Listing { + amenities + } + `; + + const data = maskFragment( + deepFreeze({ + __typename: "Listing", + id: "1", + reviews: [5, 5, 3, 4], + amenities: ["pool", "hot tub", "backyard"], + }), + fragment, + new InMemoryCache(), + "ListingFragment" + ); + + expect(data).toEqual({ + __typename: "Listing", + id: "1", + reviews: [5, 5, 3, 4], + }); + }); + + test("unmasks results with primitive arrays with @unmask", () => { + const fragment = gql` + fragment ListingFragment on Listing { + id + reviews + ...ListingFields @unmask + } + + fragment ListingFields on Listing { + amenities + } + `; + + const data = maskFragment( + deepFreeze({ + __typename: "Listing", + id: "1", + reviews: [5, 5, 3, 4], + amenities: ["pool", "hot tub", "backyard"], + }), + fragment, + new InMemoryCache(), + "ListingFragment" + ); + + expect(data).toEqual({ + __typename: "Listing", + id: "1", + reviews: [5, 5, 3, 4], + amenities: ["pool", "hot tub", "backyard"], + }); + }); + + test('unmasks results with warnings with primitive arrays with @unmask(mode: "migrate")', () => { + using _ = spyOnConsole("warn"); + + const fragment = gql` + fragment ListingFragment on Listing { + id + reviews + ...ListingFields @unmask(mode: "migrate") + } + + fragment ListingFields on Listing { + amenities + } + `; + + const data = maskFragment( + deepFreeze({ + __typename: "Listing", + id: "1", + reviews: [5, 5, 3, 4], + amenities: ["pool", "hot tub", "backyard"], + }), + fragment, + new InMemoryCache(), + "ListingFragment" + ); + + data.__typename; + data.id; + data.reviews; + + expect(console.warn).not.toHaveBeenCalled(); + + data.amenities; + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "Accessing unmasked field on %s at path '%s'. This field will not be available when masking is enabled. Please read the field from the fragment instead.", + "fragment 'ListingFragment'", + "amenities" + ); + + expect(data).toEqual({ + __typename: "Listing", + id: "1", + reviews: [5, 5, 3, 4], + amenities: ["pool", "hot tub", "backyard"], + }); + }); });