app_test.go (23730B)
1 package web 2 3 import ( 4 "encoding/json" 5 "net/http" 6 "net/http/httptest" 7 "net/url" 8 "strings" 9 "testing" 10 "time" 11 12 "git.taler.net/taler-merchant-demos/internal/backend" 13 ) 14 15 func TestLandingRoutesAndEmbeddedStatic(t *testing.T) { 16 app, err := New(Options{Shop: "landing", Currency: "KUDOS"}) 17 if err != nil { 18 t.Fatal(err) 19 } 20 request := httptest.NewRequest(http.MethodGet, "/", nil) 21 request.Header.Set("Accept-Language", "de-DE,de;q=0.9,en;q=0.5") 22 response := httptest.NewRecorder() 23 app.ServeHTTP(response, request) 24 if response.Code != http.StatusFound || response.Header().Get("Location") != "/de/" { 25 t.Fatalf("root response = %d, location %q", response.Code, response.Header().Get("Location")) 26 } 27 request = httptest.NewRequest(http.MethodGet, "/", nil) 28 request.Header.Set("Accept-Language", "fr;q=0,en;q=1") 29 response = httptest.NewRecorder() 30 app.ServeHTTP(response, request) 31 if response.Header().Get("Location") != "/en/" { 32 t.Fatalf("quality-weighted location = %q", response.Header().Get("Location")) 33 } 34 response = httptest.NewRecorder() 35 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/en", nil)) 36 if response.Code != http.StatusPermanentRedirect || response.Header().Get("Location") != "/en/" { 37 t.Fatalf("canonical language redirect = %d, %q", response.Code, response.Header().Get("Location")) 38 } 39 40 response = httptest.NewRecorder() 41 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/en/", nil)) 42 body := response.Body.String() 43 if response.Code != http.StatusOK || !strings.Contains(body, "Step 1: Install the wallet") { 44 t.Fatalf("landing response = %d, %q", response.Code, response.Body.String()) 45 } 46 for _, expected := range []string{ 47 `class="site-header"`, `class="heading-lockup"`, `class="demo-nav"`, `class="step-list"`, 48 `taler-logo-light.svg`, `taler-logo-dark.svg`, `aria-current="page"`, `class="link-icon"`, 49 } { 50 if !strings.Contains(body, expected) { 51 t.Errorf("landing response missing branded shell marker %q", expected) 52 } 53 } 54 if strings.Contains(body, "pure.css") || strings.Contains(body, "style=") { 55 t.Errorf("landing response still includes legacy presentation markup") 56 } 57 if response.Header().Get("Cache-Control") != "private, no-store" { 58 t.Fatalf("dynamic cache policy = %q", response.Header().Get("Cache-Control")) 59 } 60 61 response = httptest.NewRecorder() 62 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/static/demo.css", nil)) 63 if response.Code != http.StatusOK || response.Header().Get("Content-Type") != "text/css; charset=utf-8" { 64 t.Fatalf("static response = %d, type %q", response.Code, response.Header().Get("Content-Type")) 65 } 66 if strings.Contains(response.Body.String(), "url(/static/") { 67 t.Fatalf("static stylesheet bypasses the forwarded prefix: %q", response.Body.String()) 68 } 69 response = httptest.NewRecorder() 70 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/static/blog.css", nil)) 71 if response.Code != http.StatusOK || strings.Contains(response.Body.String(), "url(/static/") { 72 t.Fatalf("blog stylesheet response = %d, body %q", response.Code, response.Body.String()) 73 } 74 for _, asset := range []string{"/static/taler-logo-light.svg", "/static/taler-logo-dark.svg"} { 75 response = httptest.NewRecorder() 76 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, asset, nil)) 77 if response.Code != http.StatusOK || response.Header().Get("Content-Type") != "image/svg+xml" { 78 t.Errorf("logo response for %s = %d, type %q", asset, response.Code, response.Header().Get("Content-Type")) 79 } 80 } 81 82 response = httptest.NewRecorder() 83 app.ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/en/", nil)) 84 if response.Code != http.StatusMethodNotAllowed { 85 t.Fatalf("POST language index response = %d", response.Code) 86 } 87 } 88 89 func TestPagesUseConfiguredPublicURLs(t *testing.T) { 90 app, err := New(Options{ 91 Shop: "landing", Currency: "KUDOS", 92 PublicURLs: PublicURLs{ 93 Landing: "https://landing.example/", Bank: "https://bank.example/", 94 Blog: "https://blog.example/", Donations: "https://donations.example/", 95 }, 96 }) 97 if err != nil { 98 t.Fatal(err) 99 } 100 response := httptest.NewRecorder() 101 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/de/", nil)) 102 if response.Code != http.StatusOK { 103 t.Fatalf("response status = %d", response.Code) 104 } 105 body := response.Body.String() 106 for _, expected := range []string{ 107 `href="https://landing.example/de/"`, 108 `href="https://bank.example/?lang=de"`, 109 `href="https://bank.example"`, 110 `href="https://blog.example/de/"`, 111 `href="https://donations.example/de/"`, 112 } { 113 if !strings.Contains(body, expected) { 114 t.Errorf("response missing configured public URL %q", expected) 115 } 116 } 117 } 118 119 func TestDemoPagesUseDistinctBrandThemes(t *testing.T) { 120 merchant, err := backend.New("https://merchant.example/", "secret-token:test") 121 if err != nil { 122 t.Fatal(err) 123 } 124 tests := []struct { 125 shop string 126 stylesheet string 127 component string 128 }{ 129 {shop: "landing", component: `class="step-card"`}, 130 {shop: "blog", stylesheet: "colors-blog.css", component: `class="article-card"`}, 131 {shop: "donations", stylesheet: "colors-donations.css", component: `class="demo-form form-card"`}, 132 } 133 for _, test := range tests { 134 t.Run(test.shop, func(t *testing.T) { 135 options := Options{Shop: test.shop, Currency: "KUDOS"} 136 switch test.shop { 137 case "blog": 138 options.BlogBackend = merchant 139 case "donations": 140 options.DonationBackends = map[string]*backend.Client{"gnunet": merchant, "taler": merchant, "tor": merchant} 141 } 142 app, err := New(options) 143 if err != nil { 144 t.Fatal(err) 145 } 146 response := httptest.NewRecorder() 147 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/en/", nil)) 148 body := response.Body.String() 149 if response.Code != http.StatusOK { 150 t.Fatalf("response status = %d", response.Code) 151 } 152 if !strings.Contains(body, test.component) { 153 t.Errorf("response missing themed component %q", test.component) 154 } 155 if strings.Contains(body, `>Bank<svg class="link-icon"`) { 156 t.Error("response contains external-link icon on Bank navigation item") 157 } 158 if test.stylesheet != "" && !strings.Contains(body, test.stylesheet) { 159 t.Errorf("response missing theme stylesheet %q", test.stylesheet) 160 } 161 if test.shop == "blog" { 162 for _, marker := range []string{`id="edition-info-button"`, `<dialog class="fsfs-license"`, `KUDOS:0.5`} { 163 if !strings.Contains(body, marker) { 164 t.Errorf("blog response missing %q", marker) 165 } 166 } 167 if strings.Contains(body, `<h2>Chapters</h2>`) { 168 t.Error("blog response contains redundant chapters heading") 169 } 170 } 171 }) 172 } 173 } 174 175 func TestDonationCheckoutUsesBrandedPaymentCards(t *testing.T) { 176 merchant, err := backend.New("https://merchant.example/", "secret-token:test") 177 if err != nil { 178 t.Fatal(err) 179 } 180 app, err := New(Options{ 181 Shop: "donations", Currency: "KUDOS", 182 DonationBackends: map[string]*backend.Client{"gnunet": merchant, "taler": merchant, "tor": merchant}, 183 }) 184 if err != nil { 185 t.Fatal(err) 186 } 187 response := httptest.NewRecorder() 188 request := httptest.NewRequest(http.MethodGet, "/en/checkout?donation_receiver=taler&donation_amount=KUDOS%3A1&donation_donor=Alice", nil) 189 app.ServeHTTP(response, request) 190 body := response.Body.String() 191 if response.Code != http.StatusOK { 192 t.Fatalf("checkout response = %d, body %q", response.Code, body) 193 } 194 if strings.Count(body, `class="payment-option"`) != 4 || !strings.Contains(body, `class="checkout-card"`) { 195 t.Errorf("checkout response is missing branded payment cards: %q", body) 196 } 197 if !strings.Contains(body, `value="taler" checked`) { 198 t.Errorf("checkout response no longer defaults to Taler") 199 } 200 201 response = httptest.NewRecorder() 202 request = httptest.NewRequest(http.MethodGet, "/en/donate?donation_receiver=taler&donation_amount=KUDOS%3A1&donation_donor=Alice&payment_system=lisa", nil) 203 app.ServeHTTP(response, request) 204 wantLocation := "/en/provider-not-supported?donation_amount=KUDOS%3A1&donation_donor=Alice&donation_receiver=taler" 205 if response.Code != http.StatusFound || response.Header().Get("Location") != wantLocation { 206 t.Fatalf("unsupported provider response = %d, location %q", response.Code, response.Header().Get("Location")) 207 } 208 209 response = httptest.NewRecorder() 210 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, wantLocation, nil)) 211 body = response.Body.String() 212 if response.Code != http.StatusOK || !strings.Contains(body, "Back to payment methods") || !strings.Contains(body, `href="/en/checkout?donation_amount=KUDOS%3A1&donation_donor=Alice&donation_receiver=taler"`) { 213 t.Fatalf("unsupported provider return action missing: status %d, body %q", response.Code, body) 214 } 215 216 response = httptest.NewRecorder() 217 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/en/provider-not-supported", nil)) 218 body = response.Body.String() 219 if response.Code != http.StatusOK || !strings.Contains(body, "Back to donations") || !strings.Contains(body, `href="/en/"`) { 220 t.Fatalf("direct unsupported provider fallback missing: status %d, body %q", response.Code, body) 221 } 222 } 223 224 func TestUnsupportedLocalesAreNotNegotiatedOrRouted(t *testing.T) { 225 app, err := New(Options{Shop: "landing", Currency: "KUDOS"}) 226 if err != nil { 227 t.Fatal(err) 228 } 229 request := httptest.NewRequest(http.MethodGet, "/", nil) 230 request.Header.Set("Accept-Language", "ar,zh;q=0.9") 231 response := httptest.NewRecorder() 232 app.ServeHTTP(response, request) 233 if response.Header().Get("Location") != "/en/" { 234 t.Fatalf("unsupported language negotiation location = %q", response.Header().Get("Location")) 235 } 236 237 response = httptest.NewRecorder() 238 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/ar/", nil)) 239 if response.Code != http.StatusNotFound { 240 t.Fatalf("unsupported locale response = %d", response.Code) 241 } 242 } 243 244 func TestErrorsKeepLocalizedSummary(t *testing.T) { 245 merchant := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 246 w.WriteHeader(http.StatusInternalServerError) 247 _, _ = w.Write([]byte(`{"hint":"technical backend detail","code":42}`)) 248 })) 249 defer merchant.Close() 250 client, err := backend.New(merchant.URL, "secret-token:test") 251 if err != nil { 252 t.Fatal(err) 253 } 254 app, err := New(Options{ 255 Shop: "donations", Currency: "KUDOS", 256 DonationBackends: map[string]*backend.Client{"gnunet": client, "taler": client, "tor": client}, 257 }) 258 if err != nil { 259 t.Fatal(err) 260 } 261 response := httptest.NewRecorder() 262 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/de/donation/taler?order_id=order-1", nil)) 263 body := response.Body.String() 264 if response.Code != http.StatusBadGateway || !strings.Contains(body, "<p>Backend-Anfrage fehlgeschlagen</p>") { 265 t.Fatalf("localized backend error response = %d, body %q", response.Code, body) 266 } 267 if strings.Contains(body, "<p>technical backend detail</p>") { 268 t.Fatalf("technical backend detail replaced localized summary: %q", body) 269 } 270 if !strings.Contains(body, "<pre>technical backend detail</pre>") { 271 t.Fatalf("technical backend detail was not rendered: %q", body) 272 } 273 274 response = httptest.NewRecorder() 275 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/de/checkout", nil)) 276 if response.Code != http.StatusBadRequest || !strings.Contains(response.Body.String(), "Der Parameter donation_receiver ist erforderlich.") { 277 t.Fatalf("localized missing parameter response = %d, body %q", response.Code, response.Body.String()) 278 } 279 } 280 281 func TestBlogStartsCookieCheckBeforeBackend(t *testing.T) { 282 merchant := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { 283 t.Fatal("backend called before cookie check completed") 284 })) 285 defer merchant.Close() 286 client, err := backend.New(merchant.URL, "secret-token:test") 287 if err != nil { 288 t.Fatal(err) 289 } 290 app, err := New(Options{Shop: "blog", Currency: "KUDOS", BlogBackend: client}) 291 if err != nil { 292 t.Fatal(err) 293 } 294 articles := sortedArticles(app.articles["en"]) 295 if len(articles) == 0 { 296 t.Fatal("no English articles loaded") 297 } 298 request := httptest.NewRequest(http.MethodGet, "/en/essay/"+articles[0].Slug, nil) 299 response := httptest.NewRecorder() 300 app.ServeHTTP(response, request) 301 if response.Code != http.StatusFound || !strings.Contains(response.Header().Get("Location"), "expect_state=yes") { 302 t.Fatalf("article response = %d, location %q", response.Code, response.Header().Get("Location")) 303 } 304 if !strings.Contains(response.Header().Get("Set-Cookie"), sessionCookieName+"=") { 305 t.Fatalf("session cookie missing: %q", response.Header().Get("Set-Cookie")) 306 } 307 } 308 309 func TestBlogCreatesCurrentMerchantOrder(t *testing.T) { 310 var posted backend.PostOrderRequest 311 merchant := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 312 switch { 313 case r.Method == http.MethodGet && r.URL.Path == "/private/orders": 314 _ = json.NewEncoder(w).Encode(map[string]any{"orders": []any{}}) 315 case r.Method == http.MethodPost && r.URL.Path == "/private/orders": 316 _ = json.NewDecoder(r.Body).Decode(&posted) 317 _ = json.NewEncoder(w).Encode(map[string]any{"order_id": "order-1", "token": "claim-1"}) 318 case r.Method == http.MethodGet && r.URL.Path == "/private/orders/order-1": 319 _ = json.NewEncoder(w).Encode(map[string]any{ 320 "order_status": "paid", "refunded": false, 321 "contract_terms": map[string]any{ 322 "extra": map[string]any{"article_name": r.Header.Get("X-Test-Article")}, 323 "refund_deadline": map[string]any{"t_s": time.Now().Add(time.Minute).Unix()}, 324 }, 325 }) 326 default: 327 http.Error(w, "unexpected request", http.StatusNotFound) 328 } 329 })) 330 defer merchant.Close() 331 client, err := backend.New(merchant.URL, "secret-token:test") 332 if err != nil { 333 t.Fatal(err) 334 } 335 app, err := New(Options{Shop: "blog", Currency: "KUDOS", BlogBackend: client, EnableTokens: true}) 336 if err != nil { 337 t.Fatal(err) 338 } 339 article := sortedArticles(app.articles["de"])[0] 340 articlePath := "/de/essay/" + article.Slug 341 request := httptest.NewRequest(http.MethodGet, articlePath, nil) 342 request.AddCookie(&http.Cookie{Name: sessionCookieName, Value: "browser-session"}) 343 response := httptest.NewRecorder() 344 app.ServeHTTP(response, request) 345 if response.Code != http.StatusFound || !strings.Contains(response.Header().Get("Location"), "/orders/order-1?") { 346 t.Fatalf("purchase response = %d, location %q", response.Code, response.Header().Get("Location")) 347 } 348 if posted.SessionID != "browser-session" || !posted.CreateToken { 349 t.Fatalf("post-order envelope = %#v", posted) 350 } 351 if posted.Order.Version != 1 { 352 t.Fatalf("order version = %d", posted.Order.Version) 353 } 354 if len(posted.Order.Choices) != 3 || posted.Order.Choices[0].Description != "Einen einzelnen Artikel kaufen" || posted.Order.Choices[1].Description != "Einen Monat unbegrenzten Zugriff kaufen" { 355 t.Fatalf("localized order choices = %#v", posted.Order.Choices) 356 } 357 if posted.RefundDelay.Microseconds != time.Hour.Microseconds() { 358 t.Fatalf("refund delay = %d microseconds", posted.RefundDelay.Microseconds) 359 } 360 if posted.Order.WireTransferDeadline.Seconds <= posted.Order.PayDeadline.Seconds+int64(time.Hour/time.Second) { 361 t.Fatalf("wire deadline does not allow the advertised refund window: %#v", posted.Order) 362 } 363 } 364 365 func TestDonationReceiptOrderAndFulfillment(t *testing.T) { 366 var posted backend.PostOrderRequest 367 merchant := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 368 switch r.Method { 369 case http.MethodPost: 370 _ = json.NewDecoder(r.Body).Decode(&posted) 371 _ = json.NewEncoder(w).Encode(map[string]any{"order_id": "donation-1"}) 372 case http.MethodGet: 373 _ = json.NewEncoder(w).Encode(map[string]any{ 374 "order_status": "paid", 375 "contract_terms": map[string]any{"extra": map[string]any{ 376 "receiver": "taler", "amount": "KUDOS:1", "donor": "<script>alert(1)</script>", 377 }}, 378 }) 379 } 380 })) 381 defer merchant.Close() 382 client, err := backend.New(merchant.URL, "secret-token:test") 383 if err != nil { 384 t.Fatal(err) 385 } 386 app, err := New(Options{ 387 Shop: "donations", Currency: "KUDOS", DonauURL: "https://donau.example/", 388 DonationBackends: map[string]*backend.Client{"gnunet": client, "taler": client, "tor": client}, 389 }) 390 if err != nil { 391 t.Fatal(err) 392 } 393 request := httptest.NewRequest(http.MethodGet, "http://demo.example/en/donate?donation_receiver=taler&donation_amount=KUDOS%3A1&donation_donor=Alice&payment_system=taler", nil) 394 request.Header.Set("X-Forwarded-Prefix", "/merchant") 395 request.Header.Set("X-Forwarded-Proto", "https") 396 response := httptest.NewRecorder() 397 app.ServeHTTP(response, request) 398 if response.Code != http.StatusFound || response.Header().Get("Location") != "/merchant/en/donation/taler?order_id=donation-1" { 399 t.Fatalf("donation response = %d, location %q", response.Code, response.Header().Get("Location")) 400 } 401 if !strings.HasPrefix(posted.Order.FulfillmentURL, "https://demo.example/merchant/en/donation/taler?") { 402 t.Fatalf("fulfillment URL = %q", posted.Order.FulfillmentURL) 403 } 404 405 request = httptest.NewRequest(http.MethodGet, "http://demo.example/de/donate?donation_receiver=taler&donation_amount=KUDOS%3A1&donation_donor=Alice&payment_system=taler", nil) 406 response = httptest.NewRecorder() 407 app.ServeHTTP(response, request) 408 if response.Code != http.StatusFound || posted.Order.Summary != "Spende an taler (mit Spendenquittung)" { 409 t.Fatalf("localized donation order response = %d, order = %#v", response.Code, posted.Order) 410 } 411 output := posted.Order.Choices[0].Outputs[0] 412 if output.Type != "tax-receipt" || output.TokenFamilySlug != "" || output.Amount != "" { 413 t.Fatalf("tax receipt output = %#v", output) 414 } 415 416 request = httptest.NewRequest(http.MethodGet, "http://demo.example/en/donation/taler?order_id=donation-1", nil) 417 response = httptest.NewRecorder() 418 app.ServeHTTP(response, request) 419 body := response.Body.String() 420 if response.Code != http.StatusOK || strings.Contains(body, "<script>alert(1)</script>") || !strings.Contains(body, "<script>alert(1)</script>") { 421 t.Fatalf("fulfillment response = %d, body %q", response.Code, response.Body.String()) 422 } 423 for _, marker := range []string{`data-copy-value="http://demo.example/en/donation/taler?order_id=donation-1"`, "Copy receipt link", "Donate again", `role="status" hidden`} { 424 if !strings.Contains(body, marker) { 425 t.Errorf("fulfillment response missing receipt action %q", marker) 426 } 427 } 428 } 429 430 func TestArticleReferencesBundledSupplementalData(t *testing.T) { 431 var articleName string 432 merchant := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 433 if r.Method != http.MethodGet { 434 http.Error(w, "unexpected request", http.StatusMethodNotAllowed) 435 return 436 } 437 _ = json.NewEncoder(w).Encode(backend.OrderStatusResponse{ 438 OrderStatus: "paid", 439 ContractTerms: backend.ContractTerms{ 440 Extra: backend.OrderExtra{ArticleName: articleName}, 441 RefundDeadline: backend.Timestamp{Seconds: time.Now().Add(time.Minute).Unix()}, 442 }, 443 }) 444 })) 445 defer merchant.Close() 446 client, err := backend.New(merchant.URL, "secret-token:test") 447 if err != nil { 448 t.Fatal(err) 449 } 450 app, err := New(Options{Shop: "blog", Currency: "KUDOS", BlogBackend: client}) 451 if err != nil { 452 t.Fatal(err) 453 } 454 var article Article 455 for _, candidate := range app.articles["en"] { 456 if candidate.ExtraFiles["category.png"] != "" { 457 article = candidate 458 break 459 } 460 } 461 if article.Slug == "" { 462 t.Fatal("no English article references category.png") 463 } 464 articleName = article.Slug 465 articlePath := "/en/essay/" + url.PathEscape(article.Slug) 466 request := httptest.NewRequest(http.MethodGet, articlePath, nil) 467 request.AddCookie(&http.Cookie{Name: sessionCookieName, Value: "browser-session"}) 468 request.AddCookie(&http.Cookie{Name: orderCookieName, Value: "paid-order"}) 469 response := httptest.NewRecorder() 470 app.ServeHTTP(response, request) 471 if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), url.PathEscape(article.Slug)+"/data/category.png") { 472 t.Fatalf("article response = %d, supplemental reference missing", response.Code) 473 } 474 475 response = httptest.NewRecorder() 476 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, articlePath+"/data/category.png", nil)) 477 if response.Code != http.StatusOK || response.Header().Get("Content-Type") != "image/png" || !strings.HasPrefix(response.Body.String(), "\x89PNG") { 478 t.Fatalf("supplemental response = %d, type %q", response.Code, response.Header().Get("Content-Type")) 479 } 480 481 response = httptest.NewRecorder() 482 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, articlePath+"/data/unused.jpg", nil)) 483 if response.Code != http.StatusNotFound { 484 t.Fatalf("unreferenced supplemental response = %d", response.Code) 485 } 486 } 487 488 func TestBlogRefundUsesMethodAwareRoute(t *testing.T) { 489 var refund backend.RefundRequest 490 choiceIndex := 0 491 merchant := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 492 switch { 493 case r.Method == http.MethodGet && r.URL.Path == "/private/orders/order-1": 494 _ = json.NewEncoder(w).Encode(backend.OrderStatusResponse{ 495 OrderStatus: "paid", 496 OrderStatusURL: "https://merchant.example/orders/order-1", 497 ChoiceIndex: &choiceIndex, 498 ContractTerms: backend.ContractTerms{ 499 Version: 1, 500 Choices: []backend.OrderChoice{{Amount: "KUDOS:0.5"}, {Amount: "KUDOS:10"}, {Amount: "KUDOS:0"}}, 501 RefundDeadline: backend.Timestamp{Seconds: time.Now().Add(time.Minute).Unix()}, 502 }, 503 }) 504 case r.Method == http.MethodPost && r.URL.Path == "/private/orders/order-1/refund": 505 _ = json.NewDecoder(r.Body).Decode(&refund) 506 _ = json.NewEncoder(w).Encode(map[string]any{}) 507 default: 508 http.Error(w, "unexpected request", http.StatusNotFound) 509 } 510 })) 511 defer merchant.Close() 512 client, err := backend.New(merchant.URL, "secret-token:test") 513 if err != nil { 514 t.Fatal(err) 515 } 516 app, err := New(Options{Shop: "blog", Currency: "KUDOS", BlogBackend: client}) 517 if err != nil { 518 t.Fatal(err) 519 } 520 request := httptest.NewRequest(http.MethodPost, "/de/refund/order-1", nil) 521 request.AddCookie(&http.Cookie{Name: sessionCookieName, Value: "browser-session"}) 522 response := httptest.NewRecorder() 523 app.ServeHTTP(response, request) 524 if response.Code != http.StatusFound || response.Header().Get("Location") != "https://merchant.example/orders/order-1" { 525 t.Fatalf("refund response = %d, location %q", response.Code, response.Header().Get("Location")) 526 } 527 if refund.Refund != "KUDOS:0.5" || refund.Reason != "Rückerstattung in der Demo" { 528 t.Fatalf("refund request = %#v", refund) 529 } 530 531 response = httptest.NewRecorder() 532 app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/de/refund/order-1", nil)) 533 if response.Code != http.StatusMethodNotAllowed { 534 t.Fatalf("GET refund response = %d", response.Code) 535 } 536 } 537 538 func TestRefundUsesSelectedOrderChoice(t *testing.T) { 539 app := &App{opts: Options{Currency: "KUDOS"}} 540 deadline := backend.Timestamp{Seconds: time.Now().Add(time.Minute).Unix()} 541 choices := []backend.OrderChoice{{Amount: "KUDOS:0.5"}, {Amount: "KUDOS:10"}, {Amount: "KUDOS:0"}} 542 for index, want := range []string{"KUDOS:0.5", "KUDOS:10", ""} { 543 choiceIndex := index 544 got, ok := app.refundAmount(backend.OrderStatusResponse{ 545 ChoiceIndex: &choiceIndex, 546 ContractTerms: backend.ContractTerms{ 547 Version: 1, Choices: choices, RefundDeadline: deadline, 548 }, 549 }) 550 if got != want || ok != (want != "") { 551 t.Errorf("choice %d refund = %q, %v; want %q", index, got, ok, want) 552 } 553 } 554 invalid := len(choices) 555 if amount, ok := app.refundAmount(backend.OrderStatusResponse{ 556 ChoiceIndex: &invalid, 557 ContractTerms: backend.ContractTerms{ 558 Version: 1, Choices: choices, RefundDeadline: deadline, 559 }, 560 }); ok || amount != "" { 561 t.Fatalf("invalid choice refund = %q, %v", amount, ok) 562 } 563 }