Skip to content
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

feat: introduce a new site info API endpoint #513

Merged
merged 37 commits into from
Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2489fd6
ref(fixtures): convert repoInfo to typescript
dcshzj Oct 5, 2022
2477bef
ref(services): migrate SitesService to typescript
dcshzj Oct 5, 2022
74dc9f3
tests: update unit and integration tests for SitesService
dcshzj Oct 5, 2022
9138b7d
ref(sites): migrate sites router to typescript
dcshzj Oct 5, 2022
3658486
fix: revert back to using SessionData
dcshzj Oct 6, 2022
7a7d1f4
fix: remove use of Bluebird and unused getSiteToken function
dcshzj Oct 6, 2022
f7045aa
fix: use more accurate type
dcshzj Oct 6, 2022
e34cc42
chore: remove unused variable
dcshzj Oct 6, 2022
8c2275e
refactor(tests): migrate generic axios instance to __mocks__
dcshzj Oct 6, 2022
81cf8cc
feat: introduce function to obtain latest commit details
dcshzj Oct 7, 2022
c8a96ce
feat: add function for obtaining a User by ID
dcshzj Oct 7, 2022
e5eded1
feat: introduce a new site info API endpoint
dcshzj Oct 7, 2022
aeab0f8
tests: add partial tests for SitesService
dcshzj Oct 7, 2022
e0fbc16
tests: use mockAxios directly instead of preparing an instance
dcshzj Oct 10, 2022
dcd72e7
Merge branch 'refactor/sites-service-typescript' into feat/site-info
dcshzj Oct 10, 2022
9ae3361
tests: fix SitesService unit tests to pass
dcshzj Oct 10, 2022
b126c53
chore: adjust constants to use SCREAMING_SNAKE_CASE
dcshzj Oct 10, 2022
e9b0112
fix: add authorizationMiddleware to ensure user is member of site
dcshzj Oct 11, 2022
d335ebf
chore: combine sessionData unpacking
dcshzj Oct 11, 2022
5ef57a4
fix: insert try-catch to handle errors from JSON.parse
dcshzj Oct 11, 2022
66defaa
chore: remove unnecessary check for undefined site
dcshzj Oct 11, 2022
ed61f55
chore: return instead of throwing NotFoundError
dcshzj Oct 11, 2022
9ca0838
fix: add assertion to ensure integrity of GitHubCommitData
dcshzj Oct 11, 2022
a12ad20
fix: remove need for adding site name to sessionData
dcshzj Oct 11, 2022
6ca64f2
refactor: convert routes Sites.spec.js to TypeScript
dcshzj Oct 11, 2022
4432732
refactor: redesign getUrlsOfSite to increase readability
dcshzj Oct 11, 2022
4f2735f
Merge branch 'feat/identity-phase-2' into feat/site-info
dcshzj Oct 12, 2022
baf16c8
fix: use correct endpoint to get latest commit data
dcshzj Oct 13, 2022
b90f80e
test: add unit tests for GitHubService getLatestCommitOfBranch
dcshzj Oct 13, 2022
208d143
fix: add stub for obtaining merge author details
dcshzj Oct 13, 2022
99d9140
fix: return a well-formatted response for known exceptions
dcshzj Oct 13, 2022
cdf94ea
test: enhance GitHubService test for all other error statuses
dcshzj Oct 13, 2022
1eb32dd
chore: rename isType function and return boolean directly
dcshzj Oct 13, 2022
3692b08
fix: create new siteUrls object instead of changing in-place
dcshzj Oct 13, 2022
fdf07a5
fix: handle case of null or undefined user email
dcshzj Oct 13, 2022
42e73df
chore: improve code style
dcshzj Oct 13, 2022
d8d7b2b
tests: fix output of getStagingUrl
dcshzj Oct 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
dcshzj marked this conversation as resolved.
Show resolved Hide resolved
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