Compare commits
2 Commits
a24e901b7f
...
fd3f1a6db7
| Author | SHA1 | Date | |
|---|---|---|---|
| fd3f1a6db7 | |||
| c065cbf61e |
3
.gitconfig
Normal file
3
.gitconfig
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[user]
|
||||||
|
email = chloestanton117@gmail.com
|
||||||
|
name = chloe
|
||||||
30
.gitignore
vendored
30
.gitignore
vendored
@@ -1,24 +1,8 @@
|
|||||||
# Nuxt dev/build outputs
|
bin/
|
||||||
.output
|
obj/
|
||||||
.data
|
Seasoned.Frontend/node_modules/
|
||||||
.nuxt
|
.nuxt/
|
||||||
.nitro
|
.output/
|
||||||
.cache
|
dist/
|
||||||
dist
|
.nvm/
|
||||||
|
|
||||||
# Node dependencies
|
|
||||||
node_modules
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Misc
|
|
||||||
.DS_Store
|
|
||||||
.fleet
|
|
||||||
.idea
|
|
||||||
|
|
||||||
# Local env files
|
|
||||||
.env
|
.env
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|||||||
30
Seasoned.Backend/Controllers/RecipeController.cs
Normal file
30
Seasoned.Backend/Controllers/RecipeController.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Seasoned.Backend.Services;
|
||||||
|
using Seasoned.Backend.DTOs;
|
||||||
|
|
||||||
|
namespace Seasoned.Backend.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class RecipeController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IRecipeService _recipeService;
|
||||||
|
|
||||||
|
// Dependency Injection: The service is "injected" here
|
||||||
|
public RecipeController(IRecipeService recipeService)
|
||||||
|
{
|
||||||
|
_recipeService = recipeService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("upload")]
|
||||||
|
public async Task<ActionResult<RecipeResponseDto>> UploadRecipe([FromForm] IFormFile image)
|
||||||
|
{
|
||||||
|
if (image == null || image.Length == 0)
|
||||||
|
{
|
||||||
|
return BadRequest("No image uploaded.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _recipeService.ParseRecipeImageAsync(image);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Seasoned.Backend/DTOs/RecipeResponseDto.cs
Normal file
9
Seasoned.Backend/DTOs/RecipeResponseDto.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Seasoned.Backend.DTOs;
|
||||||
|
|
||||||
|
public class RecipeResponseDto
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public List<string> Ingredients { get; set; } = new();
|
||||||
|
public List<string> Instructions { get; set; } = new();
|
||||||
|
}
|
||||||
29
Seasoned.Backend/Program.cs
Normal file
29
Seasoned.Backend/Program.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using Seasoned.Backend.Services;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Services.AddScoped<IRecipeService, RecipeService>();
|
||||||
|
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AllowAll", policy =>
|
||||||
|
{
|
||||||
|
policy.AllowAnyOrigin()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowAnyHeader();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
app.UseCors("AllowAll");
|
||||||
|
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.MapOpenApi();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
app.Run();
|
||||||
14
Seasoned.Backend/Properties/launchSettings.json
Normal file
14
Seasoned.Backend/Properties/launchSettings.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"applicationUrl": "http://localhost:5243",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Seasoned.Backend/Seasoned.Backend.csproj
Normal file
15
Seasoned.Backend/Seasoned.Backend.csproj
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UserSecretsId>71e44981-0c1f-4d90-8e87-317af8bd03ce</UserSecretsId>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.13" />
|
||||||
|
<PackageReference Include="Mscc.GenerativeAI" Version="2.2.8" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
6
Seasoned.Backend/Seasoned.Backend.http
Normal file
6
Seasoned.Backend/Seasoned.Backend.http
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@Seasoned.Backend_HostAddress = http://localhost:5243
|
||||||
|
|
||||||
|
GET {{Seasoned.Backend_HostAddress}}/weatherforecast/
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
8
Seasoned.Backend/Services/IRecipeService.cs
Normal file
8
Seasoned.Backend/Services/IRecipeService.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using Seasoned.Backend.DTOs;
|
||||||
|
|
||||||
|
namespace Seasoned.Backend.Services;
|
||||||
|
|
||||||
|
public interface IRecipeService
|
||||||
|
{
|
||||||
|
Task<RecipeResponseDto> ParseRecipeImageAsync(IFormFile image);
|
||||||
|
}
|
||||||
49
Seasoned.Backend/Services/RecipeService.cs
Normal file
49
Seasoned.Backend/Services/RecipeService.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using Seasoned.Backend.DTOs;
|
||||||
|
using Mscc.GenerativeAI;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Seasoned.Backend.Services;
|
||||||
|
|
||||||
|
public class RecipeService : IRecipeService
|
||||||
|
{
|
||||||
|
private readonly string _apiKey;
|
||||||
|
|
||||||
|
public RecipeService(IConfiguration config)
|
||||||
|
{
|
||||||
|
_apiKey = config["GeminiApiKey"] ?? throw new ArgumentNullException("API Key missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RecipeResponseDto> ParseRecipeImageAsync(IFormFile image)
|
||||||
|
{
|
||||||
|
var googleAI = new GoogleAI(_apiKey);
|
||||||
|
var model = googleAI.GenerativeModel("gemini-2.5-flash");
|
||||||
|
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
await image.CopyToAsync(ms);
|
||||||
|
var base64Image = Convert.ToBase64String(ms.ToArray());
|
||||||
|
|
||||||
|
// 1. Better Prompt: Tell Gemini exactly what the JSON should look like
|
||||||
|
var prompt = @"Extract the recipe from this image.
|
||||||
|
Return a JSON object with exactly these fields:
|
||||||
|
{
|
||||||
|
""title"": ""string"",
|
||||||
|
""description"": ""string"",
|
||||||
|
""ingredients"": [""string""],
|
||||||
|
""instructions"": [""string""]
|
||||||
|
}";
|
||||||
|
|
||||||
|
// 2. Set the Response MIME Type to application/json
|
||||||
|
var config = new GenerationConfig { ResponseMimeType = "application/json" };
|
||||||
|
var request = new GenerateContentRequest(prompt, config);
|
||||||
|
request.AddMedia(base64Image, "image/png");
|
||||||
|
|
||||||
|
var response = await model.GenerateContent(request);
|
||||||
|
|
||||||
|
// 3. Use System.Text.Json to turn that string back into our DTO
|
||||||
|
var options = new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||||
|
var result = System.Text.Json.JsonSerializer.Deserialize<RecipeResponseDto>(response.Text, options);
|
||||||
|
|
||||||
|
return result ?? new RecipeResponseDto { Title = "Error parsing JSON" };
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Seasoned.Backend/appsettings.Development.json
Normal file
8
Seasoned.Backend/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Seasoned.Backend/appsettings.json
Normal file
9
Seasoned.Backend/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
24
Seasoned.Frontend/.gitignore
vendored
Normal file
24
Seasoned.Frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
1
Seasoned.Frontend/.nuxtrc
Normal file
1
Seasoned.Frontend/.nuxtrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
telemetry.enabled=false
|
||||||
14
Seasoned.Frontend/Seasoned.code-workspace
Normal file
14
Seasoned.Frontend/Seasoned.code-workspace
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../Seasoned.Backend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../Seasoned.Tests"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
110
Seasoned.Frontend/app/app.vue
Normal file
110
Seasoned.Frontend/app/app.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<v-main>
|
||||||
|
<v-container>
|
||||||
|
<v-card class="pa-5 mx-auto mt-10" max-width="500" elevation="10">
|
||||||
|
<v-card-title class="text-center">Seasoned AI</v-card-title>
|
||||||
|
|
||||||
|
<v-divider class="my-3"></v-divider>
|
||||||
|
|
||||||
|
<v-file-input
|
||||||
|
v-model="files"
|
||||||
|
label="Pick a recipe photo"
|
||||||
|
prepend-icon="mdi-camera"
|
||||||
|
variant="outlined"
|
||||||
|
accept="image/*"
|
||||||
|
></v-file-input>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
block
|
||||||
|
size="x-large"
|
||||||
|
:loading="loading"
|
||||||
|
@click="uploadImage"
|
||||||
|
>
|
||||||
|
Analyze Recipe
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<div v-if="recipe" class="mt-5">
|
||||||
|
<h2 class="text-h4 mb-4">{{ recipe.title }}</h2>
|
||||||
|
<p class="text-subtitle-1 mb-6 text-grey-darken-1">{{ recipe.description }}</p>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="5">
|
||||||
|
<h3 class="text-h6 mb-2">Ingredients</h3>
|
||||||
|
<v-list lines="one" variant="flat" class="bg-grey-lighten-4 rounded-lg">
|
||||||
|
<v-list-item v-for="(item, i) in recipe.ingredients" :key="i">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon icon="mdi-circle-small"></v-icon>
|
||||||
|
</template>
|
||||||
|
{{ item }}
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="7">
|
||||||
|
<h3 class="text-h6 mb-2">Instructions</h3>
|
||||||
|
<v-timeline side="end" align="start" density="compact">
|
||||||
|
<v-timeline-item
|
||||||
|
v-for="(step, i) in recipe.instructions"
|
||||||
|
:key="i"
|
||||||
|
dot-color="primary"
|
||||||
|
size="x-small"
|
||||||
|
>
|
||||||
|
<div class="text-body-1">{{ step }}</div>
|
||||||
|
</v-timeline-item>
|
||||||
|
</v-timeline>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</v-main>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import axios from 'axios'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const files = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const recipe = ref(null)
|
||||||
|
|
||||||
|
const uploadImage = async () => {
|
||||||
|
// 1. Debug: Check what Vuetify is actually giving us
|
||||||
|
console.log("Files variable:", files.value);
|
||||||
|
|
||||||
|
// Vuetify 3 v-file-input can return a single File or an Array of Files
|
||||||
|
// We need to ensure we have the actual File object
|
||||||
|
const fileToUpload = Array.isArray(files.value) ? files.value[0] : files.value;
|
||||||
|
|
||||||
|
if (!fileToUpload) {
|
||||||
|
alert("Please select a file first!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// 2. Append the file. The string 'image' MUST match your C# parameter name
|
||||||
|
formData.append('image', fileToUpload);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 3. Post with explicit multipart/form-data header (Axios usually does this, but let's be sure)
|
||||||
|
const response = await axios.post('http://localhost:5000/api/recipe/upload', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
recipe.value = response.data;
|
||||||
|
console.log("Success:", response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Detailed Error:", error.response?.data || error.message);
|
||||||
|
alert("Backend error: Check the browser console for details.");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,21 +1,38 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: '2025-07-15',
|
||||||
|
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
|
|
||||||
future: {
|
future: {
|
||||||
compatibilityVersion: 4,
|
compatibilityVersion: 4,
|
||||||
},
|
},
|
||||||
|
|
||||||
srcDir: 'app/',
|
srcDir: 'app/',
|
||||||
|
|
||||||
css: [
|
css: [
|
||||||
'vuetify/lib/styles/main.sass',
|
'vuetify/lib/styles/main.sass',
|
||||||
'@mdi/font/css/materialdesignicons.min.css',
|
'@mdi/font/css/materialdesignicons.min.css',
|
||||||
],
|
],
|
||||||
|
|
||||||
build: {
|
build: {
|
||||||
transpile: ['vuetify'],
|
transpile: ['vuetify'],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Environment Variables for Gemini
|
modules: [
|
||||||
|
'vuetify-nuxt-module'
|
||||||
|
],
|
||||||
|
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
geminiApiKey: '',
|
geminiApiKey: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
vite: {
|
||||||
|
server: {
|
||||||
|
hmr: {
|
||||||
|
protocol: 'ws',
|
||||||
|
host: 'localhost',
|
||||||
|
port: 3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
2937
package-lock.json → Seasoned.Frontend/package-lock.json
generated
2937
package-lock.json → Seasoned.Frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,21 +7,29 @@
|
|||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare",
|
||||||
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/generative-ai": "^0.24.1",
|
"@google/generative-ai": "^0.24.1",
|
||||||
"@mdi/font": "^7.4.47",
|
"@mdi/font": "^7.4.47",
|
||||||
"@prisma/client": "^7.4.2",
|
"@prisma/client": "^7.4.2",
|
||||||
|
"axios": "^1.13.6",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"nuxt": "^4.1.3",
|
"nuxt": "^4.1.3",
|
||||||
"prisma": "^6.19.2",
|
"prisma": "^6.19.2",
|
||||||
"sass": "^1.97.3",
|
"sass": "^1.97.3",
|
||||||
"vue": "^3.5.29",
|
"vue": "^3.5.29",
|
||||||
"vue-router": "^4.6.4",
|
"vue-router": "^4.6.4",
|
||||||
"vuetify": "^4.0.1"
|
"vuetify": "^4.0.1",
|
||||||
|
"vuetify-nuxt-module": "^0.19.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.3.3"
|
"@types/node": "^25.3.3",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.4",
|
||||||
|
"@vue/test-utils": "^2.4.6",
|
||||||
|
"happy-dom": "^20.8.3",
|
||||||
|
"jsdom": "^28.1.0",
|
||||||
|
"vitest": "^4.0.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
14
Seasoned.Frontend/plugins/vuetify.ts
Normal file
14
Seasoned.Frontend/plugins/vuetify.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { createVuetify } from 'vuetify'
|
||||||
|
import * as components from 'vuetify/components'
|
||||||
|
import * as directives from 'vuetify/directives'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
const vuetify = createVuetify({
|
||||||
|
ssr: true,
|
||||||
|
components,
|
||||||
|
directives,
|
||||||
|
})
|
||||||
|
|
||||||
|
nuxtApp.vueApp.use(vuetify)
|
||||||
|
})
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
8
Seasoned.Frontend/test/App.spec.ts
Normal file
8
Seasoned.Frontend/test/App.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
|
||||||
|
describe('Frontend Setup', () => {
|
||||||
|
it('checks that 1 + 1 is 2', () => {
|
||||||
|
expect(1 + 1).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
16
Seasoned.Frontend/vitest.config.ts
Normal file
16
Seasoned.Frontend/vitest.config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// vitest.config.ts
|
||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
server: {
|
||||||
|
deps: {
|
||||||
|
inline: [/@exodus\/bytes/, /html-encoding-sniffer/],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
27
Seasoned.Tests/Seasoned.Tests.csproj
Normal file
27
Seasoned.Tests/Seasoned.Tests.csproj
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.8.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Seasoned.Backend\Seasoned.Backend.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
47
Seasoned.Tests/UnitTest1.cs
Normal file
47
Seasoned.Tests/UnitTest1.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Seasoned.Backend.Controllers;
|
||||||
|
using Seasoned.Backend.Services;
|
||||||
|
using Seasoned.Backend.DTOs;
|
||||||
|
|
||||||
|
namespace Seasoned.Tests;
|
||||||
|
|
||||||
|
public class RecipeControllerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ParseRecipe_ReturnsOk_WhenImageIsValid()
|
||||||
|
{
|
||||||
|
// 1. Arrange: Create a "Fake" Service
|
||||||
|
var mockService = new Mock<IRecipeService>();
|
||||||
|
var fakeRecipe = new RecipeResponseDto { Title = "Test Recipe" };
|
||||||
|
|
||||||
|
mockService.Setup(s => s.ParseImageAsync(It.IsAny<IFormFile>()))
|
||||||
|
.ReturnsAsync(fakeRecipe);
|
||||||
|
|
||||||
|
var controller = new RecipeController(mockService.Object);
|
||||||
|
|
||||||
|
// Create a fake image file
|
||||||
|
var content = "fake image content";
|
||||||
|
var fileName = "test.jpg";
|
||||||
|
var ms = new MemoryStream();
|
||||||
|
var writer = new StreamWriter(ms);
|
||||||
|
writer.Write(content);
|
||||||
|
writer.Flush();
|
||||||
|
ms.Position = 0;
|
||||||
|
var mockFile = new FormFile(ms, 0, ms.Length, "id_from_form", fileName);
|
||||||
|
|
||||||
|
// 2. Act: Call the Controller
|
||||||
|
var result = await controller.ParseRecipe(mockFile);
|
||||||
|
|
||||||
|
// 3. Assert: Check the result
|
||||||
|
var okResult = result as OkObjectResult;
|
||||||
|
okResult.Should().NotBeNull();
|
||||||
|
okResult!.StatusCode.Should().Be(200);
|
||||||
|
|
||||||
|
var returnedRecipe = okResult.Value as RecipeResponseDto;
|
||||||
|
returnedRecipe!.Title.Should().Be("Test Recipe");
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Seasoned.code-workspace
Normal file
7
Seasoned.code-workspace
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": ".."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<NuxtRouteAnnouncer />
|
|
||||||
<NuxtWelcome />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
Reference in New Issue
Block a user