feat: add editable application drafts tests and gitea deploy workflow
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
name: CI and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: job-tracker-ui/package-lock.json
|
||||
|
||||
- name: Build backend
|
||||
run: dotnet build JobTrackerApi/JobTrackerApi.csproj --configuration Release
|
||||
|
||||
- name: Test backend
|
||||
run: dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-build
|
||||
|
||||
- name: Install frontend deps
|
||||
working-directory: job-tracker-ui
|
||||
run: npm ci
|
||||
|
||||
- name: Test frontend
|
||||
working-directory: job-tracker-ui
|
||||
run: npm test -- --watchAll=false --runInBand App.test.tsx confirm.test.tsx prompt.test.tsx dialog-flow.test.tsx confirm-flow.test.tsx attachments.test.tsx
|
||||
|
||||
deploy:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Upload deploy bundle to server
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.PROD_HOST }}
|
||||
username: ${{ secrets.PROD_USER }}
|
||||
key: ${{ secrets.PROD_SSH_KEY }}
|
||||
source: "."
|
||||
target: "/opt/job-tracker/app"
|
||||
rm: true
|
||||
|
||||
- name: Run remote deploy
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.PROD_HOST }}
|
||||
username: ${{ secrets.PROD_USER }}
|
||||
key: ${{ secrets.PROD_SSH_KEY }}
|
||||
script: |
|
||||
cd /opt/job-tracker/app
|
||||
chmod +x deploy/deploy.sh
|
||||
APP_VERSION=${{ github.run_number }} \
|
||||
APP_COMMIT_SHA=${{ github.sha }} \
|
||||
APP_BUILD_STAMP="$(date -u +'%Y-%m-%d %H:%M UTC')" \
|
||||
./deploy/deploy.sh
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Security.Claims;
|
||||
using JobTrackerApi.Controllers;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace JobTrackerApi.Tests;
|
||||
|
||||
public sealed class JobApplicationsEndpointBehaviorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Save_application_drafts_updates_cover_letter_and_notes()
|
||||
{
|
||||
await using var db = CreateDb();
|
||||
var company = new Company { Name = "Acme", OwnerUserId = "user-1" };
|
||||
db.Companies.Add(company);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var job = new JobApplication { JobTitle = "Backend Dev", CompanyId = company.Id, OwnerUserId = "user-1" };
|
||||
db.JobApplications.Add(job);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var controller = CreateController(db, "user-1");
|
||||
var result = await controller.SaveApplicationDrafts(job.Id, new JobApplicationsController.SaveApplicationDraftsRequest(" Cover letter body ", " Notes body "), CancellationToken.None);
|
||||
|
||||
Assert.IsType<NoContentResult>(result);
|
||||
var saved = await db.JobApplications.FirstAsync();
|
||||
Assert.Equal("Cover letter body", saved.CoverLetterText);
|
||||
Assert.Contains("Notes body", saved.Notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Generate_application_package_rejects_missing_profile_cv()
|
||||
{
|
||||
await using var db = CreateDb();
|
||||
var company = new Company { Name = "Acme", OwnerUserId = "user-1" };
|
||||
db.Companies.Add(company);
|
||||
db.Users.Add(new ApplicationUser { Id = "user-1", UserName = "u", Email = "u@example.com" });
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var job = new JobApplication { JobTitle = "Backend Dev", CompanyId = company.Id, OwnerUserId = "user-1", Description = "Need .NET and SQL" };
|
||||
db.JobApplications.Add(job);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var controller = CreateController(db, "user-1");
|
||||
var result = await controller.GenerateApplicationPackage(job.Id, null, CancellationToken.None);
|
||||
|
||||
var badRequest = Assert.IsType<BadRequestObjectResult>(result.Result);
|
||||
Assert.Contains("Profile page", badRequest.Value?.ToString());
|
||||
}
|
||||
|
||||
private static JobApplicationsController CreateController(JobTrackerContext db, string userId)
|
||||
{
|
||||
var summarizer = new Mock<ISummarizerService>();
|
||||
summarizer.Setup(x => x.SummarizeSectionAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>())).ReturnsAsync("generated text");
|
||||
|
||||
var controller = new JobApplicationsController(db, summarizer.Object, Mock.Of<IAppEmailSender>());
|
||||
controller.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext
|
||||
{
|
||||
User = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, userId)
|
||||
}, "test"))
|
||||
}
|
||||
};
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static JobTrackerContext CreateDb()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<JobTrackerContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
var currentUser = new Mock<ICurrentUserService>();
|
||||
currentUser.SetupGet(x => x.UserId).Returns("user-1");
|
||||
return new JobTrackerContext(options, currentUser.Object);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Production deployment notes
|
||||
|
||||
## Gitea Actions
|
||||
This repo includes `.gitea/workflows/ci-deploy.yml` for:
|
||||
- backend build
|
||||
- backend tests
|
||||
- frontend tests
|
||||
- deployment to Ubuntu after successful tests on `main`
|
||||
|
||||
### Required secrets in Gitea
|
||||
- `PROD_HOST`
|
||||
- `PROD_USER`
|
||||
- `PROD_SSH_KEY`
|
||||
|
||||
## Ubuntu server setup
|
||||
Recommended app path:
|
||||
- `/opt/job-tracker/app`
|
||||
|
||||
Requirements:
|
||||
- Docker Engine
|
||||
- Docker Compose plugin
|
||||
- reverse proxy in front (Nginx, Caddy, or Traefik)
|
||||
- `.env` file present on server in `/opt/job-tracker/app/.env`
|
||||
|
||||
## Database recommendation
|
||||
For production, yes — use a real database.
|
||||
|
||||
### Recommended direction
|
||||
Short term:
|
||||
- SQLite is acceptable for a single-user or very small deployment
|
||||
- keep backups and volume persistence
|
||||
|
||||
Better production choice:
|
||||
- MariaDB or PostgreSQL
|
||||
|
||||
### My recommendation
|
||||
- **PostgreSQL** if you want the best long-term maintainability and fewer edge cases
|
||||
- **MariaDB** is also fine if that is what you already know or host elsewhere
|
||||
|
||||
If you stay on SQLite:
|
||||
- okay for small personal use
|
||||
- not ideal for concurrent writes, larger scale, or operational robustness
|
||||
|
||||
## Practical recommendation for this project
|
||||
If this app is going to be a real production service on Ubuntu:
|
||||
- move to PostgreSQL first if possible
|
||||
- MariaDB is still a reasonable option if preferred
|
||||
|
||||
## Deployment flow
|
||||
1. push to `main`
|
||||
2. Gitea Actions runs tests
|
||||
3. if green, workflow uploads repo to server
|
||||
4. `deploy/deploy.sh` runs `docker compose build && docker compose up -d`
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Missing .env in deployment directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export APP_VERSION="${APP_VERSION:-0.0.0}"
|
||||
export APP_COMMIT_SHA="${APP_COMMIT_SHA:-unknown}"
|
||||
export APP_BUILD_STAMP="${APP_BUILD_STAMP:-unknown}"
|
||||
|
||||
docker compose pull || true
|
||||
docker compose build
|
||||
docker compose up -d --remove-orphans
|
||||
|
||||
echo "Deployment complete: ${APP_VERSION} ${APP_COMMIT_SHA}"
|
||||
@@ -462,16 +462,22 @@ function ListCard({ title, items }: { title: string; items: string[] }) {
|
||||
}
|
||||
|
||||
function DraftCard({ title, content, onSave, saving }: { title: string; content: string; onSave?: (content: string) => Promise<void> | void; saving?: boolean }) {
|
||||
const [value, setValue] = React.useState(content);
|
||||
|
||||
React.useEffect(() => {
|
||||
setValue(content);
|
||||
}, [content]);
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 1, flexWrap: "wrap", mb: 1 }}>
|
||||
<Typography variant="overline">{title}</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
<Button size="small" variant="outlined" onClick={() => navigator.clipboard.writeText(content)}>Copy</Button>
|
||||
{onSave ? <Button size="small" variant="contained" disabled={saving} onClick={() => onSave(content)}>{saving ? "Saving..." : "Save"}</Button> : null}
|
||||
<Button size="small" variant="outlined" onClick={() => navigator.clipboard.writeText(value)}>Copy</Button>
|
||||
{onSave ? <Button size="small" variant="contained" disabled={saving} onClick={() => onSave(value)}>{saving ? "Saving..." : "Save"}</Button> : null}
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography sx={{ whiteSpace: "pre-wrap" }}>{content}</Typography>
|
||||
<TextField value={value} onChange={(e) => setValue(e.target.value)} multiline minRows={6} fullWidth />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user