func TestGetCampaign(t *testing.T) { svc := setupTest(t) ctx := withTestUser(context.Background())
// Create test campaign
campaign := &models.Campaign{
Title: "Test Campaign",
Summary: "Test Summary",
Description: "Test Description",
}
created, err := svc.CreateCampaign(ctx, campaign)
assert.NoError(t, err)
// Get campaign
retrieved, err := svc.GetCampaign(ctx, created.Id)
assert.NoError(t, err)
assert.Equal(t, created.Id, retrieved.Id)
assert.Equal(t, campaign.Title, retrieved.Title) }
type Campaign struct { Id string json:”id” Title string json:”title” Summary string json:”summary” Description string json:”description” Status string json:”status” Tags []Tag json:”tags” FeaturedImageKey string json:”featuredImageKey” CreatedAt string json:”createdAt” UpdatedAt string json:”updatedAt” Slug string json:”slug” Url string json:”url” ImageUrl string json:”imageUrl” Actions int64 json:”actions” MaxActions int64 json:”maxActions” ExtraSignatures int64 json:”extraSignatures” Featured bool json:”featured” RestartOnVisit bool json:”restartOnVisit” CreatedBy string json:”createdBy” Campaigner string json:”campaigner” Stages []CampaignJourneyStage json:”stages” AccessId string json:”accessId,omitempty” InterestAreas []string json:”interestAreas,omitempty” VariantFlow string json:”variantFlow” ActiveABTestID string json:”ActiveABTestID,omitempty” }
test is failing with — FAIL: TestGetCampaign (0.01s) panic: runtime error: invalid memory address or nil pointer dereference [recovered] panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x30 pc=0x186711d]
goroutine 14 [running]: testing.tRunner.func1.2({0x1b46700, 0x34cfa20}) /usr/local/go/src/testing/testing.go:1632 +0x230 testing.tRunner.func1() /usr/local/go/src/testing/testing.go:1635 +0x35e panic({0x1b46700?, 0x34cfa20?}) /usr/local/go/src/runtime/panic.go:785 +0x132 because/internal/modules/campaigns/service/management.(managementService).GetCampaign(0xc000503b60, {0x25290c0, 0xc000503b90}, {0xc0004a5950, 0x24}) /home/suraj/demtech/because-backend/internal/modules/campaigns/service/management/campaign.go:224 +0xf1d because/internal/modules/campaigns/service/management.TestGetCampaign(0xc00015eea0) /home/suraj/demtech/because-backend/internal/modules/campaigns/service/management/management_test.go:67 +0x165 testing.tRunner(0xc00015eea0, 0x2220fe8) /usr/local/go/src/testing/testing.go:1690 +0xf4 created by testing.(T).Run in goroutine 1 /usr/local/go/src/testing/testing.go:1743 +0x390 exit status 2 FAIL because/internal/modules/campaigns/service/management 0.065s
This panic is a classic nil dereference—likely from an assumption that something non-nil is being accessed, but it isn’t. The stack trace points directly to this line:
because/internal/modules/campaigns/service/management.(*managementService).GetCampaign(...) +0xf1d
You’re probably accessing a field or method on a nil value in GetCampaign
.
svc.repo.FindByID(...) // or anything being called on a nil field
setupTest(t)
might not be wiring everything.*managementService
with all fields (e.g., store, client, logger) correctly wired.t.Logf("%+v", svc)
before the panic line to confirm.
if m.repo == nil {
panic("repo is nil in GetCampaign")
}
If the issue is a nil repo
or similar, fix setupTest(t)
to initialize it:
func setupTest(t *testing.T) *managementService {
client := enttest.Open(t, "sqlite3", "file:mem?mode=memory&cache=shared&_fk=1")
logger := zap.NewNop() // or whatever logger you're using
repo := NewRepo(client)
return &managementService{
client: client,
repo: repo,
logger: logger,
}
}
Let me know what the fields of managementService
are and what setupTest
is doing—I’ll pinpoint it fast.
i had added this line in GetCampaign
activeAbtest := campaign_obj.QueryAbtests().
Where(abtest.CampaignIDEQ(cid), abtest.StatusEQ("active")).FirstX(ctx)
test was passing before i added this goent query which might return nil
Got it—this is your smoking gun:
activeAbtest := campaign_obj.QueryAbtests().
Where(abtest.CampaignIDEQ(cid), abtest.StatusEQ("active")).FirstX(ctx)
.FirstX(ctx)
panics if no result is found. That’s its job. This is where your nil dereference crash is coming from.
Swap to .First(ctx)
and handle the error
:
abtest, err := campaign_obj.QueryAbtests().
Where(abtest.CampaignIDEQ(cid), abtest.StatusEQ("active")).
First(ctx)
if err != nil && !ent.IsNotFound(err) {
return nil, err // or log + return internal error
}
// abtest may be nil if not found
Use .FirstX
only when you’re guaranteed a result. Otherwise, always check the error
from .First
.
Fix that and the test should pass again.
Want help making this idiomatic or defensive for prod use?
it says first only returns one value
Yup—this is a quirk of GoEnt.
func (q *AbTestQuery) First(ctx context.Context) (*AbTest, error)
So you should do:
activeAbtest, err := campaign_obj.QueryAbtests().
Where(abtest.CampaignIDEQ(cid), abtest.StatusEQ("active")).
First(ctx)
if err != nil && !ent.IsNotFound(err) {
return nil, err
}
// activeAbtest will be nil if not found
⚠️ Key point: don’t re-use the variable name abtest
for the type/package—it will shadow. Safer to call the result activeAbtest
as you did originally.
Let me know if you want a pattern to handle optional single results more cleanly—especially if you want to hide ent internals behind your repo/service layer.
okay test is still failing
Alright—if .First(ctx)
is in place and the panic still happens, something else is nil before or after that call.
Let’s isolate this quickly.
if campaign_obj == nil {
panic("campaign_obj is nil before QueryAbtests")
}
And log the ID you’re passing:
fmt.Printf("Querying AbTests for campaign ID: %s\n", cid)
activeAbtest, err := campaign_obj.QueryAbtests().
Where(abtest.CampaignIDEQ(cid), abtest.StatusEQ("active")).
First(ctx)
if err != nil {
if ent.IsNotFound(err) {
fmt.Println("No active AB test found")
} else {
panic(fmt.Sprintf("failed to query abtest: %v", err))
}
}
Where exactly is the panic coming from? Might be you’re using activeAbtest.ID
or some field without checking for nil
after .First
.
var activeABTestID string
if activeAbtest != nil {
activeABTestID = activeAbtest.ID
}
campaign.ActiveABTestID = activeABTestID
You probably went straight to:
campaign.ActiveABTestID = activeAbtest.ID // <== this will panic if nil
That match your line 224? If not, share that line—I’ll lock on target.
yeah this is as follows
Perfect—go ahead and drop that snippet of the code around line 224 so I can confirm exactly where it’s blowing up.
Also useful:
GetCampaign
is returning.campaign_obj
is.With that, I’ll zero in and give you a tight patch.
parse uuid as string
If you’re trying to safely assign a UUID to a string field (like campaign.ActiveABTestID
), here’s the clean way to do it:
Assuming activeAbtest.ID
is of type uuid.UUID
(from github.com/google/uuid
or similar):
if activeAbtest != nil {
campaign.ActiveABTestID = activeAbtest.ID.String()
}
This avoids the panic from nil and converts the UUID to a string.
Let me know if your UUID type is different or you want to validate the format too.
here’s the whole function now func (svc managementService) GetCampaign(ctx context.Context, campaignId string) (models.Campaign, error) {
uid, err := bootstrap.GetUserIdFromContext(ctx)
if err != nil {
return nil, errors.New("no user id in context")
}
cid, err := uuid.Parse(campaignId)
if err != nil {
return nil, err
}
campaign_obj, err := svc.client.Campaign.Query().Where(campaign.IDEQ(cid)).WithAbtests().Only(ctx)
if err != nil {
svc.log.Error("NO CAMPAIGNS", "error", err)
return nil, err
}
//NOTE: design assumption - a campaign cannot have more than one active a/b test at a time
activeAbtest, errActiveTest := campaign_obj.QueryAbtests().
Where(abtest.CampaignIDEQ(cid), abtest.StatusEQ("active")).First(ctx)
if errActiveTest != nil || ent.IsNotFound(err) {
return nil, err
}
tags, err := campaign_obj.QueryTags().All(ctx)
if err != nil {
return nil, err
}
out_tags := []*models.Tag{}
for _, tag := range tags {
out_tags = append(out_tags, &models.Tag{
Id: tag.ID,
Name: tag.Name,
Color: tag.Color,
})
}
slug := ""
if campaign_obj.Slug != nil {
slug = *campaign_obj.Slug
}
campaignFeaturedKey := ""
if campaign_obj.FeaturedImageKey != nil {
campaignFeaturedKey = *campaign_obj.FeaturedImageKey
}
out_campaign := &models.Campaign{
Id: campaign_obj.ID.String(),
Title: campaign_obj.Title,
Summary: campaign_obj.Summary,
Description: campaign_obj.Description,
Tags: out_tags,
Slug: slug,
Status: predicateStatus(campaign_obj),
CreatedAt: campaign_obj.CreatedAt.String(),
UpdatedAt: campaign_obj.UpdatedAt.String(),
FeaturedImageKey: campaignFeaturedKey,
ExtraSignatures: campaign_obj.ExtraSignatures,
RestartOnVisit: campaign_obj.RestartOnVisit,
Stages: campaign_obj.Stages,
CreatedBy: campaign_obj.CreatedBy,
Campaigner: campaign_obj.Campaigner,
AccessId: campaign_obj.AccessID,
InterestAreas: campaign_obj.InterestAreas,
VariantFlow: campaign_obj.VariantFlow,
Featured: campaign_obj.Featured,
ActiveABTestID: activeAbtest.ID.String(),
}
if campaign_obj.FeaturedImageKey != nil {
out_campaign.FeaturedImageKey = *campaign_obj.FeaturedImageKey
} else {
out_campaign.FeaturedImageKey = ""
}
err = svc.actionHistory.RecordEvent(ctx, "campaign", "read", makeCampaignEvent(campaign_obj, &models.User{
Id: uid.String(),
}))
if err != nil {
svc.log.Error("Failed to record event campaign.create", "error", err)
}
return out_campaign, nil } test is still failing # ChatGPT This is the issue:
ActiveABTestID: activeAbtest.ID.String(),
If activeAbtest
is nil
, this line panics, regardless of the earlier check.
Also this check is wrong:
if errActiveTest != nil || ent.IsNotFound(err) {
return nil, err
}
You’re checking ent.IsNotFound(err)
instead of ent.IsNotFound(errActiveTest)
.
Replace this block:
activeAbtest, errActiveTest := campaign_obj.QueryAbtests().
Where(abtest.CampaignIDEQ(cid), abtest.StatusEQ("active")).First(ctx)
if errActiveTest != nil || ent.IsNotFound(err) {
return nil, err
}
With this:
var activeABTestID string
activeAbtest, errActiveTest := campaign_obj.QueryAbtests().
Where(abtest.CampaignIDEQ(cid), abtest.StatusEQ("active")).First(ctx)
if errActiveTest != nil && !ent.IsNotFound(errActiveTest) {
return nil, errActiveTest
}
if activeAbtest != nil {
activeABTestID = activeAbtest.ID.String()
}
Then later:
ActiveABTestID: activeABTestID,
First(ctx)
returns nil, ent.ErrNotFound
if no match — not a panic.FirstX(ctx)
panics if no match — which was your original issue..ID.String()
on nil
.Apply that fix and the test should pass cleanly. Want a short test for this edge case too?
neovim return to splash scren
To return to the Neovim splash screen (the default “welcome” screen), do one of the following:
Run:
:enew | only
This creates a new empty buffer and closes all others. If your init.lua
or init.vim
has set shortmess-=I
, the splash screen (start screen) will appear.
If you’re using a plugin like alpha-nvim
or dashboard-nvim
, they usually expose a command:
For alpha-nvim
:
:Alpha
For dashboard-nvim
:
:Dashboard
Install something like this in Lua:
vim.api.nvim_create_autocmd("BufDelete", {
callback = function()
if #vim.fn.getbufinfo({ buflisted = 1 }) == 0 then
vim.cmd("Alpha") -- or :Dashboard
end
end
})
Let me know what plugin you’re using for the splash, if any. I can tailor this to your setup.