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