diff --git a/config/jest/client.config.js b/config/jest/client.config.js index d5ba3c585..975397a29 100644 --- a/config/jest/client.config.js +++ b/config/jest/client.config.js @@ -10,6 +10,8 @@ module.exports = { "/src/core/build/polyfills.js", "/src/core/client/test/setup.ts", ], + setupTestFrameworkScriptFile: + "/src/core/client/test/setupTestFramework.ts", testMatch: ["**/*.spec.{js,jsx,mjs,ts,tsx}"], testEnvironment: "node", testURL: "http://localhost", diff --git a/package-lock.json b/package-lock.json index e9e9960d1..741e7ee63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14229,6 +14229,12 @@ "integrity": "sha1-rRxg8p6HGdR8JuETgJi20YsmETQ=", "dev": true }, + "jest-mock-console": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/jest-mock-console/-/jest-mock-console-0.4.0.tgz", + "integrity": "sha512-WElCbNvfqQlD7cpfHfTn1ytZ+RjKg1Ftrvr5wEjdWP7a9esXmaiZuEAPeYUSK5fd0Cra+dR1oF8HAjjKKxDQdg==", + "dev": true + }, "jest-regex-util": { "version": "23.3.0", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-23.3.0.tgz", @@ -18045,6 +18051,11 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "permit": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/permit/-/permit-0.2.4.tgz", + "integrity": "sha512-Mp2XTEMD3mPsZIWq3bp0claE4IxXKa4C6nhSDPZgGri8Q4CLjEjAQrP/xGKq2548a2KFENmA1V7W0Lob8kTuzw==" + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", diff --git a/package.json b/package.json index 59bf99426..9a804811f 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "passport-oauth2": "^1.4.0", "passport-strategy": "^1.0.0", "performance-now": "^2.1.0", + "permit": "^0.2.4", "subscriptions-transport-ws": "^0.9.12", "tlds": "^1.203.1", "uuid": "^3.3.2" @@ -185,6 +186,7 @@ "jest": "^23.4.1", "jest-junit": "^5.1.0", "jest-localstorage-mock": "^2.2.0", + "jest-mock-console": "^0.4.0", "jsdom": "^11.11.0", "loader-utils": "^1.1.0", "material-design-icons": "^3.0.1", diff --git a/src/core/build/createWebpackConfig.ts b/src/core/build/createWebpackConfig.ts index 6085d8899..93257c63b 100644 --- a/src/core/build/createWebpackConfig.ts +++ b/src/core/build/createWebpackConfig.ts @@ -448,25 +448,41 @@ export default function createWebpackConfig({ ].filter(s => s), output: { ...baseConfig.output, - library: "Talk", + library: "Coral", // don't hash the embed, cache-busting must be completed by the requester // as this lives in a static template on the embed site. filename: "assets/js/embed.js", }, plugins: [ ...baseConfig.plugins!, - // Generates an `stream.html` file with the + + + diff --git a/src/core/client/embed/articleButton.html b/src/core/client/embed/articleButton.html new file mode 100644 index 000000000..bf0c0cf38 --- /dev/null +++ b/src/core/client/embed/articleButton.html @@ -0,0 +1,103 @@ + + + + + Talk 5.0 – Embed Stream + + + + + + + +

+ Default | Article +

+

Talk 5.0 – Article with Button

+

Dismember a mouse and then regurgitate parts of it on the family room floor. Dont wait for the storm to pass, + dance in the rain stand in front of the computer screen, so stares at human while pushing stuff off a table chew + the plant meow hiss at vacuum cleaner. Terrorize the hundred-and-twenty-pound rottweiler and steal his bed, not + sorry chew the plant. Litter kitter kitty litty little kitten big roar roar feed me rub whiskers on bare skin act + innocent sleep on keyboard, so give me attention or face the wrath of my claws for demand to be let outside at + once, and expect owner to wait for me as i think about it spread kitty litter all over house so nya nya nyan. Catty + ipsum massacre a bird in the living room and then look like the cutest and most innocent animal on the planet you + have cat to be kitten me right meow. Hiss and stare at nothing then run suddenly away refuse to come home when + humans are going to bed; stay out all night then yowl like i am dying at 4am and lick plastic bags. Chase dog then + run away purrr purr littel cat, little cat purr purr and step on your keyboard while you're gaming and then turn in + a circle . Twitch tail in permanent irritation put butt in owner's face and the dog smells bad yet attempt to leap + between furniture but woefully miscalibrate and bellyflop onto the floor; what's your problem? i meant to do that + now i shall wash myself intently. Sniff all the things groom forever, stretch tongue and leave it slightly out, + blep, but bring your owner a dead bird decide to want nothing to do with my owner today for lay on arms while + you're using the keyboard meow meow, i tell my human or scratch. Sleep on my human's head then cats take over the + world bleghbleghvomit my furball really tie the room together sleep more napping, more napping all the napping is + exhausting. When in doubt, wash drink water out of the faucet, cats are fats i like to pets them they like to meow + back and cat dog hate mouse eat string barf pillow no baths hate everything yet swat at dog kitty kitty but you + call this cat food. Cough furball into food bowl then scratch owner for a new one flex claws on the human's belly + and purr like a lawnmower for has closed eyes but still sees you groom yourself 4 hours - checked, have your beauty + sleep 18 hours - checked, be fabulous for the rest of the day - checked. Freak human out make funny noise mow mow + mow mow mow mow success now attack human flex claws on the human's belly and purr like a lawnmower or meowwww. + Terrorize the hundred-and-twenty-pound rottweiler and steal his bed, not sorry paw at your fat belly so yowling + nonstop the whole night small kitty warm kitty little balls of fur or eat owner's food reward the chosen human with + a slow blink. Gate keepers of hell plan steps for world domination for more napping, more napping all the napping + is exhausting give me some of your food give me some of your food give me some of your food meh, i don't want it so + flop over. Make meme, make cute face ears back wide eyed so sit and stare. Dead stare with ears cocked furrier and + even more furrier hairball. Stand in front of the computer screen demand to have some of whatever the human is + cooking, then sniff the offering and walk away for catasstrophe, kitty scratches couch bad kitty. Wack the mini + furry mouse intrigued by the shower, and pooping rainbow while flying in a toasted bread costume in space. + Mesmerizing birds love me! shake treat bag, yet lies down where is my slave? I'm getting hungry so lick face hiss + at owner, pee a lot, and meow repeatedly scratch at fence purrrrrr eat muffins and poutine until owner comes back. + You have cat to be kitten me right meow sniff other cat's butt and hang jaw half open thereafter but run outside as + soon as door open so munch on tasty moths or munch on tasty moths, for paw at beetle and eat it before it gets + away. Sit on human. Gnaw the corn cob massacre a bird in the living room and then look like the cutest and most + innocent animal on the planet for sit on the laptop. Meow scratch leg; meow for can opener to feed me cat fur is + the new black but hide when guests come over, and Gate keepers of hell. Refuse to come home when humans are going + to bed; stay out all night then yowl like i am dying at 4am cat slap dog in face or eat a rug and furry furry hairs + everywhere oh no human coming lie on counter don't get off counter for i like fish sit on human they not getting up + ever but meow meow but cuddle no cuddle cuddle love scratch scratch.

+

I show my fluffy belly but it's a trap! if you pet it i will tear up your hand refuse to drink water except out of + someone's glass mice, so cough hairball, eat toilet paper or curl into a furry donut lick sellotape but wack the + mini furry mouse. When owners are asleep, cry for no apparent reason. Chase imaginary bugs. Stinky cat reward the + chosen human with a slow blink, or chase dog then run away. Chew on cable scratch the furniture for you are a + captive audience while sitting on the toilet, pet me for i like cats because they are fat and fluffy and spend all + night ensuring people don't sleep sleep all day. Scoot butt on the rug need to check on human, have not seen in an + hour might be dead oh look, human is alive, hiss at human, feed me, leave fur on owners clothes, so instantly break + out into full speed gallop across the house for no reason play riveting piece on synthesizer keyboard and scoot + butt on the rug yet meow meow. Attack dog, run away and pretend to be victim annoy the old grumpy cat, start a + fight and then retreat to wash when i lose or meow go back to sleep owner brings food and water tries to pet on + head, so scratch get sprayed by water because bad cat. Meowwww pelt around the house and up and down stairs chasing + phantoms drink water out of the faucet meow meow, i tell my human. Destroy couch.

+

Ask to go outside and ask to come inside and ask to go outside and ask to come inside the dog smells bad. Lick + butt and make a weird face. Toilet paper attack claws fluff everywhere meow miao french ciao litterbox. Shake treat + bag immediately regret falling into bathtub or white cat sleeps on a black shirt so what a cat-ass-trophy! eat + owner's food spit up on light gray carpet instead of adjacent linoleum. Warm up laptop with butt lick butt fart + rainbows until owner yells pee in litter box hiss at cats scratch the box so loved it, hated it, loved it, hated it + but need to check on human, have not seen in an hour might be dead oh look, human is alive, hiss at human, feed me. +

+
+
+ +
+ + + + diff --git a/src/core/client/embed/index.html b/src/core/client/embed/index.html index b579dab8f..e0e8f74e6 100644 --- a/src/core/client/embed/index.html +++ b/src/core/client/embed/index.html @@ -9,16 +9,21 @@ -

Talk 5.0 – Embed Stream

+

+ Article | Article With Button +

+

Talk 5.0 – Embed Stream

diff --git a/src/core/client/embed/index.spec.ts b/src/core/client/embed/index.spec.ts index 66613f1c7..696e6adba 100644 --- a/src/core/client/embed/index.spec.ts +++ b/src/core/client/embed/index.spec.ts @@ -1,8 +1,10 @@ -import * as Talk from "./"; +import mockConsole from "jest-mock-console"; +import * as Coral from "./"; + +// tslint:disable:no-console describe("Basic integration test", () => { const container: HTMLElement = document.createElement("div"); - let streamInterface: ReturnType; beforeAll(() => { container.id = "basic-integration-test-id"; document.body.appendChild(container); @@ -11,13 +13,40 @@ describe("Basic integration test", () => { document.body.removeChild(container); }); it("should render iframe", () => { - streamInterface = Talk.render({ + mockConsole(); + const TalkEmbedStream = Coral.Talk.createStreamEmbed({ id: "basic-integration-test-id", }); + TalkEmbedStream.render(); expect(container.innerHTML).toMatchSnapshot(); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.error).not.toHaveBeenCalled(); + }); + it("should use canonical link", () => { + mockConsole(); + const link = document.createElement("link"); + link.rel = "canonical"; + link.href = "http://localhost/canonical"; + document.head.appendChild(link); + const TalkEmbedStream = Coral.Talk.createStreamEmbed({ + id: "basic-integration-test-id", + }); + TalkEmbedStream.render(); + expect(container.innerHTML).toMatchSnapshot(); + document.head.removeChild(link); + expect(console.warn).not.toHaveBeenCalled(); + expect(console.error).not.toHaveBeenCalled(); }); it("should remove iframe", () => { - streamInterface.remove(); + mockConsole(); + const TalkEmbedStream = Coral.Talk.createStreamEmbed({ + id: "basic-integration-test-id", + }); + TalkEmbedStream.render(); + TalkEmbedStream.remove(); expect(container.innerHTML).toBe(""); + // tslint:disable-next-line:no-console + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.error).not.toHaveBeenCalled(); }); }); diff --git a/src/core/client/embed/index.ts b/src/core/client/embed/index.ts index c440f72a8..542bc1caa 100644 --- a/src/core/client/embed/index.ts +++ b/src/core/client/embed/index.ts @@ -1,32 +1,2 @@ -import { EventEmitter2 } from "eventemitter2"; -import qs from "query-string"; - -import createStreamInterface from "./Stream"; - -export interface Config { - assetID?: string; - assetURL?: string; - commentID?: string; - rootURL?: string; - id?: string; - events?: (eventEmitter: EventEmitter2) => void; -} - -export function render(config: Config = {}) { - // Parse query params - const query = qs.parse(location.search); - const eventEmitter = new EventEmitter2({ wildcard: true }); - - if (config.events) { - config.events(eventEmitter); - } - - return createStreamInterface({ - assetID: config.assetID || query.assetID, - assetURL: config.assetURL || query.assetURL, - commentID: config.commentID || query.commentID, - id: config.id || "talk-embed-stream", - rootURL: config.rootURL || location.origin, - eventEmitter, - }); -} +import * as TalkImport from "./Talk"; +export const Talk = TalkImport; diff --git a/src/core/client/embed/onIntersect.ts b/src/core/client/embed/onIntersect.ts new file mode 100644 index 000000000..761abf970 --- /dev/null +++ b/src/core/client/embed/onIntersect.ts @@ -0,0 +1,20 @@ +export default function onIntersect(el: HTMLElement, callback: () => void) { + if (!IntersectionObserver) { + // tslint:disable-next-line:no-console + console.warn("IntersectionObserver not available"); + callback(); + return; + } + const options = { + rootMargin: "100px", + threshold: 1.0, + }; + + const observer = new IntersectionObserver(entries => { + if (entries[0].isIntersecting) { + observer.disconnect(); + callback(); + } + }, options); + observer.observe(el); +} diff --git a/src/core/client/embed/utils/index.ts b/src/core/client/embed/utils/index.ts index 9a373ce0b..84796bfff 100644 --- a/src/core/client/embed/utils/index.ts +++ b/src/core/client/embed/utils/index.ts @@ -2,3 +2,4 @@ export { default as buildURL } from "./buildURL"; export { default as ensureEndSlash } from "./ensureEndSlash"; export { default as startsWith } from "./startsWith"; export { default as prefixStorage } from "./prefixStorage"; +export { default as parseHashQuery } from "./parseHashQuery"; diff --git a/src/core/client/embed/utils/parseHashQuery.spec.ts b/src/core/client/embed/utils/parseHashQuery.spec.ts new file mode 100644 index 000000000..93a00de8e --- /dev/null +++ b/src/core/client/embed/utils/parseHashQuery.spec.ts @@ -0,0 +1,24 @@ +import parseHashQuery from "./parseHashQuery"; + +it("should parse hash", () => { + const testCases: Array<[string, ReturnType]> = [ + [ + "#commentID=comment-id", + { + commentID: "comment-id", + }, + ], + [ + "#commentID=comment-id&assetURL=asset-url", + { + commentID: "comment-id", + assetURL: "asset-url", + }, + ], + ["#", {}], + ["", {}], + ]; + testCases.forEach(tc => { + expect(parseHashQuery(tc[0])).toEqual(tc[1]); + }); +}); diff --git a/src/core/client/embed/utils/parseHashQuery.ts b/src/core/client/embed/utils/parseHashQuery.ts new file mode 100644 index 000000000..daf5f5a5a --- /dev/null +++ b/src/core/client/embed/utils/parseHashQuery.ts @@ -0,0 +1,5 @@ +import qs from "query-string"; + +export default function parseQueryHash(hash: string): Record { + return qs.parse(hash); +} diff --git a/src/core/client/framework/utils/index.ts b/src/core/client/framework/utils/index.ts index 4eca76650..0b16c4981 100644 --- a/src/core/client/framework/utils/index.ts +++ b/src/core/client/framework/utils/index.ts @@ -1,2 +1,3 @@ export { default as buildURL } from "./buildURL"; export { default as parseURL } from "./parseURL"; +export { default as modifyQuery } from "./modifyQuery"; diff --git a/src/core/client/framework/utils/modifyQuery.spec.ts b/src/core/client/framework/utils/modifyQuery.spec.ts new file mode 100644 index 000000000..638554e8f --- /dev/null +++ b/src/core/client/framework/utils/modifyQuery.spec.ts @@ -0,0 +1,30 @@ +import modifyQuery from "./modifyQuery"; + +it("should modify query", () => { + const testCases: Array<[string, Record, string]> = [ + [ + "http://localhost:8080/?a=b#hash", + { + c: "d", + }, + "http://localhost:8080/?a=b&c=d#hash", + ], + [ + "http://localhost:8080/#hash", + { + a: "b", + }, + "http://localhost:8080/?a=b#hash", + ], + [ + "http://localhost:8080/?a=b#hash", + { + a: undefined, + }, + "http://localhost:8080/#hash", + ], + ]; + testCases.forEach(([url, params, expected]) => { + expect(modifyQuery(url, params)).toEqual(expected); + }); +}); diff --git a/src/core/client/framework/utils/modifyQuery.ts b/src/core/client/framework/utils/modifyQuery.ts new file mode 100644 index 000000000..b3f0cc41a --- /dev/null +++ b/src/core/client/framework/utils/modifyQuery.ts @@ -0,0 +1,11 @@ +import qs from "query-string"; + +import buildURL from "./buildURL"; +import parseURL from "./parseURL"; + +export default function modifyQuery(url: string, params: {}) { + const parsed = parseURL(url); + const query = qs.parse(parsed.search); + parsed.search = qs.stringify({ ...query, ...params }); + return buildURL(parsed); +} diff --git a/src/core/client/framework/utils/parseHashQuery.spec.ts b/src/core/client/framework/utils/parseHashQuery.spec.ts new file mode 100644 index 000000000..2cbb6eb4d --- /dev/null +++ b/src/core/client/framework/utils/parseHashQuery.spec.ts @@ -0,0 +1,24 @@ +import parseHashQuery from "./parseHashQuery"; + +it("should parse hash", () => { + const testCases: Array<[string, ReturnType]> = [ + [ + "#commentID=comment-id", + { + commentID: "comment-id", + }, + ], + [ + "#commentID=comment-id&assetURL=asset-url", + { + commentID: "comment-id", + assetURL: "asset-url", + }, + ], + ["#", {}], + ["", {}], + ]; + testCases.forEach(([url, expected]) => { + expect(parseHashQuery(url)).toEqual(expected); + }); +}); diff --git a/src/core/client/framework/utils/parseHashQuery.ts b/src/core/client/framework/utils/parseHashQuery.ts new file mode 100644 index 000000000..daf5f5a5a --- /dev/null +++ b/src/core/client/framework/utils/parseHashQuery.ts @@ -0,0 +1,5 @@ +import qs from "query-string"; + +export default function parseQueryHash(hash: string): Record { + return qs.parse(hash); +} diff --git a/src/core/client/stream/local/initLocalState.ts b/src/core/client/stream/local/initLocalState.ts index b2f09b110..b722b630e 100644 --- a/src/core/client/stream/local/initLocalState.ts +++ b/src/core/client/stream/local/initLocalState.ts @@ -42,12 +42,8 @@ export default async function initLocalState( localRecord.setValue(query.assetID, "assetID"); } - // Saving location host for permalink until we get the asset url - the url now points to the tenant - if (location && query.assetID) { - localRecord.setValue( - `${location.origin}/?assetID=${query.assetID}`, - "assetURL" - ); + if (query.assetURL) { + localRecord.setValue(query.assetURL, "assetURL"); } if (query.commentID) { diff --git a/src/core/client/stream/tabs/comments/components/Comment/Comment.spec.tsx b/src/core/client/stream/tabs/comments/components/Comment/Comment.spec.tsx index d9c0a9acf..a793f4d1c 100644 --- a/src/core/client/stream/tabs/comments/components/Comment/Comment.spec.tsx +++ b/src/core/client/stream/tabs/comments/components/Comment/Comment.spec.tsx @@ -7,6 +7,7 @@ import Comment from "./Comment"; it("renders username and body", () => { const props: PropTypesOf = { + id: "comment-id", author: { username: "Marvin", }, diff --git a/src/core/client/stream/tabs/comments/components/Comment/Comment.tsx b/src/core/client/stream/tabs/comments/components/Comment/Comment.tsx index aee9d5740..b30621913 100644 --- a/src/core/client/stream/tabs/comments/components/Comment/Comment.tsx +++ b/src/core/client/stream/tabs/comments/components/Comment/Comment.tsx @@ -10,6 +10,7 @@ import TopBarLeft from "./TopBarLeft"; import Username from "./Username"; export interface CommentProps { + id?: string; className?: string; author: { username: string | null; @@ -28,6 +29,7 @@ const Comment: StatelessComponent = props => { className={styles.topBar} direction="row" justifyContent="space-between" + id={props.id} > {props.author && diff --git a/src/core/client/stream/tabs/comments/components/Comment/__snapshots__/Comment.spec.tsx.snap b/src/core/client/stream/tabs/comments/components/Comment/__snapshots__/Comment.spec.tsx.snap index 64419b0fa..09cd14a62 100644 --- a/src/core/client/stream/tabs/comments/components/Comment/__snapshots__/Comment.spec.tsx.snap +++ b/src/core/client/stream/tabs/comments/components/Comment/__snapshots__/Comment.spec.tsx.snap @@ -8,6 +8,7 @@ exports[`renders username and body 1`] = ` diff --git a/src/core/client/stream/tabs/comments/components/PermalinkButton/PermalinkButton.tsx b/src/core/client/stream/tabs/comments/components/PermalinkButton/PermalinkButton.tsx index cf0fad56c..d93fb46e8 100644 --- a/src/core/client/stream/tabs/comments/components/PermalinkButton/PermalinkButton.tsx +++ b/src/core/client/stream/tabs/comments/components/PermalinkButton/PermalinkButton.tsx @@ -15,7 +15,7 @@ import PermalinkPopover from "./PermalinkPopover"; interface PermalinkProps { commentID: string; - assetURL: string | null; + url: string; } class Permalink extends React.Component { @@ -28,7 +28,7 @@ class Permalink extends React.Component { ); public render() { - const { commentID, assetURL } = this.props; + const { commentID, url } = this.props; const popoverID = `permalink-popover-${commentID}`; return ( { } > diff --git a/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx b/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx index 1cd09fd2e..24e09fd23 100644 --- a/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx @@ -120,6 +120,7 @@ export class CommentContainer extends Component { return ( <> = ({ commentID, }) => { return local.assetURL ? ( - + ) : null; }; diff --git a/src/core/client/stream/tabs/comments/containers/PermalinkViewContainer.tsx b/src/core/client/stream/tabs/comments/containers/PermalinkViewContainer.tsx index 664168e36..a21057c04 100644 --- a/src/core/client/stream/tabs/comments/containers/PermalinkViewContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/PermalinkViewContainer.tsx @@ -40,6 +40,18 @@ class PermalinkViewContainer extends React.Component< // Remove the commentId url param. return buildURL({ ...urlParts, search }); } + + public componentDidMount() { + if (this.props.pym) { + const scrollTo = this.props.comment + ? document + .getElementById(`comment-${this.props.comment.id}`)! + .getBoundingClientRect().top + window.pageYOffset + : 50; + setTimeout(() => this.props.pym!.scrollParentToChildPos(scrollTo), 100); + } + } + public render() { const { comment, asset, me } = this.props; return ( @@ -66,6 +78,7 @@ const enhanced = withContext(ctx => ({ `, comment: graphql` fragment PermalinkViewContainer_comment on Comment { + id ...CommentContainer_comment } `, diff --git a/src/core/client/stream/tabs/comments/containers/StreamContainer.tsx b/src/core/client/stream/tabs/comments/containers/StreamContainer.tsx index f90e7c10a..5ca2286bc 100644 --- a/src/core/client/stream/tabs/comments/containers/StreamContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/StreamContainer.tsx @@ -135,7 +135,7 @@ const enhanced = withPaginationContainer< $count: Int! $cursor: Cursor $orderBy: COMMENT_SORT! - $assetID: ID! + $assetID: ID ) { asset(id: $assetID) { ...StreamContainer_asset diff --git a/src/core/client/stream/tabs/comments/containers/__snapshots__/CommentContainer.spec.tsx.snap b/src/core/client/stream/tabs/comments/containers/__snapshots__/CommentContainer.spec.tsx.snap index f6e7f50ba..db1219282 100644 --- a/src/core/client/stream/tabs/comments/containers/__snapshots__/CommentContainer.spec.tsx.snap +++ b/src/core/client/stream/tabs/comments/containers/__snapshots__/CommentContainer.spec.tsx.snap @@ -24,6 +24,7 @@ exports[`renders body only 1`] = ` /> } + id="comment-comment-id" indentLevel={1} showEditedMarker={false} /> @@ -54,6 +55,7 @@ exports[`renders username and body 1`] = ` /> } + id="comment-comment-id" indentLevel={1} showEditedMarker={false} /> diff --git a/src/core/client/stream/tabs/comments/queries/PermalinkViewQuery.tsx b/src/core/client/stream/tabs/comments/queries/PermalinkViewQuery.tsx index 4d1ac4611..9a3bad91f 100644 --- a/src/core/client/stream/tabs/comments/queries/PermalinkViewQuery.tsx +++ b/src/core/client/stream/tabs/comments/queries/PermalinkViewQuery.tsx @@ -45,15 +45,19 @@ export const render = ({ }; const PermalinkViewQuery: StatelessComponent = ({ - local: { commentID, assetID }, + local: { commentID, assetID, assetURL }, }) => ( query={graphql` - query PermalinkViewQuery($commentID: ID!, $assetID: ID!) { + query PermalinkViewQuery( + $commentID: ID! + $assetID: ID + $assetURL: String + ) { me { ...PermalinkViewContainer_me } - asset(id: $assetID) { + asset(id: $assetID, url: $assetURL) { ...PermalinkViewContainer_asset } comment(id: $commentID) { @@ -62,8 +66,9 @@ const PermalinkViewQuery: StatelessComponent = ({ } `} variables={{ - assetID: assetID!, commentID: commentID!, + assetID, + assetURL, }} render={render} /> @@ -74,6 +79,7 @@ const enhanced = withLocalStateContainer( fragment PermalinkViewQueryLocal on Local { assetID commentID + assetURL } ` )(PermalinkViewQuery); diff --git a/src/core/client/stream/tabs/comments/queries/StreamQuery.tsx b/src/core/client/stream/tabs/comments/queries/StreamQuery.tsx index ec61ec723..da7879f8e 100644 --- a/src/core/client/stream/tabs/comments/queries/StreamQuery.tsx +++ b/src/core/client/stream/tabs/comments/queries/StreamQuery.tsx @@ -38,21 +38,22 @@ export const render = ({ }; const StreamQuery: StatelessComponent = ({ - local: { assetID }, + local: { assetID, assetURL }, }) => ( query={graphql` - query StreamQuery($assetID: ID!) { + query StreamQuery($assetID: ID, $assetURL: String) { me { ...StreamContainer_me } - asset(id: $assetID) { + asset(id: $assetID, url: $assetURL) { ...StreamContainer_asset } } `} variables={{ - assetID: assetID!, + assetID, + assetURL, }} render={render} /> @@ -62,6 +63,7 @@ const enhanced = withLocalStateContainer( graphql` fragment StreamQueryLocal on Local { assetID + assetURL } ` )(StreamQuery); diff --git a/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap index 22f084b98..b843afa72 100644 --- a/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap @@ -199,6 +199,7 @@ exports[`cancel edit: edit canceled 1`] = ` >
s.throws(), - s => s.withArgs(undefined, { id: assets[0].id }).returns(assets[0]) + s => + s + .withArgs(undefined, { id: assets[0].id, url: null }) + .returns(assets[0]) ), me: createSinonStub( s => s.throws(), diff --git a/src/core/client/stream/test/comments/loadMore.spec.tsx b/src/core/client/stream/test/comments/loadMore.spec.tsx index 404d3c702..d51ea60fa 100644 --- a/src/core/client/stream/test/comments/loadMore.spec.tsx +++ b/src/core/client/stream/test/comments/loadMore.spec.tsx @@ -1,4 +1,5 @@ import { ReactTestRenderer } from "react-test-renderer"; +import sinon from "sinon"; import { timeout } from "talk-common/utils"; import { createSinonStub } from "talk-framework/testHelpers"; @@ -55,7 +56,15 @@ beforeEach(() => { Query: { asset: createSinonStub( s => s.throws(), - s => s.withArgs(undefined, { id: assetStub.id }).returns(assetStub) + s => + s + .withArgs( + undefined, + sinon + .match({ id: assetStub.id, url: null }) + .or(sinon.match({ id: assetStub.id })) + ) + .returns(assetStub) ), }, }; diff --git a/src/core/client/stream/test/comments/permalinkView.spec.tsx b/src/core/client/stream/test/comments/permalinkView.spec.tsx index 0bbe3e260..180abbe0b 100644 --- a/src/core/client/stream/test/comments/permalinkView.spec.tsx +++ b/src/core/client/stream/test/comments/permalinkView.spec.tsx @@ -36,7 +36,10 @@ beforeEach(() => { ), asset: createSinonStub( s => s.throws(), - s => s.withArgs(undefined, { id: assetStub.id }).returns(assetStub) + s => + s + .withArgs(undefined, { id: assetStub.id, url: null }) + .returns(assetStub) ), }, }; diff --git a/src/core/client/stream/test/comments/permalinkViewCommentNotFound.spec.tsx b/src/core/client/stream/test/comments/permalinkViewCommentNotFound.spec.tsx index 9c4517351..6ff259e93 100644 --- a/src/core/client/stream/test/comments/permalinkViewCommentNotFound.spec.tsx +++ b/src/core/client/stream/test/comments/permalinkViewCommentNotFound.spec.tsx @@ -33,7 +33,10 @@ beforeEach(() => { comment: () => null, asset: createSinonStub( s => s.throws(), - s => s.withArgs(undefined, { id: assetStub.id }).returns(assetStub) + s => + s + .withArgs(undefined, { id: assetStub.id, url: null }) + .returns(assetStub) ), }, }; diff --git a/src/core/client/stream/test/comments/renderReplies.spec.tsx b/src/core/client/stream/test/comments/renderReplies.spec.tsx index b18b77b17..11ffcf000 100644 --- a/src/core/client/stream/test/comments/renderReplies.spec.tsx +++ b/src/core/client/stream/test/comments/renderReplies.spec.tsx @@ -14,7 +14,7 @@ beforeEach(() => { s => s.throws(), s => s - .withArgs(undefined, { id: assetWithDeepReplies.id }) + .withArgs(undefined, { id: assetWithDeepReplies.id, url: null }) .returns(assetWithDeepReplies) ), }, diff --git a/src/core/client/stream/test/comments/renderStream.spec.tsx b/src/core/client/stream/test/comments/renderStream.spec.tsx index 2778acb02..6a104ea6e 100644 --- a/src/core/client/stream/test/comments/renderStream.spec.tsx +++ b/src/core/client/stream/test/comments/renderStream.spec.tsx @@ -12,7 +12,10 @@ beforeEach(() => { Query: { asset: createSinonStub( s => s.throws(), - s => s.withArgs(undefined, { id: assets[0].id }).returns(assets[0]) + s => + s + .withArgs(undefined, { id: assets[0].id, url: null }) + .returns(assets[0]) ), }, }; diff --git a/src/core/client/stream/test/comments/showAllReplies.spec.tsx b/src/core/client/stream/test/comments/showAllReplies.spec.tsx index 547ac7b33..ac1c699e2 100644 --- a/src/core/client/stream/test/comments/showAllReplies.spec.tsx +++ b/src/core/client/stream/test/comments/showAllReplies.spec.tsx @@ -71,7 +71,10 @@ beforeEach(() => { ), asset: createSinonStub( s => s.throws(), - s => s.withArgs(undefined, { id: assetStub.id }).returns(assetStub) + s => + s + .withArgs(undefined, { id: assetStub.id, url: null }) + .returns(assetStub) ), }, }; diff --git a/src/core/client/test/setupTestFramework.ts b/src/core/client/test/setupTestFramework.ts new file mode 100644 index 000000000..2f7044378 --- /dev/null +++ b/src/core/client/test/setupTestFramework.ts @@ -0,0 +1,2 @@ +// Automatically unmock console. +import "jest-mock-console/dist/setupTestFramework"; diff --git a/src/core/server/app/middleware/passport/jwt.spec.ts b/src/core/server/app/middleware/passport/jwt.spec.ts index 3c25db7a4..6078b35f3 100644 --- a/src/core/server/app/middleware/passport/jwt.spec.ts +++ b/src/core/server/app/middleware/passport/jwt.spec.ts @@ -4,65 +4,34 @@ import { Config } from "talk-common/config"; import { createJWTSigningConfig, extractJWTFromRequest, - parseAuthHeader, } from "talk-server/app/middleware/passport/jwt"; import { Request } from "talk-server/types/express"; -describe("parseAuthHeader", () => { - it("parses valid headers", () => { - const parsed = { - scheme: "bearer", - value: "token", - }; - - expect(parseAuthHeader("Bearer token")).toEqual(parsed); - - expect(parseAuthHeader("bearer token")).toEqual(parsed); - - expect(parseAuthHeader("bearer token")).toEqual(parsed); - }); - - it("parses invalid headers", () => { - expect(parseAuthHeader("this-is-a-wrong-header")).toEqual(null); - expect(parseAuthHeader("bearerthis-is-a-wrong-header")).toEqual(null); - }); -}); - describe("extractJWTFromRequest", () => { it("extracts the token from header", () => { const req = { - get: sinon - .stub() - .withArgs("authorization") - .returns("Bearer token"), + headers: { + authorization: "Bearer token", + }, + url: "", }; expect(extractJWTFromRequest((req as any) as Request)).toEqual("token"); - expect(req.get.calledOnce).toBeTruthy(); - req.get.reset(); - req.get.returns(null); + delete req.headers.authorization; + expect(extractJWTFromRequest((req as any) as Request)).toEqual(null); - expect(req.get.calledOnce).toBeTruthy(); }); it("extracts the token from query string", () => { const req = { - get: sinon - .stub() - .withArgs("authorization") - .returns(null), - query: { access_token: "token" }, + url: "", }; + expect(extractJWTFromRequest((req as any) as Request)).toEqual(null); + + req.url = "https://talk.coralproject.net/api?access_token=token"; expect(extractJWTFromRequest((req as any) as Request)).toEqual("token"); - expect(req.get.calledOnce).toBeTruthy(); - - delete req.query.access_token; - - req.get.reset(); - expect(extractJWTFromRequest((req as any) as Request)).toEqual(null); - expect(req.get.calledOnce).toBeTruthy(); }); }); diff --git a/src/core/server/app/middleware/passport/jwt.ts b/src/core/server/app/middleware/passport/jwt.ts index 3736b6e14..0359ef450 100644 --- a/src/core/server/app/middleware/passport/jwt.ts +++ b/src/core/server/app/middleware/passport/jwt.ts @@ -2,41 +2,20 @@ import { Redis } from "ioredis"; import jwt, { SignOptions } from "jsonwebtoken"; import { Db } from "mongodb"; import { Strategy } from "passport-strategy"; +import { Bearer } from "permit"; import uuid from "uuid"; import { Config } from "talk-common/config"; import { retrieveUser, User } from "talk-server/models/user"; import { Request } from "talk-server/types/express"; -const authHeaderRegex = /(\S+)\s+(\S+)/; - -export function parseAuthHeader(header: string) { - const matches = header.match(authHeaderRegex); - if (!matches || matches.length < 3) { - return null; - } - - return { - scheme: matches[1].toLowerCase(), - value: matches[2], - }; -} - export function extractJWTFromRequest(req: Request) { - const header = req.get("authorization"); - if (header) { - const parts = parseAuthHeader(header); - if (parts && parts.scheme === "bearer") { - return parts.value; - } - } + const permit = new Bearer({ + basic: "password", + query: "access_token", + }); - const token: string | undefined | false = req.query && req.query.access_token; - if (token) { - return token; - } - - return null; + return permit.check(req) || null; } function generateJTIBlacklistKey(jti: string) { diff --git a/src/types/permit.d.ts b/src/types/permit.d.ts new file mode 100644 index 000000000..c24f13a6b --- /dev/null +++ b/src/types/permit.d.ts @@ -0,0 +1,31 @@ +// TODO: (wyattjoh) following https://github.com/DefinitelyTyped/DefinitelyTyped/pull/29061 to merge then replace this with @types/permit. +declare module "permit" { + import { IncomingMessage, ServerResponse } from "http"; + + export interface PermitOptions { + scheme?: string; + proxy?: string; + realm?: string; + } + + export interface BearerOptions extends PermitOptions { + basic?: string; + header?: string; + query?: string; + } + + export class Permit { + constructor(options: PermitOptions); + check(req: IncomingMessage): void; + fail(res: ServerResponse): void; + } + + export class Bearer extends Permit { + constructor(options: BearerOptions); + check(req: IncomingMessage): string; + } + + export class Basic extends Permit { + check(req: IncomingMessage): [string, string]; + } +} \ No newline at end of file