Fix e2e test for UP, i18n & upload + various i18n fixes

This commit is contained in:
Convly 2021-09-27 17:17:24 +02:00
parent 3da1acebf4
commit 20b20a0f92
8 changed files with 287 additions and 210 deletions

View File

@ -45,7 +45,7 @@ module.exports = context => {
.hasInputEnabled()
);
for (const [attributeName, attribute] of validAttributes) {
validAttributes.forEach(([attributeName, attribute]) => {
// Scalars
if (isStrapiScalar(attribute)) {
const gqlScalar = mappers.strapiScalarToGraphQLScalar(attribute.type);
@ -101,7 +101,7 @@ module.exports = context => {
t.list.field(attributeName, { type: nonNull(dzInputName) });
}
}
});
},
});
},

View File

@ -5,11 +5,22 @@ const { prop, propEq, identity, merge } = require('lodash/fp');
const LOCALE_SCALAR_TYPENAME = 'I18NLocaleCode';
const LOCALE_ARG_PLUGIN_NAME = 'I18NLocaleArg';
const getLocalizedTypesFromRegistry = ({ strapi, typeRegistry }) => {
const { KINDS } = strapi.plugin('graphql').service('constants');
const { isLocalizedContentType } = strapi.plugin('i18n').service('content-types');
return typeRegistry.where(
({ config }) => config.kind === KINDS.type && isLocalizedContentType(config.contentType)
);
};
module.exports = ({ strapi }) => ({
register() {
const { service: getGraphQLService } = strapi.plugin('graphql');
const { service: getI18NService } = strapi.plugin('i18n');
const { isLocalizedContentType } = getI18NService('content-types');
const extensionService = getGraphQLService('extension');
const getCreateLocalizationMutationType = contentType => {
@ -20,6 +31,23 @@ module.exports = ({ strapi }) => ({
extensionService.shadowCRUD('plugin::i18n.locale').disableMutations();
// Disable unwanted fields for localized content types
Object.entries(strapi.contentTypes).forEach(([uid, ct]) => {
if (isLocalizedContentType(ct)) {
// Disable locale field in localized inputs
extensionService
.shadowCRUD(uid)
.field('locale')
.disableInput();
// Disable localizations field in localized inputs
extensionService
.shadowCRUD(uid)
.field('localizations')
.disableInput();
}
});
extensionService.use(({ nexus, typeRegistry }) => {
const i18nLocaleArgPlugin = getI18nLocaleArgPlugin({ nexus, typeRegistry });
const i18nLocaleScalar = getLocaleScalar({ nexus });
@ -33,7 +61,10 @@ module.exports = ({ strapi }) => ({
types: [i18nLocaleScalar, createLocalizationMutations],
resolversConfig: {
// Auth for createLocalization mutations
...createLocalizationResolversConfig,
// locale arg transformation for localized createEntity mutations
...getLocalizedCreateMutationsResolversConfigs({ typeRegistry }),
},
};
});
@ -66,14 +97,9 @@ module.exports = ({ strapi }) => ({
};
const getCreateLocalizationMutations = ({ nexus, typeRegistry }) => {
const { KINDS } = getGraphQLService('constants');
const { isLocalizedContentType } = getI18NService('content-types');
const localizedContentTypes = typeRegistry
.where(
({ config }) => config.kind === KINDS.type && isLocalizedContentType(config.contentType)
)
.map(prop('config.contentType'));
const localizedContentTypes = getLocalizedTypesFromRegistry({ strapi, typeRegistry }).map(
prop('config.contentType')
);
const createLocalizationComponents = localizedContentTypes.map(ct =>
getCreateLocalizationComponents(ct, { nexus })
@ -138,6 +164,33 @@ module.exports = ({ strapi }) => ({
return { mutation, resolverConfig };
};
const getLocalizedCreateMutationsResolversConfigs = ({ typeRegistry }) => {
const localizedCreateMutationsNames = getLocalizedTypesFromRegistry({
strapi,
typeRegistry,
})
.map(prop('config.contentType'))
.map(getGraphQLService('utils').naming.getCreateMutationTypeName);
return localizedCreateMutationsNames.reduce(
(acc, mutationName) => ({
...acc,
[`Mutation.${mutationName}`]: {
middlewares: [
// Set data's locale using args' locale
(resolve, parent, args, context, info) => {
args.data.locale = args.locale;
return resolve(parent, args, context, info);
},
],
},
}),
{}
);
};
const getI18nLocaleArgPlugin = ({ nexus, typeRegistry }) => {
const { isLocalizedContentType } = getI18NService('content-types');

View File

@ -22,7 +22,7 @@ const recipesModel = {
localized: true,
},
},
name: 'recipes',
name: 'recipe',
description: '',
collectionName: '',
};
@ -51,68 +51,80 @@ describe('Test Graphql API create localization', () => {
afterAll(async () => {
await strapi.query('plugin::i18n.locale').delete({ where: { id: localeId } });
await strapi.query('api::recipes.recipes').deleteMany();
await strapi.query('api::recipe.recipe').deleteMany();
await strapi.destroy();
await builder.cleanup();
});
test('Create localization for a model with plural name', async () => {
test('Create localization', async () => {
const createResponse = await graphqlQuery({
query: /* GraphQL */ `
mutation createRecipe($input: createRecipeInput) {
createRecipe(input: $input) {
recipe {
mutation createRecipe($data: RecipeInput!) {
createRecipe(data: $data) {
data {
id
name
locale
attributes {
name
locale
}
}
}
}
`,
variables: {
input: {
data: {
name: 'Recipe Name',
},
data: {
name: 'Recipe Name',
},
},
});
expect(createResponse.statusCode).toBe(200);
expect(createResponse.body.data.createRecipe.recipe).toMatchObject({
name: 'Recipe Name',
locale: 'en',
expect(createResponse.body).toMatchObject({
data: {
createRecipe: {
data: {
attributes: {
name: 'Recipe Name',
locale: 'en',
},
},
},
},
});
const recipeId = createResponse.body.data.createRecipe.recipe.id;
const recipeId = createResponse.body.data.createRecipe.data.id;
const createLocalizationResponse = await graphqlQuery({
query: /* GraphQL */ `
mutation createRecipeLocalization($input: updateRecipeInput!) {
createRecipeLocalization(input: $input) {
id
name
locale
mutation createRecipeLocalization($id: ID!, $locale: I18NLocaleCode, $data: RecipeInput!) {
createRecipeLocalization(id: $id, locale: $locale, data: $data) {
data {
id
attributes {
name
locale
}
}
}
}
`,
variables: {
input: {
where: {
id: recipeId,
},
data: {
name: 'Recipe Name fr',
locale: 'fr',
},
id: recipeId,
locale: 'fr',
data: {
name: 'Recipe Name fr',
},
},
});
expect(createLocalizationResponse.statusCode).toBe(200);
expect(createLocalizationResponse.body.data.createRecipeLocalization).toMatchObject({
name: 'Recipe Name fr',
locale: 'fr',
data: {
attributes: {
name: 'Recipe Name fr',
locale: 'fr',
},
},
});
});
});

View File

@ -3,6 +3,7 @@
const getTypes = require('./types');
const getQueries = require('./queries');
const getMutations = require('./mutations');
const getResolversConfig = require('./resolvers-configs');
module.exports = ({ strapi }) => {
const { config: graphQLConfig } = strapi.plugin('graphql');
@ -32,31 +33,12 @@ module.exports = ({ strapi }) => {
const types = getTypes({ strapi, nexus });
const queries = getQueries({ strapi, nexus });
const mutations = getMutations({ strapi, nexus });
const resolversConfig = getResolversConfig({ strapi });
return {
types: [types, queries, mutations],
resolversConfig: {
'Mutation.login': {
auth: false,
},
'Mutation.register': {
auth: false,
},
'Mutation.forgotPassword': {
auth: false,
},
'Mutation.resetPassword': {
auth: false,
},
'Mutation.emailConfirmation': {
auth: false,
},
},
resolversConfig,
};
});
};

View File

@ -0,0 +1,37 @@
'use strict';
const userUID = 'plugin::users-permissions.user';
const roleUID = 'plugin::users-permissions.role';
module.exports = ({ strapi }) => {
const { naming } = strapi.plugin('graphql').service('utils');
const user = strapi.getModel(userUID);
const role = strapi.getModel(roleUID);
const createRole = naming.getCreateMutationTypeName(role);
const updateRole = naming.getUpdateMutationTypeName(role);
const deleteRole = naming.getDeleteMutationTypeName(role);
const createUser = naming.getCreateMutationTypeName(user);
const updateUser = naming.getUpdateMutationTypeName(user);
const deleteUser = naming.getDeleteMutationTypeName(user);
return {
// Disabled auth for some operations
'Mutation.login': { auth: false },
'Mutation.register': { auth: false },
'Mutation.forgotPassword': { auth: false },
'Mutation.resetPassword': { auth: false },
'Mutation.emailConfirmation': { auth: false },
// Scoped auth for replaced CRUD operations
// Role
[`Mutation.${createRole}`]: { auth: { scope: [`${roleUID}.create`] } },
[`Mutation.${updateRole}`]: { auth: { scope: [`${roleUID}.update`] } },
[`Mutation.${deleteRole}`]: { auth: { scope: [`${roleUID}.delete`] } },
// User
[`Mutation.${createUser}`]: { auth: { scope: [`${userUID}.create`] } },
[`Mutation.${updateUser}`]: { auth: { scope: [`${userUID}.update`] } },
[`Mutation.${deleteUser}`]: { auth: { scope: [`${userUID}.delete`] } },
};
};

View File

@ -6,11 +6,10 @@ const { createAuthRequest, createRequest } = require('../../../../test/helpers/r
let strapi;
let authReq;
const data = {};
describe('Test Graphql user service', () => {
beforeAll(async () => {
strapi = await createStrapiInstance();
strapi = await createStrapiInstance({ bypassAuth: false });
authReq = await createAuthRequest({ strapi });
});
@ -27,10 +26,14 @@ describe('Test Graphql user service', () => {
body: {
query: /* GraphQL */ `
mutation {
createUser(input: { data: { username: "test", email: "test", password: "test" } }) {
user {
createUsersPermissionsUser(
data: { username: "test", email: "test", password: "test" }
) {
data {
id
username
attributes {
username
}
}
}
}
@ -40,32 +43,30 @@ describe('Test Graphql user service', () => {
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
data: {
createUser: null,
},
data: null,
errors: [
{
message: 'Forbidden',
message: 'Forbidden access',
},
],
});
});
test('createUser is authorized for admins', async () => {
test('createUser is forbidden for admins', async () => {
const res = await authReq({
url: '/graphql',
method: 'POST',
body: {
query: /* GraphQL */ `
mutation {
createUser(
input: {
data: { username: "test", email: "test-graphql@strapi.io", password: "test" }
}
createUsersPermissionsUser(
data: { username: "test", email: "test", password: "test" }
) {
user {
data {
id
username
attributes {
username
}
}
}
}
@ -73,19 +74,12 @@ describe('Test Graphql user service', () => {
},
});
expect(res.statusCode).toBe(200);
expect(res.statusCode).toBe(401);
expect(res.body).toMatchObject({
data: {
createUser: {
user: {
id: expect.anything(),
username: 'test',
},
},
},
error: 'Unauthorized',
message: 'Missing or invalid credentials',
statusCode: 401,
});
data.user = res.body.data.createUser.user;
});
});
@ -98,15 +92,15 @@ describe('Test Graphql user service', () => {
body: {
query: /* GraphQL */ `
mutation {
updateUser(
input: {
where: { id: 1 }
data: { username: "test", email: "test", password: "test" }
}
updateUsersPermissionsUser(
id: 1
data: { username: "test", email: "test", password: "test" }
) {
user {
data {
id
username
attributes {
username
}
}
}
}
@ -116,118 +110,105 @@ describe('Test Graphql user service', () => {
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
data: {
updateUser: null,
},
data: null,
errors: [
{
message: 'Forbidden',
message: 'Forbidden access',
},
],
});
});
test('updateUser is authorized for admins', async () => {
test('updateUser is forbidden for admins', async () => {
const res = await authReq({
url: '/graphql',
method: 'POST',
body: {
query: /* GraphQL */ `
mutation updateUser($id: ID!) {
updateUser(input: { where: { id: $id }, data: { username: "newUsername" } }) {
user {
mutation {
updateUsersPermissionsUser(
id: 1
data: { username: "test", email: "test", password: "test" }
) {
data {
id
username
attributes {
username
}
}
}
}
`,
variables: {
id: data.user.id,
},
},
});
expect(res.statusCode).toBe(200);
expect(res.statusCode).toBe(401);
expect(res.body).toMatchObject({
data: {
updateUser: {
user: {
id: expect.anything(),
username: 'newUsername',
error: 'Unauthorized',
message: 'Missing or invalid credentials',
statusCode: 401,
});
});
describe('Check deleteUser authorizations', () => {
test('deleteUser is forbidden to public', async () => {
const rq = createRequest({ strapi });
const res = await rq({
url: '/graphql',
method: 'POST',
body: {
query: /* GraphQL */ `
mutation deleteUser {
deleteUsersPermissionsUser(id: 1) {
data {
id
attributes {
username
}
}
}
}
`,
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
data: null,
errors: [
{
message: 'Forbidden access',
},
},
},
],
});
});
data.user = res.body.data.updateUser.user;
});
});
describe('Check deleteUser authorizations', () => {
test('deleteUser is forbidden to public', async () => {
const rq = createRequest({ strapi });
const res = await rq({
url: '/graphql',
method: 'POST',
body: {
query: /* GraphQL */ `
mutation deleteUser($id: ID!) {
deleteUser(input: { where: { id: $id } }) {
user {
id
username
test('deleteUser is authorized for admins', async () => {
const res = await authReq({
url: '/graphql',
method: 'POST',
body: {
query: /* GraphQL */ `
mutation deleteUser {
deleteUsersPermissionsUser(id: 1) {
data {
id
attributes {
username
}
}
}
}
}
`,
variables: {
id: data.user.id,
`,
},
},
});
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
data: {
deleteUser: null,
},
errors: [
{
message: 'Forbidden',
},
],
});
});
test('deleteUser is authorized for admins', async () => {
const res = await authReq({
url: '/graphql',
method: 'POST',
body: {
query: /* GraphQL */ `
mutation deleteUser($id: ID!) {
deleteUser(input: { where: { id: $id } }) {
user {
id
username
}
}
}
`,
variables: {
id: data.user.id,
},
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
data: {
deleteUser: {
user: data.user,
},
},
expect(res.statusCode).toBe(401);
expect(res.body).toMatchObject({
error: 'Unauthorized',
message: 'Missing or invalid credentials',
statusCode: 401,
});
});
});
});

View File

@ -3,17 +3,23 @@
// Test a simple default API with no relations
const { createStrapiInstance } = require('../../../../test/helpers/strapi');
const { createAuthRequest } = require('../../../../test/helpers/request');
const { createRequest } = require('../../../../test/helpers/request');
let strapi;
let rq;
let graphqlQuery;
let data = {};
const user = {
username: 'User 1',
email: 'user1@strapi.io',
password: 'test1234',
};
describe('Test Graphql Users API End to End', () => {
beforeAll(async () => {
strapi = await createStrapiInstance();
rq = await createAuthRequest({ strapi });
rq = await createRequest({ strapi });
graphqlQuery = body => {
return rq({
@ -29,12 +35,6 @@ describe('Test Graphql Users API End to End', () => {
});
describe('Test register and login', () => {
const user = {
username: 'User 1',
email: 'user1@strapi.io',
password: 'test1234',
};
test('Register a user', async () => {
const res = await graphqlQuery({
query: /* GraphQL */ `
@ -67,6 +67,7 @@ describe('Test Graphql Users API End to End', () => {
},
},
});
data.user = res.body.data.register.user;
});
@ -105,26 +106,29 @@ describe('Test Graphql Users API End to End', () => {
},
},
});
// Use the JWT returned by the login request to
// authentify the next queries or mutations
rq.setLoggedUser(user).setToken(res.body.data.login.jwt);
data.user = res.body.data.login.user;
});
test('Delete a user', async () => {
const res = await graphqlQuery({
query: /* GraphQL */ `
mutation deleteUser($input: deleteUserInput) {
deleteUser(input: $input) {
user {
email
mutation deleteUser($id: ID!) {
deleteUsersPermissionsUser(id: $id) {
data {
attributes {
email
}
}
}
}
`,
variables: {
input: {
where: {
id: data.user.id,
},
},
id: data.user.id,
},
});
@ -133,9 +137,11 @@ describe('Test Graphql Users API End to End', () => {
expect(res.statusCode).toBe(200);
expect(body).toMatchObject({
data: {
deleteUser: {
user: {
email: data.user.email,
deleteUsersPermissionsUser: {
data: {
attributes: {
email: data.user.email,
},
},
},
},

View File

@ -16,19 +16,25 @@ const superAdminLoginInfo = _.pick(superAdminCredentials, ['email', 'password'])
const TEST_APP_URL = path.resolve(__dirname, '../../testApp');
const createStrapiInstance = async ({ ensureSuperAdmin = true, logLevel = 'fatal' } = {}) => {
const createStrapiInstance = async ({
ensureSuperAdmin = true,
logLevel = 'fatal',
bypassAuth = true,
} = {}) => {
const options = { dir: TEST_APP_URL };
const instance = strapi(options);
instance.container.get('auth').register('content-api', {
name: 'test-auth',
authenticate() {
return { authenticated: true };
},
verify() {
return;
},
});
if (bypassAuth) {
instance.container.get('auth').register('content-api', {
name: 'test-auth',
authenticate() {
return { authenticated: true };
},
verify() {
return;
},
});
}
await instance.load();