Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: CI Tests

on:
push:
branches: [ main, development, testing ]
pull_request:
branches: [ main, development, testing ]

jobs:
backend-tests:
runs-on: ubuntu-latest

steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install backend deps
working-directory: backend
run: npm ci

- name: Run backend unit tests
working-directory: backend
run: npm run test

- name: Run backend e2e tests
working-directory: backend
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
run: |
cat <<EOF > .env.local
SUPABASE_URL=${SUPABASE_URL}
SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY}
SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}
PORT=3000
EOF
npm run test:e2e

frontend-tests:
runs-on: ubuntu-latest

steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install frontend deps
working-directory: frontend
run: npm ci

- name: Run frontend tests
working-directory: frontend
run: npm run test
104 changes: 104 additions & 0 deletions backend/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {
BadRequestException,
ConflictException,
InternalServerErrorException,
UnauthorizedException,
} from '@nestjs/common';
import { AuthService } from './auth.service';

// Minimal SupabaseService shape for mocking
interface MockSupabaseClient {
from: jest.Mock;
auth: {
signUp: jest.Mock;
signInWithPassword: jest.Mock;
};
}

describe('AuthService (unit)', () => {
let service: AuthService;
let mockSupabaseClient: MockSupabaseClient;
let mockSupabaseService: {
getClient: jest.Mock;
getAnonClient: jest.Mock;
};

beforeEach(() => {
mockSupabaseClient = {
from: jest.fn(),
auth: {
signUp: jest.fn(),
signInWithPassword: jest.fn(),
},
};

mockSupabaseService = {
getClient: jest.fn(() => mockSupabaseClient),
getAnonClient: jest.fn(() => mockSupabaseClient),
};

service = new AuthService(mockSupabaseService as any);
});

describe('signup', () => {
const baseSignupDto = {
email: 'TestUser@example.com',
password: 'Test1234!',
displayName: 'TestUser',
firstName: 'Test',
lastName: 'User',
};

it('throws ConflictException when display name is already taken', async () => {
// Mock supabase.from('users').select(...) to return an existing user
mockSupabaseClient.from.mockImplementation((table: string) => {
if (table === 'users') {
return {
select: jest.fn().mockResolvedValue({
data: [{ display_name: 'testuser' }],
error: null,
}),
} as any;
}
return { select: jest.fn().mockResolvedValue({ data: [], error: null }) } as any;
});

await expect(service.signup(baseSignupDto as any)).rejects.toThrow(
ConflictException,
);
await expect(service.signup(baseSignupDto as any)).rejects.toThrow(
'This display name is already taken. Please choose a different name.',
);
});

it('throws ConflictException when email is already registered', async () => {
// 1) No duplicate display name
mockSupabaseClient.from.mockImplementation((table: string) => {
if (table === 'users') {
return {
select: jest.fn().mockResolvedValue({
data: [{ display_name: 'someone-else' }],
error: null,
}),
} as any;
}
return { select: jest.fn().mockResolvedValue({ data: [], error: null }) } as any;
});

// 2) Supabase auth.signUp returns an "already registered" error
mockSupabaseClient.auth.signUp.mockResolvedValueOnce({
data: { user: null, session: null },
error: { message: 'already registered' },
});

const promise = service.signup(baseSignupDto as any);

await expect(promise).rejects.toThrow(ConflictException);
await expect(promise).rejects.toThrow(
'An account with this email already exists.',
);
});
});
});


54 changes: 50 additions & 4 deletions backend/test/app.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { config } from 'dotenv';
import { join } from 'path';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
import { config } from 'dotenv';
import { join } from 'path';

describe('AppController (e2e)', () => {
describe('App e2e', () => {
let app: INestApplication<App>;

beforeEach(async () => {
// Load test environment (points at test Supabase project, etc.)
config({ path: join(__dirname, '..', '.env.local') });

const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
Expand All @@ -29,4 +31,48 @@ describe('AppController (e2e)', () => {
.expect(200)
.expect('Hello World!');
});

it('Flow 1: signup -> login -> GET /circles returns empty list for new user', async () => {
const server = app.getHttpServer();

const ts = Date.now();
const email = `qa-user-${ts}@example.com`;
const password = 'Test1234!';
const displayName = `qa_user_${ts}`;
const firstName = 'QA';
const lastName = 'User';

// 1) Sign up a brand new user
await request(server)
.post('/auth/signup')
.send({
email,
password,
displayName,
firstName,
lastName,
})
.expect(201);

// 2) Log in with the same credentials to get a Supabase access token
const loginRes = await request(server)
.post('/auth/login')
.send({
identifier: email,
password,
})
.expect(200);

const accessToken = loginRes.body?.session?.access_token;
expect(accessToken).toBeDefined();

// 3) Call /circles with the Bearer token and expect an empty array
const circlesRes = await request(server)
.get('/circles')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);

expect(Array.isArray(circlesRes.body)).toBe(true);
expect(circlesRes.body.length).toBe(0);
});
});