DB, Collections, Search
This commit is contained in:
592
src/actions/collections.integration.test.ts
Normal file
592
src/actions/collections.integration.test.ts
Normal file
@@ -0,0 +1,592 @@
|
||||
import { describe, it, expect, beforeEach, beforeAll } from "vitest"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
/**
|
||||
* Integration tests for collections functionality
|
||||
* These tests focus on database operations and data integrity
|
||||
*/
|
||||
|
||||
describe("Collections Database Operations", () => {
|
||||
let testUser: any
|
||||
let englishLanguage: any
|
||||
|
||||
beforeEach(async () => {
|
||||
// Note: Global beforeEach in vitest.integration.setup.ts clears all data
|
||||
// So we need to recreate user and language in each test
|
||||
|
||||
// Create English language
|
||||
englishLanguage = await prisma.language.create({
|
||||
data: {
|
||||
code: "en",
|
||||
name: "English",
|
||||
nativeName: "English",
|
||||
isActive: true,
|
||||
},
|
||||
})
|
||||
|
||||
// Create test user
|
||||
testUser = await prisma.user.create({
|
||||
data: {
|
||||
email: "testcollections@example.com",
|
||||
name: "Test User",
|
||||
password: "dummy",
|
||||
role: "USER",
|
||||
isActive: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe("Collection CRUD Operations", () => {
|
||||
it("should create a collection", async () => {
|
||||
const collection = await prisma.collection.create({
|
||||
data: {
|
||||
name: "Test Collection",
|
||||
description: "Test description",
|
||||
isPublic: false,
|
||||
isGlobal: false,
|
||||
createdBy: testUser.id,
|
||||
},
|
||||
})
|
||||
|
||||
expect(collection.id).toBeDefined()
|
||||
expect(collection.name).toBe("Test Collection")
|
||||
expect(collection.createdBy).toBe(testUser.id)
|
||||
})
|
||||
|
||||
it("should update a collection", async () => {
|
||||
const collection = await prisma.collection.create({
|
||||
data: {
|
||||
name: "Test Collection",
|
||||
isPublic: false,
|
||||
isGlobal: false,
|
||||
createdBy: testUser.id,
|
||||
},
|
||||
})
|
||||
|
||||
const updated = await prisma.collection.update({
|
||||
where: { id: collection.id },
|
||||
data: {
|
||||
name: "Updated Name",
|
||||
description: "New description",
|
||||
},
|
||||
})
|
||||
|
||||
expect(updated.name).toBe("Updated Name")
|
||||
expect(updated.description).toBe("New description")
|
||||
})
|
||||
|
||||
it("should delete a collection", async () => {
|
||||
const collection = await prisma.collection.create({
|
||||
data: {
|
||||
name: "Test Collection",
|
||||
isPublic: false,
|
||||
isGlobal: false,
|
||||
createdBy: testUser.id,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.collection.delete({
|
||||
where: { id: collection.id },
|
||||
})
|
||||
|
||||
const deleted = await prisma.collection.findUnique({
|
||||
where: { id: collection.id },
|
||||
})
|
||||
|
||||
expect(deleted).toBeNull()
|
||||
})
|
||||
|
||||
it("should get user collections", async () => {
|
||||
await prisma.collection.createMany({
|
||||
data: [
|
||||
{
|
||||
name: "Collection 1",
|
||||
isPublic: false,
|
||||
isGlobal: false,
|
||||
createdBy: testUser.id,
|
||||
},
|
||||
{
|
||||
name: "Collection 2",
|
||||
isPublic: true,
|
||||
isGlobal: false,
|
||||
createdBy: testUser.id,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const collections = await prisma.collection.findMany({
|
||||
where: { createdBy: testUser.id },
|
||||
})
|
||||
|
||||
expect(collections.length).toBe(2)
|
||||
})
|
||||
|
||||
it("should get global collections", async () => {
|
||||
await prisma.collection.create({
|
||||
data: {
|
||||
name: "HSK 1",
|
||||
isPublic: true,
|
||||
isGlobal: true,
|
||||
createdBy: null,
|
||||
},
|
||||
})
|
||||
|
||||
const globalCollections = await prisma.collection.findMany({
|
||||
where: { isGlobal: true },
|
||||
})
|
||||
|
||||
expect(globalCollections.length).toBe(1)
|
||||
expect(globalCollections[0].name).toBe("HSK 1")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Collection Items and OrderIndex", () => {
|
||||
let collection: any
|
||||
let hanzi1: any
|
||||
let hanzi2: any
|
||||
let hanzi3: any
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create collection
|
||||
collection = await prisma.collection.create({
|
||||
data: {
|
||||
name: "Test Collection",
|
||||
isPublic: false,
|
||||
isGlobal: false,
|
||||
createdBy: testUser.id,
|
||||
},
|
||||
})
|
||||
|
||||
// Create hanzi
|
||||
hanzi1 = await prisma.hanzi.create({
|
||||
data: { simplified: "好", radical: "女", frequency: 100 },
|
||||
})
|
||||
hanzi2 = await prisma.hanzi.create({
|
||||
data: { simplified: "爱", radical: "爫", frequency: 200 },
|
||||
})
|
||||
hanzi3 = await prisma.hanzi.create({
|
||||
data: { simplified: "你", radical: "人", frequency: 50 },
|
||||
})
|
||||
})
|
||||
|
||||
it("should add hanzi to collection with correct orderIndex", async () => {
|
||||
await prisma.collectionItem.createMany({
|
||||
data: [
|
||||
{ collectionId: collection.id, hanziId: hanzi1.id, orderIndex: 0 },
|
||||
{ collectionId: collection.id, hanziId: hanzi2.id, orderIndex: 1 },
|
||||
{ collectionId: collection.id, hanziId: hanzi3.id, orderIndex: 2 },
|
||||
],
|
||||
})
|
||||
|
||||
const items = await prisma.collectionItem.findMany({
|
||||
where: { collectionId: collection.id },
|
||||
orderBy: { orderIndex: "asc" },
|
||||
include: { hanzi: true },
|
||||
})
|
||||
|
||||
expect(items.length).toBe(3)
|
||||
expect(items[0].hanzi.simplified).toBe("好")
|
||||
expect(items[0].orderIndex).toBe(0)
|
||||
expect(items[1].hanzi.simplified).toBe("爱")
|
||||
expect(items[1].orderIndex).toBe(1)
|
||||
expect(items[2].hanzi.simplified).toBe("你")
|
||||
expect(items[2].orderIndex).toBe(2)
|
||||
})
|
||||
|
||||
it("should preserve orderIndex after removing middle item", async () => {
|
||||
// Add three items
|
||||
await prisma.collectionItem.createMany({
|
||||
data: [
|
||||
{ collectionId: collection.id, hanziId: hanzi1.id, orderIndex: 0 },
|
||||
{ collectionId: collection.id, hanziId: hanzi2.id, orderIndex: 1 },
|
||||
{ collectionId: collection.id, hanziId: hanzi3.id, orderIndex: 2 },
|
||||
],
|
||||
})
|
||||
|
||||
// Remove middle item
|
||||
await prisma.collectionItem.deleteMany({
|
||||
where: {
|
||||
collectionId: collection.id,
|
||||
hanziId: hanzi2.id,
|
||||
},
|
||||
})
|
||||
|
||||
const items = await prisma.collectionItem.findMany({
|
||||
where: { collectionId: collection.id },
|
||||
orderBy: { orderIndex: "asc" },
|
||||
include: { hanzi: true },
|
||||
})
|
||||
|
||||
expect(items.length).toBe(2)
|
||||
expect(items[0].hanzi.simplified).toBe("好")
|
||||
expect(items[0].orderIndex).toBe(0)
|
||||
expect(items[1].hanzi.simplified).toBe("你")
|
||||
expect(items[1].orderIndex).toBe(2) // Should keep original orderIndex
|
||||
})
|
||||
|
||||
it("should prevent duplicate hanzi in collection (unique constraint)", async () => {
|
||||
await prisma.collectionItem.create({
|
||||
data: {
|
||||
collectionId: collection.id,
|
||||
hanziId: hanzi1.id,
|
||||
orderIndex: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// Try to add same hanzi again
|
||||
await expect(
|
||||
prisma.collectionItem.create({
|
||||
data: {
|
||||
collectionId: collection.id,
|
||||
hanziId: hanzi1.id,
|
||||
orderIndex: 1,
|
||||
},
|
||||
})
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("should allow same hanzi in different collections", async () => {
|
||||
const collection2 = await prisma.collection.create({
|
||||
data: {
|
||||
name: "Second Collection",
|
||||
isPublic: false,
|
||||
isGlobal: false,
|
||||
createdBy: testUser.id,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.collectionItem.createMany({
|
||||
data: [
|
||||
{ collectionId: collection.id, hanziId: hanzi1.id, orderIndex: 0 },
|
||||
{ collectionId: collection2.id, hanziId: hanzi1.id, orderIndex: 0 },
|
||||
],
|
||||
})
|
||||
|
||||
const items1 = await prisma.collectionItem.count({
|
||||
where: { collectionId: collection.id },
|
||||
})
|
||||
const items2 = await prisma.collectionItem.count({
|
||||
where: { collectionId: collection2.id },
|
||||
})
|
||||
|
||||
expect(items1).toBe(1)
|
||||
expect(items2).toBe(1)
|
||||
})
|
||||
|
||||
it("should delete collection items when collection is deleted (cascade)", async () => {
|
||||
await prisma.collectionItem.createMany({
|
||||
data: [
|
||||
{ collectionId: collection.id, hanziId: hanzi1.id, orderIndex: 0 },
|
||||
{ collectionId: collection.id, hanziId: hanzi2.id, orderIndex: 1 },
|
||||
],
|
||||
})
|
||||
|
||||
await prisma.collection.delete({
|
||||
where: { id: collection.id },
|
||||
})
|
||||
|
||||
const items = await prisma.collectionItem.findMany({
|
||||
where: { collectionId: collection.id },
|
||||
})
|
||||
|
||||
expect(items.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Hanzi Search Operations", () => {
|
||||
beforeEach(async () => {
|
||||
// Create hanzi with forms, transcriptions, and meanings
|
||||
const hanzi1 = await prisma.hanzi.create({
|
||||
data: { simplified: "好", radical: "女", frequency: 100 },
|
||||
})
|
||||
const form1 = await prisma.hanziForm.create({
|
||||
data: {
|
||||
hanziId: hanzi1.id,
|
||||
traditional: "好",
|
||||
isDefault: true,
|
||||
},
|
||||
})
|
||||
await prisma.hanziTranscription.create({
|
||||
data: {
|
||||
formId: form1.id,
|
||||
type: "pinyin",
|
||||
value: "hǎo",
|
||||
},
|
||||
})
|
||||
await prisma.hanziMeaning.create({
|
||||
data: {
|
||||
formId: form1.id,
|
||||
languageId: englishLanguage.id,
|
||||
meaning: "good",
|
||||
orderIndex: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const hanzi2 = await prisma.hanzi.create({
|
||||
data: { simplified: "你", radical: "人", frequency: 50 },
|
||||
})
|
||||
const form2 = await prisma.hanziForm.create({
|
||||
data: {
|
||||
hanziId: hanzi2.id,
|
||||
traditional: "你",
|
||||
isDefault: true,
|
||||
},
|
||||
})
|
||||
await prisma.hanziTranscription.create({
|
||||
data: {
|
||||
formId: form2.id,
|
||||
type: "pinyin",
|
||||
value: "nǐ",
|
||||
},
|
||||
})
|
||||
await prisma.hanziMeaning.create({
|
||||
data: {
|
||||
formId: form2.id,
|
||||
languageId: englishLanguage.id,
|
||||
meaning: "you",
|
||||
orderIndex: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const hanzi3 = await prisma.hanzi.create({
|
||||
data: { simplified: "中国", radical: null, frequency: 300 },
|
||||
})
|
||||
const form3 = await prisma.hanziForm.create({
|
||||
data: {
|
||||
hanziId: hanzi3.id,
|
||||
traditional: "中國",
|
||||
isDefault: true,
|
||||
},
|
||||
})
|
||||
await prisma.hanziTranscription.create({
|
||||
data: {
|
||||
formId: form3.id,
|
||||
type: "pinyin",
|
||||
value: "zhōng guó",
|
||||
},
|
||||
})
|
||||
await prisma.hanziMeaning.create({
|
||||
data: {
|
||||
formId: form3.id,
|
||||
languageId: englishLanguage.id,
|
||||
meaning: "China",
|
||||
orderIndex: 0,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should search hanzi by simplified character", async () => {
|
||||
const results = await prisma.hanzi.findMany({
|
||||
where: {
|
||||
simplified: { contains: "好" },
|
||||
},
|
||||
include: {
|
||||
forms: {
|
||||
where: { isDefault: true },
|
||||
include: {
|
||||
transcriptions: { where: { type: "pinyin" }, take: 1 },
|
||||
meanings: { orderBy: { orderIndex: "asc" }, take: 1 },
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(results.length).toBeGreaterThan(0)
|
||||
expect(results.some((h) => h.simplified === "好")).toBe(true)
|
||||
})
|
||||
|
||||
it("should search hanzi by pinyin transcription", async () => {
|
||||
const results = await prisma.hanzi.findMany({
|
||||
where: {
|
||||
forms: {
|
||||
some: {
|
||||
transcriptions: {
|
||||
some: {
|
||||
value: { contains: "hǎo" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
forms: {
|
||||
where: { isDefault: true },
|
||||
include: {
|
||||
transcriptions: { where: { type: "pinyin" }, take: 1 },
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(results.length).toBeGreaterThan(0)
|
||||
expect(results.some((h) => h.simplified === "好")).toBe(true)
|
||||
})
|
||||
|
||||
it("should search hanzi by meaning", async () => {
|
||||
const results = await prisma.hanzi.findMany({
|
||||
where: {
|
||||
forms: {
|
||||
some: {
|
||||
meanings: {
|
||||
some: {
|
||||
meaning: { contains: "good" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
forms: {
|
||||
where: { isDefault: true },
|
||||
include: {
|
||||
meanings: { orderBy: { orderIndex: "asc" }, take: 1 },
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(results.length).toBeGreaterThan(0)
|
||||
expect(results.some((h) => h.simplified === "好")).toBe(true)
|
||||
})
|
||||
|
||||
it("should support multi-character hanzi in database", async () => {
|
||||
const multiChar = await prisma.hanzi.findUnique({
|
||||
where: { simplified: "中国" },
|
||||
include: {
|
||||
forms: {
|
||||
where: { isDefault: true },
|
||||
include: {
|
||||
transcriptions: true,
|
||||
meanings: true,
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(multiChar).toBeDefined()
|
||||
expect(multiChar?.simplified).toBe("中国")
|
||||
expect(multiChar?.forms[0].traditional).toBe("中國")
|
||||
expect(multiChar?.forms[0].transcriptions[0].value).toBe("zhōng guó")
|
||||
expect(multiChar?.forms[0].meanings[0].meaning).toBe("China")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Hanzi List Parsing Logic", () => {
|
||||
beforeEach(async () => {
|
||||
// Create test hanzi
|
||||
await prisma.hanzi.createMany({
|
||||
data: [
|
||||
{ simplified: "好", radical: "女", frequency: 100 },
|
||||
{ simplified: "爱", radical: "爫", frequency: 200 },
|
||||
{ simplified: "你", radical: "人", frequency: 50 },
|
||||
{ simplified: "中国", radical: null, frequency: 300 },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("should parse newline-separated list", () => {
|
||||
const input = "好\n爱\n你"
|
||||
const parsed = input
|
||||
.split(/[\n,\s]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0)
|
||||
|
||||
expect(parsed).toEqual(["好", "爱", "你"])
|
||||
})
|
||||
|
||||
it("should parse comma-separated list", () => {
|
||||
const input = "好, 爱, 你"
|
||||
const parsed = input
|
||||
.split(/[\n,\s]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0)
|
||||
|
||||
expect(parsed).toEqual(["好", "爱", "你"])
|
||||
})
|
||||
|
||||
it("should parse space-separated list", () => {
|
||||
const input = "好 爱 你"
|
||||
const parsed = input
|
||||
.split(/[\n,\s]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0)
|
||||
|
||||
expect(parsed).toEqual(["好", "爱", "你"])
|
||||
})
|
||||
|
||||
it("should handle multi-character words", () => {
|
||||
const input = "好 中国 你"
|
||||
const parsed = input
|
||||
.split(/[\n,\s]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0)
|
||||
|
||||
expect(parsed).toEqual(["好", "中国", "你"])
|
||||
})
|
||||
|
||||
it("should detect duplicates", () => {
|
||||
const input = "好 爱 好 你"
|
||||
const chars = input
|
||||
.split(/[\n,\s]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0)
|
||||
|
||||
const seen = new Set<string>()
|
||||
const duplicates: string[] = []
|
||||
const unique = chars.filter((char) => {
|
||||
if (seen.has(char)) {
|
||||
if (!duplicates.includes(char)) {
|
||||
duplicates.push(char)
|
||||
}
|
||||
return false
|
||||
}
|
||||
seen.add(char)
|
||||
return true
|
||||
})
|
||||
|
||||
expect(unique).toEqual(["好", "爱", "你"])
|
||||
expect(duplicates).toEqual(["好"])
|
||||
})
|
||||
|
||||
it("should validate all hanzi exist (strict mode)", async () => {
|
||||
const input = ["好", "不存在", "你"]
|
||||
|
||||
const foundHanzi = await prisma.hanzi.findMany({
|
||||
where: {
|
||||
simplified: { in: input },
|
||||
},
|
||||
})
|
||||
|
||||
const foundSet = new Set(foundHanzi.map((h) => h.simplified))
|
||||
const notFound = input.filter((char) => !foundSet.has(char))
|
||||
|
||||
expect(notFound).toEqual(["不存在"])
|
||||
})
|
||||
|
||||
it("should preserve order from input", async () => {
|
||||
const input = ["你", "爱", "好"]
|
||||
|
||||
const foundHanzi = await prisma.hanzi.findMany({
|
||||
where: {
|
||||
simplified: { in: input },
|
||||
},
|
||||
})
|
||||
|
||||
// Create a map for quick lookup
|
||||
const hanziMap = new Map(foundHanzi.map((h) => [h.simplified, h]))
|
||||
|
||||
// Preserve input order
|
||||
const ordered = input
|
||||
.map((char) => hanziMap.get(char))
|
||||
.filter((h): h is NonNullable<typeof h> => h !== undefined)
|
||||
|
||||
expect(ordered[0].simplified).toBe("你")
|
||||
expect(ordered[1].simplified).toBe("爱")
|
||||
expect(ordered[2].simplified).toBe("好")
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user