diff --git a/domain/entities/HabitHistory.ts b/domain/entities/HabitHistory.ts index 5f34f3e0bec614bc01f5afd00f7775ed199a648a..66ecddbd758a644b9aa4139be6ea54937b0e8d54 100644 --- a/domain/entities/HabitHistory.ts +++ b/domain/entities/HabitHistory.ts @@ -1,8 +1,8 @@ import { getISODate, getWeekNumber } from "@/utils/dates" -import type { Habit } from "./Habit" -import type { HabitProgress } from "./HabitProgress" import type { GoalProgress } from "./Goal" import { GoalBooleanProgress, GoalNumericProgress } from "./Goal" +import type { Habit } from "./Habit" +import type { HabitProgress } from "./HabitProgress" export interface HabitHistoryJSON { habit: Habit diff --git a/domain/entities/__tests__/HabitsTracker.test.ts b/domain/entities/__tests__/HabitsTracker.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..3b5f3cb685ff2db67978ac9c2287ddee2675cb39 --- /dev/null +++ b/domain/entities/__tests__/HabitsTracker.test.ts @@ -0,0 +1,106 @@ +import { HABIT_MOCK } from "@/tests/mocks/domain/Habit" +import { GOAL_FREQUENCIES } from "../Goal" +import { HabitsTracker } from "../HabitsTracker" +import { HabitHistory } from "../HabitHistory" +import { HABIT_PROGRESS_MOCK } from "@/tests/mocks/domain/HabitProgress" + +describe("domain/entities/HabitsTracker", () => { + describe("HabitsTracker.default", () => { + for (const frequency of GOAL_FREQUENCIES) { + it(`should return empty habitsHistory for ${frequency}`, () => { + const habitsTracker = HabitsTracker.default() + expect(habitsTracker.habitsHistory[frequency]).toEqual([]) + }) + } + }) + + describe("getAllHabitsHistory", () => { + it("should return all habits history", () => { + const habitsTracker = HabitsTracker.default() + const habit = HABIT_MOCK.examplesByNames.Walk + habitsTracker.addHabit(habit) + expect(habitsTracker.getAllHabitsHistory()).toEqual([ + new HabitHistory({ + habit, + progressHistory: [], + }), + ]) + }) + + it("should return empty array when no habits are added", () => { + const habitsTracker = HabitsTracker.default() + expect(habitsTracker.getAllHabitsHistory()).toEqual([]) + }) + }) + + describe("getHabitHistoryById", () => { + it("should return habit history by id", () => { + const habitsTracker = HabitsTracker.default() + const habit = HABIT_MOCK.examplesByNames.Walk + habitsTracker.addHabit(habit) + expect(habitsTracker.getHabitHistoryById(habit.id)).toEqual( + new HabitHistory({ + habit, + progressHistory: [], + }), + ) + }) + + it("should return undefined when habit is not found", () => { + const habitsTracker = HabitsTracker.default() + expect(habitsTracker.getHabitHistoryById("invalid-id")).toBeUndefined() + }) + }) + + describe("addHabit", () => { + it("should add habit to habitsHistory", () => { + const habitsTracker = HabitsTracker.default() + const habit = HABIT_MOCK.examplesByNames.Walk + habitsTracker.addHabit(habit) + expect(habitsTracker.habitsHistory[habit.goal.frequency]).toEqual([ + new HabitHistory({ + habit, + progressHistory: [], + }), + ]) + }) + }) + + describe("editHabit", () => { + it("should edit habit in habitsHistory", () => { + const habitsTracker = HabitsTracker.default() + const habit = HABIT_MOCK.examplesByNames.Walk + habitsTracker.addHabit(habit) + habit.name = "Run" + habitsTracker.editHabit(habit) + expect(habitsTracker.habitsHistory[habit.goal.frequency]).toEqual([ + new HabitHistory({ + habit, + progressHistory: [], + }), + ]) + }) + + it("should not edit habit in habitsHistory when habit is not found", () => { + const habitsTracker = HabitsTracker.default() + const habit = HABIT_MOCK.examplesByNames.Walk + habitsTracker.editHabit(habit) + expect(habitsTracker.habitsHistory[habit.goal.frequency]).toEqual([]) + }) + }) + + describe("updateHabitProgress", () => { + it("should update habit progress in habitsHistory (add new habit progress if not yet added)", () => { + const habitsTracker = HabitsTracker.default() + const habit = HABIT_MOCK.examplesByNames["Clean the house"] + habitsTracker.addHabit(habit) + habitsTracker.updateHabitProgress(HABIT_PROGRESS_MOCK.exampleByIds[1]) + expect(habitsTracker.habitsHistory[habit.goal.frequency]).toEqual([ + new HabitHistory({ + habit, + progressHistory: [HABIT_PROGRESS_MOCK.exampleByIds[1]], + }), + ]) + }) + }) +}) diff --git a/infrastructure/instances.ts b/infrastructure/instances.ts index abf3fb76864fb865dc30076e865de26c2e7d4a6e..1e01102e7a40e4a80122da5735a9dfd308d7fd3e 100644 --- a/infrastructure/instances.ts +++ b/infrastructure/instances.ts @@ -1,19 +1,19 @@ import { AuthenticationUseCase } from "@/domain/use-cases/Authentication" +import { HabitCreateUseCase } from "@/domain/use-cases/HabitCreate" +import { HabitEditUseCase } from "@/domain/use-cases/HabitEdit" +import { HabitGoalProgressUpdateUseCase } from "@/domain/use-cases/HabitGoalProgressUpdate" +import { HabitStopUseCase } from "@/domain/use-cases/HabitStop" +import { AuthenticationPresenter } from "@/presentation/presenters/Authentication" import { RetrieveHabitsTrackerUseCase } from "../domain/use-cases/RetrieveHabitsTracker" import { HabitsTrackerPresenter } from "../presentation/presenters/HabitsTracker" import { AuthenticationSupabaseRepository } from "./supabase/repositories/Authentication" import { GetHabitProgressHistorySupabaseRepository } from "./supabase/repositories/GetHabitProgressHistory" import { GetHabitsByUserIdSupabaseRepository } from "./supabase/repositories/GetHabitsByUserId" -import { supabaseClient } from "./supabase/supabase" -import { AuthenticationPresenter } from "@/presentation/presenters/Authentication" import { HabitCreateSupabaseRepository } from "./supabase/repositories/HabitCreate" -import { HabitCreateUseCase } from "@/domain/use-cases/HabitCreate" import { HabitEditSupabaseRepository } from "./supabase/repositories/HabitEdit" -import { HabitEditUseCase } from "@/domain/use-cases/HabitEdit" import { HabitProgressCreateSupabaseRepository } from "./supabase/repositories/HabitProgressCreate" import { HabitProgressUpdateSupabaseRepository } from "./supabase/repositories/HabitProgressUpdate" -import { HabitGoalProgressUpdateUseCase } from "@/domain/use-cases/HabitGoalProgressUpdate" -import { HabitStopUseCase } from "@/domain/use-cases/HabitStop" +import { supabaseClient } from "./supabase/supabase" /** * Repositories diff --git a/infrastructure/supabase/data-transfer-objects/HabitDTO.ts b/infrastructure/supabase/data-transfer-objects/HabitDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..b0b2af41e40f2c936c87b61676c24ed3e2d4ecce --- /dev/null +++ b/infrastructure/supabase/data-transfer-objects/HabitDTO.ts @@ -0,0 +1,79 @@ +import type { Goal } from "@/domain/entities/Goal" +import { GoalBoolean, GoalNumeric } from "@/domain/entities/Goal" +import type { HabitCreateData, HabitEditData } from "@/domain/entities/Habit" +import { Habit } from "@/domain/entities/Habit" +import type { + SupabaseHabit, + SupabaseHabitInsert, + SupabaseHabitUpdate, +} from "../supabase" + +export const habitSupabaseDTO = { + fromSupabaseToDomain: (supabaseHabit: SupabaseHabit): Habit => { + let goal: Goal + if ( + supabaseHabit.goal_target != null && + supabaseHabit.goal_target_unit != null + ) { + goal = new GoalNumeric({ + frequency: supabaseHabit.goal_frequency, + target: { + value: supabaseHabit.goal_target, + unit: supabaseHabit.goal_target_unit, + }, + }) + } else { + goal = new GoalBoolean({ + frequency: supabaseHabit.goal_frequency, + }) + } + const habit = new Habit({ + id: supabaseHabit.id.toString(), + name: supabaseHabit.name, + color: supabaseHabit.color, + icon: supabaseHabit.icon, + userId: supabaseHabit.user_id.toString(), + startDate: new Date(supabaseHabit.start_date), + endDate: + supabaseHabit.end_date != null + ? new Date(supabaseHabit.end_date) + : undefined, + goal, + }) + return habit + }, + fromDomainCreateDataToSupabaseInsert: ( + habitCreateData: HabitCreateData, + ): SupabaseHabitInsert => { + return { + name: habitCreateData.name, + color: habitCreateData.color, + icon: habitCreateData.icon, + goal_frequency: habitCreateData.goal.frequency, + ...(habitCreateData.goal.target.type === "numeric" + ? { + goal_target: habitCreateData.goal.target.value, + goal_target_unit: habitCreateData.goal.target.unit, + } + : {}), + } + }, + fromDomainEditDataToSupabaseUpdate: ( + habitEditData: HabitEditData, + ): SupabaseHabitUpdate => { + return { + name: habitEditData.name, + color: habitEditData.color, + icon: habitEditData.icon, + end_date: habitEditData?.endDate?.toISOString(), + } + }, +} + +export const habitsSupabaseDTO = { + fromSupabaseToDomain: (supabaseHabits: SupabaseHabit[]): Habit[] => { + return supabaseHabits.map((supabaseHabit) => { + return habitSupabaseDTO.fromSupabaseToDomain(supabaseHabit) + }) + }, +} diff --git a/infrastructure/supabase/data-transfer-objects/HabitProgressDTO.ts b/infrastructure/supabase/data-transfer-objects/HabitProgressDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..b4afdb3f37c42dca4b69dbd9ad390909f626ae48 --- /dev/null +++ b/infrastructure/supabase/data-transfer-objects/HabitProgressDTO.ts @@ -0,0 +1,78 @@ +import type { Goal, GoalProgress } from "@/domain/entities/Goal" +import { + GoalBooleanProgress, + GoalNumericProgress, +} from "@/domain/entities/Goal" +import { HabitProgress } from "@/domain/entities/HabitProgress" +import type { HabitProgressCreateOptions } from "@/domain/repositories/HabitProgressCreate" +import type { HabitProgressUpdateOptions } from "@/domain/repositories/HabitProgressUpdate" +import type { + SupabaseHabitProgress, + SupabaseHabitProgressInsert, + SupabaseHabitProgressUpdate, +} from "../supabase" + +export const habitProgressSupabaseDTO = { + fromSupabaseToDomain: ( + supabaseHabitProgress: SupabaseHabitProgress, + goal: Goal, + ): HabitProgress => { + let goalProgress: GoalProgress | null = null + if (goal.isNumeric()) { + goalProgress = new GoalNumericProgress({ + goal, + progress: supabaseHabitProgress.goal_progress, + }) + } else if (goal.isBoolean()) { + goalProgress = new GoalBooleanProgress({ + goal, + progress: supabaseHabitProgress.goal_progress === 1, + }) + } + const habitProgress = new HabitProgress({ + id: supabaseHabitProgress.id.toString(), + habitId: supabaseHabitProgress.habit_id.toString(), + goalProgress: goalProgress as GoalProgress, + date: new Date(supabaseHabitProgress.date), + }) + return habitProgress + }, + fromDomainDataToSupabaseInsert: ( + habitProgressData: HabitProgressCreateOptions["habitProgressData"], + ): SupabaseHabitProgressInsert => { + const { goalProgress, date, habitId } = habitProgressData + let goalProgressValue = goalProgress.isCompleted() ? 1 : 0 + if (goalProgress.isNumeric()) { + goalProgressValue = goalProgress.progress + } + return { + habit_id: Number.parseInt(habitId, 10), + date: date.toISOString(), + goal_progress: goalProgressValue, + } + }, + fromDomainDataToSupabaseUpdate: ( + habitProgressData: HabitProgressUpdateOptions["habitProgressData"], + ): SupabaseHabitProgressUpdate => { + const { goalProgress, date } = habitProgressData + let goalProgressValue = goalProgress.isCompleted() ? 1 : 0 + if (goalProgress.isNumeric()) { + goalProgressValue = goalProgress.progress + } + return { + date: date.toISOString(), + goal_progress: goalProgressValue, + } + }, +} + +export const habitProgressHistorySupabaseDTO = { + fromSupabaseToDomain: ( + supabaseHabitHistory: SupabaseHabitProgress[], + goal: Goal, + ): HabitProgress[] => { + return supabaseHabitHistory.map((item) => { + return habitProgressSupabaseDTO.fromSupabaseToDomain(item, goal) + }) + }, +} diff --git a/infrastructure/supabase/data-transfer-objects/__tests__/HabitDTO.test.ts b/infrastructure/supabase/data-transfer-objects/__tests__/HabitDTO.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..c797a85bac10dca8c95bb39247c7a84b3dec49ca --- /dev/null +++ b/infrastructure/supabase/data-transfer-objects/__tests__/HabitDTO.test.ts @@ -0,0 +1,100 @@ +import type { GoalCreateData } from "@/domain/entities/Goal" +import { HABIT_MOCK } from "@/tests/mocks/domain/Habit" +import { SUPABASE_HABIT_MOCK } from "@/tests/mocks/supabase/Habit" +import { habitSupabaseDTO, habitsSupabaseDTO } from "../HabitDTO" + +describe("infrastructure/supabase/data-transfer-objects/HabitDTO", () => { + describe("habitSupabaseDTO.fromSupabaseToDomain", () => { + for (const example of SUPABASE_HABIT_MOCK.examples) { + it(`should return correct Habit entity - ${example.name}`, () => { + expect(habitSupabaseDTO.fromSupabaseToDomain(example)).toEqual( + HABIT_MOCK.examplesByNames[ + example.name as keyof typeof HABIT_MOCK.examplesByNames + ], + ) + }) + } + }) + + describe("habitSupabaseDTO.fromDomainCreateDataToSupabaseInsert", () => { + for (const example of HABIT_MOCK.examples) { + it(`should return correct SupabaseHabitInsert entity - ${example.name}`, () => { + let goalData = {} as GoalCreateData + if (example.goal.isBoolean()) { + goalData = { + frequency: example.goal.frequency, + target: { type: "boolean" }, + } + } + if (example.goal.isNumeric()) { + goalData = { + frequency: example.goal.frequency, + target: { + type: "numeric", + value: example.goal.target.value, + unit: example.goal.target.unit, + }, + } + } + + const supabaseData = + SUPABASE_HABIT_MOCK.examplesByNames[ + example.name as keyof typeof SUPABASE_HABIT_MOCK.examplesByNames + ] + expect( + habitSupabaseDTO.fromDomainCreateDataToSupabaseInsert({ + userId: example.userId, + name: example.name, + color: example.color, + icon: example.icon, + goal: goalData, + }), + ).toEqual({ + name: supabaseData.name, + color: supabaseData.color, + icon: supabaseData.icon, + goal_frequency: supabaseData.goal_frequency, + ...(supabaseData.goal_target != null && + supabaseData.goal_target_unit != null + ? { + goal_target: supabaseData.goal_target, + goal_target_unit: supabaseData.goal_target_unit, + } + : {}), + }) + }) + } + }) + + describe("habitSupabaseDTO.fromDomainEditDataToSupabaseUpdate", () => { + for (const example of HABIT_MOCK.examples) { + it(`should return correct SupabaseHabitUpdate entity - ${example.name}`, () => { + const supabaseData = + SUPABASE_HABIT_MOCK.examplesByNames[ + example.name as keyof typeof SUPABASE_HABIT_MOCK.examplesByNames + ] + expect( + habitSupabaseDTO.fromDomainEditDataToSupabaseUpdate({ + name: example.name, + color: example.color, + icon: example.icon, + id: example.id, + userId: example.userId, + }), + ).toEqual({ + name: supabaseData.name, + color: supabaseData.color, + icon: supabaseData.icon, + }) + }) + } + }) + + describe("habitsSupabaseDTO.fromSupabaseToDomain", () => { + it("should return correct Habits entities", () => { + expect( + habitsSupabaseDTO.fromSupabaseToDomain(SUPABASE_HABIT_MOCK.examples), + ).toEqual(HABIT_MOCK.examples) + }) + }) +}) diff --git a/infrastructure/supabase/data-transfer-objects/__tests__/HabitProgressDTO.test.ts b/infrastructure/supabase/data-transfer-objects/__tests__/HabitProgressDTO.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab8b21f10c882bdd12afd50f8e25b3796ceb5961 --- /dev/null +++ b/infrastructure/supabase/data-transfer-objects/__tests__/HabitProgressDTO.test.ts @@ -0,0 +1,22 @@ +import type { Habit } from "@/domain/entities/Habit" +import { HABIT_MOCK } from "@/tests/mocks/domain/Habit" +import { HABIT_PROGRESS_MOCK } from "@/tests/mocks/domain/HabitProgress" +import { SUPABASE_HABIT_PROGRESS_MOCK } from "@/tests/mocks/supabase/HabitProgress" +import { habitProgressSupabaseDTO } from "../HabitProgressDTO" + +describe("infrastructure/supabase/data-transfer-objects/HabitProgressDTO", () => { + describe("habitProgressSupabaseDTO.fromSupabaseToDomain", () => { + for (const example of SUPABASE_HABIT_PROGRESS_MOCK.examples) { + it(`should return correct HabitProgress entity - ${example.id}`, () => { + const habit = HABIT_MOCK.examplesByIds[example.habit_id] as Habit + expect( + habitProgressSupabaseDTO.fromSupabaseToDomain(example, habit.goal), + ).toEqual( + HABIT_PROGRESS_MOCK.exampleByIds[ + example.id as keyof typeof HABIT_PROGRESS_MOCK.exampleByIds + ], + ) + }) + } + }) +}) diff --git a/infrastructure/supabase/repositories/Authentication.ts b/infrastructure/supabase/repositories/Authentication.ts index 03f16047e357bed18f69c32d37ee0ac11d466104..75f1206b54596901980544c8a932b366921c2997 100644 --- a/infrastructure/supabase/repositories/Authentication.ts +++ b/infrastructure/supabase/repositories/Authentication.ts @@ -1,8 +1,8 @@ import type { Session } from "@supabase/supabase-js" -import type { AuthenticationRepository } from "@/domain/repositories/Authentication" -import { SupabaseRepository } from "./_SupabaseRepository" import { User } from "@/domain/entities/User" +import type { AuthenticationRepository } from "@/domain/repositories/Authentication" +import { SupabaseRepository } from "@/infrastructure/supabase/repositories/_SupabaseRepository" export class AuthenticationSupabaseRepository extends SupabaseRepository diff --git a/infrastructure/supabase/repositories/GetHabitProgressHistory.ts b/infrastructure/supabase/repositories/GetHabitProgressHistory.ts index 9e5c1f08ccbe5512c04856bd853692d234fac3a8..dd47c0dd2e9a0390fa0d714035bb2508c4dd968a 100644 --- a/infrastructure/supabase/repositories/GetHabitProgressHistory.ts +++ b/infrastructure/supabase/repositories/GetHabitProgressHistory.ts @@ -1,11 +1,6 @@ import type { GetHabitProgressHistoryRepository } from "@/domain/repositories/GetHabitProgressHistory" -import { SupabaseRepository } from "./_SupabaseRepository" -import { HabitProgress } from "@/domain/entities/HabitProgress" -import type { GoalProgress } from "@/domain/entities/Goal" -import { - GoalBooleanProgress, - GoalNumericProgress, -} from "@/domain/entities/Goal" +import { SupabaseRepository } from "@/infrastructure/supabase/repositories/_SupabaseRepository" +import { habitProgressHistorySupabaseDTO } from "../data-transfer-objects/HabitProgressDTO" export class GetHabitProgressHistorySupabaseRepository extends SupabaseRepository @@ -15,37 +10,15 @@ export class GetHabitProgressHistorySupabaseRepository options, ) => { const { habit } = options - const { data, error } = await this.supabaseClient + const { data } = await this.supabaseClient .from("habits_progresses") .select("*") .eq("habit_id", habit.id) - if (error != null) { - throw new Error(error.message) - } - const habitProgressHistory = data.map((item) => { - let goalProgress: GoalProgress | null = null - if (habit.goal.isNumeric()) { - goalProgress = new GoalNumericProgress({ - goal: habit.goal, - progress: item.goal_progress, - }) - } else if (habit.goal.isBoolean()) { - goalProgress = new GoalBooleanProgress({ - goal: habit.goal, - progress: item.goal_progress === 1, - }) - } - if (goalProgress == null) { - throw new Error("Goal progress is null.") - } - const habitProgress = new HabitProgress({ - id: item.id.toString(), - habitId: item.habit_id.toString(), - goalProgress, - date: new Date(item.date), - }) - return habitProgress - }) - return habitProgressHistory + .throwOnError() + const habitProgressHistory = data as NonNullable<typeof data> + return habitProgressHistorySupabaseDTO.fromSupabaseToDomain( + habitProgressHistory, + habit.goal, + ) } } diff --git a/infrastructure/supabase/repositories/GetHabitsByUserId.ts b/infrastructure/supabase/repositories/GetHabitsByUserId.ts index 3d9c022b33289e18fb1f8fb1549c25ae6b5a9db6..6026fa663112ad044bd36dcf22c0de1faf7aa13d 100644 --- a/infrastructure/supabase/repositories/GetHabitsByUserId.ts +++ b/infrastructure/supabase/repositories/GetHabitsByUserId.ts @@ -1,8 +1,6 @@ import type { GetHabitsByUserIdRepository } from "@/domain/repositories/GetHabitsByUserId" -import { SupabaseRepository } from "./_SupabaseRepository" -import { Habit } from "@/domain/entities/Habit" -import type { Goal } from "@/domain/entities/Goal" -import { GoalBoolean, GoalNumeric } from "@/domain/entities/Goal" +import { SupabaseRepository } from "@/infrastructure/supabase/repositories/_SupabaseRepository" +import { habitsSupabaseDTO } from "../data-transfer-objects/HabitDTO" export class GetHabitsByUserIdSupabaseRepository extends SupabaseRepository @@ -10,39 +8,12 @@ export class GetHabitsByUserIdSupabaseRepository { public execute: GetHabitsByUserIdRepository["execute"] = async (options) => { const { userId } = options - const { data, error } = await this.supabaseClient + const { data } = await this.supabaseClient .from("habits") .select("*") .eq("user_id", userId) - if (error != null) { - throw new Error(error.message) - } - return data.map((item) => { - let goal: Goal - if (item.goal_target != null && item.goal_target_unit != null) { - goal = new GoalNumeric({ - frequency: item.goal_frequency, - target: { - value: item.goal_target, - unit: item.goal_target_unit, - }, - }) - } else { - goal = new GoalBoolean({ - frequency: item.goal_frequency, - }) - } - const habit = new Habit({ - id: item.id.toString(), - name: item.name, - color: item.color, - icon: item.icon, - userId: item.user_id.toString(), - startDate: new Date(item.start_date), - endDate: item.end_date != null ? new Date(item.end_date) : undefined, - goal, - }) - return habit - }) + .throwOnError() + const habits = data as NonNullable<typeof data> + return habitsSupabaseDTO.fromSupabaseToDomain(habits) } } diff --git a/infrastructure/supabase/repositories/HabitCreate.ts b/infrastructure/supabase/repositories/HabitCreate.ts index 04e5ab3b863ab36d3ee82eb88e730ca8803722b4..58f17a8c79256595b42a2373325874b98400136b 100644 --- a/infrastructure/supabase/repositories/HabitCreate.ts +++ b/infrastructure/supabase/repositories/HabitCreate.ts @@ -1,7 +1,6 @@ -import { Habit } from "@/domain/entities/Habit" import type { HabitCreateRepository } from "@/domain/repositories/HabitCreate" -import { SupabaseRepository } from "./_SupabaseRepository" -import { Goal } from "@/domain/entities/Goal" +import { SupabaseRepository } from "@/infrastructure/supabase/repositories/_SupabaseRepository" +import { habitSupabaseDTO } from "../data-transfer-objects/HabitDTO" export class HabitCreateSupabaseRepository extends SupabaseRepository @@ -9,34 +8,15 @@ export class HabitCreateSupabaseRepository { public execute: HabitCreateRepository["execute"] = async (options) => { const { habitCreateData } = options - const { data, error } = await this.supabaseClient + const { data } = await this.supabaseClient .from("habits") - .insert({ - name: habitCreateData.name, - color: habitCreateData.color, - icon: habitCreateData.icon, - goal_frequency: habitCreateData.goal.frequency, - ...(habitCreateData.goal.target.type === "numeric" - ? { - goal_target: habitCreateData.goal.target.value, - goal_target_unit: habitCreateData.goal.target.unit, - } - : {}), - }) + .insert( + habitSupabaseDTO.fromDomainCreateDataToSupabaseInsert(habitCreateData), + ) .select("*") - const insertedHabit = data?.[0] - if (error != null || insertedHabit == null) { - throw new Error(error?.message ?? "Failed to create habit.") - } - const habit = new Habit({ - id: insertedHabit.id.toString(), - userId: insertedHabit.user_id.toString(), - name: insertedHabit.name, - icon: insertedHabit.icon, - goal: Goal.create(habitCreateData.goal), - color: insertedHabit.color, - startDate: new Date(insertedHabit.start_date), - }) - return habit + .single() + .throwOnError() + const insertedHabit = data as NonNullable<typeof data> + return habitSupabaseDTO.fromSupabaseToDomain(insertedHabit) } } diff --git a/infrastructure/supabase/repositories/HabitEdit.ts b/infrastructure/supabase/repositories/HabitEdit.ts index 4938707aaa058df3107eab49d982bb155fe2e8e8..bd417e22578d91d964290c8e8021b66a38c76fd3 100644 --- a/infrastructure/supabase/repositories/HabitEdit.ts +++ b/infrastructure/supabase/repositories/HabitEdit.ts @@ -1,7 +1,6 @@ -import { Habit } from "@/domain/entities/Habit" import type { HabitEditRepository } from "@/domain/repositories/HabitEdit" -import { SupabaseRepository } from "./_SupabaseRepository" -import { Goal } from "@/domain/entities/Goal" +import { SupabaseRepository } from "@/infrastructure/supabase/repositories/_SupabaseRepository" +import { habitSupabaseDTO } from "../data-transfer-objects/HabitDTO" export class HabitEditSupabaseRepository extends SupabaseRepository @@ -9,46 +8,16 @@ export class HabitEditSupabaseRepository { public execute: HabitEditRepository["execute"] = async (options) => { const { habitEditData } = options - const { data, error } = await this.supabaseClient + const { data } = await this.supabaseClient .from("habits") - .update({ - name: habitEditData.name, - color: habitEditData.color, - icon: habitEditData.icon, - end_date: habitEditData?.endDate?.toISOString(), - }) + .update( + habitSupabaseDTO.fromDomainEditDataToSupabaseUpdate(habitEditData), + ) .eq("id", habitEditData.id) .select("*") - const updatedHabit = data?.[0] - if (error != null || updatedHabit == null) { - throw new Error(error?.message ?? "Failed to edit habit.") - } - const habit = new Habit({ - id: updatedHabit.id.toString(), - userId: updatedHabit.user_id.toString(), - name: updatedHabit.name, - icon: updatedHabit.icon, - goal: Goal.create({ - frequency: updatedHabit.goal_frequency, - target: - updatedHabit.goal_target != null && - updatedHabit.goal_target_unit != null - ? { - type: "numeric", - value: updatedHabit.goal_target, - unit: updatedHabit.goal_target_unit, - } - : { - type: "boolean", - }, - }), - color: updatedHabit.color, - startDate: new Date(updatedHabit.start_date), - endDate: - updatedHabit.end_date != null - ? new Date(updatedHabit.end_date) - : undefined, - }) - return habit + .single() + .throwOnError() + const updatedHabit = data as NonNullable<typeof data> + return habitSupabaseDTO.fromSupabaseToDomain(updatedHabit) } } diff --git a/infrastructure/supabase/repositories/HabitProgressCreate.ts b/infrastructure/supabase/repositories/HabitProgressCreate.ts index 90e497989e908c1946f17bef70d0c18081dcf1cd..8a97c48292f7f7a9e9cf469d2f940c8b4698a714 100644 --- a/infrastructure/supabase/repositories/HabitProgressCreate.ts +++ b/infrastructure/supabase/repositories/HabitProgressCreate.ts @@ -1,6 +1,6 @@ import type { HabitProgressCreateRepository } from "@/domain/repositories/HabitProgressCreate" -import { SupabaseRepository } from "./_SupabaseRepository" -import { HabitProgress } from "@/domain/entities/HabitProgress" +import { SupabaseRepository } from "@/infrastructure/supabase/repositories/_SupabaseRepository" +import { habitProgressSupabaseDTO } from "../data-transfer-objects/HabitProgressDTO" export class HabitProgressCreateSupabaseRepository extends SupabaseRepository @@ -10,29 +10,20 @@ export class HabitProgressCreateSupabaseRepository options, ) => { const { habitProgressData } = options - const { goalProgress, date, habitId } = habitProgressData - let goalProgressValue = goalProgress.isCompleted() ? 1 : 0 - if (goalProgress.isNumeric()) { - goalProgressValue = goalProgress.progress - } - const { data, error } = await this.supabaseClient + const { data } = await this.supabaseClient .from("habits_progresses") - .insert({ - habit_id: Number(habitId), - date: date.toISOString(), - goal_progress: goalProgressValue, - }) + .insert( + habitProgressSupabaseDTO.fromDomainDataToSupabaseInsert( + habitProgressData, + ), + ) .select("*") - const insertedProgress = data?.[0] - if (error != null || insertedProgress == null) { - throw new Error(error?.message ?? "Failed to create habit progress.") - } - const habitProgress = new HabitProgress({ - id: insertedProgress.id.toString(), - habitId: insertedProgress.habit_id.toString(), - date: new Date(insertedProgress.date), - goalProgress, - }) - return habitProgress + .single() + .throwOnError() + const insertedProgress = data as NonNullable<typeof data> + return habitProgressSupabaseDTO.fromSupabaseToDomain( + insertedProgress, + habitProgressData.goalProgress.goal, + ) } } diff --git a/infrastructure/supabase/repositories/HabitProgressUpdate.ts b/infrastructure/supabase/repositories/HabitProgressUpdate.ts index b7b437bf810622c8351dbb8840e8761da52e8237..da04eb34a18480e61dde948147facf2e2ac5cde5 100644 --- a/infrastructure/supabase/repositories/HabitProgressUpdate.ts +++ b/infrastructure/supabase/repositories/HabitProgressUpdate.ts @@ -1,6 +1,6 @@ import type { HabitProgressUpdateRepository } from "@/domain/repositories/HabitProgressUpdate" -import { SupabaseRepository } from "./_SupabaseRepository" -import { HabitProgress } from "@/domain/entities/HabitProgress" +import { SupabaseRepository } from "@/infrastructure/supabase/repositories/_SupabaseRepository" +import { habitProgressSupabaseDTO } from "../data-transfer-objects/HabitProgressDTO" export class HabitProgressUpdateSupabaseRepository extends SupabaseRepository @@ -10,29 +10,21 @@ export class HabitProgressUpdateSupabaseRepository options, ) => { const { habitProgressData } = options - const { id, goalProgress, date } = habitProgressData - let goalProgressValue = goalProgress.isCompleted() ? 1 : 0 - if (goalProgress.isNumeric()) { - goalProgressValue = goalProgress.progress - } - const { data, error } = await this.supabaseClient + const { data } = await this.supabaseClient .from("habits_progresses") - .update({ - date: date.toISOString(), - goal_progress: goalProgressValue, - }) - .eq("id", id) + .update( + habitProgressSupabaseDTO.fromDomainDataToSupabaseUpdate( + habitProgressData, + ), + ) + .eq("id", habitProgressData.id) .select("*") - const insertedProgress = data?.[0] - if (error != null || insertedProgress == null) { - throw new Error(error?.message ?? "Failed to update habit progress.") - } - const habitProgress = new HabitProgress({ - id: insertedProgress.id.toString(), - habitId: insertedProgress.habit_id.toString(), - date: new Date(insertedProgress.date), - goalProgress, - }) - return habitProgress + .single() + .throwOnError() + const insertedProgress = data as NonNullable<typeof data> + return habitProgressSupabaseDTO.fromSupabaseToDomain( + insertedProgress, + habitProgressData.goalProgress.goal, + ) } } diff --git a/infrastructure/supabase/supabase.ts b/infrastructure/supabase/supabase.ts index 8a1dc59a0e10c93f2429f58325400b951d673687..c3fb1f812e8b3464e01651e4b844c504f46572b6 100644 --- a/infrastructure/supabase/supabase.ts +++ b/infrastructure/supabase/supabase.ts @@ -9,9 +9,19 @@ import AsyncStorage from "@react-native-async-storage/async-storage" import type { Database } from "./supabase-types" export type SupabaseUser = SupabaseUserType + export type SupabaseHabit = Database["public"]["Tables"]["habits"]["Row"] +export type SupabaseHabitInsert = + Database["public"]["Tables"]["habits"]["Insert"] +export type SupabaseHabitUpdate = + Database["public"]["Tables"]["habits"]["Update"] + export type SupabaseHabitProgress = Database["public"]["Tables"]["habits_progresses"]["Row"] +export type SupabaseHabitProgressInsert = + Database["public"]["Tables"]["habits_progresses"]["Insert"] +export type SupabaseHabitProgressUpdate = + Database["public"]["Tables"]["habits_progresses"]["Update"] const SUPABASE_URL = process.env["EXPO_PUBLIC_SUPABASE_URL"] ?? diff --git a/jest.config.json b/jest.config.json index 5909eb4f2410003ec3a8fd626ac56a4f151a6193..e75d002d4363396f104f1ac8ae65626e2521a303 100644 --- a/jest.config.json +++ b/jest.config.json @@ -10,7 +10,13 @@ "coverageReporters": ["text", "text-summary", "cobertura"], "collectCoverageFrom": [ "<rootDir>/**/*.{ts,tsx}", + "!<rootDir>/tests/**/*", + "!<rootDir>/domain/repositories/**/*", + "!<rootDir>/infrastructure/instances.ts", + "!<rootDir>/infrastructure/supabase/supabase-types.ts", + "!<rootDir>/infrastructure/supabase/supabase.ts", "!<rootDir>/presentation/react-native/ui/ExternalLink.tsx", + "!<rootDir>/presentation/react/contexts/**/*", "!<rootDir>/.expo", "!<rootDir>/app/+html.tsx", "!<rootDir>/app/**/_layout.tsx", diff --git a/presentation/presenters/_Presenter.ts b/presentation/presenters/_Presenter.ts index 1f53ccbf68f1b6c980700700a158e00fd893f55d..e500275d90e3f9435ed32b51386c785174a8d888 100644 --- a/presentation/presenters/_Presenter.ts +++ b/presentation/presenters/_Presenter.ts @@ -41,10 +41,7 @@ export abstract class Presenter<State> { public unsubscribe(listener: Listener<State>): void { const listenerIndex = this._listeners.indexOf(listener) - const listenerFound = listenerIndex !== -1 - if (listenerFound) { - this._listeners.splice(listenerIndex, 1) - } + this._listeners.splice(listenerIndex, 1) } private notifyListeners(): void { diff --git a/presentation/react-native/components/HabitForm/IconSelectorModal.tsx b/presentation/react-native/components/HabitForm/IconSelectorModal.tsx index 20389c69e1cb006ee6f26a99ab85964fdbbdec87..d130c8bf8ab01672cf5fc8af468d70393134fafa 100644 --- a/presentation/react-native/components/HabitForm/IconSelectorModal.tsx +++ b/presentation/react-native/components/HabitForm/IconSelectorModal.tsx @@ -1,9 +1,9 @@ +import type { IconName } from "@fortawesome/fontawesome-svg-core" import { fas } from "@fortawesome/free-solid-svg-icons" +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome" import { memo, useCallback, useEffect, useState, useTransition } from "react" import { Modal, ScrollView, View } from "react-native" import { Button, List, Text, TextInput } from "react-native-paper" -import type { IconName } from "@fortawesome/fontawesome-svg-core" -import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome" import { IconsList } from "./IconsList" diff --git a/presentation/react-native/components/HabitsMainPage/HabitCard.tsx b/presentation/react-native/components/HabitsMainPage/HabitCard.tsx index 4f032d3e25da5fc90a59674a05dfb122cb0d1d18..c7b40cdf2042c0916fc30497fe7c9bc4f23d6753 100644 --- a/presentation/react-native/components/HabitsMainPage/HabitCard.tsx +++ b/presentation/react-native/components/HabitsMainPage/HabitCard.tsx @@ -1,16 +1,16 @@ +import type { IconName } from "@fortawesome/free-solid-svg-icons" import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome" import { useRouter } from "expo-router" +import type LottieView from "lottie-react-native" import { useState } from "react" import { View } from "react-native" import { Checkbox, List, Text } from "react-native-paper" -import type LottieView from "lottie-react-native" -import type { IconName } from "@fortawesome/free-solid-svg-icons" import type { GoalBoolean } from "@/domain/entities/Goal" import { GoalBooleanProgress } from "@/domain/entities/Goal" import type { HabitHistory } from "@/domain/entities/HabitHistory" -import { getColorRGBAFromHex } from "@/utils/colors" import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker" +import { getColorRGBAFromHex } from "@/utils/colors" export interface HabitCardProps { habitHistory: HabitHistory diff --git a/presentation/react-native/components/HabitsMainPage/HabitsList.tsx b/presentation/react-native/components/HabitsMainPage/HabitsList.tsx index 01954324ffc773dcf05a90b8843dda40fa04fe40..beacc119c9a9bc7d35d9c550186a5d2f593b9c2d 100644 --- a/presentation/react-native/components/HabitsMainPage/HabitsList.tsx +++ b/presentation/react-native/components/HabitsMainPage/HabitsList.tsx @@ -5,8 +5,8 @@ import { Divider, List } from "react-native-paper" import type { GoalFrequency } from "@/domain/entities/Goal" import type { HabitsTracker } from "@/domain/entities/HabitsTracker" -import confettiJSON from "../../../assets/confetti.json" import { capitalize } from "@/utils/strings" +import confettiJSON from "../../../assets/confetti.json" import { HabitCard } from "./HabitCard" export interface HabitsListProps { diff --git a/presentation/react/contexts/Authentication.tsx b/presentation/react/contexts/Authentication.tsx index 85c507ca3717edc262d5a250ae22fd210c731eeb..68094319e59dab8f89ac6de77634a57608b4cbd1 100644 --- a/presentation/react/contexts/Authentication.tsx +++ b/presentation/react/contexts/Authentication.tsx @@ -1,11 +1,11 @@ import { createContext, useContext, useEffect } from "react" -import { usePresenterState } from "@/presentation/react/hooks/usePresenterState" +import { authenticationPresenter } from "@/infrastructure/instances" import type { AuthenticationPresenter, AuthenticationPresenterState, } from "@/presentation/presenters/Authentication" -import { authenticationPresenter } from "@/infrastructure/instances" +import { usePresenterState } from "@/presentation/react/hooks/usePresenterState" export interface AuthenticationContextValue extends AuthenticationPresenterState { diff --git a/presentation/react/contexts/HabitsTracker.tsx b/presentation/react/contexts/HabitsTracker.tsx index a4b1948533881eb677dc11e3577bdbce2f74ceed..bded388db7edb36d100635f9ef34014864fff849 100644 --- a/presentation/react/contexts/HabitsTracker.tsx +++ b/presentation/react/contexts/HabitsTracker.tsx @@ -1,11 +1,11 @@ import { createContext, useContext, useEffect } from "react" +import { habitsTrackerPresenter } from "@/infrastructure/instances" import type { HabitsTrackerPresenter, HabitsTrackerPresenterState, } from "@/presentation/presenters/HabitsTracker" import { usePresenterState } from "@/presentation/react/hooks/usePresenterState" -import { habitsTrackerPresenter } from "@/infrastructure/instances" import { useAuthentication } from "./Authentication" export interface HabitsTrackerContextValue extends HabitsTrackerPresenterState { diff --git a/presentation/react/hooks/__tests__/usePresenterState.test.ts b/presentation/react/hooks/__tests__/usePresenterState.test.ts index 4c71adfcf6a9a3293fd3ac1eed6569b8d1cce377..5c7d6e04b6a76ca92e85819473d857cef725c6e5 100644 --- a/presentation/react/hooks/__tests__/usePresenterState.test.ts +++ b/presentation/react/hooks/__tests__/usePresenterState.test.ts @@ -1,7 +1,7 @@ import { act, renderHook } from "@testing-library/react-native" -import { usePresenterState } from "@/presentation/react/hooks/usePresenterState" import { Presenter } from "@/presentation/presenters/_Presenter" +import { usePresenterState } from "@/presentation/react/hooks/usePresenterState" interface MockCountPresenterState { count: number diff --git a/tests/mocks/domain/Habit.ts b/tests/mocks/domain/Habit.ts new file mode 100644 index 0000000000000000000000000000000000000000..5c2ddfbebed527aec04a23bd0b90d8b935ce9962 --- /dev/null +++ b/tests/mocks/domain/Habit.ts @@ -0,0 +1,115 @@ +import { GoalBoolean, GoalNumeric } from "@/domain/entities/Goal" +import type { HabitData } from "@/domain/entities/Habit" +import { Habit } from "@/domain/entities/Habit" +import { USER_MOCK } from "./User" +import { ONE_DAY_MILLISECONDS } from "@/utils/dates" + +interface HabitMockCreateOptions extends Omit<HabitData, "startDate"> { + startDate?: Date +} +const habitMockCreate = (options: HabitMockCreateOptions): Habit => { + const { + id, + userId, + name, + color, + icon, + goal, + startDate = new Date(), + endDate, + } = options + + return new Habit({ + id, + userId, + name, + color, + icon, + goal, + startDate, + endDate, + }) +} + +const examplesByNames = { + "Wake up at 07h00": habitMockCreate({ + id: "1", + userId: USER_MOCK.example.id, + name: "Wake up at 07h00", + color: "#006CFF", + icon: "bed", + goal: new GoalBoolean({ + frequency: "daily", + }), + }), + "Learn English": habitMockCreate({ + id: "2", + userId: USER_MOCK.example.id, + name: "Learn English", + color: "#EB4034", + icon: "language", + goal: new GoalNumeric({ + frequency: "daily", + target: { + value: 30, + unit: "minutes", + }, + }), + }), + Walk: habitMockCreate({ + id: "3", + userId: USER_MOCK.example.id, + name: "Walk", + color: "#228B22", + icon: "person-walking", + goal: new GoalNumeric({ + frequency: "daily", + target: { + value: 5000, + unit: "steps", + }, + }), + }), + "Clean the house": habitMockCreate({ + id: "4", + userId: USER_MOCK.example.id, + name: "Clean the house", + color: "#808080", + icon: "broom", + goal: new GoalBoolean({ + frequency: "weekly", + }), + }), + "Solve Programming Challenges": habitMockCreate({ + id: "5", + userId: USER_MOCK.example.id, + name: "Solve Programming Challenges", + color: "#DE3163", + icon: "code", + goal: new GoalNumeric({ + frequency: "monthly", + target: { + value: 5, + unit: "challenges", + }, + }), + endDate: new Date(Date.now() + ONE_DAY_MILLISECONDS), + }), +} as const + +export const examplesByIds = { + [examplesByNames["Wake up at 07h00"].id]: examplesByNames["Wake up at 07h00"], + [examplesByNames["Learn English"].id]: examplesByNames["Learn English"], + [examplesByNames.Walk.id]: examplesByNames.Walk, + [examplesByNames["Clean the house"].id]: examplesByNames["Clean the house"], + [examplesByNames["Solve Programming Challenges"].id]: + examplesByNames["Solve Programming Challenges"], +} as const + +export const HABIT_MOCK = { + create: habitMockCreate, + example: examplesByNames["Wake up at 07h00"], + examplesByNames, + examplesByIds, + examples: Object.values(examplesByNames), +} diff --git a/tests/mocks/domain/HabitProgress.ts b/tests/mocks/domain/HabitProgress.ts new file mode 100644 index 0000000000000000000000000000000000000000..7496ba17c634c5fae09481e04206915ee765e7cf --- /dev/null +++ b/tests/mocks/domain/HabitProgress.ts @@ -0,0 +1,51 @@ +import type { GoalBoolean, GoalNumeric } from "@/domain/entities/Goal" +import { + GoalBooleanProgress, + GoalNumericProgress, +} from "@/domain/entities/Goal" +import type { HabitProgressData } from "@/domain/entities/HabitProgress" +import { HabitProgress } from "@/domain/entities/HabitProgress" +import { HABIT_MOCK } from "./Habit" + +interface HabitProgressMockCreateOptions + extends Omit<HabitProgressData, "date"> { + date?: Date +} + +const habitProgressMockCreate = ( + options: HabitProgressMockCreateOptions, +): HabitProgress => { + const { id, habitId, goalProgress, date = new Date() } = options + + return new HabitProgress({ + date, + goalProgress, + habitId, + id, + }) +} + +const exampleByIds = { + 1: habitProgressMockCreate({ + id: "1", + habitId: HABIT_MOCK.examplesByNames["Clean the house"].id, + goalProgress: new GoalBooleanProgress({ + goal: HABIT_MOCK.examplesByNames["Clean the house"].goal as GoalBoolean, + progress: true, + }), + }), + 2: habitProgressMockCreate({ + id: "2", + habitId: HABIT_MOCK.examplesByNames.Walk.id, + goalProgress: new GoalNumericProgress({ + goal: HABIT_MOCK.examplesByNames.Walk.goal as GoalNumeric, + progress: 4_733, + }), + }), +} as const + +export const HABIT_PROGRESS_MOCK = { + create: habitProgressMockCreate, + exampleByIds, + examples: Object.values(exampleByIds), +} diff --git a/tests/mocks/domain/User.ts b/tests/mocks/domain/User.ts new file mode 100644 index 0000000000000000000000000000000000000000..40aa91c44f2096b1e9b28c9f4e2b8994096af127 --- /dev/null +++ b/tests/mocks/domain/User.ts @@ -0,0 +1,30 @@ +import type { UserData } from "@/domain/entities/User" +import { User } from "@/domain/entities/User" + +const USER_MOCK_ID = "ab054ee9-fbb4-473e-942b-bbf4415f4bef" +const USER_MOCK_EMAIL = "test@test.com" +const USER_MOCK_DISPLAY_NAME = "Test" + +interface UserMockCreateOptions { + id?: UserData["id"] + email?: UserData["email"] + displayName?: UserData["displayName"] +} +const userMockCreate = (options: UserMockCreateOptions = {}): User => { + const { + id = USER_MOCK_ID, + email = USER_MOCK_EMAIL, + displayName = USER_MOCK_DISPLAY_NAME, + } = options + + return new User({ + id, + email, + displayName, + }) +} + +export const USER_MOCK = { + create: userMockCreate, + example: userMockCreate(), +} diff --git a/tests/mocks/supabase/Habit.ts b/tests/mocks/supabase/Habit.ts new file mode 100644 index 0000000000000000000000000000000000000000..e399e7fb4f62dcf3e6d66e206216a513df31a77c --- /dev/null +++ b/tests/mocks/supabase/Habit.ts @@ -0,0 +1,79 @@ +import type { SupabaseHabit } from "@/infrastructure/supabase/supabase" +import { HABIT_MOCK } from "../domain/Habit" +import { SUPABASE_USER_MOCK } from "./User" + +interface SupabaseHabitMockCreateOptions { + id: SupabaseHabit["id"] + userId: SupabaseHabit["user_id"] + name: SupabaseHabit["name"] + color: SupabaseHabit["color"] + icon: SupabaseHabit["icon"] + startDate?: Date + endDate: Date | null + goalFrequency: SupabaseHabit["goal_frequency"] + goalTarget: SupabaseHabit["goal_target"] | null + goalTargetUnit: SupabaseHabit["goal_target_unit"] | null +} +const supabaseHabitMockCreate = ( + options: SupabaseHabitMockCreateOptions, +): SupabaseHabit => { + const { + id, + userId, + name, + color, + icon, + startDate = new Date(), + endDate, + goalFrequency, + goalTarget, + goalTargetUnit, + } = options + + return { + id, + user_id: userId, + name, + color, + icon, + start_date: startDate.toISOString(), + end_date: endDate?.toISOString() ?? null, + goal_frequency: goalFrequency, + goal_target: goalTarget, + goal_target_unit: goalTargetUnit, + } +} + +const examplesByNames = Object.fromEntries( + Object.entries(HABIT_MOCK.examplesByNames).map(([name, habit]) => { + const goalTarget = habit.goal.isNumeric() ? habit.goal.target.value : null + const goalTargetUnit = habit.goal.isNumeric() + ? habit.goal.target.unit + : null + return [ + name, + supabaseHabitMockCreate({ + id: Number.parseInt(habit.id, 10), + userId: SUPABASE_USER_MOCK.example.id, + name: habit.name, + color: habit.color, + icon: habit.icon, + startDate: habit.startDate, + endDate: habit.endDate ?? null, + goalFrequency: habit.goal.frequency, + goalTarget, + goalTargetUnit, + }), + ] + }), +) as { + [key in keyof (typeof HABIT_MOCK)["examplesByNames"]]: SupabaseHabit +} + +export const SUPABASE_HABIT_MOCK = { + create: supabaseHabitMockCreate, + example: + examplesByNames[HABIT_MOCK.example.name as keyof typeof examplesByNames], + examples: Object.values(examplesByNames), + examplesByNames, +} diff --git a/tests/mocks/supabase/HabitProgress.ts b/tests/mocks/supabase/HabitProgress.ts new file mode 100644 index 0000000000000000000000000000000000000000..0fdbd653eeae86072061abb05398fb0f34a0f561 --- /dev/null +++ b/tests/mocks/supabase/HabitProgress.ts @@ -0,0 +1,49 @@ +import type { SupabaseHabitProgress } from "@/infrastructure/supabase/supabase" +import { HABIT_PROGRESS_MOCK } from "../domain/HabitProgress" + +interface SupabaseHabitProgressMockCreateOptions { + id: SupabaseHabitProgress["id"] + habitId: SupabaseHabitProgress["habit_id"] + date?: Date + goalProgress: SupabaseHabitProgress["goal_progress"] +} +const supabaseHabitProgressMockCreate = ( + options: SupabaseHabitProgressMockCreateOptions, +): SupabaseHabitProgress => { + const { id, habitId, date = new Date(), goalProgress } = options + + return { + id, + habit_id: habitId, + date: date.toISOString(), + goal_progress: goalProgress, + } +} + +const exampleByIds = Object.fromEntries( + Object.entries(HABIT_PROGRESS_MOCK.exampleByIds).map( + ([id, habitProgress]) => { + return [ + id, + supabaseHabitProgressMockCreate({ + id: Number.parseInt(habitProgress.id, 10), + habitId: Number.parseInt(habitProgress.habitId, 10), + date: new Date(habitProgress.date), + goalProgress: habitProgress.goalProgress.isNumeric() + ? habitProgress.goalProgress.progress + : habitProgress.goalProgress.isCompleted() + ? 1 + : 0, + }), + ] + }, + ), +) as { + [key in keyof (typeof HABIT_PROGRESS_MOCK)["exampleByIds"]]: SupabaseHabitProgress +} + +export const SUPABASE_HABIT_PROGRESS_MOCK = { + create: supabaseHabitProgressMockCreate, + exampleByIds, + examples: Object.values(exampleByIds), +} diff --git a/tests/mocks/supabase/User.ts b/tests/mocks/supabase/User.ts new file mode 100644 index 0000000000000000000000000000000000000000..56257f97395487b46b782319bebdc2eaca075d12 --- /dev/null +++ b/tests/mocks/supabase/User.ts @@ -0,0 +1,63 @@ +import type { SupabaseUser } from "@/infrastructure/supabase/supabase" +import { USER_MOCK } from "../domain/User" + +interface SupabaseUserMockCreateOptions { + id?: SupabaseUser["id"] + email?: SupabaseUser["email"] + displayName?: SupabaseUser["user_metadata"]["display_name"] + date?: Date +} +const supabaseUserMockCreate = ( + options: SupabaseUserMockCreateOptions = {}, +): SupabaseUser => { + const { + id = USER_MOCK.example.id, + email = USER_MOCK.example.email, + displayName = USER_MOCK.example.displayName, + date = new Date(), + } = options + + return { + id, + app_metadata: { provider: "email", providers: ["email"] }, + user_metadata: { display_name: displayName }, + aud: "authenticated", + email, + confirmation_sent_at: undefined, + recovery_sent_at: undefined, + email_change_sent_at: undefined, + new_email: "", + new_phone: "", + invited_at: undefined, + action_link: "", + created_at: date.toISOString(), + confirmed_at: undefined, + email_confirmed_at: date.toISOString(), + phone_confirmed_at: undefined, + last_sign_in_at: undefined, + role: "authenticated", + updated_at: date.toISOString(), + identities: [ + { + id, + user_id: id, + identity_data: { + sub: id, + email, + }, + provider: "email", + identity_id: id, + last_sign_in_at: date.toISOString(), + created_at: date.toISOString(), + updated_at: date.toISOString(), + }, + ], + is_anonymous: false, + factors: [], + } +} + +export const SUPABASE_USER_MOCK = { + create: supabaseUserMockCreate, + example: supabaseUserMockCreate(), +}