Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
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
3 changes: 1 addition & 2 deletions backend/src/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const permissions = shield(
{
Query: {
"*": forbidden,
posts: isAuthenticated,
posts: allow,
users: isAuthenticated,
},
Mutation: {
Expand All @@ -39,6 +39,5 @@ export const permissions = shield(
},
{
allowExternalErrors: true,
fallbackRule: isAuthenticated,
}
);
10 changes: 0 additions & 10 deletions backend/src/posts.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,6 @@ describe("queries", () => {

let postQuery = () => query({ query: POSTS });

it("throws error when user is not authorised", async () => {
userId = null;
await expect(postQuery()).resolves.toMatchObject({
data: {
posts: null,
},
errors: [expect.objectContaining({ message: "Not Authorised!" })],
});
});

it("returns empty array", async () => {
await expect(postQuery()).resolves.toMatchObject({
errors: undefined,
Expand Down
3 changes: 2 additions & 1 deletion webapp/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ npm-debug.log*
storybook-static

### Nuxt.js###
.nuxt
.nuxt
static/sw.js
59 changes: 59 additions & 0 deletions webapp/components/LoginForm/LoginForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<template>
<div>
<div v-if="loggedIn">You are Logged in</div>
<form onsubmit="event.preventDefault();">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<form onsubmit="event.preventDefault();">
<form @submit.prevent="submit">

<input
type="email"
aria-label="Email"
v-model="email"
placeholder="Email"
/>
<input
type="password"
aria-label="Password"
v-model="password"
placeholder="Password"
/>
<div v-if="invalidCredentials">Falsche Email oder Passwort</div>
<input type="submit" aria-label="Login" value="Login" @click="submit" />
</form>
</div>
</template>

<script>
import { mapState, mapGetters, mapActions } from "vuex";
export default {
computed: {
...mapGetters(["loggedIn"]),
...mapState(["currentUser"]),
},
methods: {
...mapActions(["login"]),
submit: async function () {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
submit: async function () {
async submit() {

try {
await this.login({
email: this.email,
password: this.password,
apolloClient: this.$apollo,
});
this.$router.push({
path: "/",
});
this.invalidCredentials = false;
} catch (error) {
this.invalidCredentials = true;
}
},
},
data: function () {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
data: function () {
data() {

return {
email: "",
password: "",
invalidCredentials: false,
};
},
};
</script>

<style>
</style>
32 changes: 20 additions & 12 deletions webapp/components/NewsItem/NewsItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,48 @@
<div>
<div class="item-title">{{ item.title }} ({{ item.votes }})</div>
<div class="item-buttons">
<button @click="upvote">Upvote</button>
<button @click="downvote">Downvote</button>
<button @click="remove">Remove</button>
<button v-if="loggedIn" @click="upvote">Upvote</button>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⭐ For an upvote button that behaves according to the authentication state of your user

<button v-if="loggedIn" @click="downvote">Downvote</button>
<button v-if="isAuthor" @click="remove">Remove</button>
<button v-if="isAuthor" @click="edit">Edit</button>
Comment on lines +5 to +8

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⭐Implemented Upvote and Downvote in logged in state, Remove and Edit only if it's the author. Optional: Only show the upvote or downvote button if you didn't upvoted / downvoted before.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➕ 1️⃣

</div>
</div>
</template>

<script>
import { mapGetters, mapState } from "vuex";
export default {
props: ["item"],
methods: {
upvote: function () {
let item = {...this.item};
item.votes++;
this.$emit("updateItem", item);
this.$emit("upvote");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to use this.$apollo.mutate(/* .. */) here directly and remove the events. Other components will re-render, since you update the cache.

},
downvote: function () {
let item = {...this.item};
item.votes--;
this.$emit("updateItem", item);
this.$emit("downvote");
},
remove: function () {
this.$emit("removeItem", this.item);
this.$emit("remove", this.item);
},
edit: function () {
this.$emit("edit", this.item);
},
},
computed: {
...mapGetters(["loggedIn"]),
...mapState(["currentUser"]),
isAuthor: function () {
return this.currentUser === this.item.author.id;
},
},
};
</script>

<style>
.item-title {
.post-title {
font-size: 24px;
text-align: center;
}
.item-buttons {
.post-buttons {
padding: 15px 0;
}
</style>
151 changes: 103 additions & 48 deletions webapp/components/NewsList/NewsList.spec.js
Original file line number Diff line number Diff line change
@@ -1,75 +1,130 @@
import { shallowMount, mount } from "@vue/test-utils";
import { createLocalVue, mount } from "@vue/test-utils";
import VueApollo from "vue-apollo";
import Vuex from "vuex";
import { createMockClient } from "mock-apollo-client";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it seems to have helped you, good to know!

import NewsList from "./NewsList.vue";
import NewsItem from "../NewsItem/NewsItem.vue";
import allPostsQuery from "../../gql/Posts.gql";

const testItems = [
{ id: 1, title: "VueJS", votes: 1 },
{ id: 2, title: "TDD", votes: 4 },
{ id: 3, title: "React", votes: 0 },
];
const descendingTestItems = [
{ id: 2, title: "TDD", votes: 4 },
{ id: 1, title: "VueJS", votes: 1 },
{ id: 3, title: "React", votes: 0 },
];
const ascendingTestItems = [
{ id: 3, title: "React", votes: 0 },
{ id: 1, title: "VueJS", votes: 1 },
{ id: 2, title: "TDD", votes: 4 },
];
const postListMock = {
data: {
posts: [
{
id: "1",
title: "Vue",
votes: 4,
author: {
id: "1",
__typename: "User",
},
__typename: "Post",
},
{
id: "2",
title: "React",
votes: 0,
author: {
id: "2",
__typename: "User",
},
__typename: "Post",
},
{
id: "3",
title: "TDD",
votes: 2,
author: {
__typename: "User",
id: "3",
},
__typename: "Post",
},
],
},
};

const localVue = createLocalVue();
localVue.use(VueApollo);
localVue.use(Vuex);

describe("NewsList.vue", () => {
describe("empty", () => {
it("renders a message when the item list is empty", () => {
const wrapper = shallowMount(NewsList, {
propsData: {
initialItems: [],
let wrapper;
let mockClient;
let apolloProvider;
let requestHandlers;
let store = new Vuex.Store({
getters: {
loggedIn: () => false,
},
mutations: {
setToken() {},
},
});

const createComponent = (handlers) => {
mockClient = createMockClient({
resolvers: {},
});
requestHandlers = {
allPostsQueryHandler: jest.fn().mockResolvedValue({ ...postListMock }),
...handlers,
};
mockClient.setRequestHandler(allPostsQuery, requestHandlers.allPostsQueryHandler);
apolloProvider = new VueApollo({ defaultClient: mockClient });
const getToken = jest.fn();
wrapper = mount(NewsList, {
store,
localVue,
apolloProvider,
mocks: {
$apolloHelpers: {
getToken,
},
},
});
};

afterEach(() => {
wrapper.destroy();
mockClient = null;
apolloProvider = null;
});

it("renders a Vue component", () => {
createComponent();
expect(wrapper.exists()).toBe(true);
expect(wrapper.vm.$apollo.queries.items).toBeTruthy();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need to test this.

});

describe("empty", () => {
it("renders a message when the item list is empty", async () => {
createComponent({
allPostsQueryHandler: jest.fn().mockResolvedValue({ data: { posts: [] } }),
});
await wrapper.vm.$nextTick();

Comment on lines +100 to +105

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⭐ Refactor looks good. We also used wrapper.vm.$nextTick() which is considered ugly but at least it works.

expect(wrapper.find("#emptyListMessage").text()).toBe("The list is empty :(");
});
});
describe("not empty", () => {
let wrapper;
beforeEach(() => {
wrapper = mount(NewsList, {
propsData: {
initialItems: testItems,
},
});
beforeEach(async () => {
createComponent();
await wrapper.vm.$nextTick();
});
it("does not render empty list message when item list is filled", () => {
expect(wrapper.find("#emptyListMessage").exists()).toBe(false);
});
it("orderedItems sorts items in descending order by default", () => {
let localThis = {
items: testItems,
descending: true,
};
expect(NewsList.computed.orderedItems.call(localThis)).toEqual(descendingTestItems);
});
it("renders items in descending order by default", () => {
let newsItems = wrapper.findAllComponents(NewsItem);
expect(newsItems.wrappers.map((i) => i.props("item").title)).toEqual(
descendingTestItems.map((i) => i.title)
);
expect(newsItems.wrappers.map((i) => i.props("item").title)).toEqual(["Vue", "TDD", "React"]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better to read.

});
describe("Reverse order", () => {
it("orderedItems sorts items in ascending order", () => {
let localThis = {
items: testItems,
descending: false,
};
expect(NewsList.computed.orderedItems.call(localThis)).toEqual(ascendingTestItems);
});
describe("click 'Reverse Order'", () => {
it("renders items in ascending order", async () => {
let reverseOrderButton = wrapper.find("#reverseOrder");
await reverseOrderButton.trigger("click");
let newsItems = wrapper.findAllComponents(NewsItem);
expect(newsItems.wrappers.map((i) => i.props("item").title)).toEqual(
ascendingTestItems.map((i) => i.title)
);
expect(newsItems.wrappers.map((i) => i.props("item").title)).toEqual(["React", "TDD", "Vue"]);
});
});
});
Expand Down
Loading