diff --git a/packages/graphql/schema.graphql b/packages/graphql/schema.graphql index d9a243c9f6..d0207ba753 100644 --- a/packages/graphql/schema.graphql +++ b/packages/graphql/schema.graphql @@ -18,6 +18,9 @@ type App { """All known projects for the app""" projects: [Project!]! + + """Cypress Cloud""" + user: User } """ @@ -46,9 +49,15 @@ type Mutation { """Create a Cypress config file for a new project""" appCreateConfigFile(code: String!, configFilename: String!): App + """Auth with Cypress Cloud""" + authenticate: App + """Initializes the plugins for the current active project""" initializePlugins: Project + """Log out of Cypress Cloud""" + logout: App + """Installs the dependencies for the component testing step""" wizardInstallDependencies: Wizard @@ -102,6 +111,12 @@ type Project { type Query { app: App! + """Get runs for a given projectId on Cypress Cloud""" + runs(projectId: String!): App + + """Namespace for user and authentication""" + user: User + """Metadata about the wizard, null if we arent showing the wizard""" wizard: Wizard } @@ -123,6 +138,13 @@ type TestingTypeInfo { title: String } +"""Namespace for information related to authentication with Cypress Cloud""" +type User { + authToken: String + email: String + name: String +} + """ The Wizard is a container for any state associated with initial onboarding to Cypress """ diff --git a/packages/graphql/src/actions/BaseActions.ts b/packages/graphql/src/actions/BaseActions.ts index 1c28534cd7..f27f819e62 100644 --- a/packages/graphql/src/actions/BaseActions.ts +++ b/packages/graphql/src/actions/BaseActions.ts @@ -51,4 +51,7 @@ export abstract class BaseActions { } abstract createProjectBase(input: NxsMutationArgs<'addProject'>['input']): ProjectBaseContract | Promise + abstract authenticate (): Promise + abstract logout (): Promise + abstract getRuns ({ projectId }: { projectId: string }): Promise } diff --git a/packages/graphql/src/context/BaseContext.ts b/packages/graphql/src/context/BaseContext.ts index 0bb5443c0f..53f0fc158e 100644 --- a/packages/graphql/src/context/BaseContext.ts +++ b/packages/graphql/src/context/BaseContext.ts @@ -1,5 +1,5 @@ import type { BaseActions } from '../actions/BaseActions' -import { App, Wizard } from '../entities' +import { App, User, Wizard } from '../entities' import type { Project } from '../entities/Project' /** @@ -12,6 +12,7 @@ import type { Project } from '../entities/Project' export abstract class BaseContext { abstract readonly actions: BaseActions abstract projects: Project[] + abstract user?: User = undefined wizard = new Wizard() app = new App(this) diff --git a/packages/graphql/src/entities/App.ts b/packages/graphql/src/entities/App.ts index 0a4667453f..e8c3ec319e 100644 --- a/packages/graphql/src/entities/App.ts +++ b/packages/graphql/src/entities/App.ts @@ -1,6 +1,7 @@ import { nxs, NxsResult } from 'nexus-decorators' import type { BaseContext } from '../context/BaseContext' import { Project } from './Project' +import { User } from './User' @nxs.objectType({ description: 'Namespace for information related to the app', @@ -28,4 +29,11 @@ export class App { get activeProject (): NxsResult<'App', 'activeProject'> { return this.projects.find((p) => p.isCurrent) ?? null } + + @nxs.field.type(() => User, { + description: 'Cypress Cloud', + }) + get user (): NxsResult<'App', 'user'> { + return this.ctx.user ?? null + } } diff --git a/packages/graphql/src/entities/Mutation.ts b/packages/graphql/src/entities/Mutation.ts index b978137aad..634685666d 100644 --- a/packages/graphql/src/entities/Mutation.ts +++ b/packages/graphql/src/entities/Mutation.ts @@ -107,6 +107,31 @@ export const mutation = mutationType({ }, }) + t.field('authenticate', { + type: 'App', + description: 'Auth with Cypress Cloud', + async resolve (_root, args, ctx) { + // already authenticated this session - just return + if (ctx.user) { + return ctx.app + } + + await ctx.actions.authenticate() + + return ctx.app + }, + }) + + t.field('logout', { + type: 'App', + description: 'Log out of Cypress Cloud', + async resolve (_root, args, ctx) { + await ctx.actions.logout() + + return ctx.app + }, + }) + t.field('initializePlugins', { type: 'Project', description: 'Initializes the plugins for the current active project', diff --git a/packages/graphql/src/entities/Query.ts b/packages/graphql/src/entities/Query.ts index c21a9346ed..6407062ba4 100644 --- a/packages/graphql/src/entities/Query.ts +++ b/packages/graphql/src/entities/Query.ts @@ -1,6 +1,7 @@ -import { nxs, NxsQueryResult, NxsResult } from 'nexus-decorators' +import { nxs, NxsArgs, NxsQueryResult, NxsResult } from 'nexus-decorators' import type { NexusGenTypes } from '../gen/nxs.gen' import { App } from './App' +import { User } from './User' import { Wizard } from './Wizard' @nxs.objectType({ @@ -18,4 +19,23 @@ export class Query { wizard (args: unknown, ctx: NexusGenTypes['context']): NxsResult<'App', 'wizard'> { return ctx.wizard } + + @nxs.field.type(() => User, { + description: 'Namespace for user and authentication', + }) + user (args: unknown, ctx: NexusGenTypes['context']): NxsResult<'App', 'user'> { + return ctx.user ?? null + } + + @nxs.field.type(() => App, { + description: 'Get runs for a given projectId on Cypress Cloud', + args (t) { + t.nonNull.string('projectId') + }, + }) + async runs (args: NxsArgs<'Query', 'runs'>, ctx: NexusGenTypes['context']): Promise> { + await ctx.actions.getRuns({ projectId: args.projectId }) + + return ctx.app + } } diff --git a/packages/graphql/src/entities/User.ts b/packages/graphql/src/entities/User.ts new file mode 100644 index 0000000000..ace93e1f30 --- /dev/null +++ b/packages/graphql/src/entities/User.ts @@ -0,0 +1,29 @@ +import { nxs, NxsResult } from 'nexus-decorators' + +export interface AuthenticatedUser { + name: string + email: string + authToken: string +} + +@nxs.objectType({ + description: 'Namespace for information related to authentication with Cypress Cloud', +}) +export class User { + constructor (private user: AuthenticatedUser) {} + + @nxs.field.string() + get name (): NxsResult<'User', 'name'> { + return this.user?.name ?? null + } + + @nxs.field.string() + get email (): NxsResult<'User', 'email'> { + return this.user?.email ?? null + } + + @nxs.field.string() + get authToken (): NxsResult<'User', 'authToken'> { + return this.user?.authToken ?? null + } +} diff --git a/packages/graphql/src/entities/index.ts b/packages/graphql/src/entities/index.ts index dbc8ff63af..1e3bce5fbe 100644 --- a/packages/graphql/src/entities/index.ts +++ b/packages/graphql/src/entities/index.ts @@ -6,6 +6,8 @@ export * from './Project' export * from './Query' +export * from './User' + export * from './TestingTypeInfo' export * from './Wizard' diff --git a/packages/graphql/src/gen/nxs.gen.ts b/packages/graphql/src/gen/nxs.gen.ts index 087c1f5a9d..8a564a9785 100644 --- a/packages/graphql/src/gen/nxs.gen.ts +++ b/packages/graphql/src/gen/nxs.gen.ts @@ -9,6 +9,7 @@ import type { BaseContext } from "./../context/BaseContext" import type { App } from "./../entities/App" import type { Project } from "./../entities/Project" import type { Query } from "./../entities/Query" +import type { User } from "./../entities/User" import type { TestingTypeInfo } from "./../entities/TestingTypeInfo" import type { Wizard } from "./../entities/Wizard" import type { WizardBundler } from "./../entities/WizardBundler" @@ -79,6 +80,7 @@ export interface NexusGenObjects { Project: Project; Query: Query; TestingTypeInfo: TestingTypeInfo; + User: User; Wizard: Wizard; WizardBundler: WizardBundler; WizardFrontendFramework: WizardFrontendFramework; @@ -100,11 +102,14 @@ export interface NexusGenFieldTypes { activeProject: NexusGenRootTypes['Project'] | null; // Project isFirstOpen: boolean; // Boolean! projects: NexusGenRootTypes['Project'][]; // [Project!]! + user: NexusGenRootTypes['User'] | null; // User } Mutation: { // field return type addProject: NexusGenRootTypes['Project']; // Project! appCreateConfigFile: NexusGenRootTypes['App'] | null; // App + authenticate: NexusGenRootTypes['App'] | null; // App initializePlugins: NexusGenRootTypes['Project'] | null; // Project + logout: NexusGenRootTypes['App'] | null; // App wizardInstallDependencies: NexusGenRootTypes['Wizard'] | null; // Wizard wizardNavigate: NexusGenRootTypes['Wizard'] | null; // Wizard wizardNavigateForward: NexusGenRootTypes['Wizard'] | null; // Wizard @@ -125,6 +130,8 @@ export interface NexusGenFieldTypes { } Query: { // field return type app: NexusGenRootTypes['App']; // App! + runs: NexusGenRootTypes['App'] | null; // App + user: NexusGenRootTypes['User'] | null; // User wizard: NexusGenRootTypes['Wizard'] | null; // Wizard } TestingTypeInfo: { // field return type @@ -132,6 +139,11 @@ export interface NexusGenFieldTypes { id: NexusGenEnums['TestingTypeEnum']; // TestingTypeEnum! title: string | null; // String } + User: { // field return type + authToken: string | null; // String + email: string | null; // String + name: string | null; // String + } Wizard: { // field return type allBundlers: NexusGenRootTypes['WizardBundler'][]; // [WizardBundler!]! bundler: NexusGenRootTypes['WizardBundler'] | null; // WizardBundler @@ -170,11 +182,14 @@ export interface NexusGenFieldTypeNames { activeProject: 'Project' isFirstOpen: 'Boolean' projects: 'Project' + user: 'User' } Mutation: { // field return type name addProject: 'Project' appCreateConfigFile: 'App' + authenticate: 'App' initializePlugins: 'Project' + logout: 'App' wizardInstallDependencies: 'Wizard' wizardNavigate: 'Wizard' wizardNavigateForward: 'Wizard' @@ -195,6 +210,8 @@ export interface NexusGenFieldTypeNames { } Query: { // field return type name app: 'App' + runs: 'App' + user: 'User' wizard: 'Wizard' } TestingTypeInfo: { // field return type name @@ -202,6 +219,11 @@ export interface NexusGenFieldTypeNames { id: 'TestingTypeEnum' title: 'String' } + User: { // field return type name + authToken: 'String' + email: 'String' + name: 'String' + } Wizard: { // field return type name allBundlers: 'WizardBundler' bundler: 'WizardBundler' @@ -260,6 +282,11 @@ export interface NexusGenArgTypes { type: NexusGenEnums['TestingTypeEnum']; // TestingTypeEnum! } } + Query: { + runs: { // args + projectId: string; // String! + } + } Wizard: { sampleCode: { // args lang: NexusGenEnums['WizardCodeLanguage']; // WizardCodeLanguage! diff --git a/packages/graphql/src/testing/testUnionType.ts b/packages/graphql/src/testing/testUnionType.ts index 4edb3cff5c..3f7661ba69 100644 --- a/packages/graphql/src/testing/testUnionType.ts +++ b/packages/graphql/src/testing/testUnionType.ts @@ -7,6 +7,7 @@ export interface TestSourceTypeLookup { Project: NexusGenObjects['Project'], Query: NexusGenObjects['Query'], TestingTypeInfo: NexusGenObjects['TestingTypeInfo'], + User: NexusGenObjects['User'], Wizard: NexusGenObjects['Wizard'], WizardBundler: NexusGenObjects['WizardBundler'], WizardFrontendFramework: NexusGenObjects['WizardFrontendFramework'], @@ -25,6 +26,7 @@ export const testUnionType = unionType({ 'Project', 'Query', 'TestingTypeInfo', + 'User', 'Wizard', 'WizardBundler', 'WizardFrontendFramework', diff --git a/packages/graphql/test/.mocharc.js b/packages/graphql/test/.mocharc.js index 4f160cc6a9..4ba52ba2c8 100644 --- a/packages/graphql/test/.mocharc.js +++ b/packages/graphql/test/.mocharc.js @@ -1,6 +1 @@ -const path = require('path') - -module.exports = { - spec: 'test/unit/**/*.spec.{js,ts,tsx,jsx}', - require: path.resolve(__dirname, 'spec_helper.js'), -} +module.exports = {} diff --git a/packages/graphql/test/integration/App.spec.ts b/packages/graphql/test/integration/App.spec.ts new file mode 100644 index 0000000000..e25a25bb6a --- /dev/null +++ b/packages/graphql/test/integration/App.spec.ts @@ -0,0 +1,33 @@ +import { expect } from 'chai' +import { initGraphql, makeRequest, TestContext } from './utils' + +describe('App', () => { + describe('authenticate', () => { + it('assigns a new user', async () => { + const context = new TestContext() + const { endpoint } = await initGraphql(context) + + const result = await makeRequest(endpoint, ` + mutation Authenticate { + authenticate { + user { + email + name + authToken + } + } + } + `) + + expect(result).to.eql({ + authenticate: { + user: { + email: 'test@cypress.io', + name: 'cypress test', + authToken: 'test-auth-token', + }, + }, + }) + }) + }) +}) diff --git a/packages/graphql/test/integration/Wizard.spec.ts b/packages/graphql/test/integration/Wizard.spec.ts index b02546173c..61aa5870d0 100644 --- a/packages/graphql/test/integration/Wizard.spec.ts +++ b/packages/graphql/test/integration/Wizard.spec.ts @@ -1,70 +1,7 @@ -import type { NxsMutationArgs } from 'nexus-decorators' import snapshot from 'snap-shot-it' import { expect } from 'chai' -import axios from 'axios' -import { BaseActions, BaseContext, Project, Wizard } from '../../src' -import { startGraphQLServer, closeGraphQLServer, setServerContext } from '../../src/server' - -class TestActions extends BaseActions { - installDependencies () {} - createConfigFile () {} - createProjectBase (input: NxsMutationArgs<'addProject'>['input']) { - return new Project({ - isCurrent: true, - projectRoot: '/foo/bar', - projectBase: { - isOpen: true, - initializePlugins: () => Promise.resolve(), - }, - }) - } -} - -interface TestContextInjectionOptions { - wizard?: Wizard -} - -class TestContext extends BaseContext { - projects: Project[] = [] - readonly actions: BaseActions - - constructor ({ wizard }: TestContextInjectionOptions = {}) { - super() - this.actions = new TestActions(this) - if (wizard) { - this.wizard = wizard - } - } -} - -/** - * Creates a new GraphQL server to query during integration tests. - * Also performsn any clean up from previous tests. - * Optionally you may provide a context to orchestrate testing - * specific scenarios or states. - */ -const initGraphql = async (ctx: BaseContext) => { - await closeGraphQLServer() - if (ctx) { - setServerContext(ctx) - } - - return startGraphQLServer({ port: 51515 }) -} - -const makeRequest = async (endpoint: string, query: string) => { - const res = await axios.post(endpoint, - JSON.stringify({ - query, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }) - - return res.data.data -} +import { Wizard } from '../../src' +import { initGraphql, makeRequest, TestContext } from './utils' describe('Wizard', () => { describe('sampleCode', () => { diff --git a/packages/graphql/test/integration/utils.ts b/packages/graphql/test/integration/utils.ts new file mode 100644 index 0000000000..cc5ce6b5da --- /dev/null +++ b/packages/graphql/test/integration/utils.ts @@ -0,0 +1,82 @@ +import type { NxsMutationArgs } from 'nexus-decorators' +import axios from 'axios' +import { BaseActions, BaseContext, Project, User, Wizard } from '../../src' +import { startGraphQLServer, closeGraphQLServer, setServerContext } from '../../src/server' + +class TestActions extends BaseActions { + async authenticate () { + this.ctx.user = new User({ + authToken: 'test-auth-token', + email: 'test@cypress.io', + name: 'cypress test', + }) + } + + async getRuns ({ projectId }: { projectId: string }) {} + + async logout () { + this.ctx.user = undefined + } + + installDependencies () {} + + createConfigFile () {} + + createProjectBase (input: NxsMutationArgs<'addProject'>['input']) { + return new Project({ + isCurrent: true, + projectRoot: '/foo/bar', + projectBase: { + isOpen: true, + initializePlugins: () => Promise.resolve(), + }, + }) + } +} + +interface TestContextInjectionOptions { + wizard?: Wizard +} + +export class TestContext extends BaseContext { + projects: Project[] = [] + readonly actions: BaseActions + user: undefined + + constructor ({ wizard }: TestContextInjectionOptions = {}) { + super() + this.actions = new TestActions(this) + if (wizard) { + this.wizard = wizard + } + } +} + +/** + * Creates a new GraphQL server to query during integration tests. + * Also performsn any clean up from previous tests. + * Optionally you may provide a context to orchestrate testing + * specific scenarios or states. + */ +export const initGraphql = async (ctx: BaseContext) => { + await closeGraphQLServer() + if (ctx) { + setServerContext(ctx) + } + + return startGraphQLServer({ port: 51515 }) +} + +export const makeRequest = async (endpoint: string, query: string) => { + const res = await axios.post(endpoint, + JSON.stringify({ + query, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }) + + return res.data.data +} diff --git a/packages/launchpad/src/setup/Auth.vue b/packages/launchpad/src/setup/Auth.vue new file mode 100644 index 0000000000..6606b46e89 --- /dev/null +++ b/packages/launchpad/src/setup/Auth.vue @@ -0,0 +1,70 @@ + + + \ No newline at end of file diff --git a/packages/launchpad/src/setup/Wizard.vue b/packages/launchpad/src/setup/Wizard.vue index f0e6e9ccb1..7e37f5ee31 100644 --- a/packages/launchpad/src/setup/Wizard.vue +++ b/packages/launchpad/src/setup/Wizard.vue @@ -1,5 +1,6 @@