test: add draft workflow coverage and sqlite migration helpers
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
using JobTrackerApi.Controllers;
|
||||||
|
using JobTrackerApi.Services;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace JobTrackerApi.Tests;
|
||||||
|
|
||||||
|
public sealed class ProductionConfigTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Summarizer_service_exposes_section_summarization_api()
|
||||||
|
{
|
||||||
|
var method = typeof(ISummarizerService).GetMethod("SummarizeSectionAsync");
|
||||||
|
Assert.NotNull(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Profile_cv_controller_supports_pdf_and_docx_extensions()
|
||||||
|
{
|
||||||
|
var field = typeof(ProfileCvController).GetField("AllowedExtensions", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
|
||||||
|
Assert.NotNull(field);
|
||||||
|
var values = ((System.Collections.IEnumerable)field!.GetValue(null)!).Cast<string>().ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
Assert.Contains(".pdf", values);
|
||||||
|
Assert.Contains(".docx", values);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace JobTrackerApi.Tests;
|
||||||
|
|
||||||
|
public sealed class SqliteMigrationHelperTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Export_payload_shape_can_be_serialized()
|
||||||
|
{
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
users = new[] { new { Id = "u1", Email = "u@example.com" } },
|
||||||
|
companies = new[] { new { Id = 1, Name = "Acme" } },
|
||||||
|
jobs = new[] { new { Id = 1, JobTitle = "Backend Dev" } }
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(payload);
|
||||||
|
Assert.Contains("users", json);
|
||||||
|
Assert.Contains("companies", json);
|
||||||
|
Assert.Contains("jobs", json);
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-6
@@ -37,15 +37,22 @@ That means the MariaDB schema is created automatically as long as:
|
|||||||
- the backend container can reach `mariadb`
|
- the backend container can reach `mariadb`
|
||||||
|
|
||||||
## 4. SQLite to MariaDB migration notes
|
## 4. SQLite to MariaDB migration notes
|
||||||
This repo does **not** yet include an automated data migration tool.
|
This repo now includes helper tooling to make migration safer:
|
||||||
If you already have real data in SQLite, recommended migration path is:
|
- `deploy/sqlite_export.py` exports the important SQLite tables into JSON
|
||||||
|
- `deploy/sqlite_preview.py` lets you inspect the exported payload quickly
|
||||||
|
|
||||||
|
Suggested migration path:
|
||||||
|
|
||||||
1. Stop writes to the app
|
1. Stop writes to the app
|
||||||
2. Back up SQLite DB file
|
2. Back up SQLite DB file
|
||||||
3. Start MariaDB-backed environment on a staging copy
|
3. Export SQLite data:
|
||||||
4. Export/import the critical tables with a small migration script or one-time tool
|
- `python deploy/sqlite_export.py /path/to/jobtracker.db sqlite-export.json`
|
||||||
5. Validate users, companies, jobs, correspondence, attachments metadata
|
4. Preview the export:
|
||||||
6. Switch production `.env` to MariaDB
|
- `python deploy/sqlite_preview.py sqlite-export.json`
|
||||||
|
5. Start MariaDB-backed environment on a staging copy
|
||||||
|
6. Import with a one-time script/manual loader tailored to your production data
|
||||||
|
7. Validate users, companies, jobs, correspondence, attachments metadata
|
||||||
|
8. Switch production `.env` to MariaDB
|
||||||
|
|
||||||
## Tables you would likely want to migrate
|
## Tables you would likely want to migrate
|
||||||
- `AspNetUsers`
|
- `AspNetUsers`
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
TABLES = [
|
||||||
|
'AspNetUsers',
|
||||||
|
'AspNetRoles',
|
||||||
|
'AspNetUserRoles',
|
||||||
|
'Companies',
|
||||||
|
'JobApplications',
|
||||||
|
'Correspondences',
|
||||||
|
'Attachments',
|
||||||
|
'JobEvents',
|
||||||
|
'GmailConnections',
|
||||||
|
'UserRuleSettings',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def export_db(sqlite_path: str, output_path: str) -> None:
|
||||||
|
conn = sqlite3.connect(sqlite_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
payload = {}
|
||||||
|
for table in TABLES:
|
||||||
|
try:
|
||||||
|
rows = conn.execute(f'SELECT * FROM {table}').fetchall()
|
||||||
|
payload[table] = [dict(row) for row in rows]
|
||||||
|
except sqlite3.Error:
|
||||||
|
payload[table] = []
|
||||||
|
Path(output_path).write_text(json.dumps(payload, default=str, indent=2), encoding='utf-8')
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print('Usage: sqlite_export.py <sqlite-db-path> <output-json>')
|
||||||
|
raise SystemExit(1)
|
||||||
|
export_db(sys.argv[1], sys.argv[2])
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def main(path: str) -> None:
|
||||||
|
payload = json.loads(Path(path).read_text(encoding='utf-8'))
|
||||||
|
for table, rows in payload.items():
|
||||||
|
print(f'-- {table}: {len(rows)} rows')
|
||||||
|
for row in rows[:2]:
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print('Usage: sqlite_preview.py <export-json>')
|
||||||
|
raise SystemExit(1)
|
||||||
|
main(sys.argv[1])
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import { ConfirmProvider } from './confirm';
|
||||||
|
import { PromptProvider } from './prompt';
|
||||||
|
import { ToastProvider } from './toast';
|
||||||
|
import JobDetailsDialog from './components/JobDetailsDialog';
|
||||||
|
import { api } from './api';
|
||||||
|
|
||||||
|
jest.mock('./api', () => ({
|
||||||
|
api: {
|
||||||
|
get: jest.fn(),
|
||||||
|
post: jest.fn(),
|
||||||
|
put: jest.fn(),
|
||||||
|
patch: jest.fn(),
|
||||||
|
delete: jest.fn(),
|
||||||
|
interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockedApi = api as jest.Mocked<typeof api>;
|
||||||
|
|
||||||
|
function renderDialog() {
|
||||||
|
return render(
|
||||||
|
<ToastProvider>
|
||||||
|
<ConfirmProvider>
|
||||||
|
<PromptProvider>
|
||||||
|
<JobDetailsDialog open jobId={42} onClose={() => {}} />
|
||||||
|
</PromptProvider>
|
||||||
|
</ConfirmProvider>
|
||||||
|
</ToastProvider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockedApi.get.mockImplementation((url: string) => {
|
||||||
|
if (url === '/jobapplications/42') {
|
||||||
|
return Promise.resolve({ data: { id: 42, jobTitle: 'Backend Developer', status: 'Applied', dateApplied: new Date().toISOString(), daysSince: 3, company: { name: 'Acme', recruiterEmail: 'recruiter@acme.test' }, tailoredCvText: '', shortSummary: 'summary' } } as any);
|
||||||
|
}
|
||||||
|
if (url === '/auth/me') {
|
||||||
|
return Promise.resolve({ data: { roles: [], profileCvText: 'Master CV text' } } as any);
|
||||||
|
}
|
||||||
|
if (url === '/jobapplications/42/history') {
|
||||||
|
return Promise.resolve({ data: [] } as any);
|
||||||
|
}
|
||||||
|
return Promise.resolve({ data: [] } as any);
|
||||||
|
});
|
||||||
|
mockedApi.post.mockResolvedValue({ data: { tailoredCvText: 'Generated CV', coverLetterDraft: 'Draft letter', applicationAnswerDraft: 'Draft answer', recruiterMessageDraft: 'Recruiter hello', keyPoints: ['Lead with .NET'] } } as any);
|
||||||
|
mockedApi.put.mockResolvedValue({ data: {} } as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('generated application package can be edited and saved', async () => {
|
||||||
|
renderDialog();
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole('tab', { name: /tailored cv/i }));
|
||||||
|
fireEvent.click(await screen.findByRole('button', { name: /generate application package/i }));
|
||||||
|
|
||||||
|
expect(await screen.findByDisplayValue('Generated CV')).toBeInTheDocument();
|
||||||
|
const coverLetter = await screen.findByDisplayValue('Draft letter');
|
||||||
|
fireEvent.change(coverLetter, { target: { value: 'Edited cover letter' } });
|
||||||
|
|
||||||
|
const saveButtons = screen.getAllByRole('button', { name: /^save$/i });
|
||||||
|
fireEvent.click(saveButtons[0]);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockedApi.put).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user