summaryrefslogtreecommitdiff
path: root/doc/development/spam_protection_and_captcha/exploratory_testing.md
blob: 1bcd336ce933bd87e6bb649190dff12dc584901b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
---
stage: Manage
group: Authentication and Authorization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---

# Exploratory testing of CAPTCHAs

You can reliably test CAPTCHA on review apps, and in your local development environment (GDK).
You can always:

- Force a reCAPTCHA to appear where it is supported.
- Force a checkbox to display, instead of street sign images to find and select.

To set up testing, follow the configuration on this page.

## Use appropriate test data

Make sure you are testing a scenario which has spam/CAPTCHA enabled. For example:
make sure you are editing a _public_ snippet, as only public snippets are checked for spam.

## Enable feature flags

Enable any relevant feature flag, if the spam/CAPTCHA support is behind a feature flag.

## Set up Akismet and reCAPTCHA

1. To set up reCAPTCHA:
    1. Review the [GitLab reCAPTCHA documentation](../../integration/recaptcha.md).
    1. Get Google's official test reCAPTCHA credentials using the instructions from
       [Google's reCAPTCHA documentation](https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do).
        1. For **Site key**, use: `6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI`
        1. For **Secret key**, use: `6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe`
    1. Go to **Admin -> Settings -> Reporting** settings: `http://gdk.test:3000/admin/application_settings/reporting#js-spam-settings`
    1. Select **Enable reCAPTCHA**. Enabling for login is not required unless you are testing that feature.
    1. Enter the **Site key** and **Secret key**.
1. To set up Akismet:
    1. Review the [GitLab documentation on Akismet](../../integration/akismet.md).
    1. Get an Akismet API key. You can sign up for [a testing key from Akismet](https://akismet.com).
       You must enter your local host (such as`gdk.test`) and email when signing up.
    1. Go to GitLab Akismet settings page, for example:
       `http://gdk.test:3000/admin/application_settings/reporting#js-spam-settings`
    1. Enable Akismet and enter your Akismet **API key**.
1. To force an Akismet false-positive spam check, refer to the
   [Akismet API documentation](https://akismet.com/development/api/#comment-check) and
   [Akismet Getting Started documentation](https://docs.akismet.com/getting-started/confirm/) for more details:
    1. You can use `akismet-guaranteed-spam@example.com` as the author email to force spam using the following steps:
        1. Go to user email settings: `http://gdk.test:3000/-/profile/emails`
        1. Add `akismet-guaranteed-spam@example.com` as a secondary email for the administrator user.
        1. Confirm it in the Rails console: `bin/rails c` -> `User.find_by_username('root').emails.last.confirm`
        1. Switch this verified email to be your primary email:
           1. Go to **Avatar dropdown list -> Edit Profile -> Main Settings**.
           1. For **Email**, enter `akismet-guaranteed-spam@example.com` to replace `admin@example.com`.
           1. Select **Update Profile Settings** to save your changes.

## Test in the web UI

After you have all the above configuration in place, you can test CAPTCHAs. Test
in an area of the application which already has CAPTCHA support, such as:

- Creating or editing an issue.
- Creating or editing a public snippet. Only **public** snippets are checked for spam.

## Test in a development environment

After you force Spam Flagging + CAPTCHA using the steps above, you can test the
behavior with any spam-protected model/controller action.

### Test with CAPTCHA enabled (CONDITIONAL_ALLOW verdict)

If CAPTCHA is enabled in these areas, you must solve the CAPTCHA popup modal before you can resubmit the form:

- **Admin -> Settings -> Reporting -> Spam**
- **Anti-bot Protection -> Enable reCAPTCHA**

<!-- vale gitlab.Substitutions = NO -->

### Testing with CAPTCHA disabled ("DISALLOW" verdict)

<!-- vale gitlab.Substitutions = YES -->

If CAPTCHA is disabled in **Admin -> Settings -> Reporting -> Spam** and **Anti-bot Protection -> Enable reCAPTCHA**,
no CAPTCHA popup displays. You are prevented from submitting the form at all.

### HTML page to render reCAPTCHA

NOTE:
If you use **Google's official test reCAPTCHA credentials** listed in
[Set up Akismet and reCAPTCHA](#set-up-akismet-and-recaptcha), the
CAPTCHA response string does not matter. It can be any string. If you use a
real, valid key pair, you must solve the CAPTCHA to obtain a
valid CAPTCHA response to use. You can do this once only, and only before it expires.

To directly test the GraphQL API via GraphQL Explorer (`http://gdk.test:3000/-/graphql-explorer`),
get a reCAPTCHA response string via this form: `public/recaptcha.html` (`http://gdk.test:3000/recaptcha.html`):

```html
<html>
<head>
  <title>reCAPTCHA demo: Explicit render after an onload callback</title>
  <script type="text/javascript">
  var onloadCallback = function() {
    grecaptcha.render('html_element', {
      'sitekey' : '6Ld05AsaAAAAAMsm1yTUp4qsdFARN15rQJPPqv6i'
    });
  };
  function onSubmit() {
    window.document.getElementById('recaptchaResponse').innerHTML = grecaptcha.getResponse();
    return false;
  }
  </script>
</head>
<body>
<form onsubmit="return onSubmit()">
  <div id="html_element"></div>
  <br>
  <input type="submit" value="Submit">
</form>
<div>
  <h1>recaptchaResponse:</h1>
  <div id="recaptchaResponse"></div>
</div>
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit"
        async defer>
</script>
</body>
</html>
```

## Spam/CAPTCHA API exploratory testing examples

These sections describe the steps needed to perform manual exploratory testing of
various scenarios of the Spam and CAPTCHA behavior for the REST and GraphQL APIs.

For the prerequisites, you must:

1. Perform all the steps listed above to enable Spam and CAPTCHA in the development environment,
   and force form submissions to require a CAPTCHA.
1. Ensure you have created an HTML page to render CAPTCHA under the `/public` directory,
   with a page that contains a form to manually generate a valid CAPTCHA response string.
   If you use **Google's official test reCAPTCHA credentials** listed in
   [Set up Akismet and reCAPTCHA](#set-up-akismet-and-recaptcha), the contents of the
   CAPTCHA response string don't matter.
1. Go to **Admin -> Settings -> Reporting -> Spam and Anti-bot protection**.
1. Select or clear **Enable reCAPTCHA** and **Enable Akismet** according to your
   scenario's needs.

The following examples use snippet creation as an example. You could also use
snippet updates, issue creation, or issue updates. Issues and snippets are the
only models with full Spam and CAPTCHA support.

### Initial setup

1. Create an API token.
1. Export it in your terminal for the REST commands: `export PRIVATE_TOKEN=<your_api_token>`
1. Ensure you are logged into GitLab development environment at `localhost:3000` before using GraphiQL explorer,
   because it uses your logged-in user as authorization for running GraphQL queries.
1. For the GraphQL examples, use the GraphiQL explorer at `http://localhost:3000/-/graphql-explorer`.
1. Use the `--include` (`-i`) option to `curl` to print the HTTP response headers, including the status code.

### Scenario: Akismet and CAPTCHA enabled

In this example, Akismet and CAPTCHA are enabled:

1. [Initial request](#initial-request).

<!-- TODO in future edit

Some example videos:

- REST API:

![CAPTCHA REST API](/uploads/b148cbe45496e6f4a4f63d00bb9fbd8a/captcha_rest_api.mov)

GraphQL API:

![CAPTCHA GraphQL API](/uploads/3c7ef0fad0b84bd588572bae51519463/captcha_graphql_api.mov)

-->

#### Initial request

This initial request fails because no CAPTCHA response is provided.

REST request:

```shell
curl --request POST --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/snippets?title=Title&file_name=FileName&content=Content&visibility=public"
```

REST response:

```shell
{"needs_captcha_response":true,"spam_log_id":42,"captcha_site_key":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","message":{"error":"Your snippet has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."}}
```

GraphQL request:

```graphql
mutation {
    createSnippet(input: {
        title: "Title"
        visibilityLevel: public
        blobActions: [
            {
                action: create
                filePath: "BlobPath"
                content: "BlobContent"
            }
        ]
    }) {
        snippet {
            id
            title
        }
        errors
    }
}
```

GraphQL response:

```json
{
  "data": {
    "createSnippet": null
  },
  "errors": [
    {
      "message": "Request denied. Solve CAPTCHA challenge and retry",
      "locations": [
        {
          "line": 22,
          "column": 5
        }
      ],
      "path": [
        "createSnippet"
      ],
      "extensions": {
        "needs_captcha_response": true,
        "spam_log_id": 140,
        "captcha_site_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
      }
    }
  ]
}
```

#### Second request

This request succeeds because a CAPTCHA response is provided.

REST request:

```shell
export CAPTCHA_RESPONSE="<CAPTCHA response obtained from HTML page to render CAPTCHA>"
export SPAM_LOG_ID="<spam_log_id obtained from initial REST response>"
curl --request POST --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" --header "X-GitLab-Captcha-Response: $CAPTCHA_RESPONSE" --header "X-GitLab-Spam-Log-Id: $SPAM_LOG_ID" "http://localhost:3000/api/v4/snippets?title=Title&file_name=FileName&content=Content&visibility=public"
```

REST response:

```shell
{"id":42,"title":"Title","description":null,"visibility":"public", "other_fields": "..."}
```

GraphQL request:

NOTE:
The GitLab GraphiQL implementation doesn't allow passing of headers, so we must write
this as a `curl` query. Here, `--data-binary` is used to properly handle escaped double quotes
in the JSON-embedded query.

```shell
export CAPTCHA_RESPONSE="<CAPTCHA response obtained from HTML page to render CAPTCHA>"
export SPAM_LOG_ID="<spam_log_id obtained from initial REST response>"
curl --include "http://localhost:3000/api/graphql" --header "Authorization: Bearer $PRIVATE_TOKEN" --header "Content-Type: application/json" --header "X-GitLab-Captcha-Response: $CAPTCHA_RESPONSE" --header "X-GitLab-Spam-Log-Id: $SPAM_LOG_ID" --request POST --data-binary '{"query": "mutation {createSnippet(input: {title: \"Title\" visibilityLevel: public blobActions: [ { action: create filePath: \"BlobPath\" content: \"BlobContent\" } ] }) { snippet { id title } errors }}"}'
```

GraphQL response:

```json
{"data":{"createSnippet":{"snippet":{"id":"gid://gitlab/PersonalSnippet/42","title":"Title"},"errors":[]}}}
```

### Scenario: Akismet enabled, CAPTCHA disabled

For this scenario, ensure you clear **Enable reCAPTCHA** in the Admin Area settings as described above.
If CAPTCHA is not enabled, any request flagged as potential spam fails with no chance to resubmit,
even if it could otherwise be resubmitted if CAPTCHA were enabled and successfully solved.

The REST request is the same as if CAPTCHA was enabled:

```shell
curl --request POST --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/snippets?title=Title&file_name=FileName&content=Content&visibility=public"
```

REST response:

```shell
{"message":{"error":"Your snippet has been recognized as spam and has been discarded."}}
```

GraphQL request:

```graphql
mutation {
    createSnippet(input: {
        title: "Title"
        visibilityLevel: public
        blobActions: [
            {
                action: create
                filePath: "BlobPath"
                content: "BlobContent"
            }
        ]
    }) {
        snippet {
            id
            title
        }
        errors
    }
}
```

GraphQL response:

```json
{
  "data": {
    "createSnippet": null
  },
  "errors": [
    {
      "message": "Request denied. Spam detected",
      "locations": [
        {
          "line": 22,
          "column": 5
        }
      ],
      "path": [
        "createSnippet"
      ],
      "extensions": {
        "spam": true
      }
    }
  ]
}
```

### Scenario: `allow_possible_spam` feature flag enabled

With the `allow_possible_spam` feature flag enabled, the API returns a 200 response. Any
valid request is successful and no CAPTCHA is presented, even if the request is considered
spam.