We should finish the API:s so that they achieve feature parity with the web app. The lacking functionality is to be able to fetch the daily status of each habit, and to track a habit as having been achieved.

I’m thinking the first will be at a GET endpoint at /api/habits/{date}, where {date} is written in ISO format. So today would be /api/habits/2023-02-23.

And then you would achieve a habit – and I am really starting to wonder why I didn’t just call that to “track” a habit – by posting to /api/habits/{date}/{habit-id}/achieve with an empty (but still JSON) body, i.e. {}. Is that a weird API? A bit weird, perhaps.

It will be up to the client to determine what today’s date is. In other words, you’ll be able to track days after the fact. Maybe I’ll add some limits to that.

Today, I’ll start with just writing the tests for this.

I’ll write some methods to call the endpoints:

public class ApiTests {
    // ,,,

    private void achieveHabit(String username, Long habitId, String date) {
        record Request() { }
        record Response() { }

        final var response = sendReceiving(
            Response.class,
            POST(
                uri("/api/habits/" + date + "/" + habitId + "/achieve"),
                new Request()
            )
                .header("Authorization", testDataManager.authHeader(username))
                .build()
        );

        assertThat(response.statusCode()).isEqualTo(200);
    }

    private List<HabitForDate> getHabitsForDate(String username, String date) {
        record GetHabitsForDateResponse(List<HabitForDate> habits) { }

        final var response = sendReceiving(
            GetHabitsForDateResponse.class,
            GET(uri("/api/habits/" + date))
                .header("Authorization", testDataManager.authHeader(username))
                .build()
        );

        assertThat(response.statusCode()).isEqualTo(200);
        return response.body().habits;
    }

    // ...
}

And then the test becomes:

public class ApiTests {
    // ,,,

    @Test
    void create_habit_and_achieve_it() {
        final var username = testDataManager.createRandomUser();

        final var habitsBefore = getHabits(username);
        assertThat(habitsBefore).isEmpty();

        createHabit(username, "Go for a walk");

        final var habitsAfter = getHabits(username);
        assertThat(habitsAfter).hasSize(1);
        final var habit = habitsAfter.get(0);
        assertThat(habit.description()).isEqualTo("Go for a walk");

        String date = "2020-01-01";

        // Get habits-for-date before achieving
        final var habitsForDateBefore = getHabitsForDate(username, date);
        assertThat(habitsForDateBefore).hasSize(1);
        final var habitForDate = habitsForDateBefore.get(0);
        assertThat(habitForDate.description()).isEqualTo("Go for a walk");
        assertThat(habitForDate.achievementId()).isNull();

        // Achieve habit
        achieveHabit(username, habit.id(), date);

        // Get habits-for-date after achieving
        final var habitsForDateAfter = getHabitsForDate(username, date);
        assertThat(habitsForDateAfter).hasSize(1);
        final var habitForDateAfter = habitsForDateAfter.get(0);
        assertThat(habitForDateAfter.description()).isEqualTo("Go for a walk");
        assertThat(habitForDateAfter.achievementId()).isNotNull();
    }

    // ...
}

The test is failing as expected, as the endpoints are not implemented.

Continue to part thirty-nine.