@stardeck-customer-apps/testing 0.22.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/SKILL.md CHANGED
@@ -218,6 +218,13 @@ const integrations = createIntegrationsClient();
218
218
  await integrations.line.push("U123", { type: "text", text: "Your order shipped" });
219
219
  expect(app.messages.channel("line").to("U123")[0].body.text).toMatch(/shipped/i);
220
220
 
221
+ // Signed in through LINE (a shop's LINE MINI App): the session carries the
222
+ // person's shop-provider LINE user ID. Push to the ID on the SESSION.
223
+ app.asUser({ id: "buyer-1", lineUserId: "Ushop-provider-1" });
224
+ // route under test: const s = await getSession(); await integrations.line.push(s.user.lineUserId, ...)
225
+ await callRoute(POST);
226
+ expect(app.messages.channel("line").to("Ushop-provider-1")).toHaveLength(1);
227
+
221
228
  // LINE Flex card — the simulator preserves the complete LINE-native container.
222
229
  const card = {
223
230
  type: "bubble" as const,
@@ -322,7 +329,18 @@ expect(app.edge.latestDisplay()?.action).toBe("show");
322
329
  or the platform's test-run deadline.
323
330
 
324
331
  - `app.asUser(partial?)` — sign in a test user; `getSession()` in app code
325
- resolves to it. `app.signOut()` clears.
332
+ resolves to it. `app.signOut()` clears. Pass `lineUserId` to sign in as a
333
+ person who came through LINE sign-in inside the shop's LINE MINI App; it is
334
+ the same field production sets, so app code reads `session.user.lineUserId`.
335
+ `issueSession({ lineUserId })` carries it through cookie auth and the real
336
+ `/api/auth/me` route too.
337
+ - `app.setLiffId(id | null)` / `createTestApp({ liffId })` — the LIFF ID the
338
+ platform forwards to the app (`null` by default = no LINE MINI App). The real
339
+ project-auth `/api/auth/me` returns it as `liffId` on every response, signed
340
+ in or not, which is what makes `useLiff()` start LIFF. `reset()` restores the
341
+ `createTestApp` value. The simulator does not run LIFF sign-in itself
342
+ (`POST /api/auth/liff` needs a real LINE ID token), so seed the signed-in
343
+ state with `asUser`/`issueSession` instead.
326
344
  - `app.issueSession(partial?)` — mint access/refresh tokens for cookie-based
327
345
  auth flows (`__stardeck:sess`).
328
346
  - `app.inbox` — emails sent through the email-sdk: `.latest(addr?)`,
@@ -619,6 +637,20 @@ per-field validation (unit-test the route), login/logout (platform-provided).
619
637
  never `waitForTimeout` (web-first assertions auto-wait), every test
620
638
  independent of other tests' data and ordering.
621
639
 
640
+ ## Testing LINE flows
641
+
642
+ - Sign in as a LINE person with `app.asUser({ lineUserId: "U..." })` and call
643
+ the route with `callRoute`; assert the push with
644
+ `app.messages.channel("line").to("U...")`. Cover the person with no LINE
645
+ identity too (`asUser()` without `lineUserId`): the route must send nothing.
646
+ - The recipient of a LINE push comes from `session.user.lineUserId` on the
647
+ server, never from a request body, query string or client-side value. A
648
+ `lineUserId` a browser sends can be forged to message any friend of the shop's
649
+ LINE Official Account. Do not write a route that takes one, and do not write a
650
+ test that posts one to make a push happen; sign in with `lineUserId` instead.
651
+ - Whether the person is a friend of the shop's OA is a client concern
652
+ (`useLiff().isFriend`); a server route checks only that `lineUserId` exists.
653
+
622
654
  ## Rules
623
655
 
624
656
  - Test **business logic and route handlers**, not framework plumbing. Focus on
package/dist/index.d.mts CHANGED
@@ -38,6 +38,12 @@ interface TestUser {
38
38
  projectId?: string | null;
39
39
  /** Cross-app customer ownership key returned by the platform identity resolver. */
40
40
  identityId?: string | null;
41
+ /**
42
+ * The person's LINE user ID under the shop's own LINE provider. Set it to sign in
43
+ * as someone who came through LINE sign-in inside the shop's LINE MINI App; app
44
+ * code reads it from `session.user.lineUserId` and pushes to it.
45
+ */
46
+ lineUserId?: string | null;
41
47
  }
42
48
  interface CapturedEmail {
43
49
  resendId: string;
@@ -615,6 +621,12 @@ interface TestStarlens {
615
621
  get count(): number;
616
622
  }
617
623
  interface TestAppOptions {
624
+ /**
625
+ * The LIFF ID the platform forwards to the app for a LINE MINI App shop. It is
626
+ * what `GET /api/auth/me` returns as `liffId` and what makes `useLiff()` start
627
+ * LIFF. Defaults to `null` (the app has no LINE MINI App). Restored by `reset()`.
628
+ */
629
+ liffId?: string | null;
618
630
  /** Data stores exposed through the production-shaped STARDECK_DATA_STORES manifest. */
619
631
  dataStores?: TestDataStoreInput[];
620
632
  /**
@@ -669,6 +681,11 @@ interface TestApp {
669
681
  asUser(user?: Partial<TestUser>): TestUser;
670
682
  /** Clear the active test user. */
671
683
  signOut(): void;
684
+ /**
685
+ * Set (or clear with `null`) the LIFF ID the platform forwards to the app, for
686
+ * the rest of the test. The real project-auth `me` route returns it as `liffId`.
687
+ */
688
+ setLiffId(liffId: string | null): void;
672
689
  /**
673
690
  * Mint access/refresh tokens for cookie-based auth flows. The simulated
674
691
  * control-plane verify endpoint resolves these tokens back to the user.
package/dist/index.d.ts CHANGED
@@ -38,6 +38,12 @@ interface TestUser {
38
38
  projectId?: string | null;
39
39
  /** Cross-app customer ownership key returned by the platform identity resolver. */
40
40
  identityId?: string | null;
41
+ /**
42
+ * The person's LINE user ID under the shop's own LINE provider. Set it to sign in
43
+ * as someone who came through LINE sign-in inside the shop's LINE MINI App; app
44
+ * code reads it from `session.user.lineUserId` and pushes to it.
45
+ */
46
+ lineUserId?: string | null;
41
47
  }
42
48
  interface CapturedEmail {
43
49
  resendId: string;
@@ -615,6 +621,12 @@ interface TestStarlens {
615
621
  get count(): number;
616
622
  }
617
623
  interface TestAppOptions {
624
+ /**
625
+ * The LIFF ID the platform forwards to the app for a LINE MINI App shop. It is
626
+ * what `GET /api/auth/me` returns as `liffId` and what makes `useLiff()` start
627
+ * LIFF. Defaults to `null` (the app has no LINE MINI App). Restored by `reset()`.
628
+ */
629
+ liffId?: string | null;
618
630
  /** Data stores exposed through the production-shaped STARDECK_DATA_STORES manifest. */
619
631
  dataStores?: TestDataStoreInput[];
620
632
  /**
@@ -669,6 +681,11 @@ interface TestApp {
669
681
  asUser(user?: Partial<TestUser>): TestUser;
670
682
  /** Clear the active test user. */
671
683
  signOut(): void;
684
+ /**
685
+ * Set (or clear with `null`) the LIFF ID the platform forwards to the app, for
686
+ * the rest of the test. The real project-auth `me` route returns it as `liffId`.
687
+ */
688
+ setLiffId(liffId: string | null): void;
672
689
  /**
673
690
  * Mint access/refresh tokens for cookie-based auth flows. The simulated
674
691
  * control-plane verify endpoint resolves these tokens back to the user.
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: [],
@@ -2066,6 +2068,9 @@ async function callRoute(handler, options = {}) {
2066
2068
  if (user && !headers.has("x-stardeck-user")) {
2067
2069
  headers.set("x-stardeck-user", JSON.stringify(user));
2068
2070
  }
2071
+ if (state.liffId && !headers.has(LIFF_ID_HEADER)) {
2072
+ headers.set(LIFF_ID_HEADER, state.liffId);
2073
+ }
2069
2074
  if (options.body !== void 0 && !headers.has("Content-Type")) {
2070
2075
  headers.set("Content-Type", "application/json");
2071
2076
  }
@@ -3856,6 +3861,8 @@ async function createTestApp(options = {}) {
3856
3861
  }
3857
3862
  applyTestEnv(options);
3858
3863
  state.allowNetwork = options.allowNetwork ?? false;
3864
+ const initialLiffId = options.liffId ?? null;
3865
+ state.liffId = initialLiffId;
3859
3866
  installFetchRouter();
3860
3867
  const schemaSql = loadSchemaSql(options) ?? "";
3861
3868
  const db = await bootWithSchema(schemaSql);
@@ -3908,6 +3915,9 @@ async function createTestApp(options = {}) {
3908
3915
  signOut() {
3909
3916
  state.currentUser = null;
3910
3917
  },
3918
+ setLiffId(liffId) {
3919
+ state.liffId = liffId;
3920
+ },
3911
3921
  issueSession(user) {
3912
3922
  const fullUser = buildUser(user);
3913
3923
  const tokens = {
@@ -3930,6 +3940,7 @@ async function createTestApp(options = {}) {
3930
3940
  }
3931
3941
  }
3932
3942
  state.currentUser = null;
3943
+ state.liffId = initialLiffId;
3933
3944
  state.sessions.clear();
3934
3945
  state.refreshSessions.clear();
3935
3946
  state.emails = [];
@@ -3944,6 +3955,7 @@ async function createTestApp(options = {}) {
3944
3955
  async close() {
3945
3956
  state.db = null;
3946
3957
  state.currentUser = null;
3958
+ state.liffId = null;
3947
3959
  state.sessions.clear();
3948
3960
  state.refreshSessions.clear();
3949
3961
  state.emails = [];
package/dist/index.mjs CHANGED
@@ -271,6 +271,7 @@ var TEST_ENV_DEFAULTS = {
271
271
  DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
272
272
  STORAGE_URL: STORAGE_TEST_URL
273
273
  };
274
+ var LIFF_ID_HEADER = "x-stardeck-liff-id";
274
275
  var DEFAULT_TEST_USER = {
275
276
  id: "test-user-1",
276
277
  email: "test-user@example.com",
@@ -285,6 +286,7 @@ var state = globalSingleton("state", () => ({
285
286
  db: null,
286
287
  lastHarnessDataStoreManifest: null,
287
288
  currentUser: null,
289
+ liffId: null,
288
290
  sessions: /* @__PURE__ */ new Map(),
289
291
  refreshSessions: /* @__PURE__ */ new Map(),
290
292
  emails: [],
@@ -2027,6 +2029,9 @@ async function callRoute(handler, options = {}) {
2027
2029
  if (user && !headers.has("x-stardeck-user")) {
2028
2030
  headers.set("x-stardeck-user", JSON.stringify(user));
2029
2031
  }
2032
+ if (state.liffId && !headers.has(LIFF_ID_HEADER)) {
2033
+ headers.set(LIFF_ID_HEADER, state.liffId);
2034
+ }
2030
2035
  if (options.body !== void 0 && !headers.has("Content-Type")) {
2031
2036
  headers.set("Content-Type", "application/json");
2032
2037
  }
@@ -3817,6 +3822,8 @@ async function createTestApp(options = {}) {
3817
3822
  }
3818
3823
  applyTestEnv(options);
3819
3824
  state.allowNetwork = options.allowNetwork ?? false;
3825
+ const initialLiffId = options.liffId ?? null;
3826
+ state.liffId = initialLiffId;
3820
3827
  installFetchRouter();
3821
3828
  const schemaSql = loadSchemaSql(options) ?? "";
3822
3829
  const db = await bootWithSchema(schemaSql);
@@ -3869,6 +3876,9 @@ async function createTestApp(options = {}) {
3869
3876
  signOut() {
3870
3877
  state.currentUser = null;
3871
3878
  },
3879
+ setLiffId(liffId) {
3880
+ state.liffId = liffId;
3881
+ },
3872
3882
  issueSession(user) {
3873
3883
  const fullUser = buildUser(user);
3874
3884
  const tokens = {
@@ -3891,6 +3901,7 @@ async function createTestApp(options = {}) {
3891
3901
  }
3892
3902
  }
3893
3903
  state.currentUser = null;
3904
+ state.liffId = initialLiffId;
3894
3905
  state.sessions.clear();
3895
3906
  state.refreshSessions.clear();
3896
3907
  state.emails = [];
@@ -3905,6 +3916,7 @@ async function createTestApp(options = {}) {
3905
3916
  async close() {
3906
3917
  state.db = null;
3907
3918
  state.currentUser = null;
3919
+ state.liffId = null;
3908
3920
  state.sessions.clear();
3909
3921
  state.refreshSessions.clear();
3910
3922
  state.emails = [];
@@ -28,6 +28,12 @@ __export(headers_shim_exports, {
28
28
  module.exports = __toCommonJS(headers_shim_exports);
29
29
  var import_node_async_hooks = require("async_hooks");
30
30
 
31
+ // src/constants.ts
32
+ var DATA_STORE_TEST_HOST = "db.stardeck.test";
33
+ var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
34
+ var APP_TEST_ORIGIN = "http://localhost:3333";
35
+ var LIFF_ID_HEADER = "x-stardeck-liff-id";
36
+
31
37
  // src/global-singleton.ts
32
38
  function globalSingleton(key, create) {
33
39
  const symbol = /* @__PURE__ */ Symbol.for(`stardeck-testing.${key}`);
@@ -36,16 +42,12 @@ function globalSingleton(key, create) {
36
42
  return holder[symbol];
37
43
  }
38
44
 
39
- // src/constants.ts
40
- var DATA_STORE_TEST_HOST = "db.stardeck.test";
41
- var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
42
- var APP_TEST_ORIGIN = "http://localhost:3333";
43
-
44
45
  // src/state.ts
45
46
  var state = globalSingleton("state", () => ({
46
47
  db: null,
47
48
  lastHarnessDataStoreManifest: null,
48
49
  currentUser: null,
50
+ liffId: null,
49
51
  sessions: /* @__PURE__ */ new Map(),
50
52
  refreshSessions: /* @__PURE__ */ new Map(),
51
53
  emails: [],
@@ -105,6 +107,7 @@ function ambientScope() {
105
107
  if (state.currentUser) {
106
108
  headers2.set("x-stardeck-user", JSON.stringify(state.currentUser));
107
109
  }
110
+ if (state.liffId) headers2.set(LIFF_ID_HEADER, state.liffId);
108
111
  return { headers: headers2, cookies: /* @__PURE__ */ new Map() };
109
112
  }
110
113
  function currentScope() {
@@ -1,6 +1,12 @@
1
1
  // src/next/headers-shim.ts
2
2
  import { AsyncLocalStorage } from "async_hooks";
3
3
 
4
+ // src/constants.ts
5
+ var DATA_STORE_TEST_HOST = "db.stardeck.test";
6
+ var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
7
+ var APP_TEST_ORIGIN = "http://localhost:3333";
8
+ var LIFF_ID_HEADER = "x-stardeck-liff-id";
9
+
4
10
  // src/global-singleton.ts
5
11
  function globalSingleton(key, create) {
6
12
  const symbol = /* @__PURE__ */ Symbol.for(`stardeck-testing.${key}`);
@@ -9,16 +15,12 @@ function globalSingleton(key, create) {
9
15
  return holder[symbol];
10
16
  }
11
17
 
12
- // src/constants.ts
13
- var DATA_STORE_TEST_HOST = "db.stardeck.test";
14
- var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
15
- var APP_TEST_ORIGIN = "http://localhost:3333";
16
-
17
18
  // src/state.ts
18
19
  var state = globalSingleton("state", () => ({
19
20
  db: null,
20
21
  lastHarnessDataStoreManifest: null,
21
22
  currentUser: null,
23
+ liffId: null,
22
24
  sessions: /* @__PURE__ */ new Map(),
23
25
  refreshSessions: /* @__PURE__ */ new Map(),
24
26
  emails: [],
@@ -78,6 +80,7 @@ function ambientScope() {
78
80
  if (state.currentUser) {
79
81
  headers2.set("x-stardeck-user", JSON.stringify(state.currentUser));
80
82
  }
83
+ if (state.liffId) headers2.set(LIFF_ID_HEADER, state.liffId);
81
84
  return { headers: headers2, cookies: /* @__PURE__ */ new Map() };
82
85
  }
83
86
  function currentScope() {
package/dist/setup.js CHANGED
@@ -52,6 +52,7 @@ var state = globalSingleton("state", () => ({
52
52
  db: null,
53
53
  lastHarnessDataStoreManifest: null,
54
54
  currentUser: null,
55
+ liffId: null,
55
56
  sessions: /* @__PURE__ */ new Map(),
56
57
  refreshSessions: /* @__PURE__ */ new Map(),
57
58
  emails: [],
package/dist/setup.mjs CHANGED
@@ -28,6 +28,7 @@ var state = globalSingleton("state", () => ({
28
28
  db: null,
29
29
  lastHarnessDataStoreManifest: null,
30
30
  currentUser: null,
31
+ liffId: null,
31
32
  sessions: /* @__PURE__ */ new Map(),
32
33
  refreshSessions: /* @__PURE__ */ new Map(),
33
34
  emails: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/testing",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Vitest test harness for Stardeck customer apps — in-process Postgres (PGlite) plus a control-plane simulator so the real Stardeck SDKs run unmodified in tests",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",