Skip to content

Commit 060fa61

Browse files
committed
Add mutation for creating users
1 parent 625a103 commit 060fa61

File tree

8 files changed

+314
-0
lines changed

8 files changed

+314
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
module Mutations
4+
module Users
5+
class Create < BaseMutation
6+
description 'Admin-create an user.'
7+
8+
field :user, Types::UserType, null: true, description: 'The created user.'
9+
10+
argument :admin, Boolean, required: false, description: 'Admin status for the user.'
11+
argument :email, String, required: true, description: 'Email for the user.'
12+
argument :firstname, String, required: false, description: 'Firstname for the user.'
13+
argument :lastname, String, required: false, description: 'Lastname for the user.'
14+
argument :password, String, required: true, description: 'Password for the user.'
15+
argument :password_repeat,
16+
String,
17+
required: true,
18+
description: 'Password repeat for the user to check for typos.'
19+
argument :username, String, required: true, description: 'Username for the user.'
20+
21+
def resolve(**params)
22+
if params[:password] != params.delete(:password_repeat)
23+
return { user: nil, errors: [create_error(:invalid_password_repeat, 'Invalid password repeat')] }
24+
end
25+
26+
::Users::CreateService.new(
27+
current_authentication,
28+
**params
29+
).execute.to_mutation_response(success_key: :user)
30+
end
31+
end
32+
end
33+
end

app/graphql/types/mutation_type.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class MutationType < Types::BaseObject
3535
mount_mutation Mutations::Users::Mfa::BackupCodes::Rotate
3636
mount_mutation Mutations::Users::Mfa::Totp::GenerateSecret
3737
mount_mutation Mutations::Users::Mfa::Totp::ValidateSecret
38+
mount_mutation Mutations::Users::Create
3839
mount_mutation Mutations::Users::EmailVerification
3940
mount_mutation Mutations::Users::Login
4041
mount_mutation Mutations::Users::Logout

app/models/audit_event.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class AuditEvent < ApplicationRecord
3939
email_verified: 35,
4040
password_reset_requested: 36,
4141
password_reset: 37,
42+
user_created: 39,
4243
}.with_indifferent_access
4344

4445
# rubocop:disable Lint/StructNewOverride

app/policies/global_policy.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ class GlobalPolicy < BasePolicy
2121
enable :delete_runtime
2222
enable :rotate_runtime_token
2323
enable :list_users
24+
enable :create_user
2425
end
2526
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# frozen_string_literal: true
2+
3+
module Users
4+
class CreateService
5+
include Sagittarius::Database::Transactional
6+
7+
attr_reader :current_authentication, :params
8+
9+
def initialize(current_authentication, **params)
10+
@current_authentication = current_authentication
11+
@params = params
12+
end
13+
14+
def execute
15+
unless Ability.allowed?(current_authentication, :create_user, :global)
16+
return ServiceResponse.error(message: 'Missing permissions', error_code: :missing_permission)
17+
end
18+
19+
transactional do
20+
user = User.create(**params)
21+
unless user.persisted?
22+
return ServiceResponse.error(message: 'User is invalid', error_code: :invalid_user,
23+
details: user.errors)
24+
end
25+
26+
AuditService.audit(
27+
:user_created,
28+
author_id: current_authentication.user.id,
29+
entity: user,
30+
details: { **params.except(:password) },
31+
target: AuditEvent::GLOBAL_TARGET
32+
)
33+
34+
ServiceResponse.success(payload: user)
35+
end
36+
end
37+
end
38+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
title: usersCreate
3+
---
4+
5+
Admin-create an user.
6+
7+
## Arguments
8+
9+
| Name | Type | Description |
10+
|------|------|-------------|
11+
| `admin` | [`Boolean`](../scalar/boolean.md) | Admin status for the user. |
12+
| `clientMutationId` | [`String`](../scalar/string.md) | A unique identifier for the client performing the mutation. |
13+
| `email` | [`String!`](../scalar/string.md) | Email for the user. |
14+
| `firstname` | [`String`](../scalar/string.md) | Firstname for the user. |
15+
| `lastname` | [`String`](../scalar/string.md) | Lastname for the user. |
16+
| `password` | [`String!`](../scalar/string.md) | Password for the user. |
17+
| `passwordRepeat` | [`String!`](../scalar/string.md) | Password repeat for the user to check for typos. |
18+
| `username` | [`String!`](../scalar/string.md) | Username for the user. |
19+
20+
## Fields
21+
22+
| Name | Type | Description |
23+
|------|------|-------------|
24+
| `clientMutationId` | [`String`](../scalar/string.md) | A unique identifier for the client performing the mutation. |
25+
| `errors` | [`[Error!]!`](../object/error.md) | Errors encountered during execution of the mutation. |
26+
| `user` | [`User`](../object/user.md) | The created user. |
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'usersCreate Mutation' do
6+
include GraphqlHelpers
7+
8+
let(:mutation) do
9+
<<~QUERY
10+
mutation($input: UsersCreateInput!) {
11+
usersCreate(input: $input) {
12+
#{error_query}
13+
user {
14+
id
15+
email
16+
username
17+
firstname
18+
lastname
19+
admin
20+
}
21+
}
22+
}
23+
QUERY
24+
end
25+
26+
let(:variables) { { input: input } }
27+
let(:password) { 'Password123!' }
28+
29+
before do
30+
post_graphql mutation, variables: variables, current_user: current_user
31+
end
32+
33+
context 'when creating a user as admin' do
34+
let(:current_user) { create(:user, :admin) }
35+
let(:input) do
36+
{
37+
email: generate(:email),
38+
username: generate(:username),
39+
password: password,
40+
passwordRepeat: password,
41+
firstname: 'Graph',
42+
lastname: 'QL',
43+
admin: false,
44+
}
45+
end
46+
47+
it 'creates user' do
48+
expect(graphql_data_at(:users_create, :user, :id)).to be_present
49+
expect(graphql_data_at(:users_create, :user, :email)).to eq(input[:email])
50+
expect(graphql_data_at(:users_create, :user, :username)).to eq(input[:username])
51+
52+
user = SagittariusSchema.object_from_id(graphql_data_at(:users_create, :user, :id))
53+
54+
is_expected.to create_audit_event(
55+
:user_created,
56+
author_id: current_user.id,
57+
entity_id: user.id,
58+
entity_type: 'User',
59+
details: {
60+
'email' => input[:email],
61+
'username' => input[:username],
62+
'firstname' => input[:firstname],
63+
'lastname' => input[:lastname],
64+
'admin' => input[:admin],
65+
},
66+
target_id: 0,
67+
target_type: 'global'
68+
)
69+
end
70+
end
71+
72+
context 'when non-admin attempts to create a user' do
73+
let(:current_user) { create(:user) }
74+
let(:input) do
75+
{
76+
email: generate(:email),
77+
username: generate(:username),
78+
password: password,
79+
passwordRepeat: password,
80+
firstname: 'Graph',
81+
lastname: 'QL',
82+
admin: false,
83+
}
84+
end
85+
86+
it 'does not create user and returns an error' do
87+
expect(graphql_data_at(:users_create, :user)).to be_nil
88+
expect(graphql_data_at(:users_create, :errors)).to be_present
89+
is_expected.not_to create_audit_event
90+
end
91+
end
92+
93+
context 'when password repeat does not match' do
94+
let(:current_user) { create(:user, :admin) }
95+
let(:input) do
96+
{
97+
email: generate(:email),
98+
username: generate(:username),
99+
password: password,
100+
passwordRepeat: 'mismatch',
101+
firstname: 'Graph',
102+
lastname: 'QL',
103+
admin: false,
104+
}
105+
end
106+
107+
it 'returns a validation error and does not create the user' do
108+
expect(graphql_data_at(:users_create, :user)).to be_nil
109+
expect(graphql_data_at(:users_create, :errors)).to be_present
110+
is_expected.not_to create_audit_event
111+
end
112+
end
113+
end
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe Users::CreateService do
6+
subject(:service_response) do
7+
described_class.new(create_authentication(current_user), **params).execute
8+
end
9+
10+
let!(:current_user) { create(:user, :admin) }
11+
let(:params) do
12+
{
13+
email: generate(:email),
14+
username: generate(:username),
15+
password: 'Password123!',
16+
firstname: 'Test',
17+
lastname: 'User',
18+
admin: false,
19+
}
20+
end
21+
22+
shared_examples 'does not create' do
23+
it { is_expected.to be_error }
24+
25+
it 'does not create user' do
26+
expect { service_response }.not_to change { User.count }
27+
end
28+
29+
it { expect { service_response }.not_to create_audit_event }
30+
end
31+
32+
context 'when not authenticated' do
33+
let(:current_user) { nil }
34+
35+
it_behaves_like 'does not create'
36+
end
37+
38+
context 'when params are invalid' do
39+
context 'when email is invalid' do
40+
let(:params) do
41+
{
42+
email: 'invalid-email',
43+
username: generate(:username),
44+
password: 'pw',
45+
firstname: 'T',
46+
lastname: 'U',
47+
admin: false,
48+
}
49+
end
50+
51+
it_behaves_like 'does not create'
52+
end
53+
54+
context 'when username is too long' do
55+
let(:params) do
56+
{
57+
email: generate(:email),
58+
username: 'a' * 100,
59+
password: 'Password123!',
60+
firstname: 'T',
61+
lastname: 'U',
62+
admin: false,
63+
}
64+
end
65+
66+
it_behaves_like 'does not create'
67+
end
68+
end
69+
70+
context 'when user and params are valid and user is admin' do
71+
let(:params) do
72+
{
73+
email: generate(:email),
74+
username: generate(:username),
75+
password: 'Password123!',
76+
firstname: 'First',
77+
lastname: 'Last',
78+
admin: true,
79+
}
80+
end
81+
82+
it { is_expected.to be_success }
83+
it { expect(service_response.payload.reload).to be_valid }
84+
85+
it do
86+
is_expected.to create_audit_event(
87+
:user_created,
88+
author_id: current_user.id,
89+
entity_type: 'User',
90+
details: {
91+
'email' => params[:email],
92+
'username' => params[:username],
93+
'firstname' => params[:firstname],
94+
'lastname' => params[:lastname],
95+
'admin' => params[:admin],
96+
},
97+
target_type: 'global'
98+
)
99+
end
100+
end
101+
end

0 commit comments

Comments
 (0)