1
0
mirror of https://github.com/mgerb/classic-wow-forums synced 2026-01-09 00:42:47 +00:00

little bit of everything - it's been a long day

This commit is contained in:
2018-01-14 23:46:15 -06:00
parent d9d7f2d202
commit efbee265d3
25 changed files with 298 additions and 125 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

View File

@@ -2,10 +2,10 @@
z-index: 1;
top: 0;
left: 0;
height: calc(100% - 96px);
height: 100%;
width: 100%;
position: fixed;
background-color: rgba(0, 0, 0, .5);
background-color: rgba(0, 0, 0, .9);
padding-top: 50px;
}
@@ -15,21 +15,22 @@
flex-direction: column;
padding-bottom: 40px;
&__character-count {
align-self: flex-end;
}
&__title {
height: 25px;
margin-bottom: 20px;
}
&__text-area {
min-height: 100px;
resize: none;
margin-bottom: 20px;
overflow-wrap: break-word;
}
&__preview {
min-height: 100px;
background-color: #161616;
border-radius: 2px;
padding: 5px 10px;
overflow-y: auto;

View File

@@ -5,15 +5,17 @@ import { ContentContainer } from '../content-container/content-container';
import './editor.scss';
interface Props {
categoryId: string;
categoryId?: string;
onClose: (cancel: boolean) => any;
threadId?: string;
}
interface State {
title: string;
content: string;
contentPreview: string;
characterCount: number;
contentCharacterCount: number;
titleCharacterCount: number;
errorMessage?: string;
}
@@ -24,7 +26,8 @@ export class Editor extends React.Component<Props, State> {
title: '',
content: '',
contentPreview: '',
characterCount: 0,
contentCharacterCount: 0,
titleCharacterCount: 0,
};
}
@@ -32,12 +35,48 @@ export class Editor extends React.Component<Props, State> {
this.setState({
content: event.target.value,
contentPreview: marked(event.target.value, { sanitize: true }),
characterCount: event.target.value.length,
contentCharacterCount: event.target.value.length,
});
}
onSubmit() {
onTitleChange(event: any) {
this.setState({
title: event.target.value,
titleCharacterCount: event.target.value.length,
});
}
onSubmit(event: any) {
event.preventDefault();
if (this.props.threadId) {
this.newReply();
} else {
this.newThread();
}
}
async newReply() {
const { content } = this.state;
if (content === '') {
this.setState({ errorMessage: 'Content must not be blank.' });
return;
}
const data = {
content,
thread_id: this.props.threadId,
};
try {
await axios.post('/api/reply', data);
this.props.onClose(false);
} catch (e) {
this.setState({ errorMessage: 'Server error. Please try again later.' });
}
}
async newThread() {
const { title, content } = this.state;
if (title === '' || content === '') {
@@ -51,10 +90,6 @@ export class Editor extends React.Component<Props, State> {
category_id: this.props.categoryId,
};
this.newThread(data);
}
async newThread(data: any) {
try {
await axios.post('/api/thread', data);
this.props.onClose(false);
@@ -63,25 +98,46 @@ export class Editor extends React.Component<Props, State> {
}
}
renderThreadPortion() {
return (
<div className="flex flex--column">
<h2 style={{ color: 'white' }}>New Topic</h2>
<label>Title</label>
<input type="text"
className="input editor__title"
onChange={event => this.onTitleChange(event)}
maxLength={300}/>
<div className="editor__character-count">{this.state.contentCharacterCount}/2000</div>
</div>
);
}
// TODO: quote
renderReplyPortion() {
return (
<h2 style={{ color: 'white' }}>New Reply</h2>
);
}
render() {
return (
<div className="editor-container">
<ContentContainer className="editor">
<h2 style={{ color: 'white' }}>New Topic</h2>
<label>Title</label>
<input type="text" className="input editor__title" onChange={event => this.setState({ title: event.target.value })}/>
<label>Content</label>
<textarea className="input editor__text-area flex-1" onChange={event => this.onContentChange(event)}/>
<label>Content Preview</label>
<div className="editor__preview flex-1" dangerouslySetInnerHTML={{ __html: this.state.contentPreview }}></div>
<div className="editor__submit">
<a onClick={() => this.onSubmit()}>Submit</a>
<a onClick={() => this.props.onClose(true)} style={{ marginLeft: '10px' }}>Cancel</a>
<span className="editor__error-message">{this.state.errorMessage}</span>
<span style={{ float: 'right' }}>{this.state.characterCount}/2000</span>
</div>
</ContentContainer>
<form onSubmit={event => this.onSubmit(event)} onReset={() => this.props.onClose(true)}>
<ContentContainer className="editor">
{this.props.threadId ? this.renderReplyPortion() : this.renderThreadPortion()}
<label>Content</label>
<textarea className="input editor__text-area flex-1" onChange={event => this.onContentChange(event)} maxLength={2000}/>
<div className="editor__character-count">{this.state.contentCharacterCount}/2000</div>
<label>Preview</label>
<div className="editor__preview flex-1" dangerouslySetInnerHTML={{ __html: this.state.contentPreview }}></div>
<div className="editor__submit">
<input type="submit" value="Submit" className="input__button"/>
<input type="reset" value="Cancel" className="input__button" style={{ marginLeft: '10px' }}/>
<span className="editor__error-message">{this.state.errorMessage}</span>
</div>
</ContentContainer>
</form>
</div>
);
}

View File

@@ -0,0 +1,6 @@
.portrait-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
}

View File

@@ -4,6 +4,7 @@ import { Portrait } from '../portrait/portrait';
import { UserStore } from '../../stores/user-store';
import { CharacterService } from '../../services';
import { Oauth } from '../../util';
import './login-button.scss';
interface Props {
className?: string;
@@ -24,7 +25,7 @@ export class LoginButton extends React.Component<Props, State> {
renderPortrait() {
const avatarSrc = CharacterService.getAvatar(this.props.userStore!.user!.character_avatar!);
return (
<div style={{ padding: '10px' }}>
<div className="portrait-container">
<div onClick={() => this.props.onNavigate('/user-account')} style={{ cursor: 'pointer' }}>
{avatarSrc && <Portrait imageSrc={avatarSrc}/>}
</div>

View File

@@ -1,4 +1,6 @@
export interface RepyModel {
import { UserModel } from './user';
export interface ReplyModel {
content: string;
edited: boolean;
id: number;
@@ -7,4 +9,5 @@ export interface RepyModel {
thread_id: number;
updated_at: string;
user_id: number;
user: UserModel;
}

View File

@@ -1,20 +1,20 @@
import { RepyModel } from './reply';
import { ReplyModel } from './reply';
import { UserModel } from './user';
export interface ThreadModel {
category_id: number;
content: string;
title: string;
edited: boolean;
id: number;
inserted_at: string;
last_reply: { id: number; battletag: string };
last_reply: UserModel;
last_reply_id: number;
locked: boolean;
replies: RepyModel[];
replies: ReplyModel[];
reply_count: number;
sticky: boolean;
updated_at: string;
user: { id: number; battletag: string };
user: UserModel;
user_id: number;
view_count: number;
}

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { Link, RouteComponentProps } from 'react-router-dom';
import { get } from 'lodash';
import { inject, observer } from 'mobx-react';
import { ThreadService } from '../../services';
import { Editor, ForumNav, LoginButton, ScrollToTop } from '../../components';
@@ -107,10 +106,13 @@ export class Forum extends React.Component<Props, State> {
<div className={`forum-row ${index % 2 === 0 && 'forum-row--dark'}`} key={index}>
{this.renderCell('flag', { maxWidth: '50px' })}
{this.renderCell(<Link to={`/f/${categoryId}/${thread.id}`}>{thread.title}</Link>, { minWidth: '200px' })}
{this.renderCell(<b>{thread.user.battletag}</b>, { maxWidth: '150px' })}
{this.renderCell(<b>{thread.user.character_name || thread.user.battletag}</b>, { maxWidth: '150px' })}
{this.renderCell(<b>{thread.reply_count}</b>, { maxWidth: '150px' }, true)}
{this.renderCell(<b>{thread.view_count}</b>, { maxWidth: '150px' }, true)}
{this.renderCell(<span>by <b>{get(thread, 'last_reply.battletag')}</b></span>, { maxWidth: '200px' })}
{this.renderCell(
<span>by <b>{thread.last_reply.character_name || thread.last_reply.battletag}</b></span>,
{ maxWidth: '200px' },
)}
</div>
);
});

View File

@@ -122,7 +122,7 @@ export class Home extends React.Component<Props, State> {
{this.renderTopic(139, 'Bug Report Forum', bugs, `Found a bug on this site? Help us squash it by reporting it here!`)}
</div>
<hr />
<hr className="hr"/>
<div className="topic-row topic-row__classes">
<div className="topic-item topic-item__classes">
@@ -193,7 +193,7 @@ export class Home extends React.Component<Props, State> {
</div>
</div>
<hr />
<hr className="hr"/>
</ContentContainer>
</ScrollToTop>
);

View File

@@ -46,8 +46,11 @@
.reply {
display: flex;
background-color: rgb(22, 22, 22);
// border: 1px solid #343434;
background-color: rgb(37, 37, 37);
&--dark {
background-color: rgb(22, 22, 22);
}
&__user-container {
display: flex;
@@ -89,3 +92,8 @@
width: 100%;
height: 15px;
}
.character-name {
color: #FFAC04;
font-weight: bold;
}

View File

@@ -2,14 +2,16 @@ import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { get, map } from 'lodash';
import marked from 'marked';
import { ThreadService } from '../../services';
import { Portrait, ScrollToTop } from '../../components';
import { ThreadModel } from '../../model';
import { DateTime } from 'luxon';
import { CharacterService, ThreadService } from '../../services';
import { Editor, Portrait, ScrollToTop } from '../../components';
import { ReplyModel, ThreadModel } from '../../model';
import './thread.scss';
interface Props extends RouteComponentProps<any> {}
interface State {
showEditor: boolean;
thread?: ThreadModel;
}
@@ -17,41 +19,89 @@ export class Thread extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {};
this.state = {
showEditor: false,
};
}
componentDidMount() {
this.getThreads();
this.getReplies();
}
private async getThreads() {
private async getReplies() {
const thread = await ThreadService.getThread(this.props.match.params['threadId']);
thread.replies = map([thread as any, ...thread.replies], (reply) => { // add the thread topic to the front of the list
thread.replies = map(thread.replies, (reply) => { // add the thread topic to the front of the list
reply.content = marked(reply.content, { sanitize: true });
return reply;
});
this.setState({ thread });
}
private onReplyClick() {
this.setState({ showEditor: true });
}
private onQuoteClick(reply: ReplyModel) {
console.log(reply);
}
private onEditorClose(cancel: boolean) {
this.setState({ showEditor: false });
if (!cancel) {
this.getReplies();
}
}
private navigateForumIndex() {
this.props.history.push(`/f/${this.state.thread!.category_id}`);
}
private getTimeFormat(dateTime: string) {
return DateTime.fromISO(dateTime).toLocaleString({
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'short',
});
}
renderUserInfo(reply: ReplyModel) {
const { battletag, character_avatar, character_class, character_guild, character_name, character_realm } = reply.user;
return (
<div className="reply__user-container">
<Portrait imageSrc={CharacterService.getAvatar(character_avatar)}/>
<div style={{ textAlign: 'center' }}>
<div className="character-name">{character_name || battletag}</div>
{character_class && <div><small>{character_class}</small></div>}
{character_guild && <div><small><b>Guild: </b>{character_guild}</small></div>}
{character_realm && <div><small><b>Realm: </b>{character_realm}</small></div>}
</div>
</div>
);
}
renderReplies(): any {
return this.state.thread!.replies.map((reply, index) => {
const replyDark = index % 2 === 0 ? 'reply--dark' : '';
return (
<div className="reply-container" key={index}>
<div className="reply">
<div className="reply__user-container">
<Portrait imageSrc={require('../../assets/Tyren.gif')}/>
<div>Tyren</div>
<div>Blizzard Poster</div>
</div>
<div className={`reply ${replyDark}`}>
{this.renderUserInfo(reply)}
<div className="flex-1">
<div className="reply__title">
<div>
<b>{`${index + 1}. `}{this.state.thread!.title}</b>
<small style={{ paddingLeft: '5px' }}>| {reply.inserted_at}</small>
<b>{`${index + 1}. `}{index > 0 && 'Re: '}{this.state.thread!.title}</b>
<small style={{ paddingLeft: '5px' }}>| {this.getTimeFormat(reply.inserted_at)}</small>
</div>
<div>
<img src={require('../../assets/quote-button.gif')} className="reply__title__button"/>
<img src={require('../../assets/reply-button.gif')} className="reply__title__button"/>
<img src={require('../../assets/quote-button.gif')}
className="reply__title__button"
onClick={() => this.onQuoteClick(reply)}/>
<img src={require('../../assets/reply-button.gif')}
className="reply__title__button"
onClick={() => this.onReplyClick()}/>
</div>
</div>
<div className="reply__content" dangerouslySetInnerHTML={{ __html: reply.content }}/>
@@ -62,22 +112,23 @@ export class Thread extends React.Component<Props, State> {
});
}
private navigateForumIndex() {
this.props.history.push(`/f/${this.state.thread!.category_id}`);
}
render() {
if (!this.state.thread) {
return <div></div>;
}
const replies = get(this.state, 'thread.replies');
return (
<ScrollToTop {...this.props}>
{this.state.showEditor && <Editor threadId={this.props.match.params['threadId']} onClose={cancel => this.onEditorClose(cancel)}/>}
<div className="topic-bg">
<div className="threadTopic-container">
<div className="threadTopic">
<img src={require('../../assets/sticky.gif')} style={{ marginRight: '5px' }}/>
<b>Topic: </b><small style={{ paddingLeft: '15px', color: 'white' }}>| 12/20/2005 1:11:44 AM PST</small>
<b>Topic: </b>
<small style={{ paddingLeft: '15px', color: 'white' }}>| {this.getTimeFormat(this.state.thread!.inserted_at)}</small>
</div>
</div>
<img src={require('../../assets/forum-index.gif')}

View File

@@ -177,7 +177,7 @@ export class UserAccount extends React.Component<Props, State> {
return (
<ScrollToTop>
<ContentContainer style={{ minHeight: '500px', paddingTop: '40px' }}>
<div className="flex">
<div className="flex" style={{ marginBottom: '20px' }}>
{character_avatar && <Portrait imageSrc={CharacterService.getAvatar(character_avatar!)}/>}
<div style={{ paddingLeft: '10px' }}>
{battletag && <div><b>Battletag: </b>{battletag}</div>}

View File

@@ -1,4 +1,5 @@
$fontPrimary: #cccccc;
$linkColor: #FFB019;
html {
font-family: Arial,Helvetica,Sans-Serif;
@@ -19,7 +20,7 @@ body {
}
a {
color: #FFB019;
color: $linkColor;
font-weight: bold;
font-size: 9pt;
text-decoration: underline;
@@ -35,6 +36,20 @@ a {
}
hr {
size: 1;
background-color: #9E9E9E;
display: block;
unicode-bidi: isolate;
-webkit-margin-before: 0.5em;
-webkit-margin-after: 0.5em;
-webkit-margin-start: auto;
-webkit-margin-end: auto;
overflow: hidden;
border: none;
height: 1px;
}
.hr {
background: rgb(71, 71, 71);
width: 80%;
height: 0.5px;
@@ -44,6 +59,10 @@ hr {
clear: both;
}
h1, h2, h3, h4, h5, p {
margin-top: 0;
}
.inline-block {
display: inline-block;
}
@@ -58,6 +77,10 @@ hr {
&--wrap {
flex-wrap: wrap;
}
&--column {
flex-direction: column;
}
}
.flex-1 {
@@ -89,4 +112,18 @@ span.grey {
&:focus {
outline: none;
}
&__button {
background: none;
border: none;
text-decoration: underline;
color: $linkColor;
font-weight: bold;
font-size: 9pt;
cursor: pointer;
&:hover {
color: white;
}
}
}

View File

@@ -1,9 +1,9 @@
import { find, filter, get } from 'lodash';
import { AvatarModel } from '../model';
const getAvatar = (title: string): any => {
const getAvatar = (title?: string): any => {
const av = find(avatarList, { title });
return get(av, 'imageSrc');
return get(av, 'imageSrc') || avatarList[0].imageSrc;
};
const getClass = (index: number): {id: number, name: string; races: number[] } => {
@@ -66,6 +66,11 @@ const classList = [
];
const avatarList: AvatarModel[] = [
{
raceId: 0,
title: 'unknown',
imageSrc: require('../assets/avatars/unknown.gif'),
},
{
raceId: 1,
title: 'dwarf_f',

View File

@@ -14,6 +14,7 @@
"dependencies": {
"@types/fingerprintjs2": "^1.5.1",
"@types/lodash": "^4.14.92",
"@types/luxon": "^0.2.2",
"@types/marked": "^0.3.0",
"@types/node": "^9.3.0",
"@types/query-string": "^5.0.1",
@@ -38,6 +39,7 @@
"font-awesome": "^4.7.0",
"html-webpack-plugin": "^2.30.1",
"lodash": "^4.17.4",
"luxon": "^0.3.1",
"marked": "^0.3.12",
"mobx": "^3.4.1",
"mobx-react": "^4.3.5",

View File

@@ -14,6 +14,10 @@
version "4.14.92"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.92.tgz#6e3cb0b71a1e12180a47a42a744e856c3ae99a57"
"@types/luxon@^0.2.2":
version "0.2.2"
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-0.2.2.tgz#ca83c1026980b111254ef78587ee027483e57df4"
"@types/marked@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.3.0.tgz#583c223dd33385a1dda01aaf77b0cd0411c4b524"
@@ -3665,6 +3669,10 @@ lru-cache@^4.0.1:
pseudomap "^1.0.2"
yallist "^2.1.2"
luxon@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-0.3.1.tgz#627f78f5fea94ac19e1cf2900f398a9338422bbd"
macaddress@^0.2.8:
version "0.2.8"
resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"

View File

@@ -12,12 +12,13 @@ defmodule MyApp.Data.Reply do
field :content, :string
field :edited, :boolean, default: false
field :quote_id, :integer
timestamps()
has_one :user, Data.User, foreign_key: :id, references: :user_id
timestamps(type: :utc_datetime)
end
defp insert_changeset(reply, params \\ %{}) do
reply
|> cast(params, [:user_id, :thread_id, :content, :quote])
|> cast(params, [:user_id, :thread_id, :content, :quote_id])
|> validate_required([:user_id, :thread_id, :content])
|> foreign_key_constraint(:user_id)
|> foreign_key_constraint(:thread_id)
@@ -33,10 +34,11 @@ defmodule MyApp.Data.Reply do
@spec insert(map) :: {:ok, map} | {:error, map}
def insert(params) do
insert_changeset(%Data.Reply{}, params)
{:ok, data} = insert_changeset(%Data.Reply{}, params)
|> Repo.insert
|> Data.Util.process_insert_or_update
|> update_thread_new_reply
{:ok, Map.drop(data, [:user])} # drop user because we can't encode it if it's not preloaded
end
defp update_thread_new_reply({:error, error}), do: {:error, error}

View File

@@ -15,11 +15,11 @@ defmodule MyApp.Data.Thread do
field :sticky, :boolean, default: false
field :locked, :boolean, default: false
field :edited, :boolean, default: false
field :reply_count, :integer, default: 1
field :reply_count, :integer, default: 0
has_many :replies, Data.Reply
has_one :user, Data.User, foreign_key: :id, references: :user_id
has_one :last_reply, Data.User, foreign_key: :id, references: :last_reply_id
timestamps()
timestamps(type: :utc_datetime)
end
defp insert_changeset(thread, params \\ %{}) do
@@ -39,7 +39,7 @@ defmodule MyApp.Data.Thread do
def get(thread_id) do
query = from t in Data.Thread,
where: t.id == ^thread_id,
preload: [:user, :last_reply, :replies]
preload: [:user, :last_reply, replies: :user]
Repo.one(query)
|> process_get
@@ -79,18 +79,20 @@ defmodule MyApp.Data.Thread do
@spec insert(map) :: {:ok, map} | {:error, map}
def insert(params) do
Repo.transaction(fn ->
{_, data} = Repo.transaction(fn ->
params = Map.put(params, "last_reply_id", Map.get(params, "user_id"))
{:ok, thread} = insert_changeset(%Data.Thread{}, params)
|> Repo.insert
{:ok, _} = Repo.insert(%Data.Reply{
{:ok, data} = Repo.insert(%Data.Reply{
thread_id: Map.get(thread, :id),
content: Map.get(params, "content"),
user_id: Map.get(params, "user_id")
})
"ok"
# return the new thread we inserted - drop associations because we don't load them
{:ok, Map.drop(thread, [:last_reply, :replies, :user])}
end)
data
end
# this doesn't update the 'updated_at' field which is what we want

View File

@@ -15,7 +15,7 @@ defmodule MyApp.Data.User do
field :character_class, :string
field :character_realm, :string
field :character_avatar, :string
timestamps()
timestamps(type: :utc_datetime)
end
defp changeset(user, params \\ %{}) do

View File

@@ -3,7 +3,7 @@ defmodule MyApp.Repo.Migrations.CreateThread do
def change do
create table(:thread) do
add :title, :string
add :title, :string, size: 300
add :category_id, :integer
add :view_count, :integer
add :user_id, references(:user)

View File

@@ -13,18 +13,19 @@ defmodule MyApp.Data.ThreadTest do
end
test "insert: try to insert with no parameters" do
assert insert(%{}) == {:error,
[%{title: "can't be blank"}, %{category_id: "can't be blank"},
%{content: "can't be blank"}, %{user_id: "can't be blank"}]}
{error, _} = catch_error(insert(%{}))
assert error == :badmatch
end
test "insert: insert as invalid user" do
assert insert(new_thread(9238748)) == {:error, [%{user_id: "does not exist"}]}
{:badmatch, {:error, data}} = catch_error(insert(new_thread(9238748)))
assert data.errors == [user_id: {"does not exist", []}]
end
test "insert: insert as invalid category_id" do
{:ok, user} = new_user()
assert insert(new_thread(user.id, 2342342343)) == {:error, [%{category_id: "does not exist"}]}
{error, _} = catch_error(insert(new_thread(user.id, 2342342343)))
assert error == :badmatch
end
test "new thread should be inserted" do
@@ -33,7 +34,6 @@ defmodule MyApp.Data.ThreadTest do
assert thread.title == "test title"
assert thread.category_id == 1
assert thread.user_id == user.id
assert thread.content == "test content"
end
# TODO: update thread

View File

@@ -41,6 +41,11 @@ defmodule MyApp.Data.UserTest do
battletag: "mgerb",
id: user.id,
permissions: "user",
character_avatar: nil,
character_class: nil,
character_guild: nil,
character_name: nil,
character_realm: nil,
}
end

View File

@@ -7,21 +7,14 @@ defmodule MyAppWeb.ReplyControllerTest do
new_conn = build_conn()
|> put_req_header("authorization", "Bearer " <> user.token)
conn = post(new_conn, "/api/reply")
body = conn |> response(400) |> Poison.decode!
{:badmatch, {:error, data}} = conn = catch_error(post(new_conn, "/api/reply"))
assert data == [%{thread_id: "can't be blank"}, %{content: "can't be blank"}]
assert body["error"]["message"] == [
%{"thread_id" => "can't be blank"},
%{"content" => "can't be blank"},
]
{:badmatch, {:error, data}} = conn = catch_error(post(new_conn, "/api/reply", %{"content" => "t"}))
assert data == [%{thread_id: "can't be blank"}]
conn = post(new_conn, "/api/reply", %{"content" => "t"})
body = conn |> response(400) |> Poison.decode!
assert body["error"]["message"] == [%{"thread_id" => "can't be blank"}]
conn = post(new_conn, "/api/reply", %{"content" => "t", "thread_id" => 1})
body = conn |> response(400) |> Poison.decode!
assert body["error"]["message"] == [%{"thread_id" => "does not exist"}]
{:badmatch, {:error, data}} = conn = catch_error(post(new_conn, "/api/reply", %{"content" => "t", "thread_id" => 1}))
assert data == [%{thread_id: "does not exist"}]
end
test "insert new reply should succeed" do
@@ -30,7 +23,7 @@ defmodule MyAppWeb.ReplyControllerTest do
|> put_req_header("authorization", "Bearer " <> user.token)
# insert new thread first
conn = post(new_conn, "/api/thread", %{"title" => "t", "category_id" => 1, "content" => "t"})
conn = post(new_conn, "/api/thread", %{"title" => "t", "category_id" => 1})
body = conn |> response(200) |> Poison.decode!
conn = post(new_conn, "/api/reply", %{"content" => "c", "thread_id" => body["data"]["id"]})
@@ -41,15 +34,13 @@ defmodule MyAppWeb.ReplyControllerTest do
assert data["content"] == "c"
assert data["edited"] == false
assert data["quote"] == false
assert data["quote_id"] == nil
# make sure thread reply count and last reply id are updated
conn = get(new_conn, "/api/thread?category_id=1")
body = conn |> response(200) |> Poison.decode!
# conn = get(new_conn, "/api/thread?category_id=1")
# body = conn |> response(200) |> Poison.decode!
data = Enum.at(body["data"], 0)
assert data["reply_count"] == 1
assert data["last_reply_id"] == user_id
# assert Enum.at(body["data"], 0) == "ok"
end
end

View File

@@ -7,29 +7,22 @@ defmodule MyAppWeb.ThreadControllerTest do
new_conn = build_conn()
|> put_req_header("authorization", "Bearer " <> user.token)
conn = post(new_conn, "/api/thread")
body = conn |> response(400) |> Poison.decode!
{:badmatch, {:error, data}} = catch_error(post(new_conn, "/api/thread"))
assert body["error"]["message"] == [
%{"title" => "can't be blank"},
%{"category_id" => "can't be blank"},
%{"content" => "can't be blank"},
assert data.errors == [
title: {"can't be blank", [validation: :required]},
category_id: {"can't be blank", [validation: :required]},
]
conn = post(new_conn, "/api/thread", %{"title" => "t"})
body = conn |> response(400) |> Poison.decode!
assert body["error"]["message"] == [
%{"category_id" => "can't be blank"},
%{"content" => "can't be blank"},
{:badmatch, {:error, data}} = catch_error(post(new_conn, "/api/thread", %{"title" => "t"}))
assert data.errors == [
category_id: {"can't be blank", [validation: :required]},
]
conn = post(new_conn, "/api/thread", %{"title" => "t", "category_id" => 1})
body = conn |> response(400) |> Poison.decode!
assert body["error"]["message"] == [%{"content" => "can't be blank"}]
{:badmatch, {:error, data}} = catch_error(post(new_conn, "/api/thread", %{"title" => "t", "category_id" => 100000}))
conn = post(new_conn, "/api/thread", %{"title" => "t", "category_id" => 100000, "content" => "t"})
body = conn |> response(400) |> Poison.decode!
assert body["error"]["message"] == [%{"category_id" => "does not exist"}]
assert data.errors == [category_id: {"does not exist", []}]
end
test "insert new thread should succeed" do
@@ -40,10 +33,10 @@ defmodule MyAppWeb.ThreadControllerTest do
body = conn |> response(200) |> Poison.decode!
data = body["data"]
# assert body == "test"
assert data["user_id"] == user.id
assert data["category_id"] == 1
assert data["title"] == "t"
assert data["content"] == "t"
end
# TODO: update thread / delete thread