Skip to content
Merged
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
94 changes: 94 additions & 0 deletions apps/auth/__tests__/deletion.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { DeletionUseCases } from '../src/useCases/deletion.js'
import { UsersUseCases } from '../src/useCases/users.js'
import { DeletionRequestStatus, User } from '@auto-drive/models'
import { closeDatabase, getDatabase } from '../src/drivers/pg.js'
import { dbMigration } from './utils/dbMigrate.js'
import { createUnonboardedUser } from './utils/mocks.js'

describe('DeletionUseCases', () => {
let testUser: User

beforeAll(async () => {
await getDatabase()
await dbMigration.up()

// Onboard a test user
const unonboarded = createUnonboardedUser()
const onboarded = await UsersUseCases.onboardUser(unonboarded)
if (!onboarded) {
throw new Error('Failed to onboard test user')
}
testUser = onboarded
})

afterAll(async () => {
await closeDatabase()
await dbMigration.down()
})

it('should create a deletion request', async () => {
const request = await DeletionUseCases.requestDeletion(
testUser,
'Testing deletion',
)

expect(request).toBeDefined()
expect(request.userPublicId).toBe(testUser.publicId)
expect(request.status).toBe(DeletionRequestStatus.Pending)
expect(request.reason).toBe('Testing deletion')
expect(request.scheduledAnonymisationAt).toBeDefined()

// Scheduled date should be approximately 30 days from now
const scheduledDate = new Date(request.scheduledAnonymisationAt)
const now = new Date()
const diffDays =
(scheduledDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
expect(diffDays).toBeGreaterThan(29)
expect(diffDays).toBeLessThan(31)
})

it('should return existing request if one is already pending', async () => {
const request1 = await DeletionUseCases.requestDeletion(testUser)
const request2 = await DeletionUseCases.requestDeletion(testUser)

expect(request1.id).toBe(request2.id)
})

it('should return deletion status for user', async () => {
const status = await DeletionUseCases.getDeletionStatus(testUser)

expect(status).toBeDefined()
expect(status?.status).toBe(DeletionRequestStatus.Pending)
})

it('should cancel a deletion request', async () => {
const cancelled = await DeletionUseCases.cancelDeletion(testUser)

expect(cancelled).toBeDefined()
expect(cancelled?.status).toBe(DeletionRequestStatus.Cancelled)
})

it('should return null when no pending request exists', async () => {
const status = await DeletionUseCases.getDeletionStatus(testUser)
expect(status).toBeNull()
})

it('should return null when cancelling with no pending request', async () => {
const result = await DeletionUseCases.cancelDeletion(testUser)
expect(result).toBeNull()
})

describe('admin operations', () => {
it('should throw for non-admin user listing deletion requests', async () => {
await expect(
DeletionUseCases.getAllDeletionRequests(testUser),
).rejects.toThrow('User does not have admin privileges')
})

it('should throw for non-admin user updating admin notes', async () => {
await expect(
DeletionUseCases.updateAdminNotes(testUser, 'some-id', 'notes'),
).rejects.toThrow('User does not have admin privileges')
})
})
})
57 changes: 57 additions & 0 deletions apps/auth/migrations/20260331000000-deletion-requests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use strict'

var dbm
var type
var seed
var fs = require('fs')
var path = require('path')
var Promise

/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate
type = dbm.dataType
seed = seedLink
Promise = options.Promise
}

exports.up = function (db) {
var filePath = path.join(
__dirname,
'sqls',
'20260331000000-deletion-requests-up.sql',
)
return new Promise(function (resolve, reject) {
fs.readFile(filePath, { encoding: 'utf-8' }, function (err, data) {
if (err) return reject(err)

resolve(data)
})
}).then(function (data) {
return db.runSql(data)
})
}

exports.down = function (db) {
var filePath = path.join(
__dirname,
'sqls',
'20260331000000-deletion-requests-down.sql',
)
return new Promise(function (resolve, reject) {
fs.readFile(filePath, { encoding: 'utf-8' }, function (err, data) {
if (err) return reject(err)

resolve(data)
})
}).then(function (data) {
return db.runSql(data)
})
}

exports._meta = {
version: 1,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS users.deletion_requests;
23 changes: 23 additions & 0 deletions apps/auth/migrations/sqls/20260331000000-deletion-requests-up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
CREATE TABLE users.deletion_requests (
id text NOT NULL DEFAULT gen_random_uuid()::text,
user_public_id text NOT NULL,
oauth_provider text NOT NULL,
oauth_user_id text NOT NULL,
status text NOT NULL DEFAULT 'pending',
requested_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
scheduled_anonymisation_at timestamptz NOT NULL,
completed_at timestamptz NULL,
cancelled_at timestamptz NULL,
reason text NULL,
admin_notes text NULL,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT deletion_requests_pkey PRIMARY KEY (id),
CONSTRAINT deletion_requests_status_check CHECK (status IN ('pending', 'cancelled', 'processing', 'completed', 'failed')),
CONSTRAINT deletion_requests_user_fk FOREIGN KEY (oauth_provider, oauth_user_id)
REFERENCES users.users(oauth_provider, oauth_user_id)
);

CREATE INDEX idx_deletion_requests_status ON users.deletion_requests (status);
CREATE INDEX idx_deletion_requests_scheduled ON users.deletion_requests (scheduled_anonymisation_at) WHERE status = 'pending';
CREATE INDEX idx_deletion_requests_user ON users.deletion_requests (user_public_id);
Loading
Loading