Skip to content

Commit

Permalink
Allow the profile page to fail
Browse files Browse the repository at this point in the history
This change is a prerequisite to removing the hard-coded data.

Refs #976
  • Loading branch information
thewilkybarkid committed May 30, 2023
1 parent f69c192 commit 4953776
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 50 deletions.
5 changes: 2 additions & 3 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type { Option } from 'fp-ts/Option'
import * as R from 'fp-ts/Reader'
import * as RTE from 'fp-ts/ReaderTaskEither'
import * as RA from 'fp-ts/ReadonlyArray'
import * as T from 'fp-ts/Task'
import * as TE from 'fp-ts/TaskEither'
import { type Lazy, constant, flip, flow, pipe } from 'fp-ts/function'
import { identity } from 'fp-ts/function'
Expand Down Expand Up @@ -365,9 +364,9 @@ export const router: P.Parser<RM.ReaderMiddleware<AppEnv, StatusOpen, ResponseEn
P.map(
R.local((env: AppEnv) => ({
...env,
getName: () => T.of('Daniela Saderi'),
getName: () => TE.of('Daniela Saderi'),
getPrereviews: () =>
T.of([
TE.of([
{
id: 6577344,
reviewers: ['Ahmet Bakirbas', 'Allison Barnes', 'JOHN LILLY JIMMY', 'Daniela Saderi', 'ARPITA YADAV'],
Expand Down
33 changes: 17 additions & 16 deletions src/profile.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Temporal } from '@js-temporal/polyfill'
import { format } from 'fp-ts-routing'
import type { Reader } from 'fp-ts/Reader'
import * as RT from 'fp-ts/ReaderTask'
import * as RTE from 'fp-ts/ReaderTaskEither'
import * as RNEA from 'fp-ts/ReadonlyNonEmptyArray'
import type * as T from 'fp-ts/Task'
import type * as TE from 'fp-ts/TaskEither'
import { flow, pipe } from 'fp-ts/function'
import { Status, type StatusOpen } from 'hyper-ts'
import * as M from 'hyper-ts/lib/Middleware'
import * as RM from 'hyper-ts/lib/ReaderMiddleware'
import type { LanguageCode } from 'iso-639-1'
import { getLangDir } from 'rtl-detect'
import { match } from 'ts-pattern'
import { type Html, html, plainText, rawHtml, sendHtml } from './html'
import { notFound, serviceUnavailable } from './middleware'
import { page } from './page'
import type { PreprintId } from './preprint-id'
import { reviewMatch } from './routes'
Expand All @@ -32,31 +32,37 @@ type Prereviews = RNEA.ReadonlyNonEmptyArray<{
}>

export interface GetPrereviewsEnv {
getPrereviews: () => T.Task<Prereviews>
getPrereviews: () => TE.TaskEither<'not-found' | 'unavailable', Prereviews>
}

export interface GetNameEnv {
getName: () => T.Task<string>
getName: () => TE.TaskEither<'not-found' | 'unavailable', string>
}

const getPrereviews = pipe(
RT.ask<GetPrereviewsEnv>(),
RT.chainTaskK(({ getPrereviews }) => getPrereviews()),
RTE.ask<GetPrereviewsEnv>(),
RTE.chainTaskEitherK(({ getPrereviews }) => getPrereviews()),
)

const getName = pipe(
RT.ask<GetNameEnv>(),
RT.chainTaskK(({ getName }) => getName()),
RTE.ask<GetNameEnv>(),
RTE.chainTaskEitherK(({ getName }) => getName()),
)

export const profile = pipe(
fromReaderTask(getPrereviews),
RM.fromReaderTaskEither(getPrereviews),
RM.bindTo('prereviews'),
RM.apSW('name', fromReaderTask(getName)),
RM.apSW('name', RM.fromReaderTaskEither(getName)),
RM.apSW('user', maybeGetUser),
chainReaderKW(createPage),
RM.ichainFirst(() => RM.status(Status.OK)),
RM.ichainMiddlewareKW(sendHtml),
RM.orElseW(error =>
match(error)
.with('not-found', () => notFound)
.with('unavailable', () => serviceUnavailable)
.exhaustive(),
),
)

function createPage({ name, prereviews, user }: { name: string; prereviews: Prereviews; user?: User }) {
Expand Down Expand Up @@ -143,8 +149,3 @@ function chainReaderKW<R2, A, B>(
): <R1, I, E>(ma: RM.ReaderMiddleware<R1, I, I, E, A>) => RM.ReaderMiddleware<R1 & R2, I, I, E, B> {
return RM.chainW(fromReaderK(f))
}

// https://github.com/DenisFrezzato/hyper-ts/pull/87
function fromReaderTask<R, I = StatusOpen, A = never>(fa: RT.ReaderTask<R, A>): RM.ReaderMiddleware<R, I, I, never, A> {
return r => M.fromTask(fa(r))
}
174 changes: 143 additions & 31 deletions test/profile.test.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,152 @@
import { test } from '@fast-check/jest'
import { expect } from '@jest/globals'
import { describe, expect } from '@jest/globals'
import * as E from 'fp-ts/Either'
import * as T from 'fp-ts/Task'
import * as TE from 'fp-ts/TaskEither'
import { MediaType, Status } from 'hyper-ts'
import * as M from 'hyper-ts/lib/Middleware'
import * as _ from '../src/profile'
import * as fc from './fc'
import { runMiddleware } from './middleware'

test.prop([
fc.connection({ method: fc.requestMethod() }),
fc.string(),
fc.nonEmptyArray(
fc.record({
id: fc.integer(),
reviewers: fc.nonEmptyArray(fc.string()),
published: fc.plainDate(),
preprint: fc.preprintTitle(),
}),
),
fc.either(fc.constant('no-session' as const), fc.user()),
])('profile', async (connection, name, prereviews, user) => {
const actual = await runMiddleware(
_.profile({
getName: () => T.of(name),
getPrereviews: () => T.of(prereviews),
getUser: () => M.fromEither(user),
}),
connection,
)()

expect(actual).toStrictEqual(
E.right([
{ type: 'setStatus', status: Status.OK },
{ type: 'setHeader', name: 'Content-Type', value: MediaType.textHTML },
{ type: 'setBody', body: expect.anything() },
]),
)
describe('profile', () => {
test.prop([
fc.connection({ method: fc.requestMethod() }),
fc.string(),
fc.nonEmptyArray(
fc.record({
id: fc.integer(),
reviewers: fc.nonEmptyArray(fc.string()),
published: fc.plainDate(),
preprint: fc.preprintTitle(),
}),
),
fc.either(fc.constant('no-session' as const), fc.user()),
])('when the data can be loaded', async (connection, name, prereviews, user) => {
const actual = await runMiddleware(
_.profile({
getName: () => TE.of(name),
getPrereviews: () => TE.of(prereviews),
getUser: () => M.fromEither(user),
}),
connection,
)()

expect(actual).toStrictEqual(
E.right([
{ type: 'setStatus', status: Status.OK },
{ type: 'setHeader', name: 'Content-Type', value: MediaType.textHTML },
{ type: 'setBody', body: expect.anything() },
]),
)
})

test.prop([
fc.connection({ method: fc.requestMethod() }),
fc.string(),
fc.either(fc.constant('no-session' as const), fc.user()),
])('when there are no PREreviews', async (connection, name, user) => {
const actual = await runMiddleware(
_.profile({
getName: () => TE.of(name),
getPrereviews: () => TE.left('not-found'),
getUser: () => M.fromEither(user),
}),
connection,
)()

expect(actual).toStrictEqual(
E.right([
{ type: 'setStatus', status: Status.NotFound },
{ type: 'setHeader', name: 'Cache-Control', value: 'no-store, must-revalidate' },
{ type: 'setHeader', name: 'Content-Type', value: MediaType.textHTML },
{ type: 'setBody', body: expect.anything() },
]),
)
})

test.prop([
fc.connection({ method: fc.requestMethod() }),
fc.string(),
fc.either(fc.constant('no-session' as const), fc.user()),
])("when the PREreviews can't be loaded", async (connection, name, user) => {
const actual = await runMiddleware(
_.profile({
getName: () => TE.of(name),
getPrereviews: () => TE.left('unavailable'),
getUser: () => M.fromEither(user),
}),
connection,
)()

expect(actual).toStrictEqual(
E.right([
{ type: 'setStatus', status: Status.ServiceUnavailable },
{ type: 'setHeader', name: 'Cache-Control', value: 'no-store, must-revalidate' },
{ type: 'setHeader', name: 'Content-Type', value: MediaType.textHTML },
{ type: 'setBody', body: expect.anything() },
]),
)
})

test.prop([
fc.connection({ method: fc.requestMethod() }),
fc.nonEmptyArray(
fc.record({
id: fc.integer(),
reviewers: fc.nonEmptyArray(fc.string()),
published: fc.plainDate(),
preprint: fc.preprintTitle(),
}),
),
fc.either(fc.constant('no-session' as const), fc.user()),
])("when the name can't be found", async (connection, prereviews, user) => {
const actual = await runMiddleware(
_.profile({
getName: () => TE.left('not-found'),
getPrereviews: () => TE.of(prereviews),
getUser: () => M.fromEither(user),
}),
connection,
)()

expect(actual).toStrictEqual(
E.right([
{ type: 'setStatus', status: Status.NotFound },
{ type: 'setHeader', name: 'Cache-Control', value: 'no-store, must-revalidate' },
{ type: 'setHeader', name: 'Content-Type', value: MediaType.textHTML },
{ type: 'setBody', body: expect.anything() },
]),
)
})

test.prop([
fc.connection({ method: fc.requestMethod() }),
fc.nonEmptyArray(
fc.record({
id: fc.integer(),
reviewers: fc.nonEmptyArray(fc.string()),
published: fc.plainDate(),
preprint: fc.preprintTitle(),
}),
),
fc.either(fc.constant('no-session' as const), fc.user()),
])('when the name is unavailable', async (connection, prereviews, user) => {
const actual = await runMiddleware(
_.profile({
getName: () => TE.left('unavailable'),
getPrereviews: () => TE.of(prereviews),
getUser: () => M.fromEither(user),
}),
connection,
)()

expect(actual).toStrictEqual(
E.right([
{ type: 'setStatus', status: Status.ServiceUnavailable },
{ type: 'setHeader', name: 'Cache-Control', value: 'no-store, must-revalidate' },
{ type: 'setHeader', name: 'Content-Type', value: MediaType.textHTML },
{ type: 'setBody', body: expect.anything() },
]),
)
})
})

0 comments on commit 4953776

Please sign in to comment.