// // TrefleDTOsTests.swift // PlantGuideTests // // Unit tests for Trefle API DTO decoding. // Tests JSON decoding of all DTOs using sample Trefle API responses. // import XCTest @testable import PlantGuide final class TrefleDTOsTests: XCTestCase { // MARK: - Properties private var decoder: JSONDecoder! // MARK: - Setup override func setUp() { super.setUp() decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase } override func tearDown() { decoder = nil super.tearDown() } // MARK: - Sample JSON Data /// Sample search response JSON from Trefle API documentation private let searchResponseJSON = """ { "data": [ { "id": 834, "common_name": "Swiss cheese plant", "slug": "monstera-deliciosa", "scientific_name": "Monstera deliciosa", "year": 1849, "bibliography": "Vidensk. Meddel. Naturhist. Foren. Kjøbenhavn 1849: 19 (1849)", "author": "Liebm.", "family_common_name": "Arum family", "genus_id": 1254, "image_url": "https://bs.plantnet.org/image/o/abc123", "genus": "Monstera", "family": "Araceae" } ], "links": { "self": "/api/v1/plants/search?q=monstera", "first": "/api/v1/plants/search?page=1&q=monstera", "last": "/api/v1/plants/search?page=1&q=monstera" }, "meta": { "total": 12 } } """.data(using: .utf8)! /// Sample species detail response JSON from Trefle API documentation private let speciesResponseJSON = """ { "data": { "id": 834, "common_name": "Swiss cheese plant", "slug": "monstera-deliciosa", "scientific_name": "Monstera deliciosa", "growth": { "light": 6, "atmospheric_humidity": 8, "minimum_temperature": { "deg_c": 15 }, "maximum_temperature": { "deg_c": 30 }, "soil_humidity": 7, "soil_nutriments": 5 }, "specifications": { "growth_rate": "moderate", "toxicity": "mild" } }, "meta": { "last_modified": "2023-01-15T12:00:00Z" } } """.data(using: .utf8)! // MARK: - TrefleSearchResponseDTO Tests func testSearchResponseDecoding() throws { // When let response = try decoder.decode(TrefleSearchResponseDTO.self, from: searchResponseJSON) // Then XCTAssertEqual(response.data.count, 1) XCTAssertEqual(response.meta.total, 12) XCTAssertEqual(response.links.selfLink, "/api/v1/plants/search?q=monstera") } func testSearchResponseDataContainsPlantSummary() throws { // When let response = try decoder.decode(TrefleSearchResponseDTO.self, from: searchResponseJSON) let plant = response.data.first // Then XCTAssertNotNil(plant) XCTAssertEqual(plant?.id, 834) XCTAssertEqual(plant?.commonName, "Swiss cheese plant") XCTAssertEqual(plant?.slug, "monstera-deliciosa") XCTAssertEqual(plant?.scientificName, "Monstera deliciosa") XCTAssertEqual(plant?.family, "Araceae") XCTAssertEqual(plant?.genus, "Monstera") XCTAssertEqual(plant?.imageUrl, "https://bs.plantnet.org/image/o/abc123") } func testSearchResponseLinksDecoding() throws { // When let response = try decoder.decode(TrefleSearchResponseDTO.self, from: searchResponseJSON) let links = response.links // Then XCTAssertEqual(links.selfLink, "/api/v1/plants/search?q=monstera") XCTAssertEqual(links.first, "/api/v1/plants/search?page=1&q=monstera") XCTAssertEqual(links.last, "/api/v1/plants/search?page=1&q=monstera") XCTAssertNil(links.next) XCTAssertNil(links.prev) } // MARK: - TrefleSpeciesResponseDTO Tests func testSpeciesResponseDecoding() throws { // When let response = try decoder.decode(TrefleSpeciesResponseDTO.self, from: speciesResponseJSON) // Then XCTAssertEqual(response.data.id, 834) XCTAssertEqual(response.data.scientificName, "Monstera deliciosa") XCTAssertEqual(response.meta.lastModified, "2023-01-15T12:00:00Z") } func testSpeciesResponseGrowthDataDecoding() throws { // When let response = try decoder.decode(TrefleSpeciesResponseDTO.self, from: speciesResponseJSON) let growth = response.data.growth // Then XCTAssertNotNil(growth) XCTAssertEqual(growth?.light, 6) XCTAssertEqual(growth?.atmosphericHumidity, 8) XCTAssertEqual(growth?.soilHumidity, 7) XCTAssertEqual(growth?.soilNutriments, 5) } func testSpeciesResponseTemperatureDecoding() throws { // When let response = try decoder.decode(TrefleSpeciesResponseDTO.self, from: speciesResponseJSON) let growth = response.data.growth // Then XCTAssertNotNil(growth?.minimumTemperature) XCTAssertEqual(growth?.minimumTemperature?.degC, 15) XCTAssertNotNil(growth?.maximumTemperature) XCTAssertEqual(growth?.maximumTemperature?.degC, 30) } func testSpeciesResponseSpecificationsDecoding() throws { // When let response = try decoder.decode(TrefleSpeciesResponseDTO.self, from: speciesResponseJSON) let specifications = response.data.specifications // Then XCTAssertNotNil(specifications) XCTAssertEqual(specifications?.growthRate, "moderate") XCTAssertEqual(specifications?.toxicity, "mild") } // MARK: - TreflePlantSummaryDTO Tests func testPlantSummaryDecodingWithAllFields() throws { // Given let json = """ { "id": 123, "common_name": "Test Plant", "slug": "test-plant", "scientific_name": "Testus plantus", "family": "Testaceae", "genus": "Testus", "image_url": "https://example.com/image.jpg" } """.data(using: .utf8)! // When let summary = try decoder.decode(TreflePlantSummaryDTO.self, from: json) // Then XCTAssertEqual(summary.id, 123) XCTAssertEqual(summary.commonName, "Test Plant") XCTAssertEqual(summary.slug, "test-plant") XCTAssertEqual(summary.scientificName, "Testus plantus") XCTAssertEqual(summary.family, "Testaceae") XCTAssertEqual(summary.genus, "Testus") XCTAssertEqual(summary.imageUrl, "https://example.com/image.jpg") } func testPlantSummaryDecodingWithNullOptionalFields() throws { // Given let json = """ { "id": 456, "common_name": null, "slug": "unknown-plant", "scientific_name": "Unknown species", "family": null, "genus": null, "image_url": null } """.data(using: .utf8)! // When let summary = try decoder.decode(TreflePlantSummaryDTO.self, from: json) // Then XCTAssertEqual(summary.id, 456) XCTAssertNil(summary.commonName) XCTAssertEqual(summary.slug, "unknown-plant") XCTAssertEqual(summary.scientificName, "Unknown species") XCTAssertNil(summary.family) XCTAssertNil(summary.genus) XCTAssertNil(summary.imageUrl) } // MARK: - TrefleGrowthDTO Tests func testGrowthDTODecodingWithAllFields() throws { // Given let json = """ { "light": 7, "atmospheric_humidity": 5, "growth_months": ["mar", "apr", "may"], "bloom_months": ["jun", "jul"], "fruit_months": ["sep", "oct"], "minimum_precipitation": { "mm": 500 }, "maximum_precipitation": { "mm": 1500 }, "minimum_temperature": { "deg_c": 10, "deg_f": 50 }, "maximum_temperature": { "deg_c": 35, "deg_f": 95 }, "soil_nutriments": 6, "soil_humidity": 4, "ph_minimum": 5.5, "ph_maximum": 7.0 } """.data(using: .utf8)! // When let growth = try decoder.decode(TrefleGrowthDTO.self, from: json) // Then XCTAssertEqual(growth.light, 7) XCTAssertEqual(growth.atmosphericHumidity, 5) XCTAssertEqual(growth.growthMonths, ["mar", "apr", "may"]) XCTAssertEqual(growth.bloomMonths, ["jun", "jul"]) XCTAssertEqual(growth.fruitMonths, ["sep", "oct"]) XCTAssertEqual(growth.minimumPrecipitation?.mm, 500) XCTAssertEqual(growth.maximumPrecipitation?.mm, 1500) XCTAssertEqual(growth.minimumTemperature?.degC, 10) XCTAssertEqual(growth.minimumTemperature?.degF, 50) XCTAssertEqual(growth.maximumTemperature?.degC, 35) XCTAssertEqual(growth.soilNutriments, 6) XCTAssertEqual(growth.soilHumidity, 4) XCTAssertEqual(growth.phMinimum, 5.5) XCTAssertEqual(growth.phMaximum, 7.0) } func testGrowthDTODecodingWithMinimalData() throws { // Given let json = """ { "light": null, "atmospheric_humidity": null } """.data(using: .utf8)! // When let growth = try decoder.decode(TrefleGrowthDTO.self, from: json) // Then XCTAssertNil(growth.light) XCTAssertNil(growth.atmosphericHumidity) XCTAssertNil(growth.growthMonths) XCTAssertNil(growth.bloomMonths) XCTAssertNil(growth.minimumTemperature) XCTAssertNil(growth.maximumTemperature) } // MARK: - TrefleMeasurementDTO Tests func testMeasurementDTODecodingCelsius() throws { // Given let json = """ { "deg_c": 25.5, "deg_f": 77.9 } """.data(using: .utf8)! // When let measurement = try decoder.decode(TrefleMeasurementDTO.self, from: json) // Then XCTAssertEqual(measurement.degC, 25.5) XCTAssertEqual(measurement.degF, 77.9) XCTAssertNil(measurement.cm) XCTAssertNil(measurement.mm) } func testMeasurementDTODecodingHeight() throws { // Given let json = """ { "cm": 150 } """.data(using: .utf8)! // When let measurement = try decoder.decode(TrefleMeasurementDTO.self, from: json) // Then XCTAssertEqual(measurement.cm, 150) XCTAssertNil(measurement.mm) XCTAssertNil(measurement.degC) XCTAssertNil(measurement.degF) } // MARK: - TrefleSpecificationsDTO Tests func testSpecificationsDTODecoding() throws { // Given let json = """ { "growth_rate": "rapid", "toxicity": "high", "average_height": { "cm": 200 }, "maximum_height": { "cm": 500 } } """.data(using: .utf8)! // When let specs = try decoder.decode(TrefleSpecificationsDTO.self, from: json) // Then XCTAssertEqual(specs.growthRate, "rapid") XCTAssertEqual(specs.toxicity, "high") XCTAssertEqual(specs.averageHeight?.cm, 200) XCTAssertEqual(specs.maximumHeight?.cm, 500) } // MARK: - TrefleImagesDTO Tests func testImagesDTODecoding() throws { // Given let json = """ { "flower": [ { "id": 1, "image_url": "https://example.com/flower1.jpg" }, { "id": 2, "image_url": "https://example.com/flower2.jpg" } ], "leaf": [ { "id": 3, "image_url": "https://example.com/leaf1.jpg" } ], "bark": null, "fruit": [], "habit": null } """.data(using: .utf8)! // When let images = try decoder.decode(TrefleImagesDTO.self, from: json) // Then XCTAssertEqual(images.flower?.count, 2) XCTAssertEqual(images.flower?.first?.id, 1) XCTAssertEqual(images.flower?.first?.imageUrl, "https://example.com/flower1.jpg") XCTAssertEqual(images.leaf?.count, 1) XCTAssertNil(images.bark) XCTAssertEqual(images.fruit?.count, 0) XCTAssertNil(images.habit) } // MARK: - TrefleLinksDTO Tests func testLinksDTODecodingWithPagination() throws { // Given let json = """ { "self": "/api/v1/plants/search?q=rose&page=2", "first": "/api/v1/plants/search?q=rose&page=1", "last": "/api/v1/plants/search?q=rose&page=10", "next": "/api/v1/plants/search?q=rose&page=3", "prev": "/api/v1/plants/search?q=rose&page=1" } """.data(using: .utf8)! // When let links = try decoder.decode(TrefleLinksDTO.self, from: json) // Then XCTAssertEqual(links.selfLink, "/api/v1/plants/search?q=rose&page=2") XCTAssertEqual(links.first, "/api/v1/plants/search?q=rose&page=1") XCTAssertEqual(links.last, "/api/v1/plants/search?q=rose&page=10") XCTAssertEqual(links.next, "/api/v1/plants/search?q=rose&page=3") XCTAssertEqual(links.prev, "/api/v1/plants/search?q=rose&page=1") } // MARK: - TrefleMetaDTO Tests func testMetaDTODecodingSearchResponse() throws { // Given let json = """ { "total": 150 } """.data(using: .utf8)! // When let meta = try decoder.decode(TrefleMetaDTO.self, from: json) // Then XCTAssertEqual(meta.total, 150) XCTAssertNil(meta.lastModified) } func testMetaDTODecodingSpeciesResponse() throws { // Given let json = """ { "last_modified": "2024-06-15T10:30:00Z" } """.data(using: .utf8)! // When let meta = try decoder.decode(TrefleMetaDTO.self, from: json) // Then XCTAssertNil(meta.total) XCTAssertEqual(meta.lastModified, "2024-06-15T10:30:00Z") } // MARK: - Empty Response Tests func testSearchResponseWithEmptyData() throws { // Given let json = """ { "data": [], "links": { "self": "/api/v1/plants/search?q=xyznonexistent", "first": "/api/v1/plants/search?page=1&q=xyznonexistent" }, "meta": { "total": 0 } } """.data(using: .utf8)! // When let response = try decoder.decode(TrefleSearchResponseDTO.self, from: json) // Then XCTAssertTrue(response.data.isEmpty) XCTAssertEqual(response.meta.total, 0) } // MARK: - TrefleSpeciesDTO Full Test func testFullSpeciesDTODecoding() throws { // Given let json = """ { "id": 999, "common_name": "Test Species", "slug": "test-species", "scientific_name": "Testus speciesus", "year": 2000, "bibliography": "Test Bibliography 2000: 1 (2000)", "author": "Test Author", "family_common_name": "Test Family", "family": "Testaceae", "genus": "Testus", "genus_id": 100, "image_url": "https://example.com/main.jpg", "images": { "flower": [{ "id": 1, "image_url": "https://example.com/flower.jpg" }] }, "specifications": { "growth_rate": "slow", "toxicity": "none" }, "growth": { "light": 5, "atmospheric_humidity": 6, "bloom_months": ["apr", "may", "jun"], "minimum_temperature": { "deg_c": 10 }, "maximum_temperature": { "deg_c": 25 }, "soil_nutriments": 4 } } """.data(using: .utf8)! // When let species = try decoder.decode(TrefleSpeciesDTO.self, from: json) // Then XCTAssertEqual(species.id, 999) XCTAssertEqual(species.commonName, "Test Species") XCTAssertEqual(species.slug, "test-species") XCTAssertEqual(species.scientificName, "Testus speciesus") XCTAssertEqual(species.year, 2000) XCTAssertEqual(species.bibliography, "Test Bibliography 2000: 1 (2000)") XCTAssertEqual(species.author, "Test Author") XCTAssertEqual(species.familyCommonName, "Test Family") XCTAssertEqual(species.family, "Testaceae") XCTAssertEqual(species.genus, "Testus") XCTAssertEqual(species.genusId, 100) XCTAssertEqual(species.imageUrl, "https://example.com/main.jpg") XCTAssertNotNil(species.images) XCTAssertEqual(species.images?.flower?.count, 1) XCTAssertNotNil(species.specifications) XCTAssertEqual(species.specifications?.growthRate, "slow") XCTAssertNotNil(species.growth) XCTAssertEqual(species.growth?.light, 5) XCTAssertEqual(species.growth?.bloomMonths, ["apr", "may", "jun"]) } }