-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add Badge api #357
Open
jhk482001
wants to merge
9
commits into
master
Choose a base branch
from
badge-api
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
add Badge api #357
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
91c69b3
add badge-api
jhk482001 2ff4870
add badge-api , fix fixtures issue
jhk482001 2067348
add graphql for badge , update user's model
jhk482001 350e3b2
fix: lint
MrOrz 20177e7
refactor(graphql): simplify user badges field type by default to empt…
MrOrz 497610b
fix(awardBadge): fix test fixtures and unit test
MrOrz 227aaeb
move badge api to adm api folder
jhk482001 8431d58
add checking issuer for awarding badge
jhk482001 ca9c1ee
remove test env file, update User's model
jhk482001 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import type { User } from 'rumors-db/schema/users'; | ||
import type { Badge } from 'rumors-db/schema/badges'; | ||
|
||
export default { | ||
'/users/doc/user-to-award-badge': { | ||
name: 'user-to-award-badge', | ||
createdAt: '2020-01-01T00:00:00.000Z', | ||
googleId: 'some-google-id', | ||
badges: [], | ||
} satisfies User, | ||
|
||
'/users/doc/user-already-award-badge': { | ||
name: 'user-already-award-badge', | ||
createdAt: '2020-01-01T00:00:00.000Z', | ||
googleId: 'some-google-id', | ||
badges: [ | ||
{ | ||
badgeId: 'test-certification-001', | ||
badgeMetaData: '{"from":"some-orgnization"}', | ||
isDisplayed: false, | ||
createdAt: '2020-01-01T00:00:00.000Z', | ||
updatedAt: '2020-01-01T00:00:00.000Z', | ||
}, | ||
], | ||
} satisfies User, | ||
|
||
'/badges/doc/test-certification-001': { | ||
name: 'Test Certification', | ||
displayName: 'Test Certification', | ||
description: 'A test certification badge', | ||
link: 'https://badge.source.com', | ||
icon: 'https://badge.source.com/icon.png', | ||
borderImage: 'https://badge.source.com/border.png', | ||
issuers: ['[email protected]', 'service-token-123'], | ||
createdAt: '2020-01-01T00:00:00.000Z', | ||
updatedAt: '2020-01-01T00:00:00.000Z', | ||
} satisfies Badge, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import MockDate from 'mockdate'; | ||
|
||
import { loadFixtures, unloadFixtures } from 'util/fixtures'; | ||
import client from 'util/client'; | ||
import awardBadge from '../awardBadge'; | ||
import fixtures from '../__fixtures__/awardBadge'; | ||
|
||
const FIXED_DATE = 612921600000; | ||
|
||
beforeEach(async () => { | ||
await loadFixtures(fixtures); | ||
MockDate.set(FIXED_DATE); | ||
}); | ||
|
||
afterEach(async () => { | ||
await unloadFixtures(fixtures); | ||
MockDate.reset(); | ||
}); | ||
|
||
describe('awardBadge', () => { | ||
it('fails if userId is not valid', async () => { | ||
await expect( | ||
awardBadge({ | ||
userId: 'not-exist', | ||
badgeId: 'badge id', | ||
badgeMetaData: '{}', | ||
request: { userId: '[email protected]' }, | ||
}) | ||
).rejects.toMatchInlineSnapshot( | ||
`[HTTPError: User with ID=not-exist does not exist]` | ||
); | ||
}); | ||
|
||
/** | ||
* Asserts the document in database is the same as in the fixture, | ||
* i.e. the document is not modified | ||
* | ||
* @param {string} fixtureKey | ||
* @param {{index: string; id: string;}} clientGetArgs - Arguments for client.get() | ||
*/ | ||
|
||
it('correctly sets the awarded badge id when authorized', async () => { | ||
const result = await awardBadge({ | ||
userId: 'user-to-award-badge', | ||
badgeId: 'test-certification-001', | ||
badgeMetaData: '{"from":"some-orgnization"}', | ||
request: { userId: '[email protected]' }, | ||
}); | ||
|
||
expect(result).toMatchInlineSnapshot(` | ||
Object { | ||
"badgeId": "test-certification-001", | ||
"badgeMetaData": "{\\"from\\":\\"some-orgnization\\"}", | ||
} | ||
`); | ||
|
||
const { | ||
body: { _source: userWithBadge }, | ||
} = await client.get({ | ||
index: 'users', | ||
type: 'doc', | ||
id: 'user-to-award-badge', | ||
}); | ||
|
||
// Assert that badgeId is written on the user | ||
expect(userWithBadge).toMatchInlineSnapshot(` | ||
Object { | ||
"badges": Array [ | ||
Object { | ||
"badgeId": "test-certification-001", | ||
"badgeMetaData": "{\\"from\\":\\"some-orgnization\\"}", | ||
"createdAt": "1989-06-04T00:00:00.000Z", | ||
"isDisplayed": true, | ||
"updatedAt": "1989-06-04T00:00:00.000Z", | ||
}, | ||
], | ||
"createdAt": "2020-01-01T00:00:00.000Z", | ||
"googleId": "some-google-id", | ||
"name": "user-to-award-badge", | ||
} | ||
`); | ||
}); | ||
|
||
it('allows service token to award badge', async () => { | ||
const result = await awardBadge({ | ||
userId: 'user-to-award-badge', | ||
badgeId: 'test-certification-001', | ||
badgeMetaData: '{"from":"service"}', | ||
request: { userId: 'service-token-123' }, | ||
}); | ||
|
||
expect(result).toMatchInlineSnapshot(` | ||
Object { | ||
"badgeId": "test-certification-001", | ||
"badgeMetaData": "{\\"from\\":\\"service\\"}", | ||
} | ||
`); | ||
|
||
const { | ||
body: { _source: userWithBadge }, | ||
} = await client.get({ | ||
index: 'users', | ||
type: 'doc', | ||
id: 'user-to-award-badge', | ||
}); | ||
|
||
expect(userWithBadge.badges).toHaveLength(1); | ||
expect(userWithBadge.badges[0].badgeId).toBe('test-certification-001'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
/** | ||
* Given userId & award badge (Id and metadata). | ||
* | ||
*/ | ||
import { HTTPError } from 'fets'; | ||
|
||
import client from 'util/client'; | ||
|
||
/** | ||
* Update user to write badgeId. Throws if user does not exist. | ||
* | ||
* @param userId | ||
* @param badgeId | ||
* @param badgeMetaData | ||
*/ | ||
async function appendBadgeToList( | ||
userId: string, | ||
badgeId: string, | ||
badgeMetaData: string | ||
) { | ||
const now = new Date().toISOString(); | ||
|
||
try { | ||
const { | ||
body: { result: setbadgeIdResult }, | ||
} = await client.update({ | ||
index: 'users', | ||
type: 'doc', | ||
id: userId, | ||
body: { | ||
script: { | ||
source: ` | ||
if (ctx._source.badges == null) { | ||
ctx._source.badges = []; | ||
} | ||
ctx._source.badges.add(params.badge); | ||
`, | ||
params: { | ||
badge: { | ||
badgeId: badgeId, | ||
badgeMetaData: badgeMetaData, | ||
createdAt: now, | ||
isDisplayed: true, | ||
updatedAt: now, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
/* istanbul ignore if */ | ||
if (setbadgeIdResult === 'noop') { | ||
console.log(`Info: user ID ${userId} already has set the same badgeId.`); | ||
} | ||
} catch (e) { | ||
console.log(e); | ||
/* istanbul ignore else */ | ||
if ( | ||
e && | ||
typeof e === 'object' && | ||
'message' in e && | ||
e.message === 'document_missing_exception' | ||
) { | ||
throw new HTTPError(400, `User with ID=${userId} does not exist`); | ||
} | ||
|
||
throw e; | ||
} | ||
} | ||
|
||
/** | ||
* Verify if the badge exists and if the current user is authorized to issue it | ||
* | ||
* @param badgeId - ID of the badge to verify | ||
* @param requestUserId - ID of the user making the request | ||
* @throws {HTTPError} if badge doesn't exist or user is not authorized | ||
*/ | ||
async function verifyBadgeIssuer(badgeId: string, requestUserId: string) { | ||
try { | ||
const { | ||
body: { _source: badge }, | ||
} = await client.get({ | ||
index: 'badges', | ||
type: 'doc', | ||
id: badgeId, | ||
}); | ||
|
||
if (!badge) { | ||
throw new HTTPError(404, `Badge with ID=${badgeId} does not exist`); | ||
} | ||
|
||
if (!badge.issuers?.includes(requestUserId)) { | ||
throw new HTTPError( | ||
403, | ||
`User ${requestUserId} is not authorized to issue badge ${badgeId}` | ||
); | ||
} | ||
} catch (e) { | ||
if (e instanceof HTTPError) throw e; | ||
throw new HTTPError(404, `Badge with ID=${badgeId} does not exist`); | ||
} | ||
} | ||
|
||
type awardBadgeReturnValue = { | ||
badgeId: string; | ||
badgeMetaData: string; | ||
}; | ||
|
||
async function main({ | ||
userId, | ||
badgeId, | ||
badgeMetaData, | ||
request, | ||
}: { | ||
userId: string; | ||
badgeId: string; | ||
badgeMetaData: string; | ||
request: { userId: string }; | ||
}): Promise<awardBadgeReturnValue> { | ||
// Check if user exists first | ||
try { | ||
const { body } = await client.get({ | ||
index: 'users', | ||
type: 'doc', | ||
id: userId, | ||
}); | ||
if (!body._source) { | ||
throw new HTTPError(400, `User with ID=${userId} does not exist`); | ||
} | ||
} catch (e) { | ||
if (e instanceof HTTPError) throw e; | ||
throw new HTTPError(400, `User with ID=${userId} does not exist`); | ||
} | ||
|
||
// Verify if the current user/service is authorized to issue this badge | ||
await verifyBadgeIssuer(badgeId, request.userId); | ||
|
||
await appendBadgeToList(userId, badgeId, badgeMetaData); | ||
|
||
return { | ||
badgeId, | ||
badgeMetaData, | ||
}; | ||
} | ||
|
||
export default main; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import DataLoader from 'dataloader'; | ||
import client, { processMeta } from 'util/client'; | ||
|
||
export default () => | ||
new DataLoader(async (slugs) => { | ||
const body = []; | ||
|
||
slugs.forEach(({ slug }) => { | ||
body.push({ index: 'badges', type: 'doc' }); | ||
|
||
body.push({ | ||
query: { | ||
term: { slug }, | ||
}, | ||
size: 1, | ||
}); | ||
}); | ||
|
||
return ( | ||
await client.msearch({ | ||
body, | ||
}) | ||
).body.responses.map(({ hits }) => { | ||
if (!hits || !hits.hits || hits.hits.length == 0) return null; | ||
return processMeta(hits.hits[0]); | ||
}); | ||
}); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: why is this change needed?