mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
basic specs for inviteCodes
This commit is contained in:
parent
a1967815bf
commit
dd6cafed4c
@ -149,7 +149,7 @@ export default shield(
|
||||
pinPost: isAdmin,
|
||||
unpinPost: isAdmin,
|
||||
UpdateDonations: isAdmin,
|
||||
CreateInviteCode: isAuthenticated,
|
||||
GenerateInviteCode: isAuthenticated,
|
||||
},
|
||||
User: {
|
||||
email: or(isMyOwn, isAdmin),
|
||||
|
||||
@ -100,15 +100,15 @@ export default {
|
||||
target: 'User',
|
||||
direction: 'in',
|
||||
},
|
||||
createdInvite: {
|
||||
inviteCodes: {
|
||||
type: 'relationship',
|
||||
relationship: 'CREATED',
|
||||
relationship: 'GENERATED',
|
||||
target: 'InviteCode',
|
||||
direction: 'out',
|
||||
},
|
||||
usedInvite: {
|
||||
redeemedInviteCode: {
|
||||
type: 'relationship',
|
||||
relationship: 'USED',
|
||||
relationship: 'REDEEMED',
|
||||
target: 'InviteCode',
|
||||
direction: 'out',
|
||||
},
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export default function generateInviteCode() {
|
||||
return Array.from({length: 6}, (n = Math.floor(Math.random() * 36)) => {
|
||||
return Array.from({ length: 6 }, (n = Math.floor(Math.random() * 36)) => {
|
||||
return String.fromCharCode(n > 9 ? n + 55 : n + 48)
|
||||
}).join('')
|
||||
}).join('')
|
||||
}
|
||||
|
||||
@ -1,65 +1,60 @@
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import generateInvieCode from './helpers/generateInviteCode'
|
||||
import Resolver from './helpers/Resolver'
|
||||
|
||||
const uniqueInviteCode = async (session, code) => {
|
||||
return session.readTransaction(async (txc) => {
|
||||
const result = await txc.run(
|
||||
`MATCH (ic:InviteCode { id: $code }) RETURN count(ic) AS count`,
|
||||
{ code },
|
||||
)
|
||||
return parseInt(String(result.records[0].get('count'))) === 0
|
||||
const result = await txc.run(`MATCH (ic:InviteCode { id: $code }) RETURN count(ic) AS count`, {
|
||||
code,
|
||||
})
|
||||
return parseInt(String(result.records[0].get('count'))) === 0
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateInviteCode: async (_parent, args, context, _resolveInfo) => {
|
||||
GenerateInviteCode: async (_parent, args, context, _resolveInfo) => {
|
||||
const {
|
||||
user: { id: userId },
|
||||
} = context
|
||||
const session = context.driver.session()
|
||||
let code = generateInvieCode()
|
||||
let response
|
||||
while(!await uniqueInviteCode(session, code)) {
|
||||
while (!(await uniqueInviteCode(session, code))) {
|
||||
code = generateInvieCode()
|
||||
}
|
||||
const writeTxResultPromise = session.writeTransaction(async (txc) => {
|
||||
const result = await txc.run(
|
||||
`MATCH (user:User {id: $userId})
|
||||
MERGE (user)-[:CREATED]->(ic:InviteCode {
|
||||
code: $code,
|
||||
createdAt: toString(datetime()),
|
||||
uses: $uses,
|
||||
maxUses: $maxUses,
|
||||
active: true
|
||||
}) RETURN ic AS inviteCode`,
|
||||
MERGE (user)-[:GENERATED]->(ic:InviteCode { code: $code })
|
||||
ON CREATE SET
|
||||
ic.createdAt = toString(datetime()),
|
||||
ic.expiresAt = $expiresAt
|
||||
RETURN ic AS inviteCode`,
|
||||
{
|
||||
userId,
|
||||
code,
|
||||
maxUses: args.maxUses,
|
||||
uses: 0,
|
||||
expiresAt: args.expiresAt,
|
||||
},
|
||||
)
|
||||
return result.records.map((record) => record.get('inviteCode').properties)
|
||||
})
|
||||
try {
|
||||
const txResult = await writeTxResultPromise
|
||||
console.log(txResult)
|
||||
response = txResult[0]
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return response
|
||||
}
|
||||
},
|
||||
},
|
||||
InviteCode: {
|
||||
...Resolver('InviteCode', {
|
||||
undefinedToNull: ['expiresAt'],
|
||||
hasOne: {
|
||||
createdBy: '<-[:CREATED]-(related:User)',
|
||||
generatedBy: '<-[:GENERATED]-(related:User)',
|
||||
},
|
||||
hasMany: {
|
||||
usedBy: '<-[:USED]-(related:User)',
|
||||
redeemedBy: '<-[:REDEEMED]-(related:User)',
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
113
backend/src/schema/resolvers/inviteCodes.spec.js
Normal file
113
backend/src/schema/resolvers/inviteCodes.spec.js
Normal file
@ -0,0 +1,113 @@
|
||||
import Factory, { cleanDatabase } from '../../db/factories'
|
||||
import { getDriver } from '../../db/neo4j'
|
||||
import { gql } from '../../helpers/jest'
|
||||
import createServer from '../../server'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
|
||||
let user
|
||||
// let query
|
||||
let mutate
|
||||
|
||||
const driver = getDriver()
|
||||
|
||||
const generateInviteCodeMutation = gql`
|
||||
mutation($expiresAt: String = null) {
|
||||
GenerateInviteCode(expiresAt: $expiresAt) {
|
||||
code
|
||||
createdAt
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
user,
|
||||
}
|
||||
},
|
||||
})
|
||||
// query = createTestClient(server).query
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
describe('inviteCodes', () => {
|
||||
describe('generate invite code', () => {
|
||||
describe('as unauthenticated user', () => {
|
||||
it('returns permission denied error', async () => {
|
||||
await expect(mutate({ mutation: generateInviteCodeMutation })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
extensions: { code: 'INTERNAL_SERVER_ERROR' },
|
||||
}),
|
||||
]),
|
||||
data: {
|
||||
GenerateInviteCode: null,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('as authenticated user', () => {
|
||||
beforeAll(async () => {
|
||||
const authenticatedUser = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
role: 'user',
|
||||
},
|
||||
{
|
||||
email: 'user@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
user = await authenticatedUser.toJson()
|
||||
})
|
||||
|
||||
it('generates an invite code without expiresAt', async () => {
|
||||
await expect(mutate({ mutation: generateInviteCodeMutation })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: undefined,
|
||||
data: {
|
||||
GenerateInviteCode: {
|
||||
code: expect.stringMatching(/^[0-9A-Z]{6,6}$/),
|
||||
expiresAt: null,
|
||||
createdAt: expect.any(String),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('generates an invite code with expiresAt', async () => {
|
||||
const nextWeek = new Date()
|
||||
nextWeek.setDate(nextWeek.getDate() + 7)
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: generateInviteCodeMutation,
|
||||
variables: { expiresAt: nextWeek.toISOString() },
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: undefined,
|
||||
data: {
|
||||
GenerateInviteCode: {
|
||||
code: expect.stringMatching(/^[0-9A-Z]{6,6}$/),
|
||||
expiresAt: nextWeek.toISOString(),
|
||||
createdAt: expect.any(String),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -293,7 +293,7 @@ export default {
|
||||
avatar: '-[:AVATAR_IMAGE]->(related:Image)',
|
||||
invitedBy: '<-[:INVITED]-(related:User)',
|
||||
location: '-[:IS_IN]->(related:Location)',
|
||||
usedInviteCode: '-[:USED]->(related:InviteCode)',
|
||||
redeemedInviteCode: '-[:REDEEMED]->(related:InviteCode)',
|
||||
},
|
||||
hasMany: {
|
||||
followedBy: '<-[:FOLLOWS]-(related:User)',
|
||||
@ -305,7 +305,7 @@ export default {
|
||||
shouted: '-[:SHOUTED]->(related:Post)',
|
||||
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||
badges: '<-[:REWARDED]-(related:Badge)',
|
||||
inviteCodes: '-[:CREATED]->(related:InviteCode)',
|
||||
inviteCodes: '-[:GENERATED]->(related:InviteCode)',
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
type InviteCode {
|
||||
code: ID!
|
||||
createdAt: String
|
||||
uses: Int!
|
||||
maxUses: Int!
|
||||
createdBy: User @relation(name: "CREATED", direction: "IN")
|
||||
usedBy: [User] @relation(name: "USED", direction: "IN")
|
||||
active: Boolean!
|
||||
generatedBy: User @relation(name: "GENERATED", direction: "IN")
|
||||
redeemedBy: [User] @relation(name: "REDEEMED", direction: "IN")
|
||||
expiresAt: String
|
||||
}
|
||||
|
||||
|
||||
type Mutation {
|
||||
CreateInviteCode(maxUses: Int = 1): InviteCode
|
||||
GenerateInviteCode(expiresAt: String = null): InviteCode
|
||||
}
|
||||
|
||||
@ -56,8 +56,8 @@ type User {
|
||||
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
|
||||
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)")
|
||||
|
||||
inviteCodes: [InviteCode] @relation(name: "CREATED", direction: "OUT")
|
||||
usedInviteCode: InviteCode @relation(name: "USED", direction: "OUT")
|
||||
inviteCodes: [InviteCode] @relation(name: "GENERATED", direction: "OUT")
|
||||
redeemedInviteCode: InviteCode @relation(name: "REDEEMED", direction: "OUT")
|
||||
|
||||
# Is the currently logged in user following that user?
|
||||
followedByCurrentUser: Boolean! @cypher(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user