@bash-app/bash-common 30.247.0 → 30.254.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/README.md +2 -0
  2. package/dist/__tests__/definitionsHelpers.test.d.ts +2 -0
  3. package/dist/__tests__/definitionsHelpers.test.d.ts.map +1 -0
  4. package/dist/__tests__/definitionsHelpers.test.js +85 -0
  5. package/dist/__tests__/definitionsHelpers.test.js.map +1 -0
  6. package/dist/__tests__/extendedSchemas.test.d.ts +2 -0
  7. package/dist/__tests__/extendedSchemas.test.d.ts.map +1 -0
  8. package/dist/__tests__/extendedSchemas.test.js +40 -0
  9. package/dist/__tests__/extendedSchemas.test.js.map +1 -0
  10. package/dist/__tests__/membershipDefinitions.test.d.ts +2 -0
  11. package/dist/__tests__/membershipDefinitions.test.d.ts.map +1 -0
  12. package/dist/__tests__/membershipDefinitions.test.js +95 -0
  13. package/dist/__tests__/membershipDefinitions.test.js.map +1 -0
  14. package/dist/__tests__/ticketAdmission.test.d.ts +5 -0
  15. package/dist/__tests__/ticketAdmission.test.d.ts.map +1 -0
  16. package/dist/__tests__/ticketAdmission.test.js +42 -0
  17. package/dist/__tests__/ticketAdmission.test.js.map +1 -0
  18. package/dist/__tests__/utmAttribution.test.d.ts +2 -0
  19. package/dist/__tests__/utmAttribution.test.d.ts.map +1 -0
  20. package/dist/__tests__/utmAttribution.test.js +92 -0
  21. package/dist/__tests__/utmAttribution.test.js.map +1 -0
  22. package/dist/extendedSchemas.d.ts +2 -0
  23. package/dist/extendedSchemas.d.ts.map +1 -1
  24. package/dist/extendedSchemas.js +2 -0
  25. package/dist/extendedSchemas.js.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/sms/smsTemplates.d.ts +27 -0
  31. package/dist/sms/smsTemplates.d.ts.map +1 -0
  32. package/dist/sms/smsTemplates.js +11 -0
  33. package/dist/sms/smsTemplates.js.map +1 -0
  34. package/dist/utils/__tests__/addressUtils.test.d.ts +2 -0
  35. package/dist/utils/__tests__/addressUtils.test.d.ts.map +1 -0
  36. package/dist/utils/__tests__/addressUtils.test.js +138 -0
  37. package/dist/utils/__tests__/addressUtils.test.js.map +1 -0
  38. package/dist/utils/__tests__/apiUtils.test.d.ts +5 -0
  39. package/dist/utils/__tests__/apiUtils.test.d.ts.map +1 -0
  40. package/dist/utils/__tests__/apiUtils.test.js +161 -0
  41. package/dist/utils/__tests__/apiUtils.test.js.map +1 -0
  42. package/dist/utils/__tests__/arrayUtils.test.d.ts +2 -0
  43. package/dist/utils/__tests__/arrayUtils.test.d.ts.map +1 -0
  44. package/dist/utils/__tests__/arrayUtils.test.js +23 -0
  45. package/dist/utils/__tests__/arrayUtils.test.js.map +1 -0
  46. package/dist/utils/__tests__/blogUtils.test.d.ts +2 -0
  47. package/dist/utils/__tests__/blogUtils.test.d.ts.map +1 -0
  48. package/dist/utils/__tests__/blogUtils.test.js +102 -0
  49. package/dist/utils/__tests__/blogUtils.test.js.map +1 -0
  50. package/dist/utils/__tests__/dateTimeUtils.test.d.ts +2 -0
  51. package/dist/utils/__tests__/dateTimeUtils.test.d.ts.map +1 -0
  52. package/dist/utils/__tests__/dateTimeUtils.test.js +180 -0
  53. package/dist/utils/__tests__/dateTimeUtils.test.js.map +1 -0
  54. package/dist/utils/__tests__/discountEngine/discountCalculator.test.d.ts +2 -0
  55. package/dist/utils/__tests__/discountEngine/discountCalculator.test.d.ts.map +1 -0
  56. package/dist/utils/__tests__/discountEngine/discountCalculator.test.js +356 -0
  57. package/dist/utils/__tests__/discountEngine/discountCalculator.test.js.map +1 -0
  58. package/dist/utils/__tests__/generalDateTimeUtils.test.d.ts +2 -0
  59. package/dist/utils/__tests__/generalDateTimeUtils.test.d.ts.map +1 -0
  60. package/dist/utils/__tests__/generalDateTimeUtils.test.js +26 -0
  61. package/dist/utils/__tests__/generalDateTimeUtils.test.js.map +1 -0
  62. package/dist/utils/__tests__/luxonUtils.test.d.ts +2 -0
  63. package/dist/utils/__tests__/luxonUtils.test.d.ts.map +1 -0
  64. package/dist/utils/__tests__/luxonUtils.test.js +139 -0
  65. package/dist/utils/__tests__/luxonUtils.test.js.map +1 -0
  66. package/dist/utils/__tests__/mathUtils.test.d.ts +5 -0
  67. package/dist/utils/__tests__/mathUtils.test.d.ts.map +1 -0
  68. package/dist/utils/__tests__/mathUtils.test.js +26 -0
  69. package/dist/utils/__tests__/mathUtils.test.js.map +1 -0
  70. package/dist/utils/__tests__/objUtils.test.d.ts +5 -0
  71. package/dist/utils/__tests__/objUtils.test.d.ts.map +1 -0
  72. package/dist/utils/__tests__/objUtils.test.js +41 -0
  73. package/dist/utils/__tests__/objUtils.test.js.map +1 -0
  74. package/dist/utils/__tests__/paymentUtils.test.js +48 -1
  75. package/dist/utils/__tests__/paymentUtils.test.js.map +1 -1
  76. package/dist/utils/__tests__/qrCodeUtils.test.js +16 -1
  77. package/dist/utils/__tests__/qrCodeUtils.test.js.map +1 -1
  78. package/dist/utils/__tests__/reviewUtils.test.d.ts +2 -0
  79. package/dist/utils/__tests__/reviewUtils.test.d.ts.map +1 -0
  80. package/dist/utils/__tests__/reviewUtils.test.js +49 -0
  81. package/dist/utils/__tests__/reviewUtils.test.js.map +1 -0
  82. package/dist/utils/__tests__/slugUtils.test.d.ts +2 -0
  83. package/dist/utils/__tests__/slugUtils.test.d.ts.map +1 -0
  84. package/dist/utils/__tests__/slugUtils.test.js +103 -0
  85. package/dist/utils/__tests__/slugUtils.test.js.map +1 -0
  86. package/dist/utils/__tests__/sortUtils.test.d.ts +5 -0
  87. package/dist/utils/__tests__/sortUtils.test.d.ts.map +1 -0
  88. package/dist/utils/__tests__/sortUtils.test.js +28 -0
  89. package/dist/utils/__tests__/sortUtils.test.js.map +1 -0
  90. package/dist/utils/__tests__/stringUtils.test.d.ts +2 -0
  91. package/dist/utils/__tests__/stringUtils.test.d.ts.map +1 -0
  92. package/dist/utils/__tests__/stringUtils.test.js +27 -0
  93. package/dist/utils/__tests__/stringUtils.test.js.map +1 -0
  94. package/dist/utils/__tests__/ticketListUtils.test.d.ts +2 -0
  95. package/dist/utils/__tests__/ticketListUtils.test.d.ts.map +1 -0
  96. package/dist/utils/__tests__/ticketListUtils.test.js +41 -0
  97. package/dist/utils/__tests__/ticketListUtils.test.js.map +1 -0
  98. package/dist/utils/__tests__/typeUtils.test.d.ts +2 -0
  99. package/dist/utils/__tests__/typeUtils.test.d.ts.map +1 -0
  100. package/dist/utils/__tests__/typeUtils.test.js +11 -0
  101. package/dist/utils/__tests__/typeUtils.test.js.map +1 -0
  102. package/dist/utils/__tests__/userSubscriptionUtils.test.d.ts +5 -0
  103. package/dist/utils/__tests__/userSubscriptionUtils.test.d.ts.map +1 -0
  104. package/dist/utils/__tests__/userSubscriptionUtils.test.js +66 -0
  105. package/dist/utils/__tests__/userSubscriptionUtils.test.js.map +1 -0
  106. package/dist/utils/__tests__/userUtils.test.d.ts +2 -0
  107. package/dist/utils/__tests__/userUtils.test.d.ts.map +1 -0
  108. package/dist/utils/__tests__/userUtils.test.js +34 -0
  109. package/dist/utils/__tests__/userUtils.test.js.map +1 -0
  110. package/dist/utils/__tests__/venueTypeFormatter.test.d.ts +2 -0
  111. package/dist/utils/__tests__/venueTypeFormatter.test.d.ts.map +1 -0
  112. package/dist/utils/__tests__/venueTypeFormatter.test.js +25 -0
  113. package/dist/utils/__tests__/venueTypeFormatter.test.js.map +1 -0
  114. package/dist/utils/service/__tests__/regexUtils.test.d.ts +2 -0
  115. package/dist/utils/service/__tests__/regexUtils.test.d.ts.map +1 -0
  116. package/dist/utils/service/__tests__/regexUtils.test.js +51 -0
  117. package/dist/utils/service/__tests__/regexUtils.test.js.map +1 -0
  118. package/dist/utils/service/__tests__/serviceDBUtils.test.d.ts +2 -0
  119. package/dist/utils/service/__tests__/serviceDBUtils.test.d.ts.map +1 -0
  120. package/dist/utils/service/__tests__/serviceDBUtils.test.js +17 -0
  121. package/dist/utils/service/__tests__/serviceDBUtils.test.js.map +1 -0
  122. package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.d.ts +2 -0
  123. package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.d.ts.map +1 -0
  124. package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.js +60 -0
  125. package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.js.map +1 -0
  126. package/dist/utils/service/__tests__/serviceUtils.test.d.ts +2 -0
  127. package/dist/utils/service/__tests__/serviceUtils.test.d.ts.map +1 -0
  128. package/dist/utils/service/__tests__/serviceUtils.test.js +135 -0
  129. package/dist/utils/service/__tests__/serviceUtils.test.js.map +1 -0
  130. package/dist/utils/service/serviceFeaturedSortUtils.d.ts.map +1 -1
  131. package/dist/utils/service/serviceFeaturedSortUtils.js +5 -1
  132. package/dist/utils/service/serviceFeaturedSortUtils.js.map +1 -1
  133. package/dist/utils/ticketListUtils.d.ts.map +1 -1
  134. package/dist/utils/ticketListUtils.js +11 -7
  135. package/dist/utils/ticketListUtils.js.map +1 -1
  136. package/package.json +1 -1
  137. package/prisma/schema.prisma +47 -0
  138. package/src/__tests__/definitionsHelpers.test.ts +136 -0
  139. package/src/__tests__/extendedSchemas.test.ts +50 -0
  140. package/src/__tests__/membershipDefinitions.test.ts +144 -0
  141. package/src/__tests__/ticketAdmission.test.ts +73 -0
  142. package/src/__tests__/utmAttribution.test.ts +118 -0
  143. package/src/extendedSchemas.ts +2 -0
  144. package/src/index.ts +1 -0
  145. package/src/sms/smsTemplates.ts +20 -0
  146. package/src/utils/__tests__/addressUtils.test.ts +176 -0
  147. package/src/utils/__tests__/apiUtils.test.ts +219 -0
  148. package/src/utils/__tests__/arrayUtils.test.ts +26 -0
  149. package/src/utils/__tests__/blogUtils.test.ts +145 -0
  150. package/src/utils/__tests__/dateTimeUtils.test.ts +233 -0
  151. package/src/utils/__tests__/discountEngine/discountCalculator.test.ts +410 -0
  152. package/src/utils/__tests__/generalDateTimeUtils.test.ts +34 -0
  153. package/src/utils/__tests__/luxonUtils.test.ts +171 -0
  154. package/src/utils/__tests__/mathUtils.test.ts +30 -0
  155. package/src/utils/__tests__/objUtils.test.ts +48 -0
  156. package/src/utils/__tests__/paymentUtils.test.ts +59 -0
  157. package/src/utils/__tests__/qrCodeUtils.test.ts +28 -1
  158. package/src/utils/__tests__/reviewUtils.test.ts +65 -0
  159. package/src/utils/__tests__/slugUtils.test.ts +143 -0
  160. package/src/utils/__tests__/sortUtils.test.ts +33 -0
  161. package/src/utils/__tests__/stringUtils.test.ts +37 -0
  162. package/src/utils/__tests__/ticketListUtils.test.ts +47 -0
  163. package/src/utils/__tests__/typeUtils.test.ts +12 -0
  164. package/src/utils/__tests__/userSubscriptionUtils.test.ts +87 -0
  165. package/src/utils/__tests__/userUtils.test.ts +41 -0
  166. package/src/utils/__tests__/venueTypeFormatter.test.ts +31 -0
  167. package/src/utils/service/__tests__/regexUtils.test.ts +66 -0
  168. package/src/utils/service/__tests__/serviceDBUtils.test.ts +19 -0
  169. package/src/utils/service/__tests__/serviceFeaturedSortUtils.test.ts +67 -0
  170. package/src/utils/service/__tests__/serviceUtils.test.ts +190 -0
  171. package/src/utils/service/serviceFeaturedSortUtils.ts +5 -1
  172. package/src/utils/ticketListUtils.ts +13 -6
@@ -0,0 +1,176 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import {
3
+ addressHasEnoughDataForGeolocation,
4
+ addressValuesToDatabaseAddressString,
5
+ databaseAddressStringToAddressValues,
6
+ formatLocalityForCardDisplay,
7
+ databaseAddressStringToOneLineString,
8
+ databaseAddressStringToDisplayString,
9
+ addressToDisplayString,
10
+ extractAddressComponents,
11
+ } from "../addressUtils.js";
12
+
13
+ describe("addressUtils", () => {
14
+ describe("addressHasEnoughDataForGeolocation", () => {
15
+ test("returns true when place, city, and state are set", () => {
16
+ expect(
17
+ addressHasEnoughDataForGeolocation({
18
+ place: "Madison Square Garden",
19
+ city: "New York",
20
+ state: "NY",
21
+ } as any)
22
+ ).toBe(true);
23
+ });
24
+
25
+ test("returns true when street, city, and state are set", () => {
26
+ expect(
27
+ addressHasEnoughDataForGeolocation({
28
+ street: "4 Pennsylvania Plaza",
29
+ city: "New York",
30
+ state: "NY",
31
+ } as any)
32
+ ).toBe(true);
33
+ });
34
+
35
+ test("returns false when city or state is missing", () => {
36
+ expect(
37
+ addressHasEnoughDataForGeolocation({ place: "Venue", city: "NYC", state: "" } as any)
38
+ ).toBe(false);
39
+ });
40
+ });
41
+
42
+ describe("addressValuesToDatabaseAddressString", () => {
43
+ test("joins fields with pipe delimiter", () => {
44
+ expect(
45
+ addressValuesToDatabaseAddressString({
46
+ place: "The Loft",
47
+ street: "123 Main St",
48
+ city: "Brooklyn",
49
+ state: "NY",
50
+ zipCode: "11201",
51
+ country: "USA",
52
+ })
53
+ ).toBe("The Loft|123 Main St|Brooklyn|NY|11201|USA");
54
+ });
55
+ });
56
+
57
+ describe("databaseAddressStringToAddressValues", () => {
58
+ test("parses pipe-delimited strings", () => {
59
+ expect(databaseAddressStringToAddressValues("Venue|1 Ave|NYC|NY|10001|USA")).toEqual({
60
+ place: "Venue",
61
+ street: "1 Ave",
62
+ city: "NYC",
63
+ state: "NY",
64
+ zipCode: "10001",
65
+ country: "USA",
66
+ });
67
+ });
68
+
69
+ test("parses legacy bullet-delimited 5-part strings", () => {
70
+ expect(
71
+ databaseAddressStringToAddressValues("123 Main St • Brooklyn • NY • 11201 • USA")
72
+ ).toEqual({
73
+ place: "",
74
+ street: "123 Main St",
75
+ city: "Brooklyn",
76
+ state: "NY",
77
+ zipCode: "11201",
78
+ country: "USA",
79
+ });
80
+ });
81
+
82
+ test("treats undelimited string as place-only", () => {
83
+ expect(databaseAddressStringToAddressValues("Central Park")).toEqual({
84
+ place: "Central Park",
85
+ street: "",
86
+ city: "",
87
+ state: "",
88
+ zipCode: "",
89
+ country: "",
90
+ });
91
+ });
92
+
93
+ test("returns empty address for null/undefined", () => {
94
+ expect(databaseAddressStringToAddressValues(null)).toEqual({
95
+ place: "",
96
+ street: "",
97
+ city: "",
98
+ state: "",
99
+ zipCode: "",
100
+ country: "",
101
+ });
102
+ });
103
+ });
104
+
105
+ describe("formatLocalityForCardDisplay", () => {
106
+ test("joins city, state, and country", () => {
107
+ expect(formatLocalityForCardDisplay("Brooklyn", "NY", "USA")).toBe("Brooklyn, NY, USA");
108
+ });
109
+
110
+ test("skips empty parts", () => {
111
+ expect(formatLocalityForCardDisplay("Austin", null, "USA")).toBe("Austin, USA");
112
+ });
113
+ });
114
+
115
+ describe("databaseAddressStringToOneLineString", () => {
116
+ test("formats full pipe-delimited address", () => {
117
+ const db = addressValuesToDatabaseAddressString({
118
+ place: "Rooftop Bar",
119
+ street: "99 King St",
120
+ city: "Seattle",
121
+ state: "WA",
122
+ zipCode: "98101",
123
+ country: "USA",
124
+ });
125
+ expect(databaseAddressStringToOneLineString(db)).toBe(
126
+ "Rooftop Bar, 99 King St, Seattle, WA 98101"
127
+ );
128
+ });
129
+
130
+ test("returns place-only for undelimited strings", () => {
131
+ expect(databaseAddressStringToOneLineString("Secret Warehouse")).toBe("Secret Warehouse");
132
+ });
133
+ });
134
+
135
+ describe("addressToDisplayString", () => {
136
+ test("builds comma-separated line with zip", () => {
137
+ expect(
138
+ addressToDisplayString({
139
+ place: "Studio",
140
+ street: "5th Ave",
141
+ city: "NYC",
142
+ state: "NY",
143
+ zipCode: "10003",
144
+ country: "USA",
145
+ })
146
+ ).toBe("Studio, 5th Ave, NYC, NY 10003");
147
+ });
148
+ });
149
+
150
+ describe("extractAddressComponents", () => {
151
+ test("maps Google place components to IAddress", () => {
152
+ expect(
153
+ extractAddressComponents({
154
+ place_id: "place-123",
155
+ name: "Bash HQ",
156
+ address_components: [
157
+ { types: ["street_number"], long_name: "100", short_name: "100" },
158
+ { types: ["route"], long_name: "Broadway", short_name: "Broadway" },
159
+ { types: ["locality"], long_name: "New York", short_name: "New York" },
160
+ { types: ["administrative_area_level_1"], long_name: "New York", short_name: "NY" },
161
+ { types: ["postal_code"], long_name: "10005", short_name: "10005" },
162
+ { types: ["country"], long_name: "United States", short_name: "US" },
163
+ ],
164
+ })
165
+ ).toEqual({
166
+ googlePlaceId: "place-123",
167
+ place: "Bash HQ",
168
+ street: "100 Broadway",
169
+ city: "New York",
170
+ state: "NY",
171
+ zipCode: "10005",
172
+ country: "United States",
173
+ });
174
+ });
175
+ });
176
+ });
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Tests for shared contact/user helpers and environment detection.
3
+ */
4
+
5
+ import { describe, expect, test, beforeEach, afterEach } from "@jest/globals";
6
+ import {
7
+ getUserName,
8
+ getUserImage,
9
+ isContactAndNotUser,
10
+ getContactOrUserEmail,
11
+ getUserIdAndNotContactId,
12
+ getBashEventDetailUrlParamsFromBashEvent,
13
+ isProduction,
14
+ } from "../apiUtils.js";
15
+
16
+ describe("apiUtils", () => {
17
+ describe("isContactAndNotUser", () => {
18
+ test("returns true when contactOwnerId is set", () => {
19
+ expect(isContactAndNotUser({ contactOwnerId: "owner-1", fullName: "Jane" })).toBe(true);
20
+ });
21
+
22
+ test("returns false for user-shaped objects", () => {
23
+ expect(isContactAndNotUser({ id: "user-1", email: "jane@example.com" })).toBe(false);
24
+ });
25
+ });
26
+
27
+ describe("getUserName", () => {
28
+ test("returns empty string for undefined", () => {
29
+ expect(getUserName(undefined)).toBe("");
30
+ });
31
+
32
+ test("returns contact fullName when present", () => {
33
+ expect(
34
+ getUserName({ contactOwnerId: "owner-1", fullName: "Jane Contact", contactEmail: "j@x.com" })
35
+ ).toBe("Jane Contact");
36
+ });
37
+
38
+ test("falls back to contactEmail for contacts without fullName", () => {
39
+ expect(getUserName({ contactOwnerId: "owner-1", contactEmail: "j@x.com" })).toBe("j@x.com");
40
+ });
41
+
42
+ test("combines givenName and familyName for users", () => {
43
+ expect(getUserName({ givenName: "Jane", familyName: "Doe", email: "j@x.com" })).toBe("Jane Doe");
44
+ });
45
+
46
+ test("falls back to email when name parts missing", () => {
47
+ expect(getUserName({ email: "solo@example.com" })).toBe("solo@example.com");
48
+ });
49
+ });
50
+
51
+ describe("getUserImage", () => {
52
+ const noProfilePicture = "/noProfilePicture.svg";
53
+
54
+ test("returns empty string for undefined", () => {
55
+ expect(getUserImage(undefined)).toBe("");
56
+ });
57
+
58
+ test("returns placeholder for contacts", () => {
59
+ expect(getUserImage({ contactOwnerId: "owner-1", fullName: "Jane" })).toBe(noProfilePicture);
60
+ });
61
+
62
+ test("returns uploaded image when only upload exists", () => {
63
+ expect(getUserImage({ uploadedImage: "https://cdn.example/upload.jpg" })).toBe(
64
+ "https://cdn.example/upload.jpg"
65
+ );
66
+ });
67
+
68
+ test("returns google image when only google image exists", () => {
69
+ expect(getUserImage({ image: "https://lh3.google.com/photo.jpg" })).toBe(
70
+ "https://lh3.google.com/photo.jpg"
71
+ );
72
+ });
73
+
74
+ test("treats whitespace-only image URLs as missing", () => {
75
+ expect(getUserImage({ uploadedImage: " ", image: "https://google.com/p.jpg" })).toBe(
76
+ "https://google.com/p.jpg"
77
+ );
78
+ });
79
+
80
+ test("prefers most recently updated image when both exist", () => {
81
+ const older = new Date("2024-01-01");
82
+ const newer = new Date("2024-06-01");
83
+ expect(
84
+ getUserImage({
85
+ uploadedImage: "https://cdn.example/upload.jpg",
86
+ uploadedImageUpdatedAt: newer,
87
+ image: "https://lh3.google.com/photo.jpg",
88
+ imageUpdatedAt: older,
89
+ })
90
+ ).toBe("https://cdn.example/upload.jpg");
91
+ expect(
92
+ getUserImage({
93
+ uploadedImage: "https://cdn.example/upload.jpg",
94
+ uploadedImageUpdatedAt: older,
95
+ image: "https://lh3.google.com/photo.jpg",
96
+ imageUpdatedAt: newer,
97
+ })
98
+ ).toBe("https://lh3.google.com/photo.jpg");
99
+ });
100
+
101
+ test("falls back to uploadedImage when both exist without timestamps", () => {
102
+ expect(
103
+ getUserImage({
104
+ uploadedImage: "https://cdn.example/upload.jpg",
105
+ image: "https://lh3.google.com/photo.jpg",
106
+ })
107
+ ).toBe("https://cdn.example/upload.jpg");
108
+ });
109
+
110
+ test("returns placeholder when user has no images", () => {
111
+ expect(getUserImage({ email: "j@x.com" })).toBe(noProfilePicture);
112
+ });
113
+ });
114
+
115
+ describe("getContactOrUserEmail", () => {
116
+ test("returns contactEmail for contacts", () => {
117
+ expect(getContactOrUserEmail({ contactOwnerId: "o1", contactEmail: "c@x.com" })).toBe("c@x.com");
118
+ });
119
+
120
+ test("returns email for users", () => {
121
+ expect(getContactOrUserEmail({ id: "u1", email: "u@x.com" })).toBe("u@x.com");
122
+ });
123
+ });
124
+
125
+ describe("getUserIdAndNotContactId", () => {
126
+ test("returns null for contacts", () => {
127
+ expect(getUserIdAndNotContactId({ contactOwnerId: "o1" })).toBeNull();
128
+ });
129
+
130
+ test("returns user id for users", () => {
131
+ expect(getUserIdAndNotContactId({ id: "user-42" })).toBe("user-42");
132
+ });
133
+ });
134
+
135
+ describe("getBashEventDetailUrlParamsFromBashEvent", () => {
136
+ test("returns empty string for undefined event", () => {
137
+ expect(getBashEventDetailUrlParamsFromBashEvent(undefined)).toBe("");
138
+ });
139
+
140
+ test("returns empty string when title/desc/cover are absent", () => {
141
+ expect(
142
+ getBashEventDetailUrlParamsFromBashEvent({
143
+ title: "",
144
+ description: null,
145
+ coverPhoto: null,
146
+ } as any)
147
+ ).toBe("");
148
+ });
149
+
150
+ test("truncates long descriptions before building params", () => {
151
+ const longDesc = "abcdefghijklmnop";
152
+ const result = getBashEventDetailUrlParamsFromBashEvent(
153
+ { title: "Party", description: longDesc, coverPhoto: null } as any,
154
+ "cover.jpg"
155
+ );
156
+ expect(result).toBe("");
157
+ });
158
+ });
159
+
160
+ describe("isProduction", () => {
161
+ const envKeys = ["NODE_ENV", "ENVIRONMENT", "BASH_ENV"] as const;
162
+ const saved: Partial<Record<(typeof envKeys)[number], string | undefined>> = {};
163
+
164
+ beforeEach(() => {
165
+ for (const key of envKeys) {
166
+ saved[key] = process.env[key];
167
+ }
168
+ });
169
+
170
+ afterEach(() => {
171
+ for (const key of envKeys) {
172
+ if (saved[key] === undefined) {
173
+ delete process.env[key];
174
+ } else {
175
+ process.env[key] = saved[key];
176
+ }
177
+ }
178
+ });
179
+
180
+ test("returns false when ENVIRONMENT contains qa", () => {
181
+ process.env.NODE_ENV = "production";
182
+ process.env.ENVIRONMENT = "qa-staging";
183
+ expect(isProduction()).toBe(false);
184
+ });
185
+
186
+ test("returns false when BASH_ENV contains qa", () => {
187
+ process.env.NODE_ENV = "production";
188
+ process.env.BASH_ENV = "QA";
189
+ expect(isProduction()).toBe(false);
190
+ });
191
+
192
+ test("returns true only when NODE_ENV is production and env is explicitly production", () => {
193
+ process.env.NODE_ENV = "production";
194
+ process.env.ENVIRONMENT = "production";
195
+ delete process.env.BASH_ENV;
196
+ expect(isProduction()).toBe(true);
197
+ });
198
+
199
+ test("returns true when BASH_ENV is production", () => {
200
+ process.env.NODE_ENV = "production";
201
+ delete process.env.ENVIRONMENT;
202
+ process.env.BASH_ENV = "production";
203
+ expect(isProduction()).toBe(true);
204
+ });
205
+
206
+ test("returns false in development even with production env vars", () => {
207
+ process.env.NODE_ENV = "development";
208
+ process.env.ENVIRONMENT = "production";
209
+ expect(isProduction()).toBe(false);
210
+ });
211
+
212
+ test("returns false when NODE_ENV is production but env vars are unset", () => {
213
+ process.env.NODE_ENV = "production";
214
+ delete process.env.ENVIRONMENT;
215
+ delete process.env.BASH_ENV;
216
+ expect(isProduction()).toBe(false);
217
+ });
218
+ });
219
+ });
@@ -0,0 +1,26 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { asyncFilter } from "../arrayUtils.js";
3
+
4
+ describe("asyncFilter", () => {
5
+ test("keeps items where async predicate resolves true", async () => {
6
+ const nums = [1, 2, 3, 4];
7
+ const result = await asyncFilter(nums, async (n) => n % 2 === 0);
8
+ expect(result).toEqual([2, 4]);
9
+ });
10
+
11
+ test("returns empty array when no items match", async () => {
12
+ const result = await asyncFilter([1, 3, 5], async () => false);
13
+ expect(result).toEqual([]);
14
+ });
15
+
16
+ test("returns empty array for empty input", async () => {
17
+ const result = await asyncFilter([], async () => true);
18
+ expect(result).toEqual([]);
19
+ });
20
+
21
+ test("preserves order of matching items", async () => {
22
+ const items = ["a", "b", "c", "d"];
23
+ const result = await asyncFilter(items, async (v) => v !== "b");
24
+ expect(result).toEqual(["a", "c", "d"]);
25
+ });
26
+ });
@@ -0,0 +1,145 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { DEFAULT_BLOG_IMAGE } from "../../definitions.js";
3
+ import {
4
+ calculateReadingTime,
5
+ formatPublishDate,
6
+ generateBlogDetailUrl,
7
+ generateExcerpt,
8
+ generateMetaDescription,
9
+ generateMetaTitle,
10
+ getBlogDetailUrl,
11
+ getBlogImageUrl,
12
+ highlightSearchTerms,
13
+ parseBlogUrlParams,
14
+ processTags,
15
+ validateBlogContent,
16
+ } from "../blogUtils.js";
17
+
18
+ describe("blogUtils", () => {
19
+ describe("URL helpers", () => {
20
+ test("generateBlogDetailUrl with slug", () => {
21
+ expect(generateBlogDetailUrl("abc123", "my-post")).toBe("/blog/abc123-my-post");
22
+ });
23
+
24
+ test("generateBlogDetailUrl without slug", () => {
25
+ expect(generateBlogDetailUrl("abc123")).toBe("/blog/abc123");
26
+ });
27
+
28
+ test("getBlogDetailUrl prefers explicit slug", () => {
29
+ expect(getBlogDetailUrl("id1", "saved-slug", "Ignored Title")).toBe(
30
+ "/blog/id1-saved-slug"
31
+ );
32
+ });
33
+
34
+ test("getBlogDetailUrl generates slug from title when slug missing", () => {
35
+ expect(getBlogDetailUrl("id1", undefined, "Hello World Party")).toBe(
36
+ "/blog/id1-hello-world-party"
37
+ );
38
+ });
39
+
40
+ test("getBlogDetailUrl falls back to id-only URL", () => {
41
+ expect(getBlogDetailUrl("id1")).toBe("/blog/id1");
42
+ });
43
+
44
+ test("parseBlogUrlParams handles id-only param", () => {
45
+ expect(parseBlogUrlParams("abc123")).toEqual({ id: "abc123" });
46
+ });
47
+
48
+ test("parseBlogUrlParams splits id-slug at first hyphen", () => {
49
+ expect(parseBlogUrlParams("abc123-my-cool-post")).toEqual({
50
+ id: "abc123",
51
+ slug: "my-cool-post",
52
+ });
53
+ });
54
+
55
+ test("parseBlogUrlParams returns null for empty input", () => {
56
+ expect(parseBlogUrlParams("")).toBeNull();
57
+ });
58
+ });
59
+
60
+ describe("content processing", () => {
61
+ test("generateExcerpt strips HTML tags", () => {
62
+ expect(generateExcerpt("<p>Hello <strong>world</strong></p>")).toBe("Hello world");
63
+ });
64
+
65
+ test("generateExcerpt truncates at word boundary with ellipsis", () => {
66
+ const long = "word ".repeat(50);
67
+ const excerpt = generateExcerpt(long, 20);
68
+ expect(excerpt.endsWith("...")).toBe(true);
69
+ expect(excerpt.length).toBeLessThanOrEqual(23);
70
+ });
71
+
72
+ test("calculateReadingTime rounds up by word count", () => {
73
+ const words = Array.from({ length: 401 }, (_, i) => `w${i}`).join(" ");
74
+ expect(calculateReadingTime(words)).toBe(3);
75
+ });
76
+
77
+ test("getBlogImageUrl uses default when missing", () => {
78
+ expect(getBlogImageUrl(null)).toBe(DEFAULT_BLOG_IMAGE);
79
+ expect(getBlogImageUrl("https://cdn.example/img.jpg")).toBe(
80
+ "https://cdn.example/img.jpg"
81
+ );
82
+ });
83
+ });
84
+
85
+ describe("SEO helpers", () => {
86
+ test("generateMetaTitle appends site name", () => {
87
+ expect(generateMetaTitle("My Post")).toBe("My Post | Bash");
88
+ expect(generateMetaTitle("My Post", "Custom")).toBe("My Post | Custom");
89
+ });
90
+
91
+ test("generateMetaDescription uses excerpt or fallback", () => {
92
+ const content = "x".repeat(200);
93
+ expect(generateMetaDescription(content, "Title")).toContain("...");
94
+ expect(generateMetaDescription("", "Title")).toContain("Title");
95
+ });
96
+ });
97
+
98
+ describe("validateBlogContent", () => {
99
+ test("accepts valid title and content", () => {
100
+ expect(
101
+ validateBlogContent("Good title", "x".repeat(100))
102
+ ).toEqual({ isValid: true, errors: [] });
103
+ });
104
+
105
+ test("collects validation errors", () => {
106
+ const result = validateBlogContent("", "short");
107
+ expect(result.isValid).toBe(false);
108
+ expect(result.errors).toContain("Title is required");
109
+ expect(result.errors).toContain("Content must be at least 100 characters");
110
+ });
111
+ });
112
+
113
+ describe("processTags", () => {
114
+ test("splits, trims, lowercases, and caps at 10 tags", () => {
115
+ const tags = processTags(" Jazz, ROCK , ,Blues, " + "tag,".repeat(15));
116
+ expect(tags[0]).toBe("jazz");
117
+ expect(tags[1]).toBe("rock");
118
+ expect(tags.length).toBeLessThanOrEqual(10);
119
+ });
120
+
121
+ test("returns empty array for blank input", () => {
122
+ expect(processTags("")).toEqual([]);
123
+ });
124
+ });
125
+
126
+ describe("highlightSearchTerms", () => {
127
+ test("wraps matches in mark tags case-insensitively", () => {
128
+ expect(highlightSearchTerms("Hello Bash world", "bash")).toBe(
129
+ "Hello <mark>Bash</mark> world"
130
+ );
131
+ });
132
+
133
+ test("returns text unchanged when search term empty", () => {
134
+ expect(highlightSearchTerms("Hello", "")).toBe("Hello");
135
+ });
136
+ });
137
+
138
+ describe("formatPublishDate", () => {
139
+ test("formats Date and ISO string consistently", () => {
140
+ const date = new Date("2024-06-15T12:00:00Z");
141
+ expect(formatPublishDate(date)).toMatch(/June/);
142
+ expect(formatPublishDate("2024-06-15T12:00:00Z")).toMatch(/2024/);
143
+ });
144
+ });
145
+ });