@adriankulik/create-fullstack-app 1.2.0 → 1.4.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.
Files changed (35) hide show
  1. package/README.md +13 -7
  2. package/cli/index.js +165 -75
  3. package/cli/logo.js +11 -0
  4. package/package.json +4 -4
  5. package/templates/backend/dotnet/Program.cs +32 -0
  6. package/templates/backend/dotnet/Properties/launchSettings.json +41 -0
  7. package/templates/backend/dotnet/Tests/GlobalUsings.cs +1 -0
  8. package/templates/backend/dotnet/Tests/Tests.csproj +30 -0
  9. package/templates/backend/dotnet/Tests/UnitTest1.cs +43 -0
  10. package/templates/backend/dotnet/Tests/obj/Tests.csproj.nuget.dgspec.json +147 -0
  11. package/templates/backend/dotnet/Tests/obj/Tests.csproj.nuget.g.props +26 -0
  12. package/templates/backend/dotnet/Tests/obj/Tests.csproj.nuget.g.targets +15 -0
  13. package/templates/backend/dotnet/Tests/obj/project.assets.json +7485 -0
  14. package/templates/backend/dotnet/Tests/obj/project.nuget.cache +132 -0
  15. package/templates/backend/dotnet/appsettings.Development.json +8 -0
  16. package/templates/backend/dotnet/appsettings.json +9 -0
  17. package/templates/backend/dotnet/backend.sln +28 -0
  18. package/templates/backend/dotnet/dotnet.csproj +17 -0
  19. package/templates/backend/dotnet/dotnet.http +6 -0
  20. package/templates/backend/dotnet/lint.sh +2 -0
  21. package/templates/backend/dotnet/start.sh +2 -0
  22. package/templates/backend/dotnet/test.sh +2 -0
  23. package/templates/backend/fastapi/main.py +1 -3
  24. package/templates/backend/fastapi/test_main.py +0 -4
  25. package/templates/backend/flask/main.py +1 -3
  26. package/templates/backend/flask/test_main.py +0 -4
  27. package/templates/backend/nodejs/.eslintrc.json +15 -0
  28. package/templates/backend/nodejs/lint.sh +2 -0
  29. package/templates/backend/nodejs/package-lock.json +5227 -0
  30. package/templates/backend/nodejs/package.json +27 -0
  31. package/templates/backend/nodejs/src/index.test.ts +22 -0
  32. package/templates/backend/nodejs/src/index.ts +23 -0
  33. package/templates/backend/nodejs/start.sh +2 -0
  34. package/templates/backend/nodejs/test.sh +2 -0
  35. package/templates/backend/nodejs/tsconfig.json +14 -0
package/README.md CHANGED
@@ -5,6 +5,7 @@ A CLI tool for scaffolding a full-stack web application with your choice of fron
5
5
  ## Prerequisites
6
6
 
7
7
  Before running the CLI or the scaffolded applications, ensure you have the following installed on your system:
8
+
8
9
  - **Node.js**: v20 or newer
9
10
  - **npm**: v10 or newer
10
11
  - **Python**: v3.11 or newer
@@ -17,7 +18,7 @@ To generate a new full-stack application, simply run:
17
18
  npx @adriankulik/create-fullstack-app@latest
18
19
  ```
19
20
 
20
- *(If you are developing locally, run `npm install` in the root directory and execute `node cli/index.js`).*
21
+ _(If you are developing locally, run `npm install` in the root directory and execute `node cli/index.js`)._
21
22
 
22
23
  You will be prompted to choose a project name, your preferred frontend, and backend framework.
23
24
 
@@ -25,15 +26,18 @@ You will be prompted to choose a project name, your preferred frontend, and back
25
26
 
26
27
  - **Frontend Options**:
27
28
  - Next.js (v16.2.4)
28
- - Angular (v21.0.0) - *Configured with Zone.js*
29
+ - Angular (v21.0.0) - _Configured with Zone.js_
29
30
  - Vue (v3.5.33)
30
31
  - Svelte (v5.55.5)
31
- - **Backend Options**:
32
+ - **Backend Options**:
32
33
  - FastAPI (v0.136.1)
33
34
  - Flask (v3.1.3)
35
+ - .NET (v8.0)
36
+ - Node.js Express (v4.19.2)
37
+ - Rust Rocket (v0.5.0)
34
38
  - **Opinionated Defaults**: We bake in sensible, predefined choices (e.g., Angular uses `zone.js`, testing is unified under Playwright and framework-native test runners).
35
39
  - **Unified Scripts**: `start.sh`, `test.sh`, and `lint.sh` available out of the box to manage both frontend and backend seamlessly.
36
- - **CI/CD Ready**: Includes a pre-configured `.github/workflows/cli-e2e.yml` that tests both ends. *Tip: To enforce this, enable branch protection in your GitHub repository settings and require the "test" status check to pass.*
40
+ - **CI/CD Ready**: Includes a pre-configured `.github/workflows/cli-e2e.yml` that tests both ends. _Tip: To enforce this, enable branch protection in your GitHub repository settings and require the "test" status check to pass._
37
41
  - **Developer Experience**: Includes `.vscode/extensions.json` recommending the necessary linters and formatters, and an `AGENTS.md` file providing behavioral guidelines for AI agents (based on [andrej-karpathy-skills](https://github.com/forrestchang/andrej-karpathy-skills)). It is AI development friendly, including an extensive set of agent skills in `.agents/skills` sourced from [claude-skills](https://github.com/alirezarezvani/claude-skills).
38
42
  <details>
39
43
  <summary>Included Agent Skills</summary>
@@ -60,7 +64,7 @@ my-app/
60
64
  └── lint.sh
61
65
  ```
62
66
 
63
- *Note: Depending on your environment, you may need to ensure the generated shell scripts are executable by running `chmod +x *.sh` inside your new project directory.*
67
+ \_Note: Depending on your environment, you may need to ensure the generated shell scripts are executable by running `chmod +x *.sh` inside your new project directory.\
64
68
 
65
69
  ## Local E2E Testing
66
70
 
@@ -85,11 +89,10 @@ Then run it:
85
89
  Future plans for this CLI include:
86
90
 
87
91
  - Adding frontend framework-specific component libraries during the scaffold step.
88
- - Tackling **.NET** as a new backend option.
89
92
 
90
93
  ## Acknowledgements
91
94
 
92
- This project redistributes boilerplate code and configuration files generated by the official CLIs and templates of the integrated frameworks. We are grateful to the maintainers of these projects:
95
+ This project redistributes boilerplate code and configuration files generated by the official CLIs and templates of the integrated frameworks. I'm grateful to the maintainers of these projects:
93
96
 
94
97
  - [Next.js](https://nextjs.org/) (MIT License)
95
98
  - [Angular](https://angular.dev/) (MIT License)
@@ -97,3 +100,6 @@ This project redistributes boilerplate code and configuration files generated by
97
100
  - [Svelte](https://svelte.dev/) (MIT License)
98
101
  - [FastAPI](https://fastapi.tiangolo.com/) (MIT License)
99
102
  - [Flask](https://flask.palletsprojects.com/) (BSD 3-Clause License)
103
+ - [.NET](https://dotnet.microsoft.com/) (MIT License)
104
+ - [Express](https://expressjs.com/) (MIT License)
105
+ - [Rocket](https://rocket.rs/) (MIT License)
package/cli/index.js CHANGED
@@ -1,61 +1,104 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const prompts = require('prompts');
4
- const pc = require('picocolors');
5
- const fs = require('fs-extra');
6
- const path = require('path');
7
- const { execSync } = require('child_process');
3
+ const prompts = require("prompts");
4
+ const pc = require("picocolors");
5
+ const fs = require("fs-extra");
6
+ const path = require("path");
7
+ const { execSync } = require("child_process");
8
8
 
9
9
  async function main() {
10
- console.log(pc.cyan('\nWelcome to create-fullstack-app!\n'));
10
+ const { asciiArt } = require("./logo");
11
+ console.log(pc.cyan(asciiArt));
12
+ console.log(pc.cyan("\nWelcome to create-fullstack-app!\n"));
11
13
 
12
14
  // Simple argument parsing
13
15
  const args = process.argv.slice(2);
14
- let argProjectName = args[0] && !args[0].startsWith('--') ? args[0] : null;
16
+ let argProjectName = args[0] && !args[0].startsWith("--") ? args[0] : null;
15
17
  let argFrontend = null;
16
18
  let argBackend = null;
17
19
 
18
20
  for (let i = 0; i < args.length; i++) {
19
- if (args[i] === '--frontend' && args[i + 1]) argFrontend = args[i + 1];
20
- if (args[i] === '--backend' && args[i + 1]) argBackend = args[i + 1];
21
+ if (args[i] === "--frontend" && args[i + 1]) argFrontend = args[i + 1];
22
+ if (args[i] === "--backend" && args[i + 1]) argBackend = args[i + 1];
21
23
  }
22
24
 
23
25
  const questions = [];
24
26
  if (!argProjectName) {
25
27
  questions.push({
26
- type: 'text',
27
- name: 'projectName',
28
- message: 'What is your project named?',
29
- initial: 'my-fullstack-app',
30
- validate: value => value.trim().length > 0 ? true : 'Project name cannot be empty'
28
+ type: "text",
29
+ name: "projectName",
30
+ message: "What is your project named?",
31
+ initial: "my-fullstack-app",
32
+ validate: (value) =>
33
+ value.trim().length > 0 ? true : "Project name cannot be empty",
31
34
  });
32
35
  }
33
36
 
34
37
  if (!argFrontend) {
35
38
  questions.push({
36
- type: 'select',
37
- name: 'frontend',
38
- message: 'Which frontend framework would you like to use?',
39
+ type: "select",
40
+ name: "frontend",
41
+ message: "Which frontend framework would you like to use?",
39
42
  choices: [
40
- { title: 'Next.js', value: 'nextjs', description: 'React framework' },
41
- { title: 'Angular', value: 'angular', description: 'Enterprise-grade platform' },
42
- { title: 'Vue', value: 'vue', description: 'Progressive JavaScript framework' },
43
- { title: 'Svelte', value: 'svelte', description: 'Cybernetically enhanced web apps' }
43
+ { title: "Next.js", value: "nextjs", description: "React framework" },
44
+ {
45
+ title: "Angular",
46
+ value: "angular",
47
+ description: "Enterprise-grade platform",
48
+ },
49
+ {
50
+ title: "Vue",
51
+ value: "vue",
52
+ description: "Progressive JavaScript framework",
53
+ },
54
+ {
55
+ title: "Svelte",
56
+ value: "svelte",
57
+ description: "Cybernetically enhanced web apps",
58
+ },
44
59
  ],
45
- initial: 0
60
+ initial: 0,
46
61
  });
47
62
  }
48
63
 
49
64
  if (!argBackend) {
50
65
  questions.push({
51
- type: 'select',
52
- name: 'backend',
53
- message: 'Which backend framework would you like to use?',
66
+ type: "select",
67
+ name: "backend",
68
+ message: "Which backend framework would you like to use?",
54
69
  choices: [
55
- { title: 'FastAPI', value: 'fastapi', description: 'Modern, fast Python web framework' },
56
- { title: 'Flask', value: 'flask', description: 'Lightweight WSGI web application framework' }
70
+ {
71
+ title: "FastAPI",
72
+ value: "fastapi",
73
+ description: "Modern, fast Python web framework",
74
+ },
75
+ {
76
+ title: "Flask",
77
+ value: "flask",
78
+ description: "Lightweight Python WSGI web application framework",
79
+ },
80
+ {
81
+ title: ".NET",
82
+ value: "dotnet",
83
+ description: "Robust, C#, high-performance framework by Microsoft",
84
+ },
85
+ {
86
+ title: "Spring Boot",
87
+ value: "spring-boot",
88
+ description: "Powerful, Java, enterprise-ready framework",
89
+ },
90
+ {
91
+ title: "Node.js (Express)",
92
+ value: "nodejs",
93
+ description: "Minimalist web framework for Node.js",
94
+ },
95
+ {
96
+ title: "Rust (Rocket)",
97
+ value: "rust",
98
+ description: "Web framework for Rust with focus on ease of use",
99
+ }
57
100
  ],
58
- initial: 0
101
+ initial: 0,
59
102
  });
60
103
  }
61
104
 
@@ -66,7 +109,7 @@ async function main() {
66
109
  const backend = argBackend || response.backend;
67
110
 
68
111
  if (!projectName || !frontend || !backend) {
69
- console.log(pc.red('Setup cancelled.'));
112
+ console.log(pc.red("Setup cancelled."));
70
113
  process.exit(1);
71
114
  }
72
115
  const targetDir = path.resolve(process.cwd(), projectName);
@@ -79,28 +122,31 @@ async function main() {
79
122
  console.log(pc.blue(`\nScaffolding project in ${targetDir}...`));
80
123
 
81
124
  // Determine paths to templates relative to the CLI script
82
- const templatesDir = path.resolve(__dirname, '../templates');
83
- const baseDir = path.join(templatesDir, 'base');
84
- const frontendDir = path.join(templatesDir, 'frontend', frontend);
85
- const backendDir = path.join(templatesDir, 'backend', backend);
125
+ const templatesDir = path.resolve(__dirname, "../templates");
126
+ const baseDir = path.join(templatesDir, "base");
127
+ const frontendDir = path.join(templatesDir, "frontend", frontend);
128
+ const backendDir = path.join(templatesDir, "backend", backend);
86
129
 
87
130
  try {
88
131
  // 1. Copy base template
89
132
  await fs.copy(baseDir, targetDir);
90
- if (fs.existsSync(path.join(targetDir, 'gitignore'))) {
91
- await fs.rename(path.join(targetDir, 'gitignore'), path.join(targetDir, '.gitignore'));
133
+ if (fs.existsSync(path.join(targetDir, "gitignore"))) {
134
+ await fs.rename(
135
+ path.join(targetDir, "gitignore"),
136
+ path.join(targetDir, ".gitignore"),
137
+ );
92
138
  }
93
139
 
94
140
  // 2. Copy frontend
95
- const targetFrontend = path.join(targetDir, 'frontend');
141
+ const targetFrontend = path.join(targetDir, "frontend");
96
142
  await fs.copy(frontendDir, targetFrontend);
97
143
 
98
144
  // 3. Copy backend
99
- const targetBackend = path.join(targetDir, 'backend');
145
+ const targetBackend = path.join(targetDir, "backend");
100
146
  await fs.copy(backendDir, targetBackend);
101
147
 
102
148
  // 4. Update package.json name for the frontend
103
- const pkgPath = path.join(targetFrontend, 'package.json');
149
+ const pkgPath = path.join(targetFrontend, "package.json");
104
150
  if (fs.existsSync(pkgPath)) {
105
151
  const pkg = await fs.readJson(pkgPath);
106
152
  pkg.name = `${projectName}-frontend`;
@@ -110,11 +156,11 @@ async function main() {
110
156
  // 5. Make all shell scripts executable
111
157
  const makeExecutable = (dir) => {
112
158
  const files = fs.readdirSync(dir);
113
- files.forEach(file => {
159
+ files.forEach((file) => {
114
160
  const filePath = path.join(dir, file);
115
161
  if (fs.statSync(filePath).isDirectory()) {
116
162
  makeExecutable(filePath);
117
- } else if (file.endsWith('.sh')) {
163
+ } else if (file.endsWith(".sh")) {
118
164
  fs.chmodSync(filePath, 0o755);
119
165
  }
120
166
  });
@@ -122,58 +168,102 @@ async function main() {
122
168
  makeExecutable(targetDir);
123
169
 
124
170
  // 6. Install dependencies
125
- console.log(pc.blue('\nInstalling dependencies (this may take a minute)...'));
171
+ console.log(
172
+ pc.blue("\nInstalling dependencies (this may take a minute)..."),
173
+ );
126
174
 
127
175
  // Frontend installation
128
- console.log(pc.cyan(' Installing frontend dependencies...'));
129
- execSync('npm ci', { cwd: targetFrontend, stdio: 'inherit' });
176
+ console.log(pc.cyan(" Installing frontend dependencies..."));
177
+ execSync("npm ci", { cwd: targetFrontend, stdio: "inherit" });
130
178
 
131
- // Backend installation
132
- console.log(pc.cyan(' Setting up backend virtual environment...'));
133
- const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
134
- try {
135
- execSync(`${pythonCmd} -m venv venv`, { cwd: targetBackend, stdio: 'inherit' });
136
-
137
- const pipPath = process.platform === 'win32'
138
- ? path.join('venv', 'Scripts', 'pip')
139
- : path.join('venv', 'bin', 'pip');
140
-
141
- console.log(pc.cyan(' Installing backend dependencies...'));
142
- execSync(`${pipPath} install -r requirements.txt`, { cwd: targetBackend, stdio: 'inherit' });
143
- } catch (e) {
144
- console.log(pc.yellow(' Could not set up Python virtual environment automatically. Please set it up manually.'));
179
+ if (backend === "fastapi" || backend === "flask") {
180
+ console.log(pc.cyan(" Setting up backend virtual environment..."));
181
+ const pythonCmd = process.platform === "win32" ? "python" : "python3";
182
+ try {
183
+ execSync(`${pythonCmd} -m venv venv`, {
184
+ cwd: targetBackend,
185
+ stdio: "inherit",
186
+ });
187
+
188
+ const pipPath =
189
+ process.platform === "win32"
190
+ ? path.join("venv", "Scripts", "pip")
191
+ : path.join("venv", "bin", "pip");
192
+
193
+ console.log(pc.cyan(" Installing backend dependencies..."));
194
+ execSync(`${pipPath} install -r requirements.txt`, {
195
+ cwd: targetBackend,
196
+ stdio: "inherit",
197
+ });
198
+ } catch (e) {
199
+ console.log(
200
+ pc.yellow(
201
+ " Could not set up Python virtual environment automatically. Please set it up manually.",
202
+ ),
203
+ );
204
+ }
205
+ } else if (backend === "dotnet") {
206
+ console.log(pc.cyan(" Restoring .NET dependencies..."));
207
+ try {
208
+ execSync("dotnet restore backend.sln", { cwd: targetBackend, stdio: "inherit" });
209
+ } catch (e) {
210
+ console.log(
211
+ pc.yellow(
212
+ " Could not restore .NET dependencies automatically. Please make sure .NET SDK is installed.",
213
+ ),
214
+ );
215
+ }
216
+ } else if (backend === "spring-boot") {
217
+ console.log(
218
+ pc.cyan(
219
+ " Spring Boot project created. Dependencies will be downloaded via Maven wrapper on first run.",
220
+ ),
221
+ );
222
+ } else if (backend === "nodejs") {
223
+ console.log(pc.cyan(" Installing Node.js backend dependencies..."));
224
+ execSync("npm ci", { cwd: targetBackend, stdio: "inherit" });
225
+ } else if (backend === "rust") {
226
+ console.log(pc.cyan(" Rust backend created. Cargo will manage dependencies automatically on first run."));
145
227
  }
146
228
 
147
229
  // 7. Initialize Git repository
148
- console.log(pc.blue('\nInitializing Git repository...'));
230
+ console.log(pc.blue("\nInitializing Git repository..."));
149
231
  try {
150
- execSync('git init', { cwd: targetDir, stdio: 'ignore' });
151
- execSync('git add .', { cwd: targetDir, stdio: 'ignore' });
152
- execSync('git commit -m "Initial commit from create-fullstack-app"', { cwd: targetDir, stdio: 'ignore' });
153
- console.log(pc.cyan(' Git repository initialized.'));
232
+ execSync("git init", { cwd: targetDir, stdio: "ignore" });
233
+ execSync("git add .", { cwd: targetDir, stdio: "ignore" });
234
+ execSync('git commit -m "Initial commit from create-fullstack-app"', {
235
+ cwd: targetDir,
236
+ stdio: "ignore",
237
+ });
238
+ console.log(pc.cyan(" Git repository initialized."));
154
239
  } catch (e) {
155
- console.log(pc.yellow(' Could not initialize Git repository automatically. You may need to run git init yourself.'));
240
+ console.log(
241
+ pc.yellow(
242
+ " Could not initialize Git repository automatically. You may need to run git init yourself.",
243
+ ),
244
+ );
156
245
  }
157
246
 
158
247
  console.log(pc.green(`\nSuccess! Created ${projectName} at ${targetDir}`));
159
- console.log('\nInside that directory, you can run several commands:\n');
248
+ console.log("\nInside that directory, you can run several commands:\n");
160
249
 
161
- console.log(` ${pc.cyan('./start.sh')}`);
162
- console.log(' Starts both the backend and frontend development servers.\n');
250
+ console.log(` ${pc.cyan("./start.sh")}`);
251
+ console.log(
252
+ " Starts both the backend and frontend development servers.\n",
253
+ );
163
254
 
164
- console.log(` ${pc.cyan('./test.sh')}`);
165
- console.log(' Runs tests for both frontend and backend.\n');
255
+ console.log(` ${pc.cyan("./test.sh")}`);
256
+ console.log(" Runs tests for both frontend and backend.\n");
166
257
 
167
- console.log(` ${pc.cyan('./lint.sh')}`);
168
- console.log(' Lints and formats both frontend and backend code.\n');
258
+ console.log(` ${pc.cyan("./lint.sh")}`);
259
+ console.log(" Lints and formats both frontend and backend code.\n");
169
260
 
170
- console.log('\nWe suggest that you begin by typing:\n');
261
+ console.log("\nWe suggest that you begin by typing:\n");
171
262
  console.log(pc.cyan(` cd ${projectName}`));
172
- console.log(pc.cyan(' ./start.sh'));
173
- console.log('\nHappy coding!');
174
-
263
+ console.log(pc.cyan(" ./start.sh"));
264
+ console.log("\nHappy coding!");
175
265
  } catch (error) {
176
- console.error(pc.red('Error scaffolding project:'), error);
266
+ console.error(pc.red("Error scaffolding project:"), error);
177
267
  process.exit(1);
178
268
  }
179
269
  }
package/cli/logo.js ADDED
@@ -0,0 +1,11 @@
1
+ module.exports = {
2
+ asciiArt: `
3
+ █▒▒ █▒▒▒▒▒▒▒▒ █▒
4
+ █▒▒ █▒▒ █▒▒ █▒ ▒▒
5
+ █▒▒ █▒▒ █▒ █▒▒
6
+ █▒▒ █▒▒▒▒▒▒ █▒▒ █▒▒
7
+ █▒▒ █▒▒ █▒▒▒▒▒▒ █▒▒
8
+ █▒▒ █▒▒ █▒▒ █▒▒ █▒▒
9
+ █▒▒▒▒ █▒▒ █▒▒ █▒▒
10
+ `,
11
+ };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.0",
6
+ "version": "1.4.0",
7
7
  "description": "A CLI tool for scaffolding a full-stack web application with your choice of frontend and backend technologies.",
8
8
  "main": "cli/index.js",
9
9
  "bin": {
@@ -22,9 +22,9 @@
22
22
  "prompts": "^2.4.2"
23
23
  },
24
24
  "devDependencies": {
25
- "@playwright/test": "^1.59.1",
25
+ "@playwright/test": "^1.60.0",
26
26
  "vitest": "^4.1.5",
27
- "wait-on": "^9.0.5"
27
+ "wait-on": "^9.0.10"
28
28
  },
29
29
  "scripts": {
30
30
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "keywords": [],
37
37
  "author": "",
38
- "license": "ISC",
38
+ "license": "MIT",
39
39
  "bugs": {
40
40
  "url": "https://github.com/adriankulik/create-fullstack-app/issues"
41
41
  },
@@ -0,0 +1,32 @@
1
+ using Microsoft.AspNetCore.Mvc;
2
+
3
+ var builder = WebApplication.CreateBuilder(args);
4
+
5
+ // Add CORS to allow frontend to access
6
+ builder.Services.AddCors(options =>
7
+ {
8
+ options.AddDefaultPolicy(policy =>
9
+ {
10
+ policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
11
+ });
12
+ });
13
+
14
+ var app = builder.Build();
15
+
16
+ app.UseCors();
17
+
18
+ app.MapPost("/api/multiply", ([FromBody] MultiplyRequest request) =>
19
+ {
20
+ return new { result = request.Number * 2 };
21
+ });
22
+
23
+
24
+
25
+ app.Run("http://localhost:8000");
26
+
27
+ class MultiplyRequest
28
+ {
29
+ public double Number { get; set; }
30
+ }
31
+
32
+ public partial class Program { }
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "http://json.schemastore.org/launchsettings.json",
3
+ "iisSettings": {
4
+ "windowsAuthentication": false,
5
+ "anonymousAuthentication": true,
6
+ "iisExpress": {
7
+ "applicationUrl": "http://localhost:46754",
8
+ "sslPort": 44359
9
+ }
10
+ },
11
+ "profiles": {
12
+ "http": {
13
+ "commandName": "Project",
14
+ "dotnetRunMessages": true,
15
+ "launchBrowser": true,
16
+ "launchUrl": "weatherforecast",
17
+ "applicationUrl": "http://localhost:5028",
18
+ "environmentVariables": {
19
+ "ASPNETCORE_ENVIRONMENT": "Development"
20
+ }
21
+ },
22
+ "https": {
23
+ "commandName": "Project",
24
+ "dotnetRunMessages": true,
25
+ "launchBrowser": true,
26
+ "launchUrl": "weatherforecast",
27
+ "applicationUrl": "https://localhost:7200;http://localhost:5028",
28
+ "environmentVariables": {
29
+ "ASPNETCORE_ENVIRONMENT": "Development"
30
+ }
31
+ },
32
+ "IIS Express": {
33
+ "commandName": "IISExpress",
34
+ "launchBrowser": true,
35
+ "launchUrl": "weatherforecast",
36
+ "environmentVariables": {
37
+ "ASPNETCORE_ENVIRONMENT": "Development"
38
+ }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1 @@
1
+ global using Xunit;
@@ -0,0 +1,30 @@
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+
3
+ <PropertyGroup>
4
+ <TargetFramework>net8.0</TargetFramework>
5
+ <ImplicitUsings>enable</ImplicitUsings>
6
+ <Nullable>enable</Nullable>
7
+
8
+ <IsPackable>false</IsPackable>
9
+ <IsTestProject>true</IsTestProject>
10
+ </PropertyGroup>
11
+
12
+ <ItemGroup>
13
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
14
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
15
+ <PackageReference Include="xunit" Version="2.4.2" />
16
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
17
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
18
+ <PrivateAssets>all</PrivateAssets>
19
+ </PackageReference>
20
+ <PackageReference Include="coverlet.collector" Version="6.0.0">
21
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
22
+ <PrivateAssets>all</PrivateAssets>
23
+ </PackageReference>
24
+ </ItemGroup>
25
+
26
+ <ItemGroup>
27
+ <ProjectReference Include="..\dotnet.csproj" />
28
+ </ItemGroup>
29
+
30
+ </Project>
@@ -0,0 +1,43 @@
1
+ using System.Net.Http.Json;
2
+ using Microsoft.AspNetCore.Mvc.Testing;
3
+
4
+ namespace Tests;
5
+
6
+ public class UnitTest1 : IClassFixture<WebApplicationFactory<Program>>
7
+ {
8
+ private readonly WebApplicationFactory<Program> _factory;
9
+
10
+ public UnitTest1(WebApplicationFactory<Program> factory)
11
+ {
12
+ _factory = factory;
13
+ }
14
+
15
+ [Fact]
16
+ public async Task MultiplyEndpoint_ReturnsDouble()
17
+ {
18
+ var client = _factory.CreateClient();
19
+ var request = new { number = 5 };
20
+
21
+ var response = await client.PostAsJsonAsync("/api/multiply", request);
22
+ response.EnsureSuccessStatusCode();
23
+
24
+ var result = await response.Content.ReadFromJsonAsync<MultiplyResponse>();
25
+ Assert.NotNull(result);
26
+ Assert.Equal(10, result.result);
27
+ }
28
+
29
+ [Fact]
30
+ public async Task MultiplyEndpoint_Returns400_ForInvalidInput()
31
+ {
32
+ var client = _factory.CreateClient();
33
+ var request = new { number = "not a number" };
34
+
35
+ var response = await client.PostAsJsonAsync("/api/multiply", request);
36
+ Assert.Equal(System.Net.HttpStatusCode.BadRequest, response.StatusCode);
37
+ }
38
+
39
+ class MultiplyResponse
40
+ {
41
+ public double result { get; set; }
42
+ }
43
+ }