@stardeck-customer-apps/testing 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -310,6 +310,7 @@ var TEST_ENV_DEFAULTS = {
310
310
  DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
311
311
  STORAGE_URL: STORAGE_TEST_URL
312
312
  };
313
+ var LIFF_ID_HEADER = "x-stardeck-liff-id";
313
314
  var DEFAULT_TEST_USER = {
314
315
  id: "test-user-1",
315
316
  email: "test-user@example.com",
@@ -324,6 +325,7 @@ var state = globalSingleton("state", () => ({
324
325
  db: null,
325
326
  lastHarnessDataStoreManifest: null,
326
327
  currentUser: null,
328
+ liffId: null,
327
329
  sessions: /* @__PURE__ */ new Map(),
328
330
  refreshSessions: /* @__PURE__ */ new Map(),
329
331
  emails: [],
@@ -348,6 +350,11 @@ var state = globalSingleton("state", () => ({
348
350
  boltIntentCounter: 0,
349
351
  boltCharges: [],
350
352
  boltChargeCounter: 0,
353
+ directCharges: /* @__PURE__ */ new Map(),
354
+ directChargeCounter: 0,
355
+ cardTokens: /* @__PURE__ */ new Map(),
356
+ cardTokenCounter: 0,
357
+ cardTokenizationEnabled: true,
351
358
  uploads: [],
352
359
  uploadCounter: 0,
353
360
  storageFiles: /* @__PURE__ */ new Map(),
@@ -1715,6 +1722,311 @@ function createDirectory() {
1715
1722
  };
1716
1723
  }
1717
1724
 
1725
+ // src/simulator/beam-direct-charges.ts
1726
+ var SIM_BEAM_HOST = "beam.stardeck.test";
1727
+ var SIM_BEAM_MERCHANT_ID = "sim_merchant";
1728
+ var SIM_BEAM_PUBLISHABLE_KEY = "pk_test_sim";
1729
+ var DIRECT_CHARGE_METHODS = /* @__PURE__ */ new Set([
1730
+ "CARD",
1731
+ "CARD_INSTALLMENTS",
1732
+ "CARD_TOKEN",
1733
+ "CARD_TOKEN_INSTALLMENTS",
1734
+ "QR_PROMPT_PAY",
1735
+ "ALIPAY",
1736
+ "ALIPAY_PLUS",
1737
+ "WECHAT_PAY",
1738
+ "TRUE_MONEY",
1739
+ "LINE_PAY",
1740
+ "SHOPEE_PAY",
1741
+ "KPLUS",
1742
+ "MAKE",
1743
+ "SCB_EASY",
1744
+ "KRUNGSRI_APP",
1745
+ "BANGKOK_BANK_APP",
1746
+ "SPAY_LATER"
1747
+ ]);
1748
+ var TEST_CARDS = {
1749
+ "4111111111111111": { outcome: "SUCCEEDED" },
1750
+ "5372074248113841": { outcome: "SUCCEEDED" },
1751
+ "378282246310005": { outcome: "SUCCEEDED" },
1752
+ "4953261730509988": { outcome: "REDIRECT" },
1753
+ "4389783280144506": { outcome: "REDIRECT" },
1754
+ "4943129900084541": { outcome: "FAILED", failureCode: "CH_INSUFFICIENT_FUNDS" },
1755
+ "4111111111000025": { outcome: "FAILED", failureCode: "CH_CARD_DECLINED" },
1756
+ "4111111111000033": { outcome: "FAILED", failureCode: "CH_CARD_RESTRICTED" }
1757
+ };
1758
+ var SIM_QR_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
1759
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1760
+ function invalid(issues) {
1761
+ return json({ error: "Invalid request body", details: issues }, 400);
1762
+ }
1763
+ var DEVICE_TYPES = /* @__PURE__ */ new Set(["ANDROID", "IOS", "WEB"]);
1764
+ function issue(path, message, code = "custom") {
1765
+ return { path, message, code };
1766
+ }
1767
+ function validateChargeFields(body) {
1768
+ const issues = [];
1769
+ const isInt = (v) => typeof v === "number" && Number.isInteger(v);
1770
+ if (!isInt(body.amount) || body.amount < 1) {
1771
+ issues.push(issue(["amount"], "Number must be greater than 0", "too_small"));
1772
+ }
1773
+ if (typeof body.deploymentId !== "string" || !UUID_RE.test(body.deploymentId)) {
1774
+ issues.push(issue(["deploymentId"], "Invalid deployment ID", "invalid_string"));
1775
+ }
1776
+ if (body.referenceId !== void 0 && (typeof body.referenceId !== "string" || body.referenceId.length > 100)) {
1777
+ issues.push(issue(["referenceId"], "Too long", "too_big"));
1778
+ }
1779
+ const expiry = body.expiryDurationInSec;
1780
+ if (expiry !== void 0 && (!isInt(expiry) || expiry < 60 || expiry > 3600)) {
1781
+ issues.push(issue(["expiryDurationInSec"], "Must be an integer from 60 to 3600", "too_big"));
1782
+ }
1783
+ if (body.returnUrl !== void 0 && (typeof body.returnUrl !== "string" || !URL.canParse(body.returnUrl))) {
1784
+ issues.push(issue(["returnUrl"], "Invalid URL", "invalid_format"));
1785
+ }
1786
+ if (body.deviceType !== void 0 && !DEVICE_TYPES.has(String(body.deviceType))) {
1787
+ issues.push(issue(["deviceType"], "Invalid enum value", "invalid_value"));
1788
+ }
1789
+ const key = body.idempotencyKey;
1790
+ if (key !== void 0 && (typeof key !== "string" || key.length < 1 || key.length > 255)) {
1791
+ issues.push(issue(["idempotencyKey"], "Must be 1 to 255 characters", "too_small"));
1792
+ }
1793
+ return issues;
1794
+ }
1795
+ function validateChargeMethod(body) {
1796
+ const method = body.paymentMethod;
1797
+ const type = method?.paymentMethodType;
1798
+ if (typeof type !== "string" || !DIRECT_CHARGE_METHODS.has(type)) {
1799
+ return [issue(["paymentMethod", "paymentMethodType"], "Invalid enum value", "invalid_value")];
1800
+ }
1801
+ const issues = [];
1802
+ const isToken = type === "CARD_TOKEN" || type === "CARD_TOKEN_INSTALLMENTS";
1803
+ const token = method?.cardToken;
1804
+ if (isToken && (typeof token?.cardTokenId !== "string" || token.cardTokenId === "")) {
1805
+ issues.push(
1806
+ issue(["paymentMethod", "cardToken"], `${type} requires paymentMethod.cardToken.cardTokenId`)
1807
+ );
1808
+ }
1809
+ if (token?.securityCode !== void 0 && !/^\d{3,4}$/.test(String(token.securityCode))) {
1810
+ issues.push(
1811
+ issue(
1812
+ ["paymentMethod", "cardToken", "securityCode"],
1813
+ "Invalid security code",
1814
+ "invalid_format"
1815
+ )
1816
+ );
1817
+ }
1818
+ if (type === "CARD_TOKEN_INSTALLMENTS" && ![3, 4, 6, 10].includes(method?.installmentPeriod)) {
1819
+ issues.push(
1820
+ issue(
1821
+ ["paymentMethod", "installmentPeriod"],
1822
+ "CARD_TOKEN_INSTALLMENTS requires paymentMethod.installmentPeriod (3, 4, 6 or 10)"
1823
+ )
1824
+ );
1825
+ }
1826
+ if (type !== "QR_PROMPT_PAY" && !body.returnUrl) {
1827
+ issues.push(
1828
+ issue(
1829
+ ["returnUrl"],
1830
+ `${type} sends the shopper away to pay; returnUrl is required to bring them back`
1831
+ )
1832
+ );
1833
+ }
1834
+ if (type === "CARD" || type === "CARD_INSTALLMENTS") {
1835
+ issues.push(
1836
+ issue(
1837
+ ["paymentMethod", "paymentMethodType"],
1838
+ `${type} takes raw card data; tokenize the card in the browser and charge CARD_TOKEN instead`
1839
+ )
1840
+ );
1841
+ }
1842
+ return issues;
1843
+ }
1844
+ function validateDirectCharge(body) {
1845
+ return [...validateChargeFields(body), ...validateChargeMethod(body)];
1846
+ }
1847
+ function chargeResponse(charge) {
1848
+ const response = {
1849
+ chargeId: charge.id,
1850
+ actionRequired: charge.actionRequired,
1851
+ paymentMethodType: charge.paymentMethodType
1852
+ };
1853
+ if (charge.actionRequired === "ENCODED_IMAGE") {
1854
+ response.encodedImage = {
1855
+ imageBase64Encoded: SIM_QR_PNG,
1856
+ rawData: `00020101021230SIM${charge.id}`,
1857
+ expiry: charge.expiresAt
1858
+ };
1859
+ }
1860
+ if (charge.actionRequired === "REDIRECT") {
1861
+ response.redirect = { redirectUrl: charge.redirectUrl };
1862
+ response.redirectUrl = charge.redirectUrl;
1863
+ }
1864
+ return response;
1865
+ }
1866
+ function nextChargeId() {
1867
+ state.directChargeCounter += 1;
1868
+ return `ch_sim_${state.directChargeCounter}`;
1869
+ }
1870
+ function createCharge(body) {
1871
+ const method = body.paymentMethod;
1872
+ const type = method.paymentMethodType;
1873
+ const id = nextChargeId();
1874
+ const now3 = /* @__PURE__ */ new Date();
1875
+ const expirySec = typeof body.expiryDurationInSec === "number" ? body.expiryDurationInSec : 30 * 60;
1876
+ const returnUrl = typeof body.returnUrl === "string" ? body.returnUrl : null;
1877
+ let actionRequired = "REDIRECT";
1878
+ let status = "PENDING";
1879
+ let failureCode = null;
1880
+ let cardLast4 = null;
1881
+ if (type === "QR_PROMPT_PAY") {
1882
+ actionRequired = "ENCODED_IMAGE";
1883
+ } else if (type === "CARD_TOKEN" || type === "CARD_TOKEN_INSTALLMENTS") {
1884
+ const token = state.cardTokens.get(method.cardToken.cardTokenId);
1885
+ if (!token) {
1886
+ return json({ error: "Card token not found", code: "CARD_TOKEN_NOT_FOUND" }, 404);
1887
+ }
1888
+ cardLast4 = token.last4;
1889
+ const card = TEST_CARDS[token.pan] ?? {
1890
+ outcome: "FAILED",
1891
+ failureCode: "CH_PROCESSING_FAILED"
1892
+ };
1893
+ if (card.outcome === "REDIRECT") {
1894
+ actionRequired = "REDIRECT";
1895
+ } else {
1896
+ actionRequired = "NONE";
1897
+ status = card.outcome;
1898
+ failureCode = card.failureCode ?? null;
1899
+ }
1900
+ }
1901
+ const charge = {
1902
+ id,
1903
+ status,
1904
+ amount: body.amount,
1905
+ currency: String(body.currency ?? "THB"),
1906
+ paymentMethodType: type,
1907
+ referenceId: typeof body.referenceId === "string" ? body.referenceId : null,
1908
+ deviceType: typeof body.deviceType === "string" ? body.deviceType : null,
1909
+ returnUrl,
1910
+ actionRequired,
1911
+ redirectUrl: actionRequired === "REDIRECT" ? `https://${SIM_BEAM_HOST}/pay/${id}${returnUrl ? `?returnUrl=${encodeURIComponent(returnUrl)}` : ""}` : null,
1912
+ expiresAt: new Date(now3.getTime() + expirySec * 1e3).toISOString(),
1913
+ failureCode,
1914
+ cardLast4,
1915
+ idempotencyKey: typeof body.idempotencyKey === "string" ? body.idempotencyKey : null,
1916
+ createdAt: now3.toISOString()
1917
+ };
1918
+ state.directCharges.set(id, charge);
1919
+ return charge;
1920
+ }
1921
+ async function handleDirectChargeRequest(request, pathname) {
1922
+ if (request.method === "POST" && /^\/api\/store\/beam\/[^/]+\/charges$/.test(pathname)) {
1923
+ const body = await readJsonBody(request);
1924
+ const issues = validateDirectCharge(body);
1925
+ if (issues.length > 0) return invalid(issues);
1926
+ const key = typeof body.idempotencyKey === "string" ? body.idempotencyKey : null;
1927
+ if (key) {
1928
+ const existing = [...state.directCharges.values()].find((c) => c.idempotencyKey === key);
1929
+ if (existing) return json(chargeResponse(existing));
1930
+ }
1931
+ const created = createCharge(body);
1932
+ if (created instanceof Response) return created;
1933
+ return json(chargeResponse(created));
1934
+ }
1935
+ const getMatch = pathname.match(/^\/api\/store\/beam\/[^/]+\/charges\/([^/]+)$/);
1936
+ if (request.method === "GET" && getMatch) {
1937
+ const charge = state.directCharges.get(decodeURIComponent(getMatch[1]));
1938
+ if (!charge) return json({ error: "Charge not found", code: "NOT_FOUND" }, 404);
1939
+ return json({ charge: toChargeDetail(charge) });
1940
+ }
1941
+ if (request.method === "GET" && /^\/api\/store\/beam\/[^/]+\/card-tokenization$/.test(pathname)) {
1942
+ if (!state.cardTokenizationEnabled) {
1943
+ return json(
1944
+ {
1945
+ error: "Add the Beam publishable key to this merchant account to take cards in your own checkout",
1946
+ code: "CARD_TOKENIZATION_NOT_CONFIGURED"
1947
+ },
1948
+ 409
1949
+ );
1950
+ }
1951
+ return json({
1952
+ merchantId: SIM_BEAM_MERCHANT_ID,
1953
+ publishableKey: SIM_BEAM_PUBLISHABLE_KEY,
1954
+ tokenizeUrl: `https://${SIM_BEAM_HOST}/client/v1/card-tokens`,
1955
+ isSandbox: true
1956
+ });
1957
+ }
1958
+ return null;
1959
+ }
1960
+ function toChargeDetail(charge) {
1961
+ return {
1962
+ id: charge.id,
1963
+ chargeId: charge.id,
1964
+ status: charge.status,
1965
+ amount: charge.amount,
1966
+ currency: charge.currency,
1967
+ paymentMethodType: charge.paymentMethodType,
1968
+ ...charge.referenceId !== null ? { referenceId: charge.referenceId } : {},
1969
+ ...charge.failureCode ? { failureCode: charge.failureCode } : {},
1970
+ source: "API",
1971
+ createdAt: charge.createdAt
1972
+ };
1973
+ }
1974
+ async function handleCardTokenRequest(request, url) {
1975
+ if (request.method !== "POST" || url.pathname !== "/client/v1/card-tokens") {
1976
+ return json({ error: `No Beam simulator for ${request.method} ${url.pathname}` }, 404);
1977
+ }
1978
+ const expected = `Basic ${btoa(`${SIM_BEAM_MERCHANT_ID}:${SIM_BEAM_PUBLISHABLE_KEY}`)}`;
1979
+ if (request.headers.get("Authorization") !== expected) {
1980
+ return json(
1981
+ {
1982
+ message: "invalid credentials",
1983
+ error: { errorCode: "INVALID_CREDENTIALS_ERROR", errorMessage: "invalid credentials" }
1984
+ },
1985
+ 401
1986
+ );
1987
+ }
1988
+ const body = await readJsonBody(request);
1989
+ const pan = typeof body.pan === "string" ? body.pan : "";
1990
+ const month = body.expiryMonth;
1991
+ const year = body.expiryYear;
1992
+ if (!/^\d{12,19}$/.test(pan) || typeof month !== "number" || month < 1 || month > 12 || typeof year !== "number" || year < 0 || year > 99) {
1993
+ return json(
1994
+ {
1995
+ message: "inputs are failing validation",
1996
+ error: {
1997
+ errorCode: "API_VALIDATION_ERROR",
1998
+ errorMessage: "inputs are failing validation"
1999
+ }
2000
+ },
2001
+ 400
2002
+ );
2003
+ }
2004
+ state.cardTokenCounter += 1;
2005
+ const token = {
2006
+ id: `ctok_sim_${state.cardTokenCounter}`,
2007
+ pan,
2008
+ last4: pan.slice(-4),
2009
+ expiryMonth: month,
2010
+ expiryYear: year
2011
+ };
2012
+ state.cardTokens.set(token.id, token);
2013
+ return json({ id: token.id });
2014
+ }
2015
+ function chargeWebhookPayload(charge) {
2016
+ return {
2017
+ chargeId: charge.id,
2018
+ merchantId: SIM_BEAM_MERCHANT_ID,
2019
+ ...charge.referenceId !== null ? { referenceId: charge.referenceId } : {},
2020
+ status: charge.status,
2021
+ currency: charge.currency,
2022
+ amount: charge.amount,
2023
+ source: "API",
2024
+ sourceId: charge.id,
2025
+ ...charge.failureCode ? { failureCode: charge.failureCode } : {},
2026
+ paymentMethod: { paymentMethodType: charge.paymentMethodType }
2027
+ };
2028
+ }
2029
+
1718
2030
  // src/simulator/payments.ts
1719
2031
  var import_node_crypto6 = __toESM(require("crypto"));
1720
2032
 
@@ -1756,6 +2068,9 @@ async function callRoute(handler, options = {}) {
1756
2068
  if (user && !headers.has("x-stardeck-user")) {
1757
2069
  headers.set("x-stardeck-user", JSON.stringify(user));
1758
2070
  }
2071
+ if (state.liffId && !headers.has(LIFF_ID_HEADER)) {
2072
+ headers.set(LIFF_ID_HEADER, state.liffId);
2073
+ }
1759
2074
  if (options.body !== void 0 && !headers.has("Content-Type")) {
1760
2075
  headers.set("Content-Type", "application/json");
1761
2076
  }
@@ -1829,7 +2144,7 @@ var BEAM_BOLT_PAYMENT_METHODS = [
1829
2144
  "SPAY_LATER"
1830
2145
  ];
1831
2146
  var BEAM_BOLT_INTENT_STATUSES = ["PENDING", "PAID", "FAILED", "CANCELED", "EXPIRED"];
1832
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2147
+ var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1833
2148
  function payErr(error, status = 400, code, details) {
1834
2149
  const body = { error };
1835
2150
  if (code) body.code = code;
@@ -1843,6 +2158,12 @@ function paymentOperationForRequest(request, pathname) {
1843
2158
  if (request.method === "GET" && /^\/api\/store\/beam\/[^/]+\/payment-links\/[^/]+$/.test(pathname)) {
1844
2159
  return "beam.getPaymentLinkStatus";
1845
2160
  }
2161
+ if (request.method === "POST" && /^\/api\/store\/beam\/[^/]+\/charges$/.test(pathname)) {
2162
+ return "beam.createDirectCharge";
2163
+ }
2164
+ if (request.method === "GET" && /^\/api\/store\/beam\/[^/]+\/charges\/[^/]+$/.test(pathname)) {
2165
+ return "beam.getCharge";
2166
+ }
1846
2167
  if (request.method === "POST" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout$/.test(pathname)) {
1847
2168
  return "stripe.createCheckout";
1848
2169
  }
@@ -2018,6 +2339,8 @@ async function handlePaymentsRequest(request, url) {
2018
2339
  const pathname = url.pathname;
2019
2340
  const injectedFailure = consumePaymentFailure(request, pathname);
2020
2341
  if (injectedFailure) return injectedFailure;
2342
+ const directChargeResponse = await handleDirectChargeRequest(request, pathname);
2343
+ if (directChargeResponse) return directChargeResponse;
2021
2344
  if (/\/billing-portal$/.test(pathname)) {
2022
2345
  return payErr("Not found", 404, "NOT_FOUND");
2023
2346
  }
@@ -2114,7 +2437,7 @@ async function handlePaymentsRequest(request, url) {
2114
2437
  code: "too_small"
2115
2438
  });
2116
2439
  }
2117
- if (typeof body.deploymentId !== "string" || !UUID_RE.test(body.deploymentId)) {
2440
+ if (typeof body.deploymentId !== "string" || !UUID_RE2.test(body.deploymentId)) {
2118
2441
  issues.push({
2119
2442
  path: ["deploymentId"],
2120
2443
  message: "Invalid uuid",
@@ -2301,6 +2624,18 @@ async function handlePaymentsRequest(request, url) {
2301
2624
  }
2302
2625
  return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
2303
2626
  }
2627
+ function requirePendingDirectCharge(chargeId) {
2628
+ const charge = state.directCharges.get(chargeId);
2629
+ if (!charge) {
2630
+ throw new Error(`[stardeck-testing] Unknown direct charge id: ${chargeId}`);
2631
+ }
2632
+ if (charge.status !== "PENDING") {
2633
+ throw new Error(
2634
+ `[stardeck-testing] Direct charge ${chargeId} is ${charge.status}, not PENDING`
2635
+ );
2636
+ }
2637
+ return charge;
2638
+ }
2304
2639
  function createPayments() {
2305
2640
  return {
2306
2641
  get checkouts() {
@@ -2419,6 +2754,26 @@ function createPayments() {
2419
2754
  if (!intent) return void 0;
2420
2755
  return toBoltIntentRecord(intent);
2421
2756
  },
2757
+ get directCharges() {
2758
+ return [...state.directCharges.values()];
2759
+ },
2760
+ getDirectCharge(chargeId) {
2761
+ return state.directCharges.get(chargeId);
2762
+ },
2763
+ completeDirectCharge(chargeId) {
2764
+ const charge = requirePendingDirectCharge(chargeId);
2765
+ charge.status = "SUCCEEDED";
2766
+ return chargeWebhookPayload(charge);
2767
+ },
2768
+ failDirectCharge(chargeId, failureCode = "CH_PROCESSING_FAILED") {
2769
+ const charge = requirePendingDirectCharge(chargeId);
2770
+ charge.status = "FAILED";
2771
+ charge.failureCode = failureCode;
2772
+ return chargeWebhookPayload(charge);
2773
+ },
2774
+ setCardTokenization(enabled) {
2775
+ state.cardTokenizationEnabled = enabled;
2776
+ },
2422
2777
  clear() {
2423
2778
  state.checkouts = [];
2424
2779
  state.checkoutCounter = 0;
@@ -2433,6 +2788,11 @@ function createPayments() {
2433
2788
  state.boltIntentCounter = 0;
2434
2789
  state.boltCharges = [];
2435
2790
  state.boltChargeCounter = 0;
2791
+ state.directCharges.clear();
2792
+ state.directChargeCounter = 0;
2793
+ state.cardTokens.clear();
2794
+ state.cardTokenCounter = 0;
2795
+ state.cardTokenizationEnabled = true;
2436
2796
  },
2437
2797
  get count() {
2438
2798
  return state.checkouts.length;
@@ -3312,6 +3672,9 @@ async function handleSimulatedRequest(request, url) {
3312
3672
  if (isStorageHost) {
3313
3673
  return handleStorageRequest(request, url);
3314
3674
  }
3675
+ if (url.hostname === SIM_BEAM_HOST) {
3676
+ return handleCardTokenRequest(request, url);
3677
+ }
3315
3678
  if (isEmail && request.method === "POST") {
3316
3679
  return handleEmailSend(request);
3317
3680
  }
@@ -3498,6 +3861,8 @@ async function createTestApp(options = {}) {
3498
3861
  }
3499
3862
  applyTestEnv(options);
3500
3863
  state.allowNetwork = options.allowNetwork ?? false;
3864
+ const initialLiffId = options.liffId ?? null;
3865
+ state.liffId = initialLiffId;
3501
3866
  installFetchRouter();
3502
3867
  const schemaSql = loadSchemaSql(options) ?? "";
3503
3868
  const db = await bootWithSchema(schemaSql);
@@ -3550,6 +3915,9 @@ async function createTestApp(options = {}) {
3550
3915
  signOut() {
3551
3916
  state.currentUser = null;
3552
3917
  },
3918
+ setLiffId(liffId) {
3919
+ state.liffId = liffId;
3920
+ },
3553
3921
  issueSession(user) {
3554
3922
  const fullUser = buildUser(user);
3555
3923
  const tokens = {
@@ -3572,6 +3940,7 @@ async function createTestApp(options = {}) {
3572
3940
  }
3573
3941
  }
3574
3942
  state.currentUser = null;
3943
+ state.liffId = initialLiffId;
3575
3944
  state.sessions.clear();
3576
3945
  state.refreshSessions.clear();
3577
3946
  state.emails = [];
@@ -3586,6 +3955,7 @@ async function createTestApp(options = {}) {
3586
3955
  async close() {
3587
3956
  state.db = null;
3588
3957
  state.currentUser = null;
3958
+ state.liffId = null;
3589
3959
  state.sessions.clear();
3590
3960
  state.refreshSessions.clear();
3591
3961
  state.emails = [];