Skip to content

Commit

Permalink
feat: introduce a new site info API endpoint (#513)
Browse files Browse the repository at this point in the history
* ref(fixtures): convert repoInfo to typescript

* ref(services): migrate SitesService to typescript

* tests: update unit and integration tests for SitesService

* ref(sites): migrate sites router to typescript

* fix: revert back to using SessionData

* fix: remove use of Bluebird and unused getSiteToken function

* fix: use more accurate type

* chore: remove unused variable

* refactor(tests): migrate generic axios instance to __mocks__

* feat: introduce function to obtain latest commit details

* feat: add function for obtaining a User by ID

* feat: introduce a new site info API endpoint

* tests: add partial tests for SitesService

* tests: use mockAxios directly instead of preparing an instance

* tests: fix SitesService unit tests to pass

* chore: adjust constants to use SCREAMING_SNAKE_CASE

* fix: add authorizationMiddleware to ensure user is member of site

* chore: combine sessionData unpacking

* fix: insert try-catch to handle errors from JSON.parse

* chore: remove unnecessary check for undefined site

* chore: return instead of throwing NotFoundError

* fix: add assertion to ensure integrity of GitHubCommitData

* fix: remove need for adding site name to sessionData

* refactor: convert routes Sites.spec.js to TypeScript

* refactor: redesign getUrlsOfSite to increase readability

* fix: use correct endpoint to get latest commit data

* test: add unit tests for GitHubService getLatestCommitOfBranch

* fix: add stub for obtaining merge author details

* fix: return a well-formatted response for known exceptions

* test: enhance GitHubService test for all other error statuses

* chore: rename isType function and return boolean directly

* fix: create new siteUrls object instead of changing in-place

* fix: handle case of null or undefined user email

* chore: improve code style

* tests: fix output of getStagingUrl
  • Loading branch information
dcshzj authored Oct 14, 2022
1 parent c84c64c commit 7c6c4fa
Show file tree
Hide file tree
Showing 13 changed files with 899 additions and 102 deletions.
24 changes: 24 additions & 0 deletions src/fixtures/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Attributes } from "sequelize/types"

import { User, SiteMember } from "@database/models"

import { mockIsomerUserId } from "./sessionData"

export const mockRecipient = "[email protected]"
export const mockSubject = "mock subject"
export const mockBody = "somebody"
Expand Down Expand Up @@ -138,3 +140,25 @@ export const mockSiteOrmResponseWithNoCollaborators = {
id: 1,
site_members: "",
}

export const MOCK_COMMIT_MESSAGE_ONE = "Update file: Example.md"
export const MOCK_COMMIT_FILENAME_ONE = "Example.md"
export const MOCK_GITHUB_NAME_ONE = "testuser"
export const MOCK_GITHUB_EMAIL_ADDRESS_ONE = "[email protected]"
export const MOCK_GITHUB_DATE_ONE = "2022-09-22T04:07:53Z"
export const MOCK_COMMIT_MESSAGE_OBJECT_ONE = {
message: MOCK_COMMIT_MESSAGE_ONE,
fileName: MOCK_COMMIT_FILENAME_ONE,
userId: mockIsomerUserId,
}

export const MOCK_COMMIT_MESSAGE_TWO = "Update file: Test.md"
export const MOCK_COMMIT_FILENAME_TWO = "Test.md"
export const MOCK_GITHUB_NAME_TWO = "testuser2"
export const MOCK_GITHUB_EMAIL_ADDRESS_TWO = "[email protected]"
export const MOCK_GITHUB_DATE_TWO = "2022-09-28T06:25:14Z"
export const MOCK_COMMIT_MESSAGE_OBJECT_TWO = {
message: MOCK_COMMIT_MESSAGE_TWO,
fileName: MOCK_COMMIT_FILENAME_TWO,
userId: mockIsomerUserId,
}
30 changes: 27 additions & 3 deletions src/integration/Sites.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@ import express from "express"
import mockAxios from "jest-mock-axios"
import request from "supertest"

import { IsomerAdmin, Repo, Site, SiteMember, User } from "@database/models"
import {
IsomerAdmin,
Repo,
Site,
SiteMember,
User,
Whitelist,
} from "@database/models"
import { generateRouter } from "@fixtures/app"
import UserSessionData from "@root/classes/UserSessionData"
import { mockEmail, mockIsomerUserId } from "@root/fixtures/sessionData"
import { getAuthorizationMiddleware } from "@root/middleware"
import { SitesRouter as _SitesRouter } from "@root/routes/v2/authenticated/sites"
import { GitHubService } from "@root/services/db/GitHubService"
import { ConfigYmlService } from "@root/services/fileServices/YmlFileServices/ConfigYmlService"
import IsomerAdminsService from "@root/services/identity/IsomerAdminsService"
import SitesService from "@root/services/identity/SitesService"
import TokenStore from "@root/services/identity/TokenStore"
import { getUsersService } from "@services/identity"
import { getIdentityAuthService, getUsersService } from "@services/identity"
import CollaboratorsService from "@services/identity/CollaboratorsService"
import { sequelize } from "@tests/database"

const mockSite = "mockSite"
Expand All @@ -27,6 +36,7 @@ const configYmlService = new ConfigYmlService({ gitHubService })
const usersService = getUsersService(sequelize)
const isomerAdminsService = new IsomerAdminsService({ repository: IsomerAdmin })
const tokenStore = new TokenStore()
const identityAuthService = getIdentityAuthService(gitHubService)
const sitesService = new SitesService({
siteRepository: Site,
gitHubService,
Expand All @@ -35,8 +45,22 @@ const sitesService = new SitesService({
isomerAdminsService,
tokenStore,
})
const collaboratorsService = new CollaboratorsService({
siteRepository: Site,
siteMemberRepository: SiteMember,
sitesService,
usersService,
whitelist: Whitelist,
})

const authorizationMiddleware = getAuthorizationMiddleware({
identityAuthService,
usersService,
isomerAdminsService,
collaboratorsService,
})

const SitesRouter = new _SitesRouter({ sitesService })
const SitesRouter = new _SitesRouter({ sitesService, authorizationMiddleware })
const sitesSubrouter = SitesRouter.getRouter()

// Set up express with defaults and use the router under test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,40 @@
const express = require("express")
const request = require("supertest")
import express from "express"
import request from "supertest"

const { attachReadRouteHandlerWrapper } = require("@middleware/routeHandler")
import type { AuthorizationMiddleware } from "@middleware/authorization"
import { attachReadRouteHandlerWrapper } from "@middleware/routeHandler"

const { generateRouter } = require("@fixtures/app")
const {
import { generateRouter } from "@fixtures/app"
import {
mockSiteName,
mockUserSessionData,
mockUserWithSiteSessionData,
} = require("@fixtures/sessionData")
} from "@fixtures/sessionData"
import type SitesService from "@services/identity/SitesService"

const { SitesRouter } = require("../sites")
import { SitesRouter } from "../sites"

describe("Sites Router", () => {
const mockSitesService = {
getSites: jest.fn(),
checkHasAccess: jest.fn(),
getLastUpdated: jest.fn(),
getStagingUrl: jest.fn(),
getSiteInfo: jest.fn(),
}

const mockAuthorizationMiddleware = {
verifySiteMember: jest.fn(),
}

const router = new SitesRouter({
sitesService: mockSitesService,
sitesService: (mockSitesService as unknown) as SitesService,
authorizationMiddleware: (mockAuthorizationMiddleware as unknown) as AuthorizationMiddleware,
})

const subrouter = express()

// We can use read route handler here because we don't need to lock the repo
subrouter.get("/", attachReadRouteHandlerWrapper(router.getSites))
subrouter.get(
"/:siteName",
attachReadRouteHandlerWrapper(router.checkHasAccess)
)
subrouter.get(
"/:siteName/lastUpdated",
attachReadRouteHandlerWrapper(router.getLastUpdated)
Expand All @@ -40,6 +43,10 @@ describe("Sites Router", () => {
"/:siteName/stagingUrl",
attachReadRouteHandlerWrapper(router.getStagingUrl)
)
subrouter.get(
"/:siteName/info",
attachReadRouteHandlerWrapper(router.getSiteInfo)
)
const app = generateRouter(subrouter)

beforeEach(() => {
Expand Down Expand Up @@ -77,18 +84,39 @@ describe("Sites Router", () => {
})

describe("getStagingUrl", () => {
it("returns the last updated time", async () => {
it("returns the site's staging URL", async () => {
const stagingUrl = "staging-url"
mockSitesService.getStagingUrl.mockResolvedValueOnce(stagingUrl)

const resp = await request(app)
.get(`/${mockSiteName}/stagingUrl`)
.expect(200)

expect(resp.body).toStrictEqual({ stagingUrl })
expect(resp.body).toStrictEqual({ possibleStagingUrl: stagingUrl })
expect(mockSitesService.getStagingUrl).toHaveBeenCalledWith(
mockUserWithSiteSessionData
)
})
})

describe("getSiteInfo", () => {
it("returns the site's info", async () => {
const siteInfo = {
savedAt: 12345678,
savedBy: "[email protected]",
publishedAt: 23456789,
publishedBy: "[email protected]",
stagingUrl: "staging-url",
siteUrl: "prod-url",
}
mockSitesService.getSiteInfo.mockResolvedValueOnce(siteInfo)

const resp = await request(app).get(`/${mockSiteName}/info`).expect(200)

expect(resp.body).toStrictEqual(siteInfo)
expect(mockSitesService.getSiteInfo).toHaveBeenCalledWith(
mockUserWithSiteSessionData
)
})
})
})
5 changes: 4 additions & 1 deletion src/routes/v2/authenticated/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ const getAuthenticatedSubrouter = ({
}) => {
const netlifyTomlService = new NetlifyTomlService()

const sitesV2Router = new SitesRouter({ sitesService })
const sitesV2Router = new SitesRouter({
sitesService,
authorizationMiddleware,
})
const collaboratorsRouter = new CollaboratorsRouter({
collaboratorsService,
authorizationMiddleware,
Expand Down
72 changes: 44 additions & 28 deletions src/routes/v2/authenticated/sites.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,40 @@
import autoBind from "auto-bind"
import express from "express"

import type { AuthorizationMiddleware } from "@middleware/authorization"
import { attachReadRouteHandlerWrapper } from "@middleware/routeHandler"

import UserWithSiteSessionData from "@classes/UserWithSiteSessionData"

import type UserSessionData from "@root/classes/UserSessionData"
import { BaseIsomerError } from "@root/errors/BaseError"
import { attachSiteHandler } from "@root/middleware"
import type { RequestHandler } from "@root/types"
import type SitesService from "@services/identity/SitesService"

type SitesRouterProps = {
sitesService: SitesService
authorizationMiddleware: AuthorizationMiddleware
}

export class SitesRouter {
private readonly sitesService

constructor({ sitesService }: SitesRouterProps) {
private readonly authorizationMiddleware

constructor({ sitesService, authorizationMiddleware }: SitesRouterProps) {
this.sitesService = sitesService
this.authorizationMiddleware = authorizationMiddleware
// We need to bind all methods because we don't invoke them from the class directly
autoBind(this)
}

addSiteNameToSessionData(userSessionData: UserSessionData, siteName: string) {
const { githubId, accessToken, isomerUserId, email } = userSessionData
return new UserWithSiteSessionData({
githubId,
accessToken,
isomerUserId,
email,
siteName,
})
}

getSites: RequestHandler<
never,
unknown,
never,
never,
{ userSessionData: UserWithSiteSessionData }
{ userSessionData: UserSessionData }
> = async (req, res) => {
const { userSessionData } = res.locals
const siteNames = await this.sitesService.getSites(userSessionData)
Expand All @@ -51,14 +46,9 @@ export class SitesRouter {
unknown,
never,
never,
{ userSessionData: UserWithSiteSessionData }
{ userWithSiteSessionData: UserWithSiteSessionData }
> = async (req, res) => {
const { userSessionData } = res.locals
const { siteName } = req.params
const userWithSiteSessionData = this.addSiteNameToSessionData(
userSessionData,
siteName
)
const { userWithSiteSessionData } = res.locals
const lastUpdated = await this.sitesService.getLastUpdated(
userWithSiteSessionData
)
Expand All @@ -70,18 +60,38 @@ export class SitesRouter {
unknown,
never,
never,
{ userSessionData: UserWithSiteSessionData }
{ userWithSiteSessionData: UserWithSiteSessionData }
> = async (req, res) => {
const { userSessionData } = res.locals
const { siteName } = req.params
const userWithSiteSessionData = this.addSiteNameToSessionData(
userSessionData,
siteName
const { userWithSiteSessionData } = res.locals
const possibleStagingUrl = await this.sitesService.getStagingUrl(
userWithSiteSessionData
)
const stagingUrl = await this.sitesService.getStagingUrl(

// Check for error and throw
if (possibleStagingUrl instanceof BaseIsomerError) {
return res.status(404).json({ message: possibleStagingUrl.message })
}
return res.status(200).json({ possibleStagingUrl })
}

getSiteInfo: RequestHandler<
{ siteName: string },
unknown,
never,
never,
{ userWithSiteSessionData: UserWithSiteSessionData }
> = async (req, res) => {
const { userWithSiteSessionData } = res.locals

const possibleSiteInfo = await this.sitesService.getSiteInfo(
userWithSiteSessionData
)
return res.status(200).json({ stagingUrl })

// Check for error and throw
if (possibleSiteInfo instanceof BaseIsomerError) {
return res.status(400).json({ message: possibleSiteInfo.message })
}
return res.status(200).json(possibleSiteInfo)
}

getRouter() {
Expand All @@ -98,6 +108,12 @@ export class SitesRouter {
attachSiteHandler,
attachReadRouteHandlerWrapper(this.getStagingUrl)
)
router.get(
"/:siteName/info",
attachSiteHandler,
this.authorizationMiddleware.verifySiteMember,
attachReadRouteHandlerWrapper(this.getSiteInfo)
)

return router
}
Expand Down
22 changes: 22 additions & 0 deletions src/services/db/GitHubService.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
inputNameConflictErrorMsg,
} = require("@errors/ConflictError")
const { NotFoundError } = require("@errors/NotFoundError")
const { UnprocessableError } = require("@errors/UnprocessableError")

class GitHubService {
constructor({ axiosInstance }) {
Expand Down Expand Up @@ -284,6 +285,27 @@ class GitHubService {
return { treeSha, currentCommitSha }
}

async getLatestCommitOfBranch(sessionData, branch) {
const { accessToken, siteName } = sessionData
const endpoint = `${siteName}/commits/${branch}`
const headers = {
Authorization: `token ${accessToken}`,
}
// Get the commits of the repo
try {
const { data: latestCommit } = await this.axiosInstance.get(endpoint, {
headers,
})
const { commit: latestCommitMeta } = latestCommit
return latestCommitMeta
} catch (err) {
const { status } = err.response
if (status === 422)
throw new UnprocessableError(`Branch ${branch} does not exist`)
throw err
}
}

async getTree(sessionData, githubSessionData, { isRecursive }) {
const { accessToken } = sessionData
const { siteName } = sessionData
Expand Down
Loading

0 comments on commit 7c6c4fa

Please sign in to comment.