| name | test-resource-service |
| description | Test resource services that handle CRUD operations. Use when testing services that extend BaseService with authorization and event emission. Triggers on "test note service", "test resource service", "test crud service". |
Test Resource Service
Tests resource services that extend BaseService and perform CRUD operations with authorization and event emission.
Quick Reference
Location: tests/services/{entity-name}.service.test.ts
Dependencies: MockDB repository, vitest, user fixtures
Test Structure
import { describe, it, expect, beforeEach, vi } from "vitest";
import { {Entity}Service } from "@/services/{entity-name}.service";
import { MockDb{Entity}Repository } from "@/repositories/mockdb/{entity-name}.mockdb.repository";
import type { Create{Entity}Type, {Entity}Type } from "@/schemas/{entity-name}.schema";
import type { AuthenticatedUserContextType } from "@/schemas/user.schemas";
import { UnauthorizedError } from "@/errors";
import { appEvents } from "@/events/event-emitter";
// User fixtures
const adminUser: AuthenticatedUserContextType = {
userId: "admin-1",
globalRole: "admin",
};
const regularUser: AuthenticatedUserContextType = {
userId: "user-1",
globalRole: "user",
};
const otherUser: AuthenticatedUserContextType = {
userId: "user-2",
globalRole: "user",
};
describe("{Entity}Service", () => {
let repository: MockDb{Entity}Repository;
let service: {Entity}Service;
beforeEach(() => {
repository = new MockDb{Entity}Repository();
service = new {Entity}Service(repository);
repository.clear();
appEvents.removeAllListeners();
});
// Test groups...
});
Test Categories
1. Create Tests
describe("create", () => {
it("allows admin to create", async () => {
const data: Create{Entity}Type = { /* valid data */ };
const result = await service.create(data, adminUser);
expect(result.createdBy).toBe(adminUser.userId);
});
it("allows regular user to create", async () => {
const data: Create{Entity}Type = { /* valid data */ };
const result = await service.create(data, regularUser);
expect(result.createdBy).toBe(regularUser.userId);
});
});
2. GetAll Tests (Authorization Filtering)
describe("getAll", () => {
beforeEach(async () => {
await service.create(
{
/* data */
},
regularUser,
);
await service.create(
{
/* data */
},
adminUser,
);
await service.create(
{
/* data */
},
otherUser,
);
});
it("returns all items for admin", async () => {
const result = await service.getAll({}, adminUser);
expect(result.data.length).toBe(3);
});
it("returns only user's items for regular user", async () => {
const result = await service.getAll({}, regularUser);
expect(result.data.length).toBe(1);
expect(
result.data.every((item) => item.createdBy === regularUser.userId),
).toBe(true);
});
it("supports search and pagination", async () => {
const result = await service.getAll(
{ search: "term", page: 1, limit: 10 },
adminUser,
);
// Assert filtered results
});
});
3. GetById Tests (Authorization)
describe("getById", () => {
let item: {Entity}Type;
beforeEach(async () => {
item = await service.create({ /* data */ }, regularUser);
});
it("allows owner to get their item", async () => {
const found = await service.getById(item.id, regularUser);
expect(found).not.toBeNull();
expect(found!.id).toBe(item.id);
});
it("allows admin to get any item", async () => {
const found = await service.getById(item.id, adminUser);
expect(found).not.toBeNull();
});
it("denies other users", async () => {
await expect(service.getById(item.id, otherUser)).rejects.toThrow(UnauthorizedError);
});
it("returns null for non-existent item", async () => {
await expect(service.getById("not-exist", adminUser)).resolves.toBeNull();
});
});
4. Update Tests (Authorization)
describe("update", () => {
let item: {Entity}Type;
beforeEach(async () => {
item = await service.create({ /* data */ }, regularUser);
});
it("allows owner to update", async () => {
const updated = await service.update(item.id, { /* new data */ }, regularUser);
expect(updated).not.toBeNull();
});
it("allows admin to update", async () => {
const updated = await service.update(item.id, { /* new data */ }, adminUser);
expect(updated).not.toBeNull();
});
it("denies other users", async () => {
await expect(service.update(item.id, { /* data */ }, otherUser)).rejects.toThrow(UnauthorizedError);
});
it("returns null for non-existent item", async () => {
await expect(service.update("not-exist", { /* data */ }, adminUser)).resolves.toBeNull();
});
});
5. Delete Tests (Authorization)
describe("delete", () => {
let item: {Entity}Type;
beforeEach(async () => {
item = await service.create({ /* data */ }, regularUser);
});
it("allows owner to delete", async () => {
const result = await service.delete(item.id, regularUser);
expect(result).toBe(true);
});
it("allows admin to delete", async () => {
const result = await service.delete(item.id, adminUser);
expect(result).toBe(true);
});
it("denies other users", async () => {
await expect(service.delete(item.id, otherUser)).rejects.toThrow(UnauthorizedError);
});
it("returns false for non-existent item", async () => {
await expect(service.delete("not-exist", adminUser)).resolves.toBe(false);
});
});
6. Event Emission Tests
describe("Event Emission", () => {
it("emits created event after successful creation", async () => {
const eventSpy = vi.fn();
appEvents.on("{entities}:created", eventSpy);
const item = await service.create(
{
/* data */
},
regularUser,
);
expect(eventSpy).toHaveBeenCalledWith({
id: expect.any(String),
action: "created",
data: item,
user: expect.objectContaining({
userId: regularUser.userId,
globalRole: regularUser.globalRole,
}),
timestamp: expect.any(Date),
resourceType: "{entities}",
});
});
it("emits updated event after successful update", async () => {
const item = await service.create(
{
/* data */
},
regularUser,
);
const eventSpy = vi.fn();
appEvents.on("{entities}:updated", eventSpy);
await service.update(
item.id,
{
/* new data */
},
regularUser,
);
expect(eventSpy).toHaveBeenCalledWith(
expect.objectContaining({
action: "updated",
resourceType: "{entities}",
}),
);
});
it("emits deleted event after successful deletion", async () => {
const item = await service.create(
{
/* data */
},
regularUser,
);
const eventSpy = vi.fn();
appEvents.on("{entities}:deleted", eventSpy);
await service.delete(item.id, regularUser);
expect(eventSpy).toHaveBeenCalledWith(
expect.objectContaining({
action: "deleted",
resourceType: "{entities}",
}),
);
});
it("does not emit events for failed operations", async () => {
const eventSpy = vi.fn();
appEvents.on("{entities}:updated", eventSpy);
appEvents.on("{entities}:deleted", eventSpy);
await service.update(
"non-existent",
{
/* data */
},
adminUser,
);
await service.delete("non-existent", adminUser);
expect(eventSpy).not.toHaveBeenCalled();
});
it("does not emit events for unauthorized operations", async () => {
const item = await service.create(
{
/* data */
},
regularUser,
);
const eventSpy = vi.fn();
appEvents.on("{entities}:updated", eventSpy);
try {
await service.update(
item.id,
{
/* data */
},
otherUser,
);
} catch {
// Expected
}
expect(eventSpy).not.toHaveBeenCalled();
});
});
Key Patterns
Use Real MockDB Repository
Inject the actual MockDB repository for integration-like tests:
beforeEach(() => {
repository = new MockDbNoteRepository();
service = new NoteService(repository);
repository.clear(); // Reset state
});
Clear Event Listeners
Prevent test pollution by removing listeners:
beforeEach(() => {
appEvents.removeAllListeners();
});
Test Authorization Matrix
| User Type | Own Resource | Other's Resource | Non-existent |
|---|---|---|---|
| Admin | Allow | Allow | null/false |
| Owner | Allow | Deny (throw) | null/false |
| Other | Deny (throw) | Deny (throw) | null/false |
Complete Example
See REFERENCE.md for a complete note.service.test.ts implementation.
What NOT to Do
- Do NOT mock the repository methods (use real MockDB)
- Do NOT forget to clear repository between tests
- Do NOT forget to remove event listeners between tests
- Do NOT skip testing authorization for all user types
- Do NOT skip testing event emission for all operations
See Also
test-service- Guide for choosing test typetest-utility-service- Testing utility servicescreate-resource-service- Creating the service to test