@@ -23,11 +23,13 @@ import (
2323// createTestDatabase creates a test database entity
2424func 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