@@ -0,0 +1,260 @@
1+module Test.CallbackTest where
2+
3+import Prelude
4+
5+import Data.Maybe (Maybe(..))
6+import Data.String as String
7+import Effect (Effect)
8+import Type.Proxy (Proxy(..))
9+import Yoga.HTTP.API.Path (Path, Lit)
10+import Yoga.HTTP.API.Route (GET, POST, Route, Request, buildOpenAPISpec, toOpenAPI)
11+import Yoga.HTTP.API.Route.Encoding (JSON)
12+import Yoga.HTTP.API.Route.OpenAPIMetadata (Callback, type (#))
13+import Yoga.JSON (writeJSON)
14+import ViTest (ViTest, describe, test)
15+import ViTest.Expect (expectToBe)
16+
17+--------------------------------------------------------------------------------
18+-- Type Definitions for Callback Tests
19+--------------------------------------------------------------------------------
20+
21+-- Payment processing with callback for completion notification
22+type PaymentRequest = { amount :: Int, currency :: String, callbackUrl :: String }
23+type PaymentResponse = { transactionId :: String, status :: String }
24+type PaymentCallbackRequest = { status :: String, transactionId :: String }
25+type PaymentCallbackResponse = { received :: Boolean }
26+
27+type PaymentRoute =
28+ Route POST (Path (Lit "payment"))
29+ (Request { body :: JSON PaymentRequest })
30+ ( ok :: { body :: PaymentResponse }
31+ ) # Callback "onPaymentComplete" "{$request.body#/callbackUrl}" POST
32+ (JSON PaymentCallbackRequest)
33+ (ok :: { body :: PaymentCallbackResponse })
34+
35+-- Webhook subscription with multiple callbacks
36+type WebhookSubscription = { url :: String, events :: Array String }
37+type WebhookEvent = { event :: String, data :: String, timestamp :: String }
38+type WebhookAck = { acknowledged :: Boolean }
39+
40+type WebhookRoute =
41+ Route POST (Path (Lit "webhooks"))
42+ (Request { body :: JSON WebhookSubscription })
43+ ( created :: { body :: { id :: String } }
44+ )
45+ # Callback "onUserCreated" "{$request.body#/url}" POST
46+ (JSON WebhookEvent)
47+ ( ok :: { body :: WebhookAck }
48+ , badRequest :: { body :: { error :: String } }
49+ )
50+ # Callback "onUserUpdated" "{$request.body#/url}" POST
51+ (JSON WebhookEvent)
52+ (ok :: { body :: WebhookAck })
53+
54+-- Async job processing with status callback
55+type JobRequest = { task :: String, statusUrl :: String }
56+type JobResponse = { jobId :: String }
57+type JobStatusUpdate = { jobId :: String, status :: String, progress :: Int }
58+type StatusAck = { received :: Boolean }
59+
60+type AsyncJobRoute =
61+ Route POST (Path (Lit "jobs"))
62+ (Request { body :: JSON JobRequest })
63+ ( accepted :: { body :: JobResponse }
64+ ) # Callback "onStatusUpdate" "{$request.body#/statusUrl}" POST
65+ (JSON JobStatusUpdate)
66+ (ok :: { body :: StatusAck })
67+
68+-- Notification service with GET callback
69+type NotificationRequest = { message :: String, confirmUrl :: String }
70+type NotificationResponse = { notificationId :: String }
71+
72+type NotificationRoute =
73+ Route POST (Path (Lit "notifications"))
74+ (Request { body :: JSON NotificationRequest })
75+ ( created :: { body :: NotificationResponse }
76+ ) # Callback "onDeliveryConfirm" "{$request.body#/confirmUrl}" GET
77+ (JSON { delivered :: Boolean })
78+ (ok :: { body :: { confirmed :: Boolean } })
79+
80+--------------------------------------------------------------------------------
81+-- Basic Callback Tests
82+--------------------------------------------------------------------------------
83+
84+testPaymentCallback :: Effect ViTest
85+testPaymentCallback = describe "OpenAPI Callbacks - Payment Processing" do
86+ _ <- test "includes callbacks in operation" do
87+ let
88+ result = toOpenAPI @PaymentRoute
89+ expectToBe true (String.contains (String.Pattern "callbacks") result)
90+
91+ _ <- test "includes callback name onPaymentComplete" do
92+ let
93+ result = toOpenAPI @PaymentRoute
94+ expectToBe true (String.contains (String.Pattern "onPaymentComplete") result)
95+
96+ _ <- test "includes callback expression with runtime reference" do
97+ let
98+ result = toOpenAPI @PaymentRoute
99+ expectToBe true (String.contains (String.Pattern "{$request.body#/callbackUrl}") result)
100+
101+ _ <- test "includes callback method POST" do
102+ let
103+ result = toOpenAPI @PaymentRoute
104+ expectToBe true (String.contains (String.Pattern "\"post\"") result)
105+
106+ _ <- test "includes callback request body schema" do
107+ let
108+ result = toOpenAPI @PaymentRoute
109+ expectToBe true (String.contains (String.Pattern "status") result)
110+ expectToBe true (String.contains (String.Pattern "transactionId") result)
111+
112+ test "includes callback response schema" do
113+ let
114+ result = toOpenAPI @PaymentRoute
115+ expectToBe true (String.contains (String.Pattern "received") result)
116+
117+--------------------------------------------------------------------------------
118+-- Multiple Callbacks Tests
119+--------------------------------------------------------------------------------
120+
121+testMultipleCallbacks :: Effect ViTest
122+testMultipleCallbacks = describe "OpenAPI Callbacks - Multiple Webhooks" do
123+ _ <- test "includes multiple callback definitions" do
124+ let
125+ result = toOpenAPI @WebhookRoute
126+ expectToBe true (String.contains (String.Pattern "onUserCreated") result)
127+ expectToBe true (String.contains (String.Pattern "onUserUpdated") result)
128+
129+ _ <- test "each callback has its own expression" do
130+ let
131+ result = toOpenAPI @WebhookRoute
132+ expectToBe true (String.contains (String.Pattern "{$request.body#/url}") result)
133+
134+ test "callbacks with multiple response types" do
135+ let
136+ result = toOpenAPI @WebhookRoute
137+ expectToBe true (String.contains (String.Pattern "\"200\"") result)
138+ expectToBe true (String.contains (String.Pattern "\"400\"") result)
139+
140+--------------------------------------------------------------------------------
141+-- Different HTTP Methods Tests
142+--------------------------------------------------------------------------------
143+
144+testCallbackMethods :: Effect ViTest
145+testCallbackMethods = describe "OpenAPI Callbacks - HTTP Methods" do
146+ _ <- test "supports POST callback method" do
147+ let
148+ result = toOpenAPI @AsyncJobRoute
149+ expectToBe true (String.contains (String.Pattern "onStatusUpdate") result)
150+ expectToBe true (String.contains (String.Pattern "\"post\"") result)
151+
152+ test "supports GET callback method" do
153+ let
154+ result = toOpenAPI @NotificationRoute
155+ expectToBe true (String.contains (String.Pattern "onDeliveryConfirm") result)
156+ expectToBe true (String.contains (String.Pattern "\"get\"") result)
157+
158+--------------------------------------------------------------------------------
159+-- API Spec with Callbacks
160+--------------------------------------------------------------------------------
161+
162+type CallbackAPI =
163+ { payment :: PaymentRoute
164+ , webhook :: WebhookRoute
165+ , job :: AsyncJobRoute
166+ , notification :: NotificationRoute
167+ }
168+
169+testAPISpecWithCallbacks :: Effect ViTest
170+testAPISpecWithCallbacks = describe "OpenAPI Spec - API with Callbacks" do
171+ _ <- test "builds complete spec with callback operations" do
172+ let
173+ spec = buildOpenAPISpec @CallbackAPI
174+ { title: "Callback Test API"
175+ , version: "1.0.0"
176+ , description: Nothing
177+ , contact: Nothing
178+ , license: Nothing
179+ }
180+ json = writeJSON spec
181+ expectToBe true (String.contains (String.Pattern "callbacks") json)
182+
183+ _ <- test "includes all callback operations in spec" do
184+ let
185+ spec = buildOpenAPISpec @CallbackAPI
186+ { title: "Callback Test API"
187+ , version: "1.0.0"
188+ , description: Nothing
189+ , contact: Nothing
190+ , license: Nothing
191+ }
192+ json = writeJSON spec
193+ expectToBe true (String.contains (String.Pattern "onPaymentComplete") json)
194+ expectToBe true (String.contains (String.Pattern "onUserCreated") json)
195+ expectToBe true (String.contains (String.Pattern "onStatusUpdate") json)
196+ expectToBe true (String.contains (String.Pattern "onDeliveryConfirm") json)
197+
198+ test "callback expressions use runtime references" do
199+ let
200+ spec = buildOpenAPISpec @CallbackAPI
201+ { title: "Callback Test API"
202+ , version: "1.0.0"
203+ , description: Nothing
204+ , contact: Nothing
205+ , license: Nothing
206+ }
207+ json = writeJSON spec
208+ expectToBe true (String.contains (String.Pattern "{$request.body#/callbackUrl}") json)
209+ expectToBe true (String.contains (String.Pattern "{$request.body#/url}") json)
210+ expectToBe true (String.contains (String.Pattern "{$request.body#/statusUrl}") json)
211+ expectToBe true (String.contains (String.Pattern "{$request.body#/confirmUrl}") json)
212+
213+--------------------------------------------------------------------------------
214+-- Real-World Webhook Scenarios
215+--------------------------------------------------------------------------------
216+
217+-- GitHub-style webhook
218+type GitHubWebhookSetup = { url :: String, secret :: String }
219+type GitHubPushEvent = { ref :: String, commits :: Array { message :: String } }
220+
221+type GitHubWebhookRoute =
222+ Route POST (Path (Lit "github" /\ Lit "webhooks"))
223+ (Request { body :: JSON GitHubWebhookSetup })
224+ ( created :: { body :: { id :: Int } }
225+ ) # Callback "push" "{$request.body#/url}" POST
226+ (JSON GitHubPushEvent)
227+ (ok :: { body :: Unit })
228+
229+-- Stripe-style payment webhook
230+type StripeCheckout = { successUrl :: String, cancelUrl :: String }
231+type StripeEvent = { type :: String, data :: { amount :: Int } }
232+
233+type StripeWebhookRoute =
234+ Route POST (Path (Lit "stripe" /\ Lit "checkout"))
235+ (Request { body :: JSON StripeCheckout })
236+ ( ok :: { body :: { sessionId :: String } }
237+ )
238+ # Callback "checkout.completed" "{$request.body#/successUrl}" POST
239+ (JSON StripeEvent)
240+ (ok :: { body :: Unit })
241+ # Callback "checkout.canceled" "{$request.body#/cancelUrl}" POST
242+ (JSON StripeEvent)
243+ (ok :: { body :: Unit })
244+
245+testRealWorldWebhooks :: Effect ViTest
246+testRealWorldWebhooks = describe "OpenAPI Callbacks - Real-World Webhooks" do
247+ _ <- test "GitHub-style webhook with push callback" do
248+ let
249+ result = toOpenAPI @GitHubWebhookRoute
250+ expectToBe true (String.contains (String.Pattern "push") result)
251+ expectToBe true (String.contains (String.Pattern "ref") result)
252+ expectToBe true (String.contains (String.Pattern "commits") result)
253+
254+ test "Stripe-style webhook with success and cancel callbacks" do
255+ let
256+ result = toOpenAPI @StripeWebhookRoute
257+ expectToBe true (String.contains (String.Pattern "checkout.completed") result)
258+ expectToBe true (String.contains (String.Pattern "checkout.canceled") result)
259+ expectToBe true (String.contains (String.Pattern "{$request.body#/successUrl}") result)
260+ expectToBe true (String.contains (String.Pattern "{$request.body#/cancelUrl}") result)