diff --git a/backend/package.json b/backend/package.json index fdc4c4800..f70c97737 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,7 +15,7 @@ "test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand", "test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/", "test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand", - "test:jest": "run-p --race test:before:* 'test:jest:cmd {@}' --", + "test:jest": "run-p --race test:before:* \"test:jest:cmd {@}\" --", "test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --", "test:jest:debug": "run-p --race test:before:* 'test:jest:cmd:debug {@}' --", "db:script:seed": "wait-on tcp:4001 && babel-node src/seed/seed-db.js", diff --git a/backend/src/middleware/permissionsMiddleware.spec.js b/backend/src/middleware/permissionsMiddleware.spec.js index 89904a7bf..e3c4beb00 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.js +++ b/backend/src/middleware/permissionsMiddleware.spec.js @@ -40,10 +40,13 @@ describe('authorization', () => { }) it('does not expose the owner\'s email address', async () => { + let response = {} try { await action() } catch (error) { - expect(error.response.data).toEqual({ User: [ { email: null } ] }) + response = error.response.data + } finally { + expect(response).toEqual({ User: [ null ] }) } }) }) @@ -74,11 +77,13 @@ describe('authorization', () => { }) it('does not expose the owner\'s email address', async () => { + let response try { await action() } catch (error) { - expect(error.response.data).toEqual({ User: [ { email: null } ] }) + response = error.response.data } + expect(response).toEqual({ User: [ null ] }) }) }) }) diff --git a/backend/src/middleware/slugify/uniqueSlug.js b/backend/src/middleware/slugify/uniqueSlug.js index 8b04edc6f..64e38c8ae 100644 --- a/backend/src/middleware/slugify/uniqueSlug.js +++ b/backend/src/middleware/slugify/uniqueSlug.js @@ -1,6 +1,6 @@ import slugify from 'slug' export default async function uniqueSlug (string, isUnique) { - let slug = slugify(string, { + let slug = slugify(string || 'anonymous', { lower: true }) if (await isUnique(slug)) return slug diff --git a/backend/src/middleware/slugify/uniqueSlug.spec.js b/backend/src/middleware/slugify/uniqueSlug.spec.js index 190899795..6772a20c2 100644 --- a/backend/src/middleware/slugify/uniqueSlug.spec.js +++ b/backend/src/middleware/slugify/uniqueSlug.spec.js @@ -15,4 +15,11 @@ describe('uniqueSlug', () => { .mockResolvedValueOnce(true) expect(uniqueSlug(string, isUnique)).resolves.toEqual('hello-world-1') }) + + it('slugify null string', () => { + const string = null + const isUnique = jest.fn() + .mockResolvedValue(true) + expect(uniqueSlug(string, isUnique)).resolves.toEqual('anonymous') + }) }) diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index bd524e0e4..6e667056c 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -77,7 +77,7 @@ describe('slugify', () => { describe('CreateUser', () => { const action = async (mutation, params) => { return authenticatedClient.request(`mutation { - ${mutation}(password: "yo", ${params}) { slug } + ${mutation}(password: "yo", email: "123@123.de", ${params}) { slug } }`) } it('generates a slug based on name', async () => { diff --git a/backend/src/middleware/userMiddleware.js b/backend/src/middleware/userMiddleware.js index 2979fdadf..4789b4cbd 100644 --- a/backend/src/middleware/userMiddleware.js +++ b/backend/src/middleware/userMiddleware.js @@ -1,5 +1,7 @@ -import createOrUpdateLocations from './nodes/locations' import dotenv from 'dotenv' +import { UserInputError } from 'apollo-server' + +import createOrUpdateLocations from './nodes/locations' dotenv.config() @@ -11,6 +13,10 @@ export default { return result }, UpdateUser: async (resolve, root, args, context, info) => { + const USERNAME_MIN_LENGTH = 3 // TODO move to the correct place + if (!args.name || args.name.length < USERNAME_MIN_LENGTH) { + throw new UserInputError(`Username must be at least ${USERNAME_MIN_LENGTH} characters long!`) + } const result = await resolve(root, args, context, info) await createOrUpdateLocations(args.id, args.locationName, context.driver) return result diff --git a/backend/src/middleware/userMiddleware.spec.js b/backend/src/middleware/userMiddleware.spec.js new file mode 100644 index 000000000..9aa8f96c1 --- /dev/null +++ b/backend/src/middleware/userMiddleware.spec.js @@ -0,0 +1,81 @@ +import { GraphQLClient } from 'graphql-request' +import { host } from '../jest/helpers' +import Factory from '../seed/factories' + +const factory = Factory() +let client + +afterAll(async () => { + await factory.cleanDatabase() +}) + +describe('userMiddleware', () => { + describe('create User', () => { + const mutation = ` + mutation($id: ID, $password: String!, $email: String!) { + CreateUser(id: $id, password: $password, email: $email) { + id + } + } + ` + client = new GraphQLClient(host) + + it('with password and email', async () => { + const variables = { + password: '123', + email: '123@123.de' + } + const expected = { + CreateUser: { + id: expect.any(String) + } + } + await expect(client.request(mutation, variables)) + .resolves.toEqual(expected) + }) + }) + + describe('update User', () => { + const mutation = ` + mutation($id: ID!, $name: String) { + UpdateUser(id: $id, name: $name) { + name + } + } + ` + client = new GraphQLClient(host) + + // TODO why is this failing - it returns { UpdateUser: null } - that should not be + /* it('name within specifications', async () => { + const variables = { + id: 'u1', + name: 'Peter Lustig' + } + const expected = { + UpdateUser: { + name: 'Peter Lustig' + } + } + await expect(client.request(mutation, variables)) + .resolves.toEqual(expected) + }) */ + + it('with no name', async () => { + const variables = { + id: 'u1' + } + const expected = 'Username must be at least 3 characters long!' + await expect(client.request(mutation, variables)) + .rejects.toThrow(expected) + }) + + it('with too short name', async () => { + const variables = { + id: 'u1' + } + const expected = 'Username must be at least 3 characters long!' + await expect(client.request(mutation, variables)) + .rejects.toThrow(expected) + }) + }) +}) diff --git a/backend/src/resolvers/user_management.spec.js b/backend/src/resolvers/user_management.spec.js index 94ec04203..7c0be08f3 100644 --- a/backend/src/resolvers/user_management.spec.js +++ b/backend/src/resolvers/user_management.spec.js @@ -339,7 +339,7 @@ describe('do not expose private RSA key', () => { email: 'apfel-strudel@test.org' } await client.request(gql` - mutation($id: ID, $password: String!, $slug: String, $name: String, $email: String) { + mutation($id: ID, $password: String!, $slug: String, $name: String, $email: String!) { CreateUser(id: $id, password: $password, slug: $slug, name: $name, email: $email) { id } diff --git a/backend/src/schema.graphql b/backend/src/schema.graphql index 4638fbd0d..902a7abf9 100644 --- a/backend/src/schema.graphql +++ b/backend/src/schema.graphql @@ -95,7 +95,7 @@ type User { id: ID! actorId: String name: String - email: String + email: String! slug: String password: String! avatar: String diff --git a/backend/src/seed/factories/users.js b/backend/src/seed/factories/users.js index 1bca0e243..a088b4c54 100644 --- a/backend/src/seed/factories/users.js +++ b/backend/src/seed/factories/users.js @@ -20,7 +20,7 @@ export default function create (params) { $name: String $slug: String $password: String! - $email: String + $email: String! $avatar: String $about: String $role: UserGroupEnum diff --git a/webapp/components/User/index.spec.js b/webapp/components/User/index.spec.js index 9f45ae83a..4bc286d20 100644 --- a/webapp/components/User/index.spec.js +++ b/webapp/components/User/index.spec.js @@ -51,8 +51,8 @@ describe('User', () => { it('renders anonymous user', () => { const wrapper = Wrapper() - expect(wrapper.text()).not.toMatch('Tilda Swinton') - expect(wrapper.text()).toMatch('Anonymus') + expect(wrapper.text()).toBe('') + expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym') }) describe('given an user', () => { @@ -65,7 +65,7 @@ describe('User', () => { it('renders user name', () => { const wrapper = Wrapper() - expect(wrapper.text()).not.toMatch('Anonymous') + expect(mocks.$t).not.toHaveBeenCalledWith('profile.userAnonym') expect(wrapper.text()).toMatch('Tilda Swinton') }) @@ -77,7 +77,7 @@ describe('User', () => { it('renders anonymous user', () => { const wrapper = Wrapper() expect(wrapper.text()).not.toMatch('Tilda Swinton') - expect(wrapper.text()).toMatch('Anonymus') + expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym') }) describe('current user is a moderator', () => { diff --git a/webapp/components/User/index.vue b/webapp/components/User/index.vue index 6b0731981..857f4f302 100644 --- a/webapp/components/User/index.vue +++ b/webapp/components/User/index.vue @@ -12,7 +12,7 @@ Anonymus + >{{ $t('profile.userAnonym') }} @@ -47,7 +47,7 @@ {{ user.name | truncate(trunc, 18) }} + >{{ userName | truncate(18) }}
- {{ $t('login.hello') }} {{ user.name }} + {{ $t('login.hello') }} + {{ userName }} @@ -146,6 +149,10 @@ export default { quickSearchResults: 'search/quickResults', quickSearchPending: 'search/quickPending' }), + userName() { + const { name } = this.user || {} + return name || this.$t('profile.userAnonym') + }, routes() { if (!this.user.slug) { return [] diff --git a/webapp/locales/de.json b/webapp/locales/de.json index efaf6f312..f4ea82e37 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -21,6 +21,7 @@ "following": "Folgt", "shouted": "Empfohlen", "commented": "Kommentiert", + "userAnonym": "Anonymus", "socialMedia": "Wo sonst finde ich" }, "notifications": { @@ -38,8 +39,10 @@ "data": { "name": "Deine Daten", "labelName": "Dein Name", + "namePlaceholder": "Petra Lustig", "labelCity": "Deine Stadt oder Region", - "labelBio": "Über dich" + "labelBio": "Über dich", + "success": "Deine Daten wurden erfolgreich aktualisiert!" }, "security": { "name": "Sicherheit", @@ -224,4 +227,4 @@ "shoutButton": { "shouted": "empfohlen" } -} +} \ No newline at end of file diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 023f0d362..0dc953666 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -21,6 +21,7 @@ "following": "Following", "shouted": "Shouted", "commented": "Commented", + "userAnonym": "Anonymous", "socialMedia": "Where else can I find" }, "notifications": { @@ -38,8 +39,10 @@ "data": { "name": "Your data", "labelName": "Your Name", + "namePlaceholder": "Femanon Funny", "labelCity": "Your City or Region", - "labelBio": "About You" + "labelBio": "About You", + "success": "Your data was successfully updated!" }, "security": { "name": "Security", @@ -224,4 +227,4 @@ "shoutButton": { "shouted": "shouted" } -} +} \ No newline at end of file diff --git a/webapp/locales/it.json b/webapp/locales/it.json index 0225babad..25005a07a 100644 --- a/webapp/locales/it.json +++ b/webapp/locales/it.json @@ -15,13 +15,15 @@ "followers": "Seguenti", "following": "Seguendo", "shouted": "Gridato", - "commented": "Commentato" + "commented": "Commentato", + "userAnonym": "Anonymous" }, "settings": { "name": "Impostazioni", "data": { "name": "I tuoi dati", "labelName": "Nome", + "namePlaceholder": "Anonymous", "labelCity": "La tua città o regione", "labelBio": "Su di te" }, diff --git a/webapp/locales/pl.json b/webapp/locales/pl.json index 0f2147996..506a04f1b 100644 --- a/webapp/locales/pl.json +++ b/webapp/locales/pl.json @@ -15,13 +15,15 @@ "followers": "Obserwujący", "following": "Obserwowani", "shouted": "Krzyknij", - "commented": "Skomentuj" + "commented": "Skomentuj", + "userAnonym": "Anonymous" }, "settings": { "name": "Ustawienia", "data": { "name": "Twoje dane", "labelName": "Twoje dane", + "namePlaceholder": "Anonymous", "labelCity": "Twoje miasto lub region", "labelBio": "O Tobie" }, diff --git a/webapp/locales/pt.json b/webapp/locales/pt.json index 4151f49c7..0636ba6f9 100644 --- a/webapp/locales/pt.json +++ b/webapp/locales/pt.json @@ -15,13 +15,15 @@ "followers": "Seguidores", "following": "Seguindo", "shouted": "Aclamou", - "commented": "Comentou" + "commented": "Comentou", + "userAnonym": "Anonymous" }, "settings": { "name": "Configurações", "data": { "name": "Seus dados", "labelName": "Seu nome", + "namePlaceholder": "Anonymous", "labelCity": "Sua cidade ou estado", "labelBio": "Sobre você" }, diff --git a/webapp/pages/profile/_id/_slug.vue b/webapp/pages/profile/_id/_slug.vue index d577b79c8..ecb0baa9d 100644 --- a/webapp/pages/profile/_id/_slug.vue +++ b/webapp/pages/profile/_id/_slug.vue @@ -16,7 +16,7 @@ > @@ -35,7 +35,7 @@ align="center" no-margin > - {{ user.name }} + {{ userName }} - Wem folgt {{ user.name | truncate(15) }}? + Wem folgt {{ userName | truncate(15) }}? @@ -168,7 +168,7 @@ tag="h5" color="soft" > - Wer folgt {{ user.name | truncate(15) }}? + Wer folgt {{ userName | truncate(15) }}? - - + - + {{ link.username }} @@ -393,6 +389,10 @@ export default { const username = url.split('/').pop() return { url, username, favicon } }) + }, + userName() { + const { name } = this.user || {} + return name || this.$t('profile.userAnonym') } }, watch: { diff --git a/webapp/pages/settings/index.spec.js b/webapp/pages/settings/index.spec.js new file mode 100644 index 000000000..8ee68172e --- /dev/null +++ b/webapp/pages/settings/index.spec.js @@ -0,0 +1,73 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import index from './index.vue' +import Vue from 'vue' +import Vuex from 'vuex' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Vuex) +localVue.use(Styleguide) + +describe('index.vue', () => { + let Wrapper + let store + let mocks + let getters + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $apollo: { + mutate: jest + .fn() + .mockRejectedValue({ message: 'Ouch!' }) + .mockResolvedValueOnce({ + data: { + UpdateUser: { + id: 'u1', + name: 'Peter', + locationName: 'Berlin', + about: 'Smth' + } + } + }) + }, + $toast: { + error: jest.fn(), + success: jest.fn() + } + } + getters = { + 'auth/user': () => { + return {} + } + } + }) + + describe('mount', () => { + const Wrapper = () => { + store = new Vuex.Store({ + getters + }) + return mount(index, { store, mocks, localVue }) + } + + it('renders', () => { + expect(Wrapper().contains('div')).toBe(true) + }) + + describe('given a new username and hitting submit', () => { + it('calls updateUser mutation', () => { + const wrapper = Wrapper() + const input = wrapper.find('#name') + const submitForm = wrapper.find('.ds-form') + + input.setValue('Peter') + submitForm.trigger('submit') + + expect(mocks.$apollo.mutate).toHaveBeenCalled() + }) + }) + }) +}) diff --git a/webapp/pages/settings/index.vue b/webapp/pages/settings/index.vue index 5ff7c171e..a3b2298c3 100644 --- a/webapp/pages/settings/index.vue +++ b/webapp/pages/settings/index.vue @@ -9,7 +9,7 @@ model="name" icon="user" :label="$t('settings.data.labelName')" - :placeholder="$t('settings.data.labelName')" + :placeholder="$t('settings.data.namePlaceholder')" /> @@ -36,7 +36,7 @@ style="float: right;" icon="check" type="submit" - :loading="sending" + :loading="loadingData" primary > {{ $t('actions.save') }} @@ -88,8 +88,8 @@ export default { return { axiosSource: null, cities: [], - sending: false, - loading: false, + loadingData: false, + loadingGeo: false, formData: {} } }, @@ -111,13 +111,13 @@ export default { ...mapMutations({ setCurrentUser: 'auth/SET_USER' }), - submit() { - this.sending = true + async submit() { + this.loadingData = true const { name, about } = this.formData let { locationName } = this.formData locationName = locationName && (locationName['label'] || locationName) - this.$apollo - .mutate({ + try { + const { data } = await this.$apollo.mutate({ mutation, variables: { id: this.currentUser.id, @@ -135,15 +135,12 @@ export default { }) } }) - .then(data => { - this.$toast.success('Updated user') - }) - .catch(err => { - this.$toast.error(err.message) - }) - .finally(() => { - this.sending = false - }) + this.$toast.success(this.$t('settings.data.success')) + } catch (err) { + this.$toast.error(err.message) + } finally { + this.loadingData = false + } }, handleCityInput(value) { clearTimeout(timeout) @@ -181,7 +178,7 @@ export default { return } - this.loading = true + this.loadingGeo = true this.axiosSource = CancelToken.source() const place = encodeURIComponent(value) @@ -198,7 +195,7 @@ export default { this.cities = this.processCityResults(res) }) .finally(() => { - this.loading = false + this.loadingGeo = false }) } }