create-gramstax 0.9.0 → 0.11.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/src/index.cjs +5 -4
- package/dist/src/index.js +5 -4
- package/dist/src/templates/README.md +0 -10
- package/dist/src/templates/e2e/blank.test.ts +9 -0
- package/dist/src/templates/e2e/general-status.test.ts +28 -0
- package/dist/src/templates/e2e/help.test.ts +28 -0
- package/dist/src/templates/e2e/random-number.test.ts +28 -0
- package/dist/src/templates/e2e/setup.ts +15 -0
- package/dist/src/templates/e2e/start.test.ts +33 -0
- package/dist/src/templates/e2e/sum-number.test.ts +44 -0
- package/dist/src/templates/e2e/username-notfound.test.ts +24 -0
- package/dist/src/templates/package.json +2 -1
- package/dist/src/templates/src/db/db.ts +24 -0
- package/dist/src/templates/src/db/repositories/user.ts +47 -0
- package/dist/src/templates/tsconfig.json +3 -0
- package/package.json +1 -1
- package/dist/src/templates/e2e/main.test.ts +0 -287
package/dist/src/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";var
|
|
2
|
+
"use strict";var B=Object.create;var v=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var Y=Object.getOwnPropertyNames;var V=Object.getPrototypeOf,q=Object.prototype.hasOwnProperty;var $=(s,e)=>v(s,"name",{value:e,configurable:!0});var K=(s,e,n,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of Y(e))!q.call(s,a)&&a!==n&&v(s,a,{get:()=>e[a],enumerable:!(t=G(e,a))||t.enumerable});return s};var w=(s,e,n)=>(n=s!=null?B(V(s)):{},K(e||!s||!s.__esModule?v(n,"default",{value:s,enumerable:!0}):n,s));var z=$(()=>typeof document>"u"?new URL(`file:${__filename}`).href:document.currentScript&&document.currentScript.tagName.toUpperCase()==="SCRIPT"?document.currentScript.src:new URL("main.js",document.baseURI).href,"getImportMetaUrl"),f=z();var T=require("logging-pretty"),i=new T.LoggingPretty;var r=w(require("fs"),1),o=w(require("path"),1),R=require("url");var M=require("fs");var p=class s{static{$(this,"FsUt")}static DEFAULT_IGNORED_ENTRIES=new Set([".git",".gitignore",".gitkeep",".DS_Store","Thumbs.db"]);static isDirectoryEffectivelyEmpty(e,n=s.DEFAULT_IGNORED_ENTRIES){return(0,M.readdirSync)(e).filter(c=>!n.has(c)).length===0}static resolvePackageManagerCommand(e,n=process.platform){return n!=="win32"||e==="bun"?e:`${e}.cmd`}static resolveInitialWorkingDirectory(e=process.cwd(),n=process.env.INIT_CWD){return n??e}};var j=require("child_process"),P=w(require("enquirer"),1);var H=(0,R.fileURLToPath)(f),J=o.dirname(H),k=class{constructor(e,n){this.projectDirectory=e;this.version=n?.version,this.cliOptions=n?.cliOptions}projectDirectory;static{$(this,"Create")}config;version;cliOptions;printBanner(){let e=`
|
|
3
3
|
$$$$$$\\ $$\\
|
|
4
4
|
$$ __$$\\ $$ |
|
|
5
5
|
$$ / \\__| $$$$$$\\ $$$$$$\\ $$$$$$\\$$$$\\ $$$$$$$\\ $$$$$$\\ $$$$$$\\ $$\\ $$\\
|
|
@@ -8,6 +8,7 @@ $$ |\\_$$ |$$ | \\__|$$$$$$$ |$$ / $$ / $$ |\\$$$$$$\\ $$ | $$$$$$$ | \\
|
|
|
8
8
|
$$ | $$ |$$ | $$ __$$ |$$ | $$ | $$ | \\____$$\\ $$ |$$\\ $$ __$$ | $$ $$<
|
|
9
9
|
\\$$$$$$ |$$ | \\$$$$$$$ |$$ | $$ | $$ |$$$$$$$ | \\$$$$ |\\$$$$$$$ |$$ /\\$$\\
|
|
10
10
|
\\______/ \\__| \\_______|\\__| \\__| \\__|\\_______/ \\____/ \\_______|\\__/ \\__|
|
|
11
|
-
|
|
12
|
-
`,
|
|
13
|
-
`)}getPackageManagerCommands(e){return{bun:{install:"bun install",dev:"bun dev",start:"bun start"},npm:{install:"npm install",dev:"npm run dev",start:"npm start"},pnpm:{install:"pnpm install",dev:"pnpm dev",start:"pnpm start"},yarn:{install:"yarn install",dev:"yarn dev",start:"yarn start"}}[e]}copyDirectorySync(e,t
|
|
11
|
+
|
|
12
|
+
`,n=this.version?`v${this.version}`:"v0.0.0";console.log(`${e}${n}
|
|
13
|
+
`)}getPackageManagerCommands(e){return{bun:{install:"bun install",dev:"bun dev",start:"bun start"},npm:{install:"npm install",dev:"npm run dev",start:"npm start"},pnpm:{install:"pnpm install",dev:"pnpm dev",start:"pnpm start"},yarn:{install:"yarn install",dev:"yarn dev",start:"yarn start"}}[e]}copyDirectorySync(e,n,t={}){r.existsSync(n)||r.mkdirSync(n,{recursive:!0});let a=r.readdirSync(e,{withFileTypes:!0});for(let c of a){let h=o.join(e,c.name),d=o.join(n,c.name);if(c.isDirectory())this.copyDirectorySync(h,d,t);else if(c.isFile()){let l=r.readFileSync(h,"utf-8");l=this.replaceTemplateVariables(l,t),r.writeFileSync(d,l)}}}replaceTemplateVariables(e,n){let t=e;for(let[a,c]of Object.entries(n))t=t.replace(new RegExp(`{{${a}}}`,"g"),c);return t}async promptForProjectName(){return(await P.default.prompt({type:"input",name:"projectName",message:"What is your project named?",initial:"",validate:$(n=>n===""||/^[a-z0-9-_]+$/i.test(n)?!0:"Project name can only contain letters, numbers, hyphens, and underscores","validate")})).projectName}async promptForConfiguration(){let e=await P.default.prompt([{type:"select",name:"packageManager",message:"Which package manager would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"npm",message:"npm",value:"npm"},{name:"pnpm",message:"pnpm",value:"pnpm"},{name:"yarn",message:"Yarn",value:"yarn"}],initial:0},{type:"select",name:"runtime",message:"Which runtime would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"node",message:"Node.js",value:"node"}],initial:0},{type:"confirm",name:"lmdb",message:"Would you like to include @gramstax/lmdb for persistent storage? (recommended)",initial:!0},{type:"confirm",name:"workerThread",message:"Would you like to include @gramstax/worker-thread for background jobs? (recommended)",initial:!0},{type:"confirm",name:"eslint",message:"Would you like to use ESLint?",initial:!0}]);return{packageManager:e.packageManager,runtime:e.runtime,lmdb:e.lmdb,workerThread:e.workerThread,eslint:e.eslint}}async installDependencies(e,n){return await new Promise(t=>{let a=(0,j.spawn)(p.resolvePackageManagerCommand(n),["install"],{cwd:e,stdio:"inherit",shell:!1});a.on("close",c=>c===0?t(!0):(i.fail("Dependencies installation failed"),t(!1))),a.on("error",c=>(i.fail(`Dependencies installation failed: ${c.message}`),t(!1)))})}async gatherProjectConfig(){this.printBanner();let e=this.cliOptions,n=p.resolveInitialWorkingDirectory(),t=this.projectDirectory??"",a,c=!1;if(t)a=o.resolve(n,t);else{if(e&&this._hasAnyCliFlag())throw i.error("Project name is required when using CLI flags"),new Error("Missing project name");t=await this.promptForProjectName(),t===""?(t=o.basename(n),a=n,c=!0):a=o.resolve(n,t)}let d=e&&this._hasAnyCliFlag()?{packageManager:e.packageManager??"bun",runtime:e.runtime??"bun",lmdb:e.lmdb??!0,workerThread:e.workerThread??!0,eslint:e.eslint??!0}:await this.promptForConfiguration(),{packageManager:l,runtime:m,lmdb:_,workerThread:u,eslint:y}=d;if(i.info(`Project: ${t}`),i.info(`Location: ${a}`),i.info(`Package Manager: ${l}`),i.info(`Runtime: ${m}`),i.info(`LMDB Storage: ${_?"Yes":"No"}`),i.info(`Worker Thread: ${u?"Yes":"No"}`),i.info(`ESLint: ${y?"Yes":"No"}`),c){if(!p.isDirectoryEffectivelyEmpty(a))throw new Error("Current directory is not empty")}else if(r.existsSync(a))throw new Error(`Directory "${t}" already exists`);return{projectName:t,projectPath:a,packageManager:l,runtime:m,setupEslint:y,useLmdb:_,useWorkerThread:u}}_hasAnyCliFlag(){let e=this.cliOptions;return!!(e&&(e.packageManager!==void 0||e.runtime!==void 0||e.lmdb!==void 0||e.workerThread!==void 0||e.eslint!==void 0))}async executeProjectGeneration(e){let{projectName:n,projectPath:t,packageManager:a,runtime:c,setupEslint:h,useLmdb:d,useWorkerThread:l}=e,m=this.getPackageManagerCommands(a),_={projectName:n,installCmd:m.install,runDevCmd:m.dev,runStartCmd:m.start};i.info("Creating project directory.."),r.existsSync(t)||r.mkdirSync(t,{recursive:!0}),i.info("Copying template files.."),this.copyDirectorySync(o.join(J,"templates"),t,_),i.info("Creating logs directory.."),r.mkdirSync(o.join(t,"logs"),{recursive:!0}),r.writeFileSync(o.join(t,"logs/.gitkeep"),"");let u=o.join(t,".env"),y=o.join(t,".env.example");if(r.existsSync(y)&&(i.info("Creating .env file.."),r.renameSync(y,u)),c!=="bun"){let g=o.join(t,"bunfig.toml");r.existsSync(g)&&r.unlinkSync(g)}let S=o.join(t,"src","db");if(d){i.info("Keeping @gramstax/lmdb files..");let g=r.readFileSync(u,"utf-8");r.writeFileSync(u,g+`DB_PATH="./data"
|
|
14
|
+
`),r.mkdirSync(o.join(t,"data"),{recursive:!0}),r.writeFileSync(o.join(t,"data/.gitkeep"),"")}else i.info("Removing @gramstax/lmdb files.."),r.existsSync(S)&&r.rmSync(S,{recursive:!0,force:!0});let C=o.join(t,"src","workers");if(l?i.info("Keeping @gramstax/worker-thread files.."):(i.info("Removing @gramstax/worker-thread files.."),r.existsSync(C)&&r.rmSync(C,{recursive:!0,force:!0})),i.info("Installing dependencies.."),!await this.installDependencies(t,a))throw new Error("Dependencies installation failed");let b=o.join(t,"eslint.config.ts.template"),U=o.join(t,"eslint.config.ts");if(h){i.info("Installing ESLint dependencies..");let g=p.resolvePackageManagerCommand(a),x=a==="yarn"?["add","-D"]:["install","-D"];x.push("eslint","@eslint/js","globals","typescript-eslint"),await new Promise(E=>{let D=(0,j.spawn)(g,x,{cwd:t,stdio:"inherit",shell:!1});D.on("close",()=>E()),D.on("error",A=>{i.fail(`Failed to install ESLint: ${A.message}`),E()})}),r.existsSync(b)&&r.renameSync(b,U)}else r.existsSync(b)&&r.unlinkSync(b);i.info("Project created successfully!"),i.info(`Next steps: ${e.projectPath!==process.cwd()?`cd ${n} && `:""}${m.install} && ${m.dev}`),i.info("Done..")}async createProject(){try{this.config=await this.gatherProjectConfig(),await this.executeProjectGeneration(this.config)}catch(e){i.error(`Failed to create project: ${e instanceof Error?e.message:String(e)}`),process.exit(1)}}};var L=require("commander"),N=require("fs"),I=require("path"),O=require("url"),Q=(0,O.fileURLToPath)(f),F=JSON.parse((0,N.readFileSync)((0,I.join)(Q,"../../package.json"),"utf-8")).version,W=new L.Command;W.name("create-gramstax").description("CLI to create a new Gramstax Telegram bot project").version(F).argument("[project-directory]","Directory to create the project in").option("--pm <name>","Package manager (bun, npm, pnpm, yarn)").option("--runtime <name>","Runtime (bun, node)").option("--lmdb","Include @gramstax/lmdb (default: true)").option("--no-lmdb","Skip @gramstax/lmdb").option("--worker-thread","Include @gramstax/worker-thread (default: true)").option("--no-worker-thread","Skip @gramstax/worker-thread").option("--eslint","Include ESLint (default: true)").option("--no-eslint","Skip ESLint").action(async(s,e)=>{try{let n={};e?.pm&&(n.packageManager=e.pm),e?.runtime&&(n.runtime=e.runtime),e?.lmdb!==void 0&&(n.lmdb=e.lmdb),e?.workerThread!==void 0&&(n.workerThread=e.workerThread),e?.eslint!==void 0&&(n.eslint=e.eslint),await new k(s,{version:F,cliOptions:n}).createProject()}catch(n){i.error(`Failed to create project: ${String(n)}`),process.exit(1)}});W.parse();
|
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var F=Object.defineProperty;var h=($,e)=>F($,"name",{value:e,configurable:!0});import{LoggingPretty as L}from"logging-pretty";var i=new L;import*as r from"fs";import*as a from"path";import{fileURLToPath as O}from"url";import{readdirSync as N}from"fs";var m=class ${static{h(this,"FsUt")}static DEFAULT_IGNORED_ENTRIES=new Set([".git",".gitignore",".gitkeep",".DS_Store","Thumbs.db"]);static isDirectoryEffectivelyEmpty(e,n=$.DEFAULT_IGNORED_ENTRIES){return N(e).filter(s=>!n.has(s)).length===0}static resolvePackageManagerCommand(e,n=process.platform){return n!=="win32"||e==="bun"?e:`${e}.cmd`}static resolveInitialWorkingDirectory(e=process.cwd(),n=process.env.INIT_CWD){return n??e}};import{spawn as C}from"child_process";import x from"enquirer";var I=O(import.meta.url),W=a.dirname(I),b=class{constructor(e,n){this.projectDirectory=e;this.version=n?.version,this.cliOptions=n?.cliOptions}projectDirectory;static{h(this,"Create")}config;version;cliOptions;printBanner(){let e=`
|
|
3
3
|
$$$$$$\\ $$\\
|
|
4
4
|
$$ __$$\\ $$ |
|
|
5
5
|
$$ / \\__| $$$$$$\\ $$$$$$\\ $$$$$$\\$$$$\\ $$$$$$$\\ $$$$$$\\ $$$$$$\\ $$\\ $$\\
|
|
@@ -8,6 +8,7 @@ $$ |\\_$$ |$$ | \\__|$$$$$$$ |$$ / $$ / $$ |\\$$$$$$\\ $$ | $$$$$$$ | \\
|
|
|
8
8
|
$$ | $$ |$$ | $$ __$$ |$$ | $$ | $$ | \\____$$\\ $$ |$$\\ $$ __$$ | $$ $$<
|
|
9
9
|
\\$$$$$$ |$$ | \\$$$$$$$ |$$ | $$ | $$ |$$$$$$$ | \\$$$$ |\\$$$$$$$ |$$ /\\$$\\
|
|
10
10
|
\\______/ \\__| \\_______|\\__| \\__| \\__|\\_______/ \\____/ \\_______|\\__/ \\__|
|
|
11
|
-
|
|
12
|
-
`,
|
|
13
|
-
`)}getPackageManagerCommands(e){return{bun:{install:"bun install",dev:"bun dev",start:"bun start"},npm:{install:"npm install",dev:"npm run dev",start:"npm start"},pnpm:{install:"pnpm install",dev:"pnpm dev",start:"pnpm start"},yarn:{install:"yarn install",dev:"yarn dev",start:"yarn start"}}[e]}copyDirectorySync(e,t
|
|
11
|
+
|
|
12
|
+
`,n=this.version?`v${this.version}`:"v0.0.0";console.log(`${e}${n}
|
|
13
|
+
`)}getPackageManagerCommands(e){return{bun:{install:"bun install",dev:"bun dev",start:"bun start"},npm:{install:"npm install",dev:"npm run dev",start:"npm start"},pnpm:{install:"pnpm install",dev:"pnpm dev",start:"pnpm start"},yarn:{install:"yarn install",dev:"yarn dev",start:"yarn start"}}[e]}copyDirectorySync(e,n,t={}){r.existsSync(n)||r.mkdirSync(n,{recursive:!0});let o=r.readdirSync(e,{withFileTypes:!0});for(let s of o){let u=a.join(e,s.name),p=a.join(n,s.name);if(s.isDirectory())this.copyDirectorySync(u,p,t);else if(s.isFile()){let c=r.readFileSync(u,"utf-8");c=this.replaceTemplateVariables(c,t),r.writeFileSync(p,c)}}}replaceTemplateVariables(e,n){let t=e;for(let[o,s]of Object.entries(n))t=t.replace(new RegExp(`{{${o}}}`,"g"),s);return t}async promptForProjectName(){return(await x.prompt({type:"input",name:"projectName",message:"What is your project named?",initial:"",validate:h(n=>n===""||/^[a-z0-9-_]+$/i.test(n)?!0:"Project name can only contain letters, numbers, hyphens, and underscores","validate")})).projectName}async promptForConfiguration(){let e=await x.prompt([{type:"select",name:"packageManager",message:"Which package manager would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"npm",message:"npm",value:"npm"},{name:"pnpm",message:"pnpm",value:"pnpm"},{name:"yarn",message:"Yarn",value:"yarn"}],initial:0},{type:"select",name:"runtime",message:"Which runtime would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"node",message:"Node.js",value:"node"}],initial:0},{type:"confirm",name:"lmdb",message:"Would you like to include @gramstax/lmdb for persistent storage? (recommended)",initial:!0},{type:"confirm",name:"workerThread",message:"Would you like to include @gramstax/worker-thread for background jobs? (recommended)",initial:!0},{type:"confirm",name:"eslint",message:"Would you like to use ESLint?",initial:!0}]);return{packageManager:e.packageManager,runtime:e.runtime,lmdb:e.lmdb,workerThread:e.workerThread,eslint:e.eslint}}async installDependencies(e,n){return await new Promise(t=>{let o=C(m.resolvePackageManagerCommand(n),["install"],{cwd:e,stdio:"inherit",shell:!1});o.on("close",s=>s===0?t(!0):(i.fail("Dependencies installation failed"),t(!1))),o.on("error",s=>(i.fail(`Dependencies installation failed: ${s.message}`),t(!1)))})}async gatherProjectConfig(){this.printBanner();let e=this.cliOptions,n=m.resolveInitialWorkingDirectory(),t=this.projectDirectory??"",o,s=!1;if(t)o=a.resolve(n,t);else{if(e&&this._hasAnyCliFlag())throw i.error("Project name is required when using CLI flags"),new Error("Missing project name");t=await this.promptForProjectName(),t===""?(t=a.basename(n),o=n,s=!0):o=a.resolve(n,t)}let p=e&&this._hasAnyCliFlag()?{packageManager:e.packageManager??"bun",runtime:e.runtime??"bun",lmdb:e.lmdb??!0,workerThread:e.workerThread??!0,eslint:e.eslint??!0}:await this.promptForConfiguration(),{packageManager:c,runtime:l,lmdb:y,workerThread:d,eslint:f}=p;if(i.info(`Project: ${t}`),i.info(`Location: ${o}`),i.info(`Package Manager: ${c}`),i.info(`Runtime: ${l}`),i.info(`LMDB Storage: ${y?"Yes":"No"}`),i.info(`Worker Thread: ${d?"Yes":"No"}`),i.info(`ESLint: ${f?"Yes":"No"}`),s){if(!m.isDirectoryEffectivelyEmpty(o))throw new Error("Current directory is not empty")}else if(r.existsSync(o))throw new Error(`Directory "${t}" already exists`);return{projectName:t,projectPath:o,packageManager:c,runtime:l,setupEslint:f,useLmdb:y,useWorkerThread:d}}_hasAnyCliFlag(){let e=this.cliOptions;return!!(e&&(e.packageManager!==void 0||e.runtime!==void 0||e.lmdb!==void 0||e.workerThread!==void 0||e.eslint!==void 0))}async executeProjectGeneration(e){let{projectName:n,projectPath:t,packageManager:o,runtime:s,setupEslint:u,useLmdb:p,useWorkerThread:c}=e,l=this.getPackageManagerCommands(o),y={projectName:n,installCmd:l.install,runDevCmd:l.dev,runStartCmd:l.start};i.info("Creating project directory.."),r.existsSync(t)||r.mkdirSync(t,{recursive:!0}),i.info("Copying template files.."),this.copyDirectorySync(a.join(W,"templates"),t,y),i.info("Creating logs directory.."),r.mkdirSync(a.join(t,"logs"),{recursive:!0}),r.writeFileSync(a.join(t,"logs/.gitkeep"),"");let d=a.join(t,".env"),f=a.join(t,".env.example");if(r.existsSync(f)&&(i.info("Creating .env file.."),r.renameSync(f,d)),s!=="bun"){let g=a.join(t,"bunfig.toml");r.existsSync(g)&&r.unlinkSync(g)}let k=a.join(t,"src","db");if(p){i.info("Keeping @gramstax/lmdb files..");let g=r.readFileSync(d,"utf-8");r.writeFileSync(d,g+`DB_PATH="./data"
|
|
14
|
+
`),r.mkdirSync(a.join(t,"data"),{recursive:!0}),r.writeFileSync(a.join(t,"data/.gitkeep"),"")}else i.info("Removing @gramstax/lmdb files.."),r.existsSync(k)&&r.rmSync(k,{recursive:!0,force:!0});let v=a.join(t,"src","workers");if(c?i.info("Keeping @gramstax/worker-thread files.."):(i.info("Removing @gramstax/worker-thread files.."),r.existsSync(v)&&r.rmSync(v,{recursive:!0,force:!0})),i.info("Installing dependencies.."),!await this.installDependencies(t,o))throw new Error("Dependencies installation failed");let _=a.join(t,"eslint.config.ts.template"),T=a.join(t,"eslint.config.ts");if(u){i.info("Installing ESLint dependencies..");let g=m.resolvePackageManagerCommand(o),w=o==="yarn"?["add","-D"]:["install","-D"];w.push("eslint","@eslint/js","globals","typescript-eslint"),await new Promise(j=>{let P=C(g,w,{cwd:t,stdio:"inherit",shell:!1});P.on("close",()=>j()),P.on("error",M=>{i.fail(`Failed to install ESLint: ${M.message}`),j()})}),r.existsSync(_)&&r.renameSync(_,T)}else r.existsSync(_)&&r.unlinkSync(_);i.info("Project created successfully!"),i.info(`Next steps: ${e.projectPath!==process.cwd()?`cd ${n} && `:""}${l.install} && ${l.dev}`),i.info("Done..")}async createProject(){try{this.config=await this.gatherProjectConfig(),await this.executeProjectGeneration(this.config)}catch(e){i.error(`Failed to create project: ${e instanceof Error?e.message:String(e)}`),process.exit(1)}}};import{Command as A}from"commander";import{readFileSync as B}from"fs";import{join as G}from"path";import{fileURLToPath as U}from"url";var Y=U(import.meta.url),E=JSON.parse(B(G(Y,"../../package.json"),"utf-8")).version,D=new A;D.name("create-gramstax").description("CLI to create a new Gramstax Telegram bot project").version(E).argument("[project-directory]","Directory to create the project in").option("--pm <name>","Package manager (bun, npm, pnpm, yarn)").option("--runtime <name>","Runtime (bun, node)").option("--lmdb","Include @gramstax/lmdb (default: true)").option("--no-lmdb","Skip @gramstax/lmdb").option("--worker-thread","Include @gramstax/worker-thread (default: true)").option("--no-worker-thread","Skip @gramstax/worker-thread").option("--eslint","Include ESLint (default: true)").option("--no-eslint","Skip ESLint").action(async($,e)=>{try{let n={};e?.pm&&(n.packageManager=e.pm),e?.runtime&&(n.runtime=e.runtime),e?.lmdb!==void 0&&(n.lmdb=e.lmdb),e?.workerThread!==void 0&&(n.workerThread=e.workerThread),e?.eslint!==void 0&&(n.eslint=e.eslint),await new b($,{version:E,cliOptions:n}).createProject()}catch(n){i.error(`Failed to create project: ${String(n)}`),process.exit(1)}});D.parse();
|
|
@@ -4,8 +4,6 @@ A Telegram bot built with Gramstax framework.
|
|
|
4
4
|
|
|
5
5
|
## Getting Started
|
|
6
6
|
|
|
7
|
-
### Development
|
|
8
|
-
|
|
9
7
|
1. Install dependencies:
|
|
10
8
|
|
|
11
9
|
```bash
|
|
@@ -22,12 +20,6 @@ TELEGRAM_BOT_TOKEN="<...your_bot_token_here...>"
|
|
|
22
20
|
|
|
23
21
|
3. Run the development server:
|
|
24
22
|
|
|
25
|
-
```bash
|
|
26
|
-
{{runDevCmd}}
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
#### Using Bun directly
|
|
30
|
-
|
|
31
23
|
```bash
|
|
32
24
|
{{runStartCmd}}
|
|
33
25
|
```
|
|
@@ -54,5 +46,3 @@ TELEGRAM_BOT_TOKEN="<...your_bot_token_here...>"
|
|
|
54
46
|
## Learn More
|
|
55
47
|
|
|
56
48
|
- [Gramstax Documentation](https://github.com/gramstax/gramstax)
|
|
57
|
-
- [Grammy Documentation](https://grammy.dev/)
|
|
58
|
-
- [Telegram Bot API](https://core.telegram.org/bots/api)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
|
+
import {e2e} from "./setup"
|
|
3
|
+
|
|
4
|
+
const intent = `general-status`
|
|
5
|
+
const asUser = {user: {id: 8001, username: `testuser`}, chat: {id: 8001}}
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
e2e.clearCalls()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe(`E2E: GeneralStatusPage`, () => {
|
|
12
|
+
it(`show 'Route not found' for unrecognized callback query`, async () => {
|
|
13
|
+
await e2e.sendText(`/start`, asUser)
|
|
14
|
+
e2e.clearCalls()
|
|
15
|
+
|
|
16
|
+
const responses = await e2e.sendCallbackQuery(`nonexistent-callback-data`, asUser)
|
|
17
|
+
const responsesId = await e2e.sendCallbackQuery(`nonexistent-callback-data`, {user: {id: 8002, username: `pengguna`, language_code: `id`}, chat: {id: 8002}})
|
|
18
|
+
|
|
19
|
+
expect(responses.length).toBeGreaterThan(0)
|
|
20
|
+
expect(responsesId.length).toBeGreaterThan(0)
|
|
21
|
+
const reply = responses[0]!
|
|
22
|
+
const replyId = responsesId[0]!
|
|
23
|
+
expect(reply).toBeDefined()
|
|
24
|
+
expect(replyId).toBeDefined()
|
|
25
|
+
expect(reply.body?.text).toBe(e2e.gmsg(intent, `d`, {isNotFound: true}))
|
|
26
|
+
expect(replyId.body?.text).toBe(e2e.gmsg(intent, `d`, {isNotFound: true}, `id`))
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
|
+
import {e2e} from "./setup"
|
|
3
|
+
|
|
4
|
+
const intent = `help`
|
|
5
|
+
const asUser = {user: {id: 7001, username: `testuser`}, chat: {id: 7001}}
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
e2e.clearCalls()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe(`E2E: HelpPage`, () => {
|
|
12
|
+
it(`show help text when navigating from /start -> Help`, async () => {
|
|
13
|
+
await e2e.sendText(`/start`, asUser)
|
|
14
|
+
e2e.clearCalls()
|
|
15
|
+
|
|
16
|
+
const responses = await e2e.sendCallbackQuery(intent, asUser)
|
|
17
|
+
const responsesId = await e2e.sendCallbackQuery(intent, {user: {id: 7002, username: `pengguna`, language_code: `id`}, chat: {id: 7002}})
|
|
18
|
+
|
|
19
|
+
expect(responses.length).toBeGreaterThan(0)
|
|
20
|
+
expect(responsesId.length).toBeGreaterThan(0)
|
|
21
|
+
const edit = responses[0]!
|
|
22
|
+
const editId = responsesId[0]!
|
|
23
|
+
expect(edit.body?.text).toBe(e2e.gmsg(intent, `s1`, {}))
|
|
24
|
+
expect(editId.body?.text).toBe(e2e.gmsg(intent, `s1`, {}, `id`))
|
|
25
|
+
expect(e2e.extractButtonLabels(edit)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}))
|
|
26
|
+
expect(e2e.extractButtonLabels(editId)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}, `id`))
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
|
+
import {e2e} from "./setup"
|
|
3
|
+
|
|
4
|
+
const intent = `random-number`
|
|
5
|
+
const asUser = {user: {id: 2003, username: `testuser`}, chat: {id: 2003}}
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
e2e.clearCalls()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe(`E2E: RandomNumberPage`, () => {
|
|
12
|
+
it(`show generated random number when navigating from /start`, async () => {
|
|
13
|
+
await e2e.sendText(`/start`, asUser)
|
|
14
|
+
e2e.clearCalls()
|
|
15
|
+
|
|
16
|
+
const responses = await e2e.sendCallbackQuery(intent, asUser)
|
|
17
|
+
const responsesId = await e2e.sendCallbackQuery(intent, {user: {id: 6001, username: `pengguna`, language_code: `id`}, chat: {id: 6001}})
|
|
18
|
+
|
|
19
|
+
expect(responses.length).toBeGreaterThan(0)
|
|
20
|
+
expect(responsesId.length).toBeGreaterThan(0)
|
|
21
|
+
const edit = responses[0]!
|
|
22
|
+
const editId = responsesId[0]!
|
|
23
|
+
expect(edit.body?.text).toContain(`Random Number`)
|
|
24
|
+
expect(editId.body?.text).toContain(`Angka Acak`)
|
|
25
|
+
expect(e2e.extractButtonLabels(edit)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}))
|
|
26
|
+
expect(e2e.extractButtonLabels(editId)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}, `id`))
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {join} from "node:path"
|
|
2
|
+
import {BotCore} from "../src/core/bot"
|
|
3
|
+
import {TemplateManager} from "gramstax"
|
|
4
|
+
|
|
5
|
+
export type {ITE2eApiCall, ITE2eUpdateOptions} from "gramstax"
|
|
6
|
+
|
|
7
|
+
const templateManager = new TemplateManager({path: [join(import.meta.dir, `../src/pages`)]})
|
|
8
|
+
export const bot = new BotCore({
|
|
9
|
+
deploy: `test`,
|
|
10
|
+
token: `test`,
|
|
11
|
+
log: false,
|
|
12
|
+
templateManager
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const e2e = bot.e2e!
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
|
+
import {e2e} from "./setup"
|
|
3
|
+
|
|
4
|
+
const intent = `start`
|
|
5
|
+
const asUser = {user: {id: 2001, username: `testuser`}, chat: {id: 2001}}
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
e2e.clearCalls()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe(`E2E: StartPage`, () => {
|
|
12
|
+
it(`respond to /start command with welcome message and full keyboard`, async () => {
|
|
13
|
+
const responses = await e2e.sendText(`/start`, asUser)
|
|
14
|
+
const responsesId = await e2e.sendText(`/start`, {user: {id: 5001, username: `pengguna`, language_code: `id`}, chat: {id: 5001}})
|
|
15
|
+
|
|
16
|
+
expect(responses.length).toBeGreaterThan(0)
|
|
17
|
+
expect(responsesId.length).toBeGreaterThan(0)
|
|
18
|
+
const reply = responses[0]!
|
|
19
|
+
const replyId = responsesId[0]!
|
|
20
|
+
expect(reply.body?.text).toContain(`Welcome`)
|
|
21
|
+
expect(replyId.body?.text).toContain(`Selamat datang`)
|
|
22
|
+
expect(e2e.extractButtonLabels(reply)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}))
|
|
23
|
+
expect(e2e.extractButtonLabels(replyId)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}, `id`))
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it(`capture outbound requests in e2e.calls`, async () => {
|
|
27
|
+
await e2e.sendText(`/start`, asUser)
|
|
28
|
+
|
|
29
|
+
const sendMessageReq = e2e.lastCall(`sendMessage`)
|
|
30
|
+
expect(sendMessageReq).toBeDefined()
|
|
31
|
+
expect(sendMessageReq?.body?.text).toContain(`Welcome`)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
|
+
import {e2e} from "./setup"
|
|
3
|
+
|
|
4
|
+
const intent = `sum-number`
|
|
5
|
+
const asUser = {user: {id: 2004, username: `testuser`}, chat: {id: 2004}}
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
e2e.clearCalls()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe(`E2E: SumNumberPage`, () => {
|
|
12
|
+
it(`complete two-step sum flow and show result`, async () => {
|
|
13
|
+
await e2e.sendText(`/start`, asUser)
|
|
14
|
+
e2e.clearCalls()
|
|
15
|
+
|
|
16
|
+
const step1 = await e2e.sendCallbackQuery(intent, asUser)
|
|
17
|
+
expect(step1.length).toBeGreaterThan(0)
|
|
18
|
+
expect(step1[0]!.body?.text).toBe(e2e.gmsg(intent, `s1`, {}))
|
|
19
|
+
e2e.clearCalls()
|
|
20
|
+
|
|
21
|
+
const step2 = await e2e.sendText(`23`, asUser)
|
|
22
|
+
expect(step2.length).toBeGreaterThan(0)
|
|
23
|
+
expect(step2[0]!.body?.text).toBe(e2e.gmsg(intent, `s2`, {}))
|
|
24
|
+
e2e.clearCalls()
|
|
25
|
+
|
|
26
|
+
const result = await e2e.sendText(`10`, asUser)
|
|
27
|
+
expect(result.length).toBeGreaterThan(0)
|
|
28
|
+
expect(result[0]!.body?.text).toContain(`Result Sum`)
|
|
29
|
+
expect(result[0]!.body?.text).toContain(`33`)
|
|
30
|
+
expect(e2e.extractButtonLabels(result[0])).toEqual(e2e.gkb(intent, `s`, `inline`, {}))
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it(`re-prompt step 1 on invalid (non-numeric) input`, async () => {
|
|
34
|
+
await e2e.sendText(`/start`, asUser)
|
|
35
|
+
e2e.clearCalls()
|
|
36
|
+
|
|
37
|
+
await e2e.sendCallbackQuery(intent, asUser)
|
|
38
|
+
e2e.clearCalls()
|
|
39
|
+
|
|
40
|
+
const retry = await e2e.sendText(`not-a-number`, asUser)
|
|
41
|
+
expect(retry.length).toBeGreaterThan(0)
|
|
42
|
+
expect(retry[0]!.body?.text).toBe(e2e.gmsg(intent, `s1`, {}))
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
|
+
import {e2e} from "./setup"
|
|
3
|
+
|
|
4
|
+
const intent = `username-notfound`
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
e2e.clearCalls()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
describe(`E2E: UserNameNotFoundPage`, () => {
|
|
11
|
+
it(`blocks user without username and shows username required page`, async () => {
|
|
12
|
+
const responses = await e2e.sendText(`/start`, {
|
|
13
|
+
user: {id: 77777, first_name: `NoUsername`},
|
|
14
|
+
chat: {id: 77777, type: `private`, first_name: `NoUsername`}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
expect(responses.length).toBeGreaterThan(0)
|
|
18
|
+
const reply = responses[0]!
|
|
19
|
+
expect(reply).toBeDefined()
|
|
20
|
+
expect(reply.body?.text).toBe(e2e.gmsg(intent, `s1`, {}))
|
|
21
|
+
expect(reply.body?.text).toContain(`Username Required`)
|
|
22
|
+
expect(e2e.extractButtonLabels(reply)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}))
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {LmdbEnv} from "@gramstax/lmdb"
|
|
2
|
+
import {UserRepo} from "./repositories/user"
|
|
3
|
+
|
|
4
|
+
export class Db extends LmdbEnv {
|
|
5
|
+
private _ready: Promise<void>
|
|
6
|
+
|
|
7
|
+
constructor(storePath: string) {
|
|
8
|
+
super({path: storePath, maxDbs: 10, compression: true, encoding: `json`})
|
|
9
|
+
this._ready = this._init()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private async _init() {
|
|
13
|
+
await this.open()
|
|
14
|
+
this.register(UserRepo)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async ready() {
|
|
18
|
+
await this._ready
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get userRepo(): UserRepo {
|
|
22
|
+
return this.get(UserRepo)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {LmdbRepository} from "@gramstax/lmdb"
|
|
2
|
+
|
|
3
|
+
export type IKUser = number
|
|
4
|
+
|
|
5
|
+
export type IVUser = {
|
|
6
|
+
tgId: number
|
|
7
|
+
username: string
|
|
8
|
+
firstName: string
|
|
9
|
+
lastName: string
|
|
10
|
+
languageCode: string
|
|
11
|
+
createdAt: number
|
|
12
|
+
updatedAt: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class UserRepo extends LmdbRepository<IVUser, IKUser> {
|
|
16
|
+
static dbName = `user`
|
|
17
|
+
|
|
18
|
+
async findOrCreate(tgId: number, data: {username: string; firstName: string; lastName: string; languageCode: string}): Promise<{user: IVUser; created: boolean}> {
|
|
19
|
+
return this.env.transaction(async () => {
|
|
20
|
+
const existing = await this.get(tgId)
|
|
21
|
+
if (existing) {
|
|
22
|
+
const updated: IVUser = {...existing, ...data, updatedAt: Date.now()}
|
|
23
|
+
await this.set(tgId, updated)
|
|
24
|
+
return {user: updated, created: false}
|
|
25
|
+
}
|
|
26
|
+
const now = Date.now()
|
|
27
|
+
const user: IVUser = {tgId, ...data, createdAt: now, updatedAt: now}
|
|
28
|
+
await this.set(tgId, user)
|
|
29
|
+
return {user, created: true}
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async touch(tgId: number) {
|
|
34
|
+
return this.env.transaction(async () => {
|
|
35
|
+
const user = await this.get(tgId)
|
|
36
|
+
if (user) {
|
|
37
|
+
await this.set(tgId, {...user, updatedAt: Date.now()})
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async displayName(tgId: number): Promise<string> {
|
|
43
|
+
const user = await this.get(tgId)
|
|
44
|
+
if (!user) return `User#${tgId}`
|
|
45
|
+
return user.firstName || user.username || `User#${tgId}`
|
|
46
|
+
}
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* E2E Test Flow for Template Bot
|
|
3
|
-
*
|
|
4
|
-
* Setup:
|
|
5
|
-
* 1. MockTelegramServer starts on random port → apiRoot captured
|
|
6
|
-
* 2. BotCore initialized with webhook:http://127.0.0.1:3009/webhook/e2e-template
|
|
7
|
-
* 3. CacheExternal (memory) shared across tests, cleared in beforeEach
|
|
8
|
-
*
|
|
9
|
-
* Test Flow:
|
|
10
|
-
* T1. /start → Welcome message + inline keyboard (Random Number, Sum Number, Help)
|
|
11
|
-
* T2. /start → callback:help → editMessage → Help Message + Back button
|
|
12
|
-
* T3. /start → callback:random-number → editMessage → Random Number + Shake/Back buttons
|
|
13
|
-
* T4. /start → callback:random-number → callback:random-number → editMessage → refreshed number
|
|
14
|
-
* T5. /start → callback:sum-number → text:23 → text:10 → Result Sum: 33 + Home button
|
|
15
|
-
* T6. /start → callback:sum-number → text:not-a-number → re-prompt Input Left Number
|
|
16
|
-
* T7. /start → callback:nonexistent_action → graceful handling (no crash)
|
|
17
|
-
* T8. /start → verify sendMessage request captured in mockServer.requests
|
|
18
|
-
* T9. /start → callback:help → callback:random-number → sequential chain
|
|
19
|
-
* T10. sendPhoto → Route Not Found (no handler for photo)
|
|
20
|
-
* T11. /start with custom user/chat → custom ID propagation verified
|
|
21
|
-
* T12. /start without username → Username Required page (UserGuard blocks)
|
|
22
|
-
* T13. /start → callback:help → callback:random-number → keyboard structure validation (text + callback_data)
|
|
23
|
-
*
|
|
24
|
-
* Cleanup:
|
|
25
|
-
* - botCore.stop() + mockServer.stop() in afterAll
|
|
26
|
-
* - cacheSession.clear() + mockServer.clearRequests() in beforeEach
|
|
27
|
-
*/
|
|
28
|
-
import {join} from "node:path"
|
|
29
|
-
import {BotCore} from "../src/core/bot"
|
|
30
|
-
import {TemplateManager, CacheExternal} from "gramstax"
|
|
31
|
-
import {MockTelegramServer} from "gramstax"
|
|
32
|
-
import {describe, it, expect, beforeAll, afterAll, beforeEach} from "bun:test"
|
|
33
|
-
|
|
34
|
-
let mockServer: MockTelegramServer
|
|
35
|
-
let botCore: BotCore
|
|
36
|
-
let cacheSession: CacheExternal
|
|
37
|
-
|
|
38
|
-
const USER_1 = {
|
|
39
|
-
responseTimeoutMs: undefined, // 9000,
|
|
40
|
-
user: {
|
|
41
|
-
id: 123456789,
|
|
42
|
-
first_name: `Test`,
|
|
43
|
-
last_name: `user`,
|
|
44
|
-
username: `testuser`,
|
|
45
|
-
language_code: `en`,
|
|
46
|
-
is_bot: false
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
beforeAll(async () => {
|
|
51
|
-
mockServer = new MockTelegramServer({log: false})
|
|
52
|
-
await mockServer.start()
|
|
53
|
-
|
|
54
|
-
const templateManager = new TemplateManager({path: [join(import.meta.dir, `../src/pages`)]})
|
|
55
|
-
const cwd = process.cwd()
|
|
56
|
-
process.chdir(join(import.meta.dir, `..`))
|
|
57
|
-
|
|
58
|
-
cacheSession = new CacheExternal(`memory`)
|
|
59
|
-
|
|
60
|
-
botCore = new BotCore({
|
|
61
|
-
deploy: `webhook:http://127.0.0.1:3009/webhook/e2e-template`,
|
|
62
|
-
token: `test-token`,
|
|
63
|
-
log: false,
|
|
64
|
-
templateManager,
|
|
65
|
-
cacheSession,
|
|
66
|
-
botConfig: {client: {apiRoot: mockServer.apiRoot}}
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
process.chdir(cwd)
|
|
70
|
-
|
|
71
|
-
const startedAt = Date.now()
|
|
72
|
-
while (!mockServer.webhookUrl && Date.now() - startedAt < 5000) {
|
|
73
|
-
await new Promise((r) => setTimeout(r, 50))
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
afterAll(async () => {
|
|
78
|
-
await botCore.stop()
|
|
79
|
-
await mockServer.stop()
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
beforeEach(async () => {
|
|
83
|
-
mockServer.clearRequests()
|
|
84
|
-
await cacheSession.clear()
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
describe(`E2E: Template bot via MockTelegramServer (webhook)`, () => {
|
|
88
|
-
it(`responds to /start command with welcome message and navigation buttons`, async () => {
|
|
89
|
-
const responses = await mockServer.sendText(`/start`, USER_1)
|
|
90
|
-
|
|
91
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
92
|
-
const reply = responses[0]!
|
|
93
|
-
expect(reply).toBeDefined()
|
|
94
|
-
expect(reply!.body?.text).toContain(`Welcome to the bot`)
|
|
95
|
-
|
|
96
|
-
const keyboard = reply!.body?.reply_markup?.inline_keyboard
|
|
97
|
-
expect(Array.isArray(keyboard)).toBe(true)
|
|
98
|
-
expect(keyboard.length).toBeGreaterThan(0)
|
|
99
|
-
|
|
100
|
-
const allButtons = keyboard.flat()
|
|
101
|
-
const buttonLabels = allButtons.map((b: any) => b.text)
|
|
102
|
-
expect(buttonLabels).toContain(`🎰 Random Number`)
|
|
103
|
-
expect(buttonLabels).toContain(`➕ Sum Number`)
|
|
104
|
-
expect(buttonLabels).toContain(`🆘 Help`)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it(`navigates to Help page via callback and shows back button`, async () => {
|
|
108
|
-
await mockServer.sendText(`/start`, USER_1)
|
|
109
|
-
mockServer.clearRequests()
|
|
110
|
-
|
|
111
|
-
const responses = await mockServer.sendCallbackQuery(`help`, USER_1)
|
|
112
|
-
|
|
113
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
114
|
-
const edit = responses[0]!
|
|
115
|
-
expect(edit!.body?.text).toContain(`Help Message`)
|
|
116
|
-
|
|
117
|
-
const keyboard = edit!.body?.reply_markup?.inline_keyboard
|
|
118
|
-
expect(Array.isArray(keyboard)).toBe(true)
|
|
119
|
-
const firstButton = keyboard[0]?.[0]
|
|
120
|
-
expect(firstButton?.text).toContain(`Back`)
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it(`navigates to Random Number page via callback and shows generated number`, async () => {
|
|
124
|
-
await mockServer.sendText(`/start`, USER_1)
|
|
125
|
-
mockServer.clearRequests()
|
|
126
|
-
|
|
127
|
-
const responses = await mockServer.sendCallbackQuery(`random-number`, USER_1)
|
|
128
|
-
|
|
129
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
130
|
-
const edit = responses[0]!
|
|
131
|
-
expect(edit!.body?.text).toContain(`Random Number`)
|
|
132
|
-
|
|
133
|
-
const keyboard = edit!.body?.reply_markup?.inline_keyboard
|
|
134
|
-
expect(Array.isArray(keyboard)).toBe(true)
|
|
135
|
-
const allButtons = keyboard.flat()
|
|
136
|
-
const buttonLabels = allButtons.map((b: any) => b.text)
|
|
137
|
-
expect(buttonLabels).toContain(`🎲 Shake`)
|
|
138
|
-
expect(buttonLabels).toContain(`⬅️ Back`)
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
it(`refreshes random number via Shake callback`, async () => {
|
|
142
|
-
await mockServer.sendText(`/start`, USER_1)
|
|
143
|
-
mockServer.clearRequests()
|
|
144
|
-
|
|
145
|
-
await mockServer.sendCallbackQuery(`random-number`, USER_1)
|
|
146
|
-
mockServer.clearRequests()
|
|
147
|
-
|
|
148
|
-
const responses = await mockServer.sendCallbackQuery(`random-number`, USER_1)
|
|
149
|
-
|
|
150
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
151
|
-
const edit = responses[0]!
|
|
152
|
-
expect(edit!.body?.text).toContain(`Random Number`)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it(`completes Sum Number two-step session flow`, async () => {
|
|
156
|
-
await mockServer.sendText(`/start`, USER_1)
|
|
157
|
-
mockServer.clearRequests()
|
|
158
|
-
|
|
159
|
-
const sumResponses = await mockServer.sendCallbackQuery(`sum-number`, USER_1)
|
|
160
|
-
expect(sumResponses.length).toBeGreaterThan(0)
|
|
161
|
-
expect(sumResponses[0]!.body?.text).toContain(`Input Left Number`)
|
|
162
|
-
|
|
163
|
-
mockServer.clearRequests()
|
|
164
|
-
|
|
165
|
-
const leftResponses = await mockServer.sendText(`23`, USER_1)
|
|
166
|
-
expect(leftResponses.length).toBeGreaterThan(0)
|
|
167
|
-
expect(leftResponses[0]!.body?.text).toContain(`Input Right Number`)
|
|
168
|
-
|
|
169
|
-
mockServer.clearRequests()
|
|
170
|
-
|
|
171
|
-
const resultResponses = await mockServer.sendText(`10`, USER_1)
|
|
172
|
-
expect(resultResponses.length).toBeGreaterThan(0)
|
|
173
|
-
expect(resultResponses[0]!.body?.text).toContain(`Result Sum`)
|
|
174
|
-
expect(resultResponses[0]!.body?.text).toContain(`33`)
|
|
175
|
-
|
|
176
|
-
const keyboard = resultResponses[0]!.body?.reply_markup?.inline_keyboard
|
|
177
|
-
expect(Array.isArray(keyboard)).toBe(true)
|
|
178
|
-
const firstButton = keyboard[0]?.[0]
|
|
179
|
-
expect(firstButton?.text).toContain(`Home`)
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it(`handles invalid input in Sum Number gracefully`, async () => {
|
|
183
|
-
await mockServer.sendText(`/start`, USER_1)
|
|
184
|
-
mockServer.clearRequests()
|
|
185
|
-
|
|
186
|
-
await mockServer.sendCallbackQuery(`sum-number`, USER_1)
|
|
187
|
-
mockServer.clearRequests()
|
|
188
|
-
|
|
189
|
-
const leftResponses = await mockServer.sendText(`not-a-number`, USER_1)
|
|
190
|
-
expect(leftResponses.length).toBeGreaterThan(0)
|
|
191
|
-
expect(leftResponses[0]!.body?.text).toContain(`Input Left Number`)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it(`shows route not found for unrecognized callback`, async () => {
|
|
195
|
-
await mockServer.sendText(`/start`, USER_1)
|
|
196
|
-
mockServer.clearRequests()
|
|
197
|
-
|
|
198
|
-
const responses = await mockServer.sendCallbackQuery(`nonexistent_action_${Math.random()}`, USER_1)
|
|
199
|
-
expect(responses.length).toBeGreaterThanOrEqual(0)
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it(`captures outbound requests in mockServer.requests`, async () => {
|
|
203
|
-
const responses = await mockServer.sendText(`/start`, USER_1)
|
|
204
|
-
|
|
205
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
206
|
-
const sendMessageReq = mockServer.lastRequest(`sendMessage`)
|
|
207
|
-
expect(sendMessageReq).toBeDefined()
|
|
208
|
-
expect(sendMessageReq?.body?.text).toContain(`Welcome to the bot`)
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
it(`handles multiple sequential interactions`, async () => {
|
|
212
|
-
const res1 = await mockServer.sendText(`/start`, USER_1)
|
|
213
|
-
expect(res1[0]?.body?.text).toContain(`Welcome to the bot`)
|
|
214
|
-
|
|
215
|
-
mockServer.clearRequests()
|
|
216
|
-
|
|
217
|
-
const res2 = await mockServer.sendCallbackQuery(`help`, USER_1)
|
|
218
|
-
expect(res2[0]?.body?.text).toContain(`Help Message`)
|
|
219
|
-
|
|
220
|
-
mockServer.clearRequests()
|
|
221
|
-
|
|
222
|
-
const res3 = await mockServer.sendCallbackQuery(`random-number`, USER_1)
|
|
223
|
-
expect(res3[0]?.body?.text).toContain(`Random Number`)
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
it(`sendPhoto triggers a response`, async () => {
|
|
227
|
-
const responses = await mockServer.sendPhoto({caption: `test photo`, ...USER_1})
|
|
228
|
-
|
|
229
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
230
|
-
expect(responses[0]?.body?.text).toContain(`Route Not Found`)
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
it(`supports custom user and chat options`, async () => {
|
|
234
|
-
const responses = await mockServer.sendText(`/start`, {
|
|
235
|
-
user: {id: 99999, first_name: `CustomUser`, username: `customuser`},
|
|
236
|
-
chat: {id: 88888, type: `private`, first_name: `CustomChat`}
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
240
|
-
expect(responses[0]?.body?.text).toContain(`Welcome to the bot`)
|
|
241
|
-
|
|
242
|
-
const lastUpdate = mockServer.updates[mockServer.updates.length - 1]
|
|
243
|
-
expect(lastUpdate).toBeDefined()
|
|
244
|
-
const msg = (lastUpdate as any)?.message
|
|
245
|
-
expect(msg?.from?.id).toBe(99999)
|
|
246
|
-
expect(msg?.chat?.id).toBe(88888)
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
it(`blocks user without username and shows username required page`, async () => {
|
|
250
|
-
const responses = await mockServer.sendText(`/start`, {
|
|
251
|
-
user: {id: 77777, first_name: `NoUsername`},
|
|
252
|
-
chat: {id: 77777, type: `private`, first_name: `NoUsername`}
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
256
|
-
const reply = responses[0]!
|
|
257
|
-
expect(reply.body?.text).toContain(`Username Required`)
|
|
258
|
-
expect(reply.body?.text).toContain(`set the username first`)
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
it(`validates inline_keyboard structure for all navigation pages`, async () => {
|
|
262
|
-
const res1 = await mockServer.sendText(`/start`, USER_1)
|
|
263
|
-
expect(Array.isArray(res1[0]?.body?.reply_markup?.inline_keyboard)).toBe(true)
|
|
264
|
-
|
|
265
|
-
mockServer.clearRequests()
|
|
266
|
-
|
|
267
|
-
const res2 = await mockServer.sendCallbackQuery(`help`, USER_1)
|
|
268
|
-
const kb2 = res2[0]?.body?.reply_markup?.inline_keyboard
|
|
269
|
-
expect(Array.isArray(kb2)).toBe(true)
|
|
270
|
-
expect(kb2!.length).toBeGreaterThan(0)
|
|
271
|
-
expect(kb2![0]?.[0]?.text).toBeDefined()
|
|
272
|
-
expect(kb2![0]?.[0]?.callback_data).toBeDefined()
|
|
273
|
-
|
|
274
|
-
mockServer.clearRequests()
|
|
275
|
-
|
|
276
|
-
const res3 = await mockServer.sendCallbackQuery(`random-number`, USER_1)
|
|
277
|
-
const kb3 = res3[0]?.body?.reply_markup?.inline_keyboard
|
|
278
|
-
expect(Array.isArray(kb3)).toBe(true)
|
|
279
|
-
expect(kb3!.length).toBeGreaterThan(0)
|
|
280
|
-
for (const row of kb3!) {
|
|
281
|
-
for (const btn of row) {
|
|
282
|
-
expect(btn?.text).toBeDefined()
|
|
283
|
-
expect(btn?.callback_data).toBeDefined()
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
})
|
|
287
|
-
})
|