diff --git a/.env.test b/.env.test index 8b11059..8d20c8a 100644 --- a/.env.test +++ b/.env.test @@ -26,8 +26,11 @@ export IDEA_SERVICE_API_BASE="http://localhost:3000/api" # Used to interact with Concept API from the backend side. export CONCEPT_SERVICE_API_BASE="http://localhost:3000/api" -# Maximum number of allowed requests per timeframe for creating ideas. Default: 5 -export CREATE_IDEA_LIMITER_LIMIT=3 +# Used to interact with Feedback API from the backend side. +export FEEDBACK_SERVICE_API_BASE="http://localhost:3000/api" -# Timeframe in seconds (1 hour window) for rate limits on creating ideas. Default: 3600 -export CREATE_IDEA_LIMITER_TIMEFRAME=300 +# Maximum number of allowed requests per timeframe for creating ideas. Default: 30. +export CREATE_IDEA_LIMITER_LIMIT=30 + +# Timeframe in seconds for rate limits on creating ideas. Default: 3600 seconds. +export CREATE_IDEA_LIMITER_TIMEFRAME=3600 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..9cbbc66 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,23 @@ +name: Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v3 + with: + node-version: 22.11.0 + cache: npm + + - run: npm install + - run: npm run test:coverage + + - name: Upload coverage report + uses: actions/upload-artifact@v3 + with: + name: coverage-report + path: coverage/lcov-report diff --git a/Makefile b/Makefile index bc94594..94a95f8 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,10 @@ redis-cli: test: # Run test @${NPM_RUN} test +.PHONY: coverage +coverage: # Run test coverage + @${NPM_RUN} test:coverage + # # Production environment # diff --git a/__tests__/idea/domain/Aggregate.test.ts b/__tests__/idea/domain/Aggregate.test.ts new file mode 100644 index 0000000..d9cc8bb --- /dev/null +++ b/__tests__/idea/domain/Aggregate.test.ts @@ -0,0 +1,660 @@ +import { Idea } from '@/idea/domain/Aggregate' +import { Competitor } from '@/idea/domain/Competitor' +import { CompetitorAnalysis } from '@/idea/domain/CompetitorAnalysis' +import { ContentIdeasForMarketing } from '@/idea/domain/ContentIdeasForMarketing' +import { ElevatorPitch } from '@/idea/domain/ElevatorPitch' +import { GoogleTrendsKeyword } from '@/idea/domain/GoogleTrendsKeyword' +import { MarketAnalysis } from '@/idea/domain/MarketAnalysis' +import { ProductName } from '@/idea/domain/ProductName' +import { SWOTAnalysis } from '@/idea/domain/SWOTAnalysis' +import { SocialMediaCampaigns } from '@/idea/domain/SocialMediaCampaigns' +import { TargetAudience } from '@/idea/domain/TargetAudience' +import { ValueProposition } from '@/idea/domain/ValueProposition' +import { Identity } from '@/shared/Identity' + +describe('Idea Class', () => { + let validId: string + let validConceptId: string + let validProblem: string + let validMarketExistence: string + let validTargetAudiences: TargetAudience[] + + let validTargetAudience: TargetAudience + + beforeEach(() => { + validId = Identity.Generate().getValue() + validConceptId = Identity.Generate().getValue() + validProblem = 'This is a valid problem statement.' + validMarketExistence = 'The market exists because...' + + validTargetAudience = TargetAudience.New( + Identity.Generate().getValue(), + Identity.Generate().getValue(), + 'Developers', + 'Description', + ['Challenge 1'] + ) + validTargetAudience.setWhy( + 'Developers need tools to increase productivity.' + ) + validTargetAudience.setPainPoints(['Time-consuming tasks', 'Manual errors']) + validTargetAudience.setTargetingStrategy('Offer automation tools.') + + validTargetAudiences = [validTargetAudience] + }) + + describe('Creation of Idea', () => { + it('should create an Idea instance with valid inputs', () => { + const idea = Idea.New( + validId, + validConceptId, + validProblem, + validMarketExistence, + validTargetAudiences + ) + expect(idea).toBeInstanceOf(Idea) + expect(idea.getId().getValue()).toBe(validId) + expect(idea.getConceptId().getValue()).toBe(validConceptId) + expect(idea.getProblem().getValue()).toBe(validProblem) + expect(idea.getMarketExistence()).toBe(validMarketExistence) + expect(idea.getTargetAudiences()).toEqual(validTargetAudiences) + }) + }) + + describe('Validations in Idea.New()', () => { + it('should throw an error when problem is empty', () => { + expect(() => { + Idea.New( + validId, + validConceptId, + ' ', + validMarketExistence, + validTargetAudiences + ) + }).toThrow('Problem cannot be empty') + }) + + it('should throw an error when market existence is empty', () => { + expect(() => { + Idea.New( + validId, + validConceptId, + validProblem, + ' ', + validTargetAudiences + ) + }).toThrow('Market existence cannot be empty') + }) + + it('should throw an error when targetAudiences is empty', () => { + expect(() => { + Idea.New( + validId, + validConceptId, + validProblem, + validMarketExistence, + [] + ) + }).toThrow('Target audiences cannot be empty') + }) + }) + + describe('updateTargetAudience', () => { + let idea: Idea + let updatedAudience: TargetAudience + + beforeEach(() => { + idea = Idea.New( + validId, + validConceptId, + validProblem, + validMarketExistence, + validTargetAudiences + ) + + updatedAudience = TargetAudience.New( + validTargetAudience.getId().getValue(), + idea.getId().getValue(), + 'Developers', + 'Description', + ['Challenge 1'] + ) + updatedAudience.setWhy('Updated why') + updatedAudience.setPainPoints(['Updated pain point']) + updatedAudience.setTargetingStrategy('Updated targeting strategy') + }) + + it('should update an existing target audience', () => { + idea.updateTargetAudience(updatedAudience) + const audiences = idea.getTargetAudiences() + expect(audiences[0].getPainPoints()).toEqual(['Updated pain point']) + expect(audiences[0].getWhy()).toBe('Updated why') + expect(audiences[0].getTargetingStrategy()).toBe( + 'Updated targeting strategy' + ) + }) + + it('should throw an error if target audience does not exist', () => { + const nonExistingAudience = TargetAudience.New( + Identity.Generate().getValue(), + idea.getId().getValue(), + 'Developers', + 'Description', + ['Challenge 1'] + ) + + nonExistingAudience.setWhy('Why') + nonExistingAudience.setPainPoints(['Pain point']) + nonExistingAudience.setTargetingStrategy('Strategy') + expect(() => { + idea.updateTargetAudience(nonExistingAudience) + }).toThrow( + `TargetAudience with ID ${nonExistingAudience.getId().getValue()} does not exist` + ) + }) + }) + + describe('setValueProposition', () => { + let idea: Idea + let valueProposition: ValueProposition + + beforeEach(() => { + idea = Idea.New( + validId, + validConceptId, + validProblem, + validMarketExistence, + validTargetAudiences + ) + valueProposition = ValueProposition.New( + 'Saves time', + 'Automates tasks', + 'Unique automation technology' + ) + }) + + it('should set value proposition when not already set', () => { + idea.setValueProposition(valueProposition) + expect(idea.getValueProposition()).toBe(valueProposition) + }) + + it('should throw an error when value proposition is already set', () => { + idea.setValueProposition(valueProposition) + expect(() => { + idea.setValueProposition(valueProposition) + }).toThrow('ValueProposition already set') + }) + }) + + describe('setMarketAnalysis', () => { + let idea: Idea + let marketAnalysis: MarketAnalysis + + beforeEach(() => { + idea = Idea.New( + validId, + validConceptId, + validProblem, + validMarketExistence, + validTargetAudiences + ) + marketAnalysis = MarketAnalysis.New( + 'Trends in the market', + 'User behaviors', + 'Market gaps', + 'Innovation opportunities', + 'Strategic direction' + ) + }) + + it('should set market analysis when not already set', () => { + idea.setMarketAnalysis(marketAnalysis) + expect(idea.getMarketAnalysis()).toBe(marketAnalysis) + }) + + it('should throw an error when market analysis is already set', () => { + idea.setMarketAnalysis(marketAnalysis) + expect(() => { + idea.setMarketAnalysis(marketAnalysis) + }).toThrow('MarketAnalysis already set') + }) + }) + + describe('setCompetitorAnalysis', () => { + let idea: Idea + let competitorAnalysis: CompetitorAnalysis + + beforeEach(() => { + idea = Idea.New( + validId, + validConceptId, + validProblem, + validMarketExistence, + validTargetAudiences + ) + + const competitor = Competitor.New( + 'Name', + 'Product Name', + 'https://example.com', + ['Feature 1'], + 'Value Proposition', + 'User Acquisition', + ['Strength 1'], + ['Weakness 1'], + 'Differentiation Opportunity' + ) + + competitorAnalysis = CompetitorAnalysis.New( + [competitor], + { + strengths: ['Our Strengths'], + weaknesses: ['Our Weaknesses'], + }, + ['Suggestion 1'] + ) + }) + + it('should set competitor analysis when not already set', () => { + idea.setCompetitorAnalysis(competitorAnalysis) + expect(idea.getCompetitorAnalysis()).toBe(competitorAnalysis) + }) + + it('should throw an error when competitor analysis is already set', () => { + idea.setCompetitorAnalysis(competitorAnalysis) + expect(() => { + idea.setCompetitorAnalysis(competitorAnalysis) + }).toThrow('CompetitorAnalysis already set') + }) + }) + + describe('setSWOTAnalysis', () => { + let idea: Idea + let swotAnalysis: SWOTAnalysis + + beforeEach(() => { + idea = Idea.New( + validId, + validConceptId, + validProblem, + validMarketExistence, + validTargetAudiences + ) + swotAnalysis = SWOTAnalysis.New( + ['Strength 1'], + ['Weakness 1'], + ['Opportunity 1'], + ['Threat 1'] + ) + }) + + it('should set SWOT analysis when not already set', () => { + idea.setSWOTAnalysis(swotAnalysis) + expect(idea.getSWOTAnalysis()).toBe(swotAnalysis) + }) + + it('should throw an error when SWOT analysis is already set', () => { + idea.setSWOTAnalysis(swotAnalysis) + expect(() => { + idea.setSWOTAnalysis(swotAnalysis) + }).toThrow('SWOTAnalysis already set') + }) + }) + + describe('setContentIdeasForMarketing', () => { + let idea: Idea + let contentIdeas: ContentIdeasForMarketing + + beforeEach(() => { + idea = Idea.New( + validId, + validConceptId, + validProblem, + validMarketExistence, + validTargetAudiences + ) + contentIdeas = ContentIdeasForMarketing.New() + // Assume we have a valid ContentIdea instance to add + // contentIdeas.addContentIdea(validContentIdea) + }) + + it('should set content ideas for marketing when not already set', () => { + idea.setContentIdeasForMarketing(contentIdeas) + expect(idea.getContentIdeasForMarketing()).toBe(contentIdeas) + }) + + it('should throw an error when content ideas for marketing is already set', () => { + idea.setContentIdeasForMarketing(contentIdeas) + expect(() => { + idea.setContentIdeasForMarketing(contentIdeas) + }).toThrow('ContentIdeasForMarketing already set') + }) + }) + + describe('setSocialMediaCampaigns', () => { + let idea: Idea + let socialMediaCampaigns: SocialMediaCampaigns + + beforeEach(() => { + idea = Idea.New( + validId, + validConceptId, + validProblem, + validMarketExistence, + validTargetAudiences + ) + socialMediaCampaigns = SocialMediaCampaigns.New() + // Assume we have valid content to add to socialMediaCampaigns + }) + + it('should set social media campaigns when not already set', () => { + idea.setSocialMediaCampaigns(socialMediaCampaigns) + expect(idea.getSocialMediaCampaigns()).toBe(socialMediaCampaigns) + }) + + it('should throw an error when social media campaigns is already set', () => { + idea.setSocialMediaCampaigns(socialMediaCampaigns) + expect(() => { + idea.setSocialMediaCampaigns(socialMediaCampaigns) + }).toThrow('SocialMediaCampaigns already set') + }) + }) + + describe('addElevatorPitch', () => { + let idea: Idea + let elevatorPitch: ElevatorPitch + + beforeEach(() => { + idea = Idea.New( + validId, + validConceptId, + validProblem, + validMarketExistence, + validTargetAudiences + ) + elevatorPitch = ElevatorPitch.New( + 'This is a hook', + 'This is a problem', + 'This is a solution', + 'This is a value', + 'This is a CTA' + ) + }) + + it('should add an elevator pitch when it does not exist', () => { + idea.addElevatorPitch(elevatorPitch) + expect(idea.getElevatorPitches()).toEqual([elevatorPitch]) + }) + + it('should throw an error when elevator pitch already exists', () => { + idea.addElevatorPitch(elevatorPitch) + expect(() => { + idea.addElevatorPitch(elevatorPitch) + }).toThrow('ElevatorPitch already exists') + }) + }) + + describe('addGoogleTrendsKeyword', () => { + let idea: Idea + let googleTrendsKeyword: GoogleTrendsKeyword + + beforeEach(() => { + idea = Idea.New( + validId, + validConceptId, + validProblem, + validMarketExistence, + validTargetAudiences + ) + googleTrendsKeyword = GoogleTrendsKeyword.New('Keyword') + }) + + it('should add a Google Trends keyword when it does not exist', () => { + idea.addGoogleTrendsKeyword(googleTrendsKeyword) + expect(idea.getGoogleTrendsKeywords()).toEqual([googleTrendsKeyword]) + }) + + it('should throw an error when Google Trends keyword already exists', () => { + idea.addGoogleTrendsKeyword(googleTrendsKeyword) + expect(() => { + idea.addGoogleTrendsKeyword(googleTrendsKeyword) + }).toThrow('GoogleTrendsKeyword already exists') + }) + }) + + describe('addProductName', () => { + let idea: Idea + let productName: ProductName + + beforeEach(() => { + idea = Idea.New( + validId, + validConceptId, + validProblem, + validMarketExistence, + validTargetAudiences + ) + productName = ProductName.New( + 'ProductX', + ['productx.com'], + 'Because it simplifies tasks', + 'Simplify Your Work', + 'Helps users automate processes', + ['ProductY', 'ProductZ'], + 'Strong branding potential' + ) + }) + + it('should add a product name when it does not exist', () => { + idea.addProductName(productName) + expect(idea.getProductNames()).toEqual([productName]) + }) + + it('should throw an error when product name already exists', () => { + idea.addProductName(productName) + expect(() => { + idea.addProductName(productName) + }).toThrow('ProductName already exists') + }) + }) + + describe('finalizeMigration', () => { + let idea: Idea + + beforeEach(() => { + idea = Idea.New( + validId, + validConceptId, + validProblem, + validMarketExistence, + validTargetAudiences + ) + }) + + it('should finalize migration when not already migrated', () => { + idea.finalizeMigration() + expect(idea.isMigrated()).toBeTrue() + }) + + it('should throw an error when already migrated', () => { + idea.finalizeMigration() + expect(() => { + idea.finalizeMigration() + }).toThrow('Idea was already migrated') + }) + }) + + describe('archive', () => { + let idea: Idea + + beforeEach(() => { + idea = Idea.New( + validId, + validConceptId, + validProblem, + validMarketExistence, + validTargetAudiences + ) + }) + + it('should archive the idea when not already archived', () => { + idea.archive() + expect(idea.isArchived()).toBeTrue() + }) + + it('should throw an error when already archived', () => { + idea.archive() + expect(() => { + idea.archive() + }).toThrow('Idea was already archived') + }) + }) + + describe('Getter Methods', () => { + let idea: Idea + + beforeEach(() => { + idea = Idea.New( + validId, + validConceptId, + validProblem, + validMarketExistence, + validTargetAudiences + ) + }) + + it('should return the correct id', () => { + expect(idea.getId().getValue()).toBe(validId) + }) + + it('should return the correct conceptId', () => { + expect(idea.getConceptId().getValue()).toBe(validConceptId) + }) + + it('should return the correct problem', () => { + expect(idea.getProblem().getValue()).toBe(validProblem) + }) + + it('should return the correct marketExistence', () => { + expect(idea.getMarketExistence()).toBe(validMarketExistence) + }) + + it('should return the correct targetAudiences', () => { + expect(idea.getTargetAudiences()).toEqual(validTargetAudiences) + }) + + it('should return the correct valueProposition', () => { + const valueProposition = ValueProposition.New( + 'Saves time', + 'Automates tasks', + 'Unique automation technology' + ) + idea.setValueProposition(valueProposition) + expect(idea.getValueProposition()).toBe(valueProposition) + }) + + it('should return the correct marketAnalysis', () => { + const marketAnalysis = MarketAnalysis.New( + 'Trends in the market', + 'User behaviors', + 'Market gaps', + 'Innovation opportunities', + 'Strategic direction' + ) + idea.setMarketAnalysis(marketAnalysis) + expect(idea.getMarketAnalysis()).toBe(marketAnalysis) + }) + + it('should return the correct competitorAnalysis', () => { + const competitor = Competitor.New( + 'Name', + 'Product Name', + 'https://example.com', + ['Feature 1'], + 'Value Proposition', + 'User Acquisition', + ['Strength 1'], + ['Weakness 1'], + 'Differentiation Opportunity' + ) + + const competitorAnalysis = CompetitorAnalysis.New( + [competitor], + { + strengths: ['Our Strengths'], + weaknesses: ['Our Weaknesses'], + }, + ['Suggestion 1'] + ) + idea.setCompetitorAnalysis(competitorAnalysis) + expect(idea.getCompetitorAnalysis()).toBe(competitorAnalysis) + }) + + it('should return the correct productNames', () => { + const productName = ProductName.New( + 'ProductX', + ['productx.com'], + 'Because it simplifies tasks', + 'Simplify Your Work', + 'Helps users automate processes', + ['ProductY', 'ProductZ'], + 'Strong branding potential' + ) + idea.addProductName(productName) + expect(idea.getProductNames()).toEqual([productName]) + }) + + it('should return the correct swotAnalysis', () => { + const swotAnalysis = SWOTAnalysis.New( + ['Strength 1'], + ['Weakness 1'], + ['Opportunity 1'], + ['Threat 1'] + ) + idea.setSWOTAnalysis(swotAnalysis) + expect(idea.getSWOTAnalysis()).toBe(swotAnalysis) + }) + + it('should return the correct elevatorPitches', () => { + const elevatorPitch = ElevatorPitch.New( + 'This is a hook', + 'This is a problem', + 'This is a solution', + 'This is a value', + 'This is a CTA' + ) + idea.addElevatorPitch(elevatorPitch) + expect(idea.getElevatorPitches()).toEqual([elevatorPitch]) + }) + + it('should return the correct googleTrendsKeywords', () => { + const googleTrendsKeyword = GoogleTrendsKeyword.New('Keyword') + idea.addGoogleTrendsKeyword(googleTrendsKeyword) + expect(idea.getGoogleTrendsKeywords()).toEqual([googleTrendsKeyword]) + }) + + it('should return the correct contentIdeasForMarketing', () => { + const contentIdeas = ContentIdeasForMarketing.New() + idea.setContentIdeasForMarketing(contentIdeas) + expect(idea.getContentIdeasForMarketing()).toBe(contentIdeas) + }) + + it('should return the correct socialMediaCampaigns', () => { + const socialMediaCampaigns = SocialMediaCampaigns.New() + idea.setSocialMediaCampaigns(socialMediaCampaigns) + expect(idea.getSocialMediaCampaigns()).toBe(socialMediaCampaigns) + }) + + it('should return the correct migrated status', () => { + expect(idea.isMigrated()).toBeFalse() + idea.finalizeMigration() + expect(idea.isMigrated()).toBeTrue() + }) + + it('should return the correct archived status', () => { + expect(idea.isArchived()).toBeFalse() + idea.archive() + expect(idea.isArchived()).toBeTrue() + }) + }) +}) diff --git a/__tests__/idea/domain/Competitor.test.ts b/__tests__/idea/domain/Competitor.test.ts new file mode 100644 index 0000000..1f6416b --- /dev/null +++ b/__tests__/idea/domain/Competitor.test.ts @@ -0,0 +1,683 @@ +import { Competitor } from '@/idea/domain/Competitor' + +describe('Competitor Class', () => { + const validName = 'Competitor A' + const validProductName = 'Product A' + const validUrl = 'https://competitor-a.com' + const validCoreFeatures = ['Feature 1', 'Feature 2'] + const validValueProposition = 'Offers affordable solutions' + const validUserAcquisition = 'Online marketing and SEO' + const validStrengths = ['Strong brand presence'] + const validWeaknesses = ['Limited customer support'] + const validDifferentiationOpportunity = 'Better customer service' + + describe('Successful Creation', () => { + it('should create a Competitor instance with valid inputs', () => { + const competitor = Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + + expect(competitor).toBeInstanceOf(Competitor) + expect(competitor.getName()).toBe(validName) + expect(competitor.getProductName()).toBe(validProductName) + expect(competitor.getUrl()).toBe(validUrl) + expect(competitor.getCoreFeatures()).toEqual(validCoreFeatures) + expect(competitor.getValueProposition()).toBe(validValueProposition) + expect(competitor.getUserAcquisition()).toBe(validUserAcquisition) + expect(competitor.getStrengths()).toEqual(validStrengths) + expect(competitor.getWeaknesses()).toEqual(validWeaknesses) + expect(competitor.getDifferentiationOpportunity()).toBe( + validDifferentiationOpportunity + ) + }) + }) + + describe('Validation Errors', () => { + describe('Name Property', () => { + it('should throw an error when name is empty', () => { + expect(() => + Competitor.New( + '', + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Competitor name cannot be empty') + }) + + it('should throw an error when name is whitespace', () => { + expect(() => + Competitor.New( + ' ', + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Competitor name cannot be empty') + }) + + it('should throw an error when name is null or undefined', () => { + expect(() => + Competitor.New( + null as unknown as string, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Competitor name cannot be empty') + + expect(() => + Competitor.New( + undefined as unknown as string, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Competitor name cannot be empty') + }) + }) + + describe('ProductName Property', () => { + it('should throw an error when productName is empty', () => { + expect(() => + Competitor.New( + validName, + '', + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Product name cannot be empty') + }) + + it('should throw an error when productName is whitespace', () => { + expect(() => + Competitor.New( + validName, + ' ', + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Product name cannot be empty') + }) + + it('should throw an error when productName is null or undefined', () => { + expect(() => + Competitor.New( + validName, + null as unknown as string, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Product name cannot be empty') + + expect(() => + Competitor.New( + validName, + undefined as unknown as string, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Product name cannot be empty') + }) + }) + + describe('URL Property', () => { + it('should throw an error when url is empty', () => { + expect(() => + Competitor.New( + validName, + validProductName, + '', + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('URL cannot be empty') + }) + + it('should throw an error when url is whitespace', () => { + expect(() => + Competitor.New( + validName, + validProductName, + ' ', + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('URL cannot be empty') + }) + + it('should throw an error when url is null or undefined', () => { + expect(() => + Competitor.New( + validName, + validProductName, + null as unknown as string, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('URL cannot be empty') + + expect(() => + Competitor.New( + validName, + validProductName, + undefined as unknown as string, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('URL cannot be empty') + }) + + it('should throw an error when url is not valid', () => { + expect(() => + Competitor.New( + validName, + validProductName, + 'domain.tld', + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('URL is not valid') + }) + }) + + describe('CoreFeatures Property', () => { + it('should throw an error when coreFeatures is empty array', () => { + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + [], + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Core features cannot be empty') + }) + + it('should throw an error when coreFeatures is null or undefined', () => { + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + null as unknown as string[], + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Core features cannot be empty') + + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + undefined as unknown as string[], + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Core features cannot be empty') + }) + }) + + describe('ValueProposition Property', () => { + it('should throw an error when valueProposition is empty', () => { + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + '', + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Value proposition cannot be empty') + }) + + it('should throw an error when valueProposition is whitespace', () => { + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + ' ', + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Value proposition cannot be empty') + }) + + it('should throw an error when valueProposition is null or undefined', () => { + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + null as unknown as string, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Value proposition cannot be empty') + + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + undefined as unknown as string, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Value proposition cannot be empty') + }) + }) + + describe('UserAcquisition Property', () => { + it('should throw an error when userAcquisition is empty', () => { + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + '', + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('User acquisition cannot be empty') + }) + + it('should throw an error when userAcquisition is whitespace', () => { + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + ' ', + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('User acquisition cannot be empty') + }) + + it('should throw an error when userAcquisition is null or undefined', () => { + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + null as unknown as string, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('User acquisition cannot be empty') + + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + undefined as unknown as string, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('User acquisition cannot be empty') + }) + }) + + describe('Strengths Property', () => { + it('should throw an error when strengths is empty array', () => { + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + [], + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Strengths cannot be empty') + }) + + it('should throw an error when strengths is null or undefined', () => { + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + null as unknown as string[], + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Strengths cannot be empty') + + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + undefined as unknown as string[], + validWeaknesses, + validDifferentiationOpportunity + ) + ).toThrow('Strengths cannot be empty') + }) + }) + + describe('Weaknesses Property', () => { + it('should throw an error when weaknesses is empty array', () => { + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + [], + validDifferentiationOpportunity + ) + ).toThrow('Weaknesses cannot be empty') + }) + + it('should throw an error when weaknesses is null or undefined', () => { + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + null as unknown as string[], + validDifferentiationOpportunity + ) + ).toThrow('Weaknesses cannot be empty') + + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + undefined as unknown as string[], + validDifferentiationOpportunity + ) + ).toThrow('Weaknesses cannot be empty') + }) + }) + + describe('DifferentiationOpportunity Property', () => { + it('should throw an error when differentiationOpportunity is empty', () => { + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + '' + ) + ).toThrow('Differentiation opportunity cannot be empty') + }) + + it('should throw an error when differentiationOpportunity is whitespace', () => { + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + ' ' + ) + ).toThrow('Differentiation opportunity cannot be empty') + }) + + it('should throw an error when differentiationOpportunity is null or undefined', () => { + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + null as unknown as string + ) + ).toThrow('Differentiation opportunity cannot be empty') + + expect(() => + Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + undefined as unknown as string + ) + ).toThrow('Differentiation opportunity cannot be empty') + }) + }) + }) + + describe('Getter Methods', () => { + let competitor: Competitor + + beforeEach(() => { + competitor = Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + }) + + it('should return the correct name', () => { + expect(competitor.getName()).toBe(validName) + }) + + it('should return the correct productName', () => { + expect(competitor.getProductName()).toBe(validProductName) + }) + + it('should return the correct url', () => { + expect(competitor.getUrl()).toBe(validUrl) + }) + + it('should return the correct coreFeatures', () => { + expect(competitor.getCoreFeatures()).toEqual(validCoreFeatures) + }) + + it('should return the correct valueProposition', () => { + expect(competitor.getValueProposition()).toBe(validValueProposition) + }) + + it('should return the correct userAcquisition', () => { + expect(competitor.getUserAcquisition()).toBe(validUserAcquisition) + }) + + it('should return the correct strengths', () => { + expect(competitor.getStrengths()).toEqual(validStrengths) + }) + + it('should return the correct weaknesses', () => { + expect(competitor.getWeaknesses()).toEqual(validWeaknesses) + }) + + it('should return the correct differentiationOpportunity', () => { + expect(competitor.getDifferentiationOpportunity()).toBe( + validDifferentiationOpportunity + ) + }) + }) + + describe('Array Mutability', () => { + let competitor: Competitor + + beforeEach(() => { + competitor = Competitor.New( + validName, + validProductName, + validUrl, + validCoreFeatures, + validValueProposition, + validUserAcquisition, + validStrengths, + validWeaknesses, + validDifferentiationOpportunity + ) + }) + + it('should not affect internal coreFeatures when modifying the returned array', () => { + const coreFeatures = competitor.getCoreFeatures() + coreFeatures.push('New Feature') + + expect(competitor.getCoreFeatures()).toEqual(validCoreFeatures) + }) + + it('should not affect internal strengths when modifying the returned array', () => { + const strengths = competitor.getStrengths() + strengths.push('New Strength') + + expect(competitor.getStrengths()).toEqual(validStrengths) + }) + + it('should not affect internal weaknesses when modifying the returned array', () => { + const weaknesses = competitor.getWeaknesses() + weaknesses.push('New Weakness') + + expect(competitor.getWeaknesses()).toEqual(validWeaknesses) + }) + }) +}) diff --git a/__tests__/idea/domain/CompetitorAnalysis.test.ts b/__tests__/idea/domain/CompetitorAnalysis.test.ts new file mode 100644 index 0000000..0d8170b --- /dev/null +++ b/__tests__/idea/domain/CompetitorAnalysis.test.ts @@ -0,0 +1,180 @@ +import { Competitor } from '@/idea/domain/Competitor' +import { CompetitorAnalysis } from '@/idea/domain/CompetitorAnalysis' + +describe('CompetitorAnalysis Class', () => { + let validCompetitor: Competitor + let validCompetitors: Competitor[] + let validComparison: { strengths: string[]; weaknesses: string[] } + let validDifferentiationSuggestions: string[] + + beforeEach(() => { + validCompetitor = Competitor.New( + 'Competitor A', + 'Product A', + 'https://competitor-a.com', + ['Feature 1', 'Feature 2'], + 'Value Proposition A', + 'User Acquisition A', + ['Strength 1'], + ['Weakness 1'], + 'Differentiation Opportunity A' + ) + + validCompetitors = [validCompetitor] + + validComparison = { + strengths: ['Strength A'], + weaknesses: ['Weakness A'], + } + + validDifferentiationSuggestions = ['Suggestion 1', 'Suggestion 2'] + }) + + describe('Successful Creation', () => { + it('should create a CompetitorAnalysis with valid inputs', () => { + const analysis = CompetitorAnalysis.New( + validCompetitors, + validComparison, + validDifferentiationSuggestions + ) + + expect(analysis).toBeInstanceOf(CompetitorAnalysis) + }) + }) + + describe('Validation Errors', () => { + it('should throw an error when competitors array is empty', () => { + expect(() => + CompetitorAnalysis.New( + [], + validComparison, + validDifferentiationSuggestions + ) + ).toThrow('Competitors cannot be empty') + }) + + it('should throw an error when competitors contain invalid instances', () => { + const invalidCompetitors = [{} as Competitor] + expect(() => + CompetitorAnalysis.New( + invalidCompetitors, + validComparison, + validDifferentiationSuggestions + ) + ).toThrow('Competitor at index 0 is invalid') + }) + + it('should throw an error when comparison is null or undefined', () => { + expect(() => + CompetitorAnalysis.New( + validCompetitors, + null as unknown as { strengths: string[]; weaknesses: string[] }, + validDifferentiationSuggestions + ) + ).toThrow('Comparison cannot be null or undefined') + + expect(() => + CompetitorAnalysis.New( + validCompetitors, + undefined as unknown as { strengths: string[]; weaknesses: string[] }, + validDifferentiationSuggestions + ) + ).toThrow('Comparison cannot be null or undefined') + }) + + it('should throw an error when comparison strengths are empty', () => { + const invalidComparison = { strengths: [], weaknesses: ['Weakness A'] } + expect(() => + CompetitorAnalysis.New( + validCompetitors, + invalidComparison, + validDifferentiationSuggestions + ) + ).toThrow('Comparison strengths cannot be empty') + }) + + it('should throw an error when comparison weaknesses are empty', () => { + const invalidComparison = { strengths: ['Strength A'], weaknesses: [] } + expect(() => + CompetitorAnalysis.New( + validCompetitors, + invalidComparison, + validDifferentiationSuggestions + ) + ).toThrow('Comparison weaknesses cannot be empty') + }) + + it('should throw an error when differentiationSuggestions is not an array', () => { + expect(() => + CompetitorAnalysis.New( + validCompetitors, + validComparison, + null as unknown as string[] + ) + ).toThrow('Differentiation suggestions must be an array') + }) + }) + + describe('Getter Methods', () => { + let analysis: CompetitorAnalysis + + beforeEach(() => { + analysis = CompetitorAnalysis.New( + validCompetitors, + validComparison, + validDifferentiationSuggestions + ) + }) + + it('should return competitors via getCompetitors()', () => { + const competitors = analysis.getCompetitors() + expect(competitors).toEqual(validCompetitors) + }) + + it('should return comparison via getComparison()', () => { + const comparison = analysis.getComparison() + expect(comparison).toEqual(validComparison) + }) + + it('should return differentiationSuggestions via getDifferentiationSuggestions()', () => { + const suggestions = analysis.getDifferentiationSuggestions() + expect(suggestions).toEqual(validDifferentiationSuggestions) + }) + }) + + describe('Immutability', () => { + let analysis: CompetitorAnalysis + + beforeEach(() => { + analysis = CompetitorAnalysis.New( + validCompetitors, + validComparison, + validDifferentiationSuggestions + ) + }) + + it('should prevent modification of competitors array', () => { + const competitors = analysis.getCompetitors() + competitors.push(validCompetitor) + + expect(analysis.getCompetitors()).toEqual(validCompetitors) + }) + + it('should prevent modification of comparison strengths', () => { + const comparison = analysis.getComparison() + comparison.strengths.push('strength') + comparison.weaknesses.push('weakness') + + expect(analysis.getComparison()).toEqual(validComparison) + }) + + it('should prevent modification of differentiationSuggestions', () => { + const suggestions = analysis.getDifferentiationSuggestions() + suggestions.push('suggestion') + + expect(analysis.getDifferentiationSuggestions()).toEqual( + validDifferentiationSuggestions + ) + }) + }) +}) diff --git a/__tests__/idea/domain/ContentIdea.test.ts b/__tests__/idea/domain/ContentIdea.test.ts new file mode 100644 index 0000000..58a1e70 --- /dev/null +++ b/__tests__/idea/domain/ContentIdea.test.ts @@ -0,0 +1,200 @@ +import { ContentIdea } from '@/idea/domain/ContentIdea' +import { Strategy } from '@/idea/domain/Strategy' + +describe('ContentIdea Class', () => { + const validStrategy = Strategy.New('emailMarketing') + const validPlatforms = ['Email', 'Newsletter'] + const validIdeas = ['Idea 1', 'Idea 2'] + const validBenefits = ['Benefit 1', 'Benefit 2'] + + describe('Successful Creation', () => { + it('should create a ContentIdea instance with valid inputs', () => { + const contentIdea = ContentIdea.New( + validStrategy, + validPlatforms, + validIdeas, + validBenefits + ) + expect(contentIdea).toBeInstanceOf(ContentIdea) + expect(contentIdea.getStrategy()).toBe(validStrategy) + expect(contentIdea.getPlatforms()).toEqual(validPlatforms) + expect(contentIdea.getIdeas()).toEqual(validIdeas) + expect(contentIdea.getBenefits()).toEqual(validBenefits) + }) + }) + + describe('Validation Errors', () => { + describe('Strategy Parameter', () => { + it('should throw an error when strategy is null', () => { + expect(() => { + ContentIdea.New( + null as unknown as Strategy, + validPlatforms, + validIdeas, + validBenefits + ) + }).toThrow('Strategy cannot be null or undefined.') + }) + + it('should throw an error when strategy is undefined', () => { + expect(() => { + ContentIdea.New( + undefined as unknown as Strategy, + validPlatforms, + validIdeas, + validBenefits + ) + }).toThrow('Strategy cannot be null or undefined.') + }) + }) + + describe('Platforms Parameter', () => { + it('should throw an error when platforms is null or undefined', () => { + expect(() => { + ContentIdea.New( + validStrategy, + null as unknown as string[], + validIdeas, + validBenefits + ) + }).toThrow('Platforms cannot be empty.') + + expect(() => { + ContentIdea.New( + validStrategy, + undefined as unknown as string[], + validIdeas, + validBenefits + ) + }).toThrow('Platforms cannot be empty.') + }) + + it('should throw an error when platforms is an empty array', () => { + expect(() => { + ContentIdea.New(validStrategy, [], validIdeas, validBenefits) + }).toThrow('Platforms cannot be empty.') + }) + + it('should throw an error when platforms contains invalid elements', () => { + const invalidPlatforms = ['Valid Platform', '', ' ', null as any] + expect(() => { + ContentIdea.New( + validStrategy, + invalidPlatforms, + validIdeas, + validBenefits + ) + }).toThrow(/Platform at index \d+ must be a non-empty string./) + }) + }) + + describe('Ideas Parameter', () => { + it('should throw an error when ideas is null or undefined', () => { + expect(() => { + ContentIdea.New( + validStrategy, + validPlatforms, + null as unknown as string[], + validBenefits + ) + }).toThrow('Ideas cannot be empty.') + + expect(() => { + ContentIdea.New( + validStrategy, + validPlatforms, + undefined as unknown as string[], + validBenefits + ) + }).toThrow('Ideas cannot be empty.') + }) + + it('should throw an error when ideas is an empty array', () => { + expect(() => { + ContentIdea.New(validStrategy, validPlatforms, [], validBenefits) + }).toThrow('Ideas cannot be empty.') + }) + + it('should throw an error when ideas contains invalid elements', () => { + const invalidIdeas = ['Valid Idea', '', ' ', null as any] + expect(() => { + ContentIdea.New( + validStrategy, + validPlatforms, + invalidIdeas, + validBenefits + ) + }).toThrow(/Idea at index \d+ must be a non-empty string./) + }) + }) + + describe('Benefits Parameter', () => { + it('should throw an error when benefits is null or undefined', () => { + expect(() => { + ContentIdea.New( + validStrategy, + validPlatforms, + validIdeas, + null as unknown as string[] + ) + }).toThrow('Benefits cannot be empty.') + + expect(() => { + ContentIdea.New( + validStrategy, + validPlatforms, + validIdeas, + undefined as unknown as string[] + ) + }).toThrow('Benefits cannot be empty.') + }) + + it('should throw an error when benefits is an empty array', () => { + expect(() => { + ContentIdea.New(validStrategy, validPlatforms, validIdeas, []) + }).toThrow('Benefits cannot be empty.') + }) + + it('should throw an error when benefits contains invalid elements', () => { + const invalidBenefits = ['Valid Benefit', '', ' ', null as any] + expect(() => { + ContentIdea.New( + validStrategy, + validPlatforms, + validIdeas, + invalidBenefits + ) + }).toThrow(/Benefit at index \d+ must be a non-empty string./) + }) + }) + }) + + describe('Getter Methods', () => { + let contentIdea: ContentIdea + + beforeEach(() => { + contentIdea = ContentIdea.New( + validStrategy, + validPlatforms, + validIdeas, + validBenefits + ) + }) + + it('should return the correct strategy', () => { + expect(contentIdea.getStrategy()).toBe(validStrategy) + }) + + it('should return the correct platforms', () => { + expect(contentIdea.getPlatforms()).toEqual(validPlatforms) + }) + + it('should return the correct ideas', () => { + expect(contentIdea.getIdeas()).toEqual(validIdeas) + }) + + it('should return the correct benefits', () => { + expect(contentIdea.getBenefits()).toEqual(validBenefits) + }) + }) +}) diff --git a/__tests__/idea/domain/ContentIdeasForMarketing.test.ts b/__tests__/idea/domain/ContentIdeasForMarketing.test.ts new file mode 100644 index 0000000..13efed8 --- /dev/null +++ b/__tests__/idea/domain/ContentIdeasForMarketing.test.ts @@ -0,0 +1,91 @@ +import { ContentIdea } from '@/idea/domain/ContentIdea' +import { ContentIdeasForMarketing } from '@/idea/domain/ContentIdeasForMarketing' +import { Strategy } from '@/idea/domain/Strategy' + +describe('ContentIdeasForMarketing Class', () => { + const validStrategy = Strategy.New('emailMarketing') + const validPlatforms = ['Email', 'Newsletter'] + const validIdeas = ['Idea 1', 'Idea 2'] + const validBenefits = ['Benefit 1', 'Benefit 2'] + + const validContentIdea = ContentIdea.New( + validStrategy, + validPlatforms, + validIdeas, + validBenefits + ) + + describe('Successful Addition', () => { + let contentIdeasForMarketing: ContentIdeasForMarketing + + beforeEach(() => { + contentIdeasForMarketing = ContentIdeasForMarketing.New() + }) + + it('should add a valid ContentIdea', () => { + contentIdeasForMarketing.addContentIdea(validContentIdea) + expect(contentIdeasForMarketing.getContentIdeas()).toEqual([ + validContentIdea, + ]) + }) + }) + + describe('Validation Errors', () => { + let contentIdeasForMarketing: ContentIdeasForMarketing + + beforeEach(() => { + contentIdeasForMarketing = ContentIdeasForMarketing.New() + }) + + it('should throw an error when adding a null ContentIdea', () => { + expect(() => { + contentIdeasForMarketing.addContentIdea(null as unknown as ContentIdea) + }).toThrow('ContentIdea cannot be null or undefined') + }) + + it('should throw an error when adding an undefined ContentIdea', () => { + expect(() => { + contentIdeasForMarketing.addContentIdea( + undefined as unknown as ContentIdea + ) + }).toThrow('ContentIdea cannot be null or undefined') + }) + + it('should throw an error when adding an invalid ContentIdea instance', () => { + const invalidContentIdea = { + strategy: 'emailMarketing', // Should be a Strategy instance + platforms: validPlatforms, + ideas: validIdeas, + benefits: validBenefits, + } + expect(() => { + contentIdeasForMarketing.addContentIdea( + invalidContentIdea as unknown as ContentIdea + ) + }).toThrow('Invalid ContentIdea instance') + }) + }) + + describe('Getter Method', () => { + let contentIdeasForMarketing: ContentIdeasForMarketing + + beforeEach(() => { + contentIdeasForMarketing = ContentIdeasForMarketing.New() + contentIdeasForMarketing.addContentIdea(validContentIdea) + }) + + it('should return the correct contentIdeas array', () => { + expect(contentIdeasForMarketing.getContentIdeas()).toEqual([ + validContentIdea, + ]) + }) + + it('should return a copy of the contentIdeas array', () => { + const contentIdeas = contentIdeasForMarketing.getContentIdeas() + contentIdeas.push(validContentIdea) // Modify the returned array + expect(contentIdeasForMarketing.getContentIdeas()).toEqual([ + validContentIdea, + ]) // Original array should not be affected + }) + }) +}) diff --git a/__tests__/idea/domain/ElevatorPitch.test.ts b/__tests__/idea/domain/ElevatorPitch.test.ts new file mode 100644 index 0000000..2f3c691 --- /dev/null +++ b/__tests__/idea/domain/ElevatorPitch.test.ts @@ -0,0 +1,304 @@ +import { ElevatorPitch } from '@/idea/domain/ElevatorPitch' + +describe('ElevatorPitch Class', () => { + const validHook = 'Did you know that...' + const validProblem = 'Many people struggle with...' + const validSolution = 'Our product solves this by...' + const validValueProposition = 'We uniquely offer...' + const validCTA = 'Join us today to...' + + describe('Successful Creation', () => { + it('should create an ElevatorPitch instance with valid inputs', () => { + const pitch = ElevatorPitch.New( + validHook, + validProblem, + validSolution, + validValueProposition, + validCTA + ) + + expect(pitch).toBeInstanceOf(ElevatorPitch) + expect(pitch.getHook()).toBe(validHook) + expect(pitch.getProblem()).toBe(validProblem) + expect(pitch.getSolution()).toBe(validSolution) + expect(pitch.getValueProposition()).toBe(validValueProposition) + expect(pitch.getCTA()).toBe(validCTA) + }) + }) + + describe('Validation Errors', () => { + describe('Hook Property', () => { + it('should throw an error when hook is empty', () => { + expect(() => + ElevatorPitch.New( + '', + validProblem, + validSolution, + validValueProposition, + validCTA + ) + ).toThrow('Hook cannot be empty') + }) + + it('should throw an error when hook is whitespace', () => { + expect(() => + ElevatorPitch.New( + ' ', + validProblem, + validSolution, + validValueProposition, + validCTA + ) + ).toThrow('Hook cannot be empty') + }) + + it('should throw an error when hook is null or undefined', () => { + expect(() => + ElevatorPitch.New( + null as unknown as string, + validProblem, + validSolution, + validValueProposition, + validCTA + ) + ).toThrow('Hook cannot be empty') + + expect(() => + ElevatorPitch.New( + undefined as unknown as string, + validProblem, + validSolution, + validValueProposition, + validCTA + ) + ).toThrow('Hook cannot be empty') + }) + }) + + describe('Problem Property', () => { + it('should throw an error when problem is empty', () => { + expect(() => + ElevatorPitch.New( + validHook, + '', + validSolution, + validValueProposition, + validCTA + ) + ).toThrow('Problem cannot be empty') + }) + + it('should throw an error when problem is whitespace', () => { + expect(() => + ElevatorPitch.New( + validHook, + ' ', + validSolution, + validValueProposition, + validCTA + ) + ).toThrow('Problem cannot be empty') + }) + + it('should throw an error when problem is null or undefined', () => { + expect(() => + ElevatorPitch.New( + validHook, + null as unknown as string, + validSolution, + validValueProposition, + validCTA + ) + ).toThrow('Problem cannot be empty') + + expect(() => + ElevatorPitch.New( + validHook, + undefined as unknown as string, + validSolution, + validValueProposition, + validCTA + ) + ).toThrow('Problem cannot be empty') + }) + }) + + describe('Solution Property', () => { + it('should throw an error when solution is empty', () => { + expect(() => + ElevatorPitch.New( + validHook, + validProblem, + '', + validValueProposition, + validCTA + ) + ).toThrow('Solution cannot be empty') + }) + + it('should throw an error when solution is whitespace', () => { + expect(() => + ElevatorPitch.New( + validHook, + validProblem, + ' ', + validValueProposition, + validCTA + ) + ).toThrow('Solution cannot be empty') + }) + + it('should throw an error when solution is null or undefined', () => { + expect(() => + ElevatorPitch.New( + validHook, + validProblem, + null as unknown as string, + validValueProposition, + validCTA + ) + ).toThrow('Solution cannot be empty') + + expect(() => + ElevatorPitch.New( + validHook, + validProblem, + undefined as unknown as string, + validValueProposition, + validCTA + ) + ).toThrow('Solution cannot be empty') + }) + }) + + describe('ValueProposition Property', () => { + it('should throw an error when valueProposition is empty', () => { + expect(() => + ElevatorPitch.New( + validHook, + validProblem, + validSolution, + '', + validCTA + ) + ).toThrow('Value proposition cannot be empty') + }) + + it('should throw an error when valueProposition is whitespace', () => { + expect(() => + ElevatorPitch.New( + validHook, + validProblem, + validSolution, + ' ', + validCTA + ) + ).toThrow('Value proposition cannot be empty') + }) + + it('should throw an error when valueProposition is null or undefined', () => { + expect(() => + ElevatorPitch.New( + validHook, + validProblem, + validSolution, + null as unknown as string, + validCTA + ) + ).toThrow('Value proposition cannot be empty') + + expect(() => + ElevatorPitch.New( + validHook, + validProblem, + validSolution, + undefined as unknown as string, + validCTA + ) + ).toThrow('Value proposition cannot be empty') + }) + }) + + describe('CTA Property', () => { + it('should throw an error when cta is empty', () => { + expect(() => + ElevatorPitch.New( + validHook, + validProblem, + validSolution, + validValueProposition, + '' + ) + ).toThrow('Call to action cannot be empty') + }) + + it('should throw an error when cta is whitespace', () => { + expect(() => + ElevatorPitch.New( + validHook, + validProblem, + validSolution, + validValueProposition, + ' ' + ) + ).toThrow('Call to action cannot be empty') + }) + + it('should throw an error when cta is null or undefined', () => { + expect(() => + ElevatorPitch.New( + validHook, + validProblem, + validSolution, + validValueProposition, + null as unknown as string + ) + ).toThrow('Call to action cannot be empty') + + expect(() => + ElevatorPitch.New( + validHook, + validProblem, + validSolution, + validValueProposition, + undefined as unknown as string + ) + ).toThrow('Call to action cannot be empty') + }) + }) + }) + + describe('Getter Methods', () => { + let pitch: ElevatorPitch + + beforeEach(() => { + pitch = ElevatorPitch.New( + validHook, + validProblem, + validSolution, + validValueProposition, + validCTA + ) + }) + + it('should return the correct hook', () => { + expect(pitch.getHook()).toBe(validHook) + }) + + it('should return the correct problem', () => { + expect(pitch.getProblem()).toBe(validProblem) + }) + + it('should return the correct solution', () => { + expect(pitch.getSolution()).toBe(validSolution) + }) + + it('should return the correct value proposition', () => { + expect(pitch.getValueProposition()).toBe(validValueProposition) + }) + + it('should return the correct CTA', () => { + expect(pitch.getCTA()).toBe(validCTA) + }) + }) +}) diff --git a/__tests__/idea/domain/GoogleTrendsKeyword.test.ts b/__tests__/idea/domain/GoogleTrendsKeyword.test.ts new file mode 100644 index 0000000..5d3ca78 --- /dev/null +++ b/__tests__/idea/domain/GoogleTrendsKeyword.test.ts @@ -0,0 +1,78 @@ +import { GoogleTrendsKeyword } from '@/idea/domain/GoogleTrendsKeyword' + +describe('GoogleTrendsKeyword Class', () => { + const validKeyword = 'Technology' + + describe('Successful Creation', () => { + it('should create a GoogleTrendsKeyword instance with a valid keyword', () => { + const keyword = GoogleTrendsKeyword.New(validKeyword) + expect(keyword).toBeInstanceOf(GoogleTrendsKeyword) + expect(keyword.getKeyword()).toBe(validKeyword.trim()) + }) + + it('should create an instance when keyword is at minimum length', () => { + const minKeyword = 'AI' + const keyword = GoogleTrendsKeyword.New(minKeyword) + expect(keyword.getKeyword()).toBe(minKeyword) + }) + + it('should create an instance when keyword is at maximum length', () => { + const maxKeyword = 'A'.repeat(100) + const keyword = GoogleTrendsKeyword.New(maxKeyword) + expect(keyword.getKeyword()).toBe(maxKeyword) + }) + }) + + describe('Validation Errors', () => { + it('should throw an error when keyword is null', () => { + expect(() => { + GoogleTrendsKeyword.New(null as unknown as string) + }).toThrow('Keyword must be a string.') + }) + + it('should throw an error when keyword is undefined', () => { + expect(() => { + GoogleTrendsKeyword.New(undefined as unknown as string) + }).toThrow('Keyword must be a string.') + }) + + it('should throw an error when keyword is not a string', () => { + expect(() => { + GoogleTrendsKeyword.New(123 as unknown as string) + }).toThrow('Keyword must be a string.') + }) + + it('should throw an error when keyword is an empty string', () => { + expect(() => { + GoogleTrendsKeyword.New('') + }).toThrow('Keyword cannot be empty.') + }) + + it('should throw an error when keyword is whitespace only', () => { + expect(() => { + GoogleTrendsKeyword.New(' ') + }).toThrow('Keyword cannot be empty.') + }) + + it('should throw an error when keyword is shorter than minimum length', () => { + const shortKeyword = 'A' + expect(() => { + GoogleTrendsKeyword.New(shortKeyword) + }).toThrow('Keyword must be between 2 and 100 characters.') + }) + + it('should throw an error when keyword is longer than maximum length', () => { + const longKeyword = 'A'.repeat(101) + expect(() => { + GoogleTrendsKeyword.New(longKeyword) + }).toThrow('Keyword must be between 2 and 100 characters.') + }) + }) + + describe('Getter Method', () => { + it('should return the correct keyword from getKeyword()', () => { + const keyword = GoogleTrendsKeyword.New(validKeyword) + expect(keyword.getKeyword()).toBe(validKeyword.trim()) + }) + }) +}) diff --git a/__tests__/idea/domain/MarketAnalysis.test.ts b/__tests__/idea/domain/MarketAnalysis.test.ts new file mode 100644 index 0000000..eb03dee --- /dev/null +++ b/__tests__/idea/domain/MarketAnalysis.test.ts @@ -0,0 +1,313 @@ +import { MarketAnalysis } from '@/idea/domain/MarketAnalysis' + +describe('MarketAnalysis Class', () => { + const validTrends = 'Increasing demand for eco-friendly products.' + const validUserBehaviors = 'Users prefer online shopping over in-store.' + const validMarketGaps = 'Lack of affordable options in the luxury segment.' + const validInnovationOpportunities = + 'Integration of AI for personalized experiences.' + const validStrategicDirection = 'Focus on mobile-first strategies.' + + describe('Successful Creation', () => { + it('should create a MarketAnalysis instance with valid inputs', () => { + const marketAnalysis = MarketAnalysis.New( + validTrends, + validUserBehaviors, + validMarketGaps, + validInnovationOpportunities, + validStrategicDirection + ) + + expect(marketAnalysis).toBeInstanceOf(MarketAnalysis) + expect(marketAnalysis.getTrends()).toBe(validTrends) + expect(marketAnalysis.getUserBehaviors()).toBe(validUserBehaviors) + expect(marketAnalysis.getMarketGaps()).toBe(validMarketGaps) + expect(marketAnalysis.getInnovationOpportunities()).toBe( + validInnovationOpportunities + ) + expect(marketAnalysis.getStrategicDirection()).toBe( + validStrategicDirection + ) + }) + }) + + describe('Validation Errors', () => { + describe('Trends Property', () => { + it('should throw an error when trends is empty', () => { + expect(() => + MarketAnalysis.New( + '', + validUserBehaviors, + validMarketGaps, + validInnovationOpportunities, + validStrategicDirection + ) + ).toThrow('Trends cannot be empty') + }) + + it('should throw an error when trends is whitespace', () => { + expect(() => + MarketAnalysis.New( + ' ', + validUserBehaviors, + validMarketGaps, + validInnovationOpportunities, + validStrategicDirection + ) + ).toThrow('Trends cannot be empty') + }) + + it('should throw an error when trends is null or undefined', () => { + expect(() => + MarketAnalysis.New( + null as unknown as string, + validUserBehaviors, + validMarketGaps, + validInnovationOpportunities, + validStrategicDirection + ) + ).toThrow('Trends cannot be empty') + + expect(() => + MarketAnalysis.New( + undefined as unknown as string, + validUserBehaviors, + validMarketGaps, + validInnovationOpportunities, + validStrategicDirection + ) + ).toThrow('Trends cannot be empty') + }) + }) + + describe('UserBehaviors Property', () => { + it('should throw an error when userBehaviors is empty', () => { + expect(() => + MarketAnalysis.New( + validTrends, + '', + validMarketGaps, + validInnovationOpportunities, + validStrategicDirection + ) + ).toThrow('User behaviors cannot be empty') + }) + + it('should throw an error when userBehaviors is whitespace', () => { + expect(() => + MarketAnalysis.New( + validTrends, + ' ', + validMarketGaps, + validInnovationOpportunities, + validStrategicDirection + ) + ).toThrow('User behaviors cannot be empty') + }) + + it('should throw an error when userBehaviors is null or undefined', () => { + expect(() => + MarketAnalysis.New( + validTrends, + null as unknown as string, + validMarketGaps, + validInnovationOpportunities, + validStrategicDirection + ) + ).toThrow('User behaviors cannot be empty') + + expect(() => + MarketAnalysis.New( + validTrends, + undefined as unknown as string, + validMarketGaps, + validInnovationOpportunities, + validStrategicDirection + ) + ).toThrow('User behaviors cannot be empty') + }) + }) + + describe('MarketGaps Property', () => { + it('should throw an error when marketGaps is empty', () => { + expect(() => + MarketAnalysis.New( + validTrends, + validUserBehaviors, + '', + validInnovationOpportunities, + validStrategicDirection + ) + ).toThrow('Market gaps cannot be empty') + }) + + it('should throw an error when marketGaps is whitespace', () => { + expect(() => + MarketAnalysis.New( + validTrends, + validUserBehaviors, + ' ', + validInnovationOpportunities, + validStrategicDirection + ) + ).toThrow('Market gaps cannot be empty') + }) + + it('should throw an error when marketGaps is null or undefined', () => { + expect(() => + MarketAnalysis.New( + validTrends, + validUserBehaviors, + null as unknown as string, + validInnovationOpportunities, + validStrategicDirection + ) + ).toThrow('Market gaps cannot be empty') + + expect(() => + MarketAnalysis.New( + validTrends, + validUserBehaviors, + undefined as unknown as string, + validInnovationOpportunities, + validStrategicDirection + ) + ).toThrow('Market gaps cannot be empty') + }) + }) + + describe('InnovationOpportunities Property', () => { + it('should throw an error when innovationOpportunities is empty', () => { + expect(() => + MarketAnalysis.New( + validTrends, + validUserBehaviors, + validMarketGaps, + '', + validStrategicDirection + ) + ).toThrow('Innovation opportunities cannot be empty') + }) + + it('should throw an error when innovationOpportunities is whitespace', () => { + expect(() => + MarketAnalysis.New( + validTrends, + validUserBehaviors, + validMarketGaps, + ' ', + validStrategicDirection + ) + ).toThrow('Innovation opportunities cannot be empty') + }) + + it('should throw an error when innovationOpportunities is null or undefined', () => { + expect(() => + MarketAnalysis.New( + validTrends, + validUserBehaviors, + validMarketGaps, + null as unknown as string, + validStrategicDirection + ) + ).toThrow('Innovation opportunities cannot be empty') + + expect(() => + MarketAnalysis.New( + validTrends, + validUserBehaviors, + validMarketGaps, + undefined as unknown as string, + validStrategicDirection + ) + ).toThrow('Innovation opportunities cannot be empty') + }) + }) + + describe('StrategicDirection Property', () => { + it('should throw an error when strategicDirection is empty', () => { + expect(() => + MarketAnalysis.New( + validTrends, + validUserBehaviors, + validMarketGaps, + validInnovationOpportunities, + '' + ) + ).toThrow('Strategic direction cannot be empty') + }) + + it('should throw an error when strategicDirection is whitespace', () => { + expect(() => + MarketAnalysis.New( + validTrends, + validUserBehaviors, + validMarketGaps, + validInnovationOpportunities, + ' ' + ) + ).toThrow('Strategic direction cannot be empty') + }) + + it('should throw an error when strategicDirection is null or undefined', () => { + expect(() => + MarketAnalysis.New( + validTrends, + validUserBehaviors, + validMarketGaps, + validInnovationOpportunities, + null as unknown as string + ) + ).toThrow('Strategic direction cannot be empty') + + expect(() => + MarketAnalysis.New( + validTrends, + validUserBehaviors, + validMarketGaps, + validInnovationOpportunities, + undefined as unknown as string + ) + ).toThrow('Strategic direction cannot be empty') + }) + }) + }) + + describe('Getter Methods', () => { + let marketAnalysis: MarketAnalysis + + beforeEach(() => { + marketAnalysis = MarketAnalysis.New( + validTrends, + validUserBehaviors, + validMarketGaps, + validInnovationOpportunities, + validStrategicDirection + ) + }) + + it('should return the correct trends', () => { + expect(marketAnalysis.getTrends()).toBe(validTrends) + }) + + it('should return the correct userBehaviors', () => { + expect(marketAnalysis.getUserBehaviors()).toBe(validUserBehaviors) + }) + + it('should return the correct marketGaps', () => { + expect(marketAnalysis.getMarketGaps()).toBe(validMarketGaps) + }) + + it('should return the correct innovationOpportunities', () => { + expect(marketAnalysis.getInnovationOpportunities()).toBe( + validInnovationOpportunities + ) + }) + + it('should return the correct strategicDirection', () => { + expect(marketAnalysis.getStrategicDirection()).toBe( + validStrategicDirection + ) + }) + }) +}) diff --git a/__tests__/idea/domain/Problem.test.ts b/__tests__/idea/domain/Problem.test.ts new file mode 100644 index 0000000..1dc15ad --- /dev/null +++ b/__tests__/idea/domain/Problem.test.ts @@ -0,0 +1,87 @@ +import { Problem } from '@/idea/domain/Problem' + +describe('Problem Class', () => { + const validValue = + 'This is a valid problem description that is more than 30 characters long.' + + describe('Successful Creation', () => { + it('should create a Problem instance with valid input', () => { + const problem = Problem.New(validValue) + expect(problem).toBeInstanceOf(Problem) + expect(problem.getValue()).toBe(validValue.trim()) + }) + + it('should create a Problem instance with exactly 30 characters', () => { + const value = 'A'.repeat(30) + const problem = Problem.New(value) + expect(problem).toBeInstanceOf(Problem) + expect(problem.getValue()).toBe(value) + }) + + it('should create a Problem instance with exactly 2048 characters', () => { + const value = 'A'.repeat(2048) + const problem = Problem.New(value) + expect(problem).toBeInstanceOf(Problem) + expect(problem.getValue()).toBe(value) + }) + }) + + describe('Validation Errors', () => { + it('should throw an error when value is null', () => { + expect(() => { + Problem.New(null as unknown as string) + }).toThrow('Problem must be a string.') + }) + + it('should throw an error when value is undefined', () => { + expect(() => { + Problem.New(undefined as unknown as string) + }).toThrow('Problem must be a string.') + }) + + it('should throw an error when value is a number', () => { + expect(() => { + Problem.New(12345 as unknown as string) + }).toThrow('Problem must be a string.') + }) + + it('should throw an error when value is an object', () => { + expect(() => { + Problem.New({} as unknown as string) + }).toThrow('Problem must be a string.') + }) + + it('should throw an error when value is an empty string', () => { + expect(() => { + Problem.New('') + }).toThrow('Problem cannot be empty.') + }) + + it('should throw an error when value is whitespace only', () => { + expect(() => { + Problem.New(' ') + }).toThrow('Problem cannot be empty.') + }) + + it('should throw an error when value is less than 30 characters', () => { + const value = 'A'.repeat(29) + expect(() => { + Problem.New(value) + }).toThrow('Problem must be between 30 and 2048 characters.') + }) + + it('should throw an error when value is greater than 2048 characters', () => { + const value = 'A'.repeat(2049) + expect(() => { + Problem.New(value) + }).toThrow('Problem must be between 30 and 2048 characters.') + }) + }) + + describe('Getter Method', () => { + it('should return the correct value from getValue()', () => { + const problem = Problem.New(validValue) + expect(problem.getValue()).toBe(validValue.trim()) + }) + }) +}) diff --git a/__tests__/idea/domain/ProductName.test.ts b/__tests__/idea/domain/ProductName.test.ts new file mode 100644 index 0000000..f03a5a5 --- /dev/null +++ b/__tests__/idea/domain/ProductName.test.ts @@ -0,0 +1,488 @@ +import { ProductName } from '@/idea/domain/ProductName' + +describe('ProductName Class', () => { + const validProductName = 'MyProduct' + const validDomains = ['myproduct.com', 'myproduct.io'] + const validWhy = 'Because it solves a big problem.' + const validTagline = 'The best product ever.' + const validTargetAudienceInsight = 'They need this product because...' + const validSimilarNames = ['ProductX', 'ProductY'] + const validBrandingPotential = 'High potential for branding.' + + describe('Successful Creation', () => { + it('should create a ProductName instance with valid inputs', () => { + const productName = ProductName.New( + validProductName, + validDomains, + validWhy, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + + expect(productName).toBeInstanceOf(ProductName) + expect(productName.getProductName()).toBe(validProductName) + expect(productName.getDomains()).toEqual(validDomains) + expect(productName.getWhy()).toBe(validWhy) + expect(productName.getTagline()).toBe(validTagline) + expect(productName.getTargetAudienceInsight()).toBe( + validTargetAudienceInsight + ) + expect(productName.getSimilarNames()).toEqual(validSimilarNames) + expect(productName.getBrandingPotential()).toBe(validBrandingPotential) + }) + }) + + describe('Validation Errors', () => { + describe('productName Property', () => { + it('should throw an error when productName is empty', () => { + expect(() => + ProductName.New( + '', + validDomains, + validWhy, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Product name cannot be empty') + }) + + it('should throw an error when productName is whitespace', () => { + expect(() => + ProductName.New( + ' ', + validDomains, + validWhy, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Product name cannot be empty') + }) + + it('should throw an error when productName is null or undefined', () => { + expect(() => + ProductName.New( + null as unknown as string, + validDomains, + validWhy, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Product name cannot be empty') + + expect(() => + ProductName.New( + undefined as unknown as string, + validDomains, + validWhy, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Product name cannot be empty') + }) + }) + + describe('domains Property', () => { + it('should throw an error when domains is empty array', () => { + expect(() => + ProductName.New( + validProductName, + [], + validWhy, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Domains cannot be empty') + }) + + it('should throw an error when domains is null or undefined', () => { + expect(() => + ProductName.New( + validProductName, + null as unknown as string[], + validWhy, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Domains cannot be empty') + + expect(() => + ProductName.New( + validProductName, + undefined as unknown as string[], + validWhy, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Domains cannot be empty') + }) + + it('should throw an error when domains contains invalid elements', () => { + const invalidDomains = [ + 'valid.com', + '', + ' ', + null as unknown as string, + ] + expect(() => + ProductName.New( + validProductName, + invalidDomains, + validWhy, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Each domain must be a non-empty string') + }) + }) + + describe('why Property', () => { + it('should throw an error when why is empty', () => { + expect(() => + ProductName.New( + validProductName, + validDomains, + '', + validTagline, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Why cannot be empty') + }) + + it('should throw an error when why is whitespace', () => { + expect(() => + ProductName.New( + validProductName, + validDomains, + ' ', + validTagline, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Why cannot be empty') + }) + + it('should throw an error when why is null or undefined', () => { + expect(() => + ProductName.New( + validProductName, + validDomains, + null as unknown as string, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Why cannot be empty') + + expect(() => + ProductName.New( + validProductName, + validDomains, + undefined as unknown as string, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Why cannot be empty') + }) + }) + + describe('tagline Property', () => { + it('should throw an error when tagline is empty', () => { + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + '', + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Tagline cannot be empty') + }) + + it('should throw an error when tagline is whitespace', () => { + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + ' ', + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Tagline cannot be empty') + }) + + it('should throw an error when tagline is null or undefined', () => { + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + null as unknown as string, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Tagline cannot be empty') + + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + undefined as unknown as string, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Tagline cannot be empty') + }) + }) + + describe('targetAudienceInsight Property', () => { + it('should throw an error when targetAudienceInsight is empty', () => { + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + validTagline, + '', + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Target audience insight cannot be empty') + }) + + it('should throw an error when targetAudienceInsight is whitespace', () => { + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + validTagline, + ' ', + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Target audience insight cannot be empty') + }) + + it('should throw an error when targetAudienceInsight is null or undefined', () => { + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + validTagline, + null as unknown as string, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Target audience insight cannot be empty') + + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + validTagline, + undefined as unknown as string, + validSimilarNames, + validBrandingPotential + ) + ).toThrow('Target audience insight cannot be empty') + }) + }) + + describe('similarNames Property', () => { + it('should throw an error when similarNames is empty array', () => { + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + validTagline, + validTargetAudienceInsight, + [], + validBrandingPotential + ) + ).toThrow('Similar names cannot be empty') + }) + + it('should throw an error when similarNames is null or undefined', () => { + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + validTagline, + validTargetAudienceInsight, + null as unknown as string[], + validBrandingPotential + ) + ).toThrow('Similar names cannot be empty') + + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + validTagline, + validTargetAudienceInsight, + undefined as unknown as string[], + validBrandingPotential + ) + ).toThrow('Similar names cannot be empty') + }) + + it('should throw an error when similarNames contains invalid elements', () => { + const invalidSimilarNames = [ + 'ValidName', + '', + ' ', + null as unknown as string, + ] + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + validTagline, + validTargetAudienceInsight, + invalidSimilarNames, + validBrandingPotential + ) + ).toThrow('Each similar name must be a non-empty string') + }) + }) + + describe('brandingPotential Property', () => { + it('should throw an error when brandingPotential is empty', () => { + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + '' + ) + ).toThrow('Branding potential cannot be empty') + }) + + it('should throw an error when brandingPotential is whitespace', () => { + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + ' ' + ) + ).toThrow('Branding potential cannot be empty') + }) + + it('should throw an error when brandingPotential is null or undefined', () => { + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + null as unknown as string + ) + ).toThrow('Branding potential cannot be empty') + + expect(() => + ProductName.New( + validProductName, + validDomains, + validWhy, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + undefined as unknown as string + ) + ).toThrow('Branding potential cannot be empty') + }) + }) + }) + + describe('Getter Methods', () => { + let productName: ProductName + + beforeEach(() => { + productName = ProductName.New( + validProductName, + validDomains, + validWhy, + validTagline, + validTargetAudienceInsight, + validSimilarNames, + validBrandingPotential + ) + }) + + it('should return the correct productName', () => { + expect(productName.getProductName()).toBe(validProductName) + }) + + it('should return the correct domains', () => { + expect(productName.getDomains()).toEqual(validDomains) + }) + + it('should return the correct why', () => { + expect(productName.getWhy()).toBe(validWhy) + }) + + it('should return the correct tagline', () => { + expect(productName.getTagline()).toBe(validTagline) + }) + + it('should return the correct targetAudienceInsight', () => { + expect(productName.getTargetAudienceInsight()).toBe( + validTargetAudienceInsight + ) + }) + + it('should return the correct similarNames', () => { + expect(productName.getSimilarNames()).toEqual(validSimilarNames) + }) + + it('should return the correct brandingPotential', () => { + expect(productName.getBrandingPotential()).toBe(validBrandingPotential) + }) + }) +}) diff --git a/__tests__/idea/domain/SWOTAnalysis.test.ts b/__tests__/idea/domain/SWOTAnalysis.test.ts new file mode 100644 index 0000000..9868970 --- /dev/null +++ b/__tests__/idea/domain/SWOTAnalysis.test.ts @@ -0,0 +1,226 @@ +import { SWOTAnalysis } from '@/idea/domain/SWOTAnalysis' + +describe('SWOTAnalysis Class', () => { + const validStrengths = ['Strong brand', 'High customer loyalty'] + const validWeaknesses = ['Limited distribution channels', 'High costs'] + const validOpportunities = ['Market expansion', 'New technology adoption'] + const validThreats = ['Competitors', 'Market saturation'] + + describe('Successful Creation', () => { + it('should create a SWOTAnalysis instance with valid inputs', () => { + const swot = SWOTAnalysis.New( + validStrengths, + validWeaknesses, + validOpportunities, + validThreats + ) + + expect(swot).toBeInstanceOf(SWOTAnalysis) + expect(swot.getStrengths()).toEqual(validStrengths) + expect(swot.getWeaknesses()).toEqual(validWeaknesses) + expect(swot.getOpportunities()).toEqual(validOpportunities) + expect(swot.getThreats()).toEqual(validThreats) + }) + }) + + describe('Validation Errors', () => { + describe('Strengths Property', () => { + it('should throw an error when strengths is empty array', () => { + expect(() => + SWOTAnalysis.New( + [], + validWeaknesses, + validOpportunities, + validThreats + ) + ).toThrow('Strengths cannot be empty') + }) + + it('should throw an error when strengths is null or undefined', () => { + expect(() => + SWOTAnalysis.New( + null as unknown as string[], + validWeaknesses, + validOpportunities, + validThreats + ) + ).toThrow('Strengths cannot be empty') + + expect(() => + SWOTAnalysis.New( + undefined as unknown as string[], + validWeaknesses, + validOpportunities, + validThreats + ) + ).toThrow('Strengths cannot be empty') + }) + + it('should throw an error when strengths contains invalid elements', () => { + const invalidStrengths = ['Valid Strength', '', ' ', null as any] + expect(() => + SWOTAnalysis.New( + invalidStrengths, + validWeaknesses, + validOpportunities, + validThreats + ) + ).toThrow(/Strength at index \d+ must be a non-empty string/) + }) + }) + + describe('Weaknesses Property', () => { + it('should throw an error when weaknesses is empty array', () => { + expect(() => + SWOTAnalysis.New(validStrengths, [], validOpportunities, validThreats) + ).toThrow('Weaknesses cannot be empty') + }) + + it('should throw an error when weaknesses is null or undefined', () => { + expect(() => + SWOTAnalysis.New( + validStrengths, + null as unknown as string[], + validOpportunities, + validThreats + ) + ).toThrow('Weaknesses cannot be empty') + + expect(() => + SWOTAnalysis.New( + validStrengths, + undefined as unknown as string[], + validOpportunities, + validThreats + ) + ).toThrow('Weaknesses cannot be empty') + }) + + it('should throw an error when weaknesses contains invalid elements', () => { + const invalidWeaknesses = ['Valid Weakness', '', ' ', 123 as any] + expect(() => + SWOTAnalysis.New( + validStrengths, + invalidWeaknesses, + validOpportunities, + validThreats + ) + ).toThrow(/Weakness at index \d+ must be a non-empty string/) + }) + }) + + describe('Opportunities Property', () => { + it('should throw an error when opportunities is empty array', () => { + expect(() => + SWOTAnalysis.New(validStrengths, validWeaknesses, [], validThreats) + ).toThrow('Opportunities cannot be empty') + }) + + it('should throw an error when opportunities is null or undefined', () => { + expect(() => + SWOTAnalysis.New( + validStrengths, + validWeaknesses, + null as unknown as string[], + validThreats + ) + ).toThrow('Opportunities cannot be empty') + + expect(() => + SWOTAnalysis.New( + validStrengths, + validWeaknesses, + undefined as unknown as string[], + validThreats + ) + ).toThrow('Opportunities cannot be empty') + }) + + it('should throw an error when opportunities contains invalid elements', () => { + const invalidOpportunities = ['Valid Opportunity', '', ' ', {} as any] + expect(() => + SWOTAnalysis.New( + validStrengths, + validWeaknesses, + invalidOpportunities, + validThreats + ) + ).toThrow(/Opportunity at index \d+ must be a non-empty string/) + }) + }) + + describe('Threats Property', () => { + it('should throw an error when threats is empty array', () => { + expect(() => + SWOTAnalysis.New( + validStrengths, + validWeaknesses, + validOpportunities, + [] + ) + ).toThrow('Threats cannot be empty') + }) + + it('should throw an error when threats is null or undefined', () => { + expect(() => + SWOTAnalysis.New( + validStrengths, + validWeaknesses, + validOpportunities, + null as unknown as string[] + ) + ).toThrow('Threats cannot be empty') + + expect(() => + SWOTAnalysis.New( + validStrengths, + validWeaknesses, + validOpportunities, + undefined as unknown as string[] + ) + ).toThrow('Threats cannot be empty') + }) + + it('should throw an error when threats contains invalid elements', () => { + const invalidThreats = ['Valid Threat', '', ' ', null as any] + expect(() => + SWOTAnalysis.New( + validStrengths, + validWeaknesses, + validOpportunities, + invalidThreats + ) + ).toThrow(/Threat at index \d+ must be a non-empty string/) + }) + }) + }) + + describe('Getter Methods', () => { + let swot: SWOTAnalysis + + beforeEach(() => { + swot = SWOTAnalysis.New( + validStrengths, + validWeaknesses, + validOpportunities, + validThreats + ) + }) + + it('should return the correct strengths', () => { + expect(swot.getStrengths()).toEqual(validStrengths) + }) + + it('should return the correct weaknesses', () => { + expect(swot.getWeaknesses()).toEqual(validWeaknesses) + }) + + it('should return the correct opportunities', () => { + expect(swot.getOpportunities()).toEqual(validOpportunities) + }) + + it('should return the correct threats', () => { + expect(swot.getThreats()).toEqual(validThreats) + }) + }) +}) diff --git a/__tests__/idea/domain/SocialMediaCampaigns.test.ts b/__tests__/idea/domain/SocialMediaCampaigns.test.ts new file mode 100644 index 0000000..e6601d9 --- /dev/null +++ b/__tests__/idea/domain/SocialMediaCampaigns.test.ts @@ -0,0 +1,165 @@ +import { SocialMediaCampaigns } from '@/idea/domain/SocialMediaCampaigns' + +describe('SocialMediaCampaigns Class', () => { + const validShortFormContent = { + header: 'Exciting News!', + platform: 'Twitter', + content: 'We are launching a new product.', + tips: ['Use hashtags', 'Include images'], + imagePrompt: 'A picture of our new product', + } + + const validLongFormContent = { + header: 'In-depth Analysis', + platform: 'LinkedIn', + title: 'The Future of Technology', + content: 'A detailed article about upcoming tech trends.', + tips: ['Include statistics', 'Add references'], + imagePrompt: 'An infographic about tech trends', + } + + const validVideoContent = { + header: 'Watch Now', + platform: 'YouTube', + title: 'Product Demo', + script: ['Intro', 'Features', 'Conclusion'], + tips: ['Keep it under 5 minutes', 'Use captions'], + imagePrompt: 'Thumbnail of the product', + } + + describe('Successful Addition', () => { + let campaigns: SocialMediaCampaigns + + beforeEach(() => { + campaigns = SocialMediaCampaigns.New() + }) + + it('should add valid ShortFormContent', () => { + campaigns.addShortFormContent(validShortFormContent) + expect(campaigns.getShortFormContents()).toEqual([validShortFormContent]) + }) + + it('should add valid LongFormContent', () => { + campaigns.addLongFormContent(validLongFormContent) + expect(campaigns.getLongFormContents()).toEqual([validLongFormContent]) + }) + + it('should add valid VideoContent', () => { + campaigns.addVideoContent(validVideoContent) + expect(campaigns.getVideoContents()).toEqual([validVideoContent]) + }) + }) + + describe('Validation Errors', () => { + let campaigns: SocialMediaCampaigns + + beforeEach(() => { + campaigns = SocialMediaCampaigns.New() + }) + + describe('addShortFormContent', () => { + it('should throw an error when ShortFormContent is null or undefined', () => { + expect(() => { + campaigns.addShortFormContent(null as any) + }).toThrow('ShortFormContent cannot be null or undefined') + + expect(() => { + campaigns.addShortFormContent(undefined as any) + }).toThrow('ShortFormContent cannot be null or undefined') + }) + + it('should throw an error when required string properties are invalid', () => { + const invalidContent = { ...validShortFormContent, header: '' } + expect(() => { + campaigns.addShortFormContent(invalidContent) + }).toThrow('header cannot be empty') + }) + + it('should throw an error when tips array is invalid', () => { + const invalidContent = { ...validShortFormContent, tips: [] } + expect(() => { + campaigns.addShortFormContent(invalidContent) + }).toThrow('tips cannot be empty') + }) + }) + + describe('addLongFormContent', () => { + it('should throw an error when LongFormContent is null or undefined', () => { + expect(() => { + campaigns.addLongFormContent(null as any) + }).toThrow('LongFormContent cannot be null or undefined') + + expect(() => { + campaigns.addLongFormContent(undefined as any) + }).toThrow('LongFormContent cannot be null or undefined') + }) + + it('should throw an error when required string properties are invalid', () => { + const invalidContent = { ...validLongFormContent, title: ' ' } + expect(() => { + campaigns.addLongFormContent(invalidContent) + }).toThrow('title cannot be empty') + }) + + it('should throw an error when tips array contains invalid elements', () => { + const invalidContent = { + ...validLongFormContent, + tips: ['Valid tip', '', null as any], + } + expect(() => { + campaigns.addLongFormContent(invalidContent) + }).toThrow(/tips at index \d+ must be a non-empty string/) + }) + }) + + describe('addVideoContent', () => { + it('should throw an error when VideoContent is null or undefined', () => { + expect(() => { + campaigns.addVideoContent(null as any) + }).toThrow('VideoContent cannot be null or undefined') + + expect(() => { + campaigns.addVideoContent(undefined as any) + }).toThrow('VideoContent cannot be null or undefined') + }) + + it('should throw an error when script array is invalid', () => { + const invalidContent = { ...validVideoContent, script: [] } + expect(() => { + campaigns.addVideoContent(invalidContent) + }).toThrow('script cannot be empty') + }) + + it('should throw an error when a required string property is missing', () => { + const invalidContent = { ...validVideoContent } + delete invalidContent.platform + expect(() => { + campaigns.addVideoContent(invalidContent as any) + }).toThrow('platform cannot be empty') + }) + }) + }) + + describe('Getter Methods', () => { + let campaigns: SocialMediaCampaigns + + beforeEach(() => { + campaigns = SocialMediaCampaigns.New() + campaigns.addShortFormContent(validShortFormContent) + campaigns.addLongFormContent(validLongFormContent) + campaigns.addVideoContent(validVideoContent) + }) + + it('should return the correct shortFormContents', () => { + expect(campaigns.getShortFormContents()).toEqual([validShortFormContent]) + }) + + it('should return the correct longFormContents', () => { + expect(campaigns.getLongFormContents()).toEqual([validLongFormContent]) + }) + + it('should return the correct videoContents', () => { + expect(campaigns.getVideoContents()).toEqual([validVideoContent]) + }) + }) +}) diff --git a/__tests__/idea/domain/Strategy.test.ts b/__tests__/idea/domain/Strategy.test.ts new file mode 100644 index 0000000..9067d21 --- /dev/null +++ b/__tests__/idea/domain/Strategy.test.ts @@ -0,0 +1,53 @@ +import { Strategy } from '@/idea/domain/Strategy' + +describe('Strategy Class', () => { + const validSections = [ + 'socialMediaCampaigns', + 'bloggingAndGuestPosts', + 'emailMarketing', + 'surveysAndPolls', + 'videoContent', + 'infographicsAndVisualContent', + 'communityEngagement', + 'paidAdvertising', + 'webinarsAndLiveStreams', + 'partnershipsAndCollaborations', + ] + + describe('Successful Creation', () => { + validSections.forEach((section) => { + it(`should create a Strategy instance with section '${section}'`, () => { + const strategy = Strategy.New(section) + expect(strategy).toBeInstanceOf(Strategy) + expect(strategy.getName()).toBe(section) + }) + }) + }) + + describe('Validation Errors', () => { + it('should throw an error when an invalid section name is provided', () => { + const invalidSection = 'invalidSection' + expect(() => { + Strategy.New(invalidSection) + }).toThrow(`Invalid section name: ${invalidSection}`) + }) + + it('should throw an error when a null or undefined section name is provided', () => { + expect(() => { + Strategy.New(null as unknown as string) + }).toThrow('Invalid section name: null') + + expect(() => { + Strategy.New(undefined as unknown as string) + }).toThrow('Invalid section name: undefined') + }) + }) + + describe('Getter Method', () => { + it('should return the correct section name from getName()', () => { + const section = 'emailMarketing' + const strategy = Strategy.New(section) + expect(strategy.getName()).toBe(section) + }) + }) +}) diff --git a/__tests__/idea/domain/TargetAudience.test.ts b/__tests__/idea/domain/TargetAudience.test.ts new file mode 100644 index 0000000..8b39a27 --- /dev/null +++ b/__tests__/idea/domain/TargetAudience.test.ts @@ -0,0 +1,112 @@ +import { v4 as uuidv4 } from 'uuid' +import { TargetAudience } from '@/idea/domain/TargetAudience' + +describe('TargetAudience Class', () => { + const validId = uuidv4() + const validIdeaId = uuidv4() + const validSegment = 'Developers' + const validDescription = 'Developers interested in AI tools' + const validChallenges = ['Time constraints', 'Resource limitations'] + + describe('Constructor', () => { + it('should create a TargetAudience with valid inputs', () => { + const audience = TargetAudience.New( + validId, + validIdeaId, + validSegment, + validDescription, + validChallenges + ) + + expect(audience.getId().getValue()).toBe(validId) + expect(audience.getIdeaId().getValue()).toBe(validIdeaId) + expect(audience.getSegment()).toBe(validSegment) + expect(audience.getDescription()).toBe(validDescription) + expect(audience.getChallenges()).toEqual(validChallenges) + }) + + it('should throw an error if segment is empty', () => { + expect(() => + TargetAudience.New( + validId, + validIdeaId, + ' ', + validDescription, + validChallenges + ) + ).toThrow('Segment cannot be empty') + }) + + it('should throw an error if description is empty', () => { + expect(() => + TargetAudience.New( + validId, + validIdeaId, + validSegment, + ' ', + validChallenges + ) + ).toThrow('Description cannot be empty') + }) + + it('should throw an error if challenges array is empty', () => { + expect(() => + TargetAudience.New( + validId, + validIdeaId, + validSegment, + validDescription, + [] + ) + ).toThrow('Challenges cannot be empty') + }) + }) + + describe('Setters', () => { + let audience: TargetAudience + + beforeEach(() => { + audience = TargetAudience.New( + validId, + validIdeaId, + validSegment, + validDescription, + validChallenges + ) + }) + + it('should set why with a valid string', () => { + const why = 'They need tools to improve productivity' + audience.setWhy(why) + expect(audience.getWhy()).toBe(why) + }) + + it('should throw an error when setting an empty why', () => { + expect(() => audience.setWhy(' ')).toThrow('Why cannot be empty') + }) + + it('should set painPoints with a valid array', () => { + const painPoints = ['Lack of time', 'High costs'] + audience.setPainPoints(painPoints) + expect(audience.getPainPoints()).toEqual(painPoints) + }) + + it('should throw an error when setting empty painPoints array', () => { + expect(() => audience.setPainPoints([])).toThrow( + 'Pain points cannot be empty' + ) + }) + + it('should set targetingStrategy with a valid string', () => { + const strategy = 'Social media marketing' + audience.setTargetingStrategy(strategy) + expect(audience.getTargetingStrategy()).toBe(strategy) + }) + + it('should throw an error when setting an empty targetingStrategy', () => { + expect(() => audience.setTargetingStrategy(' ')).toThrow( + 'Targeting strategy cannot be empty' + ) + }) + }) +}) diff --git a/__tests__/idea/domain/ValueProposition.test.ts b/__tests__/idea/domain/ValueProposition.test.ts new file mode 100644 index 0000000..626c696 --- /dev/null +++ b/__tests__/idea/domain/ValueProposition.test.ts @@ -0,0 +1,144 @@ +import { ValueProposition } from '@/idea/domain/ValueProposition' + +describe('ValueProposition Class', () => { + const validMainBenefit = 'Saves time for users' + const validProblemSolving = 'Automates repetitive tasks' + const validDifferentiation = 'Uses advanced AI algorithms' + + describe('Successful Creation', () => { + it('should create a ValueProposition instance with valid inputs', () => { + const valueProp = ValueProposition.New( + validMainBenefit, + validProblemSolving, + validDifferentiation + ) + + expect(valueProp).toBeInstanceOf(ValueProposition) + expect(valueProp.getMainBenefit()).toBe(validMainBenefit) + expect(valueProp.getProblemSolving()).toBe(validProblemSolving) + expect(valueProp.getDifferentiation()).toBe(validDifferentiation) + }) + }) + + describe('Validation Errors', () => { + describe('MainBenefit Property', () => { + it('should throw an error when mainBenefit is empty', () => { + expect(() => + ValueProposition.New('', validProblemSolving, validDifferentiation) + ).toThrow('Main benefit cannot be empty') + }) + + it('should throw an error when mainBenefit is whitespace', () => { + expect(() => + ValueProposition.New(' ', validProblemSolving, validDifferentiation) + ).toThrow('Main benefit cannot be empty') + }) + + it('should throw an error when mainBenefit is null or undefined', () => { + expect(() => + ValueProposition.New( + null as unknown as string, + validProblemSolving, + validDifferentiation + ) + ).toThrow('Main benefit cannot be empty') + + expect(() => + ValueProposition.New( + undefined as unknown as string, + validProblemSolving, + validDifferentiation + ) + ).toThrow('Main benefit cannot be empty') + }) + }) + + describe('ProblemSolving Property', () => { + it('should throw an error when problemSolving is empty', () => { + expect(() => + ValueProposition.New(validMainBenefit, '', validDifferentiation) + ).toThrow('Problem solving cannot be empty') + }) + + it('should throw an error when problemSolving is whitespace', () => { + expect(() => + ValueProposition.New(validMainBenefit, ' ', validDifferentiation) + ).toThrow('Problem solving cannot be empty') + }) + + it('should throw an error when problemSolving is null or undefined', () => { + expect(() => + ValueProposition.New( + validMainBenefit, + null as unknown as string, + validDifferentiation + ) + ).toThrow('Problem solving cannot be empty') + + expect(() => + ValueProposition.New( + validMainBenefit, + undefined as unknown as string, + validDifferentiation + ) + ).toThrow('Problem solving cannot be empty') + }) + }) + + describe('Differentiation Property', () => { + it('should throw an error when differentiation is empty', () => { + expect(() => + ValueProposition.New(validMainBenefit, validProblemSolving, '') + ).toThrow('Differentiation cannot be empty') + }) + + it('should throw an error when differentiation is whitespace', () => { + expect(() => + ValueProposition.New(validMainBenefit, validProblemSolving, ' ') + ).toThrow('Differentiation cannot be empty') + }) + + it('should throw an error when differentiation is null or undefined', () => { + expect(() => + ValueProposition.New( + validMainBenefit, + validProblemSolving, + null as unknown as string + ) + ).toThrow('Differentiation cannot be empty') + + expect(() => + ValueProposition.New( + validMainBenefit, + validProblemSolving, + undefined as unknown as string + ) + ).toThrow('Differentiation cannot be empty') + }) + }) + }) + + describe('Getter Methods', () => { + let valueProp: ValueProposition + + beforeEach(() => { + valueProp = ValueProposition.New( + validMainBenefit, + validProblemSolving, + validDifferentiation + ) + }) + + it('should return the correct mainBenefit', () => { + expect(valueProp.getMainBenefit()).toBe(validMainBenefit) + }) + + it('should return the correct problemSolving', () => { + expect(valueProp.getProblemSolving()).toBe(validProblemSolving) + }) + + it('should return the correct differentiation', () => { + expect(valueProp.getDifferentiation()).toBe(validDifferentiation) + }) + }) +}) diff --git a/__tests__/shared/Identity.test.ts b/__tests__/shared/Identity.test.ts new file mode 100644 index 0000000..780edae --- /dev/null +++ b/__tests__/shared/Identity.test.ts @@ -0,0 +1,68 @@ +import { validate as uuidValidate } from 'uuid' +import { Identity } from '@/shared/Identity' + +describe('Identity Class', () => { + describe('New Method', () => { + it('should create an Identity with a valid UUID string', () => { + const value = '123e4567-e89b-12d3-a456-426614174000' // Example UUID + const identity = Identity.New(value) + expect(identity).toBeInstanceOf(Identity) + expect(identity.getValue()).toBe(value) + }) + + it('should throw an error when value is not a valid UUID', () => { + expect(() => Identity.New('invalid-uuid')).toThrow( + 'Value must be a valid UUID' + ) + }) + + it('should throw an error when value is an empty string', () => { + expect(() => Identity.New('')).toThrow('Value cannot be empty') + }) + + it('should throw an error when value is a string with only whitespace', () => { + expect(() => Identity.New(' ')).toThrow('Value cannot be empty') + }) + + it('should throw an error when value is null', () => { + expect(() => Identity.New(null as unknown as string)).toThrow( + 'Value cannot be empty' + ) + }) + + it('should throw an error when value is undefined', () => { + expect(() => Identity.New(undefined as unknown as string)).toThrow( + 'Value cannot be empty' + ) + }) + }) + + describe('Generate Method', () => { + it('should generate a new Identity with a valid UUID', () => { + const identity = Identity.Generate() + expect(identity).toBeInstanceOf(Identity) + const value = identity.getValue() + expect(uuidValidate(value)).toBeTrue() + }) + + it('should generate unique UUIDs', () => { + const identity1 = Identity.Generate() + const identity2 = Identity.Generate() + expect(identity1.getValue()).not.toBe(identity2.getValue()) + }) + }) + + describe('getValue Method', () => { + it('should return the correct value after creation with New', () => { + const value = '123e4567-e89b-12d3-a456-426614174000' + const identity = Identity.New(value) + expect(identity.getValue()).toBe(value) + }) + + it('should return a valid UUID after creation with Generate', () => { + const identity = Identity.Generate() + const value = identity.getValue() + expect(uuidValidate(value)).toBeTrue() + }) + }) +}) diff --git a/package-lock.json b/package-lock.json index 5f80348..4ed436c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "next-themes": "^0.4.3", "openai": "^4.65.0", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "uuid": "^11.0.3" }, "devDependencies": { "@next/bundle-analyzer": "^14.2.5", @@ -27,6 +28,7 @@ "@types/node": "^22", "@types/react": "^18", "@types/react-dom": "^18", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^8.13.0", "@typescript-eslint/parser": "^8.13.0", "cross-env": "^7.0.3", @@ -3934,6 +3936,19 @@ "webpack": ">=4.40.0" } }, + "node_modules/@sentry/webpack-plugin/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -4295,6 +4310,13 @@ "@types/node": "*" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -12843,16 +12865,16 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { diff --git a/package.json b/package.json index 8fad8c1..4031f3f 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "lint": "npm run next-lint && npm run prettier && npm run knip", "prisma:generate_client": "prisma generate", "test": "dotenv -e .env.test -- npx prisma migrate deploy && jest --detectOpenHandles -i ./__tests__/", + "test:coverage": "dotenv -e .env.test -- npx prisma migrate deploy && jest --coverage --detectOpenHandles -i ./__tests__/", "db:migrate": "prisma migrate deploy" }, "dependencies": { @@ -31,7 +32,8 @@ "next-themes": "^0.4.3", "openai": "^4.65.0", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "uuid": "^11.0.3" }, "devDependencies": { "@next/bundle-analyzer": "^14.2.5", @@ -40,6 +42,7 @@ "@types/node": "^22", "@types/react": "^18", "@types/react-dom": "^18", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^8.13.0", "@typescript-eslint/parser": "^8.13.0", "cross-env": "^7.0.3", diff --git a/src/idea/adapters/IdeaRepositorySQLite.ts b/src/idea/adapters/IdeaRepositorySQLite.ts index a37acb2..920a99a 100644 --- a/src/idea/adapters/IdeaRepositorySQLite.ts +++ b/src/idea/adapters/IdeaRepositorySQLite.ts @@ -1,6 +1,7 @@ import { Idea } from '@/idea/domain/Aggregate' +import { Competitor } from '@/idea/domain/Competitor' import { CompetitorAnalysis } from '@/idea/domain/CompetitorAnalysis' -import { ContentIdea, Strategy } from '@/idea/domain/ContentIdea' +import { ContentIdea } from '@/idea/domain/ContentIdea' import { ContentIdeasForMarketing } from '@/idea/domain/ContentIdeasForMarketing' import { ElevatorPitch } from '@/idea/domain/ElevatorPitch' import { GoogleTrendsKeyword } from '@/idea/domain/GoogleTrendsKeyword' @@ -9,6 +10,7 @@ import { ProductName } from '@/idea/domain/ProductName' import { Repository } from '@/idea/domain/Repository' import { SWOTAnalysis } from '@/idea/domain/SWOTAnalysis' import { SocialMediaCampaigns } from '@/idea/domain/SocialMediaCampaigns' +import { Strategy } from '@/idea/domain/Strategy' import { TargetAudience } from '@/idea/domain/TargetAudience' import { ValueProposition } from '@/idea/domain/ValueProposition' import { prisma } from '@/lib/prisma' @@ -394,9 +396,23 @@ export class IdeaRepositorySQLite implements Repository { competitorAnalysisModel.value ) as competitorAnalysis + const competitors = data.competitors.map((c) => + Competitor.New( + c.name, + c.productName, + c.url, + c.coreFeatures, + c.valueProposition, + c.userAcquisition, + c.strengths, + c.weaknesses, + c.differentiationOpportunity + ) + ) + idea.setCompetitorAnalysis( CompetitorAnalysis.New( - data.competitors, + competitors, data.comparison, data.differentiationSuggestions ) diff --git a/src/idea/app/commands/MakeReservation.ts b/src/idea/app/commands/MakeReservation.ts index 4349fc2..f4079ce 100644 --- a/src/idea/app/commands/MakeReservation.ts +++ b/src/idea/app/commands/MakeReservation.ts @@ -1,10 +1,10 @@ -import { randomUUID } from 'crypto' import * as Sentry from '@sentry/nextjs' import { Idea } from '@/idea/domain/Aggregate' import { Repository } from '@/idea/domain/Repository' import { TargetAudience } from '@/idea/domain/TargetAudience' import { IdeaCreated } from '@/idea/domain/events/IdeaCreated' import { EventBus } from '@/idea/events/EventBus' +import { Identity } from '@/shared/Identity' interface ConceptForReservation { success: boolean @@ -62,7 +62,7 @@ export class MakeReservationHandler { const targetAudiences = concept.content.targetAudience.map( (targetAudience) => TargetAudience.New( - randomUUID(), + Identity.Generate().getValue(), command.ideaId, targetAudience.segment, targetAudience.description, diff --git a/src/idea/app/queries/GetIdea.ts b/src/idea/app/queries/GetIdea.ts index a92f1fb..491bbb0 100644 --- a/src/idea/app/queries/GetIdea.ts +++ b/src/idea/app/queries/GetIdea.ts @@ -140,7 +140,20 @@ export class GetIdeaHandler { : null, competitorAnalysis: competitorAnalysis ? { - competitors: competitorAnalysis.getCompetitors(), + competitors: competitorAnalysis + .getCompetitors() + .map((competitor) => ({ + name: competitor.getName(), + productName: competitor.getProductName(), + url: competitor.getUrl(), + coreFeatures: competitor.getCoreFeatures(), + valueProposition: competitor.getValueProposition(), + userAcquisition: competitor.getUserAcquisition(), + strengths: competitor.getStrengths(), + weaknesses: competitor.getWeaknesses(), + differentiationOpportunity: + competitor.getDifferentiationOpportunity(), + })), comparison: competitorAnalysis.getComparison(), differentiationSuggestions: competitorAnalysis.getDifferentiationSuggestions(), @@ -180,7 +193,7 @@ export class GetIdeaHandler { contentIdeasForMarketing: contentIdeasForMarketing ? contentIdeasForMarketing.getContentIdeas().reduce( (acc, contentIdea) => { - const section = contentIdea.getSection().getName() + const section = contentIdea.getStrategy().getName() acc[section] = { platforms: contentIdea.getPlatforms(), diff --git a/src/idea/domain/Aggregate.ts b/src/idea/domain/Aggregate.ts index 28d8421..9d4fcaf 100644 --- a/src/idea/domain/Aggregate.ts +++ b/src/idea/domain/Aggregate.ts @@ -12,12 +12,6 @@ import { ValueProposition } from '@/idea/domain/ValueProposition' import { Identity } from '@/shared/Identity' export class Idea { - private readonly id: Identity - private readonly conceptId: Identity - private readonly problem: Problem - private readonly marketExistence: string - private readonly targetAudiences: TargetAudience[] = [] - private valueProposition: ValueProposition | null = null private marketAnalysis: MarketAnalysis | null = null private competitorAnalysis: CompetitorAnalysis | null = null @@ -31,22 +25,12 @@ export class Idea { private archived: boolean = false private constructor( - id: Identity, - conceptId: Identity, - problem: Problem, - marketExistence: string, - targetAudiences: TargetAudience[] - ) { - if (targetAudiences.length === 0) { - throw new Error('Target audiences cannot be empty') - } - - this.id = id - this.conceptId = conceptId - this.problem = problem - this.marketExistence = marketExistence - this.targetAudiences = targetAudiences - } + private readonly id: Identity, + private readonly conceptId: Identity, + private readonly problem: Problem, + private readonly marketExistence: string, + private readonly targetAudiences: TargetAudience[] + ) {} static New( id: string, @@ -55,11 +39,23 @@ export class Idea { marketExistence: string, targetAudiences: TargetAudience[] ): Idea { + if (!problem || problem.trim() === '') { + throw new Error('Problem cannot be empty') + } + + if (!marketExistence || marketExistence.trim() === '') { + throw new Error('Market existence cannot be empty') + } + + if (targetAudiences.length === 0) { + throw new Error('Target audiences cannot be empty') + } + return new Idea( Identity.New(id), Identity.New(conceptId), Problem.New(problem), - marketExistence, + marketExistence.trim(), targetAudiences ) } diff --git a/src/idea/domain/Competitor.ts b/src/idea/domain/Competitor.ts new file mode 100644 index 0000000..9461c8d --- /dev/null +++ b/src/idea/domain/Competitor.ts @@ -0,0 +1,117 @@ +export class Competitor { + private constructor( + private readonly name: string, + private readonly productName: string, + private readonly url: string, + private readonly coreFeatures: string[], + private readonly valueProposition: string, + private readonly userAcquisition: string, + private readonly strengths: string[], + private readonly weaknesses: string[], + private readonly differentiationOpportunity: string + ) {} + + static New( + name: string, + productName: string, + url: string, + coreFeatures: string[], + valueProposition: string, + userAcquisition: string, + strengths: string[], + weaknesses: string[], + differentiationOpportunity: string + ) { + if (!name || name.trim() === '') { + throw new Error('Competitor name cannot be empty') + } + + if (!productName || productName.trim() === '') { + throw new Error('Product name cannot be empty') + } + + if (!url || url.trim() === '') { + throw new Error('URL cannot be empty') + } + + const urlPattern = /^(http|https):\/\/[^ "]+$/ + if (!urlPattern.test(url)) { + throw new Error('URL is not valid') + } + + if (!Array.isArray(coreFeatures) || coreFeatures.length === 0) { + throw new Error('Core features cannot be empty') + } + + if (!valueProposition || valueProposition.trim() === '') { + throw new Error('Value proposition cannot be empty') + } + + if (!userAcquisition || userAcquisition.trim() === '') { + throw new Error('User acquisition cannot be empty') + } + + if (!Array.isArray(strengths) || strengths.length === 0) { + throw new Error('Strengths cannot be empty') + } + + if (!Array.isArray(weaknesses) || weaknesses.length === 0) { + throw new Error('Weaknesses cannot be empty') + } + + if ( + !differentiationOpportunity || + differentiationOpportunity.trim() === '' + ) { + throw new Error('Differentiation opportunity cannot be empty') + } + + return new Competitor( + name.trim(), + productName.trim(), + url.trim(), + coreFeatures, + valueProposition.trim(), + userAcquisition.trim(), + strengths, + weaknesses, + differentiationOpportunity.trim() + ) + } + + public getName(): string { + return this.name + } + + public getProductName(): string { + return this.productName + } + + public getUrl(): string { + return this.url + } + + public getCoreFeatures(): string[] { + return [...this.coreFeatures] + } + + public getValueProposition(): string { + return this.valueProposition + } + + public getUserAcquisition(): string { + return this.userAcquisition + } + + public getStrengths(): string[] { + return [...this.strengths] + } + + public getWeaknesses(): string[] { + return [...this.weaknesses] + } + + public getDifferentiationOpportunity(): string { + return this.differentiationOpportunity + } +} diff --git a/src/idea/domain/CompetitorAnalysis.ts b/src/idea/domain/CompetitorAnalysis.ts index 7bfcc93..3eed8b1 100644 --- a/src/idea/domain/CompetitorAnalysis.ts +++ b/src/idea/domain/CompetitorAnalysis.ts @@ -1,40 +1,55 @@ -interface Competitor { - name: string - productName: string - url: string - coreFeatures: string[] - valueProposition: string - userAcquisition: string - strengths: string[] - weaknesses: string[] - differentiationOpportunity: string -} +import { Competitor } from '@/idea/domain/Competitor' +// TODO: Extract to a value object interface Comparison { strengths: string[] weaknesses: string[] } export class CompetitorAnalysis { - private readonly competitors: Competitor[] - private readonly comparison: Comparison - private readonly differentiationSuggestions: string[] - private constructor( - competitors: Competitor[], - comparison: Comparison, - differentiationSuggestions: string[] - ) { - this.competitors = competitors - this.comparison = comparison - this.differentiationSuggestions = differentiationSuggestions - } + private readonly competitors: Competitor[], + private readonly comparison: Comparison, + private readonly differentiationSuggestions: string[] + ) {} static New( competitors: Competitor[], comparison: Comparison, differentiationSuggestions: string[] ): CompetitorAnalysis { + if (!Array.isArray(competitors) || competitors.length === 0) { + throw new Error('Competitors cannot be empty') + } + + competitors.forEach((competitor, index) => { + if (!(competitor instanceof Competitor)) { + throw new Error(`Competitor at index ${index} is invalid`) + } + }) + + if (!comparison) { + throw new Error('Comparison cannot be null or undefined') + } + + if ( + !Array.isArray(comparison.strengths) || + comparison.strengths.length === 0 + ) { + throw new Error('Comparison strengths cannot be empty') + } + + if ( + !Array.isArray(comparison.weaknesses) || + comparison.weaknesses.length === 0 + ) { + throw new Error('Comparison weaknesses cannot be empty') + } + + if (!Array.isArray(differentiationSuggestions)) { + throw new Error('Differentiation suggestions must be an array') + } + return new CompetitorAnalysis( competitors, comparison, @@ -43,14 +58,17 @@ export class CompetitorAnalysis { } public getCompetitors(): Competitor[] { - return this.competitors + return [...this.competitors] } public getComparison(): Comparison { - return this.comparison + return { + strengths: this.comparison.strengths, + weaknesses: this.comparison.weaknesses, + } } public getDifferentiationSuggestions(): string[] { - return this.differentiationSuggestions + return [...this.differentiationSuggestions] } } diff --git a/src/idea/domain/ContentIdea.ts b/src/idea/domain/ContentIdea.ts index bbe3203..cd2715c 100644 --- a/src/idea/domain/ContentIdea.ts +++ b/src/idea/domain/ContentIdea.ts @@ -1,69 +1,12 @@ -type Section = - | 'socialMediaCampaigns' - | 'bloggingAndGuestPosts' - | 'emailMarketing' - | 'surveysAndPolls' - | 'videoContent' - | 'infographicsAndVisualContent' - | 'communityEngagement' - | 'paidAdvertising' - | 'webinarsAndLiveStreams' - | 'partnershipsAndCollaborations' - -const Sections: Section[] = [ - 'socialMediaCampaigns', - 'bloggingAndGuestPosts', - 'emailMarketing', - 'surveysAndPolls', - 'videoContent', - 'infographicsAndVisualContent', - 'communityEngagement', - 'paidAdvertising', - 'webinarsAndLiveStreams', - 'partnershipsAndCollaborations', -] - -function isValidSection(name: string): name is Section { - return Sections.includes(name as Section) -} - -export class Strategy { - private readonly name: Section - - private constructor(name: Section) { - this.name = name - } - - static New(name: string): Strategy { - if (!isValidSection(name)) { - throw new Error(`Invalid section name: ${name}`) - } - - return new Strategy(name) - } - - public getName(): string { - return this.name - } -} +import { Strategy } from '@/idea/domain/Strategy' export class ContentIdea { - private readonly strategy: Strategy - private readonly platforms: string[] - private readonly ideas: string[] - private readonly benefits: string[] - private constructor( - strategy: Strategy, - platforms: string[], - ideas: string[], - benefits: string[] - ) { - this.strategy = strategy - this.platforms = platforms - this.ideas = ideas - this.benefits = benefits - } + private readonly strategy: Strategy, + private readonly platforms: string[], + private readonly ideas: string[], + private readonly benefits: string[] + ) {} static New( strategy: Strategy, @@ -71,22 +14,63 @@ export class ContentIdea { ideas: string[], benefits: string[] ): ContentIdea { - return new ContentIdea(strategy, platforms, ideas, benefits) + if (!strategy) { + throw new Error('Strategy cannot be null or undefined.') + } + + if (!Array.isArray(platforms) || platforms.length === 0) { + throw new Error('Platforms cannot be empty.') + } + + platforms.forEach((platform, index) => { + if (typeof platform !== 'string' || platform.trim() === '') { + throw new Error( + `Platform at index ${index} must be a non-empty string.` + ) + } + }) + + if (!Array.isArray(ideas) || ideas.length === 0) { + throw new Error('Ideas cannot be empty.') + } + + ideas.forEach((idea, index) => { + if (typeof idea !== 'string' || idea.trim() === '') { + throw new Error(`Idea at index ${index} must be a non-empty string.`) + } + }) + + if (!Array.isArray(benefits) || benefits.length === 0) { + throw new Error('Benefits cannot be empty.') + } + + benefits.forEach((benefit, index) => { + if (typeof benefit !== 'string' || benefit.trim() === '') { + throw new Error(`Benefit at index ${index} must be a non-empty string.`) + } + }) + + return new ContentIdea( + strategy, + platforms.map((p) => p.trim()), + ideas.map((i) => i.trim()), + benefits.map((b) => b.trim()) + ) } - public getSection(): Strategy { + public getStrategy(): Strategy { return this.strategy } public getPlatforms(): string[] { - return this.platforms + return [...this.platforms] } public getIdeas(): string[] { - return this.ideas + return [...this.ideas] } public getBenefits(): string[] { - return this.benefits + return [...this.benefits] } } diff --git a/src/idea/domain/ContentIdeasForMarketing.ts b/src/idea/domain/ContentIdeasForMarketing.ts index e9bba8a..d4b910d 100644 --- a/src/idea/domain/ContentIdeasForMarketing.ts +++ b/src/idea/domain/ContentIdeasForMarketing.ts @@ -10,10 +10,18 @@ export class ContentIdeasForMarketing { } public addContentIdea(contentIdea: ContentIdea): void { + if (!contentIdea) { + throw new Error('ContentIdea cannot be null or undefined') + } + + if (!(contentIdea instanceof ContentIdea)) { + throw new Error('Invalid ContentIdea instance') + } + this.contentIdeas.push(contentIdea) } public getContentIdeas(): ContentIdea[] { - return this.contentIdeas + return [...this.contentIdeas] } } diff --git a/src/idea/domain/ElevatorPitch.ts b/src/idea/domain/ElevatorPitch.ts index a808978..31d912e 100644 --- a/src/idea/domain/ElevatorPitch.ts +++ b/src/idea/domain/ElevatorPitch.ts @@ -1,23 +1,11 @@ export class ElevatorPitch { - private readonly hook: string - private readonly problem: string - private readonly solution: string - private readonly valueProposition: string - private readonly cta: string - private constructor( - hook: string, - problem: string, - solution: string, - valueProposition: string, - cta: string - ) { - this.hook = hook - this.problem = problem - this.solution = solution - this.valueProposition = valueProposition - this.cta = cta - } + private readonly hook: string, + private readonly problem: string, + private readonly solution: string, + private readonly valueProposition: string, + private readonly cta: string + ) {} static New( hook: string, @@ -26,7 +14,33 @@ export class ElevatorPitch { valueProposition: string, cta: string ): ElevatorPitch { - return new ElevatorPitch(hook, problem, solution, valueProposition, cta) + if (!hook || hook.trim() === '') { + throw new Error('Hook cannot be empty') + } + + if (!problem || problem.trim() === '') { + throw new Error('Problem cannot be empty') + } + + if (!solution || solution.trim() === '') { + throw new Error('Solution cannot be empty') + } + + if (!valueProposition || valueProposition.trim() === '') { + throw new Error('Value proposition cannot be empty') + } + + if (!cta || cta.trim() === '') { + throw new Error('Call to action cannot be empty') + } + + return new ElevatorPitch( + hook.trim(), + problem.trim(), + solution.trim(), + valueProposition.trim(), + cta.trim() + ) } public getHook(): string { diff --git a/src/idea/domain/GoogleTrendsKeyword.ts b/src/idea/domain/GoogleTrendsKeyword.ts index e165b6b..37aa9c9 100644 --- a/src/idea/domain/GoogleTrendsKeyword.ts +++ b/src/idea/domain/GoogleTrendsKeyword.ts @@ -1,12 +1,22 @@ export class GoogleTrendsKeyword { - private readonly keyword: string - - private constructor(keyword: string) { - this.keyword = keyword - } + private constructor(private readonly keyword: string) {} static New(keyword: string): GoogleTrendsKeyword { - return new GoogleTrendsKeyword(keyword) + if (typeof keyword !== 'string') { + throw new Error('Keyword must be a string.') + } + + const cleanKeyword = keyword.trim() + + if (cleanKeyword.length === 0) { + throw new Error('Keyword cannot be empty.') + } + + if (cleanKeyword.length < 2 || cleanKeyword.length > 100) { + throw new Error('Keyword must be between 2 and 100 characters.') + } + + return new GoogleTrendsKeyword(cleanKeyword) } public getKeyword(): string { diff --git a/src/idea/domain/MarketAnalysis.ts b/src/idea/domain/MarketAnalysis.ts index 2bc182e..b0ec056 100644 --- a/src/idea/domain/MarketAnalysis.ts +++ b/src/idea/domain/MarketAnalysis.ts @@ -1,23 +1,11 @@ export class MarketAnalysis { - private readonly trends: string - private readonly userBehaviors: string - private readonly marketGaps: string - private readonly innovationOpportunities: string - private readonly strategicDirection: string - private constructor( - trends: string, - userBehaviors: string, - marketGaps: string, - innovationOpportunities: string, - strategicDirection: string - ) { - this.trends = trends - this.userBehaviors = userBehaviors - this.marketGaps = marketGaps - this.innovationOpportunities = innovationOpportunities - this.strategicDirection = strategicDirection - } + private readonly trends: string, + private readonly userBehaviors: string, + private readonly marketGaps: string, + private readonly innovationOpportunities: string, + private readonly strategicDirection: string + ) {} static New( trends: string, @@ -26,12 +14,32 @@ export class MarketAnalysis { innovationOpportunities: string, strategicDirection: string ): MarketAnalysis { + if (!trends || trends.trim() === '') { + throw new Error('Trends cannot be empty') + } + + if (!userBehaviors || userBehaviors.trim() === '') { + throw new Error('User behaviors cannot be empty') + } + + if (!marketGaps || marketGaps.trim() === '') { + throw new Error('Market gaps cannot be empty') + } + + if (!innovationOpportunities || innovationOpportunities.trim() === '') { + throw new Error('Innovation opportunities cannot be empty') + } + + if (!strategicDirection || strategicDirection.trim() === '') { + throw new Error('Strategic direction cannot be empty') + } + return new MarketAnalysis( - trends, - userBehaviors, - marketGaps, - innovationOpportunities, - strategicDirection + trends.trim(), + userBehaviors.trim(), + marketGaps.trim(), + innovationOpportunities.trim(), + strategicDirection.trim() ) } diff --git a/src/idea/domain/Problem.ts b/src/idea/domain/Problem.ts index ef29ac7..3f01104 100644 --- a/src/idea/domain/Problem.ts +++ b/src/idea/domain/Problem.ts @@ -1,17 +1,19 @@ export class Problem { - private readonly value: string - - private constructor(value: string) { - this.value = value - } + private constructor(private readonly value: string) {} static New(value: string): Problem { + if (typeof value !== 'string') { + throw new Error('Problem must be a string.') + } + const cleanValue = value.trim() - if (!cleanValue || cleanValue.length < 30 || cleanValue.length > 2048) { - throw new Error( - 'Problem must be defined and between 30 and 2048 characters.' - ) + if (cleanValue === '') { + throw new Error('Problem cannot be empty.') + } + + if (cleanValue.length < 30 || cleanValue.length > 2048) { + throw new Error('Problem must be between 30 and 2048 characters.') } return new Problem(cleanValue) diff --git a/src/idea/domain/ProductName.ts b/src/idea/domain/ProductName.ts index 8e52e2c..2c2b98d 100644 --- a/src/idea/domain/ProductName.ts +++ b/src/idea/domain/ProductName.ts @@ -1,29 +1,13 @@ export class ProductName { - private readonly productName: string - private readonly domains: string[] - private readonly why: string - private readonly tagline: string - private readonly targetAudienceInsight: string - private readonly similarNames: string[] - private readonly brandingPotential: string - private constructor( - productName: string, - domains: string[], - why: string, - tagline: string, - targetAudienceInsight: string, - similarNames: string[], - brandingPotential: string - ) { - this.productName = productName - this.domains = domains - this.why = why - this.tagline = tagline - this.targetAudienceInsight = targetAudienceInsight - this.similarNames = similarNames - this.brandingPotential = brandingPotential - } + private readonly productName: string, + private readonly domains: string[], + private readonly why: string, + private readonly tagline: string, + private readonly targetAudienceInsight: string, + private readonly similarNames: string[], + private readonly brandingPotential: string + ) {} static New( productName: string, @@ -34,14 +18,54 @@ export class ProductName { similarNames: string[], brandingPotential: string ): ProductName { + if (!productName || productName.trim() === '') { + throw new Error('Product name cannot be empty') + } + + if (!Array.isArray(domains) || domains.length === 0) { + throw new Error('Domains cannot be empty') + } + + for (const domain of domains) { + if (typeof domain !== 'string' || domain.trim() === '') { + throw new Error('Each domain must be a non-empty string') + } + } + + if (!why || why.trim() === '') { + throw new Error('Why cannot be empty') + } + + if (!tagline || tagline.trim() === '') { + throw new Error('Tagline cannot be empty') + } + + if (!targetAudienceInsight || targetAudienceInsight.trim() === '') { + throw new Error('Target audience insight cannot be empty') + } + + if (!Array.isArray(similarNames) || similarNames.length === 0) { + throw new Error('Similar names cannot be empty') + } + + for (const name of similarNames) { + if (typeof name !== 'string' || name.trim() === '') { + throw new Error('Each similar name must be a non-empty string') + } + } + + if (!brandingPotential || brandingPotential.trim() === '') { + throw new Error('Branding potential cannot be empty') + } + return new ProductName( - productName, - domains, - why, - tagline, - targetAudienceInsight, - similarNames, - brandingPotential + productName.trim(), + domains.map((d) => d.trim()), + why.trim(), + tagline.trim(), + targetAudienceInsight.trim(), + similarNames.map((n) => n.trim()), + brandingPotential.trim() ) } @@ -50,7 +74,7 @@ export class ProductName { } public getDomains(): string[] { - return this.domains + return [...this.domains] } public getWhy(): string { @@ -66,7 +90,7 @@ export class ProductName { } public getSimilarNames(): string[] { - return this.similarNames + return [...this.similarNames] } public getBrandingPotential(): string { diff --git a/src/idea/domain/SWOTAnalysis.ts b/src/idea/domain/SWOTAnalysis.ts index 9a636cf..a1d212c 100644 --- a/src/idea/domain/SWOTAnalysis.ts +++ b/src/idea/domain/SWOTAnalysis.ts @@ -1,20 +1,10 @@ export class SWOTAnalysis { - private readonly strengths: string[] - private readonly weaknesses: string[] - private readonly opportunities: string[] - private readonly threats: string[] - private constructor( - strengths: string[], - weaknesses: string[], - opportunities: string[], - threats: string[] - ) { - this.strengths = strengths - this.weaknesses = weaknesses - this.opportunities = opportunities - this.threats = threats - } + private readonly strengths: string[], + private readonly weaknesses: string[], + private readonly opportunities: string[], + private readonly threats: string[] + ) {} static New( strengths: string[], @@ -22,22 +12,69 @@ export class SWOTAnalysis { opportunities: string[], threats: string[] ): SWOTAnalysis { - return new SWOTAnalysis(strengths, weaknesses, opportunities, threats) + if (!Array.isArray(strengths) || strengths.length === 0) { + throw new Error('Strengths cannot be empty') + } + + strengths.forEach((item, index) => { + if (typeof item !== 'string' || item.trim() === '') { + throw new Error(`Strength at index ${index} must be a non-empty string`) + } + }) + + if (!Array.isArray(weaknesses) || weaknesses.length === 0) { + throw new Error('Weaknesses cannot be empty') + } + + weaknesses.forEach((item, index) => { + if (typeof item !== 'string' || item.trim() === '') { + throw new Error(`Weakness at index ${index} must be a non-empty string`) + } + }) + + if (!Array.isArray(opportunities) || opportunities.length === 0) { + throw new Error('Opportunities cannot be empty') + } + + opportunities.forEach((item, index) => { + if (typeof item !== 'string' || item.trim() === '') { + throw new Error( + `Opportunity at index ${index} must be a non-empty string` + ) + } + }) + + if (!Array.isArray(threats) || threats.length === 0) { + throw new Error('Threats cannot be empty') + } + + threats.forEach((item, index) => { + if (typeof item !== 'string' || item.trim() === '') { + throw new Error(`Threat at index ${index} must be a non-empty string`) + } + }) + + return new SWOTAnalysis( + strengths.map((item) => item.trim()), + weaknesses.map((item) => item.trim()), + opportunities.map((item) => item.trim()), + threats.map((item) => item.trim()) + ) } public getStrengths(): string[] { - return this.strengths + return [...this.strengths] } public getWeaknesses(): string[] { - return this.weaknesses + return [...this.weaknesses] } public getOpportunities(): string[] { - return this.opportunities + return [...this.opportunities] } public getThreats(): string[] { - return this.threats + return [...this.threats] } } diff --git a/src/idea/domain/SocialMediaCampaigns.ts b/src/idea/domain/SocialMediaCampaigns.ts index 4f7c53a..69c0f4c 100644 --- a/src/idea/domain/SocialMediaCampaigns.ts +++ b/src/idea/domain/SocialMediaCampaigns.ts @@ -36,26 +36,83 @@ export class SocialMediaCampaigns { } public addShortFormContent(shortFormContent: ShortFormContent): void { + this.validateShortFormContent(shortFormContent) this.shortFormContents.push(shortFormContent) } public addLongFormContent(longFormContent: LongFormContent): void { + this.validateLongFormContent(longFormContent) this.longFormContents.push(longFormContent) } public addVideoContent(videoContent: VideoContent): void { + this.validateVideoContent(videoContent) this.videoContents.push(videoContent) } public getShortFormContents(): ShortFormContent[] { - return this.shortFormContents + return [...this.shortFormContents] } public getLongFormContents(): LongFormContent[] { - return this.longFormContents + return [...this.longFormContents] } public getVideoContents(): VideoContent[] { - return this.videoContents + return [...this.videoContents] + } + + private validateShortFormContent(content: ShortFormContent): void { + if (!content) { + throw new Error('ShortFormContent cannot be null or undefined') + } + this.validateStringProperty(content.header, 'header') + this.validateStringProperty(content.platform, 'platform') + this.validateStringProperty(content.content, 'content') + this.validateStringProperty(content.imagePrompt, 'imagePrompt') + this.validateStringArray(content.tips, 'tips') + } + + private validateLongFormContent(content: LongFormContent): void { + if (!content) { + throw new Error('LongFormContent cannot be null or undefined') + } + this.validateStringProperty(content.header, 'header') + this.validateStringProperty(content.platform, 'platform') + this.validateStringProperty(content.title, 'title') + this.validateStringProperty(content.content, 'content') + this.validateStringProperty(content.imagePrompt, 'imagePrompt') + this.validateStringArray(content.tips, 'tips') + } + + private validateVideoContent(content: VideoContent): void { + if (!content) { + throw new Error('VideoContent cannot be null or undefined') + } + this.validateStringProperty(content.header, 'header') + this.validateStringProperty(content.platform, 'platform') + this.validateStringProperty(content.title, 'title') + this.validateStringProperty(content.imagePrompt, 'imagePrompt') + this.validateStringArray(content.script, 'script') + this.validateStringArray(content.tips, 'tips') + } + + private validateStringProperty(value: string, propertyName: string): void { + if (typeof value !== 'string' || value.trim() === '') { + throw new Error(`${propertyName} cannot be empty`) + } + } + + private validateStringArray(arr: string[], propertyName: string): void { + if (!Array.isArray(arr) || arr.length === 0) { + throw new Error(`${propertyName} cannot be empty`) + } + arr.forEach((item, index) => { + if (typeof item !== 'string' || item.trim() === '') { + throw new Error( + `${propertyName} at index ${index} must be a non-empty string` + ) + } + }) } } diff --git a/src/idea/domain/Strategy.ts b/src/idea/domain/Strategy.ts new file mode 100644 index 0000000..f8cbafd --- /dev/null +++ b/src/idea/domain/Strategy.ts @@ -0,0 +1,43 @@ +type Section = + | 'socialMediaCampaigns' + | 'bloggingAndGuestPosts' + | 'emailMarketing' + | 'surveysAndPolls' + | 'videoContent' + | 'infographicsAndVisualContent' + | 'communityEngagement' + | 'paidAdvertising' + | 'webinarsAndLiveStreams' + | 'partnershipsAndCollaborations' + +const Sections: Section[] = [ + 'socialMediaCampaigns', + 'bloggingAndGuestPosts', + 'emailMarketing', + 'surveysAndPolls', + 'videoContent', + 'infographicsAndVisualContent', + 'communityEngagement', + 'paidAdvertising', + 'webinarsAndLiveStreams', + 'partnershipsAndCollaborations', +] + +function isValidSection(name: string): name is Section { + return Sections.includes(name as Section) +} + +export class Strategy { + private constructor(private readonly name: Section) {} + + static New(name: string): Strategy { + if (!isValidSection(name)) { + throw new Error(`Invalid section name: ${name}`) + } + return new Strategy(name as Section) + } + + public getName(): Section { + return this.name + } +} diff --git a/src/idea/domain/TargetAudience.ts b/src/idea/domain/TargetAudience.ts index f9abdaf..7736624 100644 --- a/src/idea/domain/TargetAudience.ts +++ b/src/idea/domain/TargetAudience.ts @@ -1,29 +1,17 @@ import { Identity } from '@/shared/Identity' export class TargetAudience { - private readonly id: Identity - private readonly ideaId: Identity - private readonly segment: string - private readonly description: string - private readonly challenges: string[] - private why: string | null = null private painPoints: string[] | null = null private targetingStrategy: string | null = null private constructor( - id: Identity, - ideaId: Identity, - segment: string, - description: string, - challenges: string[] - ) { - this.id = id - this.ideaId = ideaId - this.segment = segment - this.description = description - this.challenges = challenges - } + private readonly id: Identity, + private readonly ideaId: Identity, + private readonly segment: string, + private readonly description: string, + private readonly challenges: string[] + ) {} static New( id: string, @@ -32,17 +20,29 @@ export class TargetAudience { description: string, challenges: string[] ): TargetAudience { + if (!segment || segment.trim() === '') { + throw new Error('Segment cannot be empty') + } + + if (!description || description.trim() === '') { + throw new Error('Description cannot be empty') + } + + if (!Array.isArray(challenges) || challenges.length === 0) { + throw new Error('Challenges cannot be empty') + } + return new TargetAudience( Identity.New(id), Identity.New(ideaId), - segment, - description, + segment.trim(), + description.trim(), challenges ) } public setWhy(why: string): void { - if (!why) { + if (!why || why.trim() === '') { throw new Error('Why cannot be empty') } @@ -50,7 +50,7 @@ export class TargetAudience { } public setPainPoints(painPoints: string[]): void { - if (painPoints.length === 0) { + if (!Array.isArray(painPoints) || painPoints.length === 0) { throw new Error('Pain points cannot be empty') } @@ -58,7 +58,7 @@ export class TargetAudience { } public setTargetingStrategy(targetingStrategy: string): void { - if (!targetingStrategy) { + if (!targetingStrategy || targetingStrategy.trim() === '') { throw new Error('Targeting strategy cannot be empty') } diff --git a/src/idea/domain/ValueProposition.ts b/src/idea/domain/ValueProposition.ts index 7a61300..95c9249 100644 --- a/src/idea/domain/ValueProposition.ts +++ b/src/idea/domain/ValueProposition.ts @@ -1,24 +1,32 @@ export class ValueProposition { - private readonly mainBenefit: string - private readonly problemSolving: string - private readonly differentiation: string - private constructor( - mainBenefit: string, - problemSolving: string, - differentiation: string - ) { - this.mainBenefit = mainBenefit - this.problemSolving = problemSolving - this.differentiation = differentiation - } + private readonly mainBenefit: string, + private readonly problemSolving: string, + private readonly differentiation: string + ) {} static New( mainBenefit: string, problemSolving: string, differentiation: string ): ValueProposition { - return new ValueProposition(mainBenefit, problemSolving, differentiation) + if (!mainBenefit || mainBenefit.trim() === '') { + throw new Error('Main benefit cannot be empty') + } + + if (!problemSolving || problemSolving.trim() === '') { + throw new Error('Problem solving cannot be empty') + } + + if (!differentiation || differentiation.trim() === '') { + throw new Error('Differentiation cannot be empty') + } + + return new ValueProposition( + mainBenefit.trim(), + problemSolving.trim(), + differentiation.trim() + ) } public getMainBenefit(): string { diff --git a/src/idea/events/subscribers/CompetitorAnalysisEvaluationSubscriber.ts b/src/idea/events/subscribers/CompetitorAnalysisEvaluationSubscriber.ts index b4d0d6c..f0a5b15 100644 --- a/src/idea/events/subscribers/CompetitorAnalysisEvaluationSubscriber.ts +++ b/src/idea/events/subscribers/CompetitorAnalysisEvaluationSubscriber.ts @@ -1,11 +1,12 @@ import * as Sentry from '@sentry/nextjs' import { Idea } from '@/idea/domain/Aggregate' +import { Competitor } from '@/idea/domain/Competitor' import { CompetitorAnalysis } from '@/idea/domain/CompetitorAnalysis' import { Repository } from '@/idea/domain/Repository' import { TargetAudiencesEvaluated } from '@/idea/domain/events/TargetAudiencesEvaluated' import { EventHandler } from '@/idea/events/EventHandler' -interface Competitor { +interface CompetitorDTO { name: string productName: string url: string @@ -23,7 +24,7 @@ interface Comparison { } type Evaluation = { - competitors: Competitor[] + competitors: CompetitorDTO[] comparison: Comparison differentiationSuggestions: string[] } @@ -83,10 +84,24 @@ export class CompetitorAnalysisEvaluationSubscriber implements EventHandler { audiences ) + const competitors = evaluation.competitors.map((c) => + Competitor.New( + c.name, + c.productName, + c.url, + c.coreFeatures, + c.valueProposition, + c.userAcquisition, + c.strengths, + c.weaknesses, + c.differentiationOpportunity + ) + ) + await this.repository.updateIdea(event.payload.id, (idea): Idea => { idea.setCompetitorAnalysis( CompetitorAnalysis.New( - evaluation.competitors, + competitors, evaluation.comparison, evaluation.differentiationSuggestions ) diff --git a/src/idea/events/subscribers/ContentIdeasEvaluationSubscriber.ts b/src/idea/events/subscribers/ContentIdeasEvaluationSubscriber.ts index 8745773..6deafe2 100644 --- a/src/idea/events/subscribers/ContentIdeasEvaluationSubscriber.ts +++ b/src/idea/events/subscribers/ContentIdeasEvaluationSubscriber.ts @@ -1,8 +1,9 @@ import * as Sentry from '@sentry/nextjs' import { Idea } from '@/idea/domain/Aggregate' -import { ContentIdea, Strategy } from '@/idea/domain/ContentIdea' +import { ContentIdea } from '@/idea/domain/ContentIdea' import { ContentIdeasForMarketing } from '@/idea/domain/ContentIdeasForMarketing' import { Repository } from '@/idea/domain/Repository' +import { Strategy } from '@/idea/domain/Strategy' import { ValuePropositionEvaluated } from '@/idea/domain/events/ValuePropositionEvaluated' import { EventHandler } from '@/idea/events/EventHandler' diff --git a/src/shared/Identity.ts b/src/shared/Identity.ts index bc26250..89a9295 100644 --- a/src/shared/Identity.ts +++ b/src/shared/Identity.ts @@ -1,3 +1,5 @@ +import { validate as uuidValidate, v4 as uuidv4 } from 'uuid' + export class Identity { private readonly value: string @@ -5,14 +7,36 @@ export class Identity { this.value = value } + /** + * Creates a new Identity instance with the provided UUID. + * @param value - A string representing a UUID. + * @throws {Error} If the value is not a valid UUID. + * @returns {Identity} A new Identity instance. + */ static New(value: string): Identity { - if (value.trim() === '') { - throw new Error('Value must be defined and non-empty') + if (!value || value.trim() === '') { + throw new Error('Value cannot be empty') + } + + if (!uuidValidate(value)) { + throw new Error('Value must be a valid UUID') } return new Identity(value) } + /** + * Generates a new Identity instance with a randomly generated UUID. + * @returns {Identity} A new Identity instance. + */ + static Generate(): Identity { + return new Identity(uuidv4()) + } + + /** + * Retrieves the UUID value of the Identity. + * @returns {string} The UUID string. + */ getValue(): string { return this.value }