Merge branch 'master' into fix-db-connection-charset

This commit is contained in:
Alexander Friedland 2022-05-20 08:44:58 +02:00 committed by GitHub
commit cc088ed48c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 335 additions and 54 deletions

View File

@ -528,7 +528,7 @@ jobs:
report_name: Coverage Backend report_name: Coverage Backend
type: lcov type: lcov
result_path: ./backend/coverage/lcov.info result_path: ./backend/coverage/lcov.info
min_coverage: 64 min_coverage: 66
token: ${{ github.token }} token: ${{ github.token }}
########################################################################## ##########################################################################

View File

@ -5,15 +5,13 @@ export const searchUsers = gql`
$searchText: String! $searchText: String!
$currentPage: Int $currentPage: Int
$pageSize: Int $pageSize: Int
$filterByActivated: Boolean $filters: SearchUsersFiltersInput
$filterByDeleted: Boolean
) { ) {
searchUsers( searchUsers(
searchText: $searchText searchText: $searchText
currentPage: $currentPage currentPage: $currentPage
pageSize: $pageSize pageSize: $pageSize
filterByActivated: $filterByActivated filters: $filters
filterByDeleted: $filterByDeleted
) { ) {
userCount userCount
userList { userList {

View File

@ -71,8 +71,10 @@ describe('Creation', () => {
searchText: '', searchText: '',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: true, filters: {
filterByDeleted: false, filterByActivated: true,
filterByDeleted: false,
},
}, },
}), }),
) )
@ -271,8 +273,10 @@ describe('Creation', () => {
searchText: 'XX', searchText: 'XX',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: true, filters: {
filterByDeleted: false, filterByActivated: true,
filterByDeleted: false,
},
}, },
}), }),
) )
@ -288,8 +292,10 @@ describe('Creation', () => {
searchText: '', searchText: '',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: true, filters: {
filterByDeleted: false, filterByActivated: true,
filterByDeleted: false,
},
}, },
}), }),
) )
@ -305,8 +311,10 @@ describe('Creation', () => {
searchText: '', searchText: '',
currentPage: 2, currentPage: 2,
pageSize: 25, pageSize: 25,
filterByActivated: true, filters: {
filterByDeleted: false, filterByActivated: true,
filterByDeleted: false,
},
}, },
}), }),
) )

View File

@ -102,8 +102,10 @@ export default {
searchText: this.criteria, searchText: this.criteria,
currentPage: this.currentPage, currentPage: this.currentPage,
pageSize: this.perPage, pageSize: this.perPage,
filterByActivated: true, filters: {
filterByDeleted: false, filterByActivated: true,
filterByDeleted: false,
},
}, },
fetchPolicy: 'network-only', fetchPolicy: 'network-only',
}) })

View File

@ -7,7 +7,7 @@ const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue({ const apolloQueryMock = jest.fn().mockResolvedValue({
data: { data: {
searchUsers: { searchUsers: {
userCount: 1, userCount: 4,
userList: [ userList: [
{ {
userId: 1, userId: 1,
@ -82,8 +82,10 @@ describe('UserSearch', () => {
searchText: '', searchText: '',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: null, filters: {
filterByDeleted: null, filterByActivated: null,
filterByDeleted: null,
},
}, },
}), }),
) )
@ -101,8 +103,10 @@ describe('UserSearch', () => {
searchText: '', searchText: '',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: false, filters: {
filterByDeleted: null, filterByActivated: false,
filterByDeleted: null,
},
}, },
}), }),
) )
@ -121,8 +125,10 @@ describe('UserSearch', () => {
searchText: '', searchText: '',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: null, filters: {
filterByDeleted: true, filterByActivated: null,
filterByDeleted: true,
},
}, },
}), }),
) )
@ -141,8 +147,10 @@ describe('UserSearch', () => {
searchText: '', searchText: '',
currentPage: 2, currentPage: 2,
pageSize: 25, pageSize: 25,
filterByActivated: null, filters: {
filterByDeleted: null, filterByActivated: null,
filterByDeleted: null,
},
}, },
}), }),
) )
@ -161,8 +169,10 @@ describe('UserSearch', () => {
searchText: 'search string', searchText: 'search string',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: null, filters: {
filterByDeleted: null, filterByActivated: null,
filterByDeleted: null,
},
}, },
}), }),
) )
@ -178,8 +188,10 @@ describe('UserSearch', () => {
searchText: '', searchText: '',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: null, filters: {
filterByDeleted: null, filterByActivated: null,
filterByDeleted: null,
},
}, },
}), }),
) )

View File

@ -97,8 +97,10 @@ export default {
searchText: this.criteria, searchText: this.criteria,
currentPage: this.currentPage, currentPage: this.currentPage,
pageSize: this.perPage, pageSize: this.perPage,
filterByActivated: this.filterByActivated, filters: {
filterByDeleted: this.filterByDeleted, filterByActivated: this.filterByActivated,
filterByDeleted: this.filterByDeleted,
},
}, },
fetchPolicy: 'no-cache', fetchPolicy: 'no-cache',
}) })

View File

@ -1,4 +1,5 @@
import { ArgsType, Field, Int } from 'type-graphql' import { ArgsType, Field, Int } from 'type-graphql'
import SearchUsersFilters from '@arg/SearchUsersFilters'
@ArgsType() @ArgsType()
export default class SearchUsersArgs { export default class SearchUsersArgs {
@ -11,9 +12,6 @@ export default class SearchUsersArgs {
@Field(() => Int, { nullable: true }) @Field(() => Int, { nullable: true })
pageSize?: number pageSize?: number
@Field(() => Boolean, { nullable: true }) @Field(() => SearchUsersFilters, { nullable: true })
filterByActivated?: boolean | null filters: SearchUsersFilters
@Field(() => Boolean, { nullable: true })
filterByDeleted?: boolean | null
} }

View File

@ -0,0 +1,11 @@
import { Field, InputType, ObjectType } from 'type-graphql'
@ObjectType()
@InputType('SearchUsersFiltersInput')
export default class SearchUsersFilters {
@Field(() => Boolean, { nullable: true, defaultValue: null })
filterByActivated?: boolean | null
@Field(() => Boolean, { nullable: true, defaultValue: null })
filterByDeleted?: boolean | null
}

View File

@ -3,11 +3,11 @@ import { ArgsType, Field } from 'type-graphql'
@ArgsType() @ArgsType()
export default class TransactionLinkFilters { export default class TransactionLinkFilters {
@Field(() => Boolean, { nullable: true, defaultValue: true }) @Field(() => Boolean, { nullable: true, defaultValue: true })
withDeleted?: boolean filterByDeleted?: boolean
@Field(() => Boolean, { nullable: true, defaultValue: true }) @Field(() => Boolean, { nullable: true, defaultValue: true })
withExpired?: boolean filterByExpired?: boolean
@Field(() => Boolean, { nullable: true, defaultValue: true }) @Field(() => Boolean, { nullable: true, defaultValue: true })
withRedeemed?: boolean filterByRedeemed?: boolean
} }

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { convertObjValuesToArray } from '@/util/utilities'
import { testEnvironment, resetToken, cleanDB } from '@test/helpers' import { testEnvironment, resetToken, cleanDB } from '@test/helpers'
import { userFactory } from '@/seeds/factory/user' import { userFactory } from '@/seeds/factory/user'
import { creationFactory } from '@/seeds/factory/creation' import { creationFactory } from '@/seeds/factory/creation'
@ -11,6 +12,7 @@ import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
import { import {
deleteUser, deleteUser,
unDeleteUser, unDeleteUser,
searchUsers,
createPendingCreation, createPendingCreation,
createPendingCreations, createPendingCreations,
updatePendingCreation, updatePendingCreation,
@ -261,6 +263,224 @@ describe('AdminResolver', () => {
}) })
}) })
describe('search users', () => {
const variablesWithoutTextAndFilters = {
searchText: '',
currentPage: 1,
pageSize: 25,
filters: null,
}
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
query({
query: searchUsers,
variables: {
...variablesWithoutTextAndFilters,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
describe('without admin rights', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
it('returns an error', async () => {
await expect(
query({
query: searchUsers,
variables: {
...variablesWithoutTextAndFilters,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('with admin rights', () => {
const allUsers = {
bibi: expect.objectContaining({
email: 'bibi@bloxberg.de',
}),
garrick: expect.objectContaining({
email: 'garrick@ollivander.com',
}),
peter: expect.objectContaining({
email: 'peter@lustig.de',
}),
stephen: expect.objectContaining({
email: 'stephen@hawking.uk',
}),
}
beforeAll(async () => {
admin = await userFactory(testEnv, peterLustig)
await query({
query: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, stephenHawking)
await userFactory(testEnv, garrickOllivander)
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('without any filters', () => {
it('finds all users', async () => {
await expect(
query({
query: searchUsers,
variables: {
...variablesWithoutTextAndFilters,
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
searchUsers: {
userCount: 4,
userList: expect.arrayContaining(convertObjValuesToArray(allUsers)),
},
},
}),
)
})
})
describe('all filters are null', () => {
it('finds all users', async () => {
await expect(
query({
query: searchUsers,
variables: {
...variablesWithoutTextAndFilters,
filters: {
filterByActivated: null,
filterByDeleted: null,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
searchUsers: {
userCount: 4,
userList: expect.arrayContaining(convertObjValuesToArray(allUsers)),
},
},
}),
)
})
})
describe('filter by unchecked email', () => {
it('finds only users with unchecked email', async () => {
await expect(
query({
query: searchUsers,
variables: {
...variablesWithoutTextAndFilters,
filters: {
filterByActivated: false,
filterByDeleted: null,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
searchUsers: {
userCount: 1,
userList: expect.arrayContaining([allUsers.garrick]),
},
},
}),
)
})
})
describe('filter by deleted users', () => {
it('finds only users with deleted account', async () => {
await expect(
query({
query: searchUsers,
variables: {
...variablesWithoutTextAndFilters,
filters: {
filterByActivated: null,
filterByDeleted: true,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
searchUsers: {
userCount: 1,
userList: expect.arrayContaining([allUsers.stephen]),
},
},
}),
)
})
})
describe('filter by deleted account and unchecked email', () => {
it('finds no users', async () => {
await expect(
query({
query: searchUsers,
variables: {
...variablesWithoutTextAndFilters,
filters: {
filterByActivated: false,
filterByDeleted: true,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
searchUsers: {
userCount: 0,
userList: [],
},
},
}),
)
})
})
})
})
})
describe('creations', () => { describe('creations', () => {
const variables = { const variables = {
email: 'bibi@bloxberg.de', email: 'bibi@bloxberg.de',

View File

@ -52,23 +52,19 @@ export class AdminResolver {
@Query(() => SearchUsersResult) @Query(() => SearchUsersResult)
async searchUsers( async searchUsers(
@Args() @Args()
{ { searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs,
searchText,
currentPage = 1,
pageSize = 25,
filterByActivated = null,
filterByDeleted = null,
}: SearchUsersArgs,
): Promise<SearchUsersResult> { ): Promise<SearchUsersResult> {
const userRepository = getCustomRepository(UserRepository) const userRepository = getCustomRepository(UserRepository)
const filterCriteria: ObjectLiteral[] = [] const filterCriteria: ObjectLiteral[] = []
if (filterByActivated !== null) { if (filters) {
filterCriteria.push({ emailChecked: filterByActivated }) if (filters.filterByActivated !== null) {
} filterCriteria.push({ emailChecked: filters.filterByActivated })
}
if (filterByDeleted !== null) { if (filters.filterByDeleted !== null) {
filterCriteria.push({ deletedAt: filterByDeleted ? Not(IsNull()) : IsNull() }) filterCriteria.push({ deletedAt: filters.filterByDeleted ? Not(IsNull()) : IsNull() })
}
} }
const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked', 'deletedAt'] const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked', 'deletedAt']
@ -442,11 +438,11 @@ export class AdminResolver {
} = { } = {
userId, userId,
} }
if (!filters.withRedeemed) where.redeemedBy = null if (!filters.filterByRedeemed) where.redeemedBy = null
if (!filters.withExpired) where.validUntil = MoreThan(new Date()) if (!filters.filterByExpired) where.validUntil = MoreThan(new Date())
const [transactionLinks, count] = await dbTransactionLink.findAndCount({ const [transactionLinks, count] = await dbTransactionLink.findAndCount({
where, where,
withDeleted: filters.withDeleted, withDeleted: filters.filterByDeleted,
order: { order: {
createdAt: order, createdAt: order,
}, },

View File

@ -107,6 +107,35 @@ export const unDeleteUser = gql`
} }
` `
export const searchUsers = gql`
query (
$searchText: String!
$currentPage: Int
$pageSize: Int
$filters: SearchUsersFiltersInput
) {
searchUsers(
searchText: $searchText
currentPage: $currentPage
pageSize: $pageSize
filters: $filters
) {
userCount
userList {
userId
firstName
lastName
email
creation
emailChecked
hasElopage
emailConfirmationSend
deletedAt
}
}
}
`
export const createPendingCreations = gql` export const createPendingCreations = gql`
mutation ($pendingCreations: [CreatePendingCreationArgs!]!) { mutation ($pendingCreations: [CreatePendingCreationArgs!]!) {
createPendingCreations(pendingCreations: $pendingCreations) { createPendingCreations(pendingCreations: $pendingCreations) {

View File

@ -29,7 +29,7 @@ const context = {
} }
export const cleanDB = async () => { export const cleanDB = async () => {
// this only works as lond we do not have foreign key constraints // this only works as long we do not have foreign key constraints
for (let i = 0; i < entities.length; i++) { for (let i = 0; i < entities.length; i++) {
await resetEntity(entities[i]) await resetEntity(entities[i])
} }

View File

@ -0,0 +1,5 @@
export const convertObjValuesToArray = (obj: { [x: string]: string }): Array<string> => {
return Object.keys(obj).map(function (key) {
return obj[key]
})
}