File size: 26,720 Bytes
c9c49ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
defmodule Plausible.GoalsTest do
  use Plausible.DataCase
  alias Plausible.Goals

  test "create/2 creates goals and trims input" do
    site = new_site()
    {:ok, goal} = Goals.create(site, %{"page_path" => "/foo bar "})
    assert goal.page_path == "/foo bar"
    assert goal.display_name == "Visit /foo bar"

    {:ok, goal} =
      Goals.create(site, %{
        "event_name" => "  some event name   ",
        "display_name" => " DisplayName   "
      })

    assert goal.event_name == "some event name"
    assert goal.display_name == "DisplayName"
  end

  test "create/2 creates pageview goal and adds a leading slash if missing" do
    site = new_site()
    {:ok, goal} = Goals.create(site, %{"page_path" => "foo bar"})
    assert goal.page_path == "/foo bar"
  end

  test "create/2 validates goal name is at most 120 chars" do
    site = new_site()
    assert {:error, changeset} = Goals.create(site, %{"event_name" => String.duplicate("a", 130)})
    assert {"should be at most %{count} character(s)", _} = changeset.errors[:event_name]
  end

  test "create/2 validates scroll_threshold in range [-1, 100]" do
    site = new_site()

    {:error, changeset} =
      Goals.create(site, %{"page_path" => "/blog/post-1", "scroll_threshold" => -2})

    assert {"Should be -1 (missing) or in range [0, 100]", _} =
             changeset.errors[:scroll_threshold]

    {:error, changeset} =
      Goals.create(site, %{"page_path" => "/blog/post-1", "scroll_threshold" => 101})

    assert {"Should be -1 (missing) or in range [0, 100]", _} =
             changeset.errors[:scroll_threshold]

    assert {:ok, _} =
             Goals.create(site, %{"page_path" => "/blog/post-1", "scroll_threshold" => -1})

    assert {:ok, _} =
             Goals.create(site, %{"page_path" => "/blog/post-2", "scroll_threshold" => 50})
  end

  test "create/2 validates page path exists for scroll goals" do
    site = new_site()

    {:error, changeset} =
      Goals.create(site, %{"event_name" => "Signup", "scroll_threshold" => 50})

    assert {"page_path field missing for page scroll goal", _} =
             changeset.errors[:scroll_threshold]
  end

  test "create/2 fails when same pageview+scroll threshold config exists with different display name" do
    site = new_site()

    {:ok, _} =
      Goals.create(site, %{
        "page_path" => "/blog/post-1",
        "scroll_threshold" => 50,
        "display_name" => "Scroll 50"
      })

    {:ok, _} =
      Goals.create(site, %{
        "page_path" => "/blog/post-1",
        "scroll_threshold" => 75,
        "display_name" => "Scroll 75"
      })

    {:error, changeset} =
      Goals.create(site, %{
        "page_path" => "/blog/post-1",
        "scroll_threshold" => 50,
        "display_name" => "Scroll 50 another"
      })

    assert {"has already been taken", _} =
             changeset.errors[:page_path]
  end

  test "create/2 fails to create the same pageview goal twice" do
    site = new_site()
    {:ok, _} = Goals.create(site, %{"page_path" => "foo bar", "display_name" => "one"})

    assert {:error, changeset} =
             Goals.create(site, %{"page_path" => "foo bar", "display_name" => "two"})

    assert {"has already been taken", _} = changeset.errors[:page_path]
  end

  test "create/2 fails to create the same custom event goal twice" do
    site = new_site()
    {:ok, _} = Goals.create(site, %{"event_name" => "foo bar"})
    assert {:error, changeset} = Goals.create(site, %{"event_name" => "foo bar"})
    assert {"has already been taken", _} = changeset.errors[:display_name]
  end

  test "create/2 succeeds to create the same custom event goal thrice with different custom props and different display names each" do
    site = new_site()

    {:ok, _} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "display_name" => "Tablet Purchase",
        "custom_props" => %{"product" => "tablet"}
      })

    {:ok, _} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "display_name" => "Speaker Purchase",
        "custom_props" => %{"product" => "speaker"}
      })

    {:ok, _} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "display_name" => "General Purchase"
      })
  end

  test "create/2 fails to create the same custom event goal twice with different display names but no props each" do
    site = new_site()

    {:ok, _} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "display_name" => "General Purchase"
      })

    {:error, changeset} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "display_name" => "General Purchase 2"
      })

    assert {"has already been taken", _} = changeset.errors[:event_name]
  end

  test "create/3 fails to create a goal with more than #{Plausible.Goal.max_custom_props_per_goal()} custom props" do
    site = new_site()

    {:error, changeset} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "display_name" => "General Purchase",
        "custom_props" => %{
          "variant" => "A",
          "promo" => "true",
          "product" => "tablet",
          "limit" => "hit"
        }
      })

    assert {"use at most 3 properties per goal", _} = changeset.errors[:custom_props]
  end

  test "create/3 fails to create a goal with non-string custom prop values" do
    site = new_site()

    {:error, changeset} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "custom_props" => %{"count" => 42}
      })

    assert {"must be a map with string keys and string values" <> _, _} =
             changeset.errors[:custom_props]
  end

  test "create/3 fails to create a goal with null custom prop values" do
    site = new_site()

    {:error, changeset} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "custom_props" => %{"product" => nil}
      })

    assert {"must be a map with string keys and string values", _} =
             changeset.errors[:custom_props]
  end

  test "create/3 fails to create custom prop with key over #{Plausible.Props.max_prop_key_length()}" do
    site = new_site()

    {:error, changeset} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "custom_props" => %{
          :binary.copy("a", Plausible.Props.max_prop_key_length() + 1) => "value"
        }
      })

    assert {"key length is 1..300 characters", _} =
             changeset.errors[:custom_props]
  end

  test "create/3 fails to create custom prop with value over #{Plausible.Props.max_prop_value_length()}" do
    site = new_site()

    {:error, changeset} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "custom_props" => %{
          "key" => :binary.copy("a", Plausible.Props.max_prop_value_length() + 1)
        }
      })

    assert {"value length is 1..2000 characters", _} =
             changeset.errors[:custom_props]
  end

  test "create/2 succeeds to create the same custom event twice with different props and different display names each" do
    site = new_site()

    {:ok, _} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "display_name" => "General Purchase",
        "custom_props" => %{"variant" => "A"}
      })

    {:ok, _} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "display_name" => "General Purchase 2",
        "custom_props" => %{"variant" => "A", "foo" => "bar"}
      })
  end

  test "create/2 succeeds to create two pageview goals with different displayn names and custom props each" do
    site = new_site()

    {:ok, _} = Goals.create(site, %{"page_path" => "/index", "display_name" => "Index"})

    {:ok, _} =
      Goals.create(site, %{
        "page_path" => "/index",
        "display_name" => "Index 2",
        "custom_props" => %{"foo" => "bar"}
      })
  end

  test "create/2 fails to create the same currency goal twice" do
    site = new_site()
    {:ok, _} = Goals.create(site, %{"event_name" => "foo bar", "currency" => "EUR"})

    assert {:error, changeset} =
             Goals.create(site, %{
               "event_name" => "foo bar",
               "currency" => "EUR",
               "display_name" => "Purchase copy"
             })

    assert {"has already been taken", _} = changeset.errors[:event_name]
  end

  test "create/2 fails to create two pageview goals with same display name" do
    site = new_site()
    {:ok, _} = Goals.create(site, %{"page_path" => "/index", "display_name" => "Index"})

    assert {:error, changeset} =
             Goals.create(site, %{"page_path" => "/index-2", "display_name" => "Index"})

    assert {"has already been taken", _} =
             changeset.errors[:display_name]
  end

  test "create/2 fails when same custom props config exists with different display name" do
    site = new_site()

    {:ok, _} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "display_name" => "Purchase Event",
        "custom_props" => %{"product" => "tablet"}
      })

    {:error, changeset} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "display_name" => "Different Display Name",
        "custom_props" => %{"product" => "tablet"}
      })

    assert {"has already been taken", _} = changeset.errors[:event_name]
  end

  test "create/2 fails when same display name exists despite different custom props config" do
    site = new_site()

    {:ok, _} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "display_name" => "My Goal",
        "custom_props" => %{"product" => "tablet"}
      })

    {:error, changeset} =
      Goals.create(site, %{
        "event_name" => "Purchase",
        "display_name" => "My Goal",
        "custom_props" => %{"product" => "speaker"}
      })

    assert {"has already been taken", _} = changeset.errors[:display_name]
  end

  test "create/2 fails when same display name exists between event and pageview goals" do
    site = new_site()

    {:ok, _} =
      Goals.create(site, %{
        "event_name" => "Signup",
        "display_name" => "User Action"
      })

    {:error, changeset} =
      Goals.create(site, %{
        "page_path" => "/signup",
        "display_name" => "User Action"
      })

    assert {"has already been taken", _} = changeset.errors[:display_name]
  end

  test "create/2 fails to create a goal with 'engagement' as event_name (reserved)" do
    site = new_site()
    assert {:error, changeset} = Goals.create(site, %{"event_name" => "engagement"})

    assert {"The event name 'engagement' is reserved and cannot be used as a goal", _} =
             changeset.errors[:event_name]
  end

  on_ee do
    @journey_end_event Plausible.Stats.Exploration.Journey.Step.journey_end_event()

    test "create/2 fails to create a goal with '#{@journey_end_event}' as event_name (reserved)" do
      site = new_site()
      assert {:error, changeset} = Goals.create(site, %{"event_name" => @journey_end_event})

      assert {"The event name '#{@journey_end_event}' is reserved and cannot be used as a goal",
              _} =
               changeset.errors[:event_name]
    end

    test "create/2 sets site.updated_at for revenue goal" do
      site_1 = new_site(updated_at: DateTime.add(DateTime.utc_now(), -3600))

      {:ok, _goal_1} = Goals.create(site_1, %{"event_name" => "Checkout", "currency" => "BRL"})

      assert NaiveDateTime.compare(site_1.updated_at, Plausible.Repo.reload!(site_1).updated_at) ==
               :lt

      site_2 = new_site(updated_at: DateTime.add(DateTime.utc_now(), -3600))
      {:ok, _goal_2} = Goals.create(site_2, %{"event_name" => "Read Article", "currency" => nil})

      assert NaiveDateTime.compare(site_2.updated_at, Plausible.Repo.reload!(site_2).updated_at) ==
               :eq
    end

    test "create/2 creates revenue goal" do
      site = new_site()
      {:ok, goal} = Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"})
      assert goal.event_name == "Purchase"
      assert goal.page_path == nil
      assert goal.currency == :EUR
    end

    test "create/2 returns error when site does not have access to revenue goals" do
      user = new_user() |> subscribe_to_growth_plan()
      site = new_site(owner: user)

      {:error, :upgrade_required} =
        Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"})
    end

    test "create/2 returns error when site does not have access to custom props on goals" do
      user = new_user() |> subscribe_to_growth_plan()
      site = new_site(owner: user)

      {:error, :upgrade_required} =
        Goals.create(site, %{"event_name" => "Signup", "custom_props" => %{"plan" => "premium"}})
    end

    test "for_site/2 with include_goals_with_custom_props?: false excludes goals with custom props" do
      site = new_site()

      _goal_with_props =
        insert(:goal, site: site, event_name: "Purchase", custom_props: %{"product" => "Shirt"})

      goal_without_props = insert(:goal, site: site, event_name: "Signup")

      filtered = Goals.for_site(site, include_goals_with_custom_props?: false)

      assert length(filtered) == 1
      assert hd(filtered).id == goal_without_props.id
    end

    test "for_site/2 includes all goals by default (include_goals_with_custom_props? defaults to true)" do
      user = new_user()
      site = new_site(owner: user)

      {:ok, _goal_with_props} =
        Goals.create(site, %{
          "event_name" => "Purchase",
          "custom_props" => %{"product" => "Shirt"}
        })

      {:ok, _goal_without_props} = Goals.create(site, %{"event_name" => "Signup"})

      all_goals = Goals.for_site(site)

      assert length(all_goals) == 2
    end

    test "create/2 returns error when creating a revenue goal for consolidated view" do
      user = new_user()
      new_site(owner: user)
      new_site(owner: user)
      {:ok, team} = Plausible.Teams.get_or_create(user)
      site = new_consolidated_view(team)

      {:error, :revenue_goals_unavailable} =
        Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"})
    end

    test "create/2 fails for unknown currency code" do
      site = new_site()

      assert {:error, changeset} =
               Goals.create(site, %{"event_name" => "Purchase", "currency" => "Euro"})

      assert [currency: {"is invalid", _}] = changeset.errors
    end
  end

  test "update/2 updates a goal" do
    site = new_site()
    {:ok, goal1} = Goals.create(site, %{"page_path" => "/foo bar "})
    {:ok, goal2} = Goals.update(goal1, %{"page_path" => "/", "display_name" => "Homepage"})
    assert goal1.id == goal2.id
    assert goal2.page_path == "/"
    assert goal2.display_name == "Homepage"
  end

  test "update/2 cannot move goal to another site by passing site_id param" do
    site = new_site()
    victim_site = new_site()
    {:ok, goal} = Goals.create(site, %{"event_name" => "Purchase"})

    {:ok, _updated} =
      Goals.update(goal, %{
        "event_name" => "Purchase",
        "site_id" => victim_site.id
      })

    reloaded = Plausible.Repo.reload!(goal)
    assert reloaded.site_id == site.id
    assert reloaded.site_id != victim_site.id
  end

  test "update/2 also updates all segments the goal is a part of" do
    user = new_user()
    site = new_site(owner: user)
    {:ok, goal1} = Goals.create(site, %{"event_name" => "Signup"})
    {:ok, _goal2} = Goals.create(site, %{"event_name" => "Signup from nav"})

    {:ok, segment1} =
      Plausible.Segments.insert_one(user.id, site, :editor, %{
        "type" => "site",
        "segment_data" => %{
          "filters" => [
            ["is", "event:page", ["/blog"]],
            ["is", "event:goal", ["Signup from nav", "Signup"]],
            ["is", "event:props:variant", ["A"]]
          ]
        },
        "name" => "Site segment"
      })

    {:ok, segment2} =
      Plausible.Segments.insert_one(user.id, site, :editor, %{
        "type" => "personal",
        "segment_data" => %{
          "filters" => [
            ["is", "event:goal", ["Signup"]]
          ]
        },
        "name" => "Personal segment"
      })

    Goals.update(goal1, %{"display_name" => "SIGNUP"})

    assert Repo.reload!(segment1).segment_data == %{
             "filters" => [
               ["is", "event:page", ["/blog"]],
               ["is", "event:goal", ["Signup from nav", "SIGNUP"]],
               ["is", "event:props:variant", ["A"]]
             ]
           }

    assert Repo.reload!(segment2).segment_data == %{
             "filters" => [
               ["is", "event:goal", ["SIGNUP"]]
             ]
           }
  end

  test "update/2 prevents renaming event_name of a special goal" do
    site = new_site()

    for event_name <- Plausible.Goal.special_goals() do
      {:ok, goal} = Goals.create(site, %{"event_name" => event_name})

      assert {:error, changeset} =
               Goals.update(goal, %{"event_name" => "Renamed #{event_name}"})

      assert {"cannot be changed for an automated goal", _} = changeset.errors[:event_name]
    end
  end

  test "update/2 prevents renaming display_name of a special goal to a non-canonical value" do
    site = new_site()

    for event_name <- Plausible.Goal.special_goals() do
      {:ok, goal} = Goals.create(site, %{"event_name" => event_name})

      assert {:error, changeset} =
               Goals.update(goal, %{"display_name" => "Renamed #{event_name}"})

      assert {"cannot be changed for an automated goal", _} = changeset.errors[:display_name]
    end
  end

  test "update/2 allows restoring display_name of a special goal to its canonical value" do
    site = new_site()
    {:ok, goal} = Goals.create(site, %{"event_name" => "File Download"})

    # Simulate a previously broken goal by directly updating the DB
    Plausible.Repo.update_all(
      Ecto.Query.where(Plausible.Goal, id: ^goal.id),
      set: [display_name: "My File Downloads"]
    )

    broken_goal = Plausible.Repo.reload!(goal)
    assert broken_goal.display_name == "My File Downloads"

    assert {:ok, fixed} = Goals.update(broken_goal, %{"display_name" => "File Download"})
    assert fixed.display_name == "File Download"
  end

  test "update/2 allows updating non-name fields of a special goal" do
    site = new_site()
    {:ok, goal} = Goals.create(site, %{"event_name" => "File Download"})

    assert {:ok, updated} =
             Goals.update(goal, %{
               "event_name" => "File Download",
               "display_name" => "File Download",
               "custom_props" => %{"path" => "/file.jpg"}
             })

    assert updated.custom_props == %{"path" => "/file.jpg"}
  end

  test "update/2 allows renaming event_name of a regular goal" do
    site = new_site()
    {:ok, goal} = Goals.create(site, %{"event_name" => "Signup"})

    assert {:ok, updated} = Goals.update(goal, %{"event_name" => "Register"})
    assert updated.event_name == "Register"
  end

  test "update/2 allows renaming display_name of a regular goal" do
    site = new_site()
    {:ok, goal} = Goals.create(site, %{"event_name" => "Signup"})

    assert {:ok, updated} = Goals.update(goal, %{"display_name" => "User Registration"})
    assert updated.display_name == "User Registration"
  end

  on_ee do
    test "update/2 prevents changing currency of existing revenue goal" do
      site = new_site()
      {:ok, goal} = Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"})

      assert {:error, changeset} = Goals.update(goal, %{"currency" => "USD"})
      assert {"cannot change currency of existing goal", _} = changeset.errors[:currency]
    end

    test "list_revenue_goals/1 lists event_names and currencies for each revenue goal" do
      site = new_site()

      Goals.create(site, %{"event_name" => "One", "currency" => "EUR"})
      Goals.create(site, %{"event_name" => "Two", "currency" => "EUR"})
      Goals.create(site, %{"event_name" => "Three", "currency" => "USD"})
      Goals.create(site, %{"event_name" => "Four"})
      Goals.create(site, %{"page_path" => "/some-page"})

      revenue_goals = Goals.list_revenue_goals(site)

      assert length(revenue_goals) == 3
      assert %{display_name: "One", currency: :EUR} in revenue_goals
      assert %{display_name: "Two", currency: :EUR} in revenue_goals
      assert %{display_name: "Three", currency: :USD} in revenue_goals
    end
  end

  test "create/2 clears currency for pageview goals" do
    site = new_site()
    {:ok, goal} = Goals.create(site, %{"page_path" => "/purchase", "currency" => "EUR"})
    assert goal.event_name == nil
    assert goal.page_path == "/purchase"
    assert goal.currency == nil
  end

  test "for_site/1 returns trimmed input even if it was saved with trailing whitespace" do
    site = new_site()
    insert(:goal, %{site: site, event_name: " Signup "})
    insert(:goal, %{site: site, page_path: " /Signup "})

    goals = Goals.for_site(site)

    assert [%{page_path: "/Signup"}, %{event_name: "Signup"}] = goals
  end

  test "for_site/1 returns goals up to a limit" do
    site = new_site()
    for i <- 1..11, do: insert(:goal, %{site: site, event_name: "G#{i}"})

    assert Goals.count(site) == 11
    assert length(Goals.for_site(site)) == 10
  end

  test "goals are present after domain change" do
    site = new_site()
    insert(:goal, %{site: site, event_name: " Signup "})
    insert(:goal, %{site: site, page_path: " /Signup "})

    {:ok, site} = Plausible.Site.Domain.change(site, "goals.example.com")

    assert [_, _] = Goals.for_site(site)
  end

  test "goals are removed when site is deleted" do
    site = new_site()
    insert(:goal, %{site: site, event_name: " Signup "})
    insert(:goal, %{site: site, page_path: " /Signup "})

    Plausible.Site.Removal.run(site)

    assert [] = Goals.for_site(site)
  end

  test "goals can be deleted" do
    site = new_site()
    goal = insert(:goal, %{site: site, event_name: " Signup "})
    :ok = Goals.delete(goal.id, site)
    assert [] = Goals.for_site(site)
  end

  on_ee do
    test "goals can be fetched with funnel count preloaded" do
      site = new_site()

      goals =
        Enum.map(1..4, fn i ->
          {:ok, g} = Goals.create(site, %{"page_path" => "/#{i}"})
          g
        end)

      {:ok, %{id: funnel_id1}} =
        Plausible.Funnels.create(
          site,
          "Funnel1",
          [
            %{"goal_id" => Enum.at(goals, 1).id},
            %{"goal_id" => Enum.at(goals, 2).id},
            %{"goal_id" => Enum.at(goals, 3).id}
          ]
        )

      {:ok, %{id: funnel_id2}} =
        Plausible.Funnels.create(
          site,
          "Funnel2",
          [
            %{"goal_id" => Enum.at(goals, 1).id},
            %{"goal_id" => Enum.at(goals, 3).id}
          ]
        )

      assert [goal, _, _, _] = Goals.for_site(site, preload_funnels?: false)
      assert %Ecto.Association.NotLoaded{} = goal.funnels

      assert [goal, _, _, _] = Goals.for_site(site, preload_funnels?: true)
      assert [%{id: ^funnel_id1}, %{id: ^funnel_id2}] = goal.funnels
    end

    test "deleting goals with funnels triggers funnel reduction" do
      site = new_site()
      {:ok, g1} = Goals.create(site, %{"page_path" => "/1"})
      {:ok, g2} = Goals.create(site, %{"page_path" => "/2"})
      {:ok, g3} = Goals.create(site, %{"page_path" => "/3"})

      {:ok, f1} =
        Plausible.Funnels.create(
          site,
          "Funnel 3 steps",
          [
            %{"goal_id" => g1.id},
            %{"goal_id" => g2.id},
            %{"goal_id" => g3.id}
          ]
        )

      {:ok, f2} =
        Plausible.Funnels.create(
          site,
          "Funnel 2 steps",
          [
            %{"goal_id" => g1.id},
            %{"goal_id" => g2.id}
          ]
        )

      :ok = Goals.delete(g1.id, site)

      assert f1 = Plausible.Funnels.get(site.id, f1.id)
      assert Enum.count(f1.steps) == 2

      refute Plausible.Funnels.get(site.id, f2.id)
      assert Repo.all(from(fs in Plausible.Funnel.Step, where: fs.funnel_id == ^f2.id)) == []

      assert_matches [%{id: ^g3.id}, %{id: ^g2.id}] = Goals.for_site(site)
    end
  end

  test "must be either page_path or event_name" do
    site = new_site()

    assert {:error, changeset} =
             Goals.create(site, %{"page_path" => "/foo", "event_name" => "/foo"})

    assert {"cannot co-exist with page_path", _} = changeset.errors[:event_name]
  end

  test "enforces goal limit per site" do
    site = new_site()

    for i <- 1..3 do
      assert {:ok, _goal} = Goals.create(site, %{"event_name" => "Event #{i}"})
    end

    assert {:ok, _} =
             Goals.create(site, %{"event_name" => "Event 4"})

    assert {:error, changeset} =
             Goals.create(site, %{"event_name" => "Event 5"}, max_goals_per_site: 4)

    assert {"Maximum number of goals reached", _} = changeset.errors[:event_name]
    assert {"Maximum number of goals reached", _} = changeset.errors[:page_path]
  end

  test "find_or_create with upsert bypasses limit check" do
    site = new_site()

    for i <- 1..3 do
      assert {:ok, _goal} = Goals.create(site, %{"event_name" => "Event #{i}"})
    end

    assert {:ok, goal} =
             Goals.find_or_create(site, %{"goal_type" => "event", "event_name" => "Event 1"},
               max_goals_per_site: 3
             )

    assert goal.event_name == "Event 1"
  end

  test "allows creating goals after deleting some" do
    site = new_site()

    for i <- 1..3 do
      assert {:ok, _goal} = Goals.create(site, %{"event_name" => "Event #{i}"})
    end

    assert {:error, changeset} =
             Goals.create(site, %{"event_name" => "Event 4"}, max_goals_per_site: 3)

    assert {"Maximum number of goals reached", _} = changeset.errors[:event_name]
    assert {"Maximum number of goals reached", _} = changeset.errors[:page_path]

    [goal | _] = Goals.for_site(site)
    :ok = Goals.delete(goal.id, site)

    assert {:ok, _goal} = Goals.create(site, %{"event_name" => "Event 4"}, max_goals_per_site: 3)
  end

  test "batch_create_event_goals respects limit" do
    site = new_site()

    for i <- 1..6 do
      assert {:ok, _goal} = Goals.create(site, %{"event_name" => "Event #{i}"})
    end

    event_names = for i <- 6..20, do: "Event #{i}"

    created_goals = Goals.batch_create_event_goals(event_names, site, max_goals_per_site: 10)

    assert length(created_goals) == 5
    assert Enum.count(Goals.for_site(site)) == 10
  end

  test "on Mix.env == :test, max goals per site is 10 and can be overridden" do
    assert Plausible.Goals.max_goals_per_site() == 10
    assert Plausible.Goals.max_goals_per_site(max_goals_per_site: 5) == 5
  end
end