Skip to content

Commit

Permalink
feat: add editor role requirement to import endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Silthus committed Feb 9, 2024
1 parent 8a76076 commit d09167a
Show file tree
Hide file tree
Showing 10 changed files with 341 additions and 65 deletions.
129 changes: 129 additions & 0 deletions src/__tests__/import-example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
{
"areas": [
{
"areaName": "Utah",
"countryCode": "us",
"children": [
{
"areaName": "Southeast Utah",
"children": [
{
"areaName": "Indian Creek",
"children": [
{
"areaName": "Supercrack Buttress",
"climbs": [
{
"name": "The Key Flake",
"grade": "5.10",
"fa": "unknown",
"type": {
"trad": true
},
"safety": "UNSPECIFIED",
"metadata": {
"lnglat": {
"type": "Point",
"coordinates": [
-109.54552,
38.03635
]
},
"left_right_index": 1
},
"content": {
"description": "Cool off-width that requires off-width and face skills.",
"protection": "Anchors hidden up top. Need 80m to make it all the way down.",
"location": "Opposite keyhole flake. Obvious right leaning offwidth that starts atop 20 ft boulder."
}
},
{
"name": "Incredible Hand Crack",
"grade": "5.10",
"fa": "Rich Perch, John Bragg, Doug Snively, and Anne Tarver, 1978",
"type": {
"trad": true
},
"safety": "UNSPECIFIED",
"metadata": {
"lnglat": {
"type": "Point",
"coordinates": [
-109.54552,
38.03635
]
},
"left_right_index": 2
},
"content": {
"description": "Route starts at the top of the trail from the parking lot to Supercrack Buttress.",
"protection": "Cams from 2-2.5\". Heavy on 2.5\" (#2 Camalot)",
"location": ""
},
"pitches": [
{
"pitchNumber": 1,
"grade": "5.10",
"disciplines": {
"trad": true
},
"length": 100,
"boltsCount": 0,
"description": "A classic hand crack that widens slightly towards the top. Requires a range of cam sizes. Sustained and excellent quality."
},
{
"pitchNumber": 2,
"grade": "5.9",
"disciplines": {
"trad": true
},
"length": 30,
"boltsCount": 0,
"description": "Easier climbing with good protection. Features a mix of crack sizes. Shorter than the first pitch but equally enjoyable."
}
]
}
],
"gradeContext": "US",
"metadata": {
"isBoulder": false,
"isDestination": false,
"leaf": true,
"lnglat": {
"type": "Point",
"coordinates": [
-109.54552,
38.03635
]
},
"bbox": [
-109.54609091005857,
38.03590033981814,
-109.54494908994141,
38.03679966018186
]
},
"content": {
"description": ""
}
}
],
"metadata": {
"lnglat": {
"type": "Point",
"coordinates": [
-109.5724044642857,
38.069429035714286
]
}
},
"content": {
"description": "Indian Creek is a crack climbing mecca in the southeastern region of Utah, USA. Located within the [Bears Ears National Monument](https://en.wikipedia.org/wiki/Bears_Ears_National_Monument)."
}
}
]
}
]
}
]
}
106 changes: 106 additions & 0 deletions src/__tests__/import.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {ApolloServer} from "apollo-server-express";
import muuid from "uuid-mongodb";
import express from "express";
import {InMemoryDB} from "../utils/inMemoryDB.js";
import {queryAPI, setUpServer} from "../utils/testUtils.js";
import {muuidToString} from "../utils/helpers.js";
import MutableAreaDataSource from "../model/MutableAreaDataSource.js";
import exampleImportData from './import-example.json' assert {type: 'json'};
import {AreaType} from "../db/AreaTypes.js";
import {BulkImportResult} from "../db/import/json/import-json";

describe('/import', () => {
const endpoint = '/import'
let server: ApolloServer
let user: muuid.MUUID
let userUuid: string
let app: express.Application
let inMemoryDB: InMemoryDB
let testArea: AreaType

let areas: MutableAreaDataSource

beforeAll(async () => {
({server, inMemoryDB, app} = await setUpServer())
// Auth0 serializes uuids in "relaxed" mode, resulting in this hex string format
// "59f1d95a-627d-4b8c-91b9-389c7424cb54" instead of base64 "WfHZWmJ9S4yRuTicdCTLVA==".
user = muuid.mode('relaxed').v4()
userUuid = muuidToString(user)
})

beforeEach(async () => {
await inMemoryDB.clear()
areas = MutableAreaDataSource.getInstance()
await areas.addCountry('usa')
testArea = await areas.addArea(user, "Test Area", null, "us")
})

afterAll(async () => {
await server.stop()
await inMemoryDB.close()
})

it('should return 403 if no user', async () => {
const res = await queryAPI({
app,
endpoint,
body: exampleImportData
})
expect(res.status).toBe(403)
expect(res.text).toBe('Forbidden')
})

it('should return 403 if user is not an editor', async () => {
const res = await queryAPI({
app,
endpoint,
userUuid,
body: exampleImportData
})
expect(res.status).toBe(403)
expect(res.text).toBe('Forbidden')
})

it('should return 200 if user is an editor', async () => {
const res = await queryAPI({
app,
endpoint,
userUuid,
roles: ['editor'],
body: exampleImportData
})
expect(res.status).toBe(200)
})

it('should import data', async () => {
const res = await queryAPI({
app,
endpoint,
userUuid,
roles: ['editor'],
body: {
areas: [
...exampleImportData.areas,
{
id: testArea.metadata.area_id.toUUID().toString(),
areaName: "Updated Test Area",
}
],
},
});
expect(res.status).toBe(200)

const result = res.body as BulkImportResult
expect(result.addedAreaIds.length).toBe(4)

const committedAreas = await Promise.all(result.addedAreaIds.map((areaId: string) => areas.findOneAreaByUUID(muuid.from(areaId))));
expect(committedAreas.length).toBe(4);

const committedClimbs = await Promise.all(result.climbIds.map((id: string) => areas.findOneClimbByUUID(muuid.from(id))));
expect(committedClimbs.length).toBe(2);

const updatedAreas = await Promise.all(result.updatedAreaIds.map((areaId: any) => areas.findOneAreaByUUID(muuid.from(areaId))));
expect(updatedAreas.length).toBe(1);
expect(updatedAreas[0].area_name).toBe("Updated Test Area");
})
});
11 changes: 10 additions & 1 deletion src/auth/rules.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { rule, inputRule } from 'graphql-shield'
import { inputRule, rule } from 'graphql-shield'

import MediaDataSource from '../model/MutableMediaDataSource.js'
import { MediaObjectGQLInput } from '../db/MediaObjectTypes.js'
Expand All @@ -7,6 +7,15 @@ export const isEditor = rule()(async (parent, args, ctx, info) => {
return _hasUserUuid(ctx) && ctx.user.roles.includes('editor')
})

export const hasEditorRoleMiddleware = async (req, res, next): Promise<void> => {
const roles: string[] = req.user?.roles ?? []
if (_hasUserUuid(req) && roles.includes('editor')) {
next()
} else {
res.status(403).send('Forbidden')
}
}

export const isUserAdmin = rule()(async (parent, args, ctx, info) => {
return _hasUserUuid(ctx) && ctx.user.roles.includes('user_admin')
})
Expand Down
27 changes: 15 additions & 12 deletions src/db/import/json/__tests__/import-json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {ClimbType} from '../../../ClimbTypes.js';
import streamListener from '../../../edit/streamListener.js';
import {AreaJson, bulkImportJson, BulkImportResult} from '../import-json.js';
import inMemoryDB from "../../../../utils/inMemoryDB.js";
import {isFulfilled, isRejected} from "../../../../utils/testUtils.js";

type TestResult = BulkImportResult & {
addedAreas: Partial<AreaType>[];
updatedAreas: Partial<AreaType>[];
addedClimbs: Partial<ClimbType>[];
};

Expand All @@ -27,15 +29,14 @@ describe('bulk import e2e', () => {
areas,
});

const isFulfilled = <T>(
p: PromiseSettledResult<T>
): p is PromiseFulfilledResult<T> => p.status === 'fulfilled';
const isRejected = <T>(
p: PromiseSettledResult<T>
): p is PromiseRejectedResult => p.status === 'rejected';
const committedAreas = await Promise.allSettled(
result.addedAreas.map((area) =>
areas.findOneAreaByUUID(area.metadata.area_id)
const addedAreas = await Promise.allSettled(
result.addedAreaIds.map((areaId) =>
areas.findOneAreaByUUID(muuid.from(areaId))
)
);
const updatedAreas = await Promise.allSettled(
result.updatedAreaIds.map((areaId) =>
areas.findOneAreaByUUID(muuid.from(areaId))
)
);
const committedClimbs = await Promise.allSettled(
Expand All @@ -46,10 +47,12 @@ describe('bulk import e2e', () => {
...result,
errors: [
...result.errors,
...committedAreas.filter(isRejected).map((p) => p.reason),
...addedAreas.filter(isRejected).map((p) => p.reason),
...committedClimbs.filter(isRejected).map((p) => p.reason),
...updatedAreas.filter(isRejected).map((p) => p.reason),
],
addedAreas: committedAreas.filter(isFulfilled).map((p) => p.value),
addedAreas: addedAreas.filter(isFulfilled).map((p) => p.value),
updatedAreas: updatedAreas.filter(isFulfilled).map((p) => p.value),
addedClimbs: committedClimbs
.filter(isFulfilled)
.map((p) => p.value as Partial<ClimbType>),
Expand Down Expand Up @@ -241,7 +244,7 @@ describe('bulk import e2e', () => {
})
).resolves.toMatchObject({
errors: [],
addedAreas: [{area_name: 'New Name'}],
updatedAreas: [{area_name: 'New Name'}],
});
});
});
Expand Down
Loading

0 comments on commit d09167a

Please sign in to comment.