зеркало из https://github.com/mozilla/fxa.git
Merge pull request #6141 from mozilla/gql-session
feat(settings): add session verified state to graphql-api
This commit is contained in:
Коммит
b43e911d9c
|
@ -531,7 +531,9 @@ export default class AuthClient {
|
||||||
return this.sessionPost('/session/destroy', sessionToken, options);
|
return this.sessionPost('/session/destroy', sessionToken, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sessionStatus(sessionToken: string) {
|
async sessionStatus(
|
||||||
|
sessionToken: string
|
||||||
|
): Promise<{ state: 'verified' | 'unverified'; uid: string }> {
|
||||||
return this.sessionGet('/session/status', sessionToken);
|
return this.sessionGet('/session/status', sessionToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -933,7 +935,9 @@ export default class AuthClient {
|
||||||
return this.sessionPost('/totp/destroy', sessionToken, {});
|
return this.sessionPost('/totp/destroy', sessionToken, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkTotpTokenExists(sessionToken: string): Promise<{ exists: boolean, verified: boolean }> {
|
async checkTotpTokenExists(
|
||||||
|
sessionToken: string
|
||||||
|
): Promise<{ exists: boolean; verified: boolean }> {
|
||||||
return this.sessionGet('/totp/exists', sessionToken);
|
return this.sessionGet('/totp/exists', sessionToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,9 @@ export class SessionTokenAuth {
|
||||||
@Inject(fxAccountClientToken)
|
@Inject(fxAccountClientToken)
|
||||||
private authClient!: AuthClient;
|
private authClient!: AuthClient;
|
||||||
|
|
||||||
public async lookupUserId(sessionToken: string): Promise<string> {
|
public async getSessionStatus(sessionToken: string) {
|
||||||
try {
|
try {
|
||||||
const result = await this.authClient.sessionStatus(sessionToken);
|
return await this.authClient.sessionStatus(sessionToken);
|
||||||
return result.uid;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new AuthenticationError('Invalid session token');
|
throw new AuthenticationError('Invalid session token');
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,7 +215,7 @@ export class AccountResolver {
|
||||||
|
|
||||||
@Query((returns) => AccountType, { nullable: true })
|
@Query((returns) => AccountType, { nullable: true })
|
||||||
public account(@Ctx() context: Context, @Info() info: GraphQLResolveInfo) {
|
public account(@Ctx() context: Context, @Info() info: GraphQLResolveInfo) {
|
||||||
context.logger.info('account', { uid: context.authUser });
|
context.logger.info('account', { uid: context.session.uid });
|
||||||
|
|
||||||
// Introspect the query to determine if we should load the emails
|
// Introspect the query to determine if we should load the emails
|
||||||
const parsed: any = parseResolveInfo(info);
|
const parsed: any = parseResolveInfo(info);
|
||||||
|
@ -228,7 +228,7 @@ export class AccountResolver {
|
||||||
const options: AccountOptions = includeEmails
|
const options: AccountOptions = includeEmails
|
||||||
? { include: ['emails'] }
|
? { include: ['emails'] }
|
||||||
: {};
|
: {};
|
||||||
return accountByUid(context.authUser, options);
|
return accountByUid(context.session.uid, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@FieldResolver()
|
@FieldResolver()
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { GraphQLResolveInfo } from 'graphql';
|
||||||
|
import {
|
||||||
|
Ctx,
|
||||||
|
Info,
|
||||||
|
Query,
|
||||||
|
Resolver,
|
||||||
|
} from 'type-graphql';
|
||||||
|
import { Context } from '../server';
|
||||||
|
import { Session as SessionType } from './types/session'
|
||||||
|
|
||||||
|
@Resolver((of) => SessionType)
|
||||||
|
export class SessionResolver {
|
||||||
|
|
||||||
|
@Query((returns) => SessionType)
|
||||||
|
session(@Ctx() context: Context, @Info() info: GraphQLResolveInfo) {
|
||||||
|
context.logger.info('session', { uid: context.session.uid });
|
||||||
|
return {
|
||||||
|
verified: context.session.state === 'verified'
|
||||||
|
} as SessionType
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { Field, ObjectType } from 'type-graphql';
|
||||||
|
|
||||||
|
@ObjectType({ description: 'Session (token) info' })
|
||||||
|
export class Session {
|
||||||
|
@Field({
|
||||||
|
description:
|
||||||
|
'Whether the current session is verified',
|
||||||
|
})
|
||||||
|
public verified!: boolean;
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import { SessionTokenAuth } from './auth';
|
||||||
import { AuthServerSource } from './datasources/authServer';
|
import { AuthServerSource } from './datasources/authServer';
|
||||||
import { ProfileServerSource } from './datasources/profileServer';
|
import { ProfileServerSource } from './datasources/profileServer';
|
||||||
import { AccountResolver } from './resolvers/account-resolver';
|
import { AccountResolver } from './resolvers/account-resolver';
|
||||||
|
import { SessionResolver } from './resolvers/session-resolver';
|
||||||
import { reportGraphQLError } from './sentry';
|
import { reportGraphQLError } from './sentry';
|
||||||
|
|
||||||
type ServerConfig = {
|
type ServerConfig = {
|
||||||
|
@ -39,7 +40,7 @@ export function formatError(debug: boolean, logger: Logger, err: GraphQLError) {
|
||||||
* Context available to resolvers
|
* Context available to resolvers
|
||||||
*/
|
*/
|
||||||
export type Context = {
|
export type Context = {
|
||||||
authUser: string;
|
session: { uid: string; state: 'verified' | 'unverified' };
|
||||||
token: string;
|
token: string;
|
||||||
dataSources: DataSources;
|
dataSources: DataSources;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
|
@ -57,11 +58,11 @@ export async function createServer(
|
||||||
): Promise<ApolloServer> {
|
): Promise<ApolloServer> {
|
||||||
const schema = await TypeGraphQL.buildSchema({
|
const schema = await TypeGraphQL.buildSchema({
|
||||||
container: Container,
|
container: Container,
|
||||||
resolvers: [AccountResolver],
|
resolvers: [AccountResolver, SessionResolver],
|
||||||
validate: false,
|
validate: false,
|
||||||
});
|
});
|
||||||
const authHeader = config.authHeader.toLowerCase();
|
const authHeader = config.authHeader.toLowerCase();
|
||||||
const authUser = Container.get(SessionTokenAuth);
|
const authToken = Container.get(SessionTokenAuth);
|
||||||
const debugMode = config.env !== 'production';
|
const debugMode = config.env !== 'production';
|
||||||
const defaultContext = async ({ req }: { req: Request }) => {
|
const defaultContext = async ({ req }: { req: Request }) => {
|
||||||
const bearerToken = req.headers[authHeader];
|
const bearerToken = req.headers[authHeader];
|
||||||
|
@ -70,13 +71,12 @@ export async function createServer(
|
||||||
'Invalid authentcation header found at: ' + authHeader
|
'Invalid authentcation header found at: ' + authHeader
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const userId = await authUser.lookupUserId(bearerToken);
|
const session = await authToken.getSessionStatus(bearerToken);
|
||||||
return {
|
return {
|
||||||
authUser: userId,
|
|
||||||
token: bearerToken,
|
token: bearerToken,
|
||||||
|
session,
|
||||||
logger,
|
logger,
|
||||||
};
|
} as Context;
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return new ApolloServer({
|
return new ApolloServer({
|
||||||
|
|
|
@ -25,18 +25,21 @@ describe('SessionTokenAuth', () => {
|
||||||
sandbox.resetHistory();
|
sandbox.resetHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('lookupUserId', async () => {
|
describe('getSessionStatus', async () => {
|
||||||
it('looks up user successfully', async () => {
|
it('looks up user successfully', async () => {
|
||||||
fxAccountClient.sessionStatus.resolves({ uid: '9001xyz', state: 'test' });
|
fxAccountClient.sessionStatus.resolves({
|
||||||
const result = await sessionAuth.lookupUserId('token');
|
uid: '9001xyz',
|
||||||
assert.equal(result, '9001xyz');
|
state: 'unverified',
|
||||||
|
});
|
||||||
|
const result = await sessionAuth.getSessionStatus('token');
|
||||||
|
assert.equal(result.uid, '9001xyz');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws when the authClient throws', async () => {
|
it('throws when the authClient throws', async () => {
|
||||||
fxAccountClient.sessionStatus.rejects(new Error('boom'));
|
fxAccountClient.sessionStatus.rejects(new Error('boom'));
|
||||||
try {
|
try {
|
||||||
await sessionAuth.lookupUserId('token');
|
await sessionAuth.getSessionStatus('token');
|
||||||
assert.fail('lookupUserId should have thrown');
|
assert.fail('getSessionStatus should have thrown');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
assert.instanceOf(e, AuthenticationError);
|
assert.instanceOf(e, AuthenticationError);
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ describe('AuthServerSource', () => {
|
||||||
recoveryEmailSecondaryResendCode: sandbox.stub(),
|
recoveryEmailSecondaryResendCode: sandbox.stub(),
|
||||||
};
|
};
|
||||||
Container.set(fxAccountClientToken, authClient);
|
Container.set(fxAccountClientToken, authClient);
|
||||||
context = mockContext();
|
context = mockContext() as Context;
|
||||||
authSource = new AuthServerSource();
|
authSource = new AuthServerSource();
|
||||||
const config = stubInterface<DataSourceConfig<Context>>();
|
const config = stubInterface<DataSourceConfig<Context>>();
|
||||||
config.context = context;
|
config.context = context;
|
||||||
|
|
|
@ -43,7 +43,7 @@ describe('ProfileServerSource', () => {
|
||||||
};
|
};
|
||||||
Container.set(fxAccountClientToken, authClient);
|
Container.set(fxAccountClientToken, authClient);
|
||||||
Container.set(configContainerToken, config);
|
Container.set(configContainerToken, config);
|
||||||
context = mockContext();
|
context = mockContext() as Context;
|
||||||
profileSource = new ProfileServerSource();
|
profileSource = new ProfileServerSource();
|
||||||
const pluginConfig = stubInterface<DataSourceConfig<Context>>();
|
const pluginConfig = stubInterface<DataSourceConfig<Context>>();
|
||||||
pluginConfig.context = context;
|
pluginConfig.context = context;
|
||||||
|
|
|
@ -13,7 +13,10 @@ export function mockContext() {
|
||||||
profileAPI: stubInterface<ProfileServerSource>(),
|
profileAPI: stubInterface<ProfileServerSource>(),
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
authUser: '',
|
session: {
|
||||||
|
uid: '',
|
||||||
|
state: 'unverified',
|
||||||
|
},
|
||||||
token: 'test',
|
token: 'test',
|
||||||
logger: stubInterface<Logger>(),
|
logger: stubInterface<Logger>(),
|
||||||
dataSources: ds,
|
dataSources: ds,
|
||||||
|
|
|
@ -56,7 +56,7 @@ describe('accountResolver', () => {
|
||||||
accountCreated
|
accountCreated
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
context.authUser = USER_1.uid;
|
context.session.uid = USER_1.uid;
|
||||||
const result = (await graphql(
|
const result = (await graphql(
|
||||||
schema,
|
schema,
|
||||||
query,
|
query,
|
||||||
|
@ -78,7 +78,7 @@ describe('accountResolver', () => {
|
||||||
uid
|
uid
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
context.authUser = USER_2.uid;
|
context.session.uid = USER_2.uid;
|
||||||
const result = (await graphql(
|
const result = (await graphql(
|
||||||
schema,
|
schema,
|
||||||
query,
|
query,
|
||||||
|
@ -101,7 +101,7 @@ describe('accountResolver', () => {
|
||||||
clientMutationId
|
clientMutationId
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
context.authUser = USER_1.uid;
|
context.session.uid = USER_1.uid;
|
||||||
const result = (await graphql(
|
const result = (await graphql(
|
||||||
schema,
|
schema,
|
||||||
query,
|
query,
|
||||||
|
@ -125,7 +125,7 @@ describe('accountResolver', () => {
|
||||||
displayName
|
displayName
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
context.authUser = USER_1.uid;
|
context.session.uid = USER_1.uid;
|
||||||
const result = (await graphql(
|
const result = (await graphql(
|
||||||
schema,
|
schema,
|
||||||
query,
|
query,
|
||||||
|
@ -155,7 +155,7 @@ describe('accountResolver', () => {
|
||||||
clientMutationId
|
clientMutationId
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
context.authUser = USER_1.uid;
|
context.session.uid = USER_1.uid;
|
||||||
const result = (await graphql(
|
const result = (await graphql(
|
||||||
schema,
|
schema,
|
||||||
query,
|
query,
|
||||||
|
@ -188,7 +188,7 @@ describe('accountResolver', () => {
|
||||||
recoveryCodes
|
recoveryCodes
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
context.authUser = USER_1.uid;
|
context.session.uid = USER_1.uid;
|
||||||
const result = (await graphql(
|
const result = (await graphql(
|
||||||
schema,
|
schema,
|
||||||
query,
|
query,
|
||||||
|
@ -217,7 +217,7 @@ describe('accountResolver', () => {
|
||||||
recoveryCodes
|
recoveryCodes
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
context.authUser = USER_1.uid;
|
context.session.uid = USER_1.uid;
|
||||||
const result = (await graphql(
|
const result = (await graphql(
|
||||||
schema,
|
schema,
|
||||||
query,
|
query,
|
||||||
|
@ -244,7 +244,7 @@ describe('accountResolver', () => {
|
||||||
success
|
success
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
context.authUser = USER_1.uid;
|
context.session.uid = USER_1.uid;
|
||||||
const result = (await graphql(
|
const result = (await graphql(
|
||||||
schema,
|
schema,
|
||||||
query,
|
query,
|
||||||
|
@ -268,7 +268,7 @@ describe('accountResolver', () => {
|
||||||
clientMutationId
|
clientMutationId
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
context.authUser = USER_1.uid;
|
context.session.uid = USER_1.uid;
|
||||||
const result = (await graphql(
|
const result = (await graphql(
|
||||||
schema,
|
schema,
|
||||||
query,
|
query,
|
||||||
|
@ -291,7 +291,7 @@ describe('accountResolver', () => {
|
||||||
clientMutationId
|
clientMutationId
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
context.authUser = USER_1.uid;
|
context.session.uid = USER_1.uid;
|
||||||
const result = (await graphql(
|
const result = (await graphql(
|
||||||
schema,
|
schema,
|
||||||
query,
|
query,
|
||||||
|
@ -314,7 +314,7 @@ describe('accountResolver', () => {
|
||||||
clientMutationId
|
clientMutationId
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
context.authUser = USER_1.uid;
|
context.session.uid = USER_1.uid;
|
||||||
const result = (await graphql(
|
const result = (await graphql(
|
||||||
schema,
|
schema,
|
||||||
query,
|
query,
|
||||||
|
@ -339,7 +339,7 @@ describe('accountResolver', () => {
|
||||||
clientMutationId
|
clientMutationId
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
context.authUser = USER_1.uid;
|
context.session.uid = USER_1.uid;
|
||||||
const result = (await graphql(
|
const result = (await graphql(
|
||||||
schema,
|
schema,
|
||||||
query,
|
query,
|
||||||
|
|
|
@ -29,20 +29,22 @@ describe('Schema', () => {
|
||||||
schemaIntrospection = builtSchema.data!.__schema as IntrospectionSchema;
|
schemaIntrospection = builtSchema.data!.__schema as IntrospectionSchema;
|
||||||
assert.isDefined(schemaIntrospection);
|
assert.isDefined(schemaIntrospection);
|
||||||
queryType = schemaIntrospection.types.find(
|
queryType = schemaIntrospection.types.find(
|
||||||
type => type.name === (schemaIntrospection as IntrospectionSchema).queryType.name
|
(type) =>
|
||||||
|
type.name ===
|
||||||
|
(schemaIntrospection as IntrospectionSchema).queryType.name
|
||||||
) as IntrospectionObjectType;
|
) as IntrospectionObjectType;
|
||||||
|
|
||||||
const mutationTypeNameRef = schemaIntrospection.mutationType;
|
const mutationTypeNameRef = schemaIntrospection.mutationType;
|
||||||
if (mutationTypeNameRef) {
|
if (mutationTypeNameRef) {
|
||||||
mutationType = schemaIntrospection.types.find(
|
mutationType = schemaIntrospection.types.find(
|
||||||
type => type.name === mutationTypeNameRef.name
|
(type) => type.name === mutationTypeNameRef.name
|
||||||
) as IntrospectionObjectType;
|
) as IntrospectionObjectType;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function findTypeByName(name: string) {
|
function findTypeByName(name: string) {
|
||||||
return schemaIntrospection.types.find(
|
return schemaIntrospection.types.find(
|
||||||
type => type.kind === TypeKind.OBJECT && type.name === name
|
(type) => type.kind === TypeKind.OBJECT && type.name === name
|
||||||
) as IntrospectionObjectType;
|
) as IntrospectionObjectType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,13 +52,13 @@ describe('Schema', () => {
|
||||||
const typ = findTypeByName(name);
|
const typ = findTypeByName(name);
|
||||||
assert.isDefined(typ);
|
assert.isDefined(typ);
|
||||||
assert.lengthOf(typ.fields, members.length);
|
assert.lengthOf(typ.fields, members.length);
|
||||||
const typNames = typ.fields.map(it => it.name);
|
const typNames = typ.fields.map((it) => it.name);
|
||||||
assert.sameMembers(typNames, members);
|
assert.sameMembers(typNames, members);
|
||||||
}
|
}
|
||||||
|
|
||||||
it('is created with expected types', async () => {
|
it('is created with expected types', async () => {
|
||||||
const queryNames = queryType.fields.map(it => it.name);
|
const queryNames = queryType.fields.map((it) => it.name);
|
||||||
assert.sameMembers(queryNames, ['account']);
|
assert.sameMembers(queryNames, ['account', 'session']);
|
||||||
|
|
||||||
verifyTypeMembers('Account', [
|
verifyTypeMembers('Account', [
|
||||||
'uid',
|
'uid',
|
||||||
|
@ -106,5 +108,6 @@ describe('Schema', () => {
|
||||||
'os',
|
'os',
|
||||||
]);
|
]);
|
||||||
verifyTypeMembers('Location', ['city', 'country', 'state', 'stateCode']);
|
verifyTypeMembers('Location', ['city', 'country', 'state', 'stateCode']);
|
||||||
|
verifyTypeMembers('Session', ['verified']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { createServer } from '../../lib/server';
|
||||||
|
|
||||||
const sandbox = sinon.createSandbox();
|
const sandbox = sinon.createSandbox();
|
||||||
|
|
||||||
const sessionAuth = { lookupUserId: sandbox.stub() };
|
const sessionAuth = { getSessionStatus: sandbox.stub() };
|
||||||
|
|
||||||
const mockLogger = ({ info: () => {} } as unknown) as Logger;
|
const mockLogger = ({ info: () => {} } as unknown) as Logger;
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ describe('createServer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an AuthenticationError when auth server has auth error', async () => {
|
it('should throw an AuthenticationError when auth server has auth error', async () => {
|
||||||
sessionAuth.lookupUserId.rejects(
|
sessionAuth.getSessionStatus.rejects(
|
||||||
new AuthenticationError('Invalid token')
|
new AuthenticationError('Invalid token')
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
|
@ -101,12 +101,16 @@ describe('createServer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a user and the bearer token', async () => {
|
it('should return a user and the bearer token', async () => {
|
||||||
sessionAuth.lookupUserId.resolves('9001xyz');
|
sessionAuth.getSessionStatus.resolves({
|
||||||
|
uid: '9001xyz',
|
||||||
|
state: 'unverified',
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
const context = await (server as any).context({
|
const context = await (server as any).context({
|
||||||
req: { headers: { authorization: 'lolcatz' } },
|
req: { headers: { authorization: 'lolcatz' } },
|
||||||
});
|
});
|
||||||
assert.equal(context.authUser, '9001xyz');
|
assert.equal(context.session.uid, '9001xyz');
|
||||||
|
assert.equal(context.session.state, 'unverified');
|
||||||
assert.equal(context.token, 'lolcatz');
|
assert.equal(context.token, 'lolcatz');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
assert.fail('Should not have thrown an exception: ' + e);
|
assert.fail('Should not have thrown an exception: ' + e);
|
||||||
|
|
|
@ -15,9 +15,6 @@ type AccountDataHOCProps = {
|
||||||
children: (props: AccountDataHOCChildrenProps) => React.ReactNode;
|
children: (props: AccountDataHOCChildrenProps) => React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A data extraction layer for initial load.
|
|
||||||
// TODO: If an account is unverified we'll need to
|
|
||||||
// requery with less requested properties.
|
|
||||||
export const AccountDataHOC = ({ children }: AccountDataHOCProps) => {
|
export const AccountDataHOC = ({ children }: AccountDataHOCProps) => {
|
||||||
const { loading, error, data } = useQuery(GET_ACCOUNT);
|
const { loading, error, data } = useQuery(GET_ACCOUNT);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { InMemoryCache } from '@apollo/client';
|
||||||
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
|
import { render, act, wait, screen } from '@testing-library/react';
|
||||||
|
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
|
||||||
|
import { VerifiedSessionGuard, GET_SESSION } from '.';
|
||||||
|
|
||||||
|
const verifiedCache = new InMemoryCache();
|
||||||
|
verifiedCache.writeQuery({
|
||||||
|
query: GET_SESSION,
|
||||||
|
data: {
|
||||||
|
session: {
|
||||||
|
verified: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const unverifiedCache = new InMemoryCache();
|
||||||
|
unverifiedCache.writeQuery({
|
||||||
|
query: GET_SESSION,
|
||||||
|
data: {
|
||||||
|
session: {
|
||||||
|
verified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the content when verified', async () => {
|
||||||
|
render(
|
||||||
|
<MockedProvider addTypename={false} cache={verifiedCache}>
|
||||||
|
<VerifiedSessionGuard guard={<div data-testid="guard">oops</div>}>
|
||||||
|
<div data-testid="children">Content</div>
|
||||||
|
</VerifiedSessionGuard>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
await wait();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('children')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the guard when unverified', async () => {
|
||||||
|
async () => {
|
||||||
|
render(
|
||||||
|
<MockedProvider addTypename={false} cache={unverifiedCache}>
|
||||||
|
<VerifiedSessionGuard guard={<div data-testid="guard">oops</div>}>
|
||||||
|
<div>Content</div>
|
||||||
|
</VerifiedSessionGuard>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
await wait();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('guard')).toBeInTheDocument();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the guard when loading fails', async () => {
|
||||||
|
// don't pollute the test output with console logs
|
||||||
|
const consoleError = console.error;
|
||||||
|
console.error = () => {};
|
||||||
|
render(
|
||||||
|
<MockedProvider>
|
||||||
|
<VerifiedSessionGuard guard={<div data-testid="guard">oops</div>}>
|
||||||
|
<div>Content</div>
|
||||||
|
</VerifiedSessionGuard>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
await wait();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('guard')).toBeInTheDocument();
|
||||||
|
console.error = consoleError;
|
||||||
|
});
|
|
@ -0,0 +1,37 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { gql, useQuery } from '@apollo/client';
|
||||||
|
import sentryMetrics from 'fxa-shared/lib/sentry';
|
||||||
|
|
||||||
|
export const GET_SESSION = gql`
|
||||||
|
query GetSession {
|
||||||
|
session {
|
||||||
|
verified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const VerifiedSessionGuard = ({
|
||||||
|
guard,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
guard: React.ReactElement;
|
||||||
|
children: React.ReactElement;
|
||||||
|
}) => {
|
||||||
|
const { error, data } = useQuery(GET_SESSION, { fetchPolicy: 'cache-only' });
|
||||||
|
|
||||||
|
if (error || !data) {
|
||||||
|
// idk if this'll ever happen irl
|
||||||
|
const e = error || new Error('VerifiedSessionGuard missing data');
|
||||||
|
console.error(e);
|
||||||
|
sentryMetrics.captureException(e);
|
||||||
|
return guard;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.session.verified ? children : guard;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VerifiedSessionGuard;
|
|
@ -15,3 +15,6 @@ export function useAuth() {
|
||||||
}
|
}
|
||||||
return auth!;
|
return auth!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE for future self
|
||||||
|
// In cases where the sessionToken changes we should also flush the session { verified } value from apollo cache
|
||||||
|
|
|
@ -17,18 +17,18 @@ export function sessionToken(newToken?: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sessionToken is added as a local field in the cache as an example.
|
// sessionToken is added as a local field as an example.
|
||||||
export const typeDefs = gql`
|
export const typeDefs = gql`
|
||||||
extend type Query {
|
extend type Session {
|
||||||
sessionToken: String!
|
token: String!
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const cache = new InMemoryCache({
|
export const cache = new InMemoryCache({
|
||||||
typePolicies: {
|
typePolicies: {
|
||||||
Query: {
|
Session: {
|
||||||
fields: {
|
fields: {
|
||||||
sessionToken: {
|
token: {
|
||||||
read() {
|
read() {
|
||||||
return sessionToken();
|
return sessionToken();
|
||||||
},
|
},
|
||||||
|
|
Загрузка…
Ссылка в новой задаче