Skip to content

Commit 6f3cfe9

Browse files
committed
feat: allow querying questionCategories
1 parent fedb95d commit 6f3cfe9

File tree

3 files changed

+234
-2
lines changed

3 files changed

+234
-2
lines changed

graph/question.graphqls

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ extend type Query {
1616
otherwise, you can only get your own submissions.
1717
"""
1818
submission(id: ID!): Submission!
19+
20+
"""
21+
Get the list of question categories.
22+
"""
23+
questionCategories: [String!]! @scope(scope: "question:read")
1924
}
2025

2126
extend type Mutation {

graph/question.resolvers.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

graph/question_resolver_test.go

Lines changed: 214 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ import (
2323
// createTestDatabase creates a test database entity
2424
func createTestDatabase(t *testing.T, entClient *ent.Client) *ent.Database {
2525
t.Helper()
26+
// Generate unique slug and relation figure to avoid UNIQUE constraint violations
27+
uniqueID := strconv.FormatInt(time.Now().UnixNano(), 10)
2628
database, err := entClient.Database.Create().
27-
SetSlug("test-db").
29+
SetSlug("test-db-" + uniqueID).
2830
SetDescription("Test Database").
2931
SetSchema("CREATE TABLE test (id INT, name VARCHAR(255));").
30-
SetRelationFigure("test-relation-figure").
32+
SetRelationFigure("test-relation-figure-" + uniqueID).
3133
Save(context.Background())
3234
require.NoError(t, err)
3335
return database
@@ -688,3 +690,213 @@ func TestQuestionResolver_Solved(t *testing.T) {
688690
require.Contains(t, err.Error(), defs.CodeUnauthorized)
689691
})
690692
}
693+
694+
func TestQueryResolver_QuestionCategories(t *testing.T) {
695+
entClient := testhelper.NewEntSqliteClient(t)
696+
resolver := NewTestResolver(t, entClient, &mockAuthStorage{})
697+
cfg := Config{
698+
Resolvers: resolver,
699+
Directives: DirectiveRoot{Scope: directive.ScopeDirective},
700+
}
701+
srv := handler.New(NewExecutableSchema(cfg))
702+
srv.AddTransport(transport.POST{})
703+
gqlClient := client.New(srv)
704+
705+
// Create test group and user
706+
group, err := createTestGroup(t, entClient)
707+
require.NoError(t, err)
708+
709+
testUser, err := entClient.User.Create().
710+
SetName("testUser").
711+
SetEmail("test@example.com").
712+
SetGroup(group).
713+
Save(context.Background())
714+
require.NoError(t, err)
715+
716+
t.Run("success - returns empty array when no questions", func(t *testing.T) {
717+
var resp struct {
718+
QuestionCategories []string
719+
}
720+
query := `query { questionCategories }`
721+
722+
err := gqlClient.Post(query, &resp, func(bd *client.Request) {
723+
bd.HTTP = bd.HTTP.WithContext(auth.WithUser(bd.HTTP.Context(), auth.TokenInfo{
724+
UserID: testUser.ID,
725+
Scopes: []string{"question:read"},
726+
}))
727+
})
728+
require.NoError(t, err)
729+
require.NotNil(t, resp.QuestionCategories)
730+
require.Len(t, resp.QuestionCategories, 0)
731+
})
732+
733+
t.Run("success - returns single category", func(t *testing.T) {
734+
// Create test database
735+
database := createTestDatabase(t, entClient)
736+
737+
// Create question with category
738+
_, err := entClient.Question.Create().
739+
SetCategory("basic-select").
740+
SetDifficulty("easy").
741+
SetTitle("Test Query 1").
742+
SetDescription("Write a SELECT query").
743+
SetReferenceAnswer("SELECT * FROM test;").
744+
SetDatabase(database).
745+
Save(context.Background())
746+
require.NoError(t, err)
747+
748+
var resp struct {
749+
QuestionCategories []string
750+
}
751+
query := `query { questionCategories }`
752+
753+
err = gqlClient.Post(query, &resp, func(bd *client.Request) {
754+
bd.HTTP = bd.HTTP.WithContext(auth.WithUser(bd.HTTP.Context(), auth.TokenInfo{
755+
UserID: testUser.ID,
756+
Scopes: []string{"question:read"},
757+
}))
758+
})
759+
require.NoError(t, err)
760+
require.Len(t, resp.QuestionCategories, 1)
761+
require.Contains(t, resp.QuestionCategories, "basic-select")
762+
})
763+
764+
t.Run("success - returns unique categories when questions have duplicate categories", func(t *testing.T) {
765+
// Create test database
766+
database := createTestDatabase(t, entClient)
767+
768+
// Create multiple questions with same category
769+
_, err := entClient.Question.Create().
770+
SetCategory("joins").
771+
SetDifficulty("medium").
772+
SetTitle("Join Query 1").
773+
SetDescription("Write a JOIN query").
774+
SetReferenceAnswer("SELECT * FROM test t1 JOIN test t2 ON t1.id = t2.id;").
775+
SetDatabase(database).
776+
Save(context.Background())
777+
require.NoError(t, err)
778+
779+
_, err = entClient.Question.Create().
780+
SetCategory("joins").
781+
SetDifficulty("medium").
782+
SetTitle("Join Query 2").
783+
SetDescription("Write another JOIN query").
784+
SetReferenceAnswer("SELECT * FROM test t1 LEFT JOIN test t2 ON t1.id = t2.id;").
785+
SetDatabase(database).
786+
Save(context.Background())
787+
require.NoError(t, err)
788+
789+
var resp struct {
790+
QuestionCategories []string
791+
}
792+
query := `query { questionCategories }`
793+
794+
err = gqlClient.Post(query, &resp, func(bd *client.Request) {
795+
bd.HTTP = bd.HTTP.WithContext(auth.WithUser(bd.HTTP.Context(), auth.TokenInfo{
796+
UserID: testUser.ID,
797+
Scopes: []string{"question:read"},
798+
}))
799+
})
800+
require.NoError(t, err)
801+
// Should have at least one category (joins)
802+
require.GreaterOrEqual(t, len(resp.QuestionCategories), 1)
803+
require.Contains(t, resp.QuestionCategories, "joins")
804+
805+
// Count occurrences of "joins" - should only appear once
806+
joinCount := 0
807+
for _, cat := range resp.QuestionCategories {
808+
if cat == "joins" {
809+
joinCount++
810+
}
811+
}
812+
require.Equal(t, 1, joinCount, "Category 'joins' should appear exactly once")
813+
})
814+
815+
t.Run("success - returns multiple different categories", func(t *testing.T) {
816+
// Create test database
817+
database := createTestDatabase(t, entClient)
818+
819+
// Create questions with different categories
820+
_, err := entClient.Question.Create().
821+
SetCategory("aggregation").
822+
SetDifficulty("easy").
823+
SetTitle("Aggregation Query").
824+
SetDescription("Use aggregation functions").
825+
SetReferenceAnswer("SELECT COUNT(*) FROM test;").
826+
SetDatabase(database).
827+
Save(context.Background())
828+
require.NoError(t, err)
829+
830+
_, err = entClient.Question.Create().
831+
SetCategory("subqueries").
832+
SetDifficulty("hard").
833+
SetTitle("Subquery Challenge").
834+
SetDescription("Use subqueries").
835+
SetReferenceAnswer("SELECT * FROM test WHERE id IN (SELECT id FROM test);").
836+
SetDatabase(database).
837+
Save(context.Background())
838+
require.NoError(t, err)
839+
840+
var resp struct {
841+
QuestionCategories []string
842+
}
843+
query := `query { questionCategories }`
844+
845+
err = gqlClient.Post(query, &resp, func(bd *client.Request) {
846+
bd.HTTP = bd.HTTP.WithContext(auth.WithUser(bd.HTTP.Context(), auth.TokenInfo{
847+
UserID: testUser.ID,
848+
Scopes: []string{"question:read"},
849+
}))
850+
})
851+
require.NoError(t, err)
852+
require.GreaterOrEqual(t, len(resp.QuestionCategories), 2)
853+
require.Contains(t, resp.QuestionCategories, "aggregation")
854+
require.Contains(t, resp.QuestionCategories, "subqueries")
855+
})
856+
857+
t.Run("success - works with wildcard scope", func(t *testing.T) {
858+
var resp struct {
859+
QuestionCategories []string
860+
}
861+
query := `query { questionCategories }`
862+
863+
err := gqlClient.Post(query, &resp, func(bd *client.Request) {
864+
bd.HTTP = bd.HTTP.WithContext(auth.WithUser(bd.HTTP.Context(), auth.TokenInfo{
865+
UserID: testUser.ID,
866+
Scopes: []string{"*"},
867+
}))
868+
})
869+
require.NoError(t, err)
870+
require.NotNil(t, resp.QuestionCategories)
871+
// At this point we should have at least categories from previous tests in this run
872+
// Since we don't clean up between tests in the same test function, we expect at least 1
873+
require.GreaterOrEqual(t, len(resp.QuestionCategories), 1)
874+
})
875+
876+
t.Run("forbidden - user without question:read scope", func(t *testing.T) {
877+
var resp struct {
878+
QuestionCategories []string
879+
}
880+
query := `query { questionCategories }`
881+
882+
err := gqlClient.Post(query, &resp, func(bd *client.Request) {
883+
bd.HTTP = bd.HTTP.WithContext(auth.WithUser(bd.HTTP.Context(), auth.TokenInfo{
884+
UserID: testUser.ID,
885+
Scopes: []string{"submission:read"}, // Wrong scope
886+
}))
887+
})
888+
require.Error(t, err)
889+
require.Contains(t, err.Error(), defs.CodeForbidden)
890+
})
891+
892+
t.Run("unauthorized - no authentication", func(t *testing.T) {
893+
var resp struct {
894+
QuestionCategories []string
895+
}
896+
query := `query { questionCategories }`
897+
898+
err := gqlClient.Post(query, &resp)
899+
require.Error(t, err)
900+
require.Contains(t, err.Error(), defs.CodeUnauthorized)
901+
})
902+
}

0 commit comments

Comments
 (0)