diff --git a/internal/core/entity/battle.go b/internal/core/entity/battle.go index 3b795dd4..33273756 100644 --- a/internal/core/entity/battle.go +++ b/internal/core/entity/battle.go @@ -2,7 +2,6 @@ package entity import ( "errors" - "fmt" "math/rand" "time" @@ -27,10 +26,7 @@ func (b *Battle) PartnerAttack() error { return ErrInvalidState } // inflict damage to enemy - damage, err := b.Enemy.InflictDamage(*b.Partner) - if err != nil { - return fmt.Errorf("unable to inflict damage to enemy due: %w", err) - } + damage := b.Enemy.InflictDamage(*b.Partner) // set enemy last damage b.LastDamage.Enemy = damage // set battle state accordingly @@ -63,10 +59,7 @@ func (b *Battle) EnemyAttack() error { return ErrInvalidState } // inflict damage to partner - damage, err := b.Partner.InflictDamage(*b.Enemy) - if err != nil { - return fmt.Errorf("unable to inflict damage to partner due: %w", err) - } + damage := b.Partner.InflictDamage(*b.Enemy) // set partner last damage b.LastDamage.Partner = damage // set battle state accordingly diff --git a/internal/core/entity/battle_test.go b/internal/core/entity/battle_test.go index 575db343..437ab259 100644 --- a/internal/core/entity/battle_test.go +++ b/internal/core/entity/battle_test.go @@ -2,21 +2,22 @@ package entity_test import ( "fmt" + "math" "testing" "time" "github.com/Haraj-backend/hex-monscape/internal/core/entity" "github.com/Haraj-backend/hex-monscape/internal/core/testutil" "github.com/google/uuid" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewBattle(t *testing.T) { // define function for validating battle validateBattle := func(t *testing.T, battle entity.Battle, cfg entity.BattleConfig) { - assert.NotEmpty(t, battle.GameID, "GameID is empty") - assert.NotEmpty(t, battle.Partner, "Partner is empty") - assert.NotEmpty(t, battle.Enemy, "Enemy is empty") + require.NotEmpty(t, battle.GameID, "GameID is empty") + require.NotEmpty(t, battle.Partner, "Partner is empty") + require.NotEmpty(t, battle.Enemy, "Enemy is empty") } // define test cases testCases := []struct { @@ -43,7 +44,7 @@ func TestNewBattle(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.Name, func(t *testing.T) { battle, err := entity.NewBattle(testCase.Config) - assert.Equal(t, testCase.IsError, (err != nil), "unexpected error") + require.Equal(t, testCase.IsError, (err != nil), "unexpected error") if battle == nil { return } @@ -118,14 +119,27 @@ func TestPartnerAttack(t *testing.T) { battle.State = testCase.State err := battle.PartnerAttack() - assert.Equal(t, testCase.IsError, (err != nil), "unexpected error") + require.Equal(t, testCase.IsError, (err != nil), "unexpected error") if !testCase.IsError { - assert.Equal(t, battle.Enemy.BattleStats.Health, testCase.ExpectedEnemyHealth, "enemy health is not valid") + require.Equal(t, battle.Enemy.BattleStats.Health, testCase.ExpectedEnemyHealth, "enemy health is not valid") } }) } } +func TestPartnerAttackWin(t *testing.T) { + battle := initNewBattle() + // set partner attack to very high number so that enemy will be dead + battle.Partner.BattleStats.Attack = math.MaxInt + // set battle state to partner attack + battle.State = entity.StatePartnerTurn + // execute partner attack, enemy should be dead + err := battle.PartnerAttack() + require.NoError(t, err) + // validate battle state + require.Equal(t, entity.StateWin, battle.State) +} + func TestPartnerSurrender(t *testing.T) { battle := initNewBattle() // define test cases @@ -155,9 +169,9 @@ func TestPartnerSurrender(t *testing.T) { t.Run(testCase.Name, func(t *testing.T) { battle.State = testCase.State err := battle.PartnerSurrender() - assert.Equal(t, testCase.IsError, (err != nil), "unexpected error") + require.Equal(t, testCase.IsError, (err != nil), "unexpected error") if !testCase.IsError { - assert.Equal(t, entity.StateLose, battle.State) + require.Equal(t, entity.StateLose, battle.State) } }) } @@ -235,14 +249,27 @@ func TestEnemyAttack(t *testing.T) { }) battle.State = testCase.State err := battle.EnemyAttack() - assert.Equal(t, testCase.IsError, (err != nil), "unexpected error") + require.Equal(t, testCase.IsError, (err != nil), "unexpected error") if !testCase.IsError { - assert.Equal(t, battle.Partner.BattleStats.Health, testCase.ExpectedPartnerHealth, "partner health is not valid") + require.Equal(t, battle.Partner.BattleStats.Health, testCase.ExpectedPartnerHealth, "partner health is not valid") } }) } } +func TestEnemyAttackWin(t *testing.T) { + battle := initNewBattle() + // set enemy attack to very high number so that partner will be dead + battle.Enemy.BattleStats.Attack = math.MaxInt + // set battle state to enemy attack + battle.State = entity.StateEnemyTurn + // execute enemy attack, partner should be dead + err := battle.EnemyAttack() + require.NoError(t, err) + // validate battle state + require.Equal(t, entity.StateLose, battle.State) +} + func TestIsEnded(t *testing.T) { battle := initNewBattle() // define test cases @@ -283,7 +310,7 @@ func TestIsEnded(t *testing.T) { t.Run(testCase.Name, func(t *testing.T) { battle.State = testCase.State actual := battle.IsEnded() - assert.Equal(t, testCase.Expected, actual, "unexpected dead") + require.Equal(t, testCase.Expected, actual, "unexpected dead") }) } } @@ -383,9 +410,9 @@ func TestDecideTurn(t *testing.T) { }) battle.State = testCase.State state, err := battle.DecideTurn() - assert.Equal(t, testCase.IsError, (err != nil), "unexpected error") + require.Equal(t, testCase.IsError, (err != nil), "unexpected error") if !testCase.IsError { - assert.Equal(t, testCase.ExpectedState, state, "expected state is not valid") + require.Equal(t, testCase.ExpectedState, state, "expected state is not valid") } }) } diff --git a/internal/core/entity/monster.go b/internal/core/entity/monster.go index e1edf80d..2e4d8f31 100644 --- a/internal/core/entity/monster.go +++ b/internal/core/entity/monster.go @@ -1,6 +1,6 @@ package entity -const minDamage = 5 +const MinDamage = 5 type Monster struct { ID string @@ -21,10 +21,11 @@ func (p Monster) IsDead() bool { // InflictDamage is used for inflicting damage to self based // on given enemy. Returned the damage amount. -func (p *Monster) InflictDamage(enemy Monster) (int, error) { - dmg := max(enemy.BattleStats.Attack-p.BattleStats.Defense, minDamage) +func (p *Monster) InflictDamage(enemy Monster) int { + dmg := max(enemy.BattleStats.Attack-p.BattleStats.Defense, MinDamage) p.BattleStats.Health -= dmg - return dmg, nil + + return dmg } type BattleStats struct { diff --git a/internal/core/entity/monster_test.go b/internal/core/entity/monster_test.go index 12a28999..b0a2a78d 100644 --- a/internal/core/entity/monster_test.go +++ b/internal/core/entity/monster_test.go @@ -3,11 +3,11 @@ package entity_test import ( "fmt" "testing" - "time" "github.com/Haraj-backend/hex-monscape/internal/core/entity" + "github.com/Haraj-backend/hex-monscape/internal/core/testutil" "github.com/google/uuid" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIsDead(t *testing.T) { @@ -21,7 +21,7 @@ func TestIsDead(t *testing.T) { Name: "Monster is Not Dead", Monster: entity.Monster{ ID: uuid.NewString(), - Name: fmt.Sprintf("monster_%v", time.Now().Unix()), + Name: generateMonsterName(false), BattleStats: entity.BattleStats{ Health: 100, MaxHealth: 100, @@ -29,7 +29,7 @@ func TestIsDead(t *testing.T) { Defense: 100, Speed: 100, }, - AvatarURL: fmt.Sprintf("https://example.com/%v", time.Now().Unix()), + AvatarURL: generateAvatarURL(), }, Expected: false, }, @@ -37,7 +37,7 @@ func TestIsDead(t *testing.T) { Name: "Monster Has 0 Health", Monster: entity.Monster{ ID: uuid.NewString(), - Name: fmt.Sprintf("monster_%v", time.Now().Unix()), + Name: generateMonsterName(false), BattleStats: entity.BattleStats{ Health: 0, MaxHealth: 100, @@ -45,7 +45,7 @@ func TestIsDead(t *testing.T) { Defense: 100, Speed: 100, }, - AvatarURL: fmt.Sprintf("https://example.com/%v", time.Now().Unix()), + AvatarURL: generateAvatarURL(), }, Expected: true, }, @@ -53,7 +53,7 @@ func TestIsDead(t *testing.T) { Name: "Monster Has Negative Health", Monster: entity.Monster{ ID: uuid.NewString(), - Name: fmt.Sprintf("monster_%v", time.Now().Unix()), + Name: generateMonsterName(false), BattleStats: entity.BattleStats{ Health: -100, MaxHealth: 100, @@ -61,7 +61,7 @@ func TestIsDead(t *testing.T) { Defense: 100, Speed: 100, }, - AvatarURL: fmt.Sprintf("https://example.com/%v", time.Now().Unix()), + AvatarURL: generateAvatarURL(), }, Expected: true, }, @@ -71,7 +71,7 @@ func TestIsDead(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.Name, func(t *testing.T) { actual := testCase.Monster.IsDead() - assert.Equal(t, testCase.Expected, actual, "unexpected dead") + require.Equal(t, testCase.Expected, actual, "unexpected dead") }) } } @@ -88,7 +88,7 @@ func TestInflictDamage(t *testing.T) { Name: "Monster Get Zero Damage", Monster: entity.Monster{ ID: uuid.NewString(), - Name: fmt.Sprintf("monster_%v", time.Now().Unix()), + Name: generateMonsterName(false), BattleStats: entity.BattleStats{ Health: 100, MaxHealth: 100, @@ -96,11 +96,11 @@ func TestInflictDamage(t *testing.T) { Defense: 0, Speed: 100, }, - AvatarURL: fmt.Sprintf("https://example.com/%v", time.Now().Unix()), + AvatarURL: generateAvatarURL(), }, Enemy: entity.Monster{ ID: uuid.NewString(), - Name: fmt.Sprintf("enemy_%v", time.Now().Unix()), + Name: generateMonsterName(true), BattleStats: entity.BattleStats{ Health: 100, MaxHealth: 100, @@ -108,7 +108,7 @@ func TestInflictDamage(t *testing.T) { Defense: 100, Speed: 100, }, - AvatarURL: fmt.Sprintf("https://example.com/%v", time.Now().Unix()), + AvatarURL: generateAvatarURL(), }, ExpectedHealthAmount: 0, }, @@ -116,7 +116,7 @@ func TestInflictDamage(t *testing.T) { Name: "Monster Get 50 Damage", Monster: entity.Monster{ ID: uuid.NewString(), - Name: fmt.Sprintf("monster_%v", time.Now().Unix()), + Name: generateMonsterName(false), BattleStats: entity.BattleStats{ Health: 100, MaxHealth: 100, @@ -124,11 +124,11 @@ func TestInflictDamage(t *testing.T) { Defense: 50, Speed: 100, }, - AvatarURL: fmt.Sprintf("https://example.com/%v", time.Now().Unix()), + AvatarURL: generateAvatarURL(), }, Enemy: entity.Monster{ ID: uuid.NewString(), - Name: fmt.Sprintf("enemy_%v", time.Now().Unix()), + Name: generateMonsterName(true), BattleStats: entity.BattleStats{ Health: 100, MaxHealth: 100, @@ -136,20 +136,71 @@ func TestInflictDamage(t *testing.T) { Defense: 100, Speed: 100, }, - AvatarURL: fmt.Sprintf("https://example.com/%v", time.Now().Unix()), + AvatarURL: generateAvatarURL(), }, ExpectedHealthAmount: 50, }, + { + Name: "Enemy Attack is Lower Than Monster Defense", + Monster: entity.Monster{ + ID: uuid.NewString(), + Name: generateMonsterName(false), + BattleStats: entity.BattleStats{ + Health: 100, + MaxHealth: 100, + Attack: 100, + Defense: 100, + Speed: 100, + }, + AvatarURL: generateAvatarURL(), + }, + Enemy: entity.Monster{ + ID: uuid.NewString(), + Name: generateMonsterName(true), + BattleStats: entity.BattleStats{ + Health: 100, + MaxHealth: 100, + Attack: 10, + Defense: 100, + Speed: 100, + }, + AvatarURL: generateAvatarURL(), + }, + ExpectedHealthAmount: 100 - entity.MinDamage, + }, } // execute test cases for _, testCase := range testCases { t.Run(testCase.Name, func(t *testing.T) { - _, err := testCase.Monster.InflictDamage(testCase.Enemy) - if err != nil { - t.Errorf("unable to inflict damage, due: %v", err) - } - assert.Equal(t, testCase.ExpectedHealthAmount, testCase.Monster.BattleStats.Health, "unexpected health amount") + testCase.Monster.InflictDamage(testCase.Enemy) + require.Equal(t, testCase.ExpectedHealthAmount, testCase.Monster.BattleStats.Health, "unexpected health amount") }) } } + +func TestResetBattleStats(t *testing.T) { + // create new monster + m := testutil.NewTestMonster() + + // set the monster health to 0 + m.BattleStats.Health = 0 + + // reset the battle stats + m.ResetBattleStats() + + // the monster health should be equal to max health + require.Equal(t, m.BattleStats.MaxHealth, m.BattleStats.Health, "unexpected health amount") +} + +func generateMonsterName(isEnemy bool) string { + prefix := "monster" + if isEnemy { + prefix = "enemy" + } + return fmt.Sprintf("%v_%v", prefix, uuid.NewString()) +} + +func generateAvatarURL() string { + return fmt.Sprintf("https://example.com/%v.jpg", uuid.NewString()) +} diff --git a/internal/core/service/battle/service.go b/internal/core/service/battle/service.go index 00a074b7..984347b4 100644 --- a/internal/core/service/battle/service.go +++ b/internal/core/service/battle/service.go @@ -123,14 +123,11 @@ func (s *service) DecideTurn(ctx context.Context, gameID string) (*entity.Battle if err != nil { return nil, err } - if battle.State != entity.StateDecideTurn { - return nil, ErrInvalidBattleState - } - _, err = battle.DecideTurn() + newState, err := battle.DecideTurn() if err != nil { - return nil, fmt.Errorf("unable to decide turn due: %w", err) + return nil, ErrInvalidBattleState } - if battle.State == entity.StateEnemyTurn { + if newState == entity.StateEnemyTurn { err = battle.EnemyAttack() if err != nil { return nil, fmt.Errorf("unable to make enemy attack due: %w", err) @@ -154,12 +151,9 @@ func (s *service) Attack(ctx context.Context, gameID string) (*entity.Battle, er if battle == nil { return nil, ErrBattleNotFound } - if battle.State != entity.StatePartnerTurn { - return nil, ErrInvalidBattleState - } err = battle.PartnerAttack() if err != nil { - return nil, fmt.Errorf("unable to decide turn due: %w", err) + return nil, ErrInvalidBattleState } err = s.battleStorage.SaveBattle(ctx, *battle) if err != nil { @@ -176,17 +170,19 @@ func (s *service) Attack(ctx context.Context, gameID string) (*entity.Battle, er } func (s *service) Surrender(ctx context.Context, gameID string) (*entity.Battle, error) { + // get existing battle battle, err := s.GetBattle(ctx, gameID) if err != nil { return nil, err } - if battle.State != entity.StatePartnerTurn { - return nil, ErrInvalidBattleState - } + + // make partner surrender in battle err = battle.PartnerSurrender() if err != nil { - return nil, fmt.Errorf("unable to decide turn due: %w", err) + return nil, ErrInvalidBattleState } + + // save battle err = s.battleStorage.SaveBattle(ctx, *battle) if err != nil { return nil, fmt.Errorf("unable to save battle due: %w", err) diff --git a/internal/core/service/battle/service_test.go b/internal/core/service/battle/service_test.go index 7f7cc198..37583f2d 100644 --- a/internal/core/service/battle/service_test.go +++ b/internal/core/service/battle/service_test.go @@ -18,12 +18,14 @@ package battle_test import ( "context" + "math" "testing" "time" "github.com/Haraj-backend/hex-monscape/internal/core/entity" "github.com/Haraj-backend/hex-monscape/internal/core/service/battle" "github.com/Haraj-backend/hex-monscape/internal/core/testutil" + "github.com/google/uuid" "github.com/stretchr/testify/require" ) @@ -210,113 +212,159 @@ func TestServiceGetBattle(t *testing.T) { } func TestServiceDecideTurn(t *testing.T) { - battleStorage := newMockBattleStorage() - gameStorage := newMockGameStorage() - monsterStorage := newMockMonsterStorage() + // init service + output := initService(t) + // create partner, we make it's speed to 0 so enemy will always attack first partner := testutil.NewTestMonster() + partner.BattleStats.Health = 100 + partner.BattleStats.Defense = 95 + partner.BattleStats.Speed = 0 + + // create enemy, we make it's speed to 100 so enemy will always attack first + enemy := testutil.NewTestMonster() + enemy.BattleStats.Health = 100 + enemy.BattleStats.Speed = 100 + enemy.BattleStats.Attack = 100 + + // create game game, err := entity.NewGame(entity.GameConfig{ PlayerName: "Riandy R.N", Partner: partner, CreatedAt: time.Now().Unix(), }) - if err != nil { - t.Fatalf("unable to init new game, due: %v", err) - } + require.NoError(t, err) - bt, _ := entity.NewBattle(entity.BattleConfig{ + // save game + err = output.GameStorage.SaveGame(context.Background(), *game) + require.NoError(t, err) + + // create battle + bt, err := entity.NewBattle(entity.BattleConfig{ GameID: game.ID, Partner: partner, - Enemy: testutil.NewTestMonster(), + Enemy: enemy, }) + require.NoError(t, err) - err = gameStorage.SaveGame(context.Background(), *game) - if err != nil { - t.Fatalf("unable to save game, due: %v", err) - } - err = battleStorage.SaveBattle(context.Background(), *bt) - if err != nil { - t.Fatalf("unable to save battle, due: %v", err) - } + // save battle + err = output.BattleStorage.SaveBattle(context.Background(), *bt) + require.NoError(t, err) - svc, err := battle.NewService(battle.ServiceConfig{ - GameStorage: gameStorage, - BattleStorage: battleStorage, - MonsterStorage: monsterStorage, - }) - if err != nil { - t.Fatalf("unable to init new service, due: %v", err) - } - bt, err = svc.DecideTurn(context.Background(), bt.GameID) - if err != nil { - t.Fatalf("unable to decide turn, due: %v", err) - } + // decide turn, should be enemy turn + bt, err = output.Service.DecideTurn(context.Background(), bt.GameID) + require.NoError(t, err) - storedBattle, err := battleStorage.GetBattle(context.Background(), bt.GameID) + // check returned battle state, in here we expect partner reduced to 100 - (100 - 5) = 95 + storedBattle, err := output.BattleStorage.GetBattle(context.Background(), bt.GameID) require.NoError(t, err, "unable to get stored battle") - require.Equal(t, bt, storedBattle, "invalid battle stored") + require.NotNil(t, storedBattle, "stored battle is nil") + require.Equal(t, storedBattle.Partner.BattleStats.Health, 95, "invalid partner health") + + // the battle state will be DECIDE_TURN again + require.Equal(t, entity.StateDecideTurn, storedBattle.State, "invalid battle state") + + // update battle state to something else + bt.State = entity.StatePartnerTurn + err = output.BattleStorage.SaveBattle(context.Background(), *bt) + require.NoError(t, err) + + // decide turn, this time we expect error + _, err = output.Service.DecideTurn(context.Background(), bt.GameID) + require.ErrorIs(t, err, battle.ErrInvalidBattleState) } func TestServiceAttack(t *testing.T) { - battleStorage := newMockBattleStorage() - gameStorage := newMockGameStorage() - monsterStorage := newMockMonsterStorage() - + // init service + output := initService(t) + + // create partner & enemy + stdBattleStats := entity.BattleStats{ + Health: 100, + MaxHealth: 100, + Attack: 100, + Defense: 100, + Speed: 100, + } partner := testutil.NewTestMonster() + partner.BattleStats = stdBattleStats + + enemy := testutil.NewTestMonster() + enemy.BattleStats = stdBattleStats + + // create game game, err := entity.NewGame(entity.GameConfig{ PlayerName: "Riandy R.N", Partner: partner, CreatedAt: time.Now().Unix(), }) - if err != nil { - t.Fatalf("unable to init new game, due: %v", err) - } + require.NoError(t, err, "unable to init new game") + + // save game + err = output.GameStorage.SaveGame(context.Background(), *game) + require.NoError(t, err, "unable to save game") + + // perform attack without creating battle, should return error + _, err = output.Service.Attack(context.Background(), game.ID) + require.ErrorIs(t, err, battle.ErrBattleNotFound) + // create battle with state partner turn bt, _ := entity.NewBattle(entity.BattleConfig{ GameID: game.ID, Partner: partner, - Enemy: testutil.NewTestMonster(), + Enemy: enemy, }) bt.State = entity.StatePartnerTurn - err = gameStorage.SaveGame(context.Background(), *game) - if err != nil { - t.Fatalf("unable to save game, due: %v", err) - } - err = battleStorage.SaveBattle(context.Background(), *bt) - if err != nil { - t.Fatalf("unable to save battle, due: %v", err) - } + // save battle + err = output.BattleStorage.SaveBattle(context.Background(), *bt) + require.NoError(t, err, "unable to save battle") - svc, err := battle.NewService(battle.ServiceConfig{ - GameStorage: gameStorage, - BattleStorage: battleStorage, - MonsterStorage: monsterStorage, - }) - if err != nil { - t.Fatalf("unable to init new service, due: %v", err) - } - _, err = svc.Attack(context.Background(), bt.GameID) - if err != nil { - t.Fatalf("unable to attack, due: %v", err) - } + // perform attack in battle with invalid game id, should return error + _, err = output.Service.Attack(context.Background(), uuid.NewString()) + require.ErrorIs(t, err, battle.ErrGameNotFound) + + // perform attack in battle, it should be successful + _, err = output.Service.Attack(context.Background(), bt.GameID) + require.NoError(t, err, "unable to attack") + + // modify the partner attack to make the enemy die instantly + bt.Partner.BattleStats.Attack = math.MaxInt64 + bt.State = entity.StatePartnerTurn + + // save battle + err = output.BattleStorage.SaveBattle(context.Background(), *bt) + require.NoError(t, err, "unable to save battle") + + // perform attack in battle, partner should win immediately + updBattle, err := output.Service.Attack(context.Background(), bt.GameID) + require.NoError(t, err, "unable to attack") + require.Equal(t, updBattle.State, entity.StateWin, "invalid battle state") + + // check if the game battle won is incremented + updGame, err := output.GameStorage.GetGame(context.Background(), game.ID) + require.NoError(t, err, "unable to get game") + require.Equal(t, 1, updGame.BattleWon, "invalid battle won") } func TestServiceSurrender(t *testing.T) { - battleStorage := newMockBattleStorage() - gameStorage := newMockGameStorage() - monsterStorage := newMockMonsterStorage() + // init service + output := initService(t) + // create game partner := testutil.NewTestMonster() game, err := entity.NewGame(entity.GameConfig{ PlayerName: "Riandy R.N", Partner: partner, CreatedAt: time.Now().Unix(), }) - if err != nil { - t.Fatalf("unable to init new game, due: %v", err) - } + require.NoError(t, err, "unable to init new game") + // save game + err = output.GameStorage.SaveGame(context.Background(), *game) + require.NoError(t, err, "unable to save game") + + // create battle with state partner turn bt, _ := entity.NewBattle(entity.BattleConfig{ GameID: game.ID, Partner: partner, @@ -324,30 +372,62 @@ func TestServiceSurrender(t *testing.T) { }) bt.State = entity.StatePartnerTurn - err = gameStorage.SaveGame(context.Background(), *game) - if err != nil { - t.Fatalf("unable to save game, due: %v", err) - } - err = battleStorage.SaveBattle(context.Background(), *bt) - if err != nil { - t.Fatalf("unable to save battle, due: %v", err) - } + // save battle + err = output.BattleStorage.SaveBattle(context.Background(), *bt) + require.NoError(t, err, "unable to save battle") + + // surrender battle + surrenderBattle, err := output.Service.Surrender(context.Background(), bt.GameID) + require.NoError(t, err, "unable to surrender battle") + + // assert returned battle state + expectedBattle := bt + expectedBattle.State = entity.StateLose + require.Equal(t, expectedBattle, surrenderBattle, "invalid battle stored") + + // change battle state to enemy turn + bt.State = entity.StateEnemyTurn + + // update battle + err = output.BattleStorage.SaveBattle(context.Background(), *bt) + require.NoError(t, err, "unable to save battle") + + // surrender battle, this time we expect error + _, err = output.Service.Surrender(context.Background(), bt.GameID) + require.ErrorIs(t, err, battle.ErrInvalidBattleState) + + // try to surrender with invalid game id, should return error + _, err = output.Service.Surrender(context.Background(), uuid.NewString()) + require.ErrorIs(t, err, battle.ErrGameNotFound) +} + +type initServiceOutput struct { + Service battle.Service + BattleStorage *mockBattleStorage + GameStorage *mockGameStorage + MonsterStorage *mockMockStorage +} + +func initService(t *testing.T) *initServiceOutput { + // init dependencies + battleStorage := newMockBattleStorage() + gameStorage := newMockGameStorage() + monsterStorage := newMockMonsterStorage() + // init service svc, err := battle.NewService(battle.ServiceConfig{ GameStorage: gameStorage, BattleStorage: battleStorage, MonsterStorage: monsterStorage, }) - if err != nil { - t.Fatalf("unable to init new service, due: %v", err) - } - surrenderBattle, err := svc.Surrender(context.Background(), bt.GameID) - if err != nil { - t.Fatalf("unable to surrender, due: %v", err) + require.NoError(t, err) + + return &initServiceOutput{ + Service: svc, + BattleStorage: battleStorage, + GameStorage: gameStorage, + MonsterStorage: monsterStorage, } - expectedBattle := bt - expectedBattle.State = entity.StateLose - require.Equal(t, expectedBattle, surrenderBattle, "invalid battle stored") } type mockGameStorage struct { diff --git a/internal/core/service/play/service_test.go b/internal/core/service/play/service_test.go index 92d4fea3..05a7ce4b 100644 --- a/internal/core/service/play/service_test.go +++ b/internal/core/service/play/service_test.go @@ -18,11 +18,13 @@ package play_test import ( "context" + "errors" "testing" "github.com/Haraj-backend/hex-monscape/internal/core/entity" "github.com/Haraj-backend/hex-monscape/internal/core/service/play" "github.com/Haraj-backend/hex-monscape/internal/core/testutil" + "github.com/google/uuid" "github.com/stretchr/testify/require" ) @@ -74,34 +76,71 @@ func TestNewService(t *testing.T) { func TestServiceGetAvailablePartners(t *testing.T) { // initialize new service output := newService() + // get available partners retPartners, err := output.Service.GetAvailablePartners(context.Background()) require.NoError(t, err, "unexpected error") + // check returned partners require.ElementsMatch(t, output.Partners, retPartners, "mismatch partners") + + // set error on get available partners + output.PartnerStorage.SetRetErrOnGetAvailablePartners(true) + + // get available partners, should return error + _, err = output.Service.GetAvailablePartners(context.Background()) + require.Error(t, err, "expected error") } func TestServiceNewGame(t *testing.T) { // initialize new service output := newService() + // create new game partner := output.Partners[0] game, err := output.Service.NewGame(context.Background(), "Riandy R.N", partner.ID) require.NoError(t, err, "unexpected error") + // validate returned game with stored game, this is to make sure the game // is also stored on storage storedGame, err := output.GameStorage.GetGame(context.Background(), game.ID) require.NoError(t, err, "unexpected error") require.Equal(t, *game, *storedGame, "mismatch game") + + // create new game with invalid partner, should return error + game, err = output.Service.NewGame(context.Background(), "Riandy R.N", uuid.NewString()) + require.Equal(t, play.ErrPartnerNotFound, err, "mismatch error") + require.Nil(t, game, "unexpected game") + + // create new game with empty player name, should return error + game, err = output.Service.NewGame(context.Background(), "", partner.ID) + require.Error(t, err, "expected error") + require.Nil(t, game, "unexpected game") + + // set error on save game, should return error + output.GameStorage.SetRetErrOnSaveGame(true) + game, err = output.Service.NewGame(context.Background(), "Riandy R.N", partner.ID) + output.GameStorage.SetRetErrOnSaveGame(false) + require.Error(t, err, "expected error") + require.Nil(t, game, "unexpected game") + + // set error on get partner, should return error + output.PartnerStorage.SetRetErrOnGetPartner(true) + game, err = output.Service.NewGame(context.Background(), "Riandy R.N", partner.ID) + output.PartnerStorage.SetRetErrOnGetPartner(false) + require.Error(t, err, "expected error") + require.Nil(t, game, "unexpected game") } func TestServiceGetGame(t *testing.T) { // initialize new service output := newService() + // create new game partner := output.Partners[0] game, err := output.Service.NewGame(context.Background(), "Riandy R.N", partner.ID) require.NoError(t, err, "unexpected error") + // define test cases testCases := []struct { Name string @@ -130,6 +169,12 @@ func TestServiceGetGame(t *testing.T) { require.Equal(t, game, retGame, "mismatch game") }) } + + // set error on get game, should return error + output.GameStorage.SetRetErrOnGetGame(true) + game, err = output.Service.GetGame(context.Background(), game.ID) + require.Error(t, err, "expected error") + require.Nil(t, game, "unexpected game") } func newService() *newServiceOutput { @@ -140,33 +185,52 @@ func newService() *newServiceOutput { *(testutil.NewTestMonster()), *(testutil.NewTestMonster()), } + + // initialize dependencies + gameStorage := newMockGameStorage() + partnerStorage := newMockPartnerStorage(partners) + // initialize service cfg := play.ServiceConfig{ - GameStorage: newMockGameStorage(), - PartnerStorage: newMockPartnerStorage(partners), + GameStorage: gameStorage, + PartnerStorage: partnerStorage, } svc, _ := play.NewService(cfg) return &newServiceOutput{ Service: svc, - GameStorage: cfg.GameStorage, - PartnerStorage: cfg.PartnerStorage, + GameStorage: gameStorage, + PartnerStorage: partnerStorage, Partners: partners, } } type newServiceOutput struct { Service play.Service - GameStorage play.GameStorage - PartnerStorage play.PartnerStorage + GameStorage *mockGameStorage + PartnerStorage *mockPartnerStorage Partners []entity.Monster } type mockGameStorage struct { - data map[string]entity.Game + data map[string]entity.Game + retErrOnGetGame bool + retErrOnSaveGame bool +} + +func (gs *mockGameStorage) SetRetErrOnGetGame(retErr bool) { + gs.retErrOnGetGame = retErr +} + +func (gs *mockGameStorage) SetRetErrOnSaveGame(retErr bool) { + gs.retErrOnSaveGame = retErr } func (gs *mockGameStorage) GetGame(ctx context.Context, gameID string) (*entity.Game, error) { + if gs.retErrOnGetGame { + return nil, ErrIntentionalError + } + game, ok := gs.data[gameID] if !ok { return nil, nil @@ -175,6 +239,10 @@ func (gs *mockGameStorage) GetGame(ctx context.Context, gameID string) (*entity. } func (gs *mockGameStorage) SaveGame(ctx context.Context, game entity.Game) error { + if gs.retErrOnSaveGame { + return ErrIntentionalError + } + gs.data[game.ID] = game return nil } @@ -186,10 +254,23 @@ func newMockGameStorage() *mockGameStorage { } type mockPartnerStorage struct { - partnerMap map[string]entity.Monster + partnerMap map[string]entity.Monster + retErrOnGetAvailablePartners bool + retErrOnGetPartner bool +} + +func (gs *mockPartnerStorage) SetRetErrOnGetAvailablePartners(retErr bool) { + gs.retErrOnGetAvailablePartners = retErr +} + +func (gs *mockPartnerStorage) SetRetErrOnGetPartner(retErr bool) { + gs.retErrOnGetPartner = retErr } func (gs *mockPartnerStorage) GetAvailablePartners(ctx context.Context) ([]entity.Monster, error) { + if gs.retErrOnGetAvailablePartners { + return nil, ErrIntentionalError + } var partners []entity.Monster for _, v := range gs.partnerMap { partners = append(partners, v) @@ -198,6 +279,9 @@ func (gs *mockPartnerStorage) GetAvailablePartners(ctx context.Context) ([]entit } func (gs *mockPartnerStorage) GetPartner(ctx context.Context, partnerID string) (*entity.Monster, error) { + if gs.retErrOnGetPartner { + return nil, ErrIntentionalError + } partner, ok := gs.partnerMap[partnerID] if !ok { return nil, nil @@ -212,3 +296,5 @@ func newMockPartnerStorage(partners []entity.Monster) *mockPartnerStorage { } return &mockPartnerStorage{partnerMap: data} } + +var ErrIntentionalError = errors.New("intentional error") diff --git a/internal/driven/storage/memory/battlestrg/storage.go b/internal/driven/storage/memory/battlestrg/storage.go index 1da9be87..0feb5e6a 100644 --- a/internal/driven/storage/memory/battlestrg/storage.go +++ b/internal/driven/storage/memory/battlestrg/storage.go @@ -11,10 +11,7 @@ type Storage struct { } func (s *Storage) GetBattle(ctx context.Context, gameID string) (*entity.Battle, error) { - b, ok := s.data[gameID] - if !ok { - return nil, nil - } + b := s.data[gameID] return &b, nil } diff --git a/internal/driven/storage/memory/gamestrg/storage.go b/internal/driven/storage/memory/gamestrg/storage.go index 203e0772..21a0431f 100644 --- a/internal/driven/storage/memory/gamestrg/storage.go +++ b/internal/driven/storage/memory/gamestrg/storage.go @@ -11,10 +11,7 @@ type Storage struct { } func (s *Storage) GetGame(ctx context.Context, gameID string) (*entity.Game, error) { - g, ok := s.data[gameID] - if !ok { - return nil, nil - } + g := s.data[gameID] return &g, nil } diff --git a/internal/driven/storage/memory/monstrg/storage_test.go b/internal/driven/storage/memory/monstrg/storage_test.go index bd131459..ab6cb9cb 100644 --- a/internal/driven/storage/memory/monstrg/storage_test.go +++ b/internal/driven/storage/memory/monstrg/storage_test.go @@ -10,6 +10,45 @@ import ( "github.com/stretchr/testify/require" ) +func TestNew(t *testing.T) { + testCases := []struct { + Name string + Config monstrg.Config + ExpStorage bool + ExpErr bool + }{ + { + Name: "Empty Monster Data in Config", + Config: monstrg.Config{}, + ExpStorage: false, + ExpErr: true, + }, + { + Name: "Invalid Monster Data in Config", + Config: monstrg.Config{ + MonsterData: []byte(`invalid`), + }, + ExpStorage: false, + ExpErr: true, + }, + { + Name: "Valid Config", + Config: monstrg.Config{ + MonsterData: monsterData, + }, + ExpStorage: true, + ExpErr: false, + }, + } + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + storage, err := monstrg.New(testCase.Config) + assert.Equal(t, testCase.ExpStorage, storage != nil, "unexpected storage") + assert.Equal(t, testCase.ExpErr, err != nil, "unexpected error") + }) + } +} + func TestGetAvailablePartners(t *testing.T) { // init storage strg := initStorage(t)