@nextsparkjs/theme-default 0.1.0-beta.20 → 0.1.0-beta.22

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 (221) hide show
  1. package/package.json +1 -1
  2. package/tests/cypress/e2e/_devtools/access.bdd.md +262 -0
  3. package/tests/cypress/e2e/_devtools/access.cy.ts +171 -0
  4. package/tests/cypress/e2e/_devtools/navigation.bdd.md +261 -0
  5. package/tests/cypress/e2e/_devtools/navigation.cy.ts +157 -0
  6. package/tests/cypress/e2e/_devtools/pages.bdd.md +303 -0
  7. package/tests/cypress/e2e/_devtools/pages.cy.ts +184 -0
  8. package/tests/cypress/e2e/_docs/README.md +215 -0
  9. package/tests/cypress/e2e/_docs/tutorials/sector7-superadmin-teams.narration.json +155 -0
  10. package/tests/cypress/e2e/_docs/tutorials/sector7-superadmin.cy.ts +390 -0
  11. package/tests/cypress/e2e/_docs/tutorials/teams-system.doc.cy.ts +349 -0
  12. package/tests/cypress/e2e/_docs/tutorials/teams-system.narration.json +165 -0
  13. package/tests/cypress/e2e/_selectors/auth.cy.ts +306 -0
  14. package/tests/cypress/e2e/_selectors/billing.cy.ts +89 -0
  15. package/tests/cypress/e2e/_selectors/dashboard-mobile.cy.ts +113 -0
  16. package/tests/cypress/e2e/_selectors/dashboard-navigation.cy.ts +89 -0
  17. package/tests/cypress/e2e/_selectors/dashboard-sidebar.cy.ts +60 -0
  18. package/tests/cypress/e2e/_selectors/dashboard-topnav.cy.ts +146 -0
  19. package/tests/cypress/e2e/_selectors/devtools.cy.ts +210 -0
  20. package/tests/cypress/e2e/_selectors/global-search.cy.ts +88 -0
  21. package/tests/cypress/e2e/_selectors/pages-editor.cy.ts +179 -0
  22. package/tests/cypress/e2e/_selectors/posts-editor.cy.ts +282 -0
  23. package/tests/cypress/e2e/_selectors/public.cy.ts +112 -0
  24. package/tests/cypress/e2e/_selectors/settings-api-keys.cy.ts +228 -0
  25. package/tests/cypress/e2e/_selectors/settings-billing.cy.ts +105 -0
  26. package/tests/cypress/e2e/_selectors/settings-layout.cy.ts +119 -0
  27. package/tests/cypress/e2e/_selectors/settings-password.cy.ts +71 -0
  28. package/tests/cypress/e2e/_selectors/settings-profile.cy.ts +82 -0
  29. package/tests/cypress/e2e/_selectors/settings-teams.cy.ts +68 -0
  30. package/tests/cypress/e2e/_selectors/superadmin.cy.ts +185 -0
  31. package/tests/cypress/e2e/_selectors/tasks.cy.ts +242 -0
  32. package/tests/cypress/e2e/_selectors/taxonomies.cy.ts +126 -0
  33. package/tests/cypress/e2e/_selectors/teams.cy.ts +142 -0
  34. package/tests/cypress/e2e/_superadmin/all-teams.bdd.md +261 -0
  35. package/tests/cypress/e2e/_superadmin/all-teams.cy.ts +177 -0
  36. package/tests/cypress/e2e/_superadmin/all-users.bdd.md +406 -0
  37. package/tests/cypress/e2e/_superadmin/all-users.cy.ts +294 -0
  38. package/tests/cypress/e2e/_superadmin/dashboard.bdd.md +235 -0
  39. package/tests/cypress/e2e/_superadmin/dashboard.cy.ts +149 -0
  40. package/tests/cypress/e2e/_superadmin/subscriptions-overview.bdd.md +290 -0
  41. package/tests/cypress/e2e/_superadmin/subscriptions-overview.cy.ts +194 -0
  42. package/tests/cypress/e2e/ai/ai-usage.cy.ts +209 -0
  43. package/tests/cypress/e2e/ai/chat-api.cy.ts +107 -0
  44. package/tests/cypress/e2e/ai/guardrails.cy.ts +332 -0
  45. package/tests/cypress/e2e/api/billing/BillingAPIController.js +319 -0
  46. package/tests/cypress/e2e/api/billing/check-action.cy.ts +326 -0
  47. package/tests/cypress/e2e/api/billing/checkout.cy.ts +358 -0
  48. package/tests/cypress/e2e/api/billing/lifecycle.cy.ts +423 -0
  49. package/tests/cypress/e2e/api/billing/plans/README.md +345 -0
  50. package/tests/cypress/e2e/api/billing/plans/business.cy.ts +412 -0
  51. package/tests/cypress/e2e/api/billing/plans/downgrade.cy.ts +510 -0
  52. package/tests/cypress/e2e/api/billing/plans/fixtures/billing-plans.json +163 -0
  53. package/tests/cypress/e2e/api/billing/plans/free.cy.ts +500 -0
  54. package/tests/cypress/e2e/api/billing/plans/pro.cy.ts +497 -0
  55. package/tests/cypress/e2e/api/billing/plans/starter.cy.ts +342 -0
  56. package/tests/cypress/e2e/api/billing/portal.cy.ts +313 -0
  57. package/tests/cypress/e2e/api/devtools/registries.bdd.md +300 -0
  58. package/tests/cypress/e2e/api/devtools/registries.cy.ts +368 -0
  59. package/tests/cypress/e2e/api/entities/blocks-scope.cy.ts +396 -0
  60. package/tests/cypress/e2e/api/entities/customers-crud.cy.ts +648 -0
  61. package/tests/cypress/e2e/api/entities/customers-metas.cy.ts +839 -0
  62. package/tests/cypress/e2e/api/entities/pages-crud.cy.ts +425 -0
  63. package/tests/cypress/e2e/api/entities/pages-status.cy.ts +335 -0
  64. package/tests/cypress/e2e/api/entities/post-categories-crud.cy.ts +610 -0
  65. package/tests/cypress/e2e/api/entities/posts-crud.cy.ts +709 -0
  66. package/tests/cypress/e2e/api/entities/posts-status.cy.ts +396 -0
  67. package/tests/cypress/e2e/api/entities/tasks-crud.cy.ts +602 -0
  68. package/tests/cypress/e2e/api/entities/tasks-metas.cy.ts +878 -0
  69. package/tests/cypress/e2e/api/entities/users-crud.cy.ts +469 -0
  70. package/tests/cypress/e2e/api/entities/users-metas.cy.ts +913 -0
  71. package/tests/cypress/e2e/api/entities/users-security.cy.ts +375 -0
  72. package/tests/cypress/e2e/api/scheduled-actions/cron-endpoint.bdd.md +375 -0
  73. package/tests/cypress/e2e/api/scheduled-actions/cron-endpoint.cy.ts +346 -0
  74. package/tests/cypress/e2e/api/scheduled-actions/devtools-endpoint.bdd.md +451 -0
  75. package/tests/cypress/e2e/api/scheduled-actions/devtools-endpoint.cy.ts +447 -0
  76. package/tests/cypress/e2e/api/scheduled-actions/scheduling.bdd.md +649 -0
  77. package/tests/cypress/e2e/api/scheduled-actions/scheduling.cy.ts +333 -0
  78. package/tests/cypress/e2e/api/settings/api-keys.crud.cy.ts +923 -0
  79. package/tests/cypress/e2e/uat/auth/app-roles/developer-login.bdd.md +231 -0
  80. package/tests/cypress/e2e/uat/auth/app-roles/developer-login.cy.ts +144 -0
  81. package/tests/cypress/e2e/uat/auth/app-roles/superadmin-login.bdd.md +118 -0
  82. package/tests/cypress/e2e/uat/auth/app-roles/superadmin-login.cy.ts +84 -0
  83. package/tests/cypress/e2e/uat/auth/custom-roles/editor-login.bdd.md +288 -0
  84. package/tests/cypress/e2e/uat/auth/custom-roles/editor-login.cy.ts +188 -0
  85. package/tests/cypress/e2e/uat/auth/login-logout.bdd.md +160 -0
  86. package/tests/cypress/e2e/uat/auth/login-logout.cy.ts +116 -0
  87. package/tests/cypress/e2e/uat/auth/password-reset.bdd.md +289 -0
  88. package/tests/cypress/e2e/uat/auth/password-reset.cy.ts +200 -0
  89. package/tests/cypress/e2e/uat/auth/team-roles/admin-login.bdd.md +225 -0
  90. package/tests/cypress/e2e/uat/auth/team-roles/admin-login.cy.ts +148 -0
  91. package/tests/cypress/e2e/uat/auth/team-roles/member-login.bdd.md +251 -0
  92. package/tests/cypress/e2e/uat/auth/team-roles/member-login.cy.ts +163 -0
  93. package/tests/cypress/e2e/uat/auth/team-roles/owner-login.bdd.md +231 -0
  94. package/tests/cypress/e2e/uat/auth/team-roles/owner-login.cy.ts +141 -0
  95. package/tests/cypress/e2e/uat/billing/extended.bdd.md +273 -0
  96. package/tests/cypress/e2e/uat/billing/extended.cy.ts +209 -0
  97. package/tests/cypress/e2e/uat/billing/feature-gates.bdd.md +407 -0
  98. package/tests/cypress/e2e/uat/billing/feature-gates.cy.ts +307 -0
  99. package/tests/cypress/e2e/uat/billing/page.bdd.md +329 -0
  100. package/tests/cypress/e2e/uat/billing/page.cy.ts +250 -0
  101. package/tests/cypress/e2e/uat/billing/status.bdd.md +190 -0
  102. package/tests/cypress/e2e/uat/billing/status.cy.ts +145 -0
  103. package/tests/cypress/e2e/uat/billing/team-switch.bdd.md +156 -0
  104. package/tests/cypress/e2e/uat/billing/team-switch.cy.ts +122 -0
  105. package/tests/cypress/e2e/uat/billing/usage.bdd.md +218 -0
  106. package/tests/cypress/e2e/uat/billing/usage.cy.ts +176 -0
  107. package/tests/cypress/e2e/uat/blocks/hero.bdd.md +124 -0
  108. package/tests/cypress/e2e/uat/blocks/hero.cy.ts +56 -0
  109. package/tests/cypress/e2e/uat/devtools/api-tester.cy.ts +390 -0
  110. package/tests/cypress/e2e/uat/entities/customers/member.bdd.md +275 -0
  111. package/tests/cypress/e2e/uat/entities/customers/member.cy.ts +122 -0
  112. package/tests/cypress/e2e/uat/entities/customers/owner.bdd.md +243 -0
  113. package/tests/cypress/e2e/uat/entities/customers/owner.cy.ts +165 -0
  114. package/tests/cypress/e2e/uat/entities/pages/block-crud.bdd.md +476 -0
  115. package/tests/cypress/e2e/uat/entities/pages/block-crud.cy.ts +486 -0
  116. package/tests/cypress/e2e/uat/entities/pages/block-editor.bdd.md +460 -0
  117. package/tests/cypress/e2e/uat/entities/pages/block-editor.cy.ts +301 -0
  118. package/tests/cypress/e2e/uat/entities/pages/list.bdd.md +432 -0
  119. package/tests/cypress/e2e/uat/entities/pages/list.cy.ts +273 -0
  120. package/tests/cypress/e2e/uat/entities/pages/public-rendering.bdd.md +696 -0
  121. package/tests/cypress/e2e/uat/entities/pages/public-rendering.cy.ts +340 -0
  122. package/tests/cypress/e2e/uat/entities/posts/categories-api-aware.bdd.md +161 -0
  123. package/tests/cypress/e2e/uat/entities/posts/categories-api-aware.cy.ts +104 -0
  124. package/tests/cypress/e2e/uat/entities/posts/categories.bdd.md +375 -0
  125. package/tests/cypress/e2e/uat/entities/posts/categories.cy.ts +241 -0
  126. package/tests/cypress/e2e/uat/entities/posts/editor.bdd.md +429 -0
  127. package/tests/cypress/e2e/uat/entities/posts/editor.cy.ts +257 -0
  128. package/tests/cypress/e2e/uat/entities/posts/list.bdd.md +340 -0
  129. package/tests/cypress/e2e/uat/entities/posts/list.cy.ts +177 -0
  130. package/tests/cypress/e2e/uat/entities/posts/public.bdd.md +614 -0
  131. package/tests/cypress/e2e/uat/entities/posts/public.cy.ts +249 -0
  132. package/tests/cypress/e2e/uat/entities/tasks/member.bdd.md +222 -0
  133. package/tests/cypress/e2e/uat/entities/tasks/member.cy.ts +165 -0
  134. package/tests/cypress/e2e/uat/entities/tasks/owner.bdd.md +419 -0
  135. package/tests/cypress/e2e/uat/entities/tasks/owner.cy.ts +191 -0
  136. package/tests/cypress/e2e/uat/roles/editor-role.bdd.md +552 -0
  137. package/tests/cypress/e2e/uat/roles/editor-role.cy.ts +210 -0
  138. package/tests/cypress/e2e/uat/roles/member-restrictions.bdd.md +450 -0
  139. package/tests/cypress/e2e/uat/roles/member-restrictions.cy.ts +189 -0
  140. package/tests/cypress/e2e/uat/roles/owner-full-crud.bdd.md +530 -0
  141. package/tests/cypress/e2e/uat/roles/owner-full-crud.cy.ts +247 -0
  142. package/tests/cypress/e2e/uat/scheduled-actions/devtools-ui.bdd.md +736 -0
  143. package/tests/cypress/e2e/uat/scheduled-actions/devtools-ui.cy.ts +740 -0
  144. package/tests/cypress/e2e/uat/teams/roles-matrix.bdd.md +553 -0
  145. package/tests/cypress/e2e/uat/teams/roles-matrix.cy.ts +185 -0
  146. package/tests/cypress/e2e/uat/teams/switcher.bdd.md +1151 -0
  147. package/tests/cypress/e2e/uat/teams/switcher.cy.ts +497 -0
  148. package/tests/cypress/e2e/uat/teams/team-switcher.md +198 -0
  149. package/tests/cypress/fixtures/blocks.json +218 -0
  150. package/tests/cypress/fixtures/entities.json +78 -0
  151. package/tests/cypress/fixtures/page-builder.json +21 -0
  152. package/tests/cypress/src/components/CategoriesPOM.ts +382 -0
  153. package/tests/cypress/src/components/CustomersPOM.ts +439 -0
  154. package/tests/cypress/src/components/DevKeyringPOM.ts +160 -0
  155. package/tests/cypress/src/components/EntityForm.ts +375 -0
  156. package/tests/cypress/src/components/EntityList.ts +389 -0
  157. package/tests/cypress/src/components/PageBuilderPOM.ts +710 -0
  158. package/tests/cypress/src/components/PostEditorPOM.ts +370 -0
  159. package/tests/cypress/src/components/PostsListPOM.ts +223 -0
  160. package/tests/cypress/src/components/PublicPagePOM.ts +447 -0
  161. package/tests/cypress/src/components/PublicPostPOM.ts +146 -0
  162. package/tests/cypress/src/components/TasksPOM.ts +272 -0
  163. package/tests/cypress/src/components/TeamSwitcherPOM.ts +450 -0
  164. package/tests/cypress/src/components/index.ts +21 -0
  165. package/tests/cypress/src/controllers/ApiKeysAPIController.js +178 -0
  166. package/tests/cypress/src/controllers/BaseAPIController.js +317 -0
  167. package/tests/cypress/src/controllers/CustomerAPIController.js +251 -0
  168. package/tests/cypress/src/controllers/PagesAPIController.js +226 -0
  169. package/tests/cypress/src/controllers/PostsAPIController.js +250 -0
  170. package/tests/cypress/src/controllers/TaskAPIController.js +240 -0
  171. package/tests/cypress/src/controllers/UsersAPIController.js +242 -0
  172. package/tests/cypress/src/controllers/index.js +25 -0
  173. package/tests/cypress/src/core/AuthPOM.ts +450 -0
  174. package/tests/cypress/src/core/BasePOM.ts +86 -0
  175. package/tests/cypress/src/core/BlockEditorBasePOM.ts +576 -0
  176. package/tests/cypress/src/core/DashboardEntityPOM.ts +692 -0
  177. package/tests/cypress/src/core/index.ts +14 -0
  178. package/tests/cypress/src/entities/CustomersPOM.ts +172 -0
  179. package/tests/cypress/src/entities/PagesPOM.ts +137 -0
  180. package/tests/cypress/src/entities/PostsPOM.ts +137 -0
  181. package/tests/cypress/src/entities/TasksPOM.ts +176 -0
  182. package/tests/cypress/src/entities/index.ts +14 -0
  183. package/tests/cypress/src/features/BillingPOM.ts +385 -0
  184. package/tests/cypress/src/features/DashboardPOM.ts +245 -0
  185. package/tests/cypress/src/features/DevtoolsPOM.ts +739 -0
  186. package/tests/cypress/src/features/PageBuilderPOM.ts +263 -0
  187. package/tests/cypress/src/features/PostEditorPOM.ts +313 -0
  188. package/tests/cypress/src/features/ScheduledActionsPOM.ts +463 -0
  189. package/tests/cypress/src/features/SettingsPOM.ts +362 -0
  190. package/tests/cypress/src/features/SuperadminPOM.ts +331 -0
  191. package/tests/cypress/src/features/SuperadminTeamRolesPOM.ts +285 -0
  192. package/tests/cypress/src/features/index.ts +28 -0
  193. package/tests/cypress/src/helpers/ApiInterceptor.ts +177 -0
  194. package/tests/cypress/src/index.ts +101 -0
  195. package/tests/cypress/src/pages/dashboard/Dashboard.js +677 -0
  196. package/tests/cypress/src/pages/dashboard/DashboardPage.js +43 -0
  197. package/tests/cypress/src/pages/dashboard/DashboardStats.js +546 -0
  198. package/tests/cypress/src/pages/dashboard/index.js +6 -0
  199. package/tests/cypress/src/pages/index.js +5 -0
  200. package/tests/cypress/src/pages/public/FeaturesPage.js +28 -0
  201. package/tests/cypress/src/pages/public/LandingPage.js +69 -0
  202. package/tests/cypress/src/pages/public/PricingPage.js +33 -0
  203. package/tests/cypress/src/pages/public/index.js +6 -0
  204. package/tests/cypress/src/selectors.ts +46 -0
  205. package/tests/cypress/src/session-helpers.ts +500 -0
  206. package/tests/cypress/support/doc-commands.ts +260 -0
  207. package/tests/cypress/support/e2e.ts +89 -0
  208. package/tests/cypress.config.ts +165 -0
  209. package/tests/jest/components/post-header.test.tsx +377 -0
  210. package/tests/jest/config/role-config.test.ts +529 -0
  211. package/tests/jest/jest.config.ts +81 -0
  212. package/tests/jest/langchain/COVERAGE.md +372 -0
  213. package/tests/jest/langchain/guardrails.test.ts +465 -0
  214. package/tests/jest/langchain/streaming.test.ts +367 -0
  215. package/tests/jest/langchain/token-tracker.test.ts +455 -0
  216. package/tests/jest/langchain/tracer-callbacks.test.ts +881 -0
  217. package/tests/jest/langchain/tracer.test.ts +823 -0
  218. package/tests/jest/user-roles/role-helpers.test.ts +432 -0
  219. package/tests/jest/validation/categories.test.ts +429 -0
  220. package/tests/jest/validation/posts.test.ts +546 -0
  221. package/tests/tsconfig.json +15 -0
@@ -0,0 +1,878 @@
1
+ /// <reference types="cypress" />
2
+
3
+ /**
4
+ * Tasks API - Metadata Tests
5
+ *
6
+ * Comprehensive test suite for Task API endpoints with metadata functionality.
7
+ * Covers GET, POST, PATCH, DELETE operations with various metadata scenarios.
8
+ * Tests metadata parameter handling, merge behavior, and upsert functionality.
9
+ */
10
+
11
+ import * as allure from 'allure-cypress'
12
+
13
+ const TaskAPIController = require('../../../src/controllers/TaskAPIController.js')
14
+
15
+ describe('Tasks API - Metadata Operations', {
16
+ tags: ['@api', '@feat-tasks', '@metas', '@regression']
17
+ }, () => {
18
+ let taskAPI: any
19
+ let createdTasks: any[] = []
20
+
21
+ // Superadmin API key for testing
22
+ const SUPERADMIN_API_KEY = 'test_api_key_for_testing_purposes_only_not_a_real_secret_key_abc123'
23
+ const TEAM_ID = 'team-tmt-001'
24
+ const BASE_URL = Cypress.config('baseUrl') || 'http://localhost:5173'
25
+
26
+ before(() => {
27
+ // Initialize API controller with superadmin API key and team context
28
+ taskAPI = new TaskAPIController(BASE_URL, SUPERADMIN_API_KEY, TEAM_ID)
29
+ cy.log('TaskAPIController initialized for metadata tests')
30
+ cy.log(`Base URL: ${BASE_URL}`)
31
+ cy.log(`Team ID: ${TEAM_ID}`)
32
+ })
33
+
34
+ beforeEach(() => {
35
+ allure.epic('API')
36
+ allure.feature('Tasks')
37
+ allure.story('Metadata Operations')
38
+ })
39
+
40
+ afterEach(() => {
41
+ // Cleanup: Delete tasks created during tests
42
+ if (createdTasks.length > 0) {
43
+ createdTasks.forEach((task: any) => {
44
+ if (task && task.id) {
45
+ taskAPI.deleteTask(task.id)
46
+ }
47
+ })
48
+ createdTasks = []
49
+ }
50
+ })
51
+
52
+ // ============================================================
53
+ // GET /api/v1/tasks - List Tasks with Metadata
54
+ // ============================================================
55
+ describe('GET /api/v1/tasks - List Tasks with Metadata', () => {
56
+ let taskWithMetas: any
57
+
58
+ before(() => {
59
+ // Create a task with metadata for list tests
60
+ const taskData = taskAPI.generateRandomTaskData({
61
+ title: 'Task with Metas for List',
62
+ metas: {
63
+ uiPreferences: taskAPI.generateSampleMetadata('uiPreferences').uiPreferences,
64
+ notifications: taskAPI.generateSampleMetadata('notifications').notifications
65
+ }
66
+ })
67
+
68
+ taskAPI.createTask(taskData).then((response: any) => {
69
+ taskWithMetas = response.body.data
70
+ })
71
+ })
72
+
73
+ after(() => {
74
+ if (taskWithMetas && taskWithMetas.id) {
75
+ taskAPI.deleteTask(taskWithMetas.id)
76
+ }
77
+ })
78
+
79
+ it('TASKS_META_001: Should list tasks without metas property when metas param not provided', () => {
80
+ taskAPI.getTasks().then((response: any) => {
81
+ taskAPI.validateSuccessResponse(response, 200)
82
+
83
+ // Tasks should NOT have metas property when not requested
84
+ response.body.data.forEach((task: any) => {
85
+ expect(task).to.not.have.property('metas')
86
+ })
87
+
88
+ cy.log('Verified: No metas property in response without metas param')
89
+ })
90
+ })
91
+
92
+ it('TASKS_META_002: Should list tasks with metas=all including all metadata groups', () => {
93
+ taskAPI.getTasks({ metas: 'all' }).then((response: any) => {
94
+ taskAPI.validateSuccessResponse(response, 200)
95
+
96
+ // Find our test task
97
+ const foundTask = response.body.data.find((t: any) => t.id === taskWithMetas.id)
98
+ if (foundTask) {
99
+ expect(foundTask).to.have.property('metas')
100
+ expect(foundTask.metas).to.be.an('object')
101
+
102
+ cy.log(`Task has metas: ${JSON.stringify(Object.keys(foundTask.metas))}`)
103
+ }
104
+ })
105
+ })
106
+
107
+ it('TASKS_META_003: Should list tasks with metas=key1 including only specified metaKey', () => {
108
+ taskAPI.getTasks({ metas: 'uiPreferences' }).then((response: any) => {
109
+ taskAPI.validateSuccessResponse(response, 200)
110
+
111
+ // Find our test task
112
+ const foundTask = response.body.data.find((t: any) => t.id === taskWithMetas.id)
113
+ if (foundTask && foundTask.metas) {
114
+ expect(foundTask).to.have.property('metas')
115
+ // Should only have uiPreferences key
116
+ if (Object.keys(foundTask.metas).length > 0) {
117
+ expect(foundTask.metas).to.have.property('uiPreferences')
118
+ }
119
+ }
120
+
121
+ cy.log('Verified: Only requested metaKey included')
122
+ })
123
+ })
124
+
125
+ it('TASKS_META_004: Should list tasks with metas=key1,key2 including multiple metaKeys', () => {
126
+ taskAPI.getTasks({ metas: 'uiPreferences,notifications' }).then((response: any) => {
127
+ taskAPI.validateSuccessResponse(response, 200)
128
+
129
+ // Find our test task
130
+ const foundTask = response.body.data.find((t: any) => t.id === taskWithMetas.id)
131
+ if (foundTask && foundTask.metas) {
132
+ expect(foundTask).to.have.property('metas')
133
+ // May have one or both keys
134
+ const metaKeys = Object.keys(foundTask.metas)
135
+ cy.log(`Task metas keys: ${metaKeys.join(', ')}`)
136
+ }
137
+ })
138
+ })
139
+
140
+ it('TASKS_META_005: Should return empty metas object for non-existent metaKey', () => {
141
+ taskAPI.getTasks({ metas: 'nonExistentKey' }).then((response: any) => {
142
+ taskAPI.validateSuccessResponse(response, 200)
143
+
144
+ // Tasks should have metas property but it should be empty for this key
145
+ response.body.data.forEach((task: any) => {
146
+ if (task.metas) {
147
+ expect(task.metas).to.not.have.property('nonExistentKey')
148
+ }
149
+ })
150
+
151
+ cy.log('Verified: Non-existent metaKey returns empty metas')
152
+ })
153
+ })
154
+
155
+ it('TASKS_META_006: Should combine pagination with metas parameter', () => {
156
+ taskAPI.getTasks({ page: 1, limit: 5, metas: 'all' }).then((response: any) => {
157
+ taskAPI.validatePaginatedResponse(response)
158
+ expect(response.body.info.page).to.eq(1)
159
+ expect(response.body.info.limit).to.eq(5)
160
+
161
+ // Tasks with metas should have metas property
162
+ const tasksWithMetas = response.body.data.filter((t: any) => t.metas && Object.keys(t.metas).length > 0)
163
+ cy.log(`Found ${tasksWithMetas.length} tasks with metas (paginated)`)
164
+ })
165
+ })
166
+ })
167
+
168
+ // ============================================================
169
+ // GET /api/v1/tasks/{id} - Get Single Task with Metadata
170
+ // ============================================================
171
+ describe('GET /api/v1/tasks/{id} - Get Single Task with Metadata', () => {
172
+ let taskWithMetas: any
173
+ let taskWithoutMetas: any
174
+
175
+ beforeEach(() => {
176
+ // Create a task with metadata
177
+ const taskDataWithMetas = taskAPI.generateRandomTaskData({
178
+ title: 'Task with Metas',
179
+ metas: {
180
+ uiPreferences: taskAPI.generateSampleMetadata('uiPreferences').uiPreferences,
181
+ tracking: taskAPI.generateSampleMetadata('tracking').tracking
182
+ }
183
+ })
184
+
185
+ taskAPI.createTask(taskDataWithMetas).then((response: any) => {
186
+ taskWithMetas = response.body.data
187
+ createdTasks.push(taskWithMetas)
188
+ })
189
+
190
+ // Create a task without metadata
191
+ const taskDataWithoutMetas = taskAPI.generateRandomTaskData({
192
+ title: 'Task without Metas'
193
+ })
194
+
195
+ taskAPI.createTask(taskDataWithoutMetas).then((response: any) => {
196
+ taskWithoutMetas = response.body.data
197
+ createdTasks.push(taskWithoutMetas)
198
+ })
199
+ })
200
+
201
+ it('TASKS_META_010: Should get task without metas property when metas param not provided', () => {
202
+ cy.then(() => {
203
+ taskAPI.getTaskById(taskWithMetas.id).then((response: any) => {
204
+ taskAPI.validateSuccessResponse(response, 200)
205
+ expect(response.body.data).to.not.have.property('metas')
206
+
207
+ cy.log('Verified: No metas property without metas param')
208
+ })
209
+ })
210
+ })
211
+
212
+ it('TASKS_META_011: Should get task with metas=all including all metadata groups', () => {
213
+ cy.then(() => {
214
+ taskAPI.getTaskById(taskWithMetas.id, { metas: 'all' }).then((response: any) => {
215
+ taskAPI.validateSuccessResponse(response, 200)
216
+ expect(response.body.data).to.have.property('metas')
217
+ expect(response.body.data.metas).to.be.an('object')
218
+
219
+ // Should have the metas we created
220
+ expect(response.body.data.metas).to.have.property('uiPreferences')
221
+ expect(response.body.data.metas).to.have.property('tracking')
222
+
223
+ cy.log(`Task metas: ${JSON.stringify(Object.keys(response.body.data.metas))}`)
224
+ })
225
+ })
226
+ })
227
+
228
+ it('TASKS_META_012: Should get task with metas=key1 including only specified metaKey', () => {
229
+ cy.then(() => {
230
+ taskAPI.getTaskById(taskWithMetas.id, { metas: 'uiPreferences' }).then((response: any) => {
231
+ taskAPI.validateSuccessResponse(response, 200)
232
+ expect(response.body.data).to.have.property('metas')
233
+
234
+ // Should only have uiPreferences
235
+ expect(response.body.data.metas).to.have.property('uiPreferences')
236
+ expect(response.body.data.metas).to.not.have.property('tracking')
237
+
238
+ cy.log('Verified: Only uiPreferences metaKey returned')
239
+ })
240
+ })
241
+ })
242
+
243
+ it('TASKS_META_013: Should get task with metas=key1,key2 including multiple metaKeys', () => {
244
+ cy.then(() => {
245
+ taskAPI.getTaskById(taskWithMetas.id, { metas: 'uiPreferences,tracking' }).then((response: any) => {
246
+ taskAPI.validateSuccessResponse(response, 200)
247
+ expect(response.body.data).to.have.property('metas')
248
+
249
+ // Should have both keys
250
+ expect(response.body.data.metas).to.have.property('uiPreferences')
251
+ expect(response.body.data.metas).to.have.property('tracking')
252
+
253
+ cy.log('Verified: Both metaKeys returned')
254
+ })
255
+ })
256
+ })
257
+
258
+ it('TASKS_META_014: Should get task without metadata returning metas: {}', () => {
259
+ cy.then(() => {
260
+ taskAPI.getTaskById(taskWithoutMetas.id, { metas: 'all' }).then((response: any) => {
261
+ taskAPI.validateSuccessResponse(response, 200)
262
+ expect(response.body.data).to.have.property('metas')
263
+ expect(response.body.data.metas).to.deep.eq({})
264
+
265
+ cy.log('Verified: Task without metadata returns empty metas object')
266
+ })
267
+ })
268
+ })
269
+
270
+ it('TASKS_META_015: Should return empty metas for non-existent metaKey', () => {
271
+ cy.then(() => {
272
+ taskAPI.getTaskById(taskWithMetas.id, { metas: 'nonExistentKey' }).then((response: any) => {
273
+ taskAPI.validateSuccessResponse(response, 200)
274
+ expect(response.body.data).to.have.property('metas')
275
+ expect(response.body.data.metas).to.not.have.property('nonExistentKey')
276
+
277
+ cy.log('Verified: Non-existent metaKey not in response')
278
+ })
279
+ })
280
+ })
281
+ })
282
+
283
+ // ============================================================
284
+ // POST /api/v1/tasks - Create Task with Metadata
285
+ // ============================================================
286
+ describe('POST /api/v1/tasks - Create Task with Metadata', () => {
287
+ it('TASKS_META_020: Should create task without metas and no metas in response', () => {
288
+ const taskData = taskAPI.generateRandomTaskData({
289
+ title: 'Task without metas'
290
+ })
291
+
292
+ taskAPI.createTask(taskData).then((response: any) => {
293
+ taskAPI.validateSuccessResponse(response, 201)
294
+ expect(response.body.data).to.not.have.property('metas')
295
+
296
+ createdTasks.push(response.body.data)
297
+ cy.log('Verified: No metas in response when not provided')
298
+ })
299
+ })
300
+
301
+ it('TASKS_META_021: Should create task with one meta group and include in response', () => {
302
+ const taskData = taskAPI.generateRandomTaskData({
303
+ title: 'Task with one meta group',
304
+ metas: {
305
+ uiPreferences: {
306
+ colorLabel: 'green',
307
+ showInKanban: true
308
+ }
309
+ }
310
+ })
311
+
312
+ taskAPI.createTask(taskData).then((response: any) => {
313
+ taskAPI.validateSuccessResponse(response, 201)
314
+ expect(response.body.data).to.have.property('metas')
315
+ expect(response.body.data.metas).to.have.property('uiPreferences')
316
+ expect(response.body.data.metas.uiPreferences.colorLabel).to.eq('green')
317
+
318
+ createdTasks.push(response.body.data)
319
+ cy.log('Verified: Single meta group created and returned')
320
+ })
321
+ })
322
+
323
+ it('TASKS_META_022: Should create task with multiple meta groups', () => {
324
+ const taskData = taskAPI.generateRandomTaskData({
325
+ title: 'Task with multiple meta groups',
326
+ metas: {
327
+ uiPreferences: taskAPI.generateSampleMetadata('uiPreferences').uiPreferences,
328
+ notifications: taskAPI.generateSampleMetadata('notifications').notifications,
329
+ tracking: taskAPI.generateSampleMetadata('tracking').tracking
330
+ }
331
+ })
332
+
333
+ taskAPI.createTask(taskData).then((response: any) => {
334
+ taskAPI.validateSuccessResponse(response, 201)
335
+ expect(response.body.data).to.have.property('metas')
336
+ expect(response.body.data.metas).to.have.property('uiPreferences')
337
+ expect(response.body.data.metas).to.have.property('notifications')
338
+ expect(response.body.data.metas).to.have.property('tracking')
339
+
340
+ createdTasks.push(response.body.data)
341
+ cy.log('Verified: All meta groups created')
342
+ })
343
+ })
344
+
345
+ it('TASKS_META_023: Should create task with nested meta structure preserved', () => {
346
+ const nestedMetas = {
347
+ customFields: {
348
+ level1: {
349
+ level2: {
350
+ level3: {
351
+ deepValue: 'nested-test',
352
+ deepArray: [1, 2, 3]
353
+ }
354
+ }
355
+ }
356
+ }
357
+ }
358
+
359
+ const taskData = taskAPI.generateRandomTaskData({
360
+ title: 'Task with nested metas',
361
+ metas: nestedMetas
362
+ })
363
+
364
+ taskAPI.createTask(taskData).then((response: any) => {
365
+ taskAPI.validateSuccessResponse(response, 201)
366
+ expect(response.body.data).to.have.property('metas')
367
+ expect(response.body.data.metas).to.have.property('customFields')
368
+ expect(response.body.data.metas.customFields.level1.level2.level3.deepValue).to.eq('nested-test')
369
+ expect(response.body.data.metas.customFields.level1.level2.level3.deepArray).to.deep.eq([1, 2, 3])
370
+
371
+ createdTasks.push(response.body.data)
372
+ cy.log('Verified: Nested structure preserved')
373
+ })
374
+ })
375
+
376
+ it('TASKS_META_024: Should reject creation with only metas and no title', () => {
377
+ const taskData = {
378
+ metas: {
379
+ uiPreferences: {
380
+ colorLabel: 'red'
381
+ }
382
+ }
383
+ }
384
+
385
+ taskAPI.createTask(taskData).then((response: any) => {
386
+ expect(response.status).to.eq(400)
387
+ expect(response.body.success).to.be.false
388
+
389
+ cy.log('Verified: Cannot create task with only metas')
390
+ })
391
+ })
392
+
393
+ it('TASKS_META_025: Should handle invalid metas format (string instead of object)', () => {
394
+ const taskData = taskAPI.generateRandomTaskData({
395
+ title: 'Task with invalid metas',
396
+ metas: 'invalid-string-metas'
397
+ })
398
+
399
+ taskAPI.createTask(taskData).then((response: any) => {
400
+ // API should either reject or ignore invalid metas
401
+ if (response.status === 201) {
402
+ // If created, metas should not be the string
403
+ expect(response.body.data.metas).to.not.eq('invalid-string-metas')
404
+ createdTasks.push(response.body.data)
405
+ } else {
406
+ expect(response.status).to.eq(400)
407
+ }
408
+
409
+ cy.log('Verified: Invalid metas format handled')
410
+ })
411
+ })
412
+ })
413
+
414
+ // ============================================================
415
+ // PATCH /api/v1/tasks/{id} - Update Metadata
416
+ // ============================================================
417
+ describe('PATCH /api/v1/tasks/{id} - Update Metadata', () => {
418
+ let testTask: any
419
+
420
+ beforeEach(() => {
421
+ // Create a task with initial metadata
422
+ const taskData = taskAPI.generateRandomTaskData({
423
+ title: 'Task for Meta Updates',
424
+ metas: {
425
+ uiPreferences: {
426
+ theme: 'dark',
427
+ sidebar: true,
428
+ language: 'en'
429
+ },
430
+ notifications: {
431
+ emailOnDue: true,
432
+ reminderDays: 3
433
+ }
434
+ }
435
+ })
436
+
437
+ taskAPI.createTask(taskData).then((response: any) => {
438
+ testTask = response.body.data
439
+ createdTasks.push(testTask)
440
+ })
441
+ })
442
+
443
+ it('TASKS_META_030: Should update only task data without affecting metas', () => {
444
+ cy.then(() => {
445
+ const updateData = {
446
+ title: 'Updated Title Only'
447
+ }
448
+
449
+ taskAPI.updateTask(testTask.id, updateData).then((response: any) => {
450
+ taskAPI.validateSuccessResponse(response, 200)
451
+ expect(response.body.data.title).to.eq(updateData.title)
452
+ // When updating without metas, response should not include metas
453
+ expect(response.body.data).to.not.have.property('metas')
454
+
455
+ // Verify metas still exist by fetching with metas=all
456
+ taskAPI.getTaskById(testTask.id, { metas: 'all' }).then((getResponse: any) => {
457
+ expect(getResponse.body.data.metas).to.have.property('uiPreferences')
458
+ cy.log('Verified: Metas unchanged when not included in update')
459
+ })
460
+ })
461
+ })
462
+ })
463
+
464
+ it('TASKS_META_031: Should update both task data and metas', () => {
465
+ cy.then(() => {
466
+ const updateData = {
467
+ title: 'Updated with Metas',
468
+ metas: {
469
+ uiPreferences: {
470
+ theme: 'light'
471
+ }
472
+ }
473
+ }
474
+
475
+ taskAPI.updateTask(testTask.id, updateData).then((response: any) => {
476
+ taskAPI.validateSuccessResponse(response, 200)
477
+ expect(response.body.data.title).to.eq(updateData.title)
478
+ expect(response.body.data).to.have.property('metas')
479
+ expect(response.body.data.metas.uiPreferences.theme).to.eq('light')
480
+
481
+ cy.log('Verified: Both task data and metas updated')
482
+ })
483
+ })
484
+ })
485
+
486
+ it('TASKS_META_032: Should support metas-only updates without entity fields', () => {
487
+ /**
488
+ * NOTE: The generic entity API now supports metas-only updates.
489
+ * Previously this required at least one entity field, but the API
490
+ * has been updated to allow updating only metas.
491
+ */
492
+ cy.then(() => {
493
+ const updateData = {
494
+ metas: {
495
+ uiPreferences: {
496
+ newSetting: 'added-via-metas-only'
497
+ }
498
+ }
499
+ }
500
+
501
+ // Generic API now supports metas-only updates
502
+ taskAPI.updateTask(testTask.id, updateData).then((response: any) => {
503
+ taskAPI.validateSuccessResponse(response, 200)
504
+ expect(response.body.data).to.have.property('metas')
505
+ expect(response.body.data.metas.uiPreferences).to.have.property('newSetting')
506
+ expect(response.body.data.metas.uiPreferences.newSetting).to.eq('added-via-metas-only')
507
+
508
+ cy.log('Verified: Metas-only updates now supported')
509
+ })
510
+ })
511
+ })
512
+
513
+ it('TASKS_META_033: Should merge new key into existing meta group (preserving existing keys)', () => {
514
+ cy.then(() => {
515
+ // Include title to satisfy entity field requirement
516
+ const updateData = {
517
+ title: testTask.title, // No-op update to satisfy requirement
518
+ metas: {
519
+ uiPreferences: {
520
+ newSetting: 'added-value'
521
+ }
522
+ }
523
+ }
524
+
525
+ taskAPI.updateTask(testTask.id, updateData).then((response: any) => {
526
+ taskAPI.validateSuccessResponse(response, 200)
527
+ expect(response.body.data).to.have.property('metas')
528
+ expect(response.body.data.metas.uiPreferences).to.have.property('newSetting')
529
+ // Original keys should be preserved
530
+ expect(response.body.data.metas.uiPreferences).to.have.property('theme')
531
+ expect(response.body.data.metas.uiPreferences).to.have.property('sidebar')
532
+
533
+ cy.log('Verified: New key added, existing keys preserved')
534
+ })
535
+ })
536
+ })
537
+
538
+ it('TASKS_META_034: Should overwrite existing key while preserving others', () => {
539
+ cy.then(() => {
540
+ // Include title to satisfy entity field requirement
541
+ const updateData = {
542
+ title: testTask.title,
543
+ metas: {
544
+ uiPreferences: {
545
+ theme: 'light' // Overwrite existing
546
+ }
547
+ }
548
+ }
549
+
550
+ taskAPI.updateTask(testTask.id, updateData).then((response: any) => {
551
+ taskAPI.validateSuccessResponse(response, 200)
552
+ expect(response.body.data.metas.uiPreferences.theme).to.eq('light')
553
+ // Other keys should still exist
554
+ expect(response.body.data.metas.uiPreferences).to.have.property('sidebar')
555
+
556
+ cy.log('Verified: Key overwritten, others preserved')
557
+ })
558
+ })
559
+ })
560
+
561
+ it('TASKS_META_035: Should upsert (create) new metaKey if it does not exist', () => {
562
+ cy.then(() => {
563
+ // Include title to satisfy entity field requirement
564
+ const updateData = {
565
+ title: testTask.title,
566
+ metas: {
567
+ customFields: {
568
+ clientRef: 'NEW-001',
569
+ department: 'Sales'
570
+ }
571
+ }
572
+ }
573
+
574
+ taskAPI.updateTask(testTask.id, updateData).then((response: any) => {
575
+ taskAPI.validateSuccessResponse(response, 200)
576
+ expect(response.body.data.metas).to.have.property('customFields')
577
+ expect(response.body.data.metas.customFields.clientRef).to.eq('NEW-001')
578
+
579
+ cy.log('Verified: New metaKey created via upsert')
580
+ })
581
+ })
582
+ })
583
+
584
+ it('TASKS_META_036: Should update multiple meta groups simultaneously', () => {
585
+ cy.then(() => {
586
+ // Include title to satisfy entity field requirement
587
+ const updateData = {
588
+ title: testTask.title,
589
+ metas: {
590
+ uiPreferences: {
591
+ theme: 'system'
592
+ },
593
+ notifications: {
594
+ slack: true
595
+ },
596
+ tracking: {
597
+ actualHours: 5
598
+ }
599
+ }
600
+ }
601
+
602
+ taskAPI.updateTask(testTask.id, updateData).then((response: any) => {
603
+ taskAPI.validateSuccessResponse(response, 200)
604
+ expect(response.body.data.metas).to.have.property('uiPreferences')
605
+ expect(response.body.data.metas).to.have.property('notifications')
606
+ expect(response.body.data.metas).to.have.property('tracking')
607
+
608
+ cy.log('Verified: Multiple meta groups updated')
609
+ })
610
+ })
611
+ })
612
+
613
+ it('TASKS_META_037: Should handle nested object updates correctly', () => {
614
+ cy.then(() => {
615
+ // Include title to satisfy entity field requirement
616
+ const updateData = {
617
+ title: testTask.title,
618
+ metas: {
619
+ customFields: {
620
+ nested: {
621
+ level1: {
622
+ level2: {
623
+ value: 'deep-update'
624
+ }
625
+ }
626
+ }
627
+ }
628
+ }
629
+ }
630
+
631
+ taskAPI.updateTask(testTask.id, updateData).then((response: any) => {
632
+ taskAPI.validateSuccessResponse(response, 200)
633
+ expect(response.body.data.metas.customFields.nested.level1.level2.value).to.eq('deep-update')
634
+
635
+ cy.log('Verified: Nested objects updated correctly')
636
+ })
637
+ })
638
+ })
639
+
640
+ it('TASKS_META_038: Should reject update with empty metas and no task fields', () => {
641
+ cy.then(() => {
642
+ const updateData = {
643
+ metas: {}
644
+ }
645
+
646
+ taskAPI.updateTask(testTask.id, updateData).then((response: any) => {
647
+ expect(response.status).to.eq(400)
648
+ expect(response.body.success).to.be.false
649
+
650
+ cy.log('Verified: Empty metas with no fields rejected')
651
+ })
652
+ })
653
+ })
654
+
655
+ it('TASKS_META_039: Should reject update with invalid metas format (string)', () => {
656
+ cy.then(() => {
657
+ const updateData = {
658
+ metas: 'invalid-string'
659
+ }
660
+
661
+ taskAPI.updateTask(testTask.id, updateData).then((response: any) => {
662
+ expect(response.status).to.eq(400)
663
+ expect(response.body.success).to.be.false
664
+
665
+ cy.log('Verified: Invalid metas format rejected')
666
+ })
667
+ })
668
+ })
669
+ })
670
+
671
+ // ============================================================
672
+ // DELETE /api/v1/tasks/{id} - Delete with Metadata
673
+ // ============================================================
674
+ describe('DELETE /api/v1/tasks/{id} - Delete with Metadata', () => {
675
+ it('TASKS_META_050: Should delete task with metadata successfully', () => {
676
+ // Create task with metas
677
+ const taskData = taskAPI.generateRandomTaskData({
678
+ title: 'Task to Delete with Metas',
679
+ metas: {
680
+ uiPreferences: { colorLabel: 'red' },
681
+ tracking: { actualHours: 10 }
682
+ }
683
+ })
684
+
685
+ taskAPI.createTask(taskData).then((createResponse: any) => {
686
+ const taskId = createResponse.body.data.id
687
+
688
+ // Verify metas exist
689
+ taskAPI.getTaskById(taskId, { metas: 'all' }).then((getResponse: any) => {
690
+ expect(getResponse.body.data.metas).to.have.property('uiPreferences')
691
+
692
+ // Now delete
693
+ taskAPI.deleteTask(taskId).then((deleteResponse: any) => {
694
+ taskAPI.validateSuccessResponse(deleteResponse, 200)
695
+ expect(deleteResponse.body.data.success).to.be.true
696
+
697
+ cy.log('Verified: Task with metas deleted successfully')
698
+ })
699
+ })
700
+ })
701
+ })
702
+
703
+ it('TASKS_META_051: Should verify cascade delete removes metadata', () => {
704
+ // Create task with metas
705
+ const taskData = taskAPI.generateRandomTaskData({
706
+ title: 'Task for Cascade Delete Test',
707
+ metas: {
708
+ customFields: { important: true }
709
+ }
710
+ })
711
+
712
+ taskAPI.createTask(taskData).then((createResponse: any) => {
713
+ const taskId = createResponse.body.data.id
714
+
715
+ // Delete the task
716
+ taskAPI.deleteTask(taskId).then((deleteResponse: any) => {
717
+ expect(deleteResponse.status).to.eq(200)
718
+
719
+ // Verify task is gone
720
+ taskAPI.getTaskById(taskId, { metas: 'all' }).then((getResponse: any) => {
721
+ expect(getResponse.status).to.eq(404)
722
+
723
+ cy.log('Verified: Task and metas cascade deleted')
724
+ })
725
+ })
726
+ })
727
+ })
728
+
729
+ it('TASKS_META_052: Should delete task without metadata normally', () => {
730
+ // Create task without metas
731
+ const taskData = taskAPI.generateRandomTaskData({
732
+ title: 'Task without Metas to Delete'
733
+ })
734
+
735
+ taskAPI.createTask(taskData).then((createResponse: any) => {
736
+ const taskId = createResponse.body.data.id
737
+
738
+ // Delete the task
739
+ taskAPI.deleteTask(taskId).then((deleteResponse: any) => {
740
+ taskAPI.validateSuccessResponse(deleteResponse, 200)
741
+ expect(deleteResponse.body.data.success).to.be.true
742
+
743
+ // Verify deletion
744
+ taskAPI.getTaskById(taskId).then((getResponse: any) => {
745
+ expect(getResponse.status).to.eq(404)
746
+
747
+ cy.log('Verified: Task without metas deleted normally')
748
+ })
749
+ })
750
+ })
751
+ })
752
+ })
753
+
754
+ // ============================================================
755
+ // Integration - Complete CRUD Lifecycle with Metadata
756
+ // ============================================================
757
+ describe('Integration - Complete CRUD Lifecycle with Metadata', () => {
758
+ it('TASKS_META_100: Should complete full lifecycle with metas: Create → Read → Update → Delete', () => {
759
+ const initialMetas = {
760
+ uiPreferences: {
761
+ colorLabel: 'blue',
762
+ priority: 'high'
763
+ }
764
+ }
765
+
766
+ const taskData = taskAPI.generateRandomTaskData({
767
+ title: 'Lifecycle Task with Metas',
768
+ metas: initialMetas
769
+ })
770
+
771
+ // 1. CREATE with metas
772
+ taskAPI.createTask(taskData).then((createResponse: any) => {
773
+ taskAPI.validateSuccessResponse(createResponse, 201)
774
+ expect(createResponse.body.data).to.have.property('metas')
775
+ expect(createResponse.body.data.metas.uiPreferences.colorLabel).to.eq('blue')
776
+ const taskId = createResponse.body.data.id
777
+ cy.log(`1. Created task with metas: ${taskId}`)
778
+
779
+ // 2. READ with metas=all
780
+ taskAPI.getTaskById(taskId, { metas: 'all' }).then((readResponse: any) => {
781
+ taskAPI.validateSuccessResponse(readResponse, 200)
782
+ expect(readResponse.body.data.metas).to.have.property('uiPreferences')
783
+ cy.log('2. Read task with all metas')
784
+
785
+ // 3. UPDATE metas (include title to satisfy entity field requirement)
786
+ const updateMetas = {
787
+ title: 'Lifecycle Task with Metas', // Required entity field
788
+ metas: {
789
+ uiPreferences: {
790
+ colorLabel: 'green', // Update existing
791
+ newField: 'added' // Add new
792
+ },
793
+ tracking: { // Add new group
794
+ startedAt: new Date().toISOString()
795
+ }
796
+ }
797
+ }
798
+
799
+ taskAPI.updateTask(taskId, updateMetas).then((updateResponse: any) => {
800
+ taskAPI.validateSuccessResponse(updateResponse, 200)
801
+ expect(updateResponse.body.data.metas.uiPreferences.colorLabel).to.eq('green')
802
+ expect(updateResponse.body.data.metas.uiPreferences.newField).to.eq('added')
803
+ expect(updateResponse.body.data.metas).to.have.property('tracking')
804
+ cy.log('3. Updated metas with merge behavior')
805
+
806
+ // 4. DELETE
807
+ taskAPI.deleteTask(taskId).then((deleteResponse: any) => {
808
+ taskAPI.validateSuccessResponse(deleteResponse, 200)
809
+ cy.log('4. Deleted task with metas')
810
+
811
+ // 5. VERIFY DELETION
812
+ taskAPI.getTaskById(taskId, { metas: 'all' }).then((finalResponse: any) => {
813
+ expect(finalResponse.status).to.eq(404)
814
+ cy.log('5. Verified deletion (404)')
815
+ cy.log('Full lifecycle with metas completed successfully')
816
+ })
817
+ })
818
+ })
819
+ })
820
+ })
821
+ })
822
+
823
+ it('TASKS_META_101: Should verify accumulative merge works across multiple updates', () => {
824
+ const taskTitle = 'Accumulative Merge Test'
825
+ const taskData = taskAPI.generateRandomTaskData({
826
+ title: taskTitle
827
+ })
828
+
829
+ // Create task without metas
830
+ taskAPI.createTask(taskData).then((createResponse: any) => {
831
+ const taskId = createResponse.body.data.id
832
+ createdTasks.push(createResponse.body.data)
833
+
834
+ // Update 1: Add first meta group (include title to satisfy requirement)
835
+ taskAPI.updateTask(taskId, {
836
+ title: taskTitle,
837
+ metas: {
838
+ uiPreferences: { theme: 'dark' }
839
+ }
840
+ }).then((update1: any) => {
841
+ expect(update1.body.data.metas.uiPreferences.theme).to.eq('dark')
842
+
843
+ // Update 2: Add second meta group
844
+ taskAPI.updateTask(taskId, {
845
+ title: taskTitle,
846
+ metas: {
847
+ notifications: { email: true }
848
+ }
849
+ }).then((update2: any) => {
850
+ expect(update2.body.data.metas).to.have.property('notifications')
851
+
852
+ // Update 3: Add key to first group
853
+ taskAPI.updateTask(taskId, {
854
+ title: taskTitle,
855
+ metas: {
856
+ uiPreferences: { sidebar: true }
857
+ }
858
+ }).then((update3: any) => {
859
+ // Verify all accumulated metas
860
+ taskAPI.getTaskById(taskId, { metas: 'all' }).then((finalGet: any) => {
861
+ const metas = finalGet.body.data.metas
862
+
863
+ // First group should have both keys
864
+ expect(metas.uiPreferences).to.have.property('theme')
865
+ expect(metas.uiPreferences).to.have.property('sidebar')
866
+
867
+ // Second group should still exist
868
+ expect(metas).to.have.property('notifications')
869
+
870
+ cy.log('Verified: Accumulative merge preserved all metas across updates')
871
+ })
872
+ })
873
+ })
874
+ })
875
+ })
876
+ })
877
+ })
878
+ })