Cleanup
This commit is contained in:
parent
c12326fe5e
commit
75b1bab640
13 changed files with 82 additions and 3877 deletions
|
|
@ -1,222 +0,0 @@
|
|||
# API Test Suite
|
||||
|
||||
This directory contains comprehensive tests for the XTablo API, covering all endpoints and their functionality.
|
||||
|
||||
## Test Files
|
||||
|
||||
### 1. `test-utils.ts`
|
||||
|
||||
Provides testing utilities and mock factories:
|
||||
|
||||
- **Mock Clients**: Supabase, Stream Chat, S3, Email Transporter
|
||||
- **Mock Data**: Users, Profiles, Tablos, Events
|
||||
- **Helper Functions**: Context creation, stub management, assertions
|
||||
- **Environment Setup**: Mock environment variables for tests
|
||||
|
||||
### 2. `middleware.test.ts`
|
||||
|
||||
Tests for API middleware:
|
||||
|
||||
- **authMiddleware**: Bearer token authentication
|
||||
- **supabaseMiddleware**: Supabase client initialization
|
||||
- **streamChatMiddleware**: Stream Chat client initialization
|
||||
- **r2Middleware**: S3/R2 client initialization
|
||||
|
||||
### 3. `user.test.ts`
|
||||
|
||||
Tests for User Router (`/api/v1/users`):
|
||||
|
||||
- **POST /sign-up-to-stream**: User registration with Stream Chat
|
||||
- **GET /me**: Retrieve user profile with Stream token
|
||||
- **POST /mark-temporary**: Mark user as temporary and send welcome email
|
||||
|
||||
### 4. `tablo.test.ts`
|
||||
|
||||
Tests for Tablo Router (`/api/v1/tablos`):
|
||||
|
||||
- **POST /create**: Create new tablo with events
|
||||
- **POST /create-and-invite**: Create tablo and invite user
|
||||
- **PATCH /update**: Update tablo details
|
||||
- **DELETE /delete**: Soft delete tablo
|
||||
- **POST /invite**: Send tablo invitation
|
||||
- **POST /join**: Join tablo with invite token
|
||||
- **GET /members/:tablo_id**: Get tablo members
|
||||
- **POST /leave**: Leave a tablo
|
||||
- **POST /webcal/generate-url**: Generate webcal subscription URL
|
||||
|
||||
### 5. `tablo_data.test.ts`
|
||||
|
||||
Tests for Tablo Data Router (`/api/v1/tablo-data`):
|
||||
|
||||
- **GET /:tabloId/filenames**: List files in tablo
|
||||
- **GET /:tabloId/:fileName**: Get file content
|
||||
- **POST /:tabloId/:fileName**: Upload/update file
|
||||
- **DELETE /:tabloId/:fileName**: Delete file
|
||||
|
||||
### 6. `tasks.test.ts`
|
||||
|
||||
Tests for Tasks Router (`/api/v1/tasks`):
|
||||
|
||||
- **POST /sync-calendars**: Sync calendar subscriptions (with authentication)
|
||||
|
||||
### 7. `public.test.ts`
|
||||
|
||||
Tests for Public Router (`/api/public`):
|
||||
|
||||
- **GET /slots/:shortUserId/:standardName**: Get available time slots for booking
|
||||
|
||||
### 8. `helpers.test.ts`
|
||||
|
||||
Tests for helper functions:
|
||||
|
||||
- **generateICSFromEvents**: Generate ICS calendar files
|
||||
- **writeCalendarFileToR2**: Write calendar to R2 storage
|
||||
- **isTabloMember**: Check if user is tablo member
|
||||
- **isTabloAdmin**: Check if user is tablo admin
|
||||
- **getTabloFileNames**: Get list of files in tablo
|
||||
|
||||
### 9. `slots.test.ts`
|
||||
|
||||
Tests for slot generation logic (existing):
|
||||
|
||||
- Time slot generation with various configurations
|
||||
- Exception handling
|
||||
- Event conflicts
|
||||
- Buffer time
|
||||
- Minimum advance booking
|
||||
- Maximum bookings per day
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Run all tests:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
### Run tests in watch mode:
|
||||
|
||||
```bash
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
### Run specific test file:
|
||||
|
||||
```bash
|
||||
npx mocha src/__tests__/user.test.ts
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
The test suite covers:
|
||||
|
||||
1. **Authentication & Authorization**
|
||||
|
||||
- Token validation
|
||||
- User authentication
|
||||
- Admin/member access control
|
||||
|
||||
2. **CRUD Operations**
|
||||
|
||||
- Create, read, update, delete for all entities
|
||||
- Soft deletes
|
||||
- Batch operations
|
||||
|
||||
3. **Business Logic**
|
||||
|
||||
- Tablo invitations and access control
|
||||
- Calendar generation and synchronization
|
||||
- File storage and retrieval
|
||||
- Time slot availability calculation
|
||||
|
||||
4. **Error Handling**
|
||||
|
||||
- Missing required fields
|
||||
- Invalid tokens
|
||||
- Permission denied scenarios
|
||||
- Database errors
|
||||
- External service failures (S3, Stream Chat)
|
||||
|
||||
5. **Integration Points**
|
||||
- Supabase database operations
|
||||
- Stream Chat channel management
|
||||
- R2/S3 file operations
|
||||
- Email sending
|
||||
|
||||
## Testing Framework
|
||||
|
||||
- **Test Runner**: Mocha
|
||||
- **Assertions**: Chai
|
||||
- **Mocking**: Sinon
|
||||
- **Test Style**: BDD (Behavior Driven Development)
|
||||
|
||||
## Test Structure
|
||||
|
||||
Each test file follows this structure:
|
||||
|
||||
```typescript
|
||||
describe("Feature/Router Name", () => {
|
||||
beforeEach(() => {
|
||||
// Setup mocks and environment
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up stubs and restore environment
|
||||
});
|
||||
|
||||
describe("Endpoint/Function Name", () => {
|
||||
it("should handle success case", async () => {
|
||||
// Arrange: Setup test data and mocks
|
||||
// Act: Execute the function/endpoint
|
||||
// Assert: Verify the results
|
||||
});
|
||||
|
||||
it("should handle error case", async () => {
|
||||
// Test error scenarios
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Mock Strategy
|
||||
|
||||
Tests use comprehensive mocking to isolate units under test:
|
||||
|
||||
1. **Supabase Client**: Mocked query builder pattern
|
||||
2. **Stream Chat**: Mocked channel operations
|
||||
3. **S3 Client**: Mocked storage operations
|
||||
4. **Email Transporter**: Mocked email sending
|
||||
|
||||
This ensures tests run quickly and don't depend on external services.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Isolation**: Each test is independent and doesn't affect others
|
||||
2. **Clarity**: Test names clearly describe what is being tested
|
||||
3. **Coverage**: Both happy paths and error cases are tested
|
||||
4. **Maintainability**: Shared utilities reduce code duplication
|
||||
5. **Speed**: Mocking ensures tests run in milliseconds
|
||||
|
||||
## Future Improvements
|
||||
|
||||
- Integration tests with real database
|
||||
- End-to-end API tests
|
||||
- Performance benchmarks
|
||||
- Load testing
|
||||
- Code coverage reporting
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new endpoints or functionality:
|
||||
|
||||
1. Create tests first (TDD approach recommended)
|
||||
2. Follow existing test patterns
|
||||
3. Mock external dependencies
|
||||
4. Test both success and failure scenarios
|
||||
5. Ensure tests pass before committing
|
||||
|
||||
## Notes
|
||||
|
||||
- Some lint warnings for `any` types are suppressed with `biome-ignore` comments - these are intentional for test flexibility
|
||||
- Mock data is defined in `test-utils.ts` for consistency
|
||||
- Environment variables are mocked in each test file's `beforeEach` hook
|
||||
|
|
@ -1,426 +0,0 @@
|
|||
import { expect } from "chai";
|
||||
import { afterEach, beforeEach, describe, it } from "mocha";
|
||||
import sinon from "sinon";
|
||||
import {
|
||||
generateICSFromEvents,
|
||||
getTabloFileNames,
|
||||
isTabloAdmin,
|
||||
isTabloMember,
|
||||
writeCalendarFileToR2,
|
||||
} from "../helpers.js";
|
||||
import type { EventAndTablo } from "../types.js";
|
||||
import {
|
||||
createMockS3Client,
|
||||
createMockSupabaseClient,
|
||||
mockEnvVars,
|
||||
mockEvent,
|
||||
mockTablo,
|
||||
mockUser,
|
||||
} from "./test-utils.js";
|
||||
|
||||
describe("Helper Functions", () => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mock client types
|
||||
let mockSupabase: any;
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mock client types
|
||||
let mockS3: any;
|
||||
let restoreEnv: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
restoreEnv = mockEnvVars();
|
||||
mockSupabase = createMockSupabaseClient();
|
||||
mockS3 = createMockS3Client();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
restoreEnv();
|
||||
});
|
||||
|
||||
describe("generateICSFromEvents", () => {
|
||||
it("should generate valid ICS content from events", () => {
|
||||
const events: EventAndTablo[] = [
|
||||
{
|
||||
event_id: "event1",
|
||||
tablo_id: "tablo1",
|
||||
tablo_name: "Test Tablo",
|
||||
tablo_color: "bg-blue-500",
|
||||
tablo_status: "todo",
|
||||
title: "Test Event",
|
||||
description: "Test description",
|
||||
start_date: "2024-01-16",
|
||||
start_time: "10:00:00",
|
||||
end_time: "11:00:00",
|
||||
// created_by: mockUser.id,
|
||||
// created_at: "2024-01-01T00:00:00Z",
|
||||
// deleted_at: null,
|
||||
},
|
||||
];
|
||||
|
||||
const icsContent = generateICSFromEvents(events, "Test Calendar");
|
||||
|
||||
expect(icsContent).to.include("BEGIN:VCALENDAR");
|
||||
expect(icsContent).to.include("VERSION:2.0");
|
||||
expect(icsContent).to.include("X-WR-CALNAME:Test Calendar");
|
||||
expect(icsContent).to.include("BEGIN:VEVENT");
|
||||
expect(icsContent).to.include("SUMMARY:Test Event");
|
||||
expect(icsContent).to.include("DESCRIPTION:Tablo: Test Tablo");
|
||||
expect(icsContent).to.include("END:VEVENT");
|
||||
expect(icsContent).to.include("END:VCALENDAR");
|
||||
});
|
||||
|
||||
it("should handle events without end_time", () => {
|
||||
const events: EventAndTablo[] = [
|
||||
{
|
||||
event_id: "event1",
|
||||
tablo_id: "tablo1",
|
||||
tablo_name: "Test Tablo",
|
||||
tablo_color: "bg-blue-500",
|
||||
tablo_status: "todo",
|
||||
title: "Test Event",
|
||||
description: null,
|
||||
start_date: "2024-01-16",
|
||||
start_time: "10:00:00",
|
||||
end_time: null,
|
||||
created_by: mockUser.id,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
deleted_at: null,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mock event with null end_time
|
||||
} as any,
|
||||
];
|
||||
|
||||
const icsContent = generateICSFromEvents(events, "Test Calendar");
|
||||
|
||||
expect(icsContent).to.include("BEGIN:VEVENT");
|
||||
expect(icsContent).to.include("SUMMARY:Test Event");
|
||||
expect(icsContent).to.include("END:VEVENT");
|
||||
});
|
||||
|
||||
it("should escape special characters in ICS text", () => {
|
||||
const events: EventAndTablo[] = [
|
||||
{
|
||||
event_id: "event1",
|
||||
tablo_id: "tablo1",
|
||||
tablo_name: "Test; Tablo,",
|
||||
tablo_color: "bg-blue-500",
|
||||
tablo_status: "todo",
|
||||
title: "Test; Event,",
|
||||
description: "Test\\description\nwith newline",
|
||||
start_date: "2024-01-16",
|
||||
start_time: "10:00:00",
|
||||
end_time: "11:00:00",
|
||||
// created_by: mockUser.id,
|
||||
// created_at: "2024-01-01T00:00:00Z",
|
||||
// deleted_at: null,
|
||||
},
|
||||
];
|
||||
|
||||
const icsContent = generateICSFromEvents(events, "Test Calendar");
|
||||
|
||||
expect(icsContent).to.include("SUMMARY:Test\\; Event\\,");
|
||||
expect(icsContent).to.include(
|
||||
"DESCRIPTION:Tablo: Test\\; Tablo\\,\\nTest\\\\description\\nwith newline"
|
||||
);
|
||||
});
|
||||
|
||||
it("should skip events without required fields", () => {
|
||||
const events: EventAndTablo[] = [
|
||||
{
|
||||
event_id: "event1",
|
||||
tablo_id: "tablo1",
|
||||
tablo_name: "Test Tablo",
|
||||
tablo_color: "bg-blue-500",
|
||||
tablo_status: "todo",
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Testing null title case
|
||||
title: null as any,
|
||||
description: null,
|
||||
start_date: "2024-01-16",
|
||||
start_time: "10:00:00",
|
||||
end_time: "11:00:00",
|
||||
// created_by: mockUser.id,
|
||||
// created_at: "2024-01-01T00:00:00Z",
|
||||
// deleted_at: null,
|
||||
},
|
||||
];
|
||||
|
||||
const icsContent = generateICSFromEvents(events, "Test Calendar");
|
||||
|
||||
expect(icsContent).to.include("BEGIN:VCALENDAR");
|
||||
expect(icsContent).to.not.include("BEGIN:VEVENT");
|
||||
expect(icsContent).to.include("END:VCALENDAR");
|
||||
});
|
||||
|
||||
it("should handle multiple events", () => {
|
||||
const events: EventAndTablo[] = [
|
||||
{
|
||||
event_id: "event1",
|
||||
tablo_id: "tablo1",
|
||||
tablo_name: "Test Tablo",
|
||||
tablo_color: "bg-blue-500",
|
||||
tablo_status: "todo",
|
||||
title: "Event 1",
|
||||
description: "Description 1",
|
||||
start_date: "2024-01-16",
|
||||
start_time: "10:00:00",
|
||||
end_time: "11:00:00",
|
||||
// created_by: mockUser.id,
|
||||
// created_at: "2024-01-01T00:00:00Z",
|
||||
// deleted_at: null,
|
||||
},
|
||||
{
|
||||
event_id: "event2",
|
||||
tablo_id: "tablo1",
|
||||
tablo_name: "Test Tablo",
|
||||
tablo_color: "bg-blue-500",
|
||||
tablo_status: "todo",
|
||||
title: "Event 2",
|
||||
description: "Description 2",
|
||||
start_date: "2024-01-17",
|
||||
start_time: "14:00:00",
|
||||
end_time: "15:00:00",
|
||||
// created_by: mockUser.id,
|
||||
// created_at: "2024-01-01T00:00:00Z",
|
||||
// deleted_at: null,
|
||||
},
|
||||
];
|
||||
|
||||
const icsContent = generateICSFromEvents(events, "Test Calendar");
|
||||
|
||||
const eventCount = (icsContent.match(/BEGIN:VEVENT/g) || []).length;
|
||||
expect(eventCount).to.equal(2);
|
||||
expect(icsContent).to.include("SUMMARY:Event 1");
|
||||
expect(icsContent).to.include("SUMMARY:Event 2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeCalendarFileToR2", () => {
|
||||
it("should write calendar file to R2 successfully", async () => {
|
||||
const events: EventAndTablo[] = [
|
||||
{
|
||||
event_id: "event1",
|
||||
tablo_id: mockTablo.id,
|
||||
tablo_name: "Test Tablo",
|
||||
tablo_color: "bg-blue-500",
|
||||
tablo_status: "todo",
|
||||
title: "Test Event",
|
||||
description: "Test description",
|
||||
start_date: "2024-01-16",
|
||||
start_time: "10:00:00",
|
||||
end_time: "11:00:00",
|
||||
// created_by: mockUser.id,
|
||||
// created_at: "2024-01-01T00:00:00Z",
|
||||
// deleted_at: null,
|
||||
},
|
||||
];
|
||||
|
||||
const eventsBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().resolves({ data: events, error: null }),
|
||||
};
|
||||
|
||||
mockSupabase.from.withArgs("events_and_tablos").returns(eventsBuilder);
|
||||
|
||||
mockS3.send.resolves({});
|
||||
|
||||
await writeCalendarFileToR2(mockS3, mockSupabase, {
|
||||
token: "test-token",
|
||||
tabloName: "Test Tablo",
|
||||
tablo_id: mockTablo.id,
|
||||
});
|
||||
|
||||
expect(mockS3.send.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it("should throw error if events fetch fails", async () => {
|
||||
const eventsBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon
|
||||
.stub()
|
||||
.resolves({ data: null, error: { message: "Database error" } }),
|
||||
};
|
||||
|
||||
mockSupabase.from.withArgs("events_and_tablos").returns(eventsBuilder);
|
||||
|
||||
try {
|
||||
await writeCalendarFileToR2(mockS3, mockSupabase, {
|
||||
token: "test-token",
|
||||
tabloName: "Test Tablo",
|
||||
tablo_id: mockTablo.id,
|
||||
});
|
||||
expect.fail("Should have thrown an error");
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Catching error to check message
|
||||
} catch (error: any) {
|
||||
expect(error.message).to.equal("Failed to generate events");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("isTabloMember", () => {
|
||||
it("should return true if user is a member", async () => {
|
||||
const accessBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
};
|
||||
// The last eq() call should resolve with data
|
||||
accessBuilder.eq.onCall(2).resolves({
|
||||
data: [{ tablo_id: mockTablo.id, user_id: mockUser.id }],
|
||||
error: null,
|
||||
});
|
||||
|
||||
mockSupabase.from.withArgs("tablo_access").returns(accessBuilder);
|
||||
|
||||
const isMember = await isTabloMember(
|
||||
mockSupabase,
|
||||
mockTablo.id,
|
||||
mockUser.id
|
||||
);
|
||||
|
||||
expect(isMember).to.be.true;
|
||||
});
|
||||
|
||||
it("should return false if user is not a member", async () => {
|
||||
const accessBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
};
|
||||
// The last eq() call should resolve with empty data
|
||||
accessBuilder.eq.onCall(2).resolves({ data: [], error: null });
|
||||
|
||||
mockSupabase.from.withArgs("tablo_access").returns(accessBuilder);
|
||||
|
||||
const isMember = await isTabloMember(
|
||||
mockSupabase,
|
||||
mockTablo.id,
|
||||
mockUser.id
|
||||
);
|
||||
|
||||
expect(isMember).to.be.false;
|
||||
});
|
||||
|
||||
it("should return false if database error occurs", async () => {
|
||||
const accessBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
};
|
||||
// The last eq() call should resolve with error
|
||||
accessBuilder.eq
|
||||
.onCall(2)
|
||||
.resolves({ data: null, error: { message: "Database error" } });
|
||||
|
||||
mockSupabase.from.withArgs("tablo_access").returns(accessBuilder);
|
||||
|
||||
const isMember = await isTabloMember(
|
||||
mockSupabase,
|
||||
mockTablo.id,
|
||||
mockUser.id
|
||||
);
|
||||
|
||||
expect(isMember).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("isTabloAdmin", () => {
|
||||
it("should return true if user is an admin", async () => {
|
||||
const accessBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
};
|
||||
// The last eq() call (4th call - onCall(3)) should resolve with data
|
||||
accessBuilder.eq.onCall(3).resolves({
|
||||
data: [
|
||||
{ tablo_id: mockTablo.id, user_id: mockUser.id, is_admin: true },
|
||||
],
|
||||
error: null,
|
||||
});
|
||||
|
||||
mockSupabase.from.withArgs("tablo_access").returns(accessBuilder);
|
||||
|
||||
const isAdmin = await isTabloAdmin(
|
||||
mockSupabase,
|
||||
mockTablo.id,
|
||||
mockUser.id
|
||||
);
|
||||
|
||||
expect(isAdmin).to.be.true;
|
||||
});
|
||||
|
||||
it("should return false if user is not an admin", async () => {
|
||||
const accessBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
};
|
||||
// The last eq() call should resolve with empty data
|
||||
accessBuilder.eq.onCall(3).resolves({ data: [], error: null });
|
||||
|
||||
mockSupabase.from.withArgs("tablo_access").returns(accessBuilder);
|
||||
|
||||
const isAdmin = await isTabloAdmin(
|
||||
mockSupabase,
|
||||
mockTablo.id,
|
||||
mockUser.id
|
||||
);
|
||||
|
||||
expect(isAdmin).to.be.false;
|
||||
});
|
||||
|
||||
it("should return false if database error occurs", async () => {
|
||||
const accessBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
};
|
||||
// The last eq() call should resolve with error
|
||||
accessBuilder.eq
|
||||
.onCall(3)
|
||||
.resolves({ data: null, error: { message: "Database error" } });
|
||||
|
||||
mockSupabase.from.withArgs("tablo_access").returns(accessBuilder);
|
||||
|
||||
const isAdmin = await isTabloAdmin(
|
||||
mockSupabase,
|
||||
mockTablo.id,
|
||||
mockUser.id
|
||||
);
|
||||
|
||||
expect(isAdmin).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTabloFileNames", () => {
|
||||
it("should return list of file names", async () => {
|
||||
mockS3.send.resolves({
|
||||
Contents: [
|
||||
{ Key: `${mockTablo.id}/file1.txt` },
|
||||
{ Key: `${mockTablo.id}/file2.pdf` },
|
||||
{ Key: `${mockTablo.id}/file3.jpg` },
|
||||
],
|
||||
});
|
||||
|
||||
const fileNames = await getTabloFileNames(mockS3, mockTablo.id);
|
||||
|
||||
expect(fileNames).to.deep.equal(["file1.txt", "file2.pdf", "file3.jpg"]);
|
||||
});
|
||||
|
||||
it("should return empty array if no files exist", async () => {
|
||||
mockS3.send.resolves({
|
||||
Contents: [],
|
||||
});
|
||||
|
||||
const fileNames = await getTabloFileNames(mockS3, mockTablo.id);
|
||||
|
||||
expect(fileNames).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("should filter out invalid file names", async () => {
|
||||
mockS3.send.resolves({
|
||||
Contents: [
|
||||
{ Key: `${mockTablo.id}/file1.txt` },
|
||||
{ Key: `${mockTablo.id}/` }, // Empty file name
|
||||
{ Key: `${mockTablo.id}` }, // No file name
|
||||
],
|
||||
});
|
||||
|
||||
const fileNames = await getTabloFileNames(mockS3, mockTablo.id);
|
||||
|
||||
expect(fileNames).to.deep.equal(["file1.txt"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
import { expect } from "chai";
|
||||
import { afterEach, beforeEach, describe, it } from "mocha";
|
||||
import sinon from "sinon";
|
||||
import {
|
||||
authMiddleware,
|
||||
r2Middleware,
|
||||
streamChatMiddleware,
|
||||
supabaseMiddleware,
|
||||
} from "../middleware.js";
|
||||
import {
|
||||
createMockContext,
|
||||
createMockNext,
|
||||
createMockSupabaseClient,
|
||||
mockEnvVars,
|
||||
mockUser,
|
||||
} from "./test-utils.js";
|
||||
|
||||
describe("Middleware", () => {
|
||||
let restoreEnv: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
restoreEnv = mockEnvVars();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
restoreEnv();
|
||||
});
|
||||
|
||||
describe("authMiddleware", () => {
|
||||
it("should authenticate valid Bearer token", async () => {
|
||||
const mockSupabase = createMockSupabaseClient();
|
||||
const mockContext = createMockContext();
|
||||
const mockNext = createMockNext();
|
||||
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.req.header.withArgs("Authorization").returns("Bearer valid-token");
|
||||
|
||||
// Mock successful auth
|
||||
mockSupabase.auth.getUser.resolves({
|
||||
data: { user: mockUser },
|
||||
error: null,
|
||||
});
|
||||
|
||||
await authMiddleware(mockContext, mockNext);
|
||||
|
||||
expect(mockSupabase.auth.getUser.calledWith("valid-token")).to.be.true;
|
||||
expect(mockContext.set.calledWith("user", mockUser)).to.be.true;
|
||||
expect(mockNext.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it("should return 401 for missing Authorization header", async () => {
|
||||
const mockSupabase = createMockSupabaseClient();
|
||||
const mockContext = createMockContext();
|
||||
const mockNext = createMockNext();
|
||||
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.req.header.withArgs("Authorization").returns(undefined);
|
||||
mockContext.json.returns({
|
||||
error: "Missing or invalid authorization header",
|
||||
});
|
||||
|
||||
const result = await authMiddleware(mockContext, mockNext);
|
||||
|
||||
expect(mockNext.called).to.be.false;
|
||||
expect(result).to.deep.equal({
|
||||
error: "Missing or invalid authorization header",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return 401 for invalid Bearer token format", async () => {
|
||||
const mockSupabase = createMockSupabaseClient();
|
||||
const mockContext = createMockContext();
|
||||
const mockNext = createMockNext();
|
||||
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.req.header.withArgs("Authorization").returns("InvalidFormat");
|
||||
mockContext.json.returns({
|
||||
error: "Missing or invalid authorization header",
|
||||
});
|
||||
|
||||
const result = await authMiddleware(mockContext, mockNext);
|
||||
|
||||
expect(mockNext.called).to.be.false;
|
||||
expect(result).to.deep.equal({
|
||||
error: "Missing or invalid authorization header",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return 401 for invalid or expired token", async () => {
|
||||
const mockSupabase = createMockSupabaseClient();
|
||||
const mockContext = createMockContext();
|
||||
const mockNext = createMockNext();
|
||||
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.req.header.withArgs("Authorization").returns("Bearer invalid-token");
|
||||
|
||||
// Mock auth failure
|
||||
mockSupabase.auth.getUser.resolves({
|
||||
data: { user: null },
|
||||
error: { message: "Invalid token" },
|
||||
});
|
||||
|
||||
mockContext.json.returns({ error: "Invalid or expired token" });
|
||||
|
||||
const result = await authMiddleware(mockContext, mockNext);
|
||||
|
||||
expect(mockNext.called).to.be.false;
|
||||
expect(result).to.deep.equal({ error: "Invalid or expired token" });
|
||||
});
|
||||
|
||||
it("should return 401 when user is null", async () => {
|
||||
const mockSupabase = createMockSupabaseClient();
|
||||
const mockContext = createMockContext();
|
||||
const mockNext = createMockNext();
|
||||
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.req.header.withArgs("Authorization").returns("Bearer valid-token");
|
||||
|
||||
// Mock auth with null user
|
||||
mockSupabase.auth.getUser.resolves({
|
||||
data: { user: null },
|
||||
error: null,
|
||||
});
|
||||
|
||||
mockContext.json.returns({ error: "Invalid or expired token" });
|
||||
|
||||
const result = await authMiddleware(mockContext, mockNext);
|
||||
|
||||
expect(mockNext.called).to.be.false;
|
||||
expect(result).to.deep.equal({ error: "Invalid or expired token" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("supabaseMiddleware", () => {
|
||||
it("should create and set Supabase client in context", async () => {
|
||||
const mockContext = createMockContext();
|
||||
const mockNext = createMockNext();
|
||||
|
||||
await supabaseMiddleware(mockContext, mockNext);
|
||||
|
||||
expect(mockContext.set.calledOnce).to.be.true;
|
||||
const setCall = mockContext.set.getCall(0);
|
||||
expect(setCall.args[0]).to.equal("supabase");
|
||||
expect(setCall.args[1]).to.be.an("object");
|
||||
expect(mockNext.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("streamChatMiddleware", () => {
|
||||
it("should create and set Stream Chat client in context", async () => {
|
||||
const mockContext = createMockContext();
|
||||
const mockNext = createMockNext();
|
||||
|
||||
await streamChatMiddleware(mockContext, mockNext);
|
||||
|
||||
expect(mockContext.set.calledOnce).to.be.true;
|
||||
const setCall = mockContext.set.getCall(0);
|
||||
expect(setCall.args[0]).to.equal("streamServerClient");
|
||||
expect(setCall.args[1]).to.be.an("object");
|
||||
expect(mockNext.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("r2Middleware", () => {
|
||||
it("should create and set S3 client in context", async () => {
|
||||
const mockContext = createMockContext();
|
||||
const mockNext = createMockNext();
|
||||
|
||||
await r2Middleware(mockContext, mockNext);
|
||||
|
||||
expect(mockContext.set.calledOnce).to.be.true;
|
||||
const setCall = mockContext.set.getCall(0);
|
||||
expect(setCall.args[0]).to.equal("s3_client");
|
||||
expect(setCall.args[1]).to.be.an("object");
|
||||
expect(mockNext.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,509 +0,0 @@
|
|||
import { expect } from "chai";
|
||||
import { afterEach, beforeEach, describe, it } from "mocha";
|
||||
import sinon from "sinon";
|
||||
import {
|
||||
createMockContext,
|
||||
createMockSupabaseClient,
|
||||
mockEnvVars,
|
||||
mockEvent,
|
||||
mockProfile,
|
||||
} from "./test-utils.js";
|
||||
|
||||
describe("Public Router", () => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mock client types
|
||||
let mockSupabase: any;
|
||||
let restoreEnv: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
restoreEnv = mockEnvVars();
|
||||
mockSupabase = createMockSupabaseClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
restoreEnv();
|
||||
});
|
||||
|
||||
describe("GET /slots/:shortUserId/:standardName", () => {
|
||||
it("should return available slots for valid user and event type", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.param.withArgs("shortUserId").returns("testuser");
|
||||
mockContext.req.param.withArgs("standardName").returns("meeting-30min");
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
|
||||
const eventType = {
|
||||
id: "event-type-id",
|
||||
user_id: mockProfile.id,
|
||||
standard_name: "meeting-30min",
|
||||
config: {
|
||||
name: "30 Minute Meeting",
|
||||
description: "Standard meeting",
|
||||
duration: 30,
|
||||
requiresApproval: false,
|
||||
},
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
deleted_at: null,
|
||||
};
|
||||
|
||||
const availability = {
|
||||
id: "availability-id",
|
||||
user_id: mockProfile.id,
|
||||
availability_data: {
|
||||
0: { enabled: true, timeRanges: [{ start: "09:00", end: "17:00" }] },
|
||||
1: { enabled: true, timeRanges: [{ start: "09:00", end: "17:00" }] },
|
||||
2: { enabled: true, timeRanges: [{ start: "09:00", end: "17:00" }] },
|
||||
3: { enabled: true, timeRanges: [{ start: "09:00", end: "17:00" }] },
|
||||
4: { enabled: true, timeRanges: [{ start: "09:00", end: "17:00" }] },
|
||||
5: { enabled: false, timeRanges: [] },
|
||||
6: { enabled: false, timeRanges: [] },
|
||||
},
|
||||
exceptions: [],
|
||||
};
|
||||
|
||||
// Mock user lookup
|
||||
const userBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
single: sinon.stub().resolves({ data: mockProfile, error: null }),
|
||||
};
|
||||
|
||||
// Mock event type lookup
|
||||
const eventTypeBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
is: sinon.stub().returnsThis(),
|
||||
single: sinon.stub().resolves({ data: eventType, error: null }),
|
||||
};
|
||||
|
||||
// Mock availabilities lookup
|
||||
const availabilityBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
single: sinon.stub().resolves({ data: availability, error: null }),
|
||||
};
|
||||
|
||||
// Mock events lookup
|
||||
const eventsBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
gte: sinon.stub().returnsThis(),
|
||||
lte: sinon.stub().returnsThis(),
|
||||
is: sinon.stub().resolves({ data: [], error: null }),
|
||||
};
|
||||
|
||||
mockSupabase.from.callsFake((table: string) => {
|
||||
if (table === "profiles") return userBuilder;
|
||||
if (table === "event_types") return eventTypeBuilder;
|
||||
if (table === "availabilities") return availabilityBuilder;
|
||||
if (table === "events") return eventsBuilder;
|
||||
return mockSupabase.from();
|
||||
});
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const supabase = c.get("supabase");
|
||||
const shortUserId = c.req.param("shortUserId");
|
||||
const standardName = c.req.param("standardName");
|
||||
|
||||
// Get user
|
||||
const { data: userData, error: userError } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("short_user_id", shortUserId)
|
||||
.single();
|
||||
|
||||
if (userError || !userData) {
|
||||
return c.json({ error: "User not found" }, 404);
|
||||
}
|
||||
|
||||
// Get event type
|
||||
const { data: eventTypeData, error: eventTypeError } = await supabase
|
||||
.from("event_types")
|
||||
.select("*")
|
||||
.eq("user_id", userData.id)
|
||||
.eq("standard_name", standardName)
|
||||
.is("deleted_at", null)
|
||||
.single();
|
||||
|
||||
if (eventTypeError || !eventTypeData) {
|
||||
return c.json({ error: "Event type not found" }, 404);
|
||||
}
|
||||
|
||||
// Get availabilities
|
||||
const { error: availabilitiesError } = await supabase
|
||||
.from("availabilities")
|
||||
.select("*")
|
||||
.eq("user_id", userData.id)
|
||||
.single();
|
||||
|
||||
if (availabilitiesError) {
|
||||
return c.json({ error: "Availabilities not found" }, 404);
|
||||
}
|
||||
|
||||
// Get existing events
|
||||
const { error: eventsError } = await supabase
|
||||
.from("events")
|
||||
.select("*")
|
||||
.eq("created_by", userData.id)
|
||||
.gte("start_date", "2024-01-01")
|
||||
.lte("start_date", "2024-12-31")
|
||||
.is("deleted_at", null);
|
||||
|
||||
if (eventsError) {
|
||||
return c.json({ error: "Failed to fetch events" }, 500);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
user: { name: userData.name },
|
||||
eventType: eventTypeData.config,
|
||||
slots: {},
|
||||
availableSlots: [],
|
||||
});
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result.user.name).to.equal(mockProfile.name);
|
||||
expect(result.eventType.name).to.equal("30 Minute Meeting");
|
||||
});
|
||||
|
||||
it("should return 404 if user not found", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.param.withArgs("shortUserId").returns("nonexistent");
|
||||
mockContext.req.param.withArgs("standardName").returns("meeting-30min");
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
|
||||
// Mock user lookup with no data
|
||||
const userBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
single: sinon
|
||||
.stub()
|
||||
.resolves({ data: null, error: { message: "Not found" } }),
|
||||
};
|
||||
|
||||
mockSupabase.from.withArgs("profiles").returns(userBuilder);
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const supabase = c.get("supabase");
|
||||
const shortUserId = c.req.param("shortUserId");
|
||||
|
||||
const { data: userData, error: userError } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("short_user_id", shortUserId)
|
||||
.single();
|
||||
|
||||
if (userError || !userData) {
|
||||
return c.json({ error: "User not found" }, 404);
|
||||
}
|
||||
|
||||
return c.json({ message: "Success" });
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "User not found" });
|
||||
});
|
||||
|
||||
it("should return 404 if event type not found", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.param.withArgs("shortUserId").returns("testuser");
|
||||
mockContext.req.param.withArgs("standardName").returns("nonexistent");
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
|
||||
// Mock user lookup
|
||||
const userBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
single: sinon.stub().resolves({ data: mockProfile, error: null }),
|
||||
};
|
||||
|
||||
// Mock event type lookup with no data
|
||||
const eventTypeBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
is: sinon.stub().returnsThis(),
|
||||
single: sinon
|
||||
.stub()
|
||||
.resolves({ data: null, error: { message: "Not found" } }),
|
||||
};
|
||||
|
||||
mockSupabase.from.callsFake((table: string) => {
|
||||
if (table === "profiles") return userBuilder;
|
||||
if (table === "event_types") return eventTypeBuilder;
|
||||
return mockSupabase.from();
|
||||
});
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const supabase = c.get("supabase");
|
||||
const shortUserId = c.req.param("shortUserId");
|
||||
const standardName = c.req.param("standardName");
|
||||
|
||||
// Get user
|
||||
const { data: userData, error: userError } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("short_user_id", shortUserId)
|
||||
.single();
|
||||
|
||||
if (userError || !userData) {
|
||||
return c.json({ error: "User not found" }, 404);
|
||||
}
|
||||
|
||||
// Get event type
|
||||
const { data: eventTypeData, error: eventTypeError } = await supabase
|
||||
.from("event_types")
|
||||
.select("*")
|
||||
.eq("user_id", userData.id)
|
||||
.eq("standard_name", standardName)
|
||||
.is("deleted_at", null)
|
||||
.single();
|
||||
|
||||
if (eventTypeError || !eventTypeData) {
|
||||
return c.json({ error: "Event type not found" }, 404);
|
||||
}
|
||||
|
||||
return c.json({ message: "Success" });
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "Event type not found" });
|
||||
});
|
||||
|
||||
it("should return 404 if availabilities not found", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.param.withArgs("shortUserId").returns("testuser");
|
||||
mockContext.req.param.withArgs("standardName").returns("meeting-30min");
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
|
||||
const eventType = {
|
||||
id: "event-type-id",
|
||||
user_id: mockProfile.id,
|
||||
standard_name: "meeting-30min",
|
||||
config: {
|
||||
name: "30 Minute Meeting",
|
||||
description: "Standard meeting",
|
||||
duration: 30,
|
||||
requiresApproval: false,
|
||||
},
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
deleted_at: null,
|
||||
};
|
||||
|
||||
// Mock user lookup
|
||||
const userBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
single: sinon.stub().resolves({ data: mockProfile, error: null }),
|
||||
};
|
||||
|
||||
// Mock event type lookup
|
||||
const eventTypeBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
is: sinon.stub().returnsThis(),
|
||||
single: sinon.stub().resolves({ data: eventType, error: null }),
|
||||
};
|
||||
|
||||
// Mock availabilities lookup with error
|
||||
const availabilityBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
single: sinon
|
||||
.stub()
|
||||
.resolves({ data: null, error: { message: "Not found" } }),
|
||||
};
|
||||
|
||||
mockSupabase.from.callsFake((table: string) => {
|
||||
if (table === "profiles") return userBuilder;
|
||||
if (table === "event_types") return eventTypeBuilder;
|
||||
if (table === "availabilities") return availabilityBuilder;
|
||||
return mockSupabase.from();
|
||||
});
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const supabase = c.get("supabase");
|
||||
const shortUserId = c.req.param("shortUserId");
|
||||
const standardName = c.req.param("standardName");
|
||||
|
||||
// Get user
|
||||
const { data: userData, error: userError } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("short_user_id", shortUserId)
|
||||
.single();
|
||||
|
||||
if (userError || !userData) {
|
||||
return c.json({ error: "User not found" }, 404);
|
||||
}
|
||||
|
||||
// Get event type
|
||||
const { data: eventTypeData, error: eventTypeError } = await supabase
|
||||
.from("event_types")
|
||||
.select("*")
|
||||
.eq("user_id", userData.id)
|
||||
.eq("standard_name", standardName)
|
||||
.is("deleted_at", null)
|
||||
.single();
|
||||
|
||||
if (eventTypeError || !eventTypeData) {
|
||||
return c.json({ error: "Event type not found" }, 404);
|
||||
}
|
||||
|
||||
// Get availabilities
|
||||
const { error: availabilitiesError } = await supabase
|
||||
.from("availabilities")
|
||||
.select("*")
|
||||
.eq("user_id", userData.id)
|
||||
.single();
|
||||
|
||||
if (availabilitiesError) {
|
||||
return c.json({ error: "Availabilities not found" }, 404);
|
||||
}
|
||||
|
||||
return c.json({ message: "Success" });
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "Availabilities not found" });
|
||||
});
|
||||
|
||||
it("should return 500 if events query fails", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.param.withArgs("shortUserId").returns("testuser");
|
||||
mockContext.req.param.withArgs("standardName").returns("meeting-30min");
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
|
||||
const eventType = {
|
||||
id: "event-type-id",
|
||||
user_id: mockProfile.id,
|
||||
standard_name: "meeting-30min",
|
||||
config: {
|
||||
name: "30 Minute Meeting",
|
||||
description: "Standard meeting",
|
||||
duration: 30,
|
||||
requiresApproval: false,
|
||||
},
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
deleted_at: null,
|
||||
};
|
||||
|
||||
const availability = {
|
||||
id: "availability-id",
|
||||
user_id: mockProfile.id,
|
||||
availability_data: {
|
||||
0: { enabled: true, timeRanges: [{ start: "09:00", end: "17:00" }] },
|
||||
},
|
||||
exceptions: [],
|
||||
};
|
||||
|
||||
// Mock user lookup
|
||||
const userBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
single: sinon.stub().resolves({ data: mockProfile, error: null }),
|
||||
};
|
||||
|
||||
// Mock event type lookup
|
||||
const eventTypeBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
is: sinon.stub().returnsThis(),
|
||||
single: sinon.stub().resolves({ data: eventType, error: null }),
|
||||
};
|
||||
|
||||
// Mock availabilities lookup
|
||||
const availabilityBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
single: sinon.stub().resolves({ data: availability, error: null }),
|
||||
};
|
||||
|
||||
// Mock events lookup with error
|
||||
const eventsBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
gte: sinon.stub().returnsThis(),
|
||||
lte: sinon.stub().returnsThis(),
|
||||
is: sinon
|
||||
.stub()
|
||||
.resolves({ data: null, error: { message: "Database error" } }),
|
||||
};
|
||||
|
||||
mockSupabase.from.callsFake((table: string) => {
|
||||
if (table === "profiles") return userBuilder;
|
||||
if (table === "event_types") return eventTypeBuilder;
|
||||
if (table === "availabilities") return availabilityBuilder;
|
||||
if (table === "events") return eventsBuilder;
|
||||
return mockSupabase.from();
|
||||
});
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const supabase = c.get("supabase");
|
||||
const shortUserId = c.req.param("shortUserId");
|
||||
const standardName = c.req.param("standardName");
|
||||
|
||||
// Get user
|
||||
const { data: userData, error: userError } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("short_user_id", shortUserId)
|
||||
.single();
|
||||
|
||||
if (userError || !userData) {
|
||||
return c.json({ error: "User not found" }, 404);
|
||||
}
|
||||
|
||||
// Get event type
|
||||
const { data: eventTypeData, error: eventTypeError } = await supabase
|
||||
.from("event_types")
|
||||
.select("*")
|
||||
.eq("user_id", userData.id)
|
||||
.eq("standard_name", standardName)
|
||||
.is("deleted_at", null)
|
||||
.single();
|
||||
|
||||
if (eventTypeError || !eventTypeData) {
|
||||
return c.json({ error: "Event type not found" }, 404);
|
||||
}
|
||||
|
||||
// Get availabilities
|
||||
const { error: availabilitiesError } = await supabase
|
||||
.from("availabilities")
|
||||
.select("*")
|
||||
.eq("user_id", userData.id)
|
||||
.single();
|
||||
|
||||
if (availabilitiesError) {
|
||||
return c.json({ error: "Availabilities not found" }, 404);
|
||||
}
|
||||
|
||||
// Get existing events
|
||||
const { error: eventsError } = await supabase
|
||||
.from("events")
|
||||
.select("*")
|
||||
.eq("created_by", userData.id)
|
||||
.gte("start_date", "2024-01-01")
|
||||
.lte("start_date", "2024-12-31")
|
||||
.is("deleted_at", null);
|
||||
|
||||
if (eventsError) {
|
||||
return c.json({ error: "Failed to fetch events" }, 500);
|
||||
}
|
||||
|
||||
return c.json({ message: "Success" });
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "Failed to fetch events" });
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,497 +0,0 @@
|
|||
import { expect } from "chai";
|
||||
import { afterEach, beforeEach, describe, it } from "mocha";
|
||||
import sinon from "sinon";
|
||||
import {
|
||||
createMockContext,
|
||||
createMockS3Client,
|
||||
createMockSupabaseClient,
|
||||
mockEnvVars,
|
||||
mockTablo,
|
||||
mockUser,
|
||||
} from "./test-utils.js";
|
||||
|
||||
describe("Tablo Data Router", () => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mock client types
|
||||
let mockSupabase: any;
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mock client types
|
||||
let mockS3: any;
|
||||
let restoreEnv: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
restoreEnv = mockEnvVars();
|
||||
mockSupabase = createMockSupabaseClient();
|
||||
mockS3 = createMockS3Client();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
restoreEnv();
|
||||
});
|
||||
|
||||
describe("GET /:tabloId/filenames", () => {
|
||||
it("should return list of filenames for tablo member", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.param.withArgs("tabloId").returns(mockTablo.id);
|
||||
mockContext.get.withArgs("user").returns(mockUser);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.get.withArgs("s3_client").returns(mockS3);
|
||||
|
||||
// Mock tablo access check
|
||||
mockSupabase
|
||||
.from()
|
||||
.select()
|
||||
.eq()
|
||||
.single.resolves({ data: [{ tablo_id: mockTablo.id }], error: null });
|
||||
|
||||
// Mock S3 list objects
|
||||
mockS3.send.resolves({
|
||||
Contents: [
|
||||
{ Key: `${mockTablo.id}/file1.txt` },
|
||||
{ Key: `${mockTablo.id}/file2.pdf` },
|
||||
{ Key: `${mockTablo.id}/file3.jpg` },
|
||||
],
|
||||
});
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const _tabloId = c.req.param("tabloId");
|
||||
const s3_client = c.get("s3_client");
|
||||
|
||||
try {
|
||||
const result = await s3_client.send({});
|
||||
const fileNames = result.Contents?.map(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: S3 Contents type is complex
|
||||
(content: any) => content.Key?.split("/")[1]
|
||||
// biome-ignore lint/suspicious/noExplicitAny: S3 Contents type is complex
|
||||
).filter((content: any) => content?.length && content.length > 0);
|
||||
return c.json({ fileNames: fileNames || [] });
|
||||
} catch {
|
||||
return c.json({ error: "Failed to fetch tablo files" }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result.fileNames).to.deep.equal([
|
||||
"file1.txt",
|
||||
"file2.pdf",
|
||||
"file3.jpg",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return empty array if no files exist", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.param.withArgs("tabloId").returns(mockTablo.id);
|
||||
mockContext.get.withArgs("user").returns(mockUser);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.get.withArgs("s3_client").returns(mockS3);
|
||||
|
||||
// Mock S3 list objects with no contents
|
||||
mockS3.send.resolves({
|
||||
Contents: [],
|
||||
});
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const _tabloId = c.req.param("tabloId");
|
||||
const s3_client = c.get("s3_client");
|
||||
|
||||
try {
|
||||
const result = await s3_client.send({});
|
||||
const fileNames = result.Contents?.map(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: S3 Contents type is complex
|
||||
(content: any) => content.Key?.split("/")[1]
|
||||
// biome-ignore lint/suspicious/noExplicitAny: S3 Contents type is complex
|
||||
).filter((content: any) => content?.length && content.length > 0);
|
||||
return c.json({ fileNames: fileNames || [] });
|
||||
} catch {
|
||||
return c.json({ error: "Failed to fetch tablo files" }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result.fileNames).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("should return 500 if S3 operation fails", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.param.withArgs("tabloId").returns(mockTablo.id);
|
||||
mockContext.get.withArgs("user").returns(mockUser);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.get.withArgs("s3_client").returns(mockS3);
|
||||
|
||||
// Mock S3 error
|
||||
mockS3.send.rejects(new Error("S3 error"));
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const _tabloId = c.req.param("tabloId");
|
||||
const s3_client = c.get("s3_client");
|
||||
|
||||
try {
|
||||
const result = await s3_client.send({});
|
||||
const fileNames = result.Contents?.map(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: S3 Contents type is complex
|
||||
(content: any) => content.Key?.split("/")[1]
|
||||
// biome-ignore lint/suspicious/noExplicitAny: S3 Contents type is complex
|
||||
).filter((content: any) => content?.length && content.length > 0);
|
||||
return c.json({ fileNames: fileNames || [] });
|
||||
} catch {
|
||||
return c.json({ error: "Failed to fetch tablo files" }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "Failed to fetch tablo files" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /:tabloId/:fileName", () => {
|
||||
it("should return file content for tablo member", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.param.withArgs("tabloId").returns(mockTablo.id);
|
||||
mockContext.req.param.withArgs("fileName").returns("test.txt");
|
||||
mockContext.get.withArgs("user").returns(mockUser);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.get.withArgs("s3_client").returns(mockS3);
|
||||
|
||||
const fileContent = "Hello, World!";
|
||||
const mockBody = {
|
||||
transformToString: sinon.stub().resolves(fileContent),
|
||||
};
|
||||
|
||||
// Mock S3 get object
|
||||
mockS3.send.resolves({
|
||||
Body: mockBody,
|
||||
ContentType: "text/plain",
|
||||
LastModified: new Date("2024-01-01"),
|
||||
});
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const _tabloId = c.req.param("tabloId");
|
||||
const fileName = c.req.param("fileName");
|
||||
const s3_client = c.get("s3_client");
|
||||
|
||||
try {
|
||||
const response = await s3_client.send({});
|
||||
|
||||
if (!response.Body) {
|
||||
return c.json({ error: "File not found" }, 404);
|
||||
}
|
||||
|
||||
const content = await response.Body.transformToString();
|
||||
|
||||
return c.json({
|
||||
fileName,
|
||||
content,
|
||||
contentType: response.ContentType,
|
||||
lastModified: response.LastModified,
|
||||
});
|
||||
} catch {
|
||||
return c.json({ error: "Failed to fetch file" }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result.fileName).to.equal("test.txt");
|
||||
expect(result.content).to.equal(fileContent);
|
||||
expect(result.contentType).to.equal("text/plain");
|
||||
});
|
||||
|
||||
it("should return 404 if file does not exist", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.param.withArgs("tabloId").returns(mockTablo.id);
|
||||
mockContext.req.param.withArgs("fileName").returns("nonexistent.txt");
|
||||
mockContext.get.withArgs("user").returns(mockUser);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.get.withArgs("s3_client").returns(mockS3);
|
||||
|
||||
// Mock S3 get object with no body
|
||||
mockS3.send.resolves({
|
||||
Body: null,
|
||||
});
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const _tabloId = c.req.param("tabloId");
|
||||
const fileName = c.req.param("fileName");
|
||||
const s3_client = c.get("s3_client");
|
||||
|
||||
try {
|
||||
const response = await s3_client.send({});
|
||||
|
||||
if (!response.Body) {
|
||||
return c.json({ error: "File not found" }, 404);
|
||||
}
|
||||
|
||||
const content = await response.Body.transformToString();
|
||||
|
||||
return c.json({
|
||||
fileName,
|
||||
content,
|
||||
contentType: response.ContentType,
|
||||
lastModified: response.LastModified,
|
||||
});
|
||||
} catch {
|
||||
return c.json({ error: "Failed to fetch file" }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "File not found" });
|
||||
});
|
||||
|
||||
it("should return 500 if S3 operation fails", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.param.withArgs("tabloId").returns(mockTablo.id);
|
||||
mockContext.req.param.withArgs("fileName").returns("test.txt");
|
||||
mockContext.get.withArgs("user").returns(mockUser);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.get.withArgs("s3_client").returns(mockS3);
|
||||
|
||||
// Mock S3 error
|
||||
mockS3.send.rejects(new Error("S3 error"));
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const _tabloId = c.req.param("tabloId");
|
||||
const fileName = c.req.param("fileName");
|
||||
const s3_client = c.get("s3_client");
|
||||
|
||||
try {
|
||||
const response = await s3_client.send({});
|
||||
|
||||
if (!response.Body) {
|
||||
return c.json({ error: "File not found" }, 404);
|
||||
}
|
||||
|
||||
const content = await response.Body.transformToString();
|
||||
|
||||
return c.json({
|
||||
fileName,
|
||||
content,
|
||||
contentType: response.ContentType,
|
||||
lastModified: response.LastModified,
|
||||
});
|
||||
} catch {
|
||||
return c.json({ error: "Failed to fetch file" }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "Failed to fetch file" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /:tabloId/:fileName", () => {
|
||||
it("should upload file successfully for tablo admin", async () => {
|
||||
const mockContext = createMockContext();
|
||||
const fileContent = "Hello, World!";
|
||||
mockContext.req.param.withArgs("tabloId").returns(mockTablo.id);
|
||||
mockContext.req.param.withArgs("fileName").returns("test.txt");
|
||||
mockContext.req.json.resolves({
|
||||
content: fileContent,
|
||||
contentType: "text/plain",
|
||||
});
|
||||
mockContext.get.withArgs("user").returns(mockUser);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.get.withArgs("s3_client").returns(mockS3);
|
||||
|
||||
// Mock S3 put object
|
||||
mockS3.send.resolves({});
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const tabloId = c.req.param("tabloId");
|
||||
const fileName = c.req.param("fileName");
|
||||
const s3_client = c.get("s3_client");
|
||||
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const { content } = body;
|
||||
|
||||
if (!content) {
|
||||
return c.json({ error: "Content is required" }, 400);
|
||||
}
|
||||
|
||||
await s3_client.send({});
|
||||
|
||||
return c.json({
|
||||
message: "File uploaded successfully",
|
||||
fileName,
|
||||
tabloId,
|
||||
});
|
||||
} catch {
|
||||
return c.json({ error: "Failed to upload file" }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
message: "File uploaded successfully",
|
||||
fileName: "test.txt",
|
||||
tabloId: mockTablo.id,
|
||||
});
|
||||
expect(mockS3.send.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it("should return 400 if content is missing", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.param.withArgs("tabloId").returns(mockTablo.id);
|
||||
mockContext.req.param.withArgs("fileName").returns("test.txt");
|
||||
mockContext.req.json.resolves({
|
||||
contentType: "text/plain",
|
||||
});
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const _tabloId = c.req.param("tabloId");
|
||||
const _fileName = c.req.param("fileName");
|
||||
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const { content } = body;
|
||||
|
||||
if (!content) {
|
||||
return c.json({ error: "Content is required" }, 400);
|
||||
}
|
||||
|
||||
return c.json({ message: "Success" });
|
||||
} catch {
|
||||
return c.json({ error: "Failed to upload file" }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "Content is required" });
|
||||
});
|
||||
|
||||
it("should return 500 if S3 upload fails", async () => {
|
||||
const mockContext = createMockContext();
|
||||
const fileContent = "Hello, World!";
|
||||
mockContext.req.param.withArgs("tabloId").returns(mockTablo.id);
|
||||
mockContext.req.param.withArgs("fileName").returns("test.txt");
|
||||
mockContext.req.json.resolves({
|
||||
content: fileContent,
|
||||
contentType: "text/plain",
|
||||
});
|
||||
mockContext.get.withArgs("s3_client").returns(mockS3);
|
||||
|
||||
// Mock S3 error
|
||||
mockS3.send.rejects(new Error("S3 error"));
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const tabloId = c.req.param("tabloId");
|
||||
const fileName = c.req.param("fileName");
|
||||
const s3_client = c.get("s3_client");
|
||||
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const { content } = body;
|
||||
|
||||
if (!content) {
|
||||
return c.json({ error: "Content is required" }, 400);
|
||||
}
|
||||
|
||||
await s3_client.send({});
|
||||
|
||||
return c.json({
|
||||
message: "File uploaded successfully",
|
||||
fileName,
|
||||
tabloId,
|
||||
});
|
||||
} catch {
|
||||
return c.json({ error: "Failed to upload file" }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "Failed to upload file" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /:tabloId/:fileName", () => {
|
||||
it("should delete file successfully for tablo admin", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.param.withArgs("tabloId").returns(mockTablo.id);
|
||||
mockContext.req.param.withArgs("fileName").returns("test.txt");
|
||||
mockContext.get.withArgs("user").returns(mockUser);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.get.withArgs("s3_client").returns(mockS3);
|
||||
|
||||
// Mock S3 delete object
|
||||
mockS3.send.resolves({});
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const tabloId = c.req.param("tabloId");
|
||||
const fileName = c.req.param("fileName");
|
||||
const s3_client = c.get("s3_client");
|
||||
|
||||
try {
|
||||
await s3_client.send({});
|
||||
|
||||
return c.json({
|
||||
message: "File deleted successfully",
|
||||
fileName,
|
||||
tabloId,
|
||||
});
|
||||
} catch {
|
||||
return c.json({ error: "Failed to delete file" }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
message: "File deleted successfully",
|
||||
fileName: "test.txt",
|
||||
tabloId: mockTablo.id,
|
||||
});
|
||||
expect(mockS3.send.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it("should return 500 if S3 delete fails", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.param.withArgs("tabloId").returns(mockTablo.id);
|
||||
mockContext.req.param.withArgs("fileName").returns("test.txt");
|
||||
mockContext.get.withArgs("s3_client").returns(mockS3);
|
||||
|
||||
// Mock S3 error
|
||||
mockS3.send.rejects(new Error("S3 error"));
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const tabloId = c.req.param("tabloId");
|
||||
const fileName = c.req.param("fileName");
|
||||
const s3_client = c.get("s3_client");
|
||||
|
||||
try {
|
||||
await s3_client.send({});
|
||||
|
||||
return c.json({
|
||||
message: "File deleted successfully",
|
||||
fileName,
|
||||
tabloId,
|
||||
});
|
||||
} catch {
|
||||
return c.json({ error: "Failed to delete file" }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "Failed to delete file" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
import { expect } from "chai";
|
||||
import { afterEach, beforeEach, describe, it } from "mocha";
|
||||
import sinon from "sinon";
|
||||
import {
|
||||
createMockContext,
|
||||
createMockS3Client,
|
||||
createMockSupabaseClient,
|
||||
mockEnvVars,
|
||||
} from "./test-utils.js";
|
||||
|
||||
describe("Tasks Router", () => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mock client types
|
||||
let mockSupabase: any;
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mock client types
|
||||
let mockS3: any;
|
||||
let restoreEnv: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
restoreEnv = mockEnvVars();
|
||||
mockSupabase = createMockSupabaseClient();
|
||||
mockS3 = createMockS3Client();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
restoreEnv();
|
||||
});
|
||||
|
||||
describe("POST /sync-calendars", () => {
|
||||
it("should sync all calendars successfully with valid auth", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.header
|
||||
.withArgs("Authorization")
|
||||
.returns(`Basic ${process.env.SYNC_CALS_SECRET}`);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
|
||||
const subscriptions = [
|
||||
{
|
||||
token: "token1",
|
||||
tablo_id: "tablo1",
|
||||
tablos: { name: "Tablo 1" },
|
||||
},
|
||||
{
|
||||
token: "token2",
|
||||
tablo_id: "tablo2",
|
||||
tablos: { name: "Tablo 2" },
|
||||
},
|
||||
];
|
||||
|
||||
// Mock calendar subscriptions query
|
||||
const subscriptionBuilder = {
|
||||
select: sinon.stub().resolves({ data: subscriptions, error: null }),
|
||||
};
|
||||
|
||||
mockSupabase.from
|
||||
.withArgs("calendar_subscriptions")
|
||||
.returns(subscriptionBuilder);
|
||||
|
||||
// Mock events query for each tablo
|
||||
const eventsBuilder = {
|
||||
select: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().resolves({ data: [], error: null }),
|
||||
};
|
||||
|
||||
mockSupabase.from.withArgs("events_and_tablos").returns(eventsBuilder);
|
||||
|
||||
// Mock S3 send
|
||||
mockS3.send.resolves({});
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const supabase = c.get("supabase");
|
||||
if (
|
||||
c.req.header("Authorization") !==
|
||||
`Basic ${process.env.SYNC_CALS_SECRET}`
|
||||
) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from("calendar_subscriptions")
|
||||
.select("token, tablo_id, tablos(name)");
|
||||
|
||||
if (error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
}
|
||||
|
||||
return c.json({ message: "Synced calendars" });
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ message: "Synced calendars" });
|
||||
});
|
||||
|
||||
it("should return 401 if authorization header is missing", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.header.withArgs("Authorization").returns(undefined);
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
if (
|
||||
c.req.header("Authorization") !==
|
||||
`Basic ${process.env.SYNC_CALS_SECRET}`
|
||||
) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
return c.json({ message: "Success" });
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "Unauthorized" });
|
||||
});
|
||||
|
||||
it("should return 401 if authorization header is invalid", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.header
|
||||
.withArgs("Authorization")
|
||||
.returns("Basic invalid-secret");
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
if (
|
||||
c.req.header("Authorization") !==
|
||||
`Basic ${process.env.SYNC_CALS_SECRET}`
|
||||
) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
return c.json({ message: "Success" });
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "Unauthorized" });
|
||||
});
|
||||
|
||||
it("should return 500 if database error occurs", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.header
|
||||
.withArgs("Authorization")
|
||||
.returns(`Basic ${process.env.SYNC_CALS_SECRET}`);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
|
||||
// Mock calendar subscriptions query with error
|
||||
const subscriptionBuilder = {
|
||||
select: sinon
|
||||
.stub()
|
||||
.resolves({ data: null, error: { message: "Database error" } }),
|
||||
};
|
||||
|
||||
mockSupabase.from
|
||||
.withArgs("calendar_subscriptions")
|
||||
.returns(subscriptionBuilder);
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const supabase = c.get("supabase");
|
||||
if (
|
||||
c.req.header("Authorization") !==
|
||||
`Basic ${process.env.SYNC_CALS_SECRET}`
|
||||
) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from("calendar_subscriptions")
|
||||
.select("token, tablo_id, tablos(name)");
|
||||
|
||||
if (error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
}
|
||||
|
||||
return c.json({ message: "Synced calendars" });
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "Database error" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
import type { S3Client } from "@aws-sdk/client-s3";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { expect } from "chai";
|
||||
import type { SinonStub, SinonStubbedInstance } from "sinon";
|
||||
import sinon from "sinon";
|
||||
import type { StreamChat } from "stream-chat";
|
||||
|
||||
// Mock user for testing
|
||||
export const mockUser = {
|
||||
id: "test-user-id",
|
||||
email: "test@example.com",
|
||||
aud: "authenticated",
|
||||
role: "authenticated",
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
app_metadata: {},
|
||||
user_metadata: {},
|
||||
};
|
||||
|
||||
export const mockProfile = {
|
||||
id: "test-user-id",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
short_user_id: "testuser",
|
||||
is_temporary: false,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
export const mockTablo = {
|
||||
id: "test-tablo-id",
|
||||
name: "Test Tablo",
|
||||
color: "bg-blue-500",
|
||||
status: "todo",
|
||||
owner_id: "test-user-id",
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
deleted_at: null,
|
||||
};
|
||||
|
||||
export const mockEvent = {
|
||||
id: "test-event-id",
|
||||
tablo_id: "test-tablo-id",
|
||||
title: "Test Event",
|
||||
description: "Test description",
|
||||
start_date: "2024-01-16",
|
||||
start_time: "10:00",
|
||||
end_time: "11:00",
|
||||
created_by: "test-user-id",
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
deleted_at: null,
|
||||
};
|
||||
|
||||
// Create a mock Supabase client
|
||||
export function createMockSupabaseClient(): SupabaseClient {
|
||||
const mockSupabase = {
|
||||
auth: {
|
||||
getUser: sinon.stub(),
|
||||
signUp: sinon.stub(),
|
||||
signIn: sinon.stub(),
|
||||
},
|
||||
from: sinon.stub(),
|
||||
};
|
||||
|
||||
// Setup default behavior for from() which returns a query builder
|
||||
const createQueryBuilder = () => ({
|
||||
select: sinon.stub().returnsThis(),
|
||||
insert: sinon.stub().returnsThis(),
|
||||
update: sinon.stub().returnsThis(),
|
||||
delete: sinon.stub().returnsThis(),
|
||||
eq: sinon.stub().returnsThis(),
|
||||
neq: sinon.stub().returnsThis(),
|
||||
gt: sinon.stub().returnsThis(),
|
||||
gte: sinon.stub().returnsThis(),
|
||||
lt: sinon.stub().returnsThis(),
|
||||
lte: sinon.stub().returnsThis(),
|
||||
is: sinon.stub().returnsThis(),
|
||||
in: sinon.stub().returnsThis(),
|
||||
single: sinon.stub(),
|
||||
limit: sinon.stub().returnsThis(),
|
||||
order: sinon.stub().returnsThis(),
|
||||
});
|
||||
|
||||
mockSupabase.from.returns(createQueryBuilder());
|
||||
|
||||
return mockSupabase as unknown as SupabaseClient;
|
||||
}
|
||||
|
||||
// Create a mock Stream Chat client
|
||||
export function createMockStreamChatClient(): {
|
||||
mockStreamChat: StreamChat;
|
||||
mockChannel: ReturnType<StreamChat["channel"]>;
|
||||
} {
|
||||
const mockChannel = {
|
||||
create: sinon.stub().resolves(),
|
||||
update: sinon.stub().resolves(),
|
||||
delete: sinon.stub().resolves(),
|
||||
addMembers: sinon.stub().resolves(),
|
||||
removeMembers: sinon.stub().resolves(),
|
||||
sendMessage: sinon.stub().resolves(),
|
||||
};
|
||||
|
||||
const mockStreamChat = {
|
||||
upsertUser: sinon.stub().resolves(),
|
||||
createToken: sinon.stub().returns("mock-stream-token"),
|
||||
channel: sinon.stub().returns(mockChannel),
|
||||
};
|
||||
|
||||
return {
|
||||
mockStreamChat: mockStreamChat as unknown as StreamChat,
|
||||
mockChannel: mockChannel as unknown as ReturnType<StreamChat["channel"]>,
|
||||
};
|
||||
}
|
||||
|
||||
// Create a mock S3 client
|
||||
export function createMockS3Client(): S3Client {
|
||||
const mockS3 = {
|
||||
send: sinon.stub(),
|
||||
};
|
||||
|
||||
return mockS3 as unknown as S3Client;
|
||||
}
|
||||
|
||||
// Create a mock transporter
|
||||
export function createMockTransporter(): { sendMail: SinonStub } {
|
||||
return {
|
||||
sendMail: sinon.stub().resolves({ messageId: "mock-message-id" }),
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to create a mock Hono context
|
||||
export function createMockContext(overrides: Record<string, unknown> = {}) {
|
||||
const context = {
|
||||
req: {
|
||||
json: sinon.stub(),
|
||||
header: sinon.stub(),
|
||||
param: sinon.stub(),
|
||||
},
|
||||
json: sinon.stub().returnsArg(0),
|
||||
get: sinon.stub(),
|
||||
set: sinon.stub(),
|
||||
...overrides,
|
||||
};
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mock context needs flexibility
|
||||
return context as any;
|
||||
}
|
||||
|
||||
// Helper to create a mock next function
|
||||
export function createMockNext() {
|
||||
return sinon.stub().resolves();
|
||||
}
|
||||
|
||||
// Helper to reset all stubs
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Flexible stub reset utility
|
||||
export function resetAllStubs(...stubs: any[]) {
|
||||
stubs.forEach((stub) => {
|
||||
if (stub && typeof stub.reset === "function") {
|
||||
stub.reset();
|
||||
} else if (stub && typeof stub === "object") {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Need to check nested values
|
||||
Object.values(stub).forEach((value: any) => {
|
||||
if (value && typeof value.reset === "function") {
|
||||
value.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to verify stub was called with specific args
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Flexible argument checking
|
||||
export function assertCalledWith(stub: SinonStub, ...args: any[]) {
|
||||
expect(stub.calledWith(...args)).to.be.true;
|
||||
}
|
||||
|
||||
// Helper to verify stub was called once
|
||||
export function assertCalledOnce(stub: SinonStub) {
|
||||
expect(stub.calledOnce).to.be.true;
|
||||
}
|
||||
|
||||
// Helper to verify stub was not called
|
||||
export function assertNotCalled(stub: SinonStub) {
|
||||
expect(stub.called).to.be.false;
|
||||
}
|
||||
|
||||
// Mock environment variables
|
||||
export function mockEnvVars() {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
process.env.SUPABASE_URL = "https://test.supabase.co";
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY = "test-service-role-key";
|
||||
process.env.STREAM_CHAT_API_KEY = "test-stream-key";
|
||||
process.env.STREAM_CHAT_API_SECRET = "test-stream-secret";
|
||||
process.env.R2_ACCOUNT_ID = "test-r2-account";
|
||||
process.env.R2_ACCESS_KEY_ID = "test-r2-access-key";
|
||||
process.env.R2_SECRET_ACCESS_KEY = "test-r2-secret";
|
||||
process.env.NODE_ENV = "test";
|
||||
process.env.FRONTEND_URL = "https://app.test.com";
|
||||
process.env.SYNC_CALS_SECRET = "test-sync-secret";
|
||||
|
||||
return () => {
|
||||
process.env = originalEnv;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,337 +0,0 @@
|
|||
import { expect } from "chai";
|
||||
import { Hono } from "hono";
|
||||
import { afterEach, beforeEach, describe, it } from "mocha";
|
||||
import sinon from "sinon";
|
||||
import { userRouter } from "../user.js";
|
||||
import {
|
||||
createMockContext,
|
||||
createMockNext,
|
||||
createMockStreamChatClient,
|
||||
createMockSupabaseClient,
|
||||
createMockTransporter,
|
||||
mockEnvVars,
|
||||
mockProfile,
|
||||
mockUser,
|
||||
resetAllStubs,
|
||||
} from "./test-utils.js";
|
||||
|
||||
describe("User Router", () => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mock client types
|
||||
let mockSupabase: any;
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mock client types
|
||||
let mockStreamChat: any;
|
||||
let restoreEnv: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
restoreEnv = mockEnvVars();
|
||||
mockSupabase = createMockSupabaseClient();
|
||||
const streamMocks = createMockStreamChatClient();
|
||||
mockStreamChat = streamMocks.mockStreamChat;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
restoreEnv();
|
||||
});
|
||||
|
||||
describe("POST /sign-up-to-stream", () => {
|
||||
it("should successfully sign up user to Stream Chat", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.get.withArgs("user").returns(mockUser);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.get.withArgs("streamServerClient").returns(mockStreamChat);
|
||||
|
||||
// Mock Supabase response
|
||||
mockSupabase
|
||||
.from()
|
||||
.select()
|
||||
.eq()
|
||||
.single.resolves({ data: mockProfile, error: null });
|
||||
|
||||
// Create a test handler
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const { id } = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
|
||||
const { data } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("id", id)
|
||||
.single();
|
||||
|
||||
const streamServerClient = c.get("streamServerClient");
|
||||
await streamServerClient.upsertUser({
|
||||
id,
|
||||
name: data.name ?? "",
|
||||
language: "fr",
|
||||
});
|
||||
|
||||
return c.json({
|
||||
message: "User signed up to stream",
|
||||
});
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(mockStreamChat.upsertUser.calledOnce).to.be.true;
|
||||
expect(
|
||||
mockStreamChat.upsertUser.calledWith({
|
||||
id: mockUser.id,
|
||||
name: mockProfile.name,
|
||||
language: "fr",
|
||||
})
|
||||
).to.be.true;
|
||||
expect(result).to.deep.equal({ message: "User signed up to stream" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /me", () => {
|
||||
it("should return user profile with Stream token", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.get.withArgs("user").returns(mockUser);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.get.withArgs("streamServerClient").returns(mockStreamChat);
|
||||
|
||||
// Mock Supabase response
|
||||
mockSupabase
|
||||
.from()
|
||||
.select()
|
||||
.eq()
|
||||
.single.resolves({ data: mockProfile, error: null });
|
||||
|
||||
// Create a test handler
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const user = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
const streamServerClient = c.get("streamServerClient");
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
|
||||
if (!data) {
|
||||
return c.json({ error: "User not found" }, 404);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
}
|
||||
|
||||
const user_id = data.id;
|
||||
const token = streamServerClient.createToken(user_id);
|
||||
|
||||
return c.json({
|
||||
...data,
|
||||
streamToken: token,
|
||||
});
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(mockStreamChat.createToken.calledOnce).to.be.true;
|
||||
expect(mockStreamChat.createToken.calledWith(mockUser.id)).to.be.true;
|
||||
expect(result).to.deep.equal({
|
||||
...mockProfile,
|
||||
streamToken: "mock-stream-token",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return 404 if user profile not found", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.get.withArgs("user").returns(mockUser);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.get.withArgs("streamServerClient").returns(mockStreamChat);
|
||||
|
||||
// Mock Supabase response with no data
|
||||
mockSupabase
|
||||
.from()
|
||||
.select()
|
||||
.eq()
|
||||
.single.resolves({ data: null, error: null });
|
||||
|
||||
// Create a test handler
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const user = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
const streamServerClient = c.get("streamServerClient");
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
|
||||
if (!data) {
|
||||
return c.json({ error: "User not found" }, 404);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
}
|
||||
|
||||
const user_id = data.id;
|
||||
const token = streamServerClient.createToken(user_id);
|
||||
|
||||
return c.json({
|
||||
...data,
|
||||
streamToken: token,
|
||||
});
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "User not found" });
|
||||
});
|
||||
|
||||
it("should return 500 if database error occurs", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.get.withArgs("user").returns(mockUser);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
mockContext.get.withArgs("streamServerClient").returns(mockStreamChat);
|
||||
|
||||
// Mock Supabase response with error
|
||||
mockSupabase
|
||||
.from()
|
||||
.select()
|
||||
.eq()
|
||||
.single.resolves({
|
||||
data: mockProfile,
|
||||
error: { message: "Database error" },
|
||||
});
|
||||
|
||||
// Create a test handler
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const user = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
const streamServerClient = c.get("streamServerClient");
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
|
||||
if (!data) {
|
||||
return c.json({ error: "User not found" }, 404);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
}
|
||||
|
||||
const user_id = data.id;
|
||||
const token = streamServerClient.createToken(user_id);
|
||||
|
||||
return c.json({
|
||||
...data,
|
||||
streamToken: token,
|
||||
});
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "Database error" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /mark-temporary", () => {
|
||||
it("should mark user as temporary and send email", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.json.resolves({ temporary_password: "temp123" });
|
||||
mockContext.get.withArgs("user").returns(mockUser);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
|
||||
// Mock Supabase update response
|
||||
mockSupabase
|
||||
.from()
|
||||
.update()
|
||||
.eq()
|
||||
.select()
|
||||
.single.resolves({
|
||||
data: { ...mockProfile, is_temporary: true },
|
||||
error: null,
|
||||
});
|
||||
|
||||
// Create a test handler
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const user = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
|
||||
await c.req.json();
|
||||
|
||||
const { error } = await supabase
|
||||
.from("profiles")
|
||||
.update({
|
||||
is_temporary: true,
|
||||
})
|
||||
.eq("id", user.id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
message: "User marked as temporary",
|
||||
});
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ message: "User marked as temporary" });
|
||||
});
|
||||
|
||||
it("should return 500 if database update fails", async () => {
|
||||
const mockContext = createMockContext();
|
||||
mockContext.req.json.resolves({ temporary_password: "temp123" });
|
||||
mockContext.get.withArgs("user").returns(mockUser);
|
||||
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
||||
|
||||
// Mock Supabase error response
|
||||
mockSupabase
|
||||
.from()
|
||||
.update()
|
||||
.eq()
|
||||
.select()
|
||||
.single.resolves({ data: null, error: { message: "Update failed" } });
|
||||
|
||||
// Create a test handler
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
||||
const handler = async (c: any) => {
|
||||
const user = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
|
||||
await c.req.json();
|
||||
|
||||
const { error } = await supabase
|
||||
.from("profiles")
|
||||
.update({
|
||||
is_temporary: true,
|
||||
})
|
||||
.eq("id", user.id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
message: "User marked as temporary",
|
||||
});
|
||||
};
|
||||
|
||||
const result = await handler(mockContext);
|
||||
|
||||
expect(result).to.deep.equal({ error: "Update failed" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,15 +1,27 @@
|
|||
import { PutObjectCommand, type S3Client } from "@aws-sdk/client-s3";
|
||||
import { PostgrestError, type SupabaseClient, type User } from "@supabase/supabase-js";
|
||||
import {
|
||||
PostgrestError,
|
||||
type SupabaseClient,
|
||||
type User,
|
||||
} from "@supabase/supabase-js";
|
||||
import { Hono } from "hono";
|
||||
import type { Transporter } from "nodemailer";
|
||||
import type { StreamChat } from "stream-chat";
|
||||
import { config } from "./config.js";
|
||||
import type { Tables } from "./database.types.ts";
|
||||
import { generateICSFromEvents, writeCalendarFileToR2 } from "./helpers.js";
|
||||
import { authMiddleware, r2Middleware, streamChatMiddleware } from "./middleware.js";
|
||||
import {
|
||||
authMiddleware,
|
||||
r2Middleware,
|
||||
streamChatMiddleware,
|
||||
} from "./middleware.js";
|
||||
import { generateToken } from "./token.js";
|
||||
import { transporter } from "./transporter.js";
|
||||
import type { EventAndTablo, EventInsertInTablo, TabloInsert } from "./types.ts";
|
||||
import type {
|
||||
EventAndTablo,
|
||||
EventInsertInTablo,
|
||||
TabloInsert,
|
||||
} from "./types.ts";
|
||||
|
||||
export const tabloRouter = new Hono<{
|
||||
Variables: {
|
||||
|
|
@ -166,7 +178,9 @@ tabloRouter.post("/create-and-invite", async (c) => {
|
|||
const { data: insertedTablo, error } = await supabase
|
||||
.from("tablos")
|
||||
.insert({
|
||||
name: `${invitedUserDataTyped.name || "Invité"} / ${ownerDataTyped.name || "Propriétaire"}`,
|
||||
name: `${invitedUserDataTyped.name || "Invité"} / ${
|
||||
ownerDataTyped.name || "Propriétaire"
|
||||
}`,
|
||||
color: "bg-blue-500",
|
||||
status: "todo",
|
||||
owner_id: ownerId,
|
||||
|
|
@ -184,20 +198,22 @@ tabloRouter.post("/create-and-invite", async (c) => {
|
|||
}
|
||||
|
||||
// Grant access to the current user (invited user) as a non-admin member
|
||||
const { error: tabloAccessError } = await supabase.from("tablo_access").insert(
|
||||
{
|
||||
tablo_id: tabloData.id,
|
||||
user_id: user.id,
|
||||
// ** IMPORTANT **
|
||||
is_admin: false,
|
||||
// -------------
|
||||
is_active: true,
|
||||
granted_by: ownerId,
|
||||
}
|
||||
// {
|
||||
// onConflict: "tablo_id, user_id",
|
||||
// }
|
||||
);
|
||||
const { error: tabloAccessError } = await supabase
|
||||
.from("tablo_access")
|
||||
.insert(
|
||||
{
|
||||
tablo_id: tabloData.id,
|
||||
user_id: user.id,
|
||||
// ** IMPORTANT **
|
||||
is_admin: false,
|
||||
// -------------
|
||||
is_active: true,
|
||||
granted_by: ownerId,
|
||||
}
|
||||
// {
|
||||
// onConflict: "tablo_id, user_id",
|
||||
// }
|
||||
);
|
||||
|
||||
if (tabloAccessError) {
|
||||
console.error("tabloAccessError", tabloAccessError);
|
||||
|
|
@ -290,7 +306,8 @@ tabloRouter.patch("/update", async (c) => {
|
|||
|
||||
const updatedTablo = update as Tables<"tablos">;
|
||||
|
||||
const isUpdatingName = tablo.name !== undefined && tablo.name !== updatedTablo.name;
|
||||
const isUpdatingName =
|
||||
tablo.name !== undefined && tablo.name !== updatedTablo.name;
|
||||
|
||||
if (error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
|
|
@ -366,7 +383,10 @@ tabloRouter.post("/invite", async (c) => {
|
|||
}
|
||||
|
||||
if (tablo.owner_id !== sender.id) {
|
||||
return c.json({ error: "You are not allowed to invite users to this tablo" }, 400);
|
||||
return c.json(
|
||||
{ error: "You are not allowed to invite users to this tablo" },
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const { error } = await supabase.from("tablo_invites").insert({
|
||||
|
|
@ -386,7 +406,9 @@ tabloRouter.post("/invite", async (c) => {
|
|||
subject: "Vous avez été invité à un tablo",
|
||||
html: `<p>Vous avez été invité à un tablo avec <a href="${
|
||||
config.XTABLO_URL
|
||||
}/join/${encodeURIComponent(tablo.name)}?token=${encodeURIComponent(token)}">ce lien</a></p>`,
|
||||
}/join/${encodeURIComponent(tablo.name)}?token=${encodeURIComponent(
|
||||
token
|
||||
)}">ce lien</a></p>`,
|
||||
});
|
||||
|
||||
return c.json({
|
||||
|
|
@ -419,15 +441,17 @@ tabloRouter.post("/join", async (c) => {
|
|||
|
||||
const { id: invite_id, tablo_id, invited_by } = inviteData;
|
||||
|
||||
const { error: tabloAccessError } = await supabase.from("tablo_access").insert({
|
||||
tablo_id,
|
||||
user_id: joiner.id,
|
||||
// ** IMPORTANT **
|
||||
is_admin: false,
|
||||
// -------------
|
||||
is_active: true,
|
||||
granted_by: invited_by,
|
||||
});
|
||||
const { error: tabloAccessError } = await supabase
|
||||
.from("tablo_access")
|
||||
.insert({
|
||||
tablo_id,
|
||||
user_id: joiner.id,
|
||||
// ** IMPORTANT **
|
||||
is_admin: false,
|
||||
// -------------
|
||||
is_active: true,
|
||||
granted_by: invited_by,
|
||||
});
|
||||
|
||||
if (tabloAccessError) {
|
||||
console.error("tabloAccessError", tabloAccessError);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ import type { S3Client } from "@aws-sdk/client-s3";
|
|||
import type { SupabaseClient, User } from "@supabase/supabase-js";
|
||||
import { type Context, Hono, type Next } from "hono";
|
||||
import { getTabloFileNames, isTabloAdmin, isTabloMember } from "./helpers.js";
|
||||
import { authMiddleware, r2Middleware, streamChatMiddleware } from "./middleware.js";
|
||||
import {
|
||||
authMiddleware,
|
||||
r2Middleware,
|
||||
streamChatMiddleware,
|
||||
} from "./middleware.js";
|
||||
|
||||
export const tabloDataRouter = new Hono<{
|
||||
Variables: {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { google } from "googleapis";
|
||||
import nodemailer from "nodemailer";
|
||||
import type { Transporter } from "nodemailer";
|
||||
import { config } from "./config.js";
|
||||
|
||||
const OAuth2 = google.auth.OAuth2;
|
||||
|
||||
let _transporter: Transporter | null = null;
|
||||
|
||||
export const createTransporter = async () => {
|
||||
const oauth2Client = new OAuth2(
|
||||
config.EMAIL_CLIENT_ID,
|
||||
|
|
@ -31,4 +34,22 @@ export const createTransporter = async () => {
|
|||
return transporter;
|
||||
};
|
||||
|
||||
export const transporter = await createTransporter();
|
||||
// Lazy-loaded transporter to avoid top-level await
|
||||
export async function getTransporter(): Promise<Transporter> {
|
||||
if (!_transporter) {
|
||||
_transporter = await createTransporter();
|
||||
}
|
||||
return _transporter;
|
||||
}
|
||||
|
||||
// For backwards compatibility (will be deprecated)
|
||||
export let transporter: Transporter | null = null;
|
||||
|
||||
// Initialize on first import (but not blocking)
|
||||
getTransporter()
|
||||
.then((t) => {
|
||||
transporter = t;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to initialize transporter:", error);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ userRouter.post("/mark-temporary", async (c) => {
|
|||
}
|
||||
|
||||
try {
|
||||
if (profile?.email) {
|
||||
if (profile?.email && transporter) {
|
||||
const mailOptions = {
|
||||
from: "Xtablo <noreply@xtablo.com>",
|
||||
to: profile.email,
|
||||
|
|
|
|||
Loading…
Reference in a new issue